Compare commits

..

1 Commits

Author SHA1 Message Date
Ankush Menat
d3117cca0c perf: add indexes on payment entry reference
Adds index on:
1. reference doctype
2. reference name

*Why not composite index?*

There are three type of queries on this doctype

- filtering ref_doctype - doctype index helps here
- filtering ref_name - name index helps here
- filtering both - name index helps here too. Since it has sufficiently
  high cardinality. Composite index wont help in case where ref_doctype
  isn't specfied.
2022-12-12 12:32:14 +05:30
1246 changed files with 125072 additions and 76352 deletions

View File

@@ -2,32 +2,65 @@
"env": { "env": {
"browser": true, "browser": true,
"node": true, "node": true,
"es2022": true "es6": true
}, },
"parserOptions": { "parserOptions": {
"ecmaVersion": 11,
"sourceType": "module" "sourceType": "module"
}, },
"extends": "eslint:recommended", "extends": "eslint:recommended",
"rules": { "rules": {
"indent": "off", "indent": [
"brace-style": "off", "error",
"no-mixed-spaces-and-tabs": "off", "tab",
"no-useless-escape": "off", { "SwitchCase": 1 }
"space-unary-ops": ["error", { "words": true }], ],
"linebreak-style": "off", "brace-style": [
"quotes": ["off"], "error",
"semi": "off", "1tbs"
"camelcase": "off", ],
"no-unused-vars": "off", "space-unary-ops": [
"no-console": ["warn"], "error",
"no-extra-boolean-cast": ["off"], { "words": true }
"no-control-regex": ["off"] ],
"linebreak-style": [
"error",
"unix"
],
"quotes": [
"off"
],
"semi": [
"warn",
"always"
],
"camelcase": [
"off"
],
"no-unused-vars": [
"warn"
],
"no-redeclare": [
"warn"
],
"no-console": [
"warn"
],
"no-extra-boolean-cast": [
"off"
],
"no-control-regex": [
"off"
],
"space-before-blocks": "warn",
"keyword-spacing": "warn",
"comma-spacing": "warn",
"key-spacing": "warn"
}, },
"root": true, "root": true,
"globals": { "globals": {
"frappe": true, "frappe": true,
"Vue": true, "Vue": true,
"SetVueGlobals": true,
"erpnext": true, "erpnext": true,
"hub": true, "hub": true,
"$": true, "$": true,
@@ -64,10 +97,8 @@
"is_null": true, "is_null": true,
"in_list": true, "in_list": true,
"has_common": true, "has_common": true,
"posthog": true,
"has_words": true, "has_words": true,
"validate_email": true, "validate_email": true,
"open_web_template_values_editor": true,
"get_number_format": true, "get_number_format": true,
"format_number": true, "format_number": true,
"format_currency": true, "format_currency": true,
@@ -123,6 +154,7 @@
"before": true, "before": true,
"beforeEach": true, "beforeEach": true,
"onScan": true, "onScan": true,
"html2canvas": true,
"extend_cscript": true, "extend_cscript": true,
"localforage": true "localforage": true
} }

View File

@@ -66,8 +66,7 @@ ignore =
F841, F841,
E713, E713,
E712, E712,
B023, B023
B028
max-line-length = 200 max-line-length = 200

View File

@@ -3,71 +3,52 @@ import requests
from urllib.parse import urlparse from urllib.parse import urlparse
WEBSITE_REPOS = [ docs_repos = [
"frappe_docs",
"erpnext_documentation",
"erpnext_com", "erpnext_com",
"frappe_io", "frappe_io",
] ]
DOCUMENTATION_DOMAINS = [
"docs.erpnext.com",
"frappeframework.com",
]
def uri_validator(x):
result = urlparse(x)
return all([result.scheme, result.netloc, result.path])
def is_valid_url(url: str) -> bool: def docs_link_exists(body):
parts = urlparse(url) for line in body.splitlines():
return all((parts.scheme, parts.netloc, parts.path)) for word in line.split():
if word.startswith('http') and uri_validator(word):
def is_documentation_link(word: str) -> bool:
if not word.startswith("http") or not is_valid_url(word):
return False
parsed_url = urlparse(word) parsed_url = urlparse(word)
if parsed_url.netloc in DOCUMENTATION_DOMAINS:
return True
if parsed_url.netloc == "github.com": if parsed_url.netloc == "github.com":
parts = parsed_url.path.split("/") parts = parsed_url.path.split('/')
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in WEBSITE_REPOS: if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True
elif parsed_url.netloc == "docs.erpnext.com":
return True return True
return False
if __name__ == "__main__":
pr = sys.argv[1]
response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr))
def contains_documentation_link(body: str) -> bool: if response.ok:
return any(
is_documentation_link(word)
for line in body.splitlines()
for word in line.split()
)
def check_pull_request(number: str) -> "tuple[int, str]":
response = requests.get(f"https://api.github.com/repos/frappe/erpnext/pulls/{number}")
if not response.ok:
return 1, "Pull Request Not Found! ⚠️"
payload = response.json() payload = response.json()
title = (payload.get("title") or "").lower().strip() title = (payload.get("title") or "").lower().strip()
head_sha = (payload.get("head") or {}).get("sha") head_sha = (payload.get("head") or {}).get("sha")
body = (payload.get("body") or "").lower() body = (payload.get("body") or "").lower()
if ( if (title.startswith("feat")
not title.startswith("feat") and head_sha
or not head_sha and "no-docs" not in body
or "no-docs" in body and "backport" not in body
or "backport" in body
): ):
return 0, "Skipping documentation checks... 🏃" if docs_link_exists(body):
print("Documentation Link Found. You're Awesome! 🎉")
if contains_documentation_link(body): else:
return 0, "Documentation Link Found. You're Awesome! 🎉" print("Documentation Link Not Found! ⚠️")
sys.exit(1)
return 1, "Documentation Link Not Found! ⚠️" else:
print("Skipping documentation checks... 🏃")
if __name__ == "__main__":
exit_code, message = check_pull_request(sys.argv[1])
print(message)
sys.exit(exit_code)

View File

@@ -8,9 +8,8 @@ sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench pip install frappe-bench
githubbranch=${GITHUB_BASE_REF:-${GITHUB_REF##*/}}
frappeuser=${FRAPPE_USER:-"frappe"} frappeuser=${FRAPPE_USER:-"frappe"}
frappebranch=${FRAPPE_BRANCH:-$githubbranch} frappebranch=${FRAPPE_BRANCH:-${GITHUB_BASE_REF:-${GITHUB_REF##*/}}}
git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1 git clone "https://github.com/${frappeuser}/frappe" --branch "${frappebranch}" --depth 1
bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
@@ -42,17 +41,12 @@ fi
install_whktml() { install_whktml() {
if [ "$(lsb_release -rs)" = "22.04" ]; then wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
sudo apt install /tmp/wkhtmltox.deb sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
else sudo chmod o+x /usr/local/bin/wkhtmltopdf
echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)"
exit 1
fi
} }
install_whktml & install_whktml &
wkpid=$!
cd ~/frappe-bench || exit cd ~/frappe-bench || exit
@@ -61,13 +55,11 @@ sed -i 's/schedule:/# schedule:/g' Procfile
sed -i 's/socketio:/# socketio:/g' Procfile sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app payments --branch ${githubbranch%"-hotfix"} bench get-app payments
bench get-app erpnext "${GITHUB_WORKSPACE}" bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
wait $wkpid
bench start &> bench_run_logs.txt & bench start &> bench_run_logs.txt &
CI=Yes bench build --app frappe & CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes bench --site test_site reinstall --yes

View File

@@ -11,6 +11,6 @@
"root_login": "root", "root_login": "root",
"root_password": "root", "root_password": "root",
"host_name": "http://test_site:8000", "host_name": "http://test_site:8000",
"install_apps": ["payments", "erpnext"], "install_apps": ["erpnext"],
"throttle_user_limit": 100 "throttle_user_limit": 100
} }

View File

@@ -9,7 +9,7 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
stable-release: release:
name: Release name: Release
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
@@ -30,23 +30,3 @@ jobs:
head: version-${{ matrix.version }}-hotfix head: version-${{ matrix.version }}-hotfix
env: env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }} GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
beta-release:
name: Release
runs-on: ubuntu-latest
strategy:
fail-fast: false
steps:
- uses: octokit/request-action@v2.x
with:
route: POST /repos/{owner}/{repo}/pulls
owner: frappe
repo: erpnext
title: |-
"chore: release v15 beta"
body: "Automated beta release."
base: version-15-beta
head: develop
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

View File

@@ -9,22 +9,21 @@ jobs:
name: linters name: linters
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v3 - uses: actions/checkout@v2
- name: Set up Python 3.10 - name: Set up Python 3.10
uses: actions/setup-python@v4 uses: actions/setup-python@v2
with: with:
python-version: '3.10' python-version: '3.10'
cache: pip
- name: Install and Run Pre-commit - name: Install and Run Pre-commit
uses: pre-commit/action@v3.0.0 uses: pre-commit/action@v2.0.3
- name: Download Semgrep rules - name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- name: Download semgrep - name: Download semgrep
run: pip install semgrep run: pip install semgrep==0.97.0
- name: Run Semgrep rules - name: Run Semgrep rules
run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness run: semgrep ci --config ./frappe-semgrep-rules/rules --config r/python.lang.correctness

View File

@@ -43,16 +43,14 @@ jobs:
fi fi
- name: Setup Python - name: Setup Python
uses: "actions/setup-python@v4" uses: "gabrielfalcao/pyenv-action@v9"
with: with:
python-version: | versions: 3.10:latest, 3.7:latest
3.7
3.10
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: 14
check-latest: true check-latest: true
- name: Add to Hosts - name: Add to Hosts
@@ -94,6 +92,7 @@ jobs:
- name: Install - name: Install
run: | run: |
pip install frappe-bench pip install frappe-bench
pyenv global $(pyenv versions | grep '3.10')
bash ${GITHUB_WORKSPACE}/.github/helper/install.sh bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env: env:
DB: mariadb DB: mariadb
@@ -108,6 +107,7 @@ jobs:
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
pyenv global $(pyenv versions | grep '3.7')
for version in $(seq 12 13) for version in $(seq 12 13)
do do
echo "Updating to v$version" echo "Updating to v$version"
@@ -120,7 +120,7 @@ jobs:
git -C "apps/erpnext" checkout -q -f $branch_name git -C "apps/erpnext" checkout -q -f $branch_name
rm -rf ~/frappe-bench/env rm -rf ~/frappe-bench/env
bench setup env --python python3.7 bench setup env
bench pip install -e ./apps/payments bench pip install -e ./apps/payments
bench pip install -e ./apps/erpnext bench pip install -e ./apps/erpnext
@@ -132,8 +132,9 @@ jobs:
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA" git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
pyenv global $(pyenv versions | grep '3.10')
rm -rf ~/frappe-bench/env rm -rf ~/frappe-bench/env
bench -v setup env --python python3.10 bench -v setup env
bench pip install -e ./apps/payments bench pip install -e ./apps/payments
bench pip install -e ./apps/erpnext bench pip install -e ./apps/erpnext

View File

@@ -13,10 +13,10 @@ jobs:
with: with:
fetch-depth: 0 fetch-depth: 0
persist-credentials: false persist-credentials: false
- name: Setup Node.js - name: Setup Node.js v14
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: 14
- name: Setup dependencies - name: Setup dependencies
run: | run: |
npm install @semantic-release/git @semantic-release/exec --no-save npm install @semantic-release/git @semantic-release/exec --no-save

View File

@@ -1,38 +0,0 @@
# This action:
#
# 1. Generates release notes using github API.
# 2. Strips unnecessary info like chore/style etc from notes.
# 3. Updates release info.
# This action needs to be maintained on all branches that do releases.
name: 'Release Notes'
on:
workflow_dispatch:
inputs:
tag_name:
description: 'Tag of release like v13.0.0'
required: true
type: string
release:
types: [released]
permissions:
contents: read
jobs:
regen-notes:
name: 'Regenerate release notes'
runs-on: ubuntu-latest
steps:
- name: Update notes
run: |
NEW_NOTES=$(gh api --method POST -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/generate-notes -f tag_name=$RELEASE_TAG | jq -r '.body' | sed -E '/^\* (chore|ci|test|docs|style)/d' )
RELEASE_ID=$(gh api -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/tags/$RELEASE_TAG | jq -r '.id')
gh api --method PATCH -H "Accept: application/vnd.github+json" /repos/frappe/erpnext/releases/$RELEASE_ID -f body="$NEW_NOTES"
env:
GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
RELEASE_TAG: ${{ github.event.inputs.tag_name || github.event.release.tag_name }}

View File

@@ -21,7 +21,7 @@ jobs:
- uses: actions/setup-node@v3 - uses: actions/setup-node@v3
with: with:
node-version: 18 node-version: 14
check-latest: true check-latest: true
- name: Check commit titles - name: Check commit titles

View File

@@ -7,18 +7,21 @@ on:
- '**.css' - '**.css'
- '**.md' - '**.md'
- '**.html' - '**.html'
schedule: - '**.csv'
# Run everday at midnight UTC / 5:30 IST push:
- cron: "0 0 * * *" branches: [ develop ]
paths-ignore:
- '**.js'
- '**.md'
workflow_dispatch: workflow_dispatch:
inputs: inputs:
user: user:
description: 'Frappe Framework repository user (add your username for forks)' description: 'user'
required: true required: true
default: 'frappe' default: 'frappe'
type: string type: string
branch: branch:
description: 'Frappe Framework branch' description: 'Branch name'
default: 'develop' default: 'develop'
required: false required: false
type: string type: string
@@ -69,7 +72,7 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: 14
check-latest: true check-latest: true
- name: Add to Hosts - name: Add to Hosts

View File

@@ -59,7 +59,7 @@ jobs:
- name: Setup Node - name: Setup Node
uses: actions/setup-node@v2 uses: actions/setup-node@v2
with: with:
node-version: 18 node-version: 14
check-latest: true check-latest: true
- name: Add to Hosts - name: Add to Hosts

View File

@@ -16,26 +16,8 @@ repos:
- id: check-merge-conflict - id: check-merge-conflict
- id: check-ast - id: check-ast
- repo: https://github.com/pre-commit/mirrors-eslint
rev: v8.44.0
hooks:
- id: eslint
types_or: [javascript]
args: ['--quiet']
# Ignore any files that might contain jinja / bundles
exclude: |
(?x)^(
erpnext/public/dist/.*|
cypress/.*|
.*node_modules.*|
.*boilerplate.*|
erpnext/public/js/controllers/.*|
erpnext/templates/pages/order.js|
erpnext/templates/includes/.*
)$
- repo: https://github.com/PyCQA/flake8 - repo: https://github.com/PyCQA/flake8
rev: 6.0.0 rev: 5.0.4
hooks: hooks:
- id: flake8 - id: flake8
additional_dependencies: [ additional_dependencies: [
@@ -50,8 +32,8 @@ repos:
- id: black - id: black
additional_dependencies: ['click==8.0.4'] additional_dependencies: ['click==8.0.4']
- repo: https://github.com/PyCQA/isort - repo: https://github.com/timothycrosley/isort
rev: 5.12.0 rev: 5.9.1
hooks: hooks:
- id: isort - id: isort
exclude: ".*setup.py$" exclude: ".*setup.py$"

View File

@@ -3,22 +3,26 @@
# These owners will be the default owners for everything in # These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence, # the repo. Unless a later match takes precedence,
erpnext/accounts/ @deepeshgarg007 @ruthra-kumar erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @anandbaburajan @deepeshgarg007 erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/regional @deepeshgarg007 @ruthra-kumar erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/selling @deepeshgarg007 @ruthra-kumar erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/support/ @deepeshgarg007 erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
pos* erpnext/support/ @nextchamp-saqib @deepeshgarg007
pos* @nextchamp-saqib
erpnext/buying/ @rohitwaghchaure @s-aga-r erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r erpnext/maintenance/ @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
erpnext/quality_management/ @rohitwaghchaure @s-aga-r erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/controllers/ @deepeshgarg007 @rohitwaghchaure erpnext/crm/ @NagariaHussain
erpnext/patches/ @deepeshgarg007 erpnext/education/ @rutwikhdev
erpnext/projects/ @ruchamahabal
.github/ @deepeshgarg007 erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
pyproject.toml @phot0n erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
.github/ @ankush
pyproject.toml @ankush

View File

@@ -65,7 +65,7 @@ New passwords will be created for the ERPNext "Administrator" user, the MariaDB
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community. 1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext. 2. [Official documentation](https://docs.erpnext.com/) - Extensive documentation for ERPNext.
3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers. 3. [Discussion Forum](https://discuss.erpnext.com/) - Engage with community of ERPNext users and service providers.
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users. 4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users.
## Contributing ## Contributing

View File

@@ -1,9 +1,8 @@
import functools
import inspect import inspect
import frappe import frappe
__version__ = "15.0.0-dev" __version__ = "14.0.0-dev"
def get_default_company(user=None): def get_default_company(user=None):
@@ -121,14 +120,12 @@ def get_region(company=None):
You can also set global company flag in `frappe.flags.company` You can also set global company flag in `frappe.flags.company`
""" """
if company or frappe.flags.company:
if not company: return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
company = frappe.local.flags.company elif frappe.flags.country:
return frappe.flags.country
if company: else:
return frappe.get_cached_value("Company", company, "country") return frappe.get_system_settings("country")
return frappe.flags.country or frappe.get_system_settings("country")
def allow_regional(fn): def allow_regional(fn):
@@ -139,7 +136,6 @@ def allow_regional(fn):
def myfunction(): def myfunction():
pass""" pass"""
@functools.wraps(fn)
def caller(*args, **kwargs): def caller(*args, **kwargs):
overrides = frappe.get_hooks("regional_overrides", {}).get(get_region()) overrides = frappe.get_hooks("regional_overrides", {}).get(get_region())
function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}" function_path = f"{inspect.getmodule(fn).__name__}.{fn.__name__}"

View File

@@ -4,19 +4,18 @@
"creation": "2020-07-17 11:25:34.593061", "creation": "2020-07-17 11:25:34.593061",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
"filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}", "filters_json": "{\"period\":\"Monthly\",\"budget_against\":\"Cost Center\",\"show_cumulative\":0}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2023-07-19 13:13:13.307073", "modified": "2020-07-22 12:24:49.144210",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Budget Variance", "name": "Budget Variance",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Budget Variance Report", "report_name": "Budget Variance Report",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Bar", "type": "Bar",
"use_report_chart": 1, "use_report_chart": 1,

View File

@@ -4,19 +4,18 @@
"creation": "2020-07-17 11:25:34.448572", "creation": "2020-07-17 11:25:34.448572",
"docstatus": 0, "docstatus": 0,
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"erpnext.utils.get_fiscal_year()\",\"to_fiscal_year\":\"erpnext.utils.get_fiscal_year()\"}", "dynamic_filters_json": "{\"company\":\"frappe.defaults.get_user_default(\\\"Company\\\")\",\"from_fiscal_year\":\"frappe.sys_defaults.fiscal_year\",\"to_fiscal_year\":\"frappe.sys_defaults.fiscal_year\"}",
"filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}", "filters_json": "{\"filter_based_on\":\"Fiscal Year\",\"period_start_date\":\"2020-04-01\",\"period_end_date\":\"2021-03-31\",\"periodicity\":\"Yearly\",\"include_default_book_entries\":1}",
"idx": 0, "idx": 0,
"is_public": 1, "is_public": 1,
"is_standard": 1, "is_standard": 1,
"modified": "2023-07-19 13:08:56.470390", "modified": "2020-07-22 12:33:48.888943",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Profit and Loss", "name": "Profit and Loss",
"number_of_groups": 0, "number_of_groups": 0,
"owner": "Administrator", "owner": "Administrator",
"report_name": "Profit and Loss Statement", "report_name": "Profit and Loss Statement",
"roles": [],
"timeseries": 0, "timeseries": 0,
"type": "Bar", "type": "Bar",
"use_report_chart": 1, "use_report_chart": 1,

View File

@@ -136,7 +136,7 @@ def convert_deferred_revenue_to_income(
send_mail(deferred_process) send_mail(deferred_process)
def get_booking_dates(doc, item, posting_date=None, prev_posting_date=None): def get_booking_dates(doc, item, posting_date=None):
if not posting_date: if not posting_date:
posting_date = add_days(today(), -1) posting_date = add_days(today(), -1)
@@ -146,7 +146,6 @@ def get_booking_dates(doc, item, posting_date=None, prev_posting_date=None):
"deferred_revenue_account" if doc.doctype == "Sales Invoice" else "deferred_expense_account" "deferred_revenue_account" if doc.doctype == "Sales Invoice" else "deferred_expense_account"
) )
if not prev_posting_date:
prev_gl_entry = frappe.db.sql( prev_gl_entry = frappe.db.sql(
""" """
select name, posting_date from `tabGL Entry` where company=%s and account=%s and select name, posting_date from `tabGL Entry` where company=%s and account=%s and
@@ -180,8 +179,6 @@ def get_booking_dates(doc, item, posting_date=None, prev_posting_date=None):
else: else:
start_date = item.service_start_date start_date = item.service_start_date
else:
start_date = getdate(add_days(prev_posting_date, 1))
end_date = get_last_day(start_date) end_date = get_last_day(start_date)
if end_date >= item.service_end_date: if end_date >= item.service_end_date:
end_date = item.service_end_date end_date = item.service_end_date
@@ -344,15 +341,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto") accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
def _book_deferred_revenue_or_expense( def _book_deferred_revenue_or_expense(
item, item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
via_journal_entry,
submit_journal_entry,
book_deferred_entries_based_on,
prev_posting_date=None,
): ):
start_date, end_date, last_gl_entry = get_booking_dates( start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
doc, item, posting_date=posting_date, prev_posting_date=prev_posting_date
)
if not (start_date and end_date): if not (start_date and end_date):
return return
@@ -386,12 +377,9 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
if not amount: if not amount:
return return
gl_posting_date = end_date
prev_posting_date = None
# check if books nor frozen till endate: # check if books nor frozen till endate:
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto): if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
gl_posting_date = get_last_day(add_days(accounts_frozen_upto, 1)) end_date = get_last_day(add_days(accounts_frozen_upto, 1))
prev_posting_date = end_date
if via_journal_entry: if via_journal_entry:
book_revenue_via_journal_entry( book_revenue_via_journal_entry(
@@ -400,7 +388,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
debit_account, debit_account,
amount, amount,
base_amount, base_amount,
gl_posting_date, end_date,
project, project,
account_currency, account_currency,
item.cost_center, item.cost_center,
@@ -416,7 +404,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
against, against,
amount, amount,
base_amount, base_amount,
gl_posting_date, end_date,
project, project,
account_currency, account_currency,
item.cost_center, item.cost_center,
@@ -430,11 +418,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
if getdate(end_date) < getdate(posting_date) and not last_gl_entry: if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
_book_deferred_revenue_or_expense( _book_deferred_revenue_or_expense(
item, item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
via_journal_entry,
submit_journal_entry,
book_deferred_entries_based_on,
prev_posting_date,
) )
via_journal_entry = cint( via_journal_entry = cint(

View File

@@ -79,8 +79,8 @@ frappe.ui.form.on('Account', {
frm.add_custom_button(__('General Ledger'), function () { frm.add_custom_button(__('General Ledger'), function () {
frappe.route_options = { frappe.route_options = {
"account": frm.doc.name, "account": frm.doc.name,
"from_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], "from_date": frappe.sys_defaults.year_start_date,
"to_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], "to_date": frappe.sys_defaults.year_end_date,
"company": frm.doc.company "company": frm.doc.company
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");

View File

@@ -18,6 +18,7 @@
"root_type", "root_type",
"report_type", "report_type",
"account_currency", "account_currency",
"inter_company_account",
"column_break1", "column_break1",
"parent_account", "parent_account",
"account_type", "account_type",
@@ -33,11 +34,15 @@
{ {
"fieldname": "properties", "fieldname": "properties",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"oldfieldtype": "Section Break" "oldfieldtype": "Section Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break0", "fieldname": "column_break0",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1,
"width": "50%" "width": "50%"
}, },
{ {
@@ -48,7 +53,9 @@
"no_copy": 1, "no_copy": 1,
"oldfieldname": "account_name", "oldfieldname": "account_name",
"oldfieldtype": "Data", "oldfieldtype": "Data",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "account_number", "fieldname": "account_number",
@@ -56,13 +63,17 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Account Number", "label": "Account Number",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "is_group", "fieldname": "is_group",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Is Group" "label": "Is Group",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "company", "fieldname": "company",
@@ -74,7 +85,9 @@
"options": "Company", "options": "Company",
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 1, "remember_last_selected_value": 1,
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "root_type", "fieldname": "root_type",
@@ -82,7 +95,9 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Root Type", "label": "Root Type",
"options": "\nAsset\nLiability\nIncome\nExpense\nEquity", "options": "\nAsset\nLiability\nIncome\nExpense\nEquity",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "report_type", "fieldname": "report_type",
@@ -90,18 +105,32 @@
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Report Type", "label": "Report Type",
"options": "\nBalance Sheet\nProfit and Loss", "options": "\nBalance Sheet\nProfit and Loss",
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.is_group==0", "depends_on": "eval:doc.is_group==0",
"fieldname": "account_currency", "fieldname": "account_currency",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Currency", "label": "Currency",
"options": "Currency" "options": "Currency",
"show_days": 1,
"show_seconds": 1
},
{
"default": "0",
"fieldname": "inter_company_account",
"fieldtype": "Check",
"label": "Inter Company Account",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break1", "fieldname": "column_break1",
"fieldtype": "Column Break", "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1,
"width": "50%" "width": "50%"
}, },
{ {
@@ -113,7 +142,9 @@
"oldfieldtype": "Link", "oldfieldtype": "Link",
"options": "Account", "options": "Account",
"reqd": 1, "reqd": 1,
"search_index": 1 "search_index": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"description": "Setting Account Type helps in selecting this Account in transactions.", "description": "Setting Account Type helps in selecting this Account in transactions.",
@@ -123,7 +154,9 @@
"label": "Account Type", "label": "Account Type",
"oldfieldname": "account_type", "oldfieldname": "account_type",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary" "options": "\nAccumulated Depreciation\nAsset Received But Not Billed\nBank\nCash\nChargeable\nCapital Work in Progress\nCost of Goods Sold\nDepreciation\nEquity\nExpense Account\nExpenses Included In Asset Valuation\nExpenses Included In Valuation\nFixed Asset\nIncome Account\nPayable\nReceivable\nRound Off\nStock\nStock Adjustment\nStock Received But Not Billed\nService Received But Not Billed\nTax\nTemporary",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"description": "Rate at which this tax is applied", "description": "Rate at which this tax is applied",
@@ -131,7 +164,9 @@
"fieldtype": "Float", "fieldtype": "Float",
"label": "Rate", "label": "Rate",
"oldfieldname": "tax_rate", "oldfieldname": "tax_rate",
"oldfieldtype": "Currency" "oldfieldtype": "Currency",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"description": "If the account is frozen, entries are allowed to restricted users.", "description": "If the account is frozen, entries are allowed to restricted users.",
@@ -140,13 +175,17 @@
"label": "Frozen", "label": "Frozen",
"oldfieldname": "freeze_account", "oldfieldname": "freeze_account",
"oldfieldtype": "Select", "oldfieldtype": "Select",
"options": "No\nYes" "options": "No\nYes",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "balance_must_be", "fieldname": "balance_must_be",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Balance must be", "label": "Balance must be",
"options": "\nDebit\nCredit" "options": "\nDebit\nCredit",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "lft", "fieldname": "lft",
@@ -155,7 +194,9 @@
"label": "Lft", "label": "Lft",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"search_index": 1 "search_index": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "rgt", "fieldname": "rgt",
@@ -164,7 +205,9 @@
"label": "Rgt", "label": "Rgt",
"print_hide": 1, "print_hide": 1,
"read_only": 1, "read_only": 1,
"search_index": 1 "search_index": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "old_parent", "fieldname": "old_parent",
@@ -172,27 +215,33 @@
"hidden": 1, "hidden": 1,
"label": "Old Parent", "label": "Old Parent",
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"depends_on": "eval:(doc.report_type == 'Profit and Loss' && !doc.is_group)", "depends_on": "eval:(doc.report_type == 'Profit and Loss' && !doc.is_group)",
"fieldname": "include_in_gross", "fieldname": "include_in_gross",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Include in gross" "label": "Include in gross",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable" "label": "Disable",
"show_days": 1,
"show_seconds": 1
} }
], ],
"icon": "fa fa-money", "icon": "fa fa-money",
"idx": 1, "idx": 1,
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"modified": "2023-04-11 16:08:46.983677", "modified": "2020-06-11 15:15:54.338622",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Account", "name": "Account",
@@ -252,6 +301,5 @@
"show_name_in_global_search": 1, "show_name_in_global_search": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -204,11 +204,8 @@ class Account(NestedSet):
) )
def validate_account_currency(self): def validate_account_currency(self):
self.currency_explicitly_specified = True
if not self.account_currency: if not self.account_currency:
self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency") self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
self.currency_explicitly_specified = False
gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency") gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
@@ -254,10 +251,8 @@ class Account(NestedSet):
{ {
"company": company, "company": company,
# parent account's currency should be passed down to child account's curreny # parent account's currency should be passed down to child account's curreny
# if currency explicitly specified by user, child will inherit. else, default currency will be used. # if it is None, it picks it up from default company currency, which might be unintended
"account_currency": self.account_currency "account_currency": erpnext.get_company_currency(company),
if self.currency_explicitly_specified
else erpnext.get_company_currency(company),
"parent_account": parent_acc_name_map[company], "parent_account": parent_acc_name_map[company],
} }
) )
@@ -399,13 +394,7 @@ def update_account_number(name, account_name, account_number=None, from_descenda
if ancestors and not allow_independent_account_creation: if ancestors and not allow_independent_account_creation:
for ancestor in ancestors: for ancestor in ancestors:
old_name = frappe.db.get_value( if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
"Account",
{"account_number": old_acc_number, "account_name": old_acc_name, "company": ancestor},
"name",
)
if old_name:
# same account in parent company exists # same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company") allow_child_account_creation = _("Allow Account Creation Against Child Company")

View File

@@ -56,9 +56,6 @@ frappe.treeview_settings["Account"] = {
accounts = nodes; accounts = nodes;
} }
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
if(value) {
const get_balances = frappe.call({ const get_balances = frappe.call({
method: 'erpnext.accounts.utils.get_account_balances', method: 'erpnext.accounts.utils.get_account_balances',
args: { args: {
@@ -91,8 +88,6 @@ frappe.treeview_settings["Account"] = {
} }
} }
}); });
}
});
}, },
add_tree_node: 'erpnext.accounts.utils.add_ac', add_tree_node: 'erpnext.accounts.utils.add_ac',
menu_items:[ menu_items:[
@@ -194,8 +189,8 @@ frappe.treeview_settings["Account"] = {
click: function(node, btn) { click: function(node, btn) {
frappe.route_options = { frappe.route_options = {
"account": node.label, "account": node.label,
"from_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[1], "from_date": frappe.sys_defaults.year_start_date,
"to_date": erpnext.utils.get_fiscal_year(frappe.datetime.get_today(), true)[2], "to_date": frappe.sys_defaults.year_end_date,
"company": frappe.treeview_settings['Account'].treeview.page.fields_dict.company.get_value() "company": frappe.treeview_settings['Account'].treeview.page.fields_dict.company.get_value()
}; };
frappe.set_route("query-report", "General Ledger"); frappe.set_route("query-report", "General Ledger");

View File

@@ -29,7 +29,6 @@ def create_charts(
"root_type", "root_type",
"is_group", "is_group",
"tax_rate", "tax_rate",
"account_currency",
]: ]:
account_number = cstr(child.get("account_number")).strip() account_number = cstr(child.get("account_number")).strip()
@@ -96,17 +95,7 @@ def identify_is_group(child):
is_group = child.get("is_group") is_group = child.get("is_group")
elif len( elif len(
set(child.keys()) set(child.keys())
- set( - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
[
"account_name",
"account_type",
"root_type",
"is_group",
"tax_rate",
"account_number",
"account_currency",
]
)
): ):
is_group = 1 is_group = 1
else: else:
@@ -196,7 +185,6 @@ def get_account_tree_from_existing_company(existing_company):
"root_type", "root_type",
"tax_rate", "tax_rate",
"account_number", "account_number",
"account_currency",
], ],
order_by="lft, rgt", order_by="lft, rgt",
) )
@@ -279,7 +267,6 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
"root_type", "root_type",
"is_group", "is_group",
"tax_rate", "tax_rate",
"account_currency",
]: ]:
continue continue

View File

@@ -11,11 +11,11 @@
"account_number": "0027", "account_number": "0027",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Geschäftsausstattung": { "Gesch\u00e4ftsausstattung": {
"account_number": "0410", "account_number": "0410",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
"Büroeinrichtung": { "B\u00fcroeinrichtung": {
"account_number": "0420", "account_number": "0420",
"account_type": "Fixed Asset" "account_type": "Fixed Asset"
}, },
@@ -60,46 +60,36 @@
"Durchlaufende Posten": { "Durchlaufende Posten": {
"account_number": "1590" "account_number": "1590"
}, },
"Verrechnungskonto Gewinnermittlung § 4 Abs. 3 EStG, nicht ergebniswirksam": { "Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": {
"account_number": "1371" "account_number": "1371"
}, },
"Abziehbare Vorsteuer": { "Abziehbare Vorsteuer": {
"account_type": "Tax",
"is_group": 1, "is_group": 1,
"Abziehbare Vorsteuer 7 %": { "Abziehbare Vorsteuer 7%": {
"account_number": "1571", "account_number": "1571"
"account_type": "Tax",
"tax_rate": 7.0
}, },
"Abziehbare Vorsteuer 19 %": { "Abziehbare Vorsteuer 19%": {
"account_number": "1576", "account_number": "1576"
"account_type": "Tax",
"tax_rate": 19.0
}, },
"Abziehbare Vorsteuer nach § 13b UStG 19 %": { "Abziehbare Vorsteuer nach \u00a713b UStG 19%": {
"account_number": "1577", "account_number": "1577"
"account_type": "Tax", },
"tax_rate": 19.0 "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
"account_number": "3120"
} }
} }
}, },
"III. Wertpapiere": { "III. Wertpapiere": {
"is_group": 1, "is_group": 1
"Anteile an verbundenen Unternehmen (Umlaufvermögen)": {
"account_number": "1340"
},
"Anteile an herrschender oder mit Mehrheit beteiligter Gesellschaft": {
"account_number": "1344"
},
"Sonstige Wertpapiere": {
"account_number": "1348"
}
}, },
"IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": { "IV. Kassenbestand, Bundesbankguthaben, Guthaben bei Kreditinstituten und Schecks.": {
"is_group": 1, "is_group": 1,
"Kasse": { "Kasse": {
"is_group": 1,
"account_type": "Cash", "account_type": "Cash",
"is_group": 1,
"Kasse": { "Kasse": {
"is_group": 1,
"account_number": "1000", "account_number": "1000",
"account_type": "Cash" "account_type": "Cash"
} }
@@ -210,32 +200,26 @@
}, },
"Umsatzsteuer": { "Umsatzsteuer": {
"is_group": 1, "is_group": 1,
"Umsatzsteuer 7 %": {
"account_number": "1771",
"account_type": "Tax", "account_type": "Tax",
"tax_rate": 7.0 "Umsatzsteuer 7%": {
"account_number": "1771"
}, },
"Umsatzsteuer 19 %": { "Umsatzsteuer 19%": {
"account_number": "1776", "account_number": "1776"
"account_type": "Tax",
"tax_rate": 19.0
}, },
"Umsatzsteuer-Vorauszahlung": { "Umsatzsteuer-Vorauszahlung": {
"account_number": "1780", "account_number": "1780"
"account_type": "Tax"
}, },
"Umsatzsteuer-Vorauszahlung 1/11": { "Umsatzsteuer-Vorauszahlung 1/11": {
"account_number": "1781" "account_number": "1781"
}, },
"Umsatzsteuer nach § 13b UStG 19 %": { "Umsatzsteuer \u00a7 13b UStG 19%": {
"account_number": "1787", "account_number": "1787"
"account_type": "Tax",
"tax_rate": 19.0
}, },
"Umsatzsteuer Vorjahr": { "Umsatzsteuer Vorjahr": {
"account_number": "1790" "account_number": "1790"
}, },
"Umsatzsteuer frühere Jahre": { "Umsatzsteuer fr\u00fchere Jahre": {
"account_number": "1791" "account_number": "1791"
} }
} }
@@ -251,35 +235,35 @@
"is_group": 1 "is_group": 1
} }
}, },
"Erlöse u. Erträge 2/8": { "Erl\u00f6se u. Ertr\u00e4ge 2/8": {
"is_group": 1, "is_group": 1,
"root_type": "Income", "root_type": "Income",
"Erlöskonten 8": { "Erl\u00f6skonten 8": {
"is_group": 1, "is_group": 1,
"Erlöse": { "Erl\u00f6se": {
"account_number": "8200", "account_number": "8200",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Erlöse USt. 19 %": { "Erl\u00f6se USt. 19%": {
"account_number": "8400", "account_number": "8400",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Erlöse USt. 7 %": { "Erl\u00f6se USt. 7%": {
"account_number": "8300", "account_number": "8300",
"account_type": "Income Account" "account_type": "Income Account"
} }
}, },
"Ertragskonten 2": { "Ertragskonten 2": {
"is_group": 1, "is_group": 1,
"sonstige Zinsen und ähnliche Erträge": { "sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": {
"account_number": "2650", "account_number": "2650",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Außerordentliche Erträge": { "Au\u00dferordentliche Ertr\u00e4ge": {
"account_number": "2500", "account_number": "2500",
"account_type": "Income Account" "account_type": "Income Account"
}, },
"Sonstige Erträge": { "Sonstige Ertr\u00e4ge": {
"account_number": "2700", "account_number": "2700",
"account_type": "Income Account" "account_type": "Income Account"
} }
@@ -288,18 +272,6 @@
"Aufwendungen 2/4": { "Aufwendungen 2/4": {
"is_group": 1, "is_group": 1,
"root_type": "Expense", "root_type": "Expense",
"Fremdleistungen": {
"account_number": "3100",
"account_type": "Expense Account"
},
"Fremdleistungen ohne Vorsteuer": {
"account_number": "3109",
"account_type": "Expense Account"
},
"Bauleistungen eines im Inland ansässigen Unternehmers 19 % Vorsteuer und 19 % Umsatzsteuer": {
"account_number": "3120",
"account_type": "Expense Account"
},
"Wareneingang": { "Wareneingang": {
"account_number": "3200" "account_number": "3200"
}, },
@@ -374,7 +346,7 @@
}, },
"Personalkosten": { "Personalkosten": {
"is_group": 1, "is_group": 1,
"Gehälter": { "Geh\u00e4lter": {
"account_number": "4120", "account_number": "4120",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
@@ -382,15 +354,15 @@
"account_number": "4130", "account_number": "4130",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Aufwendungen für Altersvorsorge": { "Aufwendungen f\u00fcr Altersvorsorge": {
"account_number": "4165", "account_number": "4165",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Vermögenswirksame Leistungen": { "Verm\u00f6genswirksame Leistungen": {
"account_number": "4170", "account_number": "4170",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Aushilfslöhne": { "Aushilfsl\u00f6hne": {
"account_number": "4190", "account_number": "4190",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
@@ -412,18 +384,18 @@
}, },
"Reparatur/Instandhaltung": { "Reparatur/Instandhaltung": {
"is_group": 1, "is_group": 1,
"Reparaturen und Instandhaltungen von anderen Anlagen und Betriebs- und Geschäftsausstattung": { "Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": {
"account_number": "4805", "account_number": "4805",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Versicherungsbeiträge": { "Versicherungsbeitr\u00e4ge": {
"is_group": 1, "is_group": 1,
"Versicherungen": { "Versicherungen": {
"account_number": "4360", "account_number": "4360",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Beiträge": { "Beitr\u00e4ge": {
"account_number": "4380", "account_number": "4380",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
@@ -431,7 +403,7 @@
"account_number": "4390", "account_number": "4390",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"steuerlich abzugsfähige Verspätungszuschläge und Zwangsgelder": { "steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": {
"account_number": "4396", "account_number": "4396",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
@@ -446,7 +418,7 @@
"account_number": "4653", "account_number": "4653",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"nicht abzugsfähige Betriebsausg. aus Werbe-, Repräs.- u. Reisekosten": { "nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": {
"account_number": "4665", "account_number": "4665",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
@@ -473,11 +445,11 @@
"account_number": "4922", "account_number": "4922",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Bürobedarf": { "B\u00fcrobedarf": {
"account_number": "4930", "account_number": "4930",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Zeitschriften, Bücher": { "Zeitschriften, B\u00fccher": {
"account_number": "4940", "account_number": "4940",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
@@ -485,11 +457,11 @@
"account_number": "4945", "account_number": "4945",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Buchführungskosten": { "Buchf\u00fchrungskosten": {
"account_number": "4955", "account_number": "4955",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Abschluß- u. Prüfungskosten": { "Abschlu\u00df- u. Pr\u00fcfungskosten": {
"account_number": "4957", "account_number": "4957",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
@@ -497,18 +469,18 @@
"account_number": "4970", "account_number": "4970",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Werkzeuge und Kleingeräte": { "Werkzeuge und Kleinger\u00e4te": {
"account_number": "4985", "account_number": "4985",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
}, },
"Zinsaufwendungen": { "Zinsaufwendungen": {
"is_group": 1, "is_group": 1,
"Zinsaufwendungen für kurzfristige Verbindlichkeiten": { "Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": {
"account_number": "2110", "account_number": "2110",
"account_type": "Expense Account" "account_type": "Expense Account"
}, },
"Zinsaufwendungen für KFZ Finanzierung": { "Zinsaufwendungen f\u00fcr KFZ Finanzierung": {
"account_number": "2121", "account_number": "2121",
"account_type": "Expense Account" "account_type": "Expense Account"
} }
@@ -522,10 +494,10 @@
"Saldenvortrag Sachkonten": { "Saldenvortrag Sachkonten": {
"account_number": "9000" "account_number": "9000"
}, },
"Saldenvorträge Debitoren": { "Saldenvortr\u00e4ge Debitoren": {
"account_number": "9008" "account_number": "9008"
}, },
"Saldenvorträge Kreditoren": { "Saldenvortr\u00e4ge Kreditoren": {
"account_number": "9009" "account_number": "9009"
} }
} }
@@ -541,13 +513,13 @@
"Privatsteuern": { "Privatsteuern": {
"account_number": "1810" "account_number": "1810"
}, },
"Sonderausgaben beschränkt abzugsfähig": { "Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": {
"account_number": "1820" "account_number": "1820"
}, },
"Sonderausgaben unbeschränkt abzugsfähig": { "Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": {
"account_number": "1830" "account_number": "1830"
}, },
"Außergewöhnliche Belastungen": { "Au\u00dfergew\u00f6hnliche Belastungen": {
"account_number": "1850" "account_number": "1850"
}, },
"Privateinlagen": { "Privateinlagen": {

View File

@@ -2,6 +2,10 @@
"country_code": "nl", "country_code": "nl",
"name": "Netherlands - Grootboekschema", "name": "Netherlands - Grootboekschema",
"tree": { "tree": {
"FABRIKAGEREKENINGEN": {
"is_group": 1,
"root_type": "Expense"
},
"FINANCIELE REKENINGEN, KORTLOPENDE VORDERINGEN EN SCHULDEN": { "FINANCIELE REKENINGEN, KORTLOPENDE VORDERINGEN EN SCHULDEN": {
"Bank": { "Bank": {
"RABO Bank": { "RABO Bank": {
@@ -9,144 +13,6 @@
}, },
"account_type": "Bank" "account_type": "Bank"
}, },
"LIQUIDE MIDDELEN": {
"ABN-AMRO bank": {},
"Bankbetaalkaarten": {},
"Effecten": {},
"Girobetaalkaarten": {},
"Kas": {
"account_type": "Cash"
},
"Kas valuta": {
"account_type": "Cash"
},
"Kleine kas": {
"account_type": "Cash"
},
"Kruisposten": {},
"Postbank": {
"account_type": "Cash"
},
"account_type": "Cash"
},
"TUSSENREKENINGEN": {
"Betaalwijze cadeaubonnen": {
"account_type": "Cash"
},
"Betaalwijze chipknip": {
"account_type": "Cash"
},
"Betaalwijze contant": {
"account_type": "Cash"
},
"Betaalwijze pin": {
"account_type": "Cash"
},
"Inkopen Nederland hoog": {
"account_type": "Cash"
},
"Inkopen Nederland laag": {
"account_type": "Cash"
},
"Inkopen Nederland onbelast": {
"account_type": "Cash"
},
"Inkopen Nederland overig": {
"account_type": "Cash"
},
"Inkopen Nederland verlegd": {
"account_type": "Cash"
},
"Inkopen binnen EU hoog": {
"account_type": "Cash"
},
"Inkopen binnen EU laag": {
"account_type": "Cash"
},
"Inkopen binnen EU overig": {
"account_type": "Cash"
},
"Inkopen buiten EU hoog": {
"account_type": "Cash"
},
"Inkopen buiten EU laag": {
"account_type": "Cash"
},
"Inkopen buiten EU overig": {
"account_type": "Cash"
},
"Kassa 1": {
"account_type": "Cash"
},
"Kassa 2": {
"account_type": "Cash"
},
"Netto lonen": {
"account_type": "Cash"
},
"Tegenrekening Inkopen": {
"account_type": "Cash"
},
"Tussenrek. autom. betalingen": {
"account_type": "Cash"
},
"Tussenrek. autom. loonbetalingen": {
"account_type": "Cash"
},
"Tussenrek. cadeaubonbetalingen": {
"account_type": "Cash"
},
"Tussenrekening balans": {
"account_type": "Cash"
},
"Tussenrekening chipknip": {
"account_type": "Cash"
},
"Tussenrekening correcties": {
"account_type": "Cash"
},
"Tussenrekening pin": {
"account_type": "Cash"
},
"Vraagposten": {
"account_type": "Cash"
},
"VOORRAAD GRONDSTOFFEN, HULPMATERIALEN EN HANDELSGOEDEREN": {
"Emballage": {},
"Gereed product 1": {},
"Gereed product 2": {},
"Goederen 1": {},
"Goederen 2": {},
"Goederen in consignatie": {},
"Goederen onderweg": {},
"Grondstoffen 1": {},
"Grondstoffen 2": {},
"Halffabrikaten 1": {},
"Halffabrikaten 2": {},
"Hulpstoffen 1": {},
"Hulpstoffen 2": {},
"Kantoorbenodigdheden": {},
"Onderhanden werk": {},
"Verpakkingsmateriaal": {},
"Zegels": {},
"root_type": "Asset"
},
"root_type": "Asset"
},
"VORDERINGEN": {
"Debiteuren": {
"account_type": "Receivable"
},
"Dubieuze debiteuren": {},
"Overige vorderingen": {},
"Rekening-courant directie 1": {},
"Te ontvangen ziekengeld": {},
"Voorschotten personeel": {},
"Vooruitbetaalde kosten": {},
"Voorziening dubieuze debiteuren": {}
},
"root_type": "Asset"
},
"KORTLOPENDE SCHULDEN": { "KORTLOPENDE SCHULDEN": {
"Af te dragen Btw-verlegd": { "Af te dragen Btw-verlegd": {
"account_type": "Tax" "account_type": "Tax"
@@ -203,13 +69,42 @@
"Vakantiegeld 1": {}, "Vakantiegeld 1": {},
"Vakantiezegels": {}, "Vakantiezegels": {},
"Vennootschapsbelasting": {}, "Vennootschapsbelasting": {},
"Vooruit ontvangen bedr.": {}, "Vooruit ontvangen bedr.": {}
"is_group": 1, },
"root_type": "Liability" "LIQUIDE MIDDELEN": {
"ABN-AMRO bank": {},
"Bankbetaalkaarten": {},
"Effecten": {},
"Girobetaalkaarten": {},
"Kas": {
"account_type": "Cash"
},
"Kas valuta": {
"account_type": "Cash"
},
"Kleine kas": {
"account_type": "Cash"
},
"Kruisposten": {},
"Postbank": {
"account_type": "Cash"
},
"account_type": "Cash"
},
"VORDERINGEN": {
"Debiteuren": {
"account_type": "Receivable"
},
"Dubieuze debiteuren": {},
"Overige vorderingen": {},
"Rekening-courant directie 1": {},
"Te ontvangen ziekengeld": {},
"Voorschotten personeel": {},
"Vooruitbetaalde kosten": {},
"Voorziening dubieuze debiteuren": {}
},
"root_type": "Asset"
}, },
"FABRIKAGEREKENINGEN": {
"is_group": 1,
"root_type": "Expense",
"INDIRECTE KOSTEN": { "INDIRECTE KOSTEN": {
"is_group": 1, "is_group": 1,
"root_type": "Expense" "root_type": "Expense"
@@ -396,49 +291,91 @@
"Priv\u00e9-gebruik auto's": {}, "Priv\u00e9-gebruik auto's": {},
"Wegenbelasting": {} "Wegenbelasting": {}
}, },
"VOORRAAD GEREED PRODUCT EN ONDERHANDEN WERK": {
"Betalingskort. crediteuren": {},
"Garantiekosten": {},
"Hulpmaterialen": {},
"Inkomende vrachten": {
"account_type": "Expenses Included In Valuation"
},
"Inkoop import buiten EU hoog": {},
"Inkoop import buiten EU laag": {},
"Inkoop import buiten EU overig": {},
"Inkoopbonussen": {},
"Inkoopkosten": {},
"Inkoopprovisie": {},
"Inkopen BTW verlegd": {},
"Inkopen EU hoog tarief": {},
"Inkopen EU laag tarief": {},
"Inkopen EU overig": {},
"Inkopen hoog": {},
"Inkopen laag": {},
"Inkopen nul": {},
"Inkopen overig": {},
"Invoerkosten": {},
"Kosten inkoopvereniging": {},
"Kostprijs omzet grondstoffen": {
"account_type": "Cost of Goods Sold"
},
"Kostprijs omzet handelsgoederen": {},
"Onttrekking uitgev.garantie": {},
"Priv\u00e9-gebruik goederen": {},
"Stock aanpassing": {
"account_type": "Stock Adjustment"
},
"Tegenrekening inkoop": {},
"Toev. Voorz. incour. grondst.": {},
"Toevoeging garantieverpl.": {},
"Toevoeging voorz. incour. handelsgoed.": {},
"Uitbesteed werk": {},
"Voorz. Incourourant grondst.": {},
"Voorz.incour. handelsgoed.": {},
"root_type": "Expense" "root_type": "Expense"
}, },
"root_type": "Expense" "TUSSENREKENINGEN": {
} "Betaalwijze cadeaubonnen": {
"account_type": "Cash"
},
"Betaalwijze chipknip": {
"account_type": "Cash"
},
"Betaalwijze contant": {
"account_type": "Cash"
},
"Betaalwijze pin": {
"account_type": "Cash"
},
"Inkopen Nederland hoog": {
"account_type": "Cash"
},
"Inkopen Nederland laag": {
"account_type": "Cash"
},
"Inkopen Nederland onbelast": {
"account_type": "Cash"
},
"Inkopen Nederland overig": {
"account_type": "Cash"
},
"Inkopen Nederland verlegd": {
"account_type": "Cash"
},
"Inkopen binnen EU hoog": {
"account_type": "Cash"
},
"Inkopen binnen EU laag": {
"account_type": "Cash"
},
"Inkopen binnen EU overig": {
"account_type": "Cash"
},
"Inkopen buiten EU hoog": {
"account_type": "Cash"
},
"Inkopen buiten EU laag": {
"account_type": "Cash"
},
"Inkopen buiten EU overig": {
"account_type": "Cash"
},
"Kassa 1": {
"account_type": "Cash"
},
"Kassa 2": {
"account_type": "Cash"
},
"Netto lonen": {
"account_type": "Cash"
},
"Tegenrekening Inkopen": {
"account_type": "Cash"
},
"Tussenrek. autom. betalingen": {
"account_type": "Cash"
},
"Tussenrek. autom. loonbetalingen": {
"account_type": "Cash"
},
"Tussenrek. cadeaubonbetalingen": {
"account_type": "Cash"
},
"Tussenrekening balans": {
"account_type": "Cash"
},
"Tussenrekening chipknip": {
"account_type": "Cash"
},
"Tussenrekening correcties": {
"account_type": "Cash"
},
"Tussenrekening pin": {
"account_type": "Cash"
},
"Vraagposten": {
"account_type": "Cash"
},
"root_type": "Asset"
}, },
"VASTE ACTIVA, EIGEN VERMOGEN, LANGLOPEND VREEMD VERMOGEN EN VOORZIENINGEN": { "VASTE ACTIVA, EIGEN VERMOGEN, LANGLOPEND VREEMD VERMOGEN EN VOORZIENINGEN": {
"EIGEN VERMOGEN": { "EIGEN VERMOGEN": {
@@ -665,7 +602,7 @@
"account_type": "Equity" "account_type": "Equity"
} }
}, },
"root_type": "Equity" "root_type": "Asset"
}, },
"VERKOOPRESULTATEN": { "VERKOOPRESULTATEN": {
"Diensten fabric. 0% niet-EU": {}, "Diensten fabric. 0% niet-EU": {},
@@ -690,6 +627,67 @@
"Verleende Kredietbep. fabricage": {}, "Verleende Kredietbep. fabricage": {},
"Verleende Kredietbep. handel": {}, "Verleende Kredietbep. handel": {},
"root_type": "Income" "root_type": "Income"
},
"VOORRAAD GEREED PRODUCT EN ONDERHANDEN WERK": {
"Betalingskort. crediteuren": {},
"Garantiekosten": {},
"Hulpmaterialen": {},
"Inkomende vrachten": {
"account_type": "Expenses Included In Valuation"
},
"Inkoop import buiten EU hoog": {},
"Inkoop import buiten EU laag": {},
"Inkoop import buiten EU overig": {},
"Inkoopbonussen": {},
"Inkoopkosten": {},
"Inkoopprovisie": {},
"Inkopen BTW verlegd": {},
"Inkopen EU hoog tarief": {},
"Inkopen EU laag tarief": {},
"Inkopen EU overig": {},
"Inkopen hoog": {},
"Inkopen laag": {},
"Inkopen nul": {},
"Inkopen overig": {},
"Invoerkosten": {},
"Kosten inkoopvereniging": {},
"Kostprijs omzet grondstoffen": {
"account_type": "Cost of Goods Sold"
},
"Kostprijs omzet handelsgoederen": {},
"Onttrekking uitgev.garantie": {},
"Priv\u00e9-gebruik goederen": {},
"Stock aanpassing": {
"account_type": "Stock Adjustment"
},
"Tegenrekening inkoop": {},
"Toev. Voorz. incour. grondst.": {},
"Toevoeging garantieverpl.": {},
"Toevoeging voorz. incour. handelsgoed.": {},
"Uitbesteed werk": {},
"Voorz. Incourourant grondst.": {},
"Voorz.incour. handelsgoed.": {},
"root_type": "Expense"
},
"VOORRAAD GRONDSTOFFEN, HULPMATERIALEN EN HANDELSGOEDEREN": {
"Emballage": {},
"Gereed product 1": {},
"Gereed product 2": {},
"Goederen 1": {},
"Goederen 2": {},
"Goederen in consignatie": {},
"Goederen onderweg": {},
"Grondstoffen 1": {},
"Grondstoffen 2": {},
"Halffabrikaten 1": {},
"Halffabrikaten 2": {},
"Hulpstoffen 1": {},
"Hulpstoffen 2": {},
"Kantoorbenodigdheden": {},
"Onderhanden werk": {},
"Verpakkingsmateriaal": {},
"Zegels": {},
"root_type": "Asset"
} }
} }
} }

View File

@@ -5,13 +5,10 @@
import unittest import unittest
import frappe import frappe
from frappe.test_runner import make_test_records
from erpnext.accounts.doctype.account.account import merge_account, update_account_number from erpnext.accounts.doctype.account.account import merge_account, update_account_number
from erpnext.stock import get_company_default_inventory_account, get_warehouse_account from erpnext.stock import get_company_default_inventory_account, get_warehouse_account
test_dependencies = ["Company"]
class TestAccount(unittest.TestCase): class TestAccount(unittest.TestCase):
def test_rename_account(self): def test_rename_account(self):
@@ -191,58 +188,6 @@ class TestAccount(unittest.TestCase):
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4") frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5") frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5")
def test_account_currency_sync(self):
"""
In a parent->child company setup, child should inherit parent account currency if explicitly specified.
"""
make_test_records("Company")
frappe.local.flags.pop("ignore_root_company_validation", None)
def create_bank_account():
acc = frappe.new_doc("Account")
acc.account_name = "_Test Bank JPY"
acc.parent_account = "Temporary Accounts - _TC6"
acc.company = "_Test Company 6"
return acc
acc = create_bank_account()
# Explicitly set currency
acc.account_currency = "JPY"
acc.insert()
self.assertTrue(
frappe.db.exists(
{
"doctype": "Account",
"account_name": "_Test Bank JPY",
"account_currency": "JPY",
"company": "_Test Company 7",
}
)
)
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
acc = create_bank_account()
# default currency is used
acc.insert()
self.assertTrue(
frappe.db.exists(
{
"doctype": "Account",
"account_name": "_Test Bank JPY",
"account_currency": "USD",
"company": "_Test Company 7",
}
)
)
frappe.delete_doc("Account", "_Test Bank JPY - _TC6")
frappe.delete_doc("Account", "_Test Bank JPY - _TC7")
def test_child_company_account_rename_sync(self): def test_child_company_account_rename_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None) frappe.local.flags.pop("ignore_root_company_validation", None)
@@ -352,7 +297,7 @@ def _make_test_records(verbose=None):
# fixed asset depreciation # fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None], ["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None], ["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, "Depreciation", None], ["_Test Depreciations", "Expenses", 0, None, None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None], ["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
# Receivable / Payable Account # Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None], ["_Test Receivable", "Current Assets", 0, "Receivable", None],

View File

@@ -1,8 +0,0 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Account Closing Balance", {
// refresh(frm) {
// },
// });

View File

@@ -1,164 +0,0 @@
{
"actions": [],
"creation": "2023-02-21 15:20:59.586811",
"default_view": "List",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
"closing_date",
"account",
"cost_center",
"debit",
"credit",
"account_currency",
"debit_in_account_currency",
"credit_in_account_currency",
"project",
"company",
"finance_book",
"period_closing_voucher",
"is_period_closing_voucher_entry"
],
"fields": [
{
"fieldname": "closing_date",
"fieldtype": "Date",
"in_filter": 1,
"in_list_view": 1,
"label": "Closing Date",
"oldfieldname": "posting_date",
"oldfieldtype": "Date",
"search_index": 1
},
{
"fieldname": "account",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Account",
"oldfieldname": "account",
"oldfieldtype": "Link",
"options": "Account",
"search_index": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"label": "Cost Center",
"oldfieldname": "cost_center",
"oldfieldtype": "Link",
"options": "Cost Center"
},
{
"fieldname": "debit",
"fieldtype": "Currency",
"label": "Debit Amount",
"oldfieldname": "debit",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency"
},
{
"fieldname": "credit",
"fieldtype": "Currency",
"label": "Credit Amount",
"oldfieldname": "credit",
"oldfieldtype": "Currency",
"options": "Company:company:default_currency"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"options": "Currency"
},
{
"fieldname": "debit_in_account_currency",
"fieldtype": "Currency",
"label": "Debit Amount in Account Currency",
"options": "account_currency"
},
{
"fieldname": "credit_in_account_currency",
"fieldtype": "Currency",
"label": "Credit Amount in Account Currency",
"options": "account_currency"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
},
{
"fieldname": "company",
"fieldtype": "Link",
"in_filter": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Company",
"oldfieldname": "company",
"oldfieldtype": "Link",
"options": "Company",
"search_index": 1
},
{
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book"
},
{
"fieldname": "period_closing_voucher",
"fieldtype": "Link",
"in_standard_filter": 1,
"label": "Period Closing Voucher",
"options": "Period Closing Voucher",
"search_index": 1
},
{
"default": "0",
"fieldname": "is_period_closing_voucher_entry",
"fieldtype": "Check",
"label": "Is Period Closing Voucher Entry"
}
],
"icon": "fa fa-list",
"in_create": 1,
"links": [],
"modified": "2023-03-06 08:56:36.393237",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Account Closing Balance",
"owner": "Administrator",
"permissions": [
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User"
},
{
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager"
},
{
"export": 1,
"read": 1,
"report": 1,
"role": "Auditor"
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@@ -1,126 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe.model.document import Document
from frappe.utils import cint, cstr
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
class AccountClosingBalance(Document):
pass
def make_closing_entries(closing_entries, voucher_name, company, closing_date):
accounting_dimensions = get_accounting_dimensions()
previous_closing_entries = get_previous_closing_entries(
company, closing_date, accounting_dimensions
)
combined_entries = closing_entries + previous_closing_entries
merged_entries = aggregate_with_last_account_closing_balance(
combined_entries, accounting_dimensions
)
for key, value in merged_entries.items():
cle = frappe.new_doc("Account Closing Balance")
cle.update(value)
cle.update(value["dimensions"])
cle.update(
{
"period_closing_voucher": voucher_name,
"closing_date": closing_date,
}
)
cle.flags.ignore_permissions = True
cle.submit()
def aggregate_with_last_account_closing_balance(entries, accounting_dimensions):
merged_entries = {}
for entry in entries:
key, key_values = generate_key(entry, accounting_dimensions)
merged_entries.setdefault(
key,
{
"debit": 0,
"credit": 0,
"debit_in_account_currency": 0,
"credit_in_account_currency": 0,
},
)
merged_entries[key]["dimensions"] = key_values
merged_entries[key]["debit"] += entry.get("debit")
merged_entries[key]["credit"] += entry.get("credit")
merged_entries[key]["debit_in_account_currency"] += entry.get("debit_in_account_currency")
merged_entries[key]["credit_in_account_currency"] += entry.get("credit_in_account_currency")
return merged_entries
def generate_key(entry, accounting_dimensions):
key = [
cstr(entry.get("account")),
cstr(entry.get("account_currency")),
cstr(entry.get("cost_center")),
cstr(entry.get("project")),
cstr(entry.get("finance_book")),
cint(entry.get("is_period_closing_voucher_entry")),
]
key_values = {
"company": cstr(entry.get("company")),
"account": cstr(entry.get("account")),
"account_currency": cstr(entry.get("account_currency")),
"cost_center": cstr(entry.get("cost_center")),
"project": cstr(entry.get("project")),
"finance_book": cstr(entry.get("finance_book")),
"is_period_closing_voucher_entry": cint(entry.get("is_period_closing_voucher_entry")),
}
for dimension in accounting_dimensions:
key.append(cstr(entry.get(dimension)))
key_values[dimension] = cstr(entry.get(dimension))
return tuple(key), key_values
def get_previous_closing_entries(company, closing_date, accounting_dimensions):
entries = []
last_period_closing_voucher = frappe.db.get_all(
"Period Closing Voucher",
filters={"docstatus": 1, "company": company, "posting_date": ("<", closing_date)},
fields=["name"],
order_by="posting_date desc",
limit=1,
)
if last_period_closing_voucher:
account_closing_balance = frappe.qb.DocType("Account Closing Balance")
query = frappe.qb.from_(account_closing_balance).select(
account_closing_balance.company,
account_closing_balance.account,
account_closing_balance.account_currency,
account_closing_balance.debit,
account_closing_balance.credit,
account_closing_balance.debit_in_account_currency,
account_closing_balance.credit_in_account_currency,
account_closing_balance.cost_center,
account_closing_balance.project,
account_closing_balance.finance_book,
account_closing_balance.is_period_closing_voucher_entry,
)
for dimension in accounting_dimensions:
query = query.select(account_closing_balance[dimension])
query = query.where(
account_closing_balance.period_closing_voucher == last_period_closing_voucher[0].name
)
entries = query.run(as_dict=1)
return entries

View File

@@ -1,9 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestAccountClosingBalance(FrappeTestCase):
pass

View File

@@ -15,17 +15,6 @@ frappe.ui.form.on('Accounting Dimension', {
}; };
}); });
frm.set_query("offsetting_account", "dimension_defaults", function(doc, cdt, cdn) {
let d = locals[cdt][cdn];
return {
filters: {
company: d.company,
root_type: ["in", ["Asset", "Liability"]],
is_group: 0
}
}
});
if (!frm.is_new()) { if (!frm.is_new()) {
frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () { frm.add_custom_button(__('Show {0}', [frm.doc.document_type]), function () {
frappe.set_route("List", frm.doc.document_type); frappe.set_route("List", frm.doc.document_type);

View File

@@ -39,8 +39,6 @@ class AccountingDimension(Document):
if not self.is_new(): if not self.is_new():
self.validate_document_type_change() self.validate_document_type_change()
self.validate_dimension_defaults()
def validate_document_type_change(self): def validate_document_type_change(self):
doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type") doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
if doctype_before_save != self.document_type: if doctype_before_save != self.document_type:
@@ -48,27 +46,17 @@ class AccountingDimension(Document):
message += _("Please create a new Accounting Dimension if required.") message += _("Please create a new Accounting Dimension if required.")
frappe.throw(message) frappe.throw(message)
def validate_dimension_defaults(self):
companies = []
for default in self.get("dimension_defaults"):
if default.company not in companies:
companies.append(default.company)
else:
frappe.throw(_("Company {0} is added more than once").format(frappe.bold(default.company)))
def after_insert(self): def after_insert(self):
if frappe.flags.in_test: if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self) make_dimension_in_accounting_doctypes(doc=self)
else: else:
frappe.enqueue( frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
make_dimension_in_accounting_doctypes, doc=self, queue="long", enqueue_after_commit=True
)
def on_trash(self): def on_trash(self):
if frappe.flags.in_test: if frappe.flags.in_test:
delete_accounting_dimension(doc=self) delete_accounting_dimension(doc=self)
else: else:
frappe.enqueue(delete_accounting_dimension, doc=self, queue="long", enqueue_after_commit=True) frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
def set_fieldname_and_label(self): def set_fieldname_and_label(self):
if not self.label: if not self.label:
@@ -281,12 +269,6 @@ def get_dimensions(with_cost_center_and_project=False):
as_dict=1, as_dict=1,
) )
if isinstance(with_cost_center_and_project, str):
if with_cost_center_and_project.lower().strip() == "true":
with_cost_center_and_project = True
else:
with_cost_center_and_project = False
if with_cost_center_and_project: if with_cost_center_and_project:
dimension_filters.extend( dimension_filters.extend(
[ [

View File

@@ -8,10 +8,7 @@
"reference_document", "reference_document",
"default_dimension", "default_dimension",
"mandatory_for_bs", "mandatory_for_bs",
"mandatory_for_pl", "mandatory_for_pl"
"column_break_lqns",
"automatically_post_balancing_accounting_entry",
"offsetting_account"
], ],
"fields": [ "fields": [
{ {
@@ -53,23 +50,6 @@
"fieldtype": "Check", "fieldtype": "Check",
"in_list_view": 1, "in_list_view": 1,
"label": "Mandatory For Profit and Loss Account" "label": "Mandatory For Profit and Loss Account"
},
{
"default": "0",
"fieldname": "automatically_post_balancing_accounting_entry",
"fieldtype": "Check",
"label": "Automatically post balancing accounting entry"
},
{
"fieldname": "offsetting_account",
"fieldtype": "Link",
"label": "Offsetting Account",
"mandatory_depends_on": "eval: doc.automatically_post_balancing_accounting_entry",
"options": "Account"
},
{
"fieldname": "column_break_lqns",
"fieldtype": "Column Break"
} }
], ],
"istable": 1, "istable": 1,

View File

@@ -68,16 +68,6 @@ frappe.ui.form.on('Accounting Dimension Filter', {
frm.refresh_field("dimensions"); frm.refresh_field("dimensions");
frm.trigger('setup_filters'); frm.trigger('setup_filters');
}, },
apply_restriction_on_values: function(frm) {
/** If restriction on values is not applied, we should set "allow_or_restrict" to "Restrict" with an empty allowed dimension table.
* Hence it's not "restricted" on any value.
*/
if (!frm.doc.apply_restriction_on_values) {
frm.set_value("allow_or_restrict", "Restrict");
frm.clear_table("dimensions");
frm.refresh_field("dimensions");
}
}
}); });
frappe.ui.form.on('Allowed Dimension', { frappe.ui.form.on('Allowed Dimension', {

View File

@@ -10,7 +10,6 @@
"disabled", "disabled",
"column_break_2", "column_break_2",
"company", "company",
"apply_restriction_on_values",
"allow_or_restrict", "allow_or_restrict",
"section_break_4", "section_break_4",
"accounts", "accounts",
@@ -25,80 +24,94 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_list_view": 1, "in_list_view": 1,
"label": "Accounting Dimension", "label": "Accounting Dimension",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_2", "fieldname": "column_break_2",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_4", "fieldname": "section_break_4",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"hide_border": 1 "hide_border": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "column_break_6", "fieldname": "column_break_6",
"fieldtype": "Column Break" "fieldtype": "Column Break",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.apply_restriction_on_values == 1;",
"fieldname": "allow_or_restrict", "fieldname": "allow_or_restrict",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Allow Or Restrict Dimension", "label": "Allow Or Restrict Dimension",
"options": "Allow\nRestrict", "options": "Allow\nRestrict",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "accounts", "fieldname": "accounts",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Applicable On Account", "label": "Applicable On Account",
"options": "Applicable On Account", "options": "Applicable On Account",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"depends_on": "eval:doc.accounting_dimension && doc.apply_restriction_on_values", "depends_on": "eval:doc.accounting_dimension",
"fieldname": "dimensions", "fieldname": "dimensions",
"fieldtype": "Table", "fieldtype": "Table",
"label": "Applicable Dimension", "label": "Applicable Dimension",
"mandatory_depends_on": "eval:doc.apply_restriction_on_values == 1;", "options": "Allowed Dimension",
"options": "Allowed Dimension" "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "disabled", "fieldname": "disabled",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disabled" "label": "Disabled",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "company", "fieldname": "company",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Company", "label": "Company",
"options": "Company", "options": "Company",
"reqd": 1 "reqd": 1,
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "dimension_filter_help", "fieldname": "dimension_filter_help",
"fieldtype": "HTML", "fieldtype": "HTML",
"label": "Dimension Filter Help" "label": "Dimension Filter Help",
"show_days": 1,
"show_seconds": 1
}, },
{ {
"fieldname": "section_break_10", "fieldname": "section_break_10",
"fieldtype": "Section Break" "fieldtype": "Section Break",
}, "show_days": 1,
{ "show_seconds": 1
"default": "1",
"fieldname": "apply_restriction_on_values",
"fieldtype": "Check",
"label": "Apply restriction on dimension values"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"links": [], "links": [],
"modified": "2023-06-07 14:59:41.869117", "modified": "2021-02-03 12:04:58.678402",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounting Dimension Filter", "name": "Accounting Dimension Filter",
"naming_rule": "Expression",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -141,6 +154,5 @@
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [],
"track_changes": 1 "track_changes": 1
} }

View File

@@ -8,12 +8,6 @@ from frappe.model.document import Document
class AccountingDimensionFilter(Document): class AccountingDimensionFilter(Document):
def before_save(self):
# If restriction is not applied on values, then remove all the dimensions and set allow_or_restrict to Restrict
if not self.apply_restriction_on_values:
self.allow_or_restrict = "Restrict"
self.set("dimensions", [])
def validate(self): def validate(self):
self.validate_applicable_accounts() self.validate_applicable_accounts()
@@ -50,12 +44,12 @@ def get_dimension_filter_map():
a.applicable_on_account, d.dimension_value, p.accounting_dimension, a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory p.allow_or_restrict, a.is_mandatory
FROM FROM
`tabApplicable On Account` a, `tabApplicable On Account` a, `tabAllowed Dimension` d,
`tabAccounting Dimension Filter` p `tabAccounting Dimension Filter` p
LEFT JOIN `tabAllowed Dimension` d ON d.parent = p.name
WHERE WHERE
p.name = a.parent p.name = a.parent
AND p.disabled = 0 AND p.disabled = 0
AND p.name = d.parent
""", """,
as_dict=1, as_dict=1,
) )
@@ -82,5 +76,4 @@ def build_map(map_object, dimension, account, filter_value, allow_or_restrict, i
(dimension, account), (dimension, account),
{"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict}, {"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict},
) )
if filter_value:
map_object[(dimension, account)]["allowed_dimensions"].append(filter_value) map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)

View File

@@ -64,7 +64,6 @@ def create_accounting_dimension_filter():
"accounting_dimension": "Cost Center", "accounting_dimension": "Cost Center",
"allow_or_restrict": "Allow", "allow_or_restrict": "Allow",
"company": "_Test Company", "company": "_Test Company",
"apply_restriction_on_values": 1,
"accounts": [ "accounts": [
{ {
"applicable_on_account": "Sales - _TC", "applicable_on_account": "Sales - _TC",
@@ -86,7 +85,6 @@ def create_accounting_dimension_filter():
"doctype": "Accounting Dimension Filter", "doctype": "Accounting Dimension Filter",
"accounting_dimension": "Department", "accounting_dimension": "Department",
"allow_or_restrict": "Allow", "allow_or_restrict": "Allow",
"apply_restriction_on_values": 1,
"company": "_Test Company", "company": "_Test Company",
"accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}], "accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}],
"dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}], "dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}],

View File

@@ -20,11 +20,5 @@ frappe.ui.form.on('Accounting Period', {
} }
}); });
} }
frm.set_query("document_type", "closed_documents", () => {
return {
query: "erpnext.controllers.queries.get_doctypes_for_closing",
}
});
} }
}); });

View File

@@ -11,10 +11,6 @@ class OverlapError(frappe.ValidationError):
pass pass
class ClosedAccountingPeriod(frappe.ValidationError):
pass
class AccountingPeriod(Document): class AccountingPeriod(Document):
def validate(self): def validate(self):
self.validate_overlap() self.validate_overlap()
@@ -69,42 +65,3 @@ class AccountingPeriod(Document):
"closed_documents", "closed_documents",
{"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed}, {"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
) )
def validate_accounting_period_on_doc_save(doc, method=None):
if doc.doctype == "Bank Clearance":
return
elif doc.doctype == "Asset":
if doc.is_existing_asset:
return
else:
date = doc.available_for_use_date
elif doc.doctype == "Asset Repair":
date = doc.completion_date
else:
date = doc.posting_date
ap = frappe.qb.DocType("Accounting Period")
cd = frappe.qb.DocType("Closed Document")
accounting_period = (
frappe.qb.from_(ap)
.from_(cd)
.select(ap.name)
.where(
(ap.name == cd.parent)
& (ap.company == doc.company)
& (cd.closed == 1)
& (cd.document_type == doc.doctype)
& (date >= ap.start_date)
& (date <= ap.end_date)
)
).run(as_dict=1)
if accounting_period:
frappe.throw(
_("You cannot create a {0} within the closed Accounting Period {1}").format(
doc.doctype, frappe.bold(accounting_period[0]["name"])
),
ClosedAccountingPeriod,
)

View File

@@ -6,11 +6,9 @@ import unittest
import frappe import frappe
from frappe.utils import add_months, nowdate from frappe.utils import add_months, nowdate
from erpnext.accounts.doctype.accounting_period.accounting_period import ( from erpnext.accounts.doctype.accounting_period.accounting_period import OverlapError
ClosedAccountingPeriod,
OverlapError,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.general_ledger import ClosedAccountingPeriod
test_dependencies = ["Item"] test_dependencies = ["Item"]
@@ -35,9 +33,9 @@ class TestAccountingPeriod(unittest.TestCase):
ap1.save() ap1.save()
doc = create_sales_invoice( doc = create_sales_invoice(
do_not_save=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC" do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
) )
self.assertRaises(ClosedAccountingPeriod, doc.save) self.assertRaises(ClosedAccountingPeriod, doc.submit)
def tearDown(self): def tearDown(self):
for d in frappe.get_all("Accounting Period"): for d in frappe.get_all("Accounting Period"):

View File

@@ -19,8 +19,8 @@
"column_break_17", "column_break_17",
"enable_common_party_accounting", "enable_common_party_accounting",
"allow_multi_currency_invoices_against_single_party_account", "allow_multi_currency_invoices_against_single_party_account",
"journals_section", "report_setting_section",
"merge_similar_account_heads", "use_custom_cash_flow",
"deferred_accounting_settings_section", "deferred_accounting_settings_section",
"book_deferred_entries_based_on", "book_deferred_entries_based_on",
"column_break_18", "column_break_18",
@@ -31,16 +31,12 @@
"determine_address_tax_category_from", "determine_address_tax_category_from",
"column_break_19", "column_break_19",
"add_taxes_from_item_tax_template", "add_taxes_from_item_tax_template",
"book_tax_discount_loss",
"print_settings", "print_settings",
"show_inclusive_tax_in_print", "show_inclusive_tax_in_print",
"show_taxes_as_table_in_print",
"column_break_12", "column_break_12",
"show_payment_schedule_in_print", "show_payment_schedule_in_print",
"currency_exchange_section", "currency_exchange_section",
"allow_stale", "allow_stale",
"section_break_jpd0",
"auto_reconcile_payments",
"stale_days", "stale_days",
"invoicing_settings_tab", "invoicing_settings_tab",
"accounts_transactions_settings_section", "accounts_transactions_settings_section",
@@ -58,14 +54,9 @@
"closing_settings_tab", "closing_settings_tab",
"period_closing_settings_section", "period_closing_settings_section",
"acc_frozen_upto", "acc_frozen_upto",
"ignore_account_closing_balance",
"column_break_25", "column_break_25",
"frozen_accounts_modifier", "frozen_accounts_modifier",
"tab_break_dpet", "report_settings_sb"
"show_balance_in_coa",
"banking_tab",
"enable_party_matching",
"enable_fuzzy_matching"
], ],
"fields": [ "fields": [
{ {
@@ -176,9 +167,20 @@
"fieldtype": "Int", "fieldtype": "Int",
"label": "Stale Days" "label": "Stale Days"
}, },
{
"fieldname": "report_settings_sb",
"fieldtype": "Section Break",
"label": "Report Settings"
},
{
"default": "0",
"description": "Only select this if you have set up the Cash Flow Mapper documents",
"fieldname": "use_custom_cash_flow",
"fieldtype": "Check",
"label": "Enable Custom Cash Flow Format"
},
{ {
"default": "0", "default": "0",
"description": "Payment Terms from orders will be fetched into the invoices as is",
"fieldname": "automatically_fetch_payment_terms", "fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Automatically Fetch Payment Terms from Order" "label": "Automatically Fetch Payment Terms from Order"
@@ -334,86 +336,17 @@
"fieldtype": "Tab Break", "fieldtype": "Tab Break",
"label": "POS" "label": "POS"
}, },
{
"fieldname": "report_setting_section",
"fieldtype": "Section Break",
"label": "Report Setting"
},
{ {
"default": "0", "default": "0",
"description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency", "description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
"fieldname": "allow_multi_currency_invoices_against_single_party_account", "fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Allow multi-currency invoices against single party account " "label": "Allow multi-currency invoices against single party account "
},
{
"fieldname": "tab_break_dpet",
"fieldtype": "Tab Break",
"label": "Chart Of Accounts"
},
{
"default": "1",
"fieldname": "show_balance_in_coa",
"fieldtype": "Check",
"label": "Show Balances in Chart Of Accounts"
},
{
"default": "0",
"description": "Split Early Payment Discount Loss into Income and Tax Loss",
"fieldname": "book_tax_discount_loss",
"fieldtype": "Check",
"label": "Book Tax Loss on Early Payment Discount"
},
{
"fieldname": "journals_section",
"fieldtype": "Section Break",
"label": "Journals"
},
{
"default": "0",
"description": "Rows with Same Account heads will be merged on Ledger",
"fieldname": "merge_similar_account_heads",
"fieldtype": "Check",
"label": "Merge Similar Account Heads"
},
{
"fieldname": "section_break_jpd0",
"fieldtype": "Section Break",
"label": "Payment Reconciliations"
},
{
"default": "0",
"fieldname": "auto_reconcile_payments",
"fieldtype": "Check",
"label": "Auto Reconcile Payments"
},
{
"default": "0",
"fieldname": "show_taxes_as_table_in_print",
"fieldtype": "Check",
"label": "Show Taxes as Table in Print"
},
{
"fieldname": "banking_tab",
"fieldtype": "Tab Break",
"label": "Banking"
},
{
"default": "0",
"description": "Auto match and set the Party in Bank Transactions",
"fieldname": "enable_party_matching",
"fieldtype": "Check",
"label": "Enable Automatic Party Matching"
},
{
"default": "0",
"depends_on": "enable_party_matching",
"description": "Approximately match the description/party name against parties",
"fieldname": "enable_fuzzy_matching",
"fieldtype": "Check",
"label": "Enable Fuzzy Matching"
},
{
"default": "0",
"description": "Financial reports will be generated using GL Entry doctypes (should be enabled if Period Closing Voucher is not posted for all years sequentially or missing) ",
"fieldname": "ignore_account_closing_balance",
"fieldtype": "Check",
"label": "Ignore Account Closing Balance"
} }
], ],
"icon": "icon-cog", "icon": "icon-cog",
@@ -421,7 +354,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-07-27 15:05:34.000264", "modified": "2022-11-27 21:49:52.538655",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@@ -14,33 +14,22 @@ from erpnext.stock.utils import check_pending_reposting
class AccountsSettings(Document): class AccountsSettings(Document):
def validate(self): def on_update(self):
old_doc = self.get_doc_before_save() frappe.clear_cache()
clear_cache = False
if old_doc.add_taxes_from_item_tax_template != self.add_taxes_from_item_tax_template: def validate(self):
frappe.db.set_default( frappe.db.set_default(
"add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0) "add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
) )
clear_cache = True
if old_doc.enable_common_party_accounting != self.enable_common_party_accounting:
frappe.db.set_default( frappe.db.set_default(
"enable_common_party_accounting", self.get("enable_common_party_accounting", 0) "enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
) )
clear_cache = True
self.validate_stale_days() self.validate_stale_days()
if old_doc.show_payment_schedule_in_print != self.show_payment_schedule_in_print:
self.enable_payment_schedule_in_print() self.enable_payment_schedule_in_print()
if old_doc.acc_frozen_upto != self.acc_frozen_upto:
self.validate_pending_reposts() self.validate_pending_reposts()
if clear_cache:
frappe.clear_cache()
def validate_stale_days(self): def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0: if not self.allow_stale and cint(self.stale_days) <= 0:
frappe.msgprint( frappe.msgprint(

View File

@@ -8,6 +8,9 @@ frappe.ui.form.on('Bank', {
}, },
refresh: function(frm) { refresh: function(frm) {
add_fields_to_mapping_table(frm); add_fields_to_mapping_table(frm);
frappe.dynamic_link = { doc: frm.doc, fieldname: 'name', doctype: 'Bank' };
frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal); frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
if (frm.doc.__islocal) { if (frm.doc.__islocal) {
@@ -102,7 +105,7 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
} }
onScriptLoaded(me) { onScriptLoaded(me) {
me.linkHandler = Plaid.create({ // eslint-disable-line no-undef me.linkHandler = Plaid.create({
env: me.plaid_env, env: me.plaid_env,
token: me.token, token: me.token,
onSuccess: me.plaid_success onSuccess: me.plaid_success
@@ -115,10 +118,6 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
} }
plaid_success(token, response) { plaid_success(token, response) {
frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.update_bank_account_ids', {
response: response,
}).then(() => {
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' }); frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
});
} }
}; };

View File

@@ -70,6 +70,7 @@ def make_bank_account(doctype, docname):
return doc return doc
@frappe.whitelist()
def get_party_bank_account(party_type, party): def get_party_bank_account(party_type, party):
return frappe.db.get_value(party_type, party, "default_bank_account") return frappe.db.get_value(party_type, party, "default_bank_account")

View File

@@ -37,11 +37,14 @@ frappe.ui.form.on("Bank Clearance", {
refresh: function(frm) { refresh: function(frm) {
frm.disable_save(); frm.disable_save();
if (frm.doc.account && frm.doc.from_date && frm.doc.to_date) {
frm.add_custom_button(__('Get Payment Entries'), () => frm.add_custom_button(__('Get Payment Entries'), () =>
frm.trigger("get_payment_entries") frm.trigger("get_payment_entries")
); );
frm.change_custom_button_type(__('Get Payment Entries'), null, 'primary'); frm.change_custom_button_type('Get Payment Entries', null, 'primary');
}
}, },
update_clearance_date: function(frm) { update_clearance_date: function(frm) {
@@ -53,8 +56,8 @@ frappe.ui.form.on("Bank Clearance", {
frm.refresh_fields(); frm.refresh_fields();
if (!frm.doc.payment_entries.length) { if (!frm.doc.payment_entries.length) {
frm.change_custom_button_type(__('Get Payment Entries'), null, 'primary'); frm.change_custom_button_type('Get Payment Entries', null, 'primary');
frm.change_custom_button_type(__('Update Clearance Date'), null, 'default'); frm.change_custom_button_type('Update Clearance Date', null, 'default');
} }
} }
}); });
@@ -72,8 +75,8 @@ frappe.ui.form.on("Bank Clearance", {
frm.trigger("update_clearance_date") frm.trigger("update_clearance_date")
); );
frm.change_custom_button_type(__('Get Payment Entries'), null, 'default'); frm.change_custom_button_type('Get Payment Entries', null, 'default');
frm.change_custom_button_type(__('Update Clearance Date'), null, 'primary'); frm.change_custom_button_type('Update Clearance Date', null, 'primary');
} }
} }
}); });

View File

@@ -5,6 +5,7 @@
import frappe import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.model.document import Document from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt, fmt_money, getdate from frappe.utils import flt, fmt_money, getdate
import erpnext import erpnext
@@ -21,24 +22,159 @@ class BankClearance(Document):
if not self.account: if not self.account:
frappe.throw(_("Account is mandatory to get payment entries")) frappe.throw(_("Account is mandatory to get payment entries"))
entries = [] condition = ""
if not self.include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
# get entries from all the apps journal_entries = frappe.db.sql(
for method_name in frappe.get_hooks("get_payment_entries_for_bank_clearance"): """
entries += ( select
frappe.get_attr(method_name)( "Journal Entry" as payment_document, t1.name as payment_entry,
self.from_date, t1.cheque_no as cheque_number, t1.cheque_date,
self.to_date, sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
self.account, t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
self.bank_account, from
self.include_reconciled_entries, `tabJournal Entry` t1, `tabJournal Entry Account` t2
self.include_pos_transactions, where
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC
""".format(
condition=condition
),
{"account": self.account, "from": self.from_date, "to": self.to_date},
as_dict=1,
) )
or []
if self.bank_account:
condition += "and bank_account = %(bank_account)s"
payment_entries = frappe.db.sql(
"""
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount, 0) as credit,
if(paid_from=%(account)s, 0, received_amount) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`
where
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date >= %(from)s and posting_date <= %(to)s
{condition}
order by
posting_date ASC, name DESC
""".format(
condition=condition
),
{
"account": self.account,
"from": self.from_date,
"to": self.to_date,
"bank_account": self.bank_account,
},
as_dict=1,
)
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
loan_disbursements = (
frappe.qb.from_(loan_disbursement)
.select(
ConstantColumn("Loan Disbursement").as_("payment_document"),
loan_disbursement.name.as_("payment_entry"),
loan_disbursement.disbursed_amount.as_("credit"),
ConstantColumn(0).as_("debit"),
loan_disbursement.reference_number.as_("cheque_number"),
loan_disbursement.reference_date.as_("cheque_date"),
loan_disbursement.disbursement_date.as_("posting_date"),
loan_disbursement.applicant.as_("against_account"),
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.disbursement_date >= self.from_date)
.where(loan_disbursement.disbursement_date <= self.to_date)
.where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
.orderby(loan_disbursement.disbursement_date)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
).run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
query = (
frappe.qb.from_(loan_repayment)
.select(
ConstantColumn("Loan Repayment").as_("payment_document"),
loan_repayment.name.as_("payment_entry"),
loan_repayment.amount_paid.as_("debit"),
ConstantColumn(0).as_("credit"),
loan_repayment.reference_number.as_("cheque_number"),
loan_repayment.reference_date.as_("cheque_date"),
loan_repayment.applicant.as_("against_account"),
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.posting_date >= self.from_date)
.where(loan_repayment.posting_date <= self.to_date)
.where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
)
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
query = query.orderby(loan_repayment.posting_date).orderby(
loan_repayment.name, order=frappe.qb.desc
)
loan_repayments = query.run(as_dict=True)
pos_sales_invoices, pos_purchase_invoices = [], []
if self.include_pos_transactions:
pos_sales_invoices = frappe.db.sql(
"""
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.customer as against_account, sip.clearance_date,
account.account_currency, 0 as credit
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
where
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by
si.posting_date ASC, si.name DESC
""",
{"account": self.account, "from": self.from_date, "to": self.to_date},
as_dict=1,
)
pos_purchase_invoices = frappe.db.sql(
"""
select
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
account.account_currency, 0 as debit
from `tabPurchase Invoice` pi, `tabAccount` account
where
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
order by
pi.posting_date ASC, pi.name DESC
""",
{"account": self.account, "from": self.from_date, "to": self.to_date},
as_dict=1,
) )
entries = sorted( entries = sorted(
entries, list(payment_entries)
+ list(journal_entries)
+ list(pos_sales_invoices)
+ list(pos_purchase_invoices)
+ list(loan_disbursements)
+ list(loan_repayments),
key=lambda k: getdate(k["posting_date"]), key=lambda k: getdate(k["posting_date"]),
) )
@@ -91,111 +227,3 @@ class BankClearance(Document):
msgprint(_("Clearance Date updated")) msgprint(_("Clearance Date updated"))
else: else:
msgprint(_("Clearance Date not mentioned")) msgprint(_("Clearance Date not mentioned"))
def get_payment_entries_for_bank_clearance(
from_date, to_date, account, bank_account, include_reconciled_entries, include_pos_transactions
):
entries = []
condition = ""
if not include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as payment_document, t1.name as payment_entry,
t1.cheque_no as cheque_number, t1.cheque_date,
sum(t2.debit_in_account_currency) as debit, sum(t2.credit_in_account_currency) as credit,
t1.posting_date, t2.against_account, t1.clearance_date, t2.account_currency
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
where
t2.parent = t1.name and t2.account = %(account)s and t1.docstatus=1
and t1.posting_date >= %(from)s and t1.posting_date <= %(to)s
and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC
""".format(
condition=condition
),
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
if bank_account:
condition += "and bank_account = %(bank_account)s"
payment_entries = frappe.db.sql(
"""
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
if(paid_from=%(account)s, paid_amount + total_taxes_and_charges, 0) as credit,
if(paid_from=%(account)s, 0, received_amount) as debit,
posting_date, ifnull(party,if(paid_from=%(account)s,paid_to,paid_from)) as against_account, clearance_date,
if(paid_to=%(account)s, paid_to_account_currency, paid_from_account_currency) as account_currency
from `tabPayment Entry`
where
(paid_from=%(account)s or paid_to=%(account)s) and docstatus=1
and posting_date >= %(from)s and posting_date <= %(to)s
{condition}
order by
posting_date ASC, name DESC
""".format(
condition=condition
),
{
"account": account,
"from": from_date,
"to": to_date,
"bank_account": bank_account,
},
as_dict=1,
)
pos_sales_invoices, pos_purchase_invoices = [], []
if include_pos_transactions:
pos_sales_invoices = frappe.db.sql(
"""
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.customer as against_account, sip.clearance_date,
account.account_currency, 0 as credit
from `tabSales Invoice Payment` sip, `tabSales Invoice` si, `tabAccount` account
where
sip.account=%(account)s and si.docstatus=1 and sip.parent = si.name
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by
si.posting_date ASC, si.name DESC
""",
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
pos_purchase_invoices = frappe.db.sql(
"""
select
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
account.account_currency, 0 as debit
from `tabPurchase Invoice` pi, `tabAccount` account
where
pi.cash_bank_account=%(account)s and pi.docstatus=1 and account.name = pi.cash_bank_account
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
order by
pi.posting_date ASC, pi.name DESC
""",
{"account": account, "from": from_date, "to": to_date},
as_dict=1,
)
entries = (
list(payment_entries)
+ list(journal_entries)
+ list(pos_sales_invoices)
+ list(pos_purchase_invoices)
)
return entries

View File

@@ -8,75 +8,26 @@ from frappe.utils import add_months, getdate
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.tests.utils import if_lending_app_installed, if_lending_app_not_installed from erpnext.loan_management.doctype.loan.test_loan import (
create_loan,
create_loan_accounts,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
class TestBankClearance(unittest.TestCase): class TestBankClearance(unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
clear_payment_entries()
clear_loan_transactions()
make_bank_account() make_bank_account()
create_loan_accounts()
create_loan_masters()
add_transactions() add_transactions()
# Basic test case to test if bank clearance tool doesn't break # Basic test case to test if bank clearance tool doesn't break
# Detailed test can be added later # Detailed test can be added later
@if_lending_app_not_installed
def test_bank_clearance(self): def test_bank_clearance(self):
bank_clearance = frappe.get_doc("Bank Clearance")
bank_clearance.account = "_Test Bank Clearance - _TC"
bank_clearance.from_date = add_months(getdate(), -1)
bank_clearance.to_date = getdate()
bank_clearance.get_payment_entries()
self.assertEqual(len(bank_clearance.payment_entries), 1)
@if_lending_app_installed
def test_bank_clearance_with_loan(self):
from lending.loan_management.doctype.loan.test_loan import (
create_loan,
create_loan_accounts,
create_loan_type,
create_repayment_entry,
make_loan_disbursement_entry,
)
def create_loan_masters():
create_loan_type(
"Clearance Loan",
2000000,
13.5,
25,
0,
5,
"Cash",
"_Test Bank Clearance - _TC",
"_Test Bank Clearance - _TC",
"Loan Account - _TC",
"Interest Income Account - _TC",
"Penalty Income Account - _TC",
)
def make_loan():
loan = create_loan(
"_Test Customer",
"Clearance Loan",
280000,
"Repay Over Number of Periods",
20,
applicant_type="Customer",
)
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
repayment_entry = create_repayment_entry(
loan.name, "_Test Customer", getdate(), loan.loan_amount
)
repayment_entry.save()
repayment_entry.submit()
create_loan_accounts()
create_loan_masters()
make_loan()
bank_clearance = frappe.get_doc("Bank Clearance") bank_clearance = frappe.get_doc("Bank Clearance")
bank_clearance.account = "_Test Bank Clearance - _TC" bank_clearance.account = "_Test Bank Clearance - _TC"
bank_clearance.from_date = add_months(getdate(), -1) bank_clearance.from_date = add_months(getdate(), -1)
@@ -85,19 +36,6 @@ class TestBankClearance(unittest.TestCase):
self.assertEqual(len(bank_clearance.payment_entries), 3) self.assertEqual(len(bank_clearance.payment_entries), 3)
def clear_payment_entries():
frappe.db.delete("Payment Entry")
@if_lending_app_installed
def clear_loan_transactions():
for dt in [
"Loan Disbursement",
"Loan Repayment",
]:
frappe.db.delete(dt)
def make_bank_account(): def make_bank_account():
if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"): if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
frappe.get_doc( frappe.get_doc(
@@ -111,8 +49,42 @@ def make_bank_account():
).insert() ).insert()
def create_loan_masters():
create_loan_type(
"Clearance Loan",
2000000,
13.5,
25,
0,
5,
"Cash",
"_Test Bank Clearance - _TC",
"_Test Bank Clearance - _TC",
"Loan Account - _TC",
"Interest Income Account - _TC",
"Penalty Income Account - _TC",
)
def add_transactions(): def add_transactions():
make_payment_entry() make_payment_entry()
make_loan()
def make_loan():
loan = create_loan(
"_Test Customer",
"Clearance Loan",
280000,
"Repay Over Number of Periods",
20,
applicant_type="Customer",
)
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
repayment_entry.save()
repayment_entry.submit()
def make_payment_entry(): def make_payment_entry():

View File

@@ -18,30 +18,16 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}, },
onload: function (frm) { onload: function (frm) {
// Set default filter dates
let today = frappe.datetime.get_today()
frm.doc.bank_statement_from_date = frappe.datetime.add_months(today, -1);
frm.doc.bank_statement_to_date = today;
frm.trigger('bank_account'); frm.trigger('bank_account');
}, },
filter_by_reference_date: function (frm) {
if (frm.doc.filter_by_reference_date) {
frm.set_value("bank_statement_from_date", "");
frm.set_value("bank_statement_to_date", "");
} else {
frm.set_value("from_reference_date", "");
frm.set_value("to_reference_date", "");
}
},
refresh: function (frm) { refresh: function (frm) {
frm.disable_save();
frappe.require("bank-reconciliation-tool.bundle.js", () => frappe.require("bank-reconciliation-tool.bundle.js", () =>
frm.trigger("make_reconciliation_tool") frm.trigger("make_reconciliation_tool")
); );
frm.upload_statement_button = frm.page.set_secondary_action(
frm.add_custom_button(__("Upload Bank Statement"), () => __("Upload Bank Statement"),
() =>
frappe.call({ frappe.call({
method: method:
"erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement", "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement",
@@ -63,26 +49,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}, },
}) })
); );
frm.add_custom_button(__('Auto Reconcile'), function() {
frappe.call({
method: "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.auto_reconcile_vouchers",
args: {
bank_account: frm.doc.bank_account,
from_date: frm.doc.bank_statement_from_date,
to_date: frm.doc.bank_statement_to_date,
filter_by_reference_date: frm.doc.filter_by_reference_date,
from_reference_date: frm.doc.from_reference_date,
to_reference_date: frm.doc.to_reference_date,
}, },
})
});
frm.add_custom_button(__('Get Unreconciled Entries'), function() { after_save: function (frm) {
frm.trigger("make_reconciliation_tool"); frm.trigger("make_reconciliation_tool");
});
frm.change_custom_button_type(__('Get Unreconciled Entries'), null, 'primary');
}, },
bank_account: function (frm) { bank_account: function (frm) {
@@ -96,7 +66,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
r.account, r.account,
"account_currency", "account_currency",
(r) => { (r) => {
frm.doc.account_currency = r.account_currency; frm.currency = r.account_currency;
frm.trigger("render_chart"); frm.trigger("render_chart");
} }
); );
@@ -162,7 +132,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
} }
}, },
render_chart(frm) { render_chart: frappe.utils.debounce((frm) => {
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager( frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{ {
$reconciliation_tool_cards: frm.get_field( $reconciliation_tool_cards: frm.get_field(
@@ -171,10 +141,10 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
bank_statement_closing_balance: bank_statement_closing_balance:
frm.doc.bank_statement_closing_balance, frm.doc.bank_statement_closing_balance,
cleared_balance: frm.cleared_balance, cleared_balance: frm.cleared_balance,
currency: frm.doc.account_currency, currency: frm.currency,
} }
); );
}, }, 500),
render(frm) { render(frm) {
if (frm.doc.bank_account) { if (frm.doc.bank_account) {
@@ -190,9 +160,6 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
).$wrapper, ).$wrapper,
bank_statement_from_date: frm.doc.bank_statement_from_date, bank_statement_from_date: frm.doc.bank_statement_from_date,
bank_statement_to_date: frm.doc.bank_statement_to_date, bank_statement_to_date: frm.doc.bank_statement_to_date,
filter_by_reference_date: frm.doc.filter_by_reference_date,
from_reference_date: frm.doc.from_reference_date,
to_reference_date: frm.doc.to_reference_date,
bank_statement_closing_balance: bank_statement_closing_balance:
frm.doc.bank_statement_closing_balance, frm.doc.bank_statement_closing_balance,
cards_manager: frm.cards_manager, cards_manager: frm.cards_manager,

View File

@@ -10,11 +10,7 @@
"column_break_1", "column_break_1",
"bank_statement_from_date", "bank_statement_from_date",
"bank_statement_to_date", "bank_statement_to_date",
"from_reference_date",
"to_reference_date",
"filter_by_reference_date",
"column_break_2", "column_break_2",
"account_currency",
"account_opening_balance", "account_opening_balance",
"bank_statement_closing_balance", "bank_statement_closing_balance",
"section_break_1", "section_break_1",
@@ -40,13 +36,13 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{ {
"depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date", "depends_on": "eval: doc.bank_account",
"fieldname": "bank_statement_from_date", "fieldname": "bank_statement_from_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "From Date" "label": "From Date"
}, },
{ {
"depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date", "depends_on": "eval: doc.bank_statement_from_date",
"fieldname": "bank_statement_to_date", "fieldname": "bank_statement_to_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "To Date" "label": "To Date"
@@ -60,7 +56,7 @@
"fieldname": "account_opening_balance", "fieldname": "account_opening_balance",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Account Opening Balance", "label": "Account Opening Balance",
"options": "account_currency", "options": "Currency",
"read_only": 1 "read_only": 1
}, },
{ {
@@ -68,7 +64,7 @@
"fieldname": "bank_statement_closing_balance", "fieldname": "bank_statement_closing_balance",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Closing Balance", "label": "Closing Balance",
"options": "account_currency" "options": "Currency"
}, },
{ {
"fieldname": "section_break_1", "fieldname": "section_break_1",
@@ -85,40 +81,14 @@
}, },
{ {
"fieldname": "no_bank_transactions", "fieldname": "no_bank_transactions",
"fieldtype": "HTML", "fieldtype": "HTML"
"options": "<div class=\"text-muted text-center\">No Matching Bank Transactions Found</div>"
},
{
"depends_on": "eval:doc.filter_by_reference_date",
"fieldname": "from_reference_date",
"fieldtype": "Date",
"label": "From Reference Date"
},
{
"depends_on": "eval:doc.filter_by_reference_date",
"fieldname": "to_reference_date",
"fieldtype": "Date",
"label": "To Reference Date"
},
{
"default": "0",
"fieldname": "filter_by_reference_date",
"fieldtype": "Check",
"label": "Filter by Reference Date"
},
{
"fieldname": "account_currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Account Currency",
"options": "Currency"
} }
], ],
"hide_toolbar": 1, "hide_toolbar": 1,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-03-07 11:02:24.535714", "modified": "2021-04-21 11:13:49.831769",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Reconciliation Tool", "name": "Bank Reconciliation Tool",
@@ -137,6 +107,5 @@
], ],
"quick_entry": 1, "quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC"
"states": []
} }

View File

@@ -7,10 +7,10 @@ import json
import frappe import frappe
from frappe import _ from frappe import _
from frappe.model.document import Document from frappe.model.document import Document
from frappe.utils import cint, flt from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
from erpnext import get_default_cost_center from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_total_allocated_amount
from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import ( from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import (
get_amounts_not_reflected_in_system, get_amounts_not_reflected_in_system,
get_entries, get_entries,
@@ -28,7 +28,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
filters = [] filters = []
filters.append(["bank_account", "=", bank_account]) filters.append(["bank_account", "=", bank_account])
filters.append(["docstatus", "=", 1]) filters.append(["docstatus", "=", 1])
filters.append(["unallocated_amount", ">", 0.0]) filters.append(["unallocated_amount", ">", 0])
if to_date: if to_date:
filters.append(["date", "<=", to_date]) filters.append(["date", "<=", to_date])
if from_date: if from_date:
@@ -50,7 +50,6 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
"party", "party",
], ],
filters=filters, filters=filters,
order_by="date",
) )
return transactions return transactions
@@ -58,7 +57,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
@frappe.whitelist() @frappe.whitelist()
def get_account_balance(bank_account, till_date): def get_account_balance(bank_account, till_date):
# returns account balance till the specified date # returns account balance till the specified date
account = frappe.db.get_value("Bank Account", bank_account, "account") account = frappe.get_cached_value("Bank Account", bank_account, "account")
filters = frappe._dict( filters = frappe._dict(
{"account": account, "report_date": till_date, "include_pos_transactions": 1} {"account": account, "report_date": till_date, "include_pos_transactions": 1}
) )
@@ -66,7 +65,7 @@ def get_account_balance(bank_account, till_date):
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0.0, 0.0 total_debit, total_credit = 0, 0
for d in data: for d in data:
total_debit += flt(d.debit) total_debit += flt(d.debit)
total_credit += flt(d.credit) total_credit += flt(d.credit)
@@ -131,8 +130,10 @@ def create_journal_entry_bts(
fieldname=["name", "deposit", "withdrawal", "bank_account"], fieldname=["name", "deposit", "withdrawal", "bank_account"],
as_dict=True, as_dict=True,
)[0] )[0]
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") company_account = frappe.get_cached_value(
account_type = frappe.db.get_value("Account", second_account, "account_type") "Bank Account", bank_transaction.bank_account, "account"
)
account_type = frappe.get_cached_value("Account", second_account, "account_type")
if account_type in ["Receivable", "Payable"]: if account_type in ["Receivable", "Payable"]:
if not (party_type and party): if not (party_type and party):
frappe.throw( frappe.throw(
@@ -140,19 +141,17 @@ def create_journal_entry_bts(
second_account second_account
) )
) )
company = frappe.get_value("Account", company_account, "company")
accounts = [] accounts = []
# Multi Currency? # Multi Currency?
accounts.append( accounts.append(
{ {
"account": second_account, "account": second_account,
"credit_in_account_currency": bank_transaction.deposit, "credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
"debit_in_account_currency": bank_transaction.withdrawal, "debit_in_account_currency": bank_transaction.withdrawal
if bank_transaction.withdrawal > 0
else 0,
"party_type": party_type, "party_type": party_type,
"party": party, "party": party,
"cost_center": get_default_cost_center(company),
} }
) )
@@ -160,12 +159,15 @@ def create_journal_entry_bts(
{ {
"account": company_account, "account": company_account,
"bank_account": bank_transaction.bank_account, "bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal, "credit_in_account_currency": bank_transaction.withdrawal
"debit_in_account_currency": bank_transaction.deposit, if bank_transaction.withdrawal > 0
"cost_center": get_default_cost_center(company), else 0,
"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
} }
) )
company = frappe.get_cached_value("Account", company_account, "company")
journal_entry_dict = { journal_entry_dict = {
"voucher_type": entry_type, "voucher_type": entry_type,
"company": company, "company": company,
@@ -184,22 +186,16 @@ def create_journal_entry_bts(
journal_entry.insert() journal_entry.insert()
journal_entry.submit() journal_entry.submit()
if bank_transaction.deposit > 0.0: if bank_transaction.deposit > 0:
paid_amount = bank_transaction.deposit paid_amount = bank_transaction.deposit
else: else:
paid_amount = bank_transaction.withdrawal paid_amount = bank_transaction.withdrawal
vouchers = json.dumps( vouchers = json.dumps(
[ [{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
{
"payment_doctype": "Journal Entry",
"payment_name": journal_entry.name,
"amount": paid_amount,
}
]
) )
return reconcile_vouchers(bank_transaction_name, vouchers) return reconcile_vouchers(bank_transaction.name, vouchers)
@frappe.whitelist() @frappe.whitelist()
@@ -223,10 +219,12 @@ def create_payment_entry_bts(
as_dict=True, as_dict=True,
)[0] )[0]
paid_amount = bank_transaction.unallocated_amount paid_amount = bank_transaction.unallocated_amount
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay" payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account") company_account = frappe.get_cached_value(
company = frappe.get_value("Account", company_account, "company") "Bank Account", bank_transaction.bank_account, "account"
)
company = frappe.get_cached_value("Account", company_account, "company")
payment_entry_dict = { payment_entry_dict = {
"company": company, "company": company,
"payment_type": payment_type, "payment_type": payment_type,
@@ -262,52 +260,42 @@ def create_payment_entry_bts(
payment_entry.submit() payment_entry.submit()
vouchers = json.dumps( vouchers = json.dumps(
[ [{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
{
"payment_doctype": "Payment Entry",
"payment_name": payment_entry.name,
"amount": paid_amount,
}
]
) )
return reconcile_vouchers(bank_transaction_name, vouchers) return reconcile_vouchers(bank_transaction.name, vouchers)
@frappe.whitelist() @frappe.whitelist()
def auto_reconcile_vouchers( def reconcile_vouchers(bank_transaction_name, vouchers):
bank_account, # updated clear date of all the vouchers based on the bank transaction
from_date=None, vouchers = json.loads(vouchers)
to_date=None, transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
filter_by_reference_date=None, company_account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
from_reference_date=None,
to_reference_date=None, if transaction.unallocated_amount == 0:
): frappe.throw(_("This bank transaction is already fully reconciled"))
frappe.flags.auto_reconcile_vouchers = True total_amount = 0
document_types = ["payment_entry", "journal_entry"] for voucher in vouchers:
bank_transactions = get_bank_transactions(bank_account) voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
matched_transaction = [] total_amount += get_paid_amount(
for transaction in bank_transactions: frappe._dict(
linked_payments = get_linked_payments(
transaction.name,
document_types,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
)
vouchers = []
for r in linked_payments:
vouchers.append(
{ {
"payment_doctype": r[1], "payment_document": voucher["payment_doctype"],
"payment_name": r[2], "payment_entry": voucher["payment_name"],
"amount": r[4],
} }
),
transaction.currency,
company_account,
) )
transaction = frappe.get_doc("Bank Transaction", transaction.name)
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account") if total_amount > transaction.unallocated_amount:
matched_trans = 0 frappe.throw(
_(
"The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
)
)
account = frappe.get_cached_value("Bank Account", transaction.bank_account, "account")
for voucher in vouchers: for voucher in vouchers:
gl_entry = frappe.db.get_value( gl_entry = frappe.db.get_value(
"GL Entry", "GL Entry",
@@ -323,105 +311,39 @@ def auto_reconcile_vouchers(
else (gl_entry.debit, transaction.withdrawal) else (gl_entry.debit, transaction.withdrawal)
) )
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
transaction.append( transaction.append(
"payment_entries", "payment_entries",
{ {
"payment_document": voucher["payment_doctype"], "payment_document": voucher["payment_entry"].doctype,
"payment_entry": voucher["payment_name"], "payment_entry": voucher["payment_entry"].name,
"allocated_amount": allocated_amount, "allocated_amount": allocated_amount,
}, },
) )
matched_transaction.append(str(transaction.name))
transaction.save() transaction.save()
transaction.update_allocations() transaction.update_allocations()
matched_transaction_len = len(set(matched_transaction))
if matched_transaction_len == 0:
frappe.msgprint(_("No matching references found for auto reconciliation"))
elif matched_transaction_len == 1:
frappe.msgprint(_("{0} transaction is reconcilied").format(matched_transaction_len))
else:
frappe.msgprint(_("{0} transactions are reconcilied").format(matched_transaction_len))
frappe.flags.auto_reconcile_vouchers = False
return frappe.get_doc("Bank Transaction", transaction.name)
@frappe.whitelist()
def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
transaction.add_payment_entries(vouchers)
return frappe.get_doc("Bank Transaction", bank_transaction_name) return frappe.get_doc("Bank Transaction", bank_transaction_name)
@frappe.whitelist() @frappe.whitelist()
def get_linked_payments( def get_linked_payments(bank_transaction_name, document_types=None):
bank_transaction_name,
document_types=None,
from_date=None,
to_date=None,
filter_by_reference_date=None,
from_reference_date=None,
to_reference_date=None,
):
# get all matching payments for a bank transaction # get all matching payments for a bank transaction
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name) transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_account = frappe.db.get_values( bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True "Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
)[0] )[0]
(gl_account, company) = (bank_account.account, bank_account.company) (account, company) = (bank_account.account, bank_account.company)
matching = check_matching( matching = check_matching(account, company, transaction, document_types)
gl_account, return matching
company,
transaction,
document_types,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
)
return subtract_allocations(gl_account, matching)
def subtract_allocations(gl_account, vouchers): def check_matching(bank_account, company, transaction, document_types):
"Look up & subtract any existing Bank Transaction allocations" # combine all types of vouchers
copied = [] subquery = get_queries(bank_account, company, transaction, document_types)
for voucher in vouchers:
rows = get_total_allocated_amount(voucher[1], voucher[2])
amount = None
for row in rows:
if row["gl_account"] == gl_account:
amount = row["total"]
break
if amount:
l = list(voucher)
l[3] -= amount
copied.append(tuple(l))
else:
copied.append(voucher)
return copied
def check_matching(
bank_account,
company,
transaction,
document_types,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
):
exact_match = True if "exact_match" in document_types else False
filters = { filters = {
"amount": transaction.unallocated_amount, "amount": transaction.unallocated_amount,
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay", "payment_type": "Receive" if transaction.deposit > 0 else "Pay",
"reference_no": transaction.reference_number, "reference_no": transaction.reference_number,
"party_type": transaction.party_type, "party_type": transaction.party_type,
"party": transaction.party, "party": transaction.party,
@@ -430,43 +352,23 @@ def check_matching(
matching_vouchers = [] matching_vouchers = []
# get matching vouchers from all the apps matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
for method_name in frappe.get_hooks("get_matching_vouchers_for_bank_reconciliation"):
for query in subquery:
matching_vouchers.extend( matching_vouchers.extend(
frappe.get_attr(method_name)( frappe.db.sql(
bank_account, query,
company,
transaction,
document_types,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
exact_match,
filters, filters,
) )
or []
) )
return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else [] return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
def get_matching_vouchers_for_bank_reconciliation( def get_queries(bank_account, company, transaction, document_types):
bank_account,
company,
transaction,
document_types,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
exact_match,
filters,
):
# get queries to get matching vouchers # get queries to get matching vouchers
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from" amount_condition = "=" if "exact_match" in document_types else "<="
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
queries = [] queries = []
# get matching queries from all the apps # get matching queries from all the apps
@@ -477,146 +379,141 @@ def get_matching_vouchers_for_bank_reconciliation(
company, company,
transaction, transaction,
document_types, document_types,
exact_match, amount_condition,
account_from_to, account_from_to,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
) )
or [] or []
) )
vouchers = [] return queries
for query in queries:
vouchers.extend(
frappe.db.sql(
query,
filters,
)
)
return vouchers
def get_matching_queries( def get_matching_queries(
bank_account, bank_account, company, transaction, document_types, amount_condition, account_from_to
company,
transaction,
document_types,
exact_match,
account_from_to,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
): ):
queries = [] queries = []
if "payment_entry" in document_types: if "payment_entry" in document_types:
query = get_pe_matching_query( pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction)
exact_match, queries.extend([pe_amount_matching])
account_from_to,
transaction,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
)
queries.append(query)
if "journal_entry" in document_types: if "journal_entry" in document_types:
query = get_je_matching_query( je_amount_matching = get_je_matching_query(amount_condition, transaction)
exact_match, queries.extend([je_amount_matching])
transaction,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
)
queries.append(query)
if transaction.deposit > 0.0 and "sales_invoice" in document_types: if transaction.deposit > 0 and "sales_invoice" in document_types:
query = get_si_matching_query(exact_match) si_amount_matching = get_si_matching_query(amount_condition)
queries.append(query) queries.extend([si_amount_matching])
if transaction.withdrawal > 0.0: if transaction.withdrawal > 0:
if "purchase_invoice" in document_types: if "purchase_invoice" in document_types:
query = get_pi_matching_query(exact_match) pi_amount_matching = get_pi_matching_query(amount_condition)
queries.append(query) queries.extend([pi_amount_matching])
if "bank_transaction" in document_types:
query = get_bt_matching_query(exact_match, transaction)
queries.append(query)
return queries return queries
def get_bt_matching_query(exact_match, transaction): def get_loan_vouchers(bank_account, transaction, document_types, filters):
# get matching bank transaction query vouchers = []
# find bank transactions in the same bank account with opposite sign amount_condition = True if "exact_match" in document_types else False
# same bank account must have same company and currency
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
return f""" if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
SELECT if transaction.deposit > 0 and "loan_repayment" in document_types:
(CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
+ CASE WHEN {field} = %(amount)s THEN 1 ELSE 0 END
+ CASE WHEN ( party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END return vouchers
+ CASE WHEN unallocated_amount = %(amount)s THEN 1 ELSE 0 END
+ 1) AS rank,
'Bank Transaction' AS doctype,
name,
unallocated_amount AS paid_amount,
reference_number AS reference_no,
date AS reference_date,
party,
party_type,
date AS posting_date,
currency
FROM
`tabBank Transaction`
WHERE
status != 'Reconciled'
AND name != '{transaction.name}'
AND bank_account = '{transaction.bank_account}'
AND {field} {'= %(amount)s' if exact_match else '> 0.0'}
"""
def get_pe_matching_query( def get_ld_matching_query(bank_account, amount_condition, filters):
exact_match, loan_disbursement = frappe.qb.DocType("Loan Disbursement")
account_from_to, matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
transaction, matching_party = loan_disbursement.applicant_type == filters.get(
from_date, "party_type"
to_date, ) and loan_disbursement.applicant == filters.get("party")
filter_by_reference_date,
from_reference_date, rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
to_reference_date,
): rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
query = (
frappe.qb.from_(loan_disbursement)
.select(
rank + rank1 + 1,
ConstantColumn("Loan Disbursement").as_("doctype"),
loan_disbursement.name,
loan_disbursement.disbursed_amount,
loan_disbursement.reference_number,
loan_disbursement.reference_date,
loan_disbursement.applicant_type,
loan_disbursement.disbursement_date,
)
.where(loan_disbursement.docstatus == 1)
.where(loan_disbursement.clearance_date.isnull())
.where(loan_disbursement.disbursement_account == bank_account)
)
if amount_condition:
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
else:
query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
vouchers = query.run(as_list=True)
return vouchers
def get_lr_matching_query(bank_account, amount_condition, filters):
loan_repayment = frappe.qb.DocType("Loan Repayment")
matching_reference = loan_repayment.reference_number == filters.get("reference_number")
matching_party = loan_repayment.applicant_type == filters.get(
"party_type"
) and loan_repayment.applicant == filters.get("party")
rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
query = (
frappe.qb.from_(loan_repayment)
.select(
rank + rank1 + 1,
ConstantColumn("Loan Repayment").as_("doctype"),
loan_repayment.name,
loan_repayment.amount_paid,
loan_repayment.reference_number,
loan_repayment.reference_date,
loan_repayment.applicant_type,
loan_repayment.posting_date,
)
.where(loan_repayment.docstatus == 1)
.where(loan_repayment.clearance_date.isnull())
.where(loan_repayment.payment_account == bank_account)
)
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
if amount_condition:
query.where(loan_repayment.amount_paid == filters.get("amount"))
else:
query.where(loan_repayment.amount_paid <= filters.get("amount"))
vouchers = query.run()
return vouchers
def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query # get matching payment entries query
if transaction.deposit > 0.0: if transaction.deposit > 0:
currency_field = "paid_to_account_currency as currency" currency_field = "paid_to_account_currency as currency"
else: else:
currency_field = "paid_from_account_currency as currency" currency_field = "paid_from_account_currency as currency"
filter_by_date = f"AND posting_date between '{from_date}' and '{to_date}'"
order_by = " posting_date"
filter_by_reference_no = ""
if cint(filter_by_reference_date):
filter_by_date = f"AND reference_date between '{from_reference_date}' and '{to_reference_date}'"
order_by = " reference_date"
if frappe.flags.auto_reconcile_vouchers == True:
filter_by_reference_no = f"AND reference_no = '{transaction.reference_number}'"
return f""" return f"""
SELECT SELECT
(CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank, + 1 ) AS rank,
'Payment Entry' as doctype, 'Payment Entry' as doctype,
name, name,
@@ -630,78 +527,55 @@ def get_pe_matching_query(
FROM FROM
`tabPayment Entry` `tabPayment Entry`
WHERE WHERE
docstatus = 1 paid_amount {amount_condition} %(amount)s
AND docstatus = 1
AND payment_type IN (%(payment_type)s, 'Internal Transfer') AND payment_type IN (%(payment_type)s, 'Internal Transfer')
AND ifnull(clearance_date, '') = "" AND ifnull(clearance_date, '') = ""
AND {account_from_to} = %(bank_account)s AND {account_from_to} = %(bank_account)s
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
{filter_by_date}
{filter_by_reference_no}
order by{order_by}
""" """
def get_je_matching_query( def get_je_matching_query(amount_condition, transaction):
exact_match,
transaction,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
):
# get matching journal entry query # get matching journal entry query
# We have mapping at the bank level # We have mapping at the bank level
# So one bank could have both types of bank accounts like asset and liability # So one bank could have both types of bank accounts like asset and liability
# So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type # So cr_or_dr should be judged only on basis of withdrawal and deposit and not account type
cr_or_dr = "credit" if transaction.withdrawal > 0.0 else "debit" cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
filter_by_date = f"AND je.posting_date between '{from_date}' and '{to_date}'"
order_by = " je.posting_date"
filter_by_reference_no = ""
if cint(filter_by_reference_date):
filter_by_date = f"AND je.cheque_date between '{from_reference_date}' and '{to_reference_date}'"
order_by = " je.cheque_date"
if frappe.flags.auto_reconcile_vouchers == True:
filter_by_reference_no = f"AND je.cheque_no = '{transaction.reference_number}'"
return f""" return f"""
SELECT SELECT
(CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
+ CASE WHEN jea.{cr_or_dr}_in_account_currency = %(amount)s THEN 1 ELSE 0 END
+ 1) AS rank , + 1) AS rank ,
'Journal Entry' AS doctype, 'Journal Entry' as doctype,
je.name, je.name,
jea.{cr_or_dr}_in_account_currency AS paid_amount, jea.{cr_or_dr}_in_account_currency as paid_amount,
je.cheque_no AS reference_no, je.cheque_no as reference_no,
je.cheque_date AS reference_date, je.cheque_date as reference_date,
je.pay_to_recd_from AS party, je.pay_to_recd_from as party,
jea.party_type, jea.party_type,
je.posting_date, je.posting_date,
jea.account_currency AS currency jea.account_currency as currency
FROM FROM
`tabJournal Entry Account` AS jea `tabJournal Entry Account` as jea
JOIN JOIN
`tabJournal Entry` AS je `tabJournal Entry` as je
ON ON
jea.parent = je.name jea.parent = je.name
WHERE WHERE
je.docstatus = 1 (je.clearance_date is null or je.clearance_date='0000-00-00')
AND je.voucher_type NOT IN ('Opening Entry')
AND (je.clearance_date IS NULL OR je.clearance_date='0000-00-00')
AND jea.account = %(bank_account)s AND jea.account = %(bank_account)s
AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'} AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
AND je.docstatus = 1 AND je.docstatus = 1
{filter_by_date}
{filter_by_reference_no}
order by {order_by}
""" """
def get_si_matching_query(exact_match): def get_si_matching_query(amount_condition):
# get matching sales invoice query # get matchin sales invoice query
return f""" return f"""
SELECT SELECT
( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
+ CASE WHEN sip.amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank, + 1 ) AS rank,
'Sales Invoice' as doctype, 'Sales Invoice' as doctype,
si.name, si.name,
@@ -719,20 +593,18 @@ def get_si_matching_query(exact_match):
`tabSales Invoice` as si `tabSales Invoice` as si
ON ON
sip.parent = si.name sip.parent = si.name
WHERE WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
si.docstatus = 1
AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
AND sip.account = %(bank_account)s AND sip.account = %(bank_account)s
AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'} AND sip.amount {amount_condition} %(amount)s
AND si.docstatus = 1
""" """
def get_pi_matching_query(exact_match): def get_pi_matching_query(amount_condition):
# get matching purchase invoice query when they are also used as payment entries (is_paid) # get matching purchase invoice query
return f""" return f"""
SELECT SELECT
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END ( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank, + 1 ) AS rank,
'Purchase Invoice' as doctype, 'Purchase Invoice' as doctype,
name, name,
@@ -746,9 +618,9 @@ def get_pi_matching_query(exact_match):
FROM FROM
`tabPurchase Invoice` `tabPurchase Invoice`
WHERE WHERE
docstatus = 1 paid_amount {amount_condition} %(amount)s
AND docstatus = 1
AND is_paid = 1 AND is_paid = 1
AND ifnull(clearance_date, '') = "" AND ifnull(clearance_date, '') = ""
AND cash_bank_account = %(bank_account)s AND cash_bank_account = %(bank_account)s
AND paid_amount {'= %(amount)s' if exact_match else '> 0.0'}
""" """

View File

@@ -53,20 +53,19 @@ class BankStatementImport(DataImport):
if "Bank Account" not in json.dumps(preview["columns"]): if "Bank Account" not in json.dumps(preview["columns"]):
frappe.throw(_("Please add the Bank Account column")) frappe.throw(_("Please add the Bank Account column"))
from frappe.utils.background_jobs import is_job_enqueued from frappe.utils.background_jobs import is_job_queued
from frappe.utils.scheduler import is_scheduler_inactive from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test: if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")) frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
job_id = f"bank_statement_import::{self.name}" if not is_job_queued(self.name):
if not is_job_enqueued(job_id):
enqueue( enqueue(
start_import, start_import,
queue="default", queue="default",
timeout=6000, timeout=6000,
event="data_import", event="data_import",
job_id=job_id, job_name=self.name,
data_import=self.name, data_import=self.name,
bank_account=self.bank_account, bank_account=self.bank_account,
import_file_path=self.import_file, import_file_path=self.import_file,

View File

@@ -1,178 +0,0 @@
from typing import Tuple, Union
import frappe
from frappe.utils import flt
from rapidfuzz import fuzz, process
class AutoMatchParty:
"""
Matches by Account/IBAN and then by Party Name/Description sequentially.
Returns when a result is obtained.
Result (if present) is of the form: (Party Type, Party,)
"""
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)
def get(self, key):
return self.__dict__.get(key, None)
def match(self) -> Union[Tuple, None]:
result = None
result = AutoMatchbyAccountIBAN(
bank_party_account_number=self.bank_party_account_number,
bank_party_iban=self.bank_party_iban,
deposit=self.deposit,
).match()
fuzzy_matching_enabled = frappe.db.get_single_value("Accounts Settings", "enable_fuzzy_matching")
if not result and fuzzy_matching_enabled:
result = AutoMatchbyPartyNameDescription(
bank_party_name=self.bank_party_name, description=self.description, deposit=self.deposit
).match()
return result
class AutoMatchbyAccountIBAN:
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)
def get(self, key):
return self.__dict__.get(key, None)
def match(self):
if not (self.bank_party_account_number or self.bank_party_iban):
return None
result = self.match_account_in_party()
return result
def match_account_in_party(self) -> Union[Tuple, None]:
"""Check if there is a IBAN/Account No. match in Customer/Supplier/Employee"""
result = None
parties = get_parties_in_order(self.deposit)
or_filters = self.get_or_filters()
for party in parties:
party_result = frappe.db.get_all(
"Bank Account", or_filters=or_filters, pluck="party", limit_page_length=1
)
if party == "Employee" and not party_result:
# Search in Bank Accounts first for Employee, and then Employee record
if "bank_account_no" in or_filters:
or_filters["bank_ac_no"] = or_filters.pop("bank_account_no")
party_result = frappe.db.get_all(
party, or_filters=or_filters, pluck="name", limit_page_length=1
)
if party_result:
result = (
party,
party_result[0],
)
break
return result
def get_or_filters(self) -> dict:
or_filters = {}
if self.bank_party_account_number:
or_filters["bank_account_no"] = self.bank_party_account_number
if self.bank_party_iban:
or_filters["iban"] = self.bank_party_iban
return or_filters
class AutoMatchbyPartyNameDescription:
def __init__(self, **kwargs) -> None:
self.__dict__.update(kwargs)
def get(self, key):
return self.__dict__.get(key, None)
def match(self) -> Union[Tuple, None]:
# fuzzy search by customer/supplier & employee
if not (self.bank_party_name or self.description):
return None
result = self.match_party_name_desc_in_party()
return result
def match_party_name_desc_in_party(self) -> Union[Tuple, None]:
"""Fuzzy search party name and/or description against parties in the system"""
result = None
parties = get_parties_in_order(self.deposit)
for party in parties:
filters = {"status": "Active"} if party == "Employee" else {"disabled": 0}
names = frappe.get_all(party, filters=filters, pluck=party.lower() + "_name")
for field in ["bank_party_name", "description"]:
if not self.get(field):
continue
result, skip = self.fuzzy_search_and_return_result(party, names, field)
if result or skip:
break
if result or skip:
# Skip If: It was hard to distinguish between close matches and so match is None
# OR if the right match was found
break
return result
def fuzzy_search_and_return_result(self, party, names, field) -> Union[Tuple, None]:
skip = False
result = process.extract(query=self.get(field), choices=names, scorer=fuzz.token_set_ratio)
party_name, skip = self.process_fuzzy_result(result)
if not party_name:
return None, skip
return (
party,
party_name,
), skip
def process_fuzzy_result(self, result: Union[list, None]):
"""
If there are multiple valid close matches return None as result may be faulty.
Return the result only if one accurate match stands out.
Returns: Result, Skip (whether or not to discontinue matching)
"""
PARTY, SCORE, CUTOFF = 0, 1, 80
if not result or not len(result):
return None, False
first_result = result[0]
if len(result) == 1:
return (first_result[PARTY] if first_result[SCORE] > CUTOFF else None), True
second_result = result[1]
if first_result[SCORE] > CUTOFF:
# If multiple matches with the same score, return None but discontinue matching
# Matches were found but were too close to distinguish between
if first_result[SCORE] == second_result[SCORE]:
return None, True
return first_result[PARTY], True
else:
return None, False
def get_parties_in_order(deposit: float) -> list:
parties = ["Supplier", "Employee", "Customer"] # most -> least likely to receive
if flt(deposit) > 0:
parties = ["Customer", "Supplier", "Employee"] # most -> least likely to pay
return parties

View File

@@ -12,13 +12,8 @@ frappe.ui.form.on("Bank Transaction", {
}; };
}); });
}, },
refresh(frm) {
frm.add_custom_button(__('Unreconcile Transaction'), () => { bank_account: function(frm) {
frm.call('remove_payment_entries')
.then( () => frm.refresh() );
});
},
bank_account: function (frm) {
set_bank_statement_filter(frm); set_bank_statement_filter(frm);
}, },
@@ -39,7 +34,6 @@ frappe.ui.form.on("Bank Transaction", {
"Journal Entry", "Journal Entry",
"Sales Invoice", "Sales Invoice",
"Purchase Invoice", "Purchase Invoice",
"Bank Transaction",
]; ];
} }
}); });
@@ -55,7 +49,7 @@ const update_clearance_date = (frm, cdt, cdn) => {
frappe frappe
.xcall( .xcall(
"erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment", "erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
{ doctype: cdt, docname: cdn, bt_name: frm.doc.name } { doctype: cdt, docname: cdn }
) )
.then((e) => { .then((e) => {
if (e == "success") { if (e == "success") {

View File

@@ -20,11 +20,9 @@
"currency", "currency",
"section_break_10", "section_break_10",
"description", "description",
"reference_number",
"column_break_10",
"transaction_id",
"transaction_type",
"section_break_14", "section_break_14",
"reference_number",
"transaction_id",
"payment_entries", "payment_entries",
"section_break_18", "section_break_18",
"allocated_amount", "allocated_amount",
@@ -33,11 +31,7 @@
"unallocated_amount", "unallocated_amount",
"party_section", "party_section",
"party_type", "party_type",
"party", "party"
"column_break_3czf",
"bank_party_name",
"bank_party_account_number",
"bank_party_iban"
], ],
"fields": [ "fields": [
{ {
@@ -67,7 +61,7 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"options": "\nPending\nSettled\nUnreconciled\nReconciled\nCancelled" "options": "\nPending\nSettled\nUnreconciled\nReconciled"
}, },
{ {
"fieldname": "bank_account", "fieldname": "bank_account",
@@ -196,40 +190,11 @@
"label": "Withdrawal", "label": "Withdrawal",
"oldfieldname": "credit", "oldfieldname": "credit",
"options": "currency" "options": "currency"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "transaction_type",
"fieldtype": "Data",
"label": "Transaction Type",
"length": 50
},
{
"fieldname": "column_break_3czf",
"fieldtype": "Column Break"
},
{
"fieldname": "bank_party_name",
"fieldtype": "Data",
"label": "Party Name/Account Holder (Bank Statement)"
},
{
"fieldname": "bank_party_iban",
"fieldtype": "Data",
"label": "Party IBAN (Bank Statement)"
},
{
"fieldname": "bank_party_account_number",
"fieldtype": "Data",
"label": "Party Account No. (Bank Statement)"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-06-06 13:58:12.821411", "modified": "2022-03-21 19:05:04.208222",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Transaction", "name": "Bank Transaction",

View File

@@ -1,6 +1,9 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
from functools import reduce
import frappe import frappe
from frappe.utils import flt from frappe.utils import flt
@@ -15,158 +18,72 @@ class BankTransaction(StatusUpdater):
self.clear_linked_payment_entries() self.clear_linked_payment_entries()
self.set_status() self.set_status()
if frappe.db.get_single_value("Accounts Settings", "enable_party_matching"):
self.auto_set_party()
_saving_flag = False
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
def on_update_after_submit(self): def on_update_after_submit(self):
"Run on save(). Avoid recursion caused by multiple saves"
if not self._saving_flag:
self._saving_flag = True
self.clear_linked_payment_entries()
self.update_allocations() self.update_allocations()
self._saving_flag = False self.clear_linked_payment_entries()
self.set_status(update=True)
def on_cancel(self): def on_cancel(self):
self.clear_linked_payment_entries(for_cancel=True) self.clear_linked_payment_entries(for_cancel=True)
self.set_status(update=True) self.set_status(update=True)
def update_allocations(self): def update_allocations(self):
"The doctype does not allow modifications after submission, so write to the db direct"
if self.payment_entries: if self.payment_entries:
allocated_amount = sum(p.allocated_amount for p in self.payment_entries) allocated_amount = reduce(
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
)
else: else:
allocated_amount = 0.0 allocated_amount = 0
amount = abs(flt(self.withdrawal) - flt(self.deposit)) if allocated_amount:
self.db_set("allocated_amount", flt(allocated_amount)) frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
self.db_set("unallocated_amount", amount - flt(allocated_amount)) frappe.db.set_value(
self.reload() self.doctype,
self.set_status(update=True) self.name,
"unallocated_amount",
def add_payment_entries(self, vouchers): abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
if 0.0 >= self.unallocated_amount:
frappe.throw(frappe._("Bank Transaction {0} is already fully reconciled").format(self.name))
added = False
for voucher in vouchers:
# Can't add same voucher twice
found = False
for pe in self.payment_entries:
if (
pe.payment_document == voucher["payment_doctype"]
and pe.payment_entry == voucher["payment_name"]
):
found = True
if not found:
pe = {
"payment_document": voucher["payment_doctype"],
"payment_entry": voucher["payment_name"],
"allocated_amount": 0.0, # Temporary
}
child = self.append("payment_entries", pe)
added = True
# runs on_update_after_submit
if added:
self.save()
def allocate_payment_entries(self):
"""Refactored from bank reconciliation tool.
Non-zero allocations must be amended/cleared manually
Get the bank transaction amount (b) and remove as we allocate
For each payment_entry if allocated_amount == 0:
- get the amount already allocated against all transactions (t), need latest date
- get the voucher amount (from gl) (v)
- allocate (a = v - t)
- a = 0: should already be cleared, so clear & remove payment_entry
- 0 < a <= u: allocate a & clear
- 0 < a, a > u: allocate u
- 0 > a: Error: already over-allocated
- clear means: set the latest transaction date as clearance date
"""
gl_bank_account = frappe.db.get_value("Bank Account", self.bank_account, "account")
remaining_amount = self.unallocated_amount
for payment_entry in self.payment_entries:
if payment_entry.allocated_amount == 0.0:
unallocated_amount, should_clear, latest_transaction = get_clearance_details(
self, payment_entry
) )
if 0.0 == unallocated_amount: else:
if should_clear: frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
latest_transaction.clear_linked_payment_entry(payment_entry) frappe.db.set_value(
self.db_delete_payment_entry(payment_entry) self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
)
elif remaining_amount <= 0.0: amount = self.deposit or self.withdrawal
self.db_delete_payment_entry(payment_entry) if amount == self.allocated_amount:
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
elif 0.0 < unallocated_amount and unallocated_amount <= remaining_amount:
payment_entry.db_set("allocated_amount", unallocated_amount)
remaining_amount -= unallocated_amount
if should_clear:
latest_transaction.clear_linked_payment_entry(payment_entry)
elif 0.0 < unallocated_amount and unallocated_amount > remaining_amount:
payment_entry.db_set("allocated_amount", remaining_amount)
remaining_amount = 0.0
elif 0.0 > unallocated_amount:
self.db_delete_payment_entry(payment_entry)
frappe.throw(frappe._("Voucher {0} is over-allocated by {1}").format(unallocated_amount))
self.reload() self.reload()
def db_delete_payment_entry(self, payment_entry):
frappe.db.delete("Bank Transaction Payments", {"name": payment_entry.name})
@frappe.whitelist()
def remove_payment_entries(self):
for payment_entry in self.payment_entries:
self.remove_payment_entry(payment_entry)
# runs on_update_after_submit
self.save()
def remove_payment_entry(self, payment_entry):
"Clear payment entry and clearance"
self.clear_linked_payment_entry(payment_entry, for_cancel=True)
self.remove(payment_entry)
def clear_linked_payment_entries(self, for_cancel=False): def clear_linked_payment_entries(self, for_cancel=False):
if for_cancel:
for payment_entry in self.payment_entries: for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel) if payment_entry.payment_document == "Sales Invoice":
else: self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
self.allocate_payment_entries() elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
def clear_linked_payment_entry(self, payment_entry, for_cancel=False): def clear_simple_entry(self, payment_entry, for_cancel=False):
clearance_date = None if for_cancel else self.date if payment_entry.payment_document == "Payment Entry":
set_voucher_clearance( if (
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
) == "Internal Transfer"
):
def auto_set_party(self): if len(get_reconciled_bank_transactions(payment_entry)) < 2:
from erpnext.accounts.doctype.bank_transaction.auto_match_party import AutoMatchParty
if self.party_type and self.party:
return return
result = AutoMatchParty( clearance_date = self.date if not for_cancel else None
bank_party_account_number=self.bank_party_account_number,
bank_party_iban=self.bank_party_iban,
bank_party_name=self.bank_party_name,
description=self.description,
deposit=self.deposit,
).match()
if result:
party_type, party = result
frappe.db.set_value( frappe.db.set_value(
"Bank Transaction", self.name, field={"party_type": party_type, "party": party} payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
)
def clear_sales_invoice(self, payment_entry, for_cancel=False):
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
"clearance_date",
clearance_date,
) )
@@ -176,114 +93,38 @@ def get_doctypes_for_bank_reconciliation():
return frappe.get_hooks("bank_reconciliation_doctypes") return frappe.get_hooks("bank_reconciliation_doctypes")
def get_clearance_details(transaction, payment_entry): def get_reconciled_bank_transactions(payment_entry):
""" reconciled_bank_transactions = frappe.get_all(
There should only be one bank gle for a voucher. "Bank Transaction Payments",
Could be none for a Bank Transaction. filters={"payment_entry": payment_entry.payment_entry},
But if a JE, could affect two banks. fields=["parent"],
Should only clear the voucher if all bank gles are allocated.
"""
gl_bank_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
gles = get_related_bank_gl_entries(payment_entry.payment_document, payment_entry.payment_entry)
bt_allocations = get_total_allocated_amount(
payment_entry.payment_document, payment_entry.payment_entry
) )
unallocated_amount = min( return reconciled_bank_transactions
transaction.unallocated_amount,
get_paid_amount(payment_entry, transaction.currency, gl_bank_account),
)
unmatched_gles = len(gles)
latest_transaction = transaction
for gle in gles:
if gle["gl_account"] == gl_bank_account:
if gle["amount"] <= 0.0:
frappe.throw(
frappe._("Voucher {0} value is broken: {1}").format(
payment_entry.payment_entry, gle["amount"]
)
)
unmatched_gles -= 1
unallocated_amount = gle["amount"]
for a in bt_allocations:
if a["gl_account"] == gle["gl_account"]:
unallocated_amount = gle["amount"] - a["total"]
if frappe.utils.getdate(transaction.date) < a["latest_date"]:
latest_transaction = frappe.get_doc("Bank Transaction", a["latest_name"])
else:
# Must be a Journal Entry affecting more than one bank
for a in bt_allocations:
if a["gl_account"] == gle["gl_account"] and a["total"] == gle["amount"]:
unmatched_gles -= 1
return unallocated_amount, unmatched_gles == 0, latest_transaction
def get_related_bank_gl_entries(doctype, docname): def get_total_allocated_amount(payment_entry):
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql return frappe.db.sql(
result = frappe.db.sql(
""" """
SELECT SELECT
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount, SUM(btp.allocated_amount) as allocated_amount,
gle.account AS gl_account bt.name
FROM FROM
`tabGL Entry` gle `tabBank Transaction Payments` as btp
LEFT JOIN LEFT JOIN
`tabAccount` ac ON ac.name=gle.account `tabBank Transaction` bt ON bt.name=btp.parent
WHERE WHERE
ac.account_type = 'Bank' btp.payment_document = %s
AND gle.voucher_type = %(doctype)s AND
AND gle.voucher_no = %(docname)s btp.payment_entry = %s
AND is_cancelled = 0 AND
""", bt.docstatus = 1""",
dict(doctype=doctype, docname=docname), (payment_entry.payment_document, payment_entry.payment_entry),
as_dict=True, as_dict=True,
) )
return result
def get_total_allocated_amount(doctype, docname): def get_paid_amount(payment_entry, currency, bank_account):
"""
Gets the sum of allocations for a voucher on each bank GL account
along with the latest bank transaction name & date
NOTE: query may also include just saved vouchers/payments but with zero allocated_amount
"""
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
"""
SELECT total, latest_name, latest_date, gl_account FROM (
SELECT
ROW_NUMBER() OVER w AS rownum,
SUM(btp.allocated_amount) OVER(PARTITION BY ba.account) AS total,
FIRST_VALUE(bt.name) OVER w AS latest_name,
FIRST_VALUE(bt.date) OVER w AS latest_date,
ba.account AS gl_account
FROM
`tabBank Transaction Payments` btp
LEFT JOIN `tabBank Transaction` bt ON bt.name=btp.parent
LEFT JOIN `tabBank Account` ba ON ba.name=bt.bank_account
WHERE
btp.payment_document = %(doctype)s
AND btp.payment_entry = %(docname)s
AND bt.docstatus = 1
WINDOW w AS (PARTITION BY ba.account ORDER BY bt.date desc)
) temp
WHERE
rownum = 1
""",
dict(doctype=doctype, docname=docname),
as_dict=True,
)
for row in result:
# Why is this *sometimes* a byte string?
if isinstance(row["latest_name"], bytes):
row["latest_name"] = row["latest_name"].decode()
row["latest_date"] = frappe.utils.getdate(row["latest_date"])
return result
def get_paid_amount(payment_entry, currency, gl_bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]: if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount" paid_amount_field = "paid_amount"
@@ -296,7 +137,7 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
) )
elif doc.payment_type == "Pay": elif doc.payment_type == "Pay":
paid_amount_field = ( paid_amount_field = (
"paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount" "paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount"
) )
return frappe.db.get_value( return frappe.db.get_value(
@@ -304,13 +145,10 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
) )
elif payment_entry.payment_document == "Journal Entry": elif payment_entry.payment_document == "Journal Entry":
return abs( return frappe.db.get_value(
frappe.db.get_value(
"Journal Entry Account", "Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": gl_bank_account}, {"parent": payment_entry.payment_entry, "account": bank_account},
"sum(debit_in_account_currency-credit_in_account_currency)", "sum(credit_in_account_currency)",
)
or 0
) )
elif payment_entry.payment_document == "Expense Claim": elif payment_entry.payment_document == "Expense Claim":
@@ -328,12 +166,6 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
payment_entry.payment_document, payment_entry.payment_entry, "amount_paid" payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
) )
elif payment_entry.payment_document == "Bank Transaction":
dep, wth = frappe.db.get_value(
"Bank Transaction", payment_entry.payment_entry, ("deposit", "withdrawal")
)
return abs(flt(wth) - flt(dep))
else: else:
frappe.throw( frappe.throw(
"Please reconcile {0}: {1} manually".format( "Please reconcile {0}: {1} manually".format(
@@ -342,48 +174,18 @@ def get_paid_amount(payment_entry, currency, gl_bank_account):
) )
def set_voucher_clearance(doctype, docname, clearance_date, self): @frappe.whitelist()
if doctype in get_doctypes_for_bank_reconciliation(): def unclear_reference_payment(doctype, docname):
if ( if frappe.db.exists(doctype, docname):
doctype == "Payment Entry" doc = frappe.get_doc(doctype, docname)
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer" if doctype == "Sales Invoice":
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
):
return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
elif doctype == "Sales Invoice":
frappe.db.set_value( frappe.db.set_value(
"Sales Invoice Payment", "Sales Invoice Payment",
dict(parenttype=doctype, parent=docname), dict(parenttype=doc.payment_document, parent=doc.payment_entry),
"clearance_date", "clearance_date",
clearance_date, None,
) )
elif doctype == "Bank Transaction":
# For when a second bank transaction has fixed another, e.g. refund
bt = frappe.get_doc(doctype, docname)
if clearance_date:
vouchers = [{"payment_doctype": "Bank Transaction", "payment_name": self.name}]
bt.add_payment_entries(vouchers)
else: else:
for pe in bt.payment_entries: frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
bt.remove(pe)
bt.save()
break
return doc.payment_entry
def get_reconciled_bank_transactions(doctype, docname):
return frappe.get_all(
"Bank Transaction Payments",
filters={"payment_document": doctype, "payment_entry": docname},
pluck="parent",
)
@frappe.whitelist()
def unclear_reference_payment(doctype, docname, bt_name):
bt = frappe.get_doc("Bank Transaction", bt_name)
set_voucher_clearance(doctype, docname, None, bt)
return docname

View File

@@ -1,151 +0,0 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import nowdate
from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account
class TestAutoMatchParty(FrappeTestCase):
@classmethod
def setUpClass(cls):
create_bank_account()
frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 1)
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 1)
return super().setUpClass()
@classmethod
def tearDownClass(cls):
frappe.db.set_single_value("Accounts Settings", "enable_party_matching", 0)
frappe.db.set_single_value("Accounts Settings", "enable_fuzzy_matching", 0)
def test_match_by_account_number(self):
create_supplier_for_match(account_no="000000003716541159")
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="562213b0ca1bf838dab8f2c6a39bbc3b",
account_no="000000003716541159",
iban="DE02000000003716541159",
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "John Doe & Co.")
def test_match_by_iban(self):
create_supplier_for_match(iban="DE02000000003716541159")
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="c5455a224602afaa51592a9d9250600d",
account_no="000000003716541159",
iban="DE02000000003716541159",
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "John Doe & Co.")
def test_match_by_party_name(self):
create_supplier_for_match(supplier_name="Jackson Ella W.")
doc = create_bank_transaction(
withdrawal=1200,
transaction_id="1f6f661f347ff7b1ea588665f473adb1",
party_name="Ella Jackson",
iban="DE04000000003716545346",
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "Jackson Ella W.")
def test_match_by_description(self):
create_supplier_for_match(supplier_name="Microsoft")
doc = create_bank_transaction(
description="Auftraggeber: microsoft payments Buchungstext: msft ..e3006b5hdy. ref. j375979555927627/5536",
withdrawal=1200,
transaction_id="8df880a2d09c3bed3fea358ca5168c5a",
party_name="",
)
self.assertEqual(doc.party_type, "Supplier")
self.assertEqual(doc.party, "Microsoft")
def test_skip_match_if_multiple_close_results(self):
create_supplier_for_match(supplier_name="Adithya Medical & General Stores")
create_supplier_for_match(supplier_name="Adithya Medical And General Stores")
doc = create_bank_transaction(
description="Paracetamol Consignment, SINV-0009",
withdrawal=24.85,
transaction_id="3a1da4ee2dc5a980138d56ef3460cbd9",
party_name="Adithya Medical & General",
)
# Mapping is skipped as both Supplier names have the same match score
self.assertEqual(doc.party_type, None)
self.assertEqual(doc.party, None)
def create_supplier_for_match(supplier_name="John Doe & Co.", iban=None, account_no=None):
if frappe.db.exists("Supplier", {"supplier_name": supplier_name}):
# Update related Bank Account details
if not (iban or account_no):
return
frappe.db.set_value(
dt="Bank Account",
dn={"party": supplier_name},
field={"iban": iban, "bank_account_no": account_no},
)
return
# Create Supplier and Bank Account for the same
supplier = frappe.new_doc("Supplier")
supplier.supplier_name = supplier_name
supplier.supplier_group = "Services"
supplier.supplier_type = "Company"
supplier.insert()
if not frappe.db.exists("Bank", "TestBank"):
bank = frappe.new_doc("Bank")
bank.bank_name = "TestBank"
bank.insert(ignore_if_duplicate=True)
if not frappe.db.exists("Bank Account", supplier.name + " - " + "TestBank"):
bank_account = frappe.new_doc("Bank Account")
bank_account.account_name = supplier.name
bank_account.bank = "TestBank"
bank_account.iban = iban
bank_account.bank_account_no = account_no
bank_account.party_type = "Supplier"
bank_account.party = supplier.name
bank_account.insert()
def create_bank_transaction(
description=None,
withdrawal=0,
deposit=0,
transaction_id=None,
party_name=None,
account_no=None,
iban=None,
):
doc = frappe.new_doc("Bank Transaction")
doc.update(
{
"doctype": "Bank Transaction",
"description": description or "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
"date": nowdate(),
"withdrawal": withdrawal,
"deposit": deposit,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank",
"transaction_id": transaction_id,
"bank_party_name": party_name,
"bank_party_account_number": account_no,
"bank_party_iban": iban,
}
)
doc.insert()
doc.submit()
doc.reload()
return doc

View File

@@ -5,7 +5,6 @@ import json
import unittest import unittest
import frappe import frappe
from frappe import utils
from frappe.tests.utils import FrappeTestCase from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import ( from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
@@ -16,7 +15,6 @@ from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_paymen
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.tests.utils import if_lending_app_installed
test_dependencies = ["Item", "Cost Center"] test_dependencies = ["Item", "Cost Center"]
@@ -24,13 +22,14 @@ test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(FrappeTestCase): class TestBankTransaction(FrappeTestCase):
def setUp(self): def setUp(self):
for dt in [ for dt in [
"Loan Repayment",
"Bank Transaction", "Bank Transaction",
"Payment Entry", "Payment Entry",
"Payment Entry Reference", "Payment Entry Reference",
"POS Profile", "POS Profile",
]: ]:
frappe.db.delete(dt) frappe.db.delete(dt)
clear_loan_transactions()
make_pos_profile() make_pos_profile()
add_transactions() add_transactions()
add_vouchers() add_vouchers()
@@ -41,12 +40,7 @@ class TestBankTransaction(FrappeTestCase):
"Bank Transaction", "Bank Transaction",
dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"), dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"),
) )
linked_payments = get_linked_payments( linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
bank_transaction.name,
["payment_entry", "exact_match"],
from_date=bank_transaction.date,
to_date=utils.today(),
)
self.assertTrue(linked_payments[0][6] == "Conrad Electronic") self.assertTrue(linked_payments[0][6] == "Conrad Electronic")
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment # This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
@@ -87,12 +81,7 @@ class TestBankTransaction(FrappeTestCase):
"Bank Transaction", "Bank Transaction",
dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"), dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"),
) )
linked_payments = get_linked_payments( linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
bank_transaction.name,
["payment_entry", "exact_match"],
from_date=bank_transaction.date,
to_date=utils.today(),
)
self.assertTrue(linked_payments[0][3]) self.assertTrue(linked_payments[0][3])
# Check error if already reconciled # Check error if already reconciled
@@ -160,9 +149,8 @@ class TestBankTransaction(FrappeTestCase):
is not None is not None
) )
@if_lending_app_installed
def test_matching_loan_repayment(self): def test_matching_loan_repayment(self):
from lending.loan_management.doctype.loan.test_loan import create_loan_accounts from erpnext.loan_management.doctype.loan.test_loan import create_loan_accounts
create_loan_accounts() create_loan_accounts()
bank_account = frappe.get_doc( bank_account = frappe.get_doc(
@@ -191,11 +179,6 @@ class TestBankTransaction(FrappeTestCase):
self.assertEqual(linked_payments[0][2], repayment_entry.name) self.assertEqual(linked_payments[0][2], repayment_entry.name)
@if_lending_app_installed
def clear_loan_transactions():
frappe.db.delete("Loan Repayment")
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
try: try:
frappe.get_doc( frappe.get_doc(
@@ -406,18 +389,16 @@ def add_vouchers():
si.submit() si.submit()
@if_lending_app_installed
def create_loan_and_repayment(): def create_loan_and_repayment():
from lending.loan_management.doctype.loan.test_loan import ( from erpnext.loan_management.doctype.loan.test_loan import (
create_loan, create_loan,
create_loan_type, create_loan_type,
create_repayment_entry, create_repayment_entry,
make_loan_disbursement_entry, make_loan_disbursement_entry,
) )
from lending.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import ( from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import (
process_loan_interest_accrual_for_term_loans, process_loan_interest_accrual_for_term_loans,
) )
from erpnext.setup.doctype.employee.test_employee import make_employee from erpnext.setup.doctype.employee.test_employee import make_employee
create_loan_type( create_loan_type(

View File

@@ -125,27 +125,14 @@ def validate_expense_against_budget(args, expense_amount=0):
if not args.account: if not args.account:
return return
default_dimensions = [ for budget_against in ["project", "cost_center"] + get_accounting_dimensions():
{
"fieldname": "project",
"document_type": "Project",
},
{
"fieldname": "cost_center",
"document_type": "Cost Center",
},
]
for dimension in default_dimensions + get_accounting_dimensions(as_list=False):
budget_against = dimension.get("fieldname")
if ( if (
args.get(budget_against) args.get(budget_against)
and args.account and args.account
and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"}) and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
): ):
doctype = dimension.get("document_type") doctype = frappe.unscrub(budget_against)
if frappe.get_cached_value("DocType", doctype, "is_tree"): if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"]) lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
@@ -197,11 +184,6 @@ def validate_budget_records(args, budget_records, expense_amount):
amount = expense_amount or get_amount(args, budget) amount = expense_amount or get_amount(args, budget)
yearly_action, monthly_action = get_actions(args, budget) yearly_action, monthly_action = get_actions(args, budget)
if yearly_action in ("Stop", "Warn"):
compare_expense_with_budget(
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
)
if monthly_action in ["Stop", "Warn"]: if monthly_action in ["Stop", "Warn"]:
budget_amount = get_accumulated_monthly_budget( budget_amount = get_accumulated_monthly_budget(
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
@@ -213,28 +195,28 @@ def validate_budget_records(args, budget_records, expense_amount):
args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount args, budget_amount, _("Accumulated Monthly"), monthly_action, budget.budget_against, amount
) )
if (
yearly_action in ("Stop", "Warn")
and monthly_action != "Stop"
and yearly_action != monthly_action
):
compare_expense_with_budget(
args, flt(budget.budget_amount), _("Annual"), yearly_action, budget.budget_against, amount
)
def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0): def compare_expense_with_budget(args, budget_amount, action_for, action, budget_against, amount=0):
actual_expense = get_actual_expense(args) actual_expense = amount or get_actual_expense(args)
total_expense = actual_expense + amount
if total_expense > budget_amount:
if actual_expense > budget_amount: if actual_expense > budget_amount:
error_tense = _("is already")
diff = actual_expense - budget_amount diff = actual_expense - budget_amount
else:
error_tense = _("will be")
diff = total_expense - budget_amount
currency = frappe.get_cached_value("Company", args.company, "default_currency") currency = frappe.get_cached_value("Company", args.company, "default_currency")
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format( msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format(
_(action_for), _(action_for),
frappe.bold(args.account), frappe.bold(args.account),
frappe.unscrub(args.budget_against_field), args.budget_against_field,
frappe.bold(budget_against), frappe.bold(budget_against),
frappe.bold(fmt_money(budget_amount, currency=currency)), frappe.bold(fmt_money(budget_amount, currency=currency)),
error_tense,
frappe.bold(fmt_money(diff, currency=currency)), frappe.bold(fmt_money(diff, currency=currency)),
) )
@@ -245,9 +227,9 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
action = "Warn" action = "Warn"
if action == "Stop": if action == "Stop":
frappe.throw(msg, BudgetError, title=_("Budget Exceeded")) frappe.throw(msg, BudgetError)
else: else:
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded")) frappe.msgprint(msg, indicator="orange")
def get_actions(args, budget): def get_actions(args, budget):
@@ -369,9 +351,7 @@ def get_actual_expense(args):
""" """
select sum(gle.debit) - sum(gle.credit) select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle from `tabGL Entry` gle
where where gle.account=%(account)s
is_cancelled = 0
and gle.account=%(account)s
{condition1} {condition1}
and gle.fiscal_year=%(fiscal_year)s and gle.fiscal_year=%(fiscal_year)s
and gle.company=%(company)s and gle.company=%(company)s

View File

@@ -0,0 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cash Flow Mapper', {
});

View File

@@ -0,0 +1,275 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "field:section_name",
"beta": 0,
"creation": "2018-02-08 10:00:14.066519",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Section Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_header",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Section Header",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"description": "e.g Adjustments for:",
"fieldname": "section_leader",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Section Leader",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_subtotal",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Section Subtotal",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_footer",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Section Footer",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "accounts",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Accounts",
"length": 0,
"no_copy": 0,
"options": "Cash Flow Mapping Template Details",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "position",
"fieldtype": "Int",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Position",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-02-15 18:28:55.034933",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow Mapper",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "name",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class CashFlowMapper(Document):
pass

View File

@@ -0,0 +1,25 @@
DEFAULT_MAPPERS = [
{
"doctype": "Cash Flow Mapper",
"section_footer": "Net cash generated by operating activities",
"section_header": "Cash flows from operating activities",
"section_leader": "Adjustments for",
"section_name": "Operating Activities",
"position": 0,
"section_subtotal": "Cash generated from operations",
},
{
"doctype": "Cash Flow Mapper",
"position": 1,
"section_footer": "Net cash used in investing activities",
"section_header": "Cash flows from investing activities",
"section_name": "Investing Activities",
},
{
"doctype": "Cash Flow Mapper",
"position": 2,
"section_footer": "Net cash used in financing activites",
"section_header": "Cash flows from financing activities",
"section_name": "Financing Activities",
},
]

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestCashFlowMapper(unittest.TestCase):
pass

View File

@@ -0,0 +1,43 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cash Flow Mapping', {
refresh: function(frm) {
frm.events.disable_unchecked_fields(frm);
},
reset_check_fields: function(frm) {
frm.fields.filter(field => field.df.fieldtype === 'Check')
.map(field => frm.set_df_property(field.df.fieldname, 'read_only', 0));
},
has_checked_field(frm) {
const val = frm.fields.filter(field => field.value === 1);
return val.length ? 1 : 0;
},
_disable_unchecked_fields: function(frm) {
// get value of clicked field
frm.fields.filter(field => field.value === 0)
.map(field => frm.set_df_property(field.df.fieldname, 'read_only', 1));
},
disable_unchecked_fields: function(frm) {
frm.events.reset_check_fields(frm);
const checked = frm.events.has_checked_field(frm);
if (checked) {
frm.events._disable_unchecked_fields(frm);
}
},
is_working_capital: function(frm) {
frm.events.disable_unchecked_fields(frm);
},
is_finance_cost: function(frm) {
frm.events.disable_unchecked_fields(frm);
},
is_income_tax_liability: function(frm) {
frm.events.disable_unchecked_fields(frm);
},
is_income_tax_expense: function(frm) {
frm.events.disable_unchecked_fields(frm);
},
is_finance_cost_adjustment: function(frm) {
frm.events.disable_unchecked_fields(frm);
}
});

View File

@@ -0,0 +1,359 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 1,
"autoname": "field:mapping_name",
"beta": 0,
"creation": "2018-02-08 09:28:44.678364",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "mapping_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "label",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Label",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "accounts",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Accounts",
"length": 0,
"no_copy": 0,
"options": "Cash Flow Mapping Accounts",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "sb_1",
"fieldtype": "Section Break",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Select Maximum Of 1",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_finance_cost",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Finance Cost",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_working_capital",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Working Capital",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_finance_cost_adjustment",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Finance Cost Adjustment",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_income_tax_liability",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Income Tax Liability",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "0",
"fieldname": "is_income_tax_expense",
"fieldtype": "Check",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Is Income Tax Expense",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-02-15 08:25:18.693533",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow Mapping",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
},
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Administrator",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 0,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "name",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@@ -0,0 +1,22 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
import frappe
from frappe import _
from frappe.model.document import Document
class CashFlowMapping(Document):
def validate(self):
self.validate_checked_options()
def validate_checked_options(self):
checked_fields = [
d for d in self.meta.fields if d.fieldtype == "Check" and self.get(d.fieldname) == 1
]
if len(checked_fields) > 1:
frappe.throw(
_("You can only select a maximum of one option from the list of check boxes."),
title=_("Error"),
)

View File

@@ -0,0 +1,28 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
import frappe
class TestCashFlowMapping(unittest.TestCase):
def setUp(self):
if frappe.db.exists("Cash Flow Mapping", "Test Mapping"):
frappe.delete_doc("Cash Flow Mappping", "Test Mapping")
def tearDown(self):
frappe.delete_doc("Cash Flow Mapping", "Test Mapping")
def test_multiple_selections_not_allowed(self):
doc = frappe.new_doc("Cash Flow Mapping")
doc.mapping_name = "Test Mapping"
doc.label = "Test label"
doc.append("accounts", {"account": "Accounts Receivable - _TC"})
doc.is_working_capital = 1
doc.is_finance_cost = 1
self.assertRaises(frappe.ValidationError, doc.insert)
doc.is_finance_cost = 0
doc.insert()

View File

@@ -0,0 +1,73 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "field:account",
"beta": 0,
"creation": "2018-02-08 09:25:34.353995",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "account",
"fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "account",
"length": 0,
"no_copy": 0,
"options": "Account",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 1,
"max_attachments": 0,
"modified": "2018-02-08 09:25:34.353995",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow Mapping Accounts",
"name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class CashFlowMappingAccounts(Document):
pass

View File

@@ -0,0 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cash Flow Mapping Template', {
});

View File

@@ -0,0 +1,123 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"creation": "2018-02-08 10:20:18.316801",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"fields": [
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "template_name",
"fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 0,
"label": "Template Name",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
},
{
"allow_bulk_edit": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "mapping",
"fieldtype": "Table",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 0,
"in_standard_filter": 0,
"label": "Cash Flow Mapping",
"length": 0,
"no_copy": 0,
"options": "Cash Flow Mapping Template Details",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 0,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 1,
"search_index": 0,
"set_only_once": 0,
"unique": 0
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 0,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-02-08 10:20:18.316801",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Cash Flow Mapping Template",
"name_case": "",
"owner": "Administrator",
"permissions": [
{
"amend": 0,
"apply_user_permissions": 0,
"cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 0,
"write": 1
}
],
"quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1,
"track_seen": 0
}

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class CashFlowMappingTemplate(Document):
pass

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestCashFlowMappingTemplate(unittest.TestCase):
pass

View File

@@ -0,0 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
frappe.ui.form.on('Cash Flow Mapping Template Details', {
});

View File

@@ -1,34 +1,34 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1, "creation": "2018-02-08 10:18:48.513608",
"creation": "2023-06-20 14:01:35.362233",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"user" "mapping"
], ],
"fields": [ "fields": [
{ {
"fieldname": "user", "fieldname": "mapping",
"fieldtype": "Link", "fieldtype": "Link",
"in_list_view": 1, "in_list_view": 1,
"label": "User", "label": "Mapping",
"options": "User", "options": "Cash Flow Mapping",
"reqd": 1, "reqd": 1,
"search_index": 1 "unique": 1
} }
], ],
"index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2023-06-26 14:15:34.695605", "modified": "2022-02-21 03:34:57.902332",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Utilities", "module": "Accounts",
"name": "Portal User", "name": "Cash Flow Mapping Template Details",
"owner": "Administrator", "owner": "Administrator",
"permissions": [], "permissions": [],
"quick_entry": 1,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": [],
"track_changes": 1
} }

View File

@@ -0,0 +1,9 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from frappe.model.document import Document
class CashFlowMappingTemplateDetails(Document):
pass

View File

@@ -0,0 +1,8 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
import unittest
class TestCashFlowMappingTemplateDetails(unittest.TestCase):
pass

View File

@@ -36,7 +36,7 @@ def validate_columns(data):
no_of_columns = max([len(d) for d in data]) no_of_columns = max([len(d) for d in data])
if no_of_columns > 8: if no_of_columns > 7:
frappe.throw( frappe.throw(
_("More columns found than expected. Please compare the uploaded file with standard template"), _("More columns found than expected. Please compare the uploaded file with standard template"),
title=(_("Wrong Template")), title=(_("Wrong Template")),
@@ -233,7 +233,6 @@ def build_forest(data):
is_group, is_group,
account_type, account_type,
root_type, root_type,
account_currency,
) = i ) = i
if not account_name: if not account_name:
@@ -254,8 +253,6 @@ def build_forest(data):
charts_map[account_name]["account_type"] = account_type charts_map[account_name]["account_type"] = account_type
if root_type: if root_type:
charts_map[account_name]["root_type"] = root_type charts_map[account_name]["root_type"] = root_type
if account_currency:
charts_map[account_name]["account_currency"] = account_currency
path = return_parent(data, account_name)[::-1] path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created paths.append(path) # List of path is created
line_no += 1 line_no += 1
@@ -318,21 +315,20 @@ def get_template(template_type):
"Is Group", "Is Group",
"Account Type", "Account Type",
"Root Type", "Root Type",
"Account Currency",
] ]
writer = UnicodeWriter() writer = UnicodeWriter()
writer.writerow(fields) writer.writerow(fields)
if template_type == "Blank Template": if template_type == "Blank Template":
for root_type in get_root_types(): for root_type in get_root_types():
writer.writerow(["", "", "", "", 1, "", root_type]) writer.writerow(["", "", "", 1, "", root_type])
for account in get_mandatory_group_accounts(): for account in get_mandatory_group_accounts():
writer.writerow(["", "", "", "", 1, account, "Asset"]) writer.writerow(["", "", "", 1, account, "Asset"])
for account_type in get_mandatory_account_types(): for account_type in get_mandatory_account_types():
writer.writerow( writer.writerow(
["", "", "", "", 0, account_type.get("account_type"), account_type.get("root_type")] ["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
) )
else: else:
writer = get_sample_template(writer) writer = get_sample_template(writer)
@@ -489,10 +485,6 @@ def set_default_accounts(company):
"default_payable_account": frappe.db.get_value( "default_payable_account": frappe.db.get_value(
"Account", {"company": company.name, "account_type": "Payable", "is_group": 0} "Account", {"company": company.name, "account_type": "Payable", "is_group": 0}
), ),
"default_provisional_account": frappe.db.get_value(
"Account",
{"company": company.name, "account_type": "Service Received But Not Billed", "is_group": 0},
),
} }
) )

View File

@@ -70,7 +70,7 @@ frappe.ui.form.on('Cost Center', {
} }
], ],
primary_action: function() { primary_action: function() {
let data = d.get_values(); var data = d.get_values();
if(data.cost_center_name === frm.doc.cost_center_name && data.cost_center_number === frm.doc.cost_center_number) { if(data.cost_center_name === frm.doc.cost_center_name && data.cost_center_number === frm.doc.cost_center_number) {
d.hide(); d.hide();
return; return;
@@ -91,8 +91,8 @@ frappe.ui.form.on('Cost Center', {
if(r.message) { if(r.message) {
frappe.set_route("Form", "Cost Center", r.message); frappe.set_route("Form", "Cost Center", r.message);
} else { } else {
frm.set_value("cost_center_name", data.cost_center_name); me.frm.set_value("cost_center_name", data.cost_center_name);
frm.set_value("cost_center_number", data.cost_center_number); me.frm.set_value("cost_center_number", data.cost_center_number);
} }
d.hide(); d.hide();
} }

View File

@@ -28,13 +28,8 @@ class InvalidDateError(frappe.ValidationError):
class CostCenterAllocation(Document): class CostCenterAllocation(Document):
def __init__(self, *args, **kwargs):
super(CostCenterAllocation, self).__init__(*args, **kwargs)
self._skip_from_date_validation = False
def validate(self): def validate(self):
self.validate_total_allocation_percentage() self.validate_total_allocation_percentage()
if not self._skip_from_date_validation:
self.validate_from_date_based_on_existing_gle() self.validate_from_date_based_on_existing_gle()
self.validate_backdated_allocation() self.validate_backdated_allocation()
self.validate_main_cost_center() self.validate_main_cost_center()

View File

@@ -6,7 +6,6 @@
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"api_details_section", "api_details_section",
"disabled",
"service_provider", "service_provider",
"api_endpoint", "api_endpoint",
"url", "url",
@@ -78,18 +77,12 @@
"label": "Service Provider", "label": "Service Provider",
"options": "frankfurter.app\nexchangerate.host\nCustom", "options": "frankfurter.app\nexchangerate.host\nCustom",
"reqd": 1 "reqd": 1
},
{
"default": "0",
"fieldname": "disabled",
"fieldtype": "Check",
"label": "Disabled"
} }
], ],
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2023-01-09 12:19:03.955906", "modified": "2022-01-10 15:51:14.521174",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Currency Exchange Settings", "name": "Currency Exchange Settings",

View File

@@ -1,14 +1,13 @@
// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Dunning", { frappe.ui.form.on("Dunning", {
setup: function (frm) { setup: function (frm) {
frm.set_query("sales_invoice", "overdue_payments", () => { frm.set_query("sales_invoice", () => {
return { return {
filters: { filters: {
docstatus: 1, docstatus: 1,
company: frm.doc.company, company: frm.doc.company,
customer: frm.doc.customer,
outstanding_amount: [">", 0], outstanding_amount: [">", 0],
status: "Overdue" status: "Overdue"
}, },
@@ -23,24 +22,14 @@ frappe.ui.form.on("Dunning", {
} }
}; };
}); });
frm.set_query("cost_center", () => {
return {
filters: {
company: frm.doc.company,
is_group: 0
}
};
});
frm.set_query("contact_person", erpnext.queries.contact_query);
frm.set_query("customer_address", erpnext.queries.address_query);
frm.set_query("company_address", erpnext.queries.company_address_query);
// cannot add rows manually, only via button "Fetch Overdue Payments"
frm.set_df_property("overdue_payments", "cannot_add_rows", true);
}, },
refresh: function (frm) { refresh: function (frm) {
frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1); frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1);
frm.set_df_property(
"sales_invoice",
"read_only",
frm.doc.__islocal ? 0 : 1
);
if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") { if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") {
frm.add_custom_button(__("Resolve"), () => { frm.add_custom_button(__("Resolve"), () => {
frm.set_value("status", "Resolved"); frm.set_value("status", "Resolved");
@@ -51,111 +40,42 @@ frappe.ui.form.on("Dunning", {
__("Payment"), __("Payment"),
function () { function () {
frm.events.make_payment_entry(frm); frm.events.make_payment_entry(frm);
}, __("Create") },__("Create")
); );
frm.page.set_inner_btn_group_as_primary(__("Create")); frm.page.set_inner_btn_group_as_primary(__("Create"));
} }
if (frm.doc.docstatus === 0) { if(frm.doc.docstatus > 0) {
frm.add_custom_button(__("Fetch Overdue Payments"), () => { frm.add_custom_button(__('Ledger'), function() {
erpnext.utils.map_current_doc({ frappe.route_options = {
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", "voucher_no": frm.doc.name,
source_doctype: "Sales Invoice", "from_date": frm.doc.posting_date,
date_field: "due_date", "to_date": frm.doc.posting_date,
target: frm, "company": frm.doc.company,
setters: { "show_cancelled_entries": frm.doc.docstatus === 2
customer: frm.doc.customer || undefined, };
}, frappe.set_route("query-report", "General Ledger");
get_query_filters: { }, __('View'));
docstatus: 1,
status: "Overdue",
company: frm.doc.company
},
allow_child_item_selection: true,
child_fieldname: "payment_schedule",
child_columns: ["due_date", "outstanding"],
});
});
}
frappe.dynamic_link = { doc: frm.doc, fieldname: 'customer', doctype: 'Customer' };
frm.toggle_display("customer_name", (frm.doc.customer_name && frm.doc.customer_name !== frm.doc.customer));
},
// When multiple companies are set up. in case company name is changed set default company address
company: function (frm) {
if (frm.doc.company) {
frappe.call({
method: "erpnext.setup.doctype.company.company.get_default_company_address",
args: { name: frm.doc.company, existing_address: frm.doc.company_address || "" },
debounce: 2000,
callback: function (r) {
frm.set_value("company_address", r && r.message || "");
}
});
if (frm.fields_dict.currency) {
const company_currency = erpnext.get_currency(frm.doc.company);
if (!frm.doc.currency) {
frm.set_value("currency", company_currency);
}
if (frm.doc.currency == company_currency) {
frm.set_value("conversion_rate", 1.0);
}
}
const company_doc = frappe.get_doc(":Company", frm.doc.company);
if (company_doc.default_letter_head) {
if (frm.fields_dict.letter_head) {
frm.set_value("letter_head", company_doc.default_letter_head);
}
}
} }
}, },
currency: function (frm) { overdue_days: function (frm) {
// this.set_dynamic_labels(); frappe.db.get_value(
const company_currency = erpnext.get_currency(frm.doc.company); "Dunning Type",
// Added `ignore_pricing_rule` to determine if document is loading after mapping from another doc {
if (frm.doc.currency && frm.doc.currency !== company_currency) { start_day: ["<", frm.doc.overdue_days],
frappe.call({ end_day: [">=", frm.doc.overdue_days],
method: "erpnext.setup.utils.get_exchange_rate",
args: {
transaction_date: frm.doc.posting_date,
from_currency: frm.doc.currency,
to_currency: company_currency,
args: "for_selling"
}, },
freeze: true, "dunning_type",
freeze_message: __("Fetching exchange rates ..."), (r) => {
callback: function(r) { if (r) {
const exchange_rate = flt(r.message); frm.set_value("dunning_type", r.dunning_type);
if (exchange_rate != frm.doc.conversion_rate) {
frm.set_value("conversion_rate", exchange_rate);
}
}
});
} else { } else {
frm.trigger("conversion_rate"); frm.set_value("dunning_type", "");
frm.set_value("rate_of_interest", "");
frm.set_value("dunning_fee", "");
} }
},
customer: (frm) => {
erpnext.utils.get_party_details(frm);
},
conversion_rate: function (frm) {
if (frm.doc.currency === erpnext.get_currency(frm.doc.company)) {
frm.set_value("conversion_rate", 1.0);
} }
);
// Make read only if Accounts Settings doesn't allow stale rates
frm.set_df_property("conversion_rate", "read_only", erpnext.stale_rate_allowed() ? 0 : 1);
},
customer_address: function (frm) {
erpnext.utils.get_address_display(frm, "customer_address");
},
company_address: function (frm) {
erpnext.utils.get_address_display(frm, "company_address");
}, },
dunning_type: function (frm) { dunning_type: function (frm) {
frm.trigger("get_dunning_letter_text"); frm.trigger("get_dunning_letter_text");
@@ -186,57 +106,44 @@ frappe.ui.form.on("Dunning", {
}); });
} }
}, },
due_date: function (frm) {
frm.trigger("calculate_overdue_days");
},
posting_date: function (frm) { posting_date: function (frm) {
frm.trigger("calculate_overdue_days"); frm.trigger("calculate_overdue_days");
}, },
rate_of_interest: function (frm) { rate_of_interest: function (frm) {
frm.trigger("calculate_interest"); frm.trigger("calculate_interest_and_amount");
},
outstanding_amount: function (frm) {
frm.trigger("calculate_interest_and_amount");
},
interest_amount: function (frm) {
frm.trigger("calculate_interest_and_amount");
}, },
dunning_fee: function (frm) { dunning_fee: function (frm) {
frm.trigger("calculate_totals"); frm.trigger("calculate_interest_and_amount");
}, },
overdue_payments_add: function (frm) { sales_invoice: function (frm) {
frm.trigger("calculate_totals"); frm.trigger("calculate_overdue_days");
},
overdue_payments_remove: function (frm) {
frm.trigger("calculate_totals");
}, },
calculate_overdue_days: function (frm) { calculate_overdue_days: function (frm) {
frm.doc.overdue_payments.forEach((row) => { if (frm.doc.posting_date && frm.doc.due_date) {
if (frm.doc.posting_date && row.due_date) {
const overdue_days = moment(frm.doc.posting_date).diff( const overdue_days = moment(frm.doc.posting_date).diff(
row.due_date, frm.doc.due_date,
"days" "days"
); );
frappe.model.set_value(row.doctype, row.name, "overdue_days", overdue_days); frm.set_value("overdue_days", overdue_days);
} }
});
}, },
calculate_interest: function (frm) { calculate_interest_and_amount: function (frm) {
frm.doc.overdue_payments.forEach((row) => { const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100;
const interest_per_day = frm.doc.rate_of_interest / 100 / 365; const interest_amount = flt((interest_per_year * cint(frm.doc.overdue_days)) / 365 || 0, precision('interest_amount'));
const interest = flt((interest_per_day * row.overdue_days * row.outstanding), precision("interest", row)); const dunning_amount = flt(interest_amount + frm.doc.dunning_fee, precision('dunning_amount'));
frappe.model.set_value(row.doctype, row.name, "interest", interest); const grand_total = flt(frm.doc.outstanding_amount + dunning_amount, precision('grand_total'));
}); frm.set_value("interest_amount", interest_amount);
}, frm.set_value("dunning_amount", dunning_amount);
calculate_totals: function (frm) { frm.set_value("grand_total", grand_total);
const total_interest = frm.doc.overdue_payments
.reduce((prev, cur) => prev + cur.interest, 0);
const total_outstanding = frm.doc.overdue_payments
.reduce((prev, cur) => prev + cur.outstanding, 0);
const dunning_amount = total_interest + frm.doc.dunning_fee;
const base_dunning_amount = dunning_amount * frm.doc.conversion_rate;
const grand_total = total_outstanding + dunning_amount;
function setWithPrecison(field, value) {
frm.set_value(field, flt(value, precision(field)));
}
setWithPrecison("total_outstanding", total_outstanding);
setWithPrecison("total_interest", total_interest);
setWithPrecison("dunning_amount", dunning_amount);
setWithPrecison("base_dunning_amount", base_dunning_amount);
setWithPrecison("grand_total", grand_total);
}, },
make_payment_entry: function (frm) { make_payment_entry: function (frm) {
return frappe.call({ return frappe.call({
@@ -253,9 +160,3 @@ frappe.ui.form.on("Dunning", {
}); });
}, },
}); });
frappe.ui.form.on("Overdue Payment", {
interest: function (frm) {
frm.trigger("calculate_totals");
}
});

View File

@@ -2,60 +2,49 @@
"actions": [], "actions": [],
"allow_events_in_timeline": 1, "allow_events_in_timeline": 1,
"autoname": "naming_series:", "autoname": "naming_series:",
"beta": 1,
"creation": "2019-07-05 16:34:31.013238", "creation": "2019-07-05 16:34:31.013238",
"doctype": "DocType", "doctype": "DocType",
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"title",
"naming_series", "naming_series",
"sales_invoice",
"customer", "customer",
"customer_name", "customer_name",
"outstanding_amount",
"currency",
"conversion_rate",
"column_break_3", "column_break_3",
"company", "company",
"posting_date", "posting_date",
"posting_time", "posting_time",
"status", "due_date",
"section_break_9", "overdue_days",
"currency",
"column_break_11",
"conversion_rate",
"address_and_contact_section", "address_and_contact_section",
"customer_address",
"address_display", "address_display",
"contact_person",
"contact_display", "contact_display",
"column_break_16",
"company_address",
"company_address_display",
"contact_mobile", "contact_mobile",
"contact_email", "contact_email",
"column_break_18",
"company_address_display",
"section_break_6", "section_break_6",
"dunning_type", "dunning_type",
"dunning_fee",
"column_break_8", "column_break_8",
"rate_of_interest", "rate_of_interest",
"interest_amount",
"section_break_12", "section_break_12",
"overdue_payments",
"section_break_28",
"total_interest",
"dunning_fee",
"column_break_17",
"dunning_amount", "dunning_amount",
"base_dunning_amount",
"section_break_32",
"spacer",
"column_break_33",
"total_outstanding",
"grand_total", "grand_total",
"printing_settings_section", "income_account",
"column_break_17",
"status",
"printing_setting_section",
"language", "language",
"body_text", "body_text",
"column_break_22", "column_break_22",
"letter_head", "letter_head",
"closing_text", "closing_text",
"accounting_details_section",
"income_account",
"column_break_48",
"cost_center",
"amended_from" "amended_from"
], ],
"fields": [ "fields": [
@@ -71,17 +60,32 @@
"fieldname": "naming_series", "fieldname": "naming_series",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Series", "label": "Series",
"options": "DUNN-.MM.-.YY.-", "options": "DUNN-.MM.-.YY.-"
"print_hide": 1
}, },
{ {
"fetch_from": "customer.customer_name", "fieldname": "sales_invoice",
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Sales Invoice",
"options": "Sales Invoice",
"reqd": 1
},
{
"fetch_from": "sales_invoice.customer_name",
"fieldname": "customer_name", "fieldname": "customer_name",
"fieldtype": "Data", "fieldtype": "Data",
"in_list_view": 1, "in_list_view": 1,
"label": "Customer Name", "label": "Customer Name",
"read_only": 1 "read_only": 1
}, },
{
"fetch_from": "sales_invoice.outstanding_amount",
"fieldname": "outstanding_amount",
"fieldtype": "Currency",
"label": "Outstanding Amount",
"read_only": 1
},
{ {
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break" "fieldtype": "Column Break"
@@ -90,8 +94,13 @@
"default": "Today", "default": "Today",
"fieldname": "posting_date", "fieldname": "posting_date",
"fieldtype": "Date", "fieldtype": "Date",
"label": "Date", "label": "Date"
"reqd": 1 },
{
"fieldname": "overdue_days",
"fieldtype": "Int",
"label": "Overdue Days",
"read_only": 1
}, },
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
@@ -103,7 +112,16 @@
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Dunning Type", "label": "Dunning Type",
"options": "Dunning Type" "options": "Dunning Type",
"reqd": 1
},
{
"default": "0",
"fieldname": "interest_amount",
"fieldtype": "Currency",
"label": "Interest Amount",
"precision": "2",
"read_only": 1
}, },
{ {
"fieldname": "column_break_8", "fieldname": "column_break_8",
@@ -116,7 +134,6 @@
"fieldname": "dunning_fee", "fieldname": "dunning_fee",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Dunning Fee", "label": "Dunning Fee",
"options": "currency",
"precision": "2" "precision": "2"
}, },
{ {
@@ -127,24 +144,36 @@
"fieldname": "column_break_17", "fieldname": "column_break_17",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "printing_setting_section",
"fieldtype": "Section Break",
"label": "Printing Setting"
},
{ {
"fieldname": "language", "fieldname": "language",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Print Language", "label": "Print Language",
"options": "Language", "options": "Language"
"print_hide": 1
}, },
{ {
"fieldname": "letter_head", "fieldname": "letter_head",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Letter Head", "label": "Letter Head",
"options": "Letter Head", "options": "Letter Head"
"print_hide": 1
}, },
{ {
"fieldname": "column_break_22", "fieldname": "column_break_22",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fetch_from": "sales_invoice.currency",
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency",
"read_only": 1
},
{ {
"fieldname": "amended_from", "fieldname": "amended_from",
"fieldtype": "Link", "fieldtype": "Link",
@@ -154,6 +183,14 @@
"print_hide": 1, "print_hide": 1,
"read_only": 1 "read_only": 1
}, },
{
"allow_on_submit": 1,
"default": "{customer_name}",
"fieldname": "title",
"fieldtype": "Data",
"hidden": 1,
"label": "Title"
},
{ {
"fieldname": "body_text", "fieldname": "body_text",
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
@@ -164,6 +201,13 @@
"fieldtype": "Text Editor", "fieldtype": "Text Editor",
"label": "Closing Text" "label": "Closing Text"
}, },
{
"fetch_from": "sales_invoice.due_date",
"fieldname": "due_date",
"fieldtype": "Date",
"label": "Due Date",
"read_only": 1
},
{ {
"fieldname": "posting_time", "fieldname": "posting_time",
"fieldtype": "Time", "fieldtype": "Time",
@@ -178,37 +222,44 @@
"label": "Rate of Interest (%) Yearly" "label": "Rate of Interest (%) Yearly"
}, },
{ {
"collapsible": 1,
"fieldname": "address_and_contact_section", "fieldname": "address_and_contact_section",
"fieldtype": "Section Break", "fieldtype": "Section Break",
"label": "Address and Contact" "label": "Address and Contact"
}, },
{ {
"fetch_from": "sales_invoice.address_display",
"fieldname": "address_display", "fieldname": "address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Address", "label": "Address",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_display",
"fieldname": "contact_display", "fieldname": "contact_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Contact", "label": "Contact",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_mobile",
"fieldname": "contact_mobile", "fieldname": "contact_mobile",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Mobile No", "label": "Mobile No",
"options": "Phone",
"read_only": 1 "read_only": 1
}, },
{ {
"fieldname": "column_break_18",
"fieldtype": "Column Break"
},
{
"fetch_from": "sales_invoice.company_address_display",
"fieldname": "company_address_display", "fieldname": "company_address_display",
"fieldtype": "Small Text", "fieldtype": "Small Text",
"label": "Company Address Display", "label": "Company Address",
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.contact_email",
"fieldname": "contact_email", "fieldname": "contact_email",
"fieldtype": "Data", "fieldtype": "Data",
"label": "Contact Email", "label": "Contact Email",
@@ -216,18 +267,18 @@
"read_only": 1 "read_only": 1
}, },
{ {
"fetch_from": "sales_invoice.customer",
"fieldname": "customer", "fieldname": "customer",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Customer", "label": "Customer",
"options": "Customer", "options": "Customer",
"reqd": 1 "read_only": 1
}, },
{ {
"default": "0", "default": "0",
"fieldname": "grand_total", "fieldname": "grand_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Grand Total", "label": "Grand Total",
"options": "currency",
"precision": "2", "precision": "2",
"read_only": 1 "read_only": 1
}, },
@@ -238,154 +289,36 @@
"fieldtype": "Select", "fieldtype": "Select",
"in_standard_filter": 1, "in_standard_filter": 1,
"label": "Status", "label": "Status",
"options": "Draft\nResolved\nUnresolved\nCancelled", "options": "Draft\nResolved\nUnresolved\nCancelled"
},
{
"fieldname": "dunning_amount",
"fieldtype": "Currency",
"hidden": 1,
"label": "Dunning Amount",
"read_only": 1 "read_only": 1
}, },
{ {
"description": "For dunning fee and interest",
"fetch_from": "dunning_type.income_account",
"fieldname": "income_account", "fieldname": "income_account",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Income Account", "label": "Income Account",
"options": "Account", "options": "Account"
"print_hide": 1
},
{
"fieldname": "overdue_payments",
"fieldtype": "Table",
"label": "Overdue Payments",
"options": "Overdue Payment"
},
{
"fieldname": "section_break_28",
"fieldtype": "Section Break"
},
{
"default": "0",
"fieldname": "total_interest",
"fieldtype": "Currency",
"label": "Total Interest",
"options": "currency",
"precision": "2",
"read_only": 1
},
{
"fieldname": "total_outstanding",
"fieldtype": "Currency",
"label": "Total Outstanding",
"options": "currency",
"read_only": 1
},
{
"fieldname": "customer_address",
"fieldtype": "Link",
"label": "Customer Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "contact_person",
"fieldtype": "Link",
"label": "Contact Person",
"options": "Contact",
"print_hide": 1
},
{
"default": "0",
"fieldname": "dunning_amount",
"fieldtype": "Currency",
"label": "Dunning Amount",
"options": "currency",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_details_section",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fetch_from": "dunning_type.cost_center",
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center",
"print_hide": 1
},
{
"collapsible": 1,
"fieldname": "printing_settings_section",
"fieldtype": "Section Break",
"label": "Printing Settings"
},
{
"fieldname": "section_break_32",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_33",
"fieldtype": "Column Break"
},
{
"fieldname": "spacer",
"fieldtype": "Data",
"hidden": 1,
"label": "Spacer",
"print_hide": 1,
"read_only": 1,
"report_hide": 1
},
{
"fieldname": "column_break_16",
"fieldtype": "Column Break"
},
{
"fieldname": "company_address",
"fieldtype": "Link",
"label": "Company Address",
"options": "Address",
"print_hide": 1
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Currency"
},
{
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
}, },
{ {
"fetch_from": "sales_invoice.conversion_rate",
"fieldname": "conversion_rate", "fieldname": "conversion_rate",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Conversion Rate" "hidden": 1,
}, "label": "Conversion Rate",
{
"default": "0",
"fieldname": "base_dunning_amount",
"fieldtype": "Currency",
"label": "Dunning Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1 "read_only": 1
},
{
"fieldname": "column_break_48",
"fieldtype": "Column Break"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2023-06-15 15:46:53.865712", "modified": "2020-08-03 18:55:43.683053",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning", "name": "Dunning",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
@@ -432,7 +365,6 @@
], ],
"sort_field": "modified", "sort_field": "modified",
"sort_order": "ASC", "sort_order": "ASC",
"states": [],
"title_field": "customer_name", "title_field": "customer_name",
"track_changes": 1 "track_changes": 1
} }

View File

@@ -1,150 +1,131 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt # For license information, please see license.txt
"""
# Accounting
1. Payment of outstanding invoices with dunning amount
- Debit full amount to bank
- Credit invoiced amount to receivables
- Credit dunning amount to interest and similar revenue
-> Resolves dunning automatically
"""
import json import json
import frappe import frappe
from frappe import _ from frappe.utils import cint, flt, getdate
from frappe.contacts.doctype.address.address import get_address_display
from frappe.utils import getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
from erpnext.controllers.accounts_controller import AccountsController from erpnext.controllers.accounts_controller import AccountsController
class Dunning(AccountsController): class Dunning(AccountsController):
def validate(self): def validate(self):
self.validate_same_currency() self.validate_overdue_days()
self.validate_overdue_payments() self.validate_amount()
self.validate_totals() if not self.income_account:
self.set_party_details() self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account")
self.set_dunning_level()
def validate_same_currency(self): def validate_overdue_days(self):
""" self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
Throw an error if invoice currency differs from dunning currency.
""" def validate_amount(self):
for row in self.overdue_payments: amounts = calculate_interest_and_amount(
invoice_currency = frappe.get_value("Sales Invoice", row.sales_invoice, "currency") self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
if invoice_currency != self.currency:
frappe.throw(
_(
"The currency of invoice {} ({}) is different from the currency of this dunning ({})."
).format(row.sales_invoice, invoice_currency, self.currency)
) )
if self.interest_amount != amounts.get("interest_amount"):
self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount"))
if self.dunning_amount != amounts.get("dunning_amount"):
self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount"))
if self.grand_total != amounts.get("grand_total"):
self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total"))
def validate_overdue_payments(self): def on_submit(self):
daily_interest = self.rate_of_interest / 100 / 365 self.make_gl_entries()
for row in self.overdue_payments: def on_cancel(self):
row.overdue_days = (getdate(self.posting_date) - getdate(row.due_date)).days or 0 if self.dunning_amount:
row.interest = row.outstanding * daily_interest * row.overdue_days self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def validate_totals(self): def make_gl_entries(self):
self.total_outstanding = sum(row.outstanding for row in self.overdue_payments) if not self.dunning_amount:
self.total_interest = sum(row.interest for row in self.overdue_payments) return
self.dunning_amount = self.total_interest + self.dunning_fee gl_entries = []
self.base_dunning_amount = self.dunning_amount * self.conversion_rate invoice_fields = [
self.grand_total = self.total_outstanding + self.dunning_amount "project",
"cost_center",
"debit_to",
"party_account_currency",
"conversion_rate",
"cost_center",
]
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
def set_party_details(self): accounting_dimensions = get_accounting_dimensions()
from erpnext.accounts.party import _get_party_details invoice_fields.extend(accounting_dimensions)
party_details = _get_party_details( dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
self.customer, default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
ignore_permissions=self.flags.ignore_permissions,
doctype=self.doctype,
company=self.company,
posting_date=self.get("posting_date"),
fetch_payment_terms_template=False,
party_address=self.customer_address,
company_address=self.get("company_address"),
)
for field in [
"customer_address",
"address_display",
"company_address",
"contact_person",
"contact_display",
"contact_mobile",
]:
self.set(field, party_details.get(field))
self.set("company_address_display", get_address_display(self.company_address)) gl_entries.append(
self.get_gl_dict(
def set_dunning_level(self): {
for row in self.overdue_payments: "account": inv.debit_to,
past_dunnings = frappe.get_all( "party_type": "Customer",
"Overdue Payment", "party": self.customer,
filters={ "due_date": self.due_date,
"payment_schedule": row.payment_schedule, "against": self.income_account,
"parent": ("!=", row.parent), "debit": dunning_in_company_currency,
"docstatus": 1, "debit_in_account_currency": self.dunning_amount,
"against_voucher": self.name,
"against_voucher_type": "Dunning",
"cost_center": inv.cost_center or default_cost_center,
"project": inv.project,
}, },
inv.party_account_currency,
item=inv,
)
)
gl_entries.append(
self.get_gl_dict(
{
"account": self.income_account,
"against": self.customer,
"credit": dunning_in_company_currency,
"cost_center": inv.cost_center or default_cost_center,
"credit_in_account_currency": self.dunning_amount,
"project": inv.project,
},
item=inv,
)
)
make_gl_entries(
gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
) )
row.dunning_level = len(past_dunnings) + 1
def resolve_dunning(doc, state): def resolve_dunning(doc, state):
"""
Check if all payments have been made and resolve dunning, if yes. Called
when a Payment Entry is submitted.
"""
for reference in doc.references: for reference in doc.references:
# Consider partial and full payments: if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0:
# Submitting full payment: outstanding_amount will be 0 dunnings = frappe.get_list(
# Submitting 1st partial payment: outstanding_amount will be the pending installment "Dunning",
# Cancelling full payment: outstanding_amount will revert to total amount filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")},
# Cancelling last partial payment: outstanding_amount will revert to pending amount ignore_permissions=True,
submit_condition = reference.outstanding_amount < reference.total_amount )
cancel_condition = reference.outstanding_amount <= reference.total_amount
if reference.reference_doctype == "Sales Invoice" and (
submit_condition if doc.docstatus == 1 else cancel_condition
):
state = "Resolved" if doc.docstatus == 2 else "Unresolved"
dunnings = get_linked_dunnings_as_per_state(reference.reference_name, state)
for dunning in dunnings: for dunning in dunnings:
resolve = True frappe.db.set_value("Dunning", dunning.name, "status", "Resolved")
dunning = frappe.get_doc("Dunning", dunning.get("name"))
for overdue_payment in dunning.overdue_payments:
outstanding_inv = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
outstanding_ps = frappe.get_value(
"Payment Schedule", overdue_payment.payment_schedule, "outstanding"
)
resolve = False if (outstanding_ps > 0 and outstanding_inv > 0) else True
dunning.status = "Resolved" if resolve else "Unresolved"
dunning.save()
def get_linked_dunnings_as_per_state(sales_invoice, state): def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
dunning = frappe.qb.DocType("Dunning") interest_amount = 0
overdue_payment = frappe.qb.DocType("Overdue Payment") grand_total = flt(outstanding_amount) + flt(dunning_fee)
if rate_of_interest:
return ( interest_per_year = flt(outstanding_amount) * flt(rate_of_interest) / 100
frappe.qb.from_(dunning) interest_amount = (interest_per_year * cint(overdue_days)) / 365
.join(overdue_payment) grand_total += flt(interest_amount)
.on(overdue_payment.parent == dunning.name) dunning_amount = flt(interest_amount) + flt(dunning_fee)
.select(dunning.name) return {
.where( "interest_amount": interest_amount,
(dunning.status == state) "grand_total": grand_total,
& (dunning.docstatus != 2) "dunning_amount": dunning_amount,
& (overdue_payment.sales_invoice == sales_invoice) }
)
).run(as_dict=True)
@frappe.whitelist() @frappe.whitelist()

View File

@@ -0,0 +1,12 @@
from frappe import _
def get_data():
return {
"fieldname": "dunning",
"non_standard_fieldnames": {
"Journal Entry": "reference_name",
"Payment Entry": "reference_name",
},
"transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}],
}

View File

@@ -1,197 +1,162 @@
# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt # See license.txt
import unittest
import frappe import frappe
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate, today from frappe.utils import add_days, nowdate, today
from erpnext import get_default_cost_center from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import (
unlink_payment_on_cancel_of_invoice, unlink_payment_on_cancel_of_invoice,
) )
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
create_dunning as create_dunning_from_sales_invoice,
)
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import ( from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import (
create_sales_invoice_against_cost_center, create_sales_invoice_against_cost_center,
) )
test_dependencies = ["Company", "Cost Center"]
class TestDunning(unittest.TestCase):
class TestDunning(FrappeTestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(self):
super().setUpClass() create_dunning_type()
create_dunning_type("First Notice", fee=0.0, interest=0.0, is_default=1) create_dunning_type_with_zero_interest_rate()
create_dunning_type("Second Notice", fee=10.0, interest=10.0, is_default=0)
unlink_payment_on_cancel_of_invoice() unlink_payment_on_cancel_of_invoice()
@classmethod @classmethod
def tearDownClass(cls): def tearDownClass(self):
unlink_payment_on_cancel_of_invoice(0) unlink_payment_on_cancel_of_invoice(0)
super().tearDownClass()
def test_dunning_without_fees(self): def test_dunning(self):
dunning = create_dunning(overdue_days=20) dunning = create_dunning()
amounts = calculate_interest_and_amount(
dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
)
self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
self.assertEqual(round(dunning.total_outstanding, 2), 100.00) def test_dunning_with_zero_interest_rate(self):
self.assertEqual(round(dunning.total_interest, 2), 0.00) dunning = create_dunning_with_zero_interest_rate()
self.assertEqual(round(dunning.dunning_fee, 2), 0.00) amounts = calculate_interest_and_amount(
self.assertEqual(round(dunning.dunning_amount, 2), 0.00) dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
self.assertEqual(round(dunning.grand_total, 2), 100.00) )
self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
self.assertEqual(round(amounts.get("grand_total"), 2), 120)
def test_dunning_with_fees_and_interest(self): def test_gl_entries(self):
dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC") dunning = create_dunning()
dunning.submit()
gl_entries = frappe.db.sql(
"""select account, debit, credit
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
order by account asc""",
dunning.name,
as_dict=1,
)
self.assertTrue(gl_entries)
expected_values = dict(
(d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
)
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
self.assertEqual(round(dunning.total_outstanding, 2), 100.00) def test_payment_entry(self):
self.assertEqual(round(dunning.total_interest, 2), 0.41) dunning = create_dunning()
self.assertEqual(round(dunning.dunning_fee, 2), 10.00)
self.assertEqual(round(dunning.dunning_amount, 2), 10.41)
self.assertEqual(round(dunning.grand_total, 2), 110.41)
def test_dunning_with_payment_entry(self):
dunning = create_dunning(overdue_days=15, dunning_type_name="Second Notice - _TC")
dunning.submit() dunning.submit()
pe = get_payment_entry("Dunning", dunning.name) pe = get_payment_entry("Dunning", dunning.name)
pe.reference_no = "1" pe.reference_no = "1"
pe.reference_date = nowdate() pe.reference_date = nowdate()
pe.paid_from_account_currency = dunning.currency
pe.paid_to_account_currency = dunning.currency
pe.source_exchange_rate = 1
pe.target_exchange_rate = 1
pe.insert() pe.insert()
pe.submit() pe.submit()
si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
self.assertEqual(si_doc.outstanding_amount, 0)
for overdue_payment in dunning.overdue_payments:
outstanding_amount = frappe.get_value(
"Sales Invoice", overdue_payment.sales_invoice, "outstanding_amount"
)
self.assertEqual(outstanding_amount, 0)
dunning.reload() def create_dunning():
self.assertEqual(dunning.status, "Resolved") posting_date = add_days(today(), -20)
due_date = add_days(today(), -15)
def test_dunning_and_payment_against_partially_due_invoice(self):
"""
Create SI with first installment overdue. Check impact of Dunning and Payment Entry.
"""
create_payment_terms_template_for_dunning()
sales_invoice = create_sales_invoice_against_cost_center( sales_invoice = create_sales_invoice_against_cost_center(
posting_date=add_days(today(), -1 * 6), posting_date=posting_date, due_date=due_date, status="Overdue"
qty=1,
rate=100,
do_not_submit=True,
) )
sales_invoice.payment_terms_template = "_Test 50-50 for Dunning" dunning_type = frappe.get_doc("Dunning Type", "First Notice")
sales_invoice.submit() dunning = frappe.new_doc("Dunning")
dunning = create_dunning_from_sales_invoice(sales_invoice.name) dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
self.assertEqual(len(dunning.overdue_payments), 1) dunning.outstanding_amount = sales_invoice.outstanding_amount
self.assertEqual(dunning.overdue_payments[0].payment_term, "_Test Payment Term 1 for Dunning") dunning.debit_to = sales_invoice.debit_to
dunning.currency = sales_invoice.currency
dunning.submit() dunning.company = sales_invoice.company
pe = get_payment_entry("Dunning", dunning.name) dunning.posting_date = nowdate()
pe.reference_no, pe.reference_date = "2", nowdate() dunning.due_date = sales_invoice.due_date
pe.insert() dunning.dunning_type = "First Notice"
pe.submit()
sales_invoice.load_from_db()
dunning.load_from_db()
self.assertEqual(sales_invoice.status, "Partly Paid")
self.assertEqual(sales_invoice.payment_schedule[0].outstanding, 0)
self.assertEqual(dunning.status, "Resolved")
# Test impact on cancellation of PE
pe.cancel()
sales_invoice.reload()
dunning.reload()
self.assertEqual(sales_invoice.status, "Overdue")
self.assertEqual(dunning.status, "Unresolved")
def create_dunning(overdue_days, dunning_type_name=None):
posting_date = add_days(today(), -1 * overdue_days)
sales_invoice = create_sales_invoice_against_cost_center(
posting_date=posting_date, qty=1, rate=100
)
dunning = create_dunning_from_sales_invoice(sales_invoice.name)
if dunning_type_name:
dunning_type = frappe.get_doc("Dunning Type", dunning_type_name)
dunning.dunning_type = dunning_type.name
dunning.rate_of_interest = dunning_type.rate_of_interest dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee dunning.dunning_fee = dunning_type.dunning_fee
dunning.income_account = dunning_type.income_account dunning.save()
dunning.cost_center = dunning_type.cost_center return dunning
return dunning.save()
def create_dunning_type(title, fee, interest, is_default): def create_dunning_with_zero_interest_rate():
company = "_Test Company" posting_date = add_days(today(), -20)
if frappe.db.exists("Dunning Type", f"{title} - _TC"): due_date = add_days(today(), -15)
return sales_invoice = create_sales_invoice_against_cost_center(
posting_date=posting_date, due_date=due_date, status="Overdue"
)
dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
dunning.outstanding_amount = sales_invoice.outstanding_amount
dunning.debit_to = sales_invoice.debit_to
dunning.currency = sales_invoice.currency
dunning.company = sales_invoice.company
dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date
dunning.dunning_type = "First Notice with 0% Rate of Interest"
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type") dunning_type = frappe.new_doc("Dunning Type")
dunning_type.dunning_type = title dunning_type.dunning_type = "First Notice"
dunning_type.company = company dunning_type.start_day = 10
dunning_type.is_default = is_default dunning_type.end_day = 20
dunning_type.dunning_fee = fee dunning_type.dunning_fee = 20
dunning_type.rate_of_interest = interest dunning_type.rate_of_interest = 8
dunning_type.income_account = get_income_account(company)
dunning_type.cost_center = get_default_cost_center(company)
dunning_type.append( dunning_type.append(
"dunning_letter_text", "dunning_letter_text",
{ {
"language": "en", "language": "en",
"body_text": "We have still not received payment for our invoice", "body_text": "We have still not received payment for our invoice ",
"closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.", "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
}, },
) )
dunning_type.insert() dunning_type.save()
def get_income_account(company): def create_dunning_type_with_zero_interest_rate():
return ( dunning_type = frappe.new_doc("Dunning Type")
frappe.get_value("Company", company, "default_income_account") dunning_type.dunning_type = "First Notice with 0% Rate of Interest"
or frappe.get_all( dunning_type.start_day = 10
"Account", dunning_type.end_day = 20
filters={"is_group": 0, "company": company}, dunning_type.dunning_fee = 20
or_filters={ dunning_type.rate_of_interest = 0
"report_type": "Profit and Loss", dunning_type.append(
"account_type": ("in", ("Income Account", "Temporary")), "dunning_letter_text",
{
"language": "en",
"body_text": "We have still not received payment for our invoice ",
"closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.",
}, },
limit=1,
pluck="name",
)[0]
) )
dunning_type.save()
def create_payment_terms_template_for_dunning():
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_term
create_payment_term("_Test Payment Term 1 for Dunning")
create_payment_term("_Test Payment Term 2 for Dunning")
if not frappe.db.exists("Payment Terms Template", "_Test 50-50 for Dunning"):
frappe.get_doc(
{
"doctype": "Payment Terms Template",
"template_name": "_Test 50-50 for Dunning",
"allocate_payment_based_on_payment_terms": 1,
"terms": [
{
"doctype": "Payment Terms Template Detail",
"payment_term": "_Test Payment Term 1 for Dunning",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 5,
},
{
"doctype": "Payment Terms Template Detail",
"payment_term": "_Test Payment Term 2 for Dunning",
"invoice_portion": 50.00,
"credit_days_based_on": "Day(s) after invoice date",
"credit_days": 10,
},
],
}
).insert()

View File

@@ -1,24 +1,8 @@
// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt // For license information, please see license.txt
frappe.ui.form.on("Dunning Type", { frappe.ui.form.on('Dunning Type', {
setup: function (frm) { // refresh: function(frm) {
frm.set_query("income_account", () => {
return { // }
filters: {
root_type: "Income",
is_group: 0,
company: frm.doc.company,
},
};
});
frm.set_query("cost_center", () => {
return {
filters: {
is_group: 0,
company: frm.doc.company,
},
};
});
},
}); });

View File

@@ -1,26 +1,23 @@
{ {
"actions": [], "actions": [],
"allow_rename": 1, "allow_rename": 1,
"beta": 1, "autoname": "field:dunning_type",
"creation": "2019-12-04 04:59:08.003664", "creation": "2019-12-04 04:59:08.003664",
"doctype": "DocType", "doctype": "DocType",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "engine": "InnoDB",
"field_order": [ "field_order": [
"dunning_type", "dunning_type",
"is_default", "overdue_interval_section",
"column_break_3", "start_day",
"company", "column_break_4",
"end_day",
"section_break_6", "section_break_6",
"dunning_fee", "dunning_fee",
"column_break_8", "column_break_8",
"rate_of_interest", "rate_of_interest",
"text_block_section", "text_block_section",
"dunning_letter_text", "dunning_letter_text"
"section_break_9",
"income_account",
"column_break_13",
"cost_center"
], ],
"fields": [ "fields": [
{ {
@@ -48,6 +45,10 @@
"fieldtype": "Table", "fieldtype": "Table",
"options": "Dunning Letter Text" "options": "Dunning Letter Text"
}, },
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{ {
"fieldname": "section_break_6", "fieldname": "section_break_6",
"fieldtype": "Section Break" "fieldtype": "Section Break"
@@ -56,62 +57,33 @@
"fieldname": "column_break_8", "fieldname": "column_break_8",
"fieldtype": "Column Break" "fieldtype": "Column Break"
}, },
{
"fieldname": "overdue_interval_section",
"fieldtype": "Section Break",
"label": "Overdue Interval"
},
{
"fieldname": "start_day",
"fieldtype": "Int",
"label": "Start Day"
},
{
"fieldname": "end_day",
"fieldtype": "Int",
"label": "End Day"
},
{ {
"fieldname": "rate_of_interest", "fieldname": "rate_of_interest",
"fieldtype": "Float", "fieldtype": "Float",
"in_list_view": 1, "in_list_view": 1,
"label": "Rate of Interest (%) Yearly" "label": "Rate of Interest (%) Yearly"
},
{
"default": "0",
"fieldname": "is_default",
"fieldtype": "Check",
"label": "Is Default"
},
{
"fieldname": "section_break_9",
"fieldtype": "Section Break",
"label": "Accounting Details"
},
{
"fieldname": "income_account",
"fieldtype": "Link",
"label": "Income Account",
"options": "Account"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "column_break_3",
"fieldtype": "Column Break"
},
{
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"fieldname": "column_break_13",
"fieldtype": "Column Break"
} }
], ],
"links": [ "links": [],
{ "modified": "2020-07-15 17:14:17.835074",
"link_doctype": "Dunning",
"link_fieldname": "dunning_type"
}
],
"modified": "2021-11-13 00:25:35.659283",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Dunning Type", "name": "Dunning Type",
"naming_rule": "By script",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {

View File

@@ -2,11 +2,9 @@
# For license information, please see license.txt # For license information, please see license.txt
import frappe # import frappe
from frappe.model.document import Document from frappe.model.document import Document
class DunningType(Document): class DunningType(Document):
def autoname(self): pass
company_abbr = frappe.get_value("Company", self.company, "abbr")
self.name = f"{self.dunning_type} - {company_abbr}"

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