diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 0000000..056f12f --- /dev/null +++ b/.eslintrc @@ -0,0 +1,128 @@ +{ + "env": { + "browser": true, + "node": true, + "es2022": true + }, + "parserOptions": { + "sourceType": "module" + }, + "extends": "eslint:recommended", + "rules": { + "indent": "off", + "brace-style": "off", + "no-mixed-spaces-and-tabs": "off", + "no-useless-escape": "off", + "space-unary-ops": ["error", { "words": true }], + "linebreak-style": "off", + "quotes": ["off"], + "semi": "off", + "camelcase": "off", + "no-unused-vars": "off", + "no-console": ["warn"], + "no-extra-boolean-cast": ["off"], + "no-control-regex": ["off"], + }, + "root": true, + "globals": { + "frappe": true, + "erpnext": true, + "posnext": true, + "onScan": true, + "cur_pos": true, + "Vue": true, + "SetVueGlobals": true, + "__": true, + "repl": true, + "Class": true, + "locals": true, + "cint": true, + "cstr": true, + "cur_frm": true, + "cur_dialog": true, + "cur_page": true, + "cur_list": true, + "cur_tree": true, + "msg_dialog": true, + "is_null": true, + "in_list": true, + "has_common": true, + "posthog": true, + "has_words": true, + "validate_email": true, + "open_web_template_values_editor": true, + "validate_name": true, + "validate_phone": true, + "validate_url": true, + "get_number_format": true, + "format_number": true, + "format_currency": true, + "comment_when": true, + "open_url_post": true, + "toTitle": true, + "lstrip": true, + "rstrip": true, + "strip": true, + "strip_html": true, + "replace_all": true, + "flt": true, + "precision": true, + "CREATE": true, + "AMEND": true, + "CANCEL": true, + "copy_dict": true, + "get_number_format_info": true, + "strip_number_groups": true, + "print_table": true, + "Layout": true, + "web_form_settings": true, + "$c": true, + "$a": true, + "$i": true, + "$bg": true, + "$y": true, + "$c_obj": true, + "refresh_many": true, + "refresh_field": true, + "toggle_field": true, + "get_field_obj": true, + "get_query_params": true, + "unhide_field": true, + "hide_field": true, + "set_field_options": true, + "getCookie": true, + "getCookies": true, + "get_url_arg": true, + "md5": true, + "$": true, + "jQuery": true, + "moment": true, + "hljs": true, + "Awesomplete": true, + "Sortable": true, + "Showdown": true, + "Taggle": true, + "Gantt": true, + "Slick": true, + "Webcam": true, + "PhotoSwipe": true, + "PhotoSwipeUI_Default": true, + "io": true, + "JsBarcode": true, + "L": true, + "Chart": true, + "DataTable": true, + "Cypress": true, + "cy": true, + "it": true, + "describe": true, + "expect": true, + "context": true, + "before": true, + "beforeEach": true, + "after": true, + "qz": true, + "localforage": true, + "extend_cscript": true + } +} \ No newline at end of file diff --git a/.flake8_strict b/.flake8_strict new file mode 100644 index 0000000..3e8f7dd --- /dev/null +++ b/.flake8_strict @@ -0,0 +1,74 @@ +[flake8] +ignore = + B007, + B009, + B010, + B950, + E101, + E111, + E114, + E116, + E117, + E121, + E122, + E123, + E124, + E125, + E126, + E127, + E128, + E131, + E201, + E202, + E203, + E211, + E221, + E222, + E223, + E224, + E225, + E226, + E228, + E231, + E241, + E242, + E251, + E261, + E262, + E265, + E266, + E271, + E272, + E273, + E274, + E301, + E302, + E303, + E305, + E306, + E402, + E501, + E502, + E701, + E702, + E703, + E741, + F403, + W191, + W291, + W292, + W293, + W391, + W503, + W504, + E711, + E129, + F841, + E713, + E712, + B023, + B028 + + +max-line-length = 200 +exclude=.github/helper/semgrep_rules,test_*.py diff --git a/.github/ISSUE_TEMPLATE/bug_report.yaml b/.github/ISSUE_TEMPLATE/bug_report.yaml new file mode 100644 index 0000000..6da391c --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yaml @@ -0,0 +1,78 @@ +name: 🐞 Bug Report +description: File a bug/issue +title: "[BUG] " +labels: ["bug"] +body: +- type: textarea + attributes: + label: Current Behavior + description: A concise description of what the bug is. + validations: + required: true +- type: textarea + attributes: + label: Steps To Reproduce + description: Steps to reproduce the behavior. + placeholder: | + 1. Go to '...' + 1. Click on '....' + 1. Scroll down to '....' + 1. See error + validations: + required: true +- type: textarea + attributes: + label: Expected Behavior + description: A concise description of what you expected to happen. + validations: + required: true +- type: textarea + attributes: + label: Anything else? + description: | + Screenshots? Links? References? Anything that will give us more context about the issue you are encountering! + + Tip: You can attach images or log files by clicking this area to highlight it and then dragging files in. + validations: + required: false +- type: textarea + attributes: + label: Environment and Versions + description: | + examples: + - **Frappe Version**: v15.88.0 + - **ERPNext Version**: v15.88.0 + - **POSNext** (posnext): v0.6.2 + value: | + - Frappe Version: + - ERPNext Version: + - POSNext: + render: markdown + validations: + required: false +- type: dropdown + id: version + attributes: + label: Hosting Method + description: How is frappe installed/used? + multiple: true + options: + - Frappe Cloud + - Custom Frappe Press instance + - Remote or local Docker Instance + - Barebones + - Other + validations: + required: true +- type: dropdown + id: browsers + attributes: + label: What browsers are you seeing the problem on? + multiple: true + options: + - Chrome + - Microsoft Edge + - Firefox + - Safari + - Other + - N/A \ No newline at end of file diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..eb6e443 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,5 @@ +blank_issues_enabled: false +contact_links: + - name: Starktail (Pty) Ltd + url: https://starktail.com + about: More information on Starktail. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yaml b/.github/ISSUE_TEMPLATE/feature_request.yaml new file mode 100644 index 0000000..b02c92f --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yaml @@ -0,0 +1,33 @@ +name: πŸš€ Feature Request +description: Request a new feature, improvement or enhancement +title: "<title>" +labels: ["enhancement"] +body: +- type: textarea + attributes: + label: Is your feature request related to a problem? Please describe. + description: | + A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + validations: + required: true +- type: textarea + attributes: + label: Describe the solution you'd like + description: A clear and concise description of what you want to happen. + validations: + required: true +- type: textarea + attributes: + label: Describe alternatives you've considered + description: A clear and concise description of any alternative solutions or features you've considered. + validations: + required: true +- type: textarea + attributes: + label: Additional context + description: | + Add any other context or screenshots about the feature request here. + + Tip: You can attach images by clicking this area to highlight it and then dragging files in. + validations: + required: false \ No newline at end of file diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..eeef4e0 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,36 @@ +## Description + +>*This text should be replaced* +> +>*Please include a summary of the change and which issue is fixed. Please also include relevant motivation and context. List any dependencies that are required for this change.* + +## Type of change + +- [ ] Bug fix (change which fixes an issue) +- [ ] New feature (change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) + + +## Tests + +- [ ] [Unit Tests](https://frappeframework.com/docs/user/en/guides/automated-testing/unit-testing) have been updated or added, as required +- [ ] [UI Tests](https://frappeframework.com/docs/user/en/ui-testing) have been updated or added, as required + +## Checklist: + +- [ ] My code follows [Naming Guidelines](https://github.com/frappe/erpnext/wiki/Naming-Guidelines) (DocType, Field and Variable naming) +- [ ] No Form changes */* My code follows the [Form Design Guidelines](https://github.com/frappe/erpnext/wiki/Form-Design-Guidelines) +- [ ] My code follows the [Coding Standards](https://github.com/frappe/erpnext/wiki/Coding-Standards) of this project +- [ ] My code follows the [Code Security Guidelines](https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines) of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas */* No comments necessary +- [ ] I have made corresponding additions/changes to the documentation +- [ ] All business logic and validations are on the server-side */* No business logic or validation changes +- [ ] No patches are necessary */* Migration Patches have been added to the correct subdirectory of `/patches` and `patches.txt` have been updated + + +## User Experience: + +>*This text can be deleted* +> +>*If your change involves user experience, add a screenshot/animated GIF. An animated GIF guarantees that you have tested your change and there are no unintended errors.* diff --git a/.github/helper/install.sh b/.github/helper/install.sh new file mode 100644 index 0000000..a06fedc --- /dev/null +++ b/.github/helper/install.sh @@ -0,0 +1,61 @@ +#!/bin/bash + +set -e + +cd ~ || exit + +sudo apt update +sudo apt remove mysql-server mysql-client +sudo apt install libcups2-dev redis-server mariadb-client libmariadb-dev +# Dependencies for cypress: https://docs.cypress.io/guides/continuous-integration/introduction#UbuntuDebian +sudo apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb + +pip install frappe-bench + +bench init --skip-assets --python "$(which python)" --frappe-branch "$TEST_AGAINST_FRAPPE_VERSION" ~/frappe-bench + +mkdir ~/frappe-bench/sites/test_site +cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/ + +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'" +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe" +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'" + +mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES" + +install_whktml() { + wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb + sudo apt install /tmp/wkhtmltox.deb + +} +install_whktml & + +cd ~/frappe-bench || exit + +sed -i 's/watch:/# watch:/g' Procfile +sed -i 's/schedule:/# schedule:/g' Procfile +sed -i 's/socketio:/# socketio:/g' Procfile +sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile + +# Get dependent apps + +bench get-app https://github.com/frappe/erpnext --branch $TEST_AGAINST_ERPNEXT_VERSION --resolve-deps + + +bench get-app --overwrite posnext "${GITHUB_WORKSPACE}" +bench --verbose setup env --python python3.10 +bench --verbose setup requirements --dev + +bench start &>> ~/frappe-bench/bench_start.log & +CI=Yes bench build --app frappe & +bench --site test_site reinstall --yes + +# Install dependent apps + +bench --verbose --site test_site install-app erpnext + + +bench --verbose --site test_site install-app posnext \ No newline at end of file diff --git a/.github/helper/site_config.json b/.github/helper/site_config.json new file mode 100644 index 0000000..9aedb39 --- /dev/null +++ b/.github/helper/site_config.json @@ -0,0 +1,15 @@ +{ + "db_host": "127.0.0.1", + "db_port": 3306, + "db_name": "test_frappe", + "db_password": "test_frappe", + "auto_email_id": "test@example.com", + "mail_server": "smtp.example.com", + "mail_login": "test@example.com", + "mail_password": "test", + "admin_password": "admin", + "root_login": "root", + "root_password": "root", + "host_name": "http://test_site:8000", + "throttle_user_limit": 100 +} \ No newline at end of file diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 52fe72a..8a8af9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,29 +4,27 @@ name: CI on: push: branches: - - develop + - version-15 pull_request: concurrency: group: develop-posnext-${{ github.event.number }} cancel-in-progress: true +env: + TEST_AGAINST_FRAPPE_VERSION: v15.88.0 + + TEST_AGAINST_ERPNEXT_VERSION: v15.87.1 + + jobs: tests: - runs-on: ubuntu-latest + runs-on: ubuntu-22.04 strategy: fail-fast: false - name: Server + name: Backend Unit Tests & UI Tests services: - redis-cache: - image: redis:alpine - ports: - - 13000:6379 - redis-queue: - image: redis:alpine - ports: - - 11000:6379 mariadb: image: mariadb:10.6 env: @@ -37,7 +35,12 @@ jobs: steps: - name: Clone - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Find tests + run: | + echo "Finding tests" + grep -rn "def test" > /dev/null - name: Setup Python uses: actions/setup-python@v4 @@ -47,11 +50,15 @@ jobs: - name: Setup Node uses: actions/setup-node@v3 with: - node-version: 18 + node-version: 22 check-latest: true + - name: Add to Hosts + run: | + echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts + - name: Cache pip - uses: actions/cache@v2 + uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py', '**/setup.cfg') }} @@ -63,7 +70,8 @@ jobs: id: yarn-cache-dir-path run: 'echo "dir=$(yarn cache dir)" >> $GITHUB_OUTPUT' - - uses: actions/cache@v3 + - name: Cache yarn + uses: actions/cache@v3 id: yarn-cache with: path: ${{ steps.yarn-cache-dir-path.outputs.dir }} @@ -71,31 +79,66 @@ jobs: restore-keys: | ${{ runner.os }}-yarn- - - name: Install MariaDB Client - run: sudo apt-get install mariadb-client-10.6 + - name: Cache cypress binary + uses: actions/cache@v3 + with: + path: ~/.cache/Cypress + key: ${{ runner.os }}-cypress - - name: Setup + - name: Install run: | - pip install frappe-bench - bench init --skip-redis-config-generation --skip-assets --python "$(which python)" ~/frappe-bench - mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'" - mariadb --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'" + bash ${GITHUB_WORKSPACE}/.github/helper/install.sh - - name: Install + - name: Run Unit Tests working-directory: /home/runner/frappe-bench run: | - bench get-app posnext $GITHUB_WORKSPACE - bench setup requirements --dev - bench new-site --db-root-password root --admin-password admin test_site - bench --site test_site install-app posnext - bench build + bench --site test_site set-config allow_tests true + bench --site test_site run-tests --app posnext --coverage env: - CI: 'Yes' + TYPE: server - - name: Run Tests + - name: Run UI Tests working-directory: /home/runner/frappe-bench run: | - bench --site test_site set-config allow_tests true - bench --site test_site run-tests --app posnext + set -x + echo "Setting Up Procfile..." + + sed -i 's/^watch:/# watch:/g' Procfile + sed -i 's/^schedule:/# schedule:/g' Procfile + if [ "$TYPE" == "server" ]; then + sed -i 's/^socketio:/# socketio:/g' Procfile; + sed -i 's/^redis_socketio:/# redis_socketio:/g' Procfile; + fi + + echo "Starting Bench..." + export FRAPPE_TUNE_GC=True + bench start &> bench_start.log & + bench --site test_site run-ui-tests posnext --headless -- --record + + env: TYPE: server + CYPRESS_RECORD_KEY: ${{ secrets.CYPRESS_RECORD_KEY }} + + - name: Upload coverage data + uses: codecov/codecov-action@v3 + with: + name: Backend + token: ${{ secrets.CODECOV_TOKEN }} + # fail_ci_if_error: true + files: /home/runner/frappe-bench/sites/coverage.xml + verbose: true + + - name: Upload UI coverage data + uses: codecov/codecov-action@v3 + with: + name: Cypress + # fail_ci_if_error: true + files: /home/runner/frappe-bench/apps/frappe/.cypress-coverage/clover.xml + verbose: true + + - name: Show bench output + if: ${{ failure() }} + run: | + cat ~/frappe-bench/bench_start.log + diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml new file mode 100644 index 0000000..d4533c7 --- /dev/null +++ b/.github/workflows/linters.yml @@ -0,0 +1,99 @@ + +name: Linters + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: commitcheck-frappe-${{ github.event_name }}-${{ github.event.number }} + cancel-in-progress: true + +jobs: + commit-lint: + name: 'Semantic Commits' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v5 + with: + fetch-depth: 200 + - uses: actions/setup-node@v6 + with: + node-version: 22 + check-latest: true + + - name: Check commit titles + env: + npm_config_ignore_scripts: "true" + run: | + npm i -D --no-save \ + @commitlint/cli @commitlint/config-conventional \ + conventional-changelog-conventionalcommits + npx commitlint --verbose \ + --from ${{ github.event.pull_request.base.sha }} \ + --to ${{ github.event.pull_request.head.sha }} + + + linter: + name: 'Semgrep Rules' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: '3.10' + cache: pip + + - name: Download Semgrep rules + run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules + + - name: Run Semgrep rules + run: | + pip install semgrep + semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness + + deps-vulnerable-check: + name: 'Vulnerable Dependency Check' + runs-on: ubuntu-latest + + steps: + - uses: actions/setup-python@v6 + with: + python-version: '3.13' + + - uses: actions/checkout@v5 + + - name: Cache pip + uses: actions/cache@v4 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('**/*requirements.txt', '**/pyproject.toml', '**/setup.py') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Install and run pip-audit + run: | + pip install pip-audit + cd ${GITHUB_WORKSPACE} + pip-audit --desc on . + + precommit: + name: 'Pre-Commit' + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + + steps: + - uses: actions/checkout@v5 + - uses: actions/setup-python@v6 + with: + python-version: '3.10' + cache: pip + - uses: pre-commit/action@v3.0.1 diff --git a/.gitignore b/.gitignore index ba04025..feeef2a 100644 --- a/.gitignore +++ b/.gitignore @@ -3,5 +3,16 @@ *.egg-info *.swp tags -node_modules -__pycache__ \ No newline at end of file +posnext/docs/current +posnext/public/dist +node_modules/ +cypress/videos +cypress/screenshots +posnext/public/node_modules + +# Package management is managed by bench +yarn.lock + +# Docs +docs/.vitepress/dist +docs/.vitepress/cache \ No newline at end of file diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..5429937 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,69 @@ +exclude: 'node_modules|.git' +default_stages: [pre-commit] +fail_fast: false + + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + files: "posnext.*" + exclude: ".*json$|.*txt$|.*csv|.*md|.*svg" + - id: check-merge-conflict + - id: check-ast + - id: check-json + - id: check-toml + - id: check-yaml + - id: debug-statements + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.14.4 + hooks: + - id: ruff + name: "Run ruff import sorter" + args: ["--select=I", "--fix"] + + - id: ruff + name: "Run ruff linter" + + - id: ruff-format + name: "Run ruff formatter" + + - repo: https://github.com/pre-commit/mirrors-prettier + rev: v3.1.0 + hooks: + - id: prettier + types_or: [javascript, vue, scss] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + posnext/public/dist/.*| + .*node_modules.*| + .*boilerplate.*| + posnext/templates/includes/.*| + posnext/public/js/lib/.* + )$ + + + - repo: https://github.com/pre-commit/mirrors-eslint + rev: v8.56.0 + hooks: + - id: eslint + types_or: [javascript] + args: ['--quiet'] + # Ignore any files that might contain jinja / bundles + exclude: | + (?x)^( + posnext/public/dist/.*| + cypress/.*| + .*node_modules.*| + .*boilerplate.*| + posnext/templates/includes/.*| + posnext/public/js/lib/.* + )$ + +ci: + autoupdate_schedule: weekly + skip: [] + submodules: false diff --git a/README.md b/README.md index be232ca..e76b665 100644 --- a/README.md +++ b/README.md @@ -1,146 +1,137 @@ -# POSNext Documentation +<div align="center" markdown="1"> -## πŸš€ Update Note +<img src="https://raw.githubusercontent.com/frappe/erpnext/develop/erpnext/public/images/erpnext-logo.png" width="80" /> -We have introduced an upgraded branch (**Version 15**) for POSNext, fully compatible with **ERPNext Version 14 and Version 15**. In this update, the POS Invoice step has been streamlined, and invoices are now created directly within the **Sales Invoice** module, enhancing efficiency and simplifying the workflow. ---- +# POSNext -## πŸ“– Introduction +**POSNext for ERPNext** +![demo screenshot](docs/images/screenshot.png) +</div> -POSNext is an **open-source Point of Sale (POS) system** designed specifically for ERPNext. It is a fork of the default ERPNext POS, enriched with additional features inspired by **POSAwesome** and further innovations to meet the demands of modern retail and business environments. POSNext serves as a **flexible, customizable alternative** to POSAwesome, offering users improved adaptability and enhanced functionalities. ---- +### POSNext -## 🏁 Getting Started +![CI workflow](#) +![codecov](#) -POSNext is integrated with ERPNext’s default POS module. To begin using POSNext, ensure you have an **active installation of ERPNext**. +POSNext is an **open-source Point of Sale (POS) system** designed specifically for ERPNext. This repo is a fork of POSNext, which is a fork of the default ERPNext POS, enriched with additional features inspired by **POSAwesome** and further innovations to meet the demands of modern retail and business environments. POSNext serves as a **flexible, customizable alternative** to POSAwesome, offering users improved adaptability and enhanced functionalities. -### πŸ“Œ Prerequisites -- A running instance of **ERPNext Version 15 or 14** ---- +### License -## πŸ”§ Setting Up POSNext +MIT -### πŸ“₯ Installation -- **Available on Frappe Cloud Marketplace** -### βš™οΈ Configuration +### Features -1. **Access POS Profile Settings**: Navigate to the POS Profile settings within ERPNext. -2. **Configure Basic Settings**: Set up essential configurations such as currency, default warehouse, and user-specific settings. -3. **Assign User Roles and Permissions**: Define user roles and permissions tailored to POS operations, ensuring access control and security. +- Full Compatibility with ERPNext POS Features +- Profile Lock in POS Settings +- Show Order List Button +- Show Held Button +- Mobile Number-Based Customer Identification +- Show Checkout Button +- Show Only List View +- Show Only Card View +- Show Open Form View +- Show Toggle for Recent Orders +- Save as Draft Option +- Close POS Option +- Default View Setting (Card/List) +- Allow Adding New Items on Separate Lines +- Display Posting Date +- Show OEM Part Number +- Show Logical Rack Location +- Edit Rate and UOM (Unit of Measure) +- Enable Credit Sales +- Add Additional Notes +- Include and Exclude Tax Options +- Display Alternative Items for POS Search +- Configure Mobile Number Length +- Send Invoice via WhatsApp +- Customizable POS Profile +- Credit Sale +- Incoming Rate -### βœ… Features & Enhancements +### User documentation -#### πŸ”„ Full Compatibility with ERPNext POS Features -- POSNext retains all **core ERPNext POS** functionalities, ensuring seamless integration with existing ERPNext features. +πŸ“„ POSNext: https://[yoursite].com/posnext -#### πŸ” Profile Lock in POS Settings -- **Make POS settings read-only** to prevent unauthorized changes and maintain configuration integrity. +### Installation -#### πŸ“‹ Show Order List Button -- Adds an **"Order List"** button in POS, allowing users to conveniently view all past orders. +You can install this app using the [bench](https://github.com/frappe/bench) CLI: -#### πŸ›’ Show Held Button -- Enables users to **place orders on hold** and complete them later. +```bash +cd $PATH_TO_YOUR_BENCH +bench get-app $URL_OF_THIS_REPO --branch develop +bench install-app posnext +``` -#### πŸ“± Mobile Number-Based Customer Identification -- **Locks the customer field** and uses mobile numbers for customer identification, ensuring accuracy. +### Development -#### 🏁 Show Checkout Button -- Adds a **"Checkout"** button for easy finalization of transactions. +#### Tests -#### πŸ”³ Show Only List View -- Limits the POS interface to **List View**, displaying item details in a structured list format. +To run unit tests: -#### πŸƒ Show Only Card View -- Configures POS to display items exclusively in **Card View**, enhancing item selection with a visual card layout. +```shell +bench --site test_site run-tests --app posnext --coverage +``` -#### πŸ“ Show Open Form View -- Adds an **optional detailed form view** within the POS menu for expanded transaction details. +To run UI/integration tests: -#### πŸ” Show Toggle for Recent Orders -- Enables a **toggle switch** in POS for viewing recent transactions. +The following depencies are required +```shell +sudo apt update +# Dependencies for cypress: https://docs.cypress.io/guides/continuous-integration/introduction#UbuntuDebian +sudo apt-get install libgtk2.0-0 libgtk-3-0 libgbm-dev libnotify-dev libgconf-2-4 libnss3 libxss1 libasound2 libxtst6 xauth xvfb -#### πŸ“‚ Save as Draft Option -- Allows users to **save orders as drafts** for later review or editing. +sudo apt-get install chromium +``` -#### ❌ Close POS Option -- Adds a **POS Close** option for session management and security. +```shell +bench --site test_site run-ui-tests posnext --headless --browser chromium +``` -#### πŸŽ›οΈ Default View Setting (Card/List) -- Allows users to **choose a default layout** between Card View and List View. +#### Contributing -#### βž• Allow Adding New Items on Separate Lines -- Enables users to **add new items on individual lines**, improving item organization. +This app uses `pre-commit` for code formatting and linting. Please [install pre-commit](https://pre-commit.com/#installation) and enable it for this repository: -#### πŸ“… Display Posting Date -- Shows the **transaction posting date** in POS for enhanced record-keeping. +```bash +cd apps/posnext +pre-commit install -#### πŸ”’ Show OEM Part Number -- Displays the **OEM part number** of items in POS for quick identification. +#(optional) Run against all the files +pre-commit run --all-files +``` -#### πŸ“ Show Logical Rack Location -- Displays the **logical rack location** of items in POS, assisting in efficient inventory management. +Pre-commit is configured to use the following tools for checking and formatting your code: -#### πŸ’° Edit Rate and UOM (Unit of Measure) -- **Modify item rates and UOMs** directly in POS for pricing flexibility. +- ruff +- eslint +- prettier +- pyupgrade -#### πŸ’³ Enable Credit Sales -- Supports **credit sales**, allowing customers to purchase on credit terms. -#### πŸ—’οΈ Add Additional Notes -- Provides an option to **add comments or instructions** to transactions. +We use [Semgrep](https://semgrep.dev/docs/getting-started/) rules specific to [Frappe Framework](https://github.com/frappe/frappe) +```shell +# Install semgrep +python3 -m pip install semgrep -#### 🏦 Include and Exclude Tax Options -- Allows users to **include or exclude tax** from transactions. +# Clone the rules repository +git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules -#### πŸ”„ Display Alternative Items for POS Search -- Shows **alternative items** during searches, useful for substitutions. +# Run semgrep specifying rules folder as config +semgrep --config=/workspace/development/frappe-semgrep-rules/rules apps/posnext +``` -#### πŸ“² Configure Mobile Number Length -- Sets **mobile number validation** based on country requirements. +#### Updating Documentation -#### 🟒 Send Invoice via WhatsApp -- Enables **sending invoices directly to customers** via WhatsApp. +For documentation, we use [vitepress](https://vitepress.dev/). You can run `yarn docs:dev` to preview the docs when applying changes -#### βš™οΈ Customizable POS Profile -- Allows **tailored profile adjustments** to support **multi-currency transactions** and other business needs. +#### CI -#### πŸ’΅ Credit Sale -- Enables **credit sales tracking** for customers making purchases on credit. - -#### πŸ“Š Incoming Rate -- Tracks the **cost at which items are received** or procured. - ---- - -## ☁️ Deployment Options - -### πŸš€ Managed Hosting -Deploy POSNext on **(https://frappecloud.com/marketplace/apps/posnext)** for a hassle-free experience. Frappe Cloud handles installation, updates, security, and support. - -### πŸ”§ Self-Hosting -To set up POSNext on your own server: - -bench get-app branch version-15 https://github.com/exvas/posnext.git - -bench setup requirements - -bench build --app posnext - -bench restart - -bench --site [your.site.name] install-app posnext - -bench --site [your.site.name] migrate - -## 🀝 Contributing -We welcome contributions! (https://github.com/exvas/POSNext/pulls) - -## πŸ“œ License -POSNext is released under the [MIT License](https://github.com/posnext/app/blob/develop/LICENSE). +This app can use GitHub Actions for CI. The following workflows are configured: +- CI: Installs this app and runs unit tests on every push to `develop` branch. +- Linters: Runs [Frappe Semgrep Rules](https://github.com/frappe/semgrep-rules) and [pip-audit](https://pypi.org/project/pip-audit/) on every pull request, as well as [Semgrep](https://semgrep.dev/docs/getting-started/) diff --git a/commitlint.config.js b/commitlint.config.js new file mode 100644 index 0000000..bd5cebc --- /dev/null +++ b/commitlint.config.js @@ -0,0 +1,25 @@ +module.exports = { + parserPreset: "conventional-changelog-conventionalcommits", + rules: { + "subject-empty": [2, "never"], + "type-case": [2, "always", "lower-case"], + "type-empty": [2, "never"], + "type-enum": [ + 2, + "always", + [ + "build", + "chore", + "ci", + "docs", + "feat", + "fix", + "perf", + "refactor", + "revert", + "style", + "test", + ], + ], + }, +}; diff --git a/cypress.config.js b/cypress.config.js new file mode 100644 index 0000000..d961bf4 --- /dev/null +++ b/cypress.config.js @@ -0,0 +1,25 @@ +const { defineConfig } = require("cypress"); + +module.exports = defineConfig({ + projectId: "venmyd", + adminPassword: "admin", + testUser: "frappe@example.com", + defaultCommandTimeout: 20000, + pageLoadTimeout: 15000, + video: true, + viewportHeight: 960, + viewportWidth: 1400, + retries: { + runMode: 2, + openMode: 2, + }, + e2e: { + // We've imported your old cypress plugins here. + // You may want to clean this up later by importing these. + setupNodeEvents(on, config) { + return require("./cypress/plugins/index.js")(on, config); + }, + baseUrl: "http://test_site_ui:8000", + specPattern: ["./cypress/integration/*.js", "**/ui_test_*.js"], + }, +}); diff --git a/cypress/integration/todo.js b/cypress/integration/todo.js new file mode 100644 index 0000000..0cf096d --- /dev/null +++ b/cypress/integration/todo.js @@ -0,0 +1,25 @@ +context("ToDo", () => { + before(() => { + cy.login("Administrator", "admin"); + cy.visit("/desk"); + }); + + it("creates a new todo", () => { + cy.visit("/app/todo/new"); + cy.get_field("description", "Text Editor") + .type("this is a test todo", { force: true }) + .wait(400); + cy.get(".page-title").should("contain", "Not Saved"); + cy.intercept({ + method: "POST", + url: "api/method/frappe.desk.form.save.savedocs", + }).as("form_save"); + cy.get(".primary-action").click(); + cy.wait("@form_save").its("response.statusCode").should("eq", 200); + + cy.go_to_list("ToDo"); + cy.clear_filters(); + cy.get(".page-head").findByTitle("To Do").should("exist"); + cy.get(".list-row").should("contain", "this is a test todo"); + }); +}); diff --git a/cypress/plugins/index.js b/cypress/plugins/index.js new file mode 100644 index 0000000..7f662a1 --- /dev/null +++ b/cypress/plugins/index.js @@ -0,0 +1,17 @@ +// *********************************************************** +// This example plugins/index.js can be used to load plugins +// +// You can change the location of this file or turn off loading +// the plugins file with the 'pluginsFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/plugins-guide +// *********************************************************** + +// This function is called when a project is opened or re-opened (e.g. due to +// the project's config changing) + +module.exports = (on, config) => { + require("@cypress/code-coverage/task")(on, config); + return config; +}; diff --git a/cypress/support/commands.js b/cypress/support/commands.js new file mode 100644 index 0000000..1675ba1 --- /dev/null +++ b/cypress/support/commands.js @@ -0,0 +1,576 @@ +import "@testing-library/cypress/add-commands"; +import "@4tw/cypress-drag-drop"; +import "cypress-real-events/support"; +// *********************************************** +// This example commands.js shows you how to +// create various custom commands and overwrite +// existing commands. +// +// For more comprehensive examples of custom +// commands please read more here: +// https://on.cypress.io/custom-commands +// *********************************************** +// +// +// -- This is a parent command -- +// Cypress.Commands.add("login", (email, password) => { ... }); +// +// +// -- This is a child command -- +// Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }); +// +// +// -- This is a dual command -- +// Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }); +// +// +// -- This is will overwrite an existing command -- +// Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }); + +Cypress.Commands.add("login", (email, password) => { + if (!email) { + email = Cypress.config("testUser") || "Administrator"; + } + if (!password) { + password = Cypress.env("adminPassword"); + } + // cy.session clears all localStorage on new login, so we need to retain the last route + const session_last_route = window.localStorage.getItem("session_last_route"); + return cy + .session( + [email, password] || "", + () => { + return cy.request({ + url: "/api/method/login", + method: "POST", + body: { + usr: email, + pwd: password, + }, + }); + }, + { + cacheAcrossSpecs: true, + }, + ) + .then(() => { + if (session_last_route) { + window.localStorage.setItem("session_last_route", session_last_route); + } + }); +}); + +Cypress.Commands.add("call", (method, args) => { + return cy + .window() + .its("frappe.csrf_token") + .then((csrf_token) => { + return cy + .request({ + url: `/api/method/${method}`, + method: "POST", + body: args, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-Frappe-CSRF-Token": csrf_token, + }, + }) + .then((res) => { + expect(res.status).eq(200); + if (method === "logout") { + Cypress.session.clearAllSavedSessions(); + } + return res.body; + }); + }); +}); + +Cypress.Commands.add("get_list", (doctype, fields = [], filters = []) => { + filters = JSON.stringify(filters); + fields = JSON.stringify(fields); + let url = `/api/resource/${doctype}?fields=${fields}&filters=${filters}`; + return cy + .window() + .its("frappe.csrf_token") + .then((csrf_token) => { + return cy + .request({ + method: "GET", + url, + headers: { + Accept: "application/json", + "X-Frappe-CSRF-Token": csrf_token, + }, + }) + .then((res) => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add("get_doc", (doctype, name) => { + return cy + .window() + .its("frappe.csrf_token") + .then((csrf_token) => { + return cy + .request({ + method: "GET", + url: `/api/resource/${doctype}/${name}`, + headers: { + Accept: "application/json", + "X-Frappe-CSRF-Token": csrf_token, + }, + }) + .then((res) => { + expect(res.status).eq(200); + return res.body; + }); + }); +}); + +Cypress.Commands.add("remove_doc", (doctype, name) => { + return cy + .window() + .its("frappe.csrf_token") + .then((csrf_token) => { + return cy + .request({ + method: "DELETE", + url: `/api/resource/${doctype}/${name}`, + headers: { + Accept: "application/json", + "X-Frappe-CSRF-Token": csrf_token, + }, + }) + .then((res) => { + expect(res.status).eq(202); + return res.body; + }); + }); +}); + +Cypress.Commands.add("create_records", (doc) => { + return cy + .call("frappe.tests.ui_test_helpers.create_if_not_exists", { + doc: JSON.stringify(doc), + }) + .then((r) => r.message); +}); + +Cypress.Commands.add("set_value", (doctype, name, obj) => { + return cy.call("frappe.client.set_value", { + doctype, + name, + fieldname: obj, + }); +}); + +Cypress.Commands.add("fill_field", (fieldname, value, fieldtype = "Data") => { + cy.get_field(fieldname, fieldtype).as("input"); + + if (["Date", "Time", "Datetime"].includes(fieldtype)) { + cy.get("@input").clear().wait(200); + cy.get("@input").click().wait(200); + cy.get(".datepickers-container .datepicker.active").should("exist"); + } + if (fieldtype === "Time") { + cy.get("@input").clear().wait(200); + } + + if (fieldtype === "Select") { + cy.get("@input").select(value); + } else { + cy.get("@input").type(value, { + waitForAnimations: false, + parseSpecialCharSequences: false, + force: true, + delay: 100, + }); + } + return cy.get("@input"); +}); + +Cypress.Commands.add("get_field", (fieldname, fieldtype = "Data") => { + let field_element = fieldtype === "Select" ? "select" : "input"; + let selector = `[data-fieldname="${fieldname}"] ${field_element}:visible`; + + selector = + fieldtype !== "Select" + ? selector + : `[data-fieldname="${fieldname}"] ${field_element}`; + + if (fieldtype === "Text Editor") { + selector = `[data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]:visible`; + } + if (fieldtype === "Code") { + selector = `[data-fieldname="${fieldname}"] .ace_text-input`; + } + if (fieldtype === "Markdown Editor") { + selector = `[data-fieldname="${fieldname}"] .ace-editor-target`; + } + + return cy.get(selector).first(); +}); + +Cypress.Commands.add( + "fill_table_field", + (tablefieldname, row_idx, fieldname, value, fieldtype = "Data") => { + cy.get_table_field(tablefieldname, row_idx, fieldname, fieldtype).as( + "input", + ); + + if (["Date", "Time", "Datetime"].includes(fieldtype)) { + cy.get("@input").click().wait(200); + cy.get(".datepickers-container .datepicker.active").should("exist"); + } + if (fieldtype === "Time") { + cy.get("@input").clear().wait(200); + } + + if (fieldtype === "Select") { + cy.get("@input").select(value); + } else { + cy.get("@input").type(value, { waitForAnimations: false, force: true }); + } + return cy.get("@input"); + }, +); + +Cypress.Commands.add( + "get_table_field", + (tablefieldname, row_idx, fieldname, fieldtype = "Data") => { + let selector = `.frappe-control[data-fieldname="${tablefieldname}"]`; + selector += ` [data-idx="${row_idx}"]`; + + if (fieldtype === "Text Editor") { + selector += ` [data-fieldname="${fieldname}"] .ql-editor[contenteditable=true]`; + } else if (fieldtype === "Code") { + selector += ` [data-fieldname="${fieldname}"] .ace_text-input`; + } else { + selector += ` [data-fieldname="${fieldname}"]`; + return cy + .get(selector) + .find(".form-control:visible, .static-area:visible") + .first(); + } + return cy.get(selector); + }, +); + +Cypress.Commands.add("awesomebar", (text) => { + cy.get("#navbar-search").type(`${text}{downarrow}{enter}`, { delay: 700 }); +}); + +Cypress.Commands.add("new_form", (doctype) => { + let dt_in_route = doctype.toLowerCase().replace(/ /g, "-"); + cy.visit(`/app/${dt_in_route}/new`); + cy.get("body").should(($body) => { + const dataRoute = $body.attr("data-route"); + expect(dataRoute).to.match( + new RegExp(`^Form/${doctype}/new-${dt_in_route}-`), + ); + }); + cy.get("body").should("have.attr", "data-ajax-state", "complete"); +}); + +Cypress.Commands.add("select_form_tab", (label) => { + cy.get(".form-tabs-list [data-toggle='tab']") + .contains(label) + .click() + .wait(500); +}); + +Cypress.Commands.add("go_to_list", (doctype) => { + let dt_in_route = doctype.toLowerCase().replace(/ /g, "-"); + cy.visit(`/app/${dt_in_route}`); +}); + +Cypress.Commands.add("clear_cache", () => { + cy.window() + .its("frappe") + .then((frappe) => { + frappe.ui.toolbar.clear_cache(); + }); +}); + +Cypress.Commands.add("dialog", (opts) => { + return cy + .window({ log: false }) + .its("frappe", { log: false }) + .then((frappe) => { + Cypress.log({ + name: "dialog", + displayName: "dialog", + message: "frappe.ui.Dialog", + consoleProps: () => { + return { + options: opts, + dialog: d, + }; + }, + }); + + var d = new frappe.ui.Dialog(opts); + d.show(); + return d; + }); +}); + +Cypress.Commands.add("get_open_dialog", () => { + return cy.get(".modal:visible").last(); +}); + +Cypress.Commands.add("save", () => { + cy.intercept("/api/method/frappe.desk.form.save.savedocs").as("save_call"); + cy.get(`.page-container:visible button[data-label="Save"]`).click({ + force: true, + }); + cy.wait("@save_call"); +}); +Cypress.Commands.add("hide_dialog", () => { + cy.wait(500); + cy.get_open_dialog().focus().find(".btn-modal-close").click(); + cy.get(".modal:visible").should("not.exist"); +}); + +Cypress.Commands.add("clear_dialogs", () => { + cy.window().then((win) => { + win.$(".modal, .modal-backdrop").remove(); + }); + cy.get(".modal").should("not.exist"); +}); + +Cypress.Commands.add("clear_datepickers", () => { + cy.window().then((win) => { + win.$(".datepicker").remove(); + }); + cy.get(".datepicker").should("not.exist"); +}); + +Cypress.Commands.add("insert_doc", (doctype, args, ignore_duplicate) => { + if (!args.doctype) { + args.doctype = doctype; + } + return cy + .window() + .its("frappe.csrf_token") + .then((csrf_token) => { + return cy + .request({ + method: "POST", + url: `/api/resource/${doctype}`, + body: args, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-Frappe-CSRF-Token": csrf_token, + }, + failOnStatusCode: !ignore_duplicate, + }) + .then((res) => { + let status_codes = [200]; + if (ignore_duplicate) { + status_codes.push(409); + } + + let message = null; + if (ignore_duplicate && !status_codes.includes(res.status)) { + message = `Document insert failed, response: ${JSON.stringify( + res, + null, + "\t", + )}`; + } + expect(res.status).to.be.oneOf(status_codes, message); + return res.body.data; + }); + }); +}); + +Cypress.Commands.add("update_doc", (doctype, docname, args) => { + return cy + .window() + .its("frappe.csrf_token") + .then((csrf_token) => { + return cy + .request({ + method: "PUT", + url: `/api/resource/${doctype}/${docname}`, + body: args, + headers: { + Accept: "application/json", + "Content-Type": "application/json", + "X-Frappe-CSRF-Token": csrf_token, + }, + }) + .then((res) => { + expect(res.status).to.eq(200); + return res.body.data; + }); + }); +}); + +Cypress.Commands.add("switch_to_user", (user) => { + cy.call("logout"); + cy.wait(200); + cy.login(user); + cy.reload(); +}); + +Cypress.Commands.add("add_role", (user, role) => { + cy.window() + .its("frappe") + .then((frappe) => { + const session_user = frappe.session.user; + add_remove_role("add", user, role, session_user); + }); +}); + +Cypress.Commands.add("remove_role", (user, role) => { + cy.window() + .its("frappe") + .then((frappe) => { + const session_user = frappe.session.user; + add_remove_role("remove", user, role, session_user); + }); +}); + +const add_remove_role = (action, user, role, session_user) => { + if (session_user !== "Administrator") { + cy.switch_to_user("Administrator"); + } + + cy.call("frappe.tests.ui_test_helpers.add_remove_role", { + action: action, + user: user, + role: role, + }); + + if (session_user !== "Administrator") { + cy.switch_to_user(session_user); + } +}; + +Cypress.Commands.add("open_list_filter", () => { + cy.get(".filter-section .filter-button").click(); + cy.wait(300); + cy.get(".filter-popover").should("exist"); +}); + +Cypress.Commands.add("click_custom_action_button", (name) => { + cy.get(`.custom-actions [data-label="${encodeURIComponent(name)}"]`).click(); +}); + +Cypress.Commands.add("click_action_button", (name) => { + cy.findByRole("button", { name: "Actions" }).click(); + cy.get( + `.actions-btn-group [data-label="${encodeURIComponent(name)}"]`, + ).click(); +}); + +Cypress.Commands.add("click_menu_button", (name) => { + cy.get(".standard-actions .menu-btn-group > .btn").click(); + cy.get(`.menu-btn-group [data-label="${encodeURIComponent(name)}"]`).click(); +}); + +Cypress.Commands.add("clear_filters", () => { + let has_filter = false; + cy.intercept({ + method: "POST", + url: "api/method/frappe.model.utils.user_settings.save", + }).as("filter-saved"); + cy.get(".filter-section .filter-button").click({ force: true }); + cy.wait(300); + cy.get(".filter-popover").should("exist"); + cy.get(".filter-popover").then((popover) => { + if (popover.find("input.input-with-feedback")[0].value != "") { + has_filter = true; + } + }); + cy.get(".filter-popover").find(".clear-filters").click(); + cy.get(".filter-section .filter-button").click(); + cy.window() + .its("cur_list") + .then((cur_list) => { + cur_list && cur_list.filter_area && cur_list.filter_area.clear(); + has_filter && cy.wait("@filter-saved"); + }); +}); + +Cypress.Commands.add("click_modal_primary_button", (btn_name) => { + cy.wait(400); + cy.get(".modal-footer > .standard-actions > .btn-primary") + .contains(btn_name) + .click({ force: true }); +}); + +Cypress.Commands.add("click_sidebar_button", (btn_name) => { + cy.get(".list-group-by-fields .list-link > a") + .contains(btn_name) + .click({ force: true }); +}); + +Cypress.Commands.add("click_listview_row_item", (row_no) => { + cy.get(".list-row > .level-left > .list-subject > .level-item > .ellipsis") + .eq(row_no) + .click({ force: true }); +}); + +Cypress.Commands.add("click_listview_row_item_with_text", (text) => { + cy.get(".list-row > .level-left > .list-subject > .level-item > .ellipsis") + .contains(text) + .first() + .click({ force: true }); +}); + +Cypress.Commands.add("click_filter_button", () => { + cy.get(".filter-button").click(); +}); + +Cypress.Commands.add("click_listview_primary_button", (btn_name) => { + cy.get(".primary-action").contains(btn_name).click({ force: true }); +}); + +Cypress.Commands.add("click_doc_primary_button", (btn_name) => { + cy.get(".primary-action").contains(btn_name).click({ force: true }); +}); + +Cypress.Commands.add("click_timeline_action_btn", (btn_name) => { + cy.get(".timeline-message-box .actions .action-btn") + .contains(btn_name) + .click(); +}); + +Cypress.Commands.add("select_listview_row_checkbox", (row_no) => { + cy.get(".frappe-list .select-like > .list-row-checkbox").eq(row_no).click(); +}); + +Cypress.Commands.add("click_form_section", (section_name) => { + cy.get(".section-head").contains(section_name).click(); +}); + +const compare_document = (expected, actual) => { + for (const prop in expected) { + if (expected[prop] instanceof Array) { + // recursively compare child documents. + expected[prop].forEach((item, idx) => { + compare_document(item, actual[prop][idx]); + }); + } else { + assert.equal(expected[prop], actual[prop], `${prop} should be equal.`); + } + } +}; + +Cypress.Commands.add("compare_document", (expected_document) => { + cy.window() + .its("cur_frm") + .then((frm) => { + // Don't remove this, cypress can't magically wait for events it has no control over. + cy.wait(1000); + compare_document(expected_document, frm.doc); + }); +}); diff --git a/cypress/support/e2e.js b/cypress/support/e2e.js new file mode 100644 index 0000000..5bf61ea --- /dev/null +++ b/cypress/support/e2e.js @@ -0,0 +1,25 @@ +// *********************************************************** +// This example support/index.js is processed and +// loaded automatically before your test files. +// +// This is a great place to put global configuration and +// behavior that modifies Cypress. +// +// You can change the location of this file or turn off +// automatically serving support files with the +// 'supportFile' configuration option. +// +// You can read more here: +// https://on.cypress.io/configuration +// *********************************************************** + +// Import commands.js using ES2015 syntax: +import "./commands"; +import "@cypress/code-coverage/support"; + +Cypress.on("uncaught:exception", (err, runnable) => { + return false; +}); + +// Alternatively you can use CommonJS syntax: +// require('./commands') diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts new file mode 100644 index 0000000..2346204 --- /dev/null +++ b/docs/.vitepress/config.mts @@ -0,0 +1,51 @@ +import { defineConfig } from 'vitepress' + +// https://vitepress.dev/reference/site-config +export default defineConfig({ + title: "POSNext Documentation", + description: "POSNext Documentation", + outDir: '../posnext/www', + assetsDir: 'assets/posnext', + themeConfig: { + // https://vitepress.dev/reference/default-theme-config + nav: [ + { text: 'Desk', link: '/' }, + { text: 'Documentation Home', link: '/posnext_introduction' }, + { text: 'Starktail', link: 'https://starktail.com' } + ], + + sidebar: [ + { text: 'Introduction', link: '/posnext_introduction.md' } + ], + + socialLinks: [ + { icon: 'whatsapp', link: 'https://wa.me/27686318877?text=Hi%2C%20I%20have%20a%20question%20on%20POSNext'}, + { icon: 'mailgun', link: 'mailto:support@starktail.com'}, + { icon: 'github', link: 'https://github.com/Starktail/posnext' } + ], + + editLink: { + pattern: 'https://github.com/Starktail/posnext/edit/version-15/docs/:path' + } + }, + // Set metaChunk to avoid having window.__VP_HASH_MAP__ in the generated HTML, + // as this blocks jinja template rendering for frappe portal pages + metaChunk: true, + ignoreDeadLinks: [ + // ignore all links starting with /app/ (these point to doctypes or other resources + // hosted on the frappe site, and won't be alive at build time) + /^\/app\// + ], + // Links that point to pages outside our vitepress docs, like Doctype links should not + // be appended with .html + transformHtml: (code) => { + return code.replace(/href="(\/app\/[^"]*)\.html"/g, 'href="$1"'); + }, + // Inline ALL images to avoid having images in the public directory + vite: { + build: { + assetsInlineLimit: 52428800, // 50 MB, + chunkSizeWarningLimit: 2000 // 2000 KB + }, + }, +}) \ No newline at end of file diff --git a/docs/images/.gitkeep b/docs/images/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/images/screenshot.png b/docs/images/screenshot.png new file mode 100644 index 0000000..c950a9f Binary files /dev/null and b/docs/images/screenshot.png differ diff --git a/docs/posnext_introduction.md b/docs/posnext_introduction.md new file mode 100644 index 0000000..75c3994 --- /dev/null +++ b/docs/posnext_introduction.md @@ -0,0 +1,23 @@ +# Introduction +## What is POSNext? + +This is a [Frappe](https://frappeframework.com/) custom app, intended to ... + +## Key Features + +1. Feature 1 + +## Under the Hood + +- [Frappe Framework](https://frappe.io/framework): A full-stack web application framework written in Python and Javascript. The framework provides a robust foundation for building web applications, including a database abstraction layer, user authentication, and a REST API. + + +## Installation + +Go [here](https://github.com/Starktail/posnext) for installation. + +## Support + +- [Starktail Website](https://starktail.com) +- [Starktail Email Support](mailto:support@starktail.com) +- [Starktail WhatsApp Support](https://wa.me/27686318877?text=Hi%2C%20I%20have%20a%20question%20on%20POSNext) diff --git a/package.json b/package.json index 34a0441..a3a2152 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,22 @@ { - - "devDependencies": {}, - "dependencies": { - "onscan.js": "^1.5.2" + "name": "posnext", + "version": "0.0.1", + "author": "Starktail (Pty) Ltd <support@starktail.com>", + "main": "index.js", + "devDependencies": { + "vitepress": "^2.0.0-alpha.12", + "vue": "^3.5.24", + "@4tw/cypress-drag-drop": "^2.2.5", + "@cypress/code-coverage": "^3", + "@testing-library/cypress": "^10", + "@testing-library/dom": "8.17.1", + "cypress": "^13.1.0", + "cypress-real-events": "^1.7.6" + }, + "scripts": { + "docs:dev": "vitepress dev docs --host", + "build": "vitepress build docs && ./prepare_help_files.sh", + "docs:build-gh": "vitepress build docs --outDir docs/.vitepress/dist", + "docs:preview": "vitepress preview docs" } -} +} \ No newline at end of file diff --git a/pos_controller.js b/pos_controller.js index 5fbea9d..3534408 100644 --- a/pos_controller.js +++ b/pos_controller.js @@ -1,928 +1,1051 @@ -frappe.provide('posnext.PointOfSale'); -var selected_item = null +frappe.provide("posnext.PointOfSale"); +var selected_item = null; posnext.PointOfSale.Controller = class { - constructor(wrapper) { - console.log("CONTROLLLLLERE") - this.wrapper = $(wrapper).find('.layout-main-section'); - this.page = wrapper.page; - this.add_ledger_balance_box(); - frappe.run_serially([ - () => this.reload_status = false, - () => this.check_opening_entry(""), - () => this.reload_status = true, - ]); - - - - } - - // Function to add the ledger balance box - add_ledger_balance_box() { - const updateLedgerBalance = (customerSection, balance) => { - let ledgerBalanceDiv = document.getElementById("ledger-balance-box"); - - if (!ledgerBalanceDiv) { - ledgerBalanceDiv = document.createElement("div"); - ledgerBalanceDiv.id = "ledger-balance-box"; - ledgerBalanceDiv.style.marginTop = "10px"; - ledgerBalanceDiv.style.padding = "10px"; - ledgerBalanceDiv.style.border = "1px solid #ccc"; - ledgerBalanceDiv.style.borderRadius = "4px"; - ledgerBalanceDiv.style.backgroundColor = "#f0f8ff"; - ledgerBalanceDiv.style.fontWeight = "bold"; - customerSection.appendChild(ledgerBalanceDiv); - } - - // Update the balance - ledgerBalanceDiv.textContent = `Ledger Balance: Rs. ${balance}`; - }; - - const fetchLedgerBalance = async (customerId) => { - try { - const response = await frappe.call({ - method: "posnext.controllers.queries.get_ledger_balance", - args: { customer: customerId }, - }); - return response.message || "0.00"; // Default to 0.00 if no balance found - } catch (error) { - console.error("Failed to fetch ledger balance:", error); - return "Error"; - } - }; - - // Watch for customer section changes - const observer = new MutationObserver(async () => { - const customerSection = document.querySelector(".customer-section"); - - if (customerSection) { - const resetCustomerBtn = customerSection.querySelector(".reset-customer-btn"); - const customerId = resetCustomerBtn?.getAttribute("data-customer"); - - if (customerId && customerId !== "undefined") { - const ledgerBalance = await fetchLedgerBalance(customerId); - updateLedgerBalance(customerSection, ledgerBalance); - } else { - updateLedgerBalance(customerSection, "Select a Customer"); - } + constructor(wrapper) { + console.log("CONTROLLLLLERE"); + this.wrapper = $(wrapper).find(".layout-main-section"); + this.page = wrapper.page; + this.add_ledger_balance_box(); + frappe.run_serially([ + () => (this.reload_status = false), + () => this.check_opening_entry(""), + () => (this.reload_status = true), + ]); + } + + // Function to add the ledger balance box + add_ledger_balance_box() { + const updateLedgerBalance = (customerSection, balance) => { + let ledgerBalanceDiv = document.getElementById("ledger-balance-box"); + + if (!ledgerBalanceDiv) { + ledgerBalanceDiv = document.createElement("div"); + ledgerBalanceDiv.id = "ledger-balance-box"; + ledgerBalanceDiv.style.marginTop = "10px"; + ledgerBalanceDiv.style.padding = "10px"; + ledgerBalanceDiv.style.border = "1px solid #ccc"; + ledgerBalanceDiv.style.borderRadius = "4px"; + ledgerBalanceDiv.style.backgroundColor = "#f0f8ff"; + ledgerBalanceDiv.style.fontWeight = "bold"; + customerSection.appendChild(ledgerBalanceDiv); + } + + // Update the balance + ledgerBalanceDiv.textContent = `Ledger Balance: Rs. ${balance}`; + }; + + const fetchLedgerBalance = async (customerId) => { + try { + const response = await frappe.call({ + method: "posnext.controllers.queries.get_ledger_balance", + args: { customer: customerId }, + }); + return response.message || "0.00"; // Default to 0.00 if no balance found + } catch (error) { + console.error("Failed to fetch ledger balance:", error); + return "Error"; + } + }; + + // Watch for customer section changes + const observer = new MutationObserver(async () => { + const customerSection = document.querySelector(".customer-section"); + + if (customerSection) { + const resetCustomerBtn = customerSection.querySelector( + ".reset-customer-btn", + ); + const customerId = resetCustomerBtn?.getAttribute("data-customer"); + + if (customerId && customerId !== "undefined") { + const ledgerBalance = await fetchLedgerBalance(customerId); + updateLedgerBalance(customerSection, ledgerBalance); + } else { + updateLedgerBalance(customerSection, "Select a Customer"); + } + } + }); + + observer.observe(document.body, { + childList: true, + subtree: true, + }); + } + + fetch_opening_entry(value) { + return frappe.call( + "posnext.posnext.page.posnext.point_of_sale.check_opening_entry", + { user: frappe.session.user, value: value }, + ); + } + + check_opening_entry(value = "") { + this.fetch_opening_entry(value).then((r) => { + if (r.message.length) { + // assuming only one opening voucher is available for the current user + this.prepare_app_defaults(r.message[0]); + } else { + this.create_opening_voucher(); + } + }); + } + + create_opening_voucher() { + const me = this; + const table_fields = [ + { + fieldname: "mode_of_payment", + fieldtype: "Link", + in_list_view: 1, + label: "Mode of Payment", + options: "Mode of Payment", + reqd: 1, + }, + { + fieldname: "opening_amount", + fieldtype: "Currency", + in_list_view: 1, + label: "Opening Amount", + options: "company:company_currency", + change: function () { + dialog.fields_dict.balance_details.df.data.some((d) => { + if (d.idx == this.doc.idx) { + d.opening_amount = this.value; + dialog.fields_dict.balance_details.grid.refresh(); + return true; } + }); + }, + }, + ]; + const fetch_pos_payment_methods = () => { + const pos_profile = dialog.fields_dict.pos_profile.get_value(); + if (!pos_profile) return; + frappe.db.get_doc("POS Profile", pos_profile).then(({ payments }) => { + dialog.fields_dict.balance_details.df.data = []; + payments.forEach((pay) => { + const { mode_of_payment } = pay; + dialog.fields_dict.balance_details.df.data.push({ + mode_of_payment, + opening_amount: "0", + }); }); - - observer.observe(document.body, { - childList: true, - subtree: true, + dialog.fields_dict.balance_details.grid.refresh(); + }); + }; + const dialog = new frappe.ui.Dialog({ + title: __("Create POS Opening Entry"), + static: true, + fields: [ + { + fieldtype: "Link", + label: __("Company"), + default: frappe.defaults.get_default("company"), + options: "Company", + fieldname: "company", + reqd: 1, + }, + { + fieldtype: "Link", + label: __("POS Profile"), + options: "POS Profile", + fieldname: "pos_profile", + reqd: 1, + get_query: () => pos_profile_query(), + onchange: () => fetch_pos_payment_methods(), + }, + { + fieldname: "balance_details", + fieldtype: "Table", + label: "Opening Balance Details", + cannot_add_rows: false, + in_place_edit: true, + reqd: 1, + data: [], + fields: table_fields, + }, + ], + primary_action: async function ({ + company, + pos_profile, + balance_details, + }) { + if (!balance_details.length) { + frappe.show_alert({ + message: __( + "Please add Mode of payments and opening balance details.", + ), + indicator: "red", + }); + return frappe.utils.play_sound("error"); + } + + // filter balance details for empty rows + balance_details = balance_details.filter((d) => d.mode_of_payment); + + const method = + "posnext.posnext.page.posnext.point_of_sale.create_opening_voucher"; + const res = await frappe.call({ + method, + args: { pos_profile, company, balance_details }, + freeze: true, }); -}; - - fetch_opening_entry(value) { - return frappe.call("posnext.posnext.page.posnext.point_of_sale.check_opening_entry", { "user": frappe.session.user, "value": value }); - } - - check_opening_entry(value = "") { - this.fetch_opening_entry(value).then((r) => { - if (r.message.length) { - // assuming only one opening voucher is available for the current user - this.prepare_app_defaults(r.message[0]); - } else { - this.create_opening_voucher(); - } - }); - } - - create_opening_voucher() { - const me = this; - const table_fields = [ - { - fieldname: "mode_of_payment", fieldtype: "Link", - in_list_view: 1, label: "Mode of Payment", - options: "Mode of Payment", reqd: 1 - }, - { - fieldname: "opening_amount", fieldtype: "Currency", - in_list_view: 1, label: "Opening Amount", - options: "company:company_currency", - change: function () { - dialog.fields_dict.balance_details.df.data.some(d => { - if (d.idx == this.doc.idx) { - d.opening_amount = this.value; - dialog.fields_dict.balance_details.grid.refresh(); - return true; - } - }); - } - } - ]; - const fetch_pos_payment_methods = () => { - const pos_profile = dialog.fields_dict.pos_profile.get_value(); - if (!pos_profile) return; - frappe.db.get_doc("POS Profile", pos_profile).then(({ payments }) => { - dialog.fields_dict.balance_details.df.data = []; - payments.forEach(pay => { - const { mode_of_payment } = pay; - dialog.fields_dict.balance_details.df.data.push({ mode_of_payment, opening_amount: '0' }); - }); - dialog.fields_dict.balance_details.grid.refresh(); - }); - } - const dialog = new frappe.ui.Dialog({ - title: __('Create POS Opening Entry'), - static: true, - fields: [ - { - fieldtype: 'Link', label: __('Company'), default: frappe.defaults.get_default('company'), - options: 'Company', fieldname: 'company', reqd: 1 - }, - { - fieldtype: 'Link', label: __('POS Profile'), - options: 'POS Profile', fieldname: 'pos_profile', reqd: 1, - get_query: () => pos_profile_query(), - onchange: () => fetch_pos_payment_methods() - }, - { - fieldname: "balance_details", - fieldtype: "Table", - label: "Opening Balance Details", - cannot_add_rows: false, - in_place_edit: true, - reqd: 1, - data: [], - fields: table_fields - } - ], - primary_action: async function({ company, pos_profile, balance_details }) { - if (!balance_details.length) { - frappe.show_alert({ - message: __("Please add Mode of payments and opening balance details."), - indicator: 'red' - }) - return frappe.utils.play_sound("error"); - } - - // filter balance details for empty rows - balance_details = balance_details.filter(d => d.mode_of_payment); - - const method = "posnext.posnext.page.posnext.point_of_sale.create_opening_voucher"; - const res = await frappe.call({ method, args: { pos_profile, company, balance_details }, freeze:true }); - !res.exc && me.prepare_app_defaults(res.message); - dialog.hide(); - }, - primary_action_label: __('Submit') - }); - dialog.show(); - const pos_profile_query = () => { - return { - query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query', - filters: { company: dialog.fields_dict.company.get_value() } - } - }; - } - - async prepare_app_defaults(data) { - this.pos_opening = data.name; - this.company = data.company; - this.pos_profile = data.pos_profile; - this.pos_opening_time = data.period_start_date; - this.item_stock_map = {}; - this.settings = {}; - window.current_pos_profile = this.pos_profile - frappe.db.get_value('Stock Settings', undefined, 'allow_negative_stock').then(({ message }) => { - this.allow_negative_stock = flt(message.allow_negative_stock) || false; - }); - - frappe.call({ - method: "posnext.posnext.page.posnext.point_of_sale.get_pos_profile_data", - args: { "pos_profile": this.pos_profile }, - callback: (res) => { - const profile = res.message; - - Object.assign(this.settings, profile); - this.settings.customer_groups = profile.customer_groups.map(group => group.name); - - this.make_app(); - } - }); - } - - set_opening_entry_status() { - this.page.set_title_sub( - `<span class="indicator orange"> + !res.exc && me.prepare_app_defaults(res.message); + dialog.hide(); + }, + primary_action_label: __("Submit"), + }); + dialog.show(); + const pos_profile_query = () => { + return { + query: + "erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query", + filters: { company: dialog.fields_dict.company.get_value() }, + }; + }; + } + + async prepare_app_defaults(data) { + this.pos_opening = data.name; + this.company = data.company; + this.pos_profile = data.pos_profile; + this.pos_opening_time = data.period_start_date; + this.item_stock_map = {}; + this.settings = {}; + window.current_pos_profile = this.pos_profile; + frappe.db + .get_value("Stock Settings", undefined, "allow_negative_stock") + .then(({ message }) => { + this.allow_negative_stock = flt(message.allow_negative_stock) || false; + }); + + frappe.call({ + method: "posnext.posnext.page.posnext.point_of_sale.get_pos_profile_data", + args: { pos_profile: this.pos_profile }, + callback: (res) => { + const profile = res.message; + + Object.assign(this.settings, profile); + this.settings.customer_groups = profile.customer_groups.map( + (group) => group.name, + ); + + this.make_app(); + }, + }); + } + + set_opening_entry_status() { + this.page.set_title_sub( + `<span class="indicator orange"> <a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}"> Opened at ${moment(this.pos_opening_time).format("Do MMMM, h:mma")} </a> - </span>`); - } - - make_app() { - this.prepare_dom(); - this.prepare_components(); - this.prepare_menu(); - this.make_new_invoice(); - } - - prepare_dom() { - this.wrapper.append( - `<div class="point-of-sale-app"></div>` - ); - - this.$components_wrapper = this.wrapper.find('.point-of-sale-app'); - } - - prepare_components() { - this.init_item_selector(); - this.init_item_details(); - this.init_item_cart(); - this.init_payments(); - this.init_recent_order_list(); - this.init_order_summary(); - } - - prepare_menu() { - this.page.clear_menu(); - if(this.settings.custom_show_open_form_view){ - this.page.add_menu_item(__("Open Form View"), this.open_form_view.bind(this), false, 'Ctrl+F'); - } - if(this.settings.custom_show_toggle_recent_orders) { - this.page.add_menu_item(__("Toggle Recent Orders"), this.toggle_recent_order.bind(this), false, 'Ctrl+O'); - } - if(this.settings.custom_show_save_as_draft) { - this.page.add_menu_item(__("Save as Draft"), this.save_draft_invoice.bind(this), false, 'Ctrl+S'); - } - if(this.settings.custom_show_close_the_pos) { - this.page.add_menu_item(__('Close the POS'), this.close_pos.bind(this), false, 'Shift+Ctrl+C'); - } - } - - open_form_view() { - frappe.model.sync(this.frm.doc); - frappe.set_route("Form", this.frm.doc.doctype, this.frm.doc.name); - } - - toggle_recent_order() { - const show = this.recent_order_list.$component.is(':hidden'); - this.toggle_recent_order_list(show); - } - - save_draft_invoice() { - if (!this.$components_wrapper.is(":visible")) return; - console.log(this.frm.doc.items) - if (this.frm.doc.items.length == 0) { - frappe.show_alert({ - message: __("You must add atleast one item to save it as draft."), - indicator:'red' - }); - frappe.utils.play_sound("error"); - return; - } - - this.frm.save(undefined, undefined, undefined, () => { - frappe.show_alert({ - message: __("There was an error saving the document."), - indicator: 'red' - }); - frappe.utils.play_sound("error"); - }).then(() => { - frappe.run_serially([ - () => frappe.dom.freeze(), - () => this.make_new_invoice(true), - () => frappe.dom.unfreeze() - - - ]); - - - - }); - } - - close_pos() { - if (!this.$components_wrapper.is(":visible")) return; - - let voucher = frappe.model.get_new_doc('POS Closing Entry'); - voucher.pos_profile = this.frm.doc.pos_profile; - voucher.user = frappe.session.user; - voucher.company = this.frm.doc.company; - voucher.pos_opening_entry = this.pos_opening; - voucher.period_end_date = frappe.datetime.now_datetime(); - voucher.posting_date = frappe.datetime.now_date(); - voucher.posting_time = frappe.datetime.now_time(); - frappe.set_route('Form', 'POS Closing Entry', voucher.name); - } - - init_item_selector() { - if(this.frm){ - this.frm.doc.set_warehouse = this.settings.warehouse - } - this.item_selector = new posnext.PointOfSale.ItemSelector({ - wrapper: this.$components_wrapper, - pos_profile: this.pos_profile, - settings: this.settings, - reload_status: this.reload_status, - currency: this.settings.currency, - events: { - check_opening_entry: () => this.check_opening_entry(), - item_selected: args => this.on_cart_update(args), - init_item_cart: () => this.init_item_cart(), - init_item_details: () => this.init_item_details(), - change_items: (args) => this.change_items(args), - get_frm: () => this.frm || {} - } - }) - } - change_items(items){ - var me = this - this.frm = items; - this.cart.load_invoice() - } - - init_item_cart() { - this.cart = new posnext.PointOfSale.ItemCart({ - wrapper: this.$components_wrapper, - settings: this.settings, - events: { - get_frm: () => this.frm, - remove_item_from_cart: (item) => { - this.item_details.current_item = item - this.item_details.name = item.name - this.item_details.doctype= item.doctype - - }, - form_updated: (item, field, value) => { - this.item_details.current_item = item - const item_row = frappe.model.get_doc(item.doctype, item.name); - if(field === 'qty' && this.frm.doc.is_return && value >=0){ - frappe.throw("Qty must be negative for return document" ) - } - if (item_row && item_row[field] != value) { - const args = { - field, - value, - item: this.item_details.current_item - }; - return this.on_cart_update(args); - } - - return Promise.resolve(); - }, - cart_item_clicked: (item) => { - - const item_row = this.get_item_from_frm(item); - - if(selected_item && selected_item['name'] == item['name']){ - selected_item = null - } else { - selected_item = item_row - } - this.item_details.toggle_item_details_section(item_row); - }, - - numpad_event: (value, action) => this.update_item_field(value, action), - - checkout: () => this.save_and_checkout(), - - edit_cart: () => this.payment.edit_cart(), - save_draft_invoice: () => this.save_draft_invoice(), - toggle_recent_order: () => this.toggle_recent_order(), - customer_details_updated: (details) => { - this.customer_details = details; - // will add/remove LP payment method - this.payment.render_loyalty_points_payment_mode(); - } - } - }) - } - - init_item_details() { - this.item_details = new posnext.PointOfSale.ItemDetails({ - wrapper: this.$components_wrapper, - settings: this.settings, - events: { - get_frm: () => this.frm, - - toggle_item_selector: (minimize) => { - this.item_selector.resize_selector(minimize); - this.cart.toggle_numpad(minimize); - }, - - form_updated: (item, field, value) => { - const item_row = frappe.model.get_doc(item.doctype, item.name); - if(field === 'qty' && this.frm.doc.is_return && value >=0){ - frappe.throw("Qty must be negative for return document" ) - } - if (item_row && item_row[field] != value) { - const args = { - field, - value, - item: this.item_details.current_item - }; - return this.on_cart_update(args); - } - - return Promise.resolve(); - }, - - highlight_cart_item: (item) => { - const cart_item = this.cart.get_cart_item(item); - this.cart.toggle_item_highlight(cart_item); - }, - - item_field_focused: (fieldname) => { - this.cart.toggle_numpad_field_edit(fieldname); - }, - set_value_in_current_cart_item: (selector, value) => { - this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item); - }, - clone_new_batch_item_in_frm: (batch_serial_map, item) => { - // called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches - // for each unique batch new item row is added in the form & cart - Object.keys(batch_serial_map).forEach(batch => { - const item_to_clone = this.frm.doc.items.find(i => i.name == item.name); - const new_row = this.frm.add_child("items", { ...item_to_clone }); - // update new serialno and batch - new_row.batch_no = batch; - new_row.serial_no = batch_serial_map[batch].join(`\n`); - new_row.qty = batch_serial_map[batch].length; - this.frm.doc.items.forEach(row => { - if (item.item_code === row.item_code) { - this.update_cart_html(row); - } - }); - }) - }, - remove_item_from_cart: () => this.remove_item_from_cart(), - get_item_stock_map: () => this.item_stock_map, - close_item_details: () => { - selected_item = null - this.item_details.toggle_item_details_section(null); - this.cart.prev_action = null; - this.cart.toggle_item_highlight(); - }, - get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse) - } - }); - if(selected_item){ - this.item_details.toggle_item_details_section(selected_item); - } - } - - init_payments() { - this.payment = new posnext.PointOfSale.Payment({ - wrapper: this.$components_wrapper, - settings: this.settings, - events: { - get_frm: () => this.frm || {}, - - get_customer_details: () => this.customer_details || {}, - - toggle_other_sections: (show) => { - if (show) { - this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : ''; - this.item_selector.toggle_component(false); - } else { - this.item_selector.toggle_component(true); - } - }, - - submit_invoice: () => { - this.frm.savesubmit() - .then((r) => { - this.toggle_components(false); - this.order_summary.toggle_component(true); - this.order_summary.load_summary_of(this.frm.doc, true); - frappe.show_alert({ - indicator: 'green', - message: __('POS invoice {0} created succesfully', [r.doc.name]) - }); - }); - } - } - }); - } - - init_recent_order_list() { - this.recent_order_list = new posnext.PointOfSale.PastOrderList({ - wrapper: this.$components_wrapper, - events: { - open_invoice_data: (name) => { - frappe.db.get_doc('Sales Invoice', name).then((doc) => { - this.order_summary.load_summary_of(doc); - }); - }, - reset_summary: () => this.order_summary.toggle_summary_placeholder(true), - previous_screen: () => { - this.recent_order_list.toggle_component(false); - this.cart.load_invoice() - this.item_selector.toggle_component(true) - this.wrapper.find('.past-order-summary').css("display","none"); - }, - - }, - settings: this.settings, - }) - } - - init_order_summary() { - this.order_summary = new posnext.PointOfSale.PastOrderSummary({ - wrapper: this.$components_wrapper, - pos_profile: this.settings, - events: { - get_frm: () => this.frm, - - process_return: (name) => { - this.recent_order_list.toggle_component(false); - frappe.db.get_doc('Sales Invoice', name).then((doc) => { - frappe.run_serially([ - () => this.make_return_invoice(doc), - () => this.cart.load_invoice(), - () => this.item_selector.toggle_component(true) - ]); - }); - }, - edit_order: (name) => { - console.log("Edit Order...") - this.recent_order_list.toggle_component(false); - frappe.run_serially([ - () => this.frm.refresh(name), - () => this.frm.call('reset_mode_of_payments'), - () => this.cart.load_invoice(), - () => this.item_selector.toggle_component(true) - ]); - }, - delete_order: (name) => { - frappe.model.delete_doc(this.frm.doc.doctype, name, () => { - this.recent_order_list.refresh_list(); - }); - }, - new_order: () => { - frappe.run_serially([ - () => frappe.dom.freeze(), - () => this.make_new_invoice(), - () => this.item_selector.toggle_component(true), - () => frappe.dom.unfreeze(), - ]); - } - } - }) - } - - toggle_recent_order_list(show) { - this.toggle_components(!show); - this.recent_order_list.toggle_component(show); - this.order_summary.toggle_component(show); - } - - toggle_components(show) { - this.cart.toggle_component(show); - this.item_selector.toggle_component(show); - - // do not show item details or payment if recent order is toggled off - !show ? (this.item_details.toggle_component(false) || this.payment.toggle_component(false)) : ''; - } - - make_new_invoice(from_held=false) { - if(from_held){ - return frappe.run_serially([ - () => frappe.dom.freeze(), - () => this.make_sales_invoice_frm(), - () => this.set_pos_profile_data(), - () => this.set_pos_profile_status(), - () => this.cart.load_invoice(), - () => frappe.dom.unfreeze(), - () => this.toggle_recent_order(), - ]); - } else { - return frappe.run_serially([ - () => frappe.dom.freeze(), - () => this.make_sales_invoice_frm(), - () => this.set_pos_profile_data(), - () => this.set_pos_profile_status(), - () => this.cart.load_invoice(), - () => frappe.dom.unfreeze(), - ]); - } - - } - - make_sales_invoice_frm() { - const doctype = 'Sales Invoice'; - return new Promise(resolve => { - if (this.frm) { - this.frm = this.get_new_frm(this.frm); - this.frm.doc.items = []; - this.frm.doc.is_pos = 1 - this.frm.doc.set_warehouse = this.settings.warehouse - resolve(); - } else { - frappe.model.with_doctype(doctype, () => { - this.frm = this.get_new_frm(); - this.frm.doc.items = []; - this.frm.doc.is_pos = 1 - this.frm.doc.set_warehouse = this.settings.warehouse - resolve(); - }); - } - }); - } - - get_new_frm(_frm) { - const doctype = 'Sales Invoice'; - const page = $('<div>'); - const frm = _frm || new frappe.ui.form.Form(doctype, page, false); - const name = frappe.model.make_new_doc_and_get_name(doctype, true); - frm.refresh(name); - - return frm; - } - - async make_return_invoice(doc) { - frappe.dom.freeze(); - this.frm = this.get_new_frm(this.frm); - this.frm.doc.items = []; - return frappe.call({ - method: "posnext.posnext.page.posnext.point_of_sale.make_sales_return", - args: { - 'source_name': doc.name, - 'target_doc': this.frm.doc - }, - callback: (r) => { - // console.log(r.message) - frappe.model.sync(r.message); - frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false; - this.set_pos_profile_data().then(() => { - frappe.dom.unfreeze(); - }); - } - }); - } - - set_pos_profile_data() { - if (this.company && !this.frm.doc.company) this.frm.doc.company = this.company; - if ((this.pos_profile && !this.frm.doc.pos_profile) | (this.frm.doc.is_return && this.pos_profile != this.frm.doc.pos_profile)) { - this.frm.doc.pos_profile = this.pos_profile; - } - - if (!this.frm.doc.company) return; - - return this.frm.trigger("set_pos_data"); - } - - set_pos_profile_status() { - this.page.set_indicator(this.pos_profile, "blue"); - } - - async on_cart_update(args) { - // frappe.dom.freeze(); - let item_row = undefined; - try { - let { field, value, item } = args; - item_row = this.get_item_from_frm(item); - const item_row_exists = !$.isEmptyObject(item_row); - - const from_selector = field === 'qty' && value === "+1"; - if (from_selector) - value = flt(item_row.stock_qty) + flt(value); - - if (item_row_exists) { - if (field === 'qty') - value = flt(value); - - if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) { - const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value; - // await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); - } - - if (this.is_current_item_being_edited(item_row) || from_selector) { - await frappe.model.set_value(item_row.doctype, item_row.name, field, value) - // this.update_cart_html(item_row); - } - - } else { - if (!this.frm.doc.customer && !this.settings.custom_mobile_number_based_customer){ - return this.raise_customer_selection_alert(); - } - frappe.flags.ignore_company_party_validation = true - const { item_code, batch_no, serial_no, rate, uom, valuation_rate, custom_item_uoms, custom_logical_rack } = item; - if (!item_code) - return; - - const new_item = { item_code, batch_no, rate, uom, [field]: value }; - if(value){ - new_item['qty'] = value - } - if (serial_no) { - await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); - new_item['serial_no'] = serial_no; - } - - if (field === 'serial_no') - new_item['qty'] = value.split(`\n`).length || 0; - item_row = this.frm.add_child('items', new_item); - - // if (field === 'qty' && value !== 0 && !this.allow_negative_stock) { - // const qty_needed = value * item_row.conversion_factor; - // await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); - // } - - await this.trigger_new_item_events(item_row); - item_row['rate'] = rate - item_row['valuation_rate'] = valuation_rate; - item_row['custom_valuation_rate'] = valuation_rate; - item_row['custom_item_uoms'] = custom_item_uoms; - item_row['custom_logical_rack'] = custom_logical_rack; - // this.update_cart_html(item_row); - if (this.item_details.$component.is(':visible')) - this.edit_item_details_of(item_row); - - if (this.check_serial_batch_selection_needed(item_row) && !this.item_details.$component.is(':visible')) - this.edit_item_details_of(item_row); - } - - } catch (error) { - console.log(error); - } finally { - // frappe.dom.unfreeze(); - - var total_incoming_rate = 0 - this.frm.doc.items.forEach(item => { - total_incoming_rate += (parseFloat(item.valuation_rate) * item.qty) - }); - this.item_selector.update_total_incoming_rate(total_incoming_rate) - - return item_row; // eslint-disable-line no-unsafe-finally - } - } - - raise_customer_selection_alert() { - frappe.dom.unfreeze(); - frappe.show_alert({ - message: __('You must select a customer before adding an item.'), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); - } - - get_item_from_frm({ name, item_code, batch_no, uom, rate }) { - let item_row = null; - - if (name) { - item_row = this.frm.doc.items.find(i => i.name == name); - } else { - // if item is clicked twice from item selector - // then "item_code, batch_no, uom, rate" will help in getting the exact item - // to increase the qty by one - const has_batch_no = (batch_no !== 'null' && batch_no !== null); - const batch_no_check = this.settings.custom_allow_add_new_items_on_new_line ? (has_batch_no && cur_frm.doc.items[i].batch_no === batch_no) : true - for(var i=0;i<cur_frm.doc.items.length;i+=1){ - if(cur_frm.doc.items[i].item_code === item_code && cur_frm.doc.items[i].uom === uom && parseFloat(cur_frm.doc.items[i].rate) === parseFloat(rate)){ - item_row = cur_frm.doc.items[i] - break - } - } - console.log(item_row) - } - return item_row || {}; - } - - edit_item_details_of(item_row) { - this.item_details.toggle_item_details_section(item_row); - } - - is_current_item_being_edited(item_row) { - return item_row.name == this.item_details.current_item.name; - } - - update_cart_html(item_row, remove_item) { - this.cart.update_item_html(item_row, remove_item); - - this.cart.update_totals_section(this.frm); - - } - - check_serial_batch_selection_needed(item_row) { - // right now item details is shown for every type of item. - // if item details is not shown for every item then this fn will be needed - const serialized = item_row.has_serial_no; - const batched = item_row.has_batch_no; - const no_serial_selected = !item_row.serial_no; - const no_batch_selected = !item_row.batch_no; - - if ((serialized && no_serial_selected) || (batched && no_batch_selected) || - (serialized && batched && (no_batch_selected || no_serial_selected))) { - return true; - } - return false; - } - - async trigger_new_item_events(item_row) { - await this.frm.script_manager.trigger('item_code', item_row.doctype, item_row.name); - await this.frm.script_manager.trigger('qty', item_row.doctype, item_row.name); - await this.frm.script_manager.trigger('discount_percentage', item_row.doctype, item_row.name); - } - - async check_stock_availability(item_row, qty_needed, warehouse) { - const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message; - const available_qty = resp[0]; - const is_stock_item = resp[1]; - - frappe.dom.unfreeze(); - const bold_uom = item_row.uom.bold(); - const bold_item_code = item_row.item_code.bold(); - const bold_warehouse = warehouse.bold(); - const bold_available_qty = available_qty.toString().bold() - if (!(available_qty > 0)) { - if (is_stock_item) { - frappe.model.clear_doc(item_row.doctype, item_row.name); - frappe.throw({ - title: __("Not Available"), - message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) - }); - } else { - return; - } - } else if (is_stock_item && available_qty < qty_needed) { - frappe.throw({ - message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.', [bold_item_code, bold_warehouse, bold_available_qty, bold_uom]), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); - } - frappe.dom.freeze(); - } - - async check_serial_no_availablilty(item_code, warehouse, serial_no) { - const method = "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos"; - const args = {filters: { item_code, warehouse }} - const res = await frappe.call({ method, args }); - - if (res.message.includes(serial_no)) { - frappe.throw({ - title: __("Not Available"), - message: __('Serial No: {0} has already been transacted into another Sales Invoice.', [serial_no.bold()]) - }); - } - } - - get_available_stock(item_code, warehouse) { - const me = this; - return frappe.call({ - method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability", - args: { - 'item_code': item_code, - 'warehouse': warehouse, - }, - callback(res) { - if (!me.item_stock_map[item_code]) - me.item_stock_map[item_code] = {}; - me.item_stock_map[item_code][warehouse] = res.message; - } - }); - } - - update_item_field(value, field_or_action) { - if (field_or_action === 'checkout') { - this.item_details.toggle_item_details_section(null); - } else if (field_or_action === 'remove') { - this.remove_item_from_cart(); - } else { - const field_control = this.item_details[`${field_or_action}_control`]; - if (!field_control) return; - field_control.set_focus(); - value != "" && field_control.set_value(value); - } - } - - remove_item_from_cart() { - frappe.dom.freeze(); - const { doctype, name, current_item } = this.item_details; - return frappe.model.set_value(doctype, name, 'qty', 0) - .then(() => { - frappe.model.clear_doc(doctype, name); - this.update_cart_html(current_item, true); - this.item_details.toggle_item_details_section(null); - frappe.dom.unfreeze(); - - var total_incoming_rate = 0 - this.frm.doc.items.forEach(item => { - total_incoming_rate += (parseFloat(item.valuation_rate) * item.qty) - }); - this.item_selector.update_total_incoming_rate(total_incoming_rate) - }) - .catch(e => console.log(e)); - } - - async save_and_checkout() { - if (this.frm.is_dirty()) { - const div = document.getElementById("customer-cart-container2"); - div.style.gridColumn = ""; - let save_error = false; - await this.frm.save(null, null, null, () => save_error = true); - // only move to payment section if save is successful - !save_error && this.payment.checkout(); - // show checkout button on error - save_error && setTimeout(() => { - this.cart.toggle_checkout_btn(true); - }, 300); // wait for save to finish - } else { - this.payment.checkout(); - } - } + </span>`, + ); + } + + make_app() { + this.prepare_dom(); + this.prepare_components(); + this.prepare_menu(); + this.make_new_invoice(); + } + + prepare_dom() { + this.wrapper.append(`<div class="point-of-sale-app"></div>`); + + this.$components_wrapper = this.wrapper.find(".point-of-sale-app"); + } + + prepare_components() { + this.init_item_selector(); + this.init_item_details(); + this.init_item_cart(); + this.init_payments(); + this.init_recent_order_list(); + this.init_order_summary(); + } + + prepare_menu() { + this.page.clear_menu(); + if (this.settings.custom_show_open_form_view) { + this.page.add_menu_item( + __("Open Form View"), + this.open_form_view.bind(this), + false, + "Ctrl+F", + ); + } + if (this.settings.custom_show_toggle_recent_orders) { + this.page.add_menu_item( + __("Toggle Recent Orders"), + this.toggle_recent_order.bind(this), + false, + "Ctrl+O", + ); + } + if (this.settings.custom_show_save_as_draft) { + this.page.add_menu_item( + __("Save as Draft"), + this.save_draft_invoice.bind(this), + false, + "Ctrl+S", + ); + } + if (this.settings.custom_show_close_the_pos) { + this.page.add_menu_item( + __("Close the POS"), + this.close_pos.bind(this), + false, + "Shift+Ctrl+C", + ); + } + } + + open_form_view() { + frappe.model.sync(this.frm.doc); + frappe.set_route("Form", this.frm.doc.doctype, this.frm.doc.name); + } + + toggle_recent_order() { + const show = this.recent_order_list.$component.is(":hidden"); + this.toggle_recent_order_list(show); + } + + save_draft_invoice() { + if (!this.$components_wrapper.is(":visible")) return; + console.log(this.frm.doc.items); + if (this.frm.doc.items.length == 0) { + frappe.show_alert({ + message: __("You must add atleast one item to save it as draft."), + indicator: "red", + }); + frappe.utils.play_sound("error"); + return; + } + + this.frm + .save(undefined, undefined, undefined, () => { + frappe.show_alert({ + message: __("There was an error saving the document."), + indicator: "red", + }); + frappe.utils.play_sound("error"); + }) + .then(() => { + frappe.run_serially([ + () => frappe.dom.freeze(), + () => this.make_new_invoice(true), + () => frappe.dom.unfreeze(), + ]); + }); + } + + close_pos() { + if (!this.$components_wrapper.is(":visible")) return; + + let voucher = frappe.model.get_new_doc("POS Closing Entry"); + voucher.pos_profile = this.frm.doc.pos_profile; + voucher.user = frappe.session.user; + voucher.company = this.frm.doc.company; + voucher.pos_opening_entry = this.pos_opening; + voucher.period_end_date = frappe.datetime.now_datetime(); + voucher.posting_date = frappe.datetime.now_date(); + voucher.posting_time = frappe.datetime.now_time(); + frappe.set_route("Form", "POS Closing Entry", voucher.name); + } + + init_item_selector() { + if (this.frm) { + this.frm.doc.set_warehouse = this.settings.warehouse; + } + this.item_selector = new posnext.PointOfSale.ItemSelector({ + wrapper: this.$components_wrapper, + pos_profile: this.pos_profile, + settings: this.settings, + reload_status: this.reload_status, + currency: this.settings.currency, + events: { + check_opening_entry: () => this.check_opening_entry(), + item_selected: (args) => this.on_cart_update(args), + init_item_cart: () => this.init_item_cart(), + init_item_details: () => this.init_item_details(), + change_items: (args) => this.change_items(args), + get_frm: () => this.frm || {}, + }, + }); + } + change_items(items) { + var me = this; + this.frm = items; + this.cart.load_invoice(); + } + + init_item_cart() { + this.cart = new posnext.PointOfSale.ItemCart({ + wrapper: this.$components_wrapper, + settings: this.settings, + events: { + get_frm: () => this.frm, + remove_item_from_cart: (item) => { + this.item_details.current_item = item; + this.item_details.name = item.name; + this.item_details.doctype = item.doctype; + }, + form_updated: (item, field, value) => { + this.item_details.current_item = item; + const item_row = frappe.model.get_doc(item.doctype, item.name); + if (field === "qty" && this.frm.doc.is_return && value >= 0) { + frappe.throw(__("Qty must be negative for return document")); + } + if (item_row && item_row[field] != value) { + const args = { + field, + value, + item: this.item_details.current_item, + }; + return this.on_cart_update(args); + } + + return Promise.resolve(); + }, + cart_item_clicked: (item) => { + const item_row = this.get_item_from_frm(item); + + if (selected_item && selected_item["name"] == item["name"]) { + selected_item = null; + } else { + selected_item = item_row; + } + this.item_details.toggle_item_details_section(item_row); + }, + + numpad_event: (value, action) => this.update_item_field(value, action), + + checkout: () => this.save_and_checkout(), + + edit_cart: () => this.payment.edit_cart(), + save_draft_invoice: () => this.save_draft_invoice(), + toggle_recent_order: () => this.toggle_recent_order(), + customer_details_updated: (details) => { + this.customer_details = details; + // will add/remove LP payment method + this.payment.render_loyalty_points_payment_mode(); + }, + }, + }); + } + + init_item_details() { + this.item_details = new posnext.PointOfSale.ItemDetails({ + wrapper: this.$components_wrapper, + settings: this.settings, + events: { + get_frm: () => this.frm, + + toggle_item_selector: (minimize) => { + this.item_selector.resize_selector(minimize); + this.cart.toggle_numpad(minimize); + }, + + form_updated: (item, field, value) => { + const item_row = frappe.model.get_doc(item.doctype, item.name); + if (field === "qty" && this.frm.doc.is_return && value >= 0) { + frappe.throw(__("Qty must be negative for return document")); + } + if (item_row && item_row[field] != value) { + const args = { + field, + value, + item: this.item_details.current_item, + }; + return this.on_cart_update(args); + } + + return Promise.resolve(); + }, + + highlight_cart_item: (item) => { + const cart_item = this.cart.get_cart_item(item); + this.cart.toggle_item_highlight(cart_item); + }, + + item_field_focused: (fieldname) => { + this.cart.toggle_numpad_field_edit(fieldname); + }, + set_value_in_current_cart_item: (selector, value) => { + this.cart.update_selector_value_in_cart_item( + selector, + value, + this.item_details.current_item, + ); + }, + clone_new_batch_item_in_frm: (batch_serial_map, item) => { + // called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches + // for each unique batch new item row is added in the form & cart + Object.keys(batch_serial_map).forEach((batch) => { + const item_to_clone = this.frm.doc.items.find( + (i) => i.name == item.name, + ); + const new_row = this.frm.add_child("items", { ...item_to_clone }); + // update new serialno and batch + new_row.batch_no = batch; + new_row.serial_no = batch_serial_map[batch].join(`\n`); + new_row.qty = batch_serial_map[batch].length; + this.frm.doc.items.forEach((row) => { + if (item.item_code === row.item_code) { + this.update_cart_html(row); + } + }); + }); + }, + remove_item_from_cart: () => this.remove_item_from_cart(), + get_item_stock_map: () => this.item_stock_map, + close_item_details: () => { + selected_item = null; + this.item_details.toggle_item_details_section(null); + this.cart.prev_action = null; + this.cart.toggle_item_highlight(); + }, + get_available_stock: (item_code, warehouse) => + this.get_available_stock(item_code, warehouse), + }, + }); + if (selected_item) { + this.item_details.toggle_item_details_section(selected_item); + } + } + + init_payments() { + this.payment = new posnext.PointOfSale.Payment({ + wrapper: this.$components_wrapper, + settings: this.settings, + events: { + get_frm: () => this.frm || {}, + + get_customer_details: () => this.customer_details || {}, + + toggle_other_sections: (show) => { + if (show) { + this.item_details.$component.is(":visible") + ? this.item_details.$component.css("display", "none") + : ""; + this.item_selector.toggle_component(false); + } else { + this.item_selector.toggle_component(true); + } + }, + + submit_invoice: () => { + this.frm.savesubmit().then((r) => { + this.toggle_components(false); + this.order_summary.toggle_component(true); + this.order_summary.load_summary_of(this.frm.doc, true); + frappe.show_alert({ + indicator: "green", + message: __("POS invoice {0} created succesfully", [r.doc.name]), + }); + }); + }, + }, + }); + } + + init_recent_order_list() { + this.recent_order_list = new posnext.PointOfSale.PastOrderList({ + wrapper: this.$components_wrapper, + events: { + open_invoice_data: (name) => { + frappe.db.get_doc("Sales Invoice", name).then((doc) => { + this.order_summary.load_summary_of(doc); + }); + }, + reset_summary: () => + this.order_summary.toggle_summary_placeholder(true), + previous_screen: () => { + this.recent_order_list.toggle_component(false); + this.cart.load_invoice(); + this.item_selector.toggle_component(true); + this.wrapper.find(".past-order-summary").css("display", "none"); + }, + }, + settings: this.settings, + }); + } + + init_order_summary() { + this.order_summary = new posnext.PointOfSale.PastOrderSummary({ + wrapper: this.$components_wrapper, + pos_profile: this.settings, + events: { + get_frm: () => this.frm, + + process_return: (name) => { + this.recent_order_list.toggle_component(false); + frappe.db.get_doc("Sales Invoice", name).then((doc) => { + frappe.run_serially([ + () => this.make_return_invoice(doc), + () => this.cart.load_invoice(), + () => this.item_selector.toggle_component(true), + ]); + }); + }, + edit_order: (name) => { + console.log("Edit Order..."); + this.recent_order_list.toggle_component(false); + frappe.run_serially([ + () => this.frm.refresh(name), + () => this.frm.call("reset_mode_of_payments"), + () => this.cart.load_invoice(), + () => this.item_selector.toggle_component(true), + ]); + }, + delete_order: (name) => { + frappe.model.delete_doc(this.frm.doc.doctype, name, () => { + this.recent_order_list.refresh_list(); + }); + }, + new_order: () => { + frappe.run_serially([ + () => frappe.dom.freeze(), + () => this.make_new_invoice(), + () => this.item_selector.toggle_component(true), + () => frappe.dom.unfreeze(), + ]); + }, + }, + }); + } + + toggle_recent_order_list(show) { + this.toggle_components(!show); + this.recent_order_list.toggle_component(show); + this.order_summary.toggle_component(show); + } + + toggle_components(show) { + this.cart.toggle_component(show); + this.item_selector.toggle_component(show); + + // do not show item details or payment if recent order is toggled off + !show + ? this.item_details.toggle_component(false) || + this.payment.toggle_component(false) + : ""; + } + + make_new_invoice(from_held = false) { + if (from_held) { + return frappe.run_serially([ + () => frappe.dom.freeze(), + () => this.make_sales_invoice_frm(), + () => this.set_pos_profile_data(), + () => this.set_pos_profile_status(), + () => this.cart.load_invoice(), + () => frappe.dom.unfreeze(), + () => this.toggle_recent_order(), + ]); + } else { + return frappe.run_serially([ + () => frappe.dom.freeze(), + () => this.make_sales_invoice_frm(), + () => this.set_pos_profile_data(), + () => this.set_pos_profile_status(), + () => this.cart.load_invoice(), + () => frappe.dom.unfreeze(), + ]); + } + } + + make_sales_invoice_frm() { + const doctype = "Sales Invoice"; + return new Promise((resolve) => { + if (this.frm) { + this.frm = this.get_new_frm(this.frm); + this.frm.doc.items = []; + this.frm.doc.is_pos = 1; + this.frm.doc.set_warehouse = this.settings.warehouse; + resolve(); + } else { + frappe.model.with_doctype(doctype, () => { + this.frm = this.get_new_frm(); + this.frm.doc.items = []; + this.frm.doc.is_pos = 1; + this.frm.doc.set_warehouse = this.settings.warehouse; + resolve(); + }); + } + }); + } + + get_new_frm(_frm) { + const doctype = "Sales Invoice"; + const page = $("<div>"); + const frm = _frm || new frappe.ui.form.Form(doctype, page, false); + const name = frappe.model.make_new_doc_and_get_name(doctype, true); + frm.refresh(name); + + return frm; + } + + async make_return_invoice(doc) { + frappe.dom.freeze(); + this.frm = this.get_new_frm(this.frm); + this.frm.doc.items = []; + return frappe.call({ + method: "posnext.posnext.page.posnext.point_of_sale.make_sales_return", + args: { + source_name: doc.name, + target_doc: this.frm.doc, + }, + callback: (r) => { + // console.log(r.message) + frappe.model.sync(r.message); + frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = + false; + this.set_pos_profile_data().then(() => { + frappe.dom.unfreeze(); + }); + }, + }); + } + + set_pos_profile_data() { + if (this.company && !this.frm.doc.company) + this.frm.doc.company = this.company; + if ( + (this.pos_profile && !this.frm.doc.pos_profile) | + (this.frm.doc.is_return && this.pos_profile != this.frm.doc.pos_profile) + ) { + this.frm.doc.pos_profile = this.pos_profile; + } + + if (!this.frm.doc.company) return; + + return this.frm.trigger("set_pos_data"); + } + + set_pos_profile_status() { + this.page.set_indicator(this.pos_profile, "blue"); + } + + async on_cart_update(args) { + // frappe.dom.freeze(); + let item_row = undefined; + try { + let { field, value, item } = args; + item_row = this.get_item_from_frm(item); + const item_row_exists = !$.isEmptyObject(item_row); + + const from_selector = field === "qty" && value === "+1"; + if (from_selector) value = flt(item_row.stock_qty) + flt(value); + + if (item_row_exists) { + if (field === "qty") value = flt(value); + + if ( + ["qty", "conversion_factor"].includes(field) && + value > 0 && + !this.allow_negative_stock + ) { + const qty_needed = + field === "qty" + ? value * item_row.conversion_factor + : item_row.qty * value; + // await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); + } + + if (this.is_current_item_being_edited(item_row) || from_selector) { + await frappe.model.set_value( + item_row.doctype, + item_row.name, + field, + value, + ); + // this.update_cart_html(item_row); + } + } else { + if ( + !this.frm.doc.customer && + !this.settings.custom_mobile_number_based_customer + ) { + return this.raise_customer_selection_alert(); + } + frappe.flags.ignore_company_party_validation = true; + const { + item_code, + batch_no, + serial_no, + rate, + uom, + valuation_rate, + custom_item_uoms, + custom_logical_rack, + } = item; + if (!item_code) return; + + const new_item = { item_code, batch_no, rate, uom, [field]: value }; + if (value) { + new_item["qty"] = value; + } + if (serial_no) { + await this.check_serial_no_availablilty( + item_code, + this.frm.doc.set_warehouse, + serial_no, + ); + new_item["serial_no"] = serial_no; + } + + if (field === "serial_no") + new_item["qty"] = value.split(`\n`).length || 0; + item_row = this.frm.add_child("items", new_item); + + // if (field === 'qty' && value !== 0 && !this.allow_negative_stock) { + // const qty_needed = value * item_row.conversion_factor; + // await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); + // } + + await this.trigger_new_item_events(item_row); + item_row["rate"] = rate; + item_row["valuation_rate"] = valuation_rate; + item_row["custom_valuation_rate"] = valuation_rate; + item_row["custom_item_uoms"] = custom_item_uoms; + item_row["custom_logical_rack"] = custom_logical_rack; + // this.update_cart_html(item_row); + if (this.item_details.$component.is(":visible")) + this.edit_item_details_of(item_row); + + if ( + this.check_serial_batch_selection_needed(item_row) && + !this.item_details.$component.is(":visible") + ) + this.edit_item_details_of(item_row); + } + } catch (error) { + console.log(error); + } finally { + // frappe.dom.unfreeze(); + + var total_incoming_rate = 0; + this.frm.doc.items.forEach((item) => { + total_incoming_rate += parseFloat(item.valuation_rate) * item.qty; + }); + this.item_selector.update_total_incoming_rate(total_incoming_rate); + + return item_row; // eslint-disable-line no-unsafe-finally + } + } + + raise_customer_selection_alert() { + frappe.dom.unfreeze(); + frappe.show_alert({ + message: __("You must select a customer before adding an item."), + indicator: "orange", + }); + frappe.utils.play_sound("error"); + } + + get_item_from_frm({ name, item_code, batch_no, uom, rate }) { + let item_row = null; + + if (name) { + item_row = this.frm.doc.items.find((i) => i.name == name); + } else { + // if item is clicked twice from item selector + // then "item_code, batch_no, uom, rate" will help in getting the exact item + // to increase the qty by one + const has_batch_no = batch_no !== "null" && batch_no !== null; + const batch_no_check = this.settings + .custom_allow_add_new_items_on_new_line + ? has_batch_no && cur_frm.doc.items[i].batch_no === batch_no // nosemgrep Overrides erpnext code + : true; + // prettier-ignore + for (var i = 0; i < cur_frm.doc.items.length; i += 1) { // nosemgrep + if ( + cur_frm.doc.items[i].item_code === item_code && // nosemgrep Overrides erpnext code + cur_frm.doc.items[i].uom === uom && // nosemgrep Overrides erpnext code + parseFloat(cur_frm.doc.items[i].rate) === parseFloat(rate) // nosemgrep Overrides erpnext code + ) { + item_row = cur_frm.doc.items[i]; // nosemgrep Overrides erpnext code + break; + } + } + console.log(item_row); + } + return item_row || {}; + } + + edit_item_details_of(item_row) { + this.item_details.toggle_item_details_section(item_row); + } + + is_current_item_being_edited(item_row) { + return item_row.name == this.item_details.current_item.name; + } + + update_cart_html(item_row, remove_item) { + this.cart.update_item_html(item_row, remove_item); + + this.cart.update_totals_section(this.frm); + } + + check_serial_batch_selection_needed(item_row) { + // right now item details is shown for every type of item. + // if item details is not shown for every item then this fn will be needed + const serialized = item_row.has_serial_no; + const batched = item_row.has_batch_no; + const no_serial_selected = !item_row.serial_no; + const no_batch_selected = !item_row.batch_no; + + if ( + (serialized && no_serial_selected) || + (batched && no_batch_selected) || + (serialized && batched && (no_batch_selected || no_serial_selected)) + ) { + return true; + } + return false; + } + + async trigger_new_item_events(item_row) { + await this.frm.script_manager.trigger( + "item_code", + item_row.doctype, + item_row.name, + ); + await this.frm.script_manager.trigger( + "qty", + item_row.doctype, + item_row.name, + ); + await this.frm.script_manager.trigger( + "discount_percentage", + item_row.doctype, + item_row.name, + ); + } + + async check_stock_availability(item_row, qty_needed, warehouse) { + const resp = (await this.get_available_stock(item_row.item_code, warehouse)) + .message; + const available_qty = resp[0]; + const is_stock_item = resp[1]; + + frappe.dom.unfreeze(); + const bold_uom = item_row.uom.bold(); + const bold_item_code = item_row.item_code.bold(); + const bold_warehouse = warehouse.bold(); + const bold_available_qty = available_qty.toString().bold(); + if (!(available_qty > 0)) { + if (is_stock_item) { + frappe.model.clear_doc(item_row.doctype, item_row.name); + frappe.throw({ + title: __("Not Available"), + message: __("Item Code: {0} is not available under warehouse {1}.", [ + bold_item_code, + bold_warehouse, + ]), + }); + } else { + return; + } + } else if (is_stock_item && available_qty < qty_needed) { + frappe.throw({ + message: __( + "Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.", + [bold_item_code, bold_warehouse, bold_available_qty, bold_uom], + ), + indicator: "orange", + }); + frappe.utils.play_sound("error"); + } + frappe.dom.freeze(); + } + + async check_serial_no_availablilty(item_code, warehouse, serial_no) { + const method = + "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos"; + const args = { filters: { item_code, warehouse } }; + const res = await frappe.call({ method, args }); + + if (res.message.includes(serial_no)) { + frappe.throw({ + title: __("Not Available"), + message: __( + "Serial No: {0} has already been transacted into another Sales Invoice.", + [serial_no.bold()], + ), + }); + } + } + + get_available_stock(item_code, warehouse) { + const me = this; + return frappe.call({ + method: + "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability", + args: { + item_code: item_code, + warehouse: warehouse, + }, + callback(res) { + if (!me.item_stock_map[item_code]) me.item_stock_map[item_code] = {}; + me.item_stock_map[item_code][warehouse] = res.message; + }, + }); + } + + update_item_field(value, field_or_action) { + if (field_or_action === "checkout") { + this.item_details.toggle_item_details_section(null); + } else if (field_or_action === "remove") { + this.remove_item_from_cart(); + } else { + const field_control = this.item_details[`${field_or_action}_control`]; + if (!field_control) return; + field_control.set_focus(); + value != "" && field_control.set_value(value); + } + } + + remove_item_from_cart() { + frappe.dom.freeze(); + const { doctype, name, current_item } = this.item_details; + return frappe.model + .set_value(doctype, name, "qty", 0) + .then(() => { + frappe.model.clear_doc(doctype, name); + this.update_cart_html(current_item, true); + this.item_details.toggle_item_details_section(null); + frappe.dom.unfreeze(); + + var total_incoming_rate = 0; + this.frm.doc.items.forEach((item) => { + total_incoming_rate += parseFloat(item.valuation_rate) * item.qty; + }); + this.item_selector.update_total_incoming_rate(total_incoming_rate); + }) + .catch((e) => console.log(e)); + } + + async save_and_checkout() { + if (this.frm.is_dirty()) { + const div = document.getElementById("customer-cart-container2"); + div.style.gridColumn = ""; + let save_error = false; + await this.frm.save(null, null, null, () => (save_error = true)); + // only move to payment section if save is successful + !save_error && this.payment.checkout(); + // show checkout button on error + save_error && + setTimeout(() => { + this.cart.toggle_checkout_btn(true); + }, 300); // wait for save to finish + } else { + this.payment.checkout(); + } + } }; diff --git a/posnext/.gitignore b/posnext/.gitignore new file mode 100644 index 0000000..64f3937 --- /dev/null +++ b/posnext/.gitignore @@ -0,0 +1,12 @@ +# Ignore vitepress dist files +www/posnext_*.py +www/404.py +www/posnext_*.html +www/404.html +www/hashmap.json +www/vp-icons.css +www/assets +public/app.*.js +public/posnext_*.js +public/style.*.css +public/chunks \ No newline at end of file diff --git a/posnext/__init__.py b/posnext/__init__.py index f102a9c..3dc1f76 100644 --- a/posnext/__init__.py +++ b/posnext/__init__.py @@ -1 +1 @@ -__version__ = "0.0.1" +__version__ = "0.1.0" diff --git a/posnext/controllers/queries.py b/posnext/controllers/queries.py index a7a810b..24ea2fd 100644 --- a/posnext/controllers/queries.py +++ b/posnext/controllers/queries.py @@ -1,24 +1,25 @@ import frappe -from frappe.utils import nowdate, unique from frappe.desk.reportview import get_filters_cond, get_match_cond +from frappe.utils import unique + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): - doctype = "Customer" - conditions = [] - cust_master_name = frappe.defaults.get_user_default("cust_master_name") + doctype = "Customer" + conditions = [] + cust_master_name = frappe.defaults.get_user_default("cust_master_name") - fields = ["name"] - if cust_master_name != "Customer Name": - fields.append("customer_name") + fields = ["name"] + if cust_master_name != "Customer Name": + fields.append("customer_name") - fields = get_fields(doctype, fields) - searchfields = frappe.get_meta(doctype).get_search_fields() - searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) + fields = get_fields(doctype, fields) + searchfields = frappe.get_meta(doctype).get_search_fields() + searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) - return frappe.db.sql( - """select {fields} from `tabCustomer` + return frappe.db.sql( + """select {fields} from `tabCustomer` where docstatus < 2 and ({scond}) and disabled=0 {fcond} {mcond} @@ -28,24 +29,32 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict= idx desc, name, customer_name limit %(page_len)s offset %(start)s""".format( - **{ - "fields": ", ".join(fields), - "scond": searchfields, - "mcond": get_match_cond(doctype), - "fcond": get_filters_cond(doctype, filters, conditions).replace("%", "%%"), - } - ), - {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, - as_dict=as_dict, - ) + **{ + "fields": ", ".join(fields), + "scond": searchfields, + "mcond": get_match_cond(doctype), + "fcond": get_filters_cond(doctype, filters, conditions).replace( + "%", "%%" + ), + } + ), + { + "txt": "%%%s%%" % txt, + "_txt": txt.replace("%", ""), + "start": start, + "page_len": page_len, + }, + as_dict=as_dict, + ) + def get_fields(doctype, fields=None): - if fields is None: - fields = [] - meta = frappe.get_meta(doctype) - fields.extend(meta.get_search_fields()) + if fields is None: + fields = [] + meta = frappe.get_meta(doctype) + fields.extend(meta.get_search_fields()) - if meta.title_field and meta.title_field.strip() not in fields: - fields.insert(1, meta.title_field.strip()) + if meta.title_field and meta.title_field.strip() not in fields: + fields.insert(1, meta.title_field.strip()) - return unique(fields) \ No newline at end of file + return unique(fields) diff --git a/posnext/doc_events/item.py b/posnext/doc_events/item.py index eefd491..9fe58ad 100644 --- a/posnext/doc_events/item.py +++ b/posnext/doc_events/item.py @@ -1,10 +1,11 @@ -import frappe -from frappe import _ -from frappe.utils.pdf import get_pdf -from frappe.www.printview import get_context import json + +import frappe +from frappe import _ from frappe.utils import now from frappe.utils.file_manager import save_file +from frappe.utils.pdf import get_pdf + def validate_item(doc, method): for x in doc.custom_items: @@ -18,31 +19,27 @@ def get_product_bundle_with_items(item_code): bundle = frappe.db.get_value("Product Bundle", {"new_item_code": item_code}, "name") if not bundle: - return None + return None bundle_doc = frappe.get_doc("Product Bundle", bundle) bundle_items = [] for item in bundle_doc.items: - bundle_items.append({ - "item_code": item.item_code, - "qty": item.qty, - "uom": item.uom - }) + bundle_items.append( + {"item_code": item.item_code, "qty": item.qty, "uom": item.uom} + ) return { "name": bundle_doc.name, "new_item_code": bundle_doc.new_item_code, - "items": bundle_items + "items": bundle_items, } @frappe.whitelist() -def print_barcodes(item_codes): +def print_barcodes(item_codes): if isinstance(item_codes, str): - item_codes = json.loads(item_codes) - items_with_barcodes = [ frappe.get_doc("Item", code) @@ -60,8 +57,8 @@ def print_barcodes(item_codes): url = f"/printview?doctype=Item&name={item_name}&format={print_format}&no_letterhead=1" return {"url": url} - html_content = ''.join( - f'<div>{frappe.get_print("Item", item.name, print_format, doc=item)}</div>' + html_content = "".join( + f"<div>{frappe.get_print('Item', item.name, print_format, doc=item)}</div>" for item in items_with_barcodes ) @@ -73,11 +70,11 @@ def print_barcodes(item_codes): content=pdf_data, dt="Item", dn=items_with_barcodes[0].name, - is_private=0 + is_private=0, ) return { "url": file_doc.file_url, "message": _(f"Generated barcodes for {len(items_with_barcodes)} items."), - "is_pdf": True - } \ No newline at end of file + "is_pdf": True, + } diff --git a/posnext/doc_events/pos_profile.py b/posnext/doc_events/pos_profile.py index dbdcbc5..4ecc494 100644 --- a/posnext/doc_events/pos_profile.py +++ b/posnext/doc_events/pos_profile.py @@ -1,7 +1,8 @@ import frappe +from frappe import _ -def validate_pf(doc,method): +def validate_pf(doc, method): if not doc.custom_edit_rate_and_uom: doc.custom_use_discount_percentage = 0 doc.custom_use_discount_amount = 0 @@ -10,7 +11,7 @@ def validate_pf(doc,method): @frappe.whitelist() def get_pos_profile_branch(pos_profile_name): if not pos_profile_name: - frappe.throw("POS Profile name is required.") + frappe.throw(_("POS Profile name is required.")) branch = frappe.db.get_value("POS Profile", pos_profile_name, "branch") return {"branch": branch} diff --git a/posnext/doc_events/sales_invoice.py b/posnext/doc_events/sales_invoice.py index 8fb317e..464055a 100644 --- a/posnext/doc_events/sales_invoice.py +++ b/posnext/doc_events/sales_invoice.py @@ -1,22 +1,32 @@ import frappe +from frappe import _ -def validate_si(doc,method): +def validate_si(doc, method): if doc.pos_profile: - show_branch = frappe.db.get_value("POS Profile",doc.pos_profile,'show_branch') - if 'branch' not in doc.__dict__ and show_branch==1: - frappe.throw("Create Branch Accounting Dimensions.") + show_branch = frappe.db.get_value("POS Profile", doc.pos_profile, "show_branch") + if "branch" not in doc.__dict__ and show_branch == 1: + frappe.throw(_("Create Branch Accounting Dimensions.")) if doc.is_return and doc.is_pos: doc.update_outstanding_for_self = 0 if doc.payments: doc.payments[0].amount = doc.rounded_total or doc.grand_total else: - mop = frappe.db.get_all("POS Payment Method", {"default": True, "allow_in_returns": True, "parent": doc.pos_profile}, "mode_of_payment") + mop = frappe.db.get_all( + "POS Payment Method", + {"default": True, "allow_in_returns": True, "parent": doc.pos_profile}, + "mode_of_payment", + ) if mop: - doc.append("payments", { - "mode_of_payment": mop[0].mode_of_payment, - "amount": doc.rounded_total or doc.grand_total - }) + doc.append( + "payments", + { + "mode_of_payment": mop[0].mode_of_payment, + "amount": doc.rounded_total or doc.grand_total, + }, + ) + + def create_delivery_note(doc, method): if doc.update_stock: return @@ -32,45 +42,63 @@ def create_delivery_note(doc, method): all_items_sufficient = True for item in doc.items: - available_qty = frappe.db.get_value( - "Bin", {"item_code": item.item_code, "warehouse": item.warehouse}, "actual_qty" - ) or 0 + available_qty = ( + frappe.db.get_value( + "Bin", + {"item_code": item.item_code, "warehouse": item.warehouse}, + "actual_qty", + ) + or 0 + ) - delivery_note.append("items", { - "item_code": item.item_code, - "uom": item.uom, - "qty": item.qty, - "rate": item.rate, - "warehouse": item.warehouse, - "against_sales_invoice": item.parent, - "si_detail": item.name, - "cost_center": item.cost_center - }) + delivery_note.append( + "items", + { + "item_code": item.item_code, + "uom": item.uom, + "qty": item.qty, + "rate": item.rate, + "warehouse": item.warehouse, + "against_sales_invoice": item.parent, + "si_detail": item.name, + "cost_center": item.cost_center, + }, + ) if item.qty > available_qty: all_items_sufficient = False for tax in doc.taxes: - delivery_note.append("taxes", { - "charge_type": tax.charge_type, - "account_head": tax.account_head, - "description": tax.description, - "rate": tax.rate, - "tax_amount": tax.tax_amount, - "total": tax.total, - "cost_center": tax.cost_center - }) + delivery_note.append( + "taxes", + { + "charge_type": tax.charge_type, + "account_head": tax.account_head, + "description": tax.description, + "rate": tax.rate, + "tax_amount": tax.tax_amount, + "total": tax.total, + "cost_center": tax.cost_center, + }, + ) for sp in doc.sales_team: - delivery_note.append("sales_team", { - "sales_person": sp.sales_person, - "allocated_percentage": sp.allocated_percentage - }) + delivery_note.append( + "sales_team", + { + "sales_person": sp.sales_person, + "allocated_percentage": sp.allocated_percentage, + }, + ) delivery_note.save() if all_items_sufficient: delivery_note.submit() - frappe.msgprint(f"Delivery Note {delivery_note.name} submitted as sufficient stock is available.") + frappe.msgprint( + f"Delivery Note {delivery_note.name} submitted as sufficient stock is available." + ) else: - frappe.msgprint(f"Delivery Note {delivery_note.name} saved as draft due to insufficient stock.") + frappe.msgprint( + f"Delivery Note {delivery_note.name} saved as draft due to insufficient stock." + ) diff --git a/posnext/hooks.py b/posnext/hooks.py index 4f87b62..11ebf70 100644 --- a/posnext/hooks.py +++ b/posnext/hooks.py @@ -39,10 +39,12 @@ # page_js = {"page" : "public/js/file.js"} # include js in doctype views -doctype_js = {"POS Profile" : "public/js/pos_profile.js", -"Sales Invoice" : "public/js/sales_invoice.js"} +doctype_js = { + "POS Profile": "public/js/pos_profile.js", + "Sales Invoice": "public/js/sales_invoice.js", +} -doctype_list_js = {"Item" : "public/js/item_list.js"} +doctype_list_js = {"Item": "public/js/item_list.js"} # doctype_tree_js = {"doctype" : "public/js/doctype_tree.js"} # doctype_calendar_js = {"doctype" : "public/js/doctype_calendar.js"} @@ -136,20 +138,16 @@ # Hook on document methods and events doc_events = { - "Item": { - "validate": "posnext.doc_events.item.validate_item" - }, - "Sales Invoice": { - "validate": [ - "posnext.doc_events.sales_invoice.validate_si", - ], - "on_submit": [ - "posnext.doc_events.sales_invoice.create_delivery_note", - ] - }, - "POS Profile": { - "validate": "posnext.doc_events.pos_profile.validate_pf" - } + "Item": {"validate": "posnext.doc_events.item.validate_item"}, + "Sales Invoice": { + "validate": [ + "posnext.doc_events.sales_invoice.validate_si", + ], + "on_submit": [ + "posnext.doc_events.sales_invoice.create_delivery_note", + ], + }, + "POS Profile": {"validate": "posnext.doc_events.pos_profile.validate_pf"}, } # Scheduled Tasks @@ -176,14 +174,14 @@ # Testing # ------- -# before_tests = "posnext.install.before_tests" +before_tests = "posnext.utils.before_tests" # Overriding Methods # ------------------------------ # override_whitelisted_methods = { - "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices": "posnext.overrides.pos_closing_entry.get_pos_invoices", - "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability": "posnext.overrides.pos_invoice.get_stock_availability" + "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices": "posnext.overrides.pos_closing_entry.get_pos_invoices", + "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability": "posnext.overrides.pos_invoice.get_stock_availability", } # # each overriding function accepts a `data` argument; @@ -250,31 +248,22 @@ # "Logging DocType Name": 30 # days to retain logs # } override_doctype_class = { - "Sales Invoice": "posnext.overrides.sales_invoice.PosnextSalesInvoice", - "POS Closing Entry": "posnext.overrides.pos_closing_entry.PosnextPOSClosingEntry", - "POS Invoice Merge Log": "posnext.overrides.pos_invoice_merge_log.PosnextPOSInvoiceMergeLog", + "Sales Invoice": "posnext.overrides.sales_invoice.PosnextSalesInvoice", + "POS Closing Entry": "posnext.overrides.pos_closing_entry.PosnextPOSClosingEntry", + "POS Invoice Merge Log": "posnext.overrides.pos_invoice_merge_log.PosnextPOSInvoiceMergeLog", } fixtures = [ - { - "doctype":"Custom Field", - "filters": [ - [ - "module", - "in", - ["Posnext"] - ] - ] - }, - { - "doctype":"Property Setter", - "filters": [ - [ - "module", - "in", - ["Posnext"] - ] - ] - }, -] \ No newline at end of file + {"doctype": "Custom Field", "filters": [["module", "in", ["Posnext"]]]}, + {"doctype": "Property Setter", "filters": [["module", "in", ["Posnext"]]]}, +] + +standard_help_items = [ + { + "item_label": "POSNext Documentation", + "item_type": "Route", + "route": "/posnext_introduction", + "is_standard": 1, + }, +] diff --git a/posnext/overrides/pos_closing_entry.py b/posnext/overrides/pos_closing_entry.py index c8b5ba0..88d03f9 100644 --- a/posnext/overrides/pos_closing_entry.py +++ b/posnext/overrides/pos_closing_entry.py @@ -1,5 +1,14 @@ import frappe -from frappe.utils import flt, get_datetime +from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import POSClosingEntry +from frappe import _ +from frappe.utils import get_datetime + +from posnext.overrides.pos_invoice_merge_log import ( + consolidate_pos_invoices, + unconsolidate_pos_invoices, +) + + @frappe.whitelist() def get_pos_invoices(start, end, pos_profile, user): print("HEEEEEEEEEEEEEEEEERE") @@ -16,17 +25,15 @@ def get_pos_invoices(start, end, pos_profile, user): as_dict=1, ) - data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)) + start_dt = get_datetime(start) + end_dt = get_datetime(end) + data = [d for d in data if start_dt <= get_datetime(d.timestamp) <= end_dt] + # need to get taxes and payments so can't avoid get_doc data = [frappe.get_doc("Sales Invoice", d.name).as_dict() for d in data] return data -from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import POSClosingEntry -from posnext.overrides.pos_invoice_merge_log import ( - consolidate_pos_invoices, - unconsolidate_pos_invoices, -) class PosnextPOSClosingEntry(POSClosingEntry): def on_submit(self): consolidate_pos_invoices(closing_entry=self) @@ -56,7 +63,9 @@ def validate_pos_invoices(self): # continue if pos_invoice.pos_profile != self.pos_profile: invalid_row.setdefault("msg", []).append( - _("Sales Profile doesn't matches {}").format(frappe.bold(self.pos_profile)) + _("Sales Profile doesn't matches {}").format( + frappe.bold(self.pos_profile) + ) ) if pos_invoice.docstatus != 1: invalid_row.setdefault("msg", []).append( @@ -64,7 +73,9 @@ def validate_pos_invoices(self): ) if pos_invoice.owner != self.user: invalid_row.setdefault("msg", []).append( - _("Sales Invoice isn't created by user {}").format(frappe.bold(self.owner)) + _("Sales Invoice isn't created by user {}").format( + frappe.bold(self.owner) + ) ) if invalid_row.get("msg"): @@ -78,4 +89,4 @@ def validate_pos_invoices(self): for msg in row.get("msg"): error_list.append(_("Row #{}: {}").format(row.get("idx"), msg)) - frappe.throw(error_list, title=_("Invalid Sales Invoices"), as_list=True) \ No newline at end of file + frappe.throw(error_list, title=_("Invalid Sales Invoices"), as_list=True) diff --git a/posnext/overrides/pos_invoice.py b/posnext/overrides/pos_invoice.py index fb8b6ec..251fd10 100644 --- a/posnext/overrides/pos_invoice.py +++ b/posnext/overrides/pos_invoice.py @@ -1,21 +1,23 @@ import frappe -from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_bin_qty,get_bundle_availability +from erpnext.accounts.doctype.pos_invoice.pos_invoice import ( + get_bin_qty, + get_bundle_availability, +) + @frappe.whitelist() def get_stock_availability(item_code, warehouse): - if frappe.db.get_value("Item", item_code, "is_stock_item"): - is_stock_item = True - bin_qty = get_bin_qty(item_code, warehouse) - # pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) - - return bin_qty, is_stock_item - else: - is_stock_item = True - if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}): - return get_bundle_availability(item_code, warehouse), is_stock_item - else: - is_stock_item = False - # Is a service item or non_stock item - return 0, is_stock_item - + if frappe.db.get_value("Item", item_code, "is_stock_item"): + is_stock_item = True + bin_qty = get_bin_qty(item_code, warehouse) + # pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) + return bin_qty, is_stock_item + else: + is_stock_item = True + if frappe.db.exists("Product Bundle", {"name": item_code, "disabled": 0}): + return get_bundle_availability(item_code, warehouse), is_stock_item + else: + is_stock_item = False + # Is a service item or non_stock item + return 0, is_stock_item diff --git a/posnext/overrides/pos_invoice_merge_log.py b/posnext/overrides/pos_invoice_merge_log.py index ce80e4b..4572beb 100644 --- a/posnext/overrides/pos_invoice_merge_log.py +++ b/posnext/overrides/pos_invoice_merge_log.py @@ -1,9 +1,14 @@ +import json + import frappe -from frappe.utils.scheduler import is_scheduler_inactive -from frappe.utils.background_jobs import enqueue, is_job_enqueued -from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import POSInvoiceMergeLog -from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime +from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import ( + POSInvoiceMergeLog, +) from frappe import _ +from frappe.utils import get_time, getdate, nowdate, nowtime +from frappe.utils.background_jobs import enqueue, is_job_enqueued +from frappe.utils.scheduler import is_scheduler_inactive + class PosnextPOSInvoiceMergeLog(POSInvoiceMergeLog): def serial_and_batch_bundle_reference_for_pos_invoice(self): @@ -11,14 +16,22 @@ def serial_and_batch_bundle_reference_for_pos_invoice(self): pos_invoice = frappe.get_doc("Sales Invoice", d.pos_invoice) for table_name in ["items", "packed_items"]: pos_invoice.set_serial_and_batch_bundle(table_name) + def on_cancel(self): - pos_invoice_docs = [frappe.get_cached_doc("Sales Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [ + frappe.get_cached_doc("Sales Invoice", d.pos_invoice) + for d in self.pos_invoices + ] self.update_pos_invoices(pos_invoice_docs) self.serial_and_batch_bundle_reference_for_pos_invoice() self.cancel_linked_invoices() + def on_submit(self): - pos_invoice_docs = [frappe.get_cached_doc("Sales Invoice", d.pos_invoice) for d in self.pos_invoices] + pos_invoice_docs = [ + frappe.get_cached_doc("Sales Invoice", d.pos_invoice) + for d in self.pos_invoices + ] returns = [d for d in pos_invoice_docs if d.get("is_return") == 1] sales = [d for d in pos_invoice_docs if d.get("is_return") == 0] @@ -32,45 +45,60 @@ def on_submit(self): self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note) + def validate_pos_invoice_status(self): for d in self.pos_invoices: status, docstatus, is_return, return_against = frappe.db.get_value( - "Sales Invoice", d.pos_invoice, ["status", "docstatus", "is_return", "return_against"] + "Sales Invoice", + d.pos_invoice, + ["status", "docstatus", "is_return", "return_against"], ) bold_pos_invoice = frappe.bold(d.pos_invoice) bold_status = frappe.bold(status) if docstatus != 1: frappe.throw( - _("Row #{}: Sales Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice) + _("Row #{}: Sales Invoice {} is not submitted yet").format( + d.idx, bold_pos_invoice + ) ) if status == "Consolidated": frappe.throw( - _("Row #{}: Sales Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status) + _("Row #{}: Sales Invoice {} has been {}").format( + d.idx, bold_pos_invoice, bold_status + ) ) if ( - is_return - and return_against - and return_against not in [d.pos_invoice for d in self.pos_invoices] + is_return + and return_against + and return_against not in [d.pos_invoice for d in self.pos_invoices] ): bold_return_against = frappe.bold(return_against) - return_against_status = frappe.db.get_value("Sales Invoice", return_against, "status") + return_against_status = frappe.db.get_value( + "Sales Invoice", return_against, "status" + ) if return_against_status != "Consolidated": # if return entry is not getting merged in the current pos closing and if it is not consolidated bold_unconsolidated = frappe.bold("not Consolidated") - msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format( - d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated + msg = _( + "Row #{}: Original Invoice {} of return invoice {} is {}." + ).format( + d.idx, + bold_return_against, + bold_pos_invoice, + bold_unconsolidated, ) msg += " " msg += _( "Original invoice should be consolidated before or along with the return invoice." ) msg += "<br><br>" - msg += _("You can add original invoice {} manually to proceed.").format( - bold_return_against - ) + msg += _( + "You can add original invoice {} manually to proceed." + ).format(bold_return_against) frappe.throw(msg) + def split_invoices(invoices): """ Splits invoices into multiple groups @@ -103,18 +131,24 @@ def split_invoices(invoices): if not item.serial_no and not item.serial_and_batch_bundle: continue - return_against_is_added = any(d for d in _invoices if d.pos_invoice == pos_invoice.return_against) + return_against_is_added = any( + d for d in _invoices if d.pos_invoice == pos_invoice.return_against + ) if return_against_is_added: break return_against_is_consolidated = ( - frappe.db.get_value("POS Invoice", pos_invoice.return_against, "status", cache=True) + frappe.db.get_value( + "POS Invoice", pos_invoice.return_against, "status", cache=True + ) == "Consolidated" ) if return_against_is_consolidated: break - pos_invoice_row = [d for d in invoices if d.pos_invoice == pos_invoice.return_against] + pos_invoice_row = [ + d for d in invoices if d.pos_invoice == pos_invoice.return_against + ] _invoices.append(pos_invoice_row) special_invoices.append(pos_invoice.return_against) break @@ -123,6 +157,7 @@ def split_invoices(invoices): return _invoices + def consolidate_pos_invoices(pos_invoices=None, closing_entry=None): invoices = pos_invoices or (closing_entry and closing_entry.get("pos_transactions")) if frappe.flags.in_test and not invoices: @@ -132,10 +167,15 @@ def consolidate_pos_invoices(pos_invoices=None, closing_entry=None): if len(invoices) >= 10 and closing_entry: closing_entry.set_status(update=True, status="Queued") - enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry) + enqueue_job( + create_merge_logs, + invoice_by_customer=invoice_by_customer, + closing_entry=closing_entry, + ) else: create_merge_logs(invoice_by_customer, closing_entry) + def get_all_unconsolidated_invoices(): filters = { "consolidated_invoice": ["in", ["", None]], @@ -157,6 +197,7 @@ def get_all_unconsolidated_invoices(): return pos_invoices + def get_invoice_customer_map(pos_invoices): # pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Customer 2' : [{}] } pos_invoice_customer_map = {} @@ -170,14 +211,20 @@ def get_invoice_customer_map(pos_invoices): def unconsolidate_pos_invoices(closing_entry): merge_logs = frappe.get_all( - "POS Invoice Merge Log", filters={"pos_closing_entry": closing_entry.name}, pluck="name" + "POS Invoice Merge Log", + filters={"pos_closing_entry": closing_entry.name}, + pluck="name", ) if len(merge_logs) >= 10: closing_entry.set_status(update=True, status="Queued") - enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry) + enqueue_job( + cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry + ) else: cancel_merge_logs(merge_logs, closing_entry) + + def cancel_merge_logs(merge_logs, closing_entry=None): try: for log in merge_logs: @@ -201,22 +248,29 @@ def cancel_merge_logs(merge_logs, closing_entry=None): raise finally: - frappe.db.commit() + # frappe.db.commit() frappe.publish_realtime("closing_process_complete", user=frappe.session.user) + def create_merge_logs(invoice_by_customer, closing_entry=None): try: for customer, invoices in invoice_by_customer.items(): for _invoices in split_invoices(invoices): merge_log = frappe.new_doc("POS Invoice Merge Log") merge_log.posting_date = ( - getdate(closing_entry.get("posting_date")) if closing_entry else nowdate() + getdate(closing_entry.get("posting_date")) + if closing_entry + else nowdate() ) merge_log.posting_time = ( - get_time(closing_entry.get("posting_time")) if closing_entry else nowtime() + get_time(closing_entry.get("posting_time")) + if closing_entry + else nowtime() ) merge_log.customer = customer - merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None + merge_log.pos_closing_entry = ( + closing_entry.get("name") if closing_entry else None + ) merge_log.set("pos_invoices", _invoices) merge_log.save(ignore_permissions=True) merge_log.submit() @@ -238,8 +292,10 @@ def create_merge_logs(invoice_by_customer, closing_entry=None): raise finally: - frappe.db.commit() + # frappe.db.commit() frappe.publish_realtime("closing_process_complete", user=frappe.session.user) + + def enqueue_job(job, **kwargs): check_scheduler_status() @@ -264,13 +320,17 @@ def enqueue_job(job, **kwargs): frappe.msgprint(msg, alert=1) + def check_scheduler_status(): if is_scheduler_inactive() and not frappe.flags.in_test: - frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive")) + frappe.throw( + _("Scheduler is inactive. Cannot enqueue job."), + title=_("Scheduler Inactive"), + ) def get_error_message(message) -> str: - try: - return message["message"] - except Exception: - return str(message) \ No newline at end of file + try: + return message["message"] + except Exception: + return str(message) diff --git a/posnext/overrides/sales_invoice.py b/posnext/overrides/sales_invoice.py index 53094d7..71cb1eb 100644 --- a/posnext/overrides/sales_invoice.py +++ b/posnext/overrides/sales_invoice.py @@ -1,36 +1,46 @@ import frappe from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( SalesInvoice, - update_multi_mode_option + update_multi_mode_option, ) from frappe import _ -from frappe.utils import add_days, cint, cstr, flt, formatdate, get_link_to_form, getdate, nowdate +from frappe.utils import flt -from six import iteritems -from frappe import msgprint -class PosnextSalesInvoice(SalesInvoice): +class PosnextSalesInvoice(SalesInvoice): @frappe.whitelist() def reset_mode_of_payments(self): if self.pos_profile: pos_profile = frappe.get_cached_doc("POS Profile", self.pos_profile) update_multi_mode_option(self, pos_profile) self.paid_amount = 0 + def validate_pos(self): if self.is_return: - self.paid_amount = self.paid_amount if not self.is_pos else self.base_rounded_total + self.paid_amount = ( + self.paid_amount if not self.is_pos else self.base_rounded_total + ) self.outstanding_amount = 0 for x in self.payments: - x.amount = self.paid_amount + x.amount = self.paid_amount x.amount = x.amount * -1 if x.amount > 0 else x.amount invoice_total = self.rounded_total or self.grand_total - if flt(self.paid_amount) + flt(self.write_off_amount) - abs(flt(invoice_total)) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)): - frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total")) + if flt(self.paid_amount) + flt(self.write_off_amount) - abs( + flt(invoice_total) + ) > 1.0 / (10.0 ** (self.precision("grand_total") + 1.0)): + frappe.throw( + _( + "Paid amount + Write Off Amount can not be greater than Grand Total" + ) + ) + def validate_pos_paid_amount(self): if len(self.payments) == 0 and self.is_pos: - custom_show_credit_sales = frappe.get_value("POS Profile",self.pos_profile,"custom_show_credit_sales") + custom_show_credit_sales = frappe.get_value( + "POS Profile", self.pos_profile, "custom_show_credit_sales" + ) if not custom_show_credit_sales: - frappe.throw(_("At least one mode of payment is required for POS invoice.")) - - + frappe.throw( + _("At least one mode of payment is required for POS invoice.") + ) diff --git a/posnext/posnext/doctype/alternative_items/alternative_items.py b/posnext/posnext/doctype/alternative_items/alternative_items.py index 7955cb3..cfc3468 100644 --- a/posnext/posnext/doctype/alternative_items/alternative_items.py +++ b/posnext/posnext/doctype/alternative_items/alternative_items.py @@ -6,4 +6,4 @@ class AlternativeItems(Document): - pass + pass diff --git a/posnext/posnext/doctype/logical_rack/logical_rack.py b/posnext/posnext/doctype/logical_rack/logical_rack.py index c34821e..36b81d5 100644 --- a/posnext/posnext/doctype/logical_rack/logical_rack.py +++ b/posnext/posnext/doctype/logical_rack/logical_rack.py @@ -6,4 +6,4 @@ class LogicalRack(Document): - pass + pass diff --git a/posnext/posnext/doctype/logical_rack/test_logical_rack.py b/posnext/posnext/doctype/logical_rack/test_logical_rack.py index 1152271..186c290 100644 --- a/posnext/posnext/doctype/logical_rack/test_logical_rack.py +++ b/posnext/posnext/doctype/logical_rack/test_logical_rack.py @@ -6,4 +6,4 @@ class TestLogicalRack(FrappeTestCase): - pass + pass diff --git a/posnext/posnext/doctype/pos_profile_whatsapp_field_names/pos_profile_whatsapp_field_names.py b/posnext/posnext/doctype/pos_profile_whatsapp_field_names/pos_profile_whatsapp_field_names.py index dacfb54..30ab987 100644 --- a/posnext/posnext/doctype/pos_profile_whatsapp_field_names/pos_profile_whatsapp_field_names.py +++ b/posnext/posnext/doctype/pos_profile_whatsapp_field_names/pos_profile_whatsapp_field_names.py @@ -6,4 +6,4 @@ class POSProfileWhatsappFieldNames(Document): - pass + pass diff --git a/posnext/posnext/page/posnext/point_of_sale.py b/posnext/posnext/page/posnext/point_of_sale.py index 45b8478..c10e730 100644 --- a/posnext/posnext/page/posnext/point_of_sale.py +++ b/posnext/posnext/page/posnext/point_of_sale.py @@ -6,137 +6,187 @@ from typing import Dict, Optional import frappe -from frappe.utils import cint -from frappe.utils.nestedset import get_root_of - from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_stock_availability -from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes, get_item_groups +from erpnext.accounts.doctype.pos_profile.pos_profile import ( + get_child_nodes, + get_item_groups, +) from erpnext.stock.utils import scan_barcode +from frappe.utils import cint +from frappe.utils.file_manager import save_file +from frappe.utils.nestedset import get_root_of +from frappe.utils.pdf import get_pdf -def search_by_term(search_term,custom_show_alternative_item_for_pos_search, warehouse, price_list): - result = search_for_serial_or_batch_or_barcode_number(search_term) or {} - - item_code = result.get("item_code", "") - serial_no = result.get("serial_no", "") - batch_no = result.get("batch_no", "") - barcode = result.get("barcode", "") - - if not result: - return - print("RESSSSULT") - print(result) - item_doc = frappe.get_doc("Item", item_code) - - if not item_doc: - return - item = { - "barcode": barcode, - "batch_no": batch_no, - "description": item_doc.description, - "is_stock_item": item_doc.is_stock_item, - "item_code": item_doc.name, - "item_image": item_doc.image, - "item_name": item_doc.item_name, - "serial_no": serial_no, - "stock_uom": item_doc.stock_uom, - "uom": item_doc.stock_uom, - "item_uoms": frappe.db.get_all("UOM Conversion Detail", {"parent": item_doc.item_code}, ["uom"], pluck="uom") - } - - if barcode: - barcode_info = next(filter(lambda x: x.barcode == barcode, item_doc.get("barcodes", [])), None) - if barcode_info and barcode_info.uom: - uom = next(filter(lambda x: x.uom == barcode_info.uom, item_doc.uoms), {}) - item.update( - { - "uom": barcode_info.uom, - "conversion_factor": uom.get("conversion_factor", 1), - } - ) - - item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) - item_stock_qty = item_stock_qty // item.get("conversion_factor", 1) - item.update({"actual_qty": item_stock_qty}) - - price = frappe.get_list( - doctype="Item Price", - filters={ - "price_list": price_list, - "item_code": item_code, - "batch_no": batch_no, - }, - fields=["uom", "currency", "price_list_rate", "batch_no"], - ) - - def __sort(p): - p_uom = p.get("uom") - - if p_uom == item.get("uom"): - return 0 - elif p_uom == item.get("stock_uom"): - return 1 - else: - return 2 - - # sort by fallback preference. always pick exact uom match if available - price = sorted(price, key=__sort) - - if len(price) > 0: - p = price.pop(0) - item.update( - { - "currency": p.get("currency"), - "price_list_rate": p.get("price_list_rate"), - } - ) - - - return {"items": [item]} +def search_by_term( + search_term, custom_show_alternative_item_for_pos_search, warehouse, price_list +): + result = search_for_serial_or_batch_or_barcode_number(search_term) or {} + + item_code = result.get("item_code", "") + serial_no = result.get("serial_no", "") + batch_no = result.get("batch_no", "") + barcode = result.get("barcode", "") + + if not result: + return + print("RESSSSULT") + print(result) + item_doc = frappe.get_doc("Item", item_code) + + if not item_doc: + return + item = { + "barcode": barcode, + "batch_no": batch_no, + "description": item_doc.description, + "is_stock_item": item_doc.is_stock_item, + "item_code": item_doc.name, + "item_image": item_doc.image, + "item_name": item_doc.item_name, + "serial_no": serial_no, + "stock_uom": item_doc.stock_uom, + "uom": item_doc.stock_uom, + "item_uoms": frappe.db.get_all( + "UOM Conversion Detail", + {"parent": item_doc.item_code}, + ["uom"], + pluck="uom", + ), + } + + if barcode: + barcode_info = next( + (x for x in item_doc.get("barcodes", []) if x.barcode == barcode), None + ) + if barcode_info and barcode_info.uom: + uom = next((x for x in item_doc.uoms if x.uom == barcode_info.uom), {}) + item.update( + { + "uom": barcode_info.uom, + "conversion_factor": uom.get("conversion_factor", 1), + } + ) + + item_stock_qty, is_stock_item = get_stock_availability(item_code, warehouse) + item_stock_qty = item_stock_qty // item.get("conversion_factor", 1) + item.update({"actual_qty": item_stock_qty}) + + price = frappe.get_list( + doctype="Item Price", + filters={ + "price_list": price_list, + "item_code": item_code, + "batch_no": batch_no, + }, + fields=["uom", "currency", "price_list_rate", "batch_no"], + ) + + def __sort(p): + p_uom = p.get("uom") + + if p_uom == item.get("uom"): + return 0 + elif p_uom == item.get("stock_uom"): + return 1 + else: + return 2 + + # sort by fallback preference. always pick exact uom match if available + price = sorted(price, key=__sort) + + if len(price) > 0: + p = price.pop(0) + item.update( + { + "currency": p.get("currency"), + "price_list_rate": p.get("price_list_rate"), + } + ) + + return {"items": [item]} @frappe.whitelist() def get_items(start, page_length, price_list, item_group, pos_profile, search_term=""): - warehouse, hide_unavailable_items,custom_show_last_incoming_rate, custom_show_alternative_item_for_pos_search,custom_show_logical_rack, custom_skip_stock_transaction_validation = frappe.db.get_value( - "POS Profile", pos_profile, ["warehouse", "hide_unavailable_items","custom_show_last_incoming_rate","custom_show_alternative_item_for_pos_search","custom_show_logical_rack", "custom_skip_stock_transaction_validation"] - ) - - result = [] - - if search_term: - result = search_by_term(search_term,custom_show_alternative_item_for_pos_search, warehouse, price_list) or [] - if result: - return result - alt_items = [] - if custom_show_alternative_item_for_pos_search: - alt_items = frappe.db.sql(""" SELECT * FROM `tabAlternative Items` - WHERE parent like %s or parent_item_name like %s or parent_item_description like %s or parent_oem_part_number like %s""",('%' + search_term + '%','%' + search_term + '%','%' + search_term + '%','%' + search_term + '%'),as_dict=1) - if not frappe.db.exists("Item Group", item_group): - item_group = get_root_of("Item Group") - - condition = get_conditions(search_term,alt_items) - condition += get_item_group_condition(pos_profile) - - lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"]) - - bin_join_selection, bin_join_condition,bin_valuation_rate,bin_join_condition_valuation = "", "","","" - if not custom_skip_stock_transaction_validation: - if hide_unavailable_items: - bin_join_selection = ", `tabBin` bin" - bin_join_condition = ( - "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" - ) - - if not bin_join_selection: - bin_join_selection = ", `tabBin` bin" - bin_valuation_rate = "bin.valuation_rate, bin.valuation_rate as custom_valuation_rate," - - bin_join_condition_valuation = ( - "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name" - ) - - items_data = frappe.db.sql( - """ + ( + warehouse, + hide_unavailable_items, + custom_show_last_incoming_rate, + custom_show_alternative_item_for_pos_search, + custom_show_logical_rack, + custom_skip_stock_transaction_validation, + ) = frappe.db.get_value( + "POS Profile", + pos_profile, + [ + "warehouse", + "hide_unavailable_items", + "custom_show_last_incoming_rate", + "custom_show_alternative_item_for_pos_search", + "custom_show_logical_rack", + "custom_skip_stock_transaction_validation", + ], + ) + + result = [] + + if search_term: + result = ( + search_by_term( + search_term, + custom_show_alternative_item_for_pos_search, + warehouse, + price_list, + ) + or [] + ) + if result: + return result + alt_items = [] + if custom_show_alternative_item_for_pos_search: + alt_items = frappe.db.sql( + """ SELECT * FROM `tabAlternative Items` + WHERE parent like %s or parent_item_name like %s or parent_item_description like %s or parent_oem_part_number like %s""", + ( + "%" + search_term + "%", + "%" + search_term + "%", + "%" + search_term + "%", + "%" + search_term + "%", + ), + as_dict=1, + ) + if not frappe.db.exists("Item Group", item_group): + item_group = get_root_of("Item Group") + + condition = get_conditions(search_term, alt_items) + condition += get_item_group_condition(pos_profile) + + lft, rgt = frappe.db.get_value("Item Group", item_group, ["lft", "rgt"]) + + ( + bin_join_selection, + bin_join_condition, + bin_valuation_rate, + bin_join_condition_valuation, + ) = "", "", "", "" + if not custom_skip_stock_transaction_validation: + if hide_unavailable_items: + bin_join_selection = ", `tabBin` bin" + bin_join_condition = "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name AND bin.actual_qty > 0" + + if not bin_join_selection: + bin_join_selection = ", `tabBin` bin" + bin_valuation_rate = ( + "bin.valuation_rate, bin.valuation_rate as custom_valuation_rate," + ) + + bin_join_condition_valuation = ( + "AND bin.warehouse = %(warehouse)s AND bin.item_code = item.name" + ) + + items_data = frappe.db.sql( + """ SELECT item.name AS item_code, item.custom_oem_part_number, @@ -161,217 +211,240 @@ def get_items(start, page_length, price_list, item_group, pos_profile, search_te item.name asc LIMIT {page_length} offset {start}""".format( - start=cint(start), - page_length=cint(page_length), - lft=cint(lft), - rgt=cint(rgt), - condition=condition, - bin_join_selection=bin_join_selection, - bin_valuation_rate=bin_valuation_rate, - bin_join_condition=bin_join_condition, - bin_join_condition_valuation=bin_join_condition_valuation - ), - {"warehouse": warehouse}, - as_dict=1, - ) - - # return (empty) list if there are no results - if not items_data: - return result - - for item in items_data: - if custom_show_logical_rack: - rack = frappe.db.sql(""" SELECT * FROM `tabLogical Rack` WHERE item=%s and pos_profile=%s """,(item.item_code,pos_profile),as_dict=1) - if len(rack) > 0: - item['rack'] = rack[0].rack_id - item['custom_logical_rack'] = rack[0].rack_id - uoms = frappe.get_doc("Item", item.item_code).get("uoms", []) - item["custom_item_uoms"] = frappe.db.get_all("UOM Conversion Detail", {"parent": item.item_code}, ["uom"], pluck="uom") - item.actual_qty, _ = get_stock_availability(item.item_code, warehouse) - item.uom = item.stock_uom - item_price = frappe.get_all( - "Item Price", - fields=["price_list_rate", "currency", "uom", "batch_no"], - filters={ - "price_list": price_list, - "item_code": item.item_code, - "selling": True, - }, - order_by="creation desc", - limit=1 - ) - - if not item_price: - result.append(item) - - for price in item_price: - uom = next(filter(lambda x: x.uom == price.uom, uoms), {}) - - if price.uom != item.stock_uom and uom and uom.conversion_factor: - item.actual_qty = item.actual_qty // uom.conversion_factor - - result.append( - { - **item, - "price_list_rate": price.get("price_list_rate"), - "currency": price.get("currency"), - "uom": price.uom or item.uom, - "batch_no": price.batch_no, - } - ) - return {"items": result} + start=cint(start), + page_length=cint(page_length), + lft=cint(lft), + rgt=cint(rgt), + condition=condition, + bin_join_selection=bin_join_selection, + bin_valuation_rate=bin_valuation_rate, + bin_join_condition=bin_join_condition, + bin_join_condition_valuation=bin_join_condition_valuation, + ), + {"warehouse": warehouse}, + as_dict=1, + ) + + # return (empty) list if there are no results + if not items_data: + return result + + for item in items_data: + if custom_show_logical_rack: + rack = frappe.db.sql( + """ SELECT * FROM `tabLogical Rack` WHERE item=%s and pos_profile=%s """, + (item.item_code, pos_profile), + as_dict=1, + ) + if len(rack) > 0: + item["rack"] = rack[0].rack_id + item["custom_logical_rack"] = rack[0].rack_id + uoms = frappe.get_doc("Item", item.item_code).get("uoms", []) + item["custom_item_uoms"] = frappe.db.get_all( + "UOM Conversion Detail", {"parent": item.item_code}, ["uom"], pluck="uom" + ) + item.actual_qty, _ = get_stock_availability(item.item_code, warehouse) + item.uom = item.stock_uom + item_price = frappe.get_all( + "Item Price", + fields=["price_list_rate", "currency", "uom", "batch_no"], + filters={ + "price_list": price_list, + "item_code": item.item_code, + "selling": True, + }, + order_by="creation desc", + limit=1, + ) + + if not item_price: + result.append(item) + + for price in item_price: + uom = next((x for x in uoms if x.uom == price.uom), {}) + + if price.uom != item.stock_uom and uom and uom.conversion_factor: + item.actual_qty = item.actual_qty // uom.conversion_factor + + result.append( + { + **item, + "price_list_rate": price.get("price_list_rate"), + "currency": price.get("currency"), + "uom": price.uom or item.uom, + "batch_no": price.batch_no, + } + ) + return {"items": result} @frappe.whitelist() -def search_for_serial_or_batch_or_barcode_number(search_value: str) -> Dict[str, Optional[str]]: - return scan_barcode(search_value) +def search_for_serial_or_batch_or_barcode_number( + search_value: str, +) -> Dict[str, Optional[str]]: + return scan_barcode(search_value) -def get_conditions(search_term,new_items): - condition = "(" +def get_conditions(search_term, new_items): + condition = "(" - condition += """(item.name like {search_term} + condition += """(item.name like {search_term} or item.item_name like {search_term} or item.description like {search_term} or item.custom_oem_part_number like {search_term}) """.format( - search_term=frappe.db.escape("%" + search_term + "%") - ) - if len(new_items) > 0: - for xx in new_items: - condition += """or (item.name like {xx} + search_term=frappe.db.escape("%" + search_term + "%") + ) + if len(new_items) > 0: + for xx in new_items: + condition += """or (item.name like {xx} or item.item_name like {xx}) """.format( - xx=frappe.db.escape("%" + xx.item + "%") - ) - condition += add_search_fields_condition(search_term) - condition += ")" + xx=frappe.db.escape("%" + xx.item + "%") + ) + condition += add_search_fields_condition(search_term) + condition += ")" - return condition + return condition def add_search_fields_condition(search_term): - condition = "" - search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"]) - if search_fields: - for field in search_fields: - condition += " or item.`{0}` like {1}".format( - field["fieldname"], frappe.db.escape("%" + search_term + "%") - ) - return condition + condition = "" + search_fields = frappe.get_all("POS Search Fields", fields=["fieldname"]) + if search_fields: + for field in search_fields: + condition += " or item.`{0}` like {1}".format( + field["fieldname"], frappe.db.escape("%" + search_term + "%") + ) + return condition def get_item_group_condition(pos_profile): - cond = "and 1=1" - item_groups = get_item_groups(pos_profile) - if item_groups: - cond = "and item.item_group in (%s)" % (", ".join(["%s"] * len(item_groups))) + cond = "and 1=1" + item_groups = get_item_groups(pos_profile) + if item_groups: + cond = "and item.item_group in (%s)" % (", ".join(["%s"] * len(item_groups))) - return cond % tuple(item_groups) + return cond % tuple(item_groups) @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def item_group_query(doctype, txt, searchfield, start, page_len, filters): - item_groups = [] - cond = "1=1" - pos_profile = filters.get("pos_profile") + item_groups = [] + cond = "1=1" + pos_profile = filters.get("pos_profile") - if pos_profile: - item_groups = get_item_groups(pos_profile) + if pos_profile: + item_groups = get_item_groups(pos_profile) - if item_groups: - cond = "name in (%s)" % (", ".join(["%s"] * len(item_groups))) - cond = cond % tuple(item_groups) + if item_groups: + cond = "name in (%s)" % (", ".join(["%s"] * len(item_groups))) + cond = cond % tuple(item_groups) - return frappe.db.sql( - """ select distinct name from `tabItem Group` + return frappe.db.sql( + """ select distinct name from `tabItem Group` where {condition} and (name like %(txt)s) limit {page_len} offset {start}""".format( - condition=cond, start=start, page_len=page_len - ), - {"txt": "%%%s%%" % txt}, - ) + condition=cond, start=start, page_len=page_len + ), + {"txt": "%%%s%%" % txt}, + ) @frappe.whitelist() -def check_opening_entry(user,value): - filters = {"user": user, "pos_closing_entry": ["in", ["", None]], "docstatus": 1} - if value: - filters['pos_profile'] = value - open_vouchers = frappe.db.get_all( - "POS Opening Entry", - filters=filters, - fields=["name", "company", "pos_profile", "period_start_date"], - order_by="period_start_date desc", - ) +def check_opening_entry(user, value): + filters = {"user": user, "pos_closing_entry": ["in", ["", None]], "docstatus": 1} + if value: + filters["pos_profile"] = value + open_vouchers = frappe.db.get_all( + "POS Opening Entry", + filters=filters, + fields=["name", "company", "pos_profile", "period_start_date"], + order_by="period_start_date desc", + ) - return open_vouchers + return open_vouchers @frappe.whitelist() def create_opening_voucher(pos_profile, company, balance_details): - balance_details = json.loads(balance_details) + balance_details = json.loads(balance_details) - new_pos_opening = frappe.get_doc( - { - "doctype": "POS Opening Entry", - "period_start_date": frappe.utils.get_datetime(), - "posting_date": frappe.utils.getdate(), - "user": frappe.session.user, - "pos_profile": pos_profile, - "company": company, - } - ) - new_pos_opening.set("balance_details", balance_details) - new_pos_opening.submit() + new_pos_opening = frappe.get_doc( + { + "doctype": "POS Opening Entry", + "period_start_date": frappe.utils.get_datetime(), + "posting_date": frappe.utils.getdate(), + "user": frappe.session.user, + "pos_profile": pos_profile, + "company": company, + } + ) + new_pos_opening.set("balance_details", balance_details) + new_pos_opening.submit() - return new_pos_opening.as_dict() + return new_pos_opening.as_dict() @frappe.whitelist() def get_past_order_list(search_term, status, pos_profile=None, limit=20): - fields = ["name", "grand_total", "currency", "customer", "posting_time", "posting_date"] - invoice_list = [] - if status == "Unpaid": - status = ["in", ["Unpaid", "Partly Paid", "Overdue"]] - - if search_term and status: - fltr1 = {"customer": ["like", "%{}%".format(search_term)], "status": status} - if pos_profile: - fltr1 = {"customer": ["like", "%{}%".format(search_term)], "status": status, "pos_profile": pos_profile} - invoices_by_customer = frappe.db.get_all( - "Sales Invoice", - filters=fltr1, - fields=fields, - page_length=limit, - ) - fltr2 = {"name": ["like", "%{}%".format(search_term)], "status": status} - if pos_profile: - fltr2 = {"name": ["like", "%{}%".format(search_term)], "status": status, "pos_profile": pos_profile} - invoices_by_name = frappe.db.get_all( - "Sales Invoice", - filters=fltr2, - fields=fields, - page_length=limit, - ) - - invoice_list = invoices_by_customer + invoices_by_name - elif status: - fltr = {"status": status} - if pos_profile: - fltr = {"status": status, "pos_profile": pos_profile} - invoice_list = frappe.db.get_all( - "Sales Invoice", filters=fltr, fields=fields, page_length=limit - ) - - return invoice_list + fields = [ + "name", + "grand_total", + "currency", + "customer", + "posting_time", + "posting_date", + ] + invoice_list = [] + if status == "Unpaid": + status = ["in", ["Unpaid", "Partly Paid", "Overdue"]] + + if search_term and status: + fltr1 = {"customer": ["like", "%{}%".format(search_term)], "status": status} + if pos_profile: + fltr1 = { + "customer": ["like", "%{}%".format(search_term)], + "status": status, + "pos_profile": pos_profile, + } + invoices_by_customer = frappe.db.get_all( + "Sales Invoice", + filters=fltr1, + fields=fields, + page_length=limit, + ) + fltr2 = {"name": ["like", "%{}%".format(search_term)], "status": status} + if pos_profile: + fltr2 = { + "name": ["like", "%{}%".format(search_term)], + "status": status, + "pos_profile": pos_profile, + } + invoices_by_name = frappe.db.get_all( + "Sales Invoice", + filters=fltr2, + fields=fields, + page_length=limit, + ) + + invoice_list = invoices_by_customer + invoices_by_name + elif status: + fltr = {"status": status} + if pos_profile: + fltr = {"status": status, "pos_profile": pos_profile} + invoice_list = frappe.db.get_all( + "Sales Invoice", filters=fltr, fields=fields, page_length=limit + ) + + return invoice_list @frappe.whitelist() def set_customer_info(fieldname, customer, value=""): - if fieldname == "loyalty_program": - frappe.db.set_value("Customer", customer, "loyalty_program", value) + if fieldname == "loyalty_program": + frappe.db.set_value("Customer", customer, "loyalty_program", value) - contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact") - if not contact: - contact = frappe.db.sql( - """ + contact = frappe.get_cached_value("Customer", customer, "customer_primary_contact") + if not contact: + contact = frappe.db.sql( + """ SELECT parent FROM `tabDynamic Link` WHERE parenttype = 'Contact' AND @@ -379,111 +452,116 @@ def set_customer_info(fieldname, customer, value=""): link_doctype = 'Customer' AND link_name = %s """, - (customer), - as_dict=1, - ) - contact = contact[0].get("parent") if contact else None - - if not contact: - new_contact = frappe.new_doc("Contact") - new_contact.is_primary_contact = 1 - new_contact.first_name = customer - new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}]) - new_contact.save() - contact = new_contact.name - frappe.db.set_value("Customer", customer, "customer_primary_contact", contact) - - contact_doc = frappe.get_doc("Contact", contact) - if fieldname == "email_id": - contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}]) - frappe.db.set_value("Customer", customer, "email_id", value) - elif fieldname == "mobile_no": - contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}]) - frappe.db.set_value("Customer", customer, "mobile_no", value) - contact_doc.save() + (customer), + as_dict=1, + ) + contact = contact[0].get("parent") if contact else None + + if not contact: + new_contact = frappe.new_doc("Contact") + new_contact.is_primary_contact = 1 + new_contact.first_name = customer + new_contact.set("links", [{"link_doctype": "Customer", "link_name": customer}]) + new_contact.save() + contact = new_contact.name + frappe.db.set_value("Customer", customer, "customer_primary_contact", contact) + + contact_doc = frappe.get_doc("Contact", contact) + if fieldname == "email_id": + contact_doc.set("email_ids", [{"email_id": value, "is_primary": 1}]) + frappe.db.set_value("Customer", customer, "email_id", value) + elif fieldname == "mobile_no": + contact_doc.set("phone_nos", [{"phone": value, "is_primary_mobile_no": 1}]) + frappe.db.set_value("Customer", customer, "mobile_no", value) + contact_doc.save() @frappe.whitelist() def get_pos_profile_data(pos_profile): - pos_profile = frappe.get_doc("POS Profile", pos_profile) - pos_profile = pos_profile.as_dict() + pos_profile = frappe.get_doc("POS Profile", pos_profile) + pos_profile = pos_profile.as_dict() - _customer_groups_with_children = [] - for row in pos_profile.customer_groups: - children = get_child_nodes("Customer Group", row.customer_group) - _customer_groups_with_children.extend(children) - for row in pos_profile.payments: - if row.default: - pos_profile['default_payment'] = row.mode_of_payment - pos_profile.customer_groups = _customer_groups_with_children - return pos_profile + _customer_groups_with_children = [] + for row in pos_profile.customer_groups: + children = get_child_nodes("Customer Group", row.customer_group) + _customer_groups_with_children.extend(children) + for row in pos_profile.payments: + if row.default: + pos_profile["default_payment"] = row.mode_of_payment + pos_profile.customer_groups = _customer_groups_with_children + return pos_profile @frappe.whitelist() def create_customer(customer): - customer_check = frappe.db.sql(""" SELECT * FROM `tabCustomer` WHERE name=%s""",customer,as_dict=1) - if len(customer_check) == 0: - obj = { - "doctype": "Customer", - "customer_name": customer - } + customer_check = frappe.db.sql( + """ SELECT * FROM `tabCustomer` WHERE name=%s""", customer, as_dict=1 + ) + if len(customer_check) == 0: + obj = {"doctype": "Customer", "customer_name": customer} - frappe.get_doc(obj).insert() - frappe.db.commit() + frappe.get_doc(obj).insert() + # frappe.db.commit() -import frappe -from frappe.utils.pdf import get_pdf -from frappe.utils.file_manager import save_file - @frappe.whitelist() def generate_pdf_and_save(docname, doctype, print_format=None): - # Get the HTML content of the print format - data = frappe.get_doc(doctype,docname) - html = frappe.get_print(doctype, docname, print_format) + # Get the HTML content of the print format + data = frappe.get_doc(doctype, docname) + html = frappe.get_print(doctype, docname, print_format) + + # Generate PDF from HTML + pdf_data = get_pdf(html) - # Generate PDF from HTML - pdf_data = get_pdf(html) + # Define file name + file_name = f"{data.customer_name + docname.split('-')[-1]}.pdf" - # Define file name - file_name = f"{data.customer_name + docname.split('-')[-1]}.pdf" + # Save the PDF as a file + file_doc = save_file(file_name, pdf_data, doctype, docname, is_private=0) + print("FILE DOOOOC") + print(file_doc) + return file_doc - # Save the PDF as a file - file_doc = save_file(file_name, pdf_data, doctype, docname, is_private=0) - print("FILE DOOOOC") - print(file_doc) - return file_doc @frappe.whitelist() def make_sales_return(source_name, target_doc=None): - from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.controllers.sales_and_purchase_return import make_return_doc - return make_return_doc("Sales Invoice", source_name, target_doc) + return make_return_doc("Sales Invoice", source_name, target_doc) @frappe.whitelist() def get_lcr(customer=None, item_code=None): - d = None - if customer and item_code: - d = frappe.db.sql(f""" + d = None + if customer and item_code: + d = frappe.db.sql( + f""" SELECT item.rate FROM `tabSales Invoice Item` item INNER JOIN `tabSales Invoice` SI ON SI.name=item.parent - WHERE SI.customer='{customer}' AND item.item_code='{item_code}' - ORDER BY SI.creation desc + WHERE SI.customer='{customer}' AND item.item_code='{item_code}' + ORDER BY SI.creation desc LIMIT 1 - """, as_dict=True) - if d: - return d[0].rate - else: - return 0 + """, + as_dict=True, + ) + if d: + return d[0].rate + else: + return 0 + @frappe.whitelist() def get_uoms(item_code): - d = frappe.db.get_all("UOM Conversion Detail", {"parent": item_code}, ["uom"], pluck="uom") - if d: - return d - else: - return [] - + d = frappe.db.get_all( + "UOM Conversion Detail", {"parent": item_code}, ["uom"], pluck="uom" + ) + if d: + return d + else: + return [] + + @frappe.whitelist() def get_barcodes(item_code): - return frappe.db.get_all("Item Barcode", filters={"parent": item_code}, fields=["barcode"]) + return frappe.db.get_all( + "Item Barcode", filters={"parent": item_code}, fields=["barcode"] + ) diff --git a/posnext/posnext/page/posnext/posnext.js b/posnext/posnext/page/posnext/posnext.js index 97a05e2..0afef3f 100644 --- a/posnext/posnext/page/posnext/posnext.js +++ b/posnext/posnext/page/posnext/posnext.js @@ -9,28 +9,28 @@ // // document.head.appendChild(script); // })();'console.log("POSNEXT POINTSALE") -frappe.pages['posnext'].on_page_load = function(wrapper) { - let fullwidth = JSON.parse(localStorage.container_fullwidth || 'false'); - if(!fullwidth){ - localStorage.container_fullwidth = true; - frappe.ui.toolbar.set_fullwidth_if_enabled(); - } - frappe.ui.make_app_page({ - parent: wrapper, - title: __('Point of Sales'), - single_column: true - }); +frappe.pages["posnext"].on_page_load = function (wrapper) { + let fullwidth = JSON.parse(localStorage.container_fullwidth || "false"); + if (!fullwidth) { + localStorage.container_fullwidth = true; + frappe.ui.toolbar.set_fullwidth_if_enabled(); + } + frappe.ui.make_app_page({ + parent: wrapper, + title: __("Point of Sales"), + single_column: true, + }); - window.wrapper = wrapper - wrapper.pos = new posnext.PointOfSale.Controller(wrapper); - window.cur_pos = wrapper.pos; -} -frappe.pages['posnext'].refresh = function(wrapper,onscan = "",value="") { - // if (document.scannerDetectionData) { - if(!onscan){ - window.onScan.detachFrom(document) - } - wrapper.pos.wrapper.html(""); - wrapper.pos.check_opening_entry(value); - // } -}; \ No newline at end of file + window.wrapper = wrapper; + wrapper.pos = new posnext.PointOfSale.Controller(wrapper); + window.cur_pos = wrapper.pos; +}; +frappe.pages["posnext"].refresh = function (wrapper, onscan = "", value = "") { + // if (document.scannerDetectionData) { + if (!onscan) { + window.onScan.detachFrom(document); + } + wrapper.pos.wrapper.html(""); + wrapper.pos.check_opening_entry(value); + // } +}; diff --git a/posnext/posnext/report/stock_balance_rack/stock_balance_rack.js b/posnext/posnext/report/stock_balance_rack/stock_balance_rack.js index c632246..f70f1ce 100644 --- a/posnext/posnext/report/stock_balance_rack/stock_balance_rack.js +++ b/posnext/posnext/report/stock_balance_rack/stock_balance_rack.js @@ -2,130 +2,131 @@ // For license information, please see license.txt frappe.query_reports["Stock Balance Rack"] = { - filters: [ - { - fieldname: "company", - label: __("Company"), - fieldtype: "Link", - width: "80", - options: "Company", - default: frappe.defaults.get_default("company"), - }, - { - fieldname: "pos_profile", - label: __("POS Profile"), - fieldtype: "Link", - width: "80", - options: "POS Profile", - reqd: true - }, - { - fieldname: "from_date", - label: __("From Date"), - fieldtype: "Date", - width: "80", - reqd: 1, - default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), - }, - { - fieldname: "to_date", - label: __("To Date"), - fieldtype: "Date", - width: "80", - reqd: 1, - default: frappe.datetime.get_today(), - }, - { - fieldname: "item_group", - label: __("Item Group"), - fieldtype: "Link", - width: "80", - options: "Item Group", - }, - { - fieldname: "item_code", - label: __("Item"), - fieldtype: "Link", - width: "80", - options: "Item", - get_query: function () { - return { - query: "erpnext.controllers.queries.item_query", - }; - }, - }, - { - fieldname: "warehouse", - label: __("Warehouse"), - fieldtype: "Link", - width: "80", - options: "Warehouse", - get_query: () => { - let warehouse_type = frappe.query_report.get_filter_value("warehouse_type"); - let company = frappe.query_report.get_filter_value("company"); + filters: [ + { + fieldname: "company", + label: __("Company"), + fieldtype: "Link", + width: "80", + options: "Company", + default: frappe.defaults.get_default("company"), + }, + { + fieldname: "pos_profile", + label: __("POS Profile"), + fieldtype: "Link", + width: "80", + options: "POS Profile", + reqd: true, + }, + { + fieldname: "from_date", + label: __("From Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), + }, + { + fieldname: "to_date", + label: __("To Date"), + fieldtype: "Date", + width: "80", + reqd: 1, + default: frappe.datetime.get_today(), + }, + { + fieldname: "item_group", + label: __("Item Group"), + fieldtype: "Link", + width: "80", + options: "Item Group", + }, + { + fieldname: "item_code", + label: __("Item"), + fieldtype: "Link", + width: "80", + options: "Item", + get_query: function () { + return { + query: "erpnext.controllers.queries.item_query", + }; + }, + }, + { + fieldname: "warehouse", + label: __("Warehouse"), + fieldtype: "Link", + width: "80", + options: "Warehouse", + get_query: () => { + let warehouse_type = + frappe.query_report.get_filter_value("warehouse_type"); + let company = frappe.query_report.get_filter_value("company"); - return { - filters: { - ...(warehouse_type && { warehouse_type }), - ...(company && { company }), - }, - }; - }, - }, - { - fieldname: "warehouse_type", - label: __("Warehouse Type"), - fieldtype: "Link", - width: "80", - options: "Warehouse Type", - }, - { - fieldname: "valuation_field_type", - label: __("Valuation Field Type"), - fieldtype: "Select", - width: "80", - options: "Currency\nFloat", - default: "Currency", - }, - { - fieldname: "include_uom", - label: __("Include UOM"), - fieldtype: "Link", - options: "UOM", - }, - { - fieldname: "show_variant_attributes", - label: __("Show Variant Attributes"), - fieldtype: "Check", - }, - { - fieldname: "show_stock_ageing_data", - label: __("Show Stock Ageing Data"), - fieldtype: "Check", - }, - { - fieldname: "ignore_closing_balance", - label: __("Ignore Closing Balance"), - fieldtype: "Check", - default: 0, - }, - { - fieldname: "include_zero_stock_items", - label: __("Include Zero Stock Items"), - fieldtype: "Check", - default: 0, - }, - ], + return { + filters: { + ...(warehouse_type && { warehouse_type }), + ...(company && { company }), + }, + }; + }, + }, + { + fieldname: "warehouse_type", + label: __("Warehouse Type"), + fieldtype: "Link", + width: "80", + options: "Warehouse Type", + }, + { + fieldname: "valuation_field_type", + label: __("Valuation Field Type"), + fieldtype: "Select", + width: "80", + options: "Currency\nFloat", + default: "Currency", + }, + { + fieldname: "include_uom", + label: __("Include UOM"), + fieldtype: "Link", + options: "UOM", + }, + { + fieldname: "show_variant_attributes", + label: __("Show Variant Attributes"), + fieldtype: "Check", + }, + { + fieldname: "show_stock_ageing_data", + label: __("Show Stock Ageing Data"), + fieldtype: "Check", + }, + { + fieldname: "ignore_closing_balance", + label: __("Ignore Closing Balance"), + fieldtype: "Check", + default: 0, + }, + { + fieldname: "include_zero_stock_items", + label: __("Include Zero Stock Items"), + fieldtype: "Check", + default: 0, + }, + ], - formatter: function (value, row, column, data, default_formatter) { - value = default_formatter(value, row, column, data); + formatter: function (value, row, column, data, default_formatter) { + value = default_formatter(value, row, column, data); - if (column.fieldname == "out_qty" && data && data.out_qty > 0) { - value = "<span style='color:red'>" + value + "</span>"; - } else if (column.fieldname == "in_qty" && data && data.in_qty > 0) { - value = "<span style='color:green'>" + value + "</span>"; - } + if (column.fieldname == "out_qty" && data && data.out_qty > 0) { + value = "<span style='color:red'>" + value + "</span>"; + } else if (column.fieldname == "in_qty" && data && data.in_qty > 0) { + value = "<span style='color:green'>" + value + "</span>"; + } - return value; - }, + return value; + }, }; diff --git a/posnext/posnext/report/stock_balance_rack/stock_balance_rack.py b/posnext/posnext/report/stock_balance_rack/stock_balance_rack.py index a7e9eca..c9ece24 100644 --- a/posnext/posnext/report/stock_balance_rack/stock_balance_rack.py +++ b/posnext/posnext/report/stock_balance_rack/stock_balance_rack.py @@ -5,653 +5,709 @@ from operator import itemgetter from typing import Any, TypedDict +import erpnext import frappe +from erpnext.stock.doctype.inventory_dimension.inventory_dimension import ( + get_inventory_dimensions, +) +from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter +from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age +from erpnext.stock.utils import add_additional_uom_columns from frappe import _ from frappe.query_builder import Order from frappe.query_builder.functions import Coalesce from frappe.utils import add_days, cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of -import erpnext -from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions -from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter -from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age -from erpnext.stock.utils import add_additional_uom_columns - class StockBalanceFilter(TypedDict): - company: str | None - from_date: str - to_date: str - item_group: str | None - item: str | None - warehouse: str | None - warehouse_type: str | None - include_uom: str | None # include extra info in converted UOM - show_stock_ageing_data: bool - show_variant_attributes: bool + company: str | None + from_date: str + to_date: str + item_group: str | None + item: str | None + warehouse: str | None + warehouse_type: str | None + include_uom: str | None # include extra info in converted UOM + show_stock_ageing_data: bool + show_variant_attributes: bool SLEntry = dict[str, Any] def execute(filters: StockBalanceFilter | None = None): - return StockBalanceReport(filters).run() + return StockBalanceReport(filters).run() class StockBalanceReport: - def __init__(self, filters: StockBalanceFilter | None) -> None: - self.filters = filters - self.from_date = getdate(filters.get("from_date")) - self.to_date = getdate(filters.get("to_date")) - - self.start_from = None - self.data = [] - self.columns = [] - self.sle_entries: list[SLEntry] = [] - self.set_company_currency() - - def set_company_currency(self) -> None: - if self.filters.get("company"): - self.company_currency = erpnext.get_company_currency(self.filters.get("company")) - else: - self.company_currency = frappe.db.get_single_value("Global Defaults", "default_currency") - - def run(self): - self.float_precision = cint(frappe.db.get_default("float_precision")) or 3 - - self.inventory_dimensions = self.get_inventory_dimension_fields() - self.prepare_opening_data_from_closing_balance() - self.prepare_stock_ledger_entries() - self.prepare_new_data() - - if not self.columns: - self.columns = self.get_columns() - - self.add_additional_uom_columns() - - return self.columns, self.data - - def prepare_opening_data_from_closing_balance(self) -> None: - self.opening_data = frappe._dict({}) - - closing_balance = self.get_closing_balance() - if not closing_balance: - return - - self.start_from = add_days(closing_balance[0].to_date, 1) - res = frappe.get_doc("Closing Stock Balance", closing_balance[0].name).get_prepared_data() - - for entry in res.data: - entry = frappe._dict(entry) - - group_by_key = self.get_group_by_key(entry) - if group_by_key not in self.opening_data: - self.opening_data.setdefault(group_by_key, entry) - - def prepare_new_data(self): - self.item_warehouse_map = self.get_item_warehouse_map() - - if self.filters.get("show_stock_ageing_data"): - self.filters["show_warehouse_wise_stock"] = True - item_wise_fifo_queue = FIFOSlots(self.filters, self.sle_entries).generate() - - _func = itemgetter(1) - - del self.sle_entries - - sre_details = self.get_sre_reserved_qty_details() - variant_values = {} - if self.filters.get("show_variant_attributes"): - variant_values = self.get_variant_values_for() - - for _key, report_data in self.item_warehouse_map.items(): - if variant_data := variant_values.get(report_data.item_code): - report_data.update(variant_data) - - if self.filters.get("show_stock_ageing_data"): - opening_fifo_queue = self.get_opening_fifo_queue(report_data) or [] - - fifo_queue = [] - if fifo_queue := item_wise_fifo_queue.get((report_data.item_code, report_data.warehouse)): - fifo_queue = fifo_queue.get("fifo_queue") - - if fifo_queue: - opening_fifo_queue.extend(fifo_queue) - - stock_ageing_data = {"average_age": 0, "earliest_age": 0, "latest_age": 0} - if opening_fifo_queue: - fifo_queue = sorted(filter(_func, opening_fifo_queue), key=_func) - if not fifo_queue: - continue - - to_date = self.to_date - stock_ageing_data["average_age"] = get_average_age(fifo_queue, to_date) - stock_ageing_data["earliest_age"] = date_diff(to_date, fifo_queue[0][1]) - stock_ageing_data["latest_age"] = date_diff(to_date, fifo_queue[-1][1]) - stock_ageing_data["fifo_queue"] = fifo_queue - - report_data.update(stock_ageing_data) - - report_data.update( - {"reserved_stock": sre_details.get((report_data.item_code, report_data.warehouse), 0.0)} - ) - - if ( - not self.filters.get("include_zero_stock_items") - and report_data - and report_data.bal_qty == 0 - and report_data.bal_val == 0 - ): - continue - - self.data.append(report_data) - - def get_item_warehouse_map(self): - item_warehouse_map = {} - self.opening_vouchers = self.get_opening_vouchers() - - if self.filters.get("show_stock_ageing_data"): - self.sle_entries = self.sle_query.run(as_dict=True) - - # HACK: This is required to avoid causing db query in flt - _system_settings = frappe.get_cached_doc("System Settings") - with frappe.db.unbuffered_cursor(): - if not self.filters.get("show_stock_ageing_data"): - self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True) - - for entry in self.sle_entries: - group_by_key = self.get_group_by_key(entry) - if group_by_key not in item_warehouse_map: - self.initialize_data(item_warehouse_map, group_by_key, entry) - - self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) - - if self.opening_data.get(group_by_key): - del self.opening_data[group_by_key] - for group_by_key, entry in self.opening_data.items(): - if group_by_key not in item_warehouse_map: - self.initialize_data(item_warehouse_map, group_by_key, entry) - item_warehouse_map = filter_items_with_no_transactions( - item_warehouse_map, self.float_precision, self.inventory_dimensions - ) - return item_warehouse_map - - def get_sre_reserved_qty_details(self) -> dict: - from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( - get_sre_reserved_qty_for_items_and_warehouses as get_reserved_qty_details, - ) - - item_code_list, warehouse_list = [], [] - for d in self.item_warehouse_map: - item_code_list.append(d[1]) - warehouse_list.append(d[2]) - - return get_reserved_qty_details(item_code_list, warehouse_list) - - def prepare_item_warehouse_map(self, item_warehouse_map, entry, group_by_key): - qty_dict = item_warehouse_map[group_by_key] - for field in self.inventory_dimensions: - qty_dict[field] = entry.get(field) - - if entry.voucher_type == "Stock Reconciliation" and (not entry.batch_no or entry.serial_no): - qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty) - else: - qty_diff = flt(entry.actual_qty) - - value_diff = flt(entry.stock_value_difference) - - if entry.posting_date < self.from_date or entry.voucher_no in self.opening_vouchers.get( - entry.voucher_type, [] - ): - qty_dict.opening_qty += qty_diff - qty_dict.opening_val += value_diff - - elif entry.posting_date >= self.from_date and entry.posting_date <= self.to_date: - if flt(qty_diff, self.float_precision) >= 0: - qty_dict.in_qty += qty_diff - qty_dict.in_val += value_diff - else: - qty_dict.out_qty += abs(qty_diff) - qty_dict.out_val += abs(value_diff) - - qty_dict.val_rate = entry.valuation_rate - qty_dict.bal_qty += qty_diff - qty_dict.bal_val += value_diff - - def initialize_data(self, item_warehouse_map, group_by_key, entry): - opening_data = self.opening_data.get(group_by_key, {}) - item_warehouse_map[group_by_key] = frappe._dict( - { - "item_code": entry.item_code, - "warehouse": entry.warehouse, - "item_group": entry.item_group, - "company": entry.company, - "currency": self.company_currency, - "stock_uom": entry.stock_uom, - "item_name": entry.item_name, - "opening_qty": opening_data.get("bal_qty") or 0.0, - "opening_val": opening_data.get("bal_val") or 0.0, - "opening_fifo_queue": opening_data.get("fifo_queue") or [], - "in_qty": 0.0, - "in_val": 0.0, - "out_qty": 0.0, - "out_val": 0.0, - "bal_qty": opening_data.get("bal_qty") or 0.0, - "bal_val": opening_data.get("bal_val") or 0.0, - "val_rate": 0.0, - "rack": entry.rack - } - ) - - def get_group_by_key(self, row) -> tuple: - group_by_key = [row.company, row.item_code, row.warehouse] - - for fieldname in self.inventory_dimensions: - if self.filters.get(fieldname): - group_by_key.append(row.get(fieldname)) - - return tuple(group_by_key) - - def get_closing_balance(self) -> list[dict[str, Any]]: - if self.filters.get("ignore_closing_balance"): - return [] - - table = frappe.qb.DocType("Closing Stock Balance") - - query = ( - frappe.qb.from_(table) - .select(table.name, table.to_date) - .where( - (table.docstatus == 1) - & (table.company == self.filters.company) - & (table.to_date <= self.from_date) - & (table.status == "Completed") - ) - .orderby(table.to_date, order=Order.desc) - .limit(1) - ) - - for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]: - if self.filters.get(fieldname): - query = query.where(table[fieldname] == self.filters.get(fieldname)) - - return query.run(as_dict=True) - - def prepare_stock_ledger_entries(self): - sle = frappe.qb.DocType("Stock Ledger Entry") - item_table = frappe.qb.DocType("Item") - logical_rack = frappe.qb.DocType("Logical Rack") - pos_profile = frappe.qb.DocType("POS Profile") - query = ( - frappe.qb.from_(sle) - .inner_join(item_table) - .on(sle.item_code == item_table.name) - .inner_join(logical_rack) - .on(logical_rack.pos_profile == self.filters.get("pos_profile")) - .inner_join(pos_profile) - .on(pos_profile.warehouse == sle.warehouse) - .select( - sle.item_code, - sle.warehouse, - sle.posting_date, - sle.actual_qty, - sle.valuation_rate, - sle.company, - sle.voucher_type, - sle.qty_after_transaction, - sle.stock_value_difference, - sle.item_code.as_("name"), - sle.voucher_no, - sle.stock_value, - sle.batch_no, - sle.serial_no, - sle.serial_and_batch_bundle, - sle.has_serial_no, - item_table.item_group, - item_table.stock_uom, - item_table.item_name, - logical_rack.rack_id.as_("rack") - ) - .where((sle.docstatus < 2) & (sle.is_cancelled == 0)) - .where(logical_rack.item == sle.item_code) - .orderby(sle.posting_datetime) - .orderby(sle.creation) - .orderby(sle.actual_qty) - ) - - query = self.apply_inventory_dimensions_filters(query, sle) - query = self.apply_warehouse_filters(query, sle) - query = self.apply_items_filters(query, item_table) - query = self.apply_date_filters(query, sle) - - if self.filters.get("company"): - query = query.where(sle.company == self.filters.get("company")) - - self.sle_query = query - - def apply_inventory_dimensions_filters(self, query, sle) -> str: - inventory_dimension_fields = self.get_inventory_dimension_fields() - if inventory_dimension_fields: - for fieldname in inventory_dimension_fields: - query = query.select(fieldname) - if self.filters.get(fieldname): - query = query.where(sle[fieldname].isin(self.filters.get(fieldname))) - - return query - - def apply_warehouse_filters(self, query, sle) -> str: - warehouse_table = frappe.qb.DocType("Warehouse") - - if self.filters.get("warehouse"): - query = apply_warehouse_filter(query, sle, self.filters) - elif warehouse_type := self.filters.get("warehouse_type"): - query = ( - query.join(warehouse_table) - .on(warehouse_table.name == sle.warehouse) - .where(warehouse_table.warehouse_type == warehouse_type) - ) - - return query - - def apply_items_filters(self, query, item_table) -> str: - if item_group := self.filters.get("item_group"): - children = get_descendants_of("Item Group", item_group, ignore_permissions=True) - query = query.where(item_table.item_group.isin([*children, item_group])) - - for field in ["item_code", "brand"]: - if not self.filters.get(field): - continue - elif field == "item_code": - query = query.where(item_table.name == self.filters.get(field)) - else: - query = query.where(item_table[field] == self.filters.get(field)) - - return query - - def apply_date_filters(self, query, sle) -> str: - if not self.filters.ignore_closing_balance and self.start_from: - query = query.where(sle.posting_date >= self.start_from) - - if self.to_date: - query = query.where(sle.posting_date <= self.to_date) - - return query - - def get_columns(self): - columns = [ - { - "label": _("Item"), - "fieldname": "item_code", - "fieldtype": "Link", - "options": "Item", - "width": 100, - }, - {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, - { - "label": _("Item Group"), - "fieldname": "item_group", - "fieldtype": "Link", - "options": "Item Group", - "width": 100, - }, - { - "label": _("Warehouse"), - "fieldname": "warehouse", - "fieldtype": "Link", - "options": "Warehouse", - "width": 100, - }, - { - "label": _("Rack"), - "fieldname": "rack", - "fieldtype": "Data", - "width": 100, - }, - ] - - for dimension in get_inventory_dimensions(): - columns.append( - { - "label": _(dimension.doctype), - "fieldname": dimension.fieldname, - "fieldtype": "Link", - "options": dimension.doctype, - "width": 110, - } - ) - - columns.extend( - [ - { - "label": _("Stock UOM"), - "fieldname": "stock_uom", - "fieldtype": "Link", - "options": "UOM", - "width": 90, - }, - { - "label": _("Balance Qty"), - "fieldname": "bal_qty", - "fieldtype": "Float", - "width": 100, - "convertible": "qty", - }, - { - "label": _("Balance Value"), - "fieldname": "bal_val", - "fieldtype": "Currency", - "width": 100, - "options": "Company:company:default_currency", - }, - { - "label": _("Opening Qty"), - "fieldname": "opening_qty", - "fieldtype": "Float", - "width": 100, - "convertible": "qty", - }, - { - "label": _("Opening Value"), - "fieldname": "opening_val", - "fieldtype": "Currency", - "width": 110, - "options": "Company:company:default_currency", - }, - { - "label": _("In Qty"), - "fieldname": "in_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, - {"label": _("In Value"), "fieldname": "in_val", "fieldtype": "Float", "width": 80}, - { - "label": _("Out Qty"), - "fieldname": "out_qty", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, - {"label": _("Out Value"), "fieldname": "out_val", "fieldtype": "Float", "width": 80}, - { - "label": _("Valuation Rate"), - "fieldname": "val_rate", - "fieldtype": self.filters.valuation_field_type or "Currency", - "width": 90, - "convertible": "rate", - "options": "Company:company:default_currency" - if self.filters.valuation_field_type == "Currency" - else None, - }, - { - "label": _("Reserved Stock"), - "fieldname": "reserved_stock", - "fieldtype": "Float", - "width": 80, - "convertible": "qty", - }, - { - "label": _("Company"), - "fieldname": "company", - "fieldtype": "Link", - "options": "Company", - "width": 100, - }, - ] - ) - - if self.filters.get("show_stock_ageing_data"): - columns += [ - {"label": _("Average Age"), "fieldname": "average_age", "width": 100}, - {"label": _("Earliest Age"), "fieldname": "earliest_age", "width": 100}, - {"label": _("Latest Age"), "fieldname": "latest_age", "width": 100}, - ] - - if self.filters.get("show_variant_attributes"): - columns += [ - {"label": att_name, "fieldname": att_name, "width": 100} - for att_name in get_variants_attributes() - ] - - return columns - - def add_additional_uom_columns(self): - if not self.filters.get("include_uom"): - return - - conversion_factors = self.get_itemwise_conversion_factor() - add_additional_uom_columns(self.columns, self.data, self.filters.include_uom, conversion_factors) - - def get_itemwise_conversion_factor(self): - items = [] - if self.filters.item_code or self.filters.item_group: - items = [d.item_code for d in self.data] - - table = frappe.qb.DocType("UOM Conversion Detail") - query = ( - frappe.qb.from_(table) - .select( - table.conversion_factor, - table.parent, - ) - .where((table.parenttype == "Item") & (table.uom == self.filters.include_uom)) - ) - - if items: - query = query.where(table.parent.isin(items)) - - result = query.run(as_dict=1) - if not result: - return {} - - return {d.parent: d.conversion_factor for d in result} - - def get_variant_values_for(self): - """Returns variant values for items.""" - attribute_map = {} - items = [] - if self.filters.item_code or self.filters.item_group: - items = [d.item_code for d in self.data] - - filters = {} - if items: - filters = {"parent": ("in", items)} - - attribute_info = frappe.get_all( - "Item Variant Attribute", - fields=["parent", "attribute", "attribute_value"], - filters=filters, - ) - - for attr in attribute_info: - attribute_map.setdefault(attr["parent"], {}) - attribute_map[attr["parent"]].update({attr["attribute"]: attr["attribute_value"]}) - - return attribute_map - - def get_opening_vouchers(self): - opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []} - - se = frappe.qb.DocType("Stock Entry") - sr = frappe.qb.DocType("Stock Reconciliation") - - vouchers_data = ( - frappe.qb.from_( - ( - frappe.qb.from_(se) - .select(se.name, Coalesce("Stock Entry").as_("voucher_type")) - .where((se.docstatus == 1) & (se.posting_date <= self.to_date) & (se.is_opening == "Yes")) - ) - + ( - frappe.qb.from_(sr) - .select(sr.name, Coalesce("Stock Reconciliation").as_("voucher_type")) - .where( - (sr.docstatus == 1) - & (sr.posting_date <= self.to_date) - & (sr.purpose == "Opening Stock") - ) - ) - ).select("voucher_type", "name") - ).run(as_dict=True) - - if vouchers_data: - for d in vouchers_data: - opening_vouchers[d.voucher_type].append(d.name) - - return opening_vouchers - - @staticmethod - def get_inventory_dimension_fields(): - return [dimension.fieldname for dimension in get_inventory_dimensions()] - - @staticmethod - def get_opening_fifo_queue(report_data): - opening_fifo_queue = report_data.get("opening_fifo_queue") or [] - for row in opening_fifo_queue: - row[1] = getdate(row[1]) - - return opening_fifo_queue + def __init__(self, filters: StockBalanceFilter | None) -> None: + self.filters = filters + self.from_date = getdate(filters.get("from_date")) + self.to_date = getdate(filters.get("to_date")) + + self.start_from = None + self.data = [] + self.columns = [] + self.sle_entries: list[SLEntry] = [] + self.set_company_currency() + + def set_company_currency(self) -> None: + if self.filters.get("company"): + self.company_currency = erpnext.get_company_currency( + self.filters.get("company") + ) + else: + self.company_currency = frappe.db.get_single_value( + "Global Defaults", "default_currency" + ) + + def run(self): + self.float_precision = cint(frappe.db.get_default("float_precision")) or 3 + + self.inventory_dimensions = self.get_inventory_dimension_fields() + self.prepare_opening_data_from_closing_balance() + self.prepare_stock_ledger_entries() + self.prepare_new_data() + + if not self.columns: + self.columns = self.get_columns() + + self.add_additional_uom_columns() + + return self.columns, self.data + + def prepare_opening_data_from_closing_balance(self) -> None: + self.opening_data = frappe._dict({}) + + closing_balance = self.get_closing_balance() + if not closing_balance: + return + + self.start_from = add_days(closing_balance[0].to_date, 1) + res = frappe.get_doc( + "Closing Stock Balance", closing_balance[0].name + ).get_prepared_data() + + for entry in res.data: + entry = frappe._dict(entry) + + group_by_key = self.get_group_by_key(entry) + if group_by_key not in self.opening_data: + self.opening_data.setdefault(group_by_key, entry) + + def prepare_new_data(self): + self.item_warehouse_map = self.get_item_warehouse_map() + + if self.filters.get("show_stock_ageing_data"): + self.filters["show_warehouse_wise_stock"] = True + item_wise_fifo_queue = FIFOSlots(self.filters, self.sle_entries).generate() + + _func = itemgetter(1) + + del self.sle_entries + + sre_details = self.get_sre_reserved_qty_details() + variant_values = {} + if self.filters.get("show_variant_attributes"): + variant_values = self.get_variant_values_for() + + for _key, report_data in self.item_warehouse_map.items(): + if variant_data := variant_values.get(report_data.item_code): + report_data.update(variant_data) + + if self.filters.get("show_stock_ageing_data"): + opening_fifo_queue = self.get_opening_fifo_queue(report_data) or [] + + fifo_queue = [] + if fifo_queue := item_wise_fifo_queue.get( + (report_data.item_code, report_data.warehouse) + ): + fifo_queue = fifo_queue.get("fifo_queue") + + if fifo_queue: + opening_fifo_queue.extend(fifo_queue) + + stock_ageing_data = { + "average_age": 0, + "earliest_age": 0, + "latest_age": 0, + } + if opening_fifo_queue: + fifo_queue = sorted( + [x for x in opening_fifo_queue if _func(x)], key=_func + ) + if not fifo_queue: + continue + + to_date = self.to_date + stock_ageing_data["average_age"] = get_average_age( + fifo_queue, to_date + ) + stock_ageing_data["earliest_age"] = date_diff( + to_date, fifo_queue[0][1] + ) + stock_ageing_data["latest_age"] = date_diff( + to_date, fifo_queue[-1][1] + ) + stock_ageing_data["fifo_queue"] = fifo_queue + + report_data.update(stock_ageing_data) + + report_data.update( + { + "reserved_stock": sre_details.get( + (report_data.item_code, report_data.warehouse), 0.0 + ) + } + ) + + if ( + not self.filters.get("include_zero_stock_items") + and report_data + and report_data.bal_qty == 0 + and report_data.bal_val == 0 + ): + continue + + self.data.append(report_data) + + def get_item_warehouse_map(self): + item_warehouse_map = {} + self.opening_vouchers = self.get_opening_vouchers() + + if self.filters.get("show_stock_ageing_data"): + self.sle_entries = self.sle_query.run(as_dict=True) + + # HACK: This is required to avoid causing db query in flt + _system_settings = frappe.get_cached_doc("System Settings") + with frappe.db.unbuffered_cursor(): + if not self.filters.get("show_stock_ageing_data"): + self.sle_entries = self.sle_query.run(as_dict=True, as_iterator=True) + + for entry in self.sle_entries: + group_by_key = self.get_group_by_key(entry) + if group_by_key not in item_warehouse_map: + self.initialize_data(item_warehouse_map, group_by_key, entry) + + self.prepare_item_warehouse_map(item_warehouse_map, entry, group_by_key) + + if self.opening_data.get(group_by_key): + del self.opening_data[group_by_key] + for group_by_key, entry in self.opening_data.items(): + if group_by_key not in item_warehouse_map: + self.initialize_data(item_warehouse_map, group_by_key, entry) + item_warehouse_map = filter_items_with_no_transactions( + item_warehouse_map, self.float_precision, self.inventory_dimensions + ) + return item_warehouse_map + + def get_sre_reserved_qty_details(self) -> dict: + from erpnext.stock.doctype.stock_reservation_entry.stock_reservation_entry import ( + get_sre_reserved_qty_for_items_and_warehouses as get_reserved_qty_details, + ) + + item_code_list, warehouse_list = [], [] + for d in self.item_warehouse_map: + item_code_list.append(d[1]) + warehouse_list.append(d[2]) + + return get_reserved_qty_details(item_code_list, warehouse_list) + + def prepare_item_warehouse_map(self, item_warehouse_map, entry, group_by_key): + qty_dict = item_warehouse_map[group_by_key] + for field in self.inventory_dimensions: + qty_dict[field] = entry.get(field) + + if entry.voucher_type == "Stock Reconciliation" and ( + not entry.batch_no or entry.serial_no + ): + qty_diff = flt(entry.qty_after_transaction) - flt(qty_dict.bal_qty) + else: + qty_diff = flt(entry.actual_qty) + + value_diff = flt(entry.stock_value_difference) + + if ( + entry.posting_date < self.from_date + or entry.voucher_no in self.opening_vouchers.get(entry.voucher_type, []) + ): + qty_dict.opening_qty += qty_diff + qty_dict.opening_val += value_diff + + elif ( + entry.posting_date >= self.from_date and entry.posting_date <= self.to_date + ): + if flt(qty_diff, self.float_precision) >= 0: + qty_dict.in_qty += qty_diff + qty_dict.in_val += value_diff + else: + qty_dict.out_qty += abs(qty_diff) + qty_dict.out_val += abs(value_diff) + + qty_dict.val_rate = entry.valuation_rate + qty_dict.bal_qty += qty_diff + qty_dict.bal_val += value_diff + + def initialize_data(self, item_warehouse_map, group_by_key, entry): + opening_data = self.opening_data.get(group_by_key, {}) + item_warehouse_map[group_by_key] = frappe._dict( + { + "item_code": entry.item_code, + "warehouse": entry.warehouse, + "item_group": entry.item_group, + "company": entry.company, + "currency": self.company_currency, + "stock_uom": entry.stock_uom, + "item_name": entry.item_name, + "opening_qty": opening_data.get("bal_qty") or 0.0, + "opening_val": opening_data.get("bal_val") or 0.0, + "opening_fifo_queue": opening_data.get("fifo_queue") or [], + "in_qty": 0.0, + "in_val": 0.0, + "out_qty": 0.0, + "out_val": 0.0, + "bal_qty": opening_data.get("bal_qty") or 0.0, + "bal_val": opening_data.get("bal_val") or 0.0, + "val_rate": 0.0, + "rack": entry.rack, + } + ) + + def get_group_by_key(self, row) -> tuple: + group_by_key = [row.company, row.item_code, row.warehouse] + + for fieldname in self.inventory_dimensions: + if self.filters.get(fieldname): + group_by_key.append(row.get(fieldname)) + + return tuple(group_by_key) + + def get_closing_balance(self) -> list[dict[str, Any]]: + if self.filters.get("ignore_closing_balance"): + return [] + + table = frappe.qb.DocType("Closing Stock Balance") + + query = ( + frappe.qb.from_(table) + .select(table.name, table.to_date) + .where( + (table.docstatus == 1) + & (table.company == self.filters.company) + & (table.to_date <= self.from_date) + & (table.status == "Completed") + ) + .orderby(table.to_date, order=Order.desc) + .limit(1) + ) + + for fieldname in ["warehouse", "item_code", "item_group", "warehouse_type"]: + if self.filters.get(fieldname): + query = query.where(table[fieldname] == self.filters.get(fieldname)) + + return query.run(as_dict=True) + + def prepare_stock_ledger_entries(self): + sle = frappe.qb.DocType("Stock Ledger Entry") + item_table = frappe.qb.DocType("Item") + logical_rack = frappe.qb.DocType("Logical Rack") + pos_profile = frappe.qb.DocType("POS Profile") + query = ( + frappe.qb.from_(sle) + .inner_join(item_table) + .on(sle.item_code == item_table.name) + .inner_join(logical_rack) + .on(logical_rack.pos_profile == self.filters.get("pos_profile")) + .inner_join(pos_profile) + .on(pos_profile.warehouse == sle.warehouse) + .select( + sle.item_code, + sle.warehouse, + sle.posting_date, + sle.actual_qty, + sle.valuation_rate, + sle.company, + sle.voucher_type, + sle.qty_after_transaction, + sle.stock_value_difference, + sle.item_code.as_("name"), + sle.voucher_no, + sle.stock_value, + sle.batch_no, + sle.serial_no, + sle.serial_and_batch_bundle, + sle.has_serial_no, + item_table.item_group, + item_table.stock_uom, + item_table.item_name, + logical_rack.rack_id.as_("rack"), + ) + .where((sle.docstatus < 2) & (sle.is_cancelled == 0)) + .where(logical_rack.item == sle.item_code) + .orderby(sle.posting_datetime) + .orderby(sle.creation) + .orderby(sle.actual_qty) + ) + + query = self.apply_inventory_dimensions_filters(query, sle) + query = self.apply_warehouse_filters(query, sle) + query = self.apply_items_filters(query, item_table) + query = self.apply_date_filters(query, sle) + + if self.filters.get("company"): + query = query.where(sle.company == self.filters.get("company")) + + self.sle_query = query + + def apply_inventory_dimensions_filters(self, query, sle) -> str: + inventory_dimension_fields = self.get_inventory_dimension_fields() + if inventory_dimension_fields: + for fieldname in inventory_dimension_fields: + query = query.select(fieldname) + if self.filters.get(fieldname): + query = query.where( + sle[fieldname].isin(self.filters.get(fieldname)) + ) + + return query + + def apply_warehouse_filters(self, query, sle) -> str: + warehouse_table = frappe.qb.DocType("Warehouse") + + if self.filters.get("warehouse"): + query = apply_warehouse_filter(query, sle, self.filters) + elif warehouse_type := self.filters.get("warehouse_type"): + query = ( + query.join(warehouse_table) + .on(warehouse_table.name == sle.warehouse) + .where(warehouse_table.warehouse_type == warehouse_type) + ) + + return query + + def apply_items_filters(self, query, item_table) -> str: + if item_group := self.filters.get("item_group"): + children = get_descendants_of( + "Item Group", item_group, ignore_permissions=True + ) + query = query.where(item_table.item_group.isin([*children, item_group])) + + for field in ["item_code", "brand"]: + if not self.filters.get(field): + continue + elif field == "item_code": + query = query.where(item_table.name == self.filters.get(field)) + else: + query = query.where(item_table[field] == self.filters.get(field)) + + return query + + def apply_date_filters(self, query, sle) -> str: + if not self.filters.ignore_closing_balance and self.start_from: + query = query.where(sle.posting_date >= self.start_from) + + if self.to_date: + query = query.where(sle.posting_date <= self.to_date) + + return query + + def get_columns(self): + columns = [ + { + "label": _("Item"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + {"label": _("Item Name"), "fieldname": "item_name", "width": 150}, + { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "Warehouse", + "width": 100, + }, + { + "label": _("Rack"), + "fieldname": "rack", + "fieldtype": "Data", + "width": 100, + }, + ] + + for dimension in get_inventory_dimensions(): + columns.append( + { + "label": _(dimension.doctype), + "fieldname": dimension.fieldname, + "fieldtype": "Link", + "options": dimension.doctype, + "width": 110, + } + ) + + columns.extend( + [ + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 90, + }, + { + "label": _("Balance Qty"), + "fieldname": "bal_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Balance Value"), + "fieldname": "bal_val", + "fieldtype": "Currency", + "width": 100, + "options": "Company:company:default_currency", + }, + { + "label": _("Opening Qty"), + "fieldname": "opening_qty", + "fieldtype": "Float", + "width": 100, + "convertible": "qty", + }, + { + "label": _("Opening Value"), + "fieldname": "opening_val", + "fieldtype": "Currency", + "width": 110, + "options": "Company:company:default_currency", + }, + { + "label": _("In Qty"), + "fieldname": "in_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("In Value"), + "fieldname": "in_val", + "fieldtype": "Float", + "width": 80, + }, + { + "label": _("Out Qty"), + "fieldname": "out_qty", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Out Value"), + "fieldname": "out_val", + "fieldtype": "Float", + "width": 80, + }, + { + "label": _("Valuation Rate"), + "fieldname": "val_rate", + "fieldtype": self.filters.valuation_field_type or "Currency", + "width": 90, + "convertible": "rate", + "options": "Company:company:default_currency" + if self.filters.valuation_field_type == "Currency" + else None, + }, + { + "label": _("Reserved Stock"), + "fieldname": "reserved_stock", + "fieldtype": "Float", + "width": 80, + "convertible": "qty", + }, + { + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 100, + }, + ] + ) + + if self.filters.get("show_stock_ageing_data"): + columns += [ + {"label": _("Average Age"), "fieldname": "average_age", "width": 100}, + {"label": _("Earliest Age"), "fieldname": "earliest_age", "width": 100}, + {"label": _("Latest Age"), "fieldname": "latest_age", "width": 100}, + ] + + if self.filters.get("show_variant_attributes"): + columns += [ + {"label": att_name, "fieldname": att_name, "width": 100} + for att_name in get_variants_attributes() + ] + + return columns + + def add_additional_uom_columns(self): + if not self.filters.get("include_uom"): + return + + conversion_factors = self.get_itemwise_conversion_factor() + add_additional_uom_columns( + self.columns, self.data, self.filters.include_uom, conversion_factors + ) + + def get_itemwise_conversion_factor(self): + items = [] + if self.filters.item_code or self.filters.item_group: + items = [d.item_code for d in self.data] + + table = frappe.qb.DocType("UOM Conversion Detail") + query = ( + frappe.qb.from_(table) + .select( + table.conversion_factor, + table.parent, + ) + .where( + (table.parenttype == "Item") & (table.uom == self.filters.include_uom) + ) + ) + + if items: + query = query.where(table.parent.isin(items)) + + result = query.run(as_dict=1) + if not result: + return {} + + return {d.parent: d.conversion_factor for d in result} + + def get_variant_values_for(self): + """Returns variant values for items.""" + attribute_map = {} + items = [] + if self.filters.item_code or self.filters.item_group: + items = [d.item_code for d in self.data] + + filters = {} + if items: + filters = {"parent": ("in", items)} + + attribute_info = frappe.get_all( + "Item Variant Attribute", + fields=["parent", "attribute", "attribute_value"], + filters=filters, + ) + + for attr in attribute_info: + attribute_map.setdefault(attr["parent"], {}) + attribute_map[attr["parent"]].update( + {attr["attribute"]: attr["attribute_value"]} + ) + + return attribute_map + + def get_opening_vouchers(self): + opening_vouchers = {"Stock Entry": [], "Stock Reconciliation": []} + + se = frappe.qb.DocType("Stock Entry") + sr = frappe.qb.DocType("Stock Reconciliation") + + vouchers_data = ( + frappe.qb.from_( + ( + frappe.qb.from_(se) + .select(se.name, Coalesce("Stock Entry").as_("voucher_type")) + .where( + (se.docstatus == 1) + & (se.posting_date <= self.to_date) + & (se.is_opening == "Yes") + ) + ) + + ( + frappe.qb.from_(sr) + .select( + sr.name, Coalesce("Stock Reconciliation").as_("voucher_type") + ) + .where( + (sr.docstatus == 1) + & (sr.posting_date <= self.to_date) + & (sr.purpose == "Opening Stock") + ) + ) + ).select("voucher_type", "name") + ).run(as_dict=True) + + if vouchers_data: + for d in vouchers_data: + opening_vouchers[d.voucher_type].append(d.name) + + return opening_vouchers + + @staticmethod + def get_inventory_dimension_fields(): + return [dimension.fieldname for dimension in get_inventory_dimensions()] + + @staticmethod + def get_opening_fifo_queue(report_data): + opening_fifo_queue = report_data.get("opening_fifo_queue") or [] + for row in opening_fifo_queue: + row[1] = getdate(row[1]) + + return opening_fifo_queue def filter_items_with_no_transactions( - iwb_map, float_precision: float, inventory_dimensions: list | None = None + iwb_map, float_precision: float, inventory_dimensions: list | None = None ): - pop_keys = [] - for group_by_key in iwb_map: - qty_dict = iwb_map[group_by_key] - - no_transactions = True - for key, val in qty_dict.items(): - if inventory_dimensions and key in inventory_dimensions: - continue - - if key in [ - "item_code", - "warehouse", - "item_name", - "item_group", - "project", - "stock_uom", - "company", - "opening_fifo_queue", - "rack", - ]: - continue - - val = flt(val, float_precision) - qty_dict[key] = val - if key != "val_rate" and val: - no_transactions = False - - if no_transactions: - pop_keys.append(group_by_key) - - for key in pop_keys: - iwb_map.pop(key) - return iwb_map + pop_keys = [] + for group_by_key in iwb_map: + qty_dict = iwb_map[group_by_key] + + no_transactions = True + for key, val in qty_dict.items(): + if inventory_dimensions and key in inventory_dimensions: + continue + + if key in [ + "item_code", + "warehouse", + "item_name", + "item_group", + "project", + "stock_uom", + "company", + "opening_fifo_queue", + "rack", + ]: + continue + + val = flt(val, float_precision) + qty_dict[key] = val + if key != "val_rate" and val: + no_transactions = False + + if no_transactions: + pop_keys.append(group_by_key) + + for key in pop_keys: + iwb_map.pop(key) + return iwb_map def get_variants_attributes() -> list[str]: - """Return all item variant attributes.""" - return frappe.get_all("Item Attribute", pluck="name") + """Return all item variant attributes.""" + return frappe.get_all("Item Attribute", pluck="name") diff --git a/posnext/public/dist/js/posnext.bundle.TN4KQRHJ.js b/posnext/public/dist/js/posnext.bundle.TN4KQRHJ.js index 623e630..c5f13be 100644 --- a/posnext/public/dist/js/posnext.bundle.TN4KQRHJ.js +++ b/posnext/public/dist/js/posnext.bundle.TN4KQRHJ.js @@ -928,7 +928,7 @@ <!--<div class="item-code-search-field" style="grid-column: span 2 / span 2"></div>--> <div class="item-group-field" style="grid-column: span 2 / span 2"></div> <div class="invoice-posting-date" style="margin-left: 10px;grid-column: span 2 / span 2"></div>` + tir + ` - + </div> <div class="items-container"></div> </section> @@ -951,7 +951,7 @@ <!--<div class="item-code-search-field" style="grid-column: span 2 / span 2"></div>--> <div class="item-group-field" style="grid-column: span 2 / span 2"></div> <div class="invoice-posting-date" style="margin-left: 10px;grid-column: span 2 / span 2"></div>` + tir + ` - + </div> <div class="cart-container" ></div> </section>` @@ -1154,10 +1154,10 @@ const me = this; const currency = me.events.get_frm().currency || me.currency; this.$cart_items_wrapper.append( - `<div class="cart-item-wrapper item-wrapper" - data-item-code="${escape(item_data.item_code)}" + `<div class="cart-item-wrapper item-wrapper" + data-item-code="${escape(item_data.item_code)}" data-serial-no="${escape(item_data.serial_no)}" - data-batch-no="${escape(item_data.batch_no)}" + data-batch-no="${escape(item_data.batch_no)}" data-uom="${escape(item_data.uom)}" data-rate="${escape(item_data.price_list_rate || 0)}" data-valuation-rate="${escape(item_data.valuation_rate || item_data.custom_valuation_rate)}" @@ -1171,7 +1171,7 @@ $item_to_update.html( `${get_item_image_html()} ${get_item_name()} - + <div style="overflow-wrap: break-word;overflow:hidden;white-space: normal;font-weight: 700;margin-right: 10px"> ${item_data.item_name} </div> @@ -1249,7 +1249,7 @@ </div> <div class="item-qty" style="flex: 1;display:block;text-align: center"><span> ${item_data.actual_qty || 0}</span></div> <div class="item-batch" style="flex: 1;display:block;text-align: center"><span> ${item_data.batch_no || 0}</span></div> - + </div>`; } else { return ` @@ -1259,7 +1259,7 @@ </div> <div class="item-qty" style="flex: 1;display:block;text-align: center"><span> ${item_data.actual_qty || 0}</span></div> <div class="item-batch" style="flex: 1;display:block;text-align: center"><span> ${item_data.batch_no || 0}</span></div> - + </div>`; } } @@ -1832,7 +1832,7 @@ border-radius: 5px; cursor: pointer; flex: 1;">${__("Order List")}</div> - </div> + </div> <div class="edit-cart-btn">${__("Edit Cart")}</div>` ); this.$add_discount_elem = this.$component.find(".add-discount-wrapper"); @@ -1966,7 +1966,7 @@ max-width: 350px; margin: 0 auto; } - + .numpad-button { padding: 15px; font-size: 18px; @@ -1976,7 +1976,7 @@ border-radius: 5px; text-align: center; } - + .numpad-button:hover { background-color: #ddd; } @@ -2769,7 +2769,7 @@ } else { if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { return ` - <div class="item-qty-rate" style="flex: 4" > + <div class="item-qty-rate" style="flex: 4" > <div class="item-qty" style="flex: 1"><span>${item_data.qty || 0}</span></div> <div class="item-qty" style="flex: 1"><span> ${item_data.uom}</span></div> <div class="item-qty" style="flex: 1"><span> ${item_data.batch}</span></div> diff --git a/posnext/public/js/item_list.js b/posnext/public/js/item_list.js index ccfdefc..c62d478 100644 --- a/posnext/public/js/item_list.js +++ b/posnext/public/js/item_list.js @@ -1,27 +1,26 @@ -frappe.listview_settings['Item'] = frappe.listview_settings['Item'] || {}; +frappe.listview_settings["Item"] = frappe.listview_settings["Item"] || {}; -frappe.listview_settings['Item'].onload = function(listview) { - listview.page.add_actions_menu_item(__('Barcode Print'), function() { - const selected_docs = listview.get_checked_items(); +frappe.listview_settings["Item"].onload = function (listview) { + listview.page.add_actions_menu_item(__("Barcode Print"), function () { + const selected_docs = listview.get_checked_items(); - if (!selected_docs.length) { - frappe.msgprint(__('Please select at least one Item')); - return; - } + if (!selected_docs.length) { + frappe.msgprint(__("Please select at least one Item")); + return; + } - const item_codes = selected_docs.map(doc => doc.name); + const item_codes = selected_docs.map((doc) => doc.name); - frappe.call({ - method: 'posnext.doc_events.item.print_barcodes', - args: { item_codes }, - callback: function(r) { - if (r.message && r.message.url) { - window.open(r.message.url, '_blank'); - } else { - frappe.msgprint(__('Could not generate barcode print URL.')); - } - } - }); + frappe.call({ + method: "posnext.doc_events.item.print_barcodes", + args: { item_codes }, + callback: function (r) { + if (r.message && r.message.url) { + window.open(r.message.url, "_blank"); + } else { + frappe.msgprint(__("Could not generate barcode print URL.")); + } + }, }); + }); }; - diff --git a/posnext/public/js/pos_controller.js b/posnext/public/js/pos_controller.js index c818d89..c720226 100644 --- a/posnext/public/js/pos_controller.js +++ b/posnext/public/js/pos_controller.js @@ -1,991 +1,1103 @@ -frappe.provide('posnext.PointOfSale'); -var selected_item = null +frappe.provide("posnext.PointOfSale"); +var selected_item = null; posnext.PointOfSale.Controller = class { - constructor(wrapper) { - console.log("CONTROLLLLLERE") - this.wrapper = $(wrapper).find('.layout-main-section'); - this.page = wrapper.page; - frappe.run_serially([ - () => this.reload_status = false, - () => this.check_opening_entry(""), - () => this.reload_status = true, - ]); - - this.setup_form_events(); - - } - setup_form_events() { - frappe.ui.form.on('Sales Invoice', { - after_save: function(frm) { - if (!frm.doc.pos_profile) return; - - frappe.db.get_doc('POS Profile', frm.doc.pos_profile) - .then(pos_profile => { - if (pos_profile.custom_stock_update) { - frm.set_value('update_stock', 0); - // frm.save(); - } - }); - } - }); - } - - - fetch_opening_entry(value) { - return frappe.call("posnext.posnext.page.posnext.point_of_sale.check_opening_entry", { "user": frappe.session.user, "value": value }); - } - - check_opening_entry(value = "") { - this.fetch_opening_entry(value).then((r) => { - if (r.message.length) { - // assuming only one opening voucher is available for the current user - this.prepare_app_defaults(r.message[0]); - } else { - this.create_opening_voucher(); - } - }); - } - - create_opening_voucher() { - const me = this; - const table_fields = [ - { - fieldname: "mode_of_payment", fieldtype: "Link", - in_list_view: 1, label: "Mode of Payment", - options: "Mode of Payment", reqd: 1 - }, - { - fieldname: "opening_amount", fieldtype: "Currency", - in_list_view: 1, label: "Opening Amount", - options: "company:company_currency", - change: function () { - dialog.fields_dict.balance_details.df.data.some(d => { - if (d.idx == this.doc.idx) { - d.opening_amount = this.value; - dialog.fields_dict.balance_details.grid.refresh(); - return true; - } - }); - } - } - ]; - const fetch_pos_payment_methods = () => { - const pos_profile = dialog.fields_dict.pos_profile.get_value(); - if (!pos_profile) return; - frappe.db.get_doc("POS Profile", pos_profile).then(({ payments }) => { - dialog.fields_dict.balance_details.df.data = []; - payments.forEach(pay => { - const { mode_of_payment } = pay; - dialog.fields_dict.balance_details.df.data.push({ mode_of_payment, opening_amount: '0' }); - }); - dialog.fields_dict.balance_details.grid.refresh(); - }); - } - const dialog = new frappe.ui.Dialog({ - title: __('Create POS Opening Entry'), - static: true, - fields: [ - { - fieldtype: 'Link', label: __('Company'), default: frappe.defaults.get_default('company'), - options: 'Company', fieldname: 'company', reqd: 1 - }, - { - fieldtype: 'Link', label: __('POS Profile'), - options: 'POS Profile', fieldname: 'pos_profile', reqd: 1, - get_query: () => pos_profile_query(), - onchange: () => fetch_pos_payment_methods() - }, - { - fieldname: "balance_details", - fieldtype: "Table", - label: "Opening Balance Details", - cannot_add_rows: false, - in_place_edit: true, - reqd: 1, - data: [], - fields: table_fields - } - ], - primary_action: async function({ company, pos_profile, balance_details }) { - if (!balance_details.length) { - frappe.show_alert({ - message: __("Please add Mode of payments and opening balance details."), - indicator: 'red' - }) - return frappe.utils.play_sound("error"); - } - - // filter balance details for empty rows - balance_details = balance_details.filter(d => d.mode_of_payment); - - const method = "posnext.posnext.page.posnext.point_of_sale.create_opening_voucher"; - const res = await frappe.call({ method, args: { pos_profile, company, balance_details }, freeze:true }); - !res.exc && me.prepare_app_defaults(res.message); - dialog.hide(); - }, - primary_action_label: __('Submit') - }); - dialog.show(); - const pos_profile_query = () => { - return { - query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query', - filters: { company: dialog.fields_dict.company.get_value() } - } - }; - } - - async prepare_app_defaults(data) { - this.pos_opening = data.name; - this.company = data.company; - this.pos_profile = data.pos_profile; - this.pos_opening_time = data.period_start_date; - this.item_stock_map = {}; - this.settings = {}; - window.current_pos_profile = this.pos_profile - frappe.db.get_value('Stock Settings', undefined, 'allow_negative_stock').then(({ message }) => { - this.allow_negative_stock = flt(message.allow_negative_stock) || false; - }); - - frappe.call({ - method: "posnext.posnext.page.posnext.point_of_sale.get_pos_profile_data", - args: { "pos_profile": this.pos_profile }, - callback: (res) => { - const profile = res.message; - - Object.assign(this.settings, profile); - this.settings.customer_groups = profile.customer_groups.map(group => group.name); - - this.make_app(); - } - }); - } - - set_opening_entry_status() { - this.page.set_title_sub( - `<span class="indicator orange"> + constructor(wrapper) { + console.log("CONTROLLLLLERE"); + this.wrapper = $(wrapper).find(".layout-main-section"); + this.page = wrapper.page; + frappe.run_serially([ + () => (this.reload_status = false), + () => this.check_opening_entry(""), + () => (this.reload_status = true), + ]); + + this.setup_form_events(); + } + setup_form_events() { + frappe.ui.form.on("Sales Invoice", { + after_save: function (frm) { + if (!frm.doc.pos_profile) return; + + frappe.db + .get_doc("POS Profile", frm.doc.pos_profile) + .then((pos_profile) => { + if (pos_profile.custom_stock_update) { + frm.set_value("update_stock", 0); + // frm.save(); + } + }); + }, + }); + } + + fetch_opening_entry(value) { + return frappe.call( + "posnext.posnext.page.posnext.point_of_sale.check_opening_entry", + { user: frappe.session.user, value: value }, + ); + } + + check_opening_entry(value = "") { + this.fetch_opening_entry(value).then((r) => { + if (r.message.length) { + // assuming only one opening voucher is available for the current user + this.prepare_app_defaults(r.message[0]); + } else { + this.create_opening_voucher(); + } + }); + } + + create_opening_voucher() { + const me = this; + const table_fields = [ + { + fieldname: "mode_of_payment", + fieldtype: "Link", + in_list_view: 1, + label: "Mode of Payment", + options: "Mode of Payment", + reqd: 1, + }, + { + fieldname: "opening_amount", + fieldtype: "Currency", + in_list_view: 1, + label: "Opening Amount", + options: "company:company_currency", + change: function () { + dialog.fields_dict.balance_details.df.data.some((d) => { + if (d.idx == this.doc.idx) { + d.opening_amount = this.value; + dialog.fields_dict.balance_details.grid.refresh(); + return true; + } + }); + }, + }, + ]; + const fetch_pos_payment_methods = () => { + const pos_profile = dialog.fields_dict.pos_profile.get_value(); + if (!pos_profile) return; + frappe.db.get_doc("POS Profile", pos_profile).then(({ payments }) => { + dialog.fields_dict.balance_details.df.data = []; + payments.forEach((pay) => { + const { mode_of_payment } = pay; + dialog.fields_dict.balance_details.df.data.push({ + mode_of_payment, + opening_amount: "0", + }); + }); + dialog.fields_dict.balance_details.grid.refresh(); + }); + }; + const dialog = new frappe.ui.Dialog({ + title: __("Create POS Opening Entry"), + static: true, + fields: [ + { + fieldtype: "Link", + label: __("Company"), + default: frappe.defaults.get_default("company"), + options: "Company", + fieldname: "company", + reqd: 1, + }, + { + fieldtype: "Link", + label: __("POS Profile"), + options: "POS Profile", + fieldname: "pos_profile", + reqd: 1, + get_query: () => pos_profile_query(), + onchange: () => fetch_pos_payment_methods(), + }, + { + fieldname: "balance_details", + fieldtype: "Table", + label: "Opening Balance Details", + cannot_add_rows: false, + in_place_edit: true, + reqd: 1, + data: [], + fields: table_fields, + }, + ], + primary_action: async function ({ + company, + pos_profile, + balance_details, + }) { + if (!balance_details.length) { + frappe.show_alert({ + message: __( + "Please add Mode of payments and opening balance details.", + ), + indicator: "red", + }); + return frappe.utils.play_sound("error"); + } + + // filter balance details for empty rows + balance_details = balance_details.filter((d) => d.mode_of_payment); + + const method = + "posnext.posnext.page.posnext.point_of_sale.create_opening_voucher"; + const res = await frappe.call({ + method, + args: { pos_profile, company, balance_details }, + freeze: true, + }); + !res.exc && me.prepare_app_defaults(res.message); + dialog.hide(); + }, + primary_action_label: __("Submit"), + }); + dialog.show(); + const pos_profile_query = () => { + return { + query: + "erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query", + filters: { company: dialog.fields_dict.company.get_value() }, + }; + }; + } + + async prepare_app_defaults(data) { + this.pos_opening = data.name; + this.company = data.company; + this.pos_profile = data.pos_profile; + this.pos_opening_time = data.period_start_date; + this.item_stock_map = {}; + this.settings = {}; + window.current_pos_profile = this.pos_profile; + frappe.db + .get_value("Stock Settings", undefined, "allow_negative_stock") + .then(({ message }) => { + this.allow_negative_stock = flt(message.allow_negative_stock) || false; + }); + + frappe.call({ + method: "posnext.posnext.page.posnext.point_of_sale.get_pos_profile_data", + args: { pos_profile: this.pos_profile }, + callback: (res) => { + const profile = res.message; + + Object.assign(this.settings, profile); + this.settings.customer_groups = profile.customer_groups.map( + (group) => group.name, + ); + + this.make_app(); + }, + }); + } + + set_opening_entry_status() { + this.page.set_title_sub( + `<span class="indicator orange"> <a class="text-muted" href="#Form/POS%20Opening%20Entry/${this.pos_opening}"> Opened at ${moment(this.pos_opening_time).format("Do MMMM, h:mma")} </a> - </span>`); - } - - make_app() { - this.prepare_dom(); - this.prepare_components(); - this.prepare_menu(); - this.make_new_invoice(); - } - - prepare_dom() { - this.wrapper.append( - `<div class="point-of-sale-app"></div>` - ); - - this.$components_wrapper = this.wrapper.find('.point-of-sale-app'); - } - - prepare_components() { - this.init_item_selector(); - this.init_item_details(); - this.init_item_cart(); - this.init_payments(); - this.init_recent_order_list(); - this.init_order_summary(); - } - - prepare_menu() { - this.page.clear_menu(); - if(this.settings.custom_show_open_form_view){ - this.page.add_menu_item(__("Open Form View"), this.open_form_view.bind(this), false, 'Ctrl+F'); - } - if(this.settings.custom_show_toggle_recent_orders) { - this.page.add_menu_item(__("Toggle Recent Orders"), this.toggle_recent_order.bind(this), false, 'Ctrl+O'); - } - if(this.settings.custom_show_save_as_draft) { - this.page.add_menu_item(__("Save as Draft"), this.save_draft_invoice.bind(this), false, 'Ctrl+S'); - } - if(this.settings.custom_show_close_the_pos) { - this.page.add_menu_item(__('Close the POS'), this.close_pos.bind(this), false, 'Shift+Ctrl+C'); - } - } - - open_form_view() { - frappe.model.sync(this.frm.doc); - frappe.set_route("Form", this.frm.doc.doctype, this.frm.doc.name); - } - - toggle_recent_order() { - const show = this.recent_order_list.$component.is(':hidden'); - this.toggle_recent_order_list(show); - } - - save_draft_invoice() { - if (!this.$components_wrapper.is(":visible")) return; - console.log(this.frm.doc.items) - if (this.frm.doc.items.length == 0) { - frappe.show_alert({ - message: __("You must add atleast one item to save it as draft."), - indicator:'red' - }); - frappe.utils.play_sound("error"); - return; - } - - this.frm.save(undefined, undefined, undefined, () => { - frappe.show_alert({ - message: __("There was an error saving the document."), - indicator: 'red' - }); - frappe.utils.play_sound("error"); - }).then(() => { - frappe.run_serially([ - () => frappe.dom.freeze(), - () => this.make_new_invoice(false), - () => frappe.dom.unfreeze() - - - ]); - - - - }); - } - - close_pos() { - if (!this.$components_wrapper.is(":visible")) return; - - let voucher = frappe.model.get_new_doc('POS Closing Entry'); - voucher.pos_profile = this.frm.doc.pos_profile; - voucher.user = frappe.session.user; - voucher.company = this.frm.doc.company; - voucher.pos_opening_entry = this.pos_opening; - voucher.period_end_date = frappe.datetime.now_datetime(); - voucher.posting_date = frappe.datetime.now_date(); - voucher.posting_time = frappe.datetime.now_time(); - frappe.set_route('Form', 'POS Closing Entry', voucher.name); - } - - init_item_selector() { - if(this.frm){ - this.frm.doc.set_warehouse = this.settings.warehouse - } - this.item_selector = new posnext.PointOfSale.ItemSelector({ - wrapper: this.$components_wrapper, - pos_profile: this.pos_profile, - settings: this.settings, - reload_status: this.reload_status, - currency: this.settings.currency, - events: { - check_opening_entry: () => this.check_opening_entry(), - item_selected: args => this.on_cart_update(args), - init_item_cart: () => this.init_item_cart(), - init_item_details: () => this.init_item_details(), - change_items: (args) => this.change_items(args), - get_frm: () => this.frm || {} - } - }) - } - change_items(items){ - var me = this - this.frm = items; - this.cart.load_invoice() - } - - init_item_cart() { - this.cart = new posnext.PointOfSale.ItemCart({ - wrapper: this.$components_wrapper, - settings: this.settings, - events: { - get_frm: () => this.frm, - remove_item_from_cart: (item) => { - this.item_details.current_item = item - this.item_details.name = item.name - this.item_details.doctype= item.doctype - - }, - form_updated: (item, field, value) => { - this.item_details.current_item = item - const item_row = frappe.model.get_doc(item.doctype, item.name); - if(field === 'qty' && this.frm.doc.is_return && value >=0){ - frappe.throw("Qty must be negative for return document" ) - } - if (item_row && item_row[field] != value) { - const args = { - field, - value, - item: this.item_details.current_item - }; - return this.on_cart_update(args); - } - - return Promise.resolve(); - }, - cart_item_clicked: (item) => { - - const item_row = this.get_item_from_frm(item); - - if(selected_item && selected_item['name'] == item['name']){ - selected_item = null - } else { - selected_item = item_row - } - this.item_details.toggle_item_details_section(item_row); - }, - - numpad_event: (value, action) => this.update_item_field(value, action), - - checkout: () => this.save_and_checkout(), - - edit_cart: () => this.payment.edit_cart(), - save_draft_invoice: () => this.save_draft_invoice(), - toggle_recent_order: () => this.toggle_recent_order(), - customer_details_updated: (details) => { - this.customer_details = details; - // will add/remove LP payment method - this.payment.render_loyalty_points_payment_mode(); - } - } - }) - } - - init_item_details() { - this.item_details = new posnext.PointOfSale.ItemDetails({ - wrapper: this.$components_wrapper, - settings: this.settings, - events: { - get_frm: () => this.frm, - - toggle_item_selector: (minimize) => { - this.item_selector.resize_selector(minimize); - this.cart.toggle_numpad(minimize); - }, - - form_updated: (item, field, value) => { - const item_row = frappe.model.get_doc(item.doctype, item.name); - if(field === 'qty' && this.frm.doc.is_return && value >=0){ - frappe.throw("Qty must be negative for return document" ) - } - if (item_row && item_row[field] != value) { - const args = { - field, - value, - item: this.item_details.current_item - }; - return this.on_cart_update(args); - } - - return Promise.resolve(); - }, - - highlight_cart_item: (item) => { - const cart_item = this.cart.get_cart_item(item); - this.cart.toggle_item_highlight(cart_item); - }, - - item_field_focused: (fieldname) => { - this.cart.toggle_numpad_field_edit(fieldname); - }, - set_value_in_current_cart_item: (selector, value) => { - this.cart.update_selector_value_in_cart_item(selector, value, this.item_details.current_item); - }, - clone_new_batch_item_in_frm: (batch_serial_map, item) => { - // called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches - // for each unique batch new item row is added in the form & cart - Object.keys(batch_serial_map).forEach(batch => { - const item_to_clone = this.frm.doc.items.find(i => i.name == item.name); - const new_row = this.frm.add_child("items", { ...item_to_clone }); - // update new serialno and batch - new_row.batch_no = batch; - new_row.serial_no = batch_serial_map[batch].join(`\n`); - new_row.qty = batch_serial_map[batch].length; - this.frm.doc.items.forEach(row => { - if (item.item_code === row.item_code) { - this.update_cart_html(row); - } - }); - }) - }, - remove_item_from_cart: () => this.remove_item_from_cart(), - get_item_stock_map: () => this.item_stock_map, - close_item_details: () => { - selected_item = null - this.item_details.toggle_item_details_section(null); - this.cart.prev_action = null; - this.cart.toggle_item_highlight(); - }, - get_available_stock: (item_code, warehouse) => this.get_available_stock(item_code, warehouse) - } - }); - if(selected_item){ - this.item_details.toggle_item_details_section(selected_item); - } - } - - init_payments() { - this.payment = new posnext.PointOfSale.Payment({ - wrapper: this.$components_wrapper, - settings: this.settings, - events: { - get_frm: () => this.frm || {}, - - get_customer_details: () => this.customer_details || {}, - - toggle_other_sections: (show) => { - if (show) { - this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : ''; - this.item_selector.toggle_component(false); - } else { - this.item_selector.toggle_component(true); - } - }, - - submit_invoice: () => { - this.frm.savesubmit() - .then((r) => { - this.toggle_components(false); - this.order_summary.toggle_component(true); - this.order_summary.load_summary_of(this.frm.doc, true); - frappe.show_alert({ - indicator: 'green', - message: __('POS invoice {0} created succesfully', [r.doc.name]) - }); - }); - } - } - }); - } - - init_recent_order_list() { - this.recent_order_list = new posnext.PointOfSale.PastOrderList({ - wrapper: this.$components_wrapper, - events: { - open_invoice_data: (name) => { - frappe.db.get_doc('Sales Invoice', name).then((doc) => { - this.order_summary.load_summary_of(doc); - }); - }, - reset_summary: () => this.order_summary.toggle_summary_placeholder(true), - previous_screen: () => { - this.recent_order_list.toggle_component(false); - this.cart.load_invoice() - this.item_selector.toggle_component(true) - this.wrapper.find('.past-order-summary').css("display","none"); - }, - - }, - settings: this.settings, - }) - } - - init_order_summary() { - this.order_summary = new posnext.PointOfSale.PastOrderSummary({ - wrapper: this.$components_wrapper, - pos_profile: this.settings, - events: { - get_frm: () => this.frm, - - process_return: (name) => { - this.recent_order_list.toggle_component(false); - frappe.db.get_doc('Sales Invoice', name).then((doc) => { - frappe.run_serially([ - () => this.make_return_invoice(doc), - () => this.cart.load_invoice(), - () => this.item_selector.toggle_component(true) - ]); - }); - }, - edit_order: (name) => { - console.log("Edit Order...") - this.recent_order_list.toggle_component(false); - frappe.run_serially([ - () => this.frm.refresh(name), - () => this.frm.call('reset_mode_of_payments'), - () => this.cart.load_invoice(), - () => this.item_selector.toggle_component(true) - ]); - }, - delete_order: (name) => { - frappe.model.delete_doc(this.frm.doc.doctype, name, () => { - this.recent_order_list.refresh_list(); - }); - }, - new_order: () => { - frappe.run_serially([ - () => frappe.dom.freeze(), - () => this.make_new_invoice(), - () => this.item_selector.toggle_component(true), - () => frappe.dom.unfreeze(), - ]); - } - } - }) - } - - toggle_recent_order_list(show) { - this.toggle_components(!show); - this.recent_order_list.toggle_component(show); - this.order_summary.toggle_component(show); - } - - toggle_components(show) { - this.cart.toggle_component(show); - this.item_selector.toggle_component(show); - - // do not show item details or payment if recent order is toggled off - !show ? (this.item_details.toggle_component(false) || this.payment.toggle_component(false)) : ''; - } - - make_new_invoice(from_held=false) { - if(from_held){ - return frappe.run_serially([ - () => frappe.dom.freeze(), - () => this.make_sales_invoice_frm(), - () => this.set_pos_profile_data(), - () => this.set_pos_profile_status(), - () => this.cart.load_invoice(), - () => frappe.dom.unfreeze(), - () => this.toggle_recent_order(), - ]); - } else { - return frappe.run_serially([ - () => frappe.dom.freeze(), - () => this.make_sales_invoice_frm(), - () => this.set_pos_profile_data(), - () => this.set_pos_profile_status(), - () => this.cart.load_invoice(), - () => frappe.dom.unfreeze(), - ]); - } - - } - - make_sales_invoice_frm() { - const doctype = 'Sales Invoice'; - return new Promise(resolve => { - if (this.frm) { - this.frm = this.get_new_frm(this.frm); - this.frm.doc.items = []; - this.frm.doc.is_pos = 1 - this.frm.doc.set_warehouse = this.settings.warehouse - resolve(); - } else { - frappe.model.with_doctype(doctype, () => { - this.frm = this.get_new_frm(); - this.frm.doc.items = []; - this.frm.doc.is_pos = 1 - this.frm.doc.set_warehouse = this.settings.warehouse - resolve(); - }); - } - }); - } - - get_new_frm(_frm) { - const doctype = 'Sales Invoice'; - const page = $('<div>'); - const frm = _frm || new frappe.ui.form.Form(doctype, page, false); - const name = frappe.model.make_new_doc_and_get_name(doctype, true); - frm.refresh(name); - - return frm; - } - - async make_return_invoice(doc) { - frappe.dom.freeze(); - this.frm = this.get_new_frm(this.frm); - this.frm.doc.items = []; - return frappe.call({ - method: "posnext.posnext.page.posnext.point_of_sale.make_sales_return", - args: { - 'source_name': doc.name, - 'target_doc': this.frm.doc - }, - callback: (r) => { - // console.log(r.message) - frappe.model.sync(r.message); - frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false; - this.set_pos_profile_data().then(() => { - frappe.dom.unfreeze(); - }); - } - }); - } - - set_pos_profile_data() { - if (this.company && !this.frm.doc.company) this.frm.doc.company = this.company; - if ((this.pos_profile && !this.frm.doc.pos_profile) | (this.frm.doc.is_return && this.pos_profile != this.frm.doc.pos_profile)) { - this.frm.doc.pos_profile = this.pos_profile; - } - - if (!this.frm.doc.company) return; - - return this.frm.trigger("set_pos_data"); - } - - set_pos_profile_status() { - this.page.set_indicator(this.pos_profile, "blue"); - } - - async on_cart_update(args) { - // frappe.dom.freeze(); - console.log("Updating Cart") - let item_row = undefined; - try { - let { field, value, item } = args; - item_row = this.get_item_from_frm(item); - const item_row_exists = !$.isEmptyObject(item_row); - - const from_selector = field === 'qty' && value === "+1"; - if (from_selector) - value = flt(item_row.stock_qty) + flt(value); - - if (item_row_exists) { - if (field === 'qty') - value = flt(value); - - if (['qty', 'conversion_factor'].includes(field) && value > 0 && !this.allow_negative_stock) { - const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value; - // await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); - } - - if (this.is_current_item_being_edited(item_row) || from_selector) { - await frappe.model.set_value(item_row.doctype, item_row.name, field, value) - // this.update_cart_html(item_row); - } - - } else { - if (!this.frm.doc.customer && !this.settings.custom_mobile_number_based_customer){ - return this.raise_customer_selection_alert(); - } - frappe.flags.ignore_company_party_validation = true - const { item_code, batch_no, serial_no, rate, uom, valuation_rate, custom_item_uoms, custom_logical_rack } = item; - if (!item_code) - return; - - if (this.settings.custom_product_bundle) { - const product_bundle = await this.get_product_bundle(item_code); - if (product_bundle && Array.isArray(product_bundle.items)) { - const bundle_items = product_bundle.items.map(bundle_item => ({ - item_code: bundle_item.item_code, - qty: bundle_item.qty * value, - rate: bundle_item.rate, - uom: bundle_item.uom, - custom_bundle_id: product_bundle.name - })); - - for (const bundle_item of bundle_items) { - const bundle_item_row = this.frm.add_child('items', bundle_item); - await this.trigger_new_item_events(bundle_item_row); - } - - this.update_cart_html(); - return; - } - } - - const new_item = { item_code, batch_no, rate, uom, [field]: value }; - if(value){ - new_item['qty'] = value - } - if (serial_no) { - await this.check_serial_no_availablilty(item_code, this.frm.doc.set_warehouse, serial_no); - new_item['serial_no'] = serial_no; - } - - if (field === 'serial_no') - new_item['qty'] = value.split(`\n`).length || 0; - item_row = this.frm.add_child('items', new_item); - - await this.trigger_new_item_events(item_row); - // item_row['rate'] = rate - // item_row['valuation_rate'] = valuation_rate; - // item_row['custom_valuation_rate'] = valuation_rate; - item_row['custom_item_uoms'] = custom_item_uoms; - item_row['custom_logical_rack'] = custom_logical_rack; - // this.update_cart_html(item_row); - if (this.item_details.$component.is(':visible')) - this.edit_item_details_of(item_row); - - if (this.check_serial_batch_selection_needed(item_row) && !this.item_details.$component.is(':visible')) - this.edit_item_details_of(item_row); - } - - } catch (error) { - console.log(error); - } finally { - // frappe.dom.unfreeze(); - - var total_incoming_rate = 0 - this.frm.doc.items.forEach(item => { - total_incoming_rate += (parseFloat(item.valuation_rate) * item.qty) - }); - this.item_selector.update_total_incoming_rate(total_incoming_rate) - - return item_row; // eslint-disable-line no-unsafe-finally - } - } - - raise_customer_selection_alert() { - frappe.dom.unfreeze(); - frappe.show_alert({ - message: __('You must select a customer before adding an item.'), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); - } - async get_product_bundle(item_code) { - const response = await frappe.call({ - method: "posnext.doc_events.item.get_product_bundle_with_items", - args: { - item_code: item_code - } - }); - return response.message; - } - - get_item_from_frm({ name, item_code, batch_no, uom, rate }) { - let item_row = null; - - if (name) { - item_row = this.frm.doc.items.find(i => i.name == name); - } else { - // if item is clicked twice from item selector - // then "item_code, batch_no, uom, rate" will help in getting the exact item - // to increase the qty by one - for (var i = 0; i < cur_frm.doc.items.length; i += 1) { - const has_batch_no = (batch_no !== 'null' && batch_no !== null); - const batch_no_check = this.settings.custom_allow_add_new_items_on_new_line - ? (has_batch_no && cur_frm.doc.items[i].batch_no === batch_no) - : true; - - if ( - cur_frm.doc.items[i].item_code === item_code && - cur_frm.doc.items[i].uom === uom && - parseFloat(cur_frm.doc.items[i].rate) === parseFloat(rate) && - batch_no_check - ) { - item_row = cur_frm.doc.items[i]; - break; - } - } - console.log(item_row); - } - return item_row || {}; - } - - - edit_item_details_of(item_row) { - this.item_details.toggle_item_details_section(item_row); - } - - is_current_item_being_edited(item_row) { - return item_row.name == this.item_details.current_item.name; - } - - update_cart_html(item_row, remove_item) { - this.cart.update_item_html(item_row, remove_item); - - this.cart.update_totals_section(this.frm); - - } - - check_serial_batch_selection_needed(item_row) { - // right now item details is shown for every type of item. - // if item details is not shown for every item then this fn will be needed - const serialized = item_row.has_serial_no; - const batched = item_row.has_batch_no; - const no_serial_selected = !item_row.serial_no; - const no_batch_selected = !item_row.batch_no; - - if ((serialized && no_serial_selected) || (batched && no_batch_selected) || - (serialized && batched && (no_batch_selected || no_serial_selected))) { - return true; - } - return false; - } - - async trigger_new_item_events(item_row) { - await this.frm.script_manager.trigger('item_code', item_row.doctype, item_row.name); - await this.frm.script_manager.trigger('qty', item_row.doctype, item_row.name); - await this.frm.script_manager.trigger('discount_percentage', item_row.doctype, item_row.name); - } - - async check_stock_availability(item_row, qty_needed, warehouse) { - const resp = (await this.get_available_stock(item_row.item_code, warehouse)).message; - const available_qty = resp[0]; - const is_stock_item = resp[1]; - - frappe.dom.unfreeze(); - const bold_uom = item_row.uom.bold(); - const bold_item_code = item_row.item_code.bold(); - const bold_warehouse = warehouse.bold(); - const bold_available_qty = available_qty.toString().bold() - if (!(available_qty > 0)) { - if (is_stock_item) { - frappe.model.clear_doc(item_row.doctype, item_row.name); - frappe.throw({ - title: __("Not Available"), - message: __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) - }); - } else { - return; - } - } else if (is_stock_item && available_qty < qty_needed) { - frappe.throw({ - message: __('Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.', [bold_item_code, bold_warehouse, bold_available_qty, bold_uom]), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); - } - frappe.dom.freeze(); - } - - async check_serial_no_availablilty(item_code, warehouse, serial_no) { - const method = "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos"; - const args = {filters: { item_code, warehouse }} - const res = await frappe.call({ method, args }); - - if (res.message.includes(serial_no)) { - frappe.throw({ - title: __("Not Available"), - message: __('Serial No: {0} has already been transacted into another Sales Invoice.', [serial_no.bold()]) - }); - } - } - - get_available_stock(item_code, warehouse) { - const me = this; - return frappe.call({ - method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability", - args: { - 'item_code': item_code, - 'warehouse': warehouse, - }, - callback(res) { - if (!me.item_stock_map[item_code]) - me.item_stock_map[item_code] = {}; - me.item_stock_map[item_code][warehouse] = res.message; - } - }); - } - - update_item_field(value, field_or_action) { - if (field_or_action === 'checkout') { - this.item_details.toggle_item_details_section(null); - } else if (field_or_action === 'remove') { - this.remove_item_from_cart(); - } else { - const field_control = this.item_details[`${field_or_action}_control`]; - if (!field_control) return; - field_control.set_focus(); - value != "" && field_control.set_value(value); - } - } - - remove_item_from_cart() { - frappe.dom.freeze(); - const { doctype, name, current_item } = this.item_details; - return frappe.model.set_value(doctype, name, 'qty', 0) - .then(() => { - frappe.model.clear_doc(doctype, name); - this.update_cart_html(current_item, true); - this.item_details.toggle_item_details_section(null); - frappe.dom.unfreeze(); - - var total_incoming_rate = 0 - this.frm.doc.items.forEach(item => { - total_incoming_rate += (parseFloat(item.valuation_rate) * item.qty) - }); - this.item_selector.update_total_incoming_rate(total_incoming_rate) - }) - .catch(e => console.log(e)); - } - - async save_and_checkout() { - if (this.frm.is_dirty()) { - const div = document.getElementById("customer-cart-container2"); - div.style.gridColumn = ""; - let save_error = false; - await this.frm.save(null, null, null, () => save_error = true); - // only move to payment section if save is successful - !save_error && this.payment.checkout(); - // show checkout button on error - save_error && setTimeout(() => { - this.cart.toggle_checkout_btn(true); - }, 300); // wait for save to finish - } else { - this.payment.checkout(); - } - } - async save_and_checkout() { - if (!this.frm.doc.items || this.frm.doc.items.length === 0) { - frappe.show_alert({ - message: __('Please add items to cart before checkout.'), - indicator: 'red' - }); - frappe.utils.play_sound("error"); - return; - } - if (this.frm.is_dirty()) { - if(this.settings.custom_add_reference_details){ - const dialog = new frappe.ui.Dialog({ - title: __('Enter Reference Details'), - fields: [ - { - fieldtype: 'Data', - label: __('Reference Number'), - fieldname: 'reference_no', - }, - { - fieldtype: 'Data', - label: __('Reference Name'), - fieldname: 'reference_name', - } - ], - primary_action_label: __('Proceed to Payment'), - primary_action: async (values) => { - this.frm.doc.custom_reference_no = values.reference_no; - this.frm.doc.custom_reference_name = values.reference_name; - - const div = document.getElementById("customer-cart-container2"); - div.style.gridColumn = ""; - - let save_error = false; - await this.frm.save(null, null, null, () => save_error = true); - - dialog.hide(); - - if (!save_error) { - this.payment.checkout(); - } else { - setTimeout(() => { - this.cart.toggle_checkout_btn(true); - }, 300); // wait for save to finish - } - } - }); - - - dialog.show(); - }else{ - - const div = document.getElementById("customer-cart-container2"); - div.style.gridColumn = ""; - let save_error = false; - await this.frm.save(null, null, null, () => save_error = true); - // only move to payment section if save is successful - !save_error && this.payment.checkout(); - // show checkout button on error - save_error && setTimeout(() => { - this.cart.toggle_checkout_btn(true); - }, 300); // wait for save to finish - } - - - - } else { - this.payment.checkout(); - } - } + </span>`, + ); + } + + make_app() { + this.prepare_dom(); + this.prepare_components(); + this.prepare_menu(); + this.make_new_invoice(); + } + + prepare_dom() { + this.wrapper.append(`<div class="point-of-sale-app"></div>`); + + this.$components_wrapper = this.wrapper.find(".point-of-sale-app"); + } + + prepare_components() { + this.init_item_selector(); + this.init_item_details(); + this.init_item_cart(); + this.init_payments(); + this.init_recent_order_list(); + this.init_order_summary(); + } + + prepare_menu() { + this.page.clear_menu(); + if (this.settings.custom_show_open_form_view) { + this.page.add_menu_item( + __("Open Form View"), + this.open_form_view.bind(this), + false, + "Ctrl+F", + ); + } + if (this.settings.custom_show_toggle_recent_orders) { + this.page.add_menu_item( + __("Toggle Recent Orders"), + this.toggle_recent_order.bind(this), + false, + "Ctrl+O", + ); + } + if (this.settings.custom_show_save_as_draft) { + this.page.add_menu_item( + __("Save as Draft"), + this.save_draft_invoice.bind(this), + false, + "Ctrl+S", + ); + } + if (this.settings.custom_show_close_the_pos) { + this.page.add_menu_item( + __("Close the POS"), + this.close_pos.bind(this), + false, + "Shift+Ctrl+C", + ); + } + } + + open_form_view() { + frappe.model.sync(this.frm.doc); + frappe.set_route("Form", this.frm.doc.doctype, this.frm.doc.name); + } + + toggle_recent_order() { + const show = this.recent_order_list.$component.is(":hidden"); + this.toggle_recent_order_list(show); + } + + save_draft_invoice() { + if (!this.$components_wrapper.is(":visible")) return; + console.log(this.frm.doc.items); + if (this.frm.doc.items.length == 0) { + frappe.show_alert({ + message: __("You must add atleast one item to save it as draft."), + indicator: "red", + }); + frappe.utils.play_sound("error"); + return; + } + + this.frm + .save(undefined, undefined, undefined, () => { + frappe.show_alert({ + message: __("There was an error saving the document."), + indicator: "red", + }); + frappe.utils.play_sound("error"); + }) + .then(() => { + frappe.run_serially([ + () => frappe.dom.freeze(), + () => this.make_new_invoice(false), + () => frappe.dom.unfreeze(), + ]); + }); + } + + close_pos() { + if (!this.$components_wrapper.is(":visible")) return; + + let voucher = frappe.model.get_new_doc("POS Closing Entry"); + voucher.pos_profile = this.frm.doc.pos_profile; + voucher.user = frappe.session.user; + voucher.company = this.frm.doc.company; + voucher.pos_opening_entry = this.pos_opening; + voucher.period_end_date = frappe.datetime.now_datetime(); + voucher.posting_date = frappe.datetime.now_date(); + voucher.posting_time = frappe.datetime.now_time(); + frappe.set_route("Form", "POS Closing Entry", voucher.name); + } + + init_item_selector() { + if (this.frm) { + this.frm.doc.set_warehouse = this.settings.warehouse; + } + this.item_selector = new posnext.PointOfSale.ItemSelector({ + wrapper: this.$components_wrapper, + pos_profile: this.pos_profile, + settings: this.settings, + reload_status: this.reload_status, + currency: this.settings.currency, + events: { + check_opening_entry: () => this.check_opening_entry(), + item_selected: (args) => this.on_cart_update(args), + init_item_cart: () => this.init_item_cart(), + init_item_details: () => this.init_item_details(), + change_items: (args) => this.change_items(args), + get_frm: () => this.frm || {}, + }, + }); + } + change_items(items) { + var me = this; + this.frm = items; + this.cart.load_invoice(); + } + + init_item_cart() { + this.cart = new posnext.PointOfSale.ItemCart({ + wrapper: this.$components_wrapper, + settings: this.settings, + events: { + get_frm: () => this.frm, + remove_item_from_cart: (item) => { + this.item_details.current_item = item; + this.item_details.name = item.name; + this.item_details.doctype = item.doctype; + }, + form_updated: (item, field, value) => { + this.item_details.current_item = item; + const item_row = frappe.model.get_doc(item.doctype, item.name); + if (field === "qty" && this.frm.doc.is_return && value >= 0) { + frappe.throw(__("Qty must be negative for return document")); + } + if (item_row && item_row[field] != value) { + const args = { + field, + value, + item: this.item_details.current_item, + }; + return this.on_cart_update(args); + } + + return Promise.resolve(); + }, + cart_item_clicked: (item) => { + const item_row = this.get_item_from_frm(item); + + if (selected_item && selected_item["name"] == item["name"]) { + selected_item = null; + } else { + selected_item = item_row; + } + this.item_details.toggle_item_details_section(item_row); + }, + + numpad_event: (value, action) => this.update_item_field(value, action), + + checkout: () => this.save_and_checkout(), + + edit_cart: () => this.payment.edit_cart(), + save_draft_invoice: () => this.save_draft_invoice(), + toggle_recent_order: () => this.toggle_recent_order(), + customer_details_updated: (details) => { + this.customer_details = details; + // will add/remove LP payment method + this.payment.render_loyalty_points_payment_mode(); + }, + }, + }); + } + + init_item_details() { + this.item_details = new posnext.PointOfSale.ItemDetails({ + wrapper: this.$components_wrapper, + settings: this.settings, + events: { + get_frm: () => this.frm, + + toggle_item_selector: (minimize) => { + this.item_selector.resize_selector(minimize); + this.cart.toggle_numpad(minimize); + }, + + form_updated: (item, field, value) => { + const item_row = frappe.model.get_doc(item.doctype, item.name); + if (field === "qty" && this.frm.doc.is_return && value >= 0) { + frappe.throw(__("Qty must be negative for return document")); + } + if (item_row && item_row[field] != value) { + const args = { + field, + value, + item: this.item_details.current_item, + }; + return this.on_cart_update(args); + } + + return Promise.resolve(); + }, + + highlight_cart_item: (item) => { + const cart_item = this.cart.get_cart_item(item); + this.cart.toggle_item_highlight(cart_item); + }, + + item_field_focused: (fieldname) => { + this.cart.toggle_numpad_field_edit(fieldname); + }, + set_value_in_current_cart_item: (selector, value) => { + this.cart.update_selector_value_in_cart_item( + selector, + value, + this.item_details.current_item, + ); + }, + clone_new_batch_item_in_frm: (batch_serial_map, item) => { + // called if serial nos are 'auto_selected' and if those serial nos belongs to multiple batches + // for each unique batch new item row is added in the form & cart + Object.keys(batch_serial_map).forEach((batch) => { + const item_to_clone = this.frm.doc.items.find( + (i) => i.name == item.name, + ); + const new_row = this.frm.add_child("items", { ...item_to_clone }); + // update new serialno and batch + new_row.batch_no = batch; + new_row.serial_no = batch_serial_map[batch].join(`\n`); + new_row.qty = batch_serial_map[batch].length; + this.frm.doc.items.forEach((row) => { + if (item.item_code === row.item_code) { + this.update_cart_html(row); + } + }); + }); + }, + remove_item_from_cart: () => this.remove_item_from_cart(), + get_item_stock_map: () => this.item_stock_map, + close_item_details: () => { + selected_item = null; + this.item_details.toggle_item_details_section(null); + this.cart.prev_action = null; + this.cart.toggle_item_highlight(); + }, + get_available_stock: (item_code, warehouse) => + this.get_available_stock(item_code, warehouse), + }, + }); + if (selected_item) { + this.item_details.toggle_item_details_section(selected_item); + } + } + + init_payments() { + this.payment = new posnext.PointOfSale.Payment({ + wrapper: this.$components_wrapper, + settings: this.settings, + events: { + get_frm: () => this.frm || {}, + + get_customer_details: () => this.customer_details || {}, + + toggle_other_sections: (show) => { + if (show) { + this.item_details.$component.is(":visible") + ? this.item_details.$component.css("display", "none") + : ""; + this.item_selector.toggle_component(false); + } else { + this.item_selector.toggle_component(true); + } + }, + + submit_invoice: () => { + this.frm.savesubmit().then((r) => { + this.toggle_components(false); + this.order_summary.toggle_component(true); + this.order_summary.load_summary_of(this.frm.doc, true); + frappe.show_alert({ + indicator: "green", + message: __("POS invoice {0} created succesfully", [r.doc.name]), + }); + }); + }, + }, + }); + } + + init_recent_order_list() { + this.recent_order_list = new posnext.PointOfSale.PastOrderList({ + wrapper: this.$components_wrapper, + events: { + open_invoice_data: (name) => { + frappe.db.get_doc("Sales Invoice", name).then((doc) => { + this.order_summary.load_summary_of(doc); + }); + }, + reset_summary: () => + this.order_summary.toggle_summary_placeholder(true), + previous_screen: () => { + this.recent_order_list.toggle_component(false); + this.cart.load_invoice(); + this.item_selector.toggle_component(true); + this.wrapper.find(".past-order-summary").css("display", "none"); + }, + }, + settings: this.settings, + }); + } + + init_order_summary() { + this.order_summary = new posnext.PointOfSale.PastOrderSummary({ + wrapper: this.$components_wrapper, + pos_profile: this.settings, + events: { + get_frm: () => this.frm, + + process_return: (name) => { + this.recent_order_list.toggle_component(false); + frappe.db.get_doc("Sales Invoice", name).then((doc) => { + frappe.run_serially([ + () => this.make_return_invoice(doc), + () => this.cart.load_invoice(), + () => this.item_selector.toggle_component(true), + ]); + }); + }, + edit_order: (name) => { + console.log("Edit Order..."); + this.recent_order_list.toggle_component(false); + frappe.run_serially([ + () => this.frm.refresh(name), + () => this.frm.call("reset_mode_of_payments"), + () => this.cart.load_invoice(), + () => this.item_selector.toggle_component(true), + ]); + }, + delete_order: (name) => { + frappe.model.delete_doc(this.frm.doc.doctype, name, () => { + this.recent_order_list.refresh_list(); + }); + }, + new_order: () => { + frappe.run_serially([ + () => frappe.dom.freeze(), + () => this.make_new_invoice(), + () => this.item_selector.toggle_component(true), + () => frappe.dom.unfreeze(), + ]); + }, + }, + }); + } + + toggle_recent_order_list(show) { + this.toggle_components(!show); + this.recent_order_list.toggle_component(show); + this.order_summary.toggle_component(show); + } + + toggle_components(show) { + this.cart.toggle_component(show); + this.item_selector.toggle_component(show); + + // do not show item details or payment if recent order is toggled off + !show + ? this.item_details.toggle_component(false) || + this.payment.toggle_component(false) + : ""; + } + + make_new_invoice(from_held = false) { + if (from_held) { + return frappe.run_serially([ + () => frappe.dom.freeze(), + () => this.make_sales_invoice_frm(), + () => this.set_pos_profile_data(), + () => this.set_pos_profile_status(), + () => this.cart.load_invoice(), + () => frappe.dom.unfreeze(), + () => this.toggle_recent_order(), + ]); + } else { + return frappe.run_serially([ + () => frappe.dom.freeze(), + () => this.make_sales_invoice_frm(), + () => this.set_pos_profile_data(), + () => this.set_pos_profile_status(), + () => this.cart.load_invoice(), + () => frappe.dom.unfreeze(), + ]); + } + } + + make_sales_invoice_frm() { + const doctype = "Sales Invoice"; + return new Promise((resolve) => { + if (this.frm) { + this.frm = this.get_new_frm(this.frm); + this.frm.doc.items = []; + this.frm.doc.is_pos = 1; + this.frm.doc.set_warehouse = this.settings.warehouse; + resolve(); + } else { + frappe.model.with_doctype(doctype, () => { + this.frm = this.get_new_frm(); + this.frm.doc.items = []; + this.frm.doc.is_pos = 1; + this.frm.doc.set_warehouse = this.settings.warehouse; + resolve(); + }); + } + }); + } + + get_new_frm(_frm) { + const doctype = "Sales Invoice"; + const page = $("<div>"); + const frm = _frm || new frappe.ui.form.Form(doctype, page, false); + const name = frappe.model.make_new_doc_and_get_name(doctype, true); + frm.refresh(name); + + return frm; + } + + async make_return_invoice(doc) { + frappe.dom.freeze(); + this.frm = this.get_new_frm(this.frm); + this.frm.doc.items = []; + return frappe.call({ + method: "posnext.posnext.page.posnext.point_of_sale.make_sales_return", + args: { + source_name: doc.name, + target_doc: this.frm.doc, + }, + callback: (r) => { + // console.log(r.message) + frappe.model.sync(r.message); + frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = + false; + this.set_pos_profile_data().then(() => { + frappe.dom.unfreeze(); + }); + }, + }); + } + + set_pos_profile_data() { + if (this.company && !this.frm.doc.company) + this.frm.doc.company = this.company; + if ( + (this.pos_profile && !this.frm.doc.pos_profile) | + (this.frm.doc.is_return && this.pos_profile != this.frm.doc.pos_profile) + ) { + this.frm.doc.pos_profile = this.pos_profile; + } + + if (!this.frm.doc.company) return; + + return this.frm.trigger("set_pos_data"); + } + + set_pos_profile_status() { + this.page.set_indicator(this.pos_profile, "blue"); + } + + async on_cart_update(args) { + // frappe.dom.freeze(); + console.log("Updating Cart"); + let item_row = undefined; + try { + let { field, value, item } = args; + item_row = this.get_item_from_frm(item); + const item_row_exists = !$.isEmptyObject(item_row); + + const from_selector = field === "qty" && value === "+1"; + if (from_selector) value = flt(item_row.stock_qty) + flt(value); + + if (item_row_exists) { + if (field === "qty") value = flt(value); + + if ( + ["qty", "conversion_factor"].includes(field) && + value > 0 && + !this.allow_negative_stock + ) { + const qty_needed = + field === "qty" + ? value * item_row.conversion_factor + : item_row.qty * value; + // await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse); + } + + if (this.is_current_item_being_edited(item_row) || from_selector) { + await frappe.model.set_value( + item_row.doctype, + item_row.name, + field, + value, + ); + // this.update_cart_html(item_row); + } + } else { + if ( + !this.frm.doc.customer && + !this.settings.custom_mobile_number_based_customer + ) { + return this.raise_customer_selection_alert(); + } + frappe.flags.ignore_company_party_validation = true; + const { + item_code, + batch_no, + serial_no, + rate, + uom, + valuation_rate, + custom_item_uoms, + custom_logical_rack, + } = item; + if (!item_code) return; + + if (this.settings.custom_product_bundle) { + const product_bundle = await this.get_product_bundle(item_code); + if (product_bundle && Array.isArray(product_bundle.items)) { + const bundle_items = product_bundle.items.map((bundle_item) => ({ + item_code: bundle_item.item_code, + qty: bundle_item.qty * value, + rate: bundle_item.rate, + uom: bundle_item.uom, + custom_bundle_id: product_bundle.name, + })); + + for (const bundle_item of bundle_items) { + const bundle_item_row = this.frm.add_child("items", bundle_item); + await this.trigger_new_item_events(bundle_item_row); + } + + this.update_cart_html(); + return; + } + } + + const new_item = { item_code, batch_no, rate, uom, [field]: value }; + if (value) { + new_item["qty"] = value; + } + if (serial_no) { + await this.check_serial_no_availablilty( + item_code, + this.frm.doc.set_warehouse, + serial_no, + ); + new_item["serial_no"] = serial_no; + } + + if (field === "serial_no") + new_item["qty"] = value.split(`\n`).length || 0; + item_row = this.frm.add_child("items", new_item); + + await this.trigger_new_item_events(item_row); + // item_row['rate'] = rate + // item_row['valuation_rate'] = valuation_rate; + // item_row['custom_valuation_rate'] = valuation_rate; + item_row["custom_item_uoms"] = custom_item_uoms; + item_row["custom_logical_rack"] = custom_logical_rack; + // this.update_cart_html(item_row); + if (this.item_details.$component.is(":visible")) + this.edit_item_details_of(item_row); + + if ( + this.check_serial_batch_selection_needed(item_row) && + !this.item_details.$component.is(":visible") + ) + this.edit_item_details_of(item_row); + } + } catch (error) { + console.log(error); + } finally { + // frappe.dom.unfreeze(); + + var total_incoming_rate = 0; + this.frm.doc.items.forEach((item) => { + total_incoming_rate += parseFloat(item.valuation_rate) * item.qty; + }); + this.item_selector.update_total_incoming_rate(total_incoming_rate); + + return item_row; // eslint-disable-line no-unsafe-finally + } + } + + raise_customer_selection_alert() { + frappe.dom.unfreeze(); + frappe.show_alert({ + message: __("You must select a customer before adding an item."), + indicator: "orange", + }); + frappe.utils.play_sound("error"); + } + async get_product_bundle(item_code) { + const response = await frappe.call({ + method: "posnext.doc_events.item.get_product_bundle_with_items", + args: { + item_code: item_code, + }, + }); + return response.message; + } + + get_item_from_frm({ name, item_code, batch_no, uom, rate }) { + let item_row = null; + + if (name) { + item_row = this.frm.doc.items.find((i) => i.name == name); + } else { + // if item is clicked twice from item selector + // then "item_code, batch_no, uom, rate" will help in getting the exact item + // to increase the qty by one + // prettier-ignore + for (var i = 0; i < cur_frm.doc.items.length; i += 1) { // nosemgrep + const has_batch_no = batch_no !== "null" && batch_no !== null; + const batch_no_check = this.settings + .custom_allow_add_new_items_on_new_line + ? has_batch_no && cur_frm.doc.items[i].batch_no === batch_no // nosemgrep Overrides erpnext code + : true; + + if ( + cur_frm.doc.items[i].item_code === item_code && // nosemgrep Overrides erpnext code + cur_frm.doc.items[i].uom === uom && // nosemgrep Overrides erpnext code + parseFloat(cur_frm.doc.items[i].rate) === parseFloat(rate) && // nosemgrep Overrides erpnext code + batch_no_check + ) { + item_row = cur_frm.doc.items[i]; // nosemgrep Overrides erpnext code + break; + } + } + console.log(item_row); + } + return item_row || {}; + } + + edit_item_details_of(item_row) { + this.item_details.toggle_item_details_section(item_row); + } + + is_current_item_being_edited(item_row) { + return item_row.name == this.item_details.current_item.name; + } + + update_cart_html(item_row, remove_item) { + this.cart.update_item_html(item_row, remove_item); + + this.cart.update_totals_section(this.frm); + } + + check_serial_batch_selection_needed(item_row) { + // right now item details is shown for every type of item. + // if item details is not shown for every item then this fn will be needed + const serialized = item_row.has_serial_no; + const batched = item_row.has_batch_no; + const no_serial_selected = !item_row.serial_no; + const no_batch_selected = !item_row.batch_no; + + if ( + (serialized && no_serial_selected) || + (batched && no_batch_selected) || + (serialized && batched && (no_batch_selected || no_serial_selected)) + ) { + return true; + } + return false; + } + + async trigger_new_item_events(item_row) { + await this.frm.script_manager.trigger( + "item_code", + item_row.doctype, + item_row.name, + ); + await this.frm.script_manager.trigger( + "qty", + item_row.doctype, + item_row.name, + ); + await this.frm.script_manager.trigger( + "discount_percentage", + item_row.doctype, + item_row.name, + ); + } + + async check_stock_availability(item_row, qty_needed, warehouse) { + const resp = (await this.get_available_stock(item_row.item_code, warehouse)) + .message; + const available_qty = resp[0]; + const is_stock_item = resp[1]; + + frappe.dom.unfreeze(); + const bold_uom = item_row.uom.bold(); + const bold_item_code = item_row.item_code.bold(); + const bold_warehouse = warehouse.bold(); + const bold_available_qty = available_qty.toString().bold(); + if (!(available_qty > 0)) { + if (is_stock_item) { + frappe.model.clear_doc(item_row.doctype, item_row.name); + frappe.throw({ + title: __("Not Available"), + message: __("Item Code: {0} is not available under warehouse {1}.", [ + bold_item_code, + bold_warehouse, + ]), + }); + } else { + return; + } + } else if (is_stock_item && available_qty < qty_needed) { + frappe.throw({ + message: __( + "Stock quantity not enough for Item Code: {0} under warehouse {1}. Available quantity {2} {3}.", + [bold_item_code, bold_warehouse, bold_available_qty, bold_uom], + ), + indicator: "orange", + }); + frappe.utils.play_sound("error"); + } + frappe.dom.freeze(); + } + + async check_serial_no_availablilty(item_code, warehouse, serial_no) { + const method = + "erpnext.stock.doctype.serial_no.serial_no.get_pos_reserved_serial_nos"; + const args = { filters: { item_code, warehouse } }; + const res = await frappe.call({ method, args }); + + if (res.message.includes(serial_no)) { + frappe.throw({ + title: __("Not Available"), + message: __( + "Serial No: {0} has already been transacted into another Sales Invoice.", + [serial_no.bold()], + ), + }); + } + } + + get_available_stock(item_code, warehouse) { + const me = this; + return frappe.call({ + method: + "erpnext.accounts.doctype.pos_invoice.pos_invoice.get_stock_availability", + args: { + item_code: item_code, + warehouse: warehouse, + }, + callback(res) { + if (!me.item_stock_map[item_code]) me.item_stock_map[item_code] = {}; + me.item_stock_map[item_code][warehouse] = res.message; + }, + }); + } + + update_item_field(value, field_or_action) { + if (field_or_action === "checkout") { + this.item_details.toggle_item_details_section(null); + } else if (field_or_action === "remove") { + this.remove_item_from_cart(); + } else { + const field_control = this.item_details[`${field_or_action}_control`]; + if (!field_control) return; + field_control.set_focus(); + value != "" && field_control.set_value(value); + } + } + + remove_item_from_cart() { + frappe.dom.freeze(); + const { doctype, name, current_item } = this.item_details; + return frappe.model + .set_value(doctype, name, "qty", 0) + .then(() => { + frappe.model.clear_doc(doctype, name); + this.update_cart_html(current_item, true); + this.item_details.toggle_item_details_section(null); + frappe.dom.unfreeze(); + + var total_incoming_rate = 0; + this.frm.doc.items.forEach((item) => { + total_incoming_rate += parseFloat(item.valuation_rate) * item.qty; + }); + this.item_selector.update_total_incoming_rate(total_incoming_rate); + }) + .catch((e) => console.log(e)); + } + + // async save_and_checkout() { + // if (this.frm.is_dirty()) { + // const div = document.getElementById("customer-cart-container2"); + // div.style.gridColumn = ""; + // let save_error = false; + // await this.frm.save(null, null, null, () => (save_error = true)); + // // only move to payment section if save is successful + // !save_error && this.payment.checkout(); + // // show checkout button on error + // save_error && + // setTimeout(() => { + // this.cart.toggle_checkout_btn(true); + // }, 300); // wait for save to finish + // } else { + // this.payment.checkout(); + // } + // } + async save_and_checkout() { + if (!this.frm.doc.items || this.frm.doc.items.length === 0) { + frappe.show_alert({ + message: __("Please add items to cart before checkout."), + indicator: "red", + }); + frappe.utils.play_sound("error"); + return; + } + if (this.frm.is_dirty()) { + if (this.settings.custom_add_reference_details) { + const dialog = new frappe.ui.Dialog({ + title: __("Enter Reference Details"), + fields: [ + { + fieldtype: "Data", + label: __("Reference Number"), + fieldname: "reference_no", + }, + { + fieldtype: "Data", + label: __("Reference Name"), + fieldname: "reference_name", + }, + ], + primary_action_label: __("Proceed to Payment"), + primary_action: async (values) => { + this.frm.doc.custom_reference_no = values.reference_no; + this.frm.doc.custom_reference_name = values.reference_name; + + const div = document.getElementById("customer-cart-container2"); + div.style.gridColumn = ""; + + let save_error = false; + await this.frm.save(null, null, null, () => (save_error = true)); + + dialog.hide(); + + if (!save_error) { + this.payment.checkout(); + } else { + setTimeout(() => { + this.cart.toggle_checkout_btn(true); + }, 300); // wait for save to finish + } + }, + }); + + dialog.show(); + } else { + const div = document.getElementById("customer-cart-container2"); + div.style.gridColumn = ""; + let save_error = false; + await this.frm.save(null, null, null, () => (save_error = true)); + // only move to payment section if save is successful + !save_error && this.payment.checkout(); + // show checkout button on error + save_error && + setTimeout(() => { + this.cart.toggle_checkout_btn(true); + }, 300); // wait for save to finish + } + } else { + this.payment.checkout(); + } + } }; diff --git a/posnext/public/js/pos_item_cart.js b/posnext/public/js/pos_item_cart.js index d0aabe1..4c19d20 100644 --- a/posnext/public/js/pos_item_cart.js +++ b/posnext/public/js/pos_item_cart.js @@ -1,193 +1,208 @@ -frappe.provide('posnext.PointOfSale'); +frappe.provide("posnext.PointOfSale"); posnext.PointOfSale.ItemCart = class { - constructor({ wrapper, events, settings }) { - this.wrapper = wrapper; - this.events = events; - this.customer_info = undefined; - this.hide_images = settings.hide_images; - this.allowed_customer_groups = settings.customer_groups; - this.allow_rate_change = settings.allow_rate_change; - this.allow_discount_change = settings.allow_discount_change; - this.show_held_button = settings.custom_show_held_button; - this.show_order_list_button = settings.custom_show_order_list_button; - this.mobile_number_based_customer = settings.custom_mobile_number_based_customer; - this.show_checkout_button = settings.custom_show_checkout_button; - this.custom_edit_rate = settings.custom_edit_rate_and_uom; - this.custom_use_discount_percentage = settings.custom_use_discount_percentage; - this.custom_use_discount_amount = settings.custom_use_discount_amount; - this.custom_use_additional_discount_amount = settings.custom_use_additional_discount_amount; - this.custom_show_incoming_rate = settings.custom_show_incoming_rate && settings.custom_edit_rate_and_uom; - this.custom_show_last_customer_rate = settings.custom_show_last_customer_rate; - this.custom_show_logical_rack_in_cart = settings.custom_show_logical_rack_in_cart && settings.custom_edit_rate_and_uom; - this.custom_show_uom_in_cart = settings.custom_show_uom_in_cart && settings.custom_edit_rate_and_uom; - this.show_branch = settings.show_branch; - this.show_batch_in_cart = settings.show_batch_in_cart - this.custom_show_item_discription = settings.custom_show_item_discription; - this.custom_show_item_barcode = settings.custom_show_item_barcode; - this.settings = settings; - this.warehouse = settings.warehouse; - this.init_component(); - } - - init_component() { - - this.prepare_dom(); - this.init_child_components(); - this.bind_events(); - this.attach_shortcuts(); - } - - prepare_dom() { - if(this.custom_edit_rate){ - this.wrapper.append( - `<section class="customer-cart-container customer-cart-container1 " style="grid-column: span 5 / span 5;" id="customer-cart-container2"></section>` - ) - } else { - this.wrapper.append( - `<section class="customer-cart-container customer-cart-container1 " id="customer-cart-container2"></section>` - ) - } - - this.$component = this.wrapper.find('.customer-cart-container1'); - } - - init_child_components() { - this.init_customer_selector(); - this.init_cart_components(); - } - - init_customer_selector() { - this.$component.append( - `<div class="customer-section"></div>` - ) - this.$customer_section = this.$component.find('.customer-section'); - this.make_customer_selector(); - } - - reset_customer_selector() { - const frm = this.events.get_frm(); - frm.set_value('customer', ''); - this.make_customer_selector(); - this.customer_field.set_focus(); - } - - init_cart_components() { - var html = `<div class="cart-container"> + constructor({ wrapper, events, settings }) { + this.wrapper = wrapper; + this.events = events; + this.customer_info = undefined; + this.hide_images = settings.hide_images; + this.allowed_customer_groups = settings.customer_groups; + this.allow_rate_change = settings.allow_rate_change; + this.allow_discount_change = settings.allow_discount_change; + this.show_held_button = settings.custom_show_held_button; + this.show_order_list_button = settings.custom_show_order_list_button; + this.mobile_number_based_customer = + settings.custom_mobile_number_based_customer; + this.show_checkout_button = settings.custom_show_checkout_button; + this.custom_edit_rate = settings.custom_edit_rate_and_uom; + this.custom_use_discount_percentage = + settings.custom_use_discount_percentage; + this.custom_use_discount_amount = settings.custom_use_discount_amount; + this.custom_use_additional_discount_amount = + settings.custom_use_additional_discount_amount; + this.custom_show_incoming_rate = + settings.custom_show_incoming_rate && settings.custom_edit_rate_and_uom; + this.custom_show_last_customer_rate = + settings.custom_show_last_customer_rate; + this.custom_show_logical_rack_in_cart = + settings.custom_show_logical_rack_in_cart && + settings.custom_edit_rate_and_uom; + this.custom_show_uom_in_cart = + settings.custom_show_uom_in_cart && settings.custom_edit_rate_and_uom; + this.show_branch = settings.show_branch; + this.show_batch_in_cart = settings.show_batch_in_cart; + this.custom_show_item_discription = settings.custom_show_item_discription; + this.custom_show_item_barcode = settings.custom_show_item_barcode; + this.settings = settings; + this.warehouse = settings.warehouse; + this.init_component(); + } + + init_component() { + this.prepare_dom(); + this.init_child_components(); + this.bind_events(); + this.attach_shortcuts(); + } + + prepare_dom() { + if (this.custom_edit_rate) { + this.wrapper.append( + `<section class="customer-cart-container customer-cart-container1 " style="grid-column: span 5 / span 5;" id="customer-cart-container2"></section>`, + ); + } else { + this.wrapper.append( + `<section class="customer-cart-container customer-cart-container1 " id="customer-cart-container2"></section>`, + ); + } + + this.$component = this.wrapper.find(".customer-cart-container1"); + } + + init_child_components() { + this.init_customer_selector(); + this.init_cart_components(); + } + + init_customer_selector() { + this.$component.append(`<div class="customer-section"></div>`); + this.$customer_section = this.$component.find(".customer-section"); + this.make_customer_selector(); + } + + reset_customer_selector() { + const frm = this.events.get_frm(); + frm.set_value("customer", ""); + this.make_customer_selector(); + this.customer_field.set_focus(); + } + + init_cart_components() { + var html = `<div class="cart-container"> <div class="abs-cart-container"> - <div class="cart-label">${__('Item Cart')}</div> + <div class="cart-label">${__("Item Cart")}</div> <div class="cart-header"> - <div class="name-header" style="flex:3">${__('Item')}</div> - <div class="qty-header" style="flex: 1">${__('Qty')}</div> - ` - if(this.custom_show_uom_in_cart){ - html += `<div class="uom-header" style="flex: 1">${__('UOM')}</div>` - } - if(this.show_batch_in_cart){ - html += `<div class="batch-header" style="flex: 1">${__('Batch')}</div>` - } - if(this.custom_edit_rate){ - html += `<div class="rate-header" style="flex: 1">${__('Rate')}</div>` - } - if(this.custom_use_discount_percentage){ - html += `<div class="discount-perc-header" style="flex: 1">${__('Disc%')}</div>` - } - if(this.custom_use_discount_amount){ - html += `<div class="discount-amount-header" style="flex: 1">${__('Disc')}</div>` - } - if(this.custom_show_incoming_rate){ - html += `<div class="incoming-rate-header" style="flex: 1">${__('Inc.Rate')}</div>` - } - if(this.custom_show_logical_rack_in_cart){ - html += `<div class="incoming-rate-header" style="flex: 1">${__('Rack')}</div>` - } - if(this.custom_show_last_customer_rate){ - html += `<div class="last-customer-rate-header" style="flex: 1">${__('LC Rate')}</div>` - } - - - html += `<div class="rate-amount-header" style="flex: 1;text-align: left">${__('Amount')}</div> + <div class="name-header" style="flex:3">${__("Item")}</div> + <div class="qty-header" style="flex: 1">${__("Qty")}</div> + `; + if (this.custom_show_uom_in_cart) { + html += `<div class="uom-header" style="flex: 1">${__("UOM")}</div>`; + } + if (this.show_batch_in_cart) { + html += `<div class="batch-header" style="flex: 1">${__("Batch")}</div>`; + } + if (this.custom_edit_rate) { + html += `<div class="rate-header" style="flex: 1">${__("Rate")}</div>`; + } + if (this.custom_use_discount_percentage) { + html += `<div class="discount-perc-header" style="flex: 1">${__( + "Disc%", + )}</div>`; + } + if (this.custom_use_discount_amount) { + html += `<div class="discount-amount-header" style="flex: 1">${__( + "Disc", + )}</div>`; + } + if (this.custom_show_incoming_rate) { + html += `<div class="incoming-rate-header" style="flex: 1">${__( + "Inc.Rate", + )}</div>`; + } + if (this.custom_show_logical_rack_in_cart) { + html += `<div class="incoming-rate-header" style="flex: 1">${__( + "Rack", + )}</div>`; + } + if (this.custom_show_last_customer_rate) { + html += `<div class="last-customer-rate-header" style="flex: 1">${__( + "LC Rate", + )}</div>`; + } + + html += `<div class="rate-amount-header" style="flex: 1;text-align: left">${__( + "Amount", + )}</div> </div> <div class="cart-items-section" ></div> <div class="cart-branch-section"></div> <div class="cart-totals-section"></div> <div class="numpad-section"></div> </div> - </div>` - this.$component.append(html); - this.$cart_container = this.$component.find('.cart-container'); - this.make_branch_section(); - this.make_cart_totals_section(); - this.make_cart_items_section(); - this.make_cart_numpad(); - } - - make_cart_items_section() { - this.$cart_header = this.$component.find('.cart-header'); - this.$cart_items_wrapper = this.$component.find('.cart-items-section'); - - this.make_no_items_placeholder(); - } - - make_no_items_placeholder() { - this.$cart_header.css('display', 'none'); - this.$cart_items_wrapper.html( - `<div class="no-item-wrapper">${__('No items in cart')}</div>` - ); - } - - get_discount_icon() { - return ( - `<svg class="discount-icon" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="none" xmlns="http://www.w3.org/2000/svg"> + </div>`; + this.$component.append(html); + this.$cart_container = this.$component.find(".cart-container"); + this.make_branch_section(); + this.make_cart_totals_section(); + this.make_cart_items_section(); + this.make_cart_numpad(); + } + + make_cart_items_section() { + this.$cart_header = this.$component.find(".cart-header"); + this.$cart_items_wrapper = this.$component.find(".cart-items-section"); + + this.make_no_items_placeholder(); + } + + make_no_items_placeholder() { + this.$cart_header.css("display", "none"); + this.$cart_items_wrapper.html( + `<div class="no-item-wrapper">${__("No items in cart")}</div>`, + ); + } + + get_discount_icon() { + return `<svg class="discount-icon" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M19 15.6213C19 15.2235 19.158 14.842 19.4393 14.5607L20.9393 13.0607C21.5251 12.4749 21.5251 11.5251 20.9393 10.9393L19.4393 9.43934C19.158 9.15804 19 8.7765 19 8.37868V6.5C19 5.67157 18.3284 5 17.5 5H15.6213C15.2235 5 14.842 4.84196 14.5607 4.56066L13.0607 3.06066C12.4749 2.47487 11.5251 2.47487 10.9393 3.06066L9.43934 4.56066C9.15804 4.84196 8.7765 5 8.37868 5H6.5C5.67157 5 5 5.67157 5 6.5V8.37868C5 8.7765 4.84196 9.15804 4.56066 9.43934L3.06066 10.9393C2.47487 11.5251 2.47487 12.4749 3.06066 13.0607L4.56066 14.5607C4.84196 14.842 5 15.2235 5 15.6213V17.5C5 18.3284 5.67157 19 6.5 19H8.37868C8.7765 19 9.15804 19.158 9.43934 19.4393L10.9393 20.9393C11.5251 21.5251 12.4749 21.5251 13.0607 20.9393L14.5607 19.4393C14.842 19.158 15.2235 19 15.6213 19H17.5C18.3284 19 19 18.3284 19 17.5V15.6213Z" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> <path d="M15 9L9 15" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> <path d="M10.5 9.5C10.5 10.0523 10.0523 10.5 9.5 10.5C8.94772 10.5 8.5 10.0523 8.5 9.5C8.5 8.94772 8.94772 8.5 9.5 8.5C10.0523 8.5 10.5 8.94772 10.5 9.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/> <path d="M15.5 14.5C15.5 15.0523 15.0523 15.5 14.5 15.5C13.9477 15.5 13.5 15.0523 13.5 14.5C13.5 13.9477 13.9477 13.5 14.5 13.5C15.0523 13.5 15.5 13.9477 15.5 14.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/> - </svg>` - ); - } + </svg>`; + } - get_branch_icon() { - return ` + get_branch_icon() { + return ` <svg class="branch-icon" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="none" xmlns="http://www.w3.org/2000/svg"> <path d="M5 3V9M5 3C6.65685 3 8 4.34315 8 6C8 7.65685 6.65685 9 5 9M5 3C3.34315 3 2 4.34315 2 6C2 7.65685 3.34315 9 5 9M19 15V21M19 15C20.6569 15 22 16.3431 22 18C22 19.6569 20.6569 21 19 21M19 15C17.3431 15 16 16.3431 16 18C16 19.6569 17.3431 21 19 21M5 9C5 13.4183 8.58172 17 13 17H16" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/> </svg> `; - } - - make_branch_section() { - if (this.show_branch) { - this.$branch_section = this.$component.find('.cart-branch-section'); - - if (this.$branch_section.length) { - this.$branch_section.append(` + } + + make_branch_section() { + if (this.show_branch) { + this.$branch_section = this.$component.find(".cart-branch-section"); + + if (this.$branch_section.length) { + this.$branch_section.append(` <br> <div class="add-branch-wrapper"> - ${this.get_branch_icon()} <span class="add-branch-text">${__('Add Branch')}</span> + ${this.get_branch_icon()} <span class="add-branch-text">${__( + "Add Branch", + )}</span> </div> `); - // Change cursor on hover - this.$branch_section.find('.add-branch-wrapper').hover( - function () { - $(this).css("background-color", "#f9f9f9"); - }, - function () { - $(this).css("background-color", "transparent"); - } - ); - } - } - } - - - make_cart_totals_section() { - this.$totals_section = this.$component.find('.cart-totals-section'); - - this.$totals_section.append( - `<div class="add-discount-wrapper"> - ${this.get_discount_icon()} ${__('Add Discount')} + // Change cursor on hover + this.$branch_section.find(".add-branch-wrapper").hover( + function () { + $(this).css("background-color", "#f9f9f9"); + }, + function () { + $(this).css("background-color", "transparent"); + }, + ); + } + } + } + + make_cart_totals_section() { + this.$totals_section = this.$component.find(".cart-totals-section"); + + this.$totals_section.append( + `<div class="add-discount-wrapper"> + ${this.get_discount_icon()} ${__("Add Discount")} </div> <div class="item-qty-total-container"> - <div class="item-qty-total-label">${__('Total Items')}</div> + <div class="item-qty-total-label">${__("Total Items")}</div> <div class="item-qty-total-value">0.00</div> </div> <div class="net-total-container"> @@ -196,7 +211,7 @@ posnext.PointOfSale.ItemCart = class { </div> <div class="taxes-container"></div> <div class="grand-total-container"> - <div>${__('Grand Total')}</div> + <div>${__("Grand Total")}</div> <div>0.00</div> </div> <div style=" display: flex;justify-content: space-between;gap: 10px;"> @@ -208,7 +223,7 @@ posnext.PointOfSale.ItemCart = class { border: none; border-radius: 5px; cursor: pointer; - flex: 1; ">${__('Checkout (F1)')}</div> + flex: 1; ">${__("Checkout (F1)")}</div> <div class="checkout-btn-held checkout-btn" style=" padding: 10px; align-items: center; @@ -217,7 +232,7 @@ posnext.PointOfSale.ItemCart = class { border: none; border-radius: 5px; cursor: pointer; - flex: 1;">${__('Held (F2)')}</div> + flex: 1;">${__("Held (F2)")}</div> <div class="checkout-btn-order checkout-btn" style=" padding: 10px; align-items: center; @@ -226,148 +241,168 @@ posnext.PointOfSale.ItemCart = class { border: none; border-radius: 5px; cursor: pointer; - flex: 1;">${__('Order List (F3)')}</div> - </div> - <div class="edit-cart-btn">${__('Edit Cart')}</div>` - ) - - this.$add_discount_elem = this.$component.find(".add-discount-wrapper"); -this.highlight_checkout_btn(true); - } - - make_cart_numpad() { - this.$numpad_section = this.$component.find('.numpad-section'); - - this.number_pad = new posnext.PointOfSale.NumberPad({ - wrapper: this.$numpad_section, - events: { - numpad_event: this.on_numpad_event.bind(this) - }, - cols: 5, - keys: [ - [ 1, 2, 3, 'Quantity' ], - [ 4, 5, 6, 'Discount' ], - [ 7, 8, 9, 'Rate' ], - [ '.', 0, 'Delete', 'Remove' ] - ], - css_classes: [ - [ '', '', '', 'col-span-2' ], - [ '', '', '', 'col-span-2' ], - [ '', '', '', 'col-span-2' ], - [ '', '', '', 'col-span-2 remove-btn' ] - ], - fieldnames_map: { 'Quantity': 'qty', 'Discount': 'discount_percentage' } - }) - - this.$numpad_section.prepend( - `<div class="numpad-totals"> + flex: 1;">${__("Order List (F3)")}</div> + </div> + <div class="edit-cart-btn">${__("Edit Cart")}</div>`, + ); + + this.$add_discount_elem = this.$component.find(".add-discount-wrapper"); + this.highlight_checkout_btn(true); + } + + make_cart_numpad() { + this.$numpad_section = this.$component.find(".numpad-section"); + + this.number_pad = new posnext.PointOfSale.NumberPad({ + wrapper: this.$numpad_section, + events: { + numpad_event: this.on_numpad_event.bind(this), + }, + cols: 5, + keys: [ + [1, 2, 3, "Quantity"], + [4, 5, 6, "Discount"], + [7, 8, 9, "Rate"], + [".", 0, "Delete", "Remove"], + ], + css_classes: [ + ["", "", "", "col-span-2"], + ["", "", "", "col-span-2"], + ["", "", "", "col-span-2"], + ["", "", "", "col-span-2 remove-btn"], + ], + fieldnames_map: { Quantity: "qty", Discount: "discount_percentage" }, + }); + + this.$numpad_section.prepend( + `<div class="numpad-totals"> <span class="numpad-item-qty-total"></span> <span class="numpad-net-total"></span> <span class="numpad-grand-total"></span> - </div>` - ) - - this.$numpad_section.append( - `<div class="numpad-btn checkout-btn" data-button-value="checkout">${__('Checkout')}</div>` - ) - } - - bind_events() { - const me = this; - this.$customer_section.on('click', '.reset-customer-btn', function () { - me.reset_customer_selector(); - }); - - this.$customer_section.on('click', '.close-details-btn', function () { - me.toggle_customer_info(false); - }); - - this.$customer_section.on('click', '.customer-display', function(e) { - if ($(e.target).closest('.reset-customer-btn').length) return; - - const show = me.$cart_container.is(':visible'); - me.toggle_customer_info(show); - }); - // - if(!me.custom_edit_rate){ - this.$cart_items_wrapper.on('click', '.cart-item-wrapper', function() { - const $cart_item = $(this); - - me.toggle_item_highlight(this); - - const payment_section_hidden = !me.$totals_section.find('.edit-cart-btn').is(':visible'); - if (!payment_section_hidden) { - // payment section is visible - // edit cart first and then open item details section - me.$totals_section.find(".edit-cart-btn").click(); - } - - const item_row_name = unescape($cart_item.attr('data-row-name')); - me.events.cart_item_clicked({ name: item_row_name }); - this.numpad_value = ''; + </div>`, + ); + + this.$numpad_section.append( + `<div class="numpad-btn checkout-btn" data-button-value="checkout">${__( + "Checkout", + )}</div>`, + ); + } + + bind_events() { + const me = this; + this.$customer_section.on("click", ".reset-customer-btn", function () { + me.reset_customer_selector(); + }); + + this.$customer_section.on("click", ".close-details-btn", function () { + me.toggle_customer_info(false); + }); + + this.$customer_section.on("click", ".customer-display", function (e) { + if ($(e.target).closest(".reset-customer-btn").length) return; + + const show = me.$cart_container.is(":visible"); + me.toggle_customer_info(show); + }); + // + if (!me.custom_edit_rate) { + this.$cart_items_wrapper.on("click", ".cart-item-wrapper", function () { + const $cart_item = $(this); + + me.toggle_item_highlight(this); + + const payment_section_hidden = !me.$totals_section + .find(".edit-cart-btn") + .is(":visible"); + if (!payment_section_hidden) { + // payment section is visible + // edit cart first and then open item details section + me.$totals_section.find(".edit-cart-btn").click(); + } + + const item_row_name = unescape($cart_item.attr("data-row-name")); + me.events.cart_item_clicked({ name: item_row_name }); + this.numpad_value = ""; + }); + } + + this.$component.on("click", ".checkout-btn", async function () { + if ($(this).attr("style").indexOf("--blue-500") == -1) return; + if ($(this).attr("class").indexOf("checkout-btn-held") !== -1) return; + if ($(this).attr("class").indexOf("checkout-btn-order") !== -1) return; + // prettier-ignore + if (!cur_frm.doc.customer && me.mobile_number_based_customer) { // nosemgrep + let d = new frappe.ui.Dialog({ + title: "Enter Mobile Number", + fields: [ + { + label: "Mobile Number", + fieldname: "mobile_number", + fieldtype: "Data", + reqd: 1, + }, + { + label: "", + fieldname: "mobile_number_numpad", + fieldtype: "HTML", + options: '<div class="mobile_number_numpad"></div>', + }, + ], + size: "small", + primary_action_label: "Continue", + primary_action: function (values) { + if ( + values["mobile_number"].length !== + me.settings.custom_mobile_number_length + ) { + frappe.throw( + "Mobile Number Length is " + + me.settings.custom_mobile_number_length.toString(), + ); + } + frappe.call({ + method: + "posnext.posnext.page.posnext.point_of_sale.create_customer", + args: { + customer: values["mobile_number"], + }, + freeze: true, + freeze_message: "Creating Customer....", + callback: async function () { + const frm = me.events.get_frm(); + frappe.dom.freeze(); + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "customer", + values["mobile_number"], + ); + frm.script_manager + .trigger("customer", frm.doc.doctype, frm.doc.name) + .then(() => { + frappe.run_serially([ + () => me.fetch_customer_details(values["mobile_number"]), + () => + me.events.customer_details_updated(me.customer_info), + () => me.update_customer_section(), + () => frappe.dom.unfreeze(), + ]); + }); + // me.fetch_customer_details(values['mobile_number']) + // me.events.customer_details_updated(me.customer_info) + // me.update_customer_section() + await me.events.checkout(); + me.toggle_checkout_btn(false); + me.allow_discount_change && + me.$add_discount_elem.removeClass("d-none"); + d.hide(); + }, }); - } - - - this.$component.on('click', '.checkout-btn', async function() { - if ($(this).attr('style').indexOf('--blue-500') == -1) return; - if ($(this).attr('class').indexOf('checkout-btn-held') !== -1) return; - if ($(this).attr('class').indexOf('checkout-btn-order') !== -1) return; - if(!cur_frm.doc.customer && me.mobile_number_based_customer){ - let d = new frappe.ui.Dialog({ - title: 'Enter Mobile Number', - fields: [ - { - label: 'Mobile Number', - fieldname: 'mobile_number', - fieldtype: 'Data', - reqd: 1 - }, - { - label: '', - fieldname: 'mobile_number_numpad', - fieldtype: 'HTML', - options: '<div class="mobile_number_numpad"></div>' - }, - ], - size: 'small', - primary_action_label: 'Continue', - primary_action: function(values) { - if(values['mobile_number'].length !== me.settings.custom_mobile_number_length){ - frappe.throw("Mobile Number Length is " + me.settings.custom_mobile_number_length.toString()) - } - frappe.call({ - method: "posnext.posnext.page.posnext.point_of_sale.create_customer", - args: { - customer: values['mobile_number'] - }, - freeze: true, - freeze_message: "Creating Customer....", - callback: async function(){ - const frm = me.events.get_frm(); - frappe.dom.freeze(); - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'customer', values['mobile_number']); - frm.script_manager.trigger('customer', frm.doc.doctype, frm.doc.name).then(() => { - frappe.run_serially([ - () => me.fetch_customer_details(values['mobile_number']), - () => me.events.customer_details_updated(me.customer_info), - () => me.update_customer_section(), - () => frappe.dom.unfreeze() - ]); - }) - // me.fetch_customer_details(values['mobile_number']) - // me.events.customer_details_updated(me.customer_info) - // me.update_customer_section() - await me.events.checkout(); - me.toggle_checkout_btn(false); - me.allow_discount_change && me.$add_discount_elem.removeClass("d-none"); - d.hide(); - } - }) - } - }); - var mobile_number_numpad_div = d.wrapper.find(".mobile_number_numpad") - mobile_number_numpad_div.append(` + }, + }); + var mobile_number_numpad_div = d.wrapper.find(".mobile_number_numpad"); + mobile_number_numpad_div.append(` <div class="custom-numpad"> <style> .custom-numpad { @@ -377,7 +412,7 @@ this.highlight_checkout_btn(true); max-width: 350px; margin: 0 auto; } - + .numpad-button { padding: 15px; font-size: 18px; @@ -387,7 +422,7 @@ this.highlight_checkout_btn(true); border-radius: 5px; text-align: center; } - + .numpad-button:hover { background-color: #ddd; } @@ -404,410 +439,509 @@ this.highlight_checkout_btn(true); <button class="numpad-button delete" style="color: red">x</button> <button class="numpad-button zero">0</button> <button class="numpad-button clear">C</button> <!-- Clear button --> - </div>`) - - d.show(); - var numpad_num = d.wrapper.find(".custom-numpad") - var numbers = ["one",'two','three','four','five','six','seven','eight','nine','zero',"plus"] - for(var xx=0;xx<numbers.length;xx+=1){ - numpad_num.on('click', '.' + numbers[xx], function() { - var current_value = d.get_value("mobile_number") - d.set_value('mobile_number', current_value + $(this)[0].innerHTML.toString()); - }) - } - numpad_num.on('click', '.clear', function() { - d.set_value('mobile_number', ""); - }) - numpad_num.on('click', '.delete', function() { - var current_value = d.get_value("mobile_number") - d.set_value('mobile_number', current_value.slice(0, -1)); - }) - - - } else { - if(!cur_frm.doc.customer && !me.mobile_number_based_customer){ - frappe.throw("Please Select a customer and add items first") - } - await me.events.checkout(); - me.toggle_checkout_btn(false); - me.allow_discount_change && me.$add_discount_elem.removeClass("d-none"); - } - - - - }); - - this.$component.on('click', '.checkout-btn-held', function() { - if ($(this).attr('style').indexOf('--blue-500') == -1) return; - - // Check for empty cart - if (!cur_frm.doc.items || cur_frm.doc.items.length === 0) { - frappe.show_alert({ - message: __('Please add items to cart before holding.'), - indicator: 'red' - }); - frappe.utils.play_sound("error"); - return; - } - - if(!cur_frm.doc.customer && me.mobile_number_based_customer) { - let d = new frappe.ui.Dialog({ - title: 'Enter Mobile Number', - fields: [ - { - label: 'Mobile Number', - fieldname: 'mobile_number', - fieldtype: 'Data', - reqd: 1 - }, - { - label: '', - fieldname: 'mobile_number_numpad', - fieldtype: 'HTML', - options: '<div class="mobile_number_numpad"></div>' - }, - ], - size: 'small', - primary_action_label: 'Continue', - primary_action: function(values) { - if(values['mobile_number'].length !== me.settings.custom_mobile_number_length){ - frappe.throw("Mobile Number Length is " + me.settings.custom_mobile_number_length.toString()); - } - if (me.settings.custom_add_reference_details) { - me.show_reference_dialog(values['mobile_number']); - } else { - me.hold_invoice(values['mobile_number']); - } - d.hide(); - } - }); - - me.setup_mobile_numpad(d); - d.show(); - } else { - if (me.settings.custom_add_reference_details) { - me.show_reference_dialog(); - } else { - me.hold_invoice(); - } - } - }); - - - this.$component.on('click', '.checkout-btn-order', () => { - this.events.toggle_recent_order(); - }); - - this.$totals_section.on('click', '.edit-cart-btn', () => { - this.events.edit_cart(); - - this.toggle_checkout_btn(true); - }); - - this.$component.on('click', '.add-discount-wrapper', () => { - const can_edit_discount = this.$add_discount_elem.find('.edit-discount-btn').length; - - if(!this.discount_field || can_edit_discount) this.show_discount_control(); - }); - - - const $wrapper = $('.add-branch-wrapper'); - const posProfileName = me.settings.name; - const branchFieldWrapper = $('<div class="branch-field"></div>'); - $wrapper.replaceWith(branchFieldWrapper); - - frappe.call({ - method: "posnext.doc_events.pos_profile.get_pos_profile_branch", - args: { - pos_profile_name: posProfileName - }, - callback: function (r) { - const branch_name = r.message && r.message.branch; - console.log(branch_name); - - let branchField = new frappe.ui.form.ControlLink({ - df: { - fieldtype: 'Link', - options: 'Branch', - fieldname: 'branch', - label: 'Branch', - placeholder: 'Select Branch', - default: branch_name, - reqd: 1, - - }, - parent: branchFieldWrapper - }); - - branchField.make(); - branchField.set_value(branch_name); - branchField.refresh(); - }, - }); - - frappe.ui.form.on("Sales Invoice", "paid_amount", frm => { - // called when discount is applied - this.update_totals_section(frm); - }); - } - - attach_shortcuts() { - for (let row of this.number_pad.keys) { - for (let btn of row) { - if (typeof btn !== 'string') continue; // do not make shortcuts for numbers - - let shortcut_key = `ctrl+${frappe.scrub(String(btn))[0]}`; - if (btn === 'Delete') shortcut_key = 'ctrl+backspace'; - if (btn === 'Remove') shortcut_key = 'shift+ctrl+backspace' - if (btn === '.') shortcut_key = 'ctrl+>'; - - // to account for fieldname map - const fieldname = this.number_pad.fieldnames[btn] ? this.number_pad.fieldnames[btn] : - typeof btn === 'string' ? frappe.scrub(btn) : btn; - - let shortcut_label = shortcut_key.split('+').map(frappe.utils.to_title_case).join('+'); - shortcut_label = frappe.utils.is_mac() ? shortcut_label.replace('Ctrl', '⌘') : shortcut_label; - this.$numpad_section.find(`.numpad-btn[data-button-value="${fieldname}"]`).attr("title", shortcut_label); - - frappe.ui.keys.on(`${shortcut_key}`, () => { - const cart_is_visible = this.$component.is(":visible"); - if (cart_is_visible && this.item_is_selected && this.$numpad_section.is(":visible")) { - this.$numpad_section.find(`.numpad-btn[data-button-value="${fieldname}"]`).click(); - } - }) - } - } - const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; - this.$component.find(".checkout-btn").attr("title", `${ctrl_label}+Enter`); - frappe.ui.keys.add_shortcut({ - shortcut: "ctrl+enter", - action: () => this.$component.find(".checkout-btn").click(), - condition: () => this.$component.is(":visible") && !this.$totals_section.find('.edit-cart-btn').is(':visible'), - description: __("Checkout Order / Submit Order / New Order"), - ignore_inputs: true, - page: cur_page.page.page - }); - this.$component.find(".edit-cart-btn").attr("title", `${ctrl_label}+E`); - frappe.ui.keys.on("ctrl+e", () => { - const item_cart_visible = this.$component.is(":visible"); - const checkout_btn_invisible = !this.$totals_section.find('.checkout-btn').is('visible'); - if (item_cart_visible && checkout_btn_invisible) { - this.$component.find(".edit-cart-btn").click(); - } - }); - this.$component.find(".add-discount-wrapper").attr("title", `${ctrl_label}+D`); - frappe.ui.keys.add_shortcut({ - shortcut: "ctrl+d", - action: () => this.$component.find(".add-discount-wrapper").click(), - condition: () => this.$add_discount_elem.is(":visible"), - description: __("Add Order Discount"), - ignore_inputs: true, - page: cur_page.page.page - }); - - - frappe.ui.keys.on("escape", () => { - const item_cart_visible = this.$component.is(":visible"); - if (item_cart_visible && this.discount_field && this.discount_field.parent.is(":visible")) { - this.discount_field.set_value(0); - } - }); - } - - toggle_item_highlight(item) { - const $cart_item = $(item); - const item_is_highlighted = $cart_item.attr("style") == "background-color:var(--gray-50);"; - - if (!item || item_is_highlighted) { - this.item_is_selected = false; - this.$cart_container.find('.cart-item-wrapper').css("background-color", ""); - } else { - $cart_item.css("background-color", "var(--control-bg)"); - this.item_is_selected = true; - this.$cart_container.find('.cart-item-wrapper').not(item).css("background-color", ""); - } - } - - make_customer_selector() { - this.$customer_section.html(` + </div>`); + + d.show(); + var numpad_num = d.wrapper.find(".custom-numpad"); + var numbers = [ + "one", + "two", + "three", + "four", + "five", + "six", + "seven", + "eight", + "nine", + "zero", + "plus", + ]; + for (var xx = 0; xx < numbers.length; xx += 1) { + numpad_num.on("click", "." + numbers[xx], function () { + var current_value = d.get_value("mobile_number"); + d.set_value( + "mobile_number", + current_value + $(this)[0].innerHTML.toString(), + ); + }); + } + numpad_num.on("click", ".clear", function () { + d.set_value("mobile_number", ""); + }); + numpad_num.on("click", ".delete", function () { + var current_value = d.get_value("mobile_number"); + d.set_value("mobile_number", current_value.slice(0, -1)); + }); + } else { + // prettier-ignore + if (!cur_frm.doc.customer && !me.mobile_number_based_customer) { // nosemgrep + frappe.throw(__("Please Select a customer and add items first")); + } + await me.events.checkout(); + me.toggle_checkout_btn(false); + me.allow_discount_change && me.$add_discount_elem.removeClass("d-none"); + } + }); + + this.$component.on("click", ".checkout-btn-held", function () { + if ($(this).attr("style").indexOf("--blue-500") == -1) return; + + // Check for empty cart + // prettier-ignore + if (!cur_frm.doc.items || cur_frm.doc.items.length === 0) { // nosemgrep + frappe.show_alert({ + message: __("Please add items to cart before holding."), + indicator: "red", + }); + frappe.utils.play_sound("error"); + return; + } + + // prettier-ignore + if (!cur_frm.doc.customer && me.mobile_number_based_customer) { // nosemgrep + let d = new frappe.ui.Dialog({ + title: "Enter Mobile Number", + fields: [ + { + label: "Mobile Number", + fieldname: "mobile_number", + fieldtype: "Data", + reqd: 1, + }, + { + label: "", + fieldname: "mobile_number_numpad", + fieldtype: "HTML", + options: '<div class="mobile_number_numpad"></div>', + }, + ], + size: "small", + primary_action_label: "Continue", + primary_action: function (values) { + if ( + values["mobile_number"].length !== + me.settings.custom_mobile_number_length + ) { + frappe.throw( + "Mobile Number Length is " + + me.settings.custom_mobile_number_length.toString(), + ); + } + if (me.settings.custom_add_reference_details) { + me.show_reference_dialog(values["mobile_number"]); + } else { + me.hold_invoice(values["mobile_number"]); + } + d.hide(); + }, + }); + + me.setup_mobile_numpad(d); + d.show(); + } else { + if (me.settings.custom_add_reference_details) { + me.show_reference_dialog(); + } else { + me.hold_invoice(); + } + } + }); + + this.$component.on("click", ".checkout-btn-order", () => { + this.events.toggle_recent_order(); + }); + + this.$totals_section.on("click", ".edit-cart-btn", () => { + this.events.edit_cart(); + + this.toggle_checkout_btn(true); + }); + + this.$component.on("click", ".add-discount-wrapper", () => { + const can_edit_discount = + this.$add_discount_elem.find(".edit-discount-btn").length; + + if (!this.discount_field || can_edit_discount) + this.show_discount_control(); + }); + + const $wrapper = $(".add-branch-wrapper"); + const posProfileName = me.settings.name; + const branchFieldWrapper = $('<div class="branch-field"></div>'); + $wrapper.replaceWith(branchFieldWrapper); + + frappe.call({ + method: "posnext.doc_events.pos_profile.get_pos_profile_branch", + args: { + pos_profile_name: posProfileName, + }, + callback: function (r) { + const branch_name = r.message && r.message.branch; + console.log(branch_name); + + let branchField = new frappe.ui.form.ControlLink({ + df: { + fieldtype: "Link", + options: "Branch", + fieldname: "branch", + label: "Branch", + placeholder: "Select Branch", + default: branch_name, + reqd: 1, + }, + parent: branchFieldWrapper, + }); + + branchField.make(); + branchField.set_value(branch_name); + branchField.refresh(); + }, + }); + + frappe.ui.form.on("Sales Invoice", "paid_amount", (frm) => { + // called when discount is applied + this.update_totals_section(frm); + }); + } + + attach_shortcuts() { + for (let row of this.number_pad.keys) { + for (let btn of row) { + if (typeof btn !== "string") continue; // do not make shortcuts for numbers + + let shortcut_key = `ctrl+${frappe.scrub(String(btn))[0]}`; + if (btn === "Delete") shortcut_key = "ctrl+backspace"; + if (btn === "Remove") shortcut_key = "shift+ctrl+backspace"; + if (btn === ".") shortcut_key = "ctrl+>"; + + // to account for fieldname map + const fieldname = this.number_pad.fieldnames[btn] + ? this.number_pad.fieldnames[btn] + : typeof btn === "string" + ? frappe.scrub(btn) + : btn; + + let shortcut_label = shortcut_key + .split("+") + .map(frappe.utils.to_title_case) + .join("+"); + shortcut_label = frappe.utils.is_mac() + ? shortcut_label.replace("Ctrl", "⌘") + : shortcut_label; + this.$numpad_section + .find(`.numpad-btn[data-button-value="${fieldname}"]`) + .attr("title", shortcut_label); + + frappe.ui.keys.on(`${shortcut_key}`, () => { + const cart_is_visible = this.$component.is(":visible"); + if ( + cart_is_visible && + this.item_is_selected && + this.$numpad_section.is(":visible") + ) { + this.$numpad_section + .find(`.numpad-btn[data-button-value="${fieldname}"]`) + .click(); + } + }); + } + } + const ctrl_label = frappe.utils.is_mac() ? "⌘" : "Ctrl"; + this.$component.find(".checkout-btn").attr("title", `${ctrl_label}+Enter`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+enter", + action: () => this.$component.find(".checkout-btn").click(), + condition: () => + this.$component.is(":visible") && + !this.$totals_section.find(".edit-cart-btn").is(":visible"), + description: __("Checkout Order / Submit Order / New Order"), + ignore_inputs: true, + page: cur_page.page.page, + }); + this.$component.find(".edit-cart-btn").attr("title", `${ctrl_label}+E`); + frappe.ui.keys.on("ctrl+e", () => { + const item_cart_visible = this.$component.is(":visible"); + const checkout_btn_invisible = !this.$totals_section + .find(".checkout-btn") + .is("visible"); + if (item_cart_visible && checkout_btn_invisible) { + this.$component.find(".edit-cart-btn").click(); + } + }); + this.$component + .find(".add-discount-wrapper") + .attr("title", `${ctrl_label}+D`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+d", + action: () => this.$component.find(".add-discount-wrapper").click(), + condition: () => this.$add_discount_elem.is(":visible"), + description: __("Add Order Discount"), + ignore_inputs: true, + page: cur_page.page.page, + }); + + frappe.ui.keys.on("escape", () => { + const item_cart_visible = this.$component.is(":visible"); + if ( + item_cart_visible && + this.discount_field && + this.discount_field.parent.is(":visible") + ) { + this.discount_field.set_value(0); + } + }); + } + + toggle_item_highlight(item) { + const $cart_item = $(item); + const item_is_highlighted = + $cart_item.attr("style") == "background-color:var(--gray-50);"; + + if (!item || item_is_highlighted) { + this.item_is_selected = false; + this.$cart_container + .find(".cart-item-wrapper") + .css("background-color", ""); + } else { + $cart_item.css("background-color", "var(--control-bg)"); + this.item_is_selected = true; + this.$cart_container + .find(".cart-item-wrapper") + .not(item) + .css("background-color", ""); + } + } + + make_customer_selector() { + this.$customer_section.html(` <div class="customer-field"></div> `); - const me = this; - const query = { query: 'posnext.controllers.queries.customer_query' }; - const allowed_customer_group = this.allowed_customer_groups || []; - if (allowed_customer_group.length) { - query.filters = { - customer_group: ['in', allowed_customer_group] - } - } - this.customer_field = frappe.ui.form.make_control({ - df: { - label: __('Customer'), - fieldtype: 'Link', - options: 'Customer', - placeholder: __('Search by customer name, phone, email.'), - read_only: this.mobile_number_based_customer, - get_query: () => query, - onchange: function() { - if (this.value) { - const frm = me.events.get_frm(); - frappe.dom.freeze(); - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'customer', this.value); - frm.script_manager.trigger('customer', frm.doc.doctype, frm.doc.name).then(() => { - frappe.run_serially([ - () => me.fetch_customer_details(this.value), - () => me.events.customer_details_updated(me.customer_info), - () => me.update_customer_section(), - () => me.update_totals_section(), - () => frappe.dom.unfreeze() - ]); - }) - } - }, - }, - parent: this.$customer_section.find('.customer-field'), - render_input: true, - }); - this.customer_field.toggle_label(false); - } - - fetch_customer_details(customer) { - if (customer) { - return new Promise((resolve) => { - frappe.db.get_value('Customer', customer, ["email_id", "mobile_no", "image", "loyalty_program"]).then(({ message }) => { - const { loyalty_program } = message; - // if loyalty program then fetch loyalty points too - if (loyalty_program) { - frappe.call({ - method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details_with_points", - args: { customer, loyalty_program, "silent": true }, - callback: (r) => { - const { loyalty_points, conversion_factor } = r.message; - if (!r.exc) { - this.customer_info = { ...message, customer, loyalty_points, conversion_factor }; - resolve(); - } - } - }); - } else { - this.customer_info = { ...message, customer }; - resolve(); - } - }); - }); - } else { - return new Promise((resolve) => { - this.customer_info = {} - resolve(); - }); - } - } - - show_discount_control() { - this.$add_discount_elem.css({ 'padding': '0px', 'border': 'none' }); - this.$add_discount_elem.html( - `<div class="add-discount-field"></div>` - ); - const me = this; - const frm = me.events.get_frm(); - let discount = frm.doc.additional_discount_percentage; - this.discount_field = null; - if(me.custom_use_additional_discount_amount){ - this.discount_field = frappe.ui.form.make_control({ - df: { - label: __('Discount'), - fieldtype: 'Data', - placeholder: ( discount ? discount : __('Enter discount amount.') ), - input_class: 'input-xs', - onchange: function() { - setTimeout(()=>{ - if (flt(this.value) != 0) { - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'discount_amount', flt(this.value)); - me.hide_discount_control(this.value); - - } else { - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'discount_amount', 0); - me.$add_discount_elem.css({ - 'border': '1px dashed var(--gray-500)', - 'padding': 'var(--padding-sm) var(--padding-md)' - }); - me.$add_discount_elem.html(`${me.get_discount_icon()} ${__('Add Discount')}`); - me.discount_field = undefined; - } - }, 3000); - }, - }, - parent: this.$add_discount_elem.find('.add-discount-field'), - render_input: true, - }); - }else{ - this.discount_field = frappe.ui.form.make_control({ - df: { - label: __('Discount'), - fieldtype: 'Data', - placeholder: ( discount ? discount + '%' : __('Enter discount percentage.') ), - input_class: 'input-xs', - onchange: function() { - setTimeout(()=>{ - if (flt(this.value) != 0) { - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value)); - me.hide_discount_control(this.value); - - } else { - frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', 0); - me.$add_discount_elem.css({ - 'border': '1px dashed var(--gray-500)', - 'padding': 'var(--padding-sm) var(--padding-md)' - }); - me.$add_discount_elem.html(`${me.get_discount_icon()} ${__('Add Discount')}`); - me.discount_field = undefined; - } - }, 3000) - }, - }, - parent: this.$add_discount_elem.find('.add-discount-field'), - render_input: true, - }); - } - this.discount_field.toggle_label(false); - this.discount_field.set_focus(); - } - - hide_discount_control(discount) { - if (!discount) { - this.$add_discount_elem.css({ 'padding': '0px', 'border': 'none' }); - this.$add_discount_elem.html( - `<div class="add-discount-field"></div>` - ); - } else { - this.$add_discount_elem.css({ - 'border': '1px dashed var(--dark-green-500)', - 'padding': 'var(--padding-sm) var(--padding-md)' - }); - if(this.custom_use_additional_discount_amount){ - this.$add_discount_elem.html( - `<div class="edit-discount-btn"> - ${this.get_discount_icon()} ${__("Additional")} ${String(discount).bold()} ${this.events.get_frm().doc.currency} ${__("discount applied")} - </div>` - ); - }else{ - this.$add_discount_elem.html( - `<div class="edit-discount-btn"> - ${this.get_discount_icon()} ${__("Additional")} ${String(discount).bold()}% ${__("discount applied")} - </div>` - ); - } - - } - } - - update_customer_section() { - const me = this; - const { customer, email_id='', mobile_no='', image } = this.customer_info || {}; - - if (customer) { - this.$customer_section.html( - `<div class="customer-details"> + const me = this; + const query = { query: "posnext.controllers.queries.customer_query" }; + const allowed_customer_group = this.allowed_customer_groups || []; + if (allowed_customer_group.length) { + query.filters = { + customer_group: ["in", allowed_customer_group], + }; + } + this.customer_field = frappe.ui.form.make_control({ + df: { + label: __("Customer"), + fieldtype: "Link", + options: "Customer", + placeholder: __("Search by customer name, phone, email."), + read_only: this.mobile_number_based_customer, + get_query: () => query, + onchange: function () { + if (this.value) { + const frm = me.events.get_frm(); + frappe.dom.freeze(); + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "customer", + this.value, + ); + frm.script_manager + .trigger("customer", frm.doc.doctype, frm.doc.name) + .then(() => { + frappe.run_serially([ + () => me.fetch_customer_details(this.value), + () => me.events.customer_details_updated(me.customer_info), + () => me.update_customer_section(), + () => me.update_totals_section(), + () => frappe.dom.unfreeze(), + ]); + }); + } + }, + }, + parent: this.$customer_section.find(".customer-field"), + render_input: true, + }); + this.customer_field.toggle_label(false); + } + + fetch_customer_details(customer) { + if (customer) { + return new Promise((resolve) => { + frappe.db + .get_value("Customer", customer, [ + "email_id", + "mobile_no", + "image", + "loyalty_program", + ]) + .then(({ message }) => { + const { loyalty_program } = message; + // if loyalty program then fetch loyalty points too + if (loyalty_program) { + frappe.call({ + method: + "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details_with_points", + args: { customer, loyalty_program, silent: true }, + callback: (r) => { + const { loyalty_points, conversion_factor } = r.message; + if (!r.exc) { + this.customer_info = { + ...message, + customer, + loyalty_points, + conversion_factor, + }; + resolve(); + } + }, + }); + } else { + this.customer_info = { ...message, customer }; + resolve(); + } + }); + }); + } else { + return new Promise((resolve) => { + this.customer_info = {}; + resolve(); + }); + } + } + + show_discount_control() { + this.$add_discount_elem.css({ padding: "0px", border: "none" }); + this.$add_discount_elem.html(`<div class="add-discount-field"></div>`); + const me = this; + const frm = me.events.get_frm(); + let discount = frm.doc.additional_discount_percentage; + this.discount_field = null; + if (me.custom_use_additional_discount_amount) { + this.discount_field = frappe.ui.form.make_control({ + df: { + label: __("Discount"), + fieldtype: "Data", + placeholder: discount ? discount : __("Enter discount amount."), + input_class: "input-xs", + onchange: function () { + setTimeout(() => { + if (flt(this.value) != 0) { + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "discount_amount", + flt(this.value), + ); + me.hide_discount_control(this.value); + } else { + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "discount_amount", + 0, + ); + me.$add_discount_elem.css({ + border: "1px dashed var(--gray-500)", + padding: "var(--padding-sm) var(--padding-md)", + }); + me.$add_discount_elem.html( + `${me.get_discount_icon()} ${__("Add Discount")}`, + ); + me.discount_field = undefined; + } + }, 3000); + }, + }, + parent: this.$add_discount_elem.find(".add-discount-field"), + render_input: true, + }); + } else { + this.discount_field = frappe.ui.form.make_control({ + df: { + label: __("Discount"), + fieldtype: "Data", + placeholder: discount + ? discount + "%" + : __("Enter discount percentage."), + input_class: "input-xs", + onchange: function () { + setTimeout(() => { + if (flt(this.value) != 0) { + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "additional_discount_percentage", + flt(this.value), + ); + me.hide_discount_control(this.value); + } else { + frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "additional_discount_percentage", + 0, + ); + me.$add_discount_elem.css({ + border: "1px dashed var(--gray-500)", + padding: "var(--padding-sm) var(--padding-md)", + }); + me.$add_discount_elem.html( + `${me.get_discount_icon()} ${__("Add Discount")}`, + ); + me.discount_field = undefined; + } + }, 3000); + }, + }, + parent: this.$add_discount_elem.find(".add-discount-field"), + render_input: true, + }); + } + this.discount_field.toggle_label(false); + this.discount_field.set_focus(); + } + + hide_discount_control(discount) { + if (!discount) { + this.$add_discount_elem.css({ padding: "0px", border: "none" }); + this.$add_discount_elem.html(`<div class="add-discount-field"></div>`); + } else { + this.$add_discount_elem.css({ + border: "1px dashed var(--dark-green-500)", + padding: "var(--padding-sm) var(--padding-md)", + }); + if (this.custom_use_additional_discount_amount) { + this.$add_discount_elem.html( + `<div class="edit-discount-btn"> + ${this.get_discount_icon()} ${__("Additional")} ${String( + discount, + ).bold()} ${this.events.get_frm().doc.currency} ${__( + "discount applied", + )} + </div>`, + ); + } else { + this.$add_discount_elem.html( + `<div class="edit-discount-btn"> + ${this.get_discount_icon()} ${__("Additional")} ${String( + discount, + ).bold()}% ${__("discount applied")} + </div>`, + ); + } + } + } + + update_customer_section() { + const me = this; + const { + customer, + email_id = "", + mobile_no = "", + image, + } = this.customer_info || {}; + + if (customer) { + this.$customer_section.html( + `<div class="customer-details"> <div class="customer-display"> ${this.get_customer_image()} <div class="customer-name-desc"> @@ -820,776 +954,908 @@ this.highlight_checkout_btn(true); </svg> </div> </div> - </div>` - ); - if(this.mobile_number_based_customer){ - this.$customer_section.find('.reset-customer-btn').css('display', 'none'); - } else { - this.$customer_section.find('.reset-customer-btn').css('display', 'flex'); - } - } else { - // reset customer selector - this.reset_customer_selector(); - } - - function get_customer_description() { - if (!email_id && !mobile_no) { - return `<div class="customer-desc">${__('Click to add email / phone')}</div>`; - } else if (email_id && !mobile_no) { - return `<div class="customer-desc">${email_id}</div>`; - } else if (mobile_no && !email_id) { - return `<div class="customer-desc">${mobile_no}</div>`; - } else { - return `<div class="customer-desc">${email_id} - ${mobile_no}</div>`; - } - } - - } - - get_customer_image() { - const { customer, image } = this.customer_info || {}; - if (image) { - return `<div class="customer-image"><img src="${image}" alt="${image}""></div>`; - } else { - return `<div class="customer-image customer-abbr">${frappe.get_abbr(customer)}</div>`; - } - } - - update_totals_section(frm) { - if (!frm) frm = this.events.get_frm(); - frm.cscript.calculate_taxes_and_totals(); - - this.render_net_total(frm.doc.items); - this.render_total_item_qty(frm.doc.items); - - let grand_total = cint(frappe.sys_defaults.disable_rounded_total) - ? frm.doc.grand_total - : frm.doc.rounded_total; - - if (!frm.doc.items || frm.doc.items.length === 0) { - if (Math.abs(grand_total) != 0.005) { - grand_total = 0.000; - } - } - - this.render_grand_total(grand_total); - this.render_taxes(frm.doc.taxes); - } - - render_net_total(items) { - const currency = this.events.get_frm().doc.currency; - var total_net_amount = 0; - items.map((item) => { - total_net_amount = total_net_amount + item.net_amount; - }); - - this.$totals_section.find('.net-total-container').html( - `<div>${__('Net Total')}</div><div>${format_currency(total_net_amount, currency)}</div>` - ) - - this.$numpad_section.find('.numpad-net-total').html( - `<div>${__('Net Total')}: <span>${format_currency(total_net_amount, currency)}</span></div>` - ); - } - - render_total_item_qty(items) { - var total_item_qty = 0; - items.map((item) => { - total_item_qty = total_item_qty + item.qty; - }); - - this.$totals_section.find('.item-qty-total-container').html( - `<div>${__('Total Quantity')}</div><div>${total_item_qty}</div>` - ); - - this.$numpad_section.find('.numpad-item-qty-total').html( - `<div>${__('Total Quantity')}: <span>${total_item_qty}</span></div>` - ); - } - - render_grand_total(value) { - const currency = this.events.get_frm().doc.currency; - this.$totals_section.find('.grand-total-container').html( - `<div>${__('Grand Total')}</div><div>${format_currency(value, currency)}</div>` - ) - - this.$numpad_section.find('.numpad-grand-total').html( - `<div>${__('Grand Total')}: <span>${format_currency(value, currency)}</span></div>` - ); - } - - render_taxes(taxes) { - if (taxes && taxes.length) { - const currency = this.events.get_frm().doc.currency; - const taxes_html = taxes.map(t => { - if (t.tax_amount_after_discount_amount == 0.0) return; - // if tax rate is 0, don't print it. - const description = /[0-9]+/.test(t.description) ? t.description : ((t.rate != 0) ? `${t.description} @ ${t.rate}%`: t.description); - return `<div class="tax-row"> + </div>`, + ); + if (this.mobile_number_based_customer) { + this.$customer_section + .find(".reset-customer-btn") + .css("display", "none"); + } else { + this.$customer_section + .find(".reset-customer-btn") + .css("display", "flex"); + } + } else { + // reset customer selector + this.reset_customer_selector(); + } + + function get_customer_description() { + if (!email_id && !mobile_no) { + return `<div class="customer-desc">${__( + "Click to add email / phone", + )}</div>`; + } else if (email_id && !mobile_no) { + return `<div class="customer-desc">${email_id}</div>`; + } else if (mobile_no && !email_id) { + return `<div class="customer-desc">${mobile_no}</div>`; + } else { + return `<div class="customer-desc">${email_id} - ${mobile_no}</div>`; + } + } + } + + get_customer_image() { + const { customer, image } = this.customer_info || {}; + if (image) { + return `<div class="customer-image"><img src="${image}" alt="${image}""></div>`; + } else { + return `<div class="customer-image customer-abbr">${frappe.get_abbr( + customer, + )}</div>`; + } + } + + update_totals_section(frm) { + if (!frm) frm = this.events.get_frm(); + frm.cscript.calculate_taxes_and_totals(); + + this.render_net_total(frm.doc.items); + this.render_total_item_qty(frm.doc.items); + + let grand_total = cint(frappe.sys_defaults.disable_rounded_total) + ? frm.doc.grand_total + : frm.doc.rounded_total; + + if (!frm.doc.items || frm.doc.items.length === 0) { + if (Math.abs(grand_total) != 0.005) { + grand_total = 0.0; + } + } + + this.render_grand_total(grand_total); + this.render_taxes(frm.doc.taxes); + } + + render_net_total(items) { + const currency = this.events.get_frm().doc.currency; + var total_net_amount = 0; + items.map((item) => { + total_net_amount = total_net_amount + item.net_amount; + }); + + this.$totals_section + .find(".net-total-container") + .html( + `<div>${__("Net Total")}</div><div>${format_currency( + total_net_amount, + currency, + )}</div>`, + ); + + this.$numpad_section + .find(".numpad-net-total") + .html( + `<div>${__("Net Total")}: <span>${format_currency( + total_net_amount, + currency, + )}</span></div>`, + ); + } + + render_total_item_qty(items) { + var total_item_qty = 0; + items.map((item) => { + total_item_qty = total_item_qty + item.qty; + }); + + this.$totals_section + .find(".item-qty-total-container") + .html(`<div>${__("Total Quantity")}</div><div>${total_item_qty}</div>`); + + this.$numpad_section + .find(".numpad-item-qty-total") + .html( + `<div>${__("Total Quantity")}: <span>${total_item_qty}</span></div>`, + ); + } + + render_grand_total(value) { + const currency = this.events.get_frm().doc.currency; + this.$totals_section + .find(".grand-total-container") + .html( + `<div>${__("Grand Total")}</div><div>${format_currency( + value, + currency, + )}</div>`, + ); + + this.$numpad_section + .find(".numpad-grand-total") + .html( + `<div>${__("Grand Total")}: <span>${format_currency( + value, + currency, + )}</span></div>`, + ); + } + + render_taxes(taxes) { + if (taxes && taxes.length) { + const currency = this.events.get_frm().doc.currency; + const taxes_html = taxes + .map((t) => { + if (t.tax_amount_after_discount_amount == 0.0) return; + // if tax rate is 0, don't print it. + const description = /[0-9]+/.test(t.description) + ? t.description + : t.rate != 0 + ? `${t.description} @ ${t.rate}%` + : t.description; + return `<div class="tax-row"> <div class="tax-label">${description}</div> - <div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, currency)}</div> + <div class="tax-value">${format_currency( + t.tax_amount_after_discount_amount, + currency, + )}</div> </div>`; - }).join(''); - this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html); - } else { - this.$totals_section.find('.taxes-container').css('display', 'none').html(''); - } - } - - get_cart_item({ name }) { - const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`; - return this.$cart_items_wrapper.find(item_selector); - } - - get_item_from_frm(item) { - const doc = this.events.get_frm().doc; - return doc.items.find(i => i.name == item.name); - } - - update_item_html(item, remove_item) { - const $item = this.get_cart_item(item); - - if (remove_item) { - $item && $item.next().remove() && $item.remove(); - } else { - const item_row = this.get_item_from_frm(item); - this.render_cart_item(item_row, $item); - } - - const no_of_cart_items = this.$cart_items_wrapper.find('.cart-item-wrapper').length; - this.highlight_checkout_btn(true); - - this.update_empty_cart_section(no_of_cart_items); - } - - render_cart_item(item_data, $item_to_update) { - const currency = this.events.get_frm().doc.currency; - const me = this; - - if (!$item_to_update.length) { - this.$cart_items_wrapper.prepend( - `<div class="cart-item-wrapper" data-row-name="${escape(item_data.name)}"></div> - <div class="seperator"></div>` - ) - $item_to_update = this.get_cart_item(item_data); - } - var item_html = `${get_item_image_html()}` - - if(me.custom_use_discount_percentage && !me.custom_use_discount_amount){ - item_html += `<div class="item-name-desc" style="flex: 2.8">` - } - if(me.custom_use_discount_amount && !me.custom_use_discount_percentage){ - item_html += `<div class="item-name-desc" style="flex: 2.8">` - } - if(me.custom_use_discount_amount && me.custom_use_discount_percentage){ - item_html += `<div class="item-name-desc" style="flex: 2.5">` - } - if(!me.custom_use_discount_amount && !me.custom_use_discount_percentage){ - item_html += `<div class="item-name-desc" style="flex: 3.5">` - } - - item_html += `<div class="item-name" style="flex: 4; white-space: normal; word-wrap: break-word; overflow: visible; line-height: 1.2;"> + }) + .join(""); + this.$totals_section + .find(".taxes-container") + .css("display", "flex") + .html(taxes_html); + } else { + this.$totals_section + .find(".taxes-container") + .css("display", "none") + .html(""); + } + } + + get_cart_item({ name }) { + const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`; + return this.$cart_items_wrapper.find(item_selector); + } + + get_item_from_frm(item) { + const doc = this.events.get_frm().doc; + return doc.items.find((i) => i.name == item.name); + } + + update_item_html(item, remove_item) { + const $item = this.get_cart_item(item); + + if (remove_item) { + $item && $item.next().remove() && $item.remove(); + } else { + const item_row = this.get_item_from_frm(item); + this.render_cart_item(item_row, $item); + } + + const no_of_cart_items = + this.$cart_items_wrapper.find(".cart-item-wrapper").length; + this.highlight_checkout_btn(true); + + this.update_empty_cart_section(no_of_cart_items); + } + + render_cart_item(item_data, $item_to_update) { + const currency = this.events.get_frm().doc.currency; + const me = this; + + if (!$item_to_update.length) { + this.$cart_items_wrapper.prepend( + `<div class="cart-item-wrapper" data-row-name="${escape( + item_data.name, + )}"></div> + <div class="seperator"></div>`, + ); + $item_to_update = this.get_cart_item(item_data); + } + var item_html = `${get_item_image_html()}`; + + if (me.custom_use_discount_percentage && !me.custom_use_discount_amount) { + item_html += `<div class="item-name-desc" style="flex: 2.8">`; + } + if (me.custom_use_discount_amount && !me.custom_use_discount_percentage) { + item_html += `<div class="item-name-desc" style="flex: 2.8">`; + } + if (me.custom_use_discount_amount && me.custom_use_discount_percentage) { + item_html += `<div class="item-name-desc" style="flex: 2.5">`; + } + if (!me.custom_use_discount_amount && !me.custom_use_discount_percentage) { + item_html += `<div class="item-name-desc" style="flex: 3.5">`; + } + + item_html += `<div class="item-name" style="flex: 4; white-space: normal; word-wrap: break-word; overflow: visible; line-height: 1.2;"> ${item_data.item_name} </div> - ${ get_description_html(item_data) } + ${get_description_html(item_data)} ${get_item_barcode(item_data)} </div> - ${get_rate_discount_html()}` - - $item_to_update.html(item_html) - if(me.custom_edit_rate){ - this[item_data.item_code + "_qty"] = frappe.ui.form.make_control({ - df: { - fieldname: "qty", - fieldtype: "Float", - onchange: function() { - // me.events.cart_item_clicked({ name: item_data.name }); - me.events.form_updated(item_data, "qty", this.value); - }, - }, - parent: $item_to_update.find(`.item-qty`), - render_input: true, - }); - var uoms = []; - if(item_data.custom_item_uoms){ - uoms = item_data.custom_item_uoms.split(","); - }else if(item_data.uom){ - uoms = [item_data.uom]; - } - if(me.custom_show_uom_in_cart){ - this[item_data.item_code + "_uom"] = frappe.ui.form.make_control({ - df: { - fieldname: "uom", - fieldtype: "Select", - onchange: function() { - me.events.form_updated(item_data, "uom", this.value); - }, - }, - parent: $item_to_update.find(`.item-uom`), - render_input: true, - }); - } - if(me.show_batch_in_cart){ - this[item_data.item_code + "_batch"] = frappe.ui.form.make_control({ - df: { - fieldname: "batch", - fieldtype: "Link", - options: "Batch", - get_query: function() { - return { - filters: { - item: item_data.item_code - } - }; - }, - onchange: function() { - me.events.form_updated(item_data, "batch_no", this.value); - }, - }, - parent: $item_to_update.find(`.item-batch`), - render_input: true, - }); - } - this[item_data.item_code + "_rate"] = frappe.ui.form.make_control({ - df: { - fieldname: "rate", - fieldtype: "Float", - read_only: !me.allow_rate_change, - onchange: function() { - me.events.form_updated(item_data, "rate", this.value); - }, - }, - parent: $item_to_update.find(`.item-rate`), - render_input: true, + ${get_rate_discount_html()}`; + + $item_to_update.html(item_html); + if (me.custom_edit_rate) { + this[item_data.item_code + "_qty"] = frappe.ui.form.make_control({ + df: { + fieldname: "qty", + fieldtype: "Float", + onchange: function () { + // me.events.cart_item_clicked({ name: item_data.name }); + me.events.form_updated(item_data, "qty", this.value); + }, + }, + parent: $item_to_update.find(`.item-qty`), + render_input: true, + }); + var uoms = []; + if (item_data.custom_item_uoms) { + uoms = item_data.custom_item_uoms.split(","); + } else if (item_data.uom) { + uoms = [item_data.uom]; + } + if (me.custom_show_uom_in_cart) { + this[item_data.item_code + "_uom"] = frappe.ui.form.make_control({ + df: { + fieldname: "uom", + fieldtype: "Select", + onchange: function () { + me.events.form_updated(item_data, "uom", this.value); + }, + }, + parent: $item_to_update.find(`.item-uom`), + render_input: true, + }); + } + if (me.show_batch_in_cart) { + this[item_data.item_code + "_batch"] = frappe.ui.form.make_control({ + df: { + fieldname: "batch", + fieldtype: "Link", + options: "Batch", + get_query: function () { + return { + filters: { + item: item_data.item_code, + }, + }; + }, + onchange: function () { + me.events.form_updated(item_data, "batch_no", this.value); + }, + }, + parent: $item_to_update.find(`.item-batch`), + render_input: true, + }); + } + this[item_data.item_code + "_rate"] = frappe.ui.form.make_control({ + df: { + fieldname: "rate", + fieldtype: "Float", + read_only: !me.allow_rate_change, + onchange: function () { + me.events.form_updated(item_data, "rate", this.value); + }, + }, + parent: $item_to_update.find(`.item-rate`), + render_input: true, + }); + if (me.custom_use_discount_percentage) { + this[item_data.item_code + "_discount"] = frappe.ui.form.make_control({ + df: { + fieldname: "discount", + fieldtype: "Float", + onchange: function () { + me.events.form_updated( + item_data, + "discount_percentage", + this.value, + ); + }, + }, + parent: $item_to_update.find(`.item-rate-discount`), + render_input: true, + }); + } + if (me.custom_use_discount_amount) { + this[item_data.item_code + "_discount_amount"] = + frappe.ui.form.make_control({ + df: { + fieldname: "discount_amount", + fieldtype: "Currency", + onchange: function () { + me.events.form_updated( + item_data, + "discount_amount", + this.value, + ); + }, + }, + parent: $item_to_update.find(`.item-rate-discount-amount`), + render_input: true, + }); + } + if (this.custom_show_incoming_rate) { + this[item_data.item_code + "_incoming_rate"] = + frappe.ui.form.make_control({ + df: { + fieldname: "incoming_rate", + fieldtype: "Float", + read_only: 1, + }, + parent: $item_to_update.find(`.item-incoming-rate`), + render_input: true, + }); + } + if (this.custom_show_logical_rack_in_cart) { + this[item_data.item_code + "_logical_rack"] = + frappe.ui.form.make_control({ + df: { + fieldname: "logical_rack", + fieldtype: "Data", + read_only: 1, + }, + parent: $item_to_update.find(`.item-logical-rack`), + render_input: true, + }); + } + if (this.custom_show_last_customer_rate) { + this[item_data.item_code + "_last_customer_rate"] = + frappe.ui.form.make_control({ + df: { + fieldname: "last_customer_rate", + fieldtype: "Float", + read_only: 1, + }, + parent: $item_to_update.find(`.item-last-customer-rate`), + render_input: true, + }); + } + this[item_data.item_code + "_amount"] = frappe.ui.form.make_control({ + df: { + fieldname: "amount", + fieldtype: "Float", + read_only: 1, + }, + parent: $item_to_update.find(`.item-rate-amount`), + render_input: true, + }); + + var delete_button = `<svg width="16px" height="16px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ff0000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M10 11V17" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M14 11V17" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M4 7H20" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M6 7H12H18V18C18 19.6569 16.6569 21 15 21H9C7.34315 21 6 19.6569 6 18V7Z" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>`; + var remove_button = frappe.ui.form.make_control({ + df: { + fieldname: "remove", + fieldtype: "Button", + label: delete_button, + }, + parent: $item_to_update.find(`.remove-button`), + render_input: true, + }); + remove_button.refresh(); // Make sure button is rendered + $(remove_button.$input).on("click", function () { + me.events.remove_item_from_cart(item_data); + me.prev_action = undefined; + me.toggle_item_highlight(); + me.events.numpad_event(undefined, "remove"); + }); + this[item_data.item_code + "_qty"].set_value(item_data.qty); + if (me.custom_show_uom_in_cart) { + this[item_data.item_code + "_uom"].df.options = uoms; + this[item_data.item_code + "_uom"].set_value(item_data.uom); + this[item_data.item_code + "_uom"].refresh(); + } + if (me.show_batch_in_cart) { + this[item_data.item_code + "_batch"].set_value(item_data.batch_no); + } + // this[item_data.item_code + "_amount"].set_value(parseFloat(item_data.amount).toFixed(3)); + // this[item_data.item_code + "_rate"].set_value(parseFloat(item_data.rate).toFixed(3)); + this[item_data.item_code + "_amount"].set_value(item_data.amount); + this[item_data.item_code + "_rate"].set_value(item_data.rate); + + if (me.custom_use_discount_percentage) { + this[item_data.item_code + "_discount"].set_value( + item_data.discount_percentage, + ); + } + if (me.custom_use_discount_amount) { + this[item_data.item_code + "_discount_amount"].set_value( + item_data.discount_amount, + ); + } + if (me.custom_show_incoming_rate) { + this[item_data.item_code + "_incoming_rate"].set_value( + item_data.custom_valuation_rate, + ); + } + if (me.custom_show_logical_rack_in_cart) { + this[item_data.item_code + "_logical_rack"].set_value( + item_data.custom_logical_rack, + ); + } + if (me.custom_show_last_customer_rate) { + if (me.customer_info.customer) { + frappe + .xcall("posnext.posnext.page.posnext.point_of_sale.get_lcr", { + customer: me.customer_info.customer, + item_code: item_data.item_code, + }) + .then((d) => { + this[item_data.item_code + "_last_customer_rate"].set_value(d); + }); + } + } + if (me.custom_show_uom_in_cart) { + frappe + .xcall("posnext.posnext.page.posnext.point_of_sale.get_uoms", { + item_code: item_data.item_code, + }) + .then((d) => { + this[item_data.item_code + "_uom"].df.options = d; + this[item_data.item_code + "_uom"].refresh(); + }); + } + } - }); - if(me.custom_use_discount_percentage){ - this[item_data.item_code + "_discount"] = frappe.ui.form.make_control({ - df: { - fieldname: "discount", - fieldtype: "Float", - onchange: function() { - me.events.form_updated(item_data, "discount_percentage", this.value); - }, - - }, - parent: $item_to_update.find(`.item-rate-discount`), - render_input: true, - }); - } - if(me.custom_use_discount_amount){ - this[item_data.item_code + "_discount_amount"] = frappe.ui.form.make_control({ - df: { - fieldname: "discount_amount", - fieldtype: "Currency", - onchange: function() { - me.events.form_updated(item_data, "discount_amount", this.value); - }, - - }, - parent: $item_to_update.find(`.item-rate-discount-amount`), - render_input: true, - }); - } - if(this.custom_show_incoming_rate){ - this[item_data.item_code + "_incoming_rate"] = frappe.ui.form.make_control({ - df: { - fieldname: "incoming_rate", - fieldtype: "Float", - read_only: 1 - }, - parent: $item_to_update.find(`.item-incoming-rate`), - render_input: true, - }); - } - if(this.custom_show_logical_rack_in_cart){ - this[item_data.item_code + "_logical_rack"] = frappe.ui.form.make_control({ - df: { - fieldname: "logical_rack", - fieldtype: "Data", - read_only: 1 - }, - parent: $item_to_update.find(`.item-logical-rack`), - render_input: true, - }); - } - if(this.custom_show_last_customer_rate){ - this[item_data.item_code + "_last_customer_rate"] = frappe.ui.form.make_control({ - df: { - fieldname: "last_customer_rate", - fieldtype: "Float", - read_only: 1 - }, - parent: $item_to_update.find(`.item-last-customer-rate`), - render_input: true, - }); - } - this[item_data.item_code + "_amount"] = frappe.ui.form.make_control({ - df: { - fieldname: "amount", - fieldtype: "Float", - read_only: 1 - }, - parent: $item_to_update.find(`.item-rate-amount`), - render_input: true, - }); + set_dynamic_rate_header_width(); - var delete_button = `<svg width="16px" height="16px" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" stroke="#ff0000"><g id="SVGRepo_bgCarrier" stroke-width="0"></g><g id="SVGRepo_tracerCarrier" stroke-linecap="round" stroke-linejoin="round"></g><g id="SVGRepo_iconCarrier"> <path d="M10 11V17" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M14 11V17" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M4 7H20" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M6 7H12H18V18C18 19.6569 16.6569 21 15 21H9C7.34315 21 6 19.6569 6 18V7Z" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> <path d="M9 5C9 3.89543 9.89543 3 11 3H13C14.1046 3 15 3.89543 15 5V7H9V5Z" stroke="#ff0000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"></path> </g></svg>` - var remove_button = frappe.ui.form.make_control({ - df: { - fieldname: "remove", - fieldtype: "Button", - label: delete_button, + function set_dynamic_rate_header_width() { + const rate_cols = Array.from( + me.$cart_items_wrapper.find(".item-rate-amount"), + ); + me.$cart_header.find(".rate-amount-header").css("width", ""); + me.$cart_items_wrapper.find(".item-rate-amount").css("width", ""); + let max_width = rate_cols.reduce((max_width, elm) => { + if ($(elm).width() > max_width) max_width = $(elm).width(); + return max_width; + }, 0); - }, - parent: $item_to_update.find(`.remove-button`), - render_input: true, - }); - remove_button.refresh(); // Make sure button is rendered - $(remove_button.$input).on("click", function() { - me.events.remove_item_from_cart(item_data) - me.prev_action = undefined; - me.toggle_item_highlight(); - me.events.numpad_event(undefined, "remove"); + max_width += 1; + if (max_width == 1) max_width = ""; - }); - this[item_data.item_code + "_qty"].set_value(item_data.qty) - if(me.custom_show_uom_in_cart){ - this[item_data.item_code + "_uom"].df.options = uoms; - this[item_data.item_code + "_uom"].set_value(item_data.uom); - this[item_data.item_code + "_uom"].refresh(); - } - if(me.show_batch_in_cart){ - this[item_data.item_code + "_batch"].set_value(item_data.batch_no); - } - // this[item_data.item_code + "_amount"].set_value(parseFloat(item_data.amount).toFixed(3)); - // this[item_data.item_code + "_rate"].set_value(parseFloat(item_data.rate).toFixed(3)); - this[item_data.item_code + "_amount"].set_value(item_data.amount); - this[item_data.item_code + "_rate"].set_value(item_data.rate); - - if(me.custom_use_discount_percentage){ - this[item_data.item_code + "_discount"].set_value(item_data.discount_percentage) - } - if(me.custom_use_discount_amount){ - this[item_data.item_code + "_discount_amount"].set_value(item_data.discount_amount) - } - if(me.custom_show_incoming_rate){ - this[item_data.item_code + "_incoming_rate"].set_value(item_data.custom_valuation_rate); - } - if(me.custom_show_logical_rack_in_cart){ - this[item_data.item_code + "_logical_rack"].set_value(item_data.custom_logical_rack); - } - if(me.custom_show_last_customer_rate){ - if (me.customer_info.customer){ - frappe.xcall("posnext.posnext.page.posnext.point_of_sale.get_lcr", { - "customer": me.customer_info.customer, "item_code": item_data.item_code - }).then(d=>{ - this[item_data.item_code + "_last_customer_rate"].set_value(d) - }) - } - } - if(me.custom_show_uom_in_cart){ - frappe.xcall("posnext.posnext.page.posnext.point_of_sale.get_uoms", { - "item_code": item_data.item_code - }).then(d=>{ - this[item_data.item_code + "_uom"].df.options = d; - this[item_data.item_code + "_uom"].refresh(); - }) - } - } - - set_dynamic_rate_header_width(); - - function set_dynamic_rate_header_width() { - const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount")); - me.$cart_header.find(".rate-amount-header").css("width", ""); - me.$cart_items_wrapper.find(".item-rate-amount").css("width", ""); - let max_width = rate_cols.reduce((max_width, elm) => { - if ($(elm).width() > max_width) - max_width = $(elm).width(); - return max_width; - }, 0); - - max_width += 1; - if (max_width == 1) max_width = ""; - - me.$cart_header.find(".rate-amount-header").css("width", max_width); - me.$cart_items_wrapper.find(".item-rate-amount").css("width", max_width); - } - - function get_rate_discount_html() { - if(me.custom_edit_rate){ - if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { - var html = ` + me.$cart_header.find(".rate-amount-header").css("width", max_width); + me.$cart_items_wrapper.find(".item-rate-amount").css("width", max_width); + } + + function get_rate_discount_html() { + if (me.custom_edit_rate) { + if ( + item_data.rate && + item_data.amount && + item_data.rate !== item_data.amount + ) { + var html = ` <div class="item-qty-rate" style="flex: 6"> <div class="item-qty" style="flex: 1"></div>`; - if(me.custom_show_uom_in_cart){ - html += `<div class="item-uom" style="flex: 1;text-align: left"></div>`; - } - if(me.show_batch_in_cart){ - html += `<div class="item-batch" style="flex: 1;text-align: left"></div>`; - } - html += `<div class="item-rate" style="flex: 1;"></div>`; - if(me.custom_use_discount_percentage){ - html += `<div class="item-rate-discount" style="flex: 1;text-align: left"></div>` - } - if(me.custom_use_discount_amount){ - html += `<div class="item-rate-discount-amount" style="flex: 1;text-align: left"></div>` - } - if(me.custom_show_incoming_rate){ - html += `<div class="item-incoming-rate" style="flex: 1"></div>` - } - if(me.custom_show_logical_rack_in_cart){ - html += `<div class="item-logical-rack" style="flex: 1"></div>` - } - if(me.custom_show_last_customer_rate){ - html += `<div class="item-last-customer-rate" style="flex: 1"></div>` - } - html += `<div class="item-rate-amount" style="flex: 1"></div> + if (me.custom_show_uom_in_cart) { + html += `<div class="item-uom" style="flex: 1;text-align: left"></div>`; + } + if (me.show_batch_in_cart) { + html += `<div class="item-batch" style="flex: 1;text-align: left"></div>`; + } + html += `<div class="item-rate" style="flex: 1;"></div>`; + if (me.custom_use_discount_percentage) { + html += `<div class="item-rate-discount" style="flex: 1;text-align: left"></div>`; + } + if (me.custom_use_discount_amount) { + html += `<div class="item-rate-discount-amount" style="flex: 1;text-align: left"></div>`; + } + if (me.custom_show_incoming_rate) { + html += `<div class="item-incoming-rate" style="flex: 1"></div>`; + } + if (me.custom_show_logical_rack_in_cart) { + html += `<div class="item-logical-rack" style="flex: 1"></div>`; + } + if (me.custom_show_last_customer_rate) { + html += `<div class="item-last-customer-rate" style="flex: 1"></div>`; + } + html += `<div class="item-rate-amount" style="flex: 1"></div> <div class="remove-button" style="margin-top:15px;display: flex;justify-content: center;align-items: center;"></div> - </div>` - return html - } else { - var html = ` + </div>`; + return html; + } else { + html = ` <div class="item-qty-rate" style="flex: 6"> <div class="item-qty" style="flex: 1"></div>`; - if(me.custom_show_uom_in_cart){ - html += `<div class="item-uom" style="flex: 1;text-align: left"></div>`; - } - if(me.show_batch_in_cart){ - html += `<div class="item-batch" style="flex: 1;text-align: left"></div>`; - } - html += `<div class="item-rate" style="flex: 1;"></div>`; - if(me.custom_use_discount_percentage){ - html += `<div class="item-rate-discount" style="flex: 1;text-align: left"></div>` - } - if(me.custom_use_discount_amount){ - html += `<div class="item-rate-discount-amount" style="flex: 1;text-align: left"></div>` - } - if(me.custom_show_incoming_rate){ - html += `<div class="item-incoming-rate" style="flex: 1"></div>` - } - if(me.custom_show_logical_rack_in_cart){ - html += `<div class="item-logical-rack" style="flex: 1"></div>` - } - if(me.custom_show_last_customer_rate){ - html += `<div class="item-last-customer-rate" style="flex: 1"></div>` - } - html += `<div class="item-rate-amount" style="flex: 1"></div> + if (me.custom_show_uom_in_cart) { + html += `<div class="item-uom" style="flex: 1;text-align: left"></div>`; + } + if (me.show_batch_in_cart) { + html += `<div class="item-batch" style="flex: 1;text-align: left"></div>`; + } + html += `<div class="item-rate" style="flex: 1;"></div>`; + if (me.custom_use_discount_percentage) { + html += `<div class="item-rate-discount" style="flex: 1;text-align: left"></div>`; + } + if (me.custom_use_discount_amount) { + html += `<div class="item-rate-discount-amount" style="flex: 1;text-align: left"></div>`; + } + if (me.custom_show_incoming_rate) { + html += `<div class="item-incoming-rate" style="flex: 1"></div>`; + } + if (me.custom_show_logical_rack_in_cart) { + html += `<div class="item-logical-rack" style="flex: 1"></div>`; + } + if (me.custom_show_last_customer_rate) { + html += `<div class="item-last-customer-rate" style="flex: 1"></div>`; + } + html += `<div class="item-rate-amount" style="flex: 1"></div> <div class="remove-button" style="margin-top:15px;display: flex;justify-content: center;align-items: center;"></div> - </div>` - return html - } - } else { - if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { - return ` - <div class="item-qty-rate" style="flex: 4" > - <div class="item-qty" style="flex: 1"><span>${item_data.qty || 0}</span></div> - <div class="item-qty" style="flex: 1"><span> ${item_data.uom}</span></div> + </div>`; + return html; + } + } else { + if ( + item_data.rate && + item_data.amount && + item_data.rate !== item_data.amount + ) { + return ` + <div class="item-qty-rate" style="flex: 4" > + <div class="item-qty" style="flex: 1"><span>${ + item_data.qty || 0 + }</span></div> + <div class="item-qty" style="flex: 1"><span> ${ + item_data.uom + }</span></div> <div class="item-qty" style="flex: 1"><span> ${item_data.batch}</span></div> <div class="item-rate-amount" style="flex: 1"> - <div class="item-rate">${parseFloat(item_data.amount).toFixed(2)}</div> - <div class="item-amount">${parseFloat(item_data.rate).toFixed(2)}</div> + <div class="item-rate">${parseFloat( + item_data.amount, + ).toFixed(2)}</div> + <div class="item-amount">${parseFloat( + item_data.rate, + ).toFixed(2)}</div> </div> - </div>` - } else { - return ` + </div>`; + } else { + return ` <div class="item-qty-rate" style="flex: 4" > - <div class="item-qty" style="flex: 1" ><span>${item_data.qty || 0}</span></div> - <div class="item-qty" style="flex: 1"><span> ${item_data.uom}</span></div> + <div class="item-qty" style="flex: 1" ><span>${ + item_data.qty || 0 + }</span></div> + <div class="item-qty" style="flex: 1"><span> ${ + item_data.uom + }</span></div> <div class="item-qty" style="flex: 1"><span> ${item_data.batch}</span></div> <div class="item-rate-amount" style="flex: 1"> - <div class="item-rate">${parseFloat(item_data.rate).toFixed(2)}</div> + <div class="item-rate">${parseFloat( + item_data.rate, + ).toFixed(2)}</div> </div> - </div>` - } - } - - } - - function get_description_html(item_data) { - const hide_description = me.custom_show_item_discription; - if (hide_description) { - if (item_data.description.indexOf('<div>') != -1) { - try { - item_data.description = $(item_data.description).text(); - } catch (error) { - item_data.description = item_data.description - .replace(/<div>/g, ' ') - .replace(/<\/div>/g, ' ') - .replace(/ +/g, ' '); - } - } - item_data.description = frappe.ellipsis(item_data.description, 45); - return `<div class="item-desc">${item_data.description}</div>`; - } - return ``; - } - - // FIXED FUNCTION: Properly handle barcode display without requiring a callback - function get_item_barcode(item_data) { - const show_barcode = me.custom_show_item_barcode; - - if (!show_barcode) { - return ''; - } - - // Create a unique placeholder ID for this item's barcodes - const barcode_placeholder_id = `barcode-${item_data.item_code.replace(/[^a-zA-Z0-9]/g, '-')}`; - - // Fetch barcodes asynchronously and update the placeholder - frappe.call({ - method: "posnext.posnext.page.posnext.point_of_sale.get_barcodes", - args: { - item_code: item_data.item_code - }, - callback: function(response) { - if (response.message && response.message.length > 0) { - const html = response.message.map(b => ` + </div>`; + } + } + } + + function get_description_html(item_data) { + const hide_description = me.custom_show_item_discription; + if (hide_description) { + if (item_data.description.indexOf("<div>") != -1) { + try { + item_data.description = $(item_data.description).text(); + } catch (error) { + item_data.description = item_data.description + .replace(/<div>/g, " ") + .replace(/<\/div>/g, " ") + .replace(/ +/g, " "); + } + } + item_data.description = frappe.ellipsis(item_data.description, 45); + return `<div class="item-desc">${item_data.description}</div>`; + } + return ``; + } + + // FIXED FUNCTION: Properly handle barcode display without requiring a callback + function get_item_barcode(item_data) { + const show_barcode = me.custom_show_item_barcode; + + if (!show_barcode) { + return ""; + } + + // Create a unique placeholder ID for this item's barcodes + const barcode_placeholder_id = `barcode-${item_data.item_code.replace( + /[^a-zA-Z0-9]/g, + "-", + )}`; + + // Fetch barcodes asynchronously and update the placeholder + frappe.call({ + method: "posnext.posnext.page.posnext.point_of_sale.get_barcodes", + args: { + item_code: item_data.item_code, + }, + callback: function (response) { + if (response.message && response.message.length > 0) { + const html = response.message + .map( + (b) => ` <div class="item-barcode" style="font-size: 12px; color: #888;"> ${b.barcode} </div> - `).join(''); - $(`#${barcode_placeholder_id}`).html(html); - } - } - }); - - // Return a placeholder div that will be filled when the data is available - return `<div id="${barcode_placeholder_id}" class="item-barcodes"></div>`; - } - - function get_item_image_html() { - const { image, item_name } = item_data; - if (!me.hide_images && image) { - return ` + `, + ) + .join(""); + $(`#${barcode_placeholder_id}`).html(html); + } + }, + }); + + // Return a placeholder div that will be filled when the data is available + return `<div id="${barcode_placeholder_id}" class="item-barcodes"></div>`; + } + + function get_item_image_html() { + const { image, item_name } = item_data; + if (!me.hide_images && image) { + return ` <div class="item-image"> <img onerror="cur_pos.cart.handle_broken_image(this)" src="${image}" alt="${frappe.get_abbr(item_name)}""> </div>`; - } else { - return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`; - } - } - } - - handle_broken_image($img) { - const item_abbr = $($img).attr('alt'); - $($img).parent().replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`); - } - - update_selector_value_in_cart_item(selector, value, item) { - const $item_to_update = this.get_cart_item(item); - $item_to_update.attr(`data-${selector}`, escape(value)); - } - - toggle_checkout_btn(show_checkout) { - if (show_checkout) { - if(this.show_checkout_button){ - this.$totals_section.find('.checkout-btn').css('display', 'flex'); - } else { - this.$totals_section.find('.checkout-btn').css('display', 'none'); - } - - if(this.show_held_button){ - this.$totals_section.find('.checkout-btn-held').css('display', 'flex'); - } else { - this.$totals_section.find('.checkout-btn-held').css('display', 'none'); - } - if(this.show_order_list_button){ - this.$totals_section.find('.checkout-btn-order').css('display', 'flex'); - } else { - this.$totals_section.find('.checkout-btn-order').css('display', 'none'); - } - this.$totals_section.find('.edit-cart-btn').css('display', 'none'); - } else { - this.$totals_section.find('.checkout-btn').css('display', 'none'); - this.$totals_section.find('.checkout-btn-held').css('display', 'none'); - this.$totals_section.find('.checkout-btn-held').css('display', 'none'); - this.$totals_section.find('.checkout-btn-order').css('display', 'none'); - this.$totals_section.find('.edit-cart-btn').css('display', 'flex'); - } - } - - highlight_checkout_btn(toggle) { - if (toggle) { - this.$add_discount_elem.css('display', 'flex'); - this.$cart_container.find('.checkout-btn').css({ - 'background-color': 'var(--blue-500)' - }); - if(this.show_held_button){ - this.$cart_container.find('.checkout-btn-held').css({ - 'background-color': 'var(--blue-500)' - }); - } else { - this.$cart_container.find('.checkout-btn-held').css({ - 'background-color': 'var(--blue-200)' - }); - } - if(this.show_order_list_button){ - this.$cart_container.find('.checkout-btn-order').css({ - 'background-color': 'var(--blue-500)' - }); - } else { - this.$cart_container.find('.checkout-btn-order').css({ - 'background-color': 'var(--blue-500)' - }); - } - - } else { - this.$add_discount_elem.css('display', 'none'); - this.$cart_container.find('.checkout-btn').css({ - 'background-color': 'var(--blue-200)' - }); - this.$cart_container.find('.checkout-btn-held').css({ - 'background-color': 'var(--blue-200)' - }); - - this.$cart_container.find('.checkout-btn-order').css({ - 'background-color': 'var(--blue-500)' - }); - } - } - - update_empty_cart_section(no_of_cart_items) { - const $no_item_element = this.$cart_items_wrapper.find('.no-item-wrapper'); - - // if cart has items and no item is present - no_of_cart_items > 0 && $no_item_element && $no_item_element.remove() && this.$cart_header.css('display', 'flex'); - - no_of_cart_items === 0 && !$no_item_element.length && this.make_no_items_placeholder(); - } - - on_numpad_event($btn) { - const current_action = $btn.attr('data-button-value'); - const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action); - const action_is_allowed = action_is_field_edit ? ( - (current_action == 'rate' && this.allow_rate_change) || - (current_action == 'discount_percentage' && this.allow_discount_change) || - (current_action == 'qty')) : true; - - const action_is_pressed_twice = this.prev_action === current_action; - const first_click_event = !this.prev_action; - const field_to_edit_changed = this.prev_action && this.prev_action != current_action; - - if (action_is_field_edit) { - if (!action_is_allowed) { - const label = current_action == 'rate' ? 'Rate'.bold() : 'Discount'.bold(); - const message = __('Editing {0} is not allowed as per POS Profile settings', [label]); - frappe.show_alert({ - indicator: 'red', - message: message - }); - frappe.utils.play_sound("error"); - return; - } - - if (first_click_event || field_to_edit_changed) { - this.prev_action = current_action; - } else if (action_is_pressed_twice) { - this.prev_action = undefined; - } - this.numpad_value = ''; - - } else if (current_action === 'checkout') { - this.prev_action = undefined; - this.toggle_item_highlight(); - this.events.numpad_event(undefined, current_action); - return; - } else if (current_action === 'remove') { - this.prev_action = undefined; - this.toggle_item_highlight(); - this.events.numpad_event(undefined, current_action); - return; - } else { - this.numpad_value = current_action === 'delete' ? this.numpad_value.slice(0, -1) : this.numpad_value + current_action; - this.numpad_value = this.numpad_value || 0; - } - - const first_click_event_is_not_field_edit = !action_is_field_edit && first_click_event; - - if (first_click_event_is_not_field_edit) { - frappe.show_alert({ - indicator: 'red', - message: __('Please select a field to edit from numpad') - }); - frappe.utils.play_sound("error"); - return; - } - - if (flt(this.numpad_value) > 100 && this.prev_action === 'discount_percentage') { - frappe.show_alert({ - message: __('Discount cannot be greater than 100%'), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); - this.numpad_value = current_action; - } - - this.highlight_numpad_btn($btn, current_action); - this.events.numpad_event(this.numpad_value, this.prev_action); - } - - highlight_numpad_btn($btn, curr_action) { - const curr_action_is_highlighted = $btn.hasClass('highlighted-numpad-btn'); - const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action); - - if (!curr_action_is_highlighted) { - $btn.addClass('highlighted-numpad-btn'); - } - if (this.prev_action === curr_action && curr_action_is_highlighted) { - // if Qty is pressed twice - $btn.removeClass('highlighted-numpad-btn'); - } - if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) { - // Order: Qty -> Rate then remove Qty highlight - const prev_btn = $(`[data-button-value='${this.prev_action}']`); - prev_btn.removeClass('highlighted-numpad-btn'); - } - if (!curr_action_is_action || curr_action === 'done') { - // if numbers are clicked - setTimeout(() => { - $btn.removeClass('highlighted-numpad-btn'); - }, 200); - } - } - - toggle_numpad(show) { - if (show) { - this.$totals_section.css('display', 'none'); - this.$numpad_section.css('display', 'flex'); - } else { - this.$totals_section.css('display', 'flex'); - this.$numpad_section.css('display', 'none'); - } - this.reset_numpad(); - } - - reset_numpad() { - this.numpad_value = ''; - this.prev_action = undefined; - this.$numpad_section.find('.highlighted-numpad-btn').removeClass('highlighted-numpad-btn'); - } - - toggle_numpad_field_edit(fieldname) { - if (['qty', 'discount_percentage', 'rate'].includes(fieldname)) { - this.$numpad_section.find(`[data-button-value="${fieldname}"]`).click(); - } - } - - toggle_customer_info(show) { - if (show) { - const { customer } = this.customer_info || {}; - - this.$cart_container.css('display', 'none'); - this.$customer_section.css({ - 'height': '100%', - 'padding-top': '0px' - }); - this.$customer_section.find('.customer-details').html( - `<div class="header"> + } else { + return `<div class="item-image item-abbr">${frappe.get_abbr( + item_name, + )}</div>`; + } + } + } + + handle_broken_image($img) { + const item_abbr = $($img).attr("alt"); + $($img) + .parent() + .replaceWith(`<div class="item-image item-abbr">${item_abbr}</div>`); + } + + update_selector_value_in_cart_item(selector, value, item) { + const $item_to_update = this.get_cart_item(item); + $item_to_update.attr(`data-${selector}`, escape(value)); + } + + toggle_checkout_btn(show_checkout) { + if (show_checkout) { + if (this.show_checkout_button) { + this.$totals_section.find(".checkout-btn").css("display", "flex"); + } else { + this.$totals_section.find(".checkout-btn").css("display", "none"); + } + + if (this.show_held_button) { + this.$totals_section.find(".checkout-btn-held").css("display", "flex"); + } else { + this.$totals_section.find(".checkout-btn-held").css("display", "none"); + } + if (this.show_order_list_button) { + this.$totals_section.find(".checkout-btn-order").css("display", "flex"); + } else { + this.$totals_section.find(".checkout-btn-order").css("display", "none"); + } + this.$totals_section.find(".edit-cart-btn").css("display", "none"); + } else { + this.$totals_section.find(".checkout-btn").css("display", "none"); + this.$totals_section.find(".checkout-btn-held").css("display", "none"); + this.$totals_section.find(".checkout-btn-held").css("display", "none"); + this.$totals_section.find(".checkout-btn-order").css("display", "none"); + this.$totals_section.find(".edit-cart-btn").css("display", "flex"); + } + } + + highlight_checkout_btn(toggle) { + if (toggle) { + this.$add_discount_elem.css("display", "flex"); + this.$cart_container.find(".checkout-btn").css({ + "background-color": "var(--blue-500)", + }); + if (this.show_held_button) { + this.$cart_container.find(".checkout-btn-held").css({ + "background-color": "var(--blue-500)", + }); + } else { + this.$cart_container.find(".checkout-btn-held").css({ + "background-color": "var(--blue-200)", + }); + } + if (this.show_order_list_button) { + this.$cart_container.find(".checkout-btn-order").css({ + "background-color": "var(--blue-500)", + }); + } else { + this.$cart_container.find(".checkout-btn-order").css({ + "background-color": "var(--blue-500)", + }); + } + } else { + this.$add_discount_elem.css("display", "none"); + this.$cart_container.find(".checkout-btn").css({ + "background-color": "var(--blue-200)", + }); + this.$cart_container.find(".checkout-btn-held").css({ + "background-color": "var(--blue-200)", + }); + + this.$cart_container.find(".checkout-btn-order").css({ + "background-color": "var(--blue-500)", + }); + } + } + + update_empty_cart_section(no_of_cart_items) { + const $no_item_element = this.$cart_items_wrapper.find(".no-item-wrapper"); + + // if cart has items and no item is present + no_of_cart_items > 0 && + $no_item_element && + $no_item_element.remove() && + this.$cart_header.css("display", "flex"); + + no_of_cart_items === 0 && + !$no_item_element.length && + this.make_no_items_placeholder(); + } + + on_numpad_event($btn) { + const current_action = $btn.attr("data-button-value"); + const action_is_field_edit = [ + "qty", + "discount_percentage", + "rate", + ].includes(current_action); + const action_is_allowed = action_is_field_edit + ? (current_action == "rate" && this.allow_rate_change) || + (current_action == "discount_percentage" && + this.allow_discount_change) || + current_action == "qty" + : true; + + const action_is_pressed_twice = this.prev_action === current_action; + const first_click_event = !this.prev_action; + const field_to_edit_changed = + this.prev_action && this.prev_action != current_action; + + if (action_is_field_edit) { + if (!action_is_allowed) { + const label = + current_action == "rate" ? "Rate".bold() : "Discount".bold(); + const message = __( + "Editing {0} is not allowed as per POS Profile settings", + [label], + ); + frappe.show_alert({ + indicator: "red", + message: message, + }); + frappe.utils.play_sound("error"); + return; + } + + if (first_click_event || field_to_edit_changed) { + this.prev_action = current_action; + } else if (action_is_pressed_twice) { + this.prev_action = undefined; + } + this.numpad_value = ""; + } else if (current_action === "checkout") { + this.prev_action = undefined; + this.toggle_item_highlight(); + this.events.numpad_event(undefined, current_action); + return; + } else if (current_action === "remove") { + this.prev_action = undefined; + this.toggle_item_highlight(); + this.events.numpad_event(undefined, current_action); + return; + } else { + this.numpad_value = + current_action === "delete" + ? this.numpad_value.slice(0, -1) + : this.numpad_value + current_action; + this.numpad_value = this.numpad_value || 0; + } + + const first_click_event_is_not_field_edit = + !action_is_field_edit && first_click_event; + + if (first_click_event_is_not_field_edit) { + frappe.show_alert({ + indicator: "red", + message: __("Please select a field to edit from numpad"), + }); + frappe.utils.play_sound("error"); + return; + } + + if ( + flt(this.numpad_value) > 100 && + this.prev_action === "discount_percentage" + ) { + frappe.show_alert({ + message: __("Discount cannot be greater than 100%"), + indicator: "orange", + }); + frappe.utils.play_sound("error"); + this.numpad_value = current_action; + } + + this.highlight_numpad_btn($btn, current_action); + this.events.numpad_event(this.numpad_value, this.prev_action); + } + + highlight_numpad_btn($btn, curr_action) { + const curr_action_is_highlighted = $btn.hasClass("highlighted-numpad-btn"); + const curr_action_is_action = [ + "qty", + "discount_percentage", + "rate", + "done", + ].includes(curr_action); + + if (!curr_action_is_highlighted) { + $btn.addClass("highlighted-numpad-btn"); + } + if (this.prev_action === curr_action && curr_action_is_highlighted) { + // if Qty is pressed twice + $btn.removeClass("highlighted-numpad-btn"); + } + if ( + this.prev_action && + this.prev_action !== curr_action && + curr_action_is_action + ) { + // Order: Qty -> Rate then remove Qty highlight + const prev_btn = $(`[data-button-value='${this.prev_action}']`); + prev_btn.removeClass("highlighted-numpad-btn"); + } + if (!curr_action_is_action || curr_action === "done") { + // if numbers are clicked + setTimeout(() => { + $btn.removeClass("highlighted-numpad-btn"); + }, 200); + } + } + + toggle_numpad(show) { + if (show) { + this.$totals_section.css("display", "none"); + this.$numpad_section.css("display", "flex"); + } else { + this.$totals_section.css("display", "flex"); + this.$numpad_section.css("display", "none"); + } + this.reset_numpad(); + } + + reset_numpad() { + this.numpad_value = ""; + this.prev_action = undefined; + this.$numpad_section + .find(".highlighted-numpad-btn") + .removeClass("highlighted-numpad-btn"); + } + + toggle_numpad_field_edit(fieldname) { + if (["qty", "discount_percentage", "rate"].includes(fieldname)) { + this.$numpad_section.find(`[data-button-value="${fieldname}"]`).click(); + } + } + + toggle_customer_info(show) { + if (show) { + const { customer } = this.customer_info || {}; + + this.$cart_container.css("display", "none"); + this.$customer_section.css({ + height: "100%", + "padding-top": "0px", + }); + this.$customer_section.find(".customer-details").html( + `<div class="header"> <div class="label">Contact Details</div> <div class="close-details-btn"> @@ -1611,126 +1877,162 @@ this.highlight_checkout_btn(true); <div class="loyalty_program-field"></div> <div class="loyalty_points-field"></div> </div> - <div class="transactions-label">Recent Transactions</div>` - ); - // transactions need to be in diff div from sticky elem for scrolling - this.$customer_section.append(`<div class="customer-transactions"></div>`); - if(this.mobile_number_based_customer){ - this.$customer_section.find('.mobile_no-field').css('display', 'none'); - this.$customer_section.find('.close-details-btn').css('display', 'none'); - } else { - this.$customer_section.find('.mobile_no-field').css('display', 'flex'); - this.$customer_section.find('.close-details-btn').css('display', 'flex'); - } - this.render_customer_fields(); - this.fetch_customer_transactions(); - - } else { - this.$cart_container.css('display', 'flex'); - this.$customer_section.css({ - 'height': '', - 'padding-top': '' - }); - - this.update_customer_section(); - } - } - - render_customer_fields() { - const $customer_form = this.$customer_section.find('.customer-fields-container'); - - const dfs = [{ - fieldname: 'email_id', - label: __('Email'), - fieldtype: 'Data', - options: 'email', - placeholder: __("Enter customer's email") - },{ - fieldname: 'mobile_no', - label: __('Phone Number'), - fieldtype: 'Data', - placeholder: __("Enter customer's phone number") - },{ - fieldname: 'loyalty_program', - label: __('Loyalty Program'), - fieldtype: 'Link', - options: 'Loyalty Program', - placeholder: __("Select Loyalty Program") - },{ - fieldname: 'loyalty_points', - label: __('Loyalty Points'), - fieldtype: 'Data', - read_only: 1 - }]; - - const me = this; - dfs.forEach(df => { - this[`customer_${df.fieldname}_field`] = frappe.ui.form.make_control({ - df: { ...df, - onchange: handle_customer_field_change, - }, - parent: $customer_form.find(`.${df.fieldname}-field`), - render_input: true, - }); - this[`customer_${df.fieldname}_field`].set_value(this.customer_info[df.fieldname]); - }) - - function handle_customer_field_change() { - const current_value = me.customer_info[this.df.fieldname]; - const current_customer = me.customer_info.customer; - - if (this.value && current_value != this.value && this.df.fieldname != 'loyalty_points') { - frappe.call({ - method: 'posnext.posnext.page.posnext.point_of_sale.set_customer_info', - args: { - fieldname: this.df.fieldname, - customer: current_customer, - value: this.value - }, - callback: (r) => { - if(!r.exc) { - me.customer_info[this.df.fieldname] = this.value; - frappe.show_alert({ - message: __("Customer contact updated successfully."), - indicator: 'green' - }); - frappe.utils.play_sound("submit"); - } - } - }); - } - } - } - - fetch_customer_transactions() { - frappe.db.get_list('Sales Invoice', { - filters: { customer: this.customer_info.customer, docstatus: 1 }, - fields: ['name', 'grand_total', 'status', 'posting_date', 'posting_time', 'currency'], - limit: 20 - }).then((res) => { - const transaction_container = this.$customer_section.find('.customer-transactions'); - - if (!res.length) { - transaction_container.html( - `<div class="no-transactions-placeholder">No recent transactions found</div>` - ) - return; - } - - const elapsed_time = moment(res[0].posting_date+" "+res[0].posting_time).fromNow(); - this.$customer_section.find('.customer-desc').html(`Last transacted ${elapsed_time}`); - - res.forEach(invoice => { - const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma"); - let indicator_color = { - 'Paid': 'green', - 'Draft': 'red', - 'Return': 'gray', - 'Consolidated': 'blue' - }; - - transaction_container.append( - `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}"> + <div class="transactions-label">Recent Transactions</div>`, + ); + // transactions need to be in diff div from sticky elem for scrolling + this.$customer_section.append( + `<div class="customer-transactions"></div>`, + ); + if (this.mobile_number_based_customer) { + this.$customer_section.find(".mobile_no-field").css("display", "none"); + this.$customer_section + .find(".close-details-btn") + .css("display", "none"); + } else { + this.$customer_section.find(".mobile_no-field").css("display", "flex"); + this.$customer_section + .find(".close-details-btn") + .css("display", "flex"); + } + this.render_customer_fields(); + this.fetch_customer_transactions(); + } else { + this.$cart_container.css("display", "flex"); + this.$customer_section.css({ + height: "", + "padding-top": "", + }); + + this.update_customer_section(); + } + } + + render_customer_fields() { + const $customer_form = this.$customer_section.find( + ".customer-fields-container", + ); + + const dfs = [ + { + fieldname: "email_id", + label: __("Email"), + fieldtype: "Data", + options: "email", + placeholder: __("Enter customer's email"), + }, + { + fieldname: "mobile_no", + label: __("Phone Number"), + fieldtype: "Data", + placeholder: __("Enter customer's phone number"), + }, + { + fieldname: "loyalty_program", + label: __("Loyalty Program"), + fieldtype: "Link", + options: "Loyalty Program", + placeholder: __("Select Loyalty Program"), + }, + { + fieldname: "loyalty_points", + label: __("Loyalty Points"), + fieldtype: "Data", + read_only: 1, + }, + ]; + + const me = this; + dfs.forEach((df) => { + this[`customer_${df.fieldname}_field`] = frappe.ui.form.make_control({ + df: { ...df, onchange: handle_customer_field_change }, + parent: $customer_form.find(`.${df.fieldname}-field`), + render_input: true, + }); + this[`customer_${df.fieldname}_field`].set_value( + this.customer_info[df.fieldname], + ); + }); + + function handle_customer_field_change() { + const current_value = me.customer_info[this.df.fieldname]; + const current_customer = me.customer_info.customer; + + if ( + this.value && + current_value != this.value && + this.df.fieldname != "loyalty_points" + ) { + frappe.call({ + method: + "posnext.posnext.page.posnext.point_of_sale.set_customer_info", + args: { + fieldname: this.df.fieldname, + customer: current_customer, + value: this.value, + }, + callback: (r) => { + if (!r.exc) { + me.customer_info[this.df.fieldname] = this.value; + frappe.show_alert({ + message: __("Customer contact updated successfully."), + indicator: "green", + }); + frappe.utils.play_sound("submit"); + } + }, + }); + } + } + } + + fetch_customer_transactions() { + frappe.db + .get_list("Sales Invoice", { + filters: { customer: this.customer_info.customer, docstatus: 1 }, + fields: [ + "name", + "grand_total", + "status", + "posting_date", + "posting_time", + "currency", + ], + limit: 20, + }) + .then((res) => { + const transaction_container = this.$customer_section.find( + ".customer-transactions", + ); + + if (!res.length) { + transaction_container.html( + `<div class="no-transactions-placeholder">No recent transactions found</div>`, + ); + return; + } + + const elapsed_time = moment( + res[0].posting_date + " " + res[0].posting_time, + ).fromNow(); + this.$customer_section + .find(".customer-desc") + .html(`Last transacted ${elapsed_time}`); + + res.forEach((invoice) => { + const posting_datetime = moment( + invoice.posting_date + " " + invoice.posting_time, + ).format("Do MMMM, h:mma"); + let indicator_color = { + Paid: "green", + Draft: "red", + Return: "gray", + Consolidated: "blue", + }; + + transaction_container.append( + `<div class="invoice-wrapper" data-invoice-name="${escape( + invoice.name, + )}"> <div class="invoice-name-date"> <div class="invoice-name">${invoice.name}</div> <div class="invoice-date">${posting_datetime}</div> @@ -1740,200 +2042,235 @@ this.highlight_checkout_btn(true); ${format_currency(invoice.grand_total, invoice.currency, 0) || 0} </div> <div class="invoice-status"> - <span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status]}"> + <span class="indicator-pill whitespace-nowrap ${ + indicator_color[invoice.status] + }"> <span>${invoice.status}</span> </span> </div> </div> </div> - <div class="seperator"></div>` - ) - }); - }); - } - - attach_refresh_field_event(frm) { - $(frm.wrapper).off('refresh-fields'); - $(frm.wrapper).on('refresh-fields', () => { - if (frm.doc.items.length) { - this.$cart_items_wrapper.html(''); - frm.doc.items.forEach(item => { - this.update_item_html(item); - }); - } - }); - } - - load_invoice() { - console.log("Load invoice") - const frm = this.events.get_frm(); - - this.attach_refresh_field_event(frm); - - this.fetch_customer_details(frm.doc.customer).then(() => { - this.events.customer_details_updated(this.customer_info); - this.update_customer_section(); - - this.$cart_items_wrapper.html(''); - if (frm.doc.items.length) { - frm.doc.items.forEach(item => { - this.update_item_html(item); - }); - } else { - this.make_no_items_placeholder(); - this.highlight_checkout_btn(true); - } - - this.update_totals_section(frm); - - if(frm.doc.docstatus === 1) { - this.$totals_section.find('.checkout-btn').css('display', 'none'); - this.$totals_section.find('.checkout-btn-held').css('display', 'none'); - if(this.show_order_list_button){ - this.$totals_section.find('.checkout-btn-order').css('display', 'flex'); - } else { - this.$totals_section.find('.checkout-btn-order').css('display', 'none'); - } - this.$totals_section.find('.edit-cart-btn').css('display', 'none'); - } else { - if(this.show_checkout_button) { - this.$totals_section.find('.checkout-btn').css('display', 'flex'); - } else { - this.$totals_section.find('.checkout-btn').css('display', 'none'); - - } - if(this.show_held_button){ - this.$totals_section.find('.checkout-btn-held').css('display', 'flex'); - } else { - this.$totals_section.find('.checkout-btn-held').css('display', 'none'); - } - if(this.show_order_list_button){ - this.$totals_section.find('.checkout-btn-order').css('display', 'flex'); - } else { - this.$totals_section.find('.checkout-btn-order').css('display', 'none'); - } - this.$totals_section.find('.edit-cart-btn').css('display', 'none'); - } - - this.toggle_component(true); - }); - } - - toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); - } - - show_reference_dialog(mobile_number = null) { - const me = this; - const dialog = new frappe.ui.Dialog({ - title: __('Enter Reference Details'), - fields: [ - { - fieldtype: 'Data', - label: __('Reference Number'), - fieldname: 'reference_no', - reqd: 1 - }, - { - fieldtype: 'Data', - label: __('Reference Name'), - fieldname: 'reference_name', - reqd: 1 - } - ], - primary_action_label: __('Hold Invoice'), - primary_action: async (values) => { - if (mobile_number) { - // Create customer if mobile number provided - await frappe.call({ - method: "posnext.posnext.page.posnext.point_of_sale.create_customer", - args: { customer: mobile_number }, - freeze: true, - freeze_message: "Creating Customer...." - }); - - const frm = me.events.get_frm(); - await frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'customer', mobile_number); - await frm.script_manager.trigger('customer', frm.doc.doctype, frm.doc.name); - } - - // Update reference details - const frm = me.events.get_frm(); - frm.doc.custom_reference_no = values.reference_no; - frm.doc.custom_reference_name = values.reference_name; - - dialog.hide(); - await me.events.save_draft_invoice(); - } - }); - dialog.show(); - } - - async hold_invoice(mobile_number = null) { - if (mobile_number) { - await frappe.call({ - method: "posnext.posnext.page.posnext.point_of_sale.create_customer", - args: { customer: mobile_number }, - freeze: true, - freeze_message: "Creating Customer...." - }); - - const frm = this.events.get_frm(); - await frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'customer', mobile_number); - await frm.script_manager.trigger('customer', frm.doc.doctype, frm.doc.name); - } - - await this.events.save_draft_invoice(); - } -} - -document.addEventListener('keydown', function (event) { - const activeElement = document.activeElement; - const isInputActive = activeElement.tagName === 'INPUT' || - activeElement.tagName === 'TEXTAREA' || - activeElement.isContentEditable; - - if (event.key === 'F1' && !isInputActive) { - event.preventDefault(); // Prevent browser help window - const checkoutButton = document.querySelector('.checkout-btn'); - if (checkoutButton) { - checkoutButton.click(); + <div class="seperator"></div>`, + ); + }); + }); + } + + attach_refresh_field_event(frm) { + $(frm.wrapper).off("refresh-fields"); + $(frm.wrapper).on("refresh-fields", () => { + if (frm.doc.items.length) { + this.$cart_items_wrapper.html(""); + frm.doc.items.forEach((item) => { + this.update_item_html(item); + }); + } + }); + } + + load_invoice() { + console.log("Load invoice"); + const frm = this.events.get_frm(); + + this.attach_refresh_field_event(frm); + + this.fetch_customer_details(frm.doc.customer).then(() => { + this.events.customer_details_updated(this.customer_info); + this.update_customer_section(); + + this.$cart_items_wrapper.html(""); + if (frm.doc.items.length) { + frm.doc.items.forEach((item) => { + this.update_item_html(item); + }); + } else { + this.make_no_items_placeholder(); + this.highlight_checkout_btn(true); + } + + this.update_totals_section(frm); + + if (frm.doc.docstatus === 1) { + this.$totals_section.find(".checkout-btn").css("display", "none"); + this.$totals_section.find(".checkout-btn-held").css("display", "none"); + if (this.show_order_list_button) { + this.$totals_section + .find(".checkout-btn-order") + .css("display", "flex"); } else { - console.warn("Checkout button not found!"); + this.$totals_section + .find(".checkout-btn-order") + .css("display", "none"); } - } - - // New shortcut for the held checkout button - if (event.key === 'F2' && !isInputActive) { - event.preventDefault(); // Prevent default action - const heldCheckoutButton = document.querySelector('.checkout-btn-held'); - if (heldCheckoutButton) { - heldCheckoutButton.click(); + this.$totals_section.find(".edit-cart-btn").css("display", "none"); + } else { + if (this.show_checkout_button) { + this.$totals_section.find(".checkout-btn").css("display", "flex"); } else { - console.warn("Held Checkout button not found!"); + this.$totals_section.find(".checkout-btn").css("display", "none"); } - } - - // ... existing code ... - // New shortcut for the checkout button order - if (event.key === 'F3' && !isInputActive) { - event.preventDefault(); // Prevent default action - const orderCheckoutButton = document.querySelector('.checkout-btn-order'); - if (orderCheckoutButton) { - orderCheckoutButton.click(); + if (this.show_held_button) { + this.$totals_section + .find(".checkout-btn-held") + .css("display", "flex"); } else { - console.warn("Order Checkout button not found!"); + this.$totals_section + .find(".checkout-btn-held") + .css("display", "none"); } - } -// ... existing code ... - // New shortcut for the search field button - if (event.key === 'F4' && !isInputActive) { - event.preventDefault(); // Prevent default action - const searchFieldButton = document.querySelector('.search-field button'); // Adjust selector as needed - if (searchFieldButton) { - searchFieldButton.click(); + if (this.show_order_list_button) { + this.$totals_section + .find(".checkout-btn-order") + .css("display", "flex"); } else { - console.warn("Search field button not found!"); + this.$totals_section + .find(".checkout-btn-order") + .css("display", "none"); } + this.$totals_section.find(".edit-cart-btn").css("display", "none"); + } + + this.toggle_component(true); + }); + } + + toggle_component(show) { + show + ? this.$component.css("display", "flex") + : this.$component.css("display", "none"); + } + + show_reference_dialog(mobile_number = null) { + const me = this; + const dialog = new frappe.ui.Dialog({ + title: __("Enter Reference Details"), + fields: [ + { + fieldtype: "Data", + label: __("Reference Number"), + fieldname: "reference_no", + reqd: 1, + }, + { + fieldtype: "Data", + label: __("Reference Name"), + fieldname: "reference_name", + reqd: 1, + }, + ], + primary_action_label: __("Hold Invoice"), + primary_action: async (values) => { + if (mobile_number) { + // Create customer if mobile number provided + await frappe.call({ + method: + "posnext.posnext.page.posnext.point_of_sale.create_customer", + args: { customer: mobile_number }, + freeze: true, + freeze_message: "Creating Customer....", + }); + + const frm = me.events.get_frm(); + await frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "customer", + mobile_number, + ); + await frm.script_manager.trigger( + "customer", + frm.doc.doctype, + frm.doc.name, + ); + } + + // Update reference details + const frm = me.events.get_frm(); + frm.doc.custom_reference_no = values.reference_no; + frm.doc.custom_reference_name = values.reference_name; + + dialog.hide(); + await me.events.save_draft_invoice(); + }, + }); + dialog.show(); + } + + async hold_invoice(mobile_number = null) { + if (mobile_number) { + await frappe.call({ + method: "posnext.posnext.page.posnext.point_of_sale.create_customer", + args: { customer: mobile_number }, + freeze: true, + freeze_message: "Creating Customer....", + }); + + const frm = this.events.get_frm(); + await frappe.model.set_value( + frm.doc.doctype, + frm.doc.name, + "customer", + mobile_number, + ); + await frm.script_manager.trigger( + "customer", + frm.doc.doctype, + frm.doc.name, + ); + } + + await this.events.save_draft_invoice(); + } +}; + +document.addEventListener("keydown", function (event) { + const activeElement = document.activeElement; + const isInputActive = + activeElement.tagName === "INPUT" || + activeElement.tagName === "TEXTAREA" || + activeElement.isContentEditable; + + if (event.key === "F1" && !isInputActive) { + event.preventDefault(); // Prevent browser help window + const checkoutButton = document.querySelector(".checkout-btn"); + if (checkoutButton) { + checkoutButton.click(); + } else { + console.warn("Checkout button not found!"); + } + } + + // New shortcut for the held checkout button + if (event.key === "F2" && !isInputActive) { + event.preventDefault(); // Prevent default action + const heldCheckoutButton = document.querySelector(".checkout-btn-held"); + if (heldCheckoutButton) { + heldCheckoutButton.click(); + } else { + console.warn("Held Checkout button not found!"); + } + } + + // ... existing code ... + // New shortcut for the checkout button order + if (event.key === "F3" && !isInputActive) { + event.preventDefault(); // Prevent default action + const orderCheckoutButton = document.querySelector(".checkout-btn-order"); + if (orderCheckoutButton) { + orderCheckoutButton.click(); + } else { + console.warn("Order Checkout button not found!"); + } + } + // ... existing code ... + // New shortcut for the search field button + if (event.key === "F4" && !isInputActive) { + event.preventDefault(); // Prevent default action + const searchFieldButton = document.querySelector(".search-field button"); // Adjust selector as needed + if (searchFieldButton) { + searchFieldButton.click(); + } else { + console.warn("Search field button not found!"); } -}); \ No newline at end of file + } +}); diff --git a/posnext/public/js/pos_item_details.js b/posnext/public/js/pos_item_details.js index a803a76..433dcd9 100644 --- a/posnext/public/js/pos_item_details.js +++ b/posnext/public/js/pos_item_details.js @@ -1,36 +1,36 @@ -frappe.provide('posnext.PointOfSale'); +frappe.provide("posnext.PointOfSale"); posnext.PointOfSale.ItemDetails = class { - constructor({ wrapper, events, settings }) { - this.wrapper = wrapper; - this.events = events; - this.hide_images = settings.hide_images; - this.allow_rate_change = settings.allow_rate_change; - this.allow_discount_change = settings.allow_discount_change; - this.custom_edit_rate_and_uom = settings.custom_edit_rate_and_uom; - this.current_item = {}; - - this.init_component(); - } - - init_component() { - this.prepare_dom(); - this.init_child_components(); - this.bind_events(); - this.attach_shortcuts(); - } - - prepare_dom() { - this.wrapper.append( - `<section class="item-details-container" id="item-details-container"></section>` - ) - - this.$component = this.wrapper.find('.item-details-container'); - } - - init_child_components() { - this.$component.html( - `<div class="item-details-header"> - <div class="label">${__('Item Detailss')}</div> + constructor({ wrapper, events, settings }) { + this.wrapper = wrapper; + this.events = events; + this.hide_images = settings.hide_images; + this.allow_rate_change = settings.allow_rate_change; + this.allow_discount_change = settings.allow_discount_change; + this.custom_edit_rate_and_uom = settings.custom_edit_rate_and_uom; + this.current_item = {}; + + this.init_component(); + } + + init_component() { + this.prepare_dom(); + this.init_child_components(); + this.bind_events(); + this.attach_shortcuts(); + } + + prepare_dom() { + this.wrapper.append( + `<section class="item-details-container" id="item-details-container"></section>`, + ); + + this.$component = this.wrapper.find(".item-details-container"); + } + + init_child_components() { + this.$component.html( + `<div class="item-details-header"> + <div class="label">${__("Item Detailss")}</div> <div class="close-btn"> <svg width="32" height="32" viewBox="0 0 14 14" fill="none"> <path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/> @@ -47,370 +47,426 @@ posnext.PointOfSale.ItemDetails = class { </div> <div class="discount-section"></div> <div class="form-container"></div> - <div class="serial-batch-container"></div>` - ) - - this.$item_name = this.$component.find('.item-name'); - this.$item_description = this.$component.find('.item-desc'); - this.$item_price = this.$component.find('.item-price'); - this.$item_image = this.$component.find('.item-image'); - this.$form_container = this.$component.find('.form-container'); - this.$dicount_section = this.$component.find('.discount-section'); - this.$serial_batch_container = this.$component.find('.serial-batch-container'); - } - - compare_with_current_item(item) { - // returns true if `item` is currently being edited - return item && item.name == this.current_item.name; - } - - async toggle_item_details_section(item) { - const current_item_changed = !this.compare_with_current_item(item); - - // if item is null or highlighted cart item is clicked twice - const hide_item_details = !Boolean(item) || !current_item_changed; - - if ((!hide_item_details && current_item_changed) || hide_item_details) { - // if item details is being closed OR if item details is opened but item is changed - // in both cases, if the current item is a serialized item, then validate and remove the item - await this.validate_serial_batch_item(); - } - if(!this.custom_edit_rate_and_uom){ - this.events.toggle_item_selector(!hide_item_details); - this.toggle_component(!hide_item_details); - } - - - if (item && current_item_changed) { - this.doctype = item.doctype; - this.item_meta = frappe.get_meta(this.doctype); - this.name = item.name; - this.item_row = item; - this.currency = this.events.get_frm().doc.currency; - - this.current_item = item; - - this.render_dom(item); - this.render_discount_dom(item); - this.render_form(item); - this.events.highlight_cart_item(item); - } else { - this.current_item = {}; - } - } - - validate_serial_batch_item() { - const doc = this.events.get_frm().doc; - const item_row = doc.items.find(item => item.name === this.name); - - if (!item_row) return; - - const serialized = item_row.has_serial_no; - const batched = item_row.has_batch_no; - const no_bundle_selected = !item_row.serial_and_batch_bundle; - - if ((serialized && no_bundle_selected) || (batched && no_bundle_selected)) { - frappe.show_alert({ - message: __("Item is removed since no serial / batch no selected."), - indicator: 'orange' - }); - frappe.utils.play_sound("cancel"); - return this.events.remove_item_from_cart(); - } - } - - render_dom(item) { - let { item_name, description, image, price_list_rate } = item; - - function get_description_html() { - if (description) { - description = description.indexOf('...') === -1 && description.length > 140 ? description.substr(0, 139) + '...' : description; - return description; - } - return ``; - } - - this.$item_name.html(item_name); - this.$item_description.html(get_description_html()); - this.$item_price.html(format_currency(price_list_rate, this.currency)); - if (!this.hide_images && image) { - this.$item_image.html( - `<img + <div class="serial-batch-container"></div>`, + ); + + this.$item_name = this.$component.find(".item-name"); + this.$item_description = this.$component.find(".item-desc"); + this.$item_price = this.$component.find(".item-price"); + this.$item_image = this.$component.find(".item-image"); + this.$form_container = this.$component.find(".form-container"); + this.$dicount_section = this.$component.find(".discount-section"); + this.$serial_batch_container = this.$component.find( + ".serial-batch-container", + ); + } + + compare_with_current_item(item) { + // returns true if `item` is currently being edited + return item && item.name == this.current_item.name; + } + + async toggle_item_details_section(item) { + const current_item_changed = !this.compare_with_current_item(item); + + // if item is null or highlighted cart item is clicked twice + const hide_item_details = !Boolean(item) || !current_item_changed; + + if ((!hide_item_details && current_item_changed) || hide_item_details) { + // if item details is being closed OR if item details is opened but item is changed + // in both cases, if the current item is a serialized item, then validate and remove the item + await this.validate_serial_batch_item(); + } + if (!this.custom_edit_rate_and_uom) { + this.events.toggle_item_selector(!hide_item_details); + this.toggle_component(!hide_item_details); + } + + if (item && current_item_changed) { + this.doctype = item.doctype; + this.item_meta = frappe.get_meta(this.doctype); + this.name = item.name; + this.item_row = item; + this.currency = this.events.get_frm().doc.currency; + + this.current_item = item; + + this.render_dom(item); + this.render_discount_dom(item); + this.render_form(item); + this.events.highlight_cart_item(item); + } else { + this.current_item = {}; + } + } + + validate_serial_batch_item() { + const doc = this.events.get_frm().doc; + const item_row = doc.items.find((item) => item.name === this.name); + + if (!item_row) return; + + const serialized = item_row.has_serial_no; + const batched = item_row.has_batch_no; + const no_bundle_selected = !item_row.serial_and_batch_bundle; + + if ((serialized && no_bundle_selected) || (batched && no_bundle_selected)) { + frappe.show_alert({ + message: __("Item is removed since no serial / batch no selected."), + indicator: "orange", + }); + frappe.utils.play_sound("cancel"); + return this.events.remove_item_from_cart(); + } + } + + render_dom(item) { + let { item_name, description, image, price_list_rate } = item; + + function get_description_html() { + if (description) { + description = + description.indexOf("...") === -1 && description.length > 140 + ? description.substr(0, 139) + "..." + : description; + return description; + } + return ``; + } + + this.$item_name.html(item_name); + this.$item_description.html(get_description_html()); + this.$item_price.html(format_currency(price_list_rate, this.currency)); + if (!this.hide_images && image) { + this.$item_image.html( + `<img onerror="cur_pos.item_details.handle_broken_image(this)" class="h-full" src="${image}" alt="${frappe.get_abbr(item_name)}" - style="object-fit: cover;">` - ); - } else { - this.$item_image.html(`<div class="item-abbr">${frappe.get_abbr(item_name)}</div>`); - } - - } - - handle_broken_image($img) { - const item_abbr = $($img).attr('alt'); - $($img).replaceWith(`<div class="item-abbr">${item_abbr}</div>`); - } - - render_discount_dom(item) { - if (item.discount_percentage) { - this.$dicount_section.html( - `<div class="item-rate">${format_currency(item.price_list_rate, this.currency)}</div> - <div class="item-discount">${item.discount_percentage}% off</div>` - ) - this.$item_price.html(format_currency(item.rate, this.currency)); - } else { - this.$dicount_section.html(``) - } - } - - render_form(item) { - const fields_to_display = this.get_form_fields(item); - this.$form_container.html(''); - - fields_to_display.forEach((fieldname, idx) => { - this.$form_container.append( - `<div class="${fieldname}-control" data-fieldname="${fieldname}"></div>` - ) - - const field_meta = this.item_meta.fields.find(df => df.fieldname === fieldname); - fieldname === 'discount_percentage' ? (field_meta.label = __('Discount (%)')) : ''; - const me = this; - var uoms = [] - frappe.db.get_doc("Item",me.current_item.item_code).then(doc => { - uoms = doc.uoms.map(item => item.uom); - }) - this[`${fieldname}_control`] = frappe.ui.form.make_control({ - df: { - ...field_meta, - onchange: function() { - me.events.form_updated(me.current_item, fieldname, this.value); - }, - get_query:function () { - if(fieldname === 'uom'){ - return { - filters: { - name: ['in',uoms] - } - } - } - return - } - }, - parent: this.$form_container.find(`.${fieldname}-control`), - render_input: true, - }) - this[`${fieldname}_control`].set_value(item[fieldname]); - }); - - this.make_auto_serial_selection_btn(item); - - this.bind_custom_control_change_event(); - } - - get_form_fields(item) { - const fields = ['qty', 'uom', 'rate', 'conversion_factor', 'discount_percentage', 'warehouse', 'actual_qty', 'price_list_rate']; - if (item.has_serial_no) fields.push('serial_no'); - if (item.has_batch_no) fields.push('batch_no'); - return fields; - } - - make_auto_serial_selection_btn(item) { - if (item.has_serial_no || item.has_batch_no) { - const label = item.has_serial_no ? __('Select Serial No') : __('Select Batch No'); - this.$form_container.append( - `<div class="btn btn-sm btn-secondary auto-fetch-btn">${label}</div>` - ); - this.$form_container.find('.serial_no-control').find('textarea').css('height', '6rem'); - } - } - - bind_custom_control_change_event() { - const me = this; - if (this.rate_control) { - this.rate_control.df.onchange = function() { - if (this.value || flt(this.value) === 0) { - me.events.form_updated(me.current_item, 'rate', this.value).then(() => { - const item_row = frappe.get_doc(me.doctype, me.name); - const doc = me.events.get_frm().doc; - me.$item_price.html(format_currency(item_row.rate, doc.currency)); - me.render_discount_dom(item_row); - }); - } - }; - this.rate_control.df.read_only = !this.allow_rate_change; - this.rate_control.refresh(); - } - - if (this.discount_percentage_control && !this.allow_discount_change) { - this.discount_percentage_control.df.read_only = 1; - this.discount_percentage_control.refresh(); - } - - if (this.warehouse_control) { - this.warehouse_control.df.reqd = 1; - this.warehouse_control.df.onchange = function() { - if (this.value) { - me.events.form_updated(me.current_item, 'warehouse', this.value).then(() => { - me.item_stock_map = me.events.get_item_stock_map(); - const available_qty = me.item_stock_map[me.item_row.item_code][this.value][0]; - const is_stock_item = Boolean(me.item_stock_map[me.item_row.item_code][this.value][1]); - if (available_qty === undefined) { - me.events.get_available_stock(me.item_row.item_code, this.value).then(() => { - // item stock map is updated now reset warehouse - me.warehouse_control.set_value(this.value); - }) - } else if (available_qty === 0 && is_stock_item) { - me.warehouse_control.set_value(''); - const bold_item_code = me.item_row.item_code.bold(); - const bold_warehouse = this.value.bold(); - frappe.throw( - __('Item Code: {0} is not available under warehouse {1}.', [bold_item_code, bold_warehouse]) - ); - } - me.actual_qty_control.set_value(available_qty); - }); - } - } - this.warehouse_control.df.get_query = () => { - return { - filters: { company: this.events.get_frm().doc.company } - } - }; - this.warehouse_control.refresh(); - } - - if (this.serial_no_control) { - this.serial_no_control.df.reqd = 1; - this.serial_no_control.df.onchange = async function() { - !me.current_item.batch_no && await me.auto_update_batch_no(); - me.events.form_updated(me.current_item, 'serial_no', this.value); - } - this.serial_no_control.refresh(); - } - - if (this.batch_no_control) { - this.batch_no_control.df.reqd = 1; - this.batch_no_control.df.get_query = () => { - return { - query: 'erpnext.controllers.queries.get_batch_no', - filters: { - item_code: me.item_row.item_code, - warehouse: me.item_row.warehouse, - posting_date: me.events.get_frm().doc.posting_date - } - } - }; - this.batch_no_control.refresh(); - } - - if (this.uom_control) { - this.uom_control.df.onchange = function() { - me.events.form_updated(me.current_item, 'uom', this.value); - - const item_row = frappe.get_doc(me.doctype, me.name); - me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value); - me.conversion_factor_control.refresh(); - } - } - - frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { - const field_control = this[`${fieldname}_control`]; - const item_row_is_being_edited = this.compare_with_current_item(item_row); - - if (item_row_is_being_edited && field_control && field_control.get_value() !== value) { - field_control.set_value(value); - cur_pos.update_cart_html(item_row); - } - }); - } - - async auto_update_batch_no() { - if (this.serial_no_control && this.batch_no_control) { - const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s); - if (!selected_serial_nos.length) return; - - // find batch nos of the selected serial no - const serials_with_batch_no = await frappe.db.get_list("Serial No", { - filters: { 'name': ["in", selected_serial_nos]}, - fields: ["batch_no", "name"] - }); - const batch_serial_map = serials_with_batch_no.reduce((acc, r) => { - if (!acc[r.batch_no]) { - acc[r.batch_no] = []; - } - acc[r.batch_no] = [...acc[r.batch_no], r.name]; - return acc; - }, {}); - // set current item's batch no and serial no - const batch_no = Object.keys(batch_serial_map)[0]; - const batch_serial_nos = batch_serial_map[batch_no].join(`\n`); - // eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch - const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length; - - const current_batch_no = this.batch_no_control.get_value(); - current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no); - - if (serial_nos_belongs_to_other_batch) { - this.serial_no_control.set_value(batch_serial_nos); - this.qty_control.set_value(batch_serial_map[batch_no].length); - - delete batch_serial_map[batch_no]; - this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item); - } - } - } - - bind_events() { - this.bind_auto_serial_fetch_event(); - this.bind_fields_to_numpad_fields(); - - this.$component.on('click', '.close-btn', () => { - this.events.close_item_details(); - }); - } - - attach_shortcuts() { - this.wrapper.find('.close-btn').attr("title", "Esc"); - frappe.ui.keys.on("escape", () => { - const item_details_visible = this.$component.is(":visible"); - if (item_details_visible) { - this.events.close_item_details(); - } - }); - } - - bind_fields_to_numpad_fields() { - const me = this; - this.$form_container.on('click', '.input-with-feedback', function() { - const fieldname = $(this).attr('data-fieldname'); - if (this.last_field_focused != fieldname) { - me.events.item_field_focused(fieldname); - this.last_field_focused = fieldname; - } - }); - } - - bind_auto_serial_fetch_event() { - this.$form_container.on('click', '.auto-fetch-btn', () => { - frappe.require("assets/erpnext/js/utils/serial_no_batch_selector.js", () => { - let frm = this.events.get_frm(); - let item_row = this.item_row; - item_row.type_of_transaction = "Outward"; - - new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { - if (r) { - frappe.model.set_value(item_row.doctype, item_row.name, { - "serial_and_batch_bundle": r.name, - "qty": Math.abs(r.total_qty) - }); - } - }); - }); - }) - } - - toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); - } -} + style="object-fit: cover;">`, + ); + } else { + this.$item_image.html( + `<div class="item-abbr">${frappe.get_abbr(item_name)}</div>`, + ); + } + } + + handle_broken_image($img) { + const item_abbr = $($img).attr("alt"); + $($img).replaceWith(`<div class="item-abbr">${item_abbr}</div>`); + } + + render_discount_dom(item) { + if (item.discount_percentage) { + this.$dicount_section.html( + `<div class="item-rate">${format_currency( + item.price_list_rate, + this.currency, + )}</div> + <div class="item-discount">${item.discount_percentage}% off</div>`, + ); + this.$item_price.html(format_currency(item.rate, this.currency)); + } else { + this.$dicount_section.html(``); + } + } + + render_form(item) { + const fields_to_display = this.get_form_fields(item); + this.$form_container.html(""); + + fields_to_display.forEach((fieldname, idx) => { + this.$form_container.append( + `<div class="${fieldname}-control" data-fieldname="${fieldname}"></div>`, + ); + + const field_meta = this.item_meta.fields.find( + (df) => df.fieldname === fieldname, + ); + fieldname === "discount_percentage" + ? (field_meta.label = __("Discount (%)")) + : ""; + const me = this; + var uoms = []; + frappe.db.get_doc("Item", me.current_item.item_code).then((doc) => { + uoms = doc.uoms.map((item) => item.uom); + }); + this[`${fieldname}_control`] = frappe.ui.form.make_control({ + df: { + ...field_meta, + onchange: function () { + me.events.form_updated(me.current_item, fieldname, this.value); + }, + get_query: function () { + if (fieldname === "uom") { + return { + filters: { + name: ["in", uoms], + }, + }; + } + return; + }, + }, + parent: this.$form_container.find(`.${fieldname}-control`), + render_input: true, + }); + this[`${fieldname}_control`].set_value(item[fieldname]); + }); + + this.make_auto_serial_selection_btn(item); + + this.bind_custom_control_change_event(); + } + + get_form_fields(item) { + const fields = [ + "qty", + "uom", + "rate", + "conversion_factor", + "discount_percentage", + "warehouse", + "actual_qty", + "price_list_rate", + ]; + if (item.has_serial_no) fields.push("serial_no"); + if (item.has_batch_no) fields.push("batch_no"); + return fields; + } + + make_auto_serial_selection_btn(item) { + if (item.has_serial_no || item.has_batch_no) { + const label = item.has_serial_no + ? __("Select Serial No") + : __("Select Batch No"); + this.$form_container.append( + `<div class="btn btn-sm btn-secondary auto-fetch-btn">${label}</div>`, + ); + this.$form_container + .find(".serial_no-control") + .find("textarea") + .css("height", "6rem"); + } + } + + bind_custom_control_change_event() { + const me = this; + if (this.rate_control) { + this.rate_control.df.onchange = function () { + if (this.value || flt(this.value) === 0) { + me.events + .form_updated(me.current_item, "rate", this.value) + .then(() => { + const item_row = frappe.get_doc(me.doctype, me.name); + const doc = me.events.get_frm().doc; + me.$item_price.html(format_currency(item_row.rate, doc.currency)); + me.render_discount_dom(item_row); + }); + } + }; + this.rate_control.df.read_only = !this.allow_rate_change; + this.rate_control.refresh(); + } + + if (this.discount_percentage_control && !this.allow_discount_change) { + this.discount_percentage_control.df.read_only = 1; + this.discount_percentage_control.refresh(); + } + + if (this.warehouse_control) { + this.warehouse_control.df.reqd = 1; + this.warehouse_control.df.onchange = function () { + if (this.value) { + me.events + .form_updated(me.current_item, "warehouse", this.value) + .then(() => { + me.item_stock_map = me.events.get_item_stock_map(); + const available_qty = + me.item_stock_map[me.item_row.item_code][this.value][0]; + const is_stock_item = Boolean( + me.item_stock_map[me.item_row.item_code][this.value][1], + ); + if (available_qty === undefined) { + me.events + .get_available_stock(me.item_row.item_code, this.value) + .then(() => { + // item stock map is updated now reset warehouse + me.warehouse_control.set_value(this.value); + }); + } else if (available_qty === 0 && is_stock_item) { + me.warehouse_control.set_value(""); + const bold_item_code = me.item_row.item_code.bold(); + const bold_warehouse = this.value.bold(); + frappe.throw( + __("Item Code: {0} is not available under warehouse {1}.", [ + bold_item_code, + bold_warehouse, + ]), + ); + } + me.actual_qty_control.set_value(available_qty); + }); + } + }; + this.warehouse_control.df.get_query = () => { + return { + filters: { company: this.events.get_frm().doc.company }, + }; + }; + this.warehouse_control.refresh(); + } + + if (this.serial_no_control) { + this.serial_no_control.df.reqd = 1; + this.serial_no_control.df.onchange = async function () { + !me.current_item.batch_no && (await me.auto_update_batch_no()); + me.events.form_updated(me.current_item, "serial_no", this.value); + }; + this.serial_no_control.refresh(); + } + + if (this.batch_no_control) { + this.batch_no_control.df.reqd = 1; + this.batch_no_control.df.get_query = () => { + return { + query: "erpnext.controllers.queries.get_batch_no", + filters: { + item_code: me.item_row.item_code, + warehouse: me.item_row.warehouse, + posting_date: me.events.get_frm().doc.posting_date, + }, + }; + }; + this.batch_no_control.refresh(); + } + + if (this.uom_control) { + this.uom_control.df.onchange = function () { + me.events.form_updated(me.current_item, "uom", this.value); + + const item_row = frappe.get_doc(me.doctype, me.name); + me.conversion_factor_control.df.read_only = + item_row.stock_uom == this.value; + me.conversion_factor_control.refresh(); + }; + } + + frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => { + const field_control = this[`${fieldname}_control`]; + const item_row_is_being_edited = this.compare_with_current_item(item_row); + + if ( + item_row_is_being_edited && + field_control && + field_control.get_value() !== value + ) { + field_control.set_value(value); + cur_pos.update_cart_html(item_row); + } + }); + } + + async auto_update_batch_no() { + if (this.serial_no_control && this.batch_no_control) { + const selected_serial_nos = this.serial_no_control + .get_value() + .split(`\n`) + .filter((s) => s); + if (!selected_serial_nos.length) return; + + // find batch nos of the selected serial no + const serials_with_batch_no = await frappe.db.get_list("Serial No", { + filters: { name: ["in", selected_serial_nos] }, + fields: ["batch_no", "name"], + }); + const batch_serial_map = serials_with_batch_no.reduce((acc, r) => { + if (!acc[r.batch_no]) { + acc[r.batch_no] = []; + } + acc[r.batch_no] = [...acc[r.batch_no], r.name]; + return acc; + }, {}); + // set current item's batch no and serial no + const batch_no = Object.keys(batch_serial_map)[0]; + const batch_serial_nos = batch_serial_map[batch_no].join(`\n`); + // eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch + const serial_nos_belongs_to_other_batch = + selected_serial_nos.length !== batch_serial_map[batch_no].length; + + const current_batch_no = this.batch_no_control.get_value(); + current_batch_no != batch_no && + (await this.batch_no_control.set_value(batch_no)); + + if (serial_nos_belongs_to_other_batch) { + this.serial_no_control.set_value(batch_serial_nos); + this.qty_control.set_value(batch_serial_map[batch_no].length); + + delete batch_serial_map[batch_no]; + this.events.clone_new_batch_item_in_frm( + batch_serial_map, + this.current_item, + ); + } + } + } + + bind_events() { + this.bind_auto_serial_fetch_event(); + this.bind_fields_to_numpad_fields(); + + this.$component.on("click", ".close-btn", () => { + this.events.close_item_details(); + }); + } + + attach_shortcuts() { + this.wrapper.find(".close-btn").attr("title", "Esc"); + frappe.ui.keys.on("escape", () => { + const item_details_visible = this.$component.is(":visible"); + if (item_details_visible) { + this.events.close_item_details(); + } + }); + } + + bind_fields_to_numpad_fields() { + const me = this; + this.$form_container.on("click", ".input-with-feedback", function () { + const fieldname = $(this).attr("data-fieldname"); + if (this.last_field_focused != fieldname) { + me.events.item_field_focused(fieldname); + this.last_field_focused = fieldname; + } + }); + } + + bind_auto_serial_fetch_event() { + this.$form_container.on("click", ".auto-fetch-btn", () => { + frappe.require( + "assets/erpnext/js/utils/serial_no_batch_selector.js", + () => { + let frm = this.events.get_frm(); + let item_row = this.item_row; + item_row.type_of_transaction = "Outward"; + + new erpnext.SerialBatchPackageSelector(frm, item_row, (r) => { + if (r) { + frappe.model.set_value(item_row.doctype, item_row.name, { + serial_and_batch_bundle: r.name, + qty: Math.abs(r.total_qty), + }); + } + }); + }, + ); + }); + } + + toggle_component(show) { + show + ? this.$component.css("display", "flex") + : this.$component.css("display", "none"); + } +}; diff --git a/posnext/public/js/pos_item_selector.js b/posnext/public/js/pos_item_selector.js index 7a1a71e..d05ec88 100644 --- a/posnext/public/js/pos_item_selector.js +++ b/posnext/public/js/pos_item_selector.js @@ -1,490 +1,633 @@ -frappe.provide('posnext.PointOfSale'); -var view = "List" +frappe.provide("posnext.PointOfSale"); +var view = "List"; + +function get_item_code_header(obj) { + var flex_value = 3; + if ( + !obj.custom_show_item_code && + !obj.custom_show_last_incoming_rate && + !obj.custom_show_oem_part_number && + !obj.custom_show_logical_rack + ) { + flex_value = 2; + } + var html_header = ``; + if (obj.custom_show_item_code) { + // flex_value -= 1 + html_header += `<div style="flex: 1">${__("Item Code")}</div>`; + } + if (obj.custom_show_last_incoming_rate) { + // flex_value -= 1 + html_header += `<div style="flex: 1">${__("Inc.Rate")}</div>`; + } + if (obj.custom_show_oem_part_number) { + // flex_value -= 1 + html_header += `<div style="flex: 1">${__("OEM")} <br> ${__( + "Part No.", + )}</div>`; + } + if (obj.custom_show_logical_rack) { + // flex_value -= 1 + html_header += `<div style="flex: 1">${__("Rack")}</div>`; + } + if (flex_value > 0) { + return ( + `<div style="flex: ` + flex_value + `">${__("Item")}</div>` + html_header + ); + } else { + return `<div>${__("Item")}</div>` + html_header; + } +} posnext.PointOfSale.ItemSelector = class { - // eslint-disable-next-line no-unused-vars - constructor({ frm, wrapper, events, pos_profile, settings,currency,init_item_cart,reload_status }) { - this.wrapper = wrapper; - this.events = events; - this.currency = currency; - this.pos_profile = pos_profile; - this.hide_images = settings.hide_images; - this.reload_status = reload_status - this.auto_add_item = settings.auto_add_item_to_cart; - this.auto_search_serial = settings.custom_auto_search_serial_number; - if(settings.custom_default_view){ - view = settings.custom_default_view - } - if(settings.custom_show_only_list_view){ - view = "List" - } - if(settings.custom_show_only_card_view){ - view = "Card" - } - this.custom_show_item_code = settings.custom_show_item_code - this.custom_show_last_incoming_rate = settings.custom_show_last_incoming_rate - this.custom_show_oem_part_number = settings.custom_show_oem_part_number - this.custom_show_posting_date = settings.custom_show_posting_date - this.custom_show_logical_rack = settings.custom_show_logical_rack - this.show_only_list_view = settings.custom_show_only_list_view - this.show_only_card_view = settings.custom_show_only_card_view - this.custom_edit_rate = settings.custom_edit_rate_and_uom - this.custom_show_incoming_rate = settings.custom_show_incoming_rate && settings.custom_edit_rate_and_uom; - this.custom_show_item_discription = settings.custom_show_item_discription; - // this.custom_edit_uom = settings.custom_edit_uom - this.inti_component(); - } - - inti_component() { - - this.prepare_dom(); - this.make_search_bar(); - this.load_items_data(); - this.bind_events(); - this.attach_shortcuts(); - } - - prepare_dom() { - var cardlist = `` - if(!this.show_only_list_view && !this.show_only_card_view){ - cardlist = ` + // eslint-disable-next-line no-unused-vars + constructor({ + frm, + wrapper, + events, + pos_profile, + settings, + currency, + init_item_cart, + reload_status, + }) { + this.wrapper = wrapper; + this.events = events; + this.currency = currency; + this.pos_profile = pos_profile; + this.hide_images = settings.hide_images; + this.reload_status = reload_status; + this.auto_add_item = settings.auto_add_item_to_cart; + this.auto_search_serial = settings.custom_auto_search_serial_number; + if (settings.custom_default_view) { + view = settings.custom_default_view; + } + if (settings.custom_show_only_list_view) { + view = "List"; + } + if (settings.custom_show_only_card_view) { + view = "Card"; + } + this.custom_show_item_code = settings.custom_show_item_code; + this.custom_show_last_incoming_rate = + settings.custom_show_last_incoming_rate; + this.custom_show_oem_part_number = settings.custom_show_oem_part_number; + this.custom_show_posting_date = settings.custom_show_posting_date; + this.custom_show_logical_rack = settings.custom_show_logical_rack; + this.show_only_list_view = settings.custom_show_only_list_view; + this.show_only_card_view = settings.custom_show_only_card_view; + this.custom_edit_rate = settings.custom_edit_rate_and_uom; + this.custom_show_incoming_rate = + settings.custom_show_incoming_rate && settings.custom_edit_rate_and_uom; + this.custom_show_item_discription = settings.custom_show_item_discription; + // this.custom_edit_uom = settings.custom_edit_uom + this.inti_component(); + } + + inti_component() { + this.prepare_dom(); + this.make_search_bar(); + this.load_items_data(); + this.bind_events(); + this.attach_shortcuts(); + } + + prepare_dom() { + var cardlist = ``; + if (!this.show_only_list_view && !this.show_only_card_view) { + cardlist = ` <div class="list-view" style="grid-column: span 1 / span 2!important;"><a class="list-span">List</a></div> <div class="card-view" style="grid-column: span 1 / span 2!important;"><a class="card-span">Card</a></div> - ` - } - - if(view === "Card" && !this.show_only_list_view){ - var tir = `` - if(this.custom_show_last_incoming_rate || this.custom_show_incoming_rate){ - tir = `<div class="total-incoming-rate" style="margin-left: 10px;grid-column: span 2 / span 2"></div>` - } - this.wrapper.append( - `<section class="items-selector" id="card-view-section" style="grid-column: span 5/span 5!important;"> - <div class="filter-section">` + cardlist + `<div class="pos-profile" style="grid-column: span 2 / span 2"></div> + `; + } + + if (view === "Card" && !this.show_only_list_view) { + var tir = ``; + if ( + this.custom_show_last_incoming_rate || + this.custom_show_incoming_rate + ) { + tir = `<div class="total-incoming-rate" style="margin-left: 10px;grid-column: span 2 / span 2"></div>`; + } + this.wrapper.append( + `<section class="items-selector" id="card-view-section" style="grid-column: span 5/span 5!important;"> + <div class="filter-section">` + + cardlist + + `<div class="pos-profile" style="grid-column: span 2 / span 2"></div> <div class="search-field" style="grid-column: span 4 / span 4"></div> <!--<div class="item-code-search-field" style="grid-column: span 2 / span 2"></div>--> <div class="item-group-field" style="grid-column: span 2 / span 2"></div> - <div class="invoice-posting-date" style="margin-left: 10px;grid-column: span 2 / span 2"></div>` + tir + ` - + <div class="invoice-posting-date" style="margin-left: 10px;grid-column: span 2 / span 2"></div>` + + tir + + ` + </div> <div class="items-container"></div> </section> - ` - ); - - this.$component = this.wrapper.find('.items-selector'); - this.$items_container = this.$component.find('.items-container'); - } else if(view === "List" && !this.show_only_card_view) { - var section = `<section class="customer-cart-container items-selector" id="list-view-section" style="grid-column: span 6 / span 6;overflow-y:hidden">` - var tir = `` - if(this.custom_edit_rate){ - section = `<section class="customer-cart-container items-selector" id="list-view-section" style="grid-column: span 5 / span 5;overflow-y:hidden">` - } - if(this.custom_show_last_incoming_rate || this.custom_show_incoming_rate){ - tir = `<div class="total-incoming-rate" style="margin-left: 10px;grid-column: span 2 / span 2"></div>` - } - - - this.wrapper.append( - section + `<div class="filter-section">` + cardlist + `<div class="pos-profile" style="grid-column: span 2 / span 2"></div> + `, + ); + + this.$component = this.wrapper.find(".items-selector"); + this.$items_container = this.$component.find(".items-container"); + } else if (view === "List" && !this.show_only_card_view) { + var section = `<section class="customer-cart-container items-selector" id="list-view-section" style="grid-column: span 6 / span 6;overflow-y:hidden">`; + tir = ``; + if (this.custom_edit_rate) { + section = `<section class="customer-cart-container items-selector" id="list-view-section" style="grid-column: span 5 / span 5;overflow-y:hidden">`; + } + if ( + this.custom_show_last_incoming_rate || + this.custom_show_incoming_rate + ) { + tir = `<div class="total-incoming-rate" style="margin-left: 10px;grid-column: span 2 / span 2"></div>`; + } + + this.wrapper.append( + section + + `<div class="filter-section">` + + cardlist + + `<div class="pos-profile" style="grid-column: span 2 / span 2"></div> <div class="search-field" style="grid-column: span 4 / span 4"></div> <!--<div class="item-code-search-field" style="grid-column: span 2 / span 2"></div>--> <div class="item-group-field" style="grid-column: span 2 / span 2"></div> - <div class="invoice-posting-date" style="margin-left: 10px;grid-column: span 2 / span 2"></div>` + tir + ` - + <div class="invoice-posting-date" style="margin-left: 10px;grid-column: span 2 / span 2"></div>` + + tir + + ` + </div> <div class="cart-container" ></div> - </section>` - ); - - this.$component = this.wrapper.find('.customer-cart-container'); - this.$items_container = this.$component.find('.cart-container'); - } - if(!this.show_only_list_view && !this.show_only_card_view) { - this.$list_view = this.$component.find('.list-view'); - this.$card_view = this.$component.find('.card-view'); - if (view === "List" && !this.show_only_list_view) { - this.$list_view.find('.list-span').css({ - "display": "inline-block", - "background-color": "#3498db", - "color": "white", - "padding": "3px 3px", - "border-radius": "20px", - "font-size": "12px", - "font-weight": "bold", - "text-transform": "uppercase", - "letter-spacing": "1px", - "cursor": "pointer", - "transition": "background-color 0.3s ease" - }); - this.$card_view.find('.card-span').css({ - "display": "", - "background-color": "", - "color": "", - "padding": "3px 3px", - "border-radius": "", - "font-size": "", - "font-weight": "", - "text-transform": "", - "letter-spacing": "", - "cursor": "", - "transition": "" - }); - } else if (view === "Card" && !this.show_only_card_view) { - this.$card_view.find('.card-span').css({ - "display": "inline-block", - "background-color": "#3498db", - "color": "white", - "padding": "3px 3px", - "border-radius": "20px", - "font-size": "12px", - "font-weight": "bold", - "text-transform": "uppercase", - "letter-spacing": "1px", - "cursor": "pointer", - "transition": "background-color 0.3s ease" - }); - this.$list_view.find('.list-span').css({ - "display": "", - "background-color": "", - "color": "", - "padding": "3px 3px", - "border-radius": "", - "font-size": "", - "font-weight": "", - "text-transform": "", - "letter-spacing": "", - "cursor": "", - "transition": "" - }); - } else { - this.$list_view.find('.list-span').css({"display": "none"}); - this.$card_view.find('.card-span').css({"display": "none"}); - - } - if (!this.show_only_card_view && !this.show_only_list_view) { - this.click_functions() - } - } - } - click_functions(){ - this.$list_view.on('click', 'a', () => { - - this.$list_view.find('.list-span').css({"display": "inline-block","background-color": "#3498db","color": "white","padding": "5px 10px", "border-radius": "20px", "font-size": "14px","font-weight": "bold", "text-transform": "uppercase","letter-spacing": "1px","cursor": "pointer", "transition": "background-color 0.3s ease"}); - this.$card_view.find('.card-span').css({"display": "","background-color": "","color": "","padding": "", "border-radius": "", "font-size": "","font-weight": "", "text-transform": "","letter-spacing": "","cursor": "", "transition": ""}); - view = "List" - if(document.getElementById("card-view-section")) document.getElementById("card-view-section").remove() - if(document.getElementById("list-view-section")) document.getElementById("list-view-section").remove() - if(document.getElementById("customer-cart-container2")) document.getElementById("customer-cart-container2").remove() - if(document.getElementById("item-details-container")) document.getElementById("item-details-container").remove() - - this.inti_component() - this.events.init_item_details() - this.events.init_item_cart() - this.events.change_items(this.events.get_frm()) - - - }); - this.$card_view.on('click', 'a', () => { - this.$card_view.find('.card-span').css({"display": "inline-block","background-color": "#3498db","color": "white","padding": "5px 10px", "border-radius": "20px", "font-size": "14px","font-weight": "bold", "text-transform": "uppercase","letter-spacing": "1px","cursor": "pointer", "transition": "background-color 0.3s ease"}); - this.$list_view.find('.list-span').css({"display": "","background-color": "","color": "","padding": "", "border-radius": "", "font-size": "","font-weight": "", "text-transform": "","letter-spacing": "","cursor": "", "transition": ""}); - view = "Card" - if(document.getElementById("card-view-section")) document.getElementById("card-view-section").remove() - if(document.getElementById("list-view-section")) document.getElementById("list-view-section").remove() - if(document.getElementById("customer-cart-container2")) document.getElementById("customer-cart-container2").remove() - if(document.getElementById("item-details-container")) document.getElementById("item-details-container").remove() - - this.inti_component() - this.events.init_item_details() - this.events.init_item_cart() - this.events.change_items(this.events.get_frm()) - - }); - } - async load_items_data() { - if (!this.item_group) { - const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name"); - this.parent_item_group = res.message.name; - } - if (!this.price_list) { - const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list"); - this.price_list = res.message.selling_price_list; - } - - this.get_items({}).then(({message}) => { - this.render_item_list(message.items); - }); - } - - get_items({start = 0, page_length = 40, search_term=''}) { - const doc = this.events.get_frm().doc; - const price_list = (doc && doc.selling_price_list) || this.price_list; - let { item_group, pos_profile } = this; - - !item_group && (item_group = this.parent_item_group); - - return frappe.call({ - method: "posnext.posnext.page.posnext.point_of_sale.get_items", - freeze: true, - args: { start, page_length, price_list, item_group, search_term, pos_profile }, - }); - } - - - render_item_list(items) { - this.$items_container.html(''); - var me = this - if(view === "List"){ - this.$items_container.append( - `<div class="abs-cart-container" style="overflow-y:hidden"> + </section>`, + ); + + this.$component = this.wrapper.find(".customer-cart-container"); + this.$items_container = this.$component.find(".cart-container"); + } + if (!this.show_only_list_view && !this.show_only_card_view) { + this.$list_view = this.$component.find(".list-view"); + this.$card_view = this.$component.find(".card-view"); + if (view === "List" && !this.show_only_list_view) { + this.$list_view.find(".list-span").css({ + display: "inline-block", + "background-color": "#3498db", + color: "white", + padding: "3px 3px", + "border-radius": "20px", + "font-size": "12px", + "font-weight": "bold", + "text-transform": "uppercase", + "letter-spacing": "1px", + cursor: "pointer", + transition: "background-color 0.3s ease", + }); + this.$card_view.find(".card-span").css({ + display: "", + "background-color": "", + color: "", + padding: "3px 3px", + "border-radius": "", + "font-size": "", + "font-weight": "", + "text-transform": "", + "letter-spacing": "", + cursor: "", + transition: "", + }); + } else if (view === "Card" && !this.show_only_card_view) { + this.$card_view.find(".card-span").css({ + display: "inline-block", + "background-color": "#3498db", + color: "white", + padding: "3px 3px", + "border-radius": "20px", + "font-size": "12px", + "font-weight": "bold", + "text-transform": "uppercase", + "letter-spacing": "1px", + cursor: "pointer", + transition: "background-color 0.3s ease", + }); + this.$list_view.find(".list-span").css({ + display: "", + "background-color": "", + color: "", + padding: "3px 3px", + "border-radius": "", + "font-size": "", + "font-weight": "", + "text-transform": "", + "letter-spacing": "", + cursor: "", + transition: "", + }); + } else { + this.$list_view.find(".list-span").css({ display: "none" }); + this.$card_view.find(".card-span").css({ display: "none" }); + } + if (!this.show_only_card_view && !this.show_only_list_view) { + this.click_functions(); + } + } + } + click_functions() { + this.$list_view.on("click", "a", () => { + this.$list_view.find(".list-span").css({ + display: "inline-block", + "background-color": "#3498db", + color: "white", + padding: "5px 10px", + "border-radius": "20px", + "font-size": "14px", + "font-weight": "bold", + "text-transform": "uppercase", + "letter-spacing": "1px", + cursor: "pointer", + transition: "background-color 0.3s ease", + }); + this.$card_view.find(".card-span").css({ + display: "", + "background-color": "", + color: "", + padding: "", + "border-radius": "", + "font-size": "", + "font-weight": "", + "text-transform": "", + "letter-spacing": "", + cursor: "", + transition: "", + }); + view = "List"; + if (document.getElementById("card-view-section")) + document.getElementById("card-view-section").remove(); + if (document.getElementById("list-view-section")) + document.getElementById("list-view-section").remove(); + if (document.getElementById("customer-cart-container2")) + document.getElementById("customer-cart-container2").remove(); + if (document.getElementById("item-details-container")) + document.getElementById("item-details-container").remove(); + + this.inti_component(); + this.events.init_item_details(); + this.events.init_item_cart(); + this.events.change_items(this.events.get_frm()); + }); + this.$card_view.on("click", "a", () => { + this.$card_view.find(".card-span").css({ + display: "inline-block", + "background-color": "#3498db", + color: "white", + padding: "5px 10px", + "border-radius": "20px", + "font-size": "14px", + "font-weight": "bold", + "text-transform": "uppercase", + "letter-spacing": "1px", + cursor: "pointer", + transition: "background-color 0.3s ease", + }); + this.$list_view.find(".list-span").css({ + display: "", + "background-color": "", + color: "", + padding: "", + "border-radius": "", + "font-size": "", + "font-weight": "", + "text-transform": "", + "letter-spacing": "", + cursor: "", + transition: "", + }); + view = "Card"; + if (document.getElementById("card-view-section")) + document.getElementById("card-view-section").remove(); + if (document.getElementById("list-view-section")) + document.getElementById("list-view-section").remove(); + if (document.getElementById("customer-cart-container2")) + document.getElementById("customer-cart-container2").remove(); + if (document.getElementById("item-details-container")) + document.getElementById("item-details-container").remove(); + + this.inti_component(); + this.events.init_item_details(); + this.events.init_item_cart(); + this.events.change_items(this.events.get_frm()); + }); + } + async load_items_data() { + if (!this.item_group) { + const res = await frappe.db.get_value( + "Item Group", + { lft: 1, is_group: 1 }, + "name", + ); + this.parent_item_group = res.message.name; + } + if (!this.price_list) { + const res = await frappe.db.get_value( + "POS Profile", + this.pos_profile, + "selling_price_list", + ); + this.price_list = res.message.selling_price_list; + } + + this.get_items({}).then(({ message }) => { + this.render_item_list(message.items); + }); + } + + get_items({ start = 0, page_length = 40, search_term = "" }) { + const doc = this.events.get_frm().doc; + const price_list = (doc && doc.selling_price_list) || this.price_list; + let { item_group, pos_profile } = this; + + !item_group && (item_group = this.parent_item_group); + + return frappe.call({ + method: "posnext.posnext.page.posnext.point_of_sale.get_items", + freeze: true, + args: { + start, + page_length, + price_list, + item_group, + search_term, + pos_profile, + }, + }); + } + + render_item_list(items) { + this.$items_container.html(""); + var me = this; + if (view === "List") { + this.$items_container.append( + `<div class="abs-cart-container" style="overflow-y:hidden"> <div class="cart-header"> - ${get_item_code_header()} - <div style="flex: 1">${__('Rate')}</div> - <div style="flex: 1">${__('Avail. Qty')}</div> - <!--<div class="qty-header">${__('UOM')}</div>--> + ${get_item_code_header(me)} + <div style="flex: 1">${__("Rate")}</div> + <div style="flex: 1">${__("Avail. Qty")}</div> + <!--<div class="qty-header">${__("UOM")}</div>--> </div> <div class="cart-items-section" style="overflow-y:scroll;font-size: 12px"></div> - </div>`) - - function get_item_code_header() { - var flex_value = 3 - if(!me.custom_show_item_code && !me.custom_show_last_incoming_rate && !me.custom_show_oem_part_number && !me.custom_show_logical_rack){ - flex_value = 2 - } - var html_header = `` - if(me.custom_show_item_code){ - // flex_value -= 1 - html_header += `<div style="flex: 1">${__('Item Code')}</div>` - } - if(me.custom_show_last_incoming_rate){ - // flex_value -= 1 - html_header += `<div style="flex: 1">${__('Inc.Rate')}</div>` - } - if(me.custom_show_oem_part_number){ - // flex_value -= 1 - html_header += `<div style="flex: 1">${__('OEM')} <br> ${__('Part No.')}</div>` - } - if(me.custom_show_logical_rack){ - // flex_value -= 1 - html_header += `<div style="flex: 1">${__('Rack')}</div>` - } - if(flex_value > 0){ - return `<div style="flex: ` + flex_value + `">${__('Item')}</div>` + html_header - } else { - return `<div>${__('Item')}</div>` + html_header - } - - - } - this.make_cart_items_section(); - - items.forEach(item => { - this.render_cart_item(item); - }); - } else { - items.forEach(item => { - var item_html = this.get_item_html(item); - this.$items_container.append(item_html); - }) - } - - // this.$cart_container = this.$component.find('.cart-container'); - - - } - make_cart_items_section() { - this.$cart_header = this.$component.find('.cart-header'); - this.$cart_items_wrapper = this.$component.find('.cart-items-section'); - - } - get_cart_item({ name }) { - const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`; - return this.$cart_items_wrapper.find(item_selector); - } - get_cart_item1({ item_code }) { - const item_selector = `.cart-item-wrapper[data-row-name="${escape(item_code)}"]`; - return this.$cart_items_wrapper.find(item_selector); - } - render_cart_item(item_data) { - const me = this; - const currency = me.events.get_frm().currency || me.currency; - this.$cart_items_wrapper.append( - `<div class="cart-item-wrapper item-wrapper" - data-item-code="${escape(item_data.item_code)}" + </div>`, + ); + + this.make_cart_items_section(); + + items.forEach((item) => { + this.render_cart_item(item); + }); + } else { + items.forEach((item) => { + var item_html = this.get_item_html(item); + this.$items_container.append(item_html); + }); + } + + // this.$cart_container = this.$component.find('.cart-container'); + } + make_cart_items_section() { + this.$cart_header = this.$component.find(".cart-header"); + this.$cart_items_wrapper = this.$component.find(".cart-items-section"); + } + get_cart_item({ name }) { + const item_selector = `.cart-item-wrapper[data-row-name="${escape(name)}"]`; + return this.$cart_items_wrapper.find(item_selector); + } + get_cart_item1({ item_code }) { + const item_selector = `.cart-item-wrapper[data-row-name="${escape( + item_code, + )}"]`; + return this.$cart_items_wrapper.find(item_selector); + } + render_cart_item(item_data) { + const me = this; + const currency = me.events.get_frm().currency || me.currency; + this.$cart_items_wrapper.append( + `<div class="cart-item-wrapper item-wrapper" + data-item-code="${escape(item_data.item_code)}" data-serial-no="${escape(item_data.serial_no)}" - data-batch-no="${escape(item_data.batch_no)}" + data-batch-no="${escape(item_data.batch_no)}" data-uom="${escape(item_data.uom)}" data-rate="${escape(item_data.price_list_rate || 0)}" - data-valuation-rate="${escape(item_data.valuation_rate || item_data.custom_valuation_rate)}" + data-valuation-rate="${escape( + item_data.valuation_rate || item_data.custom_valuation_rate, + )}" data-item-uoms="${item_data.custom_item_uoms}" data-item-logical-rack="${item_data.custom_logical_rack}" title="${item_data.item_name}" data-row-name="${escape(item_data.item_code)}"></div> - <div class="seperator"></div>` - ) - var $item_to_update = this.get_cart_item1(item_data); - $item_to_update.html( - `${get_item_image_html()} + <div class="seperator"></div>`, + ); + var $item_to_update = this.get_cart_item1(item_data); + $item_to_update.html( + `${get_item_image_html()} ${get_item_name()} - + <div style="overflow-wrap: break-word;overflow:hidden;white-space: normal;font-weight: 700;margin-right: 10px"> ${item_data.item_name} </div> ${get_description_html(item_data)} </div> ${get_item_code()} - ${get_rate_discount_html()}` - ) - - function get_item_name() { - var flex_value = 4 - if(me.custom_show_item_code && me.custom_show_last_incoming_rate && me.custom_show_oem_part_number){ - flex_value = 3 - } - // if(me.custom_show_item_code && me.custom_show_last_incoming_rate && !me.custom_show_oem_part_number){ - // flex_value = 3 - // } - if(!me.custom_show_item_code && !me.custom_show_last_incoming_rate && !me.custom_show_oem_part_number && !me.custom_show_logical_rack){ - flex_value = 2 - } - // if(me.custom_show_last_incoming_rate && me.custom_show_item_code){ - // flex_value -= 1 - // } - // if(me.custom_show_oem_part_number){ - // flex_value -= 1 - // } - return `<div class="" style="flex: ` + flex_value +`;overflow-wrap: break-word;overflow:hidden;white-space: normal">` - } - set_dynamic_rate_header_width(); - - function set_dynamic_rate_header_width() { - const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount")); - me.$cart_header.find(".rate-amount-header").css("width", ""); - me.$cart_items_wrapper.find(".item-rate-amount").css("width", ""); - var max_width = rate_cols.reduce((max_width, elm) => { - if ($(elm).width() > max_width) - max_width = $(elm).width(); - return max_width; - }, 0); - - max_width += 1; - if (max_width == 1) max_width = ""; - - me.$cart_header.find(".rate-amount-header").css("width", max_width); - me.$cart_items_wrapper.find(".item-rate-amount").css("width", max_width); - } - function get_item_code() { - var html_code = `` - if(me.custom_show_item_code){ - var item_code_flex_value = 1 - html_code += `<div class="item-code-desc" style="flex: ` + item_code_flex_value + `;text-align: left"> + ${get_rate_discount_html()}`, + ); + + function get_item_name() { + var flex_value = 4; + if ( + me.custom_show_item_code && + me.custom_show_last_incoming_rate && + me.custom_show_oem_part_number + ) { + flex_value = 3; + } + // if(me.custom_show_item_code && me.custom_show_last_incoming_rate && !me.custom_show_oem_part_number){ + // flex_value = 3 + // } + if ( + !me.custom_show_item_code && + !me.custom_show_last_incoming_rate && + !me.custom_show_oem_part_number && + !me.custom_show_logical_rack + ) { + flex_value = 2; + } + // if(me.custom_show_last_incoming_rate && me.custom_show_item_code){ + // flex_value -= 1 + // } + // if(me.custom_show_oem_part_number){ + // flex_value -= 1 + // } + return ( + `<div class="" style="flex: ` + + flex_value + + `;overflow-wrap: break-word;overflow:hidden;white-space: normal">` + ); + } + set_dynamic_rate_header_width(); + + function set_dynamic_rate_header_width() { + const rate_cols = Array.from( + me.$cart_items_wrapper.find(".item-rate-amount"), + ); + me.$cart_header.find(".rate-amount-header").css("width", ""); + me.$cart_items_wrapper.find(".item-rate-amount").css("width", ""); + var max_width = rate_cols.reduce((max_width, elm) => { + if ($(elm).width() > max_width) max_width = $(elm).width(); + return max_width; + }, 0); + + max_width += 1; + if (max_width == 1) max_width = ""; + + me.$cart_header.find(".rate-amount-header").css("width", max_width); + me.$cart_items_wrapper.find(".item-rate-amount").css("width", max_width); + } + function get_item_code() { + var html_code = ``; + if (me.custom_show_item_code) { + var item_code_flex_value = 1; + html_code += + `<div class="item-code-desc" style="flex: ` + + item_code_flex_value + + `;text-align: left"> <div class="item-code" > <b>${item_data.item_code}</b> <br> ${item_data.uom} </div> - </div>` - } - if(me.custom_show_last_incoming_rate){ - html_code += `<div class="incoming-rate-desc" style="flex: 1;text-align: left"> + </div>`; + } + if (me.custom_show_last_incoming_rate) { + html_code += `<div class="incoming-rate-desc" style="flex: 1;text-align: left"> <div class="incoming-rate" > ${parseFloat(item_data.valuation_rate).toFixed(2)} </div> - </div>` - } - if(me.custom_show_oem_part_number){ - html_code += `<div class="incoming-rate-desc" style="flex: 1;text-align: left"> + </div>`; + } + if (me.custom_show_oem_part_number) { + html_code += `<div class="incoming-rate-desc" style="flex: 1;text-align: left"> <div class="incoming-rate" > ${item_data.custom_oem_part_number || ""} </div> - </div>` - } - if(me.custom_show_logical_rack){ - html_code += `<div class="incoming-rate-desc" style="flex: 1;text-align: left"> + </div>`; + } + if (me.custom_show_logical_rack) { + html_code += `<div class="incoming-rate-desc" style="flex: 1;text-align: left"> <div class="incoming-rate" > ${item_data.rack || ""} </div> - </div>` - } - return html_code - } - function get_rate_discount_html() { - if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) { - return ` + </div>`; + } + return html_code; + } + function get_rate_discount_html() { + if ( + item_data.rate && + item_data.amount && + item_data.rate !== item_data.amount + ) { + return ` <div class="item-qty-rate" style="flex: 3"> <div class="item-rate-amount" style="flex: 1"> - <div class="item-rate" style="text-align: left">${format_currency(item_data.price_list_rate, currency)}</div> + <div class="item-rate" style="text-align: left">${format_currency( + item_data.price_list_rate, + currency, + )}</div> </div> - <div class="item-qty" style="flex: 1;display:block;text-align: center"><span> ${item_data.actual_qty || 0}</span></div> - - - </div>` - } else { - return ` + <div class="item-qty" style="flex: 1;display:block;text-align: center"><span> ${ + item_data.actual_qty || 0 + }</span></div> + + + </div>`; + } else { + return ` <div class="item-qty-rate" style="flex: 3"> <div class="item-rate-amount" style="flex: 1"> - <div class="item-rate" style="text-align: left">${format_currency(item_data.price_list_rate, currency)}</div> + <div class="item-rate" style="text-align: left">${format_currency( + item_data.price_list_rate, + currency, + )}</div> </div> - <div class="item-qty" style="flex: 1;display:block;text-align: center"><span> ${item_data.actual_qty || 0}</span></div> - - - </div>` - } - } - - function get_description_html(item_data) { - - if (me.custom_show_item_discription) { - if (item_data.description.indexOf('<div>') != -1) { - try { - item_data.description = $(item_data.description).text(); - } catch (error) { - item_data.description = item_data.description.replace(/<div>/g, ' ').replace(/<\/div>/g, ' ').replace(/ +/g, ' '); - } - } - item_data.description = frappe.ellipsis(item_data.description, 45); - return `<div class="item-desc">${item_data.description}</div>`; - } - return ``; - } - - function get_item_image_html() { - const { image, item_name } = item_data; - if (!me.hide_images && image) { - return ` + <div class="item-qty" style="flex: 1;display:block;text-align: center"><span> ${ + item_data.actual_qty || 0 + }</span></div> + + + </div>`; + } + } + + function get_description_html(item_data) { + if (me.custom_show_item_discription) { + if (item_data.description.indexOf("<div>") != -1) { + try { + item_data.description = $(item_data.description).text(); + } catch (error) { + item_data.description = item_data.description + .replace(/<div>/g, " ") + .replace(/<\/div>/g, " ") + .replace(/ +/g, " "); + } + } + item_data.description = frappe.ellipsis(item_data.description, 45); + return `<div class="item-desc">${item_data.description}</div>`; + } + return ``; + } + + function get_item_image_html() { + const { image, item_name } = item_data; + if (!me.hide_images && image) { + return ` <div class="item-image"> <img onerror="cur_pos.cart.handle_broken_image(this)" src="${image}" alt="${frappe.get_abbr(item_name)}""> </div>`; - } else { - return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`; - } - } - } - get_item_html(item) { - const me = this; - item.currency = item.currency || me.currency - // eslint-disable-next-line no-unused-vars - const { item_image, serial_no, batch_no, barcode, actual_qty, uom, price_list_rate } = item; - const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0; - let indicator_color; - let qty_to_display = actual_qty; - - if (item.is_stock_item) { - indicator_color = (actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"); - - if (Math.round(qty_to_display) > 999) { - qty_to_display = Math.round(qty_to_display)/1000; - qty_to_display = qty_to_display.toFixed(1) + 'K'; - } - } else { - indicator_color = ''; - qty_to_display = ''; - } - - function get_item_image_html() { - if (!me.hide_images && item_image) { - return `<div class="item-qty-pill"> + } else { + return `<div class="item-image item-abbr">${frappe.get_abbr( + item_name, + )}</div>`; + } + } + } + get_item_html(item) { + const me = this; + item.currency = item.currency || me.currency; + // eslint-disable-next-line no-unused-vars + const { + item_image, + serial_no, + batch_no, + barcode, + actual_qty, + uom, + price_list_rate, + } = item; + const precision = flt(price_list_rate, 2) % 1 != 0 ? 2 : 0; + let indicator_color; + let qty_to_display = actual_qty; + + if (item.is_stock_item) { + indicator_color = + actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange"; + + if (Math.round(qty_to_display) > 999) { + qty_to_display = Math.round(qty_to_display) / 1000; + qty_to_display = qty_to_display.toFixed(1) + "K"; + } + } else { + indicator_color = ""; + qty_to_display = ""; + } + + function get_item_image_html() { + if (!me.hide_images && item_image) { + return `<div class="item-qty-pill"> <span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span> </div> <div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100"> @@ -494,16 +637,15 @@ posnext.PointOfSale.ItemSelector = class { alt="${frappe.get_abbr(item.item_name)}" > </div>`; - } else { - return `<div class="item-qty-pill"> + } else { + return `<div class="item-qty-pill"> <span class="indicator-pill whitespace-nowrap ${indicator_color}">${qty_to_display}</span> </div> <div class="item-display abbr">${frappe.get_abbr(item.item_name)}</div>`; - } - } + } + } - return ( - `<div class="item-wrapper" + return `<div class="item-wrapper" data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}" data-batch-no="${escape(batch_no)}" data-uom="${escape(uom)}" data-rate="${escape(price_list_rate || 0)}" @@ -515,339 +657,370 @@ posnext.PointOfSale.ItemSelector = class { <div class="item-name"> ${frappe.ellipsis(item.item_name, 18)} </div> - <div class="item-rate">${format_currency(price_list_rate, item.currency, precision) || 0} / ${uom}</div> + <div class="item-rate">${ + format_currency(price_list_rate, item.currency, precision) || 0 + } / ${uom}</div> </div> - </div>` - ); - } - - handle_broken_image($img) { - const item_abbr = $($img).attr('alt'); - $($img).parent().replaceWith(`<div class="item-display abbr">${item_abbr}</div>`); - } - update_total_incoming_rate(total_rate){ - if(this.total_incoming_rate){ - this.total_incoming_rate.set_value(total_rate) - } - } - make_search_bar() { - const me = this; - const doc = me.events.get_frm().doc; - this.$component.find('.search-field').html(''); - // this.$component.find('.item-code-search-field').html(''); - this.$component.find('.pos-profile').html(''); - this.$component.find('.total-incoming-rate').html(''); - this.$component.find('.item-group-field').html(''); - this.$component.find('.invoice-posting-date').html(''); - frappe.db.get_single_value("POS Settings","custom_profile_lock").then(doc => { - this.pos_profile_field = frappe.ui.form.make_control({ - df: { - label: __('POS Profile'), - fieldtype: 'Link', - options: 'POS Profile', - placeholder: __('POS Profile'), - read_only: doc, - onchange: function () { - - if(me.reload_status && me.pos_profile !== this.value){ - frappe.pages['posnext'].refresh(window.wrapper,window.onScan,this.value) - } - - } - }, - parent: this.$component.find('.pos-profile'), - render_input: false, - }); - this.pos_profile_field.set_value(me.pos_profile) - this.pos_profile_field.refresh() - this.pos_profile_field.toggle_label(false); - - }) - - this.search_field = frappe.ui.form.make_control({ - df: { - label: __('Search'), - fieldtype: 'Data', - placeholder: __('Search by serial number or barcode') - }, - parent: this.$component.find('.search-field'), - render_input: true, - }); - - this.item_group_field = frappe.ui.form.make_control({ - df: { - label: __('Item Group'), - fieldtype: 'Link', - options: 'Item Group', - placeholder: __('Select item group'), - onchange: function() { - me.item_group = this.value; - !me.item_group && (me.item_group = me.parent_item_group); - me.filter_items(); - }, - get_query: function () { - return { - query: 'posnext.posnext.page.posnext.point_of_sale.item_group_query', - filters: { - pos_profile: doc ? doc.pos_profile : '' - } - }; - }, - }, - parent: this.$component.find('.item-group-field'), - render_input: true, - }); - if(this.custom_show_last_incoming_rate || this.custom_show_incoming_rate) { - this.total_incoming_rate = frappe.ui.form.make_control({ - df: { - label: __(''), - fieldtype: 'Currency', - read_only: 1, - placeholder: __('Total Incoming Rate'), - default: 0 - }, - parent: this.$component.find('.total-incoming-rate'), - render_input: true, - }); - } - if(me.custom_show_posting_date){ - this.invoice_posting_date = frappe.ui.form.make_control({ - df: { - label: __('Posting Date'), - fieldtype: 'Date', - onchange: function() { - me.events.get_frm().doc.posting_date= this.value; - me.events.get_frm().doc.set_posting_time= 1; - }, - - }, - parent: this.$component.find('.invoice-posting-date'), - render_input: true, - }); - } - - - this.search_field.toggle_label(false); - this.item_group_field.toggle_label(false); - if(this.custom_show_last_incoming_rate) { - this.total_incoming_rate.toggle_label(false); - } - if(me.custom_show_posting_date) { - this.invoice_posting_date.toggle_label(false); - this.invoice_posting_date.set_value(frappe.datetime.get_today()) - - } - - this.attach_clear_btn(); - } - - attach_clear_btn() { - this.search_field.$wrapper.find('.control-input').append( - `<span class="link-btn" style="top: 2px;"> + </div>`; + } + + handle_broken_image($img) { + const item_abbr = $($img).attr("alt"); + $($img) + .parent() + .replaceWith(`<div class="item-display abbr">${item_abbr}</div>`); + } + update_total_incoming_rate(total_rate) { + if (this.total_incoming_rate) { + this.total_incoming_rate.set_value(total_rate); + } + } + make_search_bar() { + const me = this; + const doc = me.events.get_frm().doc; + this.$component.find(".search-field").html(""); + // this.$component.find('.item-code-search-field').html(''); + this.$component.find(".pos-profile").html(""); + this.$component.find(".total-incoming-rate").html(""); + this.$component.find(".item-group-field").html(""); + this.$component.find(".invoice-posting-date").html(""); + frappe.db + .get_single_value("POS Settings", "custom_profile_lock") + .then((doc) => { + this.pos_profile_field = frappe.ui.form.make_control({ + df: { + label: __("POS Profile"), + fieldtype: "Link", + options: "POS Profile", + placeholder: __("POS Profile"), + read_only: doc, + onchange: function () { + if (me.reload_status && me.pos_profile !== this.value) { + frappe.pages["posnext"].refresh( + window.wrapper, + window.onScan, + this.value, + ); + } + }, + }, + parent: this.$component.find(".pos-profile"), + render_input: false, + }); + this.pos_profile_field.set_value(me.pos_profile); + this.pos_profile_field.refresh(); + this.pos_profile_field.toggle_label(false); + }); + + this.search_field = frappe.ui.form.make_control({ + df: { + label: __("Search"), + fieldtype: "Data", + placeholder: __("Search by serial number or barcode"), + }, + parent: this.$component.find(".search-field"), + render_input: true, + }); + + this.item_group_field = frappe.ui.form.make_control({ + df: { + label: __("Item Group"), + fieldtype: "Link", + options: "Item Group", + placeholder: __("Select item group"), + onchange: function () { + me.item_group = this.value; + !me.item_group && (me.item_group = me.parent_item_group); + me.filter_items(); + }, + get_query: function () { + return { + query: + "posnext.posnext.page.posnext.point_of_sale.item_group_query", + filters: { + pos_profile: doc ? doc.pos_profile : "", + }, + }; + }, + }, + parent: this.$component.find(".item-group-field"), + render_input: true, + }); + if (this.custom_show_last_incoming_rate || this.custom_show_incoming_rate) { + this.total_incoming_rate = frappe.ui.form.make_control({ + df: { + label: "", + fieldtype: "Currency", + read_only: 1, + placeholder: __("Total Incoming Rate"), + default: 0, + }, + parent: this.$component.find(".total-incoming-rate"), + render_input: true, + }); + } + if (me.custom_show_posting_date) { + this.invoice_posting_date = frappe.ui.form.make_control({ + df: { + label: __("Posting Date"), + fieldtype: "Date", + onchange: function () { + me.events.get_frm().doc.posting_date = this.value; + me.events.get_frm().doc.set_posting_time = 1; + }, + }, + parent: this.$component.find(".invoice-posting-date"), + render_input: true, + }); + } + + this.search_field.toggle_label(false); + this.item_group_field.toggle_label(false); + if (this.custom_show_last_incoming_rate) { + this.total_incoming_rate.toggle_label(false); + } + if (me.custom_show_posting_date) { + this.invoice_posting_date.toggle_label(false); + this.invoice_posting_date.set_value(frappe.datetime.get_today()); + } + + this.attach_clear_btn(); + } + + attach_clear_btn() { + this.search_field.$wrapper.find(".control-input").append( + `<span class="link-btn" style="top: 2px;"> <a class="btn-open no-decoration" title="${__("Clear")}"> - ${frappe.utils.icon('close', 'sm')} + ${frappe.utils.icon("close", "sm")} </a> - </span>` - ); - - this.$clear_search_btn = this.search_field.$wrapper.find('.link-btn'); - - this.$clear_search_btn.on('click', 'a', () => { - this.set_search_value(''); - this.search_field.set_focus(); - }); - } - - set_search_value(value) { - $(this.search_field.$input[0]).val(value).trigger("input"); - } - - bind_events() { - const me = this; - if(!window.onScan){ - frappe.require("https://cdn.jsdelivr.net/npm/onscan.js/onscan.min.js", function() { - window.onScan = onScan; - - onScan.decodeKeyEvent = function (oEvent) { - var iCode = this._getNormalizedKeyNum(oEvent); - switch (true) { - case iCode >= 48 && iCode <= 90: // numbers and letters - case iCode >= 106 && iCode <= 111: // operations on numeric keypad (+, -, etc.) - case (iCode >= 160 && iCode <= 164) || iCode == 170: // ^ ! # $ * - case iCode >= 186 && iCode <= 194: // (; = , - . / `) - case iCode >= 219 && iCode <= 222: // ([ \ ] ') - case iCode == 32: // spacebar - if (oEvent.key !== undefined && oEvent.key !== '') { - return oEvent.key; - } - - var sDecoded = String.fromCharCode(iCode); - switch (oEvent.shiftKey) { - case false: sDecoded = sDecoded.toLowerCase(); break; - case true: sDecoded = sDecoded.toUpperCase(); break; - } - return sDecoded; - case iCode >= 96 && iCode <= 105: // numbers on numeric keypad - return 0 + (iCode - 96); - } - return ''; - }; - - onScan.attachTo(document, { - onScan: (sScancode) => { - if (this.search_field && this.$component.is(':visible')) { - this.search_field.set_focus(); - this.set_search_value(sScancode); - this.barcode_scanned = true; - } - } - }); - }) - } - - - - this.$component.on('click', '.item-wrapper', function() { - const $item = $(this); - const item_code = unescape($item.attr('data-item-code')); - let batch_no = unescape($item.attr('data-batch-no')); - let serial_no = unescape($item.attr('data-serial-no')); - let uom = unescape($item.attr('data-uom')); - let rate = unescape($item.attr('data-rate')); - let valuation_rate = unescape($item.attr('data-valuation-rate')); - let custom_item_uoms = $item.attr('data-item-uoms'); - let custom_logical_rack = $item.attr('data-item-logical-rack') - // escape(undefined) returns "un defined" then unescape returns "undefined" - batch_no = batch_no === "undefined" ? undefined : batch_no; - serial_no = serial_no === "undefined" ? undefined : serial_no; - uom = uom === "undefined" ? undefined : uom; - rate = rate === "undefined" ? undefined : rate; - me.events.item_selected({ - field: 'qty', - value: "+1", - item: { item_code, batch_no, serial_no, uom, rate ,valuation_rate, custom_item_uoms, custom_logical_rack} - }); - // me.search_field.set_focus(); - }); - - this.search_field.$input.on('input', (e) => { - clearTimeout(this.last_search); - this.last_search = setTimeout(() => { - const search_term = e.target.value; - this.filter_items({ search_term }); - }, 300); - - // this.$clear_search_btn.toggle( - // Boolean(this.search_field.$input.val()) - // ); - }); - - // this.search_field.$input.on('focus', () => { - // this.$clear_search_btn.toggle( - // Boolean(this.search_field.$input.val()) - // ); - // }); - } - - attach_shortcuts() { - const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; - this.search_field.parent.attr("title", `${ctrl_label}+I`); - frappe.ui.keys.add_shortcut({ - shortcut: "ctrl+i", - action: () => this.search_field.set_focus(), - condition: () => this.$component.is(':visible'), - description: __("Focus on search input"), - ignore_inputs: true, - page: cur_page.page.page - }); - this.item_group_field.parent.attr("title", `${ctrl_label}+G`); - frappe.ui.keys.add_shortcut({ - shortcut: "ctrl+g", - action: () => this.item_group_field.set_focus(), - condition: () => this.$component.is(':visible'), - description: __("Focus on Item Group filter"), - ignore_inputs: true, - page: cur_page.page.page - }); - - // for selecting the last filtered item on search - frappe.ui.keys.on("enter", () => { - const selector_is_visible = this.$component.is(':visible'); - if (!selector_is_visible || this.search_field.get_value() === "") return; - - if (this.items.length == 1) { - this.$items_container.find(".item-wrapper").click(); - frappe.utils.play_sound("submit"); - this.set_search_value(''); - } else if (this.items.length == 0 && this.barcode_scanned) { - // only show alert of barcode is scanned and enter is pressed - frappe.show_alert({ - message: __("No items found. Scan barcode again."), - indicator: 'orange' - }); - frappe.utils.play_sound("error"); - this.barcode_scanned = false; - this.set_search_value(''); - } - }); - } - - filter_items({ search_term='' }={}) { - if (search_term) { - search_term = search_term.toLowerCase(); - - // memoize - this.search_index = this.search_index || {}; - if (this.search_index[search_term]) { - const items = this.search_index[search_term]; - this.items = items; - this.render_item_list(items); - if (this.auto_search_serial && this.items.length === 1) { - this.add_filtered_item_to_cart(); - } - return; - } - } - - this.get_items({ search_term }) - .then(({ message }) => { - const { items, serial_no, batch_no, barcode } = message; - if (search_term && !barcode) { - this.search_index[search_term] = items; - } - this.items = items; - this.render_item_list(items); - if (this.auto_search_serial && this.items.length === 1) { - this.add_filtered_item_to_cart(); - } - }); - } - - add_filtered_item_to_cart() { - this.$items_container.find(".item-wrapper").click(); - this.set_search_value(''); - } - - resize_selector(minimize) { - minimize ? - this.$component.find('.filter-section').css('grid-template-columns', 'repeat(1, minmax(0, 1fr))') : - this.$component.find('.filter-section').css('grid-template-columns', 'repeat(12, minmax(0, 1fr))'); - - minimize ? - this.$component.find('.search-field').css('margin', 'var(--margin-sm) 0px') : - this.$component.find('.search-field').css('margin', '0px var(--margin-sm)'); - - minimize ? - this.$component.css('grid-column', 'span 2 / span 2') : - this.$component.css('grid-column', 'span 6 / span 6'); - - minimize ? - this.$items_container.css('grid-template-columns', 'repeat(1, minmax(0, 1fr))') : - this.$items_container.css('grid-template-columns', 'repeat(4, minmax(0, 1fr))'); - } - - toggle_component(show) { - this.set_search_value(''); - this.$component.css('display', show ? 'flex': 'none'); - } + </span>`, + ); + + this.$clear_search_btn = this.search_field.$wrapper.find(".link-btn"); + + this.$clear_search_btn.on("click", "a", () => { + this.set_search_value(""); + this.search_field.set_focus(); + }); + } + + set_search_value(value) { + $(this.search_field.$input[0]).val(value).trigger("input"); + } + + bind_events() { + const me = this; + if (!window.onScan) { + frappe.require( + "https://cdn.jsdelivr.net/npm/onscan.js/onscan.min.js", + function () { + window.onScan = onScan; + + onScan.decodeKeyEvent = function (oEvent) { + var iCode = this._getNormalizedKeyNum(oEvent); + switch (true) { + case iCode >= 48 && iCode <= 90: // numbers and letters + case iCode >= 106 && iCode <= 111: // operations on numeric keypad (+, -, etc.) + case (iCode >= 160 && iCode <= 164) || iCode == 170: // ^ ! # $ * + case iCode >= 186 && iCode <= 194: // (; = , - . / `) + case iCode >= 219 && iCode <= 222: // ([ \ ] ') + case iCode == 32: // spacebar + if (oEvent.key !== undefined && oEvent.key !== "") { + return oEvent.key; + } + + var sDecoded = String.fromCharCode(iCode); + switch (oEvent.shiftKey) { + case false: + sDecoded = sDecoded.toLowerCase(); + break; + case true: + sDecoded = sDecoded.toUpperCase(); + break; + } + return sDecoded; + case iCode >= 96 && iCode <= 105: // numbers on numeric keypad + return 0 + (iCode - 96); + } + return ""; + }; + + onScan.attachTo(document, { + onScan: (sScancode) => { + if (this.search_field && this.$component.is(":visible")) { + this.search_field.set_focus(); + this.set_search_value(sScancode); + this.barcode_scanned = true; + } + }, + }); + }, + ); + } + + this.$component.on("click", ".item-wrapper", function () { + const $item = $(this); + const item_code = unescape($item.attr("data-item-code")); + let batch_no = unescape($item.attr("data-batch-no")); + let serial_no = unescape($item.attr("data-serial-no")); + let uom = unescape($item.attr("data-uom")); + let rate = unescape($item.attr("data-rate")); + let valuation_rate = unescape($item.attr("data-valuation-rate")); + let custom_item_uoms = $item.attr("data-item-uoms"); + let custom_logical_rack = $item.attr("data-item-logical-rack"); + // escape(undefined) returns "un defined" then unescape returns "undefined" + batch_no = batch_no === "undefined" ? undefined : batch_no; + serial_no = serial_no === "undefined" ? undefined : serial_no; + uom = uom === "undefined" ? undefined : uom; + rate = rate === "undefined" ? undefined : rate; + me.events.item_selected({ + field: "qty", + value: "+1", + item: { + item_code, + batch_no, + serial_no, + uom, + rate, + valuation_rate, + custom_item_uoms, + custom_logical_rack, + }, + }); + // me.search_field.set_focus(); + }); + + this.search_field.$input.on("input", (e) => { + clearTimeout(this.last_search); + this.last_search = setTimeout(() => { + const search_term = e.target.value; + this.filter_items({ search_term }); + }, 300); + + // this.$clear_search_btn.toggle( + // Boolean(this.search_field.$input.val()) + // ); + }); + + // this.search_field.$input.on('focus', () => { + // this.$clear_search_btn.toggle( + // Boolean(this.search_field.$input.val()) + // ); + // }); + } + + attach_shortcuts() { + const ctrl_label = frappe.utils.is_mac() ? "⌘" : "Ctrl"; + this.search_field.parent.attr("title", `${ctrl_label}+I`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+i", + action: () => this.search_field.set_focus(), + condition: () => this.$component.is(":visible"), + description: __("Focus on search input"), + ignore_inputs: true, + page: cur_page.page.page, + }); + this.item_group_field.parent.attr("title", `${ctrl_label}+G`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+g", + action: () => this.item_group_field.set_focus(), + condition: () => this.$component.is(":visible"), + description: __("Focus on Item Group filter"), + ignore_inputs: true, + page: cur_page.page.page, + }); + + // for selecting the last filtered item on search + frappe.ui.keys.on("enter", () => { + const selector_is_visible = this.$component.is(":visible"); + if (!selector_is_visible || this.search_field.get_value() === "") return; + + if (this.items.length == 1) { + this.$items_container.find(".item-wrapper").click(); + frappe.utils.play_sound("submit"); + this.set_search_value(""); + } else if (this.items.length == 0 && this.barcode_scanned) { + // only show alert of barcode is scanned and enter is pressed + frappe.show_alert({ + message: __("No items found. Scan barcode again."), + indicator: "orange", + }); + frappe.utils.play_sound("error"); + this.barcode_scanned = false; + this.set_search_value(""); + } + }); + } + + filter_items({ search_term = "" } = {}) { + if (search_term) { + search_term = search_term.toLowerCase(); + + // memoize + this.search_index = this.search_index || {}; + if (this.search_index[search_term]) { + const items = this.search_index[search_term]; + this.items = items; + this.render_item_list(items); + if (this.auto_search_serial && this.items.length === 1) { + this.add_filtered_item_to_cart(); + } + return; + } + } + + this.get_items({ search_term }).then(({ message }) => { + const { items, serial_no, batch_no, barcode } = message; + if (search_term && !barcode) { + this.search_index[search_term] = items; + } + this.items = items; + this.render_item_list(items); + if (this.auto_search_serial && this.items.length === 1) { + this.add_filtered_item_to_cart(); + } + }); + } + + add_filtered_item_to_cart() { + this.$items_container.find(".item-wrapper").click(); + this.set_search_value(""); + } + + resize_selector(minimize) { + minimize + ? this.$component + .find(".filter-section") + .css("grid-template-columns", "repeat(1, minmax(0, 1fr))") + : this.$component + .find(".filter-section") + .css("grid-template-columns", "repeat(12, minmax(0, 1fr))"); + + minimize + ? this.$component + .find(".search-field") + .css("margin", "var(--margin-sm) 0px") + : this.$component + .find(".search-field") + .css("margin", "0px var(--margin-sm)"); + + minimize + ? this.$component.css("grid-column", "span 2 / span 2") + : this.$component.css("grid-column", "span 6 / span 6"); + + minimize + ? this.$items_container.css( + "grid-template-columns", + "repeat(1, minmax(0, 1fr))", + ) + : this.$items_container.css( + "grid-template-columns", + "repeat(4, minmax(0, 1fr))", + ); + } + + toggle_component(show) { + this.set_search_value(""); + this.$component.css("display", show ? "flex" : "none"); + } }; diff --git a/posnext/public/js/pos_number_pad.js b/posnext/public/js/pos_number_pad.js index c59a43e..bc7ed5b 100644 --- a/posnext/public/js/pos_number_pad.js +++ b/posnext/public/js/pos_number_pad.js @@ -1,48 +1,61 @@ -frappe.provide('posnext.PointOfSale'); +frappe.provide("posnext.PointOfSale"); posnext.PointOfSale.NumberPad = class { - constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) { - this.wrapper = wrapper; - this.events = events; - this.cols = cols; - this.keys = keys; - this.css_classes = css_classes || []; - this.fieldnames = fieldnames_map || {}; + constructor({ wrapper, events, cols, keys, css_classes, fieldnames_map }) { + this.wrapper = wrapper; + this.events = events; + this.cols = cols; + this.keys = keys; + this.css_classes = css_classes || []; + this.fieldnames = fieldnames_map || {}; - this.init_component(); - } + this.init_component(); + } - init_component() { - this.prepare_dom(); - this.bind_events(); - } + init_component() { + this.prepare_dom(); + this.bind_events(); + } - prepare_dom() { - const { cols, keys, css_classes, fieldnames } = this; + prepare_dom() { + const { cols, keys, css_classes, fieldnames } = this; - function get_keys() { - return keys.reduce((a, row, i) => { - return a + row.reduce((a2, number, j) => { - const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : ''; - const fieldname = fieldnames && fieldnames[number] ? - fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number; + function get_keys() { + return keys.reduce((a, row, i) => { + return ( + a + + row.reduce((a2, number, j) => { + const class_to_append = + css_classes && css_classes[i] ? css_classes[i][j] : ""; + const fieldname = + fieldnames && fieldnames[number] + ? fieldnames[number] + : typeof number === "string" + ? frappe.scrub(number) + : number; - return a2 + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${__(number)}</div>`; - }, ''); - }, ''); - } + return ( + a2 + + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${__( + number, + )}</div>` + ); + }, "") + ); + }, ""); + } - this.wrapper.html( - `<div class="numpad-container"> + this.wrapper.html( + `<div class="numpad-container"> ${get_keys()} - </div>` - ) - } + </div>`, + ); + } - bind_events() { - const me = this; - this.wrapper.on('click', '.numpad-btn', function() { - const $btn = $(this); - me.events.numpad_event($btn); - }); - } -} + bind_events() { + const me = this; + this.wrapper.on("click", ".numpad-btn", function () { + const $btn = $(this); + me.events.numpad_event($btn); + }); + } +}; diff --git a/posnext/public/js/pos_past_order_list.js b/posnext/public/js/pos_past_order_list.js index 60dedaa..a263a2e 100644 --- a/posnext/public/js/pos_past_order_list.js +++ b/posnext/public/js/pos_past_order_list.js @@ -1,23 +1,24 @@ -frappe.provide('posnext.PointOfSale'); -var invoicess = [] +frappe.provide("posnext.PointOfSale"); +var invoicess = []; posnext.PointOfSale.PastOrderList = class { - constructor({ wrapper, events, settings }) { - this.wrapper = wrapper; - this.events = events; - this.pos_profile = settings.name - this.custom_filter_order_list_by_profile = settings.custom_filter_order_list_by_profile - this.init_component(); - } + constructor({ wrapper, events, settings }) { + this.wrapper = wrapper; + this.events = events; + this.pos_profile = settings.name; + this.custom_filter_order_list_by_profile = + settings.custom_filter_order_list_by_profile; + this.init_component(); + } - init_component() { - this.prepare_dom(); - this.make_filter_section(); - this.bind_events(); - } + init_component() { + this.prepare_dom(); + this.make_filter_section(); + this.bind_events(); + } - prepare_dom() { - this.wrapper.append( - `<section class="past-order-list"> + prepare_dom() { + this.wrapper.append( + `<section class="past-order-list"> <div class="filter-section"> <div class="label back" style="font-size: 13px "> <a> @@ -26,98 +27,99 @@ posnext.PointOfSale.PastOrderList = class { </a> </div> <br> - <div class="label">${__('Recent Orders')}</div> + <div class="label">${__("Recent Orders")}</div> <div class="search-field"></div> <div class="status-field"></div> </div> <div class="invoices-container"></div> - </section>` - ); + </section>`, + ); - this.$component = this.wrapper.find('.past-order-list'); - this.$invoices_container = this.$component.find('.invoices-container'); + this.$component = this.wrapper.find(".past-order-list"); + this.$invoices_container = this.$component.find(".invoices-container"); + } - } + bind_events() { + this.search_field.$input.on("input", (e) => { + clearTimeout(this.last_search); + this.last_search = setTimeout(() => { + const search_term = e.target.value; + this.refresh_list(search_term, this.status_field.get_value()); + }, 300); + }); + const me = this; + this.$invoices_container.on("click", ".invoice-wrapper", function () { + const invoice_name = unescape($(this).attr("data-invoice-name")); + me.events.open_invoice_data(invoice_name); + }); + this.$component.on("click", ".back", function () { + me.events.previous_screen(); + }); + } - bind_events() { - this.search_field.$input.on('input', (e) => { - clearTimeout(this.last_search); - this.last_search = setTimeout(() => { - const search_term = e.target.value; - this.refresh_list(search_term, this.status_field.get_value()); - }, 300); - }); - const me = this; - this.$invoices_container.on('click', '.invoice-wrapper', function() { - const invoice_name = unescape($(this).attr('data-invoice-name')); - me.events.open_invoice_data(invoice_name); - }); - this.$component.on('click', '.back', function() { - me.events.previous_screen() - }); - } + make_filter_section() { + const me = this; + this.search_field = frappe.ui.form.make_control({ + df: { + label: __("Search"), + fieldtype: "Data", + placeholder: __("Search by invoice id or customer name"), + }, + parent: this.$component.find(".search-field"), + render_input: true, + }); + this.status_field = frappe.ui.form.make_control({ + df: { + label: __("Invoice Status"), + fieldtype: "Select", + options: `Draft\nPaid\nUnpaid\nReturn`, + placeholder: __("Filter by invoice status"), + onchange: function () { + if (me.$component.is(":visible")) me.refresh_list(); + }, + }, + parent: this.$component.find(".status-field"), + render_input: true, + }); + this.search_field.toggle_label(false); + this.status_field.toggle_label(false); + this.status_field.set_value("Draft"); + } - make_filter_section() { - const me = this; - this.search_field = frappe.ui.form.make_control({ - df: { - label: __('Search'), - fieldtype: 'Data', - placeholder: __('Search by invoice id or customer name') - }, - parent: this.$component.find('.search-field'), - render_input: true, - }); - this.status_field = frappe.ui.form.make_control({ - df: { - label: __('Invoice Status'), - fieldtype: 'Select', - options: `Draft\nPaid\nUnpaid\nReturn`, - placeholder: __('Filter by invoice status'), - onchange: function() { - if (me.$component.is(':visible')) me.refresh_list(); - } - }, - parent: this.$component.find('.status-field'), - render_input: true, - }); - this.search_field.toggle_label(false); - this.status_field.toggle_label(false); - this.status_field.set_value('Draft'); - } + refresh_list() { + frappe.dom.freeze(); + this.events.reset_summary(); + const search_term = this.search_field.get_value(); + const status = this.status_field.get_value(); + const pos_profile = this.pos_profile; + this.$invoices_container.html(""); + let filter = { search_term, status }; + if (this.custom_filter_order_list_by_profile) { + filter = { search_term, status, pos_profile }; + } - refresh_list() { - frappe.dom.freeze(); - this.events.reset_summary(); - const search_term = this.search_field.get_value(); - const status = this.status_field.get_value(); - const pos_profile = this.pos_profile; - this.$invoices_container.html(''); - let filter = { search_term, status }; - if(this.custom_filter_order_list_by_profile){ - filter = { search_term, status, pos_profile } - } - - return frappe.call({ - method: "posnext.posnext.page.posnext.point_of_sale.get_past_order_list", - freeze: true, - args: filter, - callback: (response) => { - frappe.dom.unfreeze(); - invoicess = response.message - response.message.forEach(invoice => { - const invoice_html = this.get_invoice_html(invoice); - this.$invoices_container.append(invoice_html); - }); - } - }); + return frappe.call({ + method: "posnext.posnext.page.posnext.point_of_sale.get_past_order_list", + freeze: true, + args: filter, + callback: (response) => { + frappe.dom.unfreeze(); + invoicess = response.message; + response.message.forEach((invoice) => { + const invoice_html = this.get_invoice_html(invoice); + this.$invoices_container.append(invoice_html); + }); + }, + }); + } - } - - get_invoice_html(invoice) { - const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma"); - return ( - `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}"> + get_invoice_html(invoice) { + const posting_datetime = moment( + invoice.posting_date + " " + invoice.posting_time, + ).format("Do MMMM, h:mma"); + return `<div class="invoice-wrapper" data-invoice-name="${escape( + invoice.name, + )}"> <div class="invoice-name-date"> <div class="invoice-name">${invoice.name}</div> <div class="invoice-date"> @@ -128,21 +130,22 @@ posnext.PointOfSale.PastOrderList = class { </div> </div> <div class="invoice-total-status"> - <div class="invoice-total">${format_currency(invoice.grand_total, invoice.currency, 0) || 0}</div> + <div class="invoice-total">${ + format_currency(invoice.grand_total, invoice.currency, 0) || 0 + }</div> <div class="invoice-date">${posting_datetime}</div> </div> </div> - <div class="seperator"></div>` - ); - } - - toggle_component(show) { - frappe.run_serially([ - () => show ? this.$component.css('display', 'flex') && this.refresh_list() : this.$component.css('display', 'none'), - () => this.events.open_invoice_data(invoicess[0].name) - ]) - - + <div class="seperator"></div>`; + } - } + toggle_component(show) { + frappe.run_serially([ + () => + show + ? this.$component.css("display", "flex") && this.refresh_list() + : this.$component.css("display", "none"), + () => this.events.open_invoice_data(invoicess[0].name), + ]); + } }; diff --git a/posnext/public/js/pos_past_order_summary.js b/posnext/public/js/pos_past_order_summary.js index 3f9e165..b91121a 100644 --- a/posnext/public/js/pos_past_order_summary.js +++ b/posnext/public/js/pos_past_order_summary.js @@ -1,495 +1,581 @@ -frappe.provide('posnext.PointOfSale'); +frappe.provide("posnext.PointOfSale"); posnext.PointOfSale.PastOrderSummary = class { - constructor({ wrapper, pos_profile,events }) { - this.wrapper = wrapper; - this.pos_profile = pos_profile; - this.events = events; - - this.init_component(); - } - - init_component() { - this.prepare_dom(); - this.init_email_print_dialog(); - this.bind_events(); - this.attach_shortcuts(); - } - - prepare_dom() { - this.wrapper.append( - `<section class="past-order-summary"> + constructor({ wrapper, pos_profile, events }) { + this.wrapper = wrapper; + this.pos_profile = pos_profile; + this.events = events; + + this.init_component(); + } + + init_component() { + this.prepare_dom(); + this.init_email_print_dialog(); + this.bind_events(); + this.attach_shortcuts(); + } + + prepare_dom() { + this.wrapper.append( + `<section class="past-order-summary"> <div class="no-summary-placeholder"> - ${__('Select an invoice to load summary data')} + ${__("Select an invoice to load summary data")} </div> <div class="invoice-summary-wrapper" > <div class="abs-container" > <div class="upper-section"></div> - <div class="label">${__('Items')}</div> + <div class="label">${__("Items")}</div> <div class="items-container summary-container"></div> - <div class="label">${__('Totals')}</div> + <div class="label">${__("Totals")}</div> <div class="totals-container summary-container"></div> - <div class="label">${__('Payments')}</div> + <div class="label">${__("Payments")}</div> <div class="payments-container summary-container"></div> <div class="summary-btns"></div> </div> </div> - </section>` - ); - - this.$component = this.wrapper.find('.past-order-summary'); - this.$summary_wrapper = this.$component.find('.invoice-summary-wrapper'); - this.$summary_container = this.$component.find('.abs-container'); - this.$upper_section = this.$summary_container.find('.upper-section'); - this.$items_container = this.$summary_container.find('.items-container'); - this.$totals_container = this.$summary_container.find('.totals-container'); - this.$payment_container = this.$summary_container.find('.payments-container'); - this.$summary_btns = this.$summary_container.find('.summary-btns'); - } - - init_email_print_dialog() { - const email_dialog = new frappe.ui.Dialog({ - title: 'Email Receipt', - fields: [ - {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID', reqd: 1}, - {fieldname:'content', fieldtype:'Small Text', label:'Message (if any)'} - ], - primary_action: () => { - this.send_email(); - }, - primary_action_label: __('Send'), - }); - this.email_dialog = email_dialog; - - const print_dialog = new frappe.ui.Dialog({ - title: 'Print Receipt', - fields: [ - {fieldname: 'print', fieldtype: 'Data', label: 'Print Preview'} - ], - primary_action: () => { - this.print_receipt(); - }, - primary_action_label: __('Print'), - }); - this.print_dialog = print_dialog; - } - - get_upper_section_html(doc) { - const { status } = doc; - let indicator_color = ''; - - in_list(['Paid', 'Consolidated'], status) && (indicator_color = 'green'); - status === 'Draft' && (indicator_color = 'red'); - status === 'Return' && (indicator_color = 'grey'); - - return `<div class="left-section"> + </section>`, + ); + + this.$component = this.wrapper.find(".past-order-summary"); + this.$summary_wrapper = this.$component.find(".invoice-summary-wrapper"); + this.$summary_container = this.$component.find(".abs-container"); + this.$upper_section = this.$summary_container.find(".upper-section"); + this.$items_container = this.$summary_container.find(".items-container"); + this.$totals_container = this.$summary_container.find(".totals-container"); + this.$payment_container = this.$summary_container.find( + ".payments-container", + ); + this.$summary_btns = this.$summary_container.find(".summary-btns"); + } + + init_email_print_dialog() { + const email_dialog = new frappe.ui.Dialog({ + title: "Email Receipt", + fields: [ + { + fieldname: "email_id", + fieldtype: "Data", + options: "Email", + label: "Email ID", + reqd: 1, + }, + { + fieldname: "content", + fieldtype: "Small Text", + label: "Message (if any)", + }, + ], + primary_action: () => { + this.send_email(); + }, + primary_action_label: __("Send"), + }); + this.email_dialog = email_dialog; + + const print_dialog = new frappe.ui.Dialog({ + title: "Print Receipt", + fields: [ + { fieldname: "print", fieldtype: "Data", label: "Print Preview" }, + ], + primary_action: () => { + this.print_receipt(); + }, + primary_action_label: __("Print"), + }); + this.print_dialog = print_dialog; + } + + get_upper_section_html(doc) { + const { status } = doc; + let indicator_color = ""; + + ["Paid", "Consolidated"].includes(status) && (indicator_color = "green"); + status === "Draft" && (indicator_color = "red"); + status === "Return" && (indicator_color = "grey"); + + return `<div class="left-section"> <div class="customer-name">${doc.customer}</div> <div class="customer-email">${this.customer_email}</div> - <div class="cashier">${__('Sold by')}: ${doc.owner}</div> + <div class="cashier">${__("Sold by")}: ${doc.owner}</div> </div> <div class="right-section"> <div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div> <div class="invoice-name">${doc.name}</div> - <span class="indicator-pill whitespace-nowrap ${indicator_color}"><span>${doc.status}</span></span> + <span class="indicator-pill whitespace-nowrap ${indicator_color}"><span>${ + doc.status + }</span></span> </div>`; - } + } - get_item_html(doc, item_data) { - return `<div class="item-row-wrapper"> + get_item_html(doc, item_data) { + return `<div class="item-row-wrapper"> <div class="item-name">${item_data.item_name}</div> <div class="item-qty">${item_data.qty || 0} ${item_data.uom}</div> <div class="item-rate-disc">${get_rate_discount_html()}</div> </div>`; - function get_rate_discount_html() { - if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) { - return `<span class="item-disc">(${item_data.discount_percentage}% off)</span> + function get_rate_discount_html() { + if ( + item_data.rate && + item_data.price_list_rate && + item_data.rate !== item_data.price_list_rate + ) { + return `<span class="item-disc">(${ + item_data.discount_percentage + }% off)</span> <div class="item-rate">${format_currency(item_data.rate, doc.currency)}</div>`; - } else { - return `<div class="item-rate">${format_currency(item_data.price_list_rate || item_data.rate, doc.currency)}</div>`; - } - } - } - - get_discount_html(doc) { - if (doc.discount_amount) { - return `<div class="summary-row-wrapper"> + } else { + return `<div class="item-rate">${format_currency( + item_data.price_list_rate || item_data.rate, + doc.currency, + )}</div>`; + } + } + } + + get_discount_html(doc) { + if (doc.discount_amount) { + return `<div class="summary-row-wrapper"> <div>Discount (${doc.additional_discount_percentage} %)</div> <div>${format_currency(doc.discount_amount, doc.currency)}</div> </div>`; - } else { - return ``; - } - } - - get_net_total_html(doc) { - return `<div class="summary-row-wrapper"> - <div>${__('Net Total')}</div> + } else { + return ``; + } + } + + get_net_total_html(doc) { + return `<div class="summary-row-wrapper"> + <div>${__("Net Total")}</div> <div>${format_currency(doc.net_total, doc.currency)}</div> </div>`; - } - - get_taxes_html(doc) { - if (!doc.taxes.length) return ''; - - let taxes_html = doc.taxes.map(t => { - // if tax rate is 0, don't print it. - const description = /[0-9]+/.test(t.description) ? t.description : ((t.rate != 0) ? `${t.description} @ ${t.rate}%`: t.description); - return ` + } + + get_taxes_html(doc) { + if (!doc.taxes.length) return ""; + + let taxes_html = doc.taxes + .map((t) => { + // if tax rate is 0, don't print it. + const description = /[0-9]+/.test(t.description) + ? t.description + : t.rate != 0 + ? `${t.description} @ ${t.rate}%` + : t.description; + return ` <div class="tax-row"> <div class="tax-label">${description}</div> - <div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, doc.currency)}</div> + <div class="tax-value">${format_currency( + t.tax_amount_after_discount_amount, + doc.currency, + )}</div> </div> `; - }).join(''); + }) + .join(""); - return `<div class="taxes-wrapper">${taxes_html}</div>`; - } + return `<div class="taxes-wrapper">${taxes_html}</div>`; + } - get_grand_total_html(doc) { - return `<div class="summary-row-wrapper grand-total"> - <div>${__('Grand Total')}</div> + get_grand_total_html(doc) { + return `<div class="summary-row-wrapper grand-total"> + <div>${__("Grand Total")}</div> <div>${format_currency(doc.grand_total, doc.currency)}</div> </div>`; - } + } - get_payment_html(doc, payment) { - return `<div class="summary-row-wrapper payments"> + get_payment_html(doc, payment) { + return `<div class="summary-row-wrapper payments"> <div>${__(payment.mode_of_payment)}</div> <div>${format_currency(payment.amount, doc.currency)}</div> </div>`; - } - - bind_events() { - this.$summary_container.on('click', '.return-btn', () => { - this.events.process_return(this.doc.name); - this.toggle_component(false); - this.$component.find('.no-summary-placeholder').css('display', 'flex'); - this.$summary_wrapper.css('display', 'none'); + } + + bind_events() { + this.$summary_container.on("click", ".return-btn", () => { + this.events.process_return(this.doc.name); + this.toggle_component(false); + this.$component.find(".no-summary-placeholder").css("display", "flex"); + this.$summary_wrapper.css("display", "none"); + }); + + this.$summary_container.on("click", ".edit-btn", () => { + this.events.edit_order(this.doc.name); + this.toggle_component(false); + this.$component.find(".no-summary-placeholder").css("display", "flex"); + this.$summary_wrapper.css("display", "none"); + }); + + this.$summary_container.on("click", ".delete-btn", () => { + this.events.delete_order(this.doc.name); + this.show_summary_placeholder(); + }); + + this.$summary_container.on("click", ".send-btn", () => { + if (!this.pos_profile.custom_notification_message_whatsapp) { + frappe.show_alert({ + message: __("WhatsApp notification is not enabled in POS Profile"), + indicator: "orange", }); - - this.$summary_container.on('click', '.edit-btn', () => { - this.events.edit_order(this.doc.name); - this.toggle_component(false); - this.$component.find('.no-summary-placeholder').css('display', 'flex'); - this.$summary_wrapper.css('display', 'none'); - }); - - this.$summary_container.on('click', '.delete-btn', () => { - this.events.delete_order(this.doc.name); - this.show_summary_placeholder(); + return; + } + + if (!this.doc.customer) { + frappe.throw(__("Please select a customer first")); + return; + } + + frappe.db + .get_value("Customer", this.doc.customer, "mobile_no") + .then(({ message }) => { + if (message.mobile_no) { + const mobile_no = message.mobile_no.replace(/[^0-9]/g, ""); + const whatsapp_message = "https://wa.me/" + mobile_no + "?text="; + + // Get the print URL directly + const print_url = frappe.urllib.get_full_url( + "/printview?doctype=" + + encodeURIComponent(this.doc.doctype) + + "&name=" + + encodeURIComponent(this.doc.name) + + "&format=" + + encodeURIComponent(this.pos_profile.print_format) + + "&no_letterhead=0" + + "&_lang=" + + encodeURIComponent(frappe.boot.lang) + + "&trigger_print=1", + ); + + const final_message = + whatsapp_message + + encodeURIComponent( + "Please find your invoice here \n" + print_url, + ); + window.open(final_message); + } else { + var field_values = this.pos_profile.custom_whatsapp_field_names.map( + (x) => this.doc[x.field_name], + ); + + var message_body = formatString( + this.pos_profile.custom_whatsapp_message, + field_values, + ); + + const print_url = frappe.urllib.get_full_url( + "/printview?doctype=" + + encodeURIComponent(this.doc.doctype) + + "&name=" + + encodeURIComponent(this.doc.name) + + "&format=" + + encodeURIComponent(this.pos_profile.print_format) + + "&no_letterhead=0" + + "&_lang=" + + encodeURIComponent(frappe.boot.lang) + + "&trigger_print=1", + ); + + message_body += "\n\nPlease find your invoice here:\n" + print_url; + + var encoded_message = encodeURIComponent(message_body); + + var phone_number = this.doc.customer; + + var whatsapp_url = + "https://wa.me/" + phone_number + "?text=" + encoded_message; + + window.open(whatsapp_url, "_blank"); + } }); + }); - this.$summary_container.on('click', '.send-btn', () => { - if (!this.pos_profile.custom_notification_message_whatsapp) { - frappe.show_alert({ - message: __('WhatsApp notification is not enabled in POS Profile'), - indicator: 'orange' - }); - return; - } - - if (!this.doc.customer) { - frappe.throw(__('Please select a customer first')); - return; - } - - frappe.db.get_value('Customer', this.doc.customer, 'mobile_no') - .then(({ message }) => { - if (message.mobile_no) { - const mobile_no = message.mobile_no.replace(/[^0-9]/g, ''); - const whatsapp_message = "https://wa.me/" + mobile_no + "?text="; - - // Get the print URL directly - const print_url = frappe.urllib.get_full_url( - '/printview?doctype=' + encodeURIComponent(this.doc.doctype) + - '&name=' + encodeURIComponent(this.doc.name) + - '&format=' + encodeURIComponent(this.pos_profile.print_format) + - '&no_letterhead=0' + - '&_lang=' + encodeURIComponent(frappe.boot.lang) + - '&trigger_print=1' - ); - - const final_message = whatsapp_message + - encodeURIComponent("Please find your invoice here \n" + print_url); - window.open(final_message); - } else { - var field_values = this.pos_profile.custom_whatsapp_field_names.map(x => this.doc[x.field_name]); - - var message_body = formatString(this.pos_profile.custom_whatsapp_message, field_values); - - const print_url = frappe.urllib.get_full_url( - '/printview?doctype=' + encodeURIComponent(this.doc.doctype) + - '&name=' + encodeURIComponent(this.doc.name) + - '&format=' + encodeURIComponent(this.pos_profile.print_format) + - '&no_letterhead=0' + - '&_lang=' + encodeURIComponent(frappe.boot.lang) + - '&trigger_print=1' - ); - - message_body += "\n\nPlease find your invoice here:\n" + print_url; - - var encoded_message = encodeURIComponent(message_body); - - var phone_number = this.doc.customer; - - var whatsapp_url = "https://wa.me/" + phone_number + "?text=" + encoded_message; - - window.open(whatsapp_url, '_blank'); - } - }); - }); + function formatString(str, args) { + return str.replace(/{(\d+)}/g, function (match, number) { + return typeof args[number] !== "undefined" ? args[number] : match; + }); + } - function formatString(str, args) { - return str.replace(/{(\d+)}/g, function(match, number) { - return typeof args[number] !== 'undefined' - ? args[number] - : match; + this.$summary_container.on("click", ".new-btn", () => { + this.events.new_order(); + this.toggle_component(false); + this.$component.find(".no-summary-placeholder").css("display", "flex"); + this.$summary_wrapper.css("display", "none"); + }); + + this.$summary_container.on("click", ".email-btn", () => { + this.email_dialog.fields_dict.email_id.set_value(this.customer_email); + this.email_dialog.show(); + }); + + this.$summary_container.on("click", ".print-btn", () => { + this.print_receipt(); + }); + } + + print_receipt() { + const frm = this.events.get_frm(); + frappe.utils.print( + this.doc.doctype, + this.doc.name, + frm.pos_print_format, + this.doc.letter_head, + this.doc.language || frappe.boot.lang, + ); + } + + attach_shortcuts() { + const ctrl_label = frappe.utils.is_mac() ? "⌘" : "Ctrl"; + this.$summary_container.find(".print-btn").attr("title", `${ctrl_label}+P`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+p", + action: () => this.$summary_container.find(".print-btn").click(), + condition: () => + this.$component.is(":visible") && + this.$summary_container.find(".print-btn").is(":visible"), + description: __("Print Receipt"), + page: cur_page.page.page, + }); + this.$summary_container + .find(".new-btn") + .attr("title", `${ctrl_label}+Enter`); + frappe.ui.keys.on("ctrl+enter", () => { + const summary_is_visible = this.$component.is(":visible"); + if ( + summary_is_visible && + this.$summary_container.find(".new-btn").is(":visible") + ) { + this.$summary_container.find(".new-btn").click(); + } + }); + this.$summary_container.find(".edit-btn").attr("title", `${ctrl_label}+E`); + frappe.ui.keys.add_shortcut({ + shortcut: "ctrl+e", + action: () => this.$summary_container.find(".edit-btn").click(), + condition: () => + this.$component.is(":visible") && + this.$summary_container.find(".edit-btn").is(":visible"), + description: __("Edit Receipt"), + page: cur_page.page.page, + }); + } + + send_email() { + const frm = this.events.get_frm(); + const recipients = this.email_dialog.get_values().email_id; + const content = this.email_dialog.get_values().content; + const doc = this.doc || frm.doc; + const print_format = frm.pos_print_format; + + frappe.call({ + method: "frappe.core.doctype.communication.email.make", + args: { + recipients: recipients, + subject: __(frm.meta.name) + ": " + doc.name, + content: content ? content : __(frm.meta.name) + ": " + doc.name, + doctype: doc.doctype, + name: doc.name, + send_email: 1, + print_format, + sender_full_name: frappe.user.full_name(), + _lang: doc.language, + }, + callback: (r) => { + if (!r.exc) { + frappe.utils.play_sound("email"); + if (r.message["emails_not_sent_to"]) { + frappe.msgprint( + __("Email not sent to {0} (unsubscribed / disabled)", [ + frappe.utils.escape_html(r.message["emails_not_sent_to"]), + ]), + ); + } else { + frappe.show_alert({ + message: __("Email sent successfully."), + indicator: "green", }); + } + this.email_dialog.hide(); + } else { + frappe.msgprint( + __("There were errors while sending email. Please try again."), + ); } - - this.$summary_container.on('click', '.new-btn', () => { - this.events.new_order(); - this.toggle_component(false); - this.$component.find('.no-summary-placeholder').css('display', 'flex'); - this.$summary_wrapper.css('display', 'none'); - }); - - this.$summary_container.on('click', '.email-btn', () => { - this.email_dialog.fields_dict.email_id.set_value(this.customer_email); - this.email_dialog.show(); - }); - - this.$summary_container.on('click', '.print-btn', () => { - this.print_receipt(); + }, + }); + } + + add_summary_btns(map) { + this.$summary_btns.html(""); + map.forEach((m) => { + if (m.condition) { + m.visible_btns.forEach((b) => { + const class_name = b.split(" ")[0].toLowerCase(); + const btn = __(b); + this.$summary_btns.append( + `<div class="summary-btn btn btn-default ${class_name}-btn">${btn}</div>`, + ); }); + } + }); + this.$summary_btns.children().last().removeClass("mr-4"); + } + + toggle_summary_placeholder(show) { + if (show) { + this.$summary_wrapper.css("display", "none"); + this.$component.find(".no-summary-placeholder").css("display", "flex"); + } else { + this.$summary_wrapper.css("display", "flex"); + this.$component.find(".no-summary-placeholder").css("display", "none"); } - - print_receipt() { - const frm = this.events.get_frm(); - frappe.utils.print( - this.doc.doctype, - this.doc.name, - frm.pos_print_format, - this.doc.letter_head, - this.doc.language || frappe.boot.lang - ); - } - - attach_shortcuts() { - const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; - this.$summary_container.find('.print-btn').attr("title", `${ctrl_label}+P`); - frappe.ui.keys.add_shortcut({ - shortcut: "ctrl+p", - action: () => this.$summary_container.find('.print-btn').click(), - condition: () => this.$component.is(':visible') && this.$summary_container.find('.print-btn').is(":visible"), - description: __("Print Receipt"), - page: cur_page.page.page - }); - this.$summary_container.find('.new-btn').attr("title", `${ctrl_label}+Enter`); - frappe.ui.keys.on("ctrl+enter", () => { - const summary_is_visible = this.$component.is(":visible"); - if (summary_is_visible && this.$summary_container.find('.new-btn').is(":visible")) { - this.$summary_container.find('.new-btn').click(); - } - }); - this.$summary_container.find('.edit-btn').attr("title", `${ctrl_label}+E`); - frappe.ui.keys.add_shortcut({ - shortcut: "ctrl+e", - action: () => this.$summary_container.find('.edit-btn').click(), - condition: () => this.$component.is(':visible') && this.$summary_container.find('.edit-btn').is(":visible"), - description: __("Edit Receipt"), - page: cur_page.page.page - }); - } - - send_email() { - const frm = this.events.get_frm(); - const recipients = this.email_dialog.get_values().email_id; - const content = this.email_dialog.get_values().content; - const doc = this.doc || frm.doc; - const print_format = frm.pos_print_format; - - frappe.call({ - method: "frappe.core.doctype.communication.email.make", - args: { - recipients: recipients, - subject: __(frm.meta.name) + ': ' + doc.name, - content: content ? content : __(frm.meta.name) + ': ' + doc.name, - doctype: doc.doctype, - name: doc.name, - send_email: 1, - print_format, - sender_full_name: frappe.user.full_name(), - _lang: doc.language - }, - callback: r => { - if (!r.exc) { - frappe.utils.play_sound("email"); - if (r.message["emails_not_sent_to"]) { - frappe.msgprint(__( - "Email not sent to {0} (unsubscribed / disabled)", - [ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ] - )); - } else { - frappe.show_alert({ - message: __('Email sent successfully.'), - indicator: 'green' - }); - } - this.email_dialog.hide(); - } else { - frappe.msgprint(__("There were errors while sending email. Please try again.")); - } - } - }); - } - - add_summary_btns(map) { - this.$summary_btns.html(''); - map.forEach(m => { - if (m.condition) { - m.visible_btns.forEach(b => { - const class_name = b.split(' ')[0].toLowerCase(); - const btn = __(b); - this.$summary_btns.append( - `<div class="summary-btn btn btn-default ${class_name}-btn">${btn}</div>` - ); - }); - } - }); - this.$summary_btns.children().last().removeClass('mr-4'); - } - - toggle_summary_placeholder(show) { - if (show) { - this.$summary_wrapper.css('display', 'none'); - this.$component.find('.no-summary-placeholder').css('display', 'flex'); - } else { - this.$summary_wrapper.css('display', 'flex'); - this.$component.find('.no-summary-placeholder').css('display', 'none'); - } - } - - get_condition_btn_map(after_submission) { - if (after_submission) - return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt','Send Whatsapp', 'New Order'] }]; - - return [ - { condition: this.doc.docstatus === 0, visible_btns: ['Print Receipt','Edit Order', 'Delete Order','Send Whatsapp'] }, - { condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return','Send Whatsapp']}, - { condition: this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt','Send Whatsapp']} - ]; - } - - load_summary_of(doc, after_submission=false) { - after_submission ? - this.$component.css('grid-column', 'span 10 / span 10') : - this.$component.css('grid-column', 'span 6 / span 6'); - - this.toggle_summary_placeholder(false); - - this.doc = doc; - - this.attach_document_info(doc); - - this.attach_items_info(doc); - - this.attach_totals_info(doc); - - this.attach_payments_info(doc); - - const condition_btns_map = this.get_condition_btn_map(after_submission); - - this.add_summary_btns(condition_btns_map); - this.$summary_wrapper.css("width",after_submission ? "35%" : "60%"); - - if (after_submission) { - this.print_receipt_on_order_complete(); - } - } - - attach_document_info(doc) { - frappe.db.get_value('Customer', this.doc.customer, 'email_id').then(({ message }) => { - this.customer_email = message.email_id || ''; - const upper_section_dom = this.get_upper_section_html(doc); - this.$upper_section.html(upper_section_dom); - }); - } - - attach_items_info(doc) { - this.$items_container.html(''); - doc.items.forEach(item => { - const item_dom = this.get_item_html(doc, item); - this.$items_container.append(item_dom); - this.set_dynamic_rate_header_width(); - }); - } - - set_dynamic_rate_header_width() { - const rate_cols = Array.from(this.$items_container.find(".item-rate-disc")); - this.$items_container.find(".item-rate-disc").css("width", ""); - let max_width = rate_cols.reduce((max_width, elm) => { - if ($(elm).width() > max_width) - max_width = $(elm).width(); - return max_width; - }, 0); - - max_width += 1; - if (max_width == 1) max_width = ""; - - this.$items_container.find(".item-rate-disc").css("width", max_width); - } - - attach_payments_info(doc) { - this.$payment_container.html(''); - doc.payments.forEach(p => { - if (p.amount) { - const payment_dom = this.get_payment_html(doc, p); - this.$payment_container.append(payment_dom); - } - }); - if (doc.redeem_loyalty_points && doc.loyalty_amount) { - const payment_dom = this.get_payment_html(doc, { - mode_of_payment: 'Loyalty Points', - amount: doc.loyalty_amount, - }); - this.$payment_container.append(payment_dom); - } - } - - attach_totals_info(doc) { - this.$totals_container.html(''); - - const net_total_dom = this.get_net_total_html(doc); - const taxes_dom = this.get_taxes_html(doc); - const discount_dom = this.get_discount_html(doc); - const grand_total_dom = this.get_grand_total_html(doc); - this.$totals_container.append(net_total_dom); - this.$totals_container.append(taxes_dom); - this.$totals_container.append(discount_dom); - this.$totals_container.append(grand_total_dom); - } - - toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); - - } - - async print_receipt_on_order_complete() { - - const profile_name = this.pos_profile?.name || this.pos_profile; - - const { message } = await frappe.db.get_value( - "POS Profile", - profile_name, - ["print_receipt_on_order_complete", "print_format"] - ); - - if (message?.print_receipt_on_order_complete) { - setTimeout(() => this.print_receipt(), 300); - } - -} - - + } + + get_condition_btn_map(after_submission) { + if (after_submission) + return [ + { + condition: true, + visible_btns: [ + "Print Receipt", + "Email Receipt", + "Send Whatsapp", + "New Order", + ], + }, + ]; + + return [ + { + condition: this.doc.docstatus === 0, + visible_btns: [ + "Print Receipt", + "Edit Order", + "Delete Order", + "Send Whatsapp", + ], + }, + { + condition: !this.doc.is_return && this.doc.docstatus === 1, + visible_btns: [ + "Print Receipt", + "Email Receipt", + "Return", + "Send Whatsapp", + ], + }, + { + condition: this.doc.is_return && this.doc.docstatus === 1, + visible_btns: ["Print Receipt", "Email Receipt", "Send Whatsapp"], + }, + ]; + } + + load_summary_of(doc, after_submission = false) { + after_submission + ? this.$component.css("grid-column", "span 10 / span 10") + : this.$component.css("grid-column", "span 6 / span 6"); + + this.toggle_summary_placeholder(false); + + this.doc = doc; + + this.attach_document_info(doc); + + this.attach_items_info(doc); + + this.attach_totals_info(doc); + + this.attach_payments_info(doc); + + const condition_btns_map = this.get_condition_btn_map(after_submission); + + this.add_summary_btns(condition_btns_map); + this.$summary_wrapper.css("width", after_submission ? "35%" : "60%"); + + if (after_submission) { + this.print_receipt_on_order_complete(); + } + } + + attach_document_info(doc) { + frappe.db + .get_value("Customer", this.doc.customer, "email_id") + .then(({ message }) => { + this.customer_email = message.email_id || ""; + const upper_section_dom = this.get_upper_section_html(doc); + this.$upper_section.html(upper_section_dom); + }); + } + + attach_items_info(doc) { + this.$items_container.html(""); + doc.items.forEach((item) => { + const item_dom = this.get_item_html(doc, item); + this.$items_container.append(item_dom); + this.set_dynamic_rate_header_width(); + }); + } + + set_dynamic_rate_header_width() { + const rate_cols = Array.from(this.$items_container.find(".item-rate-disc")); + this.$items_container.find(".item-rate-disc").css("width", ""); + let max_width = rate_cols.reduce((max_width, elm) => { + if ($(elm).width() > max_width) max_width = $(elm).width(); + return max_width; + }, 0); + + max_width += 1; + if (max_width == 1) max_width = ""; + + this.$items_container.find(".item-rate-disc").css("width", max_width); + } + + attach_payments_info(doc) { + this.$payment_container.html(""); + doc.payments.forEach((p) => { + if (p.amount) { + const payment_dom = this.get_payment_html(doc, p); + this.$payment_container.append(payment_dom); + } + }); + if (doc.redeem_loyalty_points && doc.loyalty_amount) { + const payment_dom = this.get_payment_html(doc, { + mode_of_payment: "Loyalty Points", + amount: doc.loyalty_amount, + }); + this.$payment_container.append(payment_dom); + } + } + + attach_totals_info(doc) { + this.$totals_container.html(""); + + const net_total_dom = this.get_net_total_html(doc); + const taxes_dom = this.get_taxes_html(doc); + const discount_dom = this.get_discount_html(doc); + const grand_total_dom = this.get_grand_total_html(doc); + this.$totals_container.append(net_total_dom); + this.$totals_container.append(taxes_dom); + this.$totals_container.append(discount_dom); + this.$totals_container.append(grand_total_dom); + } + + toggle_component(show) { + show + ? this.$component.css("display", "flex") + : this.$component.css("display", "none"); + } + + async print_receipt_on_order_complete() { + const profile_name = this.pos_profile?.name || this.pos_profile; + + const { message } = await frappe.db.get_value("POS Profile", profile_name, [ + "print_receipt_on_order_complete", + "print_format", + ]); + + if (message?.print_receipt_on_order_complete) { + setTimeout(() => this.print_receipt(), 300); + } + } }; diff --git a/posnext/public/js/pos_payment.js b/posnext/public/js/pos_payment.js index 34435d5..c883d1e 100644 --- a/posnext/public/js/pos_payment.js +++ b/posnext/public/js/pos_payment.js @@ -1,40 +1,39 @@ /* eslint-disable no-unused-vars */ -frappe.provide('posnext.PointOfSale'); +frappe.provide("posnext.PointOfSale"); posnext.PointOfSale.Payment = class { - constructor({ events, wrapper, settings }) { - this.wrapper = wrapper; - this.events = events; - this.custom_show_sales_man = settings.custom_show_sales_man - this.custom_show_additional_note = settings.custom_show_additional_note - this.custom_edit_rate = settings.custom_edit_rate_and_uom - this.custom_show_credit_sales = settings.custom_show_credit_sales - this.default_payment = settings.default_payment - this.current_payments = [] - this.enable_coupon_code = settings.enable_coupon_code - - this.init_component(); - // this.init_component(); - if (this.enable_coupon_code){ - this.render_coupon_code_field(); - } - } - - init_component() { - this.prepare_dom(); - this.initialize_numpad(); - this.bind_events(); - this.attach_shortcuts(); - - } - - prepare_dom() { - this.wrapper.append( - `<section class="payment-container" style="grid-column: span 5 / span 5;"> - <div class="section-label payment-section">${__('Payment Method')}</div> + constructor({ events, wrapper, settings }) { + this.wrapper = wrapper; + this.events = events; + this.custom_show_sales_man = settings.custom_show_sales_man; + this.custom_show_additional_note = settings.custom_show_additional_note; + this.custom_edit_rate = settings.custom_edit_rate_and_uom; + this.custom_show_credit_sales = settings.custom_show_credit_sales; + this.default_payment = settings.default_payment; + this.current_payments = []; + this.enable_coupon_code = settings.enable_coupon_code; + + this.init_component(); + // this.init_component(); + if (this.enable_coupon_code) { + this.render_coupon_code_field(); + } + } + + init_component() { + this.prepare_dom(); + this.initialize_numpad(); + this.bind_events(); + this.attach_shortcuts(); + } + + prepare_dom() { + this.wrapper.append( + `<section class="payment-container" style="grid-column: span 5 / span 5;"> + <div class="section-label payment-section">${__("Payment Method")}</div> <div class="payment-modes"></div> <div class="fields-numpad-container"> <div class="fields-section"> - <div class="section-label">${__('Additional Information')}</div> + <div class="section-label">${__("Additional Information")}</div> <div class="coupon-code"></div> <!-- βœ… Correct class here --> <div class="invoice-fields"></div> </div> @@ -44,442 +43,503 @@ posnext.PointOfSale.Payment = class { <div class="totals"></div> </div> <div class="submit-order-btn">${__("Complete Order")}</div> - </section>` - ); - - // βœ… Assign to the right class - this.$component = this.wrapper.find('.payment-container'); - this.$payment_modes = this.$component.find('.payment-modes'); - this.$totals_section = this.$component.find('.totals-section'); - this.$totals = this.$component.find('.totals'); - this.$numpad = this.$component.find('.number-pad'); - this.$coupon_code = this.$component.find('.coupon-code'); - this.$invoice_fields_section = this.$component.find('.fields-section'); - } - - render_coupon_code_field() { - frappe.ui.form.make_control({ - df: { - label: __('Coupon Code'), - fieldtype: 'Link', - options: 'Coupon Code', - fieldname: 'coupon_code', - placeholder: __('Select a coupon'), - }, - parent: this.$component.find('.coupon-code'), - render_input: true - }); - } - - - make_invoice_fields_control() { - // frappe.db.get_doc("POS Settings", undefined).then((doc) => { - var me = this - const fields = []; - if(this.custom_show_credit_sales){ - fields.push({ - fieldname: "custom_credit_sales", - label: "Credit Sales", - fieldtype: "Check", - }) - // fields.push({ - // fieldname: "custom_credit_sales_date", - // label: "Credit Sales Date", - // fieldtype: "Date" - // }) - } - if(this.custom_show_sales_man){ - fields.push({ - fieldname: "sales_person", - label: "Sales Man", - fieldtype: "Link", - options: "Sales Person", - }) - } - if(this.custom_show_additional_note){ - fields.push({ - fieldname: "remarks", - label: "Additional Note", - fieldtype: "Small Text", - }) - } - - if (!fields.length) return; - this.$invoice_fields = this.$invoice_fields_section.find('.invoice-fields'); - this.$invoice_fields.html(''); - const frm = this.events.get_frm(); - me.current_payments = frm.doc.payments - fields.forEach(df => { - this.$invoice_fields.append( - `<div class="invoice_detail_field ${df.fieldname}-field" data-fieldname="${df.fieldname}"></div>` - ); - let df_events = { - onchange: function() { - if(this.df.fieldname === 'sales_person'){ - frm.clear_table("sales_team") - cur_frm.add_child("sales_team", { - sales_person: this.get_value(), - allocated_percentage: 100, - }) - } else { - if(this.df.fieldname === 'custom_credit_sales'){ - // $('input[data-fieldname="custom_credit_sales_date"]').css("pointer-events",this.get_value() ? "" : "none") - if(this.get_value()){ - // $('input[data-fieldname="custom_credit_sales_date"]').removeAttr('readonly') - - frm.doc.payments.forEach(p => { - const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); - me[`${mode}_control`].set_value(0); - }) - } else { - console.log(me.current_payments) - // $('input[data-fieldname="custom_credit_sales_date"]').attr('readonly', true); - me.current_payments.forEach(p => { - if(p.mode_of_payment === me.default_payment){ - const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); - me[`${mode}_control`].set_value(frm.doc.grand_total); - } - - }) - } - } - frm.set_value(this.df.fieldname, this.get_value()); - } - // if(this.df.fieldname === 'custom_credit_sales' && this.get_value()){ - // console.log("SELECTEEED MODE") - // console.log(me.$payment_modes) - // this.selected_mode.set_value(0); - // } - - } - }; - if (df.fieldtype == "Button") { - df_events = { - click: function() { - if (frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype)) { - frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname); - } - } - }; - } - - this[`${df.fieldname}_field`] = frappe.ui.form.make_control({ - df: { - ...df, - ...df_events - }, - parent: this.$invoice_fields.find(`.${df.fieldname}-field`), - render_input: true, - }); - if(df.fieldname !== 'remarks'){ - this[`${df.fieldname}_field`].set_value(frm.doc[df.fieldname]); - } - // if(df.fieldname === 'custom_credit_sales_date'){ - // this[`${df.fieldname}_field`].set_value(frappe.datetime.get_today()); - // } - }); - // }); - } - - initialize_numpad() { - const me = this; - this.number_pad = new posnext.PointOfSale.NumberPad({ - wrapper: this.$numpad, - events: { - numpad_event: function($btn) { - me.on_numpad_clicked($btn); - } - }, - cols: 3, - keys: [ - [ 1, 2, 3 ], - [ 4, 5, 6 ], - [ 7, 8, 9 ], - [ '.', 0, 'Delete' ] - ], - }); - - this.numpad_value = ''; - } - - on_numpad_clicked($btn) { - const button_value = $btn.attr('data-button-value'); - - highlight_numpad_btn($btn); - this.numpad_value = button_value === 'delete' ? this.numpad_value.slice(0, -1) : this.numpad_value + button_value; - this.selected_mode.$input.get(0).focus(); - this.selected_mode.set_value(this.numpad_value); - - function highlight_numpad_btn($btn) { - $btn.addClass('shadow-base-inner bg-selected'); - setTimeout(() => { - $btn.removeClass('shadow-base-inner bg-selected'); - }, 100); - } - } - - bind_events() { - const me = this; - - this.$payment_modes.on('click', '.mode-of-payment', function(e) { - const mode_clicked = $(this); - // if clicked element doesn't have .mode-of-payment class then return - if (!$(e.target).is(mode_clicked)) return; - - const scrollLeft = mode_clicked.offset().left - me.$payment_modes.offset().left + me.$payment_modes.scrollLeft(); - me.$payment_modes.animate({ scrollLeft }); - - const mode = mode_clicked.attr('data-mode'); - - // hide all control fields and shortcuts - $(`.mode-of-payment-control`).css('display', 'none'); - $(`.cash-shortcuts`).css('display', 'none'); - me.$payment_modes.find(`.pay-amount`).css('display', 'inline'); - me.$payment_modes.find(`.loyalty-amount-name`).css('display', 'none'); - - // remove highlight from all mode-of-payments - $('.mode-of-payment').removeClass('border-primary'); - - if (mode_clicked.hasClass('border-primary')) { - // clicked one is selected then unselect it - mode_clicked.removeClass('border-primary'); - me.selected_mode = ''; - } else { - // clicked one is not selected then select it - mode_clicked.addClass('border-primary'); - mode_clicked.find('.mode-of-payment-control').css('display', 'flex'); - mode_clicked.find('.cash-shortcuts').css('display', 'grid'); - me.$payment_modes.find(`.${mode}-amount`).css('display', 'none'); - me.$payment_modes.find(`.${mode}-name`).css('display', 'inline'); - - me.selected_mode = me[`${mode}_control`]; - me.selected_mode && me.selected_mode.$input.get(0).focus(); - me.auto_set_remaining_amount(); - } - }); - - frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => { - const contact = frm.doc.contact_mobile; - const request_button = $(this.request_for_payment_field?.$input[0]); - if (contact) { - request_button.removeClass('btn-default').addClass('btn-primary'); - } else { - request_button.removeClass('btn-primary').addClass('btn-default'); - } - }); - - frappe.ui.form.on('POS Invoice', 'coupon_code', (frm) => { - if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { - if (!frm.doc.ignore_pricing_rule) { - frm.applying_pos_coupon_code = true; - frappe.run_serially([ - () => frm.doc.ignore_pricing_rule=1, - () => frm.trigger('ignore_pricing_rule'), - () => frm.doc.ignore_pricing_rule=0, - () => frm.trigger('apply_pricing_rule'), - () => frm.save(), - () => this.update_totals_section(frm.doc), - () => (frm.applying_pos_coupon_code = false) - ]); - } else if (frm.doc.ignore_pricing_rule) { - frappe.show_alert({ - message: __("Ignore Pricing Rule is enabled. Cannot apply coupon code."), - indicator: "orange" - }); - } - } - }); - - this.setup_listener_for_payments(); - - this.$payment_modes.on('click', '.shortcut', function() { - const value = $(this).attr('data-value'); - me.selected_mode.set_value(value); - }); - - this.$component.on('click', '.submit-order-btn', () => { - const doc = this.events.get_frm().doc; - let paid_amount = doc.paid_amount - if(cur_frm.doc.custom_credit_sales && this.custom_show_credit_sales){ - cur_frm.clear_table("payments") - paid_amount = 0; - } - - const items = doc.items; - - if ((paid_amount == 0 || !items.length) && !this.custom_show_credit_sales) { - const message = items.length ? __("You cannot submit the order without payment.") : __("You cannot submit empty order."); - frappe.show_alert({ message, indicator: "orange" }); - frappe.utils.play_sound("error"); - return; - } - - this.events.submit_invoice(); - }); - - frappe.ui.form.on('POS Invoice', 'paid_amount', (frm) => { - this.update_totals_section(frm.doc); - - // need to re calculate cash shortcuts after discount is applied - const is_cash_shortcuts_invisible = !this.$payment_modes.find('.cash-shortcuts').is(':visible'); - this.attach_cash_shortcuts(frm.doc); - !is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').css('display', 'grid'); - this.render_payment_mode_dom(); - }); - - frappe.ui.form.on('POS Invoice', 'loyalty_amount', (frm) => { - const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency); - this.$payment_modes.find(`.loyalty-amount-amount`).html(formatted_currency); - }); - - frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => { - // for setting correct amount after loyalty points are redeemed - const default_mop = locals[cdt][cdn]; - const mode = default_mop.mode_of_payment.replace(/ +/g, "_").toLowerCase(); - if (this[`${mode}_control`] && this[`${mode}_control`].get_value() != default_mop.amount) { - this[`${mode}_control`].set_value(default_mop.amount); - } - }); - } - - setup_listener_for_payments() { - frappe.realtime.on("process_phone_payment", (data) => { - const doc = this.events.get_frm().doc; - const { response, amount, success, failure_message } = data; - let message, title; - - if (success) { - title = __("Payment Received"); - const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; - if (amount >= grand_total) { - frappe.dom.unfreeze(); - message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]); - this.events.submit_invoice(); - cur_frm.reload_doc(); - - } else { - message = __("Payment of {0} received successfully. Waiting for other requests to complete...", [format_currency(amount, doc.currency, 0)]); - } - } else if (failure_message) { - message = failure_message; - title = __("Payment Failed"); - } - - frappe.msgprint({ "message": message, "title": title }); - }); - } - - auto_set_remaining_amount() { - const doc = this.events.get_frm().doc; - const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; - const remaining_amount = grand_total - doc.paid_amount; - const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined; - if (!current_value && remaining_amount > 0 && this.selected_mode) { - this.selected_mode.set_value(remaining_amount); - } - } - - attach_shortcuts() { - const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl'; - this.$component.find('.submit-order-btn').attr("title", `${ctrl_label}+Enter`); - frappe.ui.keys.on("ctrl+enter", () => { - const payment_is_visible = this.$component.is(":visible"); - const active_mode = this.$payment_modes.find(".border-primary"); - if (payment_is_visible && active_mode.length) { - this.$component.find('.submit-order-btn').click(); - } - }); - - frappe.ui.keys.add_shortcut({ - shortcut: "tab", - action: () => { - const payment_is_visible = this.$component.is(":visible"); - let active_mode = this.$payment_modes.find(".border-primary"); - active_mode = active_mode.length ? active_mode.attr("data-mode") : undefined; - - if (!active_mode) return; - - const mode_of_payments = Array.from(this.$payment_modes.find(".mode-of-payment")).map(m => $(m).attr("data-mode")); - const mode_index = mode_of_payments.indexOf(active_mode); - const next_mode_index = (mode_index + 1) % mode_of_payments.length; - const next_mode_to_be_clicked = this.$payment_modes.find(`.mode-of-payment[data-mode="${mode_of_payments[next_mode_index]}"]`); - - if (payment_is_visible && mode_index != next_mode_index) { - next_mode_to_be_clicked.click(); - } - }, - condition: () => this.$component.is(':visible') && this.$payment_modes.find(".border-primary").length, - description: __("Switch Between Payment Modes"), - ignore_inputs: true, - page: cur_page.page.page - }); - } - - toggle_numpad() { - // pass - } - - render_payment_section() { - this.render_payment_mode_dom(); - this.make_invoice_fields_control(); - this.update_totals_section(); - this.focus_on_default_mop(); - } - - after_render() { - const frm = this.events.get_frm(); - frm.script_manager.trigger("after_payment_render", frm.doc.doctype, frm.doc.docname); - } - - edit_cart() { - if(this.custom_edit_rate){ - const div = document.getElementById("customer-cart-container2"); - div.style.gridColumn = "span 5 / span 5"; - } - - this.events.toggle_other_sections(false); - this.toggle_component(false); - } - - checkout() { - this.events.toggle_other_sections(true); - this.toggle_component(true); - - this.render_payment_section(); - this.after_render(); - } - - toggle_remarks_control() { - if (this.$remarks.find('.frappe-control').length) { - this.$remarks.html('+ Add Remark'); - } else { - this.$remarks.html(''); - this[`remark_control`] = frappe.ui.form.make_control({ - df: { - label: __('Remark'), - fieldtype: 'Data', - onchange: function() {} - }, - parent: this.$totals_section.find(`.remarks`), - render_input: true, - }); - this[`remark_control`].set_value(''); - } - } - - render_payment_mode_dom() { - const doc = this.events.get_frm().doc; - const payments = doc.payments; - const currency = doc.currency; - - this.$payment_modes.html(`${ - payments.map((p, i) => { - const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); - const payment_type = p.type; - const margin = i % 2 === 0 ? 'pr-2' : 'pl-2'; - const amount = p.amount > 0 ? format_currency(p.amount, currency) : ''; - - return (` + </section>`, + ); + + // βœ… Assign to the right class + this.$component = this.wrapper.find(".payment-container"); + this.$payment_modes = this.$component.find(".payment-modes"); + this.$totals_section = this.$component.find(".totals-section"); + this.$totals = this.$component.find(".totals"); + this.$numpad = this.$component.find(".number-pad"); + this.$coupon_code = this.$component.find(".coupon-code"); + this.$invoice_fields_section = this.$component.find(".fields-section"); + } + + render_coupon_code_field() { + frappe.ui.form.make_control({ + df: { + label: __("Coupon Code"), + fieldtype: "Link", + options: "Coupon Code", + fieldname: "coupon_code", + placeholder: __("Select a coupon"), + }, + parent: this.$component.find(".coupon-code"), + render_input: true, + }); + } + + make_invoice_fields_control() { + // frappe.db.get_doc("POS Settings", undefined).then((doc) => { + var me = this; + const fields = []; + if (this.custom_show_credit_sales) { + fields.push({ + fieldname: "custom_credit_sales", + label: "Credit Sales", + fieldtype: "Check", + }); + // fields.push({ + // fieldname: "custom_credit_sales_date", + // label: "Credit Sales Date", + // fieldtype: "Date" + // }) + } + if (this.custom_show_sales_man) { + fields.push({ + fieldname: "sales_person", + label: "Sales Man", + fieldtype: "Link", + options: "Sales Person", + }); + } + if (this.custom_show_additional_note) { + fields.push({ + fieldname: "remarks", + label: "Additional Note", + fieldtype: "Small Text", + }); + } + + if (!fields.length) return; + this.$invoice_fields = this.$invoice_fields_section.find(".invoice-fields"); + this.$invoice_fields.html(""); + const frm = this.events.get_frm(); + me.current_payments = frm.doc.payments; + fields.forEach((df) => { + this.$invoice_fields.append( + `<div class="invoice_detail_field ${df.fieldname}-field" data-fieldname="${df.fieldname}"></div>`, + ); + let df_events = { + onchange: function () { + if (this.df.fieldname === "sales_person") { + frm.clear_table("sales_team"); + // prettier-ignore + cur_frm.add_child("sales_team", { // nosemgrep + sales_person: this.get_value(), + allocated_percentage: 100, + }); + } else { + if (this.df.fieldname === "custom_credit_sales") { + // $('input[data-fieldname="custom_credit_sales_date"]').css("pointer-events",this.get_value() ? "" : "none") + if (this.get_value()) { + // $('input[data-fieldname="custom_credit_sales_date"]').removeAttr('readonly') + + frm.doc.payments.forEach((p) => { + const mode = p.mode_of_payment + .replace(/ +/g, "_") + .toLowerCase(); + me[`${mode}_control`].set_value(0); + }); + } else { + console.log(me.current_payments); + // $('input[data-fieldname="custom_credit_sales_date"]').attr('readonly', true); + me.current_payments.forEach((p) => { + if (p.mode_of_payment === me.default_payment) { + const mode = p.mode_of_payment + .replace(/ +/g, "_") + .toLowerCase(); + me[`${mode}_control`].set_value(frm.doc.grand_total); + } + }); + } + } + frm.set_value(this.df.fieldname, this.get_value()); + } + // if(this.df.fieldname === 'custom_credit_sales' && this.get_value()){ + // console.log("SELECTEEED MODE") + // console.log(me.$payment_modes) + // this.selected_mode.set_value(0); + // } + }, + }; + if (df.fieldtype == "Button") { + df_events = { + click: function () { + if ( + frm.script_manager.has_handlers(df.fieldname, frm.doc.doctype) + ) { + frm.script_manager.trigger( + df.fieldname, + frm.doc.doctype, + frm.doc.docname, + ); + } + }, + }; + } + + this[`${df.fieldname}_field`] = frappe.ui.form.make_control({ + df: { + ...df, + ...df_events, + }, + parent: this.$invoice_fields.find(`.${df.fieldname}-field`), + render_input: true, + }); + if (df.fieldname !== "remarks") { + this[`${df.fieldname}_field`].set_value(frm.doc[df.fieldname]); + } + // if(df.fieldname === 'custom_credit_sales_date'){ + // this[`${df.fieldname}_field`].set_value(frappe.datetime.get_today()); + // } + }); + // }); + } + + initialize_numpad() { + const me = this; + this.number_pad = new posnext.PointOfSale.NumberPad({ + wrapper: this.$numpad, + events: { + numpad_event: function ($btn) { + me.on_numpad_clicked($btn); + }, + }, + cols: 3, + keys: [ + [1, 2, 3], + [4, 5, 6], + [7, 8, 9], + [".", 0, "Delete"], + ], + }); + + this.numpad_value = ""; + } + + on_numpad_clicked($btn) { + const button_value = $btn.attr("data-button-value"); + + highlight_numpad_btn($btn); + this.numpad_value = + button_value === "delete" + ? this.numpad_value.slice(0, -1) + : this.numpad_value + button_value; + this.selected_mode.$input.get(0).focus(); + this.selected_mode.set_value(this.numpad_value); + + function highlight_numpad_btn($btn) { + $btn.addClass("shadow-base-inner bg-selected"); + setTimeout(() => { + $btn.removeClass("shadow-base-inner bg-selected"); + }, 100); + } + } + + bind_events() { + const me = this; + + this.$payment_modes.on("click", ".mode-of-payment", function (e) { + const mode_clicked = $(this); + // if clicked element doesn't have .mode-of-payment class then return + if (!$(e.target).is(mode_clicked)) return; + + const scrollLeft = + mode_clicked.offset().left - + me.$payment_modes.offset().left + + me.$payment_modes.scrollLeft(); + me.$payment_modes.animate({ scrollLeft }); + + const mode = mode_clicked.attr("data-mode"); + + // hide all control fields and shortcuts + $(`.mode-of-payment-control`).css("display", "none"); + $(`.cash-shortcuts`).css("display", "none"); + me.$payment_modes.find(`.pay-amount`).css("display", "inline"); + me.$payment_modes.find(`.loyalty-amount-name`).css("display", "none"); + + // remove highlight from all mode-of-payments + $(".mode-of-payment").removeClass("border-primary"); + + if (mode_clicked.hasClass("border-primary")) { + // clicked one is selected then unselect it + mode_clicked.removeClass("border-primary"); + me.selected_mode = ""; + } else { + // clicked one is not selected then select it + mode_clicked.addClass("border-primary"); + mode_clicked.find(".mode-of-payment-control").css("display", "flex"); + mode_clicked.find(".cash-shortcuts").css("display", "grid"); + me.$payment_modes.find(`.${mode}-amount`).css("display", "none"); + me.$payment_modes.find(`.${mode}-name`).css("display", "inline"); + + me.selected_mode = me[`${mode}_control`]; + me.selected_mode && me.selected_mode.$input.get(0).focus(); + me.auto_set_remaining_amount(); + } + }); + + frappe.ui.form.on("POS Invoice", "contact_mobile", (frm) => { + const contact = frm.doc.contact_mobile; + const request_button = $(this.request_for_payment_field?.$input[0]); + if (contact) { + request_button.removeClass("btn-default").addClass("btn-primary"); + } else { + request_button.removeClass("btn-primary").addClass("btn-default"); + } + }); + + frappe.ui.form.on("POS Invoice", "coupon_code", (frm) => { + if (frm.doc.coupon_code && !frm.applying_pos_coupon_code) { + if (!frm.doc.ignore_pricing_rule) { + frm.applying_pos_coupon_code = true; + frappe.run_serially([ + () => (frm.doc.ignore_pricing_rule = 1), + () => frm.trigger("ignore_pricing_rule"), + () => (frm.doc.ignore_pricing_rule = 0), + () => frm.trigger("apply_pricing_rule"), + () => frm.save(), + () => this.update_totals_section(frm.doc), + () => (frm.applying_pos_coupon_code = false), + ]); + } else if (frm.doc.ignore_pricing_rule) { + frappe.show_alert({ + message: __( + "Ignore Pricing Rule is enabled. Cannot apply coupon code.", + ), + indicator: "orange", + }); + } + } + }); + + this.setup_listener_for_payments(); + + this.$payment_modes.on("click", ".shortcut", function () { + const value = $(this).attr("data-value"); + me.selected_mode.set_value(value); + }); + + this.$component.on("click", ".submit-order-btn", () => { + const doc = this.events.get_frm().doc; + let paid_amount = doc.paid_amount; + // prettier-ignore + if (cur_frm.doc.custom_credit_sales && this.custom_show_credit_sales) { // nosemgrep + cur_frm.clear_table("payments"); // nosemgrep Overrides erpnext code + paid_amount = 0; + } + + const items = doc.items; + + if ( + (paid_amount == 0 || !items.length) && + !this.custom_show_credit_sales + ) { + const message = items.length + ? __("You cannot submit the order without payment.") + : __("You cannot submit empty order."); + frappe.show_alert({ message, indicator: "orange" }); + frappe.utils.play_sound("error"); + return; + } + + this.events.submit_invoice(); + }); + + frappe.ui.form.on("POS Invoice", "paid_amount", (frm) => { + this.update_totals_section(frm.doc); + + // need to re calculate cash shortcuts after discount is applied + const is_cash_shortcuts_invisible = !this.$payment_modes + .find(".cash-shortcuts") + .is(":visible"); + this.attach_cash_shortcuts(frm.doc); + !is_cash_shortcuts_invisible && + this.$payment_modes.find(".cash-shortcuts").css("display", "grid"); + this.render_payment_mode_dom(); + }); + + frappe.ui.form.on("POS Invoice", "loyalty_amount", (frm) => { + const formatted_currency = format_currency( + frm.doc.loyalty_amount, + frm.doc.currency, + ); + this.$payment_modes + .find(`.loyalty-amount-amount`) + .html(formatted_currency); + }); + + frappe.ui.form.on("Sales Invoice Payment", "amount", (frm, cdt, cdn) => { + // for setting correct amount after loyalty points are redeemed + const default_mop = locals[cdt][cdn]; + const mode = default_mop.mode_of_payment + .replace(/ +/g, "_") + .toLowerCase(); + if ( + this[`${mode}_control`] && + this[`${mode}_control`].get_value() != default_mop.amount + ) { + this[`${mode}_control`].set_value(default_mop.amount); + } + }); + } + + setup_listener_for_payments() { + frappe.realtime.on("process_phone_payment", (data) => { + const doc = this.events.get_frm().doc; + const { response, amount, success, failure_message } = data; + let message, title; + + if (success) { + title = __("Payment Received"); + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) + ? doc.grand_total + : doc.rounded_total; + if (amount >= grand_total) { + frappe.dom.unfreeze(); + message = __("Payment of {0} received successfully.", [ + format_currency(amount, doc.currency, 0), + ]); + this.events.submit_invoice(); + cur_frm.reload_doc(); // nosemgrep Overrides erpnext code + } else { + message = __( + "Payment of {0} received successfully. Waiting for other requests to complete...", + [format_currency(amount, doc.currency, 0)], + ); + } + } else if (failure_message) { + message = failure_message; + title = __("Payment Failed"); + } + + frappe.msgprint({ message: message, title: title }); + }); + } + + auto_set_remaining_amount() { + const doc = this.events.get_frm().doc; + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) + ? doc.grand_total + : doc.rounded_total; + const remaining_amount = grand_total - doc.paid_amount; + const current_value = this.selected_mode + ? this.selected_mode.get_value() + : undefined; + if (!current_value && remaining_amount > 0 && this.selected_mode) { + this.selected_mode.set_value(remaining_amount); + } + } + + attach_shortcuts() { + const ctrl_label = frappe.utils.is_mac() ? "⌘" : "Ctrl"; + this.$component + .find(".submit-order-btn") + .attr("title", `${ctrl_label}+Enter`); + frappe.ui.keys.on("ctrl+enter", () => { + const payment_is_visible = this.$component.is(":visible"); + const active_mode = this.$payment_modes.find(".border-primary"); + if (payment_is_visible && active_mode.length) { + this.$component.find(".submit-order-btn").click(); + } + }); + + frappe.ui.keys.add_shortcut({ + shortcut: "tab", + action: () => { + const payment_is_visible = this.$component.is(":visible"); + let active_mode = this.$payment_modes.find(".border-primary"); + active_mode = active_mode.length + ? active_mode.attr("data-mode") + : undefined; + + if (!active_mode) return; + + const mode_of_payments = Array.from( + this.$payment_modes.find(".mode-of-payment"), + ).map((m) => $(m).attr("data-mode")); + const mode_index = mode_of_payments.indexOf(active_mode); + const next_mode_index = (mode_index + 1) % mode_of_payments.length; + const next_mode_to_be_clicked = this.$payment_modes.find( + `.mode-of-payment[data-mode="${mode_of_payments[next_mode_index]}"]`, + ); + + if (payment_is_visible && mode_index != next_mode_index) { + next_mode_to_be_clicked.click(); + } + }, + condition: () => + this.$component.is(":visible") && + this.$payment_modes.find(".border-primary").length, + description: __("Switch Between Payment Modes"), + ignore_inputs: true, + page: cur_page.page.page, + }); + } + + toggle_numpad() { + // pass + } + + render_payment_section() { + this.render_payment_mode_dom(); + this.make_invoice_fields_control(); + this.update_totals_section(); + this.focus_on_default_mop(); + } + + after_render() { + const frm = this.events.get_frm(); + frm.script_manager.trigger( + "after_payment_render", + frm.doc.doctype, + frm.doc.docname, + ); + } + + edit_cart() { + if (this.custom_edit_rate) { + const div = document.getElementById("customer-cart-container2"); + div.style.gridColumn = "span 5 / span 5"; + } + + this.events.toggle_other_sections(false); + this.toggle_component(false); + } + + checkout() { + this.events.toggle_other_sections(true); + this.toggle_component(true); + + this.render_payment_section(); + this.after_render(); + } + + toggle_remarks_control() { + if (this.$remarks.find(".frappe-control").length) { + this.$remarks.html("+ Add Remark"); + } else { + this.$remarks.html(""); + this[`remark_control`] = frappe.ui.form.make_control({ + df: { + label: __("Remark"), + fieldtype: "Data", + onchange: function () {}, + }, + parent: this.$totals_section.find(`.remarks`), + render_input: true, + }); + this[`remark_control`].set_value(""); + } + } + + render_payment_mode_dom() { + const doc = this.events.get_frm().doc; + const payments = doc.payments; + const currency = doc.currency; + + this.$payment_modes.html( + `${payments + .map((p, i) => { + const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); + const payment_type = p.type; + const margin = i % 2 === 0 ? "pr-2" : "pl-2"; + const amount = + p.amount > 0 ? format_currency(p.amount, currency) : ""; + + return ` <div class="payment-mode-wrapper"> <div class="mode-of-payment" data-mode="${mode}" data-payment-type="${payment_type}"> ${p.mode_of_payment} @@ -487,201 +547,259 @@ posnext.PointOfSale.Payment = class { <div class="${mode} mode-of-payment-control"></div> </div> </div> - `); - }).join('') - }`); - this.current_payments = payments - payments.forEach(p => { - const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); - const me = this; - this[`${mode}_control`] = frappe.ui.form.make_control({ - df: { - label: p.mode_of_payment, - fieldtype: 'Currency', - placeholder: __('Enter {0} amount.', [p.mode_of_payment]), - onchange: function() { - console.log(p.doctype) - console.log(p.name) - const current_value = frappe.model.get_value(p.doctype, p.name, 'amount'); - if (current_value != this.value) { - frappe.model - .set_value(p.doctype, p.name, 'amount', flt(this.value)) - .then(() => me.update_totals_section()) - - const formatted_currency = format_currency(this.value, currency); - me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency); - } - } - }, - parent: this.$payment_modes.find(`.${mode}.mode-of-payment-control`), - render_input: true, - }); - this[`${mode}_control`].toggle_label(false); - this[`${mode}_control`].set_value(p.amount); - }); - - this.render_loyalty_points_payment_mode(); - - this.attach_cash_shortcuts(doc); - } - - focus_on_default_mop() { - const doc = this.events.get_frm().doc; - const payments = doc.payments; - payments.forEach(p => { - const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); - if (p.default) { - setTimeout(() => { - this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click(); - }, 500); - } - }); - } - - attach_cash_shortcuts(doc) { - const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; - const currency = doc.currency; - - const shortcuts = this.get_cash_shortcuts(flt(grand_total)); - - this.$payment_modes.find('.cash-shortcuts').remove(); - let shortcuts_html = shortcuts.map(s => { - return `<div class="shortcut" data-value="${s}">${format_currency(s, currency, 0)}</div>`; - }).join(''); - - this.$payment_modes.find('[data-payment-type="Cash"]').find('.mode-of-payment-control') - .after(`<div class="cash-shortcuts">${shortcuts_html}</div>`); - } - - get_cash_shortcuts(grand_total) { - let steps = [1, 5, 10]; - const digits = String(Math.round(grand_total)).length; - - steps = steps.map(x => x * (10 ** (digits - 2))); - - const get_nearest = (amount, x) => { - let nearest_x = Math.ceil((amount / x)) * x; - return nearest_x === amount ? nearest_x + x : nearest_x; - }; - - return steps.reduce((finalArr, x) => { - let nearest_x = get_nearest(grand_total, x); - nearest_x = finalArr.indexOf(nearest_x) != -1 ? nearest_x + x : nearest_x; - return [...finalArr, nearest_x]; - }, []); - } - - render_loyalty_points_payment_mode() { - const me = this; - const doc = this.events.get_frm().doc; - const { loyalty_program, loyalty_points, conversion_factor } = this.events.get_customer_details(); - - this.$payment_modes.find(`.mode-of-payment[data-mode="loyalty-amount"]`).parent().remove(); - - if (!loyalty_program) return; - - let description, read_only, max_redeemable_amount; - if (!loyalty_points) { - description = __("You don't have enough points to redeem."); - read_only = true; - } else { - max_redeemable_amount = flt(flt(loyalty_points) * flt(conversion_factor), precision("loyalty_amount", doc)); - description = __("You can redeem upto {0}.", [format_currency(max_redeemable_amount)]); - read_only = false; - } - - const margin = this.$payment_modes.children().length % 2 === 0 ? 'pr-2' : 'pl-2'; - const amount = doc.loyalty_amount > 0 ? format_currency(doc.loyalty_amount, doc.currency) : ''; - this.$payment_modes.append( - `<div class="payment-mode-wrapper"> + `; + }) + .join("")}`, + ); + this.current_payments = payments; + payments.forEach((p) => { + const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); + const me = this; + this[`${mode}_control`] = frappe.ui.form.make_control({ + df: { + label: p.mode_of_payment, + fieldtype: "Currency", + placeholder: __("Enter {0} amount.", [p.mode_of_payment]), + onchange: function () { + console.log(p.doctype); + console.log(p.name); + const current_value = frappe.model.get_value( + p.doctype, + p.name, + "amount", + ); + if (current_value != this.value) { + frappe.model + .set_value(p.doctype, p.name, "amount", flt(this.value)) + .then(() => me.update_totals_section()); + + const formatted_currency = format_currency(this.value, currency); + me.$payment_modes + .find(`.${mode}-amount`) + .html(formatted_currency); + } + }, + }, + parent: this.$payment_modes.find(`.${mode}.mode-of-payment-control`), + render_input: true, + }); + this[`${mode}_control`].toggle_label(false); + this[`${mode}_control`].set_value(p.amount); + }); + + this.render_loyalty_points_payment_mode(); + + this.attach_cash_shortcuts(doc); + } + + focus_on_default_mop() { + const doc = this.events.get_frm().doc; + const payments = doc.payments; + payments.forEach((p) => { + const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase(); + if (p.default) { + setTimeout(() => { + this.$payment_modes + .find(`.${mode}.mode-of-payment-control`) + .parent() + .click(); + }, 500); + } + }); + } + + attach_cash_shortcuts(doc) { + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) + ? doc.grand_total + : doc.rounded_total; + const currency = doc.currency; + + const shortcuts = this.get_cash_shortcuts(flt(grand_total)); + + this.$payment_modes.find(".cash-shortcuts").remove(); + let shortcuts_html = shortcuts + .map((s) => { + return `<div class="shortcut" data-value="${s}">${format_currency( + s, + currency, + 0, + )}</div>`; + }) + .join(""); + + this.$payment_modes + .find('[data-payment-type="Cash"]') + .find(".mode-of-payment-control") + .after(`<div class="cash-shortcuts">${shortcuts_html}</div>`); + } + + get_cash_shortcuts(grand_total) { + let steps = [1, 5, 10]; + const digits = String(Math.round(grand_total)).length; + + steps = steps.map((x) => x * 10 ** (digits - 2)); + + const get_nearest = (amount, x) => { + let nearest_x = Math.ceil(amount / x) * x; + return nearest_x === amount ? nearest_x + x : nearest_x; + }; + + return steps.reduce((finalArr, x) => { + let nearest_x = get_nearest(grand_total, x); + nearest_x = finalArr.indexOf(nearest_x) != -1 ? nearest_x + x : nearest_x; + return [...finalArr, nearest_x]; + }, []); + } + + render_loyalty_points_payment_mode() { + const me = this; + const doc = this.events.get_frm().doc; + const { loyalty_program, loyalty_points, conversion_factor } = + this.events.get_customer_details(); + + this.$payment_modes + .find(`.mode-of-payment[data-mode="loyalty-amount"]`) + .parent() + .remove(); + + if (!loyalty_program) return; + + let description, read_only, max_redeemable_amount; + if (!loyalty_points) { + description = __("You don't have enough points to redeem."); + read_only = true; + } else { + max_redeemable_amount = flt( + flt(loyalty_points) * flt(conversion_factor), + precision("loyalty_amount", doc), + ); + description = __("You can redeem upto {0}.", [ + format_currency(max_redeemable_amount), + ]); + read_only = false; + } + + const margin = + this.$payment_modes.children().length % 2 === 0 ? "pr-2" : "pl-2"; + const amount = + doc.loyalty_amount > 0 + ? format_currency(doc.loyalty_amount, doc.currency) + : ""; + this.$payment_modes.append( + `<div class="payment-mode-wrapper"> <div class="mode-of-payment loyalty-card" data-mode="loyalty-amount" data-payment-type="loyalty-amount"> Redeem Loyalty Points <div class="loyalty-amount-amount pay-amount">${amount}</div> <div class="loyalty-amount-name">${loyalty_program}</div> <div class="loyalty-amount mode-of-payment-control"></div> </div> - </div>` - ); - - this['loyalty-amount_control'] = frappe.ui.form.make_control({ - df: { - label: __("Redeem Loyalty Points"), - fieldtype: 'Currency', - placeholder: __("Enter amount to be redeemed."), - options: 'company:currency', - read_only, - onchange: async function() { - if (!loyalty_points) return; - - if (this.value > max_redeemable_amount) { - frappe.show_alert({ - message: __("You cannot redeem more than {0}.", [format_currency(max_redeemable_amount)]), - indicator: "red" - }); - frappe.utils.play_sound("submit"); - me['loyalty-amount_control'].set_value(0); - return; - } - const redeem_loyalty_points = this.value > 0 ? 1 : 0; - await frappe.model.set_value(doc.doctype, doc.name, 'redeem_loyalty_points', redeem_loyalty_points); - frappe.model.set_value(doc.doctype, doc.name, 'loyalty_points', parseInt(this.value / conversion_factor)); - }, - description - }, - parent: this.$payment_modes.find(`.loyalty-amount.mode-of-payment-control`), - render_input: true, - }); - this['loyalty-amount_control'].toggle_label(false); - - // this.render_add_payment_method_dom(); - } - - render_add_payment_method_dom() { - const docstatus = this.events.get_frm().doc.docstatus; - if (docstatus === 0) - this.$payment_modes.append( - `<div class="w-full pr-2"> + </div>`, + ); + + this["loyalty-amount_control"] = frappe.ui.form.make_control({ + df: { + label: __("Redeem Loyalty Points"), + fieldtype: "Currency", + placeholder: __("Enter amount to be redeemed."), + options: "company:currency", + read_only, + onchange: async function () { + if (!loyalty_points) return; + + if (this.value > max_redeemable_amount) { + frappe.show_alert({ + message: __("You cannot redeem more than {0}.", [ + format_currency(max_redeemable_amount), + ]), + indicator: "red", + }); + frappe.utils.play_sound("submit"); + me["loyalty-amount_control"].set_value(0); + return; + } + const redeem_loyalty_points = this.value > 0 ? 1 : 0; + await frappe.model.set_value( + doc.doctype, + doc.name, + "redeem_loyalty_points", + redeem_loyalty_points, + ); + frappe.model.set_value( + doc.doctype, + doc.name, + "loyalty_points", + parseInt(this.value / conversion_factor), + ); + }, + description, + }, + parent: this.$payment_modes.find( + `.loyalty-amount.mode-of-payment-control`, + ), + render_input: true, + }); + this["loyalty-amount_control"].toggle_label(false); + + // this.render_add_payment_method_dom(); + } + + render_add_payment_method_dom() { + const docstatus = this.events.get_frm().doc.docstatus; + if (docstatus === 0) + this.$payment_modes.append( + `<div class="w-full pr-2"> <div class="add-mode-of-payment w-half text-grey mb-4 no-select pointer">+ Add Payment Method</div> - </div>` - ); - } - - update_totals_section(doc) { - if (!doc) doc = this.events.get_frm().doc; - let branch_value = $('.input-with-feedback[data-fieldname="branch"]').val(); - frappe.model.set_value(cur_frm.doctype, cur_frm.docname, 'branch', branch_value); - // cur_frm.save() - // doc.paid_amount = doc.grand_total - const paid_amount = doc.paid_amount; - - if(cur_frm.doc.custom_credit_sales){ - const paid_amount = 0 - } - const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total; - const remaining = grand_total - doc.paid_amount; - const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined; - const currency = doc.currency; - const label = change ? __('Change') : __('To Be Paid'); - - this.$totals.html( - `<div class="col"> - <div class="total-label">${__('Grand Total')}</div> + </div>`, + ); + } + + update_totals_section(doc) { + if (!doc) doc = this.events.get_frm().doc; + let branch_value = $('.input-with-feedback[data-fieldname="branch"]').val(); + frappe.model.set_value( + cur_frm.doctype, // nosemgrep Overrides erpnext code + cur_frm.docname, // nosemgrep Overrides erpnext code + "branch", + branch_value, + ); + // cur_frm.save() + // doc.paid_amount = doc.grand_total + const paid_amount = doc.paid_amount; + + // prettier-ignore + if (cur_frm.doc.custom_credit_sales) { // nosemgrep + const paid_amount = 0; + } + const grand_total = cint(frappe.sys_defaults.disable_rounded_total) + ? doc.grand_total + : doc.rounded_total; + const remaining = grand_total - doc.paid_amount; + const change = + doc.change_amount || remaining <= 0 ? -1 * remaining : undefined; + const currency = doc.currency; + const label = change ? __("Change") : __("To Be Paid"); + + this.$totals.html( + `<div class="col"> + <div class="total-label">${__("Grand Total")}</div> <div class="value">${format_currency(grand_total, currency)}</div> </div> <div class="seperator-y"></div> <div class="col"> - <div class="total-label">${__('Paid Amount')}</div> + <div class="total-label">${__("Paid Amount")}</div> <div class="value">${format_currency(paid_amount, currency)}</div> </div> <div class="seperator-y"></div> <div class="col"> <div class="total-label">${label}</div> <div class="value">${format_currency(change || remaining, currency)}</div> - </div>` - ); - } - - toggle_component(show) { - show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none'); - } + </div>`, + ); + } + + toggle_component(show) { + show + ? this.$component.css("display", "flex") + : this.$component.css("display", "none"); + } }; diff --git a/posnext/public/js/pos_profile.js b/posnext/public/js/pos_profile.js index b7bf6ef..678dc0d 100644 --- a/posnext/public/js/pos_profile.js +++ b/posnext/public/js/pos_profile.js @@ -1,17 +1,14 @@ frappe.ui.form.on("POS Profile", { - custom_show_only_list_view: function () { - if(cur_frm.doc.custom_show_only_list_view){ - cur_frm.doc.custom_show_only_card_view = 0 - cur_frm.refresh_field("custom_show_only_card_view") - } - - }, - custom_show_only_card_view: function () { - if(cur_frm.doc.custom_show_only_card_view){ - cur_frm.doc.custom_show_only_list_view = 0 - cur_frm.refresh_field("custom_show_only_list_view") - } - - + custom_show_only_list_view: function (frm) { + if (frm.doc.custom_show_only_list_view) { + frm.doc.custom_show_only_card_view = 0; + frm.refresh_field("custom_show_only_card_view"); } -}) + }, + custom_show_only_card_view: function (frm) { + if (frm.doc.custom_show_only_card_view) { + frm.doc.custom_show_only_list_view = 0; + frm.refresh_field("custom_show_only_list_view"); + } + }, +}); diff --git a/posnext/public/js/posnext.bundle.js b/posnext/public/js/posnext.bundle.js index 5f3e2f1..2763bb6 100644 --- a/posnext/public/js/posnext.bundle.js +++ b/posnext/public/js/posnext.bundle.js @@ -6,4 +6,3 @@ import "./pos_number_pad.js"; import "./pos_payment.js"; import "./pos_past_order_list.js"; import "./pos_past_order_summary.js"; - diff --git a/posnext/public/js/sales_invoice.js b/posnext/public/js/sales_invoice.js index 3be0864..f849f86 100644 --- a/posnext/public/js/sales_invoice.js +++ b/posnext/public/js/sales_invoice.js @@ -1,7 +1,7 @@ -frappe.ui.form.on('Sales Invoice', { - company() { - erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); - this.frm.set_value("set_warehouse", ""); - this.frm.set_value("taxes_and_charges", ""); - } -}) \ No newline at end of file +frappe.ui.form.on("Sales Invoice", { + company() { + erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype); + this.frm.set_value("set_warehouse", ""); + this.frm.set_value("taxes_and_charges", ""); + }, +}); diff --git a/posnext/utils.py b/posnext/utils.py new file mode 100644 index 0000000..e146e61 --- /dev/null +++ b/posnext/utils.py @@ -0,0 +1,35 @@ +import frappe +from erpnext.setup.utils import enable_all_roles_and_domains +from frappe.utils import now_datetime + + +def before_tests(): + frappe.clear_cache() + # complete setup if missing + from frappe.desk.page.setup_wizard.setup_wizard import setup_complete + + print("Running before_tests") + + if not frappe.db.a_row_exists("Company"): + print("Running setup_complete because company does not exist") + current_year = now_datetime().year + setup_complete( + { + "currency": "ZAR", + "full_name": "Test User", + "company_name": "Sun Power Pty Ltd", + "timezone": "Africa/Johannesburg", + "company_abbr": "SP", + "industry": "Manufacturing", + "country": "South Africa", + "fy_start_date": f"{current_year}-01-01", + "fy_end_date": f"{current_year}-12-31", + "language": "english", + "company_tagline": "Testing", + "email": "test@erpnext.com", + "password": "test", + "chart_of_accounts": "Standard", + } + ) + enable_all_roles_and_domains() + frappe.db.commit() # nosemgrep diff --git a/prepare_help_files.sh b/prepare_help_files.sh new file mode 100755 index 0000000..9a0f1cd --- /dev/null +++ b/prepare_help_files.sh @@ -0,0 +1,21 @@ +#!/bin/bash + +# This is used in pacakge.json to block the help pages from public access, and copy the assets to the correct directory + +AUTH_CONTENT="import frappe +from frappe import _ + +if frappe.session.user=='Guest': + frappe.throw(_(\"You need to be logged in to access this page\"), frappe.PermissionError)" + +for file in posnext/www/posnext_*.html; do + if [ -f "$file" ]; then + py_file="posnext/www/$(basename "$file" .html).py" + echo "$AUTH_CONTENT" > "$py_file" + fi +done + +rm -rf ./posnext/public/chunks +mv ./posnext/www/assets/posnext/chunks ./posnext/public/. +mv ./posnext/www/assets/posnext/*.js ./posnext/public/. +mv ./posnext/www/assets/posnext/*.css ./posnext/public/. \ No newline at end of file diff --git a/queries.py b/queries.py index 2222dce..18a4159 100644 --- a/queries.py +++ b/queries.py @@ -1,24 +1,26 @@ import frappe -from frappe.utils import nowdate, unique +from frappe import _ from frappe.desk.reportview import get_filters_cond, get_match_cond +from frappe.utils import unique + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False): - doctype = "Customer" - conditions = [] - cust_master_name = frappe.defaults.get_user_default("cust_master_name") + doctype = "Customer" + conditions = [] + cust_master_name = frappe.defaults.get_user_default("cust_master_name") - fields = ["name"] - if cust_master_name != "Customer Name": - fields.append("customer_name") + fields = ["name"] + if cust_master_name != "Customer Name": + fields.append("customer_name") - fields = get_fields(doctype, fields) - searchfields = frappe.get_meta(doctype).get_search_fields() - searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) + fields = get_fields(doctype, fields) + searchfields = frappe.get_meta(doctype).get_search_fields() + searchfields = " or ".join(field + " like %(txt)s" for field in searchfields) - return frappe.db.sql( - """select {fields} from `tabCustomer` + return frappe.db.sql( + """select {fields} from `tabCustomer` where docstatus < 2 and ({scond}) and disabled=0 {fcond} {mcond} @@ -28,35 +30,45 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters, as_dict= idx desc, name, customer_name limit %(page_len)s offset %(start)s""".format( - **{ - "fields": ", ".join(fields), - "scond": searchfields, - "mcond": get_match_cond(doctype), - "fcond": get_filters_cond(doctype, filters, conditions).replace("%", "%%"), - } - ), - {"txt": "%%%s%%" % txt, "_txt": txt.replace("%", ""), "start": start, "page_len": page_len}, - as_dict=as_dict, - ) + **{ + "fields": ", ".join(fields), + "scond": searchfields, + "mcond": get_match_cond(doctype), + "fcond": get_filters_cond(doctype, filters, conditions).replace( + "%", "%%" + ), + } + ), + { + "txt": "%%%s%%" % txt, + "_txt": txt.replace("%", ""), + "start": start, + "page_len": page_len, + }, + as_dict=as_dict, + ) + def get_fields(doctype, fields=None): - if fields is None: - fields = [] - meta = frappe.get_meta(doctype) - fields.extend(meta.get_search_fields()) + if fields is None: + fields = [] + meta = frappe.get_meta(doctype) + fields.extend(meta.get_search_fields()) + + if meta.title_field and meta.title_field.strip() not in fields: + fields.insert(1, meta.title_field.strip()) - if meta.title_field and meta.title_field.strip() not in fields: - fields.insert(1, meta.title_field.strip()) + return unique(fields) - return unique(fields) @frappe.whitelist() def get_ledger_balance(customer): if not customer: - frappe.throw("Customer ID is required.") + frappe.throw(_("Customer ID is required.")) # Fetch receivable balance for the customer - balance = frappe.db.sql(""" + balance = frappe.db.sql( + """ SELECT SUM(debit - credit) AS receivable FROM `tabGL Entry` WHERE party_type = 'Customer' @@ -65,7 +77,10 @@ def get_ledger_balance(customer): SELECT name FROM `tabAccount` WHERE account_type = 'Receivable' ) AND is_cancelled = 0 - """, (customer,), as_dict=True) + """, + (customer,), + as_dict=True, + ) # Return the balance - return balance[0].get("receivable", 0) if balance else 0 \ No newline at end of file + return balance[0].get("receivable", 0) if balance else 0