Compare commits

...

118 Commits

Author SHA1 Message Date
objecttothis
93713f8e4b Merge branch 'master' into plugin-system-fresh 2026-05-22 02:23:52 +04:00
objecttothis
b7384296c1 Bugfix: Sale search in register not handling trailing space properly (#4557)
* Fix is_valid_receipt method bug

Strings submitted with a trailing space and no number caused an unhandled exception because Sale::exists() expects an int but a string was passed to it.

- Add guards
- Minor PSR refactor

Signed-off-by: objec <objecttothis@gmail.com>

* Address review comments

Signed-off-by: objec <objecttothis@gmail.com>

---------

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-22 01:43:24 +04:00
objec
ad901f9c2d Add Receiving type to receiving_complete event trigger
- The type isn't found in the db, so send it to plugins.
- Update documentation

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-21 13:13:48 +04:00
objec
388c8ad631 Add Receivings event trigger
Signed-off-by: objec <objecttothis@gmail.com>
2026-05-21 12:47:53 +04:00
objec
705c61b48c Update documentation
Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 23:09:58 +04:00
objec
d39067e2e1 Add event trigger for sale completion
Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 21:32:00 +04:00
objec
50eead4da4 Updating customers save triggers to pass an array
- Customer CSV import will potentially have many customerIds to send to.
- Rework mailchimp onCustomerSaved() to receive an array of ids instead of a single ID

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 19:41:38 +04:00
objec
4c7ac7b5d0 Thin contract triggers
- send only bare required data to trigger callbacks.
- Plugins for now access model, library and helpers but in the future access REST APIs only for data.

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 19:27:36 +04:00
objec
bed8a1c34d Error checking and validation
Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 19:05:23 +04:00
objec
10588867c4 Update configuration form to improve the UI
Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 17:57:34 +04:00
objec
139f754a07 Refactor manage plugin configuration and settings
- Add column to indicate control setting (installed, enabled).
- Add column to indicate plugin.
- Rework business logic to read the status properly.
- Renamed the migration to properly reflect which version it's released in.

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 17:43:42 +04:00
objec
c08872f83e Controller function updates for plugins
- Refactor get_multiple_info() to getMultipleInfo() in call
- Change data passed in customer event trigger to just the customerId.

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 17:09:06 +04:00
objec
01172fc522 Plugin related functions
- getItems() gets Item data from the item table for an array of item ids.
- getAttributeValuesBulk() front loads attribute values for an array of items.

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 17:07:17 +04:00
objec
f8fd12c5de Unify CLAUDE.md and AGENTS.md
Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 17:04:46 +04:00
objec
a15a6516a6 Update AGENTS.md and CLAUDE.md to reflect unified instructions
Signed-off-by: objec <objecttothis@gmail.com>
2026-05-20 17:03:04 +04:00
objec
84a10ec218 chore: untrack PHPUnit build artifacts
/build is already gitignored but these four files were previously
committed, causing them to be tracked despite the ignore rule.
2026-05-19 16:57:44 +04:00
objec
f650f17181 Push missing language strings file changes and HomeTest
Signed-off-by: objec <objecttothis@gmail.com>
2026-05-19 16:16:27 +04:00
objec
d699d82388 Merge remote-tracking branch 'OpensourcePOS/master' into plugin-system-fresh 2026-05-19 16:07:31 +04:00
objecttothis
b0dddc22a3 Bugfixes to get Migration working on MySQL and MariaDB (#4551)
* Bugfixes to get Migration working on MySQL

Signed-off-by: objec <objecttothis@gmail.com>

* MariaDB compatibility fixes

- Drop foreign key constraints before making charset changes
- Fix dropAllForeignKeyConstraints helper function.
- Added `IF EXISTS` to DROP statements
- Do not try to readd FK constraints for tables which were dropped.
- MariaDB 11.8.x changes the default implicit collation to uca1400 which breaks the IndiaGST migration, et. al. Explicitly declare utf8_general_ci in affected migrations.

Signed-off-by: objec <objecttothis@gmail.com>

* Fix changes which break MySQL migrations

- MySQL does not support IF EXISTS in foreign key constraints. Since the PHP is now handling dropping those constraints, these lines are redundant. Remove them.

Signed-off-by: objec <objecttothis@gmail.com>

* Resolve code review recommendations

- Add try/catch around DB connect statement
- Heed result of execute_script function and throw an exception on failure.

Signed-off-by: objec <objecttothis@gmail.com>

* Refactor out duplicate code

Signed-off-by: objec <objecttothis@gmail.com>

* Initialize array variable causing potential issues

Signed-off-by: objec <objecttothis@gmail.com>

---------

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-19 16:02:05 +04:00
objec
7afaeef6a3 Merge remote-tracking branch 'OpensourcePOS/master' into plugin-system-fresh 2026-05-19 11:28:29 +04:00
jekkos
8d6b166673 feat: Add deployment workflow with approval gates (#4522)
* feat: Add deployment workflows with approval gates

Add GitHub Actions workflows for controlled deployments:

deploy.yml - Manual Deploy:
- Triggered via Actions UI (workflow_dispatch)
- Select environment (production/staging)
- Select Docker image tag
- Reusable via workflow_call for other workflows
- Creates GitHub deployment records with status tracking
- Sends Docker Hub compatible webhook payload
- Environment input validation for workflow_call

deploy-pr.yml - PR Deploy:
- Auto-triggers when PR is approved (same-repo only)
- Deploys to staging environment
- Image tag format: pr-{number}-{short-sha}
- Posts deployment status as PR comment
- Fork PR protection: only runs for same-repo PRs

Security:
- jq-based JSON payload construction (prevents script injection)
- HMAC-SHA256 signature verification for webhook
- Untrusted inputs via env: blocks (not inline interpolation)
- Environment validation before deployment
- Fork detection guard for PR deployments

Fixes CodeRabbit review comments:
- Invalid jq string filter syntax (missing quotes)
- Unvalidated environment input in workflow_call
- Fork PR deployments blocked by pull_request_review restrictions

* refactor: Limit deployment to staging only

- Remove environment input choice (was production/staging)
- Hardcode environment to 'staging' throughout
- Simplify workflow - no environment validation needed
- Update concurrency group to deploy-staging

* refactor: Extract deployment logic to reusable deploy-core.yml

Restructure workflows to eliminate code duplication:

deploy-core.yml (new):
- Reusable workflow with all deployment logic
- Creates GitHub deployment record
- Sends webhook payload to external service
- Handles status updates
- Accepts image_tag, sha, description, pr_number inputs
- Outputs deployment_id and status

deploy.yml (simplified):
- Manual trigger only
- Calls deploy-core with user-provided image_tag
- 18 lines (was 175)

deploy-pr.yml (simplified):
- PR approval trigger with fork guard
- Prepare job: checkout, generate PR image tag
- Deploy job: calls deploy-core
- Comment job: post status to PR
- 70 lines (was 204)

---------

Co-authored-by: Ollama <ollama@steganos.dev>
2026-05-18 21:48:02 +02:00
objecttothis
df24ef5193 Merge branch 'master' into plugin-system-fresh 2026-05-18 16:25:10 +04:00
objec
1c2112a78b Add model functions needed for plugins
- getDateAdded and getDatesAdded in Inventory.php
- GetDistinctCategories in Item.php
- GetBulkItemQuantities in Item_quantity.php
- GetBulkInfo in Item_taxes.php
- GetStockLocationsByItem in Stock_location.php

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-18 16:15:24 +04:00
jekkos
093ec7fb13 fix: validate attributeId > 0 in saveAttributeLink() (#4508)
- Add early validation to reject attributeId <= 0
- Ensure consistent handling of invalid attribute_id in INSERT/UPDATE paths
- Prevent foreign key constraint violations from invalid attribute references

Fixes #4460

Co-authored-by: Ollama <ollama@steganos.dev>
Co-authored-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
2026-05-18 14:13:20 +02:00
objec
ad097adccd Refactor get_info to getInfo
Signed-off-by: objec <objecttothis@gmail.com>
2026-05-18 16:12:38 +04:00
objec
796657118a Feature to pass data to config view modals
- Add getConfigViewData() to BasePlugin.php
- Add function to plugin interface

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-18 16:11:12 +04:00
objec
445a506ea8 Plugin Changes
- Remove unneeded language line from MailchimpPlugin
- Update README.md
- Normalized fields_required_message

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-18 16:08:33 +04:00
objec
5d608ec873 Refactoring and bugfixes
- Pass an array to QueryBuilder->whereNotIn()
- Refactor function names for PSR compliance
- Add explanatory PHPdocs and corrections
- Correct bug with items_taxes model
- Refactor local variables for PSR compliance

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-18 16:06:23 +04:00
jekkos
9c89a2e2cb fix: Capture CSV import failures in save_tax_data and save_inventory_quantities (#4507)
* fix: capture CSV import failures in save_tax_data and save_inventory_quantities

- Change save_tax_data() return type from void to bool
- Change save_inventory_quantities() return type from void to bool
- Accumulate failure status with &= operator in save_inventory_quantities
- Update postImportCsvFile() to capture return values and set isFailedRow
- Properly propagate failures to failCodes array

Fixes #4475

* fix: Change isset to !empty for items_taxes_data check

- isset was always true since array was initialized
- Use !empty to properly check if there are tax items to save

Address CodeRabbit review feedback

* fix: Capture inventory insert result in save_inventory_quantities

- Combine inventory insert result with success tracking
- Use &= operator to accumulate failures from both operations
- Ensure failures from inventory inserts are propagated

Address CodeRabbit review feedback

---------

Co-authored-by: Ollama <ollama@steganos.dev>
2026-05-17 22:23:43 +02:00
jekkos
2f51c4ef52 fix(security): SQL injection and path traversal vulnerabilities (#4539)
Security fixes for two vulnerabilities:

1. SQL Injection in Summary Sales Taxes Report (GHSA-5j9m-2f98-cjqw)
   - Fixed unsanitized user input concatenation in getData() method
   - Applied proper escaping using $this->db->escape() for start_date/end_date
   - Consistent with existing _where() method implementation

2. Path Traversal in Receipt Template (GHSA-h6wm-fhw2-m3q3)
   - Added ALLOWED_RECEIPT_TEMPLATES whitelist constant
   - Added isValidReceiptTemplate() validation method
   - Validate receipt_template before saving in Config controller
   - Validate receipt_template before rendering in receipt view
   - Default to 'receipt_default' for invalid values
   - Consistent with invoice_type fix pattern (commit 31d25e06d)

Affected files:
- app/Models/Reports/Summary_sales_taxes.php
- app/Libraries/Sale_lib.php
- app/Controllers/Config.php
- app/Views/sales/receipt.php

Co-authored-by: Ollama <ollama@steganos.dev>
2026-05-15 23:10:04 +02:00
jekkos
def0c27a0e fix(security): Path traversal vulnerability in getPicThumb (#4545)
Security impact:
- Authenticated attackers could read arbitrary files on the server
- Path traversal via unsanitized pic_filename parameter
- Could read .env, config files, encryption keys

Fix:
- Apply basename() to strip directory components
- Validate file extension to allowlist image types only
- Add explicit error response for invalid file types

CVE: Pending
Affected: <= 3.4.2
Reported by: Kamran Saifullah (VulDB)

Co-authored-by: Ollama <ollama@steganos.dev>
2026-05-15 22:04:29 +02:00
BhojKamal
90c981b6b7 feat: Bank transfer and wallet payment option added #4540 (#4547)
---------

Co-authored-by: Lotussoft Youngtech <lotussoftyoungtech@gmail.com>
Co-authored-by: objecttothis <17935339+objecttothis@users.noreply.github.com>
2026-05-15 20:50:34 +02:00
jekkos
6ff28d8a4d docs: Update SECURITY.md with disclosure process (#4549)
* docs: Update SECURITY.md with disclosure process and advisory template

- Update published advisories table with CVE-2026-41306 and CVE-2026-41307
- Add disclosure process timeline
- Add vulnerability template for researchers
- Explain GitHub advisory creation workflow
- Document security best practices for researchers

This streamlines the vulnerability reporting process by allowing
researchers to create draft advisories directly on GitHub, reducing
triage overhead.

* docs: Update SECURITY.md with CVE process and reporter acknowledgments

- Add CVE request procedure through GitHub
- Document that existing CVEs should be shared in reports
- Clarify no bug bounty program (voluntary triage)
- Add security best practices for researchers
- Thank security researchers for contributions
- Explain vulnerability template format

* docs: Simplify SECURITY.md - remove CVE table, link to GitHub advisories

---------

Co-authored-by: Ollama <ollama@steganos.dev>
2026-05-15 12:24:39 +02:00
objec
e5fdea85f3 Update README.md to clarify plugin
Signed-off-by: objec <objecttothis@gmail.com>
2026-05-05 16:00:15 +04:00
objec
ca07aac9a0 Bugfixes
- Remove extra column header for actions.
- Add language for code block in CLAUDE.md

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-05 15:38:37 +04:00
objec
cd91ac3ff3 Fix bugs
- Add missing `MailchimpPlugin.` prefix to lang() calls.
- Do not subscribe customer if consent is not true.
- Escape output in tabular_helper.php
- Removed testConnection() as unneeded code
- Fix activity count logic
- Whitelist Sort Column Headers for Plugins.php
- Store encrypted API key as base64 instead of raw binary to prevent truncation
- Rollback on batchSave partial failure.
- Remove dead code.
- Disable plugin before uninstalling it.
- Fix getPluginSettings() internal key leak
- Add action column to plugin headers function
- Automatically add grant to all admins in case person_id 1 is not active

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-05 15:06:04 +04:00
objec
478934321d Merge remote-tracking branch 'origin/master' into plugin-system-fresh
# Conflicts:
#	app/Language/th/Sales.php
2026-05-05 13:09:43 +04:00
objec
4d266c9b5e Correct mailchimp deletion issues and response codes
- Add function to correctly interpret subscription status from the API
- Error validation on customer deletion.
- Corrected PHPDoc to reflect reponse codes.
- Pass complete data to synchronize subscription function
- Rework request function to properly interpret response
- Add data to trigger
- Unsubscribe customer before deleting them from Mailchimp to prevent error.

Signed-off-by: objec <objecttothis@gmail.com>
2026-05-05 13:04:30 +04:00
objec
43bee7bfe4 Refactor save_customer method to saveCustomer for PSR compliance
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-30 14:21:34 +04:00
objec
f71af765f8 Add missing customer_tab_nav file for MailchimpPlugin
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-30 14:12:55 +04:00
objec
4246a915c4 - Correct README.md reference to views and information about renderView()
- Fix the output of pluginContent in the pluginHelper
- Register view injection events
- Correct the parameter type in getMailchimpViewData
- Correct the statusOptions creation business logic
- Removed unnecessary view injection point
- Corrected which variable was passed to the customer_saved event
- Assigned $customer_data['person_id'] on customer update
- Added renderView() function to BasePlugin.php

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-30 14:11:54 +04:00
objec
939012dc1b Add CLAUDE.md file for those using Claude Code (claude.ai/code)
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-30 11:26:14 +04:00
objec
d2d0c8bf37 Add routes file to MailchimpPlugin to handle custom routes
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-30 11:23:56 +04:00
objec
6c55526479 Settings fixes for MailchimpPlugin
- Add PHPdoc including @noinspection to prevent AJAX function from causing a warning.
- Add lists array to settings retrieval in MailchimpPlugin.php
- Close modal window on Submit
- Don't check API key on empty value

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-30 11:22:59 +04:00
objec
fe331c34dd Mailchimp Bugfixes
- Update README.md to reflect information about routes
- Add registerAllNamespaces() function to correctly load plugin  namespaces
- center text in modal title
- Properly decrypt the api key
- Refactor getAllLists to getLists
- Naming simplification of strings when mailchimp_ is redundant or unnecessary
- Do not attempt to decrypt a plaintext api_key pasted into the form
- Register namespaces early on in system init

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-29 18:25:21 +04:00
objec
6630fb56f6 Fix language discovery bugs
- Remove unneeded keys from Config.php
- Remove unneeded lang() function override from BasePlugin.php
- Update README.md to reflect changes to language loading
- Correct language file string
- Correct lang() function calls to remove `$this->` from the call since we aren't overriding it anymore.
- Add code to correctly register namespace so that languages load.
- Fix plugin view render bug

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-29 16:31:14 +04:00
objec
2f48e0499f Plugin discovery bugfix
- Fix namespace typo causing plugin to not load
- Code cleanup

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-29 15:20:08 +04:00
objec
cbabe1d56c Bugfix: Fix recursive view call
- Fix bug causing all plugin views to be rendered on every page.
- Simplify code
- Refactor manage.php view to use bootstrap tables

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-29 15:02:48 +04:00
objec
8c1c9d85dc Language Refactor
- Correct key name in language files.
- Update translations.
- Correct key usage.

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-29 14:54:16 +04:00
objec
97adee0c28 Language changes
- Refactor Plugins.php language keys
- Correct spacing between key and `=>`
- Replaced `"` with `'` to avoid calling the PHP string parser
- Propagated Plugins.php language string file to other languages
- removed redundant `plugin_` from key names

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-29 11:28:26 +04:00
objec
32997d48c0 Fix persistence problem with plugin registration.
- Move the PluginManager creation to a service.
- Move plugin discovery to creation.
- Create static discovery and namespaces variables in the PluginManager.php library
- Refactor persistent namespace declarations
- Refactor redundant code to private function.
- Remove whitespace
- Remove enable setting from MailchimpPlugin. That is handled by the PluginManager.php
- Update Events.php to call the pluginManager service
- Correct typo in enabled setting for BasePlugin to accurately reflect the database naming.

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-29 10:31:04 +04:00
objec
1a9e84bd37 Fix bugs preventing plugins from working
- Move Plugins controller and rename to reflect the rest of the code.
- Lazy load event registrations.
- Autoload classes so plugins are discovered.
- Remove TODO
- Remove unneeded use statement
- Correct typo in namespace of MailchimpConnector Library
- Add class names to autoload class map
- Move Plugin discovery to post_controller_constructor event

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-23 16:26:50 +04:00
objec
9d0b14a8ce Add plugin string and translations for module name
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-23 13:33:59 +04:00
objec
c796b52c22 Correct mistakes in plugin code related to loading.
- Add plugin module to list of required admin modules.
- Don't trigger autoloader in plugin discovery.
- Delete plugins_config.php which is no longer needed for managing plugins.
- Remove references to plugins_configuration in config views.
- Correct the form submission URL path.

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-23 12:42:34 +04:00
objec
f863be68f9 Plugins migration
- Remove blank line
- Add plugin SVG icon to gulpfile.js
- Add plugin details to SQL migration script

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-23 11:25:21 +04:00
objec
54c476a498 MailchimpPlugin simplifications.
- Removed use statements
- Refactored key name
- Refactored MailchimpLibrary.php functions
- Removed
- Refactored function name for clarity
- Removed calls to model functions
- Corrected alignment of `=>` in language files

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-22 18:32:44 +04:00
objec
ec139c477a Removed unnecessary models since information will be fetched directly from the mailchimp API each time.
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-22 17:00:34 +04:00
objec
bae361c637 Langauge file changes
- Added phrase to MailchimpPlugin.php language files
- updated calls to lang to call the correct phrases

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-22 15:32:05 +04:00
objec
dcfdc212da Language Strings refactor
- Add strings representing subscription statuses.
- Resort strings alphabetically per standard practice.
- Aligned strings.
- Corrected es_ES string error

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 18:21:03 +04:00
objec
c217fd770c Language Strings refactor
- Remove the `mailchimp_` prefix from strings as it is redundant.
- Replace `"` with `'` to avoid calling the PHP string parser when not needed.
- Correct the French translation of a phrase for the tooltip.
- Resort strings alphabetically.
- Add strings representing subscription statuses.

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 16:37:09 +04:00
objec
6ef6f49693 StatusOptions
- Generate StatusOptions View Data from enum rather than hardcode.
- Add language strings

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 16:20:44 +04:00
objec
eae6417f97 Changed Mailchimp Subscription Status Options to be sent to the view not hardcoded.
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 15:50:05 +04:00
objec
4cfff5388c Refactor Customer Tab View
- Add variables for correct type hinting.
- Refactor lang() calls to call the correct file

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 15:32:38 +04:00
objec
a77b95f0cc Update MailchimpPlugin Modeling
- made updated_at and deleted_at to be nullable
- Add getter for mailchimpId
- Changed primaryKey to customer_id so that baseModel functions could be used natively rather than writing custom code.
- Removed unneeded getByCustomerId() since BaseModel->find() can be used now.

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 15:27:11 +04:00
objec
506cded6e9 Update MailchimpLibrary.php
- Corrected access of settings
- Use SubscriptionModel->find() from BaseModel instead of custom function
- Add deleteSubscription() function
- Removed registration for customer_loaded event since it isn't needed for this plugin
- Refactored out customer data injection to library function
- Removed settings parameter from synchronizeSubscription() since these values are loaded when library is instantiated.
- Refactored delete subscription callback to call library function.
- Remove functions that aren't necessary
- added lang() implementation todo

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 15:24:34 +04:00
objec
2639a8b212 Update MailchimpLibrary.php
- Corrected access of settings
- Use SubscriptionModel->find() from BaseModel instead of custom function
- Add deleteSubscription() function

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 14:52:08 +04:00
objec
f6106e7ead Add mailchimp controller class
- AJAX called checkMailchimpApiKey function
- getLists helper function

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 14:50:21 +04:00
objec
202c016dd8 Remove MailchimpController class
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 12:31:22 +04:00
objec
d47ead8747 Corrected AGENTS.md
- Updated PHP version to reflect the project parameters
- Updated minimum PHPUnit version
- Added instructions regarding Plugins

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 12:30:21 +04:00
objec
0e5ba88f6c MailchimpLibrary.php rework
- Rework source for mailchimp settings
- Added synchronizeSubscription() function

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 12:28:21 +04:00
objec
d88cf54f99 MailchimpPlugin.php rework
- Remove unneeded using statements
- Register Customer View Tab to MailchimpPlugin
- Create callback function
- Refactor business logic to Library Code
- Refactor naming of Mailchimp_lib to MailchimpLibrary for PSR compliance

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 12:26:42 +04:00
objec
2f200b47c6 Add getByCustomerId method to SubscriptionModel.php
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 12:23:52 +04:00
objec
f819bc92f8 Add casts to Subscription entity
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-21 12:15:44 +04:00
objec
db180d134e Views
- Added injection point into Customers Controller.
- Registered event listeners for the view hook.
- created initial customer tab view.
- Removed unnecessary comments

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-18 00:13:55 +04:00
objec
edd97a3c78 Modeling Data
- Subscription table modeling
- Subscription status table modeling

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-18 00:11:35 +04:00
objec
d73bfd39f6 Refactoring
- PSR compliance refactors
- Added Events trigger for customer save in Customers.php controller
- Temporarily moved code over to the Mailchimp plugin
- Replaced == with === to prevent type coercion.

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-17 17:01:24 +04:00
objec
d9d93e0d9d Mailchimp PLugin
- Corrected grammar in PHPdocs
- PSR refactoring of local variables and code blocks
- Moved MailchimpPlugin.php to its own plugin folder
- Refactored out mailchimp code to the plugin
- Created customer_loaded event trigger

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-16 17:29:46 +04:00
objec
bfb4ad4617 Licensing
- Added LICENSE
- Updated README.md to reflect that Plugins need to contain LICENSE

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-15 16:16:49 +04:00
objec
00cd13f735 Move Customer Mailchimp business logic to Mailchimp Plugin space
- Create MailchimpController class
- Refactor Mailchimp_lib class to MailchimpLibrary
- Deleted useless PHPdocs
- Refactored MailchimpConnector class into its own library class
- Refactored out hungarian notation
- PSR compliant naming changes

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-15 15:20:42 +04:00
objec
43972b8f0e Moved Mailchimp_lib.php to the MailchimpPlugin
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 18:14:47 +04:00
objec
ff3c7d1b14 Refactor integrations to plugins
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 18:08:29 +04:00
objec
9fc918b53d Refactor integrations to plugins
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 18:05:57 +04:00
objec
56f68f7577 Update .gitignore to ignore the /build folder.
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 18:02:23 +04:00
objec
65fb6339d7 Translations
- Deleted Mailchimp string from de-DE Sales langugage file.
- Finished translating missing phrases

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 17:57:23 +04:00
objec
57a19bb35f Renamed integrations to plugins
- integrations_config.php to plugins_config.php in views

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 17:56:17 +04:00
objec
c81cf4a5cc Translations for MailChimp Plugin
- Moved Language strings over to the plugin directory
- Removed them from the original language files (Config.php, Customers.php, Sales.php)
- Added translations for missing languages related to MailchimpPlugin.php

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 17:44:22 +04:00
objec
1918f3e6e2 Add language identifiers for text in README.md
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 12:05:19 +04:00
objec
196e87fa49 Resolve potential conflict with plugin settings installed and enabled
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-14 11:56:17 +04:00
objec
ebd1c8fa0e Merge remote-tracking branch 'origin/master' into plugin-system-fresh 2026-04-14 10:53:50 +04:00
objec
f842be50b3 Move Mailchimp plugin business logic
- Remove business logic from Config controller
- Add business logic to MailchimpPlugin.php
- Add config view for mailchimp plugin
- gutted integrations_config.php
- Added parameter type to getConfig() function parameter to remove warning in override

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-08 14:47:47 +04:00
objec
e94e5e634c Update Config Controller
- Removed postSaveMailchimp(). This is handled in the plugin
- Removed view $data related additions from mailchimp. Those are handled in the plugin
- Removed extra whitespace

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-08 14:09:59 +04:00
objec
bcff389b34 Mailchimp config view
- Update MailchimpPlugin.php to use config path
- Created Mailchimp config view.

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-08 13:53:16 +04:00
objec
deb122246c Updated Plugin manage controller
- Removed whitespace
- Replaced hardcoded csrf_token_name with csrf_token()
- Added empty line to the end of the file

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-08 13:51:32 +04:00
objec
e1b76d2e0d Update MailchimpPlugin.php
- Replaced full declarations with an import
- Added exception to use the return value of subscribeCustomer()

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-08 13:33:32 +04:00
objec
84ea65b1bd Added check for plugin table before attempting to load plugins
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-08 12:19:24 +04:00
objec
a19fe03ecf Whitespace and refactoring corrections
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-08 11:39:29 +04:00
objec
15227523d9 Correct function naming to use PSR compliant naming conventions
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-08 11:37:32 +04:00
objec
1587d4276d Merge remote-tracking branch 'origin/master' into plugin-system-fresh 2026-04-08 11:06:24 +04:00
objec
d6967704e6 Revert "CSV Import fixes"
This reverts commit 80dc62948d.
2026-04-03 00:02:07 +04:00
objec
80dc62948d CSV Import fixes
- Corrected Capitalization in File Handling Logic
- Populated $allowedStockLocations before sending it to the validation function

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-02 23:51:07 +04:00
objec
acb3b18584 Load the namespace for the file so languages work in plugins
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-02 17:07:27 +04:00
objec
19cdb76bb3 Removed whitespace in file
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-02 16:26:32 +04:00
objec
07f1a35e9d Remove old MailchimpLibrary from Autoload.php
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-02 16:16:41 +04:00
objec
3b0476f2b3 Save settings through the parent saveSettings function
- Remove extra whitespace
- Save Settings function returned true even if there were errors. Corrected this behavior.

Signed-off-by: objec <objecttothis@gmail.com>
2026-04-02 15:50:31 +04:00
objec
2fa324ba4e Remove whitespace from file
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-02 15:43:27 +04:00
objec
8b14ed81e0 Restore CI4 PHPdocs to prevent complicated upgrade diffs
Signed-off-by: objec <objecttothis@gmail.com>
2026-04-02 15:38:20 +04:00
Ollama
0ea3ced674 fix: Rename Plugin_config methods to avoid conflict with CodeIgniter Model::set()
The PluginConfig class extends CodeIgniter\Model which has its own set() method
for query building. Renaming get()/set() to getValue()/setValue() avoids this conflict.

Also fixed:
- batchSave() to use setValue() instead of set()
- Updated all callers in PluginManager and BasePlugin to use renamed methods
2026-03-24 08:07:18 +00:00
Ollama
896ed87797 fix: Address CodeRabbit AI review comments
- Move plugin discovery to pre_system in Events.php (allows events to be registered before they fire)
- Add plugin existence check in disablePlugin()
- Add is_subclass_of check before instantiating plugin classes
- Fix str_replace prefix removal in getPluginSettings using str_starts_with + substr
- Add down() migration to drop table on rollback
- Fix saveSettings to JSON-encode arrays/objects
- Update README to use MailchimpPlugin as reference implementation
- Remove CasposPlugin examples from documentation
2026-03-22 19:47:09 +00:00
Ollama
eb264ad76d refactor: Address review comments - PSR-12 naming and plugin cleanup
- Rename Plugin_config to PluginConfig (PSR-12 class naming)
- Remove non-functioning CasposPlugin example
- Remove ExamplePlugin (MailchimpPlugin serves as example)
- Fix privacy issue: Don't log customer email in MailchimpPlugin
- Remove unnecessary PHPDocs
- Fix PSR-12 brace placement
2026-03-22 19:40:36 +00:00
Ollama
10a64e7af9 refactor: Remove redundant isEnabled() checks from callback methods
The PluginManager only registers events for enabled plugins, so
callbacks are never invoked for disabled plugins. This makes
$this->isEnabled() checks in callbacks redundant.

Changes:
- Remove redundant isEnabled() checks from all plugin callbacks
- Clarify in README that isEnabled() checks are not needed
- Use log_message() instead of log() in plugins (PSR-12)
- Fix PSR-12 brace placement in CasposPlugin
2026-03-20 19:48:27 +00:00
Ollama
6e99f05d63 refactor: Update MailchimpPlugin as proper example plugin
- Reword docblock to remove 'Example' - it's a functioning plugin
- Rename 'Mailchimp Integration' to 'Mailchimp' (context makes it clear)
- Use lang() method for translatable strings with self-contained language file
- Use log_message() instead of log() for PSR-12 consistency
- Add missing language strings: mailchimp_description, mailchimp_api_key_required
- Add getPluginDir() method for language helper
2026-03-20 18:32:42 +00:00
Ollama
c430c7afb5 refactor: Move mailchimp language strings to self-contained plugin directory
- Create app/Plugins/MailchimpPlugin/Language/en/MailchimpPlugin.php
- Remove mailchimp strings from core app/Language/en/Plugins.php
- Plugin language files are now self-contained per the documentation
2026-03-19 18:24:48 +00:00
Ollama
519347f4f5 refactor: Fix PSR-12 and documentation issues
- Consolidate duplicate documentation sections
- Move Internationalization section after Plugin Views
- Remove redundant Example Plugin Structure and View Hooks sections
- Fix PSR-12 brace style in plugin_helper.php
- Fix PSR-12 brace style in PluginInterface.php (remove unnecessary PHPdocs)
- Fix PSR-12 brace style in BasePlugin.php (remove unnecessary PHPdocs)
- Use log_message() instead of error_log() in migration
- Add IF NOT EXISTS to plugin_config table creation for resilience
- Convert snake_case to camelCase for class names throughout docs
2026-03-19 18:20:05 +00:00
Ollama
62d84411b2 docs: Fix documentation consistency issues
- Add Language folder to all plugin structure examples
- Convert snake_case to camelCase for class names (PSR-12)
- Add Language folder to initial plugin structure diagram
- Add Language folder to Complex Plugin structure
- Update all namespace references to use camelCase
2026-03-18 22:06:09 +00:00
Ollama
6bd4bb545d docs: Add internationalization section showing self-contained plugin language files
Adds documentation example showing how plugins can embed their own
language files within the plugin directory structure, keeping plugins
fully self-contained without modifying core language files.
2026-03-17 14:36:13 +00:00
Ollama
66f7d70749 feat(plugins): add view hooks for injecting plugin content into core views
Add event-based view hook system allowing plugins to inject UI elements
into core views without modifying core files. Includes helper functions
and example CasposPlugin demonstrating the pattern.
2026-03-12 10:13:12 +00:00
Ollama
bd8b4fa6c1 feat(plugins): Support self-contained plugin directories
- PluginManager now recursively scans app/Plugins/ to discover plugins
- Supports both single-file plugins (MyPlugin.php) and directory plugins (MyPlugin/MyPlugin.php)
- Plugins can contain their own Models, Controllers, Views, Libraries, Helpers
- Uses PSR-4 namespacing: App\Plugins\PluginName for files, App\Plugins\PluginName\Subdir for subdirectories
- Users can install plugins by simply dropping a folder into app/Plugins/
- Updated README with comprehensive documentation on both plugin formats

This makes plugin installation much easier - just drop the plugin folder and it works.
2026-03-09 21:58:53 +01:00
Ollama
a9669ddf19 feat(plugins): Implement modular plugin system with self-registering events
This implements a clean plugin architecture based on PR #4255 discussion:

Core Components:
- PluginInterface: Standard contract all plugins must implement
- BasePlugin: Abstract class with common functionality
- PluginManager: Discovers and loads plugins from app/Plugins/
- Plugin_config: Model for plugin settings storage

Architecture:
- Each plugin registers its own event listeners via registerEvents()
- No hardcoded plugin dependencies in core Events.php
- Generic event triggers (item_sale, item_change, etc.) remain in core code
- Plugins can be enabled/disabled via database settings
- Clean separation: plugin orchestrators vs MVC components

Example Implementations:
- ExamplePlugin: Simple plugin demonstrating event logging
- MailchimpPlugin: Integration with Mailchimp for customer sync

Admin UI:
- Plugin management controller at Controllers/Plugins/Manage.php
- Plugin management view at Views/plugins/manage.php

Database:
- ospos_plugin_config table for plugin settings (key-value store)
- Migration creates table with timestamps

Documentation:
- Comprehensive README with architecture patterns
- Simple vs complex plugin examples
- MVC directory structure guidance
2026-03-09 21:58:53 +01:00
360 changed files with 6440 additions and 2238 deletions

219
.github/workflows/deploy-core.yml vendored Normal file
View File

@@ -0,0 +1,219 @@
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 Normal file
View File

@@ -0,0 +1,79 @@
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 Normal file
View File

@@ -0,0 +1,23 @@
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

2
.gitignore vendored
View File

@@ -87,3 +87,5 @@ auth.json
/app/Database/database.sql
/writable/cache/settings
/.env.bak
/.php-cs-fixer.cache
/build

127
AGENTS.md
View File

@@ -1,40 +1,125 @@
# Agent Instructions
This document provides guidance for AI agents working on the Open Source Point of Sale (OSPOS) codebase.
This document is the single source of truth for all AI agents working on the Open Source Point of Sale (OSPOS) codebase. Read it fully before making any changes.
## Project Overview
OpenSourcePOS is a web-based Point of Sale system built on **CodeIgniter 4** (PHP 8.2+) with MySQL/MariaDB. Frontend uses Bootstrap 3 (Bootstrap 5 migration in progress) and jQuery, with assets built via Gulp.
## Common Commands
```bash
# PHP dependencies
composer install
# Frontend dependencies and asset build
npm install
npm run build # Runs Gulp: compiles and copies all CSS/JS to public/resources/
# Run full test suite
composer test
# Run a single test file
vendor/bin/phpunit tests/unit/AppTest.php
# Lint / code style check
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php --dry-run
# Apply code style fixes
vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php
```
Tests require a MariaDB/MySQL database (see CI config in `.github/workflows/phpunit.yml`).
## Architecture
### Framework & Entry Point
- **Framework**: CodeIgniter 4 — MVC with QueryBuilder ORM, no Eloquent
- **Web root**: `public/``public/index.php` is the only entry point
- **Routes**: `app/Config/Routes.php`
- **App config**: `app/Config/App.php` (version, session, security settings)
- **Environment**: `.env` file (copy from `.env.example`); `CI_ENVIRONMENT` controls dev/prod/test mode
### Directory Layout
```text
app/
├── Config/ # CI4 config classes
├── Controllers/ # ~27 controllers (Sales, Items, Reports, Customers, etc.)
├── Models/ # ~28 models (Sale, Item, Customer, Supplier, etc.)
├── Views/ # PHP view templates
├── Libraries/ # Business logic (Sale_lib, Tax_lib, Receiving_lib, etc.)
├── Plugins/ # Plugin system — each plugin is a subdirectory here
├── Database/ # Migrations (ospos_ prefix) and seeds
├── Language/ # i18n files (IETF BCP 47 locale names)
├── Filters/ # Request/response filters (auth, HTTPS, etc.)
└── Events/ # CI4 event subscribers
public/
└── resources/ # Built CSS/JS (do not edit directly — generated by npm run build)
tests/ # PHPUnit test suite
```
### Key Libraries
`app/Libraries/` holds core business logic:
- `Sale_lib.php` — sale cart state, pricing, discounts, tax calculation
- `Tax_lib.php` — multi-tier tax engine
- `Receiving_lib.php` — purchase orders / receivings
- `Barcode_lib.php` — barcode generation
- `Email_lib.php` — email delivery
- `Token_lib.php` — CSRF/session token management
### Database
- Table prefix: `ospos_` (defined in `app/Config/Database.php`)
- Migrations live in `app/Database/Migrations/` and run automatically on first access
- CodeIgniter QueryBuilder throughout — no raw SQL unless necessary
### Plugin System
Plugins live in `app/Plugins/<PluginName>/` and are auto-discovered by `PluginManager`. Each plugin:
- Extends `BasePlugin` or implements `PluginInterface`
- Registers event hooks (e.g., `item_sale`, `customer_saved`, view hooks like `customer_tabs`)
- Can include its own `Views/`, `Models/`, `Controllers/`, and `Language/` subdirectories
- Configuration stored in `ospos_plugin_config` table
- See `app/Plugins/README.md` for plugin structure, event hooks, and LICENSE requirements
### Frontend Build
`gulpfile.js` (Gulp 5) copies vendor CSS/JS from `node_modules/` into `public/resources/`. Run `npm run build` after installing npm packages or changing gulp tasks. Do not manually edit files under `public/resources/`.
## Code Style
- Follow PHP CodeIgniter 4 coding standards
- Run PHP-CS-Fixer before committing: `vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php`
- Write PHP 8.1+ compatible code with proper type declarations
- Use PSR-12 naming conventions: `camelCase` for variables and functions, `PascalCase` for classes, `UPPER_CASE` for constants
- **PSR-12** enforced via PHP-CS-Fixer (config: `.php-cs-fixer.no-header.php`)
- `camelCase` for variables and methods; `PascalCase` for classes; `UPPER_CASE` for constants
- PHP 8.2+ features acceptable (named arguments, enums, readonly properties)
- Views in `app/Views/errors/html/` are excluded from the fixer
- Run fixer before committing: `vendor/bin/php-cs-fixer fix --config=.php-cs-fixer.no-header.php`
## Development
## Development Workflow
- Create a new git worktree for each issue, based on the latest state of `origin/master`
- Commit fixes to the worktree and push to the remote
## Testing
- Run PHPUnit tests: `composer test`
- Tests must pass before submitting changes
## Build
- Install dependencies: `composer install && npm install`
- Build assets: `npm run build` or `gulp`
- Tests must pass before submitting changes (`composer test`)
- Minimum PHPUnit version: 10.5.16+. Default config: `phpunit.xml.dist`
## Conventions
- Controllers go in `app/Controllers/`
- Models go in `app/Models/`
- Views go in `app/Views/`
- Database migrations in `app/Database/Migrations/`
- Controllers `app/Controllers/`
- Models `app/Models/`
- Views `app/Views/`
- Migrations `app/Database/Migrations/`
- Plugins → `app/Plugins/` (see `app/Plugins/README.md` for plugin structure, event hooks, and LICENSE requirements)
- Use CodeIgniter 4 framework patterns and helpers
- Sanitize user input; escape output using `esc()` helper
## Security
- `app.allowedHostnames` **must** be set in production (host header injection protection)
- HTMLPurifier for HTML sanitization; Laminas Escaper for output escaping
- CSRF tokens managed via `Token_lib` — do not bypass CI4's CSRF filter
- Session storage is database-backed (`ospos_sessions` table) for multi-instance support
- Never commit secrets, credentials, or `.env` files
- Use parameterized queries to prevent SQL injection
- Validate and sanitize all user input
- Validate and sanitize all user input

3
CLAUDE.md Normal file
View File

@@ -0,0 +1,3 @@
# CLAUDE.md
> **MANDATORY INSTRUCTION**: You MUST read `AGENTS.md` in this directory before doing anything else. `AGENTS.md` is the single source of truth for this project — architecture, commands, conventions, security rules, and workflow are all defined there. Do not proceed with any task until you have read and internalized its contents.

View File

@@ -5,8 +5,9 @@
- [Supported Versions](#supported-versions)
- [Security Advisories](#security-advisories)
- [Reporting a Vulnerability](#reporting-a-vulnerability)
- [Disclosure Process](#disclosure-process)
<!-- END doctoc generated TOC please keep comment here to allow auto update -->
<!-- END doctoc generated TOC please keep comment here to allow update -->
# Security Policy
@@ -21,26 +22,116 @@ We release patches for security vulnerabilities.
## Security Advisories
The following security vulnerabilities have been published:
### High Severity
| CVE | Vulnerability | CVSS | Published | Fixed In | Credit |
|-----|--------------|------|-----------|----------|--------|
| [CVE-2025-68434](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-wjm4-hfwg-5w5r) | CSRF leading to Admin Creation | 8.8 | 2025-12-17 | 3.4.2 | @Nixon-H, @jekkos |
| [CVE-2025-68147](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-xgr7-7pvw-fpmh) | Stored XSS in Return Policy | 8.1 | 2025-12-17 | 3.4.2 | @Nixon-H, @jekkos |
| [CVE-2025-66924](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-gv8j-f6gq-g59m) | Stored XSS in Item Kits | 7.2 | 2026-03-04 | 3.4.2 | @hungnqdz, @omkaryepre |
### Medium Severity
| CVE | Vulnerability | CVSS | Published | Fixed In | Credit |
|-----|--------------|------|-----------|----------|--------|
| [CVE-2025-68658](https://github.com/opensourcepos/opensourcepos/security/advisories/GHSA-32r8-8r9r-9chw) | Stored XSS in Company Name | 4.3 | 2026-01-13 | 3.4.2 | @hungnqdz |
For a complete list including draft advisories, see our [GitHub Security Advisories page](https://github.com/opensourcepos/opensourcepos/security/advisories).
For a complete list of published and draft security advisories with CVE details, see our [GitHub Security Advisories page](https://github.com/opensourcepos/opensourcepos/security/advisories).
## Reporting a Vulnerability
Please report (suspected) security vulnerabilities to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**.
**Option 1: GitHub Security Advisory (Preferred)**
You will receive a response from us within 48 hours. If the issue is confirmed, we will release a patch as soon as possible depending on complexity but historically within a few days.
1. Create a draft security advisory directly on GitHub:
- Go to https://github.com/opensourcepos/opensourcepos/security/advisories
- Click "New draft security advisory"
- Fill in the vulnerability details using our [template below](#vulnerability-template)
- Submit as **draft** (not published)
2. Notify us for triage:
- Send an email to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)** with:
- Subject: `[GHSA] Brief description of vulnerability`
- Link to the draft advisory
- Brief summary
**Option 2: Email Report**
Send vulnerability details to **[jeroen@steganos.dev](mailto:jeroen@steganos.dev)**.
You will receive a response within 48 hours. Confirmed vulnerabilities will be patched within a few days depending on complexity.
## Disclosure Process
### Timeline
| Step | Timeline | Action |
|------|----------|--------|
| 1. Report received | Day 0 | We acknowledge within 48 hours |
| 2. Triage & confirmation | Day 1-3 | We validate the vulnerability |
| 3. Fix development | Day 3-7 | We develop and test the fix |
| 4. Patch release | Day 7-10 | We release a security patch |
| 5. CVE request | Day 7-14 | We request CVE from GitHub (if applicable) |
| 6. Advisory published | Day 14 | We publish the advisory with credit |
| 7. Public disclosure | Day 14+ | Full disclosure after patch release |
### CVE Process
**We request CVE identifiers through GitHub's security advisory system.** This is the preferred and easiest method:
1. After we confirm and fix the vulnerability, we'll request a CVE through GitHub
2. GitHub coordinates with MITRE on our behalf
3. The CVE is automatically linked to the advisory
4. You'll be credited as the reporter in the published advisory
**Already have a CVE?** If you've already obtained a CVE from another source (e.g., VulDB, CVE.MITRE.ORG), please include it in your report or advisory. We'll update our advisory to reference the existing CVE.
### No Bug Bounty Program
**Important:** Open Source Point of Sale does not offer a bug bounty program.
- All security research and vulnerability triage is done on a **voluntary basis** in our free time
- We do not offer monetary rewards for vulnerability reports
- We do credit reporters in published advisories (unless anonymity is requested)
- We greatly appreciate the security research community's efforts to help improve project security
### Security Best Practices for Researchers
- **Do not** access, modify, or delete data that doesn't belong to you
- **Do not** perform denial of service attacks
- **Do not** publicly disclose vulnerabilities before we've had time to fix them
- **Do** provide sufficient information to reproduce the vulnerability
- **Do** allow us reasonable time to fix before public disclosure
- **Do** report through official channels (GitHub advisories or email)
### Vulnerability Template
When creating a draft advisory, please include:
```
## Summary
[Brief description of the vulnerability]
## Impact
- **Confidentiality:** [High/Medium/Low - what data can be exposed]
- **Integrity:** [High/Medium/Low - what can be modified]
- **Availability:** [High/Medium/Low - service disruption potential]
- **Privilege Required:** [None/Low/High - authentication level needed]
- **CVSS v3.1:** [Score] ([Vector string])
## Details
[Technical details about the vulnerability]
**Affected Code:**
```php
// Path to affected file and vulnerable code
```
**Attack Vector:**
[How an attacker can exploit this]
## Proof of Concept
```bash
# Steps to reproduce
```
## Patch
[Suggested fix or approach]
## Affected Versions
- OpenSourcePOS X.Y.Z and earlier
## Credit
[Your GitHub username or preferred name]
```
---
**Thank you to all security researchers who have contributed to making Open Source Point of Sale more secure.** Your voluntary efforts help protect thousands of users worldwide and contribute to a safer, more trustworthy free and open-source software ecosystem. We deeply appreciate your responsible disclosure and the time you invest in improving our project.
If you've reported a vulnerability and would like to discuss CVE coordination or have questions about the process, please reach out to us at [jeroen@steganos.dev](mailto:jeroen@steganos.dev).

View File

@@ -78,6 +78,7 @@ class Autoload extends AutoloadConfig
'No_access' => '/App/Controllers/No_access.php',
'Office' => '/App/Controllers/Office.php',
'Persons' => '/App/Controllers/Persons.php',
'Plugins' => '/App/Controllers/Plugins.php',
'Receivings' => '/App/Controllers/Receivings.php',
'Reports' => '/App/Controllers/Reports.php',
'Sales' => '/App/Controllers/Sales.php',
@@ -157,9 +158,9 @@ class Autoload extends AutoloadConfig
'Barcode_lib' => '/App/Libraries/Barcode_lib.php',
'Email_lib' => '/App/Libraries/Email_lib.php',
'Item_lib' => '/App/Libraries/Item_lib.php',
'Mailchimp_lib' => '/App/Libraries/Mailchimp_lib.php',
'MY_Email' => '/App/Libraries/MY_Email.php',
'MY_Migration' => '/App/Libraries/MY_Migration.php',
'PluginManager' => '/App/Libraries/Plugins/PluginManager.php',
'Receving_lib' => '/App/Libraries/Receiving_lib.php',
'Sale_lib' => '/App/Libraries/Sale_lib.php',
'Sms_lib' => '/App/Libraries/Sms_lib.php',
@@ -203,6 +204,7 @@ class Autoload extends AutoloadConfig
'cookie',
'tabular',
'locale',
'security'
'security',
'plugin'
];
}

View File

@@ -173,4 +173,4 @@ const DEFAULT_LANGUAGE_CODE = 'en';
/**
* Admin modules - list of modules required for admin privileges
*/
const ADMIN_MODULES = ['customers', 'employees', 'giftcards', 'items', 'item_kits', 'messages', 'receivings', 'reports', 'sales', 'config', 'suppliers'];
const ADMIN_MODULES = ['customers', 'employees', 'giftcards', 'items', 'item_kits', 'messages', 'plugins', 'receivings', 'reports', 'sales', 'config', 'suppliers'];

View File

@@ -8,6 +8,7 @@ use CodeIgniter\HotReloader\HotReloader;
use App\Events\Db_log;
use App\Events\Load_config;
use App\Events\Method;
use App\Libraries\Plugins\PluginManager;
/*
* --------------------------------------------------------------------
@@ -25,6 +26,9 @@ use App\Events\Method;
* Example:
* Events::on('create', [$myInstance, 'myMethod']);
*/
Events::on('pre_system', static function (): void {
PluginManager::registerAllNamespaces();
});
Events::on('pre_system', static function (): void {
if (ENVIRONMENT !== 'testing') {
@@ -48,7 +52,6 @@ Events::on('pre_system', static function (): void {
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();
@@ -57,8 +60,12 @@ Events::on('pre_system', static function (): void {
}
});
Events::on('post_controller_constructor', static function (): void {
service('pluginManager');
}, 10);
$config = new Load_config();
Events::on('post_controller_constructor', [$config, 'load_config']);
Events::on('post_controller_constructor', [$config, 'load_config'], 1);
$db_log = new Db_log();
Events::on('DBQuery', [$db_log, 'db_log_queries']);

View File

@@ -5,6 +5,7 @@ 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
@@ -13,7 +14,7 @@ use CodeIgniter\Config\BaseConfig;
*/
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;
@@ -33,26 +34,35 @@ class OSPOS extends BaseConfig
if ($cache) {
$this->settings = decode_array($cache);
} else {
try {
$appconfig = model(Appconfig::class);
foreach ($appconfig->get_all()->getResult() as $app_config) {
$this->settings[$app_config->key] = $app_config->value;
}
$this->cache->save('settings', encode_array($this->settings));
} catch (\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'
];
}
return;
}
try {
$db = Database::connect();
if (!$db->tableExists('app_config')) {
$this->settings = $this->getDefaultSettings();
return;
}
$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'
];
}
/**
@@ -63,4 +73,4 @@ class OSPOS extends BaseConfig
$this->cache->delete('settings');
$this->set_settings();
}
}
}

View File

@@ -3,6 +3,7 @@
namespace Config;
use App\Libraries\MY_Language;
use App\Libraries\Plugins\PluginManager;
use Locale;
use HTMLPurifier;
use HTMLPurifier_Config;
@@ -61,6 +62,24 @@ class Services extends BaseService
return new MY_Language($locale);
}
public static function pluginManager(bool $getShared = true): PluginManager
{
if ($getShared) {
return static::getSharedInstance('pluginManager');
}
$manager = new PluginManager();
if ($manager->canLoadPlugins()) {
$manager->discoverPlugins();
$manager->registerPluginEvents();
} else {
log_message('debug', 'PluginManager: skipping init, plugin_config table not found.');
}
return $manager;
}
private static HTMLPurifier $htmlPurifier;
public static function htmlPurifier($getShared = true): object

View File

@@ -3,7 +3,6 @@
namespace App\Controllers;
use App\Libraries\Barcode_lib;
use App\Libraries\Mailchimp_lib;
use App\Libraries\Receiving_lib;
use App\Libraries\Sale_lib;
use App\Libraries\Tax_lib;
@@ -253,32 +252,6 @@ class Config extends Secure_Controller
$data['image_allowed_types'] = array_combine($image_allowed_types, $image_allowed_types);
$data['selected_image_allowed_types'] = explode(',', $this->config['image_allowed_types']);
// Integrations Related fields
$data['mailchimp'] = [];
if (check_encryption()) { // TODO: Hungarian notation
if (!isset($this->encrypter)) {
helper('security');
$this->encrypter = Services::encrypter();
}
$data['mailchimp']['api_key'] = (isset($this->config['mailchimp_api_key']) && !empty($this->config['mailchimp_api_key']))
? $this->encrypter->decrypt($this->config['mailchimp_api_key'])
: '';
$data['mailchimp']['list_id'] = (isset($this->config['mailchimp_list_id']) && !empty($this->config['mailchimp_list_id']))
? $this->encrypter->decrypt($this->config['mailchimp_list_id'])
: '';
// Remove any backup of .env created by check_encryption()
remove_backup();
} else {
$data['mailchimp']['api_key'] = '';
$data['mailchimp']['list_id'] = '';
}
$data['mailchimp']['lists'] = $this->_mailchimp();
return view('configs/manage', $data);
}
@@ -316,7 +289,6 @@ class Config extends Secure_Controller
return $this->response->setJSON(['success' => $success, 'message' => $message]);
}
/**
* @return array
*/
@@ -575,76 +547,6 @@ class Config extends Secure_Controller
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
}
/**
* This function fetches all the available lists from Mailchimp for the given API key
*/
private function _mailchimp(string $api_key = ''): array // TODO: Hungarian notation
{
$mailchimp_lib = new Mailchimp_lib(['api_key' => $api_key]);
$result = [];
$lists = $mailchimp_lib->getLists();
if ($lists !== false) {
if (is_array($lists) && !empty($lists['lists']) && is_array($lists['lists'])) {
foreach ($lists['lists'] as $list) {
$result[$list['id']] = $list['name'] . ' [' . $list['stats']['member_count'] . ']';
}
}
}
return $result;
}
/**
* Gets Mailchimp lists when a valid API key is inserted. Used in app/Views/configs/integrations_config.php
*
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function postCheckMailchimpApiKey(): ResponseInterface
{
$lists = $this->_mailchimp($this->request->getPost('mailchimp_api_key'));
$success = count($lists) > 0;
return $this->response->setJSON([
'success' => $success,
'message' => lang('Config.mailchimp_key_' . ($success ? '' : 'un') . 'successfully'),
'mailchimp_lists' => $lists
]);
}
/**
* Saves Mailchimp configuration. Used in app/Views/configs/integrations_config.php
*
* @throws ReflectionException
* @return ResponseInterface
* @noinspection PhpUnused
*/
public function postSaveMailchimp(): ResponseInterface
{
$api_key = '';
$list_id = '';
if (check_encryption()) {
$api_key_unencrypted = $this->request->getPost('mailchimp_api_key');
if (!empty($api_key_unencrypted)) {
$api_key = $this->encrypter->encrypt($api_key_unencrypted);
}
$list_id_unencrypted = $this->request->getPost('mailchimp_list_id');
if (!empty($list_id_unencrypted)) {
$list_id = $this->encrypter->encrypt($list_id_unencrypted);
}
}
$batch_save_data = ['mailchimp_api_key' => $api_key, 'mailchimp_list_id' => $list_id];
$success = $this->appconfig->batch_save($batch_save_data);
return $this->response->setJSON(['success' => $success, 'message' => lang('Config.saved_' . ($success ? '' : 'un') . 'successfully')]);
}
/**
* Gets all stock locations. Used in app/Views/configs/stock_config.php
*
@@ -924,7 +826,9 @@ class Config extends Secure_Controller
public function postSaveReceipt(): ResponseInterface
{
$batch_save_data = [
'receipt_template' => $this->request->getPost('receipt_template'),
'receipt_template' => Sale_lib::isValidReceiptTemplate($this->request->getPost('receipt_template'))
? $this->request->getPost('receipt_template')
: 'receipt_default',
'receipt_font_size' => $this->request->getPost('receipt_font_size', FILTER_SANITIZE_NUMBER_INT),
'print_delay_autoreturn' => $this->request->getPost('print_delay_autoreturn', FILTER_SANITIZE_NUMBER_INT),
'email_receipt_check_behaviour' => $this->request->getPost('email_receipt_check_behaviour'),
@@ -1010,8 +914,8 @@ class Config extends Secure_Controller
'work_order_enable' => $this->request->getPost('work_order_enable') != null,
'work_order_format' => $this->request->getPost('work_order_format'),
'last_used_work_order_number' => $this->request->getPost('last_used_work_order_number', FILTER_SANITIZE_NUMBER_INT),
'invoice_type' => Sale_lib::isValidInvoiceType($this->request->getPost('invoice_type'))
? $this->request->getPost('invoice_type')
'invoice_type' => Sale_lib::isValidInvoiceType($this->request->getPost('invoice_type'))
? $this->request->getPost('invoice_type')
: 'invoice'
];
@@ -1057,8 +961,8 @@ class Config extends Secure_Controller
return $fieldType === 'first' ? 'name' : '';
}
$allowed = $fieldType === 'first'
? Item::ALLOWED_SUGGESTIONS_COLUMNS
$allowed = $fieldType === 'first'
? Item::ALLOWED_SUGGESTIONS_COLUMNS
: Item::ALLOWED_SUGGESTIONS_COLUMNS_WITH_EMPTY;
$fallback = $fieldType === 'first' ? 'name' : '';

View File

@@ -2,11 +2,10 @@
namespace App\Controllers;
use App\Libraries\Mailchimp_lib;
use App\Models\Customer;
use App\Models\Customer_rewards;
use App\Models\Tax_code;
use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\DownloadResponse;
use CodeIgniter\HTTP\ResponseInterface;
use Config\OSPOS;
@@ -15,8 +14,6 @@ use stdClass;
class Customers extends Persons
{
private string $_list_id;
private Mailchimp_lib $mailchimp_lib;
private Customer_rewards $customer_rewards;
private Customer $customer;
private Tax_code $tax_code;
@@ -25,19 +22,11 @@ class Customers extends Persons
public function __construct()
{
parent::__construct('customers');
$this->mailchimp_lib = new Mailchimp_lib();
$this->customer_rewards = model(Customer_rewards::class);
$this->customer = model(Customer::class);
$this->tax_code = model(Tax_code::class);
$this->config = config(OSPOS::class)->settings;
$encrypter = Services::encrypter();
if (!empty($this->config['mailchimp_list_id'])) {
$this->_list_id = $encrypter->decrypt($this->config['mailchimp_list_id']);
} else {
$this->_list_id = '';
}
}
/**
@@ -52,11 +41,12 @@ class Customers extends Persons
/**
* Gets one row for a customer manage table. This is called using AJAX to update one row.
* @param int $row_id
* @return ResponseInterface
*/
public function getRow(int $row_id): ResponseInterface
{
$person = $this->customer->get_info($row_id);
$person = $this->customer->getInfo($row_id);
// Retrieve the total amount the customer spent so far together with min, max and average values
$stats = $this->customer->get_stats($person->person_id); // TODO: This and the next 11 lines are duplicated in search(). Extract a method.
@@ -141,14 +131,16 @@ class Customers extends Persons
/**
* Loads the customer edit form
* @param int $customerId
* @return string
*/
public function getView(int $customer_id = NEW_ENTRY): string
public function getView(int $customerId = NEW_ENTRY): string
{
// Set default values
if ($customer_id == null) $customer_id = NEW_ENTRY;
if ($customerId == null) {
$customerId = NEW_ENTRY;
}
$info = $this->customer->get_info($customer_id);
$info = $this->customer->getInfo($customerId);
foreach (get_object_vars($info) as $property => $value) {
$info->$property = $value;
}
@@ -159,7 +151,7 @@ class Customers extends Persons
$data['person_info']->employee_id = $this->employee->get_logged_in_employee_info()->person_id;
}
$employee_info = $this->employee->get_info($info->employee_id);
$employee_info = $this->employee->getInfo($info->employee_id);
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$tax_code_info = $this->tax_code->get_info($info->sales_tax_code_id);
@@ -180,7 +172,7 @@ class Customers extends Persons
$data['use_destination_based_tax'] = $this->config['use_destination_based_tax'];
// Retrieve the total amount the customer spent so far together with min, max and average values
$stats = $this->customer->get_stats($customer_id);
$stats = $this->customer->get_stats($customerId);
if (!empty($stats)) {
foreach (get_object_vars($stats) as $property => $value) {
$info->$property = $value;
@@ -188,69 +180,29 @@ class Customers extends Persons
$data['stats'] = $stats;
}
// Retrieve the info from Mailchimp only if there is an email address assigned
if (!empty($info->email)) {
// Collect Mailchimp customer info
if (($mailchimp_info = $this->mailchimp_lib->getMemberInfo($this->_list_id, $info->email)) !== false) {
$data['mailchimp_info'] = $mailchimp_info;
// Collect customer Mailchimp emails activities (stats)
if (($activities = $this->mailchimp_lib->getMemberActivity($this->_list_id, $info->email)) !== false) {
if (array_key_exists('activity', $activities)) {
$open = 0;
$unopen = 0;
$click = 0;
$total = 0;
$lastopen = '';
foreach ($activities['activity'] as $activity) {
if ($activity['action'] == 'sent') {
++$unopen;
} elseif ($activity['action'] == 'open') {
if (empty($lastopen)) {
$lastopen = substr($activity['timestamp'], 0, 10);
}
++$open;
} elseif ($activity['action'] == 'click') {
if (empty($lastopen)) {
$lastopen = substr($activity['timestamp'], 0, 10);
}
++$click;
}
++$total;
}
$data['mailchimp_activity']['total'] = $total;
$data['mailchimp_activity']['open'] = $open;
$data['mailchimp_activity']['unopen'] = $unopen;
$data['mailchimp_activity']['click'] = $click;
$data['mailchimp_activity']['lastopen'] = $lastopen;
}
}
}
}
Events::trigger('customer_loaded', $customerId);
return view("customers/form", $data);
}
/**
* Inserts/updates a customer
* @param int $customerId
* @return ResponseInterface
*/
public function postSave(int $customer_id = NEW_ENTRY): ResponseInterface
public function postSave(int $customerId = NEW_ENTRY): ResponseInterface
{
$first_name = $this->request->getPost('first_name');
$last_name = $this->request->getPost('last_name');
$firstName = $this->request->getPost('first_name');
$lastName = $this->request->getPost('last_name');
$email = strtolower($this->request->getPost('email', FILTER_SANITIZE_EMAIL));
// Format first and last name properly
$first_name = $this->nameize($first_name);
$last_name = $this->nameize($last_name);
$firstName = $this->nameize($firstName);
$lastName = $this->nameize($lastName);
$person_data = [
'first_name' => $first_name,
'last_name' => $last_name,
$personData = [
'first_name' => $firstName,
'last_name' => $lastName,
'gender' => $this->request->getPost('gender', FILTER_SANITIZE_NUMBER_INT),
'email' => $email,
'phone_number' => $this->request->getPost('phone_number'),
@@ -263,9 +215,9 @@ class Customers extends Persons
'comments' => $this->request->getPost('comments')
];
$date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $this->request->getPost('date'));
$dateFormatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $this->request->getPost('date'));
$customer_data = [
$customerData = [
'consent' => $this->request->getPost('consent') != null,
'account_number' => $this->request->getPost('account_number') == '' ? null : $this->request->getPost('account_number'),
'tax_id' => $this->request->getPost('tax_id'),
@@ -274,41 +226,32 @@ class Customers extends Persons
'discount_type' => $this->request->getPost('discount_type') == null ? PERCENT : $this->request->getPost('discount_type', FILTER_SANITIZE_NUMBER_INT),
'package_id' => $this->request->getPost('package_id') == '' ? null : $this->request->getPost('package_id'),
'taxable' => $this->request->getPost('taxable') != null,
'date' => $date_formatter->format('Y-m-d H:i:s'),
'date' => $dateFormatter->format('Y-m-d H:i:s'),
'employee_id' => $this->request->getPost('employee_id', FILTER_SANITIZE_NUMBER_INT),
'sales_tax_code_id' => $this->request->getPost('sales_tax_code_id') == '' ? null : $this->request->getPost('sales_tax_code_id', FILTER_SANITIZE_NUMBER_INT)
];
if ($this->customer->save_customer($person_data, $customer_data, $customer_id)) {
// Save customer to Mailchimp selected list // TODO: addOrUpdateMember should be refactored. Potentially pass an array or object instead of 6 parameters.
$mailchimp_status = $this->request->getPost('mailchimp_status');
$this->mailchimp_lib->addOrUpdateMember(
$this->_list_id,
$email,
$first_name,
$last_name,
$mailchimp_status == null ? "" : $mailchimp_status,
['vip' => $this->request->getPost('mailchimp_vip') != null]
);
if ($this->customer->saveCustomer($personData, $customerData, $customerId)) {
Events::trigger('customer_saved', [$customerData['person_id']]);
// New customer
if ($customer_id == NEW_ENTRY) {
if ($customerId == NEW_ENTRY) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_adding') . ' ' . $first_name . ' ' . $last_name,
'id' => $customer_data['person_id']
'message' => lang('Customers.successful_adding') . " $firstName $lastName",
'id' => $customerData['person_id']
]);
} else { // Existing customer
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_updating') . ' ' . $first_name . ' ' . $last_name,
'id' => $customer_id
'message' => lang('Customers.successful_updating') . " $firstName $lastName",
'id' => $customerId
]);
}
} else { // Failure
return $this->response->setJSON([
'success' => false,
'message' => lang('Customers.error_adding_updating') . ' ' . $first_name . ' ' . $last_name,
'message' => lang('Customers.error_adding_updating') . " $firstName $lastName",
'id' => NEW_ENTRY
]);
}
@@ -344,26 +287,23 @@ class Customers extends Persons
}
/**
* This deletes customers from the customers table
* This deletes customers from the customer's table
* @return ResponseInterface
*/
public function postDelete(): ResponseInterface
{
$customers_to_delete = $this->request->getPost('ids');
$customers_info = $this->customer->get_multiple_info($customers_to_delete);
$customersToDelete = $this->request->getPost('ids');
$customers = $this->customer->get_multiple_info($customersToDelete);
$count = 0;
foreach ($customers_info->getResult() as $info) {
if ($this->customer->delete($info->person_id)) {
// remove customer from Mailchimp selected list
$this->mailchimp_lib->removeMember($this->_list_id, $info->email);
foreach ($customers->getResult() as $customer) {
if ($this->customer->delete($customer->person_id)) {
Events::trigger('customer_deleted', (int)$customer->person_id, (string)$customer->email);
$count++;
}
}
if ($count == count($customers_to_delete)) {
if ($count === count($customersToDelete)) {
return $this->response->setJSON([
'success' => true,
'message' => lang('Customers.successful_deleted') . ' ' . $count . ' ' . lang('Customers.one_or_multiple')
@@ -411,16 +351,17 @@ class Customers extends Persons
if (($handle = fopen($_FILES['file_path']['tmp_name'], 'r')) !== false) {
// Skip the first row as it's the table description
fgetcsv($handle);
$i = 1;
$rowNumber = 1;
$failCodes = [];
$customerIds = [];
while (($data = fgetcsv($handle)) !== false) {
$consent = $data[3] == '' ? 0 : 1;
if (sizeof($data) >= 16 && $consent) {
$email = strtolower($data[4]);
$person_data = [
$personData = [
'first_name' => $data[0],
'last_name' => $data[1],
'gender' => $data[2],
@@ -435,7 +376,7 @@ class Customers extends Persons
'comments' => $data[12]
];
$customer_data = [
$customerData = [
'consent' => $consent,
'company_name' => $data[13],
'discount' => $data[15],
@@ -450,7 +391,7 @@ class Customers extends Persons
$invalidated = $this->customer->check_email_exists($email);
if ($account_number != '') {
$customer_data['account_number'] = $account_number;
$customerData['account_number'] = $account_number;
$invalidated &= $this->customer->check_account_number_exists($account_number);
}
} else {
@@ -458,16 +399,15 @@ class Customers extends Persons
}
if ($invalidated) {
$failCodes[] = $i;
log_message('error', "Row $i was not imported: Either email or account number already exist or data was invalid.");
} elseif ($this->customer->save_customer($person_data, $customer_data)) {
// Save customer to Mailchimp selected list
$this->mailchimp_lib->addOrUpdateMember($this->_list_id, $person_data['email'], $person_data['first_name'], '', $person_data['last_name']);
$failCodes[] = $rowNumber;
log_message('error', "Row $rowNumber was not imported: Either email or account number already exist or data was invalid.");
} elseif ($this->customer->saveCustomer($personData, $customerData)) {
$customerIds[] = $customerData['person_id'];
} else {
$failCodes[] = $i;
$failCodes[] = $rowNumber;
}
++$i;
++$rowNumber;
}
if (count($failCodes) > 0) {
@@ -475,6 +415,8 @@ class Customers extends Persons
return $this->response->setJSON(['success' => false, 'message' => $message]);
} else {
Events::trigger('customer_saved', $customerIds);
return $this->response->setJSON(['success' => true, 'message' => lang('Customers.csv_import_success')]);
}
} else {

View File

@@ -75,7 +75,7 @@ class Employees extends Persons
*/
public function getView(int $employee_id = NEW_ENTRY): string
{
$person_info = $this->employee->get_info($employee_id);
$person_info = $this->employee->getInfo($employee_id);
$current_user = $this->employee->get_logged_in_employee_info();
if ($employee_id != NEW_ENTRY && !$this->employee->canModifyEmployee($person_info->person_id, $current_user->person_id)) {
@@ -119,7 +119,7 @@ class Employees extends Persons
$current_user = $this->employee->get_logged_in_employee_info();
if ($employee_id != NEW_ENTRY) {
$target_employee = $this->employee->get_info($employee_id);
$target_employee = $this->employee->getInfo($employee_id);
if (!$this->employee->canModifyEmployee($target_employee->person_id, $current_user->person_id)) {
return $this->response->setJSON([
'success' => false,

View File

@@ -106,7 +106,7 @@ class Expenses extends Secure_Controller
}
} else {
$stored_employee_id = $expense_id == NEW_ENTRY ? $current_employee_id : $data['expenses_info']->employee_id;
$stored_employee = $this->employee->get_info($stored_employee_id);
$stored_employee = $this->employee->getInfo($stored_employee_id);
$data['employees'][$stored_employee_id] = $stored_employee->first_name . ' ' . $stored_employee->last_name;
}
$data['can_assign_employee'] = $can_assign_employee;

View File

@@ -51,7 +51,7 @@ class Home extends Secure_Controller
return $this->response->setStatusCode(403)->setBody(lang('Employees.unauthorized_modify'));
}
$person_info = $this->employee->get_info($employeeId);
$person_info = $this->employee->getInfo($employeeId);
foreach (get_object_vars($person_info) as $property => $value) {
$person_info->$property = $value;
}

View File

@@ -13,6 +13,7 @@ use App\Models\Item_taxes;
use App\Models\Stock_location;
use App\Models\Supplier;
use App\Models\Tax_category;
use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\ResponseInterface;
use CodeIgniter\Images\Handlers\BaseHandler;
use CodeIgniter\HTTP\DownloadResponse;
@@ -154,8 +155,23 @@ class Items extends Secure_Controller
{
helper('file');
$pic_filename = rawurldecode($pic_filename);
$file_extension = pathinfo($pic_filename, PATHINFO_EXTENSION);
// Security: Sanitize filename to prevent path traversal
// Use basename() to strip directory components and prevent '../' attacks
$pic_filename = basename(rawurldecode($pic_filename));
$file_extension = strtolower(pathinfo($pic_filename, PATHINFO_EXTENSION));
// Validate file extension against system-configured allowed image types
// Handle both legacy pipe-separated and current comma-separated formats
// Fallback to types that GD library can process for thumbnail generation
$allowed_types = $this->config['image_allowed_types'] ?? 'jpg,jpeg,gif,png,webp,bmp,tif,tiff';
$allowed_extensions = strpos($allowed_types, '|') !== false
? explode('|', $allowed_types)
: explode(',', $allowed_types);
if (!in_array($file_extension, $allowed_extensions, true)) {
return $this->response->setStatusCode(400)->setBody('Invalid file type');
}
$images = glob("./uploads/item_pics/$pic_filename");
$base_path = './uploads/item_pics/' . pathinfo($pic_filename, PATHINFO_FILENAME);
@@ -260,7 +276,7 @@ class Items extends Secure_Controller
*/
public function getRow(string $item_ids): ResponseInterface // TODO: An array would be better for parameter.
{
$item_infos = $this->item->get_multiple_info(explode(':', $item_ids), $this->item_lib->get_item_location());
$item_infos = $this->item->getMultipleInfo(explode(':', $item_ids), $this->item_lib->get_item_location());
$result = [];
@@ -476,7 +492,7 @@ class Items extends Secure_Controller
public function getGenerateBarcodes(string $item_ids): string // TODO: Passing these through as a string instead of an array limits the contents of the item_ids. Perhaps a better approach would to serialize as JSON in an array and pass through post variables?
{
$item_ids = explode(':', $item_ids);
$result = $this->item->get_multiple_info($item_ids, $this->item_lib->get_item_location())->getResultArray();
$result = $this->item->getMultipleInfo($item_ids, $this->item_lib->get_item_location())->getResultArray();
$data['barcode_config'] = $this->barcode_lib->get_barcode_config();
foreach ($result as &$item) {
@@ -596,148 +612,149 @@ class Items extends Secure_Controller
}
/**
* @param int $item_id
* @param int $itemId
* @return ResponseInterface
* @throws ReflectionException
*/
public function postSave(int $item_id = NEW_ENTRY): ResponseInterface
public function postSave(int $itemId = NEW_ENTRY): ResponseInterface
{
$upload_data = $this->upload_image();
$upload_success = empty($upload_data['error']);
$uploadData = $this->upload_image();
$uploadSuccess = empty($uploadData['error']);
$raw_receiving_quantity = $this->request->getPost('receiving_quantity');
$rawReceivingQuantity = $this->request->getPost('receiving_quantity');
$receiving_quantity = parse_quantity($raw_receiving_quantity);
$item_type = $this->request->getPost('item_type') === null ? ITEM : intval($this->request->getPost('item_type'));
$receivingQuantity = parse_quantity($rawReceivingQuantity);
$itemType = $this->request->getPost('item_type') === null ? ITEM : intval($this->request->getPost('item_type'));
if ($receiving_quantity === 0.0 && $item_type !== ITEM_TEMP) {
$receiving_quantity = 1;
if ($receivingQuantity === 0.0 && $itemType !== ITEM_TEMP) {
$receivingQuantity = 1;
}
$default_pack_name = lang('Items.default_pack_name');
$defaultPackName = lang('Items.default_pack_name');
$cost_price = parse_decimals($this->request->getPost('cost_price'));
$unit_price = parse_decimals($this->request->getPost('unit_price'));
$reorder_level = parse_quantity($this->request->getPost('reorder_level'));
$qty_per_pack = parse_quantity($this->request->getPost('qty_per_pack') ?? '');
$costPrice = parse_decimals($this->request->getPost('cost_price'));
$unitPrice = parse_decimals($this->request->getPost('unit_price'));
$reorderLevel = parse_quantity($this->request->getPost('reorder_level'));
$quantityPerPack = parse_quantity($this->request->getPost('qty_per_pack') ?? '');
// Save item data
$item_data = [
$itemData = [
'name' => $this->request->getPost('name'),
'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS),
'category' => $this->request->getPost('category'),
'item_type' => $item_type,
'item_type' => $itemType,
'stock_type' => $this->request->getPost('stock_type') === null ? HAS_STOCK : intval($this->request->getPost('stock_type')),
'supplier_id' => empty($this->request->getPost('supplier_id')) ? null : intval($this->request->getPost('supplier_id')),
'item_number' => empty($this->request->getPost('item_number')) ? null : $this->request->getPost('item_number'),
'cost_price' => $cost_price,
'unit_price' => $unit_price,
'reorder_level' => $reorder_level,
'receiving_quantity' => $receiving_quantity,
'cost_price' => $costPrice,
'unit_price' => $unitPrice,
'reorder_level' => $reorderLevel,
'receiving_quantity' => $receivingQuantity,
'allow_alt_description' => $this->request->getPost('allow_alt_description') != null,
'is_serialized' => $this->request->getPost('is_serialized') != null,
'qty_per_pack' => $this->request->getPost('qty_per_pack') == null ? 1 : parse_quantity($qty_per_pack),
'pack_name' => $this->request->getPost('pack_name') == null ? $default_pack_name : $this->request->getPost('pack_name'),
'low_sell_item_id' => $this->request->getPost('low_sell_item_id') === null ? $item_id : intval($this->request->getPost('low_sell_item_id')),
'qty_per_pack' => $this->request->getPost('qty_per_pack') == null ? 1 : parse_quantity($quantityPerPack),
'pack_name' => $this->request->getPost('pack_name') == null ? $defaultPackName : $this->request->getPost('pack_name'),
'low_sell_item_id' => $this->request->getPost('low_sell_item_id') === null ? $itemId : intval($this->request->getPost('low_sell_item_id')),
'deleted' => $this->request->getPost('is_deleted') != null,
'hsn_code' => $this->request->getPost('hsn_code') === null ? '' : $this->request->getPost('hsn_code')
];
if ($item_data['item_type'] == ITEM_TEMP) {
$item_data['stock_type'] = HAS_NO_STOCK;
$item_data['receiving_quantity'] = 0;
$item_data['reorder_level'] = 0;
if ($itemData['item_type'] == ITEM_TEMP) {
$itemData['stock_type'] = HAS_NO_STOCK;
$itemData['receiving_quantity'] = 0;
$itemData['reorder_level'] = 0;
}
$tax_category_id = $this->request->getPost('tax_category_id');
$taxCategoryId = $this->request->getPost('tax_category_id');
if (!isset($tax_category_id)) {
$item_data['tax_category_id'] = null;
if (!isset($taxCategoryId)) {
$itemData['tax_category_id'] = null;
} else {
$item_data['tax_category_id'] = empty($this->request->getPost('tax_category_id')) ? null : intval($this->request->getPost('tax_category_id'));
$itemData['tax_category_id'] = empty($this->request->getPost('tax_category_id')) ? null : intval($this->request->getPost('tax_category_id'));
}
if (!empty($upload_data['orig_name']) && $upload_data['raw_name']) {
$item_data['pic_filename'] = $upload_data['raw_name'] . '.' . $upload_data['file_ext'];
if (!empty($uploadData['orig_name']) && $uploadData['raw_name']) {
$itemData['pic_filename'] = $uploadData['raw_name'] . '.' . $uploadData['file_ext'];
}
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
$employeeId = $this->employee->get_logged_in_employee_info()->person_id;
if ($this->item->save_value($item_data, $item_id)) {
if ($this->item->save_value($itemData, $itemId)) {
$success = true;
$new_item = false;
$newItem = false;
if ($item_id === NEW_ENTRY) {
$item_id = $item_data['item_id'];
$new_item = true;
if ($itemId === NEW_ENTRY) {
$itemId = $itemData['item_id'];
$newItem = true;
}
$use_destination_based_tax = (bool)$this->config['use_destination_based_tax'];
$useDestinationBasedTax = (bool)$this->config['use_destination_based_tax'];
if (!$use_destination_based_tax) {
$items_taxes_data = [];
$tax_names = $this->request->getPost('tax_names');
$tax_percents = $this->request->getPost('tax_percents');
if (!$useDestinationBasedTax) {
$itemsTaxesData = [];
$taxNames = $this->request->getPost('tax_names');
$taxPercents = $this->request->getPost('tax_percents');
$tax_name_index = 0;
$taxNameIndex = 0;
foreach ($tax_percents as $tax_percent) {
$tax_percentage = parse_tax($tax_percent);
foreach ($taxPercents as $taxPercent) {
$taxpercentage = parse_tax($taxPercent);
if (is_numeric($tax_percentage)) {
$items_taxes_data[] = ['name' => $tax_names[$tax_name_index], 'percent' => $tax_percentage];
if (is_numeric($taxpercentage)) {
$itemsTaxesData[] = ['name' => $taxNames[$taxNameIndex], 'percent' => $taxpercentage];
}
$tax_name_index++;
$taxNameIndex++;
}
$success &= $this->item_taxes->save_value($items_taxes_data, $item_id);
$success &= $this->item_taxes->save_value($itemsTaxesData, $itemId);
}
// Save item quantity
$stock_locations = $this->stock_location->get_undeleted_all()->getResultArray();
foreach ($stock_locations as $location) {
$updated_quantity = parse_quantity($this->request->getPost('quantity_' . $location['location_id']));
$stockLocations = $this->stock_location->get_undeleted_all()->getResultArray();
foreach ($stockLocations as $location) {
$updatedQuantity = parse_quantity($this->request->getPost('quantity_' . $location['location_id']));
if ($item_data['item_type'] == ITEM_TEMP) {
$updated_quantity = 0;
if ($itemData['item_type'] == ITEM_TEMP) {
$updatedQuantity = 0;
}
$location_detail = [
'item_id' => $item_id,
$locationDetail = [
'item_id' => $itemId,
'location_id' => $location['location_id'],
'quantity' => $updated_quantity
'quantity' => $updatedQuantity
];
$item_quantity = $this->item_quantity->get_item_quantity($item_id, $location['location_id']);
$itemQuantity = $this->item_quantity->get_item_quantity($itemId, $location['location_id']);
if ($item_quantity->quantity != $updated_quantity || $new_item) {
$success = $success && $this->item_quantity->save_value($location_detail, $item_id, $location['location_id']);
if ($itemQuantity->quantity != $updatedQuantity || $newItem) {
$success = $success && $this->item_quantity->save_value($locationDetail, $itemId, $location['location_id']);
$inv_data = [
'trans_date' => date('Y-m-d H:i:s'),
'trans_items' => $item_id,
'trans_user' => $employee_id,
'trans_items' => $itemId,
'trans_user' => $employeeId,
'trans_location' => $location['location_id'],
'trans_comment' => lang('Items.manually_editing_of_quantity'),
'trans_inventory' => $updated_quantity - $item_quantity->quantity
'trans_inventory' => $updatedQuantity - $itemQuantity->quantity
];
$success = $success && $this->inventory->insert($inv_data, false);
}
}
$success = $success && $this->saveItemAttributes($item_id);
$success = $success && $this->saveItemAttributes($itemId);
if ($success && $upload_success) {
$message = lang('Items.successful_' . ($new_item ? 'adding' : 'updating')) . ' ' . $item_data['name'];
if ($success && $uploadSuccess) {
Events::trigger('item_saved', [$itemId]);
return $this->response->setJSON(['success' => true, 'message' => $message, 'id' => $item_id]);
$message = lang('Items.successful_' . ($newItem ? 'adding' : 'updating')) . ' ' . $itemData['name'];
return $this->response->setJSON(['success' => true, 'message' => $message, 'id' => $itemId]);
} else {
$message = $upload_success ? lang('Items.error_adding_updating') . ' ' . $item_data['name'] : strip_tags($upload_data['error']);
$message = $uploadSuccess ? lang('Items.error_adding_updating') . ' ' . $itemData['name'] : strip_tags($uploadData['error']);
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => $item_id]);
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => $itemId]);
}
} else {
$message = lang('Items.error_adding_updating') . ' ' . $item_data['name'];
$message = lang('Items.error_adding_updating') . ' ' . $itemData['name'];
return $this->response->setJSON(['success' => false, 'message' => $message, 'id' => NEW_ENTRY]);
}
@@ -957,7 +974,7 @@ class Items extends Secure_Controller
}
/**
* Imports items from a CSV formatted file.
* Imports items from a CSV-formatted file.
* @return ResponseInterface
* @noinspection PhpUnused
*/
@@ -982,7 +999,7 @@ class Items extends Secure_Controller
$attributeData = [];
foreach ($attributeDefinitionNames as $definitionName) {
$attributeData[$definitionName] = $this->attribute->get_definition_by_name($definitionName)[0];
$attributeData[$definitionName] = $this->attribute->getDefinitionByName($definitionName)[0];
if ($attributeData[$definitionName]['definition_type'] === DROPDOWN) {
$attributeData[$definitionName]['dropdown_values'] = $this->attribute->get_definition_values($attributeData[$definitionName]['definition_id']);
@@ -991,6 +1008,7 @@ class Items extends Secure_Controller
$db = db_connect();
$db->transBegin(); // TODO: This section needs to be reworked so that the data array is being created then passed to the Item model because $db doesn't exist in the controller without being instantiated, but database operations should be restricted to the model
$itemIds = [];
foreach ($csvRows as $key => $row) {
$isFailedRow = false;
$itemId = (int)$row['Id'];
@@ -1040,20 +1058,28 @@ class Items extends Secure_Controller
});
if (!$isFailedRow && $this->item->save_value($itemData, $itemId)) {
$this->save_tax_data($row, $itemData);
$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId);
if (!$this->save_tax_data($row, $itemData)) {
$isFailedRow = true;
}
if (!$this->save_inventory_quantities($row, $itemData, $allowedStockLocations, $employeeId)) {
$isFailedRow = true;
}
$csvAttributeValues = $this->extractAttributeData($row);
$isFailedRow = !$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData);
if (!$this->attribute->saveCSVRowAttributeData($csvAttributeValues, $itemData, $attributeData)) {
$isFailedRow = true;
}
if ($isFailedRow) {
$failedRow = $key + 2;
$failCodes[] = $failedRow;
log_message('error', "CSV Item import failed on line $failedRow while saving attributes.");
log_message('error', "CSV Item import failed on line $failedRow while saving item.");
continue;
}
if ($isUpdate) {
$itemData = array_merge($itemData, get_object_vars($this->item->get_info_by_id_or_number($itemId)));
}
$itemIds[] = $itemData['item_id'];
} else {
$failedRow = $key + 2;
$failCodes[] = $failedRow;
@@ -1073,6 +1099,8 @@ class Items extends Secure_Controller
$db->transCommit();
$this->attribute->deleteOrphanedValues();
Events::trigger('item_saved', [$itemIds]);
return $this->response->setJSON(['success' => true, 'message' => lang('Items.csv_import_success')]);
}
} else {
@@ -1237,13 +1265,15 @@ class Items extends Secure_Controller
* @param array $item_data
* @param array $allowed_locations
* @param int $employee_id
* @return bool Returns true on success, false on failure
* @throws ReflectionException
*/
private function save_inventory_quantities(array $row, array $item_data, array $allowed_locations, int $employee_id): void
private function save_inventory_quantities(array $row, array $item_data, array $allowed_locations, int $employee_id): bool
{
// Quantities & Inventory Section
$comment = lang('Items.inventory_CSV_import_quantity');
$is_update = (bool)$row['Id'];
$success = true;
foreach ($allowed_locations as $location_id => $location_name) {
$item_quantity_data = ['item_id' => $item_data['item_id'], 'location_id' => $location_id];
@@ -1257,20 +1287,22 @@ class Items extends Secure_Controller
if (!empty($row["location_$location_name"]) || $row["location_$location_name"] === '0') {
$item_quantity_data['quantity'] = $row["location_$location_name"];
$this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
$success &= $this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
$csv_data['trans_inventory'] = $row["location_$location_name"];
$this->inventory->insert($csv_data, false);
$success &= (bool)$this->inventory->insert($csv_data, false);
} elseif ($is_update) {
return;
continue;
} else {
$item_quantity_data['quantity'] = 0;
$this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
$success &= $this->item_quantity->save_value($item_quantity_data, $item_data['item_id'], $location_id);
$csv_data['trans_inventory'] = 0;
$this->inventory->insert($csv_data, false);
$success &= (bool)$this->inventory->insert($csv_data, false);
}
}
return (bool)$success;
}
/**
@@ -1278,8 +1310,9 @@ class Items extends Secure_Controller
*
* @param array $row
* @param array $item_data
* @return bool Returns true on success, false on failure
*/
private function save_tax_data(array $row, array $item_data): void
private function save_tax_data(array $row, array $item_data): bool
{
$items_taxes_data = [];
@@ -1291,9 +1324,11 @@ class Items extends Secure_Controller
$items_taxes_data[] = ['name' => $row['Tax 2 Name'], 'percent' => $row['Tax 2 Percent']];
}
if (isset($items_taxes_data)) {
$this->item_taxes->save_value($items_taxes_data, $item_data['item_id']);
if (!empty($items_taxes_data)) {
return $this->item_taxes->save_value($items_taxes_data, $item_data['item_id']);
}
return true;
}
/**

View File

@@ -49,6 +49,13 @@ 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' => [
@@ -62,13 +69,6 @@ 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()

View File

@@ -33,7 +33,7 @@ class Messages extends Secure_Controller
public function getView(int $person_id = NEW_ENTRY): string
{
$person = model(Person::class);
$info = $person->get_info($person_id);
$info = $person->getInfo($person_id);
foreach (get_object_vars($info) as $property => $value) {
$info->$property = $value;

View File

@@ -49,7 +49,7 @@ abstract class Persons extends Secure_Controller
*/
public function getRow(int $row_id): ResponseInterface
{
$data_row = get_person_data_row($this->person->get_info($row_id));
$data_row = get_person_data_row($this->person->getInfo($row_id));
return $this->response->setJSON($data_row);
}

169
app/Controllers/Plugins.php Normal file
View File

@@ -0,0 +1,169 @@
<?php
namespace App\Controllers;
use App\Libraries\Plugins\PluginManager;
use CodeIgniter\HTTP\ResponseInterface;
class Plugins extends Secure_Controller
{
private PluginManager $pluginManager;
public function __construct()
{
parent::__construct('plugins');
$this->pluginManager = service('pluginManager');
}
public function getIndex(): string
{
$data['table_headers'] = get_plugin_manage_table_headers();
return view('plugins/manage', $data);
}
public function getSearch(): ResponseInterface
{
$search = strtolower($this->request->getGet('search') ?? '');
$limit = (int)($this->request->getGet('limit') ?? 0);
$offset = (int)($this->request->getGet('offset') ?? 0);
$sort = $this->sanitizeSortColumn(plugin_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'name');
$order = strtolower($this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? 'asc');
$pluginData = $this->buildPluginDataArray();
if ($search !== '') {
$pluginData = array_values(array_filter($pluginData, static function (array $p) use ($search): bool {
return str_contains(strtolower($p['name']), $search)
|| str_contains(strtolower($p['description']), $search)
|| str_contains(strtolower($p['id']), $search);
}));
}
$total = count($pluginData);
usort($pluginData, static function (array $a, array $b) use ($sort, $order): int {
$valA = strtolower($a[$sort] ?? $a['name']);
$valB = strtolower($b[$sort] ?? $b['name']);
return $order === 'asc' ? strcmp($valA, $valB) : strcmp($valB, $valA);
});
$pluginData = $limit > 0 ? array_slice($pluginData, $offset, $limit) : array_slice($pluginData, $offset);
return $this->response->setJSON(['total' => $total, 'rows' => array_map('get_plugin_data_row', $pluginData)]);
}
public function getRow(string $pluginId): ResponseInterface
{
$plugin = $this->pluginManager->getPlugin($pluginId);
if (!$plugin) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.not_found')]);
}
$enabled = $this->pluginManager->getEnabledPlugins();
$pluginData = [
'id' => $plugin->getPluginId(),
'name' => $plugin->getPluginName(),
'description' => $plugin->getPluginDescription(),
'version' => $plugin->getVersion(),
'enabled' => isset($enabled[$pluginId]),
'has_config' => $plugin->getConfigView() !== null,
];
return $this->response->setJSON(get_plugin_data_row($pluginData));
}
private function buildPluginDataArray(): array
{
$plugins = $this->pluginManager->getAllPlugins();
$enabled = $this->pluginManager->getEnabledPlugins();
$result = [];
foreach ($plugins as $pluginId => $plugin) {
$result[] = [
'id' => $plugin->getPluginId(),
'name' => $plugin->getPluginName(),
'description' => $plugin->getPluginDescription(),
'version' => $plugin->getVersion(),
'enabled' => isset($enabled[$pluginId]),
'has_config' => $plugin->getConfigView() !== null,
];
}
return $result;
}
public function postEnable(string $pluginId): ResponseInterface
{
if ($this->pluginManager->enablePlugin($pluginId)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.enabled')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.enable_failed')]);
}
public function postDisable(string $pluginId): ResponseInterface
{
if ($this->pluginManager->disablePlugin($pluginId)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.disabled')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.disable_failed')]);
}
public function postUninstall(string $pluginId): ResponseInterface
{
if ($this->pluginManager->uninstallPlugin($pluginId)) {
return $this->response->setJSON(['success' => true, 'message' => lang('Plugins.uninstalled')]);
}
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.uninstall_failed')]);
}
public function getConfig(string $pluginId): ResponseInterface
{
$plugin = $this->pluginManager->getPlugin($pluginId);
if (!$plugin) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.not_found')]);
}
$configView = $plugin->getConfigView();
if (!$configView) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.no_config')]);
}
$settings = $plugin->getSettings();
$data = array_merge(['settings' => $settings, 'plugin' => $plugin], $plugin->getConfigViewData());
// Plugin views may live outside app/Views/ (absolute path from plugin's __DIR__)
if (is_file($configView . '.php')) {
$renderer = \Config\Services::renderer(dirname($configView) . DIRECTORY_SEPARATOR, null, false);
echo $renderer->setData($data)->render(basename($configView));
} else {
echo view($configView, $data);
}
return $this->response;
}
/**
* Save plugin settings by calling the plugin's saveSettings method.
*
* @param string $pluginId The plugin ID for the current plugin
* @return ResponseInterface The JSON response
* @noinspection PhpUnused Called via AJAX
*/
public function postSaveConfig(string $pluginId): ResponseInterface
{
$plugin = $this->pluginManager->getPlugin($pluginId);
if (!$plugin) {
return $this->response->setJSON(['success' => false, 'message' => lang('Plugins.not_found')]);
}
$settings = $this->request->getPost();
unset($settings['_method'], $settings[csrf_token()]);
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

@@ -11,6 +11,7 @@ use App\Models\Item_kit;
use App\Models\Receiving;
use App\Models\Stock_location;
use App\Models\Supplier;
use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\ResponseInterface;
use Config\OSPOS;
use Config\Services;
@@ -253,7 +254,7 @@ class Receivings extends Secure_Controller
}
} else {
$stored_employee_id = $receiving_info['employee_id'];
$stored_employee = $this->employee->get_info($stored_employee_id);
$stored_employee = $this->employee->getInfo($stored_employee_id);
$data['employees'][$stored_employee_id] = $stored_employee->first_name . ' ' . $stored_employee->last_name;
}
@@ -342,12 +343,12 @@ class Receivings extends Secure_Controller
}
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
$employee_info = $this->employee->get_info($employee_id);
$employee_info = $this->employee->getInfo($employee_id);
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$supplier_id = $this->receiving_lib->get_supplier();
if ($supplier_id != -1) {
$supplier_info = $this->supplier->get_info($supplier_id);
$supplier_info = $this->supplier->getInfo($supplier_id);
$data['supplier'] = $supplier_info->company_name; // TODO: duplicated code
$data['first_name'] = $supplier_info->first_name;
$data['last_name'] = $supplier_info->last_name;
@@ -367,6 +368,7 @@ class Receivings extends Secure_Controller
$data['error_message'] = lang('Receivings.transaction_failed');
} else {
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['receiving_id']);
Events::trigger('receiving_complete', (int) substr($data['receiving_id'], 5), $data['mode']);
}
$data['print_after_sale'] = $this->receiving_lib->is_print_after_sale();
@@ -422,12 +424,12 @@ class Receivings extends Secure_Controller
$data['reference'] = $this->receiving_lib->get_reference();
$data['receiving_id'] = 'RECV ' . $receiving_id;
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['receiving_id']);
$employee_info = $this->employee->get_info($receiving_info['employee_id']);
$employee_info = $this->employee->getInfo($receiving_info['employee_id']);
$data['employee'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$supplier_id = $this->receiving_lib->get_supplier(); // TODO: Duplicated code
if ($supplier_id != -1) {
$supplier_info = $this->supplier->get_info($supplier_id);
$supplier_info = $this->supplier->getInfo($supplier_id);
$data['supplier'] = $supplier_info->company_name;
$data['first_name'] = $supplier_info->first_name;
$data['last_name'] = $supplier_info->last_name;
@@ -475,7 +477,7 @@ class Receivings extends Secure_Controller
$supplier_id = $this->receiving_lib->get_supplier();
if ($supplier_id != -1) { // TODO: Duplicated Code... replace -1 with a constant
$supplier_info = $this->supplier->get_info($supplier_id);
$supplier_info = $this->supplier->getInfo($supplier_id);
$data['supplier'] = $supplier_info->company_name;
$data['first_name'] = $supplier_info->first_name;
$data['last_name'] = $supplier_info->last_name;

View File

@@ -1246,13 +1246,15 @@ class Reports extends Secure_Controller
public function get_payment_type(): array
{
return [
'all' => lang('Common.none_selected_text'),
'cash' => lang('Sales.cash'),
'due' => lang('Sales.due'),
'check' => lang('Sales.check'),
'credit' => lang('Sales.credit'),
'debit' => lang('Sales.debit'),
'invoices' => lang('Sales.invoice')
'all' => lang('Common.none_selected_text'),
'cash' => lang('Sales.cash'),
'due' => lang('Sales.due'),
'check' => lang('Sales.check'),
'credit' => lang('Sales.credit'),
'debit' => lang('Sales.debit'),
'bank_transfer' => lang('Sales.bank_transfer'),
'wallet' => lang('Sales.wallet'),
'invoices' => lang('Sales.invoice')
];
}
@@ -1341,7 +1343,7 @@ class Reports extends Secure_Controller
}
}
$customer_info = $this->customer->get_info($customer_id);
$customer_info = $this->customer->getInfo($customer_id);
$customer_name = !empty($customer_info->company_name) // TODO: This variable is not used anywhere in the code. Should it be or can it be deleted?
? "[ $customer_info->company_name ]"
: $customer_info->company_name;
@@ -1468,7 +1470,7 @@ class Reports extends Secure_Controller
}
}
$employee_info = $this->employee->get_info($employee_id);
$employee_info = $this->employee->getInfo($employee_id);
// TODO: Duplicated Code
$data = [
'title' => $employee_info->first_name . ' ' . $employee_info->last_name . ' ' . lang('Reports.report'),
@@ -1734,7 +1736,7 @@ class Reports extends Secure_Controller
];
}
$supplier_info = $this->supplier->get_info((int) $supplier_id);
$supplier_info = $this->supplier->getInfo((int) $supplier_id);
$data = [
'title' => $supplier_info->company_name . ' (' . $supplier_info->first_name . ' ' . $supplier_info->last_name . ') ' . lang('Reports.report'),
'subtitle' => $this->_get_subtitle_report(['start_date' => $start_date, 'end_date' => $end_date]),

View File

@@ -20,6 +20,7 @@ use App\Models\Stock_location;
use App\Models\Tokens\Token_invoice_count;
use App\Models\Tokens\Token_customer;
use App\Models\Tokens\Token_invoice_sequence;
use CodeIgniter\Events\Events;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Services;
use Config\OSPOS;
@@ -93,6 +94,8 @@ class Sales extends Secure_Controller
'only_check' => lang('Sales.check_filter'),
'only_creditcard' => lang('Sales.credit_filter'),
'only_debit' => lang('Sales.debit'),
'only_bank_transfer'=> lang('Sales.bank_transfer'),
'only_wallet' => lang('Sales.wallet'),
'only_invoices' => lang('Sales.invoice_filter'),
'selected_customer' => lang('Sales.selected_customer')
];
@@ -156,8 +159,10 @@ class Sales extends Secure_Controller
'selected_customer' => false,
'only_creditcard' => false,
'only_debit' => false,
'only_bank_transfer'=> false,
'only_wallet' => false,
'only_invoices' => $this->config['invoice_enable'] && $this->request->getGet('only_invoices', FILTER_SANITIZE_NUMBER_INT),
'is_valid_receipt' => $this->sale->is_valid_receipt($search)
'is_valid_receipt' => $this->sale->isValidReceipt($search)
];
// Check if any filter is set in the multiselect dropdown
@@ -194,7 +199,7 @@ class Sales extends Secure_Controller
? $this->request->getGet('term')
: null;
if ($this->sale_lib->get_mode() == 'return' && $this->sale->is_valid_receipt($receipt)) {
if ($this->sale_lib->get_mode() == 'return' && $this->sale->isValidReceipt($receipt)) {
// If a valid receipt or invoice was found the search term will be replaced with a receipt number (POS #)
$suggestions[] = $receipt;
}
@@ -229,8 +234,8 @@ class Sales extends Secure_Controller
$customer_id = (int)$this->request->getPost('customer', FILTER_SANITIZE_NUMBER_INT);
if ($this->customer->exists($customer_id)) {
$this->sale_lib->set_customer($customer_id);
$discount = $this->customer->get_info($customer_id)->discount;
$discount_type = $this->customer->get_info($customer_id)->discount_type;
$discount = $this->customer->getInfo($customer_id)->discount;
$discount_type = $this->customer->getInfo($customer_id)->discount_type;
// Apply customer default discount to items that have 0 discount
if ($discount != '') {
@@ -433,9 +438,9 @@ class Sales extends Secure_Controller
}
} elseif ($payment_type === lang('Sales.rewards')) {
$customer_id = $this->sale_lib->get_customer();
$package_id = $this->customer->get_info($customer_id)->package_id;
$package_id = $this->customer->getInfo($customer_id)->package_id;
if (!empty($package_id)) {
$points = $this->customer->get_info($customer_id)->points;
$points = $this->customer->getInfo($customer_id)->points;
$points = ($points == null ? 0 : $points);
$payments = $this->sale_lib->get_payments();
@@ -507,8 +512,8 @@ class Sales extends Secure_Controller
$customer_id = $this->sale_lib->get_customer();
if ($customer_id != NEW_ENTRY) {
// Load the customer discount if any
$customer_discount = $this->customer->get_info($customer_id)->discount;
$customer_discount_type = $this->customer->get_info($customer_id)->discount_type;
$customer_discount = $this->customer->getInfo($customer_id)->discount;
$customer_discount_type = $this->customer->getInfo($customer_id)->discount_type;
if ($customer_discount != '') {
$discount = $customer_discount;
$discount_type = $customer_discount_type;
@@ -521,7 +526,7 @@ class Sales extends Secure_Controller
$quantity = ($mode == 'return') ? -$quantity : $quantity;
$item_location = $this->sale_lib->get_sale_location();
if ($mode == 'return' && $this->sale->is_valid_receipt($item_id_or_number_or_item_kit_or_receipt)) {
if ($mode == 'return' && $this->sale->isValidReceipt($item_id_or_number_or_item_kit_or_receipt)) {
$this->sale_lib->return_entire_sale($item_id_or_number_or_item_kit_or_receipt);
} elseif ($this->item_kit->is_valid_item_kit($item_id_or_number_or_item_kit_or_receipt)) {
// Add kit item to order if one is assigned
@@ -699,7 +704,7 @@ class Sales extends Secure_Controller
$data['show_stock_locations'] = $this->stock_location->show_locations('sales');
$data['comments'] = $this->sale_lib->get_comment();
$employee_id = $this->employee->get_logged_in_employee_info()->person_id;
$employee_info = $this->employee->get_info($employee_id);
$employee_info = $this->employee->getInfo($employee_id);
$data['employee'] = $employee_info->first_name . ' ' . mb_substr($employee_info->last_name, 0, 1);
$data['company_info'] = implode("\n", [$this->config['address'], $this->config['phone']]);
@@ -904,6 +909,16 @@ class Sales extends Secure_Controller
return $this->_reload($data);
} else {
$data['barcode'] = $this->barcode_lib->generate_receipt_barcode($data['sale_id']);
// Validate receipt template to prevent path traversal
$receipt_template = $this->config['receipt_template'] ?? '';
if (!Sale_lib::isValidReceiptTemplate($receipt_template)) {
$receipt_template = 'receipt_default';
}
$data['receipt_template_view'] = $receipt_template;
Events::trigger('sale_complete', $data['sale_id_num'], $sale_type);
$this->sale_lib->clear_all();
return view('sales/receipt', $data);
}
@@ -1006,7 +1021,7 @@ class Sales extends Secure_Controller
$customer_info = '';
if ($customer_id != NEW_ENTRY) {
$customer_info = $this->customer->get_info($customer_id);
$customer_info = $this->customer->getInfo($customer_id);
$data['customer_id'] = $customer_id;
if (!empty($customer_info->company_name)) {
@@ -1029,11 +1044,11 @@ class Sales extends Secure_Controller
$data['customer_account_number'] = $customer_info->account_number;
$data['customer_discount'] = $customer_info->discount;
$data['customer_discount_type'] = $customer_info->discount_type;
$package_id = $this->customer->get_info($customer_id)->package_id;
$package_id = $this->customer->getInfo($customer_id)->package_id;
if ($package_id != null) {
$package_name = $this->customer_rewards->get_name($package_id);
$points = $this->customer->get_info($customer_id)->points;
$points = $this->customer->getInfo($customer_id)->points;
$data['customer_rewards']['package_id'] = $package_id;
$data['customer_rewards']['points'] = empty($points) ? 0 : $points;
$data['customer_rewards']['package_name'] = $package_name;
@@ -1112,7 +1127,7 @@ class Sales extends Secure_Controller
$data['amount_change'] = $data['amount_due'] * -1;
$employee_info = $this->employee->get_info($this->sale_lib->get_employee());
$employee_info = $this->employee->getInfo($this->sale_lib->get_employee());
$data['employee'] = $employee_info->first_name . ' ' . mb_substr($employee_info->last_name, 0, 1);
$this->_load_customer_data($this->sale_lib->get_customer(), $data);
@@ -1159,6 +1174,13 @@ class Sales extends Secure_Controller
}
$data['invoice_view'] = $invoice_type;
// Validate receipt template to prevent path traversal
$receipt_template = $this->config['receipt_template'] ?? '';
if (!Sale_lib::isValidReceiptTemplate($receipt_template)) {
$receipt_template = 'receipt_default';
}
$data['receipt_template_view'] = $receipt_template;
return $data;
}
@@ -1320,7 +1342,7 @@ class Sales extends Secure_Controller
$sale_info = $this->sale->get_info($sale_id)->getRowArray();
$data['selected_customer_id'] = $sale_info['customer_id'];
$data['selected_customer_name'] = $sale_info['customer_name'];
$employee_info = $this->employee->get_info($sale_info['employee_id']);
$employee_info = $this->employee->getInfo($sale_info['employee_id']);
$data['selected_employee_id'] = $sale_info['employee_id'];
$data['selected_employee_name'] = $employee_info->first_name . ' ' . $employee_info->last_name;
$data['sale_info'] = $sale_info;

View File

@@ -99,10 +99,10 @@ class Secure_Controller extends BaseController
}
/**
* @param $key
* @param string $key
* @return mixed|void
*/
public function getConfig($key)
public function getConfig(string $key)
{
if (isset($config[$key])) {
return $config[$key];

View File

@@ -34,7 +34,7 @@ class Suppliers extends Persons
*/
public function getRow($row_id): ResponseInterface
{
$data_row = get_supplier_data_row($this->supplier->get_info($row_id));
$data_row = get_supplier_data_row($this->supplier->getInfo($row_id));
$data_row['category'] = $this->supplier->get_category_name($data_row['category']);
return $this->response->setJSON($data_row);
@@ -97,7 +97,7 @@ class Suppliers extends Persons
*/
public function getView(int $supplier_id = NEW_ENTRY): string
{
$info = $this->supplier->get_info($supplier_id);
$info = $this->supplier->getInfo($supplier_id);
foreach (get_object_vars($info) as $property => $value) {
$info->$property = $value;
}

View File

@@ -2,6 +2,7 @@
namespace App\Database\Migrations;
use CodeIgniter\Database\Exceptions\DatabaseException;
use CodeIgniter\Database\Migration;
class Migration_Upgrade_To_3_1_1 extends Migration
@@ -17,7 +18,37 @@ class Migration_Upgrade_To_3_1_1 extends Migration
public function up(): void
{
helper('migration');
execute_script(APPPATH . 'Database/Migrations/sqlscripts/3.0.2_to_3.1.1.sql');
// 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));
}
/**

View File

@@ -0,0 +1,20 @@
<?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.5.0_PluginConfigTableCreate.sql');
}
public function down(): void
{
$this->forge->dropTable('plugin_config', true);
}
}

View File

@@ -327,19 +327,6 @@ 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`;
--

View File

@@ -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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
-- 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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
-- Indexes for table `ospos_expense_categories`

View File

@@ -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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
-- Indexes for table `ospos_cash_up`

View File

@@ -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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
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 AUTO_INCREMENT=1;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci 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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
-- Add support for sales tax report

View File

@@ -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;
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COLLATE=utf8_general_ci;
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

View File

@@ -0,0 +1,21 @@
CREATE TABLE IF NOT EXISTS `ospos_plugin_config` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`plugin_id` varchar(100) NOT NULL,
`key` varchar(100) NOT NULL,
`value` text NOT NULL,
`is_control` tinyint(1) NOT NULL DEFAULT 0,
`created_at` timestamp NOT NULL DEFAULT current_timestamp(),
`updated_at` timestamp NOT NULL DEFAULT current_timestamp() ON UPDATE current_timestamp(),
PRIMARY KEY (`id`),
UNIQUE KEY `uq_plugin_key` (`plugin_id`, `key`),
KEY `idx_plugin_id` (`plugin_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
INSERT IGNORE INTO `ospos_modules` (`name_lang_key`, `desc_lang_key`, `sort`, `module_id`) VALUES
('module_plugins', 'module_plugins_desc', 111, 'plugins');
INSERT IGNORE INTO `ospos_permissions` (`permission_id`, `module_id`) VALUES
('plugins', 'plugins');
INSERT IGNORE INTO `ospos_grants` (`permission_id`, `person_id`, `menu_group`)
SELECT 'plugins', `person_id`, 'office' FROM `ospos_grants` WHERE `permission_id` = 'config';

View File

@@ -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;
}

View File

@@ -172,6 +172,7 @@ 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 = [];

View File

@@ -0,0 +1,19 @@
<?php
use CodeIgniter\Events\Events;
if (!function_exists('pluginContent')) {
function pluginContent(string $section, array $data = []): string
{
ob_start();
Events::trigger("view:{$section}", $data);
return ob_get_clean() ?: '';
}
}
if (!function_exists('pluginContentExists')) {
function pluginContentExists(string $section): bool
{
return !empty(Events::listeners("view:{$section}"));
}
}

View File

@@ -933,6 +933,50 @@ function get_controller(): string
return end($controller_name_parts);
}
function plugin_headers(): array
{
return [
['name' => lang('Plugins.name'), 'escape' => false],
['description' => lang('Plugins.description')],
['version' => lang('Plugins.version'), 'escape' => false],
['status' => lang('Plugins.status'), 'escape' => false],
];
}
function get_plugin_manage_table_headers(): string
{
return transform_headers(plugin_headers(), false, true);
}
function get_plugin_data_row(array $plugin): array
{
$pluginId = $plugin['id'];
$statusHtml = $plugin['enabled']
? '<span class="label label-success">' . lang('Plugins.active') . '</span>'
: '<span class="label label-default">' . lang('Plugins.inactive') . '</span>';
$editHtml = $plugin['enabled']
? '<button class="btn btn-warning btn-xs plugin-action" data-action="disable" data-plugin-id="' . esc($pluginId) . '">'
. '<span class="glyphicon glyphicon-pause"></span> ' . lang('Plugins.disable') . '</button>'
: '<button class="btn btn-success btn-xs plugin-action" data-action="enable" data-plugin-id="' . esc($pluginId) . '">'
. '<span class="glyphicon glyphicon-play"></span> ' . lang('Plugins.enable') . '</button>';
if ($plugin['has_config'] && $plugin['enabled']) {
$editHtml .= ' <button class="btn btn-primary btn-xs plugin-config" data-plugin-id="' . esc($pluginId) . '">'
. '<span class="glyphicon glyphicon-cog"></span> ' . lang('Plugins.configure') . '</button>';
}
return [
'plugin_id' => $pluginId,
'name' => '<strong>' . esc($plugin['name']) . '</strong><br><small class="text-muted">' . esc($pluginId) . '</small>',
'description' => esc($plugin['description']),
'version' => '<span class="label label-default">' . esc($plugin['version']) . '</span>',
'status' => $statusHtml,
'edit' => $editHtml,
];
}
/**
* Restores filter values from the URL query string.
*

View File

@@ -166,8 +166,6 @@ return [
"info" => "معلومات",
"info_configuration" => "معلومات الشركة",
"input_groups" => "مجموعات الإدخال",
"integrations" => "التكامل",
"integrations_configuration" => "تكامل",
"invoice" => "الفاتورة",
"invoice_configuration" => "إعدادات طباعة الفاتورة",
"invoice_default_comments" => "التعليق الافتراضي على الفاتورة",
@@ -198,13 +196,6 @@ return [
"location_info" => "معلومات تهيئة الأماكن",
"login_form" => "نمط نموذج تسجيل الدخول",
"logout" => "هل تريد عمل نسخة إحتياطية قبل الخروج؟ اضغط [نعم] لعمل النسخة أو [الغاء] للخروج.",
"mailchimp" => "ميل تشامب",
"mailchimp_api_key" => "مفتاح ميل شيمب",
"mailchimp_configuration" => "إعدادات ميل شيمب",
"mailchimp_key_successfully" => "نجاح.",
"mailchimp_key_unsuccessfully" => "فشل.",
"mailchimp_lists" => "إعدادات ميل شيمب",
"mailchimp_tooltip" => "انقر على رمز مفتاح API.",
"message" => "الرسائل",
"message_configuration" => "إعدادات الرسائل",
"msg_msg" => "الرسائل النصية المحفوظة",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "الموظف",
"error_adding_updating" => "خطاء فى إضافة أو تحديث العميل.",
"import_items_csv" => "استيراد العملا ء من ورقة عمل اكسل",
"mailchimp_activity_click" => "النقر على البريد الإلكتروني",
"mailchimp_activity_lastopen" => "آخر رسالة إلكترونية مفتوحة",
"mailchimp_activity_open" => "رسالة إلكترونية مفتوحة",
"mailchimp_activity_total" => "تم ارسال الرسالة الإلكترونية بنجاح",
"mailchimp_activity_unopen" => "رسالة إلكترونية غير مفتوحة",
"mailchimp_email_client" => "بريد الكتروني",
"mailchimp_info" => "ميل تشيمب",
"mailchimp_member_rating" => "التقييم",
"mailchimp_status" => "الحالة",
"mailchimp_vip" => "مهم",
"max" => "الحد الأقصى",
"min" => "الحد الأدنى",
"new" => "عميل جديد",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "تحديث قاعدة البيانات.",
"office" => "المكتب",
"office_desc" => "اظهار الائحة المكتبية.",
'plugins' => 'الإضافات',
"receivings" => "استلام الأصناف",
"receivings_desc" => "معالجة أوامر الشراء و استلام الأصناف.",
"reports" => "التقارير",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'إجراءات',
'active' => 'نشط',
'configure' => 'تكوين',
'description' => 'الوصف',
'disable' => 'تعطيل',
'disable_failed' => 'فشل تعطيل الإضافة',
'disabled' => 'تم تعطيل الإضافة بنجاح',
'enable' => 'تفعيل',
'enable_failed' => 'فشل تفعيل الإضافة',
'enabled' => 'تم تفعيل الإضافة بنجاح',
'inactive' => 'غير نشط',
'management' => 'إدارة الإضافات',
'name' => 'اسم الإضافة',
'no_config' => 'هذه الإضافة لا تحتوي على خيارات تكوين',
'no_plugins_to_display' => 'لا توجد إضافات للعرض',
'not_found' => 'الإضافة غير موجودة',
'plugins' => 'الإضافات',
'settings_save_failed' => 'فشل حفظ إعدادات الإضافة',
'settings_saved' => 'تم حفظ إعدادات الإضافة بنجاح',
'status' => 'الحالة',
'uninstall' => 'إلغاء التثبيت',
'uninstall_failed' => 'فشل إلغاء تثبيت الإضافة',
'uninstalled' => 'تم إلغاء تثبيت الإضافة بنجاح',
'version' => 'الإصدار',
];

View File

@@ -41,7 +41,6 @@ return [
"customer_discount" => "الخصم",
"customer_email" => "البريد الإلكترونى",
"customer_location" => "المكان",
"customer_mailchimp_status" => "حالة بريد ميل تشيمب",
"customer_optional" => "(مطلوب للدفعات المستحقة)",
"customer_required" => "(اجباري)",
"customer_total" => "المجموع",

View File

@@ -166,8 +166,6 @@ return [
"info" => "معلومات",
"info_configuration" => "معلومات الشركة",
"input_groups" => "مجموعات الإدخال",
"integrations" => "التكامل",
"integrations_configuration" => "تكامل",
"invoice" => "الفاتورة",
"invoice_configuration" => "إعدادات طباعة الفاتورة",
"invoice_default_comments" => "التعليق الافتراضي على الفاتورة",
@@ -198,13 +196,6 @@ return [
"location_info" => "معلومات تهيئة الأماكن",
"login_form" => "نمط نموذج تسجيل الدخول",
"logout" => "هل تريد عمل نسخة إحتياطية قبل الخروج؟ اضغط [نعم] لعمل النسخة أو [الغاء] للخروج.",
"mailchimp" => "ميل تشامب",
"mailchimp_api_key" => "مفتاح ميل شيمب",
"mailchimp_configuration" => "إعدادات ميل شيمب",
"mailchimp_key_successfully" => "نجاح.",
"mailchimp_key_unsuccessfully" => "فشل.",
"mailchimp_lists" => "قوائم ميل شيمب",
"mailchimp_tooltip" => "انقر على رمز مفتاح API.",
"message" => "الرسائل",
"message_configuration" => "إعدادات الرسائل",
"msg_msg" => "الرسائل النصية المحفوظة",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "الموظف",
"error_adding_updating" => "خطاء فى إضافة أو تحديث العميل.",
"import_items_csv" => "استيراد العملا ء من ورقة عمل اكسل",
"mailchimp_activity_click" => "النقر على البريد الإلكتروني",
"mailchimp_activity_lastopen" => "آخر رسالة إلكترونية مفتوحة",
"mailchimp_activity_open" => "رسالة إلكترونية مفتوحة",
"mailchimp_activity_total" => "تم ارسال الرسالة الإلكترونية بنجاح",
"mailchimp_activity_unopen" => "رسالة إلكترونية غير مفتوحة",
"mailchimp_email_client" => "بريد الكتروني",
"mailchimp_info" => "ميل تشيمب",
"mailchimp_member_rating" => "التقييم",
"mailchimp_status" => "الحالة",
"mailchimp_vip" => "مهم",
"max" => "الحد الأقصى",
"min" => "الحد الأدنى",
"new" => "عميل جديد",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "تحديث قاعدة البيانات.",
"office" => "المكتب",
"office_desc" => "اظهار الائحة المكتبية.",
'plugins' => 'الإضافات',
"receivings" => "استلام الأصناف",
"receivings_desc" => "معالجة أوامر الشراء و استلام الأصناف.",
"reports" => "التقارير",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'إجراءات',
'active' => 'نشط',
'configure' => 'تكوين',
'description' => 'الوصف',
'disable' => 'تعطيل',
'disable_failed' => 'فشل تعطيل الإضافة',
'disabled' => 'تم تعطيل الإضافة بنجاح',
'enable' => 'تفعيل',
'enable_failed' => 'فشل تفعيل الإضافة',
'enabled' => 'تم تفعيل الإضافة بنجاح',
'inactive' => 'غير نشط',
'management' => 'إدارة الإضافات',
'name' => 'اسم الإضافة',
'no_config' => 'هذه الإضافة لا تحتوي على خيارات تكوين',
'no_plugins_to_display' => 'لا توجد إضافات للعرض',
'not_found' => 'الإضافة غير موجودة',
'plugins' => 'الإضافات',
'settings_save_failed' => 'فشل حفظ إعدادات الإضافة',
'settings_saved' => 'تم حفظ إعدادات الإضافة بنجاح',
'status' => 'الحالة',
'uninstall' => 'إلغاء التثبيت',
'uninstall_failed' => 'فشل إلغاء تثبيت الإضافة',
'uninstalled' => 'تم إلغاء تثبيت الإضافة بنجاح',
'version' => 'الإصدار',
];

View File

@@ -41,7 +41,6 @@ return [
"customer_discount" => "الخصم",
"customer_email" => "البريد الإلكترونى",
"customer_location" => "المكان",
"customer_mailchimp_status" => "حالة بريد ميل تشيمب",
"customer_optional" => "(مطلوب للدفعات المستحقة)",
"customer_required" => "(اجباري)",
"customer_total" => "المجموع",

View File

@@ -166,8 +166,6 @@ return [
"info" => "Məlumat",
"info_configuration" => "Dükan İnformasiyası",
"input_groups" => "",
"integrations" => "İnteqrasiya",
"integrations_configuration" => "Üçüncü tərəf inteqrasiya",
"invoice" => "Faktura",
"invoice_configuration" => "Faktura Çap Parametrləri",
"invoice_default_comments" => "Standart Faktura Şərhləri",
@@ -198,13 +196,6 @@ return [
"location_info" => "Yer Konfiqurasiya Məlumatı",
"login_form" => "",
"logout" => "Çıxışdan əvvəl məlumatlari ehtiyat bazasına köçürmək istəyirsinizmi? Çıxış üçün Bekap və ya [Ləğv] üçün [OK]' düyməsinə basın.",
"mailchimp" => "Mailçimp",
"mailchimp_api_key" => "Mailchimp API Açarı",
"mailchimp_configuration" => "Mailchimp Konfiqurasiyası",
"mailchimp_key_successfully" => "API Açarı etibarlıdır.",
"mailchimp_key_unsuccessfully" => "API Açarı etibarsızdır.",
"mailchimp_lists" => "Mailchimp siyahısı (lar)",
"mailchimp_tooltip" => "API Açarının İşarəsinə basın.",
"message" => "Mesaj",
"message_configuration" => "Mesaj Konfiqurasiyası",
"msg_msg" => "Saxlanılan Mətn Mesajı",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "Əməkdaş",
"error_adding_updating" => "Müştəri əlavəsində ya da yenilənməsində XƏTA.",
"import_items_csv" => "CSVdən müştəri əlavə et",
"mailchimp_activity_click" => "Elektron poçt düyməsi",
"mailchimp_activity_lastopen" => "Son açılan məktub",
"mailchimp_activity_open" => "ıq məktub",
"mailchimp_activity_total" => "Məktub göndərildi",
"mailchimp_activity_unopen" => "ılmamış məktub",
"mailchimp_email_client" => "Müştəriyə Məktub Göndər",
"mailchimp_info" => "Mailchimp-də",
"mailchimp_member_rating" => "Reytinq",
"mailchimp_status" => "Status",
"mailchimp_vip" => "siz silmək üçün heç bir müştəri seçməmisiniz",
"max" => "Ən çox xərclənən",
"min" => "Ən az xərclənən",
"new" => "Yeni Müştəri",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "ALSAN Məlumat Bazasıni Yenilə.",
"office" => "Ofis",
"office_desc" => "Ofis menyusu siyahısı modulları.",
'plugins' => 'Plaginlər',
"receivings" => "Qəbul Edilənlər",
"receivings_desc" => "Satınalma sifarişləri işləyin.",
"reports" => "Hesabatlar",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'Əməliyyatlar',
'active' => 'Aktiv',
'configure' => 'Konfiqurasiya',
'description' => 'Təsvir',
'disable' => 'Deaktiv et',
'disable_failed' => 'Plagini deaktiv etmək alınmadı',
'disabled' => 'Plagin uğurla deaktiv edildi',
'enable' => 'Aktiv et',
'enable_failed' => 'Plagini aktiv etmək alınmadı',
'enabled' => 'Plagin uğurla aktiv edildi',
'inactive' => 'Deaktiv',
'management' => 'Plagin idarəetməsi',
'name' => 'Plagin adı',
'no_config' => 'Bu plaginin konfiqurasiya seçimləri yoxdur',
'no_plugins_to_display' => 'Göstəriləcək plagin yoxdur',
'not_found' => 'Plagin tapılmadı',
'plugins' => 'Plaginlər',
'settings_save_failed' => 'Plagin parametrlərini saxlamaq alınmadı',
'settings_saved' => 'Plagin parametrləri uğurla saxlandı',
'status' => 'Status',
'uninstall' => 'Sil',
'uninstall_failed' => 'Plagini silmək alınmadı',
'uninstalled' => 'Plagin uğurla silindi',
'version' => 'Versiya',
];

View File

@@ -41,7 +41,6 @@ return [
"customer_discount" => "Endirim",
"customer_email" => "E-poçt",
"customer_location" => "Yer",
"customer_mailchimp_status" => "Mailchimp Statusu",
"customer_optional" => "(Ödənişlərdə tələb olunur)",
"customer_required" => "(Vacib)",
"customer_total" => "Cəmi",

View File

@@ -166,8 +166,6 @@ return [
"info" => "Information",
"info_configuration" => "Store Information",
"input_groups" => "",
"integrations" => "",
"integrations_configuration" => "",
"invoice" => "Invoice",
"invoice_configuration" => "Invoice Print Settings",
"invoice_default_comments" => "Default Invoice Comments",
@@ -198,13 +196,6 @@ return [
"location_info" => "Location Configuration Information",
"login_form" => "",
"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",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "Служител",
"error_adding_updating" => "Добавянето или актуализирането на клиента е неуспешно.",
"import_items_csv" => "Импортиране на клиент от CSV",
"mailchimp_activity_click" => "Email click",
"mailchimp_activity_lastopen" => "Последно отворен Имейл",
"mailchimp_activity_open" => "Имейлът е отворен",
"mailchimp_activity_total" => "Имейлът е изпратен",
"mailchimp_activity_unopen" => "Имейлът е неотворен",
"mailchimp_email_client" => "Имейл клиент",
"mailchimp_info" => "Mailchimp (Услуга)",
"mailchimp_member_rating" => "Оценка",
"mailchimp_status" => "Статус",
"mailchimp_vip" => "VIP",
"max" => "Максимално похарчени",
"min" => "Минимално похарчено",
"new" => "Нов клиент",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "Update the OSPOS Database.",
"office" => "Office",
"office_desc" => "List office menu modules.",
'plugins' => 'Плъгини',
"receivings" => "Receivings",
"receivings_desc" => "Process Purchase Orders.",
"reports" => "Reports",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'Действия',
'active' => 'Активен',
'configure' => 'Конфигуриране',
'description' => 'Описание',
'disable' => 'Деактивиране',
'disable_failed' => 'Неуспешно деактивиране на приставката',
'disabled' => 'Приставката е деактивирана успешно',
'enable' => 'Активиране',
'enable_failed' => 'Неуспешно активиране на приставката',
'enabled' => 'Приставката е активирана успешно',
'inactive' => 'Неактивен',
'management' => 'Управление на приставки',
'name' => 'Име на приставката',
'no_config' => 'Тази приставка няма опции за конфигурация',
'no_plugins_to_display' => 'Няма приставки за показване',
'not_found' => 'Приставката не е намерена',
'plugins' => 'Приставки',
'settings_save_failed' => 'Неуспешно запазване на настройките на приставката',
'settings_saved' => 'Настройките на приставката са запазени успешно',
'status' => 'Статус',
'uninstall' => 'Деинсталиране',
'uninstall_failed' => 'Неуспешно деинсталиране на приставката',
'uninstalled' => 'Приставката е деинсталирана успешно',
'version' => 'Версия',
];

View File

@@ -41,7 +41,7 @@ return [
"customer_discount" => "Намаление",
"customer_email" => "Електронна поща",
"customer_location" => "Местоположение",
"customer_mailchimp_status" => "Състояние на Mailchimp",
"mailchimp_customer_status" => "Състояние на Mailchimp",
"customer_optional" => "(Незадължително)",
"customer_required" => "(Задължително)",
"customer_total" => "Обща сума",

View File

@@ -166,8 +166,6 @@ return [
"info" => "Informacije",
"info_configuration" => "Info o web trgovini",
"input_groups" => "Grupe unosa",
"integrations" => "Integracije",
"integrations_configuration" => "Integracije trećih strana",
"invoice" => "Faktura",
"invoice_configuration" => "Podešavanja štamapnja",
"invoice_default_comments" => "Komentar na fakturi",
@@ -198,13 +196,6 @@ return [
"location_info" => "Informacije o konfiguraciji lokacije",
"login_form" => "Stil formulara za prijavu",
"logout" => "Zar ne želite da napravite rezervnu kopiju prije odjave? Kliknite [OK] za sigurnosnu kopiju, [Cancel] da biste se odjavili.",
"mailchimp" => "MeilChimp",
"mailchimp_api_key" => "MailChimp API ključ",
"mailchimp_configuration" => "MailChimp konfiguracija",
"mailchimp_key_successfully" => "API ključ je važeći.",
"mailchimp_key_unsuccessfully" => "API ključ je nevažeći.",
"mailchimp_lists" => "MailChimp lista(e)",
"mailchimp_tooltip" => "Kliknite na ikonu za API ključ.",
"message" => "Poruke",
"message_configuration" => "Konfigurisanje poruke",
"msg_msg" => "Snimljena tekst poruka",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "Zaposlenik",
"error_adding_updating" => "Dodavanje ili ažuriranje kupca nije uspjelo.",
"import_items_csv" => "Uvezi kupce iz CSV datoteke",
"mailchimp_activity_click" => "Klik na e-mail",
"mailchimp_activity_lastopen" => "Zadnji otvoreni e-mail",
"mailchimp_activity_open" => "E-mail otvoren",
"mailchimp_activity_total" => "E-mail poslat",
"mailchimp_activity_unopen" => "E-mail nije otvoren",
"mailchimp_email_client" => "E-mail klijenta",
"mailchimp_info" => "MeilChimp",
"mailchimp_member_rating" => "Ocjena",
"mailchimp_status" => "Status",
"mailchimp_vip" => "VIP",
"max" => "Maks. potrošeno",
"min" => "Min. potrošeno",
"new" => "Novi kupac",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "Ažurirajte OSPOS bazu podataka.",
"office" => "Administracija",
"office_desc" => "Lista modula kancelarijskog menija.",
'plugins' => 'Dodaci',
"receivings" => "Ulazi",
"receivings_desc" => "Obrada narudžbenica.",
"reports" => "Izvještaji",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'Akcije',
'active' => 'Aktivno',
'configure' => 'Konfiguracija',
'description' => 'Opis',
'disable' => 'Onemogući',
'disable_failed' => 'Nije uspjelo onemogućavanje dodatka',
'disabled' => 'Dodatak je uspješno onemogućen',
'enable' => 'Omogući',
'enable_failed' => 'Nije uspjelo omogućavanje dodatka',
'enabled' => 'Dodatak je uspješno omogućen',
'inactive' => 'Neaktivno',
'management' => 'Upravljanje dodacima',
'name' => 'Naziv dodatka',
'no_config' => 'Ovaj dodatak nema opcija konfiguracije',
'no_plugins_to_display' => 'Nema dodataka za prikaz',
'not_found' => 'Dodatak nije pronađen',
'plugins' => 'Dodaci',
'settings_save_failed' => 'Nije uspjelo spremanje postavki dodatka',
'settings_saved' => 'Postavke dodatka su uspješno sačuvane',
'status' => 'Status',
'uninstall' => 'Deinstaliraj',
'uninstall_failed' => 'Nije uspjelo deinstaliranje dodatka',
'uninstalled' => 'Dodatak je uspješno deinstaliran',
'version' => 'Verzija',
];

View File

@@ -41,7 +41,6 @@ return [
"customer_discount" => "Popust",
"customer_email" => "E-mail kupca",
"customer_location" => "Mjesto kupca",
"customer_mailchimp_status" => "Status MailChimp-a",
"customer_optional" => "(Potrebno za odloženo plaćanje)",
"customer_required" => "Obavezno",
"customer_total" => "Ukupno",

View File

@@ -166,8 +166,8 @@ return [
'info' => "زانیاری",
'info_configuration' => "زانیاری فڕۆشتگا",
'input_groups' => "گروپەکانی زانیارییە پێدراوەکان",
'integrations' => "یەکگرتنەکان",
'integrations_configuration' => "یەکگرتنەکانی لایەنی سێیەم",
'plugins' => "یەکگرتنەکان",
'plugins_configuration' => "یەکگرتنەکانی لایەنی سێیەم",
'invoice' => "فاکتۆرە",
'invoice_configuration' => "ڕێکخستنەکانی چاپی فاکتورە",
'invoice_default_comments' => "سەرنجەکانی فاکتۆرەی بنەڕەتیی",
@@ -198,13 +198,6 @@ return [
'location_info' => "زانیاری ڕێکخستنی شوێن",
'login_form' => "ستایلی فۆڕمی چوونەژوورەوە",
'logout' => "دەتەوێت پاڵپشت دروست بکەیت پێش چوونە دەرەوە؟ کرتە بکە لەسەر [باشە] بۆ پاڵپشت دروستکردن یان [هەڵوەشاندنەوە] بۆ چوونە دەرەوە.",
'mailchimp' => "مەیڵچیمپ",
'mailchimp_api_key' => "کلیلی (ئەی پی ئای)ی مەیڵچیمپ",
'mailchimp_configuration' => "ڕێکخستنی مەیڵچیمپ",
'mailchimp_key_successfully' => "کلیلی (ئەی پی ئای) دروستە.",
'mailchimp_key_unsuccessfully' => "کلیلی (ئەی پی ئای) نادروستە.",
'mailchimp_lists' => "لیست(ەکان)ی مەیڵچیمپ",
'mailchimp_tooltip' => "کرتە لەسەر ئایکۆنی کلیلی (ئەی پی ئای) بکە.",
'message' => "نامە",
'message_configuration' => "ڕێکخستنی نامە",
'msg_msg' => "دەقی نامەی پاشەکەوتکراو",

View File

@@ -28,16 +28,6 @@ return [
'employee' => "فەرمانبەر",
'error_adding_updating' => "زیادکردن یان نوێکردنەوەی کڕیار سەرکەوتوو نەبوو.",
'import_items_csv' => "هاوردەکردنی کڕیار لەڕێگایCSV",
'mailchimp_activity_click' => "کرتەی ئیمەیل",
'mailchimp_activity_lastopen' => "دوایین ئیمەیڵی کراوە",
'mailchimp_activity_open' => "ئیمەیڵ کرایەوە",
'mailchimp_activity_total' => "ئیمەیڵ نێردرا",
'mailchimp_activity_unopen' => "ئیمەیڵ نەکراوە",
'mailchimp_email_client' => "کڕیاری ئیمەیل",
'mailchimp_info' => "مەیڵچیمپ",
'mailchimp_member_rating' => "پلەپێدان",
'mailchimp_status' => "دۆخ",
'mailchimp_vip' => "ڤی ئای پی",
'max' => "زۆرترین. خەرجکراو",
'min' => "کەمترین. خەرجکراو",
'new' => "کڕیاری نوێ",

View File

@@ -32,6 +32,7 @@ return [
'migrate_desc' => "داتابەیسی OSPOS نوێ بکەرەوە.",
'office' => "ئۆفیس",
'office_desc' => "لیستی مۆدۆلی پێڕستی ئۆفیس نیشان بدە.",
'plugins' => 'پڵەگینەکان',
'receivings' => "وەرگرتنەکان",
'receivings_desc' => "پرۆسەی داواکاری کڕین.",
'reports' => "ڕاپۆرتەکان",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'کردارەکان',
'active' => 'چالاک',
'configure' => 'ڕێکخستن',
'description' => 'وەسف',
'disable' => 'ناچالاک کردن',
'disable_failed' => 'ناکامی لە ناچالاک کردنی پڵەگین',
'disabled' => 'پڵەگین بە سەرکەوتوویی ناچالاک کرا',
'enable' => 'چالاک کردن',
'enable_failed' => 'ناکامی لە چالاک کردنی پڵەگین',
'enabled' => 'پڵەگین بە سەرکەوتوویی چالاک کرا',
'inactive' => 'ناچالاک',
'management' => 'بەڕێوەبردنی پڵەگین',
'name' => 'ناوی پڵەگین',
'no_config' => 'ئەم پڵەگینە هیچ بژاردەیەکی ڕێکخستن نییە',
'no_plugins_to_display' => 'هیچ پڵەگینێک بۆ نیشاندان نییە',
'not_found' => 'پڵەگین نەدۆزرایەوە',
'plugins' => 'پڵەگینەکان',
'settings_save_failed' => 'ناکامی لە پاشەکەوت کردنی ڕێکخستنەکانی پڵەگین',
'settings_saved' => 'ڕێکخستنەکانی پڵەگین بە سەرکەوتوویی پاشەکەوت کران',
'status' => 'باری',
'uninstall' => 'لادانی دامەزراندن',
'uninstall_failed' => 'ناکامی لە لادانی دامەزراندنی پڵەگین',
'uninstalled' => 'پڵەگین بە سەرکەوتوویی لادرا',
'version' => 'وەشان',
];

View File

@@ -41,7 +41,6 @@ return [
'customer_discount' => "داشکاندن",
'customer_email' => "ئیمەیڵ",
'customer_location' => "ناونیشان",
'customer_mailchimp_status' => "دۆخی بەکارهێنان مایلچیمپ",
'customer_optional' => "(پێویستە بۆئەو پارانەی دەبێت بدرێت)",
'customer_required' => "(پێویستە)",
'customer_total' => "کۆی گشتی",

View File

@@ -166,8 +166,6 @@ return [
"info" => "",
"info_configuration" => "",
"input_groups" => "",
"integrations" => "",
"integrations_configuration" => "",
"invoice" => "",
"invoice_configuration" => "",
"invoice_default_comments" => "",
@@ -198,13 +196,6 @@ return [
"location_info" => "",
"login_form" => "",
"logout" => "",
"mailchimp" => "",
"mailchimp_api_key" => "",
"mailchimp_configuration" => "",
"mailchimp_key_successfully" => "",
"mailchimp_key_unsuccessfully" => "",
"mailchimp_lists" => "",
"mailchimp_tooltip" => "",
"message" => "",
"message_configuration" => "",
"msg_msg" => "",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "Zaměstnanec",
"error_adding_updating" => "Chyba při vytváření nebo aktualizaci zákazníka.",
"import_items_csv" => "Import zákazníků z CSV",
"mailchimp_activity_click" => "",
"mailchimp_activity_lastopen" => "Poslední otevřený email",
"mailchimp_activity_open" => "",
"mailchimp_activity_total" => "",
"mailchimp_activity_unopen" => "",
"mailchimp_email_client" => "",
"mailchimp_info" => "",
"mailchimp_member_rating" => "Hodnocení",
"mailchimp_status" => "",
"mailchimp_vip" => "VIP",
"max" => "",
"min" => "",
"new" => "",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "Aktualizovat databázi OSPOS.",
"office" => "Správa",
"office_desc" => "Seznam modulů pro správu.",
'plugins' => 'Doplňky',
"receivings" => "Příjem zboží",
"receivings_desc" => "",
"reports" => "Sestavy",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'Akce',
'active' => 'Aktivní',
'configure' => 'Konfigurovat',
'description' => 'Popis',
'disable' => 'Deaktivovat',
'disable_failed' => 'Deaktivace pluginu se nezdařila',
'disabled' => 'Plugin byl úspěšně deaktivován',
'enable' => 'Aktivovat',
'enable_failed' => 'Aktivace pluginu se nezdařila',
'enabled' => 'Plugin byl úspěšně aktivován',
'inactive' => 'Neaktivní',
'management' => 'Správa pluginů',
'name' => 'Název pluginu',
'no_config' => 'Tento plugin nemá žádné možnosti konfigurace',
'no_plugins_to_display' => 'Žádné pluginy k zobrazení',
'not_found' => 'Plugin nebyl nalezen',
'plugins' => 'Pluginy',
'settings_save_failed' => 'Uložení nastavení pluginu se nezdařilo',
'settings_saved' => 'Nastavení pluginu bylo úspěšně uloženo',
'status' => 'Stav',
'uninstall' => 'Odinstalovat',
'uninstall_failed' => 'Odinstalace pluginu se nezdařila',
'uninstalled' => 'Plugin byl úspěšně odinstalován',
'version' => 'Verze',
];

View File

@@ -41,7 +41,6 @@ return [
"customer_discount" => "Sleva",
"customer_email" => "Email",
"customer_location" => "Místo",
"customer_mailchimp_status" => "Stav mailchimp",
"customer_optional" => "(Volitelné)",
"customer_required" => "(Vyžadováno)",
"customer_total" => "Celkem",

View File

@@ -166,8 +166,6 @@ return [
"info" => "Information",
"info_configuration" => "Store Information",
"input_groups" => "",
"integrations" => "Integrations",
"integrations_configuration" => "Third Party Integrations",
"invoice" => "Invoice",
"invoice_configuration" => "Invoice Print Settings",
"invoice_default_comments" => "Default Invoice Comments",
@@ -198,13 +196,6 @@ return [
"location_info" => "Location Configuration Information",
"login_form" => "",
"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",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "Employee",
"error_adding_updating" => "Customer add or update failed.",
"import_items_csv" => "Customer Import from CSV",
"mailchimp_activity_click" => "Email click",
"mailchimp_activity_lastopen" => "Last open email",
"mailchimp_activity_open" => "Email open",
"mailchimp_activity_total" => "Email sent",
"mailchimp_activity_unopen" => "Email unopen",
"mailchimp_email_client" => "Email client",
"mailchimp_info" => "Mailchimp",
"mailchimp_member_rating" => "Rating",
"mailchimp_status" => "Status",
"mailchimp_vip" => "VIP",
"max" => "Max. spent",
"min" => "Min. spent",
"new" => "New Customer",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "",
"office" => "",
"office_desc" => "",
'plugins' => 'Plugins',
"receivings" => "",
"receivings_desc" => "",
"reports" => "",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'Handlinger',
'active' => 'Aktiv',
'configure' => 'Konfigurer',
'description' => 'Beskrivelse',
'disable' => 'Deaktiver',
'disable_failed' => 'Deaktivering af plugin mislykkedes',
'disabled' => 'Plugin blev deaktiveret',
'enable' => 'Aktiver',
'enable_failed' => 'Aktivering af plugin mislykkedes',
'enabled' => 'Plugin blev aktiveret',
'inactive' => 'Inaktiv',
'management' => 'Plugin-administration',
'name' => 'Plugin-navn',
'no_config' => 'Dette plugin har ingen konfigurationsmuligheder',
'no_plugins_to_display' => 'Ingen plugins at vise',
'not_found' => 'Plugin ikke fundet',
'plugins' => 'Plugins',
'settings_save_failed' => 'Gemning af plugin-indstillinger mislykkedes',
'settings_saved' => 'Plugin-indstillinger blev gemt',
'status' => 'Status',
'uninstall' => 'Afinstaller',
'uninstall_failed' => 'Afinstallation af plugin mislykkedes',
'uninstalled' => 'Plugin blev afinstalleret',
'version' => 'Version',
];

View File

@@ -41,7 +41,6 @@ return [
"customer_discount" => "Rabat",
"customer_email" => "",
"customer_location" => "",
"customer_mailchimp_status" => "",
"customer_optional" => "",
"customer_required" => "",
"customer_total" => "",

View File

@@ -166,8 +166,6 @@ return [
"info" => "Instellungen",
"info_configuration" => "Instellungen",
"input_groups" => "",
"integrations" => "",
"integrations_configuration" => "",
"invoice" => "Rechnungs",
"invoice_configuration" => "Druckereinstellungen",
"invoice_default_comments" => "Rechnungskommentar",
@@ -198,13 +196,6 @@ return [
"location_info" => "Lagerort-Information",
"login_form" => "",
"logout" => "Wollen Sie eine Sicherung machen vor dem Beenden? Klicke [OK] für Sicherung",
"mailchimp" => "",
"mailchimp_api_key" => "",
"mailchimp_configuration" => "",
"mailchimp_key_successfully" => "",
"mailchimp_key_unsuccessfully" => "",
"mailchimp_lists" => "",
"mailchimp_tooltip" => "",
"message" => "Message",
"message_configuration" => "Message Configuration",
"msg_msg" => "Saved Text Message",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "",
"error_adding_updating" => "Fehler beim Hinzufügen/Ändern",
"import_items_csv" => "Importiere Kunden via CSV",
"mailchimp_activity_click" => "",
"mailchimp_activity_lastopen" => "",
"mailchimp_activity_open" => "",
"mailchimp_activity_total" => "",
"mailchimp_activity_unopen" => "",
"mailchimp_email_client" => "",
"mailchimp_info" => "",
"mailchimp_member_rating" => "",
"mailchimp_status" => "",
"mailchimp_vip" => "",
"max" => "",
"min" => "",
"new" => "Neuer Kunde",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "",
"office" => "",
"office_desc" => "",
'plugins' => 'Plugins',
"receivings" => "Eingänge",
"receivings_desc" => "Hinzufügen, Ändern, Löschen und Suchen",
"reports" => "Berichte",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'Aktionen',
'active' => 'Aktiv',
'configure' => 'Konfigurieren',
'description' => 'Beschreibung',
'disable' => 'Deaktivieren',
'disable_failed' => 'Plugin konnte nicht deaktiviert werden',
'disabled' => 'Plugin erfolgreich deaktiviert',
'enable' => 'Aktivieren',
'enable_failed' => 'Plugin konnte nicht aktiviert werden',
'enabled' => 'Plugin erfolgreich aktiviert',
'inactive' => 'Inaktiv',
'management' => 'Plugin-Verwaltung',
'name' => 'Plugin-Name',
'no_config' => 'Dieses Plugin hat keine Konfigurationsoptionen',
'no_plugins_to_display' => 'Keine Plugins anzuzeigen',
'not_found' => 'Plugin nicht gefunden',
'plugins' => 'Plugins',
'settings_save_failed' => 'Plugin-Einstellungen konnten nicht gespeichert werden',
'settings_saved' => 'Plugin-Einstellungen erfolgreich gespeichert',
'status' => 'Status',
'uninstall' => 'Deinstallieren',
'uninstall_failed' => 'Plugin konnte nicht deinstalliert werden',
'uninstalled' => 'Plugin erfolgreich deinstalliert',
'version' => 'Version',
];

View File

@@ -41,7 +41,6 @@ return [
"customer_discount" => "Discount",
"customer_email" => "Customer Email",
"customer_location" => "Customer Location",
"customer_mailchimp_status" => "",
"customer_optional" => "",
"customer_required" => "",
"customer_total" => "Total",

View File

@@ -166,8 +166,6 @@ return [
"info" => "Informationen",
"info_configuration" => "Generelle Einstellungen",
"input_groups" => "",
"integrations" => "Integrationen",
"integrations_configuration" => "Drittanbieter Integrationen",
"invoice" => "Rechnungs",
"invoice_configuration" => "Druckereinstellungen",
"invoice_default_comments" => "Rechnungskommentar",
@@ -198,14 +196,7 @@ return [
"location_info" => "Lagerort-Information",
"login_form" => "",
"logout" => "Wollen Sie vor dem Beenden eine Sicherung erstellen? Klicke [OK] für Sicherung.",
"mailchimp" => "Mailchimp",
"mailchimp_api_key" => "Mailchimp API Schlüssel",
"mailchimp_configuration" => "Mailchimp Konfiguration",
"mailchimp_key_successfully" => "API Key ist gültig.",
"mailchimp_key_unsuccessfully" => "API Key ist ungültig.",
"mailchimp_lists" => "Mailchimp Liste(n)",
"mailchimp_tooltip" => "Icon anklicken um API Key zu erhalten.",
"message" => "Nachricht",
"message" => "Nachricht",
"message_configuration" => "Nachrichtenkonfiguration",
"msg_msg" => "Gespeicherte Nachricht",
"msg_msg_placeholder" => "Wenn Sie eine SMS Vorlage benutzen wollen, geben Sie diese hier ein, ansonsten lassen Sie dieses Feld frei.",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "Mitarbeiter",
"error_adding_updating" => "Fehler beim Hinzufügen/Ändern.",
"import_items_csv" => "Importiere Kunden via CSV",
"mailchimp_activity_click" => "E-Mail klick",
"mailchimp_activity_lastopen" => "Letzte geöffnet E-Mail",
"mailchimp_activity_open" => "E-Mail geöffnet",
"mailchimp_activity_total" => "E-Mail gesendet",
"mailchimp_activity_unopen" => "E-Mail ungeöffnet",
"mailchimp_email_client" => "E-Mail Client",
"mailchimp_info" => "Mailchimp",
"mailchimp_member_rating" => "Bewertung",
"mailchimp_status" => "Status",
"mailchimp_vip" => "VIP",
"max" => "Maximal Ausgegeben",
"min" => "Minimal Ausgegeben",
"new" => "Neuer Kunde",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "Aktualisiere die OSPOS-Datenbank.",
"office" => "Verwaltung",
"office_desc" => "Auflistung der Module für das Verwaltungs-Menü.",
'plugins' => 'Plugins',
"receivings" => "Eingänge",
"receivings_desc" => "Hinzufügen, Ändern, Löschen und Suchen von Bestellungen.",
"reports" => "Berichte",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'Aktionen',
'active' => 'Aktiv',
'configure' => 'Konfigurieren',
'description' => 'Beschreibung',
'disable' => 'Deaktivieren',
'disable_failed' => 'Plugin konnte nicht deaktiviert werden',
'disabled' => 'Plugin erfolgreich deaktiviert',
'enable' => 'Aktivieren',
'enable_failed' => 'Plugin konnte nicht aktiviert werden',
'enabled' => 'Plugin erfolgreich aktiviert',
'inactive' => 'Inaktiv',
'management' => 'Plugin-Verwaltung',
'name' => 'Plugin-Name',
'no_config' => 'Dieses Plugin hat keine Konfigurationsoptionen',
'no_plugins_to_display' => 'Keine Plugins anzuzeigen',
'not_found' => 'Plugin nicht gefunden',
'plugins' => 'Plugins',
'settings_save_failed' => 'Plugin-Einstellungen konnten nicht gespeichert werden',
'settings_saved' => 'Plugin-Einstellungen erfolgreich gespeichert',
'status' => 'Status',
'uninstall' => 'Deinstallieren',
'uninstall_failed' => 'Plugin konnte nicht deinstalliert werden',
'uninstalled' => 'Plugin erfolgreich deinstalliert',
'version' => 'Version',
];

View File

@@ -41,7 +41,6 @@ return [
"customer_discount" => "Rabatt",
"customer_email" => "Kunden eMail",
"customer_location" => "Kunden Stadt",
"customer_mailchimp_status" => "Mailchim Status",
"customer_optional" => "(Benötigt für fällige Zahlungen)",
"customer_required" => "(Benötigt)",
"customer_total" => "Gesamtbetrag",

View File

@@ -166,8 +166,6 @@ return [
"info" => "",
"info_configuration" => "",
"input_groups" => "",
"integrations" => "",
"integrations_configuration" => "",
"invoice" => "",
"invoice_configuration" => "",
"invoice_default_comments" => "",
@@ -198,13 +196,6 @@ return [
"location_info" => "",
"login_form" => "",
"logout" => "",
"mailchimp" => "",
"mailchimp_api_key" => "",
"mailchimp_configuration" => "",
"mailchimp_key_successfully" => "",
"mailchimp_key_unsuccessfully" => "",
"mailchimp_lists" => "",
"mailchimp_tooltip" => "",
"message" => "",
"message_configuration" => "",
"msg_msg" => "",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "",
"error_adding_updating" => "",
"import_items_csv" => "",
"mailchimp_activity_click" => "",
"mailchimp_activity_lastopen" => "",
"mailchimp_activity_open" => "",
"mailchimp_activity_total" => "",
"mailchimp_activity_unopen" => "",
"mailchimp_email_client" => "",
"mailchimp_info" => "",
"mailchimp_member_rating" => "",
"mailchimp_status" => "",
"mailchimp_vip" => "",
"max" => "",
"min" => "",
"new" => "",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "",
"office" => "",
"office_desc" => "",
'plugins' => 'Πρόσθετα',
"receivings" => "",
"receivings_desc" => "",
"reports" => "",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'Ενέργειες',
'active' => 'Ενεργό',
'configure' => 'Διαμόρφωση',
'description' => 'Περιγραφή',
'disable' => 'Απενεργοποίηση',
'disable_failed' => 'Η απενεργοποίηση της προσθήκης απέτυχε',
'disabled' => 'Η προσθήκη απενεργοποιήθηκε επιτυχώς',
'enable' => 'Ενεργοποίηση',
'enable_failed' => 'Η ενεργοποίηση της προσθήκης απέτυχε',
'enabled' => 'Η προσθήκη ενεργοποιήθηκε επιτυχώς',
'inactive' => 'Ανενεργό',
'management' => 'Διαχείριση Προσθηκών',
'name' => 'Όνομα Προσθήκης',
'no_config' => 'Αυτή η προσθήκη δεν έχει επιλογές διαμόρφωσης',
'no_plugins_to_display' => 'Δεν υπάρχουν προσθήκες για εμφάνιση',
'not_found' => 'Η προσθήκη δεν βρέθηκε',
'plugins' => 'Προσθήκες',
'settings_save_failed' => 'Η αποθήκευση των ρυθμίσεων της προσθήκης απέτυχε',
'settings_saved' => 'Οι ρυθμίσεις της προσθήκης αποθηκεύτηκαν επιτυχώς',
'status' => 'Κατάσταση',
'uninstall' => 'Απεγκατάσταση',
'uninstall_failed' => 'Η απεγκατάσταση της προσθήκης απέτυχε',
'uninstalled' => 'Η προσθήκη απεγκαταστάθηκε επιτυχώς',
'version' => 'Έκδοση',
];

View File

@@ -41,7 +41,6 @@ return [
"customer_discount" => "Έκπτωση",
"customer_email" => "Διεύθυνση ηλεκτρονικού ταχυδρομείου",
"customer_location" => "Τοποθεσία",
"customer_mailchimp_status" => "Κατάσταση Mailchimp",
"customer_optional" => "(Απαραίτητο για πληρωμές επί Πιστώσει)",
"customer_required" => "(Απαραίτητο)",
"customer_total" => "Σύνολο",

View File

@@ -166,8 +166,6 @@ return [
"info" => "Information",
"info_configuration" => "Shop 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",
@@ -198,13 +196,6 @@ return [
"location_info" => "Location Configuration Information",
"login_form" => "Login Form Style",
"logout" => "Don't you want to make a backup before logging out? Click [OK] to backup, [Cancel] to logout.",
"mailchimp" => "MailChimp",
"mailchimp_api_key" => "MailChimp API Key",
"mailchimp_configuration" => "MailChimp Configuration",
"mailchimp_key_successfully" => "Valid API Key.",
"mailchimp_key_unsuccessfully" => "Invalid API Key.",
"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",

View File

@@ -28,16 +28,6 @@ return [
"employee" => "Employee",
"error_adding_updating" => "Error adding/updating Customer.",
"import_items_csv" => "Customer Import from CSV",
"mailchimp_activity_click" => "Email click",
"mailchimp_activity_lastopen" => "Last open email",
"mailchimp_activity_open" => "Email open",
"mailchimp_activity_total" => "Email sent",
"mailchimp_activity_unopen" => "Email unopen",
"mailchimp_email_client" => "Email client",
"mailchimp_info" => "MailChimp",
"mailchimp_member_rating" => "Rating",
"mailchimp_status" => "Status",
"mailchimp_vip" => "VIP",
"max" => "Max spent",
"min" => "Min spent",
"new" => "New Customer",

View File

@@ -32,6 +32,7 @@ return [
"migrate_desc" => "Update the OSPOS Database.",
"office" => "Office",
"office_desc" => "List office menu modules.",
'plugins' => 'Plugins',
"receivings" => "Receivings",
"receivings_desc" => "Process Purchase Orders.",
"reports" => "Reports",

View File

@@ -0,0 +1,27 @@
<?php
return [
'actions' => 'Actions',
'active' => 'Active',
'configure' => 'Configure',
'description' => 'Description',
'disable' => 'Disable',
'disable_failed' => 'Failed to disable plugin',
'disabled' => 'Plugin disabled successfully',
'enable' => 'Enable',
'enable_failed' => 'Failed to enable plugin',
'enabled' => 'Plugin enabled successfully',
'inactive' => 'Inactive',
'management' => 'Plugin Management',
'name' => 'Plugin Name',
'no_config' => 'This plugin has no configuration options',
'no_plugins_to_display' => 'No Plugins to display',
'not_found' => 'Plugin not found',
'plugins' => 'Plugins',
'settings_save_failed' => 'Failed to save plugin settings',
'settings_saved' => 'Plugin settings saved successfully',
'status' => 'Status',
'uninstall' => 'Uninstall',
'uninstall_failed' => 'Failed to uninstall plugin',
'uninstalled' => 'Plugin uninstalled successfully',
'version' => 'Version',
];

View File

@@ -9,6 +9,7 @@ return [
"amount_due" => "Amount Due",
"amount_tendered" => "Amount Tendered",
"authorized_signature" => "Authorised Signature",
"bank_transfer" => "Bank Transfer",
"cancel_sale" => "Cancel",
"cash" => "Cash",
"cash_1" => "",
@@ -41,7 +42,6 @@ return [
"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",
@@ -223,6 +223,7 @@ return [
"update" => "Update",
"upi" => "UPI",
"visa" => "",
"wallet" => "Wallet",
"wholesale" => "",
"work_order" => "Work Order",
"work_order_number" => "Work Order Number",

View File

@@ -166,8 +166,6 @@ return [
"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",
@@ -198,13 +196,6 @@ return [
"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",

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