Merge branch 'develop' of https://github.com/frappe/erpnext into move-exotel-to-separate-app

This commit is contained in:
Suraj Shetty 2023-02-25 13:34:23 +05:30
commit 64f439600b
758 changed files with 34839 additions and 20977 deletions

View File

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

View File

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

View File

@ -24,15 +24,14 @@ fi
if [ "$DB" == "mariadb" ];then
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL character_set_server = 'utf8mb4'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "CREATE DATABASE test_frappe"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
mysql --host 127.0.0.1 --port 3306 -u root -proot -e "FLUSH PRIVILEGES"
fi
if [ "$DB" == "postgres" ];then
@ -42,12 +41,17 @@ fi
install_whktml() {
wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
sudo chmod o+x /usr/local/bin/wkhtmltopdf
if [ "$(lsb_release -rs)" = "22.04" ]; then
wget -O /tmp/wkhtmltox.deb https://github.com/wkhtmltopdf/packaging/releases/download/0.12.6.1-2/wkhtmltox_0.12.6.1-2.jammy_amd64.deb
sudo apt install /tmp/wkhtmltox.deb
else
echo "Please update this script to support wkhtmltopdf for $(lsb_release -ds)"
exit 1
fi
}
install_whktml &
wkpid=$!
cd ~/frappe-bench || exit
@ -61,6 +65,8 @@ bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
wait $wkpid
bench start &> bench_run_logs.txt &
CI=Yes bench build --app frappe &
bench --site test_site reinstall --yes

View File

@ -9,8 +9,8 @@
"mail_password": "test",
"admin_password": "admin",
"root_login": "root",
"root_password": "travis",
"root_password": "root",
"host_name": "http://test_site:8000",
"install_apps": ["erpnext"],
"install_apps": ["payments", "erpnext"],
"throttle_user_limit": 100
}

32
.github/workflows/initiate_release.yml vendored Normal file
View File

@ -0,0 +1,32 @@
# This workflow is agnostic to branches. Only maintain on develop branch.
# To add/remove versions just modify the matrix.
name: Create weekly release pull requests
on:
schedule:
# 9:30 UTC => 3 PM IST Tuesday
- cron: "30 9 * * 2"
workflow_dispatch:
jobs:
release:
name: Release
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
version: ["13", "14"]
steps:
- uses: octokit/request-action@v2.x
with:
route: POST /repos/{owner}/{repo}/pulls
owner: frappe
repo: erpnext
title: |-
"chore: release v${{ matrix.version }}"
body: "Automated weekly release."
base: version-${{ matrix.version }}
head: version-${{ matrix.version }}-hotfix
env:
GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}

View File

@ -11,7 +11,7 @@ on:
workflow_dispatch:
concurrency:
group: patch-develop-${{ github.event.number }}
group: patch-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: true
jobs:
@ -25,7 +25,7 @@ jobs:
mysql:
image: mariadb:10.3
env:
MYSQL_ALLOW_EMPTY_PASSWORD: YES
MARIADB_ROOT_PASSWORD: 'root'
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3

View File

@ -13,10 +13,10 @@ jobs:
with:
fetch-depth: 0
persist-credentials: false
- name: Setup Node.js v14
- name: Setup Node.js
uses: actions/setup-node@v2
with:
node-version: 14
node-version: 18
- name: Setup dependencies
run: |
npm install @semantic-release/git @semantic-release/exec --no-save
@ -28,4 +28,4 @@ jobs:
GIT_AUTHOR_EMAIL: "developers@frappe.io"
GIT_COMMITTER_NAME: "Frappe PR Bot"
GIT_COMMITTER_EMAIL: "developers@frappe.io"
run: npx semantic-release
run: npx semantic-release

View File

@ -16,18 +16,18 @@ on:
workflow_dispatch:
inputs:
user:
description: 'user'
description: 'Frappe Framework repository user (add your username for forks)'
required: true
default: 'frappe'
type: string
branch:
description: 'Branch name'
description: 'Frappe Framework branch'
default: 'develop'
required: false
type: string
concurrency:
group: server-mariadb-develop-${{ github.event.number }}
group: server-mariadb-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: true
jobs:
@ -45,9 +45,9 @@ jobs:
services:
mysql:
image: mariadb:10.3
image: mariadb:10.6
env:
MYSQL_ALLOW_EMPTY_PASSWORD: YES
MARIADB_ROOT_PASSWORD: 'root'
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
@ -59,7 +59,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
python-version: '3.10'
python-version: '3.11'
- name: Check for valid Python & Merge Conflicts
run: |
@ -120,7 +120,7 @@ jobs:
FRAPPE_BRANCH: ${{ github.event.inputs.branch }}
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
run: 'cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --with-coverage --total-builds 4 --build-number ${{ matrix.container }}'
env:
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}

View File

@ -9,7 +9,7 @@ on:
types: [opened, labelled, synchronize, reopened]
concurrency:
group: server-postgres-develop-${{ github.event.number }}
group: server-postgres-develop-${{ github.event_name }}-${{ github.event.number || github.event_name == 'workflow_dispatch' && github.run_id || '' }}
cancel-in-progress: true
jobs:

View File

@ -9,6 +9,7 @@ pull_request_rules:
- author!=nabinhait
- author!=ankush
- author!=deepeshgarg007
- author!=frappe-pr-bot
- author!=mergify[bot]
- or:

View File

@ -16,8 +16,8 @@ repos:
- id: check-merge-conflict
- id: check-ast
- repo: https://gitlab.com/pycqa/flake8
rev: 3.9.2
- repo: https://github.com/PyCQA/flake8
rev: 5.0.4
hooks:
- id: flake8
additional_dependencies: [
@ -32,8 +32,8 @@ repos:
- id: black
additional_dependencies: ['click==8.0.4']
- repo: https://github.com/timothycrosley/isort
rev: 5.9.1
- repo: https://github.com/PyCQA/isort
rev: 5.12.0
hooks:
- id: isort
exclude: ".*setup.py$"

View File

@ -4,29 +4,26 @@
# the repo. Unless a later match takes precedence,
erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/assets/ @anandbaburajan @deepeshgarg007
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
erpnext/regional @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/selling @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/support/ @nextchamp-saqib @deepeshgarg007
pos* @nextchamp-saqib
erpnext/buying/ @marination @rohitwaghchaure @s-aga-r
erpnext/e_commerce/ @marination
erpnext/maintenance/ @marination @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @marination @rohitwaghchaure @s-aga-r
erpnext/portal/ @marination
erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r
erpnext/shopping_cart/ @marination
erpnext/stock/ @marination @rohitwaghchaure @s-aga-r
erpnext/buying/ @rohitwaghchaure @s-aga-r
erpnext/maintenance/ @rohitwaghchaure @s-aga-r
erpnext/manufacturing/ @rohitwaghchaure @s-aga-r
erpnext/quality_management/ @rohitwaghchaure @s-aga-r
erpnext/stock/ @rohitwaghchaure @s-aga-r
erpnext/subcontracting @rohitwaghchaure @s-aga-r
erpnext/crm/ @NagariaHussain
erpnext/education/ @rutwikhdev
erpnext/crm/ @NagariaHussain
erpnext/education/ @rutwikhdev
erpnext/projects/ @ruchamahabal
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination
erpnext/public/ @nextchamp-saqib @marination
erpnext/controllers/ @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure
erpnext/patches/ @deepeshgarg007 @nextchamp-saqib
.github/ @ankush
pyproject.toml @gavindsouza @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.
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.
4. [Telegram Group](https://t.me/erpnexthelp) - Get instant help from huge community of users.
4. [Telegram Group](https://erpnext_public.t.me) - Get instant help from huge community of users.
## Contributing
@ -82,6 +82,8 @@ GNU/General Public License (see [license.txt](license.txt))
The ERPNext code is licensed as GNU General Public License (v3) and the Documentation is licensed as Creative Commons (CC-BY-SA-3.0) and the copyright is owned by Frappe Technologies Pvt Ltd (Frappe) and Contributors.
By contributing to ERPNext, you agree that your contributions will be licensed under its GNU General Public License (v3).
## Logo and Trademark Policy
Please read our [Logo and Trademark Policy](TRADEMARK_POLICY.md).

View File

@ -76,7 +76,7 @@ def get(
def build_result(account, dates, gl_entries):
result = [[getdate(date), 0.0] for date in dates]
root_type = frappe.db.get_value("Account", account, "root_type")
root_type = frappe.get_cached_value("Account", account, "root_type")
# start with the first date
date_index = 0

View File

@ -378,7 +378,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
return
# check if books nor frozen till endate:
if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
if accounts_frozen_upto and getdate(end_date) <= getdate(accounts_frozen_upto):
end_date = get_last_day(add_days(accounts_frozen_upto, 1))
if via_journal_entry:

View File

@ -37,7 +37,7 @@ class Account(NestedSet):
def autoname(self):
from erpnext.accounts.utils import get_autoname_with_number
self.name = get_autoname_with_number(self.account_number, self.account_name, None, self.company)
self.name = get_autoname_with_number(self.account_number, self.account_name, self.company)
def validate(self):
from erpnext.accounts.utils import validate_field_number
@ -58,7 +58,7 @@ class Account(NestedSet):
def validate_parent(self):
"""Fetch Parent Details and validate parent account"""
if self.parent_account:
par = frappe.db.get_value(
par = frappe.get_cached_value(
"Account", self.parent_account, ["name", "is_group", "company"], as_dict=1
)
if not par:
@ -82,7 +82,7 @@ class Account(NestedSet):
def set_root_and_report_type(self):
if self.parent_account:
par = frappe.db.get_value(
par = frappe.get_cached_value(
"Account", self.parent_account, ["report_type", "root_type"], as_dict=1
)
@ -92,7 +92,7 @@ class Account(NestedSet):
self.root_type = par.root_type
if self.is_group:
db_value = frappe.db.get_value("Account", self.name, ["report_type", "root_type"], as_dict=1)
db_value = self.get_doc_before_save()
if db_value:
if self.report_type != db_value.report_type:
frappe.db.sql(
@ -111,13 +111,13 @@ class Account(NestedSet):
)
def validate_root_details(self):
# does not exists parent
if frappe.db.exists("Account", self.name):
if not frappe.db.get_value("Account", self.name, "parent_account"):
throw(_("Root cannot be edited."), RootNotEditable)
doc_before_save = self.get_doc_before_save()
if doc_before_save and not doc_before_save.parent_account:
throw(_("Root cannot be edited."), RootNotEditable)
if not self.parent_account and not self.is_group:
frappe.throw(_("The root account {0} must be a group").format(frappe.bold(self.name)))
throw(_("The root account {0} must be a group").format(frappe.bold(self.name)))
def validate_root_company_and_sync_account_to_children(self):
# ignore validation while creating new compnay or while syncing to child companies
@ -127,7 +127,9 @@ class Account(NestedSet):
return
ancestors = get_root_company(self.company)
if ancestors:
if frappe.get_value("Company", self.company, "allow_account_creation_against_child_company"):
if frappe.get_cached_value(
"Company", self.company, "allow_account_creation_against_child_company"
):
return
if not frappe.db.get_value(
"Account", {"account_name": self.account_name, "company": ancestors[0]}, "name"
@ -138,7 +140,7 @@ class Account(NestedSet):
if not descendants:
return
parent_acc_name_map = {}
parent_acc_name, parent_acc_number = frappe.db.get_value(
parent_acc_name, parent_acc_number = frappe.get_cached_value(
"Account", self.parent_account, ["account_name", "account_number"]
)
filters = {
@ -159,27 +161,28 @@ class Account(NestedSet):
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
def validate_group_or_ledger(self):
if self.get("__islocal"):
doc_before_save = self.get_doc_before_save()
if not doc_before_save or cint(doc_before_save.is_group) == cint(self.is_group):
return
existing_is_group = frappe.db.get_value("Account", self.name, "is_group")
if cint(self.is_group) != cint(existing_is_group):
if self.check_gle_exists():
throw(_("Account with existing transaction cannot be converted to ledger"))
elif self.is_group:
if self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected."))
elif self.check_if_child_exists():
throw(_("Account with child nodes cannot be set as ledger"))
if self.check_gle_exists():
throw(_("Account with existing transaction cannot be converted to ledger"))
elif self.is_group:
if self.account_type and not self.flags.exclude_account_type_check:
throw(_("Cannot covert to Group because Account Type is selected."))
elif self.check_if_child_exists():
throw(_("Account with child nodes cannot be set as ledger"))
def validate_frozen_accounts_modifier(self):
old_value = frappe.db.get_value("Account", self.name, "freeze_account")
if old_value and old_value != self.freeze_account:
frozen_accounts_modifier = frappe.db.get_value(
"Accounts Settings", None, "frozen_accounts_modifier"
)
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
throw(_("You are not authorized to set Frozen value"))
doc_before_save = self.get_doc_before_save()
if not doc_before_save or doc_before_save.freeze_account == self.freeze_account:
return
frozen_accounts_modifier = frappe.get_cached_value(
"Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
)
if not frozen_accounts_modifier or frozen_accounts_modifier not in frappe.get_roles():
throw(_("You are not authorized to set Frozen value"))
def validate_balance_must_be_debit_or_credit(self):
from erpnext.accounts.utils import get_balance_on
@ -223,9 +226,9 @@ class Account(NestedSet):
)
# validate if parent of child company account to be added is a group
if frappe.db.get_value("Account", self.parent_account, "is_group") and not frappe.db.get_value(
"Account", parent_acc_name_map[company], "is_group"
):
if frappe.get_cached_value(
"Account", self.parent_account, "is_group"
) and not frappe.get_cached_value("Account", parent_acc_name_map[company], "is_group"):
msg = _(
"While creating account for Child Company {0}, parent account {1} found as a ledger account."
).format(company_bold, parent_acc_name_bold)
@ -377,17 +380,15 @@ def validate_account_number(name, account_number, company):
@frappe.whitelist()
def update_account_number(name, account_name, account_number=None, from_descendant=False):
account = frappe.db.get_value("Account", name, "company", as_dict=True)
account = frappe.get_cached_doc("Account", name)
if not account:
return
old_acc_name, old_acc_number = frappe.db.get_value(
"Account", name, ["account_name", "account_number"]
)
old_acc_name, old_acc_number = account.account_name, account.account_number
# check if account exists in parent company
ancestors = get_ancestors_of("Company", account.company)
allow_independent_account_creation = frappe.get_value(
allow_independent_account_creation = frappe.get_cached_value(
"Company", account.company, "allow_account_creation_against_child_company"
)
@ -435,22 +436,24 @@ def update_account_number(name, account_name, account_number=None, from_descenda
@frappe.whitelist()
def merge_account(old, new, is_group, root_type, company):
# Validate properties before merging
if not frappe.db.exists("Account", new):
new_account = frappe.get_cached_doc("Account", new)
if not new_account:
throw(_("Account {0} does not exist").format(new))
val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"]))
if val != [cint(is_group), root_type, company]:
if (new_account.is_group, new_account.root_type, new_account.company) != (
cint(is_group),
root_type,
company,
):
throw(
_(
"""Merging is only possible if following properties are same in both records. Is Group, Root Type, Company"""
)
)
if is_group and frappe.db.get_value("Account", new, "parent_account") == old:
frappe.db.set_value(
"Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account")
)
if is_group and new_account.parent_account == old:
new_account.db_set("parent_account", frappe.get_cached_value("Account", old, "parent_account"))
frappe.rename_doc("Account", old, new, merge=1, force=1)

View File

@ -56,36 +56,41 @@ frappe.treeview_settings["Account"] = {
accounts = nodes;
}
const get_balances = frappe.call({
method: 'erpnext.accounts.utils.get_account_balances',
args: {
accounts: accounts,
company: cur_tree.args.company
},
});
frappe.db.get_single_value("Accounts Settings", "show_balance_in_coa").then((value) => {
if(value) {
get_balances.then(r => {
if (!r.message || r.message.length == 0) return;
const get_balances = frappe.call({
method: 'erpnext.accounts.utils.get_account_balances',
args: {
accounts: accounts,
company: cur_tree.args.company
},
});
for (let account of r.message) {
get_balances.then(r => {
if (!r.message || r.message.length == 0) return;
const node = cur_tree.nodes && cur_tree.nodes[account.value];
if (!node || node.is_root) continue;
for (let account of r.message) {
// show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? "Dr": "Cr";
const format = (value, currency) => format_currency(Math.abs(value), currency);
const node = cur_tree.nodes && cur_tree.nodes[account.value];
if (!node || node.is_root) continue;
if (account.balance!==undefined) {
node.parent && node.parent.find('.balance-area').remove();
$('<span class="balance-area pull-right">'
+ (account.balance_in_account_currency ?
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
+ format(account.balance, account.company_currency)
+ " " + dr_or_cr
+ '</span>').insertBefore(node.$ul);
}
// show Dr if positive since balance is calculated as debit - credit else show Cr
const balance = account.balance_in_account_currency || account.balance;
const dr_or_cr = balance > 0 ? "Dr": "Cr";
const format = (value, currency) => format_currency(Math.abs(value), currency);
if (account.balance!==undefined) {
node.parent && node.parent.find('.balance-area').remove();
$('<span class="balance-area pull-right">'
+ (account.balance_in_account_currency ?
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
+ format(account.balance, account.company_currency)
+ " " + dr_or_cr
+ '</span>').insertBefore(node.$ul);
}
}
});
}
});
},

View File

@ -53,7 +53,7 @@ def create_charts(
"account_number": account_number,
"account_type": child.get("account_type"),
"account_currency": child.get("account_currency")
or frappe.db.get_value("Company", company, "default_currency"),
or frappe.get_cached_value("Company", company, "default_currency"),
"tax_rate": child.get("tax_rate"),
}
)
@ -148,7 +148,7 @@ def get_charts_for_country(country, with_standard=False):
) or frappe.local.flags.allow_unverified_charts:
charts.append(content["name"])
country_code = frappe.db.get_value("Country", country, "code")
country_code = frappe.get_cached_value("Country", country, "code")
if country_code:
folders = ("verified",)
if frappe.local.flags.allow_unverified_charts:

View File

@ -2,397 +2,438 @@
"country_code": "at",
"name": "Austria - Chart of Accounts",
"tree": {
"Summe Abschreibungen und Aufwendungen": {
"7010 bis 7080 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {},
"7100 bis 7190 Sonstige Steuern": {
"account_type": "Tax"
},
"7200 bis 7290 Instandhaltung u. Reinigung durh Dritte, Entsorgung, Beleuchtung": {},
"7300 bis 7310 Transporte durch Dritte": {},
"7320 bis 7330 Kfz - Aufwand": {},
"7340 bis 7350 Reise- und Fahraufwand": {},
"7360 bis 7370 Tag- und N\u00e4chtigungsgelder": {},
"7380 bis 7390 Nachrichtenaufwand": {},
"7400 bis 7430 Miet- und Pachtaufwand": {},
"7440 bis 7470 Leasingaufwand": {},
"7480 bis 7490 Lizenzaufwand": {},
"7500 bis 7530 Aufwand f\u00fcr beigestelltes Personal": {},
"7540 bis 7570 Provisionen an Dritte": {},
"7580 bis 7590 Aufsichtsratsverg\u00fctungen": {},
"7610 bis 7620 Druckerzeugnisse und Vervielf\u00e4ltigungen": {},
"7650 bis 7680 Werbung und Repr\u00e4sentationen": {},
"7700 bis 7740 Versicherungen": {},
"7750 bis 7760 Beratungs- und Pr\u00fcfungsaufwand": {},
"7800 bis 7810 Schadensf\u00e4lle": {},
"7840 bis 7880 Verschiedene betriebliche Aufwendungen": {},
"7910 bis 7950 Aufwandsstellenrechung der Hersteller": {},
"Abschreibungen auf aktivierte Aufwendungen f\u00fcr das Ingangs. u. Erweitern des Betriebes": {},
"Abschreibungen vom Umlaufverm\u00f6gen, soweit diese die im Unternehmen \u00fcblichen Abschreibungen \u00fcbersteigen": {},
"Aufwandsstellenrechnung": {},
"Aus- und Fortbildung": {},
"Buchwert abgegangener Anlagen, ausgenommen Finanzanlagen": {},
"B\u00fcromaterial und Drucksorten": {},
"Fachliteratur und Zeitungen ": {},
"Herstellungskosten der zur Erzielung der Umsatzerl\u00f6se erbrachten Leistungen": {},
"Mitgliedsbeitr\u00e4ge": {},
"Skontoertr\u00e4ge auf sonstige betriebliche Aufwendungen": {},
"Sonstige betrieblichen Aufwendungen": {},
"Spenden und Trinkgelder": {},
"Spesen des Geldverkehrs": {},
"Verluste aus dem Abgang vom Anlageverm\u00f6gen, ausgenommen Finanzanlagen": {},
"Vertriebskosten": {},
"Verwaltungskosten": {},
"root_type": "Expense"
},
"Summe Betriebliche Ertr\u00e4ge": {
"4400 bis 4490 Erl\u00f6sschm\u00e4lerungen": {},
"4500 bis 4570 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {},
"4580 bis 4590 andere aktivierte Eigenleistungen": {},
"4600 bis 4620 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {},
"4630 bis 4650 Ertr\u00e4ge aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {},
"4660 bis 4670 Ertr\u00e4ge aus der Zuschreibung zum Anlageverm\u00f6gen, ausgen. Finanzanlagen": {},
"4700 bis 4790 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {},
"4800 bis 4990 \u00dcbrige betriebliche Ertr\u00e4ge": {},
"Erl\u00f6se 0 % Ausfuhrlieferungen/Drittl\u00e4nder": {},
"Erl\u00f6se 10 %": {},
"Erl\u00f6se 20 %": {},
"Erl\u00f6se aus im Inland stpfl. EG Lieferungen 10 % USt": {},
"Erl\u00f6se aus im Inland stpfl. EG Lieferungen 20 % USt": {},
"Erl\u00f6se i.g. Lieferungen (stfr)": {},
"root_type": "Income"
},
"Summe Eigenkapital R\u00fccklagen Abschlusskonten": {
"9000 bis 9180 Gezeichnetes bzw. gewidmetes Kapital": {
"account_type": "Equity"
},
"9200 bis 9290 Kapitalr\u00fccklagen": {
"account_type": "Equity"
},
"9300 bis 9380 Gewinnr\u00fccklagen": {
"account_type": "Equity"
},
"9400 bis 9590 Bewertungsreserven uns sonst. unversteuerte R\u00fccklagen": {
"account_type": "Equity"
},
"9600 bis 9690 Privat und Verrechnungskonten bei Einzelunternehmen und Personengesellschaften": {},
"9700 bis 9790 Einlagen stiller Gesellschafter ": {},
"9900 bis 9999 Evidenzkonten": {},
"Bilanzgewinn (-verlust )": {
"account_type": "Equity"
},
"Er\u00f6ffnungsbilanz": {},
"Gewinn- und Verlustrechnung": {},
"Schlussbilanz": {},
"nicht eingeforderte ausstehende Einlagen": {
"account_type": "Equity"
},
"root_type": "Equity"
},
"Summe Finanzertr\u00e4ge und Aufwendungen": {
"8000 bis 8040 Ertr\u00e4ge aus Beteiligungen": {},
"8050 bis 8090 Ertr\u00e4ge aus anderen Wertpapieren und Ausleihungen des Finanzanlageverm\u00f6gens": {},
"8100 bis 8130 Sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": {},
"8220 bis 8250 Aufwendungen aus Beteiligungen": {},
"8260 bis 8270 Aufwendungen aus sonst. Fiananzanlagen und aus Wertpapieren des Umlaufverm\u00f6gens": {},
"8280 bis 8340 Zinsen und \u00e4hnliche Aufwendungem": {},
"8400 bis 8440 Au\u00dferordentliche Ertr\u00e4ge": {},
"8450 bis 8490 Au\u00dferordentliche Aufwendungen": {},
"8500 bis 8590 Steuern vom Einkommen und vom Ertrag": {
"account_type": "Tax"
},
"8600 bis 8690 Aufl\u00f6sung unversteuerten R\u00fccklagen": {},
"8700 bis 8740 Aufl\u00f6sung von Kapitalr\u00fccklagen": {},
"8750 bis 8790 Aufl\u00f6sung von Gewinnr\u00fccklagen": {},
"8800 bis 8890 Zuweisung von unversteuerten R\u00fccklagen": {},
"Buchwert abgegangener Beteiligungen": {},
"Buchwert abgegangener Wertpapiere des Umlaufverm\u00f6gens": {},
"Buchwert abgegangener sonstiger Finanzanlagen": {},
"Erl\u00f6se aus dem Abgang von Beteiligungen": {},
"Erl\u00f6se aus dem Abgang von Wertpapieren des Umlaufverm\u00f6gens": {},
"Erl\u00f6se aus dem Abgang von sonstigen Finanzanlagen": {},
"Ertr\u00e4ge aus dem Abgang von und der Zuschreibung zu Finanzanlagen": {},
"Ertr\u00e4ge aus dem Abgang von und der Zuschreibung zu Wertpapieren des Umlaufverm\u00f6gens": {},
"Gewinabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {},
"nicht ausgenutzte Lieferantenskonti": {},
"root_type": "Income"
},
"Summe Fremdkapital": {
"3020 bis 3030 Steuerr\u00fcckstellungen": {},
"3040 bis 3090 Sonstige R\u00fcckstellungen": {},
"3110 bis 3170 Verbindlichkeiten gegen\u00fcber Kredidinstituten": {},
"3180 bis 3190 Verbindlichkeiten gegen\u00fcber Finanzinstituten": {},
"3380 bis 3390 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": {
"Klasse 0 Aktiva: Anlageverm\u00f6gen": {
"0100 Konzessionen ": {"account_type": "Fixed Asset"},
"0110 Patentrechte und Lizenzen ": {"account_type": "Fixed Asset"},
"0120 Datenverarbeitungsprogramme ": {"account_type": "Fixed Asset"},
"0130 Marken, Warenzeichen und Musterschutzrechte, sonstige Urheberrechte ": {"account_type": "Fixed Asset"},
"0140 Pacht- und Mietrechte ": {"account_type": "Fixed Asset"},
"0150 Bezugs- und ähnliche Rechte ": {"account_type": "Fixed Asset"},
"0160 Geschäfts-/Firmenwert ": {"account_type": "Fixed Asset"},
"0170 Umgründungsmehrwert ": {"account_type": "Fixed Asset"},
"0180 Geleistete Anzahlungen auf immaterielle Vermögensgegenstände": {"account_type": "Fixed Asset"},
"0190 Kumulierte Abschreibungen zu immateriellen Vermögensgegenständen ": {"account_type": "Fixed Asset"},
"0200 Unbebaute Grundstücke, soweit nicht landwirtschaftlich genutzt ": {"account_type": "Fixed Asset"},
"0210 Bebaute Grundstücke (Grundwert) ": {"account_type": "Fixed Asset"},
"0220 Landwirtschaftlich genutzte Grundstücke ": {"account_type": "Fixed Asset"},
"0230 Grundstücksgleiche Rechte ": {"account_type": "Fixed Asset"},
"0300 Betriebs- und Geschäftsgebäude auf eigenem Grund ": {"account_type": "Fixed Asset"},
"0310 Wohn- und Sozialgebäude auf eigenem Grund ": {"account_type": "Fixed Asset"},
"0320 Betriebs- und Geschäftsgebäude auf fremdem Grund ": {"account_type": "Fixed Asset"},
"0330 Wohn- und Sozialgebäude auf fremdem Grund ": {"account_type": "Fixed Asset"},
"0340 Grundstückseinrichtungen auf eigenem Grund ": {"account_type": "Fixed Asset"},
"0350 Grundstückseinrichtungen auf fremdem Grund ": {"account_type": "Fixed Asset"},
"0360 Bauliche Investitionen in fremden (gepachteten) Betriebs- und Geschäftsgebäuden": {"account_type": "Fixed Asset"},
"0370 Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgebäuden": {"account_type": "Fixed Asset"},
"0390 Kumulierte Abschreibungen zu Grundstücken ": {"account_type": "Fixed Asset"},
"0400 Maschinen und Geräte ": {"account_type": "Fixed Asset"},
"0500 Maschinenwerkzeuge ": {"account_type": "Fixed Asset"},
"0510 Allgemeine Werkzeuge und Handwerkzeuge ": {"account_type": "Fixed Asset"},
"0520 Prototypen, Formen, Modelle ": {"account_type": "Fixed Asset"},
"0530 Andere Erzeugungshilfsmittel (auch Softwarewerkzeuge)": {"account_type": "Fixed Asset"},
"0540 Hebezeuge und Montageanlagen ": {"account_type": "Fixed Asset"},
"0550 Geringwertige Vermögensgegenstände, soweit im Erzeugungsprozess ": {"account_type": "Fixed Asset"},
"0560 Festwerte technische Anlagen und Maschinen ": {"account_type": "Fixed Asset"},
"0590 Kumulierte Abschreibungen zu technischen Anlagen und Maschinen ": {"account_type": "Fixed Asset"},
"0600 Betriebs- und Geschäftsausstattung, soweit nicht gesondert angeführt ": {"account_type": "Fixed Asset"},
"0610 Andere Anlagen, soweit nicht gesondert angeführt ": {"account_type": "Fixed Asset"},
"0620 Büromaschinen, EDV-Anlagen ": {"account_type": "Fixed Asset"},
"0630 PKW und Kombis ": {"account_type": "Fixed Asset"},
"0640 LKW ": {"account_type": "Fixed Asset"},
"0650 Andere Beförderungsmittel ": {"account_type": "Fixed Asset"},
"0660 Gebinde ": {"account_type": "Fixed Asset"},
"0670 Geringwertige Vermögensgegenstände, soweit nicht im Erzeugungssprozess verwendet": {"account_type": "Fixed Asset"},
"0680 Festwerte außer technische Anlagen und Maschinen ": {"account_type": "Fixed Asset"},
"0690 Kumulierte Abschreibungen zu anderen Anlagen, Betriebs- und Geschäftsausstattung": {"account_type": "Fixed Asset"},
"0700 Geleistete Anzahlungen auf Sachanlagen ": {"account_type": "Fixed Asset"},
"0710 Anlagen in Bau ": {"account_type": "Fixed Asset"},
"0790 Kumulierte Abschreibungen zu geleisteten Anzahlungen auf Sachanlagen ": {"account_type": "Fixed Asset"},
"0800 Anteile an verbundenen Unternehmen ": {"account_type": "Fixed Asset"},
"0810 Beteiligungen an Gemeinschaftsunternehmen ": {"account_type": "Fixed Asset"},
"0820 Beteiligungen an angeschlossenen (assoziierten) Unternehmen ": {"account_type": "Fixed Asset"},
"0830 Eigene Anteile, Anteile an herrschenden oder mit Mehrheit beteiligten ": {"account_type": "Fixed Asset"},
"0840 Sonstige Beteiligungen ": {"account_type": "Fixed Asset"},
"0850 Ausleihungen an verbundene Unternehmen ": {"account_type": "Fixed Asset"},
"0860 Ausleihungen an Unternehmen mit Beteiligungsverhältnis": {"account_type": "Fixed Asset"},
"0870 Ausleihungen an Gesellschafter ": {"account_type": "Fixed Asset"},
"0880 Sonstige Ausleihungen ": {"account_type": "Fixed Asset"},
"0890 Anteile an Kapitalgesellschaften ohne Beteiligungscharakter ": {"account_type": "Fixed Asset"},
"0900 Anteile an Personengesellschaften ohne Beteiligungscharakter ": {"account_type": "Fixed Asset"},
"0910 Genossenschaftsanteile ohne Beteiligungscharakter ": {"account_type": "Fixed Asset"},
"0920 Anteile an Investmentfonds ": {"account_type": "Fixed Asset"},
"0930 Festverzinsliche Wertpapiere des Anlagevermögens ": {"account_type": "Fixed Asset"},
"0980 Geleistete Anzahlungen auf Finanzanlagen ": {"account_type": "Fixed Asset"},
"0990 Kumulierte Abschreibungen zu Finanzanlagen ": {"account_type": "Fixed Asset"},
"root_type": "Asset"
},
"Klasse 1 Aktiva: Vorr\u00e4te": {
"1000 Bezugsverrechnung": {"account_type": "Stock"},
"1100 Rohstoffe": {"account_type": "Stock"},
"1200 Bezogene Teile": {"account_type": "Stock"},
"1300 Hilfsstoffe": {"account_type": "Stock"},
"1350 Betriebsstoffe": {"account_type": "Stock"},
"1360 Vorrat Energietraeger": {"account_type": "Stock"},
"1400 Unfertige Erzeugnisse": {"account_type": "Stock"},
"1500 Fertige Erzeugnisse": {"account_type": "Stock"},
"1600 Handelswarenvorrat": {"account_type": "Stock Received But Not Billed"},
"1700 Noch nicht abrechenbare Leistungen": {"account_type": "Stock"},
"1900 Wertberichtigungen": {"account_type": "Stock"},
"1800 Geleistete Anzahlungen": {"account_type": "Stock"},
"1900 Wertberichtigungen": {"account_type": "Stock"},
"root_type": "Asset"
},
"Klasse 3 Passiva: Verbindlichkeiten": {
"3000 Allgemeine Verbindlichkeiten (Schuld)": {"account_type": "Payable"},
"3010 R\u00fcckstellungen f\u00fcr Pensionen": {"account_type": "Payable"},
"3020 Steuerr\u00fcckstellungen": {"account_type": "Tax"},
"3041 Sonstige R\u00fcckstellungen": {"account_type": "Payable"},
"3110 Verbindlichkeiten gegen\u00fcber Bank": {"account_type": "Payable"},
"3150 Verbindlichkeiten Darlehen": {"account_type": "Payable"},
"3185 Verbindlichkeiten Kreditkarte": {"account_type": "Payable"},
"3380 Verbindlichkeiten aus der Annahme gezogener Wechsel u. d. Ausstellungen eigener Wechsel": {
"account_type": "Payable"
},
"3400 bis 3470 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {},
"3600 bis 3690 Verbindlichkeiten im Rahmen der sozialen Sicherheit": {},
"3700 bis 3890 \u00dcbrige sonstige Verbindlichkeiten": {},
"3900 bis 3990 Passive Rechnungsabgrenzungsposten": {},
"Anleihen (einschlie\u00dflich konvertibler)": {},
"Erhaltene Anzahlungenauf Bestellungen": {},
"R\u00fcckstellungen f\u00fcr Abfertigung": {},
"R\u00fcckstellungen f\u00fcr Pensionen": {},
"USt. \u00a719 /art (reverse charge)": {
"3400 Verbindlichkeiten gegen\u00fc. verb. Untern., Verbindl. gegen\u00fc. Untern., mit denen eine Beteiligungsverh\u00e4lnis besteht": {},
"3460 Verbindlichkeiten gegenueber Gesellschaftern": {"account_type": "Payable"},
"3470 Einlagen stiller Gesellschafter": {"account_type": "Payable"},
"3585 Verbindlichkeiten Lohnsteuer": {"account_type": "Tax"},
"3590 Verbindlichkeiten Kommunalabgaben": {"account_type": "Tax"},
"3595 Verbindlichkeiten Dienstgeberbeitrag": {"account_type": "Tax"},
"3600 Verbindlichkeiten Sozialversicherung": {"account_type": "Payable"},
"3640 Verbindlichkeiten Loehne und Gehaelter": {"account_type": "Payable"},
"3700 Sonstige Verbindlichkeiten": {"account_type": "Payable"},
"3900 Passive Rechnungsabgrenzungsposten": {"account_type": "Payable"},
"3100 Anleihen (einschlie\u00dflich konvertibler)": {"account_type": "Payable"},
"3200 Erhaltene Anzahlungen auf Bestellungen": {"account_type": "Payable"},
"3040 R\u00fcckstellungen f\u00fcr Abfertigung": {"account_type": "Payable"},
"3530 USt. \u00a719 (reverse charge)": {
"account_type": "Tax"
},
"Umsatzsteuer": {},
"Umsatzsteuer Zahllast": {
"3500 Verbindlichkeiten aus Umsatzsteuer": {"account_type": "Tax"},
"3580 Umsatzsteuer Zahllast": {
"account_type": "Tax"
},
"Umsatzsteuer aus i.g. Erwerb 10%": {
"3510 Umsatzsteuer Inland 20%": {
"account_type": "Tax"
},
"Umsatzsteuer aus i.g. Erwerb 20%": {
"3515 Umsatzsteuer Inland 10%": {
"account_type": "Tax"
},
"3520 Umsatzsteuer aus i.g. Erwerb 20%": {
"account_type": "Tax"
},
"Umsatzsteuer aus i.g. Lieferungen 10%": {
"3525 Umsatzsteuer aus i.g. Erwerb 10%": {
"account_type": "Tax"
},
"Umsatzsteuer aus i.g. Lieferungen 20%": {
"account_type": "Tax"
},
"Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {},
"Verbindlichkeiten aus Lieferungen u. Leistungen EU": {
},
"3560 Umsatzsteuer-Evidenzkonto f\u00fcr erhaltene Anzahlungen auf Bestellungen": {},
"3360 Verbindlichkeiten aus Lieferungen u. Leistungen EU": {
"account_type": "Payable"
},
"Verbindlichkeiten aus Lieferungen u. Leistungen Inland": {
"3000 Verbindlichkeiten aus Lieferungen u. Leistungen Inland": {
"account_type": "Payable"
},
"Verbindlichkeiten aus Lieferungen u. Leistungen sonst. Ausland": {
"3370 Verbindlichkeiten aus Lieferungen u. Leistungen sonst. Ausland": {
"account_type": "Payable"
},
"Verbindlichkeiten gegen\u00fcber Gesellschaften": {},
"Verrechnung Finanzamt": {
"3400 Verbindlichkeiten gegen\u00fcber verbundenen Unternehmen": {},
"3570 Verrechnung Finanzamt": {
"account_type": "Tax"
},
"root_type": "Liability"
},
"Summe Kontoklasse 0 Anlageverm\u00f6gen": {
"44 bis 49 Sonstige Maschinen und maschinelle Anlagen": {},
"920 bis 930 Festverzinsliche Wertpapiere des Anlageverm\u00f6gens": {},
"940 bis 970 Sonstige Finanzanlagen, Wertrechte": {},
"Allgemeine Werkzeuge und Handwerkzeuge": {},
"Andere Bef\u00f6rderungsmittel": {},
"Andere Betriebs- und Gesch\u00e4ftsausstattung": {},
"Andere Erzeugungshilfsmittel": {},
"Anlagen im Bau": {},
"Anteile an Investmentfonds": {},
"Anteile an Kapitalgesellschaften ohne Beteiligungscharakter": {},
"Anteile an Personengesellschaften ohne Beteiligungscharakter": {},
"Anteile an verbundenen Unternehmen": {},
"Antriebsmaschinen": {},
"Aufwendungen f\u00fcs das Ingangssetzen u. Erweitern eines Betriebes": {},
"Ausleihungen an verbundene Unternehmen": {},
"Ausleihungen an verbundene Unternehmen, mit denen ein Beteiligungsverh\u00e4lnis besteht": {},
"Bauliche Investitionen in fremden (gepachteten) Betriebs- und Gesch\u00e4ftsgeb\u00e4uden": {},
"Bauliche Investitionen in fremden (gepachteten) Wohn- und Sozialgeb\u00e4uden": {},
"Bebaute Grundst\u00fccke (Grundwert)": {},
"Beheizungs- und Beleuchtungsanlagen": {},
"Beteiligungen an Gemeinschaftunternehmen": {},
"Beteiligungen an angeschlossenen (assoziierten) Unternehmen": {},
"Betriebs- und Gesch\u00e4ftsgeb\u00e4ude auf eigenem Grund": {},
"Betriebs- und Gesch\u00e4ftsgeb\u00e4ude auf fremdem Grund": {},
"B\u00fcromaschinen, EDV - Anlagen": {},
"Datenverarbeitungsprogramme": {},
"Energieversorgungsanlagen": {},
"Fertigungsmaschinen": {},
"Gebinde": {},
"Geleistete Anzahlungen": {},
"Genossenschaften ohne Beteiligungscharakter": {},
"Geringwertige Verm\u00f6gensgegenst\u00e4nde, soweit im Erzeugerprozess verwendet": {},
"Geringwertige Verm\u00f6gensgegenst\u00e4nde, soweit nicht im Erzeugungsprozess verwendet": {},
"Gesch\u00e4fts(Firmen)wert": {},
"Grundst\u00fcckseinrichtunten auf eigenem Grund": {},
"Grundst\u00fcckseinrichtunten auf fremdem Grund": {},
"Grundst\u00fccksgleiche Rechte": {},
"Hebezeuge und Montageanlagen": {},
"Konzessionen": {},
"Kumulierte Abschreibungen": {},
"LKW": {},
"Marken, Warenzeichen und Musterschutzrechte": {},
"Maschinenwerkzeuge": {},
"Nachrichten- und Kontrollanlagen": {},
"PKW": {},
"Pacht- und Mietrechte": {},
"Patentrechte und Lizenzen": {},
"Sonstige Ausleihungen": {},
"Sonstige Beteiligungen": {},
"Transportanlagen": {},
"Unbebaute Grundst\u00fccke": {},
"Vorrichtungen, Formen und Modelle": {},
"Wohn- und Sozialgeb\u00e4ude auf eigenem Grund": {},
"Wohn- und Sozialgeb\u00e4ude auf fremdem Grund": {},
},
"Klasse 2 Aktiva: Umlaufverm\u00f6gen, Rechnungsabgrenzungen": {
"2030 Forderungen aus Lieferungen und Leistungen Inland (0% USt, umsatzsteuerfrei)": {
"account_type": "Receivable"
},
"2010 Forderungen aus Lieferungen und Leistungen Inland (10% USt, umsatzsteuerfrei)": {
"account_type": "Receivable"
},
"2000 Forderungen aus Lieferungen und Leistungen Inland (20% USt, umsatzsteuerfrei)": {
"account_type": "Receivable"
},
"2040 Forderungen aus Lieferungen und Leistungen Inland (sonstiger USt-Satz)": {
"account_type": "Receivable"
},
"2100 Forderungen aus Lieferungen und Leistungen EU": {
"account_type": "Receivable"
},
"2150 Forderungen aus Lieferungen und Leistungen Ausland (Nicht-EU)": {
"account_type": "Receivable"
},
"2200 Forderungen gegen\u00fcber verbundenen Unternehmen": {
"account_type": "Receivable"
},
"2250 Forderungen gegen\u00fcber Unternehmen, mit denen ein Beteiligungsverh\u00e4ltnis besteht": {
"account_type": "Receivable"
},
"2300 Sonstige Forderungen und Verm\u00f6gensgegenst\u00e4nde": {
"account_type": "Receivable"
},
"2630 Sonstige Wertpapiere": {
"account_type": "Stock"
},
"2750 Kassenbest\u00e4nde in Fremdw\u00e4hrung": {
"account_type": "Cash"
},
"2900 Aktive Rechnungsabrenzungsposten": {
"account_type": "Receivable"
},
"2600 Anteile an verbundenen Unternehmen": {
"account_type": "Equity"
},
"2680 Besitzwechsel ohne Forderungen": {
"account_type": "Receivable"
},
"2950 Aktiviertes Disagio": {
"account_type": "Receivable"
},
"2610 Eigene Anteile und Wertpapiere an mit Mehrheit beteiligten Unternehmen": {
"account_type": "Receivable"
},
"2570 Einfuhrumsatzsteuer (bezahlt)": {"account_type": "Tax"},
"2460 Eingeforderte aber noch nicht eingezahlte Einlagen": {
"account_type": "Receivable"
},
"2180 Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Ausland": {
"account_type": "Receivable"
},
"2130 Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. EU": {
"account_type": "Receivable"
},
"2080 Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": {
"account_type": "Receivable"
},
"2270 Einzelwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": {
"account_type": "Receivable"
},
"2230 Einzelwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": {
"account_type": "Receivable"
},
"2470 Einzelwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": {
"account_type": "Receivable"
},
"2700 Kassenbestand": {
"account_type": "Cash"
},
"2190 Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. sonstiges Ausland": {
"account_type": "Receivable"
},
"2130 Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. EU": {
"account_type": "Receivable"
},
"2100 Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": {
"account_type": "Receivable"
},
"2280 Pauschalwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": {
"account_type": "Receivable"
},
"2240 Pauschalwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": {
"account_type": "Receivable"
},
"2480 Pauschalwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": {
"account_type": "Receivable"
},
"2740 Postwertzeichen": {
"account_type": "Cash"
},
"2780 Schecks in Euro": {
"account_type": "Cash"
},
"2800 Guthaben bei Bank": {
"account_type": "Bank"
},
"2801 Guthaben bei Bank - Sparkonto": {
"account_type": "Bank"
},
"2810 Guthaben bei Paypal": {
"account_type": "Bank"
},
"2930 Mietvorauszahlungen": {
"account_type": "Receivable"
},
"2980 Abgrenzung latenter Steuern": {
"account_type": "Receivable"
},
"2500 Vorsteuer": {
"account_type": "Receivable"
},
"2510 Vorsteuer Inland 10%": {
"account_type": "Tax"
},
"2895 Schwebende Geldbewegugen": {
"account_type": "Bank"
},
"2513 Vorsteuer Inland 5%": {
"account_type": "Tax"
},
"2515 Vorsteuer Inland 20%": {
"account_type": "Tax"
},
"2520 Vorsteuer aus innergemeinschaftlichem Erwerb 10%": {
"account_type": "Tax"
},
"2525 Vorsteuer aus innergemeinschaftlichem Erwerb 20%": {
"account_type": "Tax"
},
"2530 Vorsteuer \u00a719/Art 19 ( reverse charge ) ": {
"account_type": "Tax"
},
"2690 Wertberichtigungen zu Wertpapieren und Anteilen": {
"account_type": "Receivable"
},
"root_type": "Asset"
},
"Summe Personalaufwand": {
"6000 bis 6190 L\u00f6hne": {},
"6200 bis 6390 Geh\u00e4lter": {},
"6400 bis 6440 Aufwendungen f\u00fcr Abfertigungen": {},
"6450 bis 6490 Aufwendungen f\u00fcr Altersversorgung": {},
"6500 bis 6550 Gesetzlicher Sozialaufwand Arbeiter": {},
"6560 bis 6590 Gesetzlicher Sozialaufwand Angestellte": {},
"6600 bis 6650 Lohnabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {},
"6660 bis 6690 Gehaltsabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {},
"6700 bis 6890 Sonstige Sozialaufwendungen": {},
"Aufwandsstellenrechnung": {},
"Klasse 4: Betriebliche Erträge": {
"4000 Erlöse 20 %": {"account_type": "Income Account"},
"4020 Erl\u00f6se 0 % steuerbefreit": {"account_type": "Income Account"},
"4010 Erl\u00f6se 10 %": {"account_type": "Income Account"},
"4030 Erl\u00f6se 13 %": {"account_type": "Income Account"},
"4040 Erl\u00f6se 0 % innergemeinschaftliche Lieferungen": {"account_type": "Income Account"},
"4400 Erl\u00f6sreduktion 0 % steuerbefreit": {"account_type": "Expense Account"},
"4410 Erl\u00f6sreduktion 10 %": {"account_type": "Expense Account"},
"4420 Erl\u00f6sreduktion 20 %": {"account_type": "Expense Account"},
"4430 Erl\u00f6sreduktion 13 %": {"account_type": "Expense Account"},
"4440 Erl\u00f6sreduktion 0 % innergemeinschaftliche Lieferungen": {"account_type": "Expense Account"},
"4500 Ver\u00e4nderungen des Bestandes an fertigen und unfertigen Erzeugn. sowie an noch nicht abrechenbaren Leistungen": {"account_type": "Income Account"},
"4580 Aktivierte Eigenleistungen": {"account_type": "Income Account"},
"4600 Erl\u00f6se aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"},
"4630 Ertr\u00e4ge aus dem Abgang vom Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"},
"4660 Ertr\u00e4ge aus der Zuschreibung zum Anlageverm\u00f6gen, ausgen. Finanzanlagen": {"account_type": "Income Account"},
"4700 Ertr\u00e4ge aus der Aufl\u00f6sung von R\u00fcckstellungen": {"account_type": "Income Account"},
"4800 \u00dcbrige betriebliche Ertr\u00e4ge": {"account_type": "Income Account"},
"root_type": "Income"
},
"Klasse 5: Aufwand f\u00fcr Material und Leistungen": {
"5000 Einkauf Partnerleistungen": {"account_type": "Cost of Goods Sold"},
"5100 Verbrauch an Rohstoffen": {"account_type": "Cost of Goods Sold"},
"5200 Verbrauch von bezogenen Fertig- und Einzelteilen": {"account_type": "Cost of Goods Sold"},
"5300 Verbrauch von Hilfsstoffen": {"account_type": "Cost of Goods Sold"},
"5340 Verbrauch Verpackungsmaterial": {"account_type": "Cost of Goods Sold"},
"5470 Verbrauch von Kleinmaterial": {"account_type": "Cost of Goods Sold"},
"5450 Verbrauch von Reinigungsmaterial": {"account_type": "Cost of Goods Sold"},
"5400 Verbrauch von Betriebsstoffen": {"account_type": "Cost of Goods Sold"},
"5500 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {"account_type": "Cost of Goods Sold"},
"5600 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {"account_type": "Cost of Goods Sold"},
"5700 Bearbeitung durch Dritte": {"account_type": "Cost of Goods Sold"},
"5900 Aufwandsstellenrechnung Material": {"account_type": "Cost of Goods Sold"},
"5820 Skontoertr\u00e4ge (20% USt.)": {"account_type": "Income Account"},
"5810 Skontoertr\u00e4ge (10% USt.)": {"account_type": "Income Account"},
"5010 Handelswareneinkauf 10 %": {"account_type": "Cost of Goods Sold"},
"5020 Handelswareneinkauf 20 %": {"account_type": "Cost of Goods Sold"},
"5040 Handelswareneinkauf innergemeinschaftlicher Erwerb 10 % VSt/10 % USt": {"account_type": "Cost of Goods Sold"},
"5050 Handelswareneinkauf innergemeinschaftlicher Erwerb 20 % VSt/20 % USt": {"account_type": "Cost of Goods Sold"},
"5070 Handelswareneinkauf innergemeinschaftlicher Erwerb ohne Vorsteuerabzug und 10 % USt": {"account_type": "Cost of Goods Sold"},
"5080 Handelswareneinkauf innergemeinschaftlicher Erwerb ohne Vorsteuerabzug und 20 % USt": {"account_type": "Cost of Goods Sold"},
"root_type": "Expense"
},
"Summe Umlaufverm\u00f6gen": {
"2000 bis 2007 Forderungen aus Lief. und Leist. Inland": {
"account_type": "Receivable"
},
"2100 bis 2120 Forderungen aus Lief. und Leist. EU": {
"account_type": "Receivable"
},
"2150 bis 2170 Forderungen aus Lief. und Leist. Ausland": {
"account_type": "Receivable"
},
"2200 bis 2220 Forderungen gegen\u00fcber verbundenen Unternehmen": {
"account_type": "Receivable"
},
"2250 bis 2270 Forderungen gegen\u00fcber Unternehmen, mit denen ein Beteiligungsverh\u00e4ltnis besteht": {
"account_type": "Receivable"
},
"2300 bis 2460 Sonstige Forderungen und Verm\u00f6gensgegenst\u00e4nde": {
"account_type": "Receivable"
},
"2630 bis 2670 Sonstige Wertpapiere": {
"account_type": "Receivable"
},
"2750 bis 2770 Kassenbest\u00e4nde in Fremdw\u00e4hrung": {
"account_type": "Receivable"
},
"Aktive Rechnungsabrenzungsposten": {
"account_type": "Receivable"
},
"Anteile an verbundenen Unternehmen": {
"account_type": "Receivable"
},
"Bank / Guthaben bei Kreditinstituten": {
"account_type": "Receivable"
},
"Besitzwechsel ...": {
"account_type": "Receivable"
},
"Disagio": {
"account_type": "Receivable"
},
"Eigene Anteile (Wertpapiere)": {
"account_type": "Receivable"
},
"Einfuhrumsatzsteuer (bezahlt)": {},
"Eingeforderte aber noch nicht eingezahlte Einlagen": {
"account_type": "Receivable"
},
"Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Ausland": {
"account_type": "Receivable"
},
"Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. EU": {
"account_type": "Receivable"
},
"Einzelwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": {
"account_type": "Receivable"
},
"Einzelwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": {
"account_type": "Receivable"
},
"Einzelwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": {
"account_type": "Receivable"
},
"Einzelwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": {
"account_type": "Receivable"
},
"Kassenbestand": {
"account_type": "Receivable"
},
"Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. Ausland": {
"account_type": "Receivable"
},
"Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. EU": {
"account_type": "Receivable"
},
"Pauschalwertberichtigungen zu Forderungen aus Lief. und Leist. Inland ": {
"account_type": "Receivable"
},
"Pauschalwertberichtigungen zu Forderungen gegen\u00fcber Unternehmen mit denen ein Beteiligungsverh\u00e4ltnis besteht": {
"account_type": "Receivable"
},
"Pauschalwertberichtigungen zu Forderungen gegen\u00fcber verbundenen Unternehmen": {
"account_type": "Receivable"
},
"Pauschalwertberichtigungen zu sonstigen Forderungen und Verm\u00f6gensgegenst\u00e4nden": {
"account_type": "Receivable"
},
"Postwertzeichen": {
"account_type": "Receivable"
},
"Schecks in Inlandsw\u00e4hrung": {
"account_type": "Receivable"
},
"Sonstige Anteile": {
"account_type": "Receivable"
},
"Stempelmarken": {
"account_type": "Receivable"
},
"Steuerabgrenzung": {
"account_type": "Receivable"
},
"Unterschiedsbetrag gem. Abschnitt XII Pensionskassengesetz": {
"account_type": "Receivable"
},
"Unterschiedsbetrag zur gebotenen Pensionsr\u00fcckstellung": {
"account_type": "Receivable"
},
"Vorsteuer": {
"account_type": "Receivable"
},
"Vorsteuer aus ig. Erwerb 10%": {
"account_type": "Tax"
},
"Vorsteuer aus ig. Erwerb 20%": {
"account_type": "Tax"
},
"Vorsteuer \u00a719/Art 19 ( reverse charge ) ": {
"account_type": "Tax"
},
"Wertberichtigungen": {
"account_type": "Receivable"
},
"root_type": "Asset"
},
"Summe Vorr\u00e4te": {
"1000 bis 1090 Bezugsverrechnung": {},
"1100 bis 1190 Rohstoffe": {},
"1200 bis 1290 Bezogene Teile": {},
"1300 bis 1340 Hilfsstoffe": {},
"1350 bis 1390 Betriebsstoffe": {},
"1400 bis 1490 Unfertige Erzeugniss": {},
"1500 bis 1590 Fertige Erzeugniss": {},
"1600 bis 1690 Waren": {},
"1700 bis 1790 Noch nicht abgerechenbare Leistungen": {},
"1900 bis 1990 Wertberichtigungen": {},
"geleistete Anzahlungen": {},
"root_type": "Asset"
},
"Summe Wareneinsatz": {
"5100 bis 5190 Verbrauch an Rohstoffen": {},
"5200 bis 5290 Verbrauch von bezogenen Fertig- und Einzelteilen": {},
"5300 bis 5390 Verbrauch von Hilfsstoffen": {},
"5400 bis 5490 Verbrauch von Betriebsstoffen": {},
"5500 bis 5590 Verbrauch von Werkzeugen und anderen Erzeugungshilfsmittel": {},
"5600 bis 5690 Verbrauch von Brenn- und Treibstoffen, Energie und Wasser": {},
"5700 bis 5790 Sonstige bezogene Herstellungsleistungen": {},
"Aufwandsstellenrechnung": {},
"Skontoertr\u00e4ge auf Materialaufwand": {},
"Skontoertr\u00e4ge auf sonstige bezogene Herstellungsleistungen": {},
"Wareneinkauf 10 %": {},
"Wareneinkauf 20 %": {},
"Wareneinkauf igErwerb 10 % VSt/10 % USt": {},
"Wareneinkauf igErwerb 20 % VSt/20 % USt": {},
"Wareneinkauf igErwerb ohne Vorsteuerabzug und 10 % USt": {},
"Wareneinkauf igErwerb ohne Vorsteuerabzug und 20 % USt": {},
"Klasse 6: Personalaufwand": {
"6000 L\u00f6hne": {"account_type": "Payable"},
"6200 Geh\u00e4lter": {"account_type": "Payable"},
"6400 Aufwendungen f\u00fcr Abfertigungen": {"account_type": "Payable"},
"6450 Aufwendungen f\u00fcr Altersversorgung": {"account_type": "Payable"},
"6500 Gesetzlicher Sozialaufwand Arbeiter": {"account_type": "Payable"},
"6560 Gesetzlicher Sozialaufwand Angestellte": {"account_type": "Payable"},
"6600 Lohnabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {"account_type": "Payable"},
"6660 Gehaltsabh\u00e4ngige Abgaben und Pflichtbeitr\u00e4gte": {"account_type": "Payable"},
"6700 Sonstige Sozialaufwendungen": {"account_type": "Payable"},
"6900 Aufwandsstellenrechnung Personal": {"account_type": "Payable"},
"root_type": "Expense"
},
"Klasse 7: Abschreibungen und sonstige betriebliche Aufwendungen": {
"7010 Abschreibungen auf das Anlageverm\u00f6gen (ausgenommen Finanzanlagen)": {"account_type": "Depreciation"},
"7100 Sonstige Steuern und Geb\u00fchren": {"account_type": "Tax"},
"7200 Instandhaltung u. Reinigung durch Dritte, Entsorgung, Energie": {"account_type": "Expense Account"},
"7300 Transporte durch Dritte": {"account_type": "Expense Account"},
"7310 Fahrrad - Aufwand": {"account_type": "Expense Account"},
"7320 Kfz - Aufwand": {"account_type": "Expense Account"},
"7330 LKW - Aufwand": {"account_type": "Expense Account"},
"7340 Lastenrad - Aufwand": {"account_type": "Expense Account"},
"7350 Reise- und Fahraufwand": {"account_type": "Expense Account"},
"7360 Tag- und N\u00e4chtigungsgelder": {"account_type": "Expense Account"},
"7380 Nachrichtenaufwand": {"account_type": "Expense Account"},
"7400 Miet- und Pachtaufwand": {"account_type": "Expense Account"},
"7440 Leasingaufwand": {"account_type": "Expense Account"},
"7480 Lizenzaufwand": {"account_type": "Expense Account"},
"7500 Aufwand f\u00fcr beigestelltes Personal": {"account_type": "Expense Account"},
"7540 Provisionen an Dritte": {"account_type": "Expense Account"},
"7580 Aufsichtsratsverg\u00fctungen": {"account_type": "Expense Account"},
"7610 Druckerzeugnisse und Vervielf\u00e4ltigungen": {"account_type": "Expense Account"},
"7650 Werbung und Repr\u00e4sentationen": {"account_type": "Expense Account"},
"7700 Versicherungen": {"account_type": "Expense Account"},
"7750 Beratungs- und Pr\u00fcfungsaufwand": {"account_type": "Expense Account"},
"7800 Forderungsverluste und Schadensf\u00e4lle": {"account_type": "Expense Account"},
"7840 Verschiedene betriebliche Aufwendungen": {"account_type": "Expense Account"},
"7910 Aufwandsstellenrechung der Hersteller": {"account_type": "Expense Account"},
"7060 Sofortabschreibungen geringwertig": {"account_type": "Expense Account"},
"7070 Abschreibungen vom Umlaufverm\u00f6gen, soweit diese die im Unternehmen \u00fcblichen Abschreibungen \u00fcbersteigen": {"account_type": "Depreciation"},
"7900 Aufwandsstellenrechnung": {"account_type": "Expense Account"},
"7770 Aus- und Fortbildung": {"account_type": "Expense Account"},
"7820 Buchwert abgegangener Anlagen, ausgenommen Finanzanlagen": {"account_type": "Expense Account"},
"7600 B\u00fcromaterial und Drucksorten": {"account_type": "Expense Account"},
"7630 Fachliteratur und Zeitungen ": {"account_type": "Expense Account"},
"7960 Herstellungskosten der zur Erzielung der Umsatzerl\u00f6se erbrachten Leistungen": {"account_type": "Expense Account"},
"7780 Mitgliedsbeitr\u00e4ge": {"account_type": "Expense Account"},
"7880 Skontoertr\u00e4ge auf sonstige betriebliche Aufwendungen": {"account_type": "Expense Account"},
"7990 Sonstige betrieblichen Aufwendungen": {"account_type": "Expense Account"},
"7680 Spenden und Trinkgelder": {"account_type": "Expense Account"},
"7790 Spesen des Geldverkehrs": {"account_type": "Expense Account"},
"7830 Verluste aus dem Abgang vom Anlageverm\u00f6gen, ausgenommen Finanzanlagen": {"account_type": "Expense Account"},
"7970 Vertriebskosten": {"account_type": "Expense Account"},
"7980 Verwaltungskosten": {"account_type": "Expense Account"},
"root_type": "Expense"
},
"Klasse 8: Finanz- und ausserordentliche Ertr\u00e4ge und Aufwendungen": {
"8000 Ertr\u00e4ge aus Beteiligungen": {"account_type": "Income Account"},
"8050 Ertr\u00e4ge aus anderen Wertpapieren und Ausleihungen des Finanzanlageverm\u00f6gens": {"account_type": "Income Account"},
"8100 Zinsen aus Bankguthaben": {"account_type": "Income Account"},
"8110 Zinsen aus gewaehrten Darlehen": {"account_type": "Income Account"},
"8130 Verzugszinsenertraege": {"account_type": "Income Account"},
"8220 Aufwendungen aus Beteiligungen": {"account_type": "Expense Account"},
"8260 Aufwendungen aus sonst. Fiananzanlagen und aus Wertpapieren des Umlaufverm\u00f6gens": {},
"8280 Zinsen und \u00e4hnliche Aufwendungem": {"account_type": "Expense Account"},
"8400 Au\u00dferordentliche Ertr\u00e4ge": {"account_type": "Income Account"},
"8450 Au\u00dferordentliche Aufwendungen": {"account_type": "Expense Account"},
"8500 Steuern vom Einkommen und vom Ertrag": {
"account_type": "Tax"
},
"8600 Aufl\u00f6sung unversteuerten R\u00fccklagen": {"account_type": "Income Account"},
"8700 Aufl\u00f6sung von Kapitalr\u00fccklagen": {"account_type": "Income Account"},
"8750 Aufl\u00f6sung von Gewinnr\u00fccklagen": {"account_type": "Income Account"},
"8800 Zuweisung zu unversteuerten R\u00fccklagen": {"account_type": "Expense Account"},
"8900 Zuweisung zu Gewinnr\u00fccklagen": {"account_type": "Expense Account"},
"8100 Buchwert abgegangener Beteiligungen": {"account_type": "Expense Account"},
"8130 Buchwert abgegangener Wertpapiere des Umlaufverm\u00f6gens": {"account_type": "Expense Account"},
"8120 Buchwert abgegangener sonstiger Finanzanlagen": {"account_type": "Expense Account"},
"8990 Gewinnabfuhr bzw. Verlust\u00fcberrechnung aus Ergebnisabf\u00fchrungsvertr\u00e4gen": {"account_type": "Expense Account"},
"8350 nicht ausgenutzte Lieferantenskonti": {"account_type": "Expense Account"},
"root_type": "Income"
},
"Klasse 9 Passiva: Eigenkapital, R\u00fccklagen, stille Einlagen, Abschlusskonten": {
"9000 Gezeichnetes bzw. gewidmetes Kapital": {
"account_type": "Equity"
},
"9200 Kapitalr\u00fccklagen": {
"account_type": "Equity"
},
"9300 Gewinnr\u00fccklagen": {
"account_type": "Equity"
},
"9400 Bewertungsreserven uns sonst. unversteuerte R\u00fccklagen": {
"account_type": "Equity"
},
"9600 Private Entnahmen": {"account_type": "Equity"},
"9610 Privatsteuern": {"account_type": "Equity"},
"9700 Einlagen stiller Gesellschafter ": {"account_type": "Equity"},
"9900 Evidenzkonto": {"account_type": "Equity"},
"9800 Er\u00f6ffnungsbilanzkonto (EBK)": {"account_type": "Equity"},
"9880 Jahresergebnis laut Gewinn- und Verlustrechnung (G+V)": {"account_type": "Equity"},
"9850 Schlussbilanzkonto (SBK)": {"account_type": "Round Off"},
"9190 nicht eingeforderte ausstehende Einlagen und berechtigte Entnahmen von Gesellschaftern": {
"account_type": "Equity"
},
"root_type": "Equity"
}
}
}
}
}

View File

@ -1,38 +1,38 @@
{
"country_code": "de",
"name": "SKR03 mit Kontonummern",
"tree": {
"Aktiva": {
"is_group": 1,
"country_code": "de",
"name": "SKR03 mit Kontonummern",
"tree": {
"Aktiva": {
"is_group": 1,
"root_type": "Asset",
"A - Anlagevermögen": {
"is_group": 1,
"EDV-Software": {
"account_number": "0027",
"account_type": "Fixed Asset"
},
"Gesch\u00e4ftsausstattung": {
"account_number": "0410",
"account_type": "Fixed Asset"
},
"B\u00fcroeinrichtung": {
"account_number": "0420",
"account_type": "Fixed Asset"
},
"Darlehen": {
"account_number": "0565"
},
"Maschinen": {
"account_number": "0210",
"account_type": "Fixed Asset"
},
"Betriebsausstattung": {
"account_number": "0400",
"account_type": "Fixed Asset"
},
"Ladeneinrichtung": {
"account_number": "0430",
"account_type": "Fixed Asset"
"A - Anlagevermögen": {
"is_group": 1,
"EDV-Software": {
"account_number": "0027",
"account_type": "Fixed Asset"
},
"Geschäftsausstattung": {
"account_number": "0410",
"account_type": "Fixed Asset"
},
"Büroeinrichtung": {
"account_number": "0420",
"account_type": "Fixed Asset"
},
"Darlehen": {
"account_number": "0565"
},
"Maschinen": {
"account_number": "0210",
"account_type": "Fixed Asset"
},
"Betriebsausstattung": {
"account_number": "0400",
"account_type": "Fixed Asset"
},
"Ladeneinrichtung": {
"account_number": "0430",
"account_type": "Fixed Asset"
},
"Accumulated Depreciation": {
"account_type": "Accumulated Depreciation"
@ -60,36 +60,46 @@
"Durchlaufende Posten": {
"account_number": "1590"
},
"Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": {
"Verrechnungskonto Gewinnermittlung § 4 Abs. 3 EStG, nicht ergebniswirksam": {
"account_number": "1371"
},
"Abziehbare Vorsteuer": {
"account_type": "Tax",
"is_group": 1,
"Abziehbare Vorsteuer 7%": {
"account_number": "1571"
"Abziehbare Vorsteuer 7 %": {
"account_number": "1571",
"account_type": "Tax",
"tax_rate": 7.0
},
"Abziehbare Vorsteuer 19%": {
"account_number": "1576"
"Abziehbare Vorsteuer 19 %": {
"account_number": "1576",
"account_type": "Tax",
"tax_rate": 19.0
},
"Abziehbare Vorsteuer nach \u00a713b UStG 19%": {
"account_number": "1577"
},
"Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
"account_number": "3120"
"Abziehbare Vorsteuer nach § 13b UStG 19 %": {
"account_number": "1577",
"account_type": "Tax",
"tax_rate": 19.0
}
}
},
"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.": {
"is_group": 1,
"Kasse": {
"account_type": "Cash",
"is_group": 1,
"account_type": "Cash",
"Kasse": {
"is_group": 1,
"account_number": "1000",
"account_type": "Cash"
}
@ -111,21 +121,21 @@
"C - Rechnungsabgrenzungsposten": {
"is_group": 1,
"Aktive Rechnungsabgrenzung": {
"account_number": "0980"
"account_number": "0980"
}
},
"D - Aktive latente Steuern": {
"is_group": 1,
"Aktive latente Steuern": {
"account_number": "0983"
"account_number": "0983"
}
},
"E - Aktiver Unterschiedsbetrag aus der Vermögensverrechnung": {
"is_group": 1
}
},
"Passiva": {
"is_group": 1,
},
"Passiva": {
"is_group": 1,
"root_type": "Liability",
"A. Eigenkapital": {
"is_group": 1,
@ -200,26 +210,32 @@
},
"Umsatzsteuer": {
"is_group": 1,
"account_type": "Tax",
"Umsatzsteuer 7%": {
"account_number": "1771"
"Umsatzsteuer 7 %": {
"account_number": "1771",
"account_type": "Tax",
"tax_rate": 7.0
},
"Umsatzsteuer 19%": {
"account_number": "1776"
"Umsatzsteuer 19 %": {
"account_number": "1776",
"account_type": "Tax",
"tax_rate": 19.0
},
"Umsatzsteuer-Vorauszahlung": {
"account_number": "1780"
"account_number": "1780",
"account_type": "Tax"
},
"Umsatzsteuer-Vorauszahlung 1/11": {
"account_number": "1781"
},
"Umsatzsteuer \u00a7 13b UStG 19%": {
"account_number": "1787"
"Umsatzsteuer nach § 13b UStG 19 %": {
"account_number": "1787",
"account_type": "Tax",
"tax_rate": 19.0
},
"Umsatzsteuer Vorjahr": {
"account_number": "1790"
},
"Umsatzsteuer fr\u00fchere Jahre": {
"Umsatzsteuer frühere Jahre": {
"account_number": "1791"
}
}
@ -234,44 +250,56 @@
"E. Passive latente Steuern": {
"is_group": 1
}
},
"Erl\u00f6se u. Ertr\u00e4ge 2/8": {
"is_group": 1,
"root_type": "Income",
"Erl\u00f6skonten 8": {
},
"Erlöse u. Erträge 2/8": {
"is_group": 1,
"root_type": "Income",
"Erlöskonten 8": {
"is_group": 1,
"Erl\u00f6se": {
"account_number": "8200",
"account_type": "Income Account"
},
"Erl\u00f6se USt. 19%": {
"account_number": "8400",
"account_type": "Income Account"
},
"Erl\u00f6se USt. 7%": {
"account_number": "8300",
"account_type": "Income Account"
}
},
"Ertragskonten 2": {
"is_group": 1,
"sonstige Zinsen und \u00e4hnliche Ertr\u00e4ge": {
"account_number": "2650",
"account_type": "Income Account"
},
"Au\u00dferordentliche Ertr\u00e4ge": {
"account_number": "2500",
"account_type": "Income Account"
},
"Sonstige Ertr\u00e4ge": {
"account_number": "2700",
"account_type": "Income Account"
}
}
},
"Aufwendungen 2/4": {
"is_group": 1,
"Erlöse": {
"account_number": "8200",
"account_type": "Income Account"
},
"Erlöse USt. 19 %": {
"account_number": "8400",
"account_type": "Income Account"
},
"Erlöse USt. 7 %": {
"account_number": "8300",
"account_type": "Income Account"
}
},
"Ertragskonten 2": {
"is_group": 1,
"sonstige Zinsen und ähnliche Erträge": {
"account_number": "2650",
"account_type": "Income Account"
},
"Außerordentliche Erträge": {
"account_number": "2500",
"account_type": "Income Account"
},
"Sonstige Erträge": {
"account_number": "2700",
"account_type": "Income Account"
}
}
},
"Aufwendungen 2/4": {
"is_group": 1,
"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": {
"account_number": "3200"
},
@ -298,234 +326,234 @@
"Gegenkonto 4996-4998": {
"account_number": "4999"
},
"Abschreibungen": {
"is_group": 1,
"Abschreibungen": {
"is_group": 1,
"Abschreibungen auf Sachanlagen (ohne AfA auf Kfz und Gebäude)": {
"account_number": "4830",
"account_type": "Accumulated Depreciation"
"account_number": "4830",
"account_type": "Accumulated Depreciation"
},
"Abschreibungen auf Gebäude": {
"account_number": "4831",
"account_type": "Depreciation"
"account_number": "4831",
"account_type": "Depreciation"
},
"Abschreibungen auf Kfz": {
"account_number": "4832",
"account_type": "Depreciation"
"account_number": "4832",
"account_type": "Depreciation"
},
"Sofortabschreibung GWG": {
"account_number": "4855",
"account_type": "Expense Account"
"account_number": "4855",
"account_type": "Expense Account"
}
},
"Kfz-Kosten": {
"is_group": 1,
"Kfz-Steuer": {
"account_number": "4510",
"account_type": "Expense Account"
},
"Kfz-Versicherungen": {
"account_number": "4520",
"account_type": "Expense Account"
},
"laufende Kfz-Betriebskosten": {
"account_number": "4530",
"account_type": "Expense Account"
},
"Kfz-Reparaturen": {
"account_number": "4540",
"account_type": "Expense Account"
},
"Fremdfahrzeuge": {
"account_number": "4570",
"account_type": "Expense Account"
},
"sonstige Kfz-Kosten": {
"account_number": "4580",
"account_type": "Expense Account"
}
},
"Personalkosten": {
"is_group": 1,
"Geh\u00e4lter": {
"account_number": "4120",
"account_type": "Expense Account"
},
"gesetzliche soziale Aufwendungen": {
"account_number": "4130",
"account_type": "Expense Account"
},
"Aufwendungen f\u00fcr Altersvorsorge": {
"account_number": "4165",
"account_type": "Expense Account"
},
"Verm\u00f6genswirksame Leistungen": {
"account_number": "4170",
"account_type": "Expense Account"
},
"Aushilfsl\u00f6hne": {
"account_number": "4190",
"account_type": "Expense Account"
}
},
"Raumkosten": {
"is_group": 1,
"Miete und Nebenkosten": {
"account_number": "4210",
"account_type": "Expense Account"
},
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
"account_number": "4240",
"account_type": "Expense Account"
},
"Reinigung": {
"account_number": "4250",
"account_type": "Expense Account"
}
},
"Reparatur/Instandhaltung": {
"is_group": 1,
"Reparatur u. Instandh. von Anlagen/Maschinen u. Betriebs- u. Gesch\u00e4ftsausst.": {
"account_number": "4805",
"account_type": "Expense Account"
}
},
"Versicherungsbeitr\u00e4ge": {
"is_group": 1,
"Versicherungen": {
"account_number": "4360",
"account_type": "Expense Account"
},
"Beitr\u00e4ge": {
"account_number": "4380",
"account_type": "Expense Account"
},
"sonstige Ausgaben": {
"account_number": "4390",
"account_type": "Expense Account"
},
"steuerlich abzugsf\u00e4hige Versp\u00e4tungszuschl\u00e4ge und Zwangsgelder": {
"account_number": "4396",
"account_type": "Expense Account"
}
},
"Werbe-/Reisekosten": {
"is_group": 1,
"Werbekosten": {
"account_number": "4610",
"account_type": "Expense Account"
},
"Aufmerksamkeiten": {
"account_number": "4653",
"account_type": "Expense Account"
},
"nicht abzugsf\u00e4hige Betriebsausg. aus Werbe-, Repr\u00e4s.- u. Reisekosten": {
"account_number": "4665",
"account_type": "Expense Account"
},
"Reisekosten Unternehmer": {
"account_number": "4670",
"account_type": "Expense Account"
}
},
"verschiedene Kosten": {
"is_group": 1,
"Porto": {
"account_number": "4910",
"account_type": "Expense Account"
},
"Telekom": {
"account_number": "4920",
"account_type": "Expense Account"
},
"Mobilfunk D2": {
"account_number": "4921",
"account_type": "Expense Account"
},
"Internet": {
"account_number": "4922",
"account_type": "Expense Account"
},
"B\u00fcrobedarf": {
"account_number": "4930",
"account_type": "Expense Account"
},
"Zeitschriften, B\u00fccher": {
"account_number": "4940",
"account_type": "Expense Account"
},
"Fortbildungskosten": {
"account_number": "4945",
"account_type": "Expense Account"
},
"Buchf\u00fchrungskosten": {
"account_number": "4955",
"account_type": "Expense Account"
},
"Abschlu\u00df- u. Pr\u00fcfungskosten": {
"account_number": "4957",
"account_type": "Expense Account"
},
"Nebenkosten des Geldverkehrs": {
"account_number": "4970",
"account_type": "Expense Account"
},
"Werkzeuge und Kleinger\u00e4te": {
"account_number": "4985",
"account_type": "Expense Account"
}
},
"Zinsaufwendungen": {
"is_group": 1,
"Zinsaufwendungen f\u00fcr kurzfristige Verbindlichkeiten": {
"account_number": "2110",
"account_type": "Expense Account"
},
"Zinsaufwendungen f\u00fcr KFZ Finanzierung": {
"account_number": "2121",
"account_type": "Expense Account"
}
}
},
"Anfangsbestand 9": {
"is_group": 1,
"root_type": "Equity",
"Saldenvortragskonten": {
"is_group": 1,
"Saldenvortrag Sachkonten": {
"account_number": "9000"
},
"Saldenvortr\u00e4ge Debitoren": {
"account_number": "9008"
},
"Saldenvortr\u00e4ge Kreditoren": {
"account_number": "9009"
}
}
},
"Privatkonten 1": {
"is_group": 1,
"root_type": "Equity",
"Privatentnahmen/-einlagen": {
"is_group": 1,
"Privatentnahme allgemein": {
"account_number": "1800"
},
"Privatsteuern": {
"account_number": "1810"
},
"Sonderausgaben beschr\u00e4nkt abzugsf\u00e4hig": {
"account_number": "1820"
},
"Sonderausgaben unbeschr\u00e4nkt abzugsf\u00e4hig": {
"account_number": "1830"
},
"Au\u00dfergew\u00f6hnliche Belastungen": {
"account_number": "1850"
},
"Privateinlagen": {
"account_number": "1890"
}
}
}
}
},
"Kfz-Kosten": {
"is_group": 1,
"Kfz-Steuer": {
"account_number": "4510",
"account_type": "Expense Account"
},
"Kfz-Versicherungen": {
"account_number": "4520",
"account_type": "Expense Account"
},
"laufende Kfz-Betriebskosten": {
"account_number": "4530",
"account_type": "Expense Account"
},
"Kfz-Reparaturen": {
"account_number": "4540",
"account_type": "Expense Account"
},
"Fremdfahrzeuge": {
"account_number": "4570",
"account_type": "Expense Account"
},
"sonstige Kfz-Kosten": {
"account_number": "4580",
"account_type": "Expense Account"
}
},
"Personalkosten": {
"is_group": 1,
"Gehälter": {
"account_number": "4120",
"account_type": "Expense Account"
},
"gesetzliche soziale Aufwendungen": {
"account_number": "4130",
"account_type": "Expense Account"
},
"Aufwendungen für Altersvorsorge": {
"account_number": "4165",
"account_type": "Expense Account"
},
"Vermögenswirksame Leistungen": {
"account_number": "4170",
"account_type": "Expense Account"
},
"Aushilfslöhne": {
"account_number": "4190",
"account_type": "Expense Account"
}
},
"Raumkosten": {
"is_group": 1,
"Miete und Nebenkosten": {
"account_number": "4210",
"account_type": "Expense Account"
},
"Gas, Wasser, Strom (Verwaltung, Vertrieb)": {
"account_number": "4240",
"account_type": "Expense Account"
},
"Reinigung": {
"account_number": "4250",
"account_type": "Expense Account"
}
},
"Reparatur/Instandhaltung": {
"is_group": 1,
"Reparaturen und Instandhaltungen von anderen Anlagen und Betriebs- und Geschäftsausstattung": {
"account_number": "4805",
"account_type": "Expense Account"
}
},
"Versicherungsbeiträge": {
"is_group": 1,
"Versicherungen": {
"account_number": "4360",
"account_type": "Expense Account"
},
"Beiträge": {
"account_number": "4380",
"account_type": "Expense Account"
},
"sonstige Ausgaben": {
"account_number": "4390",
"account_type": "Expense Account"
},
"steuerlich abzugsfähige Verspätungszuschläge und Zwangsgelder": {
"account_number": "4396",
"account_type": "Expense Account"
}
},
"Werbe-/Reisekosten": {
"is_group": 1,
"Werbekosten": {
"account_number": "4610",
"account_type": "Expense Account"
},
"Aufmerksamkeiten": {
"account_number": "4653",
"account_type": "Expense Account"
},
"nicht abzugsfähige Betriebsausg. aus Werbe-, Repräs.- u. Reisekosten": {
"account_number": "4665",
"account_type": "Expense Account"
},
"Reisekosten Unternehmer": {
"account_number": "4670",
"account_type": "Expense Account"
}
},
"verschiedene Kosten": {
"is_group": 1,
"Porto": {
"account_number": "4910",
"account_type": "Expense Account"
},
"Telekom": {
"account_number": "4920",
"account_type": "Expense Account"
},
"Mobilfunk D2": {
"account_number": "4921",
"account_type": "Expense Account"
},
"Internet": {
"account_number": "4922",
"account_type": "Expense Account"
},
"Bürobedarf": {
"account_number": "4930",
"account_type": "Expense Account"
},
"Zeitschriften, Bücher": {
"account_number": "4940",
"account_type": "Expense Account"
},
"Fortbildungskosten": {
"account_number": "4945",
"account_type": "Expense Account"
},
"Buchführungskosten": {
"account_number": "4955",
"account_type": "Expense Account"
},
"Abschluß- u. Prüfungskosten": {
"account_number": "4957",
"account_type": "Expense Account"
},
"Nebenkosten des Geldverkehrs": {
"account_number": "4970",
"account_type": "Expense Account"
},
"Werkzeuge und Kleingeräte": {
"account_number": "4985",
"account_type": "Expense Account"
}
},
"Zinsaufwendungen": {
"is_group": 1,
"Zinsaufwendungen für kurzfristige Verbindlichkeiten": {
"account_number": "2110",
"account_type": "Expense Account"
},
"Zinsaufwendungen für KFZ Finanzierung": {
"account_number": "2121",
"account_type": "Expense Account"
}
}
},
"Anfangsbestand 9": {
"is_group": 1,
"root_type": "Equity",
"Saldenvortragskonten": {
"is_group": 1,
"Saldenvortrag Sachkonten": {
"account_number": "9000"
},
"Saldenvorträge Debitoren": {
"account_number": "9008"
},
"Saldenvorträge Kreditoren": {
"account_number": "9009"
}
}
},
"Privatkonten 1": {
"is_group": 1,
"root_type": "Equity",
"Privatentnahmen/-einlagen": {
"is_group": 1,
"Privatentnahme allgemein": {
"account_number": "1800"
},
"Privatsteuern": {
"account_number": "1810"
},
"Sonderausgaben beschränkt abzugsfähig": {
"account_number": "1820"
},
"Sonderausgaben unbeschränkt abzugsfähig": {
"account_number": "1830"
},
"Außergewöhnliche Belastungen": {
"account_number": "1850"
},
"Privateinlagen": {
"account_number": "1890"
}
}
}
}
}

View File

@ -3,10 +3,6 @@
frappe.ui.form.on('Accounting Dimension Filter', {
refresh: function(frm, cdt, cdn) {
if (frm.doc.accounting_dimension) {
frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value');
}
let help_content =
`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
<tr><td>
@ -68,6 +64,7 @@ frappe.ui.form.on('Accounting Dimension Filter', {
frm.clear_table("dimensions");
let row = frm.add_child("dimensions");
row.accounting_dimension = frm.doc.accounting_dimension;
frm.fields_dict["dimensions"].grid.update_docfield_property("dimension_value", "label", frm.doc.accounting_dimension);
frm.refresh_field("dimensions");
frm.trigger('setup_filters');
},

View File

@ -56,7 +56,9 @@
"acc_frozen_upto",
"column_break_25",
"frozen_accounts_modifier",
"report_settings_sb"
"report_settings_sb",
"tab_break_dpet",
"show_balance_in_coa"
],
"fields": [
{
@ -91,7 +93,7 @@
},
{
"default": "0",
"description": "Enabling ensure each Sales Invoice has a unique value in Supplier Invoice No. field",
"description": "Enabling ensure each Purchase Invoice has a unique value in Supplier Invoice No. field",
"fieldname": "check_supplier_invoice_uniqueness",
"fieldtype": "Check",
"label": "Check Supplier Invoice Number Uniqueness"
@ -347,6 +349,17 @@
"fieldname": "allow_multi_currency_invoices_against_single_party_account",
"fieldtype": "Check",
"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"
}
],
"icon": "icon-cog",
@ -354,7 +367,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2022-07-11 13:37:50.605141",
"modified": "2023-01-02 12:07:42.434214",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@ -118,6 +118,10 @@ erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
}
plaid_success(token, response) {
frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
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' });
});
}
};

View File

@ -77,6 +77,6 @@ def get_party_bank_account(party_type, party):
@frappe.whitelist()
def get_bank_account_details(bank_account):
return frappe.db.get_value(
return frappe.get_cached_value(
"Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
)

View File

@ -4,6 +4,23 @@
frappe.ui.form.on("Bank Clearance", {
setup: function(frm) {
frm.add_fetch("account", "account_currency", "account_currency");
frm.set_query("account", function() {
return {
"filters": {
"account_type": ["in",["Bank","Cash"]],
"is_group": 0,
}
};
});
frm.set_query("bank_account", function () {
return {
filters: {
'is_company_account': 1
},
};
});
},
onload: function(frm) {
@ -12,14 +29,7 @@ frappe.ui.form.on("Bank Clearance", {
locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "";
frm.set_value("account", default_bank_account);
frm.set_query("account", function() {
return {
"filters": {
"account_type": ["in",["Bank","Cash"]],
"is_group": 0
}
};
});
frm.set_value("from_date", frappe.datetime.month_start());
frm.set_value("to_date", frappe.datetime.month_end());
@ -27,6 +37,11 @@ frappe.ui.form.on("Bank Clearance", {
refresh: function(frm) {
frm.disable_save();
frm.add_custom_button(__('Get Payment Entries'), () =>
frm.trigger("get_payment_entries")
);
frm.change_custom_button_type('Get Payment Entries', null, 'primary');
},
update_clearance_date: function(frm) {
@ -36,22 +51,30 @@ frappe.ui.form.on("Bank Clearance", {
callback: function(r, rt) {
frm.refresh_field("payment_entries");
frm.refresh_fields();
if (!frm.doc.payment_entries.length) {
frm.change_custom_button_type('Get Payment Entries', null, 'primary');
frm.change_custom_button_type('Update Clearance Date', null, 'default');
}
}
});
},
get_payment_entries: function(frm) {
return frappe.call({
method: "get_payment_entries",
doc: frm.doc,
callback: function(r, rt) {
frm.refresh_field("payment_entries");
frm.refresh_fields();
$(frm.fields_dict.payment_entries.wrapper).find("[data-fieldname=amount]").each(function(i,v){
if (i !=0){
$(v).addClass("text-right")
}
})
if (frm.doc.payment_entries.length) {
frm.add_custom_button(__('Update Clearance Date'), () =>
frm.trigger("update_clearance_date")
);
frm.change_custom_button_type('Get Payment Entries', null, 'default');
frm.change_custom_button_type('Update Clearance Date', null, 'primary');
}
}
});
}

View File

@ -1,4 +1,5 @@
{
"actions": [],
"allow_copy": 1,
"creation": "2013-01-10 16:34:05",
"doctype": "DocType",
@ -13,11 +14,8 @@
"bank_account",
"include_reconciled_entries",
"include_pos_transactions",
"get_payment_entries",
"section_break_10",
"payment_entries",
"update_clearance_date",
"total_amount"
"payment_entries"
],
"fields": [
{
@ -76,11 +74,6 @@
"fieldtype": "Check",
"label": "Include POS Transactions"
},
{
"fieldname": "get_payment_entries",
"fieldtype": "Button",
"label": "Get Payment Entries"
},
{
"fieldname": "section_break_10",
"fieldtype": "Section Break"
@ -91,25 +84,14 @@
"fieldtype": "Table",
"label": "Payment Entries",
"options": "Bank Clearance Detail"
},
{
"fieldname": "update_clearance_date",
"fieldtype": "Button",
"label": "Update Clearance Date"
},
{
"fieldname": "total_amount",
"fieldtype": "Currency",
"label": "Total Amount",
"options": "account_currency",
"read_only": 1
}
],
"hide_toolbar": 1,
"icon": "fa fa-check",
"idx": 1,
"issingle": 1,
"modified": "2020-04-06 16:12:06.628008",
"links": [],
"modified": "2022-11-28 17:24:13.008692",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Clearance",
@ -126,5 +108,6 @@
"quick_entry": 1,
"read_only": 1,
"sort_field": "modified",
"sort_order": "ASC"
"sort_order": "ASC",
"states": []
}

View File

@ -99,7 +99,7 @@ class BankClearance(Document):
.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, frappe.qb.desc)
.orderby(loan_disbursement.name, order=frappe.qb.desc)
).run(as_dict=1)
loan_repayment = frappe.qb.DocType("Loan Repayment")
@ -126,7 +126,9 @@ class BankClearance(Document):
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, frappe.qb.desc)
query = query.orderby(loan_repayment.posting_date).orderby(
loan_repayment.name, order=frappe.qb.desc
)
loan_repayments = query.run(as_dict=True)
@ -177,7 +179,6 @@ class BankClearance(Document):
)
self.set("payment_entries", [])
self.total_amount = 0.0
default_currency = erpnext.get_default_currency()
for d in entries:
@ -196,7 +197,6 @@ class BankClearance(Document):
d.pop("debit")
d.pop("account_currency")
row.update(d)
self.total_amount += flt(amount)
@frappe.whitelist()
def update_clearance_date(self):

View File

@ -43,20 +43,13 @@ frappe.ui.form.on('Bank Guarantee', {
reference_docname: function(frm) {
if (frm.doc.reference_docname && frm.doc.reference_doctype) {
let fields_to_fetch = ["grand_total"];
let party_field = frm.doc.reference_doctype == "Sales Order" ? "customer" : "supplier";
if (frm.doc.reference_doctype == "Sales Order") {
fields_to_fetch.push("project");
}
fields_to_fetch.push(party_field);
frappe.call({
method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_vouchar_detials",
method: "erpnext.accounts.doctype.bank_guarantee.bank_guarantee.get_voucher_details",
args: {
"column_list": fields_to_fetch,
"doctype": frm.doc.reference_doctype,
"docname": frm.doc.reference_docname
"bank_guarantee_type": frm.doc.bg_type,
"reference_name": frm.doc.reference_docname
},
callback: function(r) {
if (r.message) {

View File

@ -2,11 +2,8 @@
# For license information, please see license.txt
import json
import frappe
from frappe import _
from frappe.desk.search import sanitize_searchfield
from frappe.model.document import Document
@ -25,14 +22,18 @@ class BankGuarantee(Document):
@frappe.whitelist()
def get_vouchar_detials(column_list, doctype, docname):
column_list = json.loads(column_list)
for col in column_list:
sanitize_searchfield(col)
return frappe.db.sql(
""" select {columns} from `tab{doctype}` where name=%s""".format(
columns=", ".join(column_list), doctype=doctype
),
docname,
as_dict=1,
)[0]
def get_voucher_details(bank_guarantee_type: str, reference_name: str):
if not isinstance(reference_name, str):
raise TypeError("reference_name must be a string")
fields_to_fetch = ["grand_total"]
if bank_guarantee_type == "Receiving":
doctype = "Sales Order"
fields_to_fetch.append("customer")
fields_to_fetch.append("project")
else:
doctype = "Purchase Order"
fields_to_fetch.append("supplier")
return frappe.db.get_value(doctype, reference_name, fields_to_fetch, as_dict=True)

View File

@ -12,19 +12,31 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
},
};
});
let no_bank_transactions_text =
`<div class="text-muted text-center">${__("No Matching Bank Transactions Found")}</div>`
set_field_options("no_bank_transactions", no_bank_transactions_text);
},
onload: function (frm) {
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) {
frappe.require("bank-reconciliation-tool.bundle.js", () =>
frm.trigger("make_reconciliation_tool")
);
frm.upload_statement_button = frm.page.set_secondary_action(
__("Upload Bank Statement"),
() =>
frm.add_custom_button(__("Upload Bank Statement"), () =>
frappe.call({
method:
"erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement",
@ -46,6 +58,20 @@ 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,
},
})
});
},
after_save: function (frm) {
@ -129,7 +155,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}
},
render_chart: frappe.utils.debounce((frm) => {
render_chart(frm) {
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{
$reconciliation_tool_cards: frm.get_field(
@ -141,7 +167,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
currency: frm.currency,
}
);
}, 500),
},
render(frm) {
if (frm.doc.bank_account) {
@ -157,6 +183,9 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
).$wrapper,
bank_statement_from_date: frm.doc.bank_statement_from_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:
frm.doc.bank_statement_closing_balance,
cards_manager: frm.cards_manager,

View File

@ -10,6 +10,9 @@
"column_break_1",
"bank_statement_from_date",
"bank_statement_to_date",
"from_reference_date",
"to_reference_date",
"filter_by_reference_date",
"column_break_2",
"account_opening_balance",
"bank_statement_closing_balance",
@ -36,13 +39,13 @@
"fieldtype": "Column Break"
},
{
"depends_on": "eval: doc.bank_account",
"depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date",
"fieldname": "bank_statement_from_date",
"fieldtype": "Date",
"label": "From Date"
},
{
"depends_on": "eval: doc.bank_statement_from_date",
"depends_on": "eval: doc.bank_account && !doc.filter_by_reference_date",
"fieldname": "bank_statement_to_date",
"fieldtype": "Date",
"label": "To Date"
@ -83,13 +86,31 @@
"fieldname": "no_bank_transactions",
"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"
}
],
"hide_toolbar": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-04-21 11:13:49.831769",
"modified": "2023-01-13 13:00:02.022919",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Reconciliation Tool",
@ -108,5 +129,6 @@
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@ -8,9 +8,9 @@ import frappe
from frappe import _
from frappe.model.document import Document
from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
from frappe.utils import cint, flt
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 (
get_amounts_not_reflected_in_system,
get_entries,
@ -28,7 +28,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
filters = []
filters.append(["bank_account", "=", bank_account])
filters.append(["docstatus", "=", 1])
filters.append(["unallocated_amount", ">", 0])
filters.append(["unallocated_amount", ">", 0.0])
if to_date:
filters.append(["date", "<=", to_date])
if from_date:
@ -50,6 +50,7 @@ def get_bank_transactions(bank_account, from_date=None, to_date=None):
"party",
],
filters=filters,
order_by="date",
)
return transactions
@ -65,7 +66,7 @@ def get_account_balance(bank_account, till_date):
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
total_debit, total_credit = 0, 0
total_debit, total_credit = 0.0, 0.0
for d in data:
total_debit += flt(d.debit)
total_credit += flt(d.credit)
@ -144,10 +145,8 @@ def create_journal_entry_bts(
accounts.append(
{
"account": second_account,
"credit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
"debit_in_account_currency": bank_transaction.withdrawal
if bank_transaction.withdrawal > 0
else 0,
"credit_in_account_currency": bank_transaction.deposit,
"debit_in_account_currency": bank_transaction.withdrawal,
"party_type": party_type,
"party": party,
}
@ -157,10 +156,8 @@ def create_journal_entry_bts(
{
"account": company_account,
"bank_account": bank_transaction.bank_account,
"credit_in_account_currency": bank_transaction.withdrawal
if bank_transaction.withdrawal > 0
else 0,
"debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
"credit_in_account_currency": bank_transaction.withdrawal,
"debit_in_account_currency": bank_transaction.deposit,
}
)
@ -184,16 +181,22 @@ def create_journal_entry_bts(
journal_entry.insert()
journal_entry.submit()
if bank_transaction.deposit > 0:
if bank_transaction.deposit > 0.0:
paid_amount = bank_transaction.deposit
else:
paid_amount = bank_transaction.withdrawal
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()
@ -217,7 +220,7 @@ def create_payment_entry_bts(
as_dict=True,
)[0]
paid_amount = bank_transaction.unallocated_amount
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
payment_type = "Receive" if bank_transaction.deposit > 0.0 else "Pay"
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_value("Account", company_account, "company")
@ -256,9 +259,89 @@ def create_payment_entry_bts(
payment_entry.submit()
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()
def auto_reconcile_vouchers(
bank_account,
from_date=None,
to_date=None,
filter_by_reference_date=None,
from_reference_date=None,
to_reference_date=None,
):
frappe.flags.auto_reconcile_vouchers = True
document_types = ["payment_entry", "journal_entry"]
bank_transactions = get_bank_transactions(bank_account)
matched_transaction = []
for transaction in bank_transactions:
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_name": r[2],
"amount": r[4],
}
)
transaction = frappe.get_doc("Bank Transaction", transaction.name)
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
matched_trans = 0
for voucher in vouchers:
gl_entry = frappe.db.get_value(
"GL Entry",
dict(
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
),
["credit", "debit"],
as_dict=1,
)
gl_amount, transaction_amount = (
(gl_entry.credit, transaction.deposit)
if gl_entry.credit > 0
else (gl_entry.debit, transaction.withdrawal)
)
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
transaction.append(
"payment_entries",
{
"payment_document": voucher["payment_doctype"],
"payment_entry": voucher["payment_name"],
"allocated_amount": allocated_amount,
},
)
matched_transaction.append(str(transaction.name))
transaction.save()
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()
@ -266,80 +349,88 @@ 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)
company_account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
if transaction.unallocated_amount == 0:
frappe.throw(_("This bank transaction is already fully reconciled"))
total_amount = 0
for voucher in vouchers:
voucher["payment_entry"] = frappe.get_doc(voucher["payment_doctype"], voucher["payment_name"])
total_amount += get_paid_amount(
frappe._dict(
{
"payment_document": voucher["payment_doctype"],
"payment_entry": voucher["payment_name"],
}
),
transaction.currency,
company_account,
)
if total_amount > transaction.unallocated_amount:
frappe.throw(
_(
"The sum total of amounts of all selected vouchers should be less than the unallocated amount of the bank transaction"
)
)
account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
for voucher in vouchers:
gl_entry = frappe.db.get_value(
"GL Entry",
dict(
account=account, voucher_type=voucher["payment_doctype"], voucher_no=voucher["payment_name"]
),
["credit", "debit"],
as_dict=1,
)
gl_amount, transaction_amount = (
(gl_entry.credit, transaction.deposit)
if gl_entry.credit > 0
else (gl_entry.debit, transaction.withdrawal)
)
allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
transaction.append(
"payment_entries",
{
"payment_document": voucher["payment_entry"].doctype,
"payment_entry": voucher["payment_entry"].name,
"allocated_amount": allocated_amount,
},
)
transaction.save()
transaction.update_allocations()
transaction.add_payment_entries(vouchers)
return frappe.get_doc("Bank Transaction", bank_transaction_name)
@frappe.whitelist()
def get_linked_payments(bank_transaction_name, document_types=None):
def get_linked_payments(
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
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
bank_account = frappe.db.get_values(
"Bank Account", transaction.bank_account, ["account", "company"], as_dict=True
)[0]
(account, company) = (bank_account.account, bank_account.company)
matching = check_matching(account, company, transaction, document_types)
return matching
(gl_account, company) = (bank_account.account, bank_account.company)
matching = check_matching(
gl_account,
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 check_matching(bank_account, company, transaction, document_types):
def subtract_allocations(gl_account, vouchers):
"Look up & subtract any existing Bank Transaction allocations"
copied = []
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
# combine all types of vouchers
subquery = get_queries(bank_account, company, transaction, document_types)
subquery = get_queries(
bank_account,
company,
transaction,
document_types,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
exact_match,
)
filters = {
"amount": transaction.unallocated_amount,
"payment_type": "Receive" if transaction.deposit > 0 else "Pay",
"payment_type": "Receive" if transaction.deposit > 0.0 else "Pay",
"reference_no": transaction.reference_number,
"party_type": transaction.party_type,
"party": transaction.party,
@ -348,7 +439,9 @@ def check_matching(bank_account, company, transaction, document_types):
matching_vouchers = []
matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
matching_vouchers.extend(
get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match)
)
for query in subquery:
matching_vouchers.extend(
@ -357,14 +450,23 @@ def check_matching(bank_account, company, transaction, document_types):
filters,
)
)
return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
def get_queries(bank_account, company, transaction, document_types):
def get_queries(
bank_account,
company,
transaction,
document_types,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
exact_match,
):
# get queries to get matching vouchers
amount_condition = "=" if "exact_match" in document_types else "<="
account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
account_from_to = "paid_to" if transaction.deposit > 0.0 else "paid_from"
queries = []
# get matching queries from all the apps
@ -375,8 +477,13 @@ def get_queries(bank_account, company, transaction, document_types):
company,
transaction,
document_types,
amount_condition,
exact_match,
account_from_to,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
)
or []
)
@ -385,43 +492,106 @@ def get_queries(bank_account, company, transaction, document_types):
def get_matching_queries(
bank_account, company, transaction, document_types, amount_condition, account_from_to
bank_account,
company,
transaction,
document_types,
exact_match,
account_from_to,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
):
queries = []
if "payment_entry" in document_types:
pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction)
queries.extend([pe_amount_matching])
query = get_pe_matching_query(
exact_match,
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:
je_amount_matching = get_je_matching_query(amount_condition, transaction)
queries.extend([je_amount_matching])
query = get_je_matching_query(
exact_match,
transaction,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
)
queries.append(query)
if transaction.deposit > 0 and "sales_invoice" in document_types:
si_amount_matching = get_si_matching_query(amount_condition)
queries.extend([si_amount_matching])
if transaction.deposit > 0.0 and "sales_invoice" in document_types:
query = get_si_matching_query(exact_match)
queries.append(query)
if transaction.withdrawal > 0:
if transaction.withdrawal > 0.0:
if "purchase_invoice" in document_types:
pi_amount_matching = get_pi_matching_query(amount_condition)
queries.extend([pi_amount_matching])
query = get_pi_matching_query(exact_match)
queries.append(query)
if "bank_transaction" in document_types:
query = get_bt_matching_query(exact_match, transaction)
queries.append(query)
return queries
def get_loan_vouchers(bank_account, transaction, document_types, filters):
def get_loan_vouchers(bank_account, transaction, document_types, filters, exact_match):
vouchers = []
amount_condition = True if "exact_match" in document_types else False
if transaction.withdrawal > 0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, amount_condition, filters))
if transaction.withdrawal > 0.0 and "loan_disbursement" in document_types:
vouchers.extend(get_ld_matching_query(bank_account, exact_match, filters))
if transaction.deposit > 0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
if transaction.deposit > 0.0 and "loan_repayment" in document_types:
vouchers.extend(get_lr_matching_query(bank_account, exact_match, filters))
return vouchers
def get_ld_matching_query(bank_account, amount_condition, filters):
def get_bt_matching_query(exact_match, transaction):
# get matching bank transaction query
# find bank transactions in the same bank account with opposite sign
# same bank account must have same company and currency
field = "deposit" if transaction.withdrawal > 0.0 else "withdrawal"
return f"""
SELECT
(CASE WHEN reference_number = %(reference_no)s THEN 1 ELSE 0 END
+ 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
+ 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_ld_matching_query(bank_account, exact_match, filters):
loan_disbursement = frappe.qb.DocType("Loan Disbursement")
matching_reference = loan_disbursement.reference_number == filters.get("reference_number")
matching_party = loan_disbursement.applicant_type == filters.get(
@ -449,17 +619,17 @@ def get_ld_matching_query(bank_account, amount_condition, filters):
.where(loan_disbursement.disbursement_account == bank_account)
)
if amount_condition:
if exact_match:
query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
else:
query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
query.where(loan_disbursement.disbursed_amount > 0.0)
vouchers = query.run(as_list=True)
return vouchers
def get_lr_matching_query(bank_account, amount_condition, filters):
def get_lr_matching_query(bank_account, exact_match, 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(
@ -490,88 +660,129 @@ def get_lr_matching_query(bank_account, amount_condition, filters):
if frappe.db.has_column("Loan Repayment", "repay_from_salary"):
query = query.where((loan_repayment.repay_from_salary == 0))
if amount_condition:
if exact_match:
query.where(loan_repayment.amount_paid == filters.get("amount"))
else:
query.where(loan_repayment.amount_paid <= filters.get("amount"))
query.where(loan_repayment.amount_paid > 0.0)
vouchers = query.run()
return vouchers
def get_pe_matching_query(amount_condition, account_from_to, transaction):
def get_pe_matching_query(
exact_match,
account_from_to,
transaction,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
):
# get matching payment entries query
if transaction.deposit > 0:
if transaction.deposit > 0.0:
currency_field = "paid_to_account_currency as currency"
else:
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"""
SELECT
(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
+ 1 ) AS rank,
'Payment Entry' as doctype,
name,
paid_amount,
reference_no,
reference_date,
party,
party_type,
posting_date,
{currency_field}
FROM
`tabPayment Entry`
WHERE
paid_amount {amount_condition} %(amount)s
AND docstatus = 1
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
AND ifnull(clearance_date, '') = ""
AND {account_from_to} = %(bank_account)s
SELECT
(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 paid_amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Payment Entry' as doctype,
name,
paid_amount,
reference_no,
reference_date,
party,
party_type,
posting_date,
{currency_field}
FROM
`tabPayment Entry`
WHERE
docstatus = 1
AND payment_type IN (%(payment_type)s, 'Internal Transfer')
AND ifnull(clearance_date, '') = ""
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(amount_condition, transaction):
def get_je_matching_query(
exact_match,
transaction,
from_date,
to_date,
filter_by_reference_date,
from_reference_date,
to_reference_date,
):
# get matching journal entry query
# We have mapping at the bank level
# 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
cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
cr_or_dr = "credit" if transaction.withdrawal > 0.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"""
SELECT
(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 ,
'Journal Entry' as doctype,
'Journal Entry' AS doctype,
je.name,
jea.{cr_or_dr}_in_account_currency as paid_amount,
je.cheque_no as reference_no,
je.cheque_date as reference_date,
je.pay_to_recd_from as party,
jea.{cr_or_dr}_in_account_currency AS paid_amount,
je.cheque_no AS reference_no,
je.cheque_date AS reference_date,
je.pay_to_recd_from AS party,
jea.party_type,
je.posting_date,
jea.account_currency as currency
jea.account_currency AS currency
FROM
`tabJournal Entry Account` as jea
`tabJournal Entry Account` AS jea
JOIN
`tabJournal Entry` as je
`tabJournal Entry` AS je
ON
jea.parent = je.name
WHERE
(je.clearance_date is null or je.clearance_date='0000-00-00')
je.docstatus = 1
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.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
AND jea.{cr_or_dr}_in_account_currency {'= %(amount)s' if exact_match else '> 0.0'}
AND je.docstatus = 1
{filter_by_date}
{filter_by_reference_no}
order by {order_by}
"""
def get_si_matching_query(amount_condition):
# get matchin sales invoice query
def get_si_matching_query(exact_match):
# get matching sales invoice query
return f"""
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,
'Sales Invoice' as doctype,
si.name,
@ -589,18 +800,20 @@ def get_si_matching_query(amount_condition):
`tabSales Invoice` as si
ON
sip.parent = si.name
WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
WHERE
si.docstatus = 1
AND (sip.clearance_date is null or sip.clearance_date='0000-00-00')
AND sip.account = %(bank_account)s
AND sip.amount {amount_condition} %(amount)s
AND si.docstatus = 1
AND sip.amount {'= %(amount)s' if exact_match else '> 0.0'}
"""
def get_pi_matching_query(amount_condition):
# get matching purchase invoice query
def get_pi_matching_query(exact_match):
# get matching purchase invoice query when they are also used as payment entries (is_paid)
return f"""
SELECT
( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
+ CASE WHEN paid_amount = %(amount)s THEN 1 ELSE 0 END
+ 1 ) AS rank,
'Purchase Invoice' as doctype,
name,
@ -614,9 +827,9 @@ def get_pi_matching_query(amount_condition):
FROM
`tabPurchase Invoice`
WHERE
paid_amount {amount_condition} %(amount)s
AND docstatus = 1
docstatus = 1
AND is_paid = 1
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

@ -100,7 +100,7 @@ frappe.ui.form.on("Bank Statement Import", {
if (frm.doc.status.includes("Success")) {
frm.add_custom_button(
__("Go to {0} List", [frm.doc.reference_doctype]),
__("Go to {0} List", [__(frm.doc.reference_doctype)]),
() => frappe.set_route("List", frm.doc.reference_doctype)
);
}
@ -141,7 +141,7 @@ frappe.ui.form.on("Bank Statement Import", {
},
show_import_status(frm) {
let import_log = JSON.parse(frm.doc.import_log || "[]");
let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
let successful_records = import_log.filter((log) => log.success);
let failed_records = import_log.filter((log) => !log.success);
if (successful_records.length === 0) return;
@ -309,7 +309,7 @@ frappe.ui.form.on("Bank Statement Import", {
// method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
show_import_preview(frm, preview_data) {
let import_log = JSON.parse(frm.doc.import_log || "[]");
let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
if (
frm.import_preview &&
@ -439,7 +439,7 @@ frappe.ui.form.on("Bank Statement Import", {
},
show_import_log(frm) {
let import_log = JSON.parse(frm.doc.import_log || "[]");
let import_log = JSON.parse(frm.doc.statement_import_log || "[]");
let logs = import_log;
frm.toggle_display("import_log", false);
frm.toggle_display("import_log_section", logs.length > 0);

View File

@ -24,7 +24,7 @@
"section_import_preview",
"import_preview",
"import_log_section",
"import_log",
"statement_import_log",
"show_failed_logs",
"import_log_preview",
"reference_doctype",
@ -90,12 +90,6 @@
"options": "JSON",
"read_only": 1
},
{
"fieldname": "import_log",
"fieldtype": "Code",
"label": "Import Log",
"options": "JSON"
},
{
"fieldname": "import_log_section",
"fieldtype": "Section Break",
@ -198,11 +192,17 @@
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
"fieldname": "statement_import_log",
"fieldtype": "Code",
"label": "Statement Import Log",
"options": "JSON"
}
],
"hide_toolbar": 1,
"links": [],
"modified": "2021-05-12 14:17:37.777246",
"modified": "2022-09-07 11:11:40.293317",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Statement Import",

View File

@ -53,15 +53,13 @@ class BankStatementImport(DataImport):
if "Bank Account" not in json.dumps(preview["columns"]):
frappe.throw(_("Please add the Bank Account column"))
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.background_jobs import is_job_queued
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()]
if self.name not in enqueued_jobs:
if not is_job_queued(self.name):
enqueue(
start_import,
queue="default",

View File

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

View File

@ -20,9 +20,11 @@
"currency",
"section_break_10",
"description",
"section_break_14",
"reference_number",
"column_break_10",
"transaction_id",
"transaction_type",
"section_break_14",
"payment_entries",
"section_break_18",
"allocated_amount",
@ -190,11 +192,21 @@
"label": "Withdrawal",
"oldfieldname": "credit",
"options": "currency"
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
},
{
"fieldname": "transaction_type",
"fieldtype": "Data",
"label": "Transaction Type",
"length": 50
}
],
"is_submittable": 1,
"links": [],
"modified": "2022-03-21 19:05:04.208222",
"modified": "2022-05-29 18:36:50.475964",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
@ -248,4 +260,4 @@
"states": [],
"title_field": "bank_account",
"track_changes": 1
}
}

View File

@ -1,9 +1,6 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
from functools import reduce
import frappe
from frappe.utils import flt
@ -18,72 +15,137 @@ class BankTransaction(StatusUpdater):
self.clear_linked_payment_entries()
self.set_status()
_saving_flag = False
# nosemgrep: frappe-semgrep-rules.rules.frappe-modifying-but-not-comitting
def on_update_after_submit(self):
self.update_allocations()
self.clear_linked_payment_entries()
self.set_status(update=True)
"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._saving_flag = False
def on_cancel(self):
self.clear_linked_payment_entries(for_cancel=True)
self.set_status(update=True)
def update_allocations(self):
"The doctype does not allow modifications after submission, so write to the db direct"
if self.payment_entries:
allocated_amount = reduce(
lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
)
allocated_amount = sum(p.allocated_amount for p in self.payment_entries)
else:
allocated_amount = 0
allocated_amount = 0.0
if allocated_amount:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
frappe.db.set_value(
self.doctype,
self.name,
"unallocated_amount",
abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
)
amount = abs(flt(self.withdrawal) - flt(self.deposit))
self.db_set("allocated_amount", flt(allocated_amount))
self.db_set("unallocated_amount", amount - flt(allocated_amount))
self.reload()
self.set_status(update=True)
else:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
frappe.db.set_value(
self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
)
def add_payment_entries(self, vouchers):
"Add the vouchers with zero allocation. Save() will perform the allocations and clearance"
if 0.0 >= self.unallocated_amount:
frappe.throw(frappe._(f"Bank Transaction {self.name} is already fully reconciled"))
amount = self.deposit or self.withdrawal
if amount == self.allocated_amount:
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
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:
if should_clear:
latest_transaction.clear_linked_payment_entry(payment_entry)
self.db_delete_payment_entry(payment_entry)
elif remaining_amount <= 0.0:
self.db_delete_payment_entry(payment_entry)
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._(f"Voucher {payment_entry.payment_entry} is over-allocated by {unallocated_amount}")
)
self.reload()
def clear_linked_payment_entries(self, for_cancel=False):
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:
if payment_entry.payment_document == "Sales Invoice":
self.clear_sales_invoice(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document in get_doctypes_for_bank_reconciliation():
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
self.remove_payment_entry(payment_entry)
# runs on_update_after_submit
self.save()
def clear_simple_entry(self, payment_entry, for_cancel=False):
if payment_entry.payment_document == "Payment Entry":
if (
frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
== "Internal Transfer"
):
if len(get_reconciled_bank_transactions(payment_entry)) < 2:
return
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)
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
)
def clear_linked_payment_entries(self, for_cancel=False):
if for_cancel:
for payment_entry in self.payment_entries:
self.clear_linked_payment_entry(payment_entry, for_cancel)
else:
self.allocate_payment_entries()
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,
def clear_linked_payment_entry(self, payment_entry, for_cancel=False):
clearance_date = None if for_cancel else self.date
set_voucher_clearance(
payment_entry.payment_document, payment_entry.payment_entry, clearance_date, self
)
@ -93,38 +155,112 @@ def get_doctypes_for_bank_reconciliation():
return frappe.get_hooks("bank_reconciliation_doctypes")
def get_reconciled_bank_transactions(payment_entry):
reconciled_bank_transactions = frappe.get_all(
"Bank Transaction Payments",
filters={"payment_entry": payment_entry.payment_entry},
fields=["parent"],
def get_clearance_details(transaction, payment_entry):
"""
There should only be one bank gle for a voucher.
Could be none for a Bank Transaction.
But if a JE, could affect two banks.
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
)
return reconciled_bank_transactions
unallocated_amount = min(
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._(f"Voucher {payment_entry.payment_entry} value is broken: {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_total_allocated_amount(payment_entry):
return frappe.db.sql(
def get_related_bank_gl_entries(doctype, docname):
# nosemgrep: frappe-semgrep-rules.rules.frappe-using-db-sql
result = frappe.db.sql(
"""
SELECT
SUM(btp.allocated_amount) as allocated_amount,
bt.name
ABS(gle.credit_in_account_currency - gle.debit_in_account_currency) AS amount,
gle.account AS gl_account
FROM
`tabBank Transaction Payments` as btp
`tabGL Entry` gle
LEFT JOIN
`tabBank Transaction` bt ON bt.name=btp.parent
`tabAccount` ac ON ac.name=gle.account
WHERE
btp.payment_document = %s
AND
btp.payment_entry = %s
AND
bt.docstatus = 1""",
(payment_entry.payment_document, payment_entry.payment_entry),
ac.account_type = 'Bank'
AND gle.voucher_type = %(doctype)s
AND gle.voucher_no = %(docname)s
AND is_cancelled = 0
""",
dict(doctype=doctype, docname=docname),
as_dict=True,
)
return result
def get_paid_amount(payment_entry, currency, bank_account):
def get_total_allocated_amount(doctype, docname):
"""
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"]:
paid_amount_field = "paid_amount"
@ -137,7 +273,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
)
elif doc.payment_type == "Pay":
paid_amount_field = (
"paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount"
"paid_amount" if doc.paid_from_account_currency == currency else "base_paid_amount"
)
return frappe.db.get_value(
@ -147,7 +283,7 @@ def get_paid_amount(payment_entry, currency, bank_account):
elif payment_entry.payment_document == "Journal Entry":
return frappe.db.get_value(
"Journal Entry Account",
{"parent": payment_entry.payment_entry, "account": bank_account},
{"parent": payment_entry.payment_entry, "account": gl_bank_account},
"sum(credit_in_account_currency)",
)
@ -166,6 +302,12 @@ def get_paid_amount(payment_entry, currency, bank_account):
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:
frappe.throw(
"Please reconcile {0}: {1} manually".format(
@ -174,18 +316,55 @@ def get_paid_amount(payment_entry, currency, bank_account):
)
@frappe.whitelist()
def unclear_reference_payment(doctype, docname):
if frappe.db.exists(doctype, docname):
doc = frappe.get_doc(doctype, docname)
if doctype == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=doc.payment_document, parent=doc.payment_entry),
"clearance_date",
None,
)
else:
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
def set_voucher_clearance(doctype, docname, clearance_date, self):
if doctype in [
"Payment Entry",
"Journal Entry",
"Purchase Invoice",
"Expense Claim",
"Loan Repayment",
"Loan Disbursement",
]:
if (
doctype == "Payment Entry"
and frappe.db.get_value("Payment Entry", docname, "payment_type") == "Internal Transfer"
and len(get_reconciled_bank_transactions(doctype, docname)) < 2
):
return
frappe.db.set_value(doctype, docname, "clearance_date", clearance_date)
return doc.payment_entry
elif doctype == "Sales Invoice":
frappe.db.set_value(
"Sales Invoice Payment",
dict(parenttype=doctype, parent=docname),
"clearance_date",
clearance_date,
)
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:
for pe in bt.payment_entries:
if pe.payment_document == self.doctype and pe.payment_entry == self.name:
bt.remove(pe)
bt.save()
break
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

@ -74,7 +74,7 @@ def get_header_mapping(columns, bank_account):
def get_bank_mapping(bank_account):
bank_name = frappe.db.get_value("Bank Account", bank_account, "bank")
bank_name = frappe.get_cached_value("Bank Account", bank_account, "bank")
bank = frappe.get_doc("Bank", bank_name)
mapping = {row.file_field: row.bank_transaction_field for row in bank.bank_transaction_mapping}

View File

@ -5,6 +5,7 @@ import json
import unittest
import frappe
from frappe import utils
from frappe.tests.utils import FrappeTestCase
from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import (
@ -40,7 +41,12 @@ class TestBankTransaction(FrappeTestCase):
"Bank Transaction",
dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"),
)
linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
linked_payments = get_linked_payments(
bank_transaction.name,
["payment_entry", "exact_match"],
from_date=bank_transaction.date,
to_date=utils.today(),
)
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
@ -81,7 +87,12 @@ class TestBankTransaction(FrappeTestCase):
"Bank Transaction",
dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"),
)
linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
linked_payments = get_linked_payments(
bank_transaction.name,
["payment_entry", "exact_match"],
from_date=bank_transaction.date,
to_date=utils.today(),
)
self.assertTrue(linked_payments[0][3])
# Check error if already reconciled

View File

@ -1,6 +1,7 @@
{
"actions": [],
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2016-05-16 11:42:29.632528",
"doctype": "DocType",
"editable_grid": 1,
@ -9,6 +10,7 @@
"budget_against",
"company",
"cost_center",
"naming_series",
"project",
"fiscal_year",
"column_break_3",
@ -190,15 +192,26 @@
"label": "Budget Accounts",
"options": "Budget Account",
"reqd": 1
},
{
"fieldname": "naming_series",
"fieldtype": "Data",
"hidden": 1,
"label": "Series",
"no_copy": 1,
"print_hide": 1,
"read_only": 1,
"set_only_once": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-10-06 15:13:54.055854",
"modified": "2022-10-10 22:14:36.361509",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Budget",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@ -220,5 +233,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -5,7 +5,6 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.model.naming import make_autoname
from frappe.utils import add_months, flt, fmt_money, get_last_day, getdate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
@ -23,11 +22,6 @@ class DuplicateBudgetError(frappe.ValidationError):
class Budget(Document):
def autoname(self):
self.name = make_autoname(
self.get(frappe.scrub(self.budget_against)) + "/" + self.fiscal_year + "/.###"
)
def validate(self):
if not self.get(frappe.scrub(self.budget_against)):
frappe.throw(_("{0} is mandatory").format(self.budget_against))
@ -65,7 +59,7 @@ class Budget(Document):
account_list = []
for d in self.get("accounts"):
if d.account:
account_details = frappe.db.get_value(
account_details = frappe.get_cached_value(
"Account", d.account, ["is_group", "company", "report_type"], as_dict=1
)
@ -109,8 +103,11 @@ class Budget(Document):
):
self.applicable_on_booking_actual_expenses = 1
def before_naming(self):
self.naming_series = f"{{{frappe.scrub(self.budget_against)}}}./.{self.fiscal_year}/.###"
def validate_expense_against_budget(args):
def validate_expense_against_budget(args, expense_amount=0):
args = frappe._dict(args)
if args.get("company") and not args.fiscal_year:
@ -178,15 +175,20 @@ def validate_expense_against_budget(args):
) # nosec
if budget_records:
validate_budget_records(args, budget_records)
validate_budget_records(args, budget_records, expense_amount)
def validate_budget_records(args, budget_records):
def validate_budget_records(args, budget_records, expense_amount):
for budget in budget_records:
if flt(budget.budget_amount):
amount = get_amount(args, budget)
amount = expense_amount or get_amount(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"]:
budget_amount = get_accumulated_monthly_budget(
budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
@ -198,28 +200,28 @@ def validate_budget_records(args, budget_records):
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):
actual_expense = amount or get_actual_expense(args)
if actual_expense > budget_amount:
diff = actual_expense - budget_amount
actual_expense = get_actual_expense(args)
total_expense = actual_expense + amount
if total_expense > budget_amount:
if actual_expense > budget_amount:
error_tense = _("is already")
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")
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format(
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It {5} exceed by {6}").format(
_(action_for),
frappe.bold(args.account),
args.budget_against_field,
frappe.unscrub(args.budget_against_field),
frappe.bold(budget_against),
frappe.bold(fmt_money(budget_amount, currency=currency)),
error_tense,
frappe.bold(fmt_money(diff, currency=currency)),
)
@ -230,9 +232,9 @@ def compare_expense_with_budget(args, budget_amount, action_for, action, budget_
action = "Warn"
if action == "Stop":
frappe.throw(msg, BudgetError)
frappe.throw(msg, BudgetError, title=_("Budget Exceeded"))
else:
frappe.msgprint(msg, indicator="orange")
frappe.msgprint(msg, indicator="orange", title=_("Budget Exceeded"))
def get_actions(args, budget):
@ -309,7 +311,7 @@ def get_other_condition(args, budget, for_doc):
if args.get("fiscal_year"):
date_field = "schedule_date" if for_doc == "Material Request" else "transaction_date"
start_date, end_date = frappe.db.get_value(
start_date, end_date = frappe.get_cached_value(
"Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
)
@ -354,7 +356,9 @@ def get_actual_expense(args):
"""
select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle
where gle.account=%(account)s
where
is_cancelled = 0
and gle.account=%(account)s
{condition1}
and gle.fiscal_year=%(fiscal_year)s
and gle.company=%(company)s
@ -382,7 +386,7 @@ def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_ye
):
distribution.setdefault(d.month, d.percentage_allocation)
dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date")
dt = frappe.get_cached_value("Fiscal Year", fiscal_year, "year_start_date")
accumulated_percentage = 0.0
while dt <= getdate(posting_date):

View File

@ -334,6 +334,39 @@ class TestBudget(unittest.TestCase):
budget.cancel()
jv.cancel()
def test_monthly_budget_against_main_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.cost_center_allocation.test_cost_center_allocation import (
create_cost_center_allocation,
)
cost_centers = [
"Main Budget Cost Center 1",
"Sub Budget Cost Center 1",
"Sub Budget Cost Center 2",
]
for cc in cost_centers:
create_cost_center(cost_center_name=cc, company="_Test Company")
create_cost_center_allocation(
"_Test Company",
"Main Budget Cost Center 1 - _TC",
{"Sub Budget Cost Center 1 - _TC": 60, "Sub Budget Cost Center 2 - _TC": 40},
)
make_budget(budget_against="Cost Center", cost_center="Main Budget Cost Center 1 - _TC")
jv = make_journal_entry(
"_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC",
400000,
"Main Budget Cost Center 1 - _TC",
posting_date=nowdate(),
)
self.assertRaises(BudgetError, jv.submit)
def set_total_expense_zero(posting_date, budget_against_field=None, budget_against_CC=None):
if budget_against_field == "project":

View File

@ -45,14 +45,14 @@ def validate_columns(data):
@frappe.whitelist()
def validate_company(company):
parent_company, allow_account_creation_against_child_company = frappe.db.get_value(
"Company", {"name": company}, ["parent_company", "allow_account_creation_against_child_company"]
parent_company, allow_account_creation_against_child_company = frappe.get_cached_value(
"Company", company, ["parent_company", "allow_account_creation_against_child_company"]
)
if parent_company and (not allow_account_creation_against_child_company):
msg = _("{} is a child company.").format(frappe.bold(company)) + " "
msg += _("Please import accounts against parent company or enable {} in company master.").format(
frappe.bold("Allow Account Creation Against Child Company")
frappe.bold(_("Allow Account Creation Against Child Company"))
)
frappe.throw(msg, title=_("Wrong Company"))
@ -485,6 +485,10 @@ def set_default_accounts(company):
"default_payable_account": frappe.db.get_value(
"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

@ -16,7 +16,7 @@ class CostCenter(NestedSet):
from erpnext.accounts.utils import get_autoname_with_number
self.name = get_autoname_with_number(
self.cost_center_number, self.cost_center_name, None, self.company
self.cost_center_number, self.cost_center_name, self.company
)
def validate(self):

View File

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

View File

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

View File

@ -19,7 +19,7 @@ class Dunning(AccountsController):
self.validate_overdue_days()
self.validate_amount()
if not self.income_account:
self.income_account = frappe.db.get_value("Company", self.company, "default_income_account")
self.income_account = frappe.get_cached_value("Company", self.company, "default_income_account")
def validate_overdue_days(self):
self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0
@ -40,7 +40,7 @@ class Dunning(AccountsController):
def on_cancel(self):
if self.dunning_amount:
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def make_gl_entries(self):

View File

@ -26,7 +26,7 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
doc: frm.doc,
callback: function(r) {
if (r.message) {
frm.add_custom_button(__('Journal Entry'), function() {
frm.add_custom_button(__('Journal Entries'), function() {
return frm.events.make_jv(frm);
}, __('Create'));
}
@ -35,10 +35,11 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
}
},
get_entries: function(frm) {
get_entries: function(frm, account) {
frappe.call({
method: "get_accounts_data",
doc: cur_frm.doc,
account: account,
callback: function(r){
frappe.model.clear_table(frm.doc, "accounts");
if(r.message) {
@ -57,7 +58,6 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
let total_gain_loss = 0;
frm.doc.accounts.forEach((d) => {
d.gain_loss = flt(d.new_balance_in_base_currency, precision("new_balance_in_base_currency", d)) - flt(d.balance_in_base_currency, precision("balance_in_base_currency", d));
total_gain_loss += flt(d.gain_loss, precision("gain_loss", d));
});
@ -66,13 +66,19 @@ frappe.ui.form.on('Exchange Rate Revaluation', {
},
make_jv : function(frm) {
let revaluation_journal = null;
let zero_balance_journal = null;
frappe.call({
method: "make_jv_entry",
method: "make_jv_entries",
doc: frm.doc,
freeze: true,
freeze_message: "Making Journal Entries...",
callback: function(r){
if (r.message) {
var doc = frappe.model.sync(r.message)[0];
frappe.set_route("Form", doc.doctype, doc.name);
let response = r.message;
if(response['revaluation_jv'] || response['zero_balance_jv']) {
frappe.msgprint(__("Journals have been created"));
}
}
}
});

View File

@ -1,389 +1,160 @@
{
"allow_copy": 0,
"allow_guest_to_view": 0,
"allow_import": 1,
"allow_rename": 0,
"autoname": "ACC-ERR-.YYYY.-.#####",
"beta": 0,
"creation": "2018-04-13 18:25:55.943587",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"actions": [],
"allow_import": 1,
"autoname": "ACC-ERR-.YYYY.-.#####",
"creation": "2018-04-13 18:25:55.943587",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"posting_date",
"column_break_2",
"company",
"section_break_4",
"get_entries",
"accounts",
"section_break_6",
"gain_loss_unbooked",
"gain_loss_booked",
"column_break_10",
"total_gain_loss",
"amended_from"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"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": "Posting Date",
"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,
"translatable": 0,
"unique": 0
},
"default": "Today",
"fieldname": "posting_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Posting Date",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_2",
"fieldtype": "Column 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,
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "column_break_2",
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "company",
"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": "Company",
"length": 0,
"no_copy": 0,
"options": "Company",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "company",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_4",
"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,
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "section_break_4",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "get_entries",
"fieldtype": "Button",
"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": "Get Entries",
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "get_entries",
"fieldtype": "Button",
"label": "Get Entries"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 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": "Exchange Rate Revaluation Account",
"length": 0,
"no_copy": 1,
"options": "Exchange Rate Revaluation 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,
"translatable": 0,
"unique": 0
},
"fieldname": "accounts",
"fieldtype": "Table",
"label": "Exchange Rate Revaluation Account",
"no_copy": 1,
"options": "Exchange Rate Revaluation Account",
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_6",
"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,
"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,
"translatable": 0,
"unique": 0
},
"fieldname": "section_break_6",
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "total_gain_loss",
"fieldtype": "Currency",
"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": "Total Gain/Loss",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
},
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Exchange Rate Revaluation",
"print_hide": 1,
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
"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": "Amended From",
"length": 0,
"no_copy": 1,
"options": "Exchange Rate Revaluation",
"permlevel": 0,
"print_hide": 1,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"fieldname": "gain_loss_unbooked",
"fieldtype": "Currency",
"label": "Gain/Loss from Revaluation",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"description": "Gain/Loss accumulated in foreign currency account. Accounts with '0' balance in either Base or Account currency",
"fieldname": "gain_loss_booked",
"fieldtype": "Currency",
"label": "Gain/Loss already booked",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "total_gain_loss",
"fieldtype": "Currency",
"label": "Total Gain/Loss",
"options": "Company:company:default_currency",
"read_only": 1
},
{
"fieldname": "column_break_10",
"fieldtype": "Column Break"
}
],
"has_web_view": 0,
"hide_heading": 0,
"hide_toolbar": 0,
"idx": 0,
"image_view": 0,
"in_create": 0,
"is_submittable": 1,
"issingle": 0,
"istable": 0,
"max_attachments": 0,
"modified": "2018-08-21 16:15:34.660715",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation",
"name_case": "",
"owner": "Administrator",
],
"is_submittable": 1,
"links": [],
"modified": "2022-12-29 19:38:24.416529",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
"amend": 1,
"cancel": 1,
"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": 1,
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"submit": 1,
"write": 1
},
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
"share": 1,
"submit": 1,
"write": 1
},
},
{
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"set_user_permissions": 0,
"share": 1,
"submit": 1,
"amend": 1,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
"share": 1,
"submit": 1,
"write": 1
}
],
"quick_entry": 0,
"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,
"track_views": 0
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -3,10 +3,12 @@
import frappe
from frappe import _
from frappe import _, qb
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.utils import flt
from frappe.query_builder import Criterion, Order
from frappe.query_builder.functions import NullIf, Sum
from frappe.utils import flt, get_link_to_form
import erpnext
from erpnext.accounts.doctype.journal_entry.journal_entry import get_balance_on
@ -19,11 +21,25 @@ class ExchangeRateRevaluation(Document):
def set_total_gain_loss(self):
total_gain_loss = 0
gain_loss_booked = 0
gain_loss_unbooked = 0
for d in self.accounts:
d.gain_loss = flt(
d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")
) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))
if not d.zero_balance:
d.gain_loss = flt(
d.new_balance_in_base_currency, d.precision("new_balance_in_base_currency")
) - flt(d.balance_in_base_currency, d.precision("balance_in_base_currency"))
if d.zero_balance:
gain_loss_booked += flt(d.gain_loss, d.precision("gain_loss"))
else:
gain_loss_unbooked += flt(d.gain_loss, d.precision("gain_loss"))
total_gain_loss += flt(d.gain_loss, d.precision("gain_loss"))
self.gain_loss_booked = gain_loss_booked
self.gain_loss_unbooked = gain_loss_unbooked
self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss"))
def validate_mandatory(self):
@ -35,98 +51,206 @@ class ExchangeRateRevaluation(Document):
@frappe.whitelist()
def check_journal_entry_condition(self):
total_debit = frappe.db.get_value(
"Journal Entry Account",
{"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1},
"sum(debit) as sum",
exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
jea = qb.DocType("Journal Entry Account")
journals = (
qb.from_(jea)
.select(jea.parent)
.distinct()
.where(
(jea.reference_type == "Exchange Rate Revaluation")
& (jea.reference_name == self.name)
& (jea.docstatus == 1)
)
.run()
)
total_amt = 0
for d in self.accounts:
total_amt = total_amt + d.new_balance_in_base_currency
if journals:
gle = qb.DocType("GL Entry")
total_amt = (
qb.from_(gle)
.select((Sum(gle.credit) - Sum(gle.debit)).as_("total_amount"))
.where(
(gle.voucher_type == "Journal Entry")
& (gle.voucher_no.isin(journals))
& (gle.account == exchange_gain_loss_account)
& (gle.is_cancelled == 0)
)
.run()
)
if total_amt != total_debit:
return True
if total_amt and total_amt[0][0] != self.total_gain_loss:
return True
else:
return False
return False
return True
@frappe.whitelist()
def get_accounts_data(self, account=None):
accounts = []
def get_accounts_data(self):
self.validate_mandatory()
company_currency = erpnext.get_company_currency(self.company)
account_details = self.get_account_balance_from_gle(
company=self.company, posting_date=self.posting_date, account=None, party_type=None, party=None
)
accounts_with_new_balance = self.calculate_new_account_balance(
self.company, self.posting_date, account_details
)
if not accounts_with_new_balance:
self.throw_invalid_response_message(account_details)
return accounts_with_new_balance
@staticmethod
def get_account_balance_from_gle(company, posting_date, account, party_type, party):
account_details = []
if company and posting_date:
company_currency = erpnext.get_company_currency(company)
acc = qb.DocType("Account")
if account:
accounts = [account]
else:
res = (
qb.from_(acc)
.select(acc.name)
.where(
(acc.is_group == 0)
& (acc.report_type == "Balance Sheet")
& (acc.root_type.isin(["Asset", "Liability", "Equity"]))
& (acc.account_type != "Stock")
& (acc.company == company)
& (acc.account_currency != company_currency)
)
.orderby(acc.name)
.run(as_list=True)
)
accounts = [x[0] for x in res]
if accounts:
having_clause = (qb.Field("balance") != qb.Field("balance_in_account_currency")) & (
(qb.Field("balance_in_account_currency") != 0) | (qb.Field("balance") != 0)
)
gle = qb.DocType("GL Entry")
# conditions
conditions = []
conditions.append(gle.account.isin(accounts))
conditions.append(gle.posting_date.lte(posting_date))
conditions.append(gle.is_cancelled == 0)
if party_type:
conditions.append(gle.party_type == party_type)
if party:
conditions.append(gle.party == party)
account_details = (
qb.from_(gle)
.select(
gle.account,
gle.party_type,
gle.party,
gle.account_currency,
(Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency)).as_(
"balance_in_account_currency"
),
(Sum(gle.debit) - Sum(gle.credit)).as_("balance"),
(Sum(gle.debit) - Sum(gle.credit) == 0)
^ (Sum(gle.debit_in_account_currency) - Sum(gle.credit_in_account_currency) == 0).as_(
"zero_balance"
),
)
.where(Criterion.all(conditions))
.groupby(gle.account, NullIf(gle.party_type, ""), NullIf(gle.party, ""))
.having(having_clause)
.orderby(gle.account)
.run(as_dict=True)
)
return account_details
@staticmethod
def calculate_new_account_balance(company, posting_date, account_details):
accounts = []
company_currency = erpnext.get_company_currency(company)
precision = get_field_precision(
frappe.get_meta("Exchange Rate Revaluation Account").get_field("new_balance_in_base_currency"),
company_currency,
)
account_details = self.get_accounts_from_gle()
for d in account_details:
current_exchange_rate = (
d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0
)
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, self.posting_date)
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
}
if account_details:
# Handle Accounts with balance in both Account/Base Currency
for d in [x for x in account_details if not x.zero_balance]:
current_exchange_rate = (
d.balance / d.balance_in_account_currency if d.balance_in_account_currency else 0
)
new_exchange_rate = get_exchange_rate(d.account_currency, company_currency, posting_date)
new_balance_in_base_currency = flt(d.balance_in_account_currency * new_exchange_rate)
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": d.balance_in_account_currency,
"gain_loss": gain_loss,
}
)
if not accounts:
self.throw_invalid_response_message(account_details)
# Handle Accounts with '0' balance in Account/Base Currency
for d in [x for x in account_details if x.zero_balance]:
# TODO: Set new balance in Base/Account currency
if d.balance > 0:
current_exchange_rate = new_exchange_rate = 0
new_balance_in_account_currency = 0 # this will be '0'
new_balance_in_base_currency = 0 # this will be '0'
gain_loss = flt(new_balance_in_base_currency, precision) - flt(d.balance, precision)
else:
new_exchange_rate = 0
new_balance_in_base_currency = 0
new_balance_in_account_currency = 0
current_exchange_rate = calculate_exchange_rate_using_last_gle(
company, d.account, d.party_type, d.party
)
gain_loss = new_balance_in_account_currency - (
current_exchange_rate * d.balance_in_account_currency
)
if gain_loss:
accounts.append(
{
"account": d.account,
"party_type": d.party_type,
"party": d.party,
"account_currency": d.account_currency,
"balance_in_base_currency": d.balance,
"balance_in_account_currency": d.balance_in_account_currency,
"zero_balance": d.zero_balance,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
"new_balance_in_account_currency": new_balance_in_account_currency,
"gain_loss": gain_loss,
}
)
return accounts
def get_accounts_from_gle(self):
company_currency = erpnext.get_company_currency(self.company)
accounts = frappe.db.sql_list(
"""
select name
from tabAccount
where is_group = 0
and report_type = 'Balance Sheet'
and root_type in ('Asset', 'Liability', 'Equity')
and account_type != 'Stock'
and company=%s
and account_currency != %s
order by name""",
(self.company, company_currency),
)
account_details = []
if accounts:
account_details = frappe.db.sql(
"""
select
account, party_type, party, account_currency,
sum(debit_in_account_currency) - sum(credit_in_account_currency) as balance_in_account_currency,
sum(debit) - sum(credit) as balance
from `tabGL Entry`
where account in (%s)
and posting_date <= %s
and is_cancelled = 0
group by account, NULLIF(party_type,''), NULLIF(party,'')
having sum(debit) != sum(credit)
order by account
"""
% (", ".join(["%s"] * len(accounts)), "%s"),
tuple(accounts + [self.posting_date]),
as_dict=1,
)
return account_details
def throw_invalid_response_message(self, account_details):
if account_details:
message = _("No outstanding invoices require exchange rate revaluation")
@ -134,11 +258,7 @@ class ExchangeRateRevaluation(Document):
message = _("No outstanding invoices found")
frappe.msgprint(message)
@frappe.whitelist()
def make_jv_entry(self):
if self.total_gain_loss == 0:
return
def get_for_unrealized_gain_loss_account(self):
unrealized_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "unrealized_exchange_gain_loss_account"
)
@ -146,6 +266,130 @@ class ExchangeRateRevaluation(Document):
frappe.throw(
_("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company)
)
return unrealized_exchange_gain_loss_account
@frappe.whitelist()
def make_jv_entries(self):
zero_balance_jv = self.make_jv_for_zero_balance()
if zero_balance_jv:
frappe.msgprint(
f"Zero Balance Journal: {get_link_to_form('Journal Entry', zero_balance_jv.name)}"
)
revaluation_jv = self.make_jv_for_revaluation()
if revaluation_jv:
frappe.msgprint(
f"Revaluation Journal: {get_link_to_form('Journal Entry', revaluation_jv.name)}"
)
return {
"revaluation_jv": revaluation_jv.name if revaluation_jv else None,
"zero_balance_jv": zero_balance_jv.name if zero_balance_jv else None,
}
def make_jv_for_zero_balance(self):
if self.gain_loss_booked == 0:
return
accounts = [x for x in self.accounts if x.zero_balance]
if not accounts:
return
unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
journal_entry.company = self.company
journal_entry.posting_date = self.posting_date
journal_entry.multi_currency = 1
journal_entry_accounts = []
for d in accounts:
journal_account = frappe._dict(
{
"account": d.get("account"),
"party_type": d.get("party_type"),
"party": d.get("party"),
"account_currency": d.get("account_currency"),
"balance": flt(
d.get("balance_in_account_currency"), d.precision("balance_in_account_currency")
),
"exchange_rate": 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
# Account Currency has balance
if d.get("balance_in_account_currency") and not d.get("new_balance_in_account_currency"):
dr_or_cr = (
"credit_in_account_currency"
if d.get("balance_in_account_currency") > 0
else "debit_in_account_currency"
)
reverse_dr_or_cr = (
"debit_in_account_currency"
if dr_or_cr == "credit_in_account_currency"
else "credit_in_account_currency"
)
journal_account.update(
{
dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
),
reverse_dr_or_cr: 0,
"debit": 0,
"credit": 0,
}
)
elif d.get("balance_in_base_currency") and not d.get("new_balance_in_base_currency"):
# Base currency has balance
dr_or_cr = "credit" if d.get("balance_in_base_currency") > 0 else "debit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
journal_account.update(
{
dr_or_cr: flt(
abs(d.get("balance_in_base_currency")), d.precision("balance_in_base_currency")
),
reverse_dr_or_cr: 0,
"debit_in_account_currency": 0,
"credit_in_account_currency": 0,
}
)
journal_entry_accounts.append(journal_account)
journal_entry_accounts.append(
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
"credit": abs(self.gain_loss_booked) if self.gain_loss_booked > 0 else 0,
"debit_in_account_currency": abs(self.gain_loss_booked) if self.gain_loss_booked < 0 else 0,
"credit_in_account_currency": self.gain_loss_booked if self.gain_loss_booked > 0 else 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
}
)
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_total_debit_credit()
journal_entry.save()
return journal_entry
def make_jv_for_revaluation(self):
if self.gain_loss_unbooked == 0:
return
accounts = [x for x in self.accounts if not x.zero_balance]
if not accounts:
return
unrealized_exchange_gain_loss_account = self.get_for_unrealized_gain_loss_account()
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Rate Revaluation"
@ -154,7 +398,7 @@ class ExchangeRateRevaluation(Document):
journal_entry.multi_currency = 1
journal_entry_accounts = []
for d in self.accounts:
for d in accounts:
dr_or_cr = (
"debit_in_account_currency"
if d.get("balance_in_account_currency") > 0
@ -179,6 +423,7 @@ class ExchangeRateRevaluation(Document):
dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
),
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
@ -196,6 +441,7 @@ class ExchangeRateRevaluation(Document):
reverse_dr_or_cr: flt(
abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
),
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
@ -206,8 +452,11 @@ class ExchangeRateRevaluation(Document):
{
"account": unrealized_exchange_gain_loss_account,
"balance": get_balance_on(unrealized_exchange_gain_loss_account),
"debit_in_account_currency": abs(self.total_gain_loss) if self.total_gain_loss < 0 else 0,
"credit_in_account_currency": self.total_gain_loss if self.total_gain_loss > 0 else 0,
"debit_in_account_currency": abs(self.gain_loss_unbooked)
if self.gain_loss_unbooked < 0
else 0,
"credit_in_account_currency": self.gain_loss_unbooked if self.gain_loss_unbooked > 0 else 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
@ -217,38 +466,91 @@ class ExchangeRateRevaluation(Document):
journal_entry.set("accounts", journal_entry_accounts)
journal_entry.set_amounts_in_company_currency()
journal_entry.set_total_debit_credit()
return journal_entry.as_dict()
journal_entry.save()
return journal_entry
def calculate_exchange_rate_using_last_gle(company, account, party_type, party):
"""
Use last GL entry to calculate exchange rate
"""
last_exchange_rate = None
if company and account:
gl = qb.DocType("GL Entry")
# build conditions
conditions = []
conditions.append(gl.company == company)
conditions.append(gl.account == account)
conditions.append(gl.is_cancelled == 0)
if party_type:
conditions.append(gl.party_type == party_type)
if party:
conditions.append(gl.party == party)
voucher_type, voucher_no = (
qb.from_(gl)
.select(gl.voucher_type, gl.voucher_no)
.where(Criterion.all(conditions))
.orderby(gl.posting_date, order=Order.desc)
.limit(1)
.run()[0]
)
last_exchange_rate = (
qb.from_(gl)
.select((gl.debit - gl.credit) / (gl.debit_in_account_currency - gl.credit_in_account_currency))
.where(
(gl.voucher_type == voucher_type) & (gl.voucher_no == voucher_no) & (gl.account == account)
)
.orderby(gl.posting_date, order=Order.desc)
.limit(1)
.run()[0][0]
)
return last_exchange_rate
@frappe.whitelist()
def get_account_details(account, company, posting_date, party_type=None, party=None):
account_currency, account_type = frappe.db.get_value(
def get_account_details(company, posting_date, account, party_type=None, party=None):
if not (company and posting_date):
frappe.throw(_("Company and Posting Date is mandatory"))
account_currency, account_type = frappe.get_cached_value(
"Account", account, ["account_currency", "account_type"]
)
if account_type in ["Receivable", "Payable"] and not (party_type and party):
frappe.throw(_("Party Type and Party is mandatory for {0} account").format(account_type))
account_details = {}
company_currency = erpnext.get_company_currency(company)
balance = get_balance_on(
account, date=posting_date, party_type=party_type, party=party, in_account_currency=False
account_details = {
"account_currency": account_currency,
}
account_balance = ExchangeRateRevaluation.get_account_balance_from_gle(
company=company, posting_date=posting_date, account=account, party_type=party_type, party=party
)
if balance:
balance_in_account_currency = get_balance_on(
account, date=posting_date, party_type=party_type, party=party
if account_balance and (
account_balance[0].balance or account_balance[0].balance_in_account_currency
):
account_with_new_balance = ExchangeRateRevaluation.calculate_new_account_balance(
company, posting_date, account_balance
)
current_exchange_rate = (
balance / balance_in_account_currency if balance_in_account_currency else 0
row = account_with_new_balance[0]
account_details.update(
{
"balance_in_base_currency": row["balance_in_base_currency"],
"balance_in_account_currency": row["balance_in_account_currency"],
"current_exchange_rate": row["current_exchange_rate"],
"new_exchange_rate": row["new_exchange_rate"],
"new_balance_in_base_currency": row["new_balance_in_base_currency"],
"new_balance_in_account_currency": row["new_balance_in_account_currency"],
"zero_balance": row["zero_balance"],
"gain_loss": row["gain_loss"],
}
)
new_exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
new_balance_in_base_currency = balance_in_account_currency * new_exchange_rate
account_details = {
"account_currency": account_currency,
"balance_in_base_currency": balance,
"balance_in_account_currency": balance_in_account_currency,
"current_exchange_rate": current_exchange_rate,
"new_exchange_rate": new_exchange_rate,
"new_balance_in_base_currency": new_balance_in_base_currency,
}
return account_details

View File

@ -1,475 +1,161 @@
{
"allow_copy": 0,
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"beta": 0,
"actions": [],
"creation": "2018-04-13 18:30:06.110433",
"custom": 0,
"docstatus": 0,
"doctype": "DocType",
"document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"account",
"party_type",
"party",
"column_break_2",
"account_currency",
"account_balances",
"balance_in_account_currency",
"column_break_46yz",
"new_balance_in_account_currency",
"balances",
"current_exchange_rate",
"column_break_xown",
"new_exchange_rate",
"column_break_9",
"balance_in_base_currency",
"column_break_ukce",
"new_balance_in_base_currency",
"section_break_ngrs",
"gain_loss",
"zero_balance"
],
"fields": [
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 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,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "party_type",
"fieldtype": "Link",
"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": "Party Type",
"length": 0,
"no_copy": 0,
"options": "DocType",
"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,
"translatable": 0,
"unique": 0
"options": "DocType"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "party",
"fieldtype": "Dynamic Link",
"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": "Party",
"length": 0,
"no_copy": 0,
"options": "party_type",
"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,
"translatable": 0,
"unique": 0
"options": "party_type"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_2",
"fieldtype": "Column 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,
"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,
"translatable": 0,
"unique": 0
"fieldtype": "Column Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "account_currency",
"fieldtype": "Link",
"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": "Account Currency",
"length": 0,
"no_copy": 0,
"options": "Currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "balance_in_account_currency",
"fieldtype": "Currency",
"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": "Balance In Account Currency",
"length": 0,
"no_copy": 0,
"options": "account_currency",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "balances",
"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": "",
"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,
"translatable": 0,
"unique": 0
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "current_exchange_rate",
"fieldtype": "Float",
"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": "Current Exchange Rate",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "balance_in_base_currency",
"fieldtype": "Currency",
"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": "Balance In Base Currency",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Company:company:default_currency",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "column_break_9",
"fieldtype": "Column 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,
"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,
"translatable": 0,
"unique": 0
"fieldtype": "Section Break"
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "new_exchange_rate",
"fieldtype": "Float",
"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": "New Exchange Rate",
"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,
"translatable": 0,
"unique": 0
"reqd": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "new_balance_in_base_currency",
"fieldtype": "Currency",
"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": "New Balance In Base Currency",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Company:company:default_currency",
"read_only": 1
},
{
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_if_empty": 0,
"fieldname": "gain_loss",
"fieldtype": "Currency",
"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": "Gain/Loss",
"length": 0,
"no_copy": 0,
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1,
"remember_last_selected_value": 0,
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 0,
"translatable": 0,
"unique": 0
"options": "Company:company:default_currency",
"read_only": 1
},
{
"default": "0",
"description": "This Account has '0' balance in either Base Currency or Account Currency",
"fieldname": "zero_balance",
"fieldtype": "Check",
"label": "Zero Balance"
},
{
"fieldname": "new_balance_in_account_currency",
"fieldtype": "Currency",
"label": "New Balance In Account Currency",
"options": "account_currency",
"read_only": 1
},
{
"fieldname": "account_balances",
"fieldtype": "Section Break"
},
{
"fieldname": "column_break_46yz",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_xown",
"fieldtype": "Column Break"
},
{
"fieldname": "column_break_ukce",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_ngrs",
"fieldtype": "Section Break"
}
],
"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": "2019-06-26 18:57:51.762345",
"links": [],
"modified": "2022-12-29 19:38:52.915295",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Exchange Rate Revaluation Account",
"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,
"track_views": 0
"states": [],
"track_changes": 1
}

View File

@ -9,10 +9,6 @@ from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate
class FiscalYearIncorrectDate(frappe.ValidationError):
pass
class FiscalYear(Document):
@frappe.whitelist()
def set_as_default(self):
@ -53,23 +49,18 @@ class FiscalYear(Document):
)
def validate_dates(self):
self.validate_from_to_dates("year_start_date", "year_end_date")
if self.is_short_year:
# Fiscal Year can be shorter than one year, in some jurisdictions
# under certain circumstances. For example, in the USA and Germany.
return
if getdate(self.year_start_date) > getdate(self.year_end_date):
frappe.throw(
_("Fiscal Year Start Date should be one year earlier than Fiscal Year End Date"),
FiscalYearIncorrectDate,
)
date = getdate(self.year_start_date) + relativedelta(years=1) - relativedelta(days=1)
if getdate(self.year_end_date) != date:
frappe.throw(
_("Fiscal Year End Date should be one year after Fiscal Year Start Date"),
FiscalYearIncorrectDate,
frappe.exceptions.InvalidDates,
)
def on_update(self):
@ -169,5 +160,6 @@ def auto_create_fiscal_year():
def get_from_and_to_date(fiscal_year):
fields = ["year_start_date as from_date", "year_end_date as to_date"]
return frappe.db.get_value("Fiscal Year", fiscal_year, fields, as_dict=1)
fields = ["year_start_date", "year_end_date"]
cached_results = frappe.get_cached_value("Fiscal Year", fiscal_year, fields, as_dict=1)
return dict(from_date=cached_results.year_start_date, to_date=cached_results.year_end_date)

View File

@ -7,8 +7,6 @@ import unittest
import frappe
from frappe.utils import now_datetime
from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrectDate
test_ignore = ["Company"]
@ -26,7 +24,7 @@ class TestFiscalYear(unittest.TestCase):
}
)
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
self.assertRaises(frappe.exceptions.InvalidDates, fy.insert)
def test_record_generator():
@ -35,8 +33,8 @@ def test_record_generator():
"doctype": "Fiscal Year",
"year": "_Test Short Fiscal Year 2011",
"is_short_year": 1,
"year_end_date": "2011-04-01",
"year_start_date": "2011-12-31",
"year_start_date": "2011-04-01",
"year_end_date": "2011-12-31",
}
]

View File

@ -58,7 +58,7 @@ class GLEntry(Document):
validate_balance_type(self.account, adv_adj)
validate_frozen_account(self.account, adv_adj)
if frappe.db.get_value("Account", self.account, "account_type") not in [
if frappe.get_cached_value("Account", self.account, "account_type") not in [
"Receivable",
"Payable",
]:
@ -95,7 +95,15 @@ class GLEntry(Document):
)
# Zero value transaction is not allowed
if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))):
if not (
flt(self.debit, self.precision("debit"))
or flt(self.credit, self.precision("credit"))
or (
self.voucher_type == "Journal Entry"
and frappe.get_cached_value("Journal Entry", self.voucher_no, "voucher_type")
== "Exchange Gain Or Loss"
)
):
frappe.throw(
_("{0} {1}: Either debit or credit amount is required for {2}").format(
self.voucher_type, self.voucher_no, self.account
@ -120,7 +128,7 @@ class GLEntry(Document):
frappe.throw(msg, title=_("Missing Cost Center"))
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")
account_type = frappe.get_cached_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
if (
@ -188,7 +196,7 @@ class GLEntry(Document):
def check_pl_account(self):
if (
self.is_opening == "Yes"
and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss"
and frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss"
and not self.is_cancelled
):
frappe.throw(
@ -281,7 +289,7 @@ class GLEntry(Document):
def validate_balance_type(account, adv_adj=False):
if not adv_adj and account:
balance_must_be = frappe.db.get_value("Account", account, "balance_must_be")
balance_must_be = frappe.get_cached_value("Account", account, "balance_must_be")
if balance_must_be:
balance = frappe.db.sql(
"""select sum(debit) - sum(credit)
@ -366,7 +374,7 @@ def update_outstanding_amt(
if against_voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"]:
ref_doc = frappe.get_doc(against_voucher_type, against_voucher)
# Didn't use db_set for optimisation purpose
# Didn't use db_set for optimization purpose
ref_doc.outstanding_amount = bal
frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal)

View File

@ -21,7 +21,7 @@ class ItemTaxTemplate(Document):
check_list = []
for d in self.get("taxes"):
if d.tax_type:
account_type = frappe.db.get_value("Account", d.tax_type, "account_type")
account_type = frappe.get_cached_value("Account", d.tax_type, "account_type")
if account_type not in [
"Tax",

View File

@ -8,7 +8,7 @@ frappe.provide("erpnext.journal_entry");
frappe.ui.form.on("Journal Entry", {
setup: function(frm) {
frm.add_fetch("bank_account", "account", "account");
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', 'Journal Entry', "Repost Payment Ledger"];
},
refresh: function(frm) {
@ -173,8 +173,8 @@ frappe.ui.form.on("Journal Entry", {
var update_jv_details = function(doc, r) {
$.each(r, function(i, d) {
var row = frappe.model.add_child(doc, "Journal Entry Account", "accounts");
row.account = d.account;
row.balance = d.balance;
frappe.model.set_value(row.doctype, row.name, "account", d.account)
frappe.model.set_value(row.doctype, row.name, "balance", d.balance)
});
refresh_field("accounts");
}
@ -253,9 +253,6 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
var party_account_field = jvd.reference_type==="Sales Invoice" ? "debit_to": "credit_to";
out.filters.push([jvd.reference_type, party_account_field, "=", jvd.account]);
if (in_list(['Debit Note', 'Credit Note'], doc.voucher_type)) {
out.filters.push([jvd.reference_type, "is_return", "=", 1]);
}
}
if(in_list(["Sales Order", "Purchase Order"], jvd.reference_type)) {
@ -312,8 +309,7 @@ erpnext.accounts.JournalEntry = class JournalEntry extends frappe.ui.form.Contro
}
}
get_outstanding(doctype, docname, company, child, due_date) {
var me = this;
get_outstanding(doctype, docname, company, child) {
var args = {
"doctype": doctype,
"docname": docname,

View File

@ -88,7 +88,7 @@
"label": "Entry Type",
"oldfieldname": "voucher_type",
"oldfieldtype": "Select",
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense",
"options": "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nExchange Gain Or Loss\nDeferred Revenue\nDeferred Expense",
"reqd": 1,
"search_index": 1
},
@ -137,8 +137,7 @@
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book",
"read_only": 1
"options": "Finance Book"
},
{
"fieldname": "2_add_edit_gl_entries",
@ -539,7 +538,7 @@
"idx": 176,
"is_submittable": 1,
"links": [],
"modified": "2022-06-23 22:01:32.348337",
"modified": "2023-01-17 12:53:53.280620",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",

View File

@ -6,7 +6,7 @@ import json
import frappe
from frappe import _, msgprint, scrub
from frappe.utils import cint, cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
from frappe.utils import cstr, flt, fmt_money, formatdate, get_link_to_form, nowdate
import erpnext
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
@ -23,6 +23,9 @@ from erpnext.accounts.utils import (
get_stock_accounts,
get_stock_and_account_balance,
)
from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import (
get_depr_schedule,
)
from erpnext.controllers.accounts_controller import AccountsController
@ -34,9 +37,6 @@ class JournalEntry(AccountsController):
def __init__(self, *args, **kwargs):
super(JournalEntry, self).__init__(*args, **kwargs)
def get_feed(self):
return self.voucher_type
def validate(self):
if self.voucher_type == "Opening Entry":
self.is_opening = "Yes"
@ -81,6 +81,7 @@ class JournalEntry(AccountsController):
self.check_credit_limit()
self.make_gl_entries()
self.update_advance_paid()
self.update_asset_value()
self.update_inter_company_jv()
self.update_invoice_discounting()
@ -88,7 +89,13 @@ class JournalEntry(AccountsController):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
unlink_ref_doc_from_payment_entries(self)
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Payment Ledger Entry",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
)
self.make_gl_entries(1)
self.update_advance_paid()
self.unlink_advance_entry_reference()
@ -184,7 +191,9 @@ class JournalEntry(AccountsController):
}
)
tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details(
inv, self.tax_withholding_category
)
if not tax_withholding_details:
return
@ -223,6 +232,29 @@ class JournalEntry(AccountsController):
for d in to_remove:
self.remove(d)
def update_asset_value(self):
if self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"):
if (
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
):
processed_assets.append(d.reference_name)
asset = frappe.get_doc("Asset", d.reference_name)
if asset.calculate_depreciation:
continue
depr_value = d.debit or d.credit
asset.db_set("value_after_depreciation", asset.value_after_depreciation - depr_value)
asset.set_status()
def update_inter_company_jv(self):
if (
self.voucher_type == "Inter Company Journal Entry"
@ -281,19 +313,45 @@ class JournalEntry(AccountsController):
d.db_update()
def unlink_asset_reference(self):
if self.voucher_type != "Depreciation Entry":
return
processed_assets = []
for d in self.get("accounts"):
if d.reference_type == "Asset" and d.reference_name:
if (
d.reference_type == "Asset" and d.reference_name and d.reference_name not in processed_assets
):
processed_assets.append(d.reference_name)
asset = frappe.get_doc("Asset", d.reference_name)
for s in asset.get("schedules"):
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
idx = cint(s.finance_book_id) or 1
finance_books = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation += s.depreciation_amount
finance_books.db_update()
if asset.calculate_depreciation:
je_found = False
asset.set_status()
for row in asset.get("finance_books"):
if je_found:
break
depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book)
for s in depr_schedule or []:
if s.journal_entry == self.name:
s.db_set("journal_entry", None)
row.value_after_depreciation += s.depreciation_amount
row.db_update()
asset.set_status()
je_found = True
break
else:
depr_value = d.debit or d.credit
asset.db_set("value_after_depreciation", asset.value_after_depreciation + depr_value)
asset.set_status()
def unlink_inter_company_jv(self):
if (
@ -317,7 +375,7 @@ class JournalEntry(AccountsController):
def validate_party(self):
for d in self.get("accounts"):
account_type = frappe.db.get_value("Account", d.account, "account_type")
account_type = frappe.get_cached_value("Account", d.account, "account_type")
if account_type in ["Receivable", "Payable"]:
if not (d.party_type and d.party):
frappe.throw(
@ -380,7 +438,7 @@ class JournalEntry(AccountsController):
def validate_against_jv(self):
for d in self.get("accounts"):
if d.reference_type == "Journal Entry":
account_root_type = frappe.db.get_value("Account", d.account, "root_type")
account_root_type = frappe.get_cached_value("Account", d.account, "root_type")
if account_root_type == "Asset" and flt(d.debit) > 0:
frappe.throw(
_(
@ -590,28 +648,30 @@ class JournalEntry(AccountsController):
d.against_account = frappe.db.get_value(d.reference_type, d.reference_name, field)
else:
for d in self.get("accounts"):
if flt(d.debit > 0):
if flt(d.debit) > 0:
accounts_debited.append(d.party or d.account)
if flt(d.credit) > 0:
accounts_credited.append(d.party or d.account)
for d in self.get("accounts"):
if flt(d.debit > 0):
if flt(d.debit) > 0:
d.against_account = ", ".join(list(set(accounts_credited)))
if flt(d.credit > 0):
if flt(d.credit) > 0:
d.against_account = ", ".join(list(set(accounts_debited)))
def validate_debit_credit_amount(self):
for d in self.get("accounts"):
if not flt(d.debit) and not flt(d.credit):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
for d in self.get("accounts"):
if not flt(d.debit) and not flt(d.credit):
frappe.throw(_("Row {0}: Both Debit and Credit values cannot be zero").format(d.idx))
def validate_total_debit_and_credit(self):
self.set_total_debit_credit()
if self.difference:
frappe.throw(
_("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
)
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
if self.difference:
frappe.throw(
_("Total Debit must be equal to Total Credit. The difference is {0}").format(self.difference)
)
def set_total_debit_credit(self):
self.total_debit, self.total_credit, self.difference = 0, 0, 0
@ -629,7 +689,7 @@ class JournalEntry(AccountsController):
def validate_multi_currency(self):
alternate_currency = []
for d in self.get("accounts"):
account = frappe.db.get_value(
account = frappe.get_cached_value(
"Account", d.account, ["account_currency", "account_type"], as_dict=1
)
if account:
@ -649,16 +709,17 @@ class JournalEntry(AccountsController):
self.set_exchange_rate()
def set_amounts_in_company_currency(self):
for d in self.get("accounts"):
d.debit_in_account_currency = flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
)
d.credit_in_account_currency = flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
)
if not (self.voucher_type == "Exchange Gain Or Loss" and self.multi_currency):
for d in self.get("accounts"):
d.debit_in_account_currency = flt(
d.debit_in_account_currency, d.precision("debit_in_account_currency")
)
d.credit_in_account_currency = flt(
d.credit_in_account_currency, d.precision("credit_in_account_currency")
)
d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit"))
d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit"))
d.debit = flt(d.debit_in_account_currency * flt(d.exchange_rate), d.precision("debit"))
d.credit = flt(d.credit_in_account_currency * flt(d.exchange_rate), d.precision("credit"))
def set_exchange_rate(self):
for d in self.get("accounts"):
@ -757,10 +818,10 @@ class JournalEntry(AccountsController):
pay_to_recd_from = d.party
if pay_to_recd_from and pay_to_recd_from == d.party:
party_amount += d.debit_in_account_currency or d.credit_in_account_currency
party_amount += flt(d.debit_in_account_currency) or flt(d.credit_in_account_currency)
party_account_currency = d.account_currency
elif frappe.db.get_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
elif frappe.get_cached_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
bank_amount += d.debit_in_account_currency or d.credit_in_account_currency
bank_account_currency = d.account_currency
@ -787,7 +848,7 @@ class JournalEntry(AccountsController):
def build_gl_map(self):
gl_map = []
for d in self.get("accounts"):
if d.debit or d.credit:
if d.debit or d.credit or (self.voucher_type == "Exchange Gain Or Loss"):
r = [d.user_remark, self.remark]
r = [x for x in r if x]
remarks = "\n".join(r)
@ -835,7 +896,7 @@ class JournalEntry(AccountsController):
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
@frappe.whitelist()
def get_balance(self):
def get_balance(self, difference_account=None):
if not self.get("accounts"):
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
else:
@ -850,7 +911,13 @@ class JournalEntry(AccountsController):
blank_row = d
if not blank_row:
blank_row = self.append("accounts", {})
blank_row = self.append(
"accounts",
{
"account": difference_account,
"cost_center": erpnext.get_default_cost_center(self.company),
},
)
blank_row.exchange_rate = 1
if diff > 0:
@ -985,7 +1052,7 @@ def get_default_bank_cash_account(company, account_type=None, mode_of_payment=No
account = account_list[0].name
if account:
account_details = frappe.db.get_value(
account_details = frappe.get_cached_value(
"Account", account, ["account_currency", "account_type"], as_dict=1
)
@ -1114,7 +1181,7 @@ def get_payment_entry(ref_doc, args):
"party_type": args.get("party_type"),
"party": ref_doc.get(args.get("party_type").lower()),
"cost_center": cost_center,
"account_type": frappe.db.get_value("Account", args.get("party_account"), "account_type"),
"account_type": frappe.get_cached_value("Account", args.get("party_account"), "account_type"),
"account_currency": args.get("party_account_currency")
or get_account_currency(args.get("party_account")),
"balance": get_balance_on(args.get("party_account")),
@ -1208,6 +1275,7 @@ def get_outstanding(args):
args = json.loads(args)
company_currency = erpnext.get_company_currency(args.get("company"))
due_date = None
if args.get("doctype") == "Journal Entry":
condition = " and party=%(party)s" if args.get("party") else ""
@ -1232,10 +1300,12 @@ def get_outstanding(args):
invoice = frappe.db.get_value(
args["doctype"],
args["docname"],
["outstanding_amount", "conversion_rate", scrub(party_type)],
["outstanding_amount", "conversion_rate", scrub(party_type), "due_date"],
as_dict=1,
)
due_date = invoice.get("due_date")
exchange_rate = (
invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1
)
@ -1258,6 +1328,7 @@ def get_outstanding(args):
"exchange_rate": exchange_rate,
"party_type": party_type,
"party": invoice.get(scrub(party_type)),
"reference_due_date": due_date,
}
@ -1277,7 +1348,7 @@ def get_party_account_and_balance(company, party_type, party, cost_center=None):
"account": account,
"balance": account_balance,
"party_balance": party_balance,
"account_currency": frappe.db.get_value("Account", account, "account_currency"),
"account_currency": frappe.get_cached_value("Account", account, "account_currency"),
}
@ -1290,7 +1361,7 @@ def get_account_balance_and_party_type(
frappe.msgprint(_("No Permission"), raise_exception=1)
company_currency = erpnext.get_company_currency(company)
account_details = frappe.db.get_value(
account_details = frappe.get_cached_value(
"Account", account, ["account_type", "account_currency"], as_dict=1
)
@ -1343,7 +1414,7 @@ def get_exchange_rate(
):
from erpnext.setup.utils import get_exchange_rate
account_details = frappe.db.get_value(
account_details = frappe.get_cached_value(
"Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
)

View File

@ -202,6 +202,7 @@
"fieldname": "reference_type",
"fieldtype": "Select",
"label": "Reference Type",
"no_copy": 1,
"options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement"
},
{
@ -209,13 +210,15 @@
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Reference Name",
"no_copy": 1,
"options": "reference_type"
},
{
"depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])",
"fieldname": "reference_due_date",
"fieldtype": "Select",
"label": "Reference Due Date"
"fieldtype": "Date",
"label": "Reference Due Date",
"no_copy": 1
},
{
"fieldname": "project",
@ -274,19 +277,22 @@
"fieldname": "reference_detail_no",
"fieldtype": "Data",
"hidden": 1,
"label": "Reference Detail No"
"label": "Reference Detail No",
"no_copy": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
"modified": "2021-08-30 21:27:32.200299",
"modified": "2022-10-26 20:03:10.906259",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry Account",
"naming_rule": "Random",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -2,7 +2,7 @@
// For license information, please see license.txt
frappe.ui.form.on("Journal Entry Template", {
setup: function(frm) {
refresh: function(frm) {
frappe.model.set_default_values(frm.doc);
frm.set_query("account" ,"accounts", function(){
@ -45,21 +45,6 @@ frappe.ui.form.on("Journal Entry Template", {
frm.trigger("clear_child");
switch(frm.doc.voucher_type){
case "Opening Entry":
frm.set_value("is_opening", "Yes");
frappe.call({
type:"GET",
method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts",
args: {
"company": frm.doc.company
},
callback: function(r) {
if(r.message) {
add_accounts(frm.doc, r.message);
}
}
});
break;
case "Bank Entry":
case "Cash Entry":
frappe.call({

View File

@ -4,22 +4,20 @@
import frappe
from frappe import _
from frappe.model.document import Document
from frappe.utils.background_jobs import is_job_queued
from erpnext.accounts.doctype.account.account import merge_account
class LedgerMerge(Document):
def start_merge(self):
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.background_jobs import enqueue
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot merge accounts."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()]
if self.name not in enqueued_jobs:
if not is_job_queued(self.name):
enqueue(
start_merge,
queue="default",

View File

@ -25,7 +25,7 @@ class ModeofPayment(Document):
def validate_accounts(self):
for entry in self.accounts:
"""Error when Company of Ledger account doesn't match with Company Selected"""
if frappe.db.get_value("Account", entry.default_account, "company") != entry.company:
if frappe.get_cached_value("Account", entry.default_account, "company") != entry.company:
frappe.throw(
_("Account {0} does not match with Company {1} in Mode of Account: {2}").format(
entry.default_account, entry.company, self.name

View File

@ -20,15 +20,14 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
frm.dashboard.reset();
frm.doc.import_in_progress = true;
}
if (data.user != frappe.session.user) return;
if (data.count == data.total) {
setTimeout((title) => {
setTimeout(() => {
frm.doc.import_in_progress = false;
frm.clear_table("invoices");
frm.refresh_fields();
frm.page.clear_indicator();
frm.dashboard.hide_progress(title);
frappe.msgprint(__("Opening {0} Invoice created", [frm.doc.invoice_type]));
frm.dashboard.hide_progress();
frappe.msgprint(__("Opening {0} Invoices created", [frm.doc.invoice_type]));
}, 1500, data.title);
return;
}
@ -51,13 +50,6 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
method: "make_invoices",
freeze: 1,
freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type]),
callback: function(r) {
if (r.message.length == 1) {
frappe.msgprint(__("{0} Invoice created successfully.", [frm.doc.invoice_type]));
} else if (r.message.length < 50) {
frappe.msgprint(__("{0} Invoices created successfully.", [frm.doc.invoice_type]));
}
}
});
});

View File

@ -6,7 +6,7 @@ import frappe
from frappe import _, scrub
from frappe.model.document import Document
from frappe.utils import flt, nowdate
from frappe.utils.background_jobs import enqueue
from frappe.utils.background_jobs import enqueue, is_job_queued
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
@ -207,14 +207,12 @@ class OpeningInvoiceCreationTool(Document):
if len(invoices) < 50:
return start_import(invoices)
else:
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.utils.scheduler import is_scheduler_inactive
if is_scheduler_inactive() and not frappe.flags.in_test:
frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()]
if self.name not in enqueued_jobs:
if not is_job_queued(self.name):
enqueue(
start_import,
queue="default",
@ -257,17 +255,15 @@ def start_import(invoices):
def publish(index, total, doctype):
if total < 50:
return
frappe.publish_realtime(
"opening_invoice_creation_progress",
dict(
title=_("Opening Invoice Creation In Progress"),
message=_("Creating {} out of {} {}").format(index + 1, total, doctype),
user=frappe.session.user,
count=index + 1,
total=total,
),
user=frappe.session.user,
)

View File

@ -7,7 +7,7 @@ cur_frm.cscript.tax_table = "Advance Taxes and Charges";
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice'];
frm.ignore_doctypes_on_cancel_all = ['Sales Invoice', 'Purchase Invoice', "Repost Payment Ledger"];
if(frm.doc.__islocal) {
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
@ -1091,7 +1091,7 @@ frappe.ui.form.on('Payment Entry', {
$.each(tax_fields, function(i, fieldname) { tax[fieldname] = 0.0; });
frm.doc.paid_amount_after_tax = frm.doc.paid_amount;
frm.doc.paid_amount_after_tax = frm.doc.base_paid_amount;
});
},
@ -1182,7 +1182,7 @@ frappe.ui.form.on('Payment Entry', {
}
cumulated_tax_fraction += tax.tax_fraction_for_current_item;
frm.doc.paid_amount_after_tax = flt(frm.doc.paid_amount/(1+cumulated_tax_fraction))
frm.doc.paid_amount_after_tax = flt(frm.doc.base_paid_amount/(1+cumulated_tax_fraction))
});
},
@ -1214,6 +1214,7 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.total_taxes_and_charges = 0.0;
frm.doc.base_total_taxes_and_charges = 0.0;
let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
let actual_tax_dict = {};
// maintain actual tax rate based on idx
@ -1234,8 +1235,8 @@ frappe.ui.form.on('Payment Entry', {
}
}
tax.tax_amount = current_tax_amount;
tax.base_tax_amount = tax.tax_amount * frm.doc.source_exchange_rate;
// tax accounts are only in company currency
tax.base_tax_amount = current_tax_amount;
current_tax_amount *= (tax.add_deduct_tax == "Deduct") ? -1.0 : 1.0;
if(i==0) {
@ -1244,9 +1245,29 @@ frappe.ui.form.on('Payment Entry', {
tax.total = flt(frm.doc["taxes"][i-1].total + current_tax_amount, precision("total", tax));
}
tax.base_total = tax.total * frm.doc.source_exchange_rate;
frm.doc.total_taxes_and_charges += current_tax_amount;
frm.doc.base_total_taxes_and_charges += current_tax_amount * frm.doc.source_exchange_rate;
// tac accounts are only in company currency
tax.base_total = tax.total
// calculate total taxes and base total taxes
if(frm.doc.payment_type == "Pay") {
// tax accounts only have company currency
if(tax.currency != frm.doc.paid_to_account_currency) {
//total_taxes_and_charges has the target currency. so using target conversion rate
frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.target_exchange_rate);
} else {
frm.doc.total_taxes_and_charges += current_tax_amount;
}
} else if(frm.doc.payment_type == "Receive") {
if(tax.currency != frm.doc.paid_from_account_currency) {
//total_taxes_and_charges has the target currency. so using source conversion rate
frm.doc.total_taxes_and_charges += flt(current_tax_amount / frm.doc.source_exchange_rate);
} else {
frm.doc.total_taxes_and_charges += current_tax_amount;
}
}
frm.doc.base_total_taxes_and_charges += tax.base_tax_amount;
frm.refresh_field('taxes');
frm.refresh_field('total_taxes_and_charges');

View File

@ -239,7 +239,7 @@
"depends_on": "paid_from",
"fieldname": "paid_from_account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"label": "Account Currency (From)",
"options": "Currency",
"print_hide": 1,
"read_only": 1,
@ -249,7 +249,7 @@
"depends_on": "paid_from",
"fieldname": "paid_from_account_balance",
"fieldtype": "Currency",
"label": "Account Balance",
"label": "Account Balance (From)",
"options": "paid_from_account_currency",
"print_hide": 1,
"read_only": 1
@ -272,7 +272,7 @@
"depends_on": "paid_to",
"fieldname": "paid_to_account_currency",
"fieldtype": "Link",
"label": "Account Currency",
"label": "Account Currency (To)",
"options": "Currency",
"print_hide": 1,
"read_only": 1,
@ -282,7 +282,7 @@
"depends_on": "paid_to",
"fieldname": "paid_to_account_balance",
"fieldtype": "Currency",
"label": "Account Balance",
"label": "Account Balance (To)",
"options": "paid_to_account_currency",
"print_hide": 1,
"read_only": 1
@ -304,7 +304,8 @@
{
"fieldname": "source_exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"label": "Source Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
},
@ -333,7 +334,8 @@
{
"fieldname": "target_exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"label": "Target Exchange Rate",
"precision": "9",
"print_hide": 1,
"reqd": 1
},
@ -631,14 +633,14 @@
"depends_on": "eval:doc.party_type == 'Supplier'",
"fieldname": "purchase_taxes_and_charges_template",
"fieldtype": "Link",
"label": "Taxes and Charges Template",
"label": "Purchase Taxes and Charges Template",
"options": "Purchase Taxes and Charges Template"
},
{
"depends_on": "eval: doc.party_type == 'Customer'",
"fieldname": "sales_taxes_and_charges_template",
"fieldtype": "Link",
"label": "Taxes and Charges Template",
"label": "Sales Taxes and Charges Template",
"options": "Sales Taxes and Charges Template"
},
{
@ -731,7 +733,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2022-02-23 20:08:39.559814",
"modified": "2023-02-14 04:52:30.478523",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",

View File

@ -62,7 +62,6 @@ class PaymentEntry(AccountsController):
self.set_missing_values()
self.validate_payment_type()
self.validate_party_details()
self.validate_bank_accounts()
self.set_exchange_rate()
self.validate_mandatory()
self.validate_reference_documents()
@ -93,7 +92,13 @@ class PaymentEntry(AccountsController):
self.set_status()
def on_cancel(self):
self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Payment Ledger Entry")
self.ignore_linked_doctypes = (
"GL Entry",
"Stock Ledger Entry",
"Payment Ledger Entry",
"Repost Payment Ledger",
"Repost Payment Ledger Items",
)
self.make_gl_entries(cancel=1)
self.update_outstanding_amounts()
self.update_advance_paid()
@ -181,7 +186,11 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Party is mandatory"))
_party_name = "title" if self.party_type == "Shareholder" else self.party_type.lower() + "_name"
self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name)
if frappe.db.has_column(self.party_type, _party_name):
self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name)
else:
self.party_name = frappe.db.get_value(self.party_type, self.party, "name")
if self.party:
if not self.party_balance:
@ -239,29 +248,12 @@ class PaymentEntry(AccountsController):
if not frappe.db.exists(self.party_type, self.party):
frappe.throw(_("Invalid {0}: {1}").format(self.party_type, self.party))
if self.party_account and self.party_type in ("Customer", "Supplier"):
self.validate_account_type(
self.party_account, [erpnext.get_party_account_type(self.party_type)]
)
def validate_bank_accounts(self):
if self.payment_type in ("Pay", "Internal Transfer"):
self.validate_account_type(self.paid_from, ["Bank", "Cash"])
if self.payment_type in ("Receive", "Internal Transfer"):
self.validate_account_type(self.paid_to, ["Bank", "Cash"])
def validate_account_type(self, account, account_types):
account_type = frappe.db.get_value("Account", account, "account_type")
# if account_type not in account_types:
# frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types)))
def set_exchange_rate(self, ref_doc=None):
self.set_source_exchange_rate(ref_doc)
self.set_target_exchange_rate(ref_doc)
def set_source_exchange_rate(self, ref_doc=None):
if self.paid_from and not self.source_exchange_rate:
if self.paid_from:
if self.paid_from_account_currency == self.company_currency:
self.source_exchange_rate = 1
else:
@ -295,6 +287,9 @@ class PaymentEntry(AccountsController):
def validate_reference_documents(self):
valid_reference_doctypes = self.get_valid_reference_doctypes()
if not valid_reference_doctypes:
return
for d in self.get("references"):
if not d.allocated_amount:
continue
@ -362,7 +357,7 @@ class PaymentEntry(AccountsController):
if not d.allocated_amount:
continue
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"):
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount, is_return = frappe.get_cached_value(
d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"]
)
@ -633,7 +628,7 @@ class PaymentEntry(AccountsController):
self.payment_type == "Receive"
and self.base_total_allocated_amount < self.base_received_amount + total_deductions
and self.total_allocated_amount
< self.paid_amount + (total_deductions / self.source_exchange_rate)
< flt(self.paid_amount) + (total_deductions / self.source_exchange_rate)
):
self.unallocated_amount = (
self.base_received_amount + total_deductions - self.base_total_allocated_amount
@ -643,7 +638,7 @@ class PaymentEntry(AccountsController):
self.payment_type == "Pay"
and self.base_total_allocated_amount < (self.base_paid_amount - total_deductions)
and self.total_allocated_amount
< self.received_amount + (total_deductions / self.target_exchange_rate)
< flt(self.received_amount) + (total_deductions / self.target_exchange_rate)
):
self.unallocated_amount = (
self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
@ -695,35 +690,34 @@ class PaymentEntry(AccountsController):
)
def validate_payment_against_negative_invoice(self):
if (self.payment_type == "Pay" and self.party_type == "Customer") or (
self.payment_type == "Receive" and self.party_type == "Supplier"
if (self.payment_type != "Pay" or self.party_type != "Customer") and (
self.payment_type != "Receive" or self.party_type != "Supplier"
):
return
total_negative_outstanding = sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
total_negative_outstanding = sum(
abs(flt(d.outstanding_amount)) for d in self.get("references") if flt(d.outstanding_amount) < 0
)
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum(flt(d.amount) for d in self.deductions)
if not total_negative_outstanding:
if self.party_type == "Customer":
msg = _("Cannot pay to Customer without any negative outstanding invoice")
else:
msg = _("Cannot receive from Supplier without any negative outstanding invoice")
frappe.throw(msg, InvalidPaymentEntry)
elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
),
InvalidPaymentEntry,
)
paid_amount = self.paid_amount if self.payment_type == "Receive" else self.received_amount
additional_charges = sum([flt(d.amount) for d in self.deductions])
if not total_negative_outstanding:
frappe.throw(
_("Cannot {0} {1} {2} without any negative outstanding invoice").format(
_(self.payment_type),
(_("to") if self.party_type == "Customer" else _("from")),
self.party_type,
),
InvalidPaymentEntry,
)
elif paid_amount - additional_charges > total_negative_outstanding:
frappe.throw(
_("Paid Amount cannot be greater than total negative outstanding amount {0}").format(
total_negative_outstanding
),
InvalidPaymentEntry,
)
def set_title(self):
if frappe.flags.in_import and self.title:
# do not set title dynamically if title exists during data import.
@ -736,7 +730,7 @@ class PaymentEntry(AccountsController):
def validate_transaction_reference(self):
bank_account = self.paid_to if self.payment_type == "Receive" else self.paid_from
bank_account_type = frappe.db.get_value("Account", bank_account, "account_type")
bank_account_type = frappe.get_cached_value("Account", bank_account, "account_type")
if bank_account_type == "Bank":
if not self.reference_no or not self.reference_date:
@ -933,6 +927,13 @@ class PaymentEntry(AccountsController):
)
if not d.included_in_paid_amount:
if get_account_currency(payment_account) != self.company_currency:
if self.payment_type == "Receive":
exchange_rate = self.target_exchange_rate
elif self.payment_type in ["Pay", "Internal Transfer"]:
exchange_rate = self.source_exchange_rate
base_tax_amount = flt((tax_amount / exchange_rate), self.precision("paid_amount"))
gl_entries.append(
self.get_gl_dict(
{
@ -981,7 +982,9 @@ class PaymentEntry(AccountsController):
if self.payment_type in ("Receive", "Pay") and self.party:
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype in frappe.get_hooks("advance_payment_doctypes"):
frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid()
frappe.get_doc(
d.reference_doctype, d.reference_name, for_update=True
).set_total_advance_paid()
def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name
@ -1026,7 +1029,7 @@ class PaymentEntry(AccountsController):
for fieldname in tax_fields:
tax.set(fieldname, 0.0)
self.paid_amount_after_tax = self.paid_amount
self.paid_amount_after_tax = self.base_paid_amount
def determine_exclusive_rate(self):
if not any(cint(tax.included_in_paid_amount) for tax in self.get("taxes")):
@ -1045,7 +1048,7 @@ class PaymentEntry(AccountsController):
cumulated_tax_fraction += tax.tax_fraction_for_current_item
self.paid_amount_after_tax = flt(self.paid_amount / (1 + cumulated_tax_fraction))
self.paid_amount_after_tax = flt(self.base_paid_amount / (1 + cumulated_tax_fraction))
def calculate_taxes(self):
self.total_taxes_and_charges = 0.0
@ -1068,7 +1071,7 @@ class PaymentEntry(AccountsController):
current_tax_amount += actual_tax_dict[tax.idx]
tax.tax_amount = current_tax_amount
tax.base_tax_amount = tax.tax_amount * self.source_exchange_rate
tax.base_tax_amount = current_tax_amount
if tax.add_deduct_tax == "Deduct":
current_tax_amount *= -1.0
@ -1082,14 +1085,20 @@ class PaymentEntry(AccountsController):
self.get("taxes")[i - 1].total + current_tax_amount, self.precision("total", tax)
)
tax.base_total = tax.total * self.source_exchange_rate
tax.base_total = tax.total
if self.payment_type == "Pay":
self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
if tax.currency != self.paid_to_account_currency:
self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate)
else:
self.total_taxes_and_charges += current_tax_amount
elif self.payment_type == "Receive":
if tax.currency != self.paid_from_account_currency:
self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate)
else:
self.total_taxes_and_charges += current_tax_amount
self.base_total_taxes_and_charges += tax.base_tax_amount
if self.get("taxes"):
self.paid_amount_after_tax = self.get("taxes")[-1].base_total
@ -1184,6 +1193,8 @@ def get_outstanding_reference_documents(args):
ple = qb.DocType("Payment Ledger Entry")
common_filter = []
accounting_dimensions_filter = []
posting_and_due_date = []
# confirm that Supplier is not blocked
if args.get("party_type") == "Supplier":
@ -1200,7 +1211,7 @@ def get_outstanding_reference_documents(args):
party_account_currency = get_account_currency(args.get("party_account"))
company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency")
# Get positive outstanding sales /purchase invoices/ Fees
# Get positive outstanding sales /purchase invoices
condition = ""
if args.get("voucher_type") and args.get("voucher_no"):
condition = " and voucher_type={0} and voucher_no={1}".format(
@ -1212,7 +1223,7 @@ def get_outstanding_reference_documents(args):
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
common_filter.append(ple.cost_center == args.get("cost_center"))
accounting_dimensions_filter.append(ple.cost_center == args.get("cost_center"))
date_fields_dict = {
"posting_date": ["from_posting_date", "to_posting_date"],
@ -1224,7 +1235,7 @@ def get_outstanding_reference_documents(args):
condition += " and {0} between '{1}' and '{2}'".format(
fieldname, args.get(date_fields[0]), args.get(date_fields[1])
)
common_filter.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
posting_and_due_date.append(ple[fieldname][args.get(date_fields[0]) : args.get(date_fields[1])])
if args.get("company"):
condition += " and company = {0}".format(frappe.db.escape(args.get("company")))
@ -1235,8 +1246,10 @@ def get_outstanding_reference_documents(args):
args.get("party"),
args.get("party_account"),
common_filter=common_filter,
posting_date=posting_and_due_date,
min_outstanding=args.get("outstanding_amt_greater_than"),
max_outstanding=args.get("outstanding_amt_less_than"),
accounting_dimensions=accounting_dimensions_filter,
)
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
@ -1297,7 +1310,7 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
d.voucher_type, d.voucher_no, "payment_terms_template"
)
if payment_term_template:
allocate_payment_based_on_payment_terms = frappe.db.get_value(
allocate_payment_based_on_payment_terms = frappe.get_cached_value(
"Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
)
if allocate_payment_based_on_payment_terms:
@ -1529,7 +1542,7 @@ def get_account_details(account, date, cost_center=None):
{
"account_currency": get_account_currency(account),
"account_balance": account_balance,
"account_type": frappe.db.get_value("Account", account, "account_type"),
"account_type": frappe.get_cached_value("Account", account, "account_type"),
}
)
@ -1595,10 +1608,11 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
elif reference_doctype != "Journal Entry":
if not total_amount:
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
# for handling cases that don't have multi-currency (base field)
total_amount = ref_doc.get("grand_total") or ref_doc.get("base_grand_total")
exchange_rate = 1
else:
total_amount = ref_doc.grand_total
total_amount = ref_doc.get("grand_total")
if not exchange_rate:
# Get the exchange rate from the original ref doc
# or get it based on the posting date of the ref doc.
@ -1609,7 +1623,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
outstanding_amount = flt(total_amount) - flt(ref_doc.get("advance_paid"))
else:
# Get the exchange rate based on the posting date of the ref doc.
@ -1627,16 +1641,23 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
@frappe.whitelist()
def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=None):
def get_payment_entry(
dt, dn, party_amount=None, bank_account=None, bank_amount=None, party_type=None, payment_type=None
):
reference_doc = None
doc = frappe.get_doc(dt, dn)
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0:
if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) >= 99.99:
frappe.throw(_("Can only make payment against unbilled {0}").format(dt))
party_type = set_party_type(dt)
if not party_type:
party_type = set_party_type(dt)
party_account = set_party_account(dt, dn, doc, party_type)
party_account_currency = set_party_account_currency(dt, party_account, doc)
payment_type = set_payment_type(dt, doc)
if not payment_type:
payment_type = set_payment_type(dt, doc)
grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(
party_amount, dt, party_account_currency, doc
)
@ -1690,9 +1711,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked():
frappe.msgprint(_("{0} is on hold till {1}").format(doc.name, doc.release_date))
else:
if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_value(
if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_cached_value(
"Payment Terms Template",
{"name": doc.payment_terms_template},
doc.payment_terms_template,
"allocate_payment_based_on_payment_terms",
):
@ -1743,6 +1764,8 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.setup_party_account_field()
pe.set_missing_values()
update_accounting_dimensions(pe, doc)
if party_account and bank:
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
@ -1760,6 +1783,18 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
return pe
def update_accounting_dimensions(pe, doc):
"""
Updates accounting dimensions in Payment Entry based on the accounting dimensions in the reference document
"""
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
for dimension in get_accounting_dimensions():
pe.set(dimension, doc.get(dimension))
def get_bank_cash_account(doc, bank_account):
bank = get_default_bank_cash_account(
doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
@ -1786,8 +1821,6 @@ def set_party_account(dt, dn, doc, party_type):
party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to
elif dt == "Purchase Invoice":
party_account = doc.credit_to
elif dt == "Fees":
party_account = doc.receivable_account
else:
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
return party_account
@ -1803,8 +1836,7 @@ def set_party_account_currency(dt, party_account, doc):
def set_payment_type(dt, doc):
if (
dt == "Sales Order"
or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)
dt == "Sales Order" or (dt in ("Sales Invoice", "Dunning") and doc.outstanding_amount > 0)
) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0):
payment_type = "Receive"
else:
@ -1822,18 +1854,15 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
else:
grand_total = doc.rounded_total or doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt == "Fees":
grand_total = doc.grand_total
outstanding_amount = doc.outstanding_amount
elif dt == "Dunning":
grand_total = doc.grand_total
outstanding_amount = doc.grand_total
else:
if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
grand_total = flt(doc.get("base_rounded_total") or doc.get("base_grand_total"))
else:
grand_total = flt(doc.get("rounded_total") or doc.grand_total)
outstanding_amount = grand_total - flt(doc.advance_paid)
grand_total = flt(doc.get("rounded_total") or doc.get("grand_total"))
outstanding_amount = doc.get("outstanding_amount") or (grand_total - flt(doc.advance_paid))
return grand_total, outstanding_amount

View File

@ -4,6 +4,7 @@
import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import flt, nowdate
@ -722,6 +723,46 @@ class TestPaymentEntry(FrappeTestCase):
flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)
)
def test_gl_of_multi_currency_payment_with_taxes(self):
payment_entry = create_payment_entry(
party="_Test Supplier USD", paid_to="_Test Payable USD - _TC", save=True
)
payment_entry.append(
"taxes",
{
"account_head": "_Test Account Service Tax - _TC",
"charge_type": "Actual",
"tax_amount": 100,
"add_deduct_tax": "Add",
"description": "Test",
},
)
payment_entry.target_exchange_rate = 80
payment_entry.received_amount = 12.5
payment_entry = payment_entry.submit()
gle = qb.DocType("GL Entry")
gl_entries = (
qb.from_(gle)
.select(
gle.account,
gle.debit,
gle.credit,
gle.debit_in_account_currency,
gle.credit_in_account_currency,
)
.orderby(gle.account)
.where(gle.voucher_no == payment_entry.name)
.run()
)
expected_gl_entries = (
("_Test Account Service Tax - _TC", 100.0, 0.0, 100.0, 0.0),
("_Test Bank - _TC", 0.0, 1100.0, 0.0, 1100.0),
("_Test Payable USD - _TC", 1000.0, 0.0, 12.5, 0),
)
self.assertEqual(gl_entries, expected_gl_entries)
def test_payment_entry_against_onhold_purchase_invoice(self):
pi = make_purchase_invoice()

View File

@ -25,7 +25,8 @@
"in_list_view": 1,
"label": "Type",
"options": "DocType",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"columns": 2,
@ -35,7 +36,8 @@
"in_list_view": 1,
"label": "Name",
"options": "reference_doctype",
"reqd": 1
"reqd": 1,
"search_index": 1
},
{
"fieldname": "due_date",
@ -104,7 +106,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-09-26 17:06:55.597389",
"modified": "2022-12-12 12:31:44.919895",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
@ -113,5 +115,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -3,6 +3,7 @@
frappe.ui.form.on('Payment Gateway Account', {
refresh(frm) {
erpnext.utils.check_payments_app();
if(!frm.doc.__islocal) {
frm.set_df_property('payment_gateway', 'read_only', 1);
}

View File

@ -11,7 +11,7 @@ class PaymentGatewayAccount(Document):
self.name = self.payment_gateway + " - " + self.currency
def validate(self):
self.currency = frappe.db.get_value("Account", self.payment_account, "account_currency")
self.currency = frappe.get_cached_value("Account", self.payment_account, "account_currency")
self.update_default_payment_gateway()
self.set_as_default_if_not_set()

View File

@ -22,7 +22,8 @@
"amount",
"account_currency",
"amount_in_account_currency",
"delinked"
"delinked",
"remarks"
],
"fields": [
{
@ -136,12 +137,17 @@
"fieldtype": "Link",
"label": "Finance Book",
"options": "Finance Book"
},
{
"fieldname": "remarks",
"fieldtype": "Text",
"label": "Remarks"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2022-07-11 09:13:54.379168",
"modified": "2022-08-22 15:32:56.629430",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Ledger Entry",

View File

@ -97,7 +97,7 @@ class PaymentLedgerEntry(Document):
)
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")
account_type = frappe.get_cached_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
if (

View File

@ -3,12 +3,13 @@
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.tests.utils import FrappeTestCase, change_settings
from frappe.utils import nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
from erpnext.stock.doctype.item.test_item import create_item
@ -127,6 +128,25 @@ class TestPaymentLedgerEntry(FrappeTestCase):
payment.posting_date = posting_date
return payment
def create_sales_order(
self, qty=1, rate=100, posting_date=nowdate(), do_not_save=False, do_not_submit=False
):
so = make_sales_order(
company=self.company,
transaction_date=posting_date,
customer=self.customer,
item_code=self.item,
cost_center=self.cost_center,
warehouse=self.warehouse,
debit_to=self.debit_to,
currency="INR",
qty=qty,
rate=100,
do_not_save=do_not_save,
do_not_submit=do_not_submit,
)
return so
def clear_old_entries(self):
doctype_list = [
"GL Entry",
@ -406,3 +426,89 @@ class TestPaymentLedgerEntry(FrappeTestCase):
]
self.assertEqual(pl_entries_for_crnote[0], expected_values[0])
self.assertEqual(pl_entries_for_crnote[1], expected_values[1])
@change_settings(
"Accounts Settings",
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
)
def test_multi_payment_unlink_on_invoice_cancellation(self):
transaction_date = nowdate()
amount = 100
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
for amt in [40, 40, 20]:
# payment 1
pe = get_payment_entry(si.doctype, si.name)
pe.paid_amount = amt
pe.get("references")[0].allocated_amount = amt
pe = pe.save().submit()
si.reload()
si.cancel()
entries = frappe.db.get_list(
"Payment Ledger Entry",
filters={"against_voucher_type": si.doctype, "against_voucher_no": si.name, "delinked": 0},
)
self.assertEqual(entries, [])
# with references removed, deletion should be possible
si.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name)
@change_settings(
"Accounts Settings",
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
)
def test_multi_je_unlink_on_invoice_cancellation(self):
transaction_date = nowdate()
amount = 100
si = self.create_sales_invoice(qty=1, rate=amount, posting_date=transaction_date)
# multiple JE's against invoice
for amt in [40, 40, 20]:
je1 = self.create_journal_entry(
self.income_account, self.debit_to, amt, posting_date=transaction_date
)
je1.get("accounts")[1].party_type = "Customer"
je1.get("accounts")[1].party = self.customer
je1.get("accounts")[1].reference_type = si.doctype
je1.get("accounts")[1].reference_name = si.name
je1 = je1.save().submit()
si.reload()
si.cancel()
entries = frappe.db.get_list(
"Payment Ledger Entry",
filters={"against_voucher_type": si.doctype, "against_voucher_no": si.name, "delinked": 0},
)
self.assertEqual(entries, [])
# with references removed, deletion should be possible
si.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, si.doctype, si.name)
@change_settings(
"Accounts Settings",
{"unlink_payment_on_cancellation_of_invoice": 1, "delete_linked_ledger_entries": 1},
)
def test_advance_payment_unlink_on_order_cancellation(self):
transaction_date = nowdate()
amount = 100
so = self.create_sales_order(qty=1, rate=amount, posting_date=transaction_date).save().submit()
pe = get_payment_entry(so.doctype, so.name).save().submit()
so.reload()
so.cancel()
entries = frappe.db.get_list(
"Payment Ledger Entry",
filters={"against_voucher_type": so.doctype, "against_voucher_no": so.name, "delinked": 0},
)
self.assertEqual(entries, [])
# with references removed, deletion should be possible
so.delete()
self.assertRaises(frappe.DoesNotExistError, frappe.get_doc, so.doctype, so.name)

View File

@ -170,7 +170,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
}
reconcile() {
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount);
if (show_dialog && show_dialog.length) {
@ -179,8 +179,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
title: __("Select Difference Account"),
fields: [
{
fieldname: "allocation", fieldtype: "Table", label: __("Allocation"),
data: this.data, in_place_edit: true,
fieldname: "allocation",
fieldtype: "Table",
label: __("Allocation"),
data: this.data,
in_place_edit: true,
cannot_add_rows: true,
get_data: () => {
return this.data;
},
@ -218,6 +222,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
read_only: 1
}]
},
{
fieldtype: 'HTML',
options: "<b> New Journal Entry will be posted for the difference amount </b>"
}
],
primary_action: () => {
const args = dialog.get_values()["allocation"];
@ -234,7 +242,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
});
this.frm.doc.allocation.forEach(d => {
if (d.difference_amount && !d.difference_account) {
if (d.difference_amount) {
dialog.fields_dict.allocation.df.data.push({
'docname': d.name,
'reference_name': d.reference_name,

View File

@ -22,6 +22,8 @@ class PaymentReconciliation(Document):
def __init__(self, *args, **kwargs):
super(PaymentReconciliation, self).__init__(*args, **kwargs)
self.common_filter_conditions = []
self.accounting_dimension_filter_conditions = []
self.ple_posting_date_filter = []
@frappe.whitelist()
def get_unreconciled_entries(self):
@ -67,6 +69,10 @@ class PaymentReconciliation(Document):
def get_jv_entries(self):
condition = self.get_conditions()
if self.get("cost_center"):
condition += f" and t2.cost_center = '{self.cost_center}' "
dr_or_cr = (
"credit_in_account_currency"
if erpnext.get_party_account_type(self.party_type) == "Receivable"
@ -77,12 +83,13 @@ class PaymentReconciliation(Document):
"t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
)
# nosemgrep
journal_entries = frappe.db.sql(
"""
select
"Journal Entry" as reference_type, t1.name as reference_name,
t1.posting_date, t1.remark as remarks, t2.name as reference_row,
{dr_or_cr} as amount, t2.is_advance,
{dr_or_cr} as amount, t2.is_advance, t2.exchange_rate,
t2.account_currency as currency
from
`tabJournal Entry` t1, `tabJournal Entry Account` t2
@ -150,6 +157,7 @@ class PaymentReconciliation(Document):
return_outstanding = ple_query.get_voucher_outstandings(
vouchers=return_invoices,
common_filter=self.common_filter_conditions,
posting_date=self.ple_posting_date_filter,
min_outstanding=-(self.minimum_payment_amount) if self.minimum_payment_amount else None,
max_outstanding=-(self.maximum_payment_amount) if self.maximum_payment_amount else None,
get_payments=True,
@ -187,8 +195,10 @@ class PaymentReconciliation(Document):
self.party,
self.receivable_payable_account,
common_filter=self.common_filter_conditions,
posting_date=self.ple_posting_date_filter,
min_outstanding=self.minimum_invoice_amount if self.minimum_invoice_amount else None,
max_outstanding=self.maximum_invoice_amount if self.maximum_invoice_amount else None,
accounting_dimensions=self.accounting_dimension_filter_conditions,
)
if self.invoice_limit:
@ -209,9 +219,26 @@ class PaymentReconciliation(Document):
inv.currency = entry.get("currency")
inv.outstanding_amount = flt(entry.get("outstanding_amount"))
def get_difference_amount(self, payment_entry, invoice, allocated_amount):
difference_amount = 0
if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get(
"exchange_rate", 1
):
allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount
allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount
difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate
return difference_amount
@frappe.whitelist()
def allocate_entries(self, args):
self.validate_entries()
invoice_exchange_map = self.get_invoice_exchange_map(args.get("invoices"), args.get("payments"))
default_exchange_gain_loss_account = frappe.get_cached_value(
"Company", self.company, "exchange_gain_loss_account"
)
entries = []
for pay in args.get("payments"):
pay.update({"unreconciled_amount": pay.get("amount")})
@ -224,12 +251,22 @@ class PaymentReconciliation(Document):
res = self.get_allocated_entry(pay, inv, pay["amount"])
inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
pay["amount"] = 0
inv["exchange_rate"] = invoice_exchange_map.get(inv.get("invoice_number"))
if pay.get("reference_type") in ["Sales Invoice", "Purchase Invoice"]:
pay["exchange_rate"] = invoice_exchange_map.get(pay.get("reference_name"))
res.difference_amount = self.get_difference_amount(pay, inv, res["allocated_amount"])
res.difference_account = default_exchange_gain_loss_account
res.exchange_rate = inv.get("exchange_rate")
if pay.get("amount") == 0:
entries.append(res)
break
elif inv.get("outstanding_amount") == 0:
entries.append(res)
continue
else:
break
@ -251,6 +288,7 @@ class PaymentReconciliation(Document):
"amount": pay.get("amount"),
"allocated_amount": allocated_amount,
"difference_amount": pay.get("difference_amount"),
"currency": inv.get("currency"),
}
)
@ -273,7 +311,11 @@ class PaymentReconciliation(Document):
else:
reconciled_entry = entry_list
reconciled_entry.append(self.get_payment_details(row, dr_or_cr))
payment_details = self.get_payment_details(row, dr_or_cr)
reconciled_entry.append(payment_details)
if payment_details.difference_amount:
self.make_difference_entry(payment_details)
if entry_list:
reconcile_against_document(entry_list)
@ -284,6 +326,57 @@ class PaymentReconciliation(Document):
msgprint(_("Successfully Reconciled"))
self.get_unreconciled_entries()
def make_difference_entry(self, row):
journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = "Exchange Gain Or Loss"
journal_entry.company = self.company
journal_entry.posting_date = nowdate()
journal_entry.multi_currency = 1
party_account_currency = frappe.get_cached_value(
"Account", self.receivable_payable_account, "account_currency"
)
difference_account_currency = frappe.get_cached_value(
"Account", row.difference_account, "account_currency"
)
# Account Currency has balance
dr_or_cr = "debit" if self.party_type == "Customer" else "credit"
reverse_dr_or_cr = "debit" if dr_or_cr == "credit" else "credit"
journal_account = frappe._dict(
{
"account": self.receivable_payable_account,
"party_type": self.party_type,
"party": self.party,
"account_currency": party_account_currency,
"exchange_rate": 0,
"cost_center": erpnext.get_default_cost_center(self.company),
"reference_type": row.against_voucher_type,
"reference_name": row.against_voucher,
dr_or_cr: flt(row.difference_amount),
dr_or_cr + "_in_account_currency": 0,
}
)
journal_entry.append("accounts", journal_account)
journal_account = frappe._dict(
{
"account": row.difference_account,
"account_currency": difference_account_currency,
"exchange_rate": 1,
"cost_center": erpnext.get_default_cost_center(self.company),
reverse_dr_or_cr + "_in_account_currency": flt(row.difference_amount),
reverse_dr_or_cr: flt(row.difference_amount),
}
)
journal_entry.append("accounts", journal_account)
journal_entry.save()
journal_entry.submit()
def get_payment_details(self, row, dr_or_cr):
return frappe._dict(
{
@ -293,6 +386,7 @@ class PaymentReconciliation(Document):
"against_voucher_type": row.get("invoice_type"),
"against_voucher": row.get("invoice_number"),
"account": self.receivable_payable_account,
"exchange_rate": row.get("exchange_rate"),
"party_type": self.party_type,
"party": self.party,
"is_advance": row.get("is_advance"),
@ -317,6 +411,49 @@ class PaymentReconciliation(Document):
if not self.get("payments"):
frappe.throw(_("No records found in the Payments table"))
def get_invoice_exchange_map(self, invoices, payments):
sales_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Sales Invoice"
]
sales_invoices.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Sales Invoice"]
)
purchase_invoices = [
d.get("invoice_number") for d in invoices if d.get("invoice_type") == "Purchase Invoice"
]
purchase_invoices.extend(
[d.get("reference_name") for d in payments if d.get("reference_type") == "Purchase Invoice"]
)
invoice_exchange_map = frappe._dict()
if sales_invoices:
sales_invoice_map = frappe._dict(
frappe.db.get_all(
"Sales Invoice",
filters={"name": ("in", sales_invoices)},
fields=["name", "conversion_rate"],
as_list=1,
)
)
invoice_exchange_map.update(sales_invoice_map)
if purchase_invoices:
purchase_invoice_map = frappe._dict(
frappe.db.get_all(
"Purchase Invoice",
filters={"name": ("in", purchase_invoices)},
fields=["name", "conversion_rate"],
as_list=1,
)
)
invoice_exchange_map.update(purchase_invoice_map)
return invoice_exchange_map
def validate_allocation(self):
unreconciled_invoices = frappe._dict()
@ -350,24 +487,26 @@ class PaymentReconciliation(Document):
def build_qb_filter_conditions(self, get_invoices=False, get_return_invoices=False):
self.common_filter_conditions.clear()
self.accounting_dimension_filter_conditions.clear()
self.ple_posting_date_filter.clear()
ple = qb.DocType("Payment Ledger Entry")
self.common_filter_conditions.append(ple.company == self.company)
if self.get("cost_center") and (get_invoices or get_return_invoices):
self.common_filter_conditions.append(ple.cost_center == self.cost_center)
self.accounting_dimension_filter_conditions.append(ple.cost_center == self.cost_center)
if get_invoices:
if self.from_invoice_date:
self.common_filter_conditions.append(ple.posting_date.gte(self.from_invoice_date))
self.ple_posting_date_filter.append(ple.posting_date.gte(self.from_invoice_date))
if self.to_invoice_date:
self.common_filter_conditions.append(ple.posting_date.lte(self.to_invoice_date))
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_invoice_date))
elif get_return_invoices:
if self.from_payment_date:
self.common_filter_conditions.append(ple.posting_date.gte(self.from_payment_date))
self.ple_posting_date_filter.append(ple.posting_date.gte(self.from_payment_date))
if self.to_payment_date:
self.common_filter_conditions.append(ple.posting_date.lte(self.to_payment_date))
self.ple_posting_date_filter.append(ple.posting_date.lte(self.to_payment_date))
def get_conditions(self, get_payments=False):
condition = " and company = '{0}' ".format(self.company)

View File

@ -6,8 +6,10 @@ import unittest
import frappe
from frappe import qb
from frappe.tests.utils import FrappeTestCase
from frappe.utils import add_days, nowdate
from frappe.utils import add_days, flt, nowdate
from erpnext import get_default_cost_center
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from erpnext.accounts.doctype.payment_entry.test_payment_entry import create_payment_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.party import get_party_account
@ -20,6 +22,7 @@ class TestPaymentReconciliation(FrappeTestCase):
self.create_item()
self.create_customer()
self.create_account()
self.create_cost_center()
self.clear_old_entries()
def tearDown(self):
@ -72,33 +75,11 @@ class TestPaymentReconciliation(FrappeTestCase):
self.item = item if isinstance(item, str) else item.item_code
def create_customer(self):
if frappe.db.exists("Customer", "_Test PR Customer"):
self.customer = "_Test PR Customer"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer"
customer.type = "Individual"
customer.save()
self.customer = customer.name
if frappe.db.exists("Customer", "_Test PR Customer 2"):
self.customer2 = "_Test PR Customer 2"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer 2"
customer.type = "Individual"
customer.save()
self.customer2 = customer.name
if frappe.db.exists("Customer", "_Test PR Customer 3"):
self.customer3 = "_Test PR Customer 3"
else:
customer = frappe.new_doc("Customer")
customer.customer_name = "_Test PR Customer 3"
customer.type = "Individual"
customer.default_currency = "EUR"
customer.save()
self.customer3 = customer.name
self.customer = make_customer("_Test PR Customer")
self.customer2 = make_customer("_Test PR Customer 2")
self.customer3 = make_customer("_Test PR Customer 3", "EUR")
self.customer4 = make_customer("_Test PR Customer 4", "EUR")
self.customer5 = make_customer("_Test PR Customer 5", "EUR")
def create_account(self):
account_name = "Debtors EUR"
@ -216,6 +197,22 @@ class TestPaymentReconciliation(FrappeTestCase):
)
return je
def create_cost_center(self):
# Setup cost center
cc_name = "Sub"
self.main_cc = frappe.get_doc("Cost Center", get_default_cost_center(self.company))
cc_exists = frappe.db.get_list("Cost Center", filters={"cost_center_name": cc_name})
if cc_exists:
self.sub_cc = frappe.get_doc("Cost Center", cc_exists[0].name)
else:
sub_cc = frappe.new_doc("Cost Center")
sub_cc.cost_center_name = "Sub"
sub_cc.parent_cost_center = self.main_cc.parent_cost_center
sub_cc.company = self.main_cc.company
self.sub_cc = sub_cc.save()
def test_filter_min_max(self):
# check filter condition minimum and maximum amount
self.create_sales_invoice(qty=1, rate=300)
@ -283,6 +280,41 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.get("invoices")), 2)
self.assertEqual(len(pr.get("payments")), 2)
def test_filter_posting_date_case2(self):
"""
Posting date should not affect outstanding amount calculation
"""
from_date = add_days(nowdate(), -30)
to_date = nowdate()
self.create_payment_entry(amount=25, posting_date=from_date).submit()
self.create_sales_invoice(rate=25, qty=1, posting_date=to_date)
pr = self.create_payment_reconciliation()
pr.from_invoice_date = pr.from_payment_date = from_date
pr.to_invoice_date = pr.to_payment_date = to_date
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 1)
invoices = [x.as_dict() for x in pr.invoices]
payments = [x.as_dict() for x in pr.payments]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.reconcile()
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 0)
self.assertEqual(len(pr.payments), 0)
pr.from_invoice_date = pr.from_payment_date = to_date
pr.to_invoice_date = pr.to_payment_date = to_date
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 0)
def test_filter_invoice_limit(self):
# check filter condition - invoice limit
transaction_date = nowdate()
@ -441,6 +473,11 @@ class TestPaymentReconciliation(FrappeTestCase):
invoices = [x.as_dict() for x in pr.get("invoices")]
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
# Cr Note and Invoice are of the same currency. There shouldn't any difference amount.
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
pr.get_unreconciled_entries()
@ -474,6 +511,11 @@ class TestPaymentReconciliation(FrappeTestCase):
payments = [x.as_dict() for x in pr.get("payments")]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].allocated_amount = allocated_amount
# Cr Note and Invoice are of the same currency. There shouldn't any difference amount.
for row in pr.allocation:
self.assertEqual(flt(row.get("difference_amount")), 0.0)
pr.reconcile()
# assert outstanding
@ -543,3 +585,255 @@ class TestPaymentReconciliation(FrappeTestCase):
self.assertEqual(len(pr.payments), 1)
self.assertEqual(pr.payments[0].amount, amount)
self.assertEqual(pr.payments[0].currency, "EUR")
def test_difference_amount_via_journal_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer4
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.save().submit()
# Make payment using Journal Entry
je1 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 100, nowdate())
je1.multi_currency = 1
je1.accounts[0].exchange_rate = 1
je1.accounts[0].credit_in_account_currency = 0
je1.accounts[0].credit = 0
je1.accounts[0].debit_in_account_currency = 8000
je1.accounts[0].debit = 8000
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer4
je1.accounts[1].exchange_rate = 80
je1.accounts[1].credit_in_account_currency = 100
je1.accounts[1].credit = 8000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je1.save()
je1.submit()
je2 = self.create_journal_entry("HDFC - _PR", self.debtors_eur, 200, nowdate())
je2.multi_currency = 1
je2.accounts[0].exchange_rate = 1
je2.accounts[0].credit_in_account_currency = 0
je2.accounts[0].credit = 0
je2.accounts[0].debit_in_account_currency = 16000
je2.accounts[0].debit = 16000
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer4
je2.accounts[1].exchange_rate = 80
je2.accounts[1].credit_in_account_currency = 200
je1.accounts[1].credit = 16000
je1.accounts[1].debit_in_account_currency = 0
je1.accounts[1].debit = 0
je2.save()
je2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer4
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
# Test exact payment allocation
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Test partial payment allocation (with excess payment entry)
pr.set("allocation", [])
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
pr.allocation[0].difference_account = "Exchange Gain/Loss - _PR"
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
# Check if difference journal entry gets generated for difference amount after reconciliation
pr.reconcile()
total_debit_amount = frappe.db.get_all(
"Journal Entry Account",
{"account": self.debtors_eur, "docstatus": 1, "reference_name": si.name},
"sum(debit) as amount",
group_by="reference_name",
)[0].amount
self.assertEqual(flt(total_debit_amount, 2), -500)
def test_difference_amount_via_payment_entry(self):
# Make Sale Invoice
si = self.create_sales_invoice(
qty=1, rate=100, posting_date=nowdate(), do_not_save=True, do_not_submit=True
)
si.customer = self.customer5
si.currency = "EUR"
si.conversion_rate = 85
si.debit_to = self.debtors_eur
si.save().submit()
# Make payment using Payment Entry
pe1 = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer5,
paid_from=self.debtors_eur,
paid_to=self.bank,
paid_amount=100,
)
pe1.source_exchange_rate = 80
pe1.received_amount = 8000
pe1.save()
pe1.submit()
pe2 = create_payment_entry(
company=self.company,
payment_type="Receive",
party_type="Customer",
party=self.customer5,
paid_from=self.debtors_eur,
paid_to=self.bank,
paid_amount=200,
)
pe2.source_exchange_rate = 80
pe2.received_amount = 16000
pe2.save()
pe2.submit()
pr = self.create_payment_reconciliation()
pr.party = self.customer5
pr.receivable_payable_account = self.debtors_eur
pr.get_unreconciled_entries()
self.assertEqual(len(pr.invoices), 1)
self.assertEqual(len(pr.payments), 2)
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[0].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
pr.set("allocation", [])
pr.get_unreconciled_entries()
invoices = [x.as_dict() for x in pr.invoices]
payments = [pr.payments[1].as_dict()]
pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments}))
self.assertEqual(pr.allocation[0].allocated_amount, 100)
self.assertEqual(pr.allocation[0].difference_amount, -500)
def test_differing_cost_center_on_invoice_and_payment(self):
"""
Cost Center filter should not affect outstanding amount calculation
"""
si = self.create_sales_invoice(qty=1, rate=100, do_not_submit=True)
si.cost_center = self.main_cc.name
si.submit()
pr = get_payment_entry(si.doctype, si.name)
pr.cost_center = self.sub_cc.name
pr = pr.save().submit()
pr = self.create_payment_reconciliation()
pr.cost_center = self.main_cc.name
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 0)
self.assertEqual(len(pr.get("payments")), 0)
def test_cost_center_filter_on_vouchers(self):
"""
Test Cost Center filter is applied on Invoices, Payment Entries and Journals
"""
transaction_date = nowdate()
rate = 100
# 'Main - PR' Cost Center
si1 = self.create_sales_invoice(
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
)
si1.cost_center = self.main_cc.name
si1.submit()
pe1 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
pe1.cost_center = self.main_cc.name
pe1 = pe1.save().submit()
je1 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
je1.accounts[0].cost_center = self.main_cc.name
je1.accounts[1].cost_center = self.main_cc.name
je1.accounts[1].party_type = "Customer"
je1.accounts[1].party = self.customer
je1 = je1.save().submit()
# 'Sub - PR' Cost Center
si2 = self.create_sales_invoice(
qty=1, rate=rate, posting_date=transaction_date, do_not_submit=True
)
si2.cost_center = self.sub_cc.name
si2.submit()
pe2 = self.create_payment_entry(posting_date=transaction_date, amount=rate)
pe2.cost_center = self.sub_cc.name
pe2 = pe2.save().submit()
je2 = self.create_journal_entry(self.bank, self.debit_to, 100, transaction_date)
je2.accounts[0].cost_center = self.sub_cc.name
je2.accounts[1].cost_center = self.sub_cc.name
je2.accounts[1].party_type = "Customer"
je2.accounts[1].party = self.customer
je2 = je2.save().submit()
pr = self.create_payment_reconciliation()
pr.cost_center = self.main_cc.name
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si1.name)
self.assertEqual(len(pr.get("payments")), 2)
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [pe1.name, je1.name])
# Change cost center
pr.cost_center = self.sub_cc.name
pr.get_unreconciled_entries()
# check PR tool output
self.assertEqual(len(pr.get("invoices")), 1)
self.assertEqual(pr.get("invoices")[0].get("invoice_number"), si2.name)
self.assertEqual(len(pr.get("payments")), 2)
payment_vouchers = [x.get("reference_name") for x in pr.get("payments")]
self.assertCountEqual(payment_vouchers, [je2.name, pe2.name])
def make_customer(customer_name, currency=None):
if not frappe.db.exists("Customer", customer_name):
customer = frappe.new_doc("Customer")
customer.customer_name = customer_name
customer.type = "Individual"
if currency:
customer.default_currency = currency
customer.save()
return customer.name
else:
return customer_name

View File

@ -20,7 +20,9 @@
"section_break_5",
"difference_amount",
"column_break_7",
"difference_account"
"difference_account",
"exchange_rate",
"currency"
],
"fields": [
{
@ -37,7 +39,7 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Allocated Amount",
"options": "Currency",
"options": "currency",
"reqd": 1
},
{
@ -112,7 +114,7 @@
"fieldtype": "Currency",
"hidden": 1,
"label": "Unreconciled Amount",
"options": "Currency",
"options": "currency",
"read_only": 1
},
{
@ -120,7 +122,7 @@
"fieldtype": "Currency",
"hidden": 1,
"label": "Amount",
"options": "Currency",
"options": "currency",
"read_only": 1
},
{
@ -129,11 +131,24 @@
"hidden": 1,
"label": "Reference Row",
"read_only": 1
},
{
"fieldname": "currency",
"fieldtype": "Link",
"hidden": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"label": "Exchange Rate",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2021-10-06 11:48:59.616562",
"modified": "2022-12-24 21:01:14.882747",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",
@ -141,5 +156,6 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -11,7 +11,8 @@
"col_break1",
"amount",
"outstanding_amount",
"currency"
"currency",
"exchange_rate"
],
"fields": [
{
@ -62,11 +63,17 @@
"hidden": 1,
"label": "Currency",
"options": "Currency"
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"hidden": 1,
"label": "Exchange Rate"
}
],
"istable": 1,
"links": [],
"modified": "2021-08-24 22:42:40.923179",
"modified": "2022-11-08 18:18:02.502149",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Invoice",
@ -75,5 +82,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -15,7 +15,8 @@
"difference_amount",
"sec_break1",
"remark",
"currency"
"currency",
"exchange_rate"
],
"fields": [
{
@ -91,11 +92,17 @@
"label": "Difference Amount",
"options": "currency",
"read_only": 1
},
{
"fieldname": "exchange_rate",
"fieldtype": "Float",
"hidden": 1,
"label": "Exchange Rate"
}
],
"istable": 1,
"links": [],
"modified": "2021-08-30 10:51:48.140062",
"modified": "2022-11-08 18:18:36.268760",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Payment",
@ -103,5 +110,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@ -42,7 +42,7 @@ frappe.ui.form.on("Payment Request", "refresh", function(frm) {
});
}
if(!frm.doc.payment_gateway_account && frm.doc.status == "Initiated") {
if((!frm.doc.payment_gateway_account || frm.doc.payment_request_type == "Outward") && frm.doc.status == "Initiated") {
frm.add_custom_button(__('Create Payment Entry'), function(){
frappe.call({
method: "erpnext.accounts.doctype.payment_request.payment_request.make_payment_entry",

View File

@ -32,6 +32,10 @@
"iban",
"branch_code",
"swift_number",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"project",
"recipient_and_message",
"print_format",
"email_to",
@ -186,8 +190,10 @@
{
"fetch_from": "bank_account.bank",
"fieldname": "bank",
"fieldtype": "Read Only",
"label": "Bank"
"fieldtype": "Link",
"label": "Bank",
"options": "Bank",
"read_only": 1
},
{
"fetch_from": "bank_account.bank_account_no",
@ -360,16 +366,39 @@
"label": "Payment Channel",
"options": "\nEmail\nPhone",
"read_only": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
"options": "Cost Center"
},
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
{
"fieldname": "project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2020-09-18 12:24:14.178853",
"modified": "2022-12-21 16:56:40.115737",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Request",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@ -401,5 +430,6 @@
}
],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@ -9,8 +9,10 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import flt, get_url, nowdate
from frappe.utils.background_jobs import enqueue
from payments.utils import get_payment_gateway_controller
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
get_accounting_dimensions,
)
from erpnext.accounts.doctype.payment_entry.payment_entry import (
get_company_defaults,
get_payment_entry,
@ -19,6 +21,14 @@ from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_pla
from erpnext.accounts.party import get_party_account, get_party_bank_account
from erpnext.accounts.utils import get_account_currency
from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscription
from erpnext.utilities import payment_app_import_guard
def _get_payment_gateway_controller(*args, **kwargs):
with payment_app_import_guard():
from payments.utils import get_payment_gateway_controller
return get_payment_gateway_controller(*args, **kwargs)
class PaymentRequest(Document):
@ -35,25 +45,24 @@ class PaymentRequest(Document):
frappe.throw(_("To create a Payment Request reference document is required"))
def validate_payment_request_amount(self):
existing_payment_request_amount = get_existing_payment_request_amount(
self.reference_doctype, self.reference_name
existing_payment_request_amount = flt(
get_existing_payment_request_amount(self.reference_doctype, self.reference_name)
)
if existing_payment_request_amount:
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") != "Shopping Cart":
ref_amount = get_amount(ref_doc, self.payment_account)
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if not hasattr(ref_doc, "order_type") or getattr(ref_doc, "order_type") != "Shopping Cart":
ref_amount = get_amount(ref_doc, self.payment_account)
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
frappe.throw(
_("Total Payment Request amount cannot be greater than {0} amount").format(
self.reference_doctype
)
if existing_payment_request_amount + flt(self.grand_total) > ref_amount:
frappe.throw(
_("Total Payment Request amount cannot be greater than {0} amount").format(
self.reference_doctype
)
)
def validate_currency(self):
ref_doc = frappe.get_doc(self.reference_doctype, self.reference_name)
if self.payment_account and ref_doc.currency != frappe.db.get_value(
if self.payment_account and ref_doc.currency != frappe.get_cached_value(
"Account", self.payment_account, "account_currency"
):
frappe.throw(_("Transaction currency must be same as Payment Gateway currency"))
@ -107,7 +116,7 @@ class PaymentRequest(Document):
self.request_phone_payment()
def request_phone_payment(self):
controller = get_payment_gateway_controller(self.payment_gateway)
controller = _get_payment_gateway_controller(self.payment_gateway)
request_amount = self.get_request_amount()
payment_record = dict(
@ -156,7 +165,7 @@ class PaymentRequest(Document):
def payment_gateway_validation(self):
try:
controller = get_payment_gateway_controller(self.payment_gateway)
controller = _get_payment_gateway_controller(self.payment_gateway)
if hasattr(controller, "on_payment_request_submission"):
return controller.on_payment_request_submission(self)
else:
@ -189,7 +198,7 @@ class PaymentRequest(Document):
)
data.update({"company": frappe.defaults.get_defaults().company})
controller = get_payment_gateway_controller(self.payment_gateway)
controller = _get_payment_gateway_controller(self.payment_gateway)
controller.validate_transaction_currency(self.currency)
if hasattr(controller, "validate_minimum_transaction_amount"):
@ -254,6 +263,7 @@ class PaymentRequest(Document):
payment_entry.update(
{
"mode_of_payment": self.mode_of_payment,
"reference_no": self.name,
"reference_date": nowdate(),
"remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
@ -262,6 +272,17 @@ class PaymentRequest(Document):
}
)
# Update dimensions
payment_entry.update(
{
"cost_center": self.get("cost_center"),
"project": self.get("project"),
}
)
for dimension in get_accounting_dimensions():
payment_entry.update({dimension: self.get(dimension)})
if payment_entry.difference_amount:
company_details = get_company_defaults(ref_doc.company)
@ -403,25 +424,22 @@ def make_payment_request(**args):
else ""
)
existing_payment_request = None
if args.order_type == "Shopping Cart":
existing_payment_request = frappe.db.get_value(
"Payment Request",
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)},
)
draft_payment_request = frappe.db.get_value(
"Payment Request",
{"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": 0},
)
if existing_payment_request:
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
if draft_payment_request:
frappe.db.set_value(
"Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False
"Payment Request", draft_payment_request, "grand_total", grand_total, update_modified=False
)
pr = frappe.get_doc("Payment Request", existing_payment_request)
pr = frappe.get_doc("Payment Request", draft_payment_request)
else:
if args.order_type != "Shopping Cart":
existing_payment_request_amount = get_existing_payment_request_amount(args.dt, args.dn)
if existing_payment_request_amount:
grand_total -= existing_payment_request_amount
pr = frappe.new_doc("Payment Request")
pr.update(
{
@ -444,6 +462,17 @@ def make_payment_request(**args):
}
)
# Update dimensions
pr.update(
{
"cost_center": ref_doc.get("cost_center"),
"project": ref_doc.get("project"),
}
)
for dimension in get_accounting_dimensions():
pr.update({dimension: ref_doc.get(dimension)})
if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True
@ -466,7 +495,7 @@ def get_amount(ref_doc, payment_account=None):
"""get amount based on doctype"""
dt = ref_doc.doctype
if dt in ["Sales Order", "Purchase Order"]:
grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
grand_total = flt(ref_doc.rounded_total) - flt(ref_doc.advance_paid)
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency:

View File

@ -39,6 +39,7 @@
{
"columns": 2,
"fetch_from": "payment_term.description",
"fetch_if_empty": 1,
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
@ -159,7 +160,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2021-04-28 05:41:35.084233",
"modified": "2022-09-16 13:57:06.382859",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
@ -168,5 +169,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -43,7 +43,7 @@ class PeriodClosingVoucher(AccountsController):
make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name)
def validate_account_head(self):
closing_account_type = frappe.db.get_value("Account", self.closing_account_head, "root_type")
closing_account_type = frappe.get_cached_value("Account", self.closing_account_head, "root_type")
if closing_account_type not in ["Liability", "Equity"]:
frappe.throw(

View File

@ -25,7 +25,7 @@ frappe.ui.form.on('POS Closing Entry', {
frappe.realtime.on('closing_process_complete', async function(data) {
await frm.reload_doc();
if (frm.doc.status == 'Failed' && frm.doc.error_message && data.user == frappe.session.user) {
if (frm.doc.status == 'Failed' && frm.doc.error_message) {
frappe.msgprint({
title: __('POS Closing Failed'),
message: frm.doc.error_message,
@ -36,6 +36,15 @@ frappe.ui.form.on('POS Closing Entry', {
});
set_html_data(frm);
if (frm.doc.docstatus == 1) {
if (!frm.doc.posting_date) {
frm.set_value("posting_date", frappe.datetime.nowdate());
}
if (!frm.doc.posting_time) {
frm.set_value("posting_time", frappe.datetime.now_time());
}
}
},
refresh: function(frm) {

View File

@ -11,6 +11,7 @@
"period_end_date",
"column_break_3",
"posting_date",
"posting_time",
"pos_opening_entry",
"status",
"section_break_5",
@ -51,7 +52,6 @@
"fieldtype": "Datetime",
"in_list_view": 1,
"label": "Period End Date",
"read_only": 1,
"reqd": 1
},
{
@ -219,6 +219,13 @@
"fieldtype": "Small Text",
"label": "Error",
"read_only": 1
},
{
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time",
"no_copy": 1,
"reqd": 1
}
],
"is_submittable": 1,
@ -228,10 +235,11 @@
"link_fieldname": "pos_closing_entry"
}
],
"modified": "2021-10-20 16:19:25.340565",
"modified": "2022-08-01 11:37:14.991228",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry",
"naming_rule": "Expression (old style)",
"owner": "Administrator",
"permissions": [
{
@ -278,5 +286,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -15,11 +15,30 @@ from erpnext.controllers.status_updater import StatusUpdater
class POSClosingEntry(StatusUpdater):
def validate(self):
self.posting_date = self.posting_date or frappe.utils.nowdate()
self.posting_time = self.posting_time or frappe.utils.nowtime()
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
self.validate_duplicate_pos_invoices()
self.validate_pos_invoices()
def validate_duplicate_pos_invoices(self):
pos_occurences = {}
for idx, inv in enumerate(self.pos_transactions, 1):
pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
error_list = []
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
)
if error_list:
frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
def validate_pos_invoices(self):
invalid_rows = []
for d in self.pos_transactions:

View File

@ -5,6 +5,8 @@
frappe.provide("erpnext.accounts");
erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnext.selling.SellingController {
settings = {};
setup(doc) {
this.setup_posting_date_time_check();
super.setup(doc);
@ -12,21 +14,37 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
company() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
this.frm.set_value("set_warehouse", "");
this.frm.set_value("taxes_and_charges", "");
}
onload(doc) {
super.onload();
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log', 'POS Closing Entry'];
if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
this.frm.script_manager.trigger("is_pos");
this.frm.refresh_fields();
}
this.frm.set_query("set_warehouse", function(doc) {
return {
filters: {
company: doc.company ? doc.company : '',
}
}
});
erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
}
onload_post_render(frm) {
this.pos_profile(frm);
}
refresh(doc) {
super.refresh();
if (doc.docstatus == 1 && !doc.is_return) {
this.frm.add_custom_button(__('Return'), this.make_sales_return, __('Create'));
this.frm.page.set_inner_btn_group_as_primary(__('Create'));
@ -36,6 +54,18 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
this.frm.return_print_format = "Sales Invoice Return";
this.frm.set_value('consolidated_invoice', '');
}
this.frm.set_query("customer", (function () {
const customer_groups = this.settings?.customer_groups;
if (!customer_groups?.length) return {};
return {
filters: {
customer_group: ["in", customer_groups],
}
}
}).bind(this));
}
is_pos() {
@ -88,6 +118,25 @@ erpnext.selling.POSInvoiceController = class POSInvoiceController extends erpnex
});
}
pos_profile(frm) {
if (!frm.pos_profile || frm.pos_profile == '') {
this.update_customer_groups_settings([]);
return;
}
frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_pos_profile_data",
args: { "pos_profile": frm.pos_profile },
callback: ({ message: profile }) => {
this.update_customer_groups_settings(profile?.customer_groups);
},
});
}
update_customer_groups_settings(customer_groups) {
this.settings.customer_groups = customer_groups?.map((group) => group.name)
}
amount(){
this.write_off_outstanding_amount_automatically()
}

View File

@ -343,7 +343,8 @@
"no_copy": 1,
"options": "POS Invoice",
"print_hide": 1,
"read_only": 1
"read_only": 1,
"search_index": 1
},
{
"default": "0",
@ -1553,7 +1554,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
"modified": "2022-03-22 13:00:24.166684",
"modified": "2022-09-30 03:49:50.455199",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",

View File

@ -239,14 +239,14 @@ class POSInvoice(SalesInvoice):
frappe.bold(d.warehouse),
frappe.bold(d.qty),
)
if flt(available_stock) <= 0:
if is_stock_item and flt(available_stock) <= 0:
frappe.throw(
_("Row #{}: Item Code: {} is not available under warehouse {}.").format(
d.idx, item_code, warehouse
),
title=_("Item Unavailable"),
)
elif flt(available_stock) < flt(d.qty):
elif is_stock_item and flt(available_stock) < flt(d.qty):
frappe.throw(
_(
"Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}."
@ -335,7 +335,8 @@ class POSInvoice(SalesInvoice):
if (
self.change_amount
and self.account_for_change_amount
and frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company
and frappe.get_cached_value("Account", self.account_for_change_amount, "company")
!= self.company
):
frappe.throw(
_("The selected change account {} doesn't belongs to Company {}.").format(
@ -486,7 +487,7 @@ class POSInvoice(SalesInvoice):
customer_price_list, customer_group, customer_currency = frappe.db.get_value(
"Customer", self.customer, ["default_price_list", "customer_group", "default_currency"]
)
customer_group_price_list = frappe.db.get_value(
customer_group_price_list = frappe.get_cached_value(
"Customer Group", customer_group, "default_price_list"
)
selling_price_list = (
@ -532,8 +533,8 @@ class POSInvoice(SalesInvoice):
if not self.debit_to:
self.debit_to = get_party_account("Customer", self.customer, self.company)
self.party_account_currency = frappe.db.get_value(
"Account", self.debit_to, "account_currency", cache=True
self.party_account_currency = frappe.get_cached_value(
"Account", self.debit_to, "account_currency"
)
if not self.due_date and self.customer:
self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company)
@ -632,11 +633,12 @@ def get_stock_availability(item_code, warehouse):
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
return bin_qty - pos_sales_qty, is_stock_item
else:
is_stock_item = False
is_stock_item = True
if frappe.db.exists("Product Bundle", item_code):
return get_bundle_availability(item_code, warehouse), is_stock_item
else:
# Is a service item
is_stock_item = False
# Is a service item or non_stock item
return 0, is_stock_item
@ -650,7 +652,9 @@ def get_bundle_availability(bundle_item_code, warehouse):
available_qty = item_bin_qty - item_pos_reserved_qty
max_available_bundles = available_qty / item.qty
if bundle_bin_qty > max_available_bundles:
if bundle_bin_qty > max_available_bundles and frappe.get_value(
"Item", item.item_code, "is_stock_item"
):
bundle_bin_qty = max_available_bundles
pos_sales_qty = get_pos_reserved_qty(bundle_item_code, warehouse)
@ -671,7 +675,7 @@ def get_bin_qty(item_code, warehouse):
def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = frappe.db.sql(
"""select sum(p_item.qty) as qty
"""select sum(p_item.stock_qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent
and ifnull(p.consolidated_invoice, '') = ''

View File

@ -495,6 +495,67 @@ class TestPOSInvoice(unittest.TestCase):
self.assertRaises(frappe.ValidationError, pos.submit)
def test_value_error_on_serial_no_validation(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
se = make_serialized_item(
company="_Test Company",
target_warehouse="Stores - _TC",
cost_center="Main - _TC",
expense_account="Cost of Goods Sold - _TC",
)
serial_nos = se.get("items")[0].serial_no
# make a pos invoice
pos = create_pos_invoice(
company="_Test Company",
debit_to="Debtors - _TC",
account_for_change_amount="Cash - _TC",
warehouse="Stores - _TC",
income_account="Sales - _TC",
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
qty=1,
do_not_save=1,
)
pos.get("items")[0].has_serial_no = 1
pos.get("items")[0].serial_no = serial_nos.split("\n")[0]
pos.set("payments", [])
pos.append(
"payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1}
)
pos = pos.save().submit()
# make a return
pos_return = make_sales_return(pos.name)
pos_return.paid_amount = pos_return.grand_total
pos_return.save()
pos_return.submit()
# set docstatus to 2 for pos to trigger this issue
frappe.db.set_value("POS Invoice", pos.name, "docstatus", 2)
pos2 = create_pos_invoice(
company="_Test Company",
debit_to="Debtors - _TC",
account_for_change_amount="Cash - _TC",
warehouse="Stores - _TC",
income_account="Sales - _TC",
expense_account="Cost of Goods Sold - _TC",
cost_center="Main - _TC",
item=se.get("items")[0].item_code,
rate=1000,
qty=1,
do_not_save=1,
)
pos2.get("items")[0].has_serial_no = 1
pos2.get("items")[0].serial_no = serial_nos.split("\n")[0]
# Value error should not be triggered on validation
pos2.save()
def test_loyalty_points(self):
from erpnext.accounts.doctype.loyalty_program.loyalty_program import (
get_loyalty_program_details_with_points,

View File

@ -8,6 +8,7 @@
"engine": "InnoDB",
"field_order": [
"barcode",
"has_item_scanned",
"item_code",
"col_break1",
"item_name",
@ -808,11 +809,19 @@
"fieldtype": "Check",
"label": "Grant Commission",
"read_only": 1
},
{
"default": "0",
"depends_on": "barcode",
"fieldname": "has_item_scanned",
"fieldtype": "Check",
"label": "Has Item Scanned",
"read_only": 1
}
],
"istable": 1,
"links": [],
"modified": "2021-10-05 12:23:47.506290",
"modified": "2022-11-02 12:52:39.125295",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
@ -820,5 +829,6 @@
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
"sort_order": "DESC",
"states": []
}

View File

@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"posting_date",
"posting_time",
"merge_invoices_based_on",
"column_break_3",
"pos_closing_entry",
@ -105,12 +106,19 @@
"label": "Customer Group",
"mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
"options": "Customer Group"
},
{
"fieldname": "posting_time",
"fieldtype": "Time",
"label": "Posting Time",
"no_copy": 1,
"reqd": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-09-14 11:17:19.001142",
"modified": "2022-08-01 11:36:42.456429",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Merge Log",
@ -173,5 +181,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

@ -6,11 +6,10 @@ import json
import frappe
from frappe import _
from frappe.core.page.background_jobs.background_jobs import get_info
from frappe.model.document import Document
from frappe.model.mapper import map_child_doc, map_doc
from frappe.utils import cint, flt, getdate, nowdate
from frappe.utils.background_jobs import enqueue
from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime
from frappe.utils.background_jobs import enqueue, is_job_queued
from frappe.utils.scheduler import is_scheduler_inactive
@ -18,6 +17,22 @@ class POSInvoiceMergeLog(Document):
def validate(self):
self.validate_customer()
self.validate_pos_invoice_status()
self.validate_duplicate_pos_invoices()
def validate_duplicate_pos_invoices(self):
pos_occurences = {}
for idx, inv in enumerate(self.pos_invoices, 1):
pos_occurences.setdefault(inv.pos_invoice, []).append(idx)
error_list = []
for key, value in pos_occurences.items():
if len(value) > 1:
error_list.append(
_("{} is added multiple times on rows: {}".format(frappe.bold(key), frappe.bold(value)))
)
if error_list:
frappe.throw(error_list, title=_("Duplicate POS Invoices found"), as_list=True)
def validate_customer(self):
if self.merge_invoices_based_on == "Customer Group":
@ -99,6 +114,7 @@ class POSInvoiceMergeLog(Document):
sales_invoice.is_consolidated = 1
sales_invoice.set_posting_time = 1
sales_invoice.posting_date = getdate(self.posting_date)
sales_invoice.posting_time = get_time(self.posting_time)
sales_invoice.save()
sales_invoice.submit()
@ -115,6 +131,7 @@ class POSInvoiceMergeLog(Document):
credit_note.is_consolidated = 1
credit_note.set_posting_time = 1
credit_note.posting_date = getdate(self.posting_date)
credit_note.posting_time = get_time(self.posting_time)
# TODO: return could be against multiple sales invoice which could also have been consolidated?
# credit_note.return_against = self.consolidated_invoice
credit_note.save()
@ -402,6 +419,9 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
merge_log.posting_date = (
getdate(closing_entry.get("posting_date")) if closing_entry else nowdate()
)
merge_log.posting_time = (
get_time(closing_entry.get("posting_time")) if closing_entry else nowtime()
)
merge_log.customer = customer
merge_log.pos_closing_entry = closing_entry.get("name") if closing_entry else None
@ -421,12 +441,14 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
if closing_entry:
closing_entry.set_status(update=True, status="Failed")
if type(error_message) == list:
error_message = frappe.json.dumps(error_message)
closing_entry.db_set("error_message", error_message)
raise
finally:
frappe.db.commit()
frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user})
frappe.publish_realtime("closing_process_complete", user=frappe.session.user)
def cancel_merge_logs(merge_logs, closing_entry=None):
@ -453,7 +475,7 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
finally:
frappe.db.commit()
frappe.publish_realtime("closing_process_complete", {"user": frappe.session.user})
frappe.publish_realtime("closing_process_complete", user=frappe.session.user)
def enqueue_job(job, **kwargs):
@ -462,7 +484,7 @@ def enqueue_job(job, **kwargs):
closing_entry = kwargs.get("closing_entry") or {}
job_name = closing_entry.get("name")
if not job_already_enqueued(job_name):
if not is_job_queued(job_name):
enqueue(
job,
**kwargs,
@ -486,12 +508,6 @@ def check_scheduler_status():
frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
def job_already_enqueued(job_name):
enqueued_jobs = [d.get("job_name") for d in get_info()]
if job_name in enqueued_jobs:
return True
def safe_load_json(message):
try:
json_message = json.loads(message).get("message")

View File

@ -43,6 +43,7 @@
"currency",
"write_off_account",
"write_off_cost_center",
"write_off_limit",
"account_for_change_amount",
"disable_rounded_total",
"column_break_23",
@ -360,6 +361,14 @@
"fieldtype": "Check",
"label": "Validate Stock on Save"
},
{
"default": "1",
"description": "Auto write off precision loss while consolidation",
"fieldname": "write_off_limit",
"fieldtype": "Currency",
"label": "Write Off Limit",
"reqd": 1
},
{
"default": "0",
"description": "If enabled, the consolidated invoices will have rounded total disabled",
@ -393,7 +402,7 @@
"link_fieldname": "pos_profile"
}
],
"modified": "2022-07-21 11:16:46.911173",
"modified": "2022-08-10 12:57:06.241439",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",

View File

@ -52,7 +52,10 @@
"free_item_rate",
"column_break_42",
"free_item_uom",
"round_free_qty",
"is_recursive",
"recurse_for",
"apply_recursion_over",
"section_break_23",
"valid_from",
"valid_upto",
@ -176,7 +179,7 @@
},
{
"collapsible": 1,
"depends_on": "eval:doc.apply_on != 'Transaction'",
"depends_on": "eval:doc.apply_on != 'Transaction' && !doc.mixed_conditions",
"fieldname": "section_break_18",
"fieldtype": "Section Break",
"label": "Discount on Other Item"
@ -297,12 +300,12 @@
{
"fieldname": "min_qty",
"fieldtype": "Float",
"label": "Min Qty"
"label": "Min Qty (As Per Stock UOM)"
},
{
"fieldname": "max_qty",
"fieldtype": "Float",
"label": "Max Qty"
"label": "Max Qty (As Per Stock UOM)"
},
{
"fieldname": "column_break_21",
@ -469,7 +472,7 @@
"description": "If rate is zero them item will be treated as \"Free Item\"",
"fieldname": "free_item_rate",
"fieldtype": "Currency",
"label": "Rate"
"label": "Free Item Rate"
},
{
"collapsible": 1,
@ -481,7 +484,7 @@
"description": "System will notify to increase or decrease quantity or amount ",
"fieldname": "threshold_percentage",
"fieldtype": "Percent",
"label": "Threshold for Suggestion"
"label": "Threshold for Suggestion (In Percentage)"
},
{
"description": "Higher the number, higher the priority",
@ -578,15 +581,38 @@
"fieldtype": "Select",
"label": "Naming Series",
"options": "PRLE-.####"
},
{
"default": "0",
"fieldname": "round_free_qty",
"fieldtype": "Check",
"label": "Round Free Qty"
},
{
"depends_on": "is_recursive",
"description": "Give free item for every N quantity",
"fieldname": "recurse_for",
"fieldtype": "Float",
"label": "Recurse Every (As Per Transaction UOM)",
"mandatory_depends_on": "is_recursive"
},
{
"default": "0",
"depends_on": "is_recursive",
"description": "Qty for which recursion isn't applicable.",
"fieldname": "apply_recursion_over",
"fieldtype": "Float",
"label": "Apply Recursion Over (As Per Transaction UOM)"
}
],
"icon": "fa fa-gift",
"idx": 1,
"links": [],
"modified": "2021-08-06 15:10:04.219321",
"modified": "2023-02-14 04:53:34.887358",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
"naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@ -642,5 +668,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"title_field": "title"
}
}

View File

@ -10,7 +10,7 @@ import re
import frappe
from frappe import _, throw
from frappe.model.document import Document
from frappe.utils import cint, flt, getdate
from frappe.utils import cint, flt
apply_on_dict = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}
@ -24,6 +24,7 @@ class PricingRule(Document):
self.validate_applicable_for_selling_or_buying()
self.validate_min_max_amt()
self.validate_min_max_qty()
self.validate_recursion()
self.cleanup_fields_value()
self.validate_rate_or_discount()
self.validate_max_discount()
@ -109,6 +110,18 @@ class PricingRule(Document):
if self.min_amt and self.max_amt and flt(self.min_amt) > flt(self.max_amt):
throw(_("Min Amt can not be greater than Max Amt"))
def validate_recursion(self):
if self.price_or_product_discount != "Product":
return
if self.free_item or self.same_item:
if flt(self.recurse_for) <= 0:
self.recurse_for = 1
if self.is_recursive:
if flt(self.apply_recursion_over) > flt(self.min_qty):
throw(_("Min Qty should be greater than Recurse Over Qty"))
if flt(self.apply_recursion_over) < 0:
throw(_("Recurse Over Qty cannot be less than 0"))
def cleanup_fields_value(self):
for logic_field in ["apply_on", "applicable_for", "rate_or_discount"]:
fieldname = frappe.scrub(self.get(logic_field) or "")
@ -171,8 +184,7 @@ class PricingRule(Document):
if self.is_cumulative and not (self.valid_from and self.valid_upto):
frappe.throw(_("Valid from and valid upto fields are mandatory for the cumulative"))
if self.valid_from and self.valid_upto and getdate(self.valid_from) > getdate(self.valid_upto):
frappe.throw(_("Valid from date must be less than valid upto date"))
self.validate_from_to_dates("valid_from", "valid_upto")
def validate_condition(self):
if (
@ -243,7 +255,7 @@ def apply_pricing_rule(args, doc=None):
for item in item_list:
args_copy = copy.deepcopy(args)
args_copy.update(item)
data = get_pricing_rule_for_item(args_copy, item.get("price_list_rate"), doc=doc)
data = get_pricing_rule_for_item(args_copy, doc=doc)
out.append(data)
if (
@ -268,7 +280,19 @@ def get_serial_no_for_item(args):
return item_details
def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False):
def update_pricing_rule_uom(pricing_rule, args):
child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get(
pricing_rule.apply_on
)
apply_on_field = frappe.scrub(pricing_rule.apply_on)
for row in pricing_rule.get(child_doc):
if row.get(apply_on_field) == args.get(apply_on_field):
pricing_rule.uom = row.uom
def get_pricing_rule_for_item(args, doc=None, for_validate=False):
from erpnext.accounts.doctype.pricing_rule.utils import (
get_applied_pricing_rules,
get_pricing_rule_items,
@ -324,7 +348,8 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if isinstance(pricing_rule, str):
pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule)
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule)
update_pricing_rule_uom(pricing_rule, args)
pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or []
if pricing_rule.get("suggestion"):
continue
@ -337,7 +362,6 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other:
item_details.update(
{
"apply_rule_on_other_items": json.dumps(pricing_rule.apply_rule_on_other_items),
"price_or_product_discount": pricing_rule.price_or_product_discount,
"apply_rule_on": (
frappe.scrub(pricing_rule.apply_rule_on_other)
@ -347,6 +371,9 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa
}
)
if pricing_rule.apply_rule_on_other_items:
item_details["apply_rule_on_other_items"] = json.dumps(pricing_rule.apply_rule_on_other_items)
if pricing_rule.coupon_code_based == 1 and args.coupon_code == None:
return item_details
@ -438,12 +465,15 @@ def apply_price_discount_rule(pricing_rule, item_details, args):
if pricing_rule.currency == args.currency:
pricing_rule_rate = pricing_rule.rate
# TODO https://github.com/frappe/erpnext/pull/23636 solve this in some other way.
if pricing_rule_rate:
is_blank_uom = pricing_rule.get("uom") != args.get("uom")
# Override already set price list rate (from item price)
# if pricing_rule_rate > 0
item_details.update(
{
"price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1),
"price_list_rate": pricing_rule_rate
* (args.get("conversion_factor", 1) if is_blank_uom else 1),
}
)
item_details.update({"discount_percentage": 0.0})
@ -492,7 +522,7 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None, ra
)
if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"):
items = get_pricing_rule_items(pricing_rule)
items = get_pricing_rule_items(pricing_rule, other_items=True)
item_details.apply_on = (
frappe.scrub(pricing_rule.apply_rule_on_other)
if pricing_rule.apply_rule_on_other

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