├── .editorconfig
├── .eslintrc
├── .flake8
├── .git-blame-ignore-revs
├── .github
└── workflows
│ ├── codeql.yml
│ ├── initiate_release.yml
│ ├── linters.yml
│ ├── on_release.yml
│ └── semantic-commits.yml
├── .gitignore
├── .mergify.yml
├── .pre-commit-config.yaml
├── .releaserc
├── README.md
├── commitlint.config.js
├── docs
├── FAQS.md
├── accounting
│ ├── 1_fees_and_tax.md
│ └── 2_payout_reversal.md
├── payout
│ ├── 1_requirements.md
│ ├── 2_Authentication.md
│ ├── 3_make_payout.md
│ ├── 4_bulk_submit_without_payout.md
│ ├── 5_cancel_payout.md
│ ├── 6_setup_status_notification.md
│ └── 7_payout_tips_and_notes.md
├── reconcile
│ └── 1_sync_and_reconcile_transactions.md
├── report
│ └── 1_payout_status_report.md
└── setup
│ ├── 1_setup_test_and_live_mode.md
│ └── 2_connect_erpnext_with_razorpayx.md
├── license.txt
├── pyproject.toml
├── razorpayx_integration
├── __init__.py
├── config
│ ├── __init__.py
│ ├── desktop.py
│ └── docs.py
├── constants.py
├── hooks.py
├── install.py
├── modules.txt
├── patches.txt
├── patches
│ ├── delete_old_custom_fields.py
│ ├── delete_old_property_setters.py
│ ├── mark_creation_of_je_on_reversal.py
│ ├── post_install
│ │ └── __init__.py
│ ├── set_default_payouts_from.py
│ ├── set_payment_transfer_method.py
│ └── update_integration_doctype.py
├── public
│ ├── .gitkeep
│ ├── images
│ │ └── razorpayx-logo.png
│ └── js
│ │ ├── razorpayx_integration.bundle.js
│ │ └── utils.js
├── razorpayx_integration
│ ├── __init__.py
│ ├── apis
│ │ ├── base.py
│ │ ├── contact.py
│ │ ├── fund_account.py
│ │ ├── payout.py
│ │ ├── transaction.py
│ │ └── validate_razorpayx.py
│ ├── client_overrides
│ │ └── form
│ │ │ ├── bank_reconciliation_tool.js
│ │ │ └── payment_entry.js
│ ├── constants
│ │ ├── __init__.py
│ │ ├── custom_fields.py
│ │ ├── payouts.py
│ │ ├── property_setters.py
│ │ ├── roles.py
│ │ └── webhooks.py
│ ├── doctype
│ │ ├── __init__.py
│ │ └── razorpayx_configuration
│ │ │ ├── __init__.py
│ │ │ ├── razorpayx_configuration.js
│ │ │ ├── razorpayx_configuration.json
│ │ │ ├── razorpayx_configuration.py
│ │ │ └── test_razorpayx_configuration.py
│ ├── notification
│ │ ├── __init__.py
│ │ ├── failed_payout
│ │ │ ├── __init__.py
│ │ │ ├── failed_payout.json
│ │ │ ├── failed_payout.md
│ │ │ └── failed_payout.py
│ │ └── payout_processed
│ │ │ ├── __init__.py
│ │ │ ├── payout_processed.json
│ │ │ ├── payout_processed.md
│ │ │ └── payout_processed.py
│ ├── report
│ │ ├── __init__.py
│ │ └── razorpayx_payout_status
│ │ │ ├── __init__.py
│ │ │ ├── razorpayx_payout_status.js
│ │ │ ├── razorpayx_payout_status.json
│ │ │ └── razorpayx_payout_status.py
│ ├── server_overrides
│ │ ├── __init__.py
│ │ └── doctype
│ │ │ └── payment_entry.py
│ └── utils
│ │ ├── __init__.py
│ │ ├── bank_transaction.py
│ │ ├── payout.py
│ │ ├── validation.py
│ │ └── webhook.py
├── setup.py
├── templates
│ ├── __init__.py
│ └── pages
│ │ └── __init__.py
└── uninstall.py
└── requirements.txt
/.editorconfig:
--------------------------------------------------------------------------------
1 | # Root editor config file
2 | root = true
3 |
4 | # Common settings
5 | [*]
6 | end_of_line = lf
7 | insert_final_newline = true
8 | trim_trailing_whitespace = true
9 | charset = utf-8
10 |
11 | # python, js indentation settings
12 | [{*.py,*.js,*.vue,*.css,*.scss,*.html}]
13 | indent_style = space
14 | indent_size = 4
15 | max_line_length = 110
16 |
17 | # JSON files - mostly doctype schema files
18 | [{*.json}]
19 | insert_final_newline = false
20 | indent_style = space
21 | indent_size = 1
--------------------------------------------------------------------------------
/.eslintrc:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "node": true,
5 | "es2022": true
6 | },
7 | "parserOptions": {
8 | "sourceType": "module"
9 | },
10 | "extends": "eslint:recommended",
11 | "rules": {
12 | "indent": "off",
13 | "brace-style": "off",
14 | "no-mixed-spaces-and-tabs": "off",
15 | "no-useless-escape": "off",
16 | "space-unary-ops": ["error", { "words": true }],
17 | "linebreak-style": "off",
18 | "quotes": ["off"],
19 | "semi": "off",
20 | "camelcase": "off",
21 | "no-unused-vars": "off",
22 | "no-console": ["warn"],
23 | "no-extra-boolean-cast": ["off"],
24 | "no-control-regex": ["off"]
25 | },
26 | "root": true,
27 | "globals": {
28 | "frappe": true,
29 | "razorpayx":true,
30 | "payment_integration_utils": true,
31 | "Vue": true,
32 | "SetVueGlobals": true,
33 | "erpnext": true,
34 | "hub": true,
35 | "$": true,
36 | "jQuery": true,
37 | "moment": true,
38 | "hljs": true,
39 | "Awesomplete": true,
40 | "CalHeatMap": true,
41 | "Sortable": true,
42 | "Showdown": true,
43 | "Taggle": true,
44 | "Gantt": true,
45 | "Slick": true,
46 | "PhotoSwipe": true,
47 | "PhotoSwipeUI_Default": true,
48 | "fluxify": true,
49 | "io": true,
50 | "c3": true,
51 | "__": true,
52 | "_p": true,
53 | "_f": true,
54 | "repl": true,
55 | "Class": true,
56 | "locals": true,
57 | "cint": true,
58 | "cstr": true,
59 | "cur_frm": true,
60 | "cur_dialog": true,
61 | "cur_page": true,
62 | "cur_list": true,
63 | "cur_tree": true,
64 | "cur_pos": true,
65 | "msg_dialog": true,
66 | "is_null": true,
67 | "in_list": true,
68 | "has_common": true,
69 | "posthog": true,
70 | "has_words": true,
71 | "validate_email": true,
72 | "open_web_template_values_editor": true,
73 | "get_number_format": true,
74 | "format_number": true,
75 | "format_currency": true,
76 | "round_based_on_smallest_currency_fraction": true,
77 | "roundNumber": true,
78 | "comment_when": true,
79 | "replace_newlines": true,
80 | "open_url_post": true,
81 | "toTitle": true,
82 | "lstrip": true,
83 | "strip": true,
84 | "strip_html": true,
85 | "replace_all": true,
86 | "flt": true,
87 | "precision": true,
88 | "md5": true,
89 | "CREATE": true,
90 | "AMEND": true,
91 | "CANCEL": true,
92 | "copy_dict": true,
93 | "get_number_format_info": true,
94 | "print_table": true,
95 | "Layout": true,
96 | "web_form_settings": true,
97 | "$c": true,
98 | "$a": true,
99 | "$i": true,
100 | "$bg": true,
101 | "$y": true,
102 | "$c_obj": true,
103 | "$c_obj_csv": true,
104 | "refresh_many": true,
105 | "refresh_field": true,
106 | "toggle_field": true,
107 | "get_field_obj": true,
108 | "get_query_params": true,
109 | "unhide_field": true,
110 | "hide_field": true,
111 | "set_field_options": true,
112 | "getCookie": true,
113 | "getCookies": true,
114 | "get_url_arg": true,
115 | "get_server_fields": true,
116 | "set_multiple": true,
117 | "QUnit": true,
118 | "Chart": true,
119 | "Cypress": true,
120 | "cy": true,
121 | "describe": true,
122 | "expect": true,
123 | "it": true,
124 | "context": true,
125 | "before": true,
126 | "beforeEach": true,
127 | "onScan": true,
128 | "extend_cscript": true,
129 | "localforage": true,
130 | "Plaid": true
131 | }
132 | }
--------------------------------------------------------------------------------
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore =
3 | E121,
4 | E126,
5 | E127,
6 | E128,
7 | E203,
8 | E225,
9 | E226,
10 | E231,
11 | E241,
12 | E251,
13 | E261,
14 | E265,
15 | E302,
16 | E303,
17 | E305,
18 | E402,
19 | E501,
20 | E741,
21 | W291,
22 | W292,
23 | W293,
24 | W391,
25 | W503,
26 | W504,
27 | F403,
28 | B007,
29 | B950,
30 | W191,
31 | E124, # closing bracket, irritating while writing QB code
32 | E131, # continuation line unaligned for hanging indent
33 | E123, # closing bracket does not match indentation of opening bracket's line
34 | E101, # ensured by use of black
35 |
36 | max-line-length = 200
37 |
--------------------------------------------------------------------------------
/.git-blame-ignore-revs:
--------------------------------------------------------------------------------
1 | # Since version 2.23 (released in August 2019), git-blame has a feature
2 | # to ignore or bypass certain commits.
3 | #
4 | # This file contains a list of commits that are not likely what you
5 | # are looking for in a blame, such as mass reformatting or renaming.
6 | # You can set this file as a default ignore file for blame by running
7 | # the following command.
8 | #
9 | # $ git config blame.ignoreRevsFile .git-blame-ignore-revs
10 |
11 | # Reformatting
12 | b30fc5128304b4f806087d7abc8269aa4b76703d
13 |
14 | # tabs to spaces
15 | 2a0bfb14e15b112db56348a4d5b063f00a431d96
16 |
--------------------------------------------------------------------------------
/.github/workflows/codeql.yml:
--------------------------------------------------------------------------------
1 | name: "CodeQL"
2 |
3 | on:
4 | workflow_call:
5 | pull_request:
6 | paths-ignore:
7 | - "**.css"
8 | - "**.md"
9 | - "**.html"
10 | - "**.csv"
11 |
12 | push:
13 |
14 | schedule:
15 | - cron: "0 0 * * 1"
16 |
17 | jobs:
18 | analyze:
19 | name: Analyze
20 | runs-on: ubuntu-latest
21 | permissions:
22 | actions: read
23 | contents: read
24 | security-events: write
25 |
26 | strategy:
27 | fail-fast: false
28 | matrix:
29 | language: ["python", "javascript"]
30 |
31 | steps:
32 | - name: Checkout repository
33 | uses: actions/checkout@v4
34 |
35 | # Initializes the CodeQL tools for scanning.
36 | - name: Initialize CodeQL
37 | uses: github/codeql-action/init@v3
38 | with:
39 | languages: ${{ matrix.language }}
40 |
41 | - name: Perform CodeQL Analysis
42 | uses: github/codeql-action/analyze@v3
43 |
--------------------------------------------------------------------------------
/.github/workflows/initiate_release.yml:
--------------------------------------------------------------------------------
1 | # This workflow is agnostic to branches. Only maintain on develop branch.
2 | # To add/remove versions just modify the matrix.
3 |
4 | name: Initiate Release
5 | on:
6 | workflow_dispatch:
7 |
8 | jobs:
9 | release:
10 | name: Release
11 | runs-on: ubuntu-latest
12 | strategy:
13 | fail-fast: false
14 | matrix:
15 | version: ["15"]
16 |
17 | steps:
18 | - uses: octokit/request-action@v2.x
19 | with:
20 | route: POST /repos/{owner}/{repo}/pulls
21 | owner: resilient-tech
22 | repo: razorpayx-integration
23 | title: |-
24 | "chore: release v${{ matrix.version }}"
25 | body: "Automated Release."
26 | base: version-${{ matrix.version }}
27 | head: version-${{ matrix.version }}-hotfix
28 | env:
29 | GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }}
30 |
--------------------------------------------------------------------------------
/.github/workflows/linters.yml:
--------------------------------------------------------------------------------
1 | name: Linters
2 |
3 | on:
4 | workflow_call:
5 | pull_request:
6 | paths-ignore:
7 | - "**.md"
8 | - "**.csv"
9 |
10 | jobs:
11 | linters:
12 | name: linters
13 | runs-on: ubuntu-latest
14 | steps:
15 | - uses: actions/checkout@v4
16 |
17 | - name: Set up Python 3.10
18 | uses: actions/setup-python@v5
19 | with:
20 | python-version: "3.10"
21 |
22 | - name: Install and Run Pre-commit
23 | uses: pre-commit/action@v3.0.1
24 |
25 | - name: Download Semgrep rules
26 | run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
27 |
28 | - name: Download semgrep
29 | run: pip install semgrep
30 |
31 | - name: Run Semgrep rules
32 | run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness
33 |
--------------------------------------------------------------------------------
/.github/workflows/on_release.yml:
--------------------------------------------------------------------------------
1 | name: Generate Semantic Release
2 | on:
3 | workflow_dispatch:
4 | push:
5 | branches:
6 | - version-15
7 | jobs:
8 | release:
9 | name: Release
10 | runs-on: ubuntu-latest
11 | steps:
12 | - name: Checkout Entire Repository
13 | uses: actions/checkout@v4
14 | with:
15 | fetch-depth: 0
16 | persist-credentials: false
17 |
18 | - name: Setup Node.js
19 | uses: actions/setup-node@v4
20 | with:
21 | node-version: 20
22 |
23 | - name: Setup dependencies
24 | run: |
25 | npm install @semantic-release/git @semantic-release/exec --no-save
26 | - name: Create Release
27 | env:
28 | GH_TOKEN: ${{ secrets.BOT_TOKEN }}
29 | GITHUB_TOKEN: ${{ secrets.BOT_TOKEN }}
30 | GIT_AUTHOR_NAME: "Resilient Tech Bot"
31 | GIT_AUTHOR_EMAIL: "bot@resilient.tech"
32 | GIT_COMMITTER_NAME: "Resilient Tech Bot"
33 | GIT_COMMITTER_EMAIL: "bot@resilient.tech"
34 | run: npx semantic-release
35 |
--------------------------------------------------------------------------------
/.github/workflows/semantic-commits.yml:
--------------------------------------------------------------------------------
1 | name: Semantic Commits
2 |
3 | on:
4 | pull_request: {}
5 |
6 | permissions:
7 | contents: read
8 |
9 | concurrency:
10 | group: commitcheck-frappe-${{ github.event.number }}
11 | cancel-in-progress: true
12 |
13 | jobs:
14 | commitlint:
15 | name: Check Commit Titles
16 | runs-on: ubuntu-latest
17 | steps:
18 | - uses: actions/checkout@v4
19 | with:
20 | fetch-depth: 200
21 |
22 | - uses: actions/setup-node@v4
23 | with:
24 | node-version: 20
25 | check-latest: true
26 |
27 | - name: Check commit titles
28 | run: |
29 | npm install @commitlint/cli @commitlint/config-conventional
30 | npx commitlint --verbose --from ${{ github.event.pull_request.base.sha }} --to ${{ github.event.pull_request.head.sha }}
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | *.pyc
3 | *.egg-info
4 | *.swp
5 | tags
6 | razorpayx_integration/docs/current
7 | node_modules/
8 |
9 | ## Build Files ###
10 | dist/
11 |
12 | ### Vs Code ###
13 | .vscode/
14 |
--------------------------------------------------------------------------------
/.mergify.yml:
--------------------------------------------------------------------------------
1 | pull_request_rules:
2 | - name: Auto-close PRs on stable branch
3 | conditions:
4 | - and:
5 | - author!=sagarvora
6 | - author!=vorasmit
7 | - author!=mergify[bot]
8 | - author!=dependabot[bot]
9 | - author!=resilient-tech-bot
10 | - or:
11 | - base=version-15
12 | actions:
13 | close:
14 | comment:
15 | message: |
16 | @{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on the `develop` branch.
17 |
18 | - name: Automatic merge on CI success and review
19 | conditions:
20 | - label!=dont-merge
21 | - label!=squash
22 | - "#approved-reviews-by>=1"
23 | actions:
24 | merge:
25 | method: merge
26 |
27 | - name: Automatic squash on CI success and review
28 | conditions:
29 | - label!=dont-merge
30 | - label=squash
31 | - "#approved-reviews-by>=1"
32 | actions:
33 | merge:
34 | method: squash
35 | commit_message_template: |
36 | {{ title }} (#{{ number }})
37 | {{ body }}
38 |
39 | - name: backport to develop
40 | conditions:
41 | - label="backport develop"
42 | actions:
43 | backport:
44 | branches:
45 | - develop
46 | assignees:
47 | - "{{ author }}"
48 |
49 | - name: backport to version-15-hotfix
50 | conditions:
51 | - label="backport version-15-hotfix"
52 | actions:
53 | backport:
54 | branches:
55 | - version-15-hotfix
56 | assignees:
57 | - "{{ author }}"
58 |
59 | - name: automatically merge backport if they pass tests
60 | conditions:
61 | - author=mergify[bot]
62 | - base~=^version-
63 | - head~=^mergify/bp/
64 | - label!=conflicts
65 | actions:
66 | merge:
67 | method: merge
68 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | exclude: "node_modules|.git"
2 | default_stages: [commit]
3 | fail_fast: false
4 |
5 | repos:
6 | - repo: https://github.com/pre-commit/pre-commit-hooks
7 | rev: v4.5.0
8 | hooks:
9 | - id: trailing-whitespace
10 | files: "razorpayx_integration/.*"
11 | exclude: ".*txt$|.*csv|.*md"
12 | - id: check-yaml
13 | - id: no-commit-to-branch
14 | args: ["--branch", "version-15"]
15 | - id: check-merge-conflict
16 | - id: check-ast
17 | - id: check-json
18 | - id: check-toml
19 | - id: debug-statements
20 |
21 | - repo: https://github.com/pre-commit/mirrors-prettier
22 | rev: v2.7.1
23 | hooks:
24 | - id: prettier
25 | types_or: [javascript, vue, scss]
26 | # Ignore any files that might contain jinja / bundles
27 | exclude: |
28 | (?x)^(
29 | razorpayx_integration/public/dist/.*|
30 | cypress/.*|
31 | .*node_modules.*|
32 | .*boilerplate.*
33 | )$
34 |
35 | - repo: https://github.com/pre-commit/mirrors-eslint
36 | rev: v8.44.0
37 | hooks:
38 | - id: eslint
39 | types_or: [javascript]
40 | args: ["--quiet"]
41 | # Ignore any files that might contain jinja / bundles
42 | exclude: |
43 | (?x)^(
44 | razorpayx_integration/public/dist/.*|
45 | cypress/.*|
46 | .*node_modules.*|
47 | .*boilerplate.*
48 | )$
49 |
50 | - repo: https://github.com/astral-sh/ruff-pre-commit
51 | rev: v0.2.0
52 | hooks:
53 | - id: ruff
54 | name: "Run ruff import sorter"
55 | args: ["--select=I", "--fix"]
56 |
57 | - id: ruff
58 | name: "Run ruff linter"
59 |
60 | - id: ruff-format
61 | name: "Run ruff formatter"
62 |
63 | ci:
64 | autoupdate_schedule: weekly
65 | skip: []
66 | submodules: false
67 |
--------------------------------------------------------------------------------
/.releaserc:
--------------------------------------------------------------------------------
1 | {
2 | "branches": [
3 | "version-15"
4 | ],
5 | "plugins": [
6 | "@semantic-release/commit-analyzer",
7 | {
8 | "preset": "angular",
9 | "releaseRules": [
10 | {
11 | "breaking": true,
12 | "release": false
13 | }
14 | ]
15 | },
16 | "@semantic-release/release-notes-generator",
17 | [
18 | "@semantic-release/exec",
19 | {
20 | "prepareCmd": "sed -i -E 's/\"[0-9]+\\.[0-9]+\\.[0-9]+\"/\"${nextRelease.version}\"/' razorpayx_integration/__init__.py"
21 | }
22 | ],
23 | [
24 | "@semantic-release/git",
25 | {
26 | "assets": [
27 | "razorpayx_integration/__init__.py"
28 | ],
29 | "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
30 | }
31 | ],
32 | "@semantic-release/github"
33 | ]
34 | }
35 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
RazorpayX Integration
4 |
5 | Power your ERPNext payments with RazorpayX – Automate payouts, reconcile transactions, and manage business finances effortlessly
6 |
7 |
8 |
9 |
10 |
11 | ## 💡 Motivation
12 |
13 | Bank integrations in India are usually costly and complex, mainly available to corporate.
14 |
15 | We choose RazorpayX because:
16 |
17 | - It is a tech layer over traditional bank accounts
18 | - Funds remain secure with a regulated bank
19 | - Onboarding process is hassle-free
20 | - No upfront cost, minimal charges per transaction, transparent pricing
21 | - Robust security
22 |
23 | ## ✨ Features
24 |
25 | - Automated bulk payouts for vendors
26 | - Real-time payment status tracking & transaction reconciliation
27 | - Support for multiple payment modes (IMPS/NEFT/RTGS/UPI)
28 | - Can make payment with Link
29 | - Daily sync bank transactions
30 | - Pre-built templates for workflows and notifications
31 | - Configurable to cater to diverse business processes
32 |
33 | **📝 Note:** This integration is designed for **domestic transactions within India 🇮🇳**. Foreign currency transactions are not supported.
34 |
35 | ## 📈 Why Use This Integration?
36 |
37 | - Save Time: Eliminate manual bank transactions from **Net Banking** portals
38 | - Reduce Errors: Auto-sync payment data between ERPNext and Bank
39 | - Financial Control: Approval workflows before initiating payouts
40 | - Secure: Role based access with 2FA to authorize manual payouts
41 |
42 | ## 📦 Installation
43 |
44 | **Prerequisites**
45 |
46 | - [ERPNext](https://github.com/frappe/erpnext) Version-15 or above
47 | - [Payment Integration Utils](https://github.com/resilient-tech/payment_integration_utils)
48 | - [Payments Processor](https://github.com/resilient-tech/payments-processor) (optional: to automate workflows)
49 |
50 | **Recommendations**
51 |
52 | - We recommend you to keep **ERPNext** version latest.
53 | - Before updating `RazorpayX Integration`, update `Payment Integration Utils` first.
54 |
55 | Choose one of the following methods to install RazorpayX Integration to your ERPNext site.
56 |
57 |
58 | ☁️ Frappe Cloud
59 |
60 | Sign up for a [Frappe Cloud](https://frappecloud.com/dashboard/signup?referrer=99df7a8f) free trial, create a new site with Frappe Version-15 or above, and install ERPNext and RazorpayX-Integration from the Apps.
61 |
62 |
63 |
64 |
65 | 🐳 Docker
66 |
67 | Use [this guide](https://github.com/frappe/frappe_docker/blob/main/docs/custom-apps.md) to deploy RazorpayX-Integration by building your custom image.
68 |
69 | Sample Apps JSON
70 |
71 | ```shell
72 | export APPS_JSON='[
73 | {
74 | "url": "https://github.com/frappe/erpnext",
75 | "branch": "version-15"
76 | },
77 | {
78 | "url": "https://github.com/resilient-tech/payment_integration_utils",
79 | "branch": "version-15"
80 | },
81 | {
82 | "url": "https://github.com/resilient-tech/razorpayx-integration",
83 | "branch": "version-15"
84 | }
85 | ]'
86 |
87 | export APPS_JSON_BASE64=$(echo ${APPS_JSON} | base64 -w 0)
88 | ```
89 |
90 |
91 |
92 |
93 | ⌨️ Manual
94 |
95 | Once you've [set up a Frappe site](https://frappeframework.com/docs/v14/user/en/installation/), install app by executing the following commands:
96 |
97 | Using Bench CLI
98 |
99 | Download the App using the Bench CLI
100 |
101 | ```sh
102 | bench get-app https://github.com/resilient-tech/payment_integration_utils.git --branch version-15
103 | ```
104 |
105 | ```sh
106 | bench get-app https://github.com/resilient-tech/razorpayx-integration.git --branch version-15
107 | ```
108 |
109 | Install the App on your site
110 |
111 | ```sh
112 | bench --site [site name] install-app razorpayx_integration
113 | ```
114 |
115 |
116 |
117 | ## 📚 Documentation
118 |
119 | 1. [Connect ERPNext with RazorpayX](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/setup/2_connect_erpnext_with_razorpayx.md)
120 | 2. [Make payout via RazorpayX within ERPNext](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/payout/3_make_payout.md)
121 | 3. [Reconcile Bank Transactions via RazorpayX API](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/reconcile/1_sync_and_reconcile_transactions.md)
122 | 4. [Analyze Payout Status Report](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/report/1_payout_status_report.md)
123 | 5. [FAQs](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/FAQS.md)
124 |
125 | - Read full documentation [here](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs)
126 |
127 | 🔗 **Google Form for Discount Pricing on RazorpayX Payout Fees**: [Apply Here](http://bit.ly/3FhJOaA)
128 |
129 | ## 🤝 Contributing
130 |
131 | - [Issue Guidelines](https://github.com/frappe/erpnext/wiki/Issue-Guidelines)
132 | - [Pull Request Requirements](https://github.com/frappe/erpnext/wiki/Contribution-Guidelines)
133 |
134 | ## 📜 License
135 |
136 | [GNU General Public License (v3)](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/license.txt)
137 |
--------------------------------------------------------------------------------
/commitlint.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parserPreset: "conventional-changelog-conventionalcommits",
3 | rules: {
4 | "subject-empty": [2, "never"],
5 | "type-case": [2, "always", "lower-case"],
6 | "type-empty": [2, "never"],
7 | "type-enum": [
8 | 2,
9 | "always",
10 | ["build", "chore", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test"],
11 | ],
12 | },
13 | };
14 |
--------------------------------------------------------------------------------
/docs/FAQS.md:
--------------------------------------------------------------------------------
1 | # Frequently Asked Questions (RazorpayX Integration for Frappe/ERPNext)
2 |
3 | ### 1. Which banks are supported by RazorpayX?
4 |
5 | **Supported Banks:**
6 | RazorpayX currently supports accounts with **RBL Bank, Yes Bank, IDFC First Bank, and Axis Bank** (the latter is selectively available for enterprise customers).
7 |
8 | **Note:**
9 |
10 | - A **new bank account** is required in most cases (existing accounts may not be compatible).
11 | - Check [RazorpayX’s official website](https://razorpay.com/x/current-accounts/) for the latest updates, as supported banks may change.
12 |
13 | ---
14 |
15 | ### 2. How do I sign up for RazorpayX?
16 |
17 | **Steps to Sign Up:**
18 |
19 | 1. Visit the [RazorpayX portal](https://x.razorpay.com/auth/signup) and sign up.
20 | 2. A RazorpayX representative will contact you to guide you through **documentation requirements**, which vary by business type (e.g., sole proprietorship, LLP, private limited).
21 | 3. The approval process typically takes **7–10 business days** after submitting complete documentation.
22 |
23 | **Pro Tip:** Keep your business PAN, GST, and incorporation documents ready to expedite the process.
24 |
25 | ---
26 |
27 | ### 3. What are the pricing and fees?
28 |
29 | **Special Community Pricing:**
30 |
31 | - **Zero setup fees** or SaaS charges.
32 | - **Transaction fees** start at **₹1 per payout** (varies by transaction type and volume).
33 | - **No hidden bank charges** for covered transaction categories (e.g., standard NEFT/IMPS/UPI payouts).
34 |
35 | **Note:**
36 |
37 | - Fill out the [Google Form for Discount Pricing](http://bit.ly/3FhJOaA) to get special pricing.
38 | - A **RazorpayX** representative will contact you with the discounted pricing details.
39 |
40 | ---
41 |
42 | ### 4. How do I switch between Test Mode and Live Mode?
43 |
44 | **Steps:**
45 |
46 | 1. **In RazorpayX Dashboard:**
47 | - Toggle between **Test** (Sandbox) and **Live** modes under *Settings*.
48 | 2. **In ERPNext:**
49 | - Navigate to **RazorpayX Configuration** (via the Frappe/ERPNext dashboard).
50 | - Ensure the **API Key** and **Secret Key** match the mode (Test/Live) selected in RazorpayX.
51 |
52 | **Important:**
53 |
54 | - Test mode requires sandbox credentials (provided by RazorpayX).
55 | - Never use live credentials in test mode, or vice versa.
56 |
57 | - For more details read [Setup Test and Live Mode](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/setup/1_setup_test_and_live_mode.md)
58 |
59 | ---
60 |
61 | ### 5. Can I use multiple RazorpayX accounts?
62 |
63 | **Yes!** Follow these steps:
64 |
65 | 1. **Configure Accounts:** Add each RazorpayX account in ERPNext under *Accounts > RazorpayX Configuration*.
66 | 2. **Select During Transactions:** When creating a **Payment Entry**, choose the desired bank account from the *Company Bank Account* dropdown.
67 |
68 | **Note:** Ensure API keys for all accounts are correctly mapped to avoid payout errors.
69 |
70 | ---
71 |
72 | ### Additional Tips
73 |
74 | - **Reconciliation:** Use the *Bank Reconciliation Tool* in ERPNext to sync RazorpayX transactions automatically.
75 | - **Security:** Rotate API keys quarterly or when team members with access leave.
76 |
77 | ---
78 |
79 | **Still Have Questions?**
80 | Visit [RazorpayX Documentation](https://razorpay.com/docs/x/) or connect with their support representative.
81 |
--------------------------------------------------------------------------------
/docs/accounting/1_fees_and_tax.md:
--------------------------------------------------------------------------------
1 | # 💰 Fees and Tax Accounting
2 |
3 | 
4 |
5 | For detailed information on charges and deductions, refer to RazorpayX's [Fees and Tax Documentation 🔗](https://razorpay.com/docs/x/manage-teams/billing/).
6 |
7 | ## ⚙️ Configuration Fields
8 |
9 | ### 1. **Automate Fees Accounting**
10 |
11 | - **Enabled by default** (`Checked`).
12 | - If enabled, a **Journal Entry (JE)** is created whenever a fee is deducted on a payout.
13 | - For payouts from a **Current Account**, the JE is recorded when the payout is **Processed**.
14 | - For payouts from **RazorpayX Lite**, the JE is recorded when the payout is in the **Processing** state.
15 |
16 | ### 2. **Creditors Account**
17 |
18 | - Used in Journal Entries to **debit the transaction fees**.
19 |
20 | 
21 |
22 | ### 3. **Supplier**
23 |
24 | - The **Party and Party Type (Supplier)** associated with the Creditors Account.
25 |
26 | ### 4. **Payable Account**
27 |
28 | - Used in Journal Entries to **credit the transaction fees**.
29 | - Only applicable when payouts are made from the **Current Account**.
30 |
31 | 
32 |
33 | ## 🔄 Journal Entry (JE) Creation Process
34 |
35 | ### 🏦 **Payouts from Current Account**
36 |
37 | - **Creditors Account** ➝ **Debited**
38 | - **Payable Account** ➝ **Credited**
39 |
40 | **Example Journal Entry**:
41 | 
42 |
43 | 📌 **Important Notes**:
44 |
45 | - When using the **Current Account**, fees are **not deducted immediately**. Instead, a JE is created to reflect the expected deduction.
46 | - At the end of the day, **RazorpayX deducts the accumulated transaction fees** for all payouts made that day.
47 | - For more details, refer to [Payouts from Current Account](https://razorpay.com/docs/x/manage-teams/billing/#payouts-from-current-account).
48 | - Since this is an anticipated fee deduction, this JE **will not be used for bank reconciliation**.
49 | - When RazorpayX deducts the fees, a final JE is created where:
50 | - **Payable Account** ➝ **Debited**
51 | - **Company Account (COA)** ➝ **Credited**
52 | - More details on this will be updated soon.
53 |
54 | ### 💳 **Payouts from RazorpayX Lite**
55 |
56 | - **Creditors Account** ➝ **Debited**
57 | - **Company Account (COA)** ➝ **Credited**
58 |
59 | **Company’s RazorpayX Bank Account**:
60 | 
61 |
62 | **Company Account**:
63 | 
64 |
65 | **Example Journal Entry**:
66 | 
67 |
68 | 📌 **Important Notes**:
69 |
70 | - In **RazorpayX Lite**, fees are **deducted immediately when the payout is created**.
71 | - For more details, refer to [Payouts from RazorpayX Lite](https://razorpay.com/docs/x/manage-teams/billing/#payouts-from-razorpayx-lite).
72 | - This JE **will be used for reconciliation** in the Bank Transaction.
73 |
74 | **Example Bank Transaction**:
75 | 
76 |
--------------------------------------------------------------------------------
/docs/accounting/2_payout_reversal.md:
--------------------------------------------------------------------------------
1 | # 🔄 Payout Reversal Accounting
2 |
3 | 
4 |
5 | ## ⚙️ Configuration Fields
6 |
7 | ### 1. **Create JE on Payout Reversal**
8 |
9 | - **Enabled by default** (`Checked`).
10 | - If enabled:
11 | - Unreconcile the **Payment Entry** from which the payout was initiated.
12 | - Create a **Reversal Journal Entry** for the Payment Entry, respecting its ledger.
13 | - Reverse the **Fees Journal Entry** (if applicable).
14 |
15 | ## 🔄 Journal Entry (JE) Creation Process
16 |
17 | ### **Payout Reversal JE**
18 |
19 | - Deductions are also reversed if applicable.
20 | - Deductions/Losses are handled automatically in the reversal JE.
21 |
22 | **Payment Entry**:
23 | 
24 |
25 | **Accounting Ledger of Payment Entry**:
26 | 
27 |
28 | **Reversal JE**:
29 | 
30 |
31 | ### **Fees Reversal JE**
32 |
33 | - If fees were deducted during the original payout, a **Fees Reversal JE** is created to reverse the fees.
34 |
35 | 
36 |
37 | ### **Bank Transaction**
38 |
39 | - For reconciliation, both reversal JEs are used for the **Deposit** (credit) of the reversed amount.
40 | - If the payout was made from **RazorpayX Lite**, the **Fees Reversal JE** will be referenced.
41 | - Otherwise, only the **Payout Reversal JE** will be referenced.
42 |
43 | 
44 |
--------------------------------------------------------------------------------
/docs/payout/1_requirements.md:
--------------------------------------------------------------------------------
1 | # 📋 Requirements Before Making Payout
2 |
3 | ## 🔐 Roles and Permissions
4 |
5 | 1. **Online Payments Authorizer Role**:
6 | - The user must have the `Online Payments Authorizer` role to initiate payouts.
7 |
8 | 2. **Submit Permission for Payment Entry**:
9 | - The user must have permission to **submit** the **Payment Entry**.
10 |
11 | 3. **Read Permission for RazorpayX Configuration**:
12 | - The user must have at least **read** permission for the **RazorpayX Configuration** document.
13 |
14 | ## 🔒 Authentication Setup
15 |
16 | - For detailed steps on setting up authentication, refer to the [Authentication Guide](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/payout/2_Authentication.md).
17 |
18 | ## 💳 Payment Entry Requirements
19 |
20 | 1. **Payment Type**:
21 | - Set the payment type to **Pay**.
22 |
23 | 2. **Company Bank Account**:
24 | - Ensure the selected **Company Bank Account** is linked to the RazorpayX Configuration.
25 |
26 | 3. **Paid from Account Currency**:
27 | - The currency must be set to **INR** (Indian Rupees).
28 |
29 | 4. **Make Online Payment**:
30 | - If **Checked**:
31 | - The payout will be initiated automatically when the Payment Entry is submitted (via `Pay and Submit`).
32 | - If **Unchecked**:
33 | - The payout can be initiated manually after submission, provided the RazorpayX Configuration (via `Company bank Account`) is available.
34 |
--------------------------------------------------------------------------------
/docs/payout/2_Authentication.md:
--------------------------------------------------------------------------------
1 | # 🔐 Authentication for Making Payout
2 |
3 | ## ⚙️ Setup 2FA
4 |
5 | 1. Go to **System Settings** > **Login** tab.
6 | 2. Scroll down to the **Payment Integration** section.
7 | 3. Set the **Payment Authentication Method**.
8 | - Currently, only **OTP App** is available.
9 | - In future updates, **SMS** and **Email** will be supported.
10 | 4. Optionally, set up an **OTP Issuer Name** (e.g., Company Name or Site Name).
11 |
12 | 
13 |
14 | **Note**:
15 |
16 | - A default **Outgoing Email Account** is required to send emails for authentication.
17 | - The **Administrator** role **cannot** make payouts.
18 | - If the **Administrator** impersonates another user, they will see the **Make Payout** | **Pay and Submit** button but **cannot authenticate** to complete the payout.
19 |
20 | ## 🔢 OTP Dialog Box
21 |
22 | ### First-Time OTP Generation
23 |
24 | 
25 |
26 | ### If OTP App already configured
27 |
28 | 
29 |
30 | ## 📧 Sample Emails
31 |
32 | ### Email for QR Code Link
33 |
34 | 
35 |
36 | ### After Scanning QR Code
37 |
38 | 
39 |
40 | ## 🔄 Reset Payment OTP Secret
41 |
42 | - **Prerequisites**:
43 | - The user must have the **Online Payments Authorizer** role.
44 | - The **Payment Authentication Method** must be set to **OTP App**.
45 |
46 | - **Steps**:
47 | 1. Go to **User** > **Password**.
48 | 2. Click on **Reset Payment OTP Secret**.
49 |
50 | 
--------------------------------------------------------------------------------
/docs/payout/3_make_payout.md:
--------------------------------------------------------------------------------
1 | # 💳 Make RazorpayX Payout with Payment Entry
2 |
3 | ## 📋 Prerequisites
4 |
5 | - Ensure all [Payout Requirements](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/payout/1_requirements.md) are met.
6 | - Complete the [Authentication Setup](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/payout/2_Authentication.md) for secure transactions.
7 |
8 | ## 🚀 Make Payout on Payment Entry Submission
9 |
10 | 1. Create a **Payment Entry** with the following details:
11 | - **Payment Type**: Pay
12 | - **Company Bank Account**: Linked to RazorpayX Configuration
13 | - **Paid from Account Currency**: INR
14 | - **Make Online Payment**: Check this option to initiate the payout on submission.
15 |
16 | 2. Submit the Payment Entry to trigger the payout via `Pay and Submit`
17 |
18 | https://github.com/user-attachments/assets/15fca87c-eb1c-4173-b401-2fffa4e10888
19 |
20 | **Note**:
21 |
22 | - If a **Workflow** is active for the Payment Entry, the `Pay and Submit` button will not be available, and `Make Online Payment` will be unchecked if previously checked.
23 |
24 | ## ⏳ Make Payout After Payment Entry Submission
25 |
26 | - If the Payment Entry is submitted without checking `Make Online Payment`, user can still initiate the payout manually.
27 | - A custom button, **Make Payout**, will be available if the Company's Bank Account is valid and linked to RazorpayX.
28 |
29 | https://github.com/user-attachments/assets/53b18844-88aa-403d-aad3-c07478a76a51
30 |
31 | ## 📦 Bulk Payout
32 |
33 | 1. Select multiple **Payment Entries** in **Draft** status with valid payout information.
34 | 2. Use the **Pay and Submit** bulk action to initiate payouts for all selected entries.
35 | - **Make Online Payment** is optional and can be marked during the bulk action.
36 |
37 | **Recommendations for Bulk Payouts**:
38 |
39 | - Ensure each Payment Entry has valid:
40 | - **Company Bank Account**
41 | - **Party Bank Account**
42 | - **Payment Transfer Method**
43 | - **Contact Details** (if using **Link** for payment).
44 |
45 | - If a **Party Bank Account** is selected, the payout will be made via **NEFT** by default. Otherwise, it will be made via **Link**.
46 |
47 | https://github.com/user-attachments/assets/5cf6cb2d-3e06-4042-8295-68caae710050
48 |
49 | **Note:** A maximum of 500 Payment Entries are supported for bulk payouts.
50 |
--------------------------------------------------------------------------------
/docs/payout/4_bulk_submit_without_payout.md:
--------------------------------------------------------------------------------
1 | # 📦 Bulk Submit for Marked Online Payment
2 |
3 | ## 🛠️ How Bulk Submission Works
4 |
5 | 1. **Bulk Submit Payment Entries**:
6 | - When users bulk submit **Payment Entries** from the list view using the **Actions** menu (instead of **Pay and Submit**):
7 | - For Payment Entries with **Make Online Payment** checked, this option will be **automatically unchecked**.
8 | - The Payment Entries will be submitted **without initiating payouts**.
9 |
10 | 2. **Make Payout After Submission**:
11 | - After submission, users can still make payouts manually using the **Make Payout** button for each Payment Entry.
12 |
13 | https://github.com/user-attachments/assets/56502a54-8e94-4160-8f7f-4feefcc60852
14 |
15 | **Note**: The example above demonstrates the process for one Payment Entry, but the same applies to any number of entries.
16 |
--------------------------------------------------------------------------------
/docs/payout/5_cancel_payout.md:
--------------------------------------------------------------------------------
1 | # 🚫 Workflow to Cancel Payout
2 |
3 | ## 🛠️ Cancellation Conditions
4 |
5 | - A **Payout** can only be canceled in the following states:
6 |
7 | 1. **Not Initiated** (A custom state defined in RazorpayX Integration)
8 | 2. **Queued**
9 |
10 | ## 📝 Steps to Cancel a Payout
11 |
12 | 1. **Cancel the Payment Entry**:
13 | - To cancel a payout, cancel the **Payment Entry** from which the payout was made.
14 |
15 | 2. **Confirmation Dialog**:
16 | - If the payout is in a cancellable state, a confirmation dialog will appear to confirm the cancellation.
17 |
18 | https://github.com/user-attachments/assets/0ea12c0f-6a5e-40c2-bbf5-eb829ba9ea76
19 |
20 | ## 📌 Notes
21 |
22 | - If **Auto Cancellation** is enabled in the `RazorpayX Configuration`, the dialog box will not be shown, and the payout will be **canceled automatically**.
23 | - You can cancel a `Payment Entry` regardless of the `RazorpayX Payout Status`.
24 |
--------------------------------------------------------------------------------
/docs/payout/6_setup_status_notification.md:
--------------------------------------------------------------------------------
1 | # 🔔 Notifications
2 |
3 | 
4 |
5 | ## 📩 Notification Types
6 |
7 | Two custom notifications are provided:
8 |
9 | 1. **Payout Failed/Reversed/Canceled**:
10 | - Sent to the user who initiated the payout.
11 | - Notifies them about the failure, reversal, or cancellation of the payout.
12 |
13 | 2. **Payout Processed**:
14 | - Sent to the **party** (Supplier/Employee etc.) if their contact email is available.
15 | - Notifies them about the successful processing of the payout.
16 |
17 | ## 🛠️ How to Enable Notifications
18 |
19 | 1. Go to **Notification** in ERPNext Site.
20 | 2. Enable the relevant notifications for **Payout Failed/Reversed/Canceled** and **Payout Processed**.
21 |
22 | ## 📌 Notes
23 |
24 | - Ensure the **party's contact email** is correctly configured to receive **Payout Processed** notifications.
25 | - Customize the notification templates to match your business needs.
26 | - Default `Outgoing EMail Account` is require to send notifications.
27 |
--------------------------------------------------------------------------------
/docs/payout/7_payout_tips_and_notes.md:
--------------------------------------------------------------------------------
1 | # 💡 Payout Tips and Notes
2 |
3 | ## 🌐 Payout Link
4 |
5 | - To pay via **Payout Link**, choose the payment transfer method as **Link**.
6 |
7 | - Payouts created via **Payout Link** are managed as payouts, not as payout links.
8 | - **Example**:
9 | - Only **Payout** statuses are maintained as `RazorpayX Payout Status`.
10 | - The status will show as `Not Initiated` or `Queued` until the payout is created.
11 | - The payout is created when the party provides bank details via the link.
12 | - Once the payout is initiated, the status updates based on webhook events.
13 | - If the payout is **Canceled**, **Failed**, **Rejected**, or **Reversed**, the integration system will attempt to cancel the **Payout Link**.
14 |
15 | - **Party's Contact Details**:
16 | - To create a Payout Link, the party's contact details (email or mobile) are mandatory.
17 | - For **Employees**: The preferred email or mobile number must be set.
18 | - For others: Select the **Contact** details.
19 |
20 | ## 📝 General Notes
21 |
22 | - **Make Online Payment Checkbox**:
23 | - This checkbox appears after saving the Payment Entry (PE) for the first time if the integration is found via the Company's Bank Account and the user has the necessary permissions.
24 |
25 | - **Reconfiguring RazorpayX**:
26 | - If RazorpayX is configured after creating the PE, reselect the **Company Bank Account** and save the PE to set up the integration.
27 |
28 | - **Amended Payment Entries**:
29 | - If an **Amended Payment Entry** has its original PE marked for `Make Online Payment`, you cannot make a payout with the amended PE.
30 | - Payment details cannot be changed in such cases.
31 | - In future updates, if the original PE's payout is **Failed/Reversed/Canceled**, the amended PE will allow creating a payout.
32 |
33 | - **Payout or Payout Link Canceled/Failed**:
34 | - If a **Payout** or **Payout Link** is **Canceled/Failed** and the webhook event is captured, the Payment Entry will also be canceled.
35 |
36 | - **Payout Reversed**:
37 | - If a **Payout** is **Reversed**, only the payout status is updated, and the PE is not canceled.
38 | - Reversal Journal Entries (JE) for the Payment Entry and Fees Reversal JE will be created if configured.
39 | - For more details on Reversal Accounting, read [here](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/accounting/2_payout_reversal.md).
40 |
41 | - **Payout Description**:
42 | - Maximum length: 30 characters.
43 | - Allowed characters: `a-z`, `A-Z`, `0-9`, and spaces.
44 |
45 | - **UTR in Payment Entry**:
46 | - The **UTR** will be set after the payout is **Processed**.
47 | - Until then, the default placeholder will be:
48 |
49 | ```bash
50 | *** UTR WILL BE SET AUTOMATICALLY ***
51 | ```
52 |
--------------------------------------------------------------------------------
/docs/reconcile/1_sync_and_reconcile_transactions.md:
--------------------------------------------------------------------------------
1 | # 🔄 Sync and Reconciliation Bank Transactions via RazorpayX API
2 |
3 | ## 🛠️ Manual Transaction Sync
4 |
5 | ### In RazorpayX Configuration
6 |
7 | - Click on **Sync Transaction** and select the desired time period.
8 | 
9 |
10 | ### In the Bank Reconciliation Tool
11 |
12 | - Ensure the bank account is linked to a **RazorpayX Configuration** to enable transaction syncing.
13 |
14 | 📌 **Example:**
15 |
16 | https://github.com/user-attachments/assets/559d2d8f-4a07-4da2-bd79-57c85e6b8808
17 |
18 | ## 🔄 Automatic Sync
19 |
20 | - Transactions are automatically synced **daily** via a scheduled cron job.
21 |
--------------------------------------------------------------------------------
/docs/report/1_payout_status_report.md:
--------------------------------------------------------------------------------
1 | # 📊 RazorpayX Payout Status Report
2 |
3 | This report provides a quick overview of:
4 |
5 | - **Payout Status**
6 | - **UTR Details**
7 | - **Payout Mode** etc...
8 |
9 | ## 🔍 Filters
10 |
11 | **You can filter the report by:**
12 |
13 | - **Posting Date**: Filter payouts by specific dates.
14 | - **Payout Status**: Filter by status (e.g., Processed, Failed, Queued).
15 | - **Doc Status**: Filter by document status (e.g., Submitted, Canceled).
16 | - **Payout Mode**: Filter by payment method (e.g., NEFT, UPI, Link).
17 |
18 | ## 🔎 Accessing the Report
19 |
20 | - In the **Search Bar (Awesome Bar)**, type `RazorpayX Payout Status` to view the report.
21 |
22 | **Example**:
23 |
24 | 
25 |
--------------------------------------------------------------------------------
/docs/setup/1_setup_test_and_live_mode.md:
--------------------------------------------------------------------------------
1 | # Setup Test and Live Mode for RazorpayX Integration
2 |
3 | ## Live Mode (Production Mode) in RazorpayX Dashboard
4 |
5 | - **Purpose**: In Live Mode, actual money will be debited from your bank account.
6 | - **Use Case**: Use this mode only for real transactions in a production environment.
7 | - **Important**:
8 | - Do **not** share API keys and secrets generated in this mode.
9 | - Ensure proper security measures are in place to protect sensitive credentials.
10 |
11 | ## Test Mode in RazorpayX Dashboard
12 |
13 | - **Purpose**: In Test Mode, you can simulate transactions without debiting real money from your bank account.
14 | - **Test Balance**: Add test balance [here](https://x.razorpay.com/).
15 | 
16 |
17 | - **How to Enable Test Mode**:
18 | 1. Go to [Developer Controls](https://x.razorpay.com/settings/developer-controls).
19 | 2. Enable **Test Mode**.
20 | 
21 |
22 | - **Test Mode Dashboard**:
23 | After enabling Test Mode, your dashboard will look like this:
24 | 
25 |
26 | - **Manually Change Payout Status in Test Mode**:
27 | - In Test Mode, payout statuses need to be changed manually.
28 | - You can only change the status if it is not in a final state.
29 | - Go to [Payouts](https://x.razorpay.com/payouts).
30 | - Hover over any payout and click on **Change Status**.
31 | 
32 |
33 | ## Setup Key ID and Key Secret
34 |
35 | - **For Both Modes**:
36 | The process to generate **Key ID** and **Key Secret** is the same for both Test and Live modes.
37 | - **Important**: Never share Live Mode API credentials.
38 |
39 | ## Setup Webhook
40 |
41 | - **For Both Modes**:
42 | The process to set up webhooks is the same for both Test and Live modes.
43 |
44 | - **Test Mode Specific**:
45 | When saving the webhook in Test Mode, use the fixed OTP:
46 |
47 | ```bash
48 | 754081
49 | ```
50 |
51 | ## Setup Test and Live Mode in ERPNext Site
52 |
53 | 1. **Add Credentials**:
54 | - For each mode (Test or Live), add the corresponding **Key ID** and **Key Secret** in the ERPNext RazorpayX Configuration.
55 |
56 | 2. **Setup Webhook**:
57 | - Configure the webhook URL for each mode separately.
58 |
59 | ## Local Testing
60 |
61 | - **Use Test Mode Credentials**:
62 | - For local testing, always use Test Mode credentials to avoid real transactions.
63 |
64 | - **Live Webhook URL for Local Testing**:
65 | To test live webhooks locally, use a tool like **Ngrok**. Replace the URL in the webhook configuration as follows:
66 |
67 | Follow this [Guide](https://discuss.frappe.io/t/guide-for-using-ngrok-for-webhook-testing/141902) to get the Ngrok URL.
68 |
69 | ```bash
70 | NGROK_URL/api/method/razorpayx_integration.razorpayx_integration.utils.webhook.webhook_listener
71 | ```
72 |
73 | Replace `NGROK_URL` with the URL generated via Ngrok.
74 |
--------------------------------------------------------------------------------
/docs/setup/2_connect_erpnext_with_razorpayx.md:
--------------------------------------------------------------------------------
1 | # 🚀 Connect ERPNext with RazorpayX
2 |
3 | ## 📝 Step 1: Create a Company Bank Account with RazorpayX Details
4 |
5 | If you already have a bank account configured, you can skip this step.
6 |
7 | ### Mandatory Fields
8 |
9 | - **Is Company Account**: Enable this option.
10 | - **Bank Account No.**: Add your bank account number associated with RazorpayX.
11 |
12 | 
13 |
14 | ### For Test Mode
15 |
16 | - **Bank Account No.**: Use the **Customer Identifier** from your RazorpayX account. [Get it from here](https://x.razorpay.com/settings/banking).
17 |
18 | 
19 |
20 | ### For Live/Production Mode
21 |
22 | - **Bank Account No.**: Use your **Current Account Number** or **Customer Identifier** as per your requirement.
23 | 
24 |
25 | ## ⚙️ Step 2: Create a RazorpayX Configuration
26 |
27 | 1. In your ERPNext site, search for `RazorpayX Configuration` in the search bar and open the list view.
28 | 2. Add a new configuration.
29 |
30 | 
31 |
32 | ### Get API Credentials
33 |
34 | - **API Key** and **API Secret**:
35 | - If not available, [generate them from here](https://x.razorpay.com/settings/developer-controls).
36 | - **Note**: For first-time generation, a direct button is available to create the `KEY` and `SECRET`.
37 |
38 | 
39 |
40 | - **Account ID**:
41 | - This is your **Business ID** provided by RazorpayX. [Get it from here](https://x.razorpay.com/settings/business).
42 |
43 | 
44 |
45 | ### 🌐 Set Up Webhooks
46 |
47 | Webhooks are used for real-time payout status updates.
48 |
49 | 1. **Copy Webhook URL**: Click the `Copy Webhook URL` button in the RazorpayX Configuration.
50 | 
51 |
52 | 2. **Add Webhook URL to RazorpayX Dashboard**: Paste the URL [here](https://x.razorpay.com/settings/developer-controls).
53 | 
54 |
55 | - **Note**: Only enable supported webhooks.
56 |
57 | 3. **Supported Webhooks (11 Events)**:
58 |
59 | ```shell
60 | # Payout
61 | - payout.pending
62 | - payout.rejected
63 | - payout.queued
64 | - payout.initiated
65 | - payout.processed
66 | - payout.reversed
67 | - payout.failed
68 | # Payout Link
69 | - payout_link.cancelled
70 | - payout_link.rejected
71 | - payout_link.expired
72 | # Transaction
73 | - transaction.created
74 | ```
75 |
76 | For more details, visit [RazorpayX Webhook Events](https://razorpay.com/docs/x/apis/subscribe/#webhook-events-and-descriptions).
77 |
78 | 4. **Add Webhook Secret**: Ensure you add the webhook secret; otherwise, ERPNext won’t receive updates.
79 | - A strong webhook secret is recommended for enhanced security.
80 |
81 | **Note**:
82 |
83 | - Test and Live modes require different **API Keys** and **Webhook URLs**.
84 | - For more details on Test and Live modes, [visit here](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/setup/1_setup_test_and_live_mode.md).
85 |
86 | ### 🏦 Set Company Bank Account
87 |
88 | - Set the bank account that is associated with RazorpayX.
89 |
90 | - Set **Payouts From**
91 |
92 | - Two options are available:
93 | 1. **Current Account**
94 | 2. **RazorpayX Lite**
95 |
96 | - **Current Account**:
97 | - If the **Current Account Number** is entered in the company's bank account details, the payout amount is deducted from the bank's current account.
98 |
99 | - **RazorpayX Lite**:
100 | - If the **Customer Identifier** is entered, the payout amount is deducted from the RazorpayX Lite account.
101 |
102 | - **Default Selection**: **Current Account** is selected by default.
103 |
104 | ### 🤖 Configure Automation
105 |
106 | 1. **Automatically Cancel Payout on Payment Entry Cancellation**
107 | - If checked, the payout and payout link will be canceled automatically upon Payment Entry cancellation (helpful for bulk cancellations).
108 | - If unchecked, a confirmation dialog will appear for single Payment Entry cancellations if the payout or payout link is cancellable.
109 | - See [Cancellation Workflow](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/payout/5_cancel_payout.md) for more details.
110 |
111 | 2. **Pay on Auto Submit**
112 | - This feature is only available if the [Payments Processor](https://github.com/resilient-tech/payments-processor) app is installed.
113 | - **Checked** ☑️ by default.
114 | - If a `Payment Entry` is submitted via automation, the payout will be made if `Make Online Payment` is checked in the Payment Entry.
115 | - If unchecked and the Payment Entry is submitted with the `initiated_by_payment_processor` flag, the payout will not be made, and `Make Online Payment` will be unchecked.
116 | - 
117 |
118 | ### 🤖 Accounting
119 |
120 | - For detailed information, see [Accounting with RazorpayX Integration](https://github.com/resilient-tech/razorpayx-integration/blob/version-15/docs/accounting).
121 |
122 | ---
123 |
124 | ### ⚙️ Multiple configurations with different bank accounts for the same company associated with RazorpayX are allowed!
125 |
--------------------------------------------------------------------------------
/pyproject.toml:
--------------------------------------------------------------------------------
1 | [project]
2 | name = "razorpayx_integration"
3 | authors = [
4 | { name = "Resilient Tech", email = "info@resilient.tech"}
5 | ]
6 | description = "Automat Payments By RazorpayX API For Frappe Apps"
7 | requires-python = ">=3.10"
8 | readme = "README.md"
9 | dynamic = ["version"]
10 |
11 |
12 | [build-system]
13 | requires = ["flit_core >=3.4,<4"]
14 | build-backend = "flit_core.buildapi"
15 |
16 | [tool.ruff.lint]
17 | select = [
18 | "F",
19 | "E",
20 | "W",
21 | "I",
22 | "UP",
23 | "B",
24 | "RUF",
25 | ]
26 |
27 | ignore = [
28 | "E501", # line too long
29 | "F401", # module imported but unused
30 | "F403", # can't detect undefined names from * import
31 | "F405", # can't detect undefined names from * import
32 | ]
33 |
34 | typing-modules = ["frappe.types.DF"]
35 |
36 | [tool.ruff.format]
37 | quote-style = "double"
38 | indent-style = "space"
39 | docstring-code-format = true
40 |
41 | # These dependencies are only installed when developer mode is enabled
42 | [tool.bench.dev-dependencies]
43 | # package_name = "~=1.1.0"
44 |
45 | [tool.bench.frappe-dependencies]
46 | frappe = ">=15.0.0,<16.0.0"
47 | erpnext = ">=15.0.0,<16.0.0"
48 |
--------------------------------------------------------------------------------
/razorpayx_integration/__init__.py:
--------------------------------------------------------------------------------
1 | __version__ = "16.0.0-dev"
2 |
--------------------------------------------------------------------------------
/razorpayx_integration/config/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/config/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/config/desktop.py:
--------------------------------------------------------------------------------
1 | from frappe import _
2 |
3 |
4 | def get_data():
5 | return [
6 | {
7 | "module_name": "Razorpayx Integration",
8 | "type": "module",
9 | "label": _("Razorpayx Integration"),
10 | }
11 | ]
12 |
--------------------------------------------------------------------------------
/razorpayx_integration/config/docs.py:
--------------------------------------------------------------------------------
1 | """
2 | Configuration for docs
3 | """
4 |
5 | # source_link = "https://github.com/[org_name]/razorpayx_integration"
6 | # headline = "App that does everything"
7 | # sub_heading = "Yes, you got that right the first time, everything"
8 |
9 |
10 | def get_context(context):
11 | context.brand_html = "Razorpayx Integration"
12 |
--------------------------------------------------------------------------------
/razorpayx_integration/constants.py:
--------------------------------------------------------------------------------
1 | BUG_REPORT_URL = "https://github.com/resilient-tech/razorpayx_integration/issues/new"
2 |
3 | RAZORPAYX_CONFIG = "RazorpayX Configuration"
4 |
5 | PAYMENTS_PROCESSOR_APP = "payments_processor"
6 |
--------------------------------------------------------------------------------
/razorpayx_integration/hooks.py:
--------------------------------------------------------------------------------
1 | app_name = "razorpayx_integration"
2 | app_title = "RazorpayX Integration"
3 | app_publisher = "Resilient Tech"
4 | app_description = "Automat Payments By RazorpayX API For Frappe Apps"
5 | app_email = "info@resilient.tech"
6 | app_license = "GNU General Public License (v3)"
7 | required_apps = ["frappe/erpnext", "resilient-tech/payment_integration_utils"]
8 |
9 | after_install = "razorpayx_integration.install.after_install"
10 | before_uninstall = "razorpayx_integration.uninstall.before_uninstall"
11 |
12 | after_app_install = "razorpayx_integration.install.after_app_install"
13 | before_app_uninstall = "razorpayx_integration.uninstall.before_app_uninstall"
14 |
15 | app_include_js = "razorpayx_integration.bundle.js"
16 |
17 | export_python_type_annotations = True
18 |
19 | doctype_js = {
20 | "Payment Entry": "razorpayx_integration/client_overrides/form/payment_entry.js",
21 | "Bank Reconciliation Tool": "razorpayx_integration/client_overrides/form/bank_reconciliation_tool.js",
22 | }
23 |
24 |
25 | doc_events = {
26 | "Payment Entry": {
27 | "onload": "razorpayx_integration.razorpayx_integration.server_overrides.doctype.payment_entry.onload",
28 | "validate": "razorpayx_integration.razorpayx_integration.server_overrides.doctype.payment_entry.validate",
29 | "before_submit": "razorpayx_integration.razorpayx_integration.server_overrides.doctype.payment_entry.before_submit",
30 | "on_submit": "razorpayx_integration.razorpayx_integration.server_overrides.doctype.payment_entry.on_submit",
31 | "before_cancel": "razorpayx_integration.razorpayx_integration.server_overrides.doctype.payment_entry.before_cancel",
32 | },
33 | }
34 |
35 | scheduler_events = {
36 | "daily": [
37 | "razorpayx_integration.razorpayx_integration.utils.bank_transaction.sync_transactions_periodically"
38 | ]
39 | }
40 |
41 | payment_integration_fields = [
42 | "razorpayx_payout_desc",
43 | "razorpayx_payout_status",
44 | "razorpayx_payout_id",
45 | "razorpayx_payout_link_id",
46 | ]
47 |
--------------------------------------------------------------------------------
/razorpayx_integration/install.py:
--------------------------------------------------------------------------------
1 | import click
2 | import frappe
3 |
4 | from razorpayx_integration.constants import BUG_REPORT_URL, PAYMENTS_PROCESSOR_APP
5 | from razorpayx_integration.hooks import app_title as APP_NAME
6 | from razorpayx_integration.setup import (
7 | create_payments_processor_custom_fields,
8 | setup_customizations,
9 | )
10 |
11 | POST_INSTALL_PATCHES = []
12 |
13 |
14 | def after_install():
15 | try:
16 | setup_customizations()
17 | run_post_install_patches()
18 |
19 | except Exception as e:
20 | click.secho(
21 | (
22 | f"Installation of {APP_NAME} failed due to an error. "
23 | "Please try re-installing the app or "
24 | f"report the issue on {BUG_REPORT_URL} if not resolved."
25 | ),
26 | fg="bright_red",
27 | )
28 | raise e
29 |
30 | click.secho(f"Thank you for installing {APP_NAME}!!\n", fg="green")
31 |
32 |
33 | def run_post_install_patches():
34 | if not POST_INSTALL_PATCHES:
35 | return
36 |
37 | click.secho("Running post-install patches...", fg="yellow")
38 |
39 | if not frappe.db.exists("Company", {"country": "India"}):
40 | return
41 |
42 | frappe.flags.in_patch = True
43 |
44 | try:
45 | for patch in POST_INSTALL_PATCHES:
46 | patch_module = f"razorpayx_integration.patches.post_install.{patch}.execute"
47 | frappe.get_attr(patch_module)()
48 |
49 | finally:
50 | frappe.flags.in_patch = False
51 |
52 |
53 | def after_app_install(app_name):
54 | if app_name == PAYMENTS_PROCESSOR_APP:
55 | create_payments_processor_custom_fields()
56 |
--------------------------------------------------------------------------------
/razorpayx_integration/modules.txt:
--------------------------------------------------------------------------------
1 | Razorpayx Integration
--------------------------------------------------------------------------------
/razorpayx_integration/patches.txt:
--------------------------------------------------------------------------------
1 | [pre_model_sync]
2 |
3 |
4 | [post_model_sync]
5 | execute:from razorpayx_integration.setup import create_custom_fields; create_custom_fields() # 1
6 | execute:from razorpayx_integration.setup import create_property_setters; create_property_setters() # 1
7 | execute:from razorpayx_integration.setup import create_roles_and_permissions; create_roles_and_permissions()
8 | razorpayx_integration.patches.set_payment_transfer_method
9 | razorpayx_integration.patches.delete_old_custom_fields
10 | razorpayx_integration.patches.delete_old_property_setters
11 | razorpayx_integration.patches.update_integration_doctype
12 | razorpayx_integration.patches.set_default_payouts_from
13 | razorpayx_integration.patches.mark_creation_of_je_on_reversal
14 |
--------------------------------------------------------------------------------
/razorpayx_integration/patches/delete_old_custom_fields.py:
--------------------------------------------------------------------------------
1 | from payment_integration_utils.payment_integration_utils.setup import (
2 | delete_custom_fields,
3 | )
4 |
5 | FIELDS_TO_DELETE = {
6 | "Payment Entry": ["razorpayx_payout_mode", "razorpayx_pay_instantaneously"],
7 | "Bank Account": [
8 | "online_payment_section",
9 | "online_payment_mode",
10 | "online_payment_cb",
11 | ],
12 | }
13 |
14 |
15 | def execute():
16 | delete_custom_fields(FIELDS_TO_DELETE)
17 |
--------------------------------------------------------------------------------
/razorpayx_integration/patches/delete_old_property_setters.py:
--------------------------------------------------------------------------------
1 | from payment_integration_utils.payment_integration_utils.setup import (
2 | delete_property_setters,
3 | )
4 |
5 | PROPERTY_SETTERS_TO_DELETE = [
6 | ## Payment Entry ##
7 | {
8 | "doctype": "Payment Entry",
9 | "fieldname": "contact_person",
10 | "property": "mandatory_depends_on",
11 | },
12 | {
13 | "doctype": "Payment Entry",
14 | "fieldname": "party_upi_id",
15 | "property": "mandatory_depends_on",
16 | },
17 | {
18 | "doctype": "Payment Entry",
19 | "fieldname": "party_upi_id",
20 | "property": "depends_on",
21 | },
22 | {
23 | "doctype": "Payment Entry",
24 | "fieldname": "party_bank_account_no",
25 | "property": "mandatory_depends_on",
26 | },
27 | {
28 | "doctype": "Payment Entry",
29 | "fieldname": "party_bank_account_no",
30 | "property": "depends_on",
31 | },
32 | {
33 | "doctype": "Payment Entry",
34 | "fieldname": "party_bank_ifsc",
35 | "property": "mandatory_depends_on",
36 | },
37 | {
38 | "doctype": "Payment Entry",
39 | "fieldname": "party_bank_ifsc",
40 | "property": "depends_on",
41 | },
42 | ## Bank Account ##
43 | {
44 | "doctype": "Bank Account",
45 | "fieldname": "online_payment_mode",
46 | "property": "options",
47 | },
48 | {
49 | "doctype": "Bank Account",
50 | "fieldname": "online_payment_mode",
51 | "property": "description",
52 | },
53 | {
54 | "doctype": "Bank Account",
55 | "fieldname": "online_payment_mode",
56 | "property": "depends_on",
57 | },
58 | {
59 | "doctype": "Bank Account",
60 | "fieldname": "bank_account_no",
61 | "property": "mandatory_depends_on",
62 | },
63 | {
64 | "doctype": "Bank Account",
65 | "fieldname": "branch_code",
66 | "property": "mandatory_depends_on",
67 | },
68 | ]
69 |
70 |
71 | def execute():
72 | delete_property_setters(PROPERTY_SETTERS_TO_DELETE)
73 |
--------------------------------------------------------------------------------
/razorpayx_integration/patches/mark_creation_of_je_on_reversal.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from razorpayx_integration.constants import RAZORPAYX_CONFIG
4 |
5 |
6 | def execute():
7 | frappe.db.set_value(RAZORPAYX_CONFIG, {}, "create_je_on_reversal", 1)
8 |
--------------------------------------------------------------------------------
/razorpayx_integration/patches/post_install/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/patches/post_install/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/patches/set_default_payouts_from.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from razorpayx_integration.constants import RAZORPAYX_CONFIG
4 | from razorpayx_integration.razorpayx_integration.constants.payouts import PAYOUT_FROM
5 |
6 |
7 | def execute():
8 | frappe.db.set_value(
9 | RAZORPAYX_CONFIG, {}, "payouts_from", PAYOUT_FROM.CURRENT_ACCOUNT.value
10 | )
11 |
--------------------------------------------------------------------------------
/razorpayx_integration/patches/set_payment_transfer_method.py:
--------------------------------------------------------------------------------
1 | import frappe
2 | from payment_integration_utils.payment_integration_utils.constants.payments import (
3 | TRANSFER_METHOD,
4 | )
5 |
6 |
7 | def execute():
8 | payment_entries = frappe.get_all(
9 | "Payment Entry",
10 | filters={"make_bank_online_payment": 1},
11 | fields=[
12 | "name",
13 | "razorpayx_payout_mode",
14 | "razorpayx_pay_instantaneously",
15 | "paid_amount",
16 | ],
17 | )
18 |
19 | if not payment_entries:
20 | return
21 |
22 | updated_payment_entries = {}
23 |
24 | for payment_entry in payment_entries:
25 | if payment_entry.razorpayx_payout_mode in [
26 | TRANSFER_METHOD.LINK.value,
27 | TRANSFER_METHOD.UPI.value,
28 | ]:
29 | transfer_method = payment_entry.razorpayx_payout_mode
30 | elif payment_entry.razorpayx_pay_instantaneously:
31 | transfer_method = TRANSFER_METHOD.IMPS.value
32 | elif payment_entry.paid_amount > 2_00_000:
33 | transfer_method = TRANSFER_METHOD.RTGS.value
34 | else:
35 | transfer_method = TRANSFER_METHOD.NEFT.value
36 |
37 | updated_payment_entries[payment_entry.name] = {
38 | "payment_transfer_method": transfer_method,
39 | }
40 |
41 | if updated_payment_entries:
42 | frappe.db.bulk_update("Payment Entry", updated_payment_entries)
43 |
--------------------------------------------------------------------------------
/razorpayx_integration/patches/update_integration_doctype.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 | from razorpayx_integration.constants import RAZORPAYX_CONFIG
4 |
5 |
6 | def execute():
7 | frappe.db.set_value(
8 | "Payment Entry",
9 | {
10 | "integration_doctype": "RazorpayX Integration Setting",
11 | },
12 | "integration_doctype",
13 | RAZORPAYX_CONFIG,
14 | )
15 |
--------------------------------------------------------------------------------
/razorpayx_integration/public/.gitkeep:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/public/.gitkeep
--------------------------------------------------------------------------------
/razorpayx_integration/public/images/razorpayx-logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/public/images/razorpayx-logo.png
--------------------------------------------------------------------------------
/razorpayx_integration/public/js/razorpayx_integration.bundle.js:
--------------------------------------------------------------------------------
1 | import "./utils";
2 |
--------------------------------------------------------------------------------
/razorpayx_integration/public/js/utils.js:
--------------------------------------------------------------------------------
1 | frappe.provide("razorpayx");
2 |
3 | const RAZORPAYX_CONFIG = "RazorpayX Configuration";
4 | const DESCRIPTION_REGEX = /^[a-zA-Z0-9 ]{1,30}$/;
5 | const PAYOUT_STATUS = {
6 | "Not Initiated": "grey",
7 | Queued: "yellow",
8 | Pending: "yellow",
9 | Scheduled: "yellow",
10 | Processing: "blue",
11 | Processed: "green",
12 | Failed: "red",
13 | Cancelled: "red",
14 | Rejected: "red",
15 | Reversed: "red",
16 | };
17 |
18 | Object.assign(razorpayx, {
19 | RAZORPAYX_CONFIG,
20 |
21 | PAYOUT_STATUS,
22 |
23 | async get_razorpayx_config(bank_account, fields = "name") {
24 | const response = await frappe.db.get_value(
25 | RAZORPAYX_CONFIG,
26 | { bank_account: bank_account, disabled: 0 },
27 | fields
28 | );
29 |
30 | return response.message;
31 | },
32 |
33 | is_payout_via_razorpayx(doc) {
34 | return (
35 | doc.make_bank_online_payment &&
36 | doc.integration_doctype === RAZORPAYX_CONFIG &&
37 | doc.integration_docname
38 | );
39 | },
40 |
41 | validate_payout_description(description) {
42 | if (!description || DESCRIPTION_REGEX.test(description)) return;
43 |
44 | frappe.throw({
45 | message: __(
46 | "Must be alphanumeric and contain spaces only, with a maximum of 30 characters."
47 | ),
48 | title: __("Invalid RazorpayX Payout Description"),
49 | });
50 | },
51 | });
52 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/razorpayx_integration/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/apis/base.py:
--------------------------------------------------------------------------------
1 | import re
2 | from urllib.parse import urljoin
3 |
4 | import frappe
5 | import frappe.utils
6 | import requests
7 | from frappe import _
8 | from frappe.app import UNSAFE_HTTP_METHODS
9 | from payment_integration_utils.payment_integration_utils.constants.enums import BaseEnum
10 | from payment_integration_utils.payment_integration_utils.utils import (
11 | enqueue_integration_request,
12 | get_end_of_day_epoch,
13 | get_start_of_day_epoch,
14 | )
15 |
16 | from razorpayx_integration.constants import (
17 | RAZORPAYX_CONFIG,
18 | )
19 | from razorpayx_integration.razorpayx_integration.doctype.razorpayx_configuration.razorpayx_configuration import (
20 | RazorpayXConfiguration,
21 | )
22 |
23 | RAZORPAYX_BASE_API_URL = "https://api.razorpay.com/v1/"
24 |
25 |
26 | class SUPPORTED_HTTP_METHOD(BaseEnum):
27 | GET = "GET"
28 | DELETE = "DELETE"
29 | POST = "POST"
30 | PUT = "PUT"
31 | PATCH = "PATCH"
32 |
33 |
34 | class BaseRazorpayXAPI:
35 | """
36 | Base class for RazorpayX APIs.
37 |
38 | Must need `RazorpayX Integration Account` name to initiate API.
39 |
40 | :param config: RazorpayX Configuration name.
41 | """
42 |
43 | ### CLASS ATTRIBUTES ###
44 | BASE_PATH = ""
45 |
46 | ### SETUP ###
47 | def __init__(self, config: str, *args, **kwargs):
48 | """
49 | Initialize the RazorpayX API.
50 |
51 | :param config: RazorpayX Configuration name.
52 | """
53 | self.razorpayx_config: RazorpayXConfiguration = frappe.get_doc(
54 | RAZORPAYX_CONFIG, config
55 | )
56 |
57 | self.authenticate_razorpayx_config()
58 |
59 | self.auth = (
60 | self.razorpayx_config.key_id,
61 | self.razorpayx_config.get_password("key_secret"),
62 | )
63 | self.source_doctype = None # Source doctype for Integration Request Log
64 | self.source_docname = None # Source docname for Integration Request Log
65 | self.default_headers = {} # Default headers for API request
66 | self.default_log_values = {} # Show value in Integration Request Log
67 | self.ir_service_set = False # Service details in IR log has been set or not
68 | self.sensitive_infos = () # Sensitive info to mask in Integration Request Log
69 | self.place_holder = "************"
70 |
71 | self.setup(*args, **kwargs)
72 |
73 | def authenticate_razorpayx_config(self):
74 | """
75 | Check config is enabled or not?
76 |
77 | Check RazorpayX API credentials `Id` and `Secret` are set or not?
78 | """
79 | if self.razorpayx_config.disabled:
80 | frappe.throw(
81 | msg=_("To use {0} config, please enable it first!").format(
82 | frappe.bold(self.razorpayx_config.name)
83 | ),
84 | title=_("RazorpayX Configuration Is Disable"),
85 | )
86 |
87 | if not self.razorpayx_config.key_id or not self.razorpayx_config.key_secret:
88 | frappe.throw(
89 | msg=_("Please set RazorpayX API credentials."),
90 | title=_("API Credentials Are Missing"),
91 | )
92 |
93 | if not self.razorpayx_config.webhook_secret:
94 | frappe.msgprint(
95 | msg=_(
96 | "RazorpayX Webhook Secret is missing!
You will not receive any updates!"
97 | ),
98 | indicator="yellow",
99 | alert=True,
100 | )
101 |
102 | def setup(self, *args, **kwargs):
103 | """
104 | Override this method to setup API specific configurations.
105 | """
106 | pass
107 |
108 | ### APIs ###
109 | def get(self, *args, **kwargs):
110 | """
111 | Make `GET` HTTP request.
112 | """
113 | return self._make_request(SUPPORTED_HTTP_METHOD.GET.value, *args, **kwargs)
114 |
115 | def delete(self, *args, **kwargs):
116 | """
117 | Make `DELETE` HTTP request.
118 | """
119 | return self._make_request(SUPPORTED_HTTP_METHOD.DELETE.value, *args, **kwargs)
120 |
121 | def post(self, *args, **kwargs):
122 | """
123 | Make `POST` HTTP request.
124 | """
125 | return self._make_request(SUPPORTED_HTTP_METHOD.POST.value, *args, **kwargs)
126 |
127 | def put(self, *args, **kwargs):
128 | """
129 | Make `PUT` HTTP request.
130 | """
131 | return self._make_request(SUPPORTED_HTTP_METHOD.PUT.value, *args, **kwargs)
132 |
133 | def patch(self, *args, **kwargs):
134 | """
135 | Make `PATCH` HTTP request.
136 | """
137 | return self._make_request(SUPPORTED_HTTP_METHOD.PATCH.value, *args, **kwargs)
138 |
139 | ### API WRAPPERS ###
140 | # TODO: should add `skip` in filters (Handle pagination + if not given fetch all) (Change in sub class)
141 | def get_all(
142 | self, filters: dict | None = None, count: int | None = None
143 | ) -> list[dict] | None:
144 | """
145 | Fetches all data of given RazorpayX account for specific API.
146 |
147 | :param filters: Filters for fetching filtered response.
148 | :param count: Total number of item to be fetched.If not given fetches all.
149 | """
150 | MAX_LIMIT = 100
151 |
152 | if filters:
153 | self._clean_request(filters)
154 | self._set_epoch_time_for_date_filters(filters)
155 | self._validate_and_process_filters(filters)
156 |
157 | else:
158 | filters = {}
159 |
160 | if isinstance(count, int) and count <= 0:
161 | frappe.throw(
162 | _("Count can't be {0}").format(frappe.bold(count)),
163 | title=_("Invalid Count To Fetch Data"),
164 | )
165 |
166 | if count and count <= MAX_LIMIT:
167 | filters["count"] = count
168 | return self._fetch(filters)
169 |
170 | if count is None:
171 | FETCH_ALL_ITEMS = True
172 | else:
173 | FETCH_ALL_ITEMS = False
174 |
175 | result = []
176 | filters["count"] = MAX_LIMIT
177 | filters["skip"] = 0
178 |
179 | while True:
180 | items = self._fetch(filters)
181 |
182 | if items and isinstance(items, list):
183 | result.extend(items)
184 | else:
185 | break
186 |
187 | if len(items) < MAX_LIMIT:
188 | break
189 |
190 | if not FETCH_ALL_ITEMS:
191 | count -= len(items)
192 | if count <= 0:
193 | break
194 |
195 | filters["skip"] += MAX_LIMIT
196 |
197 | return result
198 |
199 | ### BASES ###
200 | def _make_request(
201 | self,
202 | method: str,
203 | endpoint: str = "",
204 | params: dict | None = None,
205 | headers: dict | None = None,
206 | json: dict | None = None,
207 | ):
208 | """
209 | Base for making HTTP request.
210 |
211 | Process headers,params and data then make request and return processed response.
212 | """
213 | method = method.upper()
214 | if method not in SUPPORTED_HTTP_METHOD.values():
215 | frappe.throw(_("Invalid method {0}").format(method))
216 |
217 | request_args = frappe._dict(
218 | url=self.get_url(endpoint),
219 | params=params,
220 | headers={
221 | **self.default_headers,
222 | **(headers or {}),
223 | },
224 | auth=self.auth,
225 | )
226 |
227 | # preparing log for Integration Request
228 | self._set_source_to_ir_log()
229 |
230 | ir_log = frappe._dict(
231 | **self.default_log_values,
232 | url=request_args.url,
233 | data=request_args.params,
234 | request_headers=request_args.headers.copy(),
235 | )
236 |
237 | if method in UNSAFE_HTTP_METHODS and json:
238 | request_args.json = json
239 |
240 | copied_json = json.copy()
241 |
242 | if not request_args.params:
243 | ir_log.data = copied_json
244 | else:
245 | ir_log.data = {
246 | "params": request_args.params,
247 | "body": copied_json,
248 | }
249 |
250 | response_json = None
251 |
252 | try:
253 | self._before_request(request_args)
254 |
255 | response = requests.request(method, **request_args)
256 | response_json = response.json(object_hook=frappe._dict)
257 |
258 | if response.status_code >= 400:
259 | self._handle_failed_api_response(response_json)
260 |
261 | # Raise HTTPError for other HTTP codes
262 | response.raise_for_status()
263 |
264 | return response_json
265 |
266 | except Exception as e:
267 | ir_log.error = str(e)
268 | raise e
269 | finally:
270 | if response_json:
271 | ir_log.output = response_json.copy()
272 |
273 | self._mask_sensitive_info(ir_log)
274 |
275 | if not ir_log.integration_request_service:
276 | ir_log.integration_request_service = "RazorpayX Integration"
277 |
278 | enqueue_integration_request(**ir_log)
279 |
280 | def _fetch(self, params: dict) -> list:
281 | """
282 | Fetches `items` from the API response based on the given parameters.
283 | """
284 | response = self.get(params=params)
285 | return response.get("items", [])
286 |
287 | ### API HELPERS ###
288 | def get_url(self, *path_segments):
289 | """
290 | Generate particular API's URL by combing given path_segments.
291 |
292 | Example:
293 | if path_segments = 'contact/old' then
294 | URL will `RAZORPAYX_BASE_URL/BASE_PATH/contact/old`
295 | """
296 |
297 | path_segments = list(path_segments)
298 |
299 | if self.BASE_PATH:
300 | path_segments.insert(0, self.BASE_PATH)
301 |
302 | return urljoin(
303 | RAZORPAYX_BASE_API_URL,
304 | "/".join(segment.strip("/") for segment in path_segments),
305 | )
306 |
307 | def _before_request(self, request_args):
308 | """
309 | Override in sub class to perform any operation before making the request.
310 | """
311 | return
312 |
313 | def _clean_request(self, filters: dict):
314 | """
315 | Cleans the request filters by removing any key-value pairs where
316 | the value is falsy.
317 | """
318 | keys_to_delete = [key for key, value in filters.items() if not value]
319 |
320 | for key in keys_to_delete:
321 | del filters[key]
322 |
323 | def _set_epoch_time_for_date_filters(self, filters: dict):
324 | """
325 | Converts the date filters `from` and `to` to epoch time (Unix timestamp).
326 | """
327 | if from_date := filters.get("from"):
328 | filters["from"] = get_start_of_day_epoch(from_date)
329 |
330 | if to_date := filters.get("to"):
331 | filters["to"] = get_end_of_day_epoch(to_date)
332 |
333 | def _validate_and_process_filters(self, filters: dict):
334 | """
335 | Override in sub class to validate and process filters, except date filters (from,to).
336 |
337 | Validation happen before `get_all()` to reduce API calls.
338 | """
339 | pass
340 |
341 | def sanitize_party_name(self, party_name: str) -> str:
342 | """
343 | Convert the given ERPNext party name to a valid RazorpayX Contact Name.
344 |
345 | - Replace unsupported characters with `-`.
346 | - Remove special characters from the start and end of the name.
347 | - Trim the name to 50 characters.
348 | - If the name is less than 3 characters, append `.` to the name.
349 |
350 | :param contact_name: ERPNext party name.
351 |
352 | ---
353 | - Supported characters: `a-z`, `A-Z`, `0-9`, `space`, `'` , `-` , `_` , `/` , `(` , `)` and `.`
354 | """
355 | # replace unsupported characters with `-`
356 | party_name = re.sub(r"[^a-zA-Z0-9\s'._/()-]", "-", party_name)
357 |
358 | # remove special characters from the start and end
359 | party_name = re.sub(r"^[^a-zA-Z0-9]+|[^a-zA-Z0-9.]+$", "", party_name.strip())
360 |
361 | return party_name[:50].ljust(3, ".")
362 |
363 | ### LOGGING ###
364 | def _set_service_details_to_ir_log(
365 | self, service_name: str, service_set: bool = True
366 | ):
367 | """
368 | Set the service details in the Integration Request Log.
369 |
370 | :param service_name: The service name.
371 | :param service_set: Set flag that service name for Integration request has been set or not.
372 | """
373 | self.default_log_values.update(
374 | {"integration_request_service": f"RazorpayX - {service_name}"}
375 | )
376 |
377 | self.ir_service_set = service_set
378 |
379 | def _set_source_to_ir_log(self):
380 | """
381 | Set the source document details in the Integration Request Log.
382 | """
383 | if not (self.source_doctype and self.source_docname):
384 | return
385 |
386 | self.default_log_values.update(
387 | {
388 | "reference_doctype": self.source_doctype,
389 | "reference_name": self.source_docname,
390 | }
391 | )
392 |
393 | def _mask_sensitive_info(self, ir_log: dict):
394 | """
395 | Mask sensitive information in the Integration Request Log.
396 | """
397 | pass
398 |
399 | ### ERROR HANDLING ###
400 | def _handle_failed_api_response(self, response_json: dict | None = None):
401 | """
402 | Handle failed API response from RazorpayX.
403 |
404 | ---
405 | Error response format:
406 | ```py
407 | {
408 | "error": {
409 | "code": "SERVER_ERROR",
410 | "description": "Server Is Down",
411 | "source": "NA",
412 | "step": "NA",
413 | "reason": "NA",
414 | "metadata": {},
415 | },
416 | }
417 | ```
418 |
419 | ---
420 | Reference: https://razorpay.com/docs/errors/#sample-code
421 | """
422 | error_msg = "There is some error in RazorpayX"
423 | title = _("RazorpayX API Failed")
424 |
425 | if response_json:
426 | error_msg = (
427 | response_json.get("message")
428 | or response_json.get("error", {}).get("description")
429 | or error_msg
430 | )
431 |
432 | self._handle_custom_error(error_msg, title=title)
433 |
434 | frappe.throw(
435 | msg=_(error_msg),
436 | title=title,
437 | )
438 |
439 | def _handle_custom_error(self, error_msg: str, title: str | None = None):
440 | """
441 | Handle custom error message.
442 |
443 | :param error_msg: RazorpayX API error message.
444 | :param title: Title of the error message.
445 | """
446 | match error_msg:
447 | case "Different request body sent for the same Idempotency Header":
448 | error_msg = _(
449 | "Please cancel/delete the current document and pay with a new document."
450 | )
451 |
452 | error_msg += "
"
453 |
454 | error_msg += _(
455 | "You faced this issue because payment details were changed after the first payment attempt."
456 | )
457 |
458 | title = _("Payment Details Changed")
459 |
460 | case "Authentication failed":
461 | error_msg = _(
462 | "RazorpayX API credentials are invalid. Please set valid Key ID and Key Secret."
463 | )
464 |
465 | title = _("RazorpayX Authentication Failed")
466 |
467 | case "The RazorpayX Account number is invalid.":
468 | error_msg = _(
469 | "Bank Account number is not matching with the RazorpayX account.
Please set valid Bank Account."
470 | )
471 |
472 | title = _("Invalid Bank Account Number")
473 |
474 | if not title:
475 | title = _("RazorpayX API Failed")
476 |
477 | frappe.throw(title=title, msg=error_msg)
478 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/apis/contact.py:
--------------------------------------------------------------------------------
1 | from frappe.utils import validate_email_address
2 |
3 | from razorpayx_integration.razorpayx_integration.apis.base import BaseRazorpayXAPI
4 | from razorpayx_integration.razorpayx_integration.constants.payouts import (
5 | CONTACT_TYPE,
6 | )
7 |
8 | # ! IMPORTANT: Currently this API is not maintained.
9 | # TODO: this need to be refactor and optimize
10 | # TODO: Add service details to IR log
11 | # TODO: Add source doctype and docname to IR log
12 |
13 |
14 | class RazorpayXContact(BaseRazorpayXAPI):
15 | """
16 | Handle APIs for RazorpayX Contact.
17 |
18 | :param account_name: RazorpayX account for which this `Contact` is associate.
19 |
20 | ---
21 | Reference: https://razorpay.com/docs/api/x/contacts
22 | """
23 |
24 | # * utility attributes
25 | BASE_PATH = "contacts"
26 |
27 | # * override base setup
28 | def setup(self, *args, **kwargs):
29 | pass
30 |
31 | ### APIs ###
32 | def create(self, **kwargs) -> dict:
33 | """
34 | Creates `RazorpayX Contact`.
35 |
36 | :param dict json: Full details of the contact to create.
37 | :param str name: [*] The name of the contact.
38 | :param str type: Contact's ERPNext DocType. (Ex. `Employee`, `Customer`, `Supplier`)
39 | :param str email: Email address of the contact.
40 | :param str contact: Contact number of the contact.
41 | :param str id: Reference Id for contact.
42 | :param dict notes: Additional notes for the contact.
43 |
44 | ---
45 | Example Usage:
46 | ```
47 | contact = RazorpayXContact(RAZORPAYX_BANK_ACCOUNT)
48 |
49 | # Using args
50 | response = contact.create(
51 | name="Joe Doe",
52 | type="Employee",
53 | email="joe123@gmail.com",
54 | contact="7434870169",
55 | id="empl-02",
56 | notes={
57 | "source": "ERPNext",
58 | "demo": True,
59 | }
60 | )
61 |
62 | # Using json
63 | json = {
64 | "name"="Joe Doe",
65 | "type"="Employee",
66 | "email"="joe123@gmail.com",
67 | "contact"="7434870169",
68 | "id"="empl-02",
69 | notes={
70 | "source": "ERPNext",
71 | "demo": True,
72 | }
73 | }
74 |
75 | response = contact.create(json=json)
76 | ```
77 | ---
78 |
79 | Note:
80 | - If `json` passed in args, then remaining args will be discarded.
81 | - [*] Required fields.
82 | ---
83 | Reference: https://razorpay.com/docs/api/x/contacts/create
84 | """
85 | # TODO: ? should sanitize contact name
86 | return self.post(json=self.get_mapped_request(kwargs))
87 |
88 | def get_by_id(self, id: str) -> dict:
89 | """
90 | Fetch the details of a specific `Contact` by Id.
91 |
92 | :param id: `Id` of contact to fetch (Ex.`cont_hkj012yuGJ`).
93 |
94 | ---
95 | Reference: https://razorpay.com/docs/api/x/contacts/fetch-with-id
96 | """
97 | return self.get(endpoint=id)
98 |
99 | def get_all(
100 | self, filters: dict | None = None, count: int | None = None
101 | ) -> list[dict]:
102 | """
103 | Get all `Contacts` associate with given `RazorpayX` account if limit is not given.
104 |
105 | :param filters: Result will be filtered as given filters.
106 | :param count: The number of contacts to be retrieved.
107 |
108 | :raises ValueError: If `type` is not valid.
109 |
110 | ---
111 | Example Usage:
112 | ```
113 | contact = RazorpayXContact(RAZORPAYX_BANK_ACCOUNT)
114 |
115 | filters = {
116 | "name":"joe",
117 | "email":"joe@gmail.com",
118 | "contact":"743487045",
119 | "reference_id":"empl_001",
120 | "active":1 | True,
121 | "type":"Employee",
122 | "from":"2024-01-01"
123 | "to":"2024-06-01"
124 | }
125 |
126 | response=contact.get_all(filters)
127 | ```
128 |
129 | ---
130 | Note:
131 | - `active` can be int or boolean.
132 | - `from` and `to` can be str,date,datetime (in YYYY-MM-DD).
133 |
134 | ---
135 | Reference: https://razorpay.com/docs/api/x/contacts/fetch-all
136 | """
137 | return super().get_all(filters, count)
138 |
139 | def update(self, id: str, **kwargs):
140 | """
141 | Updates `RazorpayX Contact`.
142 |
143 | :param id: Contact Id of whom to update details (Ex.`cont_hkj012yuGJ`).
144 | :param dict json: Full details of contact to create.
145 | :param str name: The contact's name.
146 | :param str type: Contact's ERPNext DocType.
147 | :param str email: Email address of the contact.
148 | :param str contact: Contact number of the contact.
149 | :param str id: Reference Id for contact.
150 | :param dict notes: Additional notes for the contact.
151 |
152 | ---
153 | Example Usage:
154 | ```
155 | contact = RazorpayXContact(RAZORPAYX_BANK_ACCOUNT)
156 |
157 | # Using args
158 | response = contact.update(
159 | name="Joe Doe",
160 | type="employee",
161 | email="joe123@gmail.com",
162 | contact="7434870169",
163 | id="empl-02",
164 | notes = {
165 | "source": "ERPNext",
166 | "demo": True,
167 | }
168 | )
169 |
170 | # Using json
171 | json = {
172 | "name"="Joe Doe",
173 | "type"="employee",
174 | "email"="joe123@gmail.com",
175 | "contact"="7434870169",
176 | "id"="empl-02",
177 | "notes"={
178 | "source": "ERPNext",
179 | "demo": True,
180 | }
181 | }
182 |
183 | response = contact.update(id='cont_hkj012yuGJ',json=json)
184 | ```
185 |
186 | ---
187 | Note:
188 | - If json passed in args, then other args will discarded.
189 |
190 | ---
191 | Reference: https://razorpay.com/docs/api/x/contacts/update
192 | """
193 | return self.patch(endpoint=id, json=self.get_mapped_request(kwargs))
194 |
195 | def activate(self, id: str) -> dict:
196 | """
197 | Activate the contact for the given `Id` if it is deactivated.
198 |
199 | :param id: `Id` of contact to make activate (Ex.`cont_hkj012yuGJ`).
200 | """
201 | return self._change_state(id=id, active=True)
202 |
203 | def deactivate(self, id: str) -> dict:
204 | """
205 | Deactivate the contact for the given `Id` if it is activated.
206 |
207 | :param id: `Id` of contact to make deactivate (Ex.`cont_hkj012yuGJ`).
208 | """
209 | return self._change_state(id=id, active=False)
210 |
211 | ### Bases ###
212 | def _change_state(self, id: str, active: bool | int) -> dict:
213 | """
214 | Change the state of the `Contact` for the given Id.
215 |
216 | :param id: Id of `Contact` to change state (Ex.`cont_hkj012yuGJ`).
217 | :param active: Represents the state. (`True`:Active,`False`:Inactive)
218 |
219 | ---
220 | Reference: https://razorpay.com/docs/api/x/contacts/activate-or-deactivate
221 | """
222 | return self.patch(endpoint=id, json={"active": active})
223 |
224 | ### Helpers ###
225 | def get_mapped_request(self, request: dict) -> dict:
226 | """
227 | Maps given request data to RazorpayX request data structure.
228 | """
229 | json = request.get("json")
230 |
231 | if json and isinstance(json, dict):
232 | if id := json.get("id"):
233 | json["reference_id"] = id
234 | del json["id"]
235 |
236 | else:
237 | json = {
238 | "name": request.get("name"),
239 | "type": request.get("type"),
240 | "email": request.get("email"),
241 | "contact": request.get("contact"),
242 | "reference_id": request.get("id"),
243 | "notes": request.get("notes"),
244 | }
245 |
246 | if json.get("type"):
247 | json["type"] = self.get_contact_type(json)
248 |
249 | self._clean_request(json)
250 | self.validate_email(json)
251 |
252 | return json
253 |
254 | def get_contact_type(self, request: dict) -> str | None:
255 | """
256 | Get the RazorpayX Contact Type for given ERPNext DocType.
257 |
258 | :param request: Request data.
259 |
260 | ---
261 | Note:
262 | - Returns `None` if `type` is not valid.
263 | - Default Contact Type is `Customer`.
264 | """
265 | doctype = request.get("type", "").upper()
266 |
267 | if not doctype:
268 | return
269 |
270 | if doctype not in CONTACT_TYPE.values():
271 | return CONTACT_TYPE.CUSTOMER.value
272 |
273 | return CONTACT_TYPE[doctype].value
274 |
275 | def validate_email(self, request: dict):
276 | if email := request.get("email"):
277 | validate_email_address(email, throw=True)
278 |
279 | def _validate_and_process_filters(self, filters: dict):
280 | self.validate_email(filters)
281 | filters["type"] = self.get_contact_type(filters)
282 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/apis/fund_account.py:
--------------------------------------------------------------------------------
1 | from razorpayx_integration.razorpayx_integration.apis.base import BaseRazorpayXAPI
2 | from razorpayx_integration.razorpayx_integration.constants.payouts import (
3 | FUND_ACCOUNT_TYPE,
4 | )
5 | from razorpayx_integration.razorpayx_integration.utils.validation import (
6 | validate_fund_account_type,
7 | )
8 |
9 | # ! IMPORTANT: Currently this API is not maintained.
10 | # TODO: this need to be refactor and optimize
11 | # TODO: Add service details to IR log
12 | # TODO: Add source doctype and docname to IR log
13 |
14 |
15 | class RazorpayXFundAccount(BaseRazorpayXAPI):
16 | """
17 | Handle APIs for RazorpayX Fund Account.
18 |
19 | :param account_name: RazorpayX account for which this `Fund Account` is associate.
20 |
21 | ---
22 | Reference: https://razorpay.com/docs/api/x/fund-accounts/
23 | """
24 |
25 | # * utility attributes
26 | BASE_PATH = "fund_accounts"
27 |
28 | # * override base setup
29 | def setup(self, *args, **kwargs):
30 | pass
31 |
32 | ### APIs ###
33 | def create_with_bank_account(
34 | self, contact_id: str, contact_name: str, ifsc_code: str, account_number: str
35 | ):
36 | """
37 | Create RazorpayX `Fund Account` with contact's bank account details.
38 |
39 | :param contact_id: The ID of the contact to which the `fund_account` is linked (Eg. `cont_00HjGh1`).
40 | :param contact_name: The account holder's name.
41 | :param ifsc_code: Unique identifier of a bank branch (Eg. `HDFC0000053`).
42 | :param account_number: The account number (Eg. `765432123456789`).
43 |
44 | ---
45 | Reference: https://razorpay.com/docs/api/x/fund-accounts/create/bank-account
46 | """
47 | json = {
48 | "contact_id": contact_id,
49 | "account_type": FUND_ACCOUNT_TYPE.BANK_ACCOUNT.value,
50 | "bank_account": {
51 | "name": contact_name,
52 | "ifsc": ifsc_code,
53 | "account_number": account_number,
54 | },
55 | }
56 |
57 | return self.post(json=json)
58 |
59 | def create_with_vpa(self, contact_id: str, vpa: str):
60 | """
61 | Create RazorpayX `Fund Account` with contact's Virtual Payment Address.
62 |
63 | :param str contact_id: The ID of the contact to which the `fund_account` is linked (Eg. `cont_00HjGh1`).
64 | :param str vpa: The contact's virtual payment address (VPA) (Eg. `joedoe@exampleupi`)
65 |
66 | ---
67 | Reference: https://razorpay.com/docs/api/x/fund-accounts/create/vpa
68 | """
69 | json = {
70 | "contact_id": contact_id,
71 | "account_type": FUND_ACCOUNT_TYPE.VPA.value,
72 | "vpa": {"address": vpa},
73 | }
74 |
75 | return self.post(json=json)
76 |
77 | def get_by_id(self, id: str):
78 | """
79 | Fetch the details of a specific `Fund Account` by Id.
80 |
81 | :param id: `Id` of fund account to fetch (Ex.`fa_00HjHue1`).
82 |
83 | ---
84 | Reference: https://razorpay.com/docs/api/x/contacts/fetch-with-id
85 | """
86 | return self.get(endpoint=id)
87 |
88 | def get_all(
89 | self, filters: dict | None = None, count: int | None = None
90 | ) -> list[dict]:
91 | """
92 | Get all `Fund Account` associate with given `RazorpayX` account if limit is not given.
93 |
94 | :param filters: Result will be filtered as given filters.
95 | :param count: The number of `Fund Account` to be retrieved.
96 |
97 | ---
98 | Example Usage:
99 | ```
100 | fund_account = RazorpayXFundAccount(RAZORPAYX_BANK_ACCOUNT)
101 | filters = {
102 | "contact_id":"cont_hkj012yuGJ",
103 | "account_type":"bank_account",
104 | "from":"2024-01-01"
105 | "to":"2024-06-01"
106 | }
107 | response=fund_account.get_all(filters)
108 | ```
109 |
110 | ---
111 | Note:
112 | - Not all filters are require.
113 | - `account_type` can be one of the ['bank_account','vpa'], if not raises an error.
114 | - `from` and `to` can be str,date,datetime (in YYYY-MM-DD).
115 |
116 | ---
117 | Reference: https://razorpay.com/docs/api/x/fund-accounts/fetch-all
118 | """
119 | return super().get_all(filters, count)
120 |
121 | def activate(self, id: str) -> dict:
122 | """
123 | Activate the `Fund Account` for the given Id if it is deactivated.
124 |
125 | :param id: Id of the `Fund Account` to make activate (Ex.`fa_00HjHue1`).
126 | """
127 | return self._change_state(id=id, active=True)
128 |
129 | def deactivate(self, id: str) -> dict:
130 | """
131 | Deactivate the `Fund Account` for the given Id if it is activated.
132 |
133 | :param id: Id of the `Fund Account` to make deactivate (Ex.`fa_00HjHue1`).
134 | """
135 | return self._change_state(id=id, active=False)
136 |
137 | ### Bases ###
138 | def _change_state(self, id: str, active: bool | int) -> dict:
139 | """
140 | Change the state of the `Fund Account` for the given Id.
141 |
142 | :param id: Id of `Fund Account` to change state (Ex.`fa_00HjHue1`).
143 | :param active: Represent state. (`True`:Active,`False`:Inactive)
144 |
145 | ---
146 | Reference: https://razorpay.com/docs/api/x/fund-accounts/activate-or-deactivate
147 | """
148 | return self.patch(endpoint=id, json={"active": active})
149 |
150 | ### Helpers ###
151 | def _validate_and_process_filters(self, filters: dict) -> dict:
152 | if account_type := filters.get("account_type"):
153 | validate_fund_account_type(account_type)
154 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/apis/transaction.py:
--------------------------------------------------------------------------------
1 | from frappe.utils import DateTimeLikeObject, today
2 |
3 | from razorpayx_integration.razorpayx_integration.apis.base import BaseRazorpayXAPI
4 |
5 |
6 | class RazorpayXTransaction(BaseRazorpayXAPI):
7 | """
8 | Handle APIs for `Transaction`.
9 |
10 | :param config: RazorpayX Configuration name
11 | for which this `Transaction` is associate.
12 |
13 | ---
14 | Reference: https://razorpay.com/docs/api/x/transactions/
15 | """
16 |
17 | # * utility attributes
18 | BASE_PATH = "transactions"
19 |
20 | # * override base setup
21 | def setup(self, *args, **kwargs):
22 | self.account_number = self.razorpayx_config.account_number
23 |
24 | ### APIs ###
25 | def get_by_id(
26 | self,
27 | transaction_id: str,
28 | *,
29 | source_doctype: str | None = None,
30 | source_docname: str | None = None,
31 | ) -> dict:
32 | """
33 | Fetch the details of a specific `Transaction` by Id.
34 |
35 | :param id: `Id` of fund account to fetch (Ex.`txn_jkHgLM02`).
36 | :param source_doctype: The source doctype of the transaction.
37 | :param source_docname: The source docname of the transaction
38 |
39 | ---
40 | Reference: https://razorpay.com/docs/api/x/transactions/fetch-with-id
41 | """
42 | self._set_service_details_to_ir_log("Get Transaction By Id")
43 | self.source_doctype = source_doctype
44 | self.source_docname = source_docname
45 |
46 | return self.get(endpoint=transaction_id)
47 |
48 | def get_all(
49 | self,
50 | *,
51 | from_date: DateTimeLikeObject | None = None,
52 | to_date: DateTimeLikeObject | None = None,
53 | count: int | None = None,
54 | source_doctype: str | None = None,
55 | source_docname: str | None = None,
56 | ) -> list[dict] | None:
57 | """
58 | Get all `Transaction` associate with given `RazorpayX` account if count is not given.
59 |
60 | :param from_date: The starting date for which transactions are to be fetched.
61 | :param to_date: The ending date for which transactions are to be fetched.
62 | :param count: The number of `Transaction` to be retrieved.
63 | :param source_doctype: The source doctype of the transaction.
64 | :param source_docname: The source docname of the transaction
65 |
66 | ---
67 | Example Usage:
68 |
69 | ```
70 | transaction = RazorpayXTransaction(RAZORPAYX_CONFIG_NAME)
71 |
72 | response = transaction.get_all(
73 | from_date="2024-01-01", to_date="2024-06-01", count=10
74 | )
75 | ```
76 |
77 | ---
78 | Note:
79 | - `from` and `to` can be str,date,datetime (in YYYY-MM-DD).
80 |
81 | ---
82 | Reference: https://razorpay.com/docs/api/x/transactions/fetch-all
83 | """
84 | filters = {}
85 |
86 | if from_date:
87 | filters["from"] = from_date
88 |
89 | if to_date:
90 | filters["to"] = to_date
91 |
92 | if "from" not in filters:
93 | filters["from"] = self.razorpayx_config.last_sync_on
94 |
95 | # account number is mandatory
96 | filters["account_number"] = self.account_number
97 |
98 | if not self.ir_service_set:
99 | self._set_service_details_to_ir_log("Get All Transactions", False)
100 |
101 | if not (self.source_doctype and self.source_docname):
102 | self.source_doctype = source_doctype
103 | self.source_docname = source_docname
104 |
105 | return super().get_all(filters=filters, count=count)
106 |
107 | def get_transactions_for_today(
108 | self,
109 | count: int | None = None,
110 | *,
111 | source_doctype: str | None = None,
112 | source_docname: str | None = None,
113 | ) -> list[dict] | None:
114 | """
115 | Get all transactions for today associate with given RazorpayX Config.
116 |
117 | :param count: The number of transactions to be retrieved.
118 | :param source_doctype: The source doctype of the transaction.
119 | :param source_docname: The source docname of the transaction
120 |
121 | ---
122 | Note: If count is not given, it will return all transactions for today.
123 | """
124 | today_date = today()
125 | self._set_service_details_to_ir_log("Get Transactions For Today")
126 |
127 | return self.get_all(
128 | from_date=today_date,
129 | to_date=today_date,
130 | count=count,
131 | source_doctype=source_doctype,
132 | source_docname=source_docname,
133 | )
134 |
135 | def get_transactions_for_date(
136 | self,
137 | date: DateTimeLikeObject,
138 | count: int | None = None,
139 | *,
140 | source_doctype: str | None = None,
141 | source_docname: str | None = None,
142 | ) -> list[dict] | None:
143 | """
144 | Get all transactions for specific date associate with given RazorpayX Config.
145 |
146 | :param date: A date string in "YYYY-MM-DD" format or a (datetime,date) object.
147 | """
148 | self._set_service_details_to_ir_log("Get Transactions For Date")
149 |
150 | return self.get_all(
151 | from_date=date,
152 | to_date=date,
153 | count=count,
154 | source_doctype=source_doctype,
155 | source_docname=source_docname,
156 | )
157 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/apis/validate_razorpayx.py:
--------------------------------------------------------------------------------
1 | """
2 | Module for API testing and validation.
3 | """
4 |
5 | from razorpayx_integration.razorpayx_integration.apis.base import BaseRazorpayXAPI
6 |
7 |
8 | class RazorpayXValidation(BaseRazorpayXAPI):
9 | """
10 | Validate RazorpayX APIs.
11 |
12 | :param id: RazorpayX API Key ID
13 | :param secret: RazorpayX API Key Secret
14 | :param account_number: RazorpayX Account Number
15 | :param source_doctype: Source Doctype
16 | :param source_docname: Source Docname
17 | """
18 |
19 | def __init__(
20 | self,
21 | id: str,
22 | secret: str,
23 | account_number: str | None = None,
24 | source_doctype: str | None = None,
25 | source_docname: str | None = None,
26 | ):
27 | """
28 | Validate RazorpayX APIs.
29 |
30 | :param id: RazorpayX API Key ID
31 | :param secret: RazorpayX API Key Secret
32 | :param account_number: RazorpayX Account Number
33 | :param source_doctype: Source Doctype
34 | :param source_docname: Source Docname
35 | """
36 | self.auth = (id, secret)
37 | self.account_number = account_number
38 | self.source_doctype = source_doctype
39 | self.source_docname = source_docname
40 |
41 | self.default_headers = {}
42 | self.default_log_values = {} # Show value in Integration Request Log
43 | self.sensitive_infos = () # Sensitive info to mask in Integration Request Log
44 |
45 | def validate_credentials(self):
46 | """
47 | Validate RazorpayX API credentials.
48 |
49 | - Key ID
50 | - Key Secret
51 | """
52 | self._set_service_details_to_ir_log("Validate API Credentials")
53 | self.set_base_path("transactions")
54 |
55 | self.get_all(filters={"account_number": self.account_number}, count=1)
56 |
57 | def set_base_path(self, path: str):
58 | """
59 | Set base API path.
60 | """
61 | self.BASE_PATH = path
62 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/client_overrides/form/bank_reconciliation_tool.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2025, Resilient Tech and contributors
2 | // For license information, please see license.txt
3 | const SYNC_BTN_LABEL = __("Sync via RazorpayX");
4 |
5 | frappe.ui.form.on("Bank Reconciliation Tool", {
6 | refresh: async function (frm) {
7 | frm.add_custom_button(SYNC_BTN_LABEL, async () => {
8 | await sync_transactions_with_razorpayx(frm.doc.bank_account, frm.__razorpayx_config);
9 |
10 | frappe.show_alert({
11 | message: __("{0} transactions synced successfully!", [frm.doc.bank_account]),
12 | indicator: "green",
13 | });
14 | });
15 |
16 | await toggle_sync_btn(frm);
17 | },
18 |
19 | bank_account: async function (frm) {
20 | await toggle_sync_btn(frm);
21 | },
22 | });
23 |
24 | function sync_transactions_with_razorpayx(bank_account, razorpayx_config) {
25 | return frappe.call({
26 | method: "razorpayx_integration.razorpayx_integration.utils.bank_transaction.sync_transactions_for_reconcile",
27 | args: { bank_account, razorpayx_config },
28 | freeze: true,
29 | freeze_message: __("Syncing Transactions. Please wait it may take a while..."),
30 | });
31 | }
32 |
33 | async function toggle_sync_btn(frm) {
34 | const btn = frm.custom_buttons[SYNC_BTN_LABEL];
35 |
36 | if (!btn) return;
37 |
38 | if (!frm.doc.bank_account) {
39 | btn.hide();
40 | return;
41 | }
42 |
43 | const { name } = await razorpayx.get_razorpayx_config(frm.doc.bank_account);
44 | frm.__razorpayx_config = name;
45 |
46 | if (name) btn.show();
47 | else btn.hide();
48 | }
49 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/client_overrides/form/payment_entry.js:
--------------------------------------------------------------------------------
1 | // ############ CONSTANTS ############ //
2 | const PE_BASE_PATH = "razorpayx_integration.razorpayx_integration.server_overrides.doctype.payment_entry";
3 |
4 | const TRANSFER_METHOD = payment_integration_utils.PAYMENT_TRANSFER_METHOD;
5 |
6 | frappe.ui.form.on("Payment Entry", {
7 | refresh: async function (frm) {
8 | // permission checks
9 | const permission = has_payout_permissions(frm);
10 | frm.toggle_display("razorpayx_payout_section", permission);
11 |
12 | if (frm.doc.integration_doctype !== razorpayx.RAZORPAYX_CONFIG || !frm.doc.integration_docname)
13 | return;
14 |
15 | // payout is/will made via RazorpayX
16 | if (frm.doc.make_bank_online_payment) {
17 | set_razorpayx_state_description(frm);
18 | set_reference_no_description(frm);
19 | }
20 |
21 | if (!permission || is_already_paid(frm)) return;
22 |
23 | // making payout manually
24 | if (frm.doc.docstatus === 1 && !frm.doc.make_bank_online_payment && frm.doc.bank_account) {
25 | frm.add_custom_button(__("Make Payout"), () => show_make_payout_dialog(frm));
26 | }
27 | },
28 |
29 | validate: function (frm) {
30 | if (!razorpayx.is_payout_via_razorpayx(frm.doc)) return;
31 |
32 | razorpayx.validate_payout_description(frm.doc.razorpayx_payout_desc);
33 | },
34 |
35 | before_submit: async function (frm) {
36 | if (
37 | !razorpayx.is_payout_via_razorpayx(frm.doc) ||
38 | is_already_paid(frm) ||
39 | !has_payout_permissions(frm)
40 | ) {
41 | return;
42 | }
43 |
44 | frappe.validate = false;
45 |
46 | return new Promise((resolve) => {
47 | const continue_submission = (auth_id) => {
48 | frappe.validate = true;
49 | frm.__making_payout = true;
50 |
51 | payment_integration_utils.set_onload(frm, "auth_id", auth_id);
52 |
53 | resolve();
54 | };
55 |
56 | return payment_integration_utils.authenticate_payment_entries(frm.docname, continue_submission);
57 | });
58 | },
59 |
60 | on_submit: function (frm) {
61 | if (!frm.__making_payout) return;
62 |
63 | frappe.show_alert({
64 | message: __("Payout has been made successfully."),
65 | indicator: "green",
66 | });
67 |
68 | delete frm.__making_payout;
69 | },
70 |
71 | before_cancel: async function (frm) {
72 | if (
73 | !razorpayx.is_payout_via_razorpayx(frm.doc) ||
74 | !["Not Initiated", "Queued"].includes(frm.doc.razorpayx_payout_status) ||
75 | !has_payout_permissions(frm) ||
76 | payment_integration_utils.get_onload(frm, "auto_cancel_payout_enabled")
77 | ) {
78 | return;
79 | }
80 |
81 | frappe.validate = false;
82 |
83 | return new Promise((resolve) => {
84 | const continue_cancellation = () => {
85 | frappe.validate = true;
86 | resolve();
87 | };
88 |
89 | return show_cancel_payout_dialog(frm, continue_cancellation);
90 | });
91 | },
92 | });
93 |
94 | // ############ HELPERS ############ //
95 | function is_already_paid(frm) {
96 | return payment_integration_utils.is_already_paid(frm);
97 | }
98 |
99 | function has_payout_permissions(frm) {
100 | return payment_integration_utils.user_has_payment_permissions(frm);
101 | }
102 |
103 | function get_indicator(status) {
104 | return razorpayx.PAYOUT_STATUS[status] || "grey";
105 | }
106 |
107 | function get_rpx_img_container(txt, styles = "", classes = "") {
108 | return `
109 |
${__(txt)}
110 |

111 |
`;
112 | }
113 |
114 | function set_razorpayx_state_description(frm) {
115 | if (frm.doc.__islocal) return;
116 |
117 | const status = frm.doc.razorpayx_payout_status;
118 |
119 | // prettier-ignore
120 | // eslint-disable-next-line
121 | const description = `
122 | ${status}
123 | ${get_rpx_img_container("via")}
124 |
`;
125 |
126 | frm.get_field("payment_type").set_new_description(description);
127 | }
128 |
129 | function set_reference_no_description(frm) {
130 | // only payout link available and got cancelled
131 | function is_payout_link_cancelled() {
132 | return (
133 | frm.doc.razorpayx_payout_link_id &&
134 | !frm.doc.razorpayx_payout_id &&
135 | frm.doc.razorpayx_payout_status === "Cancelled"
136 | );
137 | }
138 |
139 | if (!["Reversed", "Processed"].includes(frm.doc.razorpayx_payout_status) || is_payout_link_cancelled())
140 | return;
141 |
142 | frm.get_field("reference_no").set_new_description(
143 | __("This is UTR of the payout transaction done via RazorpayX")
144 | );
145 | }
146 |
147 | // ############ MAKING PAYOUT HELPERS ############ //
148 | async function show_make_payout_dialog(frm) {
149 | if (frm.is_dirty()) {
150 | frappe.throw({
151 | message: __("Please save the document's changes before making payout."),
152 | title: __("Unsaved Changes"),
153 | });
154 | }
155 |
156 | // depends on conditions
157 | const BANK_MODE = `["${TRANSFER_METHOD.NEFT}", "${TRANSFER_METHOD.RTGS}", "${TRANSFER_METHOD.IMPS}"].includes(doc.payment_transfer_method)`;
158 | const UPI_MODE = `doc.payment_transfer_method === '${TRANSFER_METHOD.UPI}'`;
159 | const LINK_MODE = `doc.payment_transfer_method === '${TRANSFER_METHOD.LINK}'`;
160 |
161 | const dialog = new frappe.ui.Dialog({
162 | title: __("Enter Payout Details"),
163 | fields: [
164 | {
165 | fieldname: "party_account_sec_break",
166 | label: __("Party Account Details"),
167 | fieldtype: "Section Break",
168 | },
169 | {
170 | fieldname: "party_bank_account",
171 | label: __("Party Bank Account"),
172 | fieldtype: "Link",
173 | options: "Bank Account",
174 | default: frm.doc.party_bank_account,
175 | get_query: function () {
176 | return {
177 | filters: {
178 | is_company_account: 0,
179 | party: frm.doc.party,
180 | party_type: frm.doc.party_type,
181 | },
182 | };
183 | },
184 | onchange: async function () {
185 | set_party_bank_details(dialog);
186 | },
187 | },
188 | {
189 | fieldname: "party_acc_cb",
190 | fieldtype: "Column Break",
191 | },
192 | {
193 | fieldname: "party_bank_account_no",
194 | label: "Party Bank Account No",
195 | fieldtype: "Data",
196 | read_only: 1,
197 | depends_on: `eval: ${BANK_MODE}`,
198 | mandatory_depends_on: `eval: ${BANK_MODE}`,
199 | default: frm.doc.party_bank_account_no,
200 | },
201 | {
202 | fieldname: "party_bank_ifsc",
203 | label: "Party Bank IFSC Code",
204 | fieldtype: "Data",
205 | read_only: 1,
206 | depends_on: `eval: ${BANK_MODE}`,
207 | mandatory_depends_on: `eval: ${BANK_MODE}`,
208 | default: frm.doc.party_bank_ifsc,
209 | },
210 | {
211 | fieldname: "party_upi_id",
212 | label: "Party UPI ID",
213 | fieldtype: "Data",
214 | read_only: 1,
215 | depends_on: `eval: ${UPI_MODE}`,
216 | mandatory_depends_on: `eval: ${UPI_MODE}`,
217 | default: frm.doc.party_upi_id,
218 | },
219 | {
220 | fieldname: "party_contact_sec_break",
221 | label: __("Party Contact Details"),
222 | fieldtype: "Section Break",
223 | },
224 | {
225 | fieldname: "contact_person",
226 | label: __("Contact"),
227 | fieldtype: "Link",
228 | options: "Contact",
229 | default: frm.doc.contact_person,
230 | mandatory_depends_on: `eval: ${LINK_MODE} && ${frm.doc.party_type !== "Employee"}`,
231 | depends_on: `eval: ${frm.doc.party_type !== "Employee"}`,
232 | get_query: function () {
233 | return {
234 | filters: {
235 | link_doctype: frm.doc.party_type,
236 | link_name: frm.doc.party,
237 | },
238 | };
239 | },
240 | onchange: async function () {
241 | set_contact_details(dialog);
242 | },
243 | },
244 | {
245 | fieldname: "contact_email",
246 | label: "Email",
247 | fieldtype: "Data",
248 | options: "Email",
249 | depends_on: "eval: doc.contact_email",
250 | read_only: 1,
251 | default: frm.doc.contact_email,
252 | },
253 | {
254 | fieldname: "party_contact_cb",
255 | fieldtype: "Column Break",
256 | },
257 | {
258 | fieldname: "contact_mobile",
259 | label: "Mobile",
260 | fieldtype: "Data",
261 | options: "Phone",
262 | depends_on: "eval: doc.contact_mobile",
263 | read_only: 1,
264 | default: frm.doc.contact_mobile,
265 | },
266 |
267 | {
268 | fieldname: "payout_section_break",
269 | label: __("Payout Details"),
270 | fieldtype: "Section Break",
271 | },
272 | {
273 | fieldname: "payment_transfer_method",
274 | label: __("Payout Transfer Method"),
275 | fieldtype: "Select",
276 | options: Object.values(TRANSFER_METHOD),
277 | default: frm.doc.payment_transfer_method,
278 | reqd: 1,
279 | description: `
280 | ${get_rpx_img_container("via")}
281 |
`,
282 | },
283 | {
284 | fieldname: "payout_cb",
285 | fieldtype: "Column Break",
286 | },
287 | {
288 | fieldname: "razorpayx_payout_desc",
289 | label: __("Payout Description"),
290 | fieldtype: "Data",
291 | length: 30,
292 | mandatory_depends_on: `eval: ${LINK_MODE}`,
293 | default: frm.doc.razorpayx_payout_desc,
294 | },
295 | ],
296 | primary_action_label: __("{0} Pay", [frappe.utils.icon(payment_integration_utils.PAY_ICON)]),
297 | primary_action: (values) => {
298 | razorpayx.validate_payout_description(values.razorpayx_payout_desc);
299 | payment_integration_utils.validate_payment_transfer_method(
300 | values.payment_transfer_method,
301 | frm.doc.paid_amount
302 | );
303 |
304 | dialog.hide();
305 |
306 | payment_integration_utils.authenticate_payment_entries(frm.docname, async (auth_id) => {
307 | await make_payout(auth_id, frm.docname, values);
308 |
309 | frappe.show_alert({
310 | message: __("Payout has been made successfully."),
311 | indicator: "green",
312 | });
313 | });
314 | },
315 | });
316 |
317 | dialog.show();
318 | }
319 |
320 | function make_payout(auth_id, docname, values) {
321 | return frappe.call({
322 | method: `${PE_BASE_PATH}.make_payout_with_razorpayx`,
323 | args: {
324 | auth_id: auth_id,
325 | docname: docname,
326 | transfer_method: values.payment_transfer_method,
327 | ...values,
328 | },
329 | freeze: true,
330 | freeze_message: __("Making Payout ..."),
331 | });
332 | }
333 |
334 | async function set_party_bank_details(dialog) {
335 | const party_bank_account = dialog.get_value("party_bank_account");
336 |
337 | if (!party_bank_account) {
338 | dialog.set_value("payment_transfer_method", TRANSFER_METHOD.LINK);
339 | return;
340 | }
341 |
342 | dialog.set_value("payment_transfer_method", TRANSFER_METHOD.NEFT);
343 |
344 | const response = await frappe.db.get_value("Bank Account", party_bank_account, [
345 | "branch_code as party_bank_ifsc",
346 | "bank_account_no as party_bank_account_no",
347 | "upi_id as party_upi_id",
348 | ]);
349 |
350 | dialog.set_values(response.message);
351 | }
352 |
353 | async function set_contact_details(dialog) {
354 | const contact_person = dialog.get_value("contact_person");
355 |
356 | if (!contact_person) {
357 | dialog.set_values({
358 | contact_email: "",
359 | contact_mobile: "",
360 | });
361 | return;
362 | }
363 |
364 | const response = await frappe.call({
365 | method: "frappe.contacts.doctype.contact.contact.get_contact_details",
366 | args: { contact: contact_person },
367 | });
368 |
369 | dialog.set_values({
370 | contact_email: response.message.contact_email,
371 | contact_mobile: response.message.contact_mobile,
372 | });
373 | }
374 |
375 | // ############ CANCELING PAYOUT HELPERS ############ //
376 | function show_cancel_payout_dialog(frm, callback) {
377 | const dialog = new frappe.ui.Dialog({
378 | title: __("Cancel Payout"),
379 | fields: [
380 | {
381 | fieldname: "cancel_payout",
382 | label: __("Cancel Payout"),
383 | fieldtype: "Check",
384 | default: 1,
385 | description: __("Payout will be cancelled along with Payment Entry if checked."),
386 | },
387 | ],
388 | primary_action_label: __("Continue"),
389 | primary_action: (values) => {
390 | dialog.hide();
391 |
392 | frappe.call({
393 | method: `${PE_BASE_PATH}.mark_payout_for_cancellation`,
394 | args: {
395 | docname: frm.docname,
396 | cancel: values.cancel_payout,
397 | },
398 | });
399 |
400 | callback && callback();
401 | },
402 | });
403 |
404 | // Make primary action button Background Red
405 | dialog.get_primary_btn().removeClass("btn-primary").addClass("btn-danger");
406 | dialog.show();
407 | }
408 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/constants/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/razorpayx_integration/constants/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/constants/custom_fields.py:
--------------------------------------------------------------------------------
1 | """
2 | Custom fields which are helpful for payments via RazorpayX
3 |
4 | Note:
5 | - Keep sequence like this:
6 | 1. fieldname
7 | 2. label
8 | 3. fieldtype
9 | 4. insert_after
10 | ...
11 | """
12 |
13 | from payment_integration_utils.payment_integration_utils.constants.payments import (
14 | TRANSFER_METHOD,
15 | )
16 |
17 | from razorpayx_integration.constants import RAZORPAYX_CONFIG
18 | from razorpayx_integration.razorpayx_integration.constants.payouts import PAYOUT_STATUS
19 | from razorpayx_integration.razorpayx_integration.constants.roles import PERMISSION_LEVEL
20 |
21 | PAYOUT_VIA_RAZORPAYX = f"doc.make_bank_online_payment && doc.integration_doctype === '{RAZORPAYX_CONFIG}' && doc.integration_docname"
22 | PAYOUT_BASE_CONDITION = f"doc.payment_type=='Pay' && doc.party && doc.party_type && doc.paid_from_account_currency === 'INR' && {PAYOUT_VIA_RAZORPAYX}"
23 |
24 | CUSTOM_FIELDS = {
25 | "Payment Entry": [
26 | #### PAYOUT SECTION START ####
27 | {
28 | "fieldname": "razorpayx_payout_section",
29 | "label": "RazorpayX Payout Details",
30 | "fieldtype": "Section Break",
31 | "insert_after": "integration_docname", ## Insert After `Integration Docname` field (Payment Utils Custom Field)
32 | "depends_on": f"eval: {PAYOUT_BASE_CONDITION}",
33 | "collapsible": 1,
34 | "collapsible_depends_on": "eval: doc.docstatus === 0",
35 | "permlevel": PERMISSION_LEVEL.SEVEN.value,
36 | },
37 | {
38 | "fieldname": "razorpayx_payout_desc",
39 | "label": "Payout Description",
40 | "fieldtype": "Data",
41 | "insert_after": "razorpayx_payout_section",
42 | "depends_on": "eval: doc.make_bank_online_payment",
43 | "mandatory_depends_on": f"eval: {PAYOUT_VIA_RAZORPAYX} && doc.payment_transfer_method === '{TRANSFER_METHOD.LINK.value}'",
44 | "length": 30,
45 | "permlevel": PERMISSION_LEVEL.SEVEN.value,
46 | "no_copy": 1,
47 | },
48 | {
49 | "fieldname": "razorpayx_payout_cb",
50 | "fieldtype": "Column Break",
51 | "insert_after": "razorpayx_payout_desc",
52 | "permlevel": PERMISSION_LEVEL.SEVEN.value,
53 | },
54 | {
55 | "fieldname": "razorpayx_payout_status",
56 | "label": "RazorpayX Payout Status",
57 | "fieldtype": "Select",
58 | "insert_after": "razorpayx_payout_cb",
59 | "options": PAYOUT_STATUS.title_case_values(as_string=True),
60 | "default": PAYOUT_STATUS.NOT_INITIATED.value.title(),
61 | "depends_on": f"eval: {PAYOUT_VIA_RAZORPAYX} && doc.creation",
62 | "read_only": 1,
63 | "allow_on_submit": 1,
64 | "in_list_view": 0, # TODO: remove after split
65 | "in_standard_filter": 1,
66 | "permlevel": PERMISSION_LEVEL.SEVEN.value,
67 | "no_copy": 1,
68 | },
69 | {
70 | "fieldname": "razorpayx_payout_id_sec",
71 | "label": "RazorpayX Payout ID Section",
72 | "fieldtype": "Section Break",
73 | "insert_after": "razorpayx_payout_status",
74 | "hidden": 1,
75 | },
76 | {
77 | "fieldname": "razorpayx_payout_id",
78 | "label": "RazorpayX Payout ID",
79 | "fieldtype": "Data",
80 | "insert_after": "razorpayx_payout_id_sec",
81 | "read_only": 1,
82 | "hidden": 1,
83 | "print_hide": 1,
84 | "permlevel": PERMISSION_LEVEL.SEVEN.value,
85 | "no_copy": 1,
86 | },
87 | {
88 | "fieldname": "razorpayx_id_cb",
89 | "fieldtype": "Column Break",
90 | "insert_after": "razorpayx_payout_id",
91 | },
92 | {
93 | "fieldname": "razorpayx_payout_link_id",
94 | "label": "RazorpayX Payout Link ID",
95 | "fieldtype": "Data",
96 | "insert_after": "razorpayx_id_cb",
97 | "read_only": 1,
98 | "hidden": 1,
99 | "print_hide": 1,
100 | "permlevel": PERMISSION_LEVEL.SEVEN.value,
101 | "no_copy": 1,
102 | },
103 | #### PAYMENT SECTION END ####
104 | ],
105 | }
106 |
107 | # payments_processor App fields
108 | PROCESSOR_FIELDS = {
109 | RAZORPAYX_CONFIG: [
110 | {
111 | "fieldname": "pay_on_auto_submit",
112 | "label": "Pay on Auto Submit",
113 | "fieldtype": "Check",
114 | "insert_after": "auto_cancel_payout",
115 | "default": "1",
116 | "description": "If the Payment Entry is submitted via the Payments Processor, then the payout will be initiated automatically.",
117 | },
118 | ]
119 | }
120 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/constants/payouts.py:
--------------------------------------------------------------------------------
1 | from payment_integration_utils.payment_integration_utils.constants.enums import BaseEnum
2 |
3 | ### REGEX ###
4 | DESCRIPTION_REGEX = r"^[a-zA-Z0-9\s]{1,30}$"
5 |
6 | ### OTHERS ###
7 | STATUS_NOTIFICATION_METHOD = "send_rpx_payout_status_update"
8 |
9 |
10 | ### ENUMS ###
11 | class PAYOUT_FROM(BaseEnum):
12 | CURRENT_ACCOUNT = "Current Account"
13 | RAZORPAYX_LITE = "RazorpayX Lite"
14 |
15 |
16 | class CONTACT_TYPE(BaseEnum):
17 | """
18 | Default Contact Type available in RazorpayX.
19 | """
20 |
21 | EMPLOYEE = "employee"
22 | SUPPLIER = "vendor"
23 | CUSTOMER = "customer"
24 | SELF = "self"
25 |
26 |
27 | class FUND_ACCOUNT_TYPE(BaseEnum):
28 | BANK_ACCOUNT = "bank_account"
29 | VPA = "vpa"
30 | # CARD = "card" # ! Not supported currently
31 |
32 |
33 | class PAYOUT_CURRENCY(BaseEnum):
34 | INR = "INR"
35 |
36 |
37 | class PAYOUT_PURPOSE(BaseEnum):
38 | """
39 | Default payout purpose available in RazorpayX.
40 | """
41 |
42 | REFUND = "refund"
43 | CASH_BACK = "cashback"
44 | PAYOUT = "payout"
45 | SALARY = "salary"
46 | UTILITY_BILL = "utility bill"
47 | VENDOR_BILL = "vendor bill"
48 |
49 |
50 | class PAYOUT_STATUS(BaseEnum):
51 | """
52 | Reference:
53 | - https://razorpay.com/docs/x/payouts/states-life-cycle/#payout-states
54 | """
55 |
56 | # Custom Status
57 | NOT_INITIATED = "not initiated"
58 |
59 | # RazorpayX Payout Status
60 | PENDING = "pending"
61 | QUEUED = "queued"
62 | SCHEDULED = "scheduled"
63 | PROCESSING = "processing"
64 | PROCESSED = "processed"
65 | CANCELLED = "cancelled"
66 | REJECTED = "rejected"
67 | FAILED = "failed"
68 | REVERSED = "reversed"
69 |
70 |
71 | class PAYOUT_LINK_STATUS(BaseEnum):
72 | """
73 | Reference:
74 | - https://razorpay.com/docs/x/payout-links/life-cycle/
75 | """
76 |
77 | # RazorpayX Payout Link Status
78 | PENDING = "pending"
79 | ISSUED = "issued"
80 | PROCESSING = "processing"
81 | PROCESSED = "processed"
82 | CANCELLED = "cancelled"
83 | REJECTED = "rejected"
84 | EXPIRED = "expired"
85 |
86 |
87 | ### MAPPINGS ###
88 | PAYOUT_ORDERS = {
89 | PAYOUT_STATUS.NOT_INITIATED.value: 1, # custom
90 | PAYOUT_STATUS.PENDING.value: 2,
91 | PAYOUT_STATUS.SCHEDULED.value: 2,
92 | PAYOUT_STATUS.QUEUED.value: 3,
93 | PAYOUT_STATUS.PROCESSING.value: 4,
94 | PAYOUT_STATUS.PROCESSED.value: 5,
95 | PAYOUT_STATUS.CANCELLED.value: 5,
96 | PAYOUT_STATUS.FAILED.value: 5,
97 | PAYOUT_STATUS.REJECTED.value: 5,
98 | PAYOUT_STATUS.REVERSED.value: 6,
99 | }
100 |
101 | PAYOUT_LINK_ORDERS = {
102 | PAYOUT_LINK_STATUS.PENDING.value: 1,
103 | PAYOUT_LINK_STATUS.ISSUED.value: 2,
104 | PAYOUT_LINK_STATUS.PROCESSING.value: 3,
105 | PAYOUT_LINK_STATUS.PROCESSED.value: 4,
106 | PAYOUT_LINK_STATUS.CANCELLED.value: 4,
107 | PAYOUT_LINK_STATUS.REJECTED.value: 4,
108 | PAYOUT_LINK_STATUS.EXPIRED.value: 4,
109 | }
110 |
111 | PAYOUT_PURPOSE_MAP = {
112 | "Supplier": PAYOUT_PURPOSE.VENDOR_BILL.value,
113 | "Customer": PAYOUT_PURPOSE.REFUND.value,
114 | "Employee": PAYOUT_PURPOSE.SALARY.value,
115 | }
116 |
117 | CONTACT_TYPE_MAP = {
118 | "Supplier": CONTACT_TYPE.SUPPLIER.value,
119 | "Customer": CONTACT_TYPE.CUSTOMER.value,
120 | "Employee": CONTACT_TYPE.EMPLOYEE.value,
121 | }
122 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/constants/property_setters.py:
--------------------------------------------------------------------------------
1 | PROPERTY_SETTERS = []
2 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/constants/roles.py:
--------------------------------------------------------------------------------
1 | from payment_integration_utils.payment_integration_utils.constants.enums import BaseEnum
2 | from payment_integration_utils.payment_integration_utils.constants.roles import (
3 | PERMISSION_LEVEL,
4 | PERMISSIONS,
5 | )
6 | from payment_integration_utils.payment_integration_utils.constants.roles import (
7 | ROLE_PROFILE as PAYMENT_PROFILES,
8 | )
9 |
10 | from razorpayx_integration.constants import RAZORPAYX_CONFIG
11 |
12 |
13 | class ROLE_PROFILE(BaseEnum):
14 | RAZORPAYX_MANAGER = "RazorpayX Integration Manager"
15 |
16 |
17 | ROLES = [
18 | ## RazorpayX Configuration ##
19 | {
20 | "doctype": RAZORPAYX_CONFIG,
21 | "role_name": ROLE_PROFILE.RAZORPAYX_MANAGER.value,
22 | "permlevels": PERMISSION_LEVEL.ZERO.value,
23 | "permissions": PERMISSIONS["Manager"],
24 | },
25 | {
26 | "doctype": RAZORPAYX_CONFIG,
27 | "role_name": PAYMENT_PROFILES.PAYMENT_AUTHORIZER.value,
28 | "permlevels": PERMISSION_LEVEL.ZERO.value,
29 | "permissions": PERMISSIONS["Basic"],
30 | },
31 | ## Bank Account ##
32 | {
33 | "doctype": "Bank Account",
34 | "role_name": ROLE_PROFILE.RAZORPAYX_MANAGER.value,
35 | "permlevels": PERMISSION_LEVEL.ZERO.value,
36 | "permissions": PERMISSIONS["Basic"],
37 | },
38 | ]
39 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/constants/webhooks.py:
--------------------------------------------------------------------------------
1 | from payment_integration_utils.payment_integration_utils.constants.enums import BaseEnum
2 |
3 |
4 | class EVENTS_TYPE(BaseEnum):
5 | PAYOUT = "payout"
6 | PAYOUT_LINK = "payout_link"
7 | TRANSACTION = "transaction"
8 | ACCOUNT = "fund_account" # ! NOTE: currently not supported
9 |
10 |
11 | class FUND_ACCOUNT_EVENT(BaseEnum):
12 | """
13 | Reference: https://razorpay.com/docs/webhooks/payloads/x/account-validation/
14 | """
15 |
16 | COMPLETED = "fund_account.validation.completed" # ! NOTE: currently not supported
17 | FAILED = "fund_account.validation.failed" # ! NOTE: currently not supported
18 |
19 |
20 | class PAYOUT_EVENT(BaseEnum):
21 | """
22 | References:
23 | - https://razorpay.com/docs/webhooks/payloads/x/payouts/
24 | - https://razorpay.com/docs/webhooks/payloads/x/payouts-approval/
25 | """
26 |
27 | PENDING = "payout.pending"
28 | REJECTED = "payout.rejected"
29 | QUEUED = "payout.queued"
30 | INITIATED = "payout.initiated"
31 | PROCESSED = "payout.processed"
32 | REVERSED = "payout.reversed"
33 | FAILED = "payout.failed"
34 | UPDATED = "payout.updated" # ! NOTE: currently not supported
35 | DOWNTIME_STARTED = "payout.downtime.started" # ! NOTE: currently not supported
36 | DOWNTIME_RESOLVED = "payout.downtime.resolved" # ! NOTE: currently not supported
37 |
38 |
39 | class PAYOUT_LINK_EVENT(BaseEnum):
40 | """
41 | Reference: https://razorpay.com/docs/webhooks/payloads/x/payout-links/
42 | """
43 |
44 | PENDING = "payout_link.pending" # ! NOTE: currently not supported
45 | PROCESSING = "payout_link.processing" # ! NOTE: currently not supported
46 | PROCESSED = "payout_link.processed" # ! NOTE: currently not supported
47 | ATTEMPTED = "payout_link.attempted" # ! NOTE: currently not supported
48 | CANCELLED = "payout_link.cancelled"
49 | REJECTED = "payout_link.rejected"
50 | EXPIRED = "payout_link.expired"
51 |
52 |
53 | class TRANSACTION_EVENT(BaseEnum):
54 | """
55 | Reference: https://razorpay.com/docs/webhooks/payloads/x/transactions/
56 | """
57 |
58 | CREATED = "transaction.created"
59 |
60 |
61 | class TRANSACTION_TYPE(BaseEnum):
62 | PAYOUT = "payout"
63 | REVERSAL = "reversal"
64 | BANK_TRANSFER = "bank_transfer"
65 |
66 |
67 | SUPPORTED_EVENTS = (
68 | ## PAYOUT ##
69 | PAYOUT_EVENT.PENDING.value,
70 | PAYOUT_EVENT.REJECTED.value,
71 | PAYOUT_EVENT.QUEUED.value,
72 | PAYOUT_EVENT.INITIATED.value,
73 | PAYOUT_EVENT.PROCESSED.value,
74 | PAYOUT_EVENT.REVERSED.value,
75 | PAYOUT_EVENT.FAILED.value,
76 | ## PAYOUT LINK ##
77 | PAYOUT_LINK_EVENT.CANCELLED.value,
78 | PAYOUT_LINK_EVENT.REJECTED.value,
79 | PAYOUT_LINK_EVENT.EXPIRED.value,
80 | ## TRANSACTION ##
81 | TRANSACTION_EVENT.CREATED.value,
82 | )
83 |
84 | # payload > source > entity
85 | SUPPORTED_TRANSACTION_TYPES = (TRANSACTION_TYPE.REVERSAL.value,)
86 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/doctype/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/razorpayx_integration/doctype/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/doctype/razorpayx_configuration/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/razorpayx_integration/doctype/razorpayx_configuration/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/doctype/razorpayx_configuration/razorpayx_configuration.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2024, Resilient Tech and contributors
2 | // For license information, please see license.txt
3 | const WEBHOOK_PATH = "razorpayx_integration.razorpayx_integration.utils.webhook.webhook_listener";
4 |
5 | frappe.ui.form.on("RazorpayX Configuration", {
6 | setup: function (frm) {
7 | frm.set_query("bank_account", function () {
8 | return {
9 | filters: {
10 | is_company_account: 1,
11 | disabled: 0,
12 | },
13 | };
14 | });
15 |
16 | const coa_filters = {
17 | account_currency: "INR",
18 | freeze_account: "No",
19 | root_type: "Liability",
20 | company: frm.doc.company,
21 | };
22 |
23 | frm.set_query("creditors_account", function () {
24 | return {
25 | filters: {
26 | ...coa_filters,
27 | account_type: "Payable",
28 | },
29 | };
30 | });
31 |
32 | frm.set_query("payable_account", function () {
33 | return {
34 | filters: coa_filters,
35 | };
36 | });
37 | },
38 |
39 | onload: function (frm) {
40 | if (!frm.is_new()) return;
41 |
42 | frm.set_intro(
43 | __(
44 | `Get RazorpayX API's Key ID and Secret from
45 |
46 | here {0}
47 |
48 | if not available.`,
49 | [frappe.utils.icon("link-url")]
50 | )
51 | );
52 | },
53 |
54 | refresh: function (frm) {
55 | // listener to copy webhook url
56 | frm.$wrapper.find(".webhook-url").on("click", function () {
57 | frappe.utils.copy_to_clipboard(`${frappe.urllib.get_base_url()}/api/method/${WEBHOOK_PATH}`);
58 | });
59 |
60 | if (frm.doc.__islocal) return;
61 |
62 | frm.add_custom_button(__("Sync Transactions"), () => {
63 | prompt_transactions_sync_date(frm);
64 | });
65 | },
66 |
67 | after_save: function (frm) {
68 | if (frm.doc.webhook_secret) return;
69 |
70 | frappe.show_alert({
71 | message: __("Webhook Secret is missing!
You will not receive any updates!"),
72 | indicator: "orange",
73 | });
74 | },
75 | });
76 |
77 | function prompt_transactions_sync_date(frm) {
78 | const default_range = [frm.doc.last_sync_on || frappe.datetime.month_start(), frappe.datetime.now_date()];
79 | const dialog = new frappe.ui.Dialog({
80 | title: __("Sync {0} Transactions", [frm.doc.bank_account]),
81 | fields: [
82 | {
83 | label: __("Date Range"),
84 | fieldname: "date_range",
85 | fieldtype: "DateRange",
86 | reqd: 1,
87 | default: default_range,
88 | },
89 | ],
90 | primary_action_label: __("{0} Sync", [frappe.utils.icon("refresh")]),
91 | primary_action: function (values) {
92 | sync_transactions(frm.docname, frm.doc.bank_account, ...values.date_range);
93 | dialog.hide();
94 | },
95 | size: "small",
96 | });
97 |
98 | dialog.get_field("date_range").datepicker.update({
99 | maxDate: new Date(frappe.datetime.get_today()),
100 | });
101 |
102 | // defaults are removed when setting maxDate
103 | dialog.set_df_property("date_range", "default", default_range);
104 |
105 | dialog.show();
106 | }
107 |
108 | function sync_transactions(razorpayx_config, bank_account, from_date, to_date) {
109 | frappe.show_alert({
110 | message: __("Syncing Transactions from {0} to {1}", [
111 | payment_integration_utils.get_date_in_user_fmt(from_date),
112 | payment_integration_utils.get_date_in_user_fmt(to_date),
113 | ]),
114 | indicator: "blue",
115 | });
116 |
117 | frappe.call({
118 | method: "razorpayx_integration.razorpayx_integration.utils.bank_transaction.sync_bank_transactions_with_razorpayx",
119 | args: { razorpayx_config, bank_account, from_date, to_date },
120 | callback: function (r) {
121 | //TODO: If it is enqueued, need changes!!
122 | if (!r.exc) {
123 | frappe.show_alert({
124 | message: __("{0} transactions synced successfully!", [razorpayx_config]),
125 | indicator: "green",
126 | });
127 | }
128 | },
129 | });
130 | }
131 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/doctype/razorpayx_configuration/razorpayx_configuration.json:
--------------------------------------------------------------------------------
1 | {
2 | "actions": [],
3 | "autoname": "format:{bank_account}",
4 | "creation": "2024-11-28 17:05:21.010259",
5 | "default_view": "List",
6 | "doctype": "DocType",
7 | "editable_grid": 1,
8 | "engine": "InnoDB",
9 | "field_order": [
10 | "api_tab",
11 | "disabled",
12 | "api_details_section",
13 | "key_id",
14 | "key_secret",
15 | "column_break_8tcki",
16 | "account_id",
17 | "webhook_secret",
18 | "account_details_section",
19 | "bank_account",
20 | "company_account",
21 | "company",
22 | "column_break_3gfbs",
23 | "payouts_from",
24 | "bank",
25 | "ifsc_code",
26 | "account_number",
27 | "section_break_bsie",
28 | "last_sync_on",
29 | "column_break_mken",
30 | "pe_config_section",
31 | "auto_cancel_payout",
32 | "accounting_tab",
33 | "fees_section",
34 | "automate_fees_accounting",
35 | "column_break_rshi",
36 | "fees_accounting_section",
37 | "creditors_account",
38 | "payable_account",
39 | "column_break_vdui",
40 | "supplier",
41 | "payout_reversal_section",
42 | "create_je_on_reversal",
43 | "column_break_ntie"
44 | ],
45 | "fields": [
46 | {
47 | "fieldname": "key_id",
48 | "fieldtype": "Data",
49 | "label": "Key ID",
50 | "no_copy": 1,
51 | "reqd": 1
52 | },
53 | {
54 | "fieldname": "key_secret",
55 | "fieldtype": "Password",
56 | "label": "Key Secret",
57 | "no_copy": 1,
58 | "reqd": 1
59 | },
60 | {
61 | "fieldname": "api_details_section",
62 | "fieldtype": "Section Break",
63 | "label": "API Details"
64 | },
65 | {
66 | "fieldname": "column_break_8tcki",
67 | "fieldtype": "Column Break"
68 | },
69 | {
70 | "description": "Copy webhook URL",
71 | "fieldname": "webhook_secret",
72 | "fieldtype": "Password",
73 | "label": "Webhook Secret",
74 | "no_copy": 1
75 | },
76 | {
77 | "fieldname": "account_details_section",
78 | "fieldtype": "Section Break",
79 | "label": "Account Details"
80 | },
81 | {
82 | "fieldname": "bank_account",
83 | "fieldtype": "Link",
84 | "in_list_view": 1,
85 | "label": "Bank Account",
86 | "no_copy": 1,
87 | "options": "Bank Account",
88 | "reqd": 1,
89 | "unique": 1
90 | },
91 | {
92 | "fieldname": "column_break_3gfbs",
93 | "fieldtype": "Column Break"
94 | },
95 | {
96 | "default": "0",
97 | "fieldname": "disabled",
98 | "fieldtype": "Check",
99 | "in_standard_filter": 1,
100 | "label": "Disabled",
101 | "no_copy": 1
102 | },
103 | {
104 | "depends_on": "eval: doc.bank_account",
105 | "fetch_from": "bank_account.company",
106 | "fieldname": "company",
107 | "fieldtype": "Link",
108 | "in_list_view": 1,
109 | "in_standard_filter": 1,
110 | "label": "Company",
111 | "no_copy": 1,
112 | "options": "Company",
113 | "read_only": 1
114 | },
115 | {
116 | "fetch_from": "bank_account.bank",
117 | "fieldname": "bank",
118 | "fieldtype": "Link",
119 | "in_standard_filter": 1,
120 | "label": "Bank",
121 | "no_copy": 1,
122 | "options": "Bank",
123 | "read_only": 1
124 | },
125 | {
126 | "fetch_from": "bank_account.bank_account_no",
127 | "fieldname": "account_number",
128 | "fieldtype": "Data",
129 | "label": "Account Number",
130 | "no_copy": 1,
131 | "read_only": 1
132 | },
133 | {
134 | "fetch_from": "bank_account.branch_code",
135 | "fieldname": "ifsc_code",
136 | "fieldtype": "Data",
137 | "label": "IFSC Code",
138 | "no_copy": 1,
139 | "read_only": 1
140 | },
141 | {
142 | "fetch_from": "bank_account.account",
143 | "fieldname": "company_account",
144 | "fieldtype": "Link",
145 | "label": "Company Account",
146 | "no_copy": 1,
147 | "options": "Account",
148 | "read_only": 1
149 | },
150 | {
151 | "fieldname": "api_tab",
152 | "fieldtype": "Tab Break",
153 | "label": "API"
154 | },
155 | {
156 | "description": " This is a Business ID found in RazorpayX settings.\n\nGet it from here.",
157 | "fieldname": "account_id",
158 | "fieldtype": "Data",
159 | "in_list_view": 1,
160 | "label": "Account ID",
161 | "no_copy": 1,
162 | "reqd": 1,
163 | "unique": 1
164 | },
165 | {
166 | "default": "0",
167 | "description": "Payout and Payout Link will be automatically cancelled on Payment Entry cancellation if possible.",
168 | "fieldname": "auto_cancel_payout",
169 | "fieldtype": "Check",
170 | "label": "Automatically Cancel Payout on Payment Entry Cancellation"
171 | },
172 | {
173 | "fieldname": "pe_config_section",
174 | "fieldtype": "Section Break",
175 | "label": "Configurations"
176 | },
177 | {
178 | "fieldname": "section_break_bsie",
179 | "fieldtype": "Section Break"
180 | },
181 | {
182 | "description": "Bank Transactions Synchronised upto this date",
183 | "fieldname": "last_sync_on",
184 | "fieldtype": "Date",
185 | "label": "Last Sync On",
186 | "read_only": 1
187 | },
188 | {
189 | "fieldname": "column_break_mken",
190 | "fieldtype": "Column Break"
191 | },
192 | {
193 | "fieldname": "column_break_vdui",
194 | "fieldtype": "Column Break"
195 | },
196 | {
197 | "depends_on": "eval: doc.payouts_from === \"Current Account\"",
198 | "fieldname": "payable_account",
199 | "fieldtype": "Link",
200 | "label": "Payable Account",
201 | "mandatory_depends_on": "eval: doc.automate_fees_accounting && doc.payouts_from === \"Current Account\"",
202 | "options": "Account"
203 | },
204 | {
205 | "fieldname": "creditors_account",
206 | "fieldtype": "Link",
207 | "label": "Creditors Account",
208 | "mandatory_depends_on": "eval: doc.automate_fees_accounting",
209 | "options": "Account"
210 | },
211 | {
212 | "default": "1",
213 | "description": "Create a Journal Entry for the Payout fees and tax.",
214 | "fieldname": "automate_fees_accounting",
215 | "fieldtype": "Check",
216 | "label": "Automate Fees Accounting"
217 | },
218 | {
219 | "fieldname": "column_break_rshi",
220 | "fieldtype": "Column Break"
221 | },
222 | {
223 | "depends_on": "eval: doc.automate_fees_accounting",
224 | "fieldname": "fees_accounting_section",
225 | "fieldtype": "Section Break"
226 | },
227 | {
228 | "depends_on": "eval: doc.bank_account",
229 | "fieldname": "accounting_tab",
230 | "fieldtype": "Tab Break",
231 | "label": "Accounting"
232 | },
233 | {
234 | "fieldname": "supplier",
235 | "fieldtype": "Link",
236 | "label": "Supplier",
237 | "mandatory_depends_on": "eval: doc.automate_fees_accounting",
238 | "options": "Supplier"
239 | },
240 | {
241 | "default": "Current Account",
242 | "depends_on": "eval: doc.bank_account",
243 | "fieldname": "payouts_from",
244 | "fieldtype": "Select",
245 | "label": "Payouts from",
246 | "mandatory_depends_on": "eval: doc.bank_account",
247 | "options": "Current Account\nRazorpayX Lite"
248 | },
249 | {
250 | "description": "Read RazorpayX\n\n \n Fees and Tax\n \n \n \nfor details on charges and deductions.",
251 | "fieldname": "fees_section",
252 | "fieldtype": "Section Break",
253 | "label": "Fees and Tax"
254 | },
255 | {
256 | "fieldname": "payout_reversal_section",
257 | "fieldtype": "Section Break",
258 | "label": "Payout Reversal"
259 | },
260 | {
261 | "fieldname": "column_break_ntie",
262 | "fieldtype": "Column Break"
263 | },
264 | {
265 | "default": "1",
266 | "description": "Automatically create a Journal Entry on Reversal of Payout and Unreconcile the Payment Entry.",
267 | "fieldname": "create_je_on_reversal",
268 | "fieldtype": "Check",
269 | "label": "Create JE on Payout Reversal"
270 | }
271 | ],
272 | "index_web_pages_for_search": 1,
273 | "links": [],
274 | "modified": "2025-03-26 09:41:53.146171",
275 | "modified_by": "Administrator",
276 | "module": "Razorpayx Integration",
277 | "name": "RazorpayX Configuration",
278 | "naming_rule": "Expression",
279 | "owner": "Administrator",
280 | "permissions": [
281 | {
282 | "create": 1,
283 | "delete": 1,
284 | "email": 1,
285 | "export": 1,
286 | "print": 1,
287 | "read": 1,
288 | "report": 1,
289 | "role": "RazorpayX Integration Manager",
290 | "select": 1,
291 | "share": 1,
292 | "write": 1
293 | },
294 | {
295 | "read": 1,
296 | "role": "Online Payments Authorizer",
297 | "select": 1
298 | }
299 | ],
300 | "sort_field": "modified",
301 | "sort_order": "DESC",
302 | "states": [],
303 | "track_changes": 1
304 | }
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/doctype/razorpayx_configuration/razorpayx_configuration.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024, Resilient Tech and contributors
2 | # For license information, please see license.txt
3 |
4 | import frappe
5 | from frappe import _
6 | from frappe.model.document import Document
7 |
8 |
9 | class RazorpayXConfiguration(Document):
10 | # begin: auto-generated types
11 | # This code is auto-generated. Do not modify anything in this block.
12 |
13 | from typing import TYPE_CHECKING
14 |
15 | if TYPE_CHECKING:
16 | from frappe.types import DF
17 |
18 | account_id: DF.Data
19 | account_number: DF.Data | None
20 | auto_cancel_payout: DF.Check
21 | automate_fees_accounting: DF.Check
22 | bank: DF.Link | None
23 | bank_account: DF.Link
24 | company: DF.Link | None
25 | company_account: DF.Link | None
26 | create_je_on_reversal: DF.Check
27 | creditors_account: DF.Link | None
28 | disabled: DF.Check
29 | ifsc_code: DF.Data | None
30 | key_id: DF.Data
31 | key_secret: DF.Password
32 | last_sync_on: DF.Date | None
33 | payable_account: DF.Link | None
34 | payouts_from: DF.Literal["Current Account", "RazorpayX Lite"]
35 | supplier: DF.Link | None
36 | webhook_secret: DF.Password | None
37 | # end: auto-generated types
38 |
39 | def validate(self):
40 | self.validate_api_credentials()
41 | self.validate_bank_account()
42 |
43 | def validate_api_credentials(self):
44 | from razorpayx_integration.razorpayx_integration.apis.validate_razorpayx import (
45 | RazorpayXValidation,
46 | )
47 |
48 | if self.disabled:
49 | return
50 |
51 | if not self.key_id or not self.key_secret:
52 | frappe.throw(
53 | msg=_("Please set RazorpayX API credentials."),
54 | title=_("API Credentials Are Missing"),
55 | )
56 |
57 | if not (
58 | self.has_value_changed("key_id")
59 | or self.has_value_changed("key_secret")
60 | or self.has_value_changed("account_number")
61 | ):
62 | return
63 |
64 | RazorpayXValidation(
65 | self.key_id,
66 | self.get_password(fieldname="key_secret"),
67 | self.account_number,
68 | ).validate_credentials()
69 |
70 | def validate_bank_account(self):
71 | bank_account = frappe.get_value(
72 | "Bank Account",
73 | self.bank_account,
74 | ["disabled", "is_company_account"],
75 | as_dict=True,
76 | )
77 |
78 | if not bank_account:
79 | frappe.throw(
80 | msg=_("Bank Account not found."),
81 | title=_("Invalid Bank Account"),
82 | )
83 |
84 | if bank_account.disabled:
85 | frappe.throw(
86 | msg=_("Bank Account is disabled. Please enable it first."),
87 | title=_("Invalid Bank Account"),
88 | )
89 |
90 | if not bank_account.is_company_account:
91 | frappe.throw(
92 | msg=_("Bank Account is not a company's bank account."),
93 | title=_("Invalid Bank Account"),
94 | )
95 |
96 | if not self.account_number:
97 | frappe.throw(
98 | msg=_(
99 | "Please set Bank Account Number in bank account."
100 | ),
101 | title=_("Account Number Is Missing"),
102 | )
103 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/doctype/razorpayx_configuration/test_razorpayx_configuration.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2024, Resilient Tech and Contributors
2 | # See license.txt
3 |
4 | # import frappe
5 | from frappe.tests.utils import FrappeTestCase
6 |
7 |
8 | class TestRazorpayXConfiguration(FrappeTestCase):
9 | pass
10 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/notification/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/razorpayx_integration/notification/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/notification/failed_payout/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/razorpayx_integration/notification/failed_payout/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/notification/failed_payout/failed_payout.json:
--------------------------------------------------------------------------------
1 | {
2 | "attach_print": 0,
3 | "channel": "Email",
4 | "condition": "doc.razorpayx_payout_status in [\"Cancelled\", \"Rejected\", \"Failed\", \"Reversed\"]",
5 | "creation": "2025-01-21 13:45:11.433147",
6 | "days_in_advance": 0,
7 | "docstatus": 0,
8 | "doctype": "Notification",
9 | "document_type": "Payment Entry",
10 | "enabled": 0,
11 | "event": "Method",
12 | "idx": 0,
13 | "is_standard": 0,
14 | "message": "Dear {{ frappe.db.get_value(\"User\", {\"email\": doc.payment_authorized_by},\"first_name\") }},
\n\nThe payout has been {{ doc.razorpayx_payout_status }}! for Payment Entry {{ doc.name }}.
\n\nFor more details, visit your ERPNext site and check the details.
\n",
15 | "message_type": "Markdown",
16 | "method": "send_rpx_payout_status_update",
17 | "modified": "2025-03-28 12:02:21.410098",
18 | "modified_by": "Administrator",
19 | "module": "Razorpayx Integration",
20 | "name": "Failed Payout",
21 | "owner": "Administrator",
22 | "recipients": [
23 | {
24 | "receiver_by_document_field": "payment_authorized_by",
25 | "receiver_by_role": ""
26 | }
27 | ],
28 | "send_system_notification": 0,
29 | "send_to_all_assignees": 0,
30 | "sender": "",
31 | "sender_email": "",
32 | "subject": "Payout has been {{ doc.razorpayx_payout_status }} for Payment Entry {{ doc.name }}",
33 | "value_changed": "razorpayx_payout_status"
34 | }
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/notification/failed_payout/failed_payout.md:
--------------------------------------------------------------------------------
1 | Dear {{ frappe.db.get_value("User", {"email": doc.payment_authorized_by},"first_name") }},
2 |
3 | The payout has been {{ doc.razorpayx_payout_status }}! for Payment Entry {{ doc.name }}.
4 |
5 | For more details, visit your ERPNext site and check the details.
6 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/notification/failed_payout/failed_payout.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 |
4 | def get_context(context):
5 | # do your magic here
6 | pass
7 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/notification/payout_processed/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/razorpayx_integration/notification/payout_processed/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/notification/payout_processed/payout_processed.json:
--------------------------------------------------------------------------------
1 | {
2 | "attach_print": 1,
3 | "channel": "Email",
4 | "condition": "doc.razorpayx_payout_status == \"Processed\"",
5 | "creation": "2025-01-29 12:26:55.180087",
6 | "days_in_advance": 0,
7 | "docstatus": 0,
8 | "doctype": "Notification",
9 | "document_type": "Payment Entry",
10 | "enabled": 0,
11 | "event": "Method",
12 | "idx": 0,
13 | "is_standard": 0,
14 | "message": "M/S {{ doc.party_name }},
\n\nA payment of {{ frappe.utils.fmt_money(doc.paid_amount,currency=\"INR\") }} has been transferred to you by {{ doc.company }}.
\n\nFor more information, please check the attachment.
\n",
15 | "message_type": "Markdown",
16 | "method": "send_rpx_payout_status_update",
17 | "modified": "2025-03-28 12:02:01.220293",
18 | "modified_by": "Administrator",
19 | "module": "Razorpayx Integration",
20 | "name": "Payout Processed",
21 | "owner": "Administrator",
22 | "recipients": [
23 | {
24 | "receiver_by_document_field": "contact_email",
25 | "receiver_by_role": ""
26 | }
27 | ],
28 | "send_system_notification": 0,
29 | "send_to_all_assignees": 0,
30 | "sender": "",
31 | "sender_email": "",
32 | "subject": "Payment of Rs. {{ frappe.utils.fmt_money(doc.paid_amount,currency=\"INR\") }} Transferred to You",
33 | "value_changed": "razorpayx_payout_status"
34 | }
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/notification/payout_processed/payout_processed.md:
--------------------------------------------------------------------------------
1 | M/S {{ doc.party_name }},
2 |
3 | A payment of {{ frappe.utils.fmt_money(doc.paid_amount,currency="INR") }} has been transferred to you by {{ doc.company }}.
4 |
5 | For more information, please check the attachment.
6 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/notification/payout_processed/payout_processed.py:
--------------------------------------------------------------------------------
1 | import frappe
2 |
3 |
4 | def get_context(context):
5 | # do your magic here
6 | pass
7 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/report/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/razorpayx_integration/report/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/report/razorpayx_payout_status/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/razorpayx_integration/report/razorpayx_payout_status/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/report/razorpayx_payout_status/razorpayx_payout_status.js:
--------------------------------------------------------------------------------
1 | // Copyright (c) 2025, Resilient Tech and contributors
2 | // For license information, please see license.txt
3 |
4 | const DOC_STATUS = { Draft: "grey", Submitted: "blue", Cancelled: "red" };
5 |
6 | const TIMESPANS = [
7 | "Today",
8 | "Yesterday",
9 | "This Week",
10 | "This Month",
11 | "This Quarter",
12 | "This Year",
13 | "Last Week",
14 | "Last Month",
15 | "Last Quarter",
16 | "Last Year",
17 | "Select Date Range",
18 | ];
19 |
20 | const UTR_PLACEHOLDER = "*** UTR WILL BE SET AUTOMATICALLY ***";
21 |
22 | frappe.query_reports["RazorpayX Payout Status"] = {
23 | filters: [
24 | {
25 | fieldname: "company",
26 | label: __("Company"),
27 | fieldtype: "Link",
28 | options: "Company",
29 | default: frappe.defaults.get_user_default("Company"),
30 | reqd: 1,
31 | },
32 | {
33 | fieldname: "date_time_span",
34 | label: __("Posting Date"),
35 | fieldtype: "Select",
36 | options: TIMESPANS,
37 | default: "This Month",
38 | reqd: 1,
39 | on_change: (report) => {
40 | if (report.get_filter_value("date_time_span") === "Select Date Range") {
41 | const date_range = report.get_filter("date_range");
42 | date_range.df.reqd = 1;
43 | date_range.set_required(1);
44 | }
45 |
46 | report.refresh();
47 | },
48 | },
49 | {
50 | fieldname: "date_range",
51 | fieldtype: "DateRange",
52 | label: __("Posting Date Range"),
53 | depends_on: "eval: doc.date_time_span === 'Select Date Range'",
54 | default: [frappe.datetime.month_start(), frappe.datetime.now_date()],
55 | },
56 | {
57 | fieldname: "party_type",
58 | label: __("Party Type"),
59 | fieldtype: "Link",
60 | options: "Party Type",
61 | get_query: function () {
62 | return {
63 | filters: {
64 | name: ["in", Object.keys(frappe.boot.party_account_types)],
65 | },
66 | };
67 | },
68 | },
69 | {
70 | fieldname: "party",
71 | label: __("Party"),
72 | fieldtype: "Dynamic Link",
73 | options: "party_type",
74 | },
75 | {
76 | fieldname: "docstatus",
77 | label: __("Document Status"),
78 | fieldtype: "MultiSelectList",
79 | get_data: () => get_multiselect_options(Object.keys(DOC_STATUS)),
80 | },
81 | {
82 | fieldname: "payout_status",
83 | label: __("Payout Status"),
84 | fieldtype: "MultiSelectList",
85 | get_data: () => get_multiselect_options(Object.keys(razorpayx.PAYOUT_STATUS)),
86 | },
87 | {
88 | fieldname: "payout_mode",
89 | label: __("Payout Mode"),
90 | fieldtype: "MultiSelectList",
91 | get_data: () =>
92 | get_multiselect_options(Object.values(payment_integration_utils.PAYMENT_TRANSFER_METHOD)),
93 | },
94 | {
95 | fieldname: "razorpayx_config",
96 | label: __("RazorpayX Configuration"),
97 | fieldtype: "Link",
98 | options: "RazorpayX Configuration",
99 | get_query: function () {
100 | return {
101 | filters: { company: frappe.query_report.get_filter_value("company") },
102 | };
103 | },
104 | },
105 | {
106 | fieldname: "payout_made_by",
107 | label: __("Payout Made By"),
108 | fieldtype: "Link",
109 | options: "User",
110 | },
111 | ],
112 |
113 | onload: function (report) {
114 | const docstatus = report.get_filter("docstatus");
115 |
116 | if (docstatus && (!docstatus.get_value() || docstatus.get_value().length === 0)) {
117 | docstatus.set_value("Submitted");
118 | }
119 | },
120 |
121 | formatter: function (value, row, column, data, default_formatter) {
122 | value = default_formatter(value, row, column, data);
123 |
124 | if (column.fieldname === "docstatus") {
125 | value = this.get_formatted_docstatus(value);
126 | } else if (column.fieldname === "payout_status") {
127 | value = this.get_formatted_payout_status(value);
128 | } else if (column.fieldname === "utr") {
129 | if (value === UTR_PLACEHOLDER) {
130 | value = `-`;
131 | }
132 | }
133 |
134 | return value;
135 | },
136 |
137 | get_formatted_docstatus: function (value) {
138 | return `
139 |
140 | ${value}
141 |
142 |
`;
143 | },
144 |
145 | get_formatted_payout_status: function (value) {
146 | return `
147 | ${value}
148 |
`;
149 | },
150 | };
151 |
152 | function get_multiselect_options(values) {
153 | const options = [];
154 | for (const option of values) {
155 | options.push({
156 | value: option,
157 | label: option,
158 | description: "",
159 | });
160 | }
161 | return options;
162 | }
163 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/report/razorpayx_payout_status/razorpayx_payout_status.json:
--------------------------------------------------------------------------------
1 | {
2 | "add_total_row": 1,
3 | "add_translate_data": 0,
4 | "columns": [],
5 | "creation": "2025-03-13 16:55:43.309877",
6 | "disabled": 0,
7 | "docstatus": 0,
8 | "doctype": "Report",
9 | "filters": [],
10 | "idx": 0,
11 | "is_standard": "Yes",
12 | "letterhead": null,
13 | "modified": "2025-03-13 16:55:43.309877",
14 | "modified_by": "Administrator",
15 | "module": "Razorpayx Integration",
16 | "name": "RazorpayX Payout Status",
17 | "owner": "Administrator",
18 | "prepared_report": 0,
19 | "ref_doctype": "Payment Entry",
20 | "report_name": "RazorpayX Payout Status",
21 | "report_type": "Script Report",
22 | "roles": [
23 | {
24 | "role": "Online Payments Authorizer"
25 | },
26 | {
27 | "role": "RazorpayX Integration Manager"
28 | }
29 | ],
30 | "timeout": 0
31 | }
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/report/razorpayx_payout_status/razorpayx_payout_status.py:
--------------------------------------------------------------------------------
1 | # Copyright (c) 2025, Resilient Tech and contributors
2 | # For license information, please see license.txt
3 |
4 | import frappe
5 | from frappe import _
6 | from frappe.query_builder.functions import Date
7 | from frappe.utils.data import get_timespan_date_range
8 |
9 | from razorpayx_integration.constants import RAZORPAYX_CONFIG
10 |
11 |
12 | def execute(filters=None):
13 | return get_columns(), get_data(filters)
14 |
15 |
16 | def get_data(filters: dict | None = None) -> list[dict]:
17 | from_date, to_date = parse_date_range(filters)
18 |
19 | PE = frappe.qb.DocType("Payment Entry")
20 |
21 | base_query = (
22 | frappe.qb.from_(PE)
23 | .select(
24 | PE.name.as_("payment_entry"),
25 | PE.posting_date,
26 | PE.company,
27 | PE.party_type,
28 | PE.party,
29 | PE.paid_amount,
30 | PE.razorpayx_payout_status.as_("payout_status"),
31 | PE.payment_transfer_method.as_("payout_mode"),
32 | PE.razorpayx_payout_desc.as_("payout_description"),
33 | PE.status.as_("docstatus"),
34 | PE.payment_authorized_by.as_("payout_made_by"),
35 | PE.integration_docname.as_("razorpayx_config"),
36 | PE.reference_no.as_("utr"),
37 | PE.razorpayx_payout_id.as_("payout_id"),
38 | PE.razorpayx_payout_link_id.as_("payout_link_id"),
39 | )
40 | .where(PE.company == filters.company)
41 | .where(PE.posting_date >= Date(from_date))
42 | .where(PE.posting_date <= Date(to_date))
43 | .where(PE.integration_doctype == RAZORPAYX_CONFIG)
44 | .where(PE.make_bank_online_payment == 1)
45 | .orderby(PE.posting_date, order=frappe.qb.desc)
46 | )
47 |
48 | # update the query based on filters
49 | if filters.party_type:
50 | base_query = base_query.where(PE.party_type == filters.party_type)
51 |
52 | if filters.party:
53 | base_query = base_query.where(PE.party == filters.party)
54 |
55 | if filters.docstatus:
56 | base_query = base_query.where(PE.status.isin(filters.docstatus))
57 |
58 | if filters.payout_status:
59 | base_query = base_query.where(
60 | PE.razorpayx_payout_status.isin(filters.payout_status)
61 | )
62 |
63 | if filters.payout_mode:
64 | base_query = base_query.where(
65 | PE.payment_transfer_method.isin(filters.payout_mode)
66 | )
67 |
68 | if filters.razorpayx_config:
69 | base_query = base_query.where(
70 | PE.integration_docname == filters.razorpayx_config
71 | )
72 |
73 | if filters.payout_made_by:
74 | base_query = base_query.where(
75 | PE.payment_authorized_by == filters.payout_made_by
76 | )
77 |
78 | return base_query.run(as_dict=True)
79 |
80 |
81 | def parse_date_range(filters: dict) -> tuple[str, str]:
82 | if filters.date_time_span == "Select Date Range":
83 | return filters.date_range
84 |
85 | return get_timespan_date_range(filters.date_time_span.lower())
86 |
87 |
88 | def get_columns() -> list[dict]:
89 | return [
90 | {
91 | "label": _("Payment Entry"),
92 | "fieldname": "payment_entry",
93 | "fieldtype": "Link",
94 | "options": "Payment Entry",
95 | "width": 200,
96 | },
97 | {
98 | "label": _("Company"),
99 | "fieldname": "company",
100 | "fieldtype": "Link",
101 | "options": "Company",
102 | "width": 180,
103 | },
104 | {
105 | "label": _("Posting Date"),
106 | "fieldname": "posting_date",
107 | "fieldtype": "Date",
108 | "width": 150,
109 | },
110 | {
111 | "label": _("Party Type"),
112 | "fieldname": "party_type",
113 | "fieldtype": "Link",
114 | "options": "Party Type",
115 | "width": 120,
116 | },
117 | {
118 | "label": _("Party"),
119 | "fieldname": "party",
120 | "fieldtype": "Dynamic Link",
121 | "options": "party_type",
122 | "width": 180,
123 | },
124 | {
125 | "label": _("Paid Amount"),
126 | "fieldname": "paid_amount",
127 | "fieldtype": "Currency",
128 | "options": "INR",
129 | "width": 150,
130 | },
131 | {
132 | "label": _("Payout Status"),
133 | "fieldname": "payout_status",
134 | "fieldtype": "Data",
135 | "width": 120,
136 | },
137 | {
138 | "label": _("Payout Mode"),
139 | "fieldname": "payout_mode",
140 | "fieldtype": "Data",
141 | "width": 100,
142 | },
143 | {
144 | "label": _("Payout Description"),
145 | "fieldname": "payout_description",
146 | "fieldtype": "Data",
147 | "width": 200,
148 | },
149 | {
150 | "label": _("Docstatus"),
151 | "fieldname": "docstatus",
152 | "fieldtype": "Data",
153 | "width": 120,
154 | },
155 | {
156 | "label": _("Payout Made By"),
157 | "fieldname": "payout_made_by",
158 | "fieldtype": "Link",
159 | "options": "User",
160 | "width": 200,
161 | },
162 | {
163 | "label": _("RazorpayX Configuration"),
164 | "fieldname": "razorpayx_config",
165 | "fieldtype": "Link",
166 | "options": "RazorpayX Configuration",
167 | "width": 150,
168 | },
169 | {
170 | "label": _("UTR"),
171 | "fieldname": "utr",
172 | "fieldtype": "Data",
173 | "width": 200,
174 | },
175 | {
176 | "label": _("Payout ID"),
177 | "fieldname": "payout_id",
178 | "fieldtype": "Data",
179 | "width": 180,
180 | },
181 | {
182 | "label": _("Payout Link ID"),
183 | "fieldname": "payout_link_id",
184 | "fieldtype": "Data",
185 | "width": 180,
186 | },
187 | ]
188 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/server_overrides/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/razorpayx_integration/server_overrides/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/server_overrides/doctype/payment_entry.py:
--------------------------------------------------------------------------------
1 | from typing import Literal
2 |
3 | import frappe
4 | from erpnext.accounts.doctype.payment_entry.payment_entry import PaymentEntry
5 | from frappe import _
6 | from payment_integration_utils.payment_integration_utils.constants.payments import (
7 | TRANSFER_METHOD,
8 | )
9 | from payment_integration_utils.payment_integration_utils.server_overrides.doctype.payment_entry import (
10 | validate_transfer_methods,
11 | )
12 | from payment_integration_utils.payment_integration_utils.utils.auth import (
13 | run_before_payment_authentication as has_payment_permissions,
14 | )
15 |
16 | from razorpayx_integration.constants import RAZORPAYX_CONFIG
17 | from razorpayx_integration.razorpayx_integration.constants.payouts import (
18 | PAYOUT_CURRENCY,
19 | )
20 | from razorpayx_integration.razorpayx_integration.utils import (
21 | is_auto_cancel_payout_enabled,
22 | is_auto_pay_enabled,
23 | is_payout_via_razorpayx,
24 | )
25 | from razorpayx_integration.razorpayx_integration.utils.payout import (
26 | PayoutWithPaymentEntry,
27 | )
28 |
29 | #### CONSTANTS ####
30 | TRANSFER_METHODS = Literal["NEFT", "RTGS", "IMPS", "UPI", "Link"]
31 | UTR_PLACEHOLDER = "*** UTR WILL BE SET AUTOMATICALLY ***"
32 |
33 |
34 | #### DOC EVENTS ####
35 | def onload(doc: PaymentEntry, method=None):
36 | if doc.docstatus == 1 and is_payout_via_razorpayx(doc):
37 | doc.set_onload(
38 | "auto_cancel_payout_enabled",
39 | is_auto_cancel_payout_enabled(doc.integration_docname),
40 | )
41 |
42 |
43 | def validate(doc: PaymentEntry, method=None):
44 | if doc.flags._is_already_paid:
45 | return
46 |
47 | set_integration_config(doc)
48 | set_for_payments_processor(doc)
49 | validate_payout_details(doc)
50 |
51 |
52 | def before_submit(doc: PaymentEntry, method=None):
53 | # for bulk submission from client side or single submission without payment
54 | if not should_uncheck_make_bank_online_payment(doc):
55 | return
56 |
57 | # PE is not authorized to make payout or auto pay is disabled
58 | doc.make_bank_online_payment = 0
59 |
60 | if frappe.flags.initiated_by_payment_processor:
61 | return
62 |
63 | # Show single alert message only
64 | alert_msg = _("Please make payout manually after Payment Entry submission.")
65 | alert_sent = False
66 |
67 | for message in frappe.message_log:
68 | if alert_msg in message.get("message"):
69 | alert_sent = True
70 | break
71 |
72 | if not alert_sent:
73 | frappe.msgprint(msg=alert_msg, alert=True)
74 |
75 |
76 | def should_uncheck_make_bank_online_payment(doc: PaymentEntry) -> bool:
77 | if not is_payout_via_razorpayx(doc):
78 | return False
79 |
80 | should_uncheck_payment_flag = (
81 | not is_auto_pay_enabled(doc.integration_docname)
82 | if frappe.flags.initiated_by_payment_processor
83 | else not doc.flags._is_already_paid and not get_auth_id(doc)
84 | )
85 |
86 | return should_uncheck_payment_flag
87 |
88 |
89 | def on_submit(doc: PaymentEntry, method=None):
90 | if not is_payout_via_razorpayx(doc):
91 | return
92 |
93 | PayoutWithPaymentEntry(doc).make(get_auth_id(doc))
94 |
95 |
96 | def before_cancel(doc: PaymentEntry, method=None):
97 | # PE is cancelled by RazorpayX webhook or PE is cancelled when payout got cancelled
98 | if not is_payout_via_razorpayx(doc) or doc.flags.__canceled_by_rpx:
99 | return
100 |
101 | PayoutWithPaymentEntry(doc).cancel()
102 |
103 |
104 | ### AUTHORIZATION ###
105 | def get_auth_id(doc: PaymentEntry) -> str | None:
106 | """
107 | Get `auth_id` from Payment Entry onload.
108 |
109 | It is used to authorize the Payment Entry to make payout.
110 | """
111 | onload = doc.get_onload() or frappe._dict()
112 | return onload.get("auth_id")
113 |
114 |
115 | #### VALIDATIONS ####
116 | def set_integration_config(doc: PaymentEntry):
117 | def reset_rpx_config():
118 | if doc.integration_doctype == RAZORPAYX_CONFIG:
119 | doc.integration_doctype = ""
120 | doc.integration_docname = ""
121 |
122 | if doc.paid_from_account_currency != PAYOUT_CURRENCY.INR.value:
123 | reset_rpx_config()
124 | return
125 |
126 | if config := frappe.db.get_value(
127 | RAZORPAYX_CONFIG, {"disabled": 0, "bank_account": doc.bank_account}
128 | ):
129 | doc.integration_doctype = RAZORPAYX_CONFIG
130 | doc.integration_docname = config
131 | else:
132 | reset_rpx_config()
133 |
134 |
135 | def set_for_payments_processor(doc: PaymentEntry):
136 | if not frappe.flags.initiated_by_payment_processor:
137 | return
138 |
139 | if doc.integration_doctype != RAZORPAYX_CONFIG:
140 | return
141 |
142 | if not is_auto_pay_enabled(doc.integration_docname):
143 | return
144 |
145 | def get_payout_desc() -> str:
146 | invoice = doc.flags.invoice_list[0]
147 | desc = invoice.bill_no or invoice.name
148 | desc = "".join(e for e in desc if e.isalnum())
149 | return desc[:30]
150 |
151 | doc.make_bank_online_payment = 1
152 | doc.razorpayx_payout_desc = get_payout_desc()
153 |
154 |
155 | def validate_payout_details(doc: PaymentEntry):
156 | if not doc.make_bank_online_payment or doc.integration_doctype != RAZORPAYX_CONFIG:
157 | return
158 |
159 | if not doc.bank_account:
160 | frappe.throw(
161 | msg=_("Company's Bank Account is mandatory to make payment."),
162 | title=_("Mandatory Field Missing"),
163 | exc=frappe.MandatoryError,
164 | )
165 |
166 | if not doc.reference_no or doc.docstatus == 0:
167 | doc.reference_no = UTR_PLACEHOLDER
168 |
169 | if (
170 | doc.payment_transfer_method == TRANSFER_METHOD.LINK.value
171 | and not doc.razorpayx_payout_desc
172 | ):
173 | frappe.throw(
174 | msg=_("Payout Description is mandatory to make Payout Link."),
175 | title=_("Mandatory Fields Missing"),
176 | exc=frappe.MandatoryError,
177 | )
178 |
179 |
180 | ### APIs ###
181 | # TODO: Make API more easy to use and less error-prone
182 | # 1. Fetch bank account details from the `party_bank_account`
183 | # 2. Fetch contact details from the `contact_person` or set directly mobile and email
184 | # 3. If party is `Employee`, fetch contact details from the Employee's contact
185 | # 4. Also check `Contact Person` and `Party Bank Account` is associated with the `Party`
186 | # 6. Based on the `transfer_method`, set the fields automatically
187 | @frappe.whitelist()
188 | def make_payout_with_razorpayx(
189 | auth_id: str,
190 | docname: str,
191 | transfer_method: TRANSFER_METHODS = TRANSFER_METHOD.LINK.value,
192 | **kwargs,
193 | ):
194 | """
195 | Make RazorpayX Payout or Payout Link with Payment Entry.
196 |
197 | :param auth_id: Authentication ID (after otp or password verification)
198 | :param docname: Payment Entry name
199 | :param transfer_method: Transfer method to make payout with (NEFT, RTGS, IMPS, UPI, Link)
200 | :param kwargs: Payout Details
201 | """
202 | has_payment_permissions(docname, throw=True)
203 | doc = frappe.get_doc("Payment Entry", docname)
204 |
205 | if doc.make_bank_online_payment:
206 | frappe.msgprint(
207 | msg=_(
208 | "Payout for {0} is already in {1} state"
209 | ).format(docname, doc.razorpayx_payout_status),
210 | alert=True,
211 | )
212 |
213 | return
214 |
215 | # Set the fields to make payout
216 | doc.db_set(
217 | {
218 | "make_bank_online_payment": 1,
219 | "payment_transfer_method": transfer_method,
220 | # Party
221 | "party_bank_account": kwargs.get("party_bank_account"),
222 | "party_bank_account_no": kwargs.get("party_bank_account_no"),
223 | "party_bank_ifsc": kwargs.get("party_bank_ifsc"),
224 | "party_upi_id": kwargs.get("party_upi_id"),
225 | "contact_person": kwargs.get("contact_person"),
226 | "contact_mobile": kwargs.get("contact_mobile"),
227 | "contact_email": kwargs.get("contact_email"),
228 | # RazorpayX
229 | "razorpayx_payout_desc": kwargs.get("razorpayx_payout_desc"),
230 | # ERPNext
231 | "reference_no": UTR_PLACEHOLDER,
232 | "remarks": doc.remarks.replace(doc.reference_no, UTR_PLACEHOLDER, 1),
233 | }
234 | )
235 |
236 | validate_transfer_methods(doc)
237 | validate_payout_details(doc)
238 | PayoutWithPaymentEntry(doc).make(auth_id)
239 |
240 |
241 | @frappe.whitelist()
242 | def mark_payout_for_cancellation(docname: str, cancel: bool | int):
243 | """
244 | Marking Payment Entry's payout or payout link for cancellation.
245 |
246 | Saving in cache to remember the action.
247 |
248 | :param docname: Payment Entry name.
249 | :param cancel: Cancel or not.
250 | """
251 |
252 | frappe.has_permission("Payment Entry", "cancel", doc=docname, throw=True)
253 |
254 | config = frappe.db.get_value("Payment Entry", docname, "integration_docname")
255 | frappe.has_permission(RAZORPAYX_CONFIG, doc=config, throw=True)
256 |
257 | key = PayoutWithPaymentEntry.get_cancel_payout_key(docname)
258 | value = "True" if cancel else "False"
259 |
260 | frappe.cache.set(key, value, 100)
261 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/utils/__init__.py:
--------------------------------------------------------------------------------
1 | import frappe
2 | from erpnext.accounts.doctype.payment_entry.payment_entry import PaymentEntry
3 |
4 | from razorpayx_integration.constants import PAYMENTS_PROCESSOR_APP, RAZORPAYX_CONFIG
5 |
6 |
7 | def is_payout_via_razorpayx(doc: PaymentEntry) -> bool:
8 | """
9 | Check if the Payment Entry is paid via RazorpayX.
10 | """
11 | return bool(
12 | doc.make_bank_online_payment
13 | and doc.integration_doctype == RAZORPAYX_CONFIG
14 | and doc.integration_docname
15 | )
16 |
17 |
18 | def is_auto_cancel_payout_enabled(razorpayx_config: str) -> bool | int:
19 | return frappe.db.get_value(RAZORPAYX_CONFIG, razorpayx_config, "auto_cancel_payout")
20 |
21 |
22 | def is_auto_pay_enabled(razorpayx_config: str) -> bool | int:
23 | if PAYMENTS_PROCESSOR_APP not in frappe.get_installed_apps():
24 | return False
25 |
26 | return frappe.db.get_value(RAZORPAYX_CONFIG, razorpayx_config, "pay_on_auto_submit")
27 |
28 |
29 | def get_fees_accounting_config(razorpayx_config: str) -> dict:
30 | return (
31 | frappe.db.get_value(
32 | RAZORPAYX_CONFIG,
33 | razorpayx_config,
34 | [
35 | "automate_fees_accounting",
36 | "payouts_from",
37 | "creditors_account",
38 | "supplier",
39 | "payable_account",
40 | ],
41 | as_dict=True,
42 | )
43 | or frappe._dict()
44 | )
45 |
46 |
47 | def is_create_je_on_reversal_enabled(razorpayx_config: str) -> bool | int:
48 | return frappe.db.get_value(
49 | RAZORPAYX_CONFIG, razorpayx_config, "create_je_on_reversal"
50 | )
51 |
52 |
53 | def get_payouts_made_from(razorpayx_config: str) -> str:
54 | return frappe.db.get_value(RAZORPAYX_CONFIG, razorpayx_config, "payouts_from")
55 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/utils/bank_transaction.py:
--------------------------------------------------------------------------------
1 | from typing import Literal
2 |
3 | import frappe
4 | from frappe import _
5 | from frappe.utils import DateTimeLikeObject, getdate
6 | from payment_integration_utils.payment_integration_utils.utils import (
7 | get_str_datetime_from_epoch,
8 | paisa_to_rupees,
9 | )
10 |
11 | from razorpayx_integration.constants import RAZORPAYX_CONFIG
12 | from razorpayx_integration.razorpayx_integration.apis.transaction import (
13 | RazorpayXTransaction,
14 | )
15 | from razorpayx_integration.razorpayx_integration.constants.payouts import PAYOUT_FROM
16 | from razorpayx_integration.razorpayx_integration.constants.webhooks import (
17 | TRANSACTION_TYPE as ENTITY,
18 | )
19 | from razorpayx_integration.razorpayx_integration.utils import get_payouts_made_from
20 |
21 |
22 | ######### PROCESSOR #########
23 | class RazorpayXBankTransaction:
24 | def __init__(
25 | self,
26 | razorpayx_config: str,
27 | from_date: DateTimeLikeObject | None = None,
28 | to_date: DateTimeLikeObject | None = None,
29 | *,
30 | bank_account: str | None = None,
31 | source_doctype: str | None = None,
32 | source_docname: str | None = None,
33 | ):
34 | self.razorpayx_config = razorpayx_config
35 | self.from_date = from_date
36 | self.to_date = to_date
37 | self.source_doctype = source_doctype
38 | self.source_docname = source_docname
39 |
40 | self.set_bank_account(bank_account)
41 |
42 | def set_bank_account(self, bank_account: str | None = None):
43 | if not bank_account:
44 | bank_account = frappe.db.get_value(
45 | doctype=RAZORPAYX_CONFIG,
46 | filters=self.razorpayx_config,
47 | fieldname="bank_account",
48 | )
49 |
50 | if not bank_account:
51 | frappe.throw(
52 | msg=_(
53 | "Company Bank Account not found for RazorpayX Configuration {0}"
54 | ).format(self.razorpayx_config),
55 | title=_("Company Bank Account Not Found"),
56 | )
57 |
58 | self.bank_account = bank_account
59 |
60 | def sync(self):
61 | transactions = self.fetch_transactions()
62 |
63 | if not transactions:
64 | return
65 |
66 | existing_transactions = self.get_existing_transactions(transactions)
67 |
68 | for transaction in transactions:
69 | if transaction["id"] in existing_transactions:
70 | continue
71 |
72 | self.create(self.map(transaction))
73 |
74 | def fetch_transactions(self) -> list[dict] | None:
75 | """
76 | Fetching Bank Transactions from RazorpayX API.
77 | """
78 | try:
79 | return RazorpayXTransaction(self.razorpayx_config).get_all(
80 | from_date=self.from_date,
81 | to_date=self.to_date,
82 | source_doctype=self.source_doctype,
83 | source_docname=self.source_docname,
84 | )
85 |
86 | except Exception:
87 | frappe.log_error(
88 | title=(
89 | f"Failed to Fetch RazorpayX Transactions for Config: {self.razorpayx_config}"
90 | ),
91 | message=frappe.get_traceback(),
92 | reference_doctype=RAZORPAYX_CONFIG,
93 | reference_name=self.razorpayx_config,
94 | )
95 |
96 | def get_existing_transactions(self, transactions: list[str]) -> set[str]:
97 | """
98 | Get existing bank account transactions from the ERPNext database.
99 |
100 | :param transactions: List of transactions from RazorpayX API.
101 | """
102 | return set(
103 | frappe.get_all(
104 | "Bank Transaction",
105 | filters={
106 | "bank_account": self.bank_account,
107 | "transaction_id": (
108 | "in",
109 | {transaction["id"] for transaction in transactions},
110 | ),
111 | },
112 | pluck="transaction_id",
113 | )
114 | )
115 |
116 | def map(self, transaction: dict):
117 | """
118 | Map RazorpayX transaction to ERPNext's Bank Transaction.
119 |
120 | :param transaction: RazorpayX Transaction
121 | """
122 |
123 | def get_description(source: dict) -> str | None:
124 | # TODO: Needs description of payout/bank transfer or other transactions
125 | notes = source.get("notes") or {}
126 |
127 | if not notes:
128 | return
129 |
130 | description = ""
131 | source_doctype = notes.get("source_doctype")
132 | source_docname = notes.get("source_docname")
133 |
134 | if source_doctype and source_docname:
135 | description = f"{source_doctype}: {source_docname}"
136 |
137 | if desc := notes.get("description"):
138 | description += f"\nNarration: {desc}"
139 |
140 | if not description:
141 | description = "\n".join(notes.values())
142 |
143 | return description
144 |
145 | # Some transactions do not have source
146 | source = transaction.get("source") or {}
147 |
148 | mapped = {
149 | "doctype": "Bank Transaction",
150 | "bank_account": self.bank_account,
151 | "transaction_id": transaction["id"],
152 | "date": get_str_datetime_from_epoch(transaction["created_at"]),
153 | "deposit": paisa_to_rupees(transaction["credit"]),
154 | "withdrawal": paisa_to_rupees(transaction["debit"]),
155 | "closing_balance": paisa_to_rupees(transaction["balance"]),
156 | "currency": transaction["currency"],
157 | "transaction_type": source.get("mode"),
158 | "description": get_description(source),
159 | "reference_number": source.get("utr") or source.get("bank_reference"),
160 | }
161 |
162 | # auto reconciliation
163 | # TODO: fees deduction at the end of the day is not handled
164 | mapped["payment_entries"] = []
165 | self.set_matching_payment_entry(mapped, source)
166 | self.set_matching_journal_entry(mapped, source)
167 |
168 | return mapped
169 |
170 | def set_matching_payment_entry(self, mapped: dict, source: dict | None = None):
171 | """
172 | Setting matching Payment Entry for the Bank Reconciliation.
173 |
174 | :param mapped: Mapped Bank Transaction
175 | :param source: Source of the transaction (In transaction response)
176 |
177 | ---
178 | Note:
179 | - Payment Entry will be find by `Payout ID` or `UTR`.
180 | - For `reversal` entity it returns without finding PE.
181 | """
182 | if not source or source.get("entity") == ENTITY.REVERSAL.value:
183 | return
184 |
185 | def get_payment_entry(**filters):
186 | # TODO: confirm company or bank account
187 | return frappe.db.get_value(
188 | "Payment Entry",
189 | {
190 | "docstatus": 1,
191 | "clearance_date": ["is", "not set"],
192 | **filters,
193 | },
194 | fieldname=["name", "paid_amount"],
195 | order_by="creation desc", # to get latest
196 | as_dict=True,
197 | )
198 |
199 | payment_entry = None
200 |
201 | # reconciliation with payout_id or bank_reference
202 | if source.get("entity") == ENTITY.PAYOUT.value:
203 | payment_entry = get_payment_entry(razorpayx_payout_id=source["id"])
204 | elif source.get("entity") == ENTITY.BANK_TRANSFER.value:
205 | payment_entry = get_payment_entry(reference_no=source["bank_reference"])
206 |
207 | # reconciliation with reference number (UTR)
208 | if not payment_entry and source.get("utr"):
209 | payment_entry = get_payment_entry(reference_no=mapped["reference_number"])
210 |
211 | if not payment_entry:
212 | return
213 |
214 | mapped["payment_entries"].append(
215 | {
216 | "payment_document": "Payment Entry",
217 | "payment_entry": payment_entry.name,
218 | "allocated_amount": payment_entry.paid_amount,
219 | }
220 | )
221 |
222 | def set_matching_journal_entry(self, mapped: dict, source: dict | None = None):
223 | """
224 | Setting matching Journal Entry for the Bank Reconciliation.
225 |
226 | :param mapped: Mapped Bank Transaction
227 | :param source: Source of the transaction (In transaction response)
228 |
229 | ---
230 | Note:
231 | - For reversal, two transactions will be created.
232 | - simple transaction with reversal id
233 | - payout reversal transaction with payout id
234 | - JE created only when payout processed or reversed
235 | - JE will be find by `Payout ID` or `Reversal ID` or `UTR` or `Bank Reference`.
236 | """
237 | if not source:
238 | return
239 |
240 | payouts_from = get_payouts_made_from(self.razorpayx_config)
241 |
242 | def get_journal_entry(
243 | check_no: str,
244 | reversal_of: Literal["set", "not set"],
245 | ) -> dict | None:
246 | return frappe.db.get_value(
247 | "Journal Entry",
248 | {
249 | "is_system_generated": 1,
250 | "docstatus": 1,
251 | "difference": 0,
252 | "cheque_no": check_no,
253 | "reversal_of": ["is", reversal_of],
254 | },
255 | fieldname=["name", "total_debit"],
256 | as_dict=True,
257 | )
258 |
259 | def is_current_account_payout() -> bool:
260 | return payouts_from == PAYOUT_FROM.CURRENT_ACCOUNT.value
261 |
262 | entity = source.get("entity")
263 |
264 | # for current account payouts, fees will not be deducted immediately but JE will be created
265 | if entity == ENTITY.PAYOUT.value and is_current_account_payout():
266 | return
267 |
268 | # get cheque no to fetch JE
269 | cheque_no = ""
270 |
271 | if entity in [ENTITY.PAYOUT.value, ENTITY.REVERSAL.value]:
272 | cheque_no = source.get("id")
273 | elif entity == ENTITY.BANK_TRANSFER.value:
274 | cheque_no = source.get("bank_reference")
275 | else:
276 | cheque_no = source.get("utr")
277 |
278 | if not cheque_no:
279 | return
280 |
281 | # finding Fees JE or Payout Reversal JE with `cheque_no`
282 | # Note: for fees `check_no` is payout_id and for reversal `check_no` is reversal_id
283 | journal_entry = get_journal_entry(cheque_no, "not set")
284 |
285 | if journal_entry:
286 | mapped["payment_entries"].append(
287 | {
288 | "payment_document": "Journal Entry",
289 | "payment_entry": journal_entry.name,
290 | "allocated_amount": journal_entry.total_debit,
291 | }
292 | )
293 |
294 | if entity != "reversal" or is_current_account_payout():
295 | return
296 |
297 | # get fees reversal JE (Only for RazorpayX Lite)
298 | fees_reversal_je = get_journal_entry(cheque_no, "set")
299 |
300 | if not fees_reversal_je:
301 | return
302 |
303 | mapped["payment_entries"].append(
304 | {
305 | "payment_document": "Journal Entry",
306 | "payment_entry": fees_reversal_je.name,
307 | "allocated_amount": fees_reversal_je.total_debit,
308 | }
309 | )
310 |
311 | # TODO: can use bulk insert?
312 | def create(self, mapped_transaction: dict):
313 | """
314 | Create Bank Transaction in the ERPNext.
315 |
316 | :param mapped_transaction: Mapped Bank Transaction
317 | """
318 | return (
319 | frappe.get_doc(mapped_transaction).insert(ignore_permissions=True).submit()
320 | )
321 |
322 |
323 | ######### APIs #########
324 | @frappe.whitelist()
325 | def sync_transactions_for_reconcile(
326 | bank_account: str, razorpayx_config: str | None = None
327 | ):
328 | """
329 | Sync RazorpayX bank account transactions.
330 |
331 | Syncs from the last sync date to the current date.
332 |
333 | If last sync date is not set, it will sync all transactions.
334 |
335 | :param bank_account: Company Bank Account
336 | :param razorpayx_config: RazorpayX Configuration
337 | """
338 | BRT = "Bank Reconciliation Tool"
339 | frappe.has_permission(BRT, throw=True)
340 |
341 | if not razorpayx_config:
342 | razorpayx_config = frappe.db.get_value(
343 | RAZORPAYX_CONFIG, {"bank_account": bank_account, "disabled": 0}
344 | )
345 |
346 | if not razorpayx_config:
347 | frappe.throw(
348 | _(
349 | "RazorpayX Configuration not found for Bank Account {0}"
350 | ).format(bank_account)
351 | )
352 |
353 | RazorpayXBankTransaction(
354 | razorpayx_config,
355 | bank_account=bank_account,
356 | source_docname=BRT,
357 | source_doctype=BRT,
358 | ).sync()
359 |
360 |
361 | # TODO: we need to enqueue this or not!!
362 | @frappe.whitelist()
363 | def sync_bank_transactions_with_razorpayx(
364 | razorpayx_config: str,
365 | from_date: DateTimeLikeObject,
366 | to_date: DateTimeLikeObject,
367 | bank_account: str | None = None,
368 | ):
369 | """
370 | Sync RazorpayX bank account transactions.
371 |
372 | :param razorpayx_config: RazorpayX Configuration which has the bank account.
373 | :param from_date: Start Date
374 | :param to_date: End Date
375 | :param bank_account: Company Bank Account
376 | """
377 | frappe.has_permission(RAZORPAYX_CONFIG, throw=True)
378 |
379 | RazorpayXBankTransaction(
380 | razorpayx_config,
381 | from_date,
382 | to_date,
383 | bank_account=bank_account,
384 | source_doctype=RAZORPAYX_CONFIG,
385 | source_docname=razorpayx_config,
386 | ).sync()
387 |
388 |
389 | def sync_transactions_periodically():
390 | """
391 | Sync all enabled RazorpayX bank account transactions.
392 |
393 | Called by scheduler.
394 | """
395 | today = getdate()
396 |
397 | configs = frappe.get_all(
398 | doctype=RAZORPAYX_CONFIG,
399 | filters={"disabled": 0},
400 | fields=["name", "bank_account"],
401 | )
402 |
403 | if not configs:
404 | return
405 |
406 | for config in configs:
407 | RazorpayXBankTransaction(config.name, bank_account=config.bank_account).sync()
408 |
409 | # update last sync date
410 | frappe.db.set_value(
411 | RAZORPAYX_CONFIG,
412 | {"name": ("in", {config.name for config in configs})},
413 | "last_sync_on",
414 | today,
415 | )
416 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/utils/payout.py:
--------------------------------------------------------------------------------
1 | import frappe
2 | from erpnext.accounts.doctype.payment_entry.payment_entry import PaymentEntry
3 | from frappe import _
4 | from payment_integration_utils.payment_integration_utils.constants.payments import (
5 | TRANSFER_METHOD as PAYOUT_MODE,
6 | )
7 | from payment_integration_utils.payment_integration_utils.utils import (
8 | is_already_paid,
9 | )
10 | from payment_integration_utils.payment_integration_utils.utils.auth import (
11 | Authenticate2FA,
12 | )
13 |
14 | from razorpayx_integration.razorpayx_integration.apis.payout import (
15 | RazorpayXCompositePayout,
16 | RazorpayXLinkPayout,
17 | )
18 | from razorpayx_integration.razorpayx_integration.constants.payouts import (
19 | PAYOUT_CURRENCY,
20 | PAYOUT_STATUS,
21 | STATUS_NOTIFICATION_METHOD,
22 | )
23 | from razorpayx_integration.razorpayx_integration.utils import (
24 | is_auto_cancel_payout_enabled,
25 | is_payout_via_razorpayx,
26 | )
27 |
28 |
29 | class PayoutWithPaymentEntry:
30 | """
31 | Handle Razorpayx Payout | Payout Link with Payment Entry.
32 |
33 | :param payment_entry: Payment Entry doc.
34 |
35 | ---
36 | Caution: 🔴 Payout with `Fund Account ID` and Payout Link with `Contact ID` are not supported.
37 | """
38 |
39 | def __init__(self, doc: PaymentEntry, *args, **kwargs):
40 | self.doc = doc
41 | self.config_name = self.doc.integration_docname
42 |
43 | ### Make Payout | Payout Link ###
44 | def make(self, auth_id: str | None = None) -> dict | None:
45 | """
46 | Make payout with given Payment Entry.
47 |
48 | :param auth_id: Authentication ID for making payout.
49 | """
50 | if is_already_paid(self.doc.amended_from):
51 | return
52 |
53 | if not self._can_make_payout():
54 | frappe.throw(
55 | msg=_(
56 | "Payout cannot be made for this Payment Entry. Please check the payout details."
57 | ),
58 | title=_("Invalid Payment Entry"),
59 | )
60 |
61 | self._is_authenticated_payout(auth_id)
62 |
63 | payout_details = self._get_payout_details()
64 |
65 | if self.doc.payment_transfer_method == PAYOUT_MODE.LINK.value:
66 | response = RazorpayXLinkPayout(self.config_name).pay(payout_details)
67 | else:
68 | response = RazorpayXCompositePayout(self.config_name).pay(payout_details)
69 |
70 | self._update_after_making(response)
71 |
72 | return response
73 |
74 | def _is_authenticated_payout(self, auth_id: str | None = None) -> bool:
75 | """
76 | Check if the Payout (Payment Entry) is authenticated or not.
77 |
78 | :param auth_id: Authentication ID
79 |
80 | ---
81 | Note: when `frappe.flags.initiated_by_payment_processor` is set, it will bypass the authentication.
82 | """
83 | if frappe.flags.initiated_by_payment_processor:
84 | return True
85 |
86 | if not auth_id:
87 | frappe.throw(
88 | title=_("Unauthorized Access"),
89 | msg=_("Authentication ID is required to make payout."),
90 | exc=frappe.PermissionError,
91 | )
92 |
93 | if not Authenticate2FA.is_authenticated(auth_id):
94 | frappe.throw(
95 | title=_("Unauthorized Access"),
96 | msg=_("You are not authorized to access this Payment Entry."),
97 | exc=frappe.PermissionError,
98 | )
99 |
100 | if self.doc.name not in Authenticate2FA.get_payment_entries(auth_id):
101 | frappe.throw(
102 | title=_("Unauthorized Access"),
103 | msg=_("This Payment Entry is not authenticated for payment."),
104 | exc=frappe.PermissionError,
105 | )
106 |
107 | return True
108 |
109 | def _can_make_payout(self) -> bool:
110 | return bool(
111 | self.doc.payment_type == "Pay"
112 | and self.doc.paid_from_account_currency == PAYOUT_CURRENCY.INR.value
113 | and self.doc.docstatus == 1
114 | and is_payout_via_razorpayx(self.doc)
115 | )
116 |
117 | def _get_payout_details(self) -> dict:
118 | return {
119 | # Mandatory for all
120 | "source_doctype": self.doc.doctype,
121 | "source_docname": self.doc.name,
122 | "amount": self.doc.paid_amount,
123 | "party_type": self.doc.party_type,
124 | "mode": self.doc.payment_transfer_method,
125 | # Party Details
126 | "party_id": self.doc.party,
127 | "party_name": self.doc.party_name,
128 | "party_payment_details": {
129 | "bank_account_no": self.doc.party_bank_account_no,
130 | "bank_ifsc": self.doc.party_bank_ifsc,
131 | "upi_id": self.doc.party_upi_id,
132 | },
133 | "party_contact_details": {
134 | "party_name": self.doc.party_name,
135 | "party_mobile": self.doc.contact_mobile,
136 | "party_email": self.doc.contact_email,
137 | },
138 | # Payment Details
139 | "description": self.doc.razorpayx_payout_desc,
140 | }
141 |
142 | def _update_after_making(self, response: dict | None = None):
143 | notify = not frappe.flags.initiated_by_payment_processor
144 |
145 | user = (
146 | frappe.get_cached_value("User", "Administrator", "email")
147 | if frappe.session.user == "Administrator"
148 | else frappe.session.user
149 | )
150 |
151 | if user:
152 | self.doc.db_set("payment_authorized_by", user, notify=notify)
153 |
154 | if not response:
155 | return
156 |
157 | values = {}
158 |
159 | entity = response.get("entity")
160 | id = response.get("id")
161 |
162 | if not entity or not id:
163 | return
164 |
165 | if entity == "payout":
166 | values["razorpayx_payout_id"] = id
167 |
168 | if status := response.get("status"):
169 | values["razorpayx_payout_status"] = status.title()
170 |
171 | elif entity == "payout_link":
172 | values["razorpayx_payout_link_id"] = id
173 |
174 | if values:
175 | self.doc.db_set(values, notify=notify)
176 |
177 | # Note: status for Payout Link are not supported
178 | if entity == "payout":
179 | self.doc.run_notifications(STATUS_NOTIFICATION_METHOD)
180 |
181 | #### Cancel Payout | Payout Link ####
182 | def cancel(self, cancel_pe: bool = False):
183 | """
184 | Cancel payout and payout link of source document.
185 |
186 | This method supported only after cancelling Payment Entry's `before cancel` doc event.
187 |
188 | :param cancel_pe: Cancel Payment Entry or not.
189 |
190 | ---
191 | Note:
192 | - ⚠️ Only `queued` payout can be cancelled, otherwise it will raise error.
193 | - ⚠️ Only `issued` payout link can be cancelled, otherwise it will raise error.
194 | """
195 | marked_to_cancel = PayoutWithPaymentEntry.is_cancel_payout_marked(self.doc.name)
196 |
197 | if not self._can_cancel_payout_or_link():
198 | # from client side manually marked to cancel
199 | if marked_to_cancel:
200 | frappe.msgprint(
201 | title=_("Invalid Action"),
202 | msg=_("Payout couldn't be cancelled."),
203 | )
204 | return
205 |
206 | if marked_to_cancel or is_auto_cancel_payout_enabled(self.config_name):
207 | self.cancel_payout(cancel_pe=cancel_pe)
208 | self.cancel_payout_link(cancel_pe=cancel_pe)
209 |
210 | def cancel_payout(self, *, cancel_pe: bool = False) -> dict:
211 | """
212 | Cancel payout.
213 |
214 | :param cancel_pe: Cancel Payment Entry or not.
215 |
216 | ---
217 | Note: ⚠️ Only `queued` payout can be cancelled, otherwise it will raise error.
218 | """
219 |
220 | if not self.doc.razorpayx_payout_id:
221 | return
222 |
223 | response = RazorpayXCompositePayout(self.config_name).cancel(
224 | self.doc.razorpayx_payout_id,
225 | source_doctype=self.doc.doctype,
226 | source_docname=self.doc.name,
227 | )
228 |
229 | self._update_after_cancelling(response, cancel_pe=cancel_pe)
230 |
231 | return response
232 |
233 | def cancel_payout_link(self, *, cancel_pe: bool = False) -> dict:
234 | """
235 | Cancel payout link.
236 |
237 | :param cancel_pe: Cancel Payment Entry or not.
238 |
239 | ---
240 | Note: ⚠️ Only `issued` payout link can be cancelled, otherwise it will raise error.
241 | """
242 | if not self.doc.razorpayx_payout_link_id:
243 | return
244 |
245 | response = RazorpayXLinkPayout(self.config_name).cancel(
246 | self.doc.razorpayx_payout_link_id,
247 | source_doctype=self.doc.doctype,
248 | source_docname=self.doc.name,
249 | )
250 |
251 | self._update_after_cancelling(response, cancel_pe=cancel_pe)
252 |
253 | return response
254 |
255 | def _can_cancel_payout_or_link(self) -> bool:
256 | return self.doc.razorpayx_payout_status.lower() in [
257 | PAYOUT_STATUS.QUEUED.value,
258 | PAYOUT_STATUS.NOT_INITIATED.value,
259 | ] and is_payout_via_razorpayx(self.doc)
260 |
261 | def _update_after_cancelling(self, response: dict, *, cancel_pe: bool = False):
262 | """
263 | Update document after cancelling payout or payout link.
264 |
265 | :param response: Cancel API response.
266 | :param cancel_pe: Cancel Payment Entry or not.
267 | """
268 |
269 | self.doc.db_set(
270 | "razorpayx_payout_status",
271 | (response.get("status") or PAYOUT_STATUS.CANCELLED.value).title(),
272 | )
273 |
274 | if cancel_pe and self.doc.docstatus == 1:
275 | self.doc.flags.__canceled_by_rpx = True
276 | self.doc.cancel()
277 |
278 | @staticmethod
279 | def get_cancel_payout_key(docname: str) -> str:
280 | return f"cancel_payout_{frappe.scrub(docname)}"
281 |
282 | @staticmethod
283 | def is_cancel_payout_marked(docname: str) -> bool:
284 | key = PayoutWithPaymentEntry.get_cancel_payout_key(docname)
285 |
286 | if flag := frappe.cache.get(key):
287 | return flag.decode("utf-8") == "True"
288 |
289 | return False
290 |
--------------------------------------------------------------------------------
/razorpayx_integration/razorpayx_integration/utils/validation.py:
--------------------------------------------------------------------------------
1 | import re
2 |
3 | import frappe
4 | from frappe import _
5 |
6 | from razorpayx_integration.razorpayx_integration.constants.payouts import (
7 | DESCRIPTION_REGEX,
8 | FUND_ACCOUNT_TYPE,
9 | )
10 |
11 |
12 | def validate_fund_account_type(type: str):
13 | """
14 | :raises frappe.ValidationError: If the type is not valid.
15 | """
16 | if FUND_ACCOUNT_TYPE.has_value(type):
17 | return
18 |
19 | frappe.throw(
20 | msg=_("Invalid Account type: {0}.
Must be one of :
{1}").format(
21 | type, FUND_ACCOUNT_TYPE.values_as_html_list()
22 | ),
23 | title=_("Invalid RazorpayX Fund Account type"),
24 | exc=frappe.ValidationError,
25 | )
26 |
27 |
28 | def validate_payout_description(description: str):
29 | """
30 | Description/Narration should be of max 30 characters and A-Z, a-z, 0-9, and space only.
31 |
32 | Standard RazorpayX Payout Description/Narration validation.
33 |
34 | :raises frappe.ValidationError: If the description is not valid.
35 | """
36 | pattern = re.compile(DESCRIPTION_REGEX)
37 |
38 | if pattern.match(description):
39 | return
40 |
41 | frappe.throw(
42 | msg=_(
43 | "Must be alphanumeric and contain spaces only, with a maximum of 30 characters."
44 | ),
45 | title=_("Invalid RazorpayX Payout Description"),
46 | exc=frappe.ValidationError,
47 | )
48 |
--------------------------------------------------------------------------------
/razorpayx_integration/setup.py:
--------------------------------------------------------------------------------
1 | import click
2 | import frappe
3 | from frappe.custom.doctype.custom_field.custom_field import (
4 | create_custom_fields as make_custom_fields,
5 | )
6 | from payment_integration_utils.payment_integration_utils.setup import (
7 | delete_custom_fields,
8 | delete_property_setters,
9 | delete_roles_and_permissions,
10 | make_roles_and_permissions,
11 | )
12 |
13 | from razorpayx_integration.constants import PAYMENTS_PROCESSOR_APP
14 | from razorpayx_integration.razorpayx_integration.constants.custom_fields import (
15 | CUSTOM_FIELDS,
16 | PROCESSOR_FIELDS,
17 | )
18 | from razorpayx_integration.razorpayx_integration.constants.property_setters import (
19 | PROPERTY_SETTERS,
20 | )
21 | from razorpayx_integration.razorpayx_integration.constants.roles import ROLES
22 |
23 |
24 | ################### After Install ###################
25 | def setup_customizations():
26 | click.secho("Creating Roles and Permissions...", fg="blue")
27 | create_roles_and_permissions()
28 |
29 | click.secho("Creating Custom Fields...", fg="blue")
30 | create_custom_fields()
31 |
32 | if PAYMENTS_PROCESSOR_APP in frappe.get_installed_apps():
33 | click.secho(
34 | f"Creating Custom Fields for {frappe.unscrub(PAYMENTS_PROCESSOR_APP)}...",
35 | fg="blue",
36 | )
37 | create_payments_processor_custom_fields()
38 |
39 | click.secho("Creating Property Setters...", fg="blue")
40 | create_property_setters()
41 |
42 |
43 | # Note: separate functions are required to use in patches
44 | def create_roles_and_permissions():
45 | make_roles_and_permissions(ROLES)
46 |
47 |
48 | def create_custom_fields():
49 | make_custom_fields(CUSTOM_FIELDS)
50 |
51 |
52 | def create_property_setters():
53 | for property_setter in PROPERTY_SETTERS:
54 | frappe.make_property_setter(property_setter)
55 |
56 |
57 | def create_payments_processor_custom_fields():
58 | make_custom_fields(PROCESSOR_FIELDS)
59 |
60 |
61 | ################### Before Uninstall ###################
62 | def delete_customizations():
63 | click.secho("Deleting Custom Fields...", fg="blue")
64 | delete_custom_fields(CUSTOM_FIELDS)
65 |
66 | click.secho(
67 | f"Deleting Custom Fields for {frappe.unscrub(PAYMENTS_PROCESSOR_APP)}...",
68 | fg="blue",
69 | )
70 | delete_payments_processor_custom_fields()
71 |
72 | click.secho("Deleting Property Setters...", fg="blue")
73 | delete_property_setters(PROPERTY_SETTERS)
74 |
75 | click.secho("Deleting Roles and Permissions...", fg="blue")
76 | delete_roles_and_permissions(ROLES)
77 |
78 |
79 | # Note: separate functions are required to use in patches
80 | def delete_payments_processor_custom_fields():
81 | delete_custom_fields(PROCESSOR_FIELDS)
82 |
--------------------------------------------------------------------------------
/razorpayx_integration/templates/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/templates/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/templates/pages/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/resilient-tech/razorpayx-integration/661dd51f5b4938b3f03d28ebace0bc297e18ee50/razorpayx_integration/templates/pages/__init__.py
--------------------------------------------------------------------------------
/razorpayx_integration/uninstall.py:
--------------------------------------------------------------------------------
1 | import click
2 |
3 | from razorpayx_integration.constants import BUG_REPORT_URL, PAYMENTS_PROCESSOR_APP
4 | from razorpayx_integration.hooks import app_title as APP_NAME
5 | from razorpayx_integration.setup import (
6 | delete_customizations,
7 | delete_payments_processor_custom_fields,
8 | )
9 |
10 |
11 | def before_uninstall():
12 | try:
13 | delete_customizations()
14 | except Exception as e:
15 | click.secho(
16 | (
17 | f"\nUninstallation of {APP_NAME} failed due to an error."
18 | "Please try re-uninstalling the app or "
19 | f"report the issue on {BUG_REPORT_URL} if not resolved."
20 | ),
21 | fg="bright_red",
22 | )
23 | raise e
24 |
25 | click.secho(f"Thank you for using {APP_NAME}!", fg="green")
26 |
27 |
28 | def before_app_uninstall(app_name):
29 | if app_name == PAYMENTS_PROCESSOR_APP:
30 | delete_payments_processor_custom_fields()
31 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | pre-commit
--------------------------------------------------------------------------------