diff --git a/.flake8 b/.flake8
index 56c9b9a3699..4b852abd7c6 100644
--- a/.flake8
+++ b/.flake8
@@ -28,6 +28,10 @@ ignore =
B007,
B950,
W191,
+ E124, # closing bracket, irritating while writing QB code
+ E131, # continuation line unaligned for hanging indent
+ E123, # closing bracket does not match indentation of opening bracket's line
+ E101, # ensured by use of black
max-line-length = 200
exclude=.github/helper/semgrep_rules
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index b5e46fb8107..9b5ea2f58ab 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -17,3 +17,6 @@ f0bcb753fb7ebbb64bb0d6906d431d002f0f7d8f
# imports cleanup
4b2be2999f2203493b49bf74c5b440d49e38b5e3
+
+# formatting with black
+c07713b860505211db2af685e2e950bf5dd7dd3a
diff --git a/.github/helper/.flake8_strict b/.github/helper/.flake8_strict
index a79137d7c32..198ec7bfe54 100644
--- a/.github/helper/.flake8_strict
+++ b/.github/helper/.flake8_strict
@@ -66,6 +66,7 @@ ignore =
F841,
E713,
E712,
+ B023
max-line-length = 200
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index e7f46410e6c..a63c5b841ca 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -2,9 +2,16 @@
set -e
+# Check for merge conflicts before proceeding
+python -m compileall -f "${GITHUB_WORKSPACE}"
+if grep -lr --exclude-dir=node_modules "^<<<<<<< " "${GITHUB_WORKSPACE}"
+ then echo "Found merge conflicts"
+ exit 1
+fi
+
cd ~ || exit
-sudo apt-get install redis-server libcups2-dev
+sudo apt update && sudo apt install redis-server libcups2-dev
pip install frappe-bench
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index 8bb44555206..30ca22aedc5 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -8,6 +8,10 @@ on:
workflow_dispatch:
+concurrency:
+ group: patch-mariadb-v13-${{ github.event.number }}
+ cancel-in-progress: true
+
jobs:
test:
runs-on: ubuntu-18.04
@@ -31,7 +35,7 @@ jobs:
- name: Setup Python
uses: actions/setup-python@v2
with:
- python-version: 3.6
+ python-version: 3.7
- name: Setup Node
uses: actions/setup-node@v2
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 00000000000..5a46002820c
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,31 @@
+name: Generate Semantic Release
+on:
+ push:
+ branches:
+ - version-13
+jobs:
+ release:
+ name: Release
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout Entire Repository
+ uses: actions/checkout@v2
+ with:
+ fetch-depth: 0
+ persist-credentials: false
+ - name: Setup Node.js v14
+ uses: actions/setup-node@v2
+ with:
+ node-version: 14
+ - name: Setup dependencies
+ run: |
+ npm install @semantic-release/git @semantic-release/exec --no-save
+ - name: Create Release
+ env:
+ GH_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.RELEASE_TOKEN }}
+ GIT_AUTHOR_NAME: "Frappe PR Bot"
+ GIT_AUTHOR_EMAIL: "developers@frappe.io"
+ GIT_COMMITTER_NAME: "Frappe PR Bot"
+ GIT_COMMITTER_EMAIL: "developers@frappe.io"
+ run: npx semantic-release
\ No newline at end of file
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml
index 6d7324d623b..5ec90df78ca 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests.yml
@@ -12,6 +12,10 @@ on:
- '**.js'
- '**.md'
+concurrency:
+ group: server-mariadb-v13-${{ github.event.number }}
+ cancel-in-progress: true
+
jobs:
test:
runs-on: ubuntu-18.04
@@ -21,7 +25,7 @@ jobs:
fail-fast: false
matrix:
- container: [1, 2, 3]
+ container: [1, 2]
name: Python Unit Tests
@@ -89,39 +93,8 @@ jobs:
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- 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 --use-orchestrator
env:
TYPE: server
CI_BUILD_ID: ${{ github.run_id }}
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
-
- - name: Upload Coverage Data
- run: |
- cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
- cd ${GITHUB_WORKSPACE}
- pip3 install coverage==5.5
- pip3 install coveralls==3.0.1
- coveralls
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- COVERALLS_FLAG_NAME: run-${{ matrix.container }}
- COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
- COVERALLS_PARALLEL: true
-
- coveralls:
- name: Coverage Wrap Up
- needs: test
- container: python:3-slim
- runs-on: ubuntu-18.04
- steps:
- - name: Clone
- uses: actions/checkout@v2
-
- - name: Coveralls Finished
- run: |
- cd ${GITHUB_WORKSPACE}
- pip3 install coverage==5.5
- pip3 install coveralls==3.0.1
- coveralls --finish
- env:
- GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
diff --git a/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
deleted file mode 100644
index 5459e86123d..00000000000
--- a/.github/workflows/ui-tests.yml
+++ /dev/null
@@ -1,113 +0,0 @@
-name: UI
-
-on:
- pull_request:
- paths-ignore:
- - '**.md'
- workflow_dispatch:
-
-jobs:
- test:
- runs-on: ubuntu-18.04
- timeout-minutes: 60
-
- strategy:
- fail-fast: false
-
- name: UI Tests (Cypress)
-
- services:
- mysql:
- image: mariadb:10.3
- env:
- MYSQL_ALLOW_EMPTY_PASSWORD: YES
- ports:
- - 3306:3306
- options: --health-cmd="mysqladmin ping" --health-interval=5s --health-timeout=2s --health-retries=3
-
- steps:
- - name: Clone
- uses: actions/checkout@v2
-
- - name: Setup Python
- uses: actions/setup-python@v2
- with:
- python-version: 3.7
-
- - uses: actions/setup-node@v2
- with:
- node-version: 14
- check-latest: true
-
- - name: Add to Hosts
- run: |
- echo "127.0.0.1 test_site" | sudo tee -a /etc/hosts
-
- - name: Cache pip
- uses: actions/cache@v2
- with:
- path: ~/.cache/pip
- key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }}
- restore-keys: |
- ${{ runner.os }}-pip-
- ${{ runner.os }}-
-
- - name: Cache node modules
- uses: actions/cache@v2
- env:
- cache-name: cache-node-modules
- with:
- path: ~/.npm
- key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}
- restore-keys: |
- ${{ runner.os }}-build-${{ env.cache-name }}-
- ${{ runner.os }}-build-
- ${{ runner.os }}-
-
- - name: Get yarn cache directory path
- id: yarn-cache-dir-path
- run: echo "::set-output name=dir::$(yarn cache dir)"
-
- - uses: actions/cache@v2
- id: yarn-cache
- with:
- path: ${{ steps.yarn-cache-dir-path.outputs.dir }}
- key: ${{ runner.os }}-yarn-${{ hashFiles('**/yarn.lock') }}
- restore-keys: |
- ${{ runner.os }}-yarn-
-
- - name: Cache cypress binary
- uses: actions/cache@v2
- with:
- path: ~/.cache
- key: ${{ runner.os }}-cypress-
- restore-keys: |
- ${{ runner.os }}-cypress-
- ${{ runner.os }}-
-
- - name: Install
- run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- env:
- DB: mariadb
- TYPE: ui
-
- - name: Site Setup
- run: cd ~/frappe-bench/ && bench --site test_site execute erpnext.setup.utils.before_tests
-
- - name: cypress pre-requisites
- run: cd ~/frappe-bench/apps/frappe && yarn add cypress-file-upload@^5 @testing-library/cypress@^8 --no-lockfile
-
-
- - name: Build Assets
- run: cd ~/frappe-bench/ && bench build
- env:
- CI: Yes
-
- - name: UI Tests
- run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
- env:
- CYPRESS_RECORD_KEY: 60a8e3bf-08f5-45b1-9269-2b207d7d30cd
-
- - name: Show bench console if tests failed
- if: ${{ failure() }}
- run: cat ~/frappe-bench/bench_run_logs.txt
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index b74d9a640da..dc3011f050f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -26,12 +26,19 @@ repos:
args: ['--config', '.github/helper/.flake8_strict']
exclude: ".*setup.py$"
+ - repo: https://github.com/adityahase/black
+ rev: 9cb0a69f4d0030cdf687eddf314468b39ed54119
+ hooks:
+ - id: black
+ additional_dependencies: ['click==8.0.4']
+
- repo: https://github.com/timothycrosley/isort
rev: 5.9.1
hooks:
- id: isort
exclude: ".*setup.py$"
+
ci:
autoupdate_schedule: weekly
skip: []
diff --git a/.releaserc b/.releaserc
new file mode 100644
index 00000000000..1f5bb53d248
--- /dev/null
+++ b/.releaserc
@@ -0,0 +1,24 @@
+{
+ "branches": ["version-13"],
+ "plugins": [
+ "@semantic-release/commit-analyzer", {
+ "preset": "angular",
+ "releaseRules": [
+ {"breaking": true, "release": false}
+ ]
+ },
+ "@semantic-release/release-notes-generator",
+ [
+ "@semantic-release/exec", {
+ "prepareCmd": 'sed -ir -E "s/\"[0-9]+\.[0-9]+\.[0-9]+\"/\"${nextRelease.version}\"/" erpnext/__init__.py'
+ }
+ ],
+ [
+ "@semantic-release/git", {
+ "assets": ["erpnext/__init__.py"],
+ "message": "chore(release): Bumped to Version ${nextRelease.version}\n\n${nextRelease.notes}"
+ }
+ ],
+ "@semantic-release/github"
+ ]
+}
\ No newline at end of file
diff --git a/CODEOWNERS b/CODEOWNERS
index a4a14de1b8e..b52062d2371 100644
--- a/CODEOWNERS
+++ b/CODEOWNERS
@@ -3,33 +3,35 @@
# These owners will be the default owners for everything in
# the repo. Unless a later match takes precedence,
-erpnext/accounts/ @nextchamp-saqib @deepeshgarg007
-erpnext/assets/ @nextchamp-saqib @deepeshgarg007
+erpnext/accounts/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
+erpnext/assets/ @nextchamp-saqib @deepeshgarg007 @ruthra-kumar
erpnext/erpnext_integrations/ @nextchamp-saqib
erpnext/loan_management/ @nextchamp-saqib @deepeshgarg007
-erpnext/regional @nextchamp-saqib @deepeshgarg007
-erpnext/selling @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 @ankush
+erpnext/buying/ @marination @rohitwaghchaure @s-aga-r
erpnext/e_commerce/ @marination
-erpnext/maintenance/ @marination @rohitwaghchaure
-erpnext/manufacturing/ @marination @rohitwaghchaure @ankush
+erpnext/maintenance/ @marination @rohitwaghchaure @s-aga-r
+erpnext/manufacturing/ @marination @rohitwaghchaure @s-aga-r
erpnext/portal/ @marination
-erpnext/quality_management/ @marination @rohitwaghchaure
+erpnext/quality_management/ @marination @rohitwaghchaure @s-aga-r
erpnext/shopping_cart/ @marination
-erpnext/stock/ @marination @rohitwaghchaure @ankush
+erpnext/stock/ @marination @rohitwaghchaure @s-aga-r
-erpnext/crm/ @ruchamahabal @pateljannat
-erpnext/education/ @ruchamahabal @pateljannat
-erpnext/healthcare/ @ruchamahabal @pateljannat @chillaranand
-erpnext/hr/ @ruchamahabal @pateljannat
+erpnext/crm/ @NagariaHussain
+erpnext/education/ @rutwikhdev
+erpnext/healthcare/ @chillaranand
+erpnext/hr/ @ruchamahabal
erpnext/non_profit/ @ruchamahabal
-erpnext/payroll @ruchamahabal @pateljannat
-erpnext/projects/ @ruchamahabal @pateljannat
+erpnext/payroll @ruchamahabal
+erpnext/projects/ @ruchamahabal
erpnext/controllers @deepeshgarg007 @nextchamp-saqib @rohitwaghchaure @marination
+erpnext/patches/ @deepeshgarg007 @nextchamp-saqib @marination rohitwaghchaure
+erpnext/public/ @nextchamp-saqib @marination
-.github/ @surajshetty3416 @ankush
-requirements.txt @gavindsouza
+.github/ @ankush
+requirements.txt @gavindsouza @ankush
diff --git a/README.md b/README.md
index 0a556f57b41..6523c9f6eda 100644
--- a/README.md
+++ b/README.md
@@ -65,6 +65,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).
+
---
## Contributing
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index bc998eba993..2c928b288de 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -1,53 +1,60 @@
-
import inspect
import frappe
from erpnext.hooks import regional_overrides
-__version__ = '13.17.0'
+__version__ = "13.36.1"
+
def get_default_company(user=None):
- '''Get default company for user'''
+ """Get default company for user"""
from frappe.defaults import get_user_default_as_list
if not user:
user = frappe.session.user
- companies = get_user_default_as_list(user, 'company')
+ companies = get_user_default_as_list(user, "company")
if companies:
default_company = companies[0]
else:
- default_company = frappe.db.get_single_value('Global Defaults', 'default_company')
+ default_company = frappe.db.get_single_value("Global Defaults", "default_company")
return default_company
def get_default_currency():
- '''Returns the currency of the default company'''
+ """Returns the currency of the default company"""
company = get_default_company()
if company:
- return frappe.get_cached_value('Company', company, 'default_currency')
+ return frappe.get_cached_value("Company", company, "default_currency")
+
def get_default_cost_center(company):
- '''Returns the default cost center of the company'''
+ """Returns the default cost center of the company"""
if not company:
return None
if not frappe.flags.company_cost_center:
frappe.flags.company_cost_center = {}
if not company in frappe.flags.company_cost_center:
- frappe.flags.company_cost_center[company] = frappe.get_cached_value('Company', company, 'cost_center')
+ frappe.flags.company_cost_center[company] = frappe.get_cached_value(
+ "Company", company, "cost_center"
+ )
return frappe.flags.company_cost_center[company]
+
def get_company_currency(company):
- '''Returns the default company currency'''
+ """Returns the default company currency"""
if not frappe.flags.company_currency:
frappe.flags.company_currency = {}
if not company in frappe.flags.company_currency:
- frappe.flags.company_currency[company] = frappe.db.get_value('Company', company, 'default_currency', cache=True)
+ frappe.flags.company_currency[company] = frappe.db.get_value(
+ "Company", company, "default_currency", cache=True
+ )
return frappe.flags.company_currency[company]
+
def set_perpetual_inventory(enable=1, company=None):
if not company:
company = "_Test Company" if frappe.flags.in_test else get_default_company()
@@ -56,9 +63,10 @@ def set_perpetual_inventory(enable=1, company=None):
company.enable_perpetual_inventory = enable
company.save()
-def encode_company_abbr(name, company):
- '''Returns name encoded with company abbreviation'''
- company_abbr = frappe.get_cached_value('Company', company, "abbr")
+
+def encode_company_abbr(name, company=None, abbr=None):
+ """Returns name encoded with company abbreviation"""
+ company_abbr = abbr or frappe.get_cached_value("Company", company, "abbr")
parts = name.rsplit(" - ", 1)
if parts[-1].lower() != company_abbr.lower():
@@ -66,65 +74,73 @@ def encode_company_abbr(name, company):
return " - ".join(parts)
+
def is_perpetual_inventory_enabled(company):
if not company:
company = "_Test Company" if frappe.flags.in_test else get_default_company()
- if not hasattr(frappe.local, 'enable_perpetual_inventory'):
+ if not hasattr(frappe.local, "enable_perpetual_inventory"):
frappe.local.enable_perpetual_inventory = {}
if not company in frappe.local.enable_perpetual_inventory:
- frappe.local.enable_perpetual_inventory[company] = frappe.get_cached_value('Company',
- company, "enable_perpetual_inventory") or 0
+ frappe.local.enable_perpetual_inventory[company] = (
+ frappe.get_cached_value("Company", company, "enable_perpetual_inventory") or 0
+ )
return frappe.local.enable_perpetual_inventory[company]
+
def get_default_finance_book(company=None):
if not company:
company = get_default_company()
- if not hasattr(frappe.local, 'default_finance_book'):
+ if not hasattr(frappe.local, "default_finance_book"):
frappe.local.default_finance_book = {}
if not company in frappe.local.default_finance_book:
- frappe.local.default_finance_book[company] = frappe.get_cached_value('Company',
- company, "default_finance_book")
+ frappe.local.default_finance_book[company] = frappe.get_cached_value(
+ "Company", company, "default_finance_book"
+ )
return frappe.local.default_finance_book[company]
+
def get_party_account_type(party_type):
- if not hasattr(frappe.local, 'party_account_types'):
+ if not hasattr(frappe.local, "party_account_types"):
frappe.local.party_account_types = {}
if not party_type in frappe.local.party_account_types:
- frappe.local.party_account_types[party_type] = frappe.db.get_value("Party Type",
- party_type, "account_type") or ''
+ frappe.local.party_account_types[party_type] = (
+ frappe.db.get_value("Party Type", party_type, "account_type") or ""
+ )
return frappe.local.party_account_types[party_type]
+
def get_region(company=None):
- '''Return the default country based on flag, company or global settings
+ """Return the default country based on flag, company or global settings
You can also set global company flag in `frappe.flags.company`
- '''
+ """
if company or frappe.flags.company:
- return frappe.get_cached_value('Company',
- company or frappe.flags.company, 'country')
+ return frappe.get_cached_value("Company", company or frappe.flags.company, "country")
elif frappe.flags.country:
return frappe.flags.country
else:
- return frappe.get_system_settings('country')
+ return frappe.get_system_settings("country")
+
def allow_regional(fn):
- '''Decorator to make a function regionally overridable
+ """Decorator to make a function regionally overridable
Example:
@erpnext.allow_regional
def myfunction():
- pass'''
+ pass"""
+
def caller(*args, **kwargs):
region = get_region()
- fn_name = inspect.getmodule(fn).__name__ + '.' + fn.__name__
+ fn_name = inspect.getmodule(fn).__name__ + "." + fn.__name__
if region in regional_overrides and fn_name in regional_overrides[region]:
return frappe.get_attr(regional_overrides[region][fn_name])(*args, **kwargs)
else:
@@ -132,10 +148,17 @@ def allow_regional(fn):
return caller
+
+@frappe.whitelist()
def get_last_membership(member):
- '''Returns last membership if exists'''
- last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
- dict(member=member, paid=1), order_by='to_date desc', limit=1)
+ """Returns last membership if exists"""
+ last_membership = frappe.get_all(
+ "Membership",
+ "name,to_date,membership_type",
+ dict(member=member, paid=1),
+ order_by="to_date desc",
+ limit=1,
+ )
if last_membership:
return last_membership[0]
diff --git a/erpnext/accounts/custom/address.py b/erpnext/accounts/custom/address.py
index a28402eaef4..d844766e899 100644
--- a/erpnext/accounts/custom/address.py
+++ b/erpnext/accounts/custom/address.py
@@ -23,32 +23,31 @@ class ERPNextAddress(Address):
if self.is_your_company_address and not [
row for row in self.links if row.link_doctype == "Company"
]:
- frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
- title=_("Company Not Linked"))
+ frappe.throw(
+ _("Address needs to be linked to a Company. Please add a row for Company in the Links table."),
+ title=_("Company Not Linked"),
+ )
def on_update(self):
"""
After Address is updated, update the related 'Primary Address' on Customer.
"""
address_display = get_address_display(self.as_dict())
- filters = {
- "customer_primary_address": self.name
- }
-
+ filters = {"customer_primary_address": self.name}
customers = frappe.db.get_all("Customer", filters=filters, as_list=True)
for customer_name in customers:
frappe.db.set_value("Customer", customer_name[0], "primary_address", address_display)
+
@frappe.whitelist()
-def get_shipping_address(company, address = None):
+def get_shipping_address(company, address=None):
filters = [
["Dynamic Link", "link_doctype", "=", "Company"],
["Dynamic Link", "link_name", "=", company],
- ["Address", "is_your_company_address", "=", 1]
+ ["Address", "is_your_company_address", "=", 1],
]
fields = ["*"]
- if address and frappe.db.get_value('Dynamic Link',
- {'parent': address, 'link_name': company}):
+ if address and frappe.db.get_value("Dynamic Link", {"parent": address, "link_name": company}):
filters.append(["Address", "name", "=", address])
if not address:
filters.append(["Address", "is_shipping_address", "=", 1])
diff --git a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
index 1c1364ed111..fefec0ee7b4 100644
--- a/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
+++ b/erpnext/accounts/dashboard_chart_source/account_balance_timeline/account_balance_timeline.py
@@ -12,15 +12,24 @@ from frappe.utils.nestedset import get_descendants_of
@frappe.whitelist()
@cache_source
-def get(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
- to_date = None, timespan = None, time_interval = None, heatmap_year = None):
+def get(
+ chart_name=None,
+ chart=None,
+ no_cache=None,
+ filters=None,
+ from_date=None,
+ to_date=None,
+ timespan=None,
+ time_interval=None,
+ heatmap_year=None,
+):
if chart_name:
- chart = frappe.get_doc('Dashboard Chart', chart_name)
+ chart = frappe.get_doc("Dashboard Chart", chart_name)
else:
chart = frappe._dict(frappe.parse_json(chart))
timespan = chart.timespan
- if chart.timespan == 'Select Date Range':
+ if chart.timespan == "Select Date Range":
from_date = chart.from_date
to_date = chart.to_date
@@ -31,17 +40,23 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
company = filters.get("company")
if not account and chart_name:
- frappe.throw(_("Account is not set for the dashboard chart {0}")
- .format(get_link_to_form("Dashboard Chart", chart_name)))
+ frappe.throw(
+ _("Account is not set for the dashboard chart {0}").format(
+ get_link_to_form("Dashboard Chart", chart_name)
+ )
+ )
if not frappe.db.exists("Account", account) and chart_name:
- frappe.throw(_("Account {0} does not exists in the dashboard chart {1}")
- .format(account, get_link_to_form("Dashboard Chart", chart_name)))
+ frappe.throw(
+ _("Account {0} does not exists in the dashboard chart {1}").format(
+ account, get_link_to_form("Dashboard Chart", chart_name)
+ )
+ )
if not to_date:
to_date = nowdate()
if not from_date:
- if timegrain in ('Monthly', 'Quarterly'):
+ if timegrain in ("Monthly", "Quarterly"):
from_date = get_from_date_from_timespan(to_date, timespan)
# fetch dates to plot
@@ -54,16 +69,14 @@ def get(chart_name = None, chart = None, no_cache = None, filters = None, from_d
result = build_result(account, dates, gl_entries)
return {
- "labels": [formatdate(r[0].strftime('%Y-%m-%d')) for r in result],
- "datasets": [{
- "name": account,
- "values": [r[1] for r in result]
- }]
+ "labels": [formatdate(r[0].strftime("%Y-%m-%d")) for r in result],
+ "datasets": [{"name": account, "values": [r[1] for r in result]}],
}
+
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.db.get_value("Account", account, "root_type")
# start with the first date
date_index = 0
@@ -78,30 +91,34 @@ def build_result(account, dates, gl_entries):
result[date_index][1] += entry.debit - entry.credit
# if account type is credit, switch balances
- if root_type not in ('Asset', 'Expense'):
+ if root_type not in ("Asset", "Expense"):
for r in result:
r[1] = -1 * r[1]
# for balance sheet accounts, the totals are cumulative
- if root_type in ('Asset', 'Liability', 'Equity'):
+ if root_type in ("Asset", "Liability", "Equity"):
for i, r in enumerate(result):
if i > 0:
- r[1] = r[1] + result[i-1][1]
+ r[1] = r[1] + result[i - 1][1]
return result
+
def get_gl_entries(account, to_date):
- child_accounts = get_descendants_of('Account', account, ignore_permissions=True)
+ child_accounts = get_descendants_of("Account", account, ignore_permissions=True)
child_accounts.append(account)
- return frappe.db.get_all('GL Entry',
- fields = ['posting_date', 'debit', 'credit'],
- filters = [
- dict(posting_date = ('<', to_date)),
- dict(account = ('in', child_accounts)),
- dict(voucher_type = ('!=', 'Period Closing Voucher'))
+ return frappe.db.get_all(
+ "GL Entry",
+ fields=["posting_date", "debit", "credit"],
+ filters=[
+ dict(posting_date=("<", to_date)),
+ dict(account=("in", child_accounts)),
+ dict(voucher_type=("!=", "Period Closing Voucher")),
],
- order_by = 'posting_date asc')
+ order_by="posting_date asc",
+ )
+
def get_dates_from_timegrain(from_date, to_date, timegrain):
days = months = years = 0
@@ -116,6 +133,8 @@ def get_dates_from_timegrain(from_date, to_date, timegrain):
dates = [get_period_ending(from_date, timegrain)]
while getdate(dates[-1]) < getdate(to_date):
- date = get_period_ending(add_to_date(dates[-1], years=years, months=months, days=days), timegrain)
+ date = get_period_ending(
+ add_to_date(dates[-1], years=years, months=months, days=days), timegrain
+ )
dates.append(date)
return dates
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index 7e270601fb5..a8776fa3448 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -1,4 +1,3 @@
-
import frappe
from frappe import _
from frappe.email import sendmail_to_system_managers
@@ -23,20 +22,23 @@ from erpnext.accounts.utils import get_account_currency
def validate_service_stop_date(doc):
- ''' Validates service_stop_date for Purchase Invoice and Sales Invoice '''
+ """Validates service_stop_date for Purchase Invoice and Sales Invoice"""
- enable_check = "enable_deferred_revenue" \
- if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
+ enable_check = (
+ "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
+ )
old_stop_dates = {}
- old_doc = frappe.db.get_all("{0} Item".format(doc.doctype),
- {"parent": doc.name}, ["name", "service_stop_date"])
+ old_doc = frappe.db.get_all(
+ "{0} Item".format(doc.doctype), {"parent": doc.name}, ["name", "service_stop_date"]
+ )
for d in old_doc:
old_stop_dates[d.name] = d.service_stop_date or ""
for item in doc.items:
- if not item.get(enable_check): continue
+ if not item.get(enable_check):
+ continue
if item.service_stop_date:
if date_diff(item.service_stop_date, item.service_start_date) < 0:
@@ -45,21 +47,31 @@ def validate_service_stop_date(doc):
if date_diff(item.service_stop_date, item.service_end_date) > 0:
frappe.throw(_("Service Stop Date cannot be after Service End Date"))
- if old_stop_dates and old_stop_dates.get(item.name) and item.service_stop_date!=old_stop_dates.get(item.name):
+ if (
+ old_stop_dates
+ and old_stop_dates.get(item.name)
+ and item.service_stop_date != old_stop_dates.get(item.name)
+ ):
frappe.throw(_("Cannot change Service Stop Date for item in row {0}").format(item.idx))
+
def build_conditions(process_type, account, company):
- conditions=''
- deferred_account = "item.deferred_revenue_account" if process_type=="Income" else "item.deferred_expense_account"
+ conditions = ""
+ deferred_account = (
+ "item.deferred_revenue_account" if process_type == "Income" else "item.deferred_expense_account"
+ )
if account:
- conditions += "AND %s='%s'"%(deferred_account, account)
+ conditions += "AND %s='%s'" % (deferred_account, account)
elif company:
conditions += f"AND p.company = {frappe.db.escape(company)}"
return conditions
-def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_date=None, conditions=''):
+
+def convert_deferred_expense_to_expense(
+ deferred_process, start_date=None, end_date=None, conditions=""
+):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
if not start_date:
@@ -68,14 +80,19 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d
end_date = add_days(today(), -1)
# check for the purchase invoice for which GL entries has to be done
- invoices = frappe.db.sql_list('''
+ invoices = frappe.db.sql_list(
+ """
select distinct item.parent
from `tabPurchase Invoice Item` item, `tabPurchase Invoice` p
where item.service_start_date<=%s and item.service_end_date>=%s
and item.enable_deferred_expense = 1 and item.parent=p.name
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
{0}
- '''.format(conditions), (end_date, start_date)) #nosec
+ """.format(
+ conditions
+ ),
+ (end_date, start_date),
+ ) # nosec
# For each invoice, book deferred expense
for invoice in invoices:
@@ -85,7 +102,10 @@ def convert_deferred_expense_to_expense(deferred_process, start_date=None, end_d
if frappe.flags.deferred_accounting_error:
send_mail(deferred_process)
-def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_date=None, conditions=''):
+
+def convert_deferred_revenue_to_income(
+ deferred_process, start_date=None, end_date=None, conditions=""
+):
# book the expense/income on the last day, but it will be trigger on the 1st of month at 12:00 AM
if not start_date:
@@ -94,14 +114,19 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da
end_date = add_days(today(), -1)
# check for the sales invoice for which GL entries has to be done
- invoices = frappe.db.sql_list('''
+ invoices = frappe.db.sql_list(
+ """
select distinct item.parent
from `tabSales Invoice Item` item, `tabSales Invoice` p
where item.service_start_date<=%s and item.service_end_date>=%s
and item.enable_deferred_revenue = 1 and item.parent=p.name
and item.docstatus = 1 and ifnull(item.amount, 0) > 0
{0}
- '''.format(conditions), (end_date, start_date)) #nosec
+ """.format(
+ conditions
+ ),
+ (end_date, start_date),
+ ) # nosec
for invoice in invoices:
doc = frappe.get_doc("Sales Invoice", invoice)
@@ -110,30 +135,43 @@ def convert_deferred_revenue_to_income(deferred_process, start_date=None, end_da
if frappe.flags.deferred_accounting_error:
send_mail(deferred_process)
+
def get_booking_dates(doc, item, posting_date=None):
if not posting_date:
posting_date = add_days(today(), -1)
last_gl_entry = False
- deferred_account = "deferred_revenue_account" if doc.doctype=="Sales Invoice" else "deferred_expense_account"
+ deferred_account = (
+ "deferred_revenue_account" if doc.doctype == "Sales Invoice" else "deferred_expense_account"
+ )
- prev_gl_entry = frappe.db.sql('''
+ prev_gl_entry = frappe.db.sql(
+ """
select name, posting_date from `tabGL Entry` where company=%s and account=%s and
voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
+ and is_cancelled = 0
order by posting_date desc limit 1
- ''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
+ """,
+ (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
+ as_dict=True,
+ )
- prev_gl_via_je = frappe.db.sql('''
+ prev_gl_via_je = frappe.db.sql(
+ """
SELECT p.name, p.posting_date FROM `tabJournal Entry` p, `tabJournal Entry Account` c
WHERE p.name = c.parent and p.company=%s and c.account=%s
and c.reference_type=%s and c.reference_name=%s
and c.reference_detail_no=%s and c.docstatus < 2 order by posting_date desc limit 1
- ''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
+ """,
+ (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
+ as_dict=True,
+ )
if prev_gl_via_je:
- if (not prev_gl_entry) or (prev_gl_entry and
- prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date):
+ if (not prev_gl_entry) or (
+ prev_gl_entry and prev_gl_entry[0].posting_date < prev_gl_via_je[0].posting_date
+ ):
prev_gl_entry = prev_gl_via_je
if prev_gl_entry:
@@ -157,66 +195,94 @@ def get_booking_dates(doc, item, posting_date=None):
else:
return None, None, None
-def calculate_monthly_amount(doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency):
+
+def calculate_monthly_amount(
+ doc, item, last_gl_entry, start_date, end_date, total_days, total_booking_days, account_currency
+):
amount, base_amount = 0, 0
if not last_gl_entry:
- total_months = (item.service_end_date.year - item.service_start_date.year) * 12 + \
- (item.service_end_date.month - item.service_start_date.month) + 1
+ total_months = (
+ (item.service_end_date.year - item.service_start_date.year) * 12
+ + (item.service_end_date.month - item.service_start_date.month)
+ + 1
+ )
- prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) \
- / flt(date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date)))
+ prorate_factor = flt(date_diff(item.service_end_date, item.service_start_date)) / flt(
+ date_diff(get_last_day(item.service_end_date), get_first_day(item.service_start_date))
+ )
actual_months = rounded(total_months * prorate_factor, 1)
- already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
+ already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
+ doc, item
+ )
base_amount = flt(item.base_net_amount / actual_months, item.precision("base_net_amount"))
if base_amount + already_booked_amount > item.base_net_amount:
base_amount = item.base_net_amount - already_booked_amount
- if account_currency==doc.company_currency:
+ if account_currency == doc.company_currency:
amount = base_amount
else:
- amount = flt(item.net_amount/actual_months, item.precision("net_amount"))
+ amount = flt(item.net_amount / actual_months, item.precision("net_amount"))
if amount + already_booked_amount_in_account_currency > item.net_amount:
amount = item.net_amount - already_booked_amount_in_account_currency
if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date):
- partial_month = flt(date_diff(end_date, start_date)) \
- / flt(date_diff(get_last_day(end_date), get_first_day(start_date)))
+ partial_month = flt(date_diff(end_date, start_date)) / flt(
+ date_diff(get_last_day(end_date), get_first_day(start_date))
+ )
base_amount = rounded(partial_month, 1) * base_amount
amount = rounded(partial_month, 1) * amount
else:
- already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
- base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
- if account_currency==doc.company_currency:
+ already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
+ doc, item
+ )
+ base_amount = flt(
+ item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
+ )
+ if account_currency == doc.company_currency:
amount = base_amount
else:
- amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount"))
+ amount = flt(
+ item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
+ )
return amount, base_amount
+
def calculate_amount(doc, item, last_gl_entry, total_days, total_booking_days, account_currency):
amount, base_amount = 0, 0
if not last_gl_entry:
- base_amount = flt(item.base_net_amount*total_booking_days/flt(total_days), item.precision("base_net_amount"))
- if account_currency==doc.company_currency:
+ base_amount = flt(
+ item.base_net_amount * total_booking_days / flt(total_days), item.precision("base_net_amount")
+ )
+ if account_currency == doc.company_currency:
amount = base_amount
else:
- amount = flt(item.net_amount*total_booking_days/flt(total_days), item.precision("net_amount"))
+ amount = flt(
+ item.net_amount * total_booking_days / flt(total_days), item.precision("net_amount")
+ )
else:
- already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(doc, item)
+ already_booked_amount, already_booked_amount_in_account_currency = get_already_booked_amount(
+ doc, item
+ )
- base_amount = flt(item.base_net_amount - already_booked_amount, item.precision("base_net_amount"))
- if account_currency==doc.company_currency:
+ base_amount = flt(
+ item.base_net_amount - already_booked_amount, item.precision("base_net_amount")
+ )
+ if account_currency == doc.company_currency:
amount = base_amount
else:
- amount = flt(item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount"))
+ amount = flt(
+ item.net_amount - already_booked_amount_in_account_currency, item.precision("net_amount")
+ )
return amount, base_amount
+
def get_already_booked_amount(doc, item):
if doc.doctype == "Sales Invoice":
total_credit_debit, total_credit_debit_currency = "debit", "debit_in_account_currency"
@@ -225,20 +291,31 @@ def get_already_booked_amount(doc, item):
total_credit_debit, total_credit_debit_currency = "credit", "credit_in_account_currency"
deferred_account = "deferred_expense_account"
- gl_entries_details = frappe.db.sql('''
+ gl_entries_details = frappe.db.sql(
+ """
select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no
from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s
+ and is_cancelled = 0
group by voucher_detail_no
- '''.format(total_credit_debit, total_credit_debit_currency),
- (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
+ """.format(
+ total_credit_debit, total_credit_debit_currency
+ ),
+ (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
+ as_dict=True,
+ )
- journal_entry_details = frappe.db.sql('''
+ journal_entry_details = frappe.db.sql(
+ """
SELECT sum(c.{0}) as total_credit, sum(c.{1}) as total_credit_in_account_currency, reference_detail_no
FROM `tabJournal Entry` p , `tabJournal Entry Account` c WHERE p.name = c.parent and
p.company = %s and c.account=%s and c.reference_type=%s and c.reference_name=%s and c.reference_detail_no=%s
and p.docstatus < 2 group by reference_detail_no
- '''.format(total_credit_debit, total_credit_debit_currency),
- (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True)
+ """.format(
+ total_credit_debit, total_credit_debit_currency
+ ),
+ (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name),
+ as_dict=True,
+ )
already_booked_amount = gl_entries_details[0].total_credit if gl_entries_details else 0
already_booked_amount += journal_entry_details[0].total_credit if journal_entry_details else 0
@@ -246,20 +323,31 @@ def get_already_booked_amount(doc, item):
if doc.currency == doc.company_currency:
already_booked_amount_in_account_currency = already_booked_amount
else:
- already_booked_amount_in_account_currency = gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0
- already_booked_amount_in_account_currency += journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0
+ already_booked_amount_in_account_currency = (
+ gl_entries_details[0].total_credit_in_account_currency if gl_entries_details else 0
+ )
+ already_booked_amount_in_account_currency += (
+ journal_entry_details[0].total_credit_in_account_currency if journal_entry_details else 0
+ )
return already_booked_amount, already_booked_amount_in_account_currency
+
def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
- enable_check = "enable_deferred_revenue" \
- if doc.doctype=="Sales Invoice" else "enable_deferred_expense"
+ enable_check = (
+ "enable_deferred_revenue" if doc.doctype == "Sales Invoice" else "enable_deferred_expense"
+ )
- def _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on):
+ accounts_frozen_upto = frappe.get_cached_value("Accounts Settings", "None", "acc_frozen_upto")
+
+ def _book_deferred_revenue_or_expense(
+ item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
+ ):
start_date, end_date, last_gl_entry = get_booking_dates(doc, item, posting_date=posting_date)
- if not (start_date and end_date): return
+ if not (start_date and end_date):
+ return
- account_currency = get_account_currency(item.expense_account)
+ account_currency = get_account_currency(item.expense_account or item.income_account)
if doc.doctype == "Sales Invoice":
against, project = doc.customer, doc.project
credit_account, debit_account = item.income_account, item.deferred_revenue_account
@@ -270,103 +358,179 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None):
total_days = date_diff(item.service_end_date, item.service_start_date) + 1
total_booking_days = date_diff(end_date, start_date) + 1
- if book_deferred_entries_based_on == 'Months':
- amount, base_amount = calculate_monthly_amount(doc, item, last_gl_entry,
- start_date, end_date, total_days, total_booking_days, account_currency)
+ if book_deferred_entries_based_on == "Months":
+ amount, base_amount = calculate_monthly_amount(
+ doc,
+ item,
+ last_gl_entry,
+ start_date,
+ end_date,
+ total_days,
+ total_booking_days,
+ account_currency,
+ )
else:
- amount, base_amount = calculate_amount(doc, item, last_gl_entry,
- total_days, total_booking_days, account_currency)
+ amount, base_amount = calculate_amount(
+ doc, item, last_gl_entry, total_days, total_booking_days, account_currency
+ )
if not amount:
return
+ # check if books nor frozen till endate:
+ if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto):
+ end_date = get_last_day(add_days(accounts_frozen_upto, 1))
+
if via_journal_entry:
- book_revenue_via_journal_entry(doc, credit_account, debit_account, against, amount,
- base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process, submit_journal_entry)
+ book_revenue_via_journal_entry(
+ doc,
+ credit_account,
+ debit_account,
+ amount,
+ base_amount,
+ end_date,
+ project,
+ account_currency,
+ item.cost_center,
+ item,
+ deferred_process,
+ submit_journal_entry,
+ )
else:
- make_gl_entries(doc, credit_account, debit_account, against,
- amount, base_amount, end_date, project, account_currency, item.cost_center, item, deferred_process)
+ make_gl_entries(
+ doc,
+ credit_account,
+ debit_account,
+ against,
+ amount,
+ base_amount,
+ end_date,
+ project,
+ account_currency,
+ item.cost_center,
+ item,
+ deferred_process,
+ )
# Returned in case of any errors because it tries to submit the same record again and again in case of errors
if frappe.flags.deferred_accounting_error:
return
if getdate(end_date) < getdate(posting_date) and not last_gl_entry:
- _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on)
+ _book_deferred_revenue_or_expense(
+ item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
+ )
- via_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_via_journal_entry'))
- submit_journal_entry = cint(frappe.db.get_singles_value('Accounts Settings', 'submit_journal_entries'))
- book_deferred_entries_based_on = frappe.db.get_singles_value('Accounts Settings', 'book_deferred_entries_based_on')
+ via_journal_entry = cint(
+ frappe.db.get_singles_value("Accounts Settings", "book_deferred_entries_via_journal_entry")
+ )
+ submit_journal_entry = cint(
+ frappe.db.get_singles_value("Accounts Settings", "submit_journal_entries")
+ )
+ book_deferred_entries_based_on = frappe.db.get_singles_value(
+ "Accounts Settings", "book_deferred_entries_based_on"
+ )
- for item in doc.get('items'):
+ for item in doc.get("items"):
if item.get(enable_check):
- _book_deferred_revenue_or_expense(item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on)
+ _book_deferred_revenue_or_expense(
+ item, via_journal_entry, submit_journal_entry, book_deferred_entries_based_on
+ )
+
def process_deferred_accounting(posting_date=None):
- ''' Converts deferred income/expense into income/expense
- Executed via background jobs on every month end '''
+ """Converts deferred income/expense into income/expense
+ Executed via background jobs on every month end"""
if not posting_date:
posting_date = today()
- if not cint(frappe.db.get_singles_value('Accounts Settings', 'automatically_process_deferred_accounting_entry')):
+ if not cint(
+ frappe.db.get_singles_value(
+ "Accounts Settings", "automatically_process_deferred_accounting_entry"
+ )
+ ):
return
start_date = add_months(today(), -1)
end_date = add_days(today(), -1)
- companies = frappe.get_all('Company')
+ companies = frappe.get_all("Company")
for company in companies:
- for record_type in ('Income', 'Expense'):
- doc = frappe.get_doc(dict(
- doctype='Process Deferred Accounting',
- company=company.name,
- posting_date=posting_date,
- start_date=start_date,
- end_date=end_date,
- type=record_type
- ))
+ for record_type in ("Income", "Expense"):
+ doc = frappe.get_doc(
+ dict(
+ doctype="Process Deferred Accounting",
+ company=company.name,
+ posting_date=posting_date,
+ start_date=start_date,
+ end_date=end_date,
+ type=record_type,
+ )
+ )
doc.insert()
doc.submit()
-def make_gl_entries(doc, credit_account, debit_account, against,
- amount, base_amount, posting_date, project, account_currency, cost_center, item, deferred_process=None):
+
+def make_gl_entries(
+ doc,
+ credit_account,
+ debit_account,
+ against,
+ amount,
+ base_amount,
+ posting_date,
+ project,
+ account_currency,
+ cost_center,
+ item,
+ deferred_process=None,
+):
# GL Entry for crediting the amount in the deferred expense
from erpnext.accounts.general_ledger import make_gl_entries
- if amount == 0: return
+ if amount == 0:
+ return
gl_entries = []
gl_entries.append(
- doc.get_gl_dict({
- "account": credit_account,
- "against": against,
- "credit": base_amount,
- "credit_in_account_currency": amount,
- "cost_center": cost_center,
- "voucher_detail_no": item.name,
- 'posting_date': posting_date,
- 'project': project,
- 'against_voucher_type': 'Process Deferred Accounting',
- 'against_voucher': deferred_process
- }, account_currency, item=item)
+ doc.get_gl_dict(
+ {
+ "account": credit_account,
+ "against": against,
+ "credit": base_amount,
+ "credit_in_account_currency": amount,
+ "cost_center": cost_center,
+ "voucher_detail_no": item.name,
+ "posting_date": posting_date,
+ "project": project,
+ "against_voucher_type": "Process Deferred Accounting",
+ "against_voucher": deferred_process,
+ },
+ account_currency,
+ item=item,
+ )
)
# GL Entry to debit the amount from the expense
gl_entries.append(
- doc.get_gl_dict({
- "account": debit_account,
- "against": against,
- "debit": base_amount,
- "debit_in_account_currency": amount,
- "cost_center": cost_center,
- "voucher_detail_no": item.name,
- 'posting_date': posting_date,
- 'project': project,
- 'against_voucher_type': 'Process Deferred Accounting',
- 'against_voucher': deferred_process
- }, account_currency, item=item)
+ doc.get_gl_dict(
+ {
+ "account": debit_account,
+ "against": against,
+ "debit": base_amount,
+ "debit_in_account_currency": amount,
+ "cost_center": cost_center,
+ "voucher_detail_no": item.name,
+ "posting_date": posting_date,
+ "project": project,
+ "against_voucher_type": "Process Deferred Accounting",
+ "against_voucher": deferred_process,
+ },
+ account_currency,
+ item=item,
+ )
)
if gl_entries:
@@ -376,72 +540,88 @@ def make_gl_entries(doc, credit_account, debit_account, against,
except Exception as e:
if frappe.flags.in_test:
traceback = frappe.get_traceback()
- frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
+ frappe.log_error(
+ title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
+ message=traceback,
+ )
raise e
else:
frappe.db.rollback()
traceback = frappe.get_traceback()
- frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
+ frappe.log_error(
+ title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
+ message=traceback,
+ )
frappe.flags.deferred_accounting_error = True
+
def send_mail(deferred_process):
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
- link = get_link_to_form('Process Deferred Accounting', deferred_process)
+ link = get_link_to_form("Process Deferred Accounting", deferred_process)
content = _("Deferred accounting failed for some invoices:") + "\n"
- content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link)
+ content += _(
+ "Please check Process Deferred Accounting {0} and submit manually after resolving errors."
+ ).format(link)
sendmail_to_system_managers(title, content)
-def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
- amount, base_amount, posting_date, project, account_currency, cost_center, item,
- deferred_process=None, submit='No'):
- if amount == 0: return
+def book_revenue_via_journal_entry(
+ doc,
+ credit_account,
+ debit_account,
+ amount,
+ base_amount,
+ posting_date,
+ project,
+ account_currency,
+ cost_center,
+ item,
+ deferred_process=None,
+ submit="No",
+):
- journal_entry = frappe.new_doc('Journal Entry')
+ if amount == 0:
+ return
+
+ journal_entry = frappe.new_doc("Journal Entry")
journal_entry.posting_date = posting_date
journal_entry.company = doc.company
- journal_entry.voucher_type = 'Deferred Revenue' if doc.doctype == 'Sales Invoice' \
- else 'Deferred Expense'
+ journal_entry.voucher_type = (
+ "Deferred Revenue" if doc.doctype == "Sales Invoice" else "Deferred Expense"
+ )
+ journal_entry.process_deferred_accounting = deferred_process
debit_entry = {
- 'account': credit_account,
- 'credit': base_amount,
- 'credit_in_account_currency': amount,
- 'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
- 'party': against,
- 'account_currency': account_currency,
- 'reference_name': doc.name,
- 'reference_type': doc.doctype,
- 'reference_detail_no': item.name,
- 'cost_center': cost_center,
- 'project': project,
+ "account": credit_account,
+ "credit": base_amount,
+ "credit_in_account_currency": amount,
+ "account_currency": account_currency,
+ "reference_name": doc.name,
+ "reference_type": doc.doctype,
+ "reference_detail_no": item.name,
+ "cost_center": cost_center,
+ "project": project,
}
credit_entry = {
- 'account': debit_account,
- 'debit': base_amount,
- 'debit_in_account_currency': amount,
- 'party_type': 'Customer' if doc.doctype == 'Sales Invoice' else 'Supplier',
- 'party': against,
- 'account_currency': account_currency,
- 'reference_name': doc.name,
- 'reference_type': doc.doctype,
- 'reference_detail_no': item.name,
- 'cost_center': cost_center,
- 'project': project,
+ "account": debit_account,
+ "debit": base_amount,
+ "debit_in_account_currency": amount,
+ "account_currency": account_currency,
+ "reference_name": doc.name,
+ "reference_type": doc.doctype,
+ "reference_detail_no": item.name,
+ "cost_center": cost_center,
+ "project": project,
}
for dimension in get_accounting_dimensions():
- debit_entry.update({
- dimension: item.get(dimension)
- })
+ debit_entry.update({dimension: item.get(dimension)})
- credit_entry.update({
- dimension: item.get(dimension)
- })
+ credit_entry.update({dimension: item.get(dimension)})
- journal_entry.append('accounts', debit_entry)
- journal_entry.append('accounts', credit_entry)
+ journal_entry.append("accounts", debit_entry)
+ journal_entry.append("accounts", credit_entry)
try:
journal_entry.save()
@@ -453,20 +633,30 @@ def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
except Exception:
frappe.db.rollback()
traceback = frappe.get_traceback()
- frappe.log_error(title=_('Error while processing deferred accounting for Invoice {0}').format(doc.name), message=traceback)
+ frappe.log_error(
+ title=_("Error while processing deferred accounting for Invoice {0}").format(doc.name),
+ message=traceback,
+ )
frappe.flags.deferred_accounting_error = True
+
def get_deferred_booking_accounts(doctype, voucher_detail_no, dr_or_cr):
- if doctype == 'Sales Invoice':
- credit_account, debit_account = frappe.db.get_value('Sales Invoice Item', {'name': voucher_detail_no},
- ['income_account', 'deferred_revenue_account'])
+ if doctype == "Sales Invoice":
+ credit_account, debit_account = frappe.db.get_value(
+ "Sales Invoice Item",
+ {"name": voucher_detail_no},
+ ["income_account", "deferred_revenue_account"],
+ )
else:
- credit_account, debit_account = frappe.db.get_value('Purchase Invoice Item', {'name': voucher_detail_no},
- ['deferred_expense_account', 'expense_account'])
+ credit_account, debit_account = frappe.db.get_value(
+ "Purchase Invoice Item",
+ {"name": voucher_detail_no},
+ ["deferred_expense_account", "expense_account"],
+ )
- if dr_or_cr == 'Debit':
+ if dr_or_cr == "Debit":
return debit_account
else:
return credit_account
diff --git a/erpnext/accounts/doctype/account/account.js b/erpnext/accounts/doctype/account/account.js
index 7a1d7359488..320e1cab7c3 100644
--- a/erpnext/accounts/doctype/account/account.js
+++ b/erpnext/accounts/doctype/account/account.js
@@ -43,12 +43,12 @@ frappe.ui.form.on('Account', {
frm.trigger('add_toolbar_buttons');
}
if (frm.has_perm('write')) {
- frm.add_custom_button(__('Update Account Name / Number'), function () {
- frm.trigger("update_account_number");
- });
frm.add_custom_button(__('Merge Account'), function () {
frm.trigger("merge_account");
- });
+ }, __('Actions'));
+ frm.add_custom_button(__('Update Account Name / Number'), function () {
+ frm.trigger("update_account_number");
+ }, __('Actions'));
}
}
},
@@ -59,11 +59,12 @@ frappe.ui.form.on('Account', {
}
},
add_toolbar_buttons: function(frm) {
- frm.add_custom_button(__('Chart of Accounts'),
- function () { frappe.set_route("Tree", "Account"); });
+ frm.add_custom_button(__('Chart of Accounts'), () => {
+ frappe.set_route("Tree", "Account");
+ }, __('View'));
if (frm.doc.is_group == 1) {
- frm.add_custom_button(__('Group to Non-Group'), function () {
+ frm.add_custom_button(__('Convert to Non-Group'), function () {
return frappe.call({
doc: frm.doc,
method: 'convert_group_to_ledger',
@@ -71,10 +72,11 @@ frappe.ui.form.on('Account', {
frm.refresh();
}
});
- });
+ }, __('Actions'));
+
} else if (cint(frm.doc.is_group) == 0
&& frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
- frm.add_custom_button(__('Ledger'), function () {
+ frm.add_custom_button(__('General Ledger'), function () {
frappe.route_options = {
"account": frm.doc.name,
"from_date": frappe.sys_defaults.year_start_date,
@@ -82,9 +84,9 @@ frappe.ui.form.on('Account', {
"company": frm.doc.company
};
frappe.set_route("query-report", "General Ledger");
- });
+ }, __('View'));
- frm.add_custom_button(__('Non-Group to Group'), function () {
+ frm.add_custom_button(__('Convert to Group'), function () {
return frappe.call({
doc: frm.doc,
method: 'convert_ledger_to_group',
@@ -92,7 +94,7 @@ frappe.ui.form.on('Account', {
frm.refresh();
}
});
- });
+ }, __('Actions'));
}
},
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index f8a06c7243f..c71ea3648b9 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -10,11 +10,17 @@ from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_
import erpnext
-class RootNotEditable(frappe.ValidationError): pass
-class BalanceMismatchError(frappe.ValidationError): pass
+class RootNotEditable(frappe.ValidationError):
+ pass
+
+
+class BalanceMismatchError(frappe.ValidationError):
+ pass
+
class Account(NestedSet):
- nsm_parent_field = 'parent_account'
+ nsm_parent_field = "parent_account"
+
def on_update(self):
if frappe.local.flags.ignore_update_nsm:
return
@@ -22,17 +28,20 @@ class Account(NestedSet):
super(Account, self).on_update()
def onload(self):
- frozen_accounts_modifier = frappe.db.get_value("Accounts Settings", "Accounts Settings",
- "frozen_accounts_modifier")
+ frozen_accounts_modifier = frappe.db.get_value(
+ "Accounts Settings", "Accounts Settings", "frozen_accounts_modifier"
+ )
if not frozen_accounts_modifier or frozen_accounts_modifier in frappe.get_roles():
self.set_onload("can_freeze_account", True)
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)
def validate(self):
from erpnext.accounts.utils import validate_field_number
+
if frappe.local.flags.allow_unverified_charts:
return
self.validate_parent()
@@ -49,22 +58,33 @@ class Account(NestedSet):
def validate_parent(self):
"""Fetch Parent Details and validate parent account"""
if self.parent_account:
- par = frappe.db.get_value("Account", self.parent_account,
- ["name", "is_group", "company"], as_dict=1)
+ par = frappe.db.get_value(
+ "Account", self.parent_account, ["name", "is_group", "company"], as_dict=1
+ )
if not par:
- throw(_("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account))
+ throw(
+ _("Account {0}: Parent account {1} does not exist").format(self.name, self.parent_account)
+ )
elif par.name == self.name:
throw(_("Account {0}: You can not assign itself as parent account").format(self.name))
elif not par.is_group:
- throw(_("Account {0}: Parent account {1} can not be a ledger").format(self.name, self.parent_account))
+ throw(
+ _("Account {0}: Parent account {1} can not be a ledger").format(
+ self.name, self.parent_account
+ )
+ )
elif par.company != self.company:
- throw(_("Account {0}: Parent account {1} does not belong to company: {2}")
- .format(self.name, self.parent_account, self.company))
+ throw(
+ _("Account {0}: Parent account {1} does not belong to company: {2}").format(
+ self.name, self.parent_account, self.company
+ )
+ )
def set_root_and_report_type(self):
if self.parent_account:
- par = frappe.db.get_value("Account", self.parent_account,
- ["report_type", "root_type"], as_dict=1)
+ par = frappe.db.get_value(
+ "Account", self.parent_account, ["report_type", "root_type"], as_dict=1
+ )
if par.report_type:
self.report_type = par.report_type
@@ -75,15 +95,20 @@ class Account(NestedSet):
db_value = frappe.db.get_value("Account", self.name, ["report_type", "root_type"], as_dict=1)
if db_value:
if self.report_type != db_value.report_type:
- frappe.db.sql("update `tabAccount` set report_type=%s where lft > %s and rgt < %s",
- (self.report_type, self.lft, self.rgt))
+ frappe.db.sql(
+ "update `tabAccount` set report_type=%s where lft > %s and rgt < %s",
+ (self.report_type, self.lft, self.rgt),
+ )
if self.root_type != db_value.root_type:
- frappe.db.sql("update `tabAccount` set root_type=%s where lft > %s and rgt < %s",
- (self.root_type, self.lft, self.rgt))
+ frappe.db.sql(
+ "update `tabAccount` set root_type=%s where lft > %s and rgt < %s",
+ (self.root_type, self.lft, self.rgt),
+ )
if self.root_type and not self.report_type:
- self.report_type = "Balance Sheet" \
- if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
+ self.report_type = (
+ "Balance Sheet" if self.root_type in ("Asset", "Liability", "Equity") else "Profit and Loss"
+ )
def validate_root_details(self):
# does not exists parent
@@ -96,21 +121,26 @@ class Account(NestedSet):
def validate_root_company_and_sync_account_to_children(self):
# ignore validation while creating new compnay or while syncing to child companies
- if frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation:
+ if (
+ frappe.local.flags.ignore_root_company_validation or self.flags.ignore_root_company_validation
+ ):
return
ancestors = get_root_company(self.company)
if ancestors:
if frappe.get_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'):
+ if not frappe.db.get_value(
+ "Account", {"account_name": self.account_name, "company": ancestors[0]}, "name"
+ ):
frappe.throw(_("Please add the account to root level Company - {}").format(ancestors[0]))
elif self.parent_account:
- descendants = get_descendants_of('Company', self.company)
- if not descendants: return
+ descendants = get_descendants_of("Company", self.company)
+ if not descendants:
+ return
parent_acc_name_map = {}
- parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \
- ["account_name", "account_number"])
+ parent_acc_name, parent_acc_number = frappe.db.get_value(
+ "Account", self.parent_account, ["account_name", "account_number"]
+ )
filters = {
"company": ["in", descendants],
"account_name": parent_acc_name,
@@ -118,10 +148,13 @@ class Account(NestedSet):
if parent_acc_number:
filters["account_number"] = parent_acc_number
- for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
+ for d in frappe.db.get_values(
+ "Account", filters=filters, fieldname=["company", "name"], as_dict=True
+ ):
parent_acc_name_map[d["company"]] = d["name"]
- if not parent_acc_name_map: return
+ if not parent_acc_name_map:
+ return
self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name)
@@ -142,26 +175,38 @@ class Account(NestedSet):
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"))
+ 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"))
def validate_balance_must_be_debit_or_credit(self):
from erpnext.accounts.utils import get_balance_on
+
if not self.get("__islocal") and self.balance_must_be:
account_balance = get_balance_on(self.name)
if account_balance > 0 and self.balance_must_be == "Credit":
- frappe.throw(_("Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'"))
+ frappe.throw(
+ _(
+ "Account balance already in Debit, you are not allowed to set 'Balance Must Be' as 'Credit'"
+ )
+ )
elif account_balance < 0 and self.balance_must_be == "Debit":
- frappe.throw(_("Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'"))
+ frappe.throw(
+ _(
+ "Account balance already in Credit, you are not allowed to set 'Balance Must Be' as 'Debit'"
+ )
+ )
def validate_account_currency(self):
if not self.account_currency:
- self.account_currency = frappe.get_cached_value('Company', self.company, "default_currency")
+ self.account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
- elif self.account_currency != frappe.db.get_value("Account", self.name, "account_currency"):
+ gl_currency = frappe.db.get_value("GL Entry", {"account": self.name}, "account_currency")
+
+ if gl_currency and self.account_currency != gl_currency:
if frappe.db.get_value("GL Entry", {"account": self.name}):
frappe.throw(_("Currency can not be changed after making entries using some other currency"))
@@ -170,45 +215,52 @@ class Account(NestedSet):
company_bold = frappe.bold(company)
parent_acc_name_bold = frappe.bold(parent_acc_name)
if not parent_acc_name_map.get(company):
- frappe.throw(_("While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA")
- .format(company_bold, parent_acc_name_bold), title=_("Account Not Found"))
+ frappe.throw(
+ _(
+ "While creating account for Child Company {0}, parent account {1} not found. Please create the parent account in corresponding COA"
+ ).format(company_bold, parent_acc_name_bold),
+ title=_("Account Not Found"),
+ )
# 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")):
- msg = _("While creating account for Child Company {0}, parent account {1} found as a ledger account.").format(company_bold, parent_acc_name_bold)
+ 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"
+ ):
+ msg = _(
+ "While creating account for Child Company {0}, parent account {1} found as a ledger account."
+ ).format(company_bold, parent_acc_name_bold)
msg += "
"
- msg += _("Please convert the parent account in corresponding child company to a group account.")
+ msg += _(
+ "Please convert the parent account in corresponding child company to a group account."
+ )
frappe.throw(msg, title=_("Invalid Parent Account"))
- filters = {
- "account_name": self.account_name,
- "company": company
- }
+ filters = {"account_name": self.account_name, "company": company}
if self.account_number:
filters["account_number"] = self.account_number
- child_account = frappe.db.get_value("Account", filters, 'name')
+ child_account = frappe.db.get_value("Account", filters, "name")
if not child_account:
doc = frappe.copy_doc(self)
doc.flags.ignore_root_company_validation = True
- doc.update({
- "company": company,
- # parent account's currency should be passed down to child account's curreny
- # if it is None, it picks it up from default company currency, which might be unintended
- "account_currency": erpnext.get_company_currency(company),
- "parent_account": parent_acc_name_map[company]
- })
+ doc.update(
+ {
+ "company": company,
+ # parent account's currency should be passed down to child account's curreny
+ # if it is None, it picks it up from default company currency, which might be unintended
+ "account_currency": erpnext.get_company_currency(company),
+ "parent_account": parent_acc_name_map[company],
+ }
+ )
doc.save()
- frappe.msgprint(_("Account {0} is added in the child company {1}")
- .format(doc.name, company))
+ frappe.msgprint(_("Account {0} is added in the child company {1}").format(doc.name, company))
elif child_account:
# update the parent company's value in child companies
doc = frappe.get_doc("Account", child_account)
parent_value_changed = False
- for field in ['account_type', 'freeze_account', 'balance_must_be']:
+ for field in ["account_type", "freeze_account", "balance_must_be"]:
if doc.get(field) != self.get(field):
parent_value_changed = True
doc.set(field, self.get(field))
@@ -243,8 +295,11 @@ class Account(NestedSet):
return frappe.db.get_value("GL Entry", {"account": self.name})
def check_if_child_exists(self):
- return frappe.db.sql("""select name from `tabAccount` where parent_account = %s
- and docstatus != 2""", self.name)
+ return frappe.db.sql(
+ """select name from `tabAccount` where parent_account = %s
+ and docstatus != 2""",
+ self.name,
+ )
def validate_mandatory(self):
if not self.root_type:
@@ -260,73 +315,99 @@ class Account(NestedSet):
super(Account, self).on_trash(True)
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_parent_account(doctype, txt, searchfield, start, page_len, filters):
- return frappe.db.sql("""select name from tabAccount
+ return frappe.db.sql(
+ """select name from tabAccount
where is_group = 1 and docstatus != 2 and company = %s
- and %s like %s order by name limit %s, %s""" %
- ("%s", searchfield, "%s", "%s", "%s"),
- (filters["company"], "%%%s%%" % txt, start, page_len), as_list=1)
+ and %s like %s order by name limit %s, %s"""
+ % ("%s", searchfield, "%s", "%s", "%s"),
+ (filters["company"], "%%%s%%" % txt, start, page_len),
+ as_list=1,
+ )
+
def get_account_currency(account):
"""Helper function to get account currency"""
if not account:
return
+
def generator():
- account_currency, company = frappe.get_cached_value("Account", account, ["account_currency", "company"])
+ account_currency, company = frappe.get_cached_value(
+ "Account", account, ["account_currency", "company"]
+ )
if not account_currency:
- account_currency = frappe.get_cached_value('Company', company, "default_currency")
+ account_currency = frappe.get_cached_value("Company", company, "default_currency")
return account_currency
return frappe.local_cache("account_currency", account, generator)
+
def on_doctype_update():
frappe.db.add_index("Account", ["lft", "rgt"])
+
def get_account_autoname(account_number, account_name, company):
# first validate if company exists
- company = frappe.get_cached_value('Company', company, ["abbr", "name"], as_dict=True)
+ company = frappe.get_cached_value("Company", company, ["abbr", "name"], as_dict=True)
if not company:
- frappe.throw(_('Company {0} does not exist').format(company))
+ frappe.throw(_("Company {0} does not exist").format(company))
parts = [account_name.strip(), company.abbr]
if cstr(account_number).strip():
parts.insert(0, cstr(account_number).strip())
- return ' - '.join(parts)
+ return " - ".join(parts)
+
def validate_account_number(name, account_number, company):
if account_number:
- account_with_same_number = frappe.db.get_value("Account",
- {"account_number": account_number, "company": company, "name": ["!=", name]})
+ account_with_same_number = frappe.db.get_value(
+ "Account", {"account_number": account_number, "company": company, "name": ["!=", name]}
+ )
if account_with_same_number:
- frappe.throw(_("Account Number {0} already used in account {1}")
- .format(account_number, account_with_same_number))
+ frappe.throw(
+ _("Account Number {0} already used in account {1}").format(
+ account_number, account_with_same_number
+ )
+ )
+
@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)
- if not account: return
+ 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 = frappe.db.get_value(
+ "Account", name, ["account_name", "account_number"]
+ )
# check if account exists in parent company
ancestors = get_ancestors_of("Company", account.company)
- allow_independent_account_creation = frappe.get_value("Company", account.company, "allow_account_creation_against_child_company")
+ allow_independent_account_creation = frappe.get_value(
+ "Company", account.company, "allow_account_creation_against_child_company"
+ )
if ancestors and not allow_independent_account_creation:
for ancestor in ancestors:
- if frappe.db.get_value("Account", {'account_name': old_acc_name, 'company': ancestor}, 'name'):
+ if frappe.db.get_value("Account", {"account_name": old_acc_name, "company": ancestor}, "name"):
# same account in parent company exists
allow_child_account_creation = _("Allow Account Creation Against Child Company")
- message = _("Account {0} exists in parent company {1}.").format(frappe.bold(old_acc_name), frappe.bold(ancestor))
+ message = _("Account {0} exists in parent company {1}.").format(
+ frappe.bold(old_acc_name), frappe.bold(ancestor)
+ )
message += "
"
- message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(frappe.bold(ancestor))
+ message += _("Renaming it is only allowed via parent company {0}, to avoid mismatch.").format(
+ frappe.bold(ancestor)
+ )
message += "
"
- message += _("To overrule this, enable '{0}' in company {1}").format(allow_child_account_creation, frappe.bold(account.company))
+ message += _("To overrule this, enable '{0}' in company {1}").format(
+ allow_child_account_creation, frappe.bold(account.company)
+ )
frappe.throw(message, title=_("Rename Not Allowed"))
@@ -339,42 +420,53 @@ def update_account_number(name, account_name, account_number=None, from_descenda
if not from_descendant:
# Update and rename in child company accounts as well
- descendants = get_descendants_of('Company', account.company)
+ descendants = get_descendants_of("Company", account.company)
if descendants:
- sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number, old_acc_number)
+ sync_update_account_number_in_child(
+ descendants, old_acc_name, account_name, account_number, old_acc_number
+ )
new_name = get_account_autoname(account_number, account_name, account.company)
if name != new_name:
frappe.rename_doc("Account", name, new_name, force=1)
return new_name
+
@frappe.whitelist()
def merge_account(old, new, is_group, root_type, company):
# Validate properties before merging
if not frappe.db.exists("Account", new):
throw(_("Account {0} does not exist").format(new))
- val = list(frappe.db.get_value("Account", new,
- ["is_group", "root_type", "company"]))
+ val = list(frappe.db.get_value("Account", new, ["is_group", "root_type", "company"]))
if val != [cint(is_group), root_type, company]:
- throw(_("""Merging is only possible if following properties are same in both records. 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"))
+ frappe.db.set_value(
+ "Account", new, "parent_account", frappe.db.get_value("Account", old, "parent_account")
+ )
frappe.rename_doc("Account", old, new, merge=1, force=1)
return new
+
@frappe.whitelist()
def get_root_company(company):
# return the topmost company in the hierarchy
- ancestors = get_ancestors_of('Company', company, "lft asc")
+ ancestors = get_ancestors_of("Company", company, "lft asc")
return [ancestors[0]] if ancestors else []
-def sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number=None, old_acc_number=None):
+
+def sync_update_account_number_in_child(
+ descendants, old_acc_name, account_name, account_number=None, old_acc_number=None
+):
filters = {
"company": ["in", descendants],
"account_name": old_acc_name,
@@ -382,5 +474,7 @@ def sync_update_account_number_in_child(descendants, old_acc_name, account_name,
if old_acc_number:
filters["account_number"] = old_acc_number
- for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True):
- update_account_number(d["name"], account_name, account_number, from_descendant=True)
+ for d in frappe.db.get_values(
+ "Account", filters=filters, fieldname=["company", "name"], as_dict=True
+ ):
+ update_account_number(d["name"], account_name, account_number, from_descendant=True)
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
index 3a5514388ca..fd3c19149ba 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
@@ -11,7 +11,9 @@ from six import iteritems
from unidecode import unidecode
-def create_charts(company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None):
+def create_charts(
+ company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None
+):
chart = custom_chart or get_chart(chart_template, existing_company)
if chart:
accounts = []
@@ -21,30 +23,41 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
if root_account:
root_type = child.get("root_type")
- if account_name not in ["account_name", "account_number", "account_type",
- "root_type", "is_group", "tax_rate"]:
+ if account_name not in [
+ "account_name",
+ "account_number",
+ "account_type",
+ "root_type",
+ "is_group",
+ "tax_rate",
+ ]:
account_number = cstr(child.get("account_number")).strip()
- account_name, account_name_in_db = add_suffix_if_duplicate(account_name,
- account_number, accounts)
+ account_name, account_name_in_db = add_suffix_if_duplicate(
+ account_name, account_number, accounts
+ )
is_group = identify_is_group(child)
- report_type = "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] \
- else "Profit and Loss"
+ report_type = (
+ "Balance Sheet" if root_type in ["Asset", "Liability", "Equity"] else "Profit and Loss"
+ )
- account = frappe.get_doc({
- "doctype": "Account",
- "account_name": child.get('account_name') if from_coa_importer else account_name,
- "company": company,
- "parent_account": parent,
- "is_group": is_group,
- "root_type": root_type,
- "report_type": report_type,
- "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"),
- "tax_rate": child.get("tax_rate")
- })
+ account = frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": child.get("account_name") if from_coa_importer else account_name,
+ "company": company,
+ "parent_account": parent,
+ "is_group": is_group,
+ "root_type": root_type,
+ "report_type": report_type,
+ "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"),
+ "tax_rate": child.get("tax_rate"),
+ }
+ )
if root_account or frappe.local.flags.allow_unverified_charts:
account.flags.ignore_mandatory = True
@@ -64,10 +77,10 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
rebuild_tree("Account", "parent_account")
frappe.local.flags.ignore_update_nsm = False
+
def add_suffix_if_duplicate(account_name, account_number, accounts):
if account_number:
- account_name_in_db = unidecode(" - ".join([account_number,
- account_name.strip().lower()]))
+ account_name_in_db = unidecode(" - ".join([account_number, account_name.strip().lower()]))
else:
account_name_in_db = unidecode(account_name.strip().lower())
@@ -77,16 +90,21 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
return account_name, account_name_in_db
+
def identify_is_group(child):
if child.get("is_group"):
is_group = child.get("is_group")
- elif len(set(child.keys()) - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])):
+ elif len(
+ set(child.keys())
+ - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])
+ ):
is_group = 1
else:
is_group = 0
return is_group
+
def get_chart(chart_template, existing_company=None):
chart = {}
if existing_company:
@@ -96,11 +114,13 @@ def get_chart(chart_template, existing_company=None):
from erpnext.accounts.doctype.account.chart_of_accounts.verified import (
standard_chart_of_accounts,
)
+
return standard_chart_of_accounts.get()
elif chart_template == "Standard with Numbers":
from erpnext.accounts.doctype.account.chart_of_accounts.verified import (
standard_chart_of_accounts_with_account_number,
)
+
return standard_chart_of_accounts_with_account_number.get()
else:
folders = ("verified",)
@@ -116,6 +136,7 @@ def get_chart(chart_template, existing_company=None):
if chart and json.loads(chart).get("name") == chart_template:
return json.loads(chart).get("tree")
+
@frappe.whitelist()
def get_charts_for_country(country, with_standard=False):
charts = []
@@ -123,9 +144,10 @@ def get_charts_for_country(country, with_standard=False):
def _get_chart_name(content):
if content:
content = json.loads(content)
- if (content and content.get("disabled", "No") == "No") \
- or frappe.local.flags.allow_unverified_charts:
- charts.append(content["name"])
+ if (
+ content and content.get("disabled", "No") == "No"
+ ) or frappe.local.flags.allow_unverified_charts:
+ charts.append(content["name"])
country_code = frappe.db.get_value("Country", country, "code")
if country_code:
@@ -152,11 +174,21 @@ def get_charts_for_country(country, with_standard=False):
def get_account_tree_from_existing_company(existing_company):
- all_accounts = frappe.get_all('Account',
- filters={'company': existing_company},
- fields = ["name", "account_name", "parent_account", "account_type",
- "is_group", "root_type", "tax_rate", "account_number"],
- order_by="lft, rgt")
+ all_accounts = frappe.get_all(
+ "Account",
+ filters={"company": existing_company},
+ fields=[
+ "name",
+ "account_name",
+ "parent_account",
+ "account_type",
+ "is_group",
+ "root_type",
+ "tax_rate",
+ "account_number",
+ ],
+ order_by="lft, rgt",
+ )
account_tree = {}
@@ -165,6 +197,7 @@ def get_account_tree_from_existing_company(existing_company):
build_account_tree(account_tree, None, all_accounts)
return account_tree
+
def build_account_tree(tree, parent, all_accounts):
# find children
parent_account = parent.name if parent else ""
@@ -193,27 +226,29 @@ def build_account_tree(tree, parent, all_accounts):
# call recursively to build a subtree for current account
build_account_tree(tree[child.account_name], child, all_accounts)
+
@frappe.whitelist()
def validate_bank_account(coa, bank_account):
accounts = []
chart = get_chart(coa)
if chart:
+
def _get_account_names(account_master):
for account_name, child in iteritems(account_master):
- if account_name not in ["account_number", "account_type",
- "root_type", "is_group", "tax_rate"]:
+ if account_name not in ["account_number", "account_type", "root_type", "is_group", "tax_rate"]:
accounts.append(account_name)
_get_account_names(child)
_get_account_names(chart)
- return (bank_account in accounts)
+ return bank_account in accounts
+
@frappe.whitelist()
def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False):
- ''' get chart template from its folder and parse the json to be rendered as tree '''
+ """get chart template from its folder and parse the json to be rendered as tree"""
chart = chart_data or get_chart(chart_template)
# if no template selected, return as it is
@@ -221,22 +256,33 @@ def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=Fals
return
accounts = []
+
def _import_accounts(children, parent):
- ''' recursively called to form a parent-child based list of dict from chart template '''
+ """recursively called to form a parent-child based list of dict from chart template"""
for account_name, child in iteritems(children):
account = {}
- if account_name in ["account_name", "account_number", "account_type",\
- "root_type", "is_group", "tax_rate"]: continue
+ if account_name in [
+ "account_name",
+ "account_number",
+ "account_type",
+ "root_type",
+ "is_group",
+ "tax_rate",
+ ]:
+ continue
if from_coa_importer:
- account_name = child['account_name']
+ account_name = child["account_name"]
- account['parent_account'] = parent
- account['expandable'] = True if identify_is_group(child) else False
- account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \
- if child.get('account_number') else account_name
+ account["parent_account"] = parent
+ account["expandable"] = True if identify_is_group(child) else False
+ account["value"] = (
+ (cstr(child.get("account_number")).strip() + " - " + account_name)
+ if child.get("account_number")
+ else account_name
+ )
accounts.append(account)
- _import_accounts(child, account['value'])
+ _import_accounts(child, account["value"])
_import_accounts(chart, None)
return accounts
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py b/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py
index 7d94c89ad7b..562b00fd000 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/import_from_openerp.py
@@ -21,6 +21,7 @@ charts = {}
all_account_types = []
all_roots = {}
+
def go():
global accounts, charts
default_account_types = get_default_account_types()
@@ -35,14 +36,16 @@ def go():
accounts, charts = {}, {}
country_path = os.path.join(path, country_dir)
manifest = ast.literal_eval(open(os.path.join(country_path, "__openerp__.py")).read())
- data_files = manifest.get("data", []) + manifest.get("init_xml", []) + \
- manifest.get("update_xml", [])
+ data_files = (
+ manifest.get("data", []) + manifest.get("init_xml", []) + manifest.get("update_xml", [])
+ )
files_path = [os.path.join(country_path, d) for d in data_files]
xml_roots = get_xml_roots(files_path)
csv_content = get_csv_contents(files_path)
prefix = country_dir if csv_content else None
- account_types = get_account_types(xml_roots.get("account.account.type", []),
- csv_content.get("account.account.type", []), prefix)
+ account_types = get_account_types(
+ xml_roots.get("account.account.type", []), csv_content.get("account.account.type", []), prefix
+ )
account_types.update(default_account_types)
if xml_roots:
@@ -55,12 +58,15 @@ def go():
create_all_roots_file()
+
def get_default_account_types():
default_types_root = []
- default_types_root.append(ET.parse(os.path.join(path, "account", "data",
- "data_account_type.xml")).getroot())
+ default_types_root.append(
+ ET.parse(os.path.join(path, "account", "data", "data_account_type.xml")).getroot()
+ )
return get_account_types(default_types_root, None, prefix="account")
+
def get_xml_roots(files_path):
xml_roots = frappe._dict()
for filepath in files_path:
@@ -69,64 +75,69 @@ def get_xml_roots(files_path):
tree = ET.parse(filepath)
root = tree.getroot()
for node in root[0].findall("record"):
- if node.get("model") in ["account.account.template",
- "account.chart.template", "account.account.type"]:
+ if node.get("model") in [
+ "account.account.template",
+ "account.chart.template",
+ "account.account.type",
+ ]:
xml_roots.setdefault(node.get("model"), []).append(root)
break
return xml_roots
+
def get_csv_contents(files_path):
csv_content = {}
for filepath in files_path:
fname = os.path.basename(filepath)
- for file_type in ["account.account.template", "account.account.type",
- "account.chart.template"]:
+ for file_type in ["account.account.template", "account.account.type", "account.chart.template"]:
if fname.startswith(file_type) and fname.endswith(".csv"):
with open(filepath, "r") as csvfile:
try:
- csv_content.setdefault(file_type, [])\
- .append(read_csv_content(csvfile.read()))
+ csv_content.setdefault(file_type, []).append(read_csv_content(csvfile.read()))
except Exception as e:
continue
return csv_content
+
def get_account_types(root_list, csv_content, prefix=None):
types = {}
account_type_map = {
- 'cash': 'Cash',
- 'bank': 'Bank',
- 'tr_cash': 'Cash',
- 'tr_bank': 'Bank',
- 'receivable': 'Receivable',
- 'tr_receivable': 'Receivable',
- 'account rec': 'Receivable',
- 'payable': 'Payable',
- 'tr_payable': 'Payable',
- 'equity': 'Equity',
- 'stocks': 'Stock',
- 'stock': 'Stock',
- 'tax': 'Tax',
- 'tr_tax': 'Tax',
- 'tax-out': 'Tax',
- 'tax-in': 'Tax',
- 'charges_personnel': 'Chargeable',
- 'fixed asset': 'Fixed Asset',
- 'cogs': 'Cost of Goods Sold',
-
+ "cash": "Cash",
+ "bank": "Bank",
+ "tr_cash": "Cash",
+ "tr_bank": "Bank",
+ "receivable": "Receivable",
+ "tr_receivable": "Receivable",
+ "account rec": "Receivable",
+ "payable": "Payable",
+ "tr_payable": "Payable",
+ "equity": "Equity",
+ "stocks": "Stock",
+ "stock": "Stock",
+ "tax": "Tax",
+ "tr_tax": "Tax",
+ "tax-out": "Tax",
+ "tax-in": "Tax",
+ "charges_personnel": "Chargeable",
+ "fixed asset": "Fixed Asset",
+ "cogs": "Cost of Goods Sold",
}
for root in root_list:
for node in root[0].findall("record"):
- if node.get("model")=="account.account.type":
+ if node.get("model") == "account.account.type":
data = {}
for field in node.findall("field"):
- if field.get("name")=="code" and field.text.lower() != "none" \
- and account_type_map.get(field.text):
- data["account_type"] = account_type_map[field.text]
+ if (
+ field.get("name") == "code"
+ and field.text.lower() != "none"
+ and account_type_map.get(field.text)
+ ):
+ data["account_type"] = account_type_map[field.text]
node_id = prefix + "." + node.get("id") if prefix else node.get("id")
types[node_id] = data
- if csv_content and csv_content[0][0]=="id":
+ if csv_content and csv_content[0][0] == "id":
for row in csv_content[1:]:
row_dict = dict(zip(csv_content[0], row))
data = {}
@@ -137,21 +148,22 @@ def get_account_types(root_list, csv_content, prefix=None):
types[node_id] = data
return types
+
def make_maps_for_xml(xml_roots, account_types, country_dir):
"""make maps for `charts` and `accounts`"""
for model, root_list in iteritems(xml_roots):
for root in root_list:
for node in root[0].findall("record"):
- if node.get("model")=="account.account.template":
+ if node.get("model") == "account.account.template":
data = {}
for field in node.findall("field"):
- if field.get("name")=="name":
+ if field.get("name") == "name":
data["name"] = field.text
- if field.get("name")=="parent_id":
+ if field.get("name") == "parent_id":
parent_id = field.get("ref") or field.get("eval")
data["parent_id"] = parent_id
- if field.get("name")=="user_type":
+ if field.get("name") == "user_type":
value = field.get("ref")
if account_types.get(value, {}).get("account_type"):
data["account_type"] = account_types[value]["account_type"]
@@ -161,16 +173,17 @@ def make_maps_for_xml(xml_roots, account_types, country_dir):
data["children"] = []
accounts[node.get("id")] = data
- if node.get("model")=="account.chart.template":
+ if node.get("model") == "account.chart.template":
data = {}
for field in node.findall("field"):
- if field.get("name")=="name":
+ if field.get("name") == "name":
data["name"] = field.text
- if field.get("name")=="account_root_id":
+ if field.get("name") == "account_root_id":
data["account_root_id"] = field.get("ref")
data["id"] = country_dir
charts.setdefault(node.get("id"), {}).update(data)
+
def make_maps_for_csv(csv_content, account_types, country_dir):
for content in csv_content.get("account.account.template", []):
for row in content[1:]:
@@ -178,7 +191,7 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
account = {
"name": data.get("name"),
"parent_id": data.get("parent_id:id") or data.get("parent_id/id"),
- "children": []
+ "children": [],
}
user_type = data.get("user_type/id") or data.get("user_type:id")
if account_types.get(user_type, {}).get("account_type"):
@@ -195,12 +208,14 @@ def make_maps_for_csv(csv_content, account_types, country_dir):
for row in content[1:]:
if row:
data = dict(zip(content[0], row))
- charts.setdefault(data.get("id"), {}).update({
- "account_root_id": data.get("account_root_id:id") or \
- data.get("account_root_id/id"),
- "name": data.get("name"),
- "id": country_dir
- })
+ charts.setdefault(data.get("id"), {}).update(
+ {
+ "account_root_id": data.get("account_root_id:id") or data.get("account_root_id/id"),
+ "name": data.get("name"),
+ "id": country_dir,
+ }
+ )
+
def make_account_trees():
"""build tree hierarchy"""
@@ -219,6 +234,7 @@ def make_account_trees():
if "children" in accounts[id] and not accounts[id].get("children"):
del accounts[id]["children"]
+
def make_charts():
"""write chart files in app/setup/doctype/company/charts"""
for chart_id in charts:
@@ -237,34 +253,38 @@ def make_charts():
chart["country_code"] = src["id"][5:]
chart["tree"] = accounts[src["account_root_id"]]
-
for key, val in chart["tree"].items():
if key in ["name", "parent_id"]:
chart["tree"].pop(key)
if type(val) == dict:
val["root_type"] = ""
if chart:
- fpath = os.path.join("erpnext", "erpnext", "accounts", "doctype", "account",
- "chart_of_accounts", filename + ".json")
+ fpath = os.path.join(
+ "erpnext", "erpnext", "accounts", "doctype", "account", "chart_of_accounts", filename + ".json"
+ )
with open(fpath, "r") as chartfile:
old_content = chartfile.read()
- if not old_content or (json.loads(old_content).get("is_active", "No") == "No" \
- and json.loads(old_content).get("disabled", "No") == "No"):
+ if not old_content or (
+ json.loads(old_content).get("is_active", "No") == "No"
+ and json.loads(old_content).get("disabled", "No") == "No"
+ ):
with open(fpath, "w") as chartfile:
chartfile.write(json.dumps(chart, indent=4, sort_keys=True))
all_roots.setdefault(filename, chart["tree"].keys())
+
def create_all_roots_file():
- with open('all_roots.txt', 'w') as f:
+ with open("all_roots.txt", "w") as f:
for filename, roots in sorted(all_roots.items()):
f.write(filename)
- f.write('\n----------------------\n')
+ f.write("\n----------------------\n")
for r in sorted(roots):
- f.write(r.encode('utf-8'))
- f.write('\n')
- f.write('\n\n\n')
+ f.write(r.encode("utf-8"))
+ f.write("\n")
+ f.write("\n\n\n")
-if __name__=="__main__":
+
+if __name__ == "__main__":
go()
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
index 9248ffa6e57..e30ad24a374 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts.py
@@ -7,182 +7,103 @@ from frappe import _
def get():
return {
- _("Application of Funds (Assets)"): {
- _("Current Assets"): {
- _("Accounts Receivable"): {
- _("Debtors"): {
- "account_type": "Receivable"
- }
- },
- _("Bank Accounts"): {
- "account_type": "Bank",
- "is_group": 1
- },
- _("Cash In Hand"): {
- _("Cash"): {
- "account_type": "Cash"
- },
- "account_type": "Cash"
- },
- _("Loans and Advances (Assets)"): {
- _("Employee Advances"): {
- },
- },
- _("Securities and Deposits"): {
- _("Earnest Money"): {}
- },
- _("Stock Assets"): {
- _("Stock In Hand"): {
- "account_type": "Stock"
- },
- "account_type": "Stock",
- },
- _("Tax Assets"): {
- "is_group": 1
- }
- },
- _("Fixed Assets"): {
- _("Capital Equipments"): {
- "account_type": "Fixed Asset"
- },
- _("Electronic Equipments"): {
- "account_type": "Fixed Asset"
- },
- _("Furnitures and Fixtures"): {
- "account_type": "Fixed Asset"
- },
- _("Office Equipments"): {
- "account_type": "Fixed Asset"
- },
- _("Plants and Machineries"): {
- "account_type": "Fixed Asset"
- },
- _("Buildings"): {
- "account_type": "Fixed Asset"
+ _("Application of Funds (Assets)"): {
+ _("Current Assets"): {
+ _("Accounts Receivable"): {_("Debtors"): {"account_type": "Receivable"}},
+ _("Bank Accounts"): {"account_type": "Bank", "is_group": 1},
+ _("Cash In Hand"): {_("Cash"): {"account_type": "Cash"}, "account_type": "Cash"},
+ _("Loans and Advances (Assets)"): {
+ _("Employee Advances"): {},
},
- _("Softwares"): {
- "account_type": "Fixed Asset"
+ _("Securities and Deposits"): {_("Earnest Money"): {}},
+ _("Stock Assets"): {
+ _("Stock In Hand"): {"account_type": "Stock"},
+ "account_type": "Stock",
},
- _("Accumulated Depreciation"): {
- "account_type": "Accumulated Depreciation"
- },
- _("CWIP Account"): {
- "account_type": "Capital Work in Progress",
- }
- },
- _("Investments"): {
- "is_group": 1
- },
- _("Temporary Accounts"): {
- _("Temporary Opening"): {
- "account_type": "Temporary"
- }
- },
- "root_type": "Asset"
- },
- _("Expenses"): {
- _("Direct Expenses"): {
- _("Stock Expenses"): {
- _("Cost of Goods Sold"): {
- "account_type": "Cost of Goods Sold"
- },
- _("Expenses Included In Asset Valuation"): {
- "account_type": "Expenses Included In Asset Valuation"
- },
- _("Expenses Included In Valuation"): {
- "account_type": "Expenses Included In Valuation"
- },
- _("Stock Adjustment"): {
- "account_type": "Stock Adjustment"
- }
- },
- },
- _("Indirect Expenses"): {
- _("Administrative Expenses"): {},
- _("Commission on Sales"): {},
- _("Depreciation"): {
- "account_type": "Depreciation"
- },
- _("Entertainment Expenses"): {},
- _("Freight and Forwarding Charges"): {
- "account_type": "Chargeable"
- },
- _("Legal Expenses"): {},
- _("Marketing Expenses"): {
- "account_type": "Chargeable"
- },
- _("Miscellaneous Expenses"): {
- "account_type": "Chargeable"
- },
- _("Office Maintenance Expenses"): {},
- _("Office Rent"): {},
- _("Postal Expenses"): {},
- _("Print and Stationery"): {},
- _("Round Off"): {
- "account_type": "Round Off"
- },
- _("Salary"): {},
- _("Sales Expenses"): {},
- _("Telephone Expenses"): {},
- _("Travel Expenses"): {},
- _("Utility Expenses"): {},
+ _("Tax Assets"): {"is_group": 1},
+ },
+ _("Fixed Assets"): {
+ _("Capital Equipments"): {"account_type": "Fixed Asset"},
+ _("Electronic Equipments"): {"account_type": "Fixed Asset"},
+ _("Furnitures and Fixtures"): {"account_type": "Fixed Asset"},
+ _("Office Equipments"): {"account_type": "Fixed Asset"},
+ _("Plants and Machineries"): {"account_type": "Fixed Asset"},
+ _("Buildings"): {"account_type": "Fixed Asset"},
+ _("Softwares"): {"account_type": "Fixed Asset"},
+ _("Accumulated Depreciation"): {"account_type": "Accumulated Depreciation"},
+ _("CWIP Account"): {
+ "account_type": "Capital Work in Progress",
+ },
+ },
+ _("Investments"): {"is_group": 1},
+ _("Temporary Accounts"): {_("Temporary Opening"): {"account_type": "Temporary"}},
+ "root_type": "Asset",
+ },
+ _("Expenses"): {
+ _("Direct Expenses"): {
+ _("Stock Expenses"): {
+ _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold"},
+ _("Expenses Included In Asset Valuation"): {
+ "account_type": "Expenses Included In Asset Valuation"
+ },
+ _("Expenses Included In Valuation"): {"account_type": "Expenses Included In Valuation"},
+ _("Stock Adjustment"): {"account_type": "Stock Adjustment"},
+ },
+ },
+ _("Indirect Expenses"): {
+ _("Administrative Expenses"): {},
+ _("Commission on Sales"): {},
+ _("Depreciation"): {"account_type": "Depreciation"},
+ _("Entertainment Expenses"): {},
+ _("Freight and Forwarding Charges"): {"account_type": "Chargeable"},
+ _("Legal Expenses"): {},
+ _("Marketing Expenses"): {"account_type": "Chargeable"},
+ _("Miscellaneous Expenses"): {"account_type": "Chargeable"},
+ _("Office Maintenance Expenses"): {},
+ _("Office Rent"): {},
+ _("Postal Expenses"): {},
+ _("Print and Stationery"): {},
+ _("Round Off"): {"account_type": "Round Off"},
+ _("Salary"): {},
+ _("Sales Expenses"): {},
+ _("Telephone Expenses"): {},
+ _("Travel Expenses"): {},
+ _("Utility Expenses"): {},
_("Write Off"): {},
_("Exchange Gain/Loss"): {},
- _("Gain/Loss on Asset Disposal"): {}
- },
- "root_type": "Expense"
- },
- _("Income"): {
- _("Direct Income"): {
- _("Sales"): {},
- _("Service"): {}
- },
- _("Indirect Income"): {
- "is_group": 1
- },
- "root_type": "Income"
- },
- _("Source of Funds (Liabilities)"): {
- _("Current Liabilities"): {
- _("Accounts Payable"): {
- _("Creditors"): {
- "account_type": "Payable"
- },
- _("Payroll Payable"): {},
- },
- _("Stock Liabilities"): {
- _("Stock Received But Not Billed"): {
- "account_type": "Stock Received But Not Billed"
- },
- _("Asset Received But Not Billed"): {
- "account_type": "Asset Received But Not Billed"
- }
- },
- _("Duties and Taxes"): {
- "account_type": "Tax",
- "is_group": 1
+ _("Gain/Loss on Asset Disposal"): {},
+ },
+ "root_type": "Expense",
+ },
+ _("Income"): {
+ _("Direct Income"): {_("Sales"): {}, _("Service"): {}},
+ _("Indirect Income"): {"is_group": 1},
+ "root_type": "Income",
+ },
+ _("Source of Funds (Liabilities)"): {
+ _("Current Liabilities"): {
+ _("Accounts Payable"): {
+ _("Creditors"): {"account_type": "Payable"},
+ _("Payroll Payable"): {},
},
+ _("Stock Liabilities"): {
+ _("Stock Received But Not Billed"): {"account_type": "Stock Received But Not Billed"},
+ _("Asset Received But Not Billed"): {"account_type": "Asset Received But Not Billed"},
+ },
+ _("Duties and Taxes"): {"account_type": "Tax", "is_group": 1},
_("Loans (Liabilities)"): {
_("Secured Loans"): {},
_("Unsecured Loans"): {},
_("Bank Overdraft Account"): {},
},
- },
- "root_type": "Liability"
- },
+ },
+ "root_type": "Liability",
+ },
_("Equity"): {
- _("Capital Stock"): {
- "account_type": "Equity"
- },
- _("Dividends Paid"): {
- "account_type": "Equity"
- },
- _("Opening Balance Equity"): {
- "account_type": "Equity"
- },
- _("Retained Earnings"): {
- "account_type": "Equity"
- },
- "root_type": "Equity"
- }
+ _("Capital Stock"): {"account_type": "Equity"},
+ _("Dividends Paid"): {"account_type": "Equity"},
+ _("Opening Balance Equity"): {"account_type": "Equity"},
+ _("Retained Earnings"): {"account_type": "Equity"},
+ "root_type": "Equity",
+ },
}
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
index 31ae17189a7..0e46f1e08a5 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
@@ -6,288 +6,153 @@ from frappe import _
def get():
- return {
- _("Application of Funds (Assets)"): {
- _("Current Assets"): {
- _("Accounts Receivable"): {
- _("Debtors"): {
- "account_type": "Receivable",
- "account_number": "1310"
- },
- "account_number": "1300"
- },
- _("Bank Accounts"): {
- "account_type": "Bank",
- "is_group": 1,
- "account_number": "1200"
- },
- _("Cash In Hand"): {
- _("Cash"): {
- "account_type": "Cash",
- "account_number": "1110"
- },
- "account_type": "Cash",
- "account_number": "1100"
- },
- _("Loans and Advances (Assets)"): {
- _("Employee Advances"): {
- "account_number": "1610"
- },
- "account_number": "1600"
- },
- _("Securities and Deposits"): {
- _("Earnest Money"): {
- "account_number": "1651"
- },
- "account_number": "1650"
- },
- _("Stock Assets"): {
- _("Stock In Hand"): {
- "account_type": "Stock",
- "account_number": "1410"
- },
- "account_type": "Stock",
- "account_number": "1400"
- },
- _("Tax Assets"): {
- "is_group": 1,
- "account_number": "1500"
- },
- "account_number": "1100-1600"
- },
- _("Fixed Assets"): {
- _("Capital Equipments"): {
- "account_type": "Fixed Asset",
- "account_number": "1710"
- },
- _("Electronic Equipments"): {
- "account_type": "Fixed Asset",
- "account_number": "1720"
- },
- _("Furnitures and Fixtures"): {
- "account_type": "Fixed Asset",
- "account_number": "1730"
- },
- _("Office Equipments"): {
- "account_type": "Fixed Asset",
- "account_number": "1740"
- },
- _("Plants and Machineries"): {
- "account_type": "Fixed Asset",
- "account_number": "1750"
- },
- _("Buildings"): {
- "account_type": "Fixed Asset",
- "account_number": "1760"
- },
- _("Softwares"): {
- "account_type": "Fixed Asset",
- "account_number": "1770"
- },
- _("Accumulated Depreciation"): {
- "account_type": "Accumulated Depreciation",
- "account_number": "1780"
- },
- _("CWIP Account"): {
- "account_type": "Capital Work in Progress",
- "account_number": "1790"
- },
- "account_number": "1700"
- },
- _("Investments"): {
- "is_group": 1,
- "account_number": "1800"
- },
- _("Temporary Accounts"): {
- _("Temporary Opening"): {
- "account_type": "Temporary",
- "account_number": "1910"
- },
- "account_number": "1900"
- },
- "root_type": "Asset",
- "account_number": "1000"
- },
- _("Expenses"): {
- _("Direct Expenses"): {
- _("Stock Expenses"): {
- _("Cost of Goods Sold"): {
- "account_type": "Cost of Goods Sold",
- "account_number": "5111"
- },
- _("Expenses Included In Asset Valuation"): {
- "account_type": "Expenses Included In Asset Valuation",
- "account_number": "5112"
- },
- _("Expenses Included In Valuation"): {
- "account_type": "Expenses Included In Valuation",
- "account_number": "5118"
- },
- _("Stock Adjustment"): {
- "account_type": "Stock Adjustment",
- "account_number": "5119"
- },
- "account_number": "5110"
- },
- "account_number": "5100"
- },
- _("Indirect Expenses"): {
- _("Administrative Expenses"): {
- "account_number": "5201"
- },
- _("Commission on Sales"): {
- "account_number": "5202"
- },
- _("Depreciation"): {
- "account_type": "Depreciation",
- "account_number": "5203"
- },
- _("Entertainment Expenses"): {
- "account_number": "5204"
- },
- _("Freight and Forwarding Charges"): {
- "account_type": "Chargeable",
- "account_number": "5205"
- },
- _("Legal Expenses"): {
- "account_number": "5206"
- },
- _("Marketing Expenses"): {
- "account_type": "Chargeable",
- "account_number": "5207"
- },
- _("Office Maintenance Expenses"): {
- "account_number": "5208"
- },
- _("Office Rent"): {
- "account_number": "5209"
- },
- _("Postal Expenses"): {
- "account_number": "5210"
- },
- _("Print and Stationery"): {
- "account_number": "5211"
- },
- _("Round Off"): {
- "account_type": "Round Off",
- "account_number": "5212"
- },
- _("Salary"): {
- "account_number": "5213"
- },
- _("Sales Expenses"): {
- "account_number": "5214"
- },
- _("Telephone Expenses"): {
- "account_number": "5215"
- },
- _("Travel Expenses"): {
- "account_number": "5216"
- },
- _("Utility Expenses"): {
- "account_number": "5217"
- },
- _("Write Off"): {
- "account_number": "5218"
- },
- _("Exchange Gain/Loss"): {
- "account_number": "5219"
- },
- _("Gain/Loss on Asset Disposal"): {
- "account_number": "5220"
- },
- _("Miscellaneous Expenses"): {
- "account_type": "Chargeable",
- "account_number": "5221"
- },
- "account_number": "5200"
- },
- "root_type": "Expense",
- "account_number": "5000"
- },
- _("Income"): {
- _("Direct Income"): {
- _("Sales"): {
- "account_number": "4110"
- },
- _("Service"): {
- "account_number": "4120"
- },
- "account_number": "4100"
- },
- _("Indirect Income"): {
- "is_group": 1,
- "account_number": "4200"
- },
- "root_type": "Income",
- "account_number": "4000"
- },
- _("Source of Funds (Liabilities)"): {
- _("Current Liabilities"): {
- _("Accounts Payable"): {
- _("Creditors"): {
- "account_type": "Payable",
- "account_number": "2110"
- },
- _("Payroll Payable"): {
- "account_number": "2120"
- },
- "account_number": "2100"
- },
- _("Stock Liabilities"): {
- _("Stock Received But Not Billed"): {
- "account_type": "Stock Received But Not Billed",
- "account_number": "2210"
- },
- _("Asset Received But Not Billed"): {
- "account_type": "Asset Received But Not Billed",
- "account_number": "2211"
- },
- "account_number": "2200"
- },
- _("Duties and Taxes"): {
- _("TDS Payable"): {
- "account_number": "2310"
- },
- "account_type": "Tax",
- "is_group": 1,
- "account_number": "2300"
- },
- _("Loans (Liabilities)"): {
- _("Secured Loans"): {
- "account_number": "2410"
- },
- _("Unsecured Loans"): {
- "account_number": "2420"
- },
- _("Bank Overdraft Account"): {
- "account_number": "2430"
- },
- "account_number": "2400"
- },
- "account_number": "2100-2400"
- },
- "root_type": "Liability",
- "account_number": "2000"
- },
- _("Equity"): {
- _("Capital Stock"): {
- "account_type": "Equity",
- "account_number": "3100"
- },
- _("Dividends Paid"): {
- "account_type": "Equity",
- "account_number": "3200"
- },
- _("Opening Balance Equity"): {
- "account_type": "Equity",
- "account_number": "3300"
- },
- _("Retained Earnings"): {
- "account_type": "Equity",
- "account_number": "3400"
- },
- "root_type": "Equity",
- "account_number": "3000"
- }
- }
+ return {
+ _("Application of Funds (Assets)"): {
+ _("Current Assets"): {
+ _("Accounts Receivable"): {
+ _("Debtors"): {"account_type": "Receivable", "account_number": "1310"},
+ "account_number": "1300",
+ },
+ _("Bank Accounts"): {"account_type": "Bank", "is_group": 1, "account_number": "1200"},
+ _("Cash In Hand"): {
+ _("Cash"): {"account_type": "Cash", "account_number": "1110"},
+ "account_type": "Cash",
+ "account_number": "1100",
+ },
+ _("Loans and Advances (Assets)"): {
+ _("Employee Advances"): {"account_number": "1610"},
+ "account_number": "1600",
+ },
+ _("Securities and Deposits"): {
+ _("Earnest Money"): {"account_number": "1651"},
+ "account_number": "1650",
+ },
+ _("Stock Assets"): {
+ _("Stock In Hand"): {"account_type": "Stock", "account_number": "1410"},
+ "account_type": "Stock",
+ "account_number": "1400",
+ },
+ _("Tax Assets"): {"is_group": 1, "account_number": "1500"},
+ "account_number": "1100-1600",
+ },
+ _("Fixed Assets"): {
+ _("Capital Equipments"): {"account_type": "Fixed Asset", "account_number": "1710"},
+ _("Electronic Equipments"): {"account_type": "Fixed Asset", "account_number": "1720"},
+ _("Furnitures and Fixtures"): {"account_type": "Fixed Asset", "account_number": "1730"},
+ _("Office Equipments"): {"account_type": "Fixed Asset", "account_number": "1740"},
+ _("Plants and Machineries"): {"account_type": "Fixed Asset", "account_number": "1750"},
+ _("Buildings"): {"account_type": "Fixed Asset", "account_number": "1760"},
+ _("Softwares"): {"account_type": "Fixed Asset", "account_number": "1770"},
+ _("Accumulated Depreciation"): {
+ "account_type": "Accumulated Depreciation",
+ "account_number": "1780",
+ },
+ _("CWIP Account"): {"account_type": "Capital Work in Progress", "account_number": "1790"},
+ "account_number": "1700",
+ },
+ _("Investments"): {"is_group": 1, "account_number": "1800"},
+ _("Temporary Accounts"): {
+ _("Temporary Opening"): {"account_type": "Temporary", "account_number": "1910"},
+ "account_number": "1900",
+ },
+ "root_type": "Asset",
+ "account_number": "1000",
+ },
+ _("Expenses"): {
+ _("Direct Expenses"): {
+ _("Stock Expenses"): {
+ _("Cost of Goods Sold"): {"account_type": "Cost of Goods Sold", "account_number": "5111"},
+ _("Expenses Included In Asset Valuation"): {
+ "account_type": "Expenses Included In Asset Valuation",
+ "account_number": "5112",
+ },
+ _("Expenses Included In Valuation"): {
+ "account_type": "Expenses Included In Valuation",
+ "account_number": "5118",
+ },
+ _("Stock Adjustment"): {"account_type": "Stock Adjustment", "account_number": "5119"},
+ "account_number": "5110",
+ },
+ "account_number": "5100",
+ },
+ _("Indirect Expenses"): {
+ _("Administrative Expenses"): {"account_number": "5201"},
+ _("Commission on Sales"): {"account_number": "5202"},
+ _("Depreciation"): {"account_type": "Depreciation", "account_number": "5203"},
+ _("Entertainment Expenses"): {"account_number": "5204"},
+ _("Freight and Forwarding Charges"): {"account_type": "Chargeable", "account_number": "5205"},
+ _("Legal Expenses"): {"account_number": "5206"},
+ _("Marketing Expenses"): {"account_type": "Chargeable", "account_number": "5207"},
+ _("Office Maintenance Expenses"): {"account_number": "5208"},
+ _("Office Rent"): {"account_number": "5209"},
+ _("Postal Expenses"): {"account_number": "5210"},
+ _("Print and Stationery"): {"account_number": "5211"},
+ _("Round Off"): {"account_type": "Round Off", "account_number": "5212"},
+ _("Salary"): {"account_number": "5213"},
+ _("Sales Expenses"): {"account_number": "5214"},
+ _("Telephone Expenses"): {"account_number": "5215"},
+ _("Travel Expenses"): {"account_number": "5216"},
+ _("Utility Expenses"): {"account_number": "5217"},
+ _("Write Off"): {"account_number": "5218"},
+ _("Exchange Gain/Loss"): {"account_number": "5219"},
+ _("Gain/Loss on Asset Disposal"): {"account_number": "5220"},
+ _("Miscellaneous Expenses"): {"account_type": "Chargeable", "account_number": "5221"},
+ "account_number": "5200",
+ },
+ "root_type": "Expense",
+ "account_number": "5000",
+ },
+ _("Income"): {
+ _("Direct Income"): {
+ _("Sales"): {"account_number": "4110"},
+ _("Service"): {"account_number": "4120"},
+ "account_number": "4100",
+ },
+ _("Indirect Income"): {"is_group": 1, "account_number": "4200"},
+ "root_type": "Income",
+ "account_number": "4000",
+ },
+ _("Source of Funds (Liabilities)"): {
+ _("Current Liabilities"): {
+ _("Accounts Payable"): {
+ _("Creditors"): {"account_type": "Payable", "account_number": "2110"},
+ _("Payroll Payable"): {"account_number": "2120"},
+ "account_number": "2100",
+ },
+ _("Stock Liabilities"): {
+ _("Stock Received But Not Billed"): {
+ "account_type": "Stock Received But Not Billed",
+ "account_number": "2210",
+ },
+ _("Asset Received But Not Billed"): {
+ "account_type": "Asset Received But Not Billed",
+ "account_number": "2211",
+ },
+ "account_number": "2200",
+ },
+ _("Duties and Taxes"): {
+ _("TDS Payable"): {"account_number": "2310"},
+ "account_type": "Tax",
+ "is_group": 1,
+ "account_number": "2300",
+ },
+ _("Loans (Liabilities)"): {
+ _("Secured Loans"): {"account_number": "2410"},
+ _("Unsecured Loans"): {"account_number": "2420"},
+ _("Bank Overdraft Account"): {"account_number": "2430"},
+ "account_number": "2400",
+ },
+ "account_number": "2100-2400",
+ },
+ "root_type": "Liability",
+ "account_number": "2000",
+ },
+ _("Equity"): {
+ _("Capital Stock"): {"account_type": "Equity", "account_number": "3100"},
+ _("Dividends Paid"): {"account_type": "Equity", "account_number": "3200"},
+ _("Opening Balance Equity"): {"account_type": "Equity", "account_number": "3300"},
+ _("Retained Earnings"): {"account_type": "Equity", "account_number": "3400"},
+ "root_type": "Equity",
+ "account_number": "3000",
+ },
+ }
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index 0715823b300..f9c9173af08 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -20,8 +20,9 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company"
acc.insert()
- account_number, account_name = frappe.db.get_value("Account", "1210 - Debtors - _TC",
- ["account_number", "account_name"])
+ account_number, account_name = frappe.db.get_value(
+ "Account", "1210 - Debtors - _TC", ["account_number", "account_name"]
+ )
self.assertEqual(account_number, "1210")
self.assertEqual(account_name, "Debtors")
@@ -30,8 +31,12 @@ class TestAccount(unittest.TestCase):
update_account_number("1210 - Debtors - _TC", new_account_name, new_account_number)
- new_acc = frappe.db.get_value("Account", "1211-11-4 - 6 - - Debtors 1 - Test - - _TC",
- ["account_name", "account_number"], as_dict=1)
+ new_acc = frappe.db.get_value(
+ "Account",
+ "1211-11-4 - 6 - - Debtors 1 - Test - - _TC",
+ ["account_name", "account_number"],
+ as_dict=1,
+ )
self.assertEqual(new_acc.account_name, "Debtors 1 - Test -")
self.assertEqual(new_acc.account_number, "1211-11-4 - 6 -")
@@ -79,7 +84,9 @@ class TestAccount(unittest.TestCase):
self.assertEqual(parent, "Securities and Deposits - _TC")
- merge_account("Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company)
+ merge_account(
+ "Securities and Deposits - _TC", "Cash In Hand - _TC", doc.is_group, doc.root_type, doc.company
+ )
parent = frappe.db.get_value("Account", "Earnest Money - _TC", "parent_account")
# Parent account of the child account changes after merging
@@ -91,14 +98,28 @@ class TestAccount(unittest.TestCase):
doc = frappe.get_doc("Account", "Current Assets - _TC")
# Raise error as is_group property doesn't match
- self.assertRaises(frappe.ValidationError, merge_account, "Current Assets - _TC",\
- "Accumulated Depreciation - _TC", doc.is_group, doc.root_type, doc.company)
+ self.assertRaises(
+ frappe.ValidationError,
+ merge_account,
+ "Current Assets - _TC",
+ "Accumulated Depreciation - _TC",
+ doc.is_group,
+ doc.root_type,
+ doc.company,
+ )
doc = frappe.get_doc("Account", "Capital Stock - _TC")
# Raise error as root_type property doesn't match
- self.assertRaises(frappe.ValidationError, merge_account, "Capital Stock - _TC",\
- "Softwares - _TC", doc.is_group, doc.root_type, doc.company)
+ self.assertRaises(
+ frappe.ValidationError,
+ merge_account,
+ "Capital Stock - _TC",
+ "Softwares - _TC",
+ doc.is_group,
+ doc.root_type,
+ doc.company,
+ )
def test_account_sync(self):
frappe.local.flags.pop("ignore_root_company_validation", None)
@@ -109,8 +130,12 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company 3"
acc.insert()
- acc_tc_4 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 4"})
- acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Sync Account", "company": "_Test Company 5"})
+ acc_tc_4 = frappe.db.get_value(
+ "Account", {"account_name": "Test Sync Account", "company": "_Test Company 4"}
+ )
+ acc_tc_5 = frappe.db.get_value(
+ "Account", {"account_name": "Test Sync Account", "company": "_Test Company 5"}
+ )
self.assertEqual(acc_tc_4, "Test Sync Account - _TC4")
self.assertEqual(acc_tc_5, "Test Sync Account - _TC5")
@@ -138,8 +163,26 @@ class TestAccount(unittest.TestCase):
update_account_number(acc.name, "Test Rename Sync Account", "1234")
# Check if renamed in children
- self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 4", "account_number": "1234"}))
- self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 5", "account_number": "1234"}))
+ self.assertTrue(
+ frappe.db.exists(
+ "Account",
+ {
+ "account_name": "Test Rename Sync Account",
+ "company": "_Test Company 4",
+ "account_number": "1234",
+ },
+ )
+ )
+ self.assertTrue(
+ frappe.db.exists(
+ "Account",
+ {
+ "account_name": "Test Rename Sync Account",
+ "company": "_Test Company 5",
+ "account_number": "1234",
+ },
+ )
+ )
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC3")
frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4")
@@ -155,25 +198,71 @@ class TestAccount(unittest.TestCase):
acc.company = "_Test Company 3"
acc.insert()
- self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 4"}))
- self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 5"}))
+ self.assertTrue(
+ frappe.db.exists(
+ "Account", {"account_name": "Test Group Account", "company": "_Test Company 4"}
+ )
+ )
+ self.assertTrue(
+ frappe.db.exists(
+ "Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
+ )
+ )
# Try renaming child company account
- acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Group Account", "company": "_Test Company 5"})
- self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account")
+ acc_tc_5 = frappe.db.get_value(
+ "Account", {"account_name": "Test Group Account", "company": "_Test Company 5"}
+ )
+ self.assertRaises(
+ frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account"
+ )
# Rename child company account with allow_account_creation_against_child_company enabled
- frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1)
+ frappe.db.set_value(
+ "Company", "_Test Company 5", "allow_account_creation_against_child_company", 1
+ )
update_account_number(acc_tc_5, "Test Modified Account")
- self.assertTrue(frappe.db.exists("Account", {'name': "Test Modified Account - _TC5", "company": "_Test Company 5"}))
+ self.assertTrue(
+ frappe.db.exists(
+ "Account", {"name": "Test Modified Account - _TC5", "company": "_Test Company 5"}
+ )
+ )
- frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0)
+ frappe.db.set_value(
+ "Company", "_Test Company 5", "allow_account_creation_against_child_company", 0
+ )
- to_delete = ["Test Group Account - _TC3", "Test Group Account - _TC4", "Test Modified Account - _TC5"]
+ to_delete = [
+ "Test Group Account - _TC3",
+ "Test Group Account - _TC4",
+ "Test Modified Account - _TC5",
+ ]
for doc in to_delete:
frappe.delete_doc("Account", doc)
+ def test_validate_account_currency(self):
+ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
+
+ if not frappe.db.get_value("Account", "Test Currency Account - _TC"):
+ acc = frappe.new_doc("Account")
+ acc.account_name = "Test Currency Account"
+ acc.parent_account = "Tax Assets - _TC"
+ acc.company = "_Test Company"
+ acc.insert()
+ else:
+ acc = frappe.get_doc("Account", "Test Currency Account - _TC")
+
+ self.assertEqual(acc.account_currency, "INR")
+
+ # Make a JV against this account
+ make_journal_entry(
+ "Test Currency Account - _TC", "Miscellaneous Expenses - _TC", 100, submit=True
+ )
+
+ acc.account_currency = "USD"
+ self.assertRaises(frappe.ValidationError, acc.save)
+
def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects
@@ -184,20 +273,16 @@ def _make_test_records(verbose=None):
["_Test Bank USD", "Bank Accounts", 0, "Bank", "USD"],
["_Test Bank EUR", "Bank Accounts", 0, "Bank", "EUR"],
["_Test Cash", "Cash In Hand", 0, "Cash", None],
-
["_Test Account Stock Expenses", "Direct Expenses", 1, None, None],
["_Test Account Shipping Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Customs Duty", "_Test Account Stock Expenses", 0, "Tax", None],
["_Test Account Insurance Charges", "_Test Account Stock Expenses", 0, "Chargeable", None],
["_Test Account Stock Adjustment", "_Test Account Stock Expenses", 0, "Stock Adjustment", None],
["_Test Employee Advance", "Current Liabilities", 0, None, None],
-
["_Test Account Tax Assets", "Current Assets", 1, None, None],
["_Test Account VAT", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Service Tax", "_Test Account Tax Assets", 0, "Tax", None],
-
["_Test Account Reserves and Surplus", "Current Liabilities", 0, None, None],
-
["_Test Account Cost for Goods Sold", "Expenses", 0, None, None],
["_Test Account Excise Duty", "_Test Account Tax Assets", 0, "Tax", None],
["_Test Account Education Cess", "_Test Account Tax Assets", 0, "Tax", None],
@@ -206,38 +291,45 @@ def _make_test_records(verbose=None):
["_Test Account Discount", "Direct Expenses", 0, None, None],
["_Test Write Off", "Indirect Expenses", 0, None, None],
["_Test Exchange Gain/Loss", "Indirect Expenses", 0, None, None],
-
["_Test Account Sales", "Direct Income", 0, None, None],
-
# related to Account Inventory Integration
["_Test Account Stock In Hand", "Current Assets", 0, None, None],
-
# fixed asset depreciation
["_Test Fixed Asset", "Current Assets", 0, "Fixed Asset", None],
["_Test Accumulated Depreciations", "Current Assets", 0, "Accumulated Depreciation", None],
["_Test Depreciations", "Expenses", 0, None, None],
["_Test Gain/Loss on Asset Disposal", "Expenses", 0, None, None],
-
# Receivable / Payable Account
["_Test Receivable", "Current Assets", 0, "Receivable", None],
["_Test Payable", "Current Liabilities", 0, "Payable", None],
["_Test Receivable USD", "Current Assets", 0, "Receivable", "USD"],
- ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"]
+ ["_Test Payable USD", "Current Liabilities", 0, "Payable", "USD"],
]
- for company, abbr in [["_Test Company", "_TC"], ["_Test Company 1", "_TC1"], ["_Test Company with perpetual inventory", "TCP1"]]:
- test_objects = make_test_objects("Account", [{
- "doctype": "Account",
- "account_name": account_name,
- "parent_account": parent_account + " - " + abbr,
- "company": company,
- "is_group": is_group,
- "account_type": account_type,
- "account_currency": currency
- } for account_name, parent_account, is_group, account_type, currency in accounts])
+ for company, abbr in [
+ ["_Test Company", "_TC"],
+ ["_Test Company 1", "_TC1"],
+ ["_Test Company with perpetual inventory", "TCP1"],
+ ]:
+ test_objects = make_test_objects(
+ "Account",
+ [
+ {
+ "doctype": "Account",
+ "account_name": account_name,
+ "parent_account": parent_account + " - " + abbr,
+ "company": company,
+ "is_group": is_group,
+ "account_type": account_type,
+ "account_currency": currency,
+ }
+ for account_name, parent_account, is_group, account_type, currency in accounts
+ ],
+ )
return test_objects
+
def get_inventory_account(company, warehouse=None):
account = None
if warehouse:
@@ -247,19 +339,24 @@ def get_inventory_account(company, warehouse=None):
return account
+
def create_account(**kwargs):
- account = frappe.db.get_value("Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")})
+ account = frappe.db.get_value(
+ "Account", filters={"account_name": kwargs.get("account_name"), "company": kwargs.get("company")}
+ )
if account:
return account
else:
- account = frappe.get_doc(dict(
- doctype = "Account",
- account_name = kwargs.get('account_name'),
- account_type = kwargs.get('account_type'),
- parent_account = kwargs.get('parent_account'),
- company = kwargs.get('company'),
- account_currency = kwargs.get('account_currency')
- ))
+ account = frappe.get_doc(
+ dict(
+ doctype="Account",
+ account_name=kwargs.get("account_name"),
+ account_type=kwargs.get("account_type"),
+ parent_account=kwargs.get("parent_account"),
+ company=kwargs.get("company"),
+ account_currency=kwargs.get("account_currency"),
+ )
+ )
account.save()
return account.name
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index b6112e0cc57..ce1ed336d0c 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -17,13 +17,21 @@ class AccountingDimension(Document):
self.set_fieldname_and_label()
def validate(self):
- if self.document_type in core_doctypes_list + ('Accounting Dimension', 'Project',
- 'Cost Center', 'Accounting Dimension Detail', 'Company', 'Account') :
+ if self.document_type in core_doctypes_list + (
+ "Accounting Dimension",
+ "Project",
+ "Cost Center",
+ "Accounting Dimension Detail",
+ "Company",
+ "Account",
+ ):
msg = _("Not allowed to create accounting dimension for {0}").format(self.document_type)
frappe.throw(msg)
- exists = frappe.db.get_value("Accounting Dimension", {'document_type': self.document_type}, ['name'])
+ exists = frappe.db.get_value(
+ "Accounting Dimension", {"document_type": self.document_type}, ["name"]
+ )
if exists and self.is_new():
frappe.throw(_("Document Type already used as a dimension"))
@@ -42,13 +50,13 @@ class AccountingDimension(Document):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:
- frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue='long')
+ frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue="long")
def on_trash(self):
if frappe.flags.in_test:
delete_accounting_dimension(doc=self)
else:
- frappe.enqueue(delete_accounting_dimension, doc=self, queue='long')
+ frappe.enqueue(delete_accounting_dimension, doc=self, queue="long")
def set_fieldname_and_label(self):
if not self.label:
@@ -60,6 +68,7 @@ class AccountingDimension(Document):
def on_update(self):
frappe.flags.accounting_dimensions = None
+
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:
doclist = get_doctypes_with_dimensions()
@@ -70,9 +79,9 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
for doctype in doclist:
if (doc_count + 1) % 2 == 0:
- insert_after_field = 'dimension_col_break'
+ insert_after_field = "dimension_col_break"
else:
- insert_after_field = 'accounting_dimensions_section'
+ insert_after_field = "accounting_dimensions_section"
df = {
"fieldname": doc.fieldname,
@@ -80,30 +89,33 @@ def make_dimension_in_accounting_doctypes(doc, doclist=None):
"fieldtype": "Link",
"options": doc.document_type,
"insert_after": insert_after_field,
- "owner": "Administrator"
+ "owner": "Administrator",
}
meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
- if df['fieldname'] not in fieldnames:
+ if df["fieldname"] not in fieldnames:
if doctype == "Budget":
add_dimension_to_budget_doctype(df.copy(), doc)
else:
- create_custom_field(doctype, df)
+ create_custom_field(doctype, df, ignore_validate=True)
count += 1
- frappe.publish_progress(count*100/len(doclist), title = _("Creating Dimensions..."))
+ frappe.publish_progress(count * 100 / len(doclist), title=_("Creating Dimensions..."))
frappe.clear_cache(doctype=doctype)
-def add_dimension_to_budget_doctype(df, doc):
- df.update({
- "insert_after": "cost_center",
- "depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type)
- })
- create_custom_field("Budget", df)
+def add_dimension_to_budget_doctype(df, doc):
+ df.update(
+ {
+ "insert_after": "cost_center",
+ "depends_on": "eval:doc.budget_against == '{0}'".format(doc.document_type),
+ }
+ )
+
+ create_custom_field("Budget", df, ignore_validate=True)
property_setter = frappe.db.exists("Property Setter", "Budget-budget_against-options")
@@ -112,36 +124,44 @@ def add_dimension_to_budget_doctype(df, doc):
property_setter_doc.value = property_setter_doc.value + "\n" + doc.document_type
property_setter_doc.save()
- frappe.clear_cache(doctype='Budget')
+ frappe.clear_cache(doctype="Budget")
else:
- frappe.get_doc({
- "doctype": "Property Setter",
- "doctype_or_field": "DocField",
- "doc_type": "Budget",
- "field_name": "budget_against",
- "property": "options",
- "property_type": "Text",
- "value": "\nCost Center\nProject\n" + doc.document_type
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Property Setter",
+ "doctype_or_field": "DocField",
+ "doc_type": "Budget",
+ "field_name": "budget_against",
+ "property": "options",
+ "property_type": "Text",
+ "value": "\nCost Center\nProject\n" + doc.document_type,
+ }
+ ).insert(ignore_permissions=True)
def delete_accounting_dimension(doc):
doclist = get_doctypes_with_dimensions()
- frappe.db.sql("""
+ frappe.db.sql(
+ """
DELETE FROM `tabCustom Field`
WHERE fieldname = %s
- AND dt IN (%s)""" % #nosec
- ('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist))
+ AND dt IN (%s)"""
+ % ("%s", ", ".join(["%s"] * len(doclist))), # nosec
+ tuple([doc.fieldname] + doclist),
+ )
- frappe.db.sql("""
+ frappe.db.sql(
+ """
DELETE FROM `tabProperty Setter`
WHERE field_name = %s
- AND doc_type IN (%s)""" % #nosec
- ('%s', ', '.join(['%s']* len(doclist))), tuple([doc.fieldname] + doclist))
+ AND doc_type IN (%s)"""
+ % ("%s", ", ".join(["%s"] * len(doclist))), # nosec
+ tuple([doc.fieldname] + doclist),
+ )
budget_against_property = frappe.get_doc("Property Setter", "Budget-budget_against-options")
- value_list = budget_against_property.value.split('\n')[3:]
+ value_list = budget_against_property.value.split("\n")[3:]
if doc.document_type in value_list:
value_list.remove(doc.document_type)
@@ -152,6 +172,7 @@ def delete_accounting_dimension(doc):
for doctype in doclist:
frappe.clear_cache(doctype=doctype)
+
@frappe.whitelist()
def disable_dimension(doc):
if frappe.flags.in_test:
@@ -159,10 +180,11 @@ def disable_dimension(doc):
else:
frappe.enqueue(toggle_disabling, doc=doc)
+
def toggle_disabling(doc):
doc = json.loads(doc)
- if doc.get('disabled'):
+ if doc.get("disabled"):
df = {"read_only": 1}
else:
df = {"read_only": 0}
@@ -170,7 +192,7 @@ def toggle_disabling(doc):
doclist = get_doctypes_with_dimensions()
for doctype in doclist:
- field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": doc.get('fieldname')})
+ field = frappe.db.get_value("Custom Field", {"dt": doctype, "fieldname": doc.get("fieldname")})
if field:
custom_field = frappe.get_doc("Custom Field", field)
custom_field.update(df)
@@ -178,61 +200,82 @@ def toggle_disabling(doc):
frappe.clear_cache(doctype=doctype)
+
def get_doctypes_with_dimensions():
return frappe.get_hooks("accounting_dimension_doctypes")
-def get_accounting_dimensions(as_list=True):
+
+def get_accounting_dimensions(as_list=True, filters=None):
+
+ if not filters:
+ filters = {"disabled": 0}
+
if frappe.flags.accounting_dimensions is None:
- frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension",
- fields=["label", "fieldname", "disabled", "document_type"])
+ frappe.flags.accounting_dimensions = frappe.get_all(
+ "Accounting Dimension",
+ fields=["label", "fieldname", "disabled", "document_type"],
+ filters=filters,
+ )
if as_list:
return [d.fieldname for d in frappe.flags.accounting_dimensions]
else:
return frappe.flags.accounting_dimensions
+
def get_checks_for_pl_and_bs_accounts():
- dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
+ dimensions = frappe.db.sql(
+ """SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
FROM `tabAccounting Dimension`p ,`tabAccounting Dimension Detail` c
- WHERE p.name = c.parent""", as_dict=1)
+ WHERE p.name = c.parent""",
+ as_dict=1,
+ )
return dimensions
-def get_dimension_with_children(doctype, dimension):
- if isinstance(dimension, list):
- dimension = dimension[0]
+def get_dimension_with_children(doctype, dimensions):
+
+ if isinstance(dimensions, str):
+ dimensions = [dimensions]
all_dimensions = []
- lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
- children = frappe.get_all(doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft")
- all_dimensions += [c.name for c in children]
+
+ for dimension in dimensions:
+ lft, rgt = frappe.db.get_value(doctype, dimension, ["lft", "rgt"])
+ children = frappe.get_all(
+ doctype, filters={"lft": [">=", lft], "rgt": ["<=", rgt]}, order_by="lft"
+ )
+ all_dimensions += [c.name for c in children]
return all_dimensions
+
@frappe.whitelist()
def get_dimensions(with_cost_center_and_project=False):
- dimension_filters = frappe.db.sql("""
+ dimension_filters = frappe.db.sql(
+ """
SELECT label, fieldname, document_type
FROM `tabAccounting Dimension`
WHERE disabled = 0
- """, as_dict=1)
+ """,
+ as_dict=1,
+ )
- default_dimensions = frappe.db.sql("""SELECT p.fieldname, c.company, c.default_dimension
+ default_dimensions = frappe.db.sql(
+ """SELECT p.fieldname, c.company, c.default_dimension
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
- WHERE c.parent = p.name""", as_dict=1)
+ WHERE c.parent = p.name""",
+ as_dict=1,
+ )
if with_cost_center_and_project:
- dimension_filters.extend([
- {
- 'fieldname': 'cost_center',
- 'document_type': 'Cost Center'
- },
- {
- 'fieldname': 'project',
- 'document_type': 'Project'
- }
- ])
+ dimension_filters.extend(
+ [
+ {"fieldname": "cost_center", "document_type": "Cost Center"},
+ {"fieldname": "project", "document_type": "Project"},
+ ]
+ )
default_dimensions_map = {}
for dimension in default_dimensions:
diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
index f781a221ddf..25ef2ea5c2c 100644
--- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
@@ -8,7 +8,8 @@ import frappe
from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
-test_dependencies = ['Cost Center', 'Location', 'Warehouse', 'Department']
+test_dependencies = ["Cost Center", "Location", "Warehouse", "Department"]
+
class TestAccountingDimension(unittest.TestCase):
def setUp(self):
@@ -18,24 +19,27 @@ class TestAccountingDimension(unittest.TestCase):
si = create_sales_invoice(do_not_save=1)
si.location = "Block 1"
- si.append("items", {
- "item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 1,
- "rate": 100,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "department": "_Test Department - _TC",
- "location": "Block 1"
- })
+ si.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 1,
+ "rate": 100,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "department": "_Test Department - _TC",
+ "location": "Block 1",
+ },
+ )
si.save()
si.submit()
gle = frappe.get_doc("GL Entry", {"voucher_no": si.name, "account": "Sales - _TC"})
- self.assertEqual(gle.get('department'), "_Test Department - _TC")
+ self.assertEqual(gle.get("department"), "_Test Department - _TC")
def test_dimension_against_journal_entry(self):
je = make_journal_entry("Sales - _TC", "Sales Expenses - _TC", 500, save=False)
@@ -50,21 +54,24 @@ class TestAccountingDimension(unittest.TestCase):
gle = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales - _TC"})
gle1 = frappe.get_doc("GL Entry", {"voucher_no": je.name, "account": "Sales Expenses - _TC"})
- self.assertEqual(gle.get('department'), "_Test Department - _TC")
- self.assertEqual(gle1.get('department'), "_Test Department - _TC")
+ self.assertEqual(gle.get("department"), "_Test Department - _TC")
+ self.assertEqual(gle1.get("department"), "_Test Department - _TC")
def test_mandatory(self):
si = create_sales_invoice(do_not_save=1)
- si.append("items", {
- "item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 1,
- "rate": 100,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "location": ""
- })
+ si.append(
+ "items",
+ {
+ "item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 1,
+ "rate": 100,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "location": "",
+ },
+ )
si.save()
self.assertRaises(frappe.ValidationError, si.submit)
@@ -72,31 +79,39 @@ class TestAccountingDimension(unittest.TestCase):
def tearDown(self):
disable_dimension()
+
def create_dimension():
frappe.set_user("Administrator")
if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
- frappe.get_doc({
- "doctype": "Accounting Dimension",
- "document_type": "Department",
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Accounting Dimension",
+ "document_type": "Department",
+ }
+ ).insert()
else:
dimension = frappe.get_doc("Accounting Dimension", "Department")
dimension.disabled = 0
dimension.save()
if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
- dimension1 = frappe.get_doc({
- "doctype": "Accounting Dimension",
- "document_type": "Location",
- })
+ dimension1 = frappe.get_doc(
+ {
+ "doctype": "Accounting Dimension",
+ "document_type": "Location",
+ }
+ )
- dimension1.append("dimension_defaults", {
- "company": "_Test Company",
- "reference_document": "Location",
- "default_dimension": "Block 1",
- "mandatory_for_bs": 1
- })
+ dimension1.append(
+ "dimension_defaults",
+ {
+ "company": "_Test Company",
+ "reference_document": "Location",
+ "default_dimension": "Block 1",
+ "mandatory_for_bs": 1,
+ },
+ )
dimension1.insert()
dimension1.save()
@@ -105,6 +120,7 @@ def create_dimension():
dimension1.disabled = 0
dimension1.save()
+
def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
dimension1.disabled = 1
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
index 7d32bad0e78..80f736fa5bb 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
@@ -19,17 +19,27 @@ class AccountingDimensionFilter(Document):
WHERE d.name = a.parent
and d.name != %s
and d.accounting_dimension = %s
- """, (self.name, self.accounting_dimension), as_dict=1)
+ """,
+ (self.name, self.accounting_dimension),
+ as_dict=1,
+ )
account_list = [d.account for d in accounts]
- for account in self.get('accounts'):
+ for account in self.get("accounts"):
if account.applicable_on_account in account_list:
- frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
- account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension)))
+ frappe.throw(
+ _("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
+ account.idx,
+ frappe.bold(account.applicable_on_account),
+ frappe.bold(self.accounting_dimension),
+ )
+ )
+
def get_dimension_filter_map():
- filters = frappe.db.sql("""
+ filters = frappe.db.sql(
+ """
SELECT
a.applicable_on_account, d.dimension_value, p.accounting_dimension,
p.allow_or_restrict, a.is_mandatory
@@ -40,22 +50,30 @@ def get_dimension_filter_map():
p.name = a.parent
AND p.disabled = 0
AND p.name = d.parent
- """, as_dict=1)
+ """,
+ as_dict=1,
+ )
dimension_filter_map = {}
for f in filters:
f.fieldname = scrub(f.accounting_dimension)
- build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value,
- f.allow_or_restrict, f.is_mandatory)
+ build_map(
+ dimension_filter_map,
+ f.fieldname,
+ f.applicable_on_account,
+ f.dimension_value,
+ f.allow_or_restrict,
+ f.is_mandatory,
+ )
return dimension_filter_map
+
def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
- map_object.setdefault((dimension, account), {
- 'allowed_dimensions': [],
- 'is_mandatory': is_mandatory,
- 'allow_or_restrict': allow_or_restrict
- })
- map_object[(dimension, account)]['allowed_dimensions'].append(filter_value)
+ map_object.setdefault(
+ (dimension, account),
+ {"allowed_dimensions": [], "is_mandatory": is_mandatory, "allow_or_restrict": allow_or_restrict},
+ )
+ map_object[(dimension, account)]["allowed_dimensions"].append(filter_value)
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
index e2f85ba21a9..f13f2f9f279 100644
--- a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
@@ -12,7 +12,8 @@ from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension imp
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
-test_dependencies = ['Location', 'Cost Center', 'Department']
+test_dependencies = ["Location", "Cost Center", "Department"]
+
class TestAccountingDimensionFilter(unittest.TestCase):
def setUp(self):
@@ -22,9 +23,9 @@ class TestAccountingDimensionFilter(unittest.TestCase):
def test_allowed_dimension_validation(self):
si = create_sales_invoice(do_not_save=1)
- si.items[0].cost_center = 'Main - _TC'
- si.department = 'Accounts - _TC'
- si.location = 'Block 1'
+ si.items[0].cost_center = "Main - _TC"
+ si.department = "Accounts - _TC"
+ si.location = "Block 1"
si.save()
self.assertRaises(InvalidAccountDimensionError, si.submit)
@@ -32,12 +33,12 @@ class TestAccountingDimensionFilter(unittest.TestCase):
def test_mandatory_dimension_validation(self):
si = create_sales_invoice(do_not_save=1)
- si.department = ''
- si.location = 'Block 1'
+ si.department = ""
+ si.location = "Block 1"
# Test with no department for Sales Account
- si.items[0].department = ''
- si.items[0].cost_center = '_Test Cost Center 2 - _TC'
+ si.items[0].department = ""
+ si.items[0].cost_center = "_Test Cost Center 2 - _TC"
si.save()
self.assertRaises(MandatoryAccountDimensionError, si.submit)
@@ -52,53 +53,54 @@ class TestAccountingDimensionFilter(unittest.TestCase):
if si.docstatus == 1:
si.cancel()
+
def create_accounting_dimension_filter():
- if not frappe.db.get_value('Accounting Dimension Filter',
- {'accounting_dimension': 'Cost Center'}):
- frappe.get_doc({
- 'doctype': 'Accounting Dimension Filter',
- 'accounting_dimension': 'Cost Center',
- 'allow_or_restrict': 'Allow',
- 'company': '_Test Company',
- 'accounts': [{
- 'applicable_on_account': 'Sales - _TC',
- }],
- 'dimensions': [{
- 'accounting_dimension': 'Cost Center',
- 'dimension_value': '_Test Cost Center 2 - _TC'
- }]
- }).insert()
+ if not frappe.db.get_value(
+ "Accounting Dimension Filter", {"accounting_dimension": "Cost Center"}
+ ):
+ frappe.get_doc(
+ {
+ "doctype": "Accounting Dimension Filter",
+ "accounting_dimension": "Cost Center",
+ "allow_or_restrict": "Allow",
+ "company": "_Test Company",
+ "accounts": [
+ {
+ "applicable_on_account": "Sales - _TC",
+ }
+ ],
+ "dimensions": [
+ {"accounting_dimension": "Cost Center", "dimension_value": "_Test Cost Center 2 - _TC"}
+ ],
+ }
+ ).insert()
else:
- doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
+ doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"})
doc.disabled = 0
doc.save()
- if not frappe.db.get_value('Accounting Dimension Filter',
- {'accounting_dimension': 'Department'}):
- frappe.get_doc({
- 'doctype': 'Accounting Dimension Filter',
- 'accounting_dimension': 'Department',
- 'allow_or_restrict': 'Allow',
- 'company': '_Test Company',
- 'accounts': [{
- 'applicable_on_account': 'Sales - _TC',
- 'is_mandatory': 1
- }],
- 'dimensions': [{
- 'accounting_dimension': 'Department',
- 'dimension_value': 'Accounts - _TC'
- }]
- }).insert()
+ if not frappe.db.get_value("Accounting Dimension Filter", {"accounting_dimension": "Department"}):
+ frappe.get_doc(
+ {
+ "doctype": "Accounting Dimension Filter",
+ "accounting_dimension": "Department",
+ "allow_or_restrict": "Allow",
+ "company": "_Test Company",
+ "accounts": [{"applicable_on_account": "Sales - _TC", "is_mandatory": 1}],
+ "dimensions": [{"accounting_dimension": "Department", "dimension_value": "Accounts - _TC"}],
+ }
+ ).insert()
else:
- doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
+ doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Department"})
doc.disabled = 0
doc.save()
+
def disable_dimension_filter():
- doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
+ doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Cost Center"})
doc.disabled = 1
doc.save()
- doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
+ doc = frappe.get_doc("Accounting Dimension Filter", {"accounting_dimension": "Department"})
doc.disabled = 1
doc.save()
diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py
index e2949378e5f..0c15d6a207d 100644
--- a/erpnext/accounts/doctype/accounting_period/accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py
@@ -7,7 +7,9 @@ from frappe import _
from frappe.model.document import Document
-class OverlapError(frappe.ValidationError): pass
+class OverlapError(frappe.ValidationError):
+ pass
+
class AccountingPeriod(Document):
def validate(self):
@@ -17,11 +19,12 @@ class AccountingPeriod(Document):
self.bootstrap_doctypes_for_closing()
def autoname(self):
- company_abbr = frappe.get_cached_value('Company', self.company, "abbr")
+ company_abbr = frappe.get_cached_value("Company", self.company, "abbr")
self.name = " - ".join([self.period_name, company_abbr])
def validate_overlap(self):
- existing_accounting_period = frappe.db.sql("""select name from `tabAccounting Period`
+ existing_accounting_period = frappe.db.sql(
+ """select name from `tabAccounting Period`
where (
(%(start_date)s between start_date and end_date)
or (%(end_date)s between start_date and end_date)
@@ -32,18 +35,29 @@ class AccountingPeriod(Document):
"start_date": self.start_date,
"end_date": self.end_date,
"name": self.name,
- "company": self.company
- }, as_dict=True)
+ "company": self.company,
+ },
+ as_dict=True,
+ )
if len(existing_accounting_period) > 0:
- frappe.throw(_("Accounting Period overlaps with {0}")
- .format(existing_accounting_period[0].get("name")), OverlapError)
+ frappe.throw(
+ _("Accounting Period overlaps with {0}").format(existing_accounting_period[0].get("name")),
+ OverlapError,
+ )
@frappe.whitelist()
def get_doctypes_for_closing(self):
docs_for_closing = []
- doctypes = ["Sales Invoice", "Purchase Invoice", "Journal Entry", "Payroll Entry", \
- "Bank Clearance", "Asset", "Stock Entry"]
+ doctypes = [
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Journal Entry",
+ "Payroll Entry",
+ "Bank Clearance",
+ "Asset",
+ "Stock Entry",
+ ]
closed_doctypes = [{"document_type": doctype, "closed": 1} for doctype in doctypes]
for closed_doctype in closed_doctypes:
docs_for_closing.append(closed_doctype)
@@ -53,7 +67,7 @@ class AccountingPeriod(Document):
def bootstrap_doctypes_for_closing(self):
if len(self.closed_documents) == 0:
for doctype_for_closing in self.get_doctypes_for_closing():
- self.append('closed_documents', {
- "document_type": doctype_for_closing.document_type,
- "closed": doctype_for_closing.closed
- })
+ self.append(
+ "closed_documents",
+ {"document_type": doctype_for_closing.document_type, "closed": doctype_for_closing.closed},
+ )
diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
index c06c2e0338b..85025d190f5 100644
--- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
@@ -10,29 +10,38 @@ from erpnext.accounts.doctype.accounting_period.accounting_period import Overlap
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.general_ledger import ClosedAccountingPeriod
-test_dependencies = ['Item']
+test_dependencies = ["Item"]
+
class TestAccountingPeriod(unittest.TestCase):
def test_overlap(self):
- ap1 = create_accounting_period(start_date = "2018-04-01",
- end_date = "2018-06-30", company = "Wind Power LLC")
+ ap1 = create_accounting_period(
+ start_date="2018-04-01", end_date="2018-06-30", company="Wind Power LLC"
+ )
ap1.save()
- ap2 = create_accounting_period(start_date = "2018-06-30",
- end_date = "2018-07-10", company = "Wind Power LLC", period_name = "Test Accounting Period 1")
+ ap2 = create_accounting_period(
+ start_date="2018-06-30",
+ end_date="2018-07-10",
+ company="Wind Power LLC",
+ period_name="Test Accounting Period 1",
+ )
self.assertRaises(OverlapError, ap2.save)
def test_accounting_period(self):
- ap1 = create_accounting_period(period_name = "Test Accounting Period 2")
+ ap1 = create_accounting_period(period_name="Test Accounting Period 2")
ap1.save()
- doc = create_sales_invoice(do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC")
+ doc = create_sales_invoice(
+ do_not_submit=1, cost_center="_Test Company - _TC", warehouse="Stores - _TC"
+ )
self.assertRaises(ClosedAccountingPeriod, doc.submit)
def tearDown(self):
for d in frappe.get_all("Accounting Period"):
frappe.delete_doc("Accounting Period", d.name)
+
def create_accounting_period(**args):
args = frappe._dict(args)
@@ -41,8 +50,6 @@ def create_accounting_period(**args):
accounting_period.end_date = args.end_date or add_months(nowdate(), 1)
accounting_period.company = args.company or "_Test Company"
accounting_period.period_name = args.period_name or "_Test_Period_Name_1"
- accounting_period.append("closed_documents", {
- "document_type": 'Sales Invoice', "closed": 1
- })
+ accounting_period.append("closed_documents", {"document_type": "Sales Invoice", "closed": 1})
return accounting_period
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index baab628b210..ea427aa7d80 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -19,6 +19,7 @@
"book_asset_depreciation_entry_automatically",
"unlink_advance_payment_on_cancelation_of_order",
"enable_common_party_accounting",
+ "allow_multi_currency_invoices_against_single_party_account",
"post_change_gl_entries",
"enable_discount_accounting",
"tax_settings_section",
@@ -276,14 +277,21 @@
"fieldname": "enable_common_party_accounting",
"fieldtype": "Check",
"label": "Enable Common Party Accounting"
- }
+ },
+ {
+ "default": "0",
+ "description": "Enabling this will allow creation of multi-currency invoices against single party account in company currency",
+ "fieldname": "allow_multi_currency_invoices_against_single_party_account",
+ "fieldtype": "Check",
+ "label": "Allow multi-currency invoices against single party account"
+ }
],
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-10-11 17:42:36.427699",
+ "modified": "2022-07-11 13:37:50.605141",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
index 48392074102..835498176c7 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.py
@@ -18,11 +18,13 @@ class AccountsSettings(Document):
frappe.clear_cache()
def validate(self):
- frappe.db.set_default("add_taxes_from_item_tax_template",
- self.get("add_taxes_from_item_tax_template", 0))
+ frappe.db.set_default(
+ "add_taxes_from_item_tax_template", self.get("add_taxes_from_item_tax_template", 0)
+ )
- frappe.db.set_default("enable_common_party_accounting",
- self.get("enable_common_party_accounting", 0))
+ frappe.db.set_default(
+ "enable_common_party_accounting", self.get("enable_common_party_accounting", 0)
+ )
self.validate_stale_days()
self.enable_payment_schedule_in_print()
@@ -32,34 +34,91 @@ class AccountsSettings(Document):
def validate_stale_days(self):
if not self.allow_stale and cint(self.stale_days) <= 0:
frappe.msgprint(
- _("Stale Days should start from 1."), title='Error', indicator='red',
- raise_exception=1)
+ _("Stale Days should start from 1."), title="Error", indicator="red", raise_exception=1
+ )
def enable_payment_schedule_in_print(self):
show_in_print = cint(self.show_payment_schedule_in_print)
for doctype in ("Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"):
- make_property_setter(doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False)
- make_property_setter(doctype, "payment_schedule", "print_hide", 0 if show_in_print else 1, "Check", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype, "due_date", "print_hide", show_in_print, "Check", validate_fields_for_doctype=False
+ )
+ make_property_setter(
+ doctype,
+ "payment_schedule",
+ "print_hide",
+ 0 if show_in_print else 1,
+ "Check",
+ validate_fields_for_doctype=False,
+ )
def toggle_discount_accounting_fields(self):
enable_discount_accounting = cint(self.enable_discount_accounting)
for doctype in ["Sales Invoice Item", "Purchase Invoice Item"]:
- make_property_setter(doctype, "discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "discount_account",
+ "hidden",
+ not (enable_discount_accounting),
+ "Check",
+ validate_fields_for_doctype=False,
+ )
if enable_discount_accounting:
- make_property_setter(doctype, "discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "discount_account",
+ "mandatory_depends_on",
+ "eval: doc.discount_amount",
+ "Code",
+ validate_fields_for_doctype=False,
+ )
else:
- make_property_setter(doctype, "discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "discount_account",
+ "mandatory_depends_on",
+ "",
+ "Code",
+ validate_fields_for_doctype=False,
+ )
for doctype in ["Sales Invoice", "Purchase Invoice"]:
- make_property_setter(doctype, "additional_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "additional_discount_account",
+ "hidden",
+ not (enable_discount_accounting),
+ "Check",
+ validate_fields_for_doctype=False,
+ )
if enable_discount_accounting:
- make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "eval: doc.discount_amount", "Code", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "additional_discount_account",
+ "mandatory_depends_on",
+ "eval: doc.discount_amount",
+ "Code",
+ validate_fields_for_doctype=False,
+ )
else:
- make_property_setter(doctype, "additional_discount_account", "mandatory_depends_on", "", "Code", validate_fields_for_doctype=False)
-
- make_property_setter("Item", "default_discount_account", "hidden", not(enable_discount_accounting), "Check", validate_fields_for_doctype=False)
+ make_property_setter(
+ doctype,
+ "additional_discount_account",
+ "mandatory_depends_on",
+ "",
+ "Code",
+ validate_fields_for_doctype=False,
+ )
+ make_property_setter(
+ "Item",
+ "default_discount_account",
+ "hidden",
+ not (enable_discount_accounting),
+ "Check",
+ validate_fields_for_doctype=False,
+ )
def validate_pending_reposts(self):
if self.acc_frozen_upto:
diff --git a/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py
index eb90fc7a861..a350cc385da 100644
--- a/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py
+++ b/erpnext/accounts/doctype/accounts_settings/test_accounts_settings.py
@@ -1,4 +1,3 @@
-
import unittest
import frappe
@@ -8,12 +7,12 @@ class TestAccountsSettings(unittest.TestCase):
def tearDown(self):
# Just in case `save` method succeeds, we need to take things back to default so that other tests
# don't break
- cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
+ cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
cur_settings.allow_stale = 1
cur_settings.save()
def test_stale_days(self):
- cur_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings')
+ cur_settings = frappe.get_doc("Accounts Settings", "Accounts Settings")
cur_settings.allow_stale = 0
cur_settings.stale_days = 0
diff --git a/erpnext/accounts/doctype/bank/bank.py b/erpnext/accounts/doctype/bank/bank.py
index f111433c321..d44be9af23e 100644
--- a/erpnext/accounts/doctype/bank/bank.py
+++ b/erpnext/accounts/doctype/bank/bank.py
@@ -15,4 +15,4 @@ class Bank(Document):
load_address_and_contact(self)
def on_trash(self):
- delete_contact_and_address('Bank', self.name)
+ delete_contact_and_address("Bank", self.name)
diff --git a/erpnext/accounts/doctype/bank/bank_dashboard.py b/erpnext/accounts/doctype/bank/bank_dashboard.py
index e7ef6aa25f5..7e40a1a6b8e 100644
--- a/erpnext/accounts/doctype/bank/bank_dashboard.py
+++ b/erpnext/accounts/doctype/bank/bank_dashboard.py
@@ -1,14 +1,8 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'bank',
- 'transactions': [
- {
- 'label': _('Bank Details'),
- 'items': ['Bank Account', 'Bank Guarantee']
- }
- ]
+ "fieldname": "bank",
+ "transactions": [{"label": _("Bank Details"), "items": ["Bank Account", "Bank Guarantee"]}],
}
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.py b/erpnext/accounts/doctype/bank_account/bank_account.py
index f9140c31d64..addcf62e5b6 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.py
+++ b/erpnext/accounts/doctype/bank_account/bank_account.py
@@ -20,7 +20,7 @@ class BankAccount(Document):
self.name = self.account_name + " - " + self.bank
def on_trash(self):
- delete_contact_and_address('BankAccount', self.name)
+ delete_contact_and_address("BankAccount", self.name)
def validate(self):
self.validate_company()
@@ -31,9 +31,9 @@ class BankAccount(Document):
frappe.throw(_("Company is manadatory for company account"))
def validate_iban(self):
- '''
+ """
Algorithm: https://en.wikipedia.org/wiki/International_Bank_Account_Number#Validating_the_IBAN
- '''
+ """
# IBAN field is optional
if not self.iban:
return
@@ -43,7 +43,7 @@ class BankAccount(Document):
return str(9 + ord(c) - 64)
# remove whitespaces, upper case to get the right number from ord()
- iban = ''.join(self.iban.split(' ')).upper()
+ iban = "".join(self.iban.split(" ")).upper()
# Move country code and checksum from the start to the end
flipped = iban[4:] + iban[:4]
@@ -52,12 +52,12 @@ class BankAccount(Document):
encoded = [encode_char(c) if ord(c) >= 65 and ord(c) <= 90 else c for c in flipped]
try:
- to_check = int(''.join(encoded))
+ to_check = int("".join(encoded))
except ValueError:
- frappe.throw(_('IBAN is not valid'))
+ frappe.throw(_("IBAN is not valid"))
if to_check % 97 != 1:
- frappe.throw(_('IBAN is not valid'))
+ frappe.throw(_("IBAN is not valid"))
@frappe.whitelist()
@@ -69,12 +69,14 @@ def make_bank_account(doctype, docname):
return doc
+
@frappe.whitelist()
def get_party_bank_account(party_type, party):
- return frappe.db.get_value(party_type,
- party, 'default_bank_account')
+ return frappe.db.get_value(party_type, party, "default_bank_account")
+
@frappe.whitelist()
def get_bank_account_details(bank_account):
- return frappe.db.get_value("Bank Account",
- bank_account, ['account', 'bank', 'bank_account_no'], as_dict=1)
+ return frappe.db.get_value(
+ "Bank Account", bank_account, ["account", "bank", "bank_account_no"], as_dict=1
+ )
diff --git a/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py b/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py
index bc08eab0351..8bf8d8a5cd0 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py
+++ b/erpnext/accounts/doctype/bank_account/bank_account_dashboard.py
@@ -1,28 +1,20 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'bank_account',
- 'non_standard_fieldnames': {
- 'Customer': 'default_bank_account',
- 'Supplier': 'default_bank_account',
+ "fieldname": "bank_account",
+ "non_standard_fieldnames": {
+ "Customer": "default_bank_account",
+ "Supplier": "default_bank_account",
},
- 'transactions': [
+ "transactions": [
{
- 'label': _('Payments'),
- 'items': ['Payment Entry', 'Payment Request', 'Payment Order', 'Payroll Entry']
+ "label": _("Payments"),
+ "items": ["Payment Entry", "Payment Request", "Payment Order", "Payroll Entry"],
},
- {
- 'label': _('Party'),
- 'items': ['Customer', 'Supplier']
- },
- {
- 'items': ['Bank Guarantee']
- },
- {
- 'items': ['Journal Entry']
- }
- ]
+ {"label": _("Party"), "items": ["Customer", "Supplier"]},
+ {"items": ["Bank Guarantee"]},
+ {"items": ["Journal Entry"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/bank_account/test_bank_account.py b/erpnext/accounts/doctype/bank_account/test_bank_account.py
index 5f23f88af6c..8949524a561 100644
--- a/erpnext/accounts/doctype/bank_account/test_bank_account.py
+++ b/erpnext/accounts/doctype/bank_account/test_bank_account.py
@@ -8,28 +8,28 @@ from frappe import ValidationError
# test_records = frappe.get_test_records('Bank Account')
-class TestBankAccount(unittest.TestCase):
+class TestBankAccount(unittest.TestCase):
def test_validate_iban(self):
valid_ibans = [
- 'GB82 WEST 1234 5698 7654 32',
- 'DE91 1000 0000 0123 4567 89',
- 'FR76 3000 6000 0112 3456 7890 189'
+ "GB82 WEST 1234 5698 7654 32",
+ "DE91 1000 0000 0123 4567 89",
+ "FR76 3000 6000 0112 3456 7890 189",
]
invalid_ibans = [
# wrong checksum (3rd place)
- 'GB72 WEST 1234 5698 7654 32',
- 'DE81 1000 0000 0123 4567 89',
- 'FR66 3000 6000 0112 3456 7890 189'
+ "GB72 WEST 1234 5698 7654 32",
+ "DE81 1000 0000 0123 4567 89",
+ "FR66 3000 6000 0112 3456 7890 189",
]
- bank_account = frappe.get_doc({'doctype':'Bank Account'})
+ bank_account = frappe.get_doc({"doctype": "Bank Account"})
try:
bank_account.validate_iban()
except AttributeError:
- msg = 'BankAccount.validate_iban() failed for empty IBAN'
+ msg = "BankAccount.validate_iban() failed for empty IBAN"
self.fail(msg=msg)
for iban in valid_ibans:
@@ -37,11 +37,11 @@ class TestBankAccount(unittest.TestCase):
try:
bank_account.validate_iban()
except ValidationError:
- msg = 'BankAccount.validate_iban() failed for valid IBAN {}'.format(iban)
+ msg = "BankAccount.validate_iban() failed for valid IBAN {}".format(iban)
self.fail(msg=msg)
for not_iban in invalid_ibans:
bank_account.iban = not_iban
- msg = 'BankAccount.validate_iban() accepted invalid IBAN {}'.format(not_iban)
+ msg = "BankAccount.validate_iban() accepted invalid IBAN {}".format(not_iban)
with self.assertRaises(ValidationError, msg=msg):
bank_account.validate_iban()
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index a3bbb2288d3..98ba399a35d 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -5,11 +5,13 @@
import frappe
from frappe import _, msgprint
from frappe.model.document import Document
-from frappe.utils import flt, fmt_money, getdate, nowdate
+from frappe.query_builder.custom import ConstantColumn
+from frappe.utils import flt, fmt_money, getdate
+
+import erpnext
+
+form_grid_templates = {"journal_entries": "templates/form_grid/bank_reconciliation_grid.html"}
-form_grid_templates = {
- "journal_entries": "templates/form_grid/bank_reconciliation_grid.html"
-}
class BankClearance(Document):
@frappe.whitelist()
@@ -24,7 +26,8 @@ class BankClearance(Document):
if not self.include_reconciled_entries:
condition = "and (clearance_date IS NULL or clearance_date='0000-00-00')"
- journal_entries = frappe.db.sql("""
+ journal_entries = frappe.db.sql(
+ """
select
"Journal Entry" as payment_document, t1.name as payment_entry,
t1.cheque_no as cheque_number, t1.cheque_date,
@@ -38,12 +41,18 @@ class BankClearance(Document):
and ifnull(t1.is_opening, 'No') = 'No' {condition}
group by t2.account, t1.name
order by t1.posting_date ASC, t1.name DESC
- """.format(condition=condition), {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
+ """.format(
+ condition=condition
+ ),
+ {"account": self.account, "from": self.from_date, "to": self.to_date},
+ as_dict=1,
+ )
if self.bank_account:
- condition += 'and bank_account = %(bank_account)s'
+ condition += "and bank_account = %(bank_account)s"
- payment_entries = frappe.db.sql("""
+ payment_entries = frappe.db.sql(
+ """
select
"Payment Entry" as payment_document, name as payment_entry,
reference_no as cheque_number, reference_date as cheque_date,
@@ -58,12 +67,69 @@ class BankClearance(Document):
{condition}
order by
posting_date ASC, name DESC
- """.format(condition=condition), {"account": self.account, "from":self.from_date,
- "to": self.to_date, "bank_account": self.bank_account}, as_dict=1)
+ """.format(
+ condition=condition
+ ),
+ {
+ "account": self.account,
+ "from": self.from_date,
+ "to": self.to_date,
+ "bank_account": self.bank_account,
+ },
+ as_dict=1,
+ )
+
+ loan_disbursement = frappe.qb.DocType("Loan Disbursement")
+
+ loan_disbursements = (
+ frappe.qb.from_(loan_disbursement)
+ .select(
+ ConstantColumn("Loan Disbursement").as_("payment_document"),
+ loan_disbursement.name.as_("payment_entry"),
+ loan_disbursement.disbursed_amount.as_("credit"),
+ ConstantColumn(0).as_("debit"),
+ loan_disbursement.reference_number.as_("cheque_number"),
+ loan_disbursement.reference_date.as_("cheque_date"),
+ loan_disbursement.disbursement_date.as_("posting_date"),
+ loan_disbursement.applicant.as_("against_account"),
+ )
+ .where(loan_disbursement.docstatus == 1)
+ .where(loan_disbursement.disbursement_date >= self.from_date)
+ .where(loan_disbursement.disbursement_date <= self.to_date)
+ .where(loan_disbursement.clearance_date.isnull())
+ .where(loan_disbursement.disbursement_account.isin([self.bank_account, self.account]))
+ .orderby(loan_disbursement.disbursement_date)
+ .orderby(loan_disbursement.name, frappe.qb.desc)
+ ).run(as_dict=1)
+
+ loan_repayment = frappe.qb.DocType("Loan Repayment")
+
+ loan_repayments = (
+ frappe.qb.from_(loan_repayment)
+ .select(
+ ConstantColumn("Loan Repayment").as_("payment_document"),
+ loan_repayment.name.as_("payment_entry"),
+ loan_repayment.amount_paid.as_("debit"),
+ ConstantColumn(0).as_("credit"),
+ loan_repayment.reference_number.as_("cheque_number"),
+ loan_repayment.reference_date.as_("cheque_date"),
+ loan_repayment.applicant.as_("against_account"),
+ loan_repayment.posting_date,
+ )
+ .where(loan_repayment.docstatus == 1)
+ .where(loan_repayment.clearance_date.isnull())
+ .where(loan_repayment.repay_from_salary == 0)
+ .where(loan_repayment.posting_date >= self.from_date)
+ .where(loan_repayment.posting_date <= self.to_date)
+ .where(loan_repayment.payment_account.isin([self.bank_account, self.account]))
+ .orderby(loan_repayment.posting_date)
+ .orderby(loan_repayment.name, frappe.qb.desc)
+ ).run(as_dict=1)
pos_sales_invoices, pos_purchase_invoices = [], []
if self.include_pos_transactions:
- pos_sales_invoices = frappe.db.sql("""
+ pos_sales_invoices = frappe.db.sql(
+ """
select
"Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit,
si.posting_date, si.customer as against_account, sip.clearance_date,
@@ -74,9 +140,13 @@ class BankClearance(Document):
and account.name = sip.account and si.posting_date >= %(from)s and si.posting_date <= %(to)s
order by
si.posting_date ASC, si.name DESC
- """, {"account":self.account, "from":self.from_date, "to":self.to_date}, as_dict=1)
+ """,
+ {"account": self.account, "from": self.from_date, "to": self.to_date},
+ as_dict=1,
+ )
- pos_purchase_invoices = frappe.db.sql("""
+ pos_purchase_invoices = frappe.db.sql(
+ """
select
"Purchase Invoice" as payment_document, pi.name as payment_entry, pi.paid_amount as credit,
pi.posting_date, pi.supplier as against_account, pi.clearance_date,
@@ -87,21 +157,36 @@ class BankClearance(Document):
and pi.posting_date >= %(from)s and pi.posting_date <= %(to)s
order by
pi.posting_date ASC, pi.name DESC
- """, {"account": self.account, "from": self.from_date, "to": self.to_date}, as_dict=1)
+ """,
+ {"account": self.account, "from": self.from_date, "to": self.to_date},
+ as_dict=1,
+ )
- entries = sorted(list(payment_entries) + list(journal_entries + list(pos_sales_invoices) + list(pos_purchase_invoices)),
- key=lambda k: k['posting_date'] or getdate(nowdate()))
+ entries = sorted(
+ list(payment_entries)
+ + list(journal_entries)
+ + list(pos_sales_invoices)
+ + list(pos_purchase_invoices)
+ + list(loan_disbursements)
+ + list(loan_repayments),
+ key=lambda k: getdate(k["posting_date"]),
+ )
- self.set('payment_entries', [])
+ self.set("payment_entries", [])
self.total_amount = 0.0
+ default_currency = erpnext.get_default_currency()
for d in entries:
- row = self.append('payment_entries', {})
+ row = self.append("payment_entries", {})
- amount = flt(d.get('debit', 0)) - flt(d.get('credit', 0))
+ amount = flt(d.get("debit", 0)) - flt(d.get("credit", 0))
+
+ if not d.get("account_currency"):
+ d.account_currency = default_currency
formatted_amount = fmt_money(abs(amount), 2, d.account_currency)
d.amount = formatted_amount + " " + (_("Dr") if amount > 0 else _("Cr"))
+ d.posting_date = getdate(d.posting_date)
d.pop("credit")
d.pop("debit")
@@ -112,21 +197,24 @@ class BankClearance(Document):
@frappe.whitelist()
def update_clearance_date(self):
clearance_date_updated = False
- for d in self.get('payment_entries'):
+ for d in self.get("payment_entries"):
if d.clearance_date:
if not d.payment_document:
frappe.throw(_("Row #{0}: Payment document is required to complete the transaction"))
if d.cheque_date and getdate(d.clearance_date) < getdate(d.cheque_date):
- frappe.throw(_("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}")
- .format(d.idx, d.clearance_date, d.cheque_date))
+ frappe.throw(
+ _("Row #{0}: Clearance date {1} cannot be before Cheque Date {2}").format(
+ d.idx, d.clearance_date, d.cheque_date
+ )
+ )
if d.clearance_date or self.include_reconciled_entries:
if not d.clearance_date:
d.clearance_date = None
payment_entry = frappe.get_doc(d.payment_document, d.payment_entry)
- payment_entry.db_set('clearance_date', d.clearance_date)
+ payment_entry.db_set("clearance_date", d.clearance_date)
clearance_date_updated = True
diff --git a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py
index 706fbbe245c..c1e55f6f723 100644
--- a/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/test_bank_clearance.py
@@ -1,9 +1,96 @@
# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-# import frappe
import unittest
+import frappe
+from frappe.utils import add_months, getdate
+
+from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.loan_management.doctype.loan.test_loan import (
+ create_loan,
+ create_loan_accounts,
+ create_loan_type,
+ create_repayment_entry,
+ make_loan_disbursement_entry,
+)
+
class TestBankClearance(unittest.TestCase):
- pass
+ @classmethod
+ def setUpClass(cls):
+ make_bank_account()
+ create_loan_accounts()
+ create_loan_masters()
+ add_transactions()
+
+ # Basic test case to test if bank clearance tool doesn't break
+ # Detailed test can be added later
+ def test_bank_clearance(self):
+ bank_clearance = frappe.get_doc("Bank Clearance")
+ bank_clearance.account = "_Test Bank Clearance - _TC"
+ bank_clearance.from_date = add_months(getdate(), -1)
+ bank_clearance.to_date = getdate()
+ bank_clearance.get_payment_entries()
+ self.assertEqual(len(bank_clearance.payment_entries), 3)
+
+
+def make_bank_account():
+ if not frappe.db.get_value("Account", "_Test Bank Clearance - _TC"):
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_type": "Bank",
+ "account_name": "_Test Bank Clearance",
+ "company": "_Test Company",
+ "parent_account": "Bank Accounts - _TC",
+ }
+ ).insert()
+
+
+def create_loan_masters():
+ create_loan_type(
+ "Clearance Loan",
+ 2000000,
+ 13.5,
+ 25,
+ 0,
+ 5,
+ "Cash",
+ "_Test Bank Clearance - _TC",
+ "_Test Bank Clearance - _TC",
+ "Loan Account - _TC",
+ "Interest Income Account - _TC",
+ "Penalty Income Account - _TC",
+ )
+
+
+def add_transactions():
+ make_payment_entry()
+ make_loan()
+
+
+def make_loan():
+ loan = create_loan(
+ "_Test Customer",
+ "Clearance Loan",
+ 280000,
+ "Repay Over Number of Periods",
+ 20,
+ applicant_type="Customer",
+ )
+ loan.submit()
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=getdate())
+ repayment_entry = create_repayment_entry(loan.name, "_Test Customer", getdate(), loan.loan_amount)
+ repayment_entry.save()
+ repayment_entry.submit()
+
+
+def make_payment_entry():
+ pi = make_purchase_invoice(supplier="_Test Supplier", qty=1, rate=690)
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank Clearance - _TC")
+ pe.reference_no = "Conrad Oct 18"
+ pe.reference_date = "2018-10-24"
+ pe.insert()
+ pe.submit()
diff --git a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py
index cfbcf16b91e..9144a29c6ef 100644
--- a/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py
+++ b/erpnext/accounts/doctype/bank_guarantee/bank_guarantee.py
@@ -23,10 +23,16 @@ class BankGuarantee(Document):
if not self.bank:
frappe.throw(_("Enter the name of the bank or lending institution before submittting."))
+
@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]
+ return frappe.db.sql(
+ """ select {columns} from `tab{doctype}` where name=%s""".format(
+ columns=", ".join(column_list), doctype=doctype
+ ),
+ docname,
+ as_dict=1,
+ )[0]
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
index f7d471b725e..cf52471912d 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -7,13 +7,17 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
frm.set_query("bank_account", function () {
return {
filters: {
- company: ["in", frm.doc.company],
+ company: frm.doc.company,
'is_company_account': 1
},
};
});
},
+ onload: function (frm) {
+ frm.trigger('bank_account');
+ },
+
refresh: function (frm) {
frappe.require("assets/js/bank-reconciliation-tool.min.js", () =>
frm.trigger("make_reconciliation_tool")
@@ -51,7 +55,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
bank_account: function (frm) {
frappe.db.get_value(
"Bank Account",
- frm.bank_account,
+ frm.doc.bank_account,
"account",
(r) => {
frappe.db.get_value(
@@ -60,6 +64,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
"account_currency",
(r) => {
frm.currency = r.account_currency;
+ frm.trigger("render_chart");
}
);
}
@@ -124,7 +129,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
}
},
- render_chart(frm) {
+ render_chart: frappe.utils.debounce((frm) => {
frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
{
$reconciliation_tool_cards: frm.get_field(
@@ -136,7 +141,7 @@ frappe.ui.form.on("Bank Reconciliation Tool", {
currency: frm.currency,
}
);
- },
+ }, 500),
render(frm) {
if (frm.doc.bank_account) {
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
index e7371fbe436..0efe086d94e 100644
--- a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -7,6 +7,7 @@ import json
import frappe
from frappe import _
from frappe.model.document import Document
+from frappe.query_builder.custom import ConstantColumn
from frappe.utils import flt
from erpnext import get_company_currency
@@ -21,48 +22,63 @@ from erpnext.accounts.utils import get_balance_on
class BankReconciliationTool(Document):
pass
+
@frappe.whitelist()
-def get_bank_transactions(bank_account, from_date = None, to_date = None):
+def get_bank_transactions(bank_account, from_date=None, to_date=None):
# returns bank transactions for a bank account
filters = []
- filters.append(['bank_account', '=', bank_account])
- filters.append(['docstatus', '=', 1])
- filters.append(['unallocated_amount', '>', 0])
+ filters.append(["bank_account", "=", bank_account])
+ filters.append(["docstatus", "=", 1])
+ filters.append(["unallocated_amount", ">", 0])
if to_date:
- filters.append(['date', '<=', to_date])
+ filters.append(["date", "<=", to_date])
if from_date:
- filters.append(['date', '>=', from_date])
+ filters.append(["date", ">=", from_date])
transactions = frappe.get_all(
- 'Bank Transaction',
- fields = ['date', 'deposit', 'withdrawal', 'currency',
- 'description', 'name', 'bank_account', 'company',
- 'unallocated_amount', 'reference_number', 'party_type', 'party'],
- filters = filters
+ "Bank Transaction",
+ fields=[
+ "date",
+ "deposit",
+ "withdrawal",
+ "currency",
+ "description",
+ "name",
+ "bank_account",
+ "company",
+ "unallocated_amount",
+ "reference_number",
+ "party_type",
+ "party",
+ ],
+ filters=filters,
)
return transactions
+
@frappe.whitelist()
def get_account_balance(bank_account, till_date):
# returns account balance till the specified date
- account = frappe.db.get_value('Bank Account', bank_account, 'account')
- filters = frappe._dict({
- "account": account,
- "report_date": till_date,
- "include_pos_transactions": 1
- })
+ account = frappe.db.get_value("Bank Account", bank_account, "account")
+ filters = frappe._dict(
+ {"account": account, "report_date": till_date, "include_pos_transactions": 1}
+ )
data = get_entries(filters)
balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
- total_debit, total_credit = 0,0
+ total_debit, total_credit = 0, 0
for d in data:
total_debit += flt(d.debit)
total_credit += flt(d.credit)
amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
- bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \
+ bank_bal = (
+ flt(balance_as_per_system)
+ - flt(total_debit)
+ + flt(total_credit)
+ amounts_not_reflected_in_system
+ )
return bank_bal
@@ -75,71 +91,94 @@ def update_bank_transaction(bank_transaction_name, reference_number, party_type=
bank_transaction.party_type = party_type
bank_transaction.party = party
bank_transaction.save()
- return frappe.db.get_all('Bank Transaction',
- filters={
- 'name': bank_transaction_name
- },
- fields=['date', 'deposit', 'withdrawal', 'currency',
- 'description', 'name', 'bank_account', 'company',
- 'unallocated_amount', 'reference_number',
- 'party_type', 'party'],
+ return frappe.db.get_all(
+ "Bank Transaction",
+ filters={"name": bank_transaction_name},
+ fields=[
+ "date",
+ "deposit",
+ "withdrawal",
+ "currency",
+ "description",
+ "name",
+ "bank_account",
+ "company",
+ "unallocated_amount",
+ "reference_number",
+ "party_type",
+ "party",
+ ],
)[0]
@frappe.whitelist()
-def create_journal_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, posting_date=None, entry_type=None,
- second_account=None, mode_of_payment=None, party_type=None, party=None, allow_edit=None):
+def create_journal_entry_bts(
+ bank_transaction_name,
+ reference_number=None,
+ reference_date=None,
+ posting_date=None,
+ entry_type=None,
+ second_account=None,
+ mode_of_payment=None,
+ party_type=None,
+ party=None,
+ allow_edit=None,
+):
# Create a new journal entry based on the bank transaction
bank_transaction = frappe.db.get_values(
- "Bank Transaction", bank_transaction_name,
- fieldname=["name", "deposit", "withdrawal", "bank_account"] ,
- as_dict=True
+ "Bank Transaction",
+ bank_transaction_name,
+ fieldname=["name", "deposit", "withdrawal", "bank_account"],
+ as_dict=True,
)[0]
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
account_type = frappe.db.get_value("Account", second_account, "account_type")
if account_type in ["Receivable", "Payable"]:
if not (party_type and party):
- frappe.throw(_("Party Type and Party is required for Receivable / Payable account {0}").format( second_account))
+ frappe.throw(
+ _("Party Type and Party is required for Receivable / Payable account {0}").format(
+ second_account
+ )
+ )
accounts = []
# Multi Currency?
- accounts.append({
+ 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,
- "party_type":party_type,
- "party":party,
- })
+ "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,
+ "party_type": party_type,
+ "party": party,
+ }
+ )
- accounts.append({
+ accounts.append(
+ {
"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,
- })
+ if bank_transaction.withdrawal > 0
+ else 0,
+ "debit_in_account_currency": bank_transaction.deposit if bank_transaction.deposit > 0 else 0,
+ }
+ )
company = frappe.get_value("Account", company_account, "company")
journal_entry_dict = {
- "voucher_type" : entry_type,
- "company" : company,
- "posting_date" : posting_date,
- "cheque_date" : reference_date,
- "cheque_no" : reference_number,
- "mode_of_payment" : mode_of_payment
+ "voucher_type": entry_type,
+ "company": company,
+ "posting_date": posting_date,
+ "cheque_date": reference_date,
+ "cheque_no": reference_number,
+ "mode_of_payment": mode_of_payment,
}
- journal_entry = frappe.new_doc('Journal Entry')
+ journal_entry = frappe.new_doc("Journal Entry")
journal_entry.update(journal_entry_dict)
journal_entry.set("accounts", accounts)
-
if allow_edit:
return journal_entry
@@ -151,21 +190,32 @@ def create_journal_entry_bts( bank_transaction_name, reference_number=None, refe
else:
paid_amount = bank_transaction.withdrawal
- vouchers = json.dumps([{
- "payment_doctype":"Journal Entry",
- "payment_name":journal_entry.name,
- "amount":paid_amount}])
+ vouchers = json.dumps(
+ [{"payment_doctype": "Journal Entry", "payment_name": journal_entry.name, "amount": paid_amount}]
+ )
return reconcile_vouchers(bank_transaction.name, vouchers)
+
@frappe.whitelist()
-def create_payment_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, party_type=None, party=None, posting_date=None,
- mode_of_payment=None, project=None, cost_center=None, allow_edit=None):
+def create_payment_entry_bts(
+ bank_transaction_name,
+ reference_number=None,
+ reference_date=None,
+ party_type=None,
+ party=None,
+ posting_date=None,
+ mode_of_payment=None,
+ project=None,
+ cost_center=None,
+ allow_edit=None,
+):
# Create a new payment entry based on the bank transaction
bank_transaction = frappe.db.get_values(
- "Bank Transaction", bank_transaction_name,
- fieldname=["name", "unallocated_amount", "deposit", "bank_account"] ,
- as_dict=True
+ "Bank Transaction",
+ bank_transaction_name,
+ fieldname=["name", "unallocated_amount", "deposit", "bank_account"],
+ as_dict=True,
)[0]
paid_amount = bank_transaction.unallocated_amount
payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
@@ -173,27 +223,26 @@ def create_payment_entry_bts( bank_transaction_name, reference_number=None, refe
company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
company = frappe.get_value("Account", company_account, "company")
payment_entry_dict = {
- "company" : company,
- "payment_type" : payment_type,
- "reference_no" : reference_number,
- "reference_date" : reference_date,
- "party_type" : party_type,
- "party" : party,
- "posting_date" : posting_date,
+ "company": company,
+ "payment_type": payment_type,
+ "reference_no": reference_number,
+ "reference_date": reference_date,
+ "party_type": party_type,
+ "party": party,
+ "posting_date": posting_date,
"paid_amount": paid_amount,
- "received_amount": paid_amount
+ "received_amount": paid_amount,
}
payment_entry = frappe.new_doc("Payment Entry")
-
payment_entry.update(payment_entry_dict)
if mode_of_payment:
- payment_entry.mode_of_payment = mode_of_payment
+ payment_entry.mode_of_payment = mode_of_payment
if project:
- payment_entry.project = project
+ payment_entry.project = project
if cost_center:
- payment_entry.cost_center = cost_center
+ payment_entry.cost_center = cost_center
if payment_type == "Receive":
payment_entry.paid_to = company_account
else:
@@ -207,78 +256,111 @@ def create_payment_entry_bts( bank_transaction_name, reference_number=None, refe
payment_entry.insert()
payment_entry.submit()
- vouchers = json.dumps([{
- "payment_doctype":"Payment Entry",
- "payment_name":payment_entry.name,
- "amount":paid_amount}])
+ vouchers = json.dumps(
+ [{"payment_doctype": "Payment Entry", "payment_name": payment_entry.name, "amount": paid_amount}]
+ )
return reconcile_vouchers(bank_transaction.name, vouchers)
+
@frappe.whitelist()
def reconcile_vouchers(bank_transaction_name, vouchers):
# updated clear date of all the vouchers based on the bank transaction
vouchers = json.loads(vouchers)
transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
+ 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)
+ 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"))
+ 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)
+ 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.append(
+ "payment_entries",
+ {
+ "payment_document": voucher["payment_entry"].doctype,
+ "payment_entry": voucher["payment_entry"].name,
+ "allocated_amount": allocated_amount,
+ },
+ )
transaction.save()
transaction.update_allocations()
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):
# 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]
+ "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
+
def check_matching(bank_account, company, transaction, document_types):
- # combine all types of vocuhers
+ # combine all types of vouchers
subquery = get_queries(bank_account, company, transaction, document_types)
filters = {
- "amount": transaction.unallocated_amount,
- "payment_type" : "Receive" if transaction.deposit > 0 else "Pay",
- "reference_no": transaction.reference_number,
- "party_type": transaction.party_type,
- "party": transaction.party,
- "bank_account": bank_account
- }
+ "amount": transaction.unallocated_amount,
+ "payment_type": "Receive" if transaction.deposit > 0 else "Pay",
+ "reference_no": transaction.reference_number,
+ "party_type": transaction.party_type,
+ "party": transaction.party,
+ "bank_account": bank_account,
+ }
matching_vouchers = []
+
+ matching_vouchers.extend(get_loan_vouchers(bank_account, transaction, document_types, filters))
+
for query in subquery:
matching_vouchers.extend(
- frappe.db.sql(query, filters,)
+ frappe.db.sql(
+ query,
+ filters,
+ )
)
- return sorted(matching_vouchers, key = lambda x: x[0], reverse=True) if matching_vouchers else []
+ return sorted(matching_vouchers, key=lambda x: x[0], reverse=True) if matching_vouchers else []
+
def get_queries(bank_account, company, transaction, document_types):
# get queries to get matching vouchers
@@ -295,7 +377,7 @@ def get_queries(bank_account, company, transaction, document_types):
queries.extend([je_amount_matching])
if transaction.deposit > 0 and "sales_invoice" in document_types:
- si_amount_matching = get_si_matching_query(amount_condition)
+ si_amount_matching = get_si_matching_query(amount_condition)
queries.extend([si_amount_matching])
if transaction.withdrawal > 0:
@@ -309,13 +391,104 @@ def get_queries(bank_account, company, transaction, document_types):
return queries
+
+def get_loan_vouchers(bank_account, transaction, document_types, filters):
+ 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.deposit > 0 and "loan_repayment" in document_types:
+ vouchers.extend(get_lr_matching_query(bank_account, amount_condition, filters))
+
+ return vouchers
+
+
+def get_ld_matching_query(bank_account, amount_condition, 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(
+ "party_type"
+ ) and loan_disbursement.applicant == filters.get("party")
+
+ rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
+
+ rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(loan_disbursement)
+ .select(
+ rank + rank1 + 1,
+ ConstantColumn("Loan Disbursement").as_("doctype"),
+ loan_disbursement.name,
+ loan_disbursement.disbursed_amount,
+ loan_disbursement.reference_number,
+ loan_disbursement.reference_date,
+ loan_disbursement.applicant_type,
+ loan_disbursement.disbursement_date,
+ )
+ .where(loan_disbursement.docstatus == 1)
+ .where(loan_disbursement.clearance_date.isnull())
+ .where(loan_disbursement.disbursement_account == bank_account)
+ )
+
+ if amount_condition:
+ query.where(loan_disbursement.disbursed_amount == filters.get("amount"))
+ else:
+ query.where(loan_disbursement.disbursed_amount <= filters.get("amount"))
+
+ vouchers = query.run(as_list=True)
+
+ return vouchers
+
+
+def get_lr_matching_query(bank_account, amount_condition, filters):
+ loan_repayment = frappe.qb.DocType("Loan Repayment")
+ matching_reference = loan_repayment.reference_number == filters.get("reference_number")
+ matching_party = loan_repayment.applicant_type == filters.get(
+ "party_type"
+ ) and loan_repayment.applicant == filters.get("party")
+
+ rank = frappe.qb.terms.Case().when(matching_reference, 1).else_(0)
+
+ rank1 = frappe.qb.terms.Case().when(matching_party, 1).else_(0)
+
+ query = (
+ frappe.qb.from_(loan_repayment)
+ .select(
+ rank + rank1 + 1,
+ ConstantColumn("Loan Repayment").as_("doctype"),
+ loan_repayment.name,
+ loan_repayment.amount_paid,
+ loan_repayment.reference_number,
+ loan_repayment.reference_date,
+ loan_repayment.applicant_type,
+ loan_repayment.posting_date,
+ )
+ .where(loan_repayment.docstatus == 1)
+ .where(loan_repayment.repay_from_salary == 0)
+ .where(loan_repayment.clearance_date.isnull())
+ .where(loan_repayment.payment_account == bank_account)
+ )
+
+ if amount_condition:
+ query.where(loan_repayment.amount_paid == filters.get("amount"))
+ else:
+ query.where(loan_repayment.amount_paid <= filters.get("amount"))
+
+ vouchers = query.run()
+
+ return vouchers
+
+
def get_pe_matching_query(amount_condition, account_from_to, transaction):
# get matching payment entries query
if transaction.deposit > 0:
currency_field = "paid_to_account_currency as currency"
else:
currency_field = "paid_from_account_currency as currency"
- return f"""
+ 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
@@ -343,13 +516,10 @@ def get_pe_matching_query(amount_condition, account_from_to, transaction):
def get_je_matching_query(amount_condition, transaction):
# get matching journal entry query
- company_account = frappe.get_value("Bank Account", transaction.bank_account, "account")
- root_type = frappe.get_value("Account", company_account, "root_type")
-
- if root_type == "Liability":
- cr_or_dr = "debit" if transaction.withdrawal > 0 else "credit"
- else:
- cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
+ # 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"
return f"""
@@ -407,6 +577,7 @@ def get_si_matching_query(amount_condition):
AND si.docstatus = 1
"""
+
def get_pi_matching_query(amount_condition):
# get matching purchase invoice query
return f"""
@@ -432,11 +603,16 @@ def get_pi_matching_query(amount_condition):
AND cash_bank_account = %(bank_account)s
"""
+
def get_ec_matching_query(bank_account, company, amount_condition):
# get matching Expense Claim query
- mode_of_payments = [x["parent"] for x in frappe.db.get_all("Mode of Payment Account",
- filters={"default_account": bank_account}, fields=["parent"])]
- mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )'
+ mode_of_payments = [
+ x["parent"]
+ for x in frappe.db.get_all(
+ "Mode of Payment Account", filters={"default_account": bank_account}, fields=["parent"]
+ )
+ ]
+ mode_of_payments = "('" + "', '".join(mode_of_payments) + "' )"
company_currency = get_company_currency(company)
return f"""
SELECT
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
index 016f29a7b51..866633f74e5 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
@@ -239,7 +239,8 @@ frappe.ui.form.on("Bank Statement Import", {
"withdrawal",
"description",
"reference_number",
- "bank_account"
+ "bank_account",
+ "currency"
],
},
});
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
index c57e862892c..0370b3d91db 100644
--- a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -17,6 +17,8 @@ from openpyxl.styles import Font
from openpyxl.utils import get_column_letter
from six import string_types
+INVALID_VALUES = ("", None)
+
class BankStatementImport(DataImport):
def __init__(self, *args, **kwargs):
@@ -49,16 +51,14 @@ class BankStatementImport(DataImport):
self.import_file, self.google_sheets_url
)
- if 'Bank Account' not in json.dumps(preview['columns']):
+ if "Bank Account" not in json.dumps(preview["columns"]):
frappe.throw(_("Please add the Bank Account column"))
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")
- )
+ frappe.throw(_("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive"))
enqueued_jobs = [d.get("job_name") for d in get_info()]
@@ -81,22 +81,41 @@ class BankStatementImport(DataImport):
return False
+
@frappe.whitelist()
def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
import_file, google_sheets_url
)
+
@frappe.whitelist()
def form_start_import(data_import):
return frappe.get_doc("Bank Statement Import", data_import).start_import()
+
@frappe.whitelist()
def download_errored_template(data_import_name):
data_import = frappe.get_doc("Bank Statement Import", data_import_name)
data_import.export_errored_rows()
-def start_import(data_import, bank_account, import_file_path, google_sheets_url, bank, template_options):
+
+def parse_data_from_template(raw_data):
+ data = []
+
+ for i, row in enumerate(raw_data):
+ if all(v in INVALID_VALUES for v in row):
+ # empty row
+ continue
+
+ data.append(row)
+
+ return data
+
+
+def start_import(
+ data_import, bank_account, import_file_path, google_sheets_url, bank, template_options
+):
"""This method runs in background job"""
update_mapping_db(bank, template_options)
@@ -104,8 +123,9 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
data_import = frappe.get_doc("Bank Statement Import", data_import)
file = import_file_path if import_file_path else google_sheets_url
- import_file = ImportFile("Bank Transaction", file = file, import_type="Insert New Records")
- data = import_file.raw_data
+ import_file = ImportFile("Bank Transaction", file=file, import_type="Insert New Records")
+
+ data = parse_data_from_template(import_file.raw_data)
if import_file_path:
add_bank_account(data, bank_account)
@@ -123,16 +143,18 @@ def start_import(data_import, bank_account, import_file_path, google_sheets_url,
frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name})
+
def update_mapping_db(bank, template_options):
bank = frappe.get_doc("Bank", bank)
for d in bank.bank_transaction_mapping:
d.delete()
for d in json.loads(template_options)["column_to_field_map"].items():
- bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1] ,"file_field": d[0]} )
+ bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1], "file_field": d[0]})
bank.save()
+
def add_bank_account(data, bank_account):
bank_account_loc = None
if "Bank Account" not in data[0]:
@@ -148,6 +170,7 @@ def add_bank_account(data, bank_account):
else:
row.append(bank_account)
+
def write_files(import_file, data):
full_file_path = import_file.file_doc.get_full_path()
parts = import_file.file_doc.get_extension()
@@ -155,11 +178,12 @@ def write_files(import_file, data):
extension = extension.lstrip(".")
if extension == "csv":
- with open(full_file_path, 'w', newline='') as file:
+ with open(full_file_path, "w", newline="") as file:
writer = csv.writer(file)
writer.writerows(data)
elif extension == "xlsx" or "xls":
- write_xlsx(data, "trans", file_path = full_file_path)
+ write_xlsx(data, "trans", file_path=full_file_path)
+
def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
# from xlsx utils with changes
@@ -174,19 +198,21 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
ws.column_dimensions[get_column_letter(i + 1)].width = column_width
row1 = ws.row_dimensions[1]
- row1.font = Font(name='Calibri', bold=True)
+ row1.font = Font(name="Calibri", bold=True)
for row in data:
clean_row = []
for item in row:
- if isinstance(item, string_types) and (sheet_name not in ['Data Import Template', 'Data Export']):
+ if isinstance(item, string_types) and (
+ sheet_name not in ["Data Import Template", "Data Export"]
+ ):
value = handle_html(item)
else:
value = item
if isinstance(item, string_types) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
# Remove illegal characters from the string
- value = re.sub(ILLEGAL_CHARACTERS_RE, '', value)
+ value = re.sub(ILLEGAL_CHARACTERS_RE, "", value)
clean_row.append(value)
@@ -195,19 +221,20 @@ def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
wb.save(file_path)
return True
+
@frappe.whitelist()
def upload_bank_statement(**args):
args = frappe._dict(args)
bsi = frappe.new_doc("Bank Statement Import")
if args.company:
- bsi.update({
- "company": args.company,
- })
+ bsi.update(
+ {
+ "company": args.company,
+ }
+ )
if args.bank_account:
- bsi.update({
- "bank_account": args.bank_account
- })
+ bsi.update({"bank_account": args.bank_account})
return bsi
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 46200873044..e43f18b5c74 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -28,17 +28,26 @@ class BankTransaction(StatusUpdater):
def update_allocations(self):
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 = reduce(
+ lambda x, y: flt(x) + flt(y), [x.allocated_amount for x in self.payment_entries]
+ )
else:
allocated_amount = 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))
+ frappe.db.set_value(
+ self.doctype,
+ self.name,
+ "unallocated_amount",
+ abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount),
+ )
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)))
+ frappe.db.set_value(
+ self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit))
+ )
amount = self.deposit or self.withdrawal
if amount == self.allocated_amount:
@@ -48,7 +57,14 @@ class BankTransaction(StatusUpdater):
def clear_linked_payment_entries(self, for_cancel=False):
for payment_entry in self.payment_entries:
- if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
+ if payment_entry.payment_document in [
+ "Payment Entry",
+ "Journal Entry",
+ "Purchase Invoice",
+ "Expense Claim",
+ "Loan Repayment",
+ "Loan Disbursement",
+ ]:
self.clear_simple_entry(payment_entry, for_cancel=for_cancel)
elif payment_entry.payment_document == "Sales Invoice":
@@ -56,38 +72,41 @@ class BankTransaction(StatusUpdater):
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 (
+ frappe.db.get_value("Payment Entry", payment_entry.payment_entry, "payment_type")
+ == "Internal Transfer"
+ ):
if len(get_reconciled_bank_transactions(payment_entry)) < 2:
return
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)
+ payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", clearance_date
+ )
def clear_sales_invoice(self, payment_entry, for_cancel=False):
clearance_date = self.date if not for_cancel else None
frappe.db.set_value(
"Sales Invoice Payment",
- dict(
- parenttype=payment_entry.payment_document,
- parent=payment_entry.payment_entry
- ),
- "clearance_date", clearance_date)
+ dict(parenttype=payment_entry.payment_document, parent=payment_entry.payment_entry),
+ "clearance_date",
+ clearance_date,
+ )
+
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']
+ "Bank Transaction Payments",
+ filters={"payment_entry": payment_entry.payment_entry},
+ fields=["parent"],
)
return reconciled_bank_transactions
+
def get_total_allocated_amount(payment_entry):
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
SELECT
SUM(btp.allocated_amount) as allocated_amount,
bt.name
@@ -100,36 +119,73 @@ def get_total_allocated_amount(payment_entry):
AND
btp.payment_entry = %s
AND
- bt.docstatus = 1""", (payment_entry.payment_document, payment_entry.payment_entry), as_dict=True)
+ bt.docstatus = 1""",
+ (payment_entry.payment_document, payment_entry.payment_entry),
+ as_dict=True,
+ )
-def get_paid_amount(payment_entry, currency):
+
+def get_paid_amount(payment_entry, currency, bank_account):
if payment_entry.payment_document in ["Payment Entry", "Sales Invoice", "Purchase Invoice"]:
paid_amount_field = "paid_amount"
- if payment_entry.payment_document == 'Payment Entry':
+ if payment_entry.payment_document == "Payment Entry":
doc = frappe.get_doc("Payment Entry", payment_entry.payment_entry)
- paid_amount_field = ("base_paid_amount"
- if doc.paid_to_account_currency == currency else "paid_amount")
- return frappe.db.get_value(payment_entry.payment_document,
- payment_entry.payment_entry, paid_amount_field)
+ if doc.payment_type == "Receive":
+ paid_amount_field = (
+ "received_amount" if doc.paid_to_account_currency == currency else "base_received_amount"
+ )
+ elif doc.payment_type == "Pay":
+ paid_amount_field = (
+ "paid_amount" if doc.paid_to_account_currency == currency else "base_paid_amount"
+ )
+
+ return frappe.db.get_value(
+ payment_entry.payment_document, payment_entry.payment_entry, paid_amount_field
+ )
elif payment_entry.payment_document == "Journal Entry":
- return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_credit")
+ return frappe.db.get_value(
+ "Journal Entry Account",
+ {"parent": payment_entry.payment_entry, "account": bank_account},
+ "sum(credit_in_account_currency)",
+ )
elif payment_entry.payment_document == "Expense Claim":
- return frappe.db.get_value(payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed")
+ return frappe.db.get_value(
+ payment_entry.payment_document, payment_entry.payment_entry, "total_amount_reimbursed"
+ )
+
+ elif payment_entry.payment_document == "Loan Disbursement":
+ return frappe.db.get_value(
+ payment_entry.payment_document, payment_entry.payment_entry, "disbursed_amount"
+ )
+
+ elif payment_entry.payment_document == "Loan Repayment":
+ return frappe.db.get_value(
+ payment_entry.payment_document, payment_entry.payment_entry, "amount_paid"
+ )
else:
- frappe.throw("Please reconcile {0}: {1} manually".format(payment_entry.payment_document, payment_entry.payment_entry))
+ frappe.throw(
+ "Please reconcile {0}: {1} manually".format(
+ payment_entry.payment_document, payment_entry.payment_entry
+ )
+ )
+
@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)
+ 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)
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py
index 6125c27cdfd..494459ef0ea 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py
@@ -19,12 +19,14 @@ def upload_bank_statement():
fcontent = frappe.local.uploaded_file
fname = frappe.local.uploaded_filename
- if frappe.safe_encode(fname).lower().endswith("csv".encode('utf-8')):
+ if frappe.safe_encode(fname).lower().endswith("csv".encode("utf-8")):
from frappe.utils.csvutils import read_csv_content
+
rows = read_csv_content(fcontent, False)
- elif frappe.safe_encode(fname).lower().endswith("xlsx".encode('utf-8')):
+ elif frappe.safe_encode(fname).lower().endswith("xlsx".encode("utf-8")):
from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
+
rows = read_xlsx_file_from_attached_file(fcontent=fcontent)
columns = rows[0]
@@ -44,12 +46,10 @@ def create_bank_entries(columns, data, bank_account):
continue
fields = {}
for key, value in iteritems(header_map):
- fields.update({key: d[int(value)-1]})
+ fields.update({key: d[int(value) - 1]})
try:
- bank_transaction = frappe.get_doc({
- "doctype": "Bank Transaction"
- })
+ bank_transaction = frappe.get_doc({"doctype": "Bank Transaction"})
bank_transaction.update(fields)
bank_transaction.date = getdate(parse_date(bank_transaction.date))
bank_transaction.bank_account = bank_account
@@ -62,6 +62,7 @@ def create_bank_entries(columns, data, bank_account):
return {"success": success, "errors": errors}
+
def get_header_mapping(columns, bank_account):
mapping = get_bank_mapping(bank_account)
@@ -72,10 +73,11 @@ def get_header_mapping(columns, bank_account):
return header_map
+
def get_bank_mapping(bank_account):
bank_name = frappe.db.get_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}
+ mapping = {row.file_field: row.bank_transaction_field for row in bank.bank_transaction_mapping}
return mapping
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 72b6893faf5..ad8e73d27bf 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -17,6 +17,7 @@ from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sal
test_dependencies = ["Item", "Cost Center"]
+
class TestBankTransaction(unittest.TestCase):
@classmethod
def setUpClass(cls):
@@ -41,21 +42,34 @@ class TestBankTransaction(unittest.TestCase):
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"))
- linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"),
+ )
+ linked_payments = get_linked_payments(bank_transaction.name, ["payment_entry", "exact_match"])
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
def test_reconcile(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"),
+ )
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1700))
- vouchers = json.dumps([{
- "payment_doctype":"Payment Entry",
- "payment_name":payment.name,
- "amount":bank_transaction.unallocated_amount}])
+ vouchers = json.dumps(
+ [
+ {
+ "payment_doctype": "Payment Entry",
+ "payment_name": payment.name,
+ "amount": bank_transaction.unallocated_amount,
+ }
+ ]
+ )
reconcile_vouchers(bank_transaction.name, vouchers)
- unallocated_amount = frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount")
+ unallocated_amount = frappe.db.get_value(
+ "Bank Transaction", bank_transaction.name, "unallocated_amount"
+ )
self.assertTrue(unallocated_amount == 0)
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
@@ -69,122 +83,177 @@ class TestBankTransaction(unittest.TestCase):
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self):
- bank_transaction = frappe.get_doc("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'])
+ bank_transaction = frappe.get_doc(
+ "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"])
self.assertTrue(linked_payments[0][3])
# Check error if already reconciled
def test_already_reconciled(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"),
+ )
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
- vouchers = json.dumps([{
- "payment_doctype":"Payment Entry",
- "payment_name":payment.name,
- "amount":bank_transaction.unallocated_amount}])
+ vouchers = json.dumps(
+ [
+ {
+ "payment_doctype": "Payment Entry",
+ "payment_name": payment.name,
+ "amount": bank_transaction.unallocated_amount,
+ }
+ ]
+ )
reconcile_vouchers(bank_transaction.name, vouchers)
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"),
+ )
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
- vouchers = json.dumps([{
- "payment_doctype":"Payment Entry",
- "payment_name":payment.name,
- "amount":bank_transaction.unallocated_amount}])
- self.assertRaises(frappe.ValidationError, reconcile_vouchers, bank_transaction_name=bank_transaction.name, vouchers=vouchers)
+ vouchers = json.dumps(
+ [
+ {
+ "payment_doctype": "Payment Entry",
+ "payment_name": payment.name,
+ "amount": bank_transaction.unallocated_amount,
+ }
+ ]
+ )
+ self.assertRaises(
+ frappe.ValidationError,
+ reconcile_vouchers,
+ bank_transaction_name=bank_transaction.name,
+ vouchers=vouchers,
+ )
# Raise an error if debitor transaction vs debitor payment
def test_clear_sales_invoice(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"))
+ bank_transaction = frappe.get_doc(
+ "Bank Transaction",
+ dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"),
+ )
payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"]))
- vouchers = json.dumps([{
- "payment_doctype":"Sales Invoice",
- "payment_name":payment.name,
- "amount":bank_transaction.unallocated_amount}])
+ vouchers = json.dumps(
+ [
+ {
+ "payment_doctype": "Sales Invoice",
+ "payment_name": payment.name,
+ "amount": bank_transaction.unallocated_amount,
+ }
+ ]
+ )
reconcile_vouchers(bank_transaction.name, vouchers=vouchers)
- self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0)
- self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None)
+ self.assertEqual(
+ frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0
+ )
+ self.assertTrue(
+ frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date")
+ is not None
+ )
+
def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"):
try:
- frappe.get_doc({
- "doctype": "Bank",
- "bank_name":bank_name,
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Bank",
+ "bank_name": bank_name,
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
try:
- frappe.get_doc({
- "doctype": "Bank Account",
- "account_name":"Checking Account",
- "bank": bank_name,
- "account": account_name
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Bank Account",
+ "account_name": "Checking Account",
+ "bank": bank_name,
+ "account": account_name,
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
+
def add_transactions():
create_bank_account()
- doc = frappe.get_doc({
- "doctype": "Bank Transaction",
- "description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
- "date": "2018-10-23",
- "deposit": 1200,
- "currency": "INR",
- "bank_account": "Checking Account - Citi Bank"
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": "1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
+ "date": "2018-10-23",
+ "deposit": 1200,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ }
+ ).insert()
doc.submit()
- doc = frappe.get_doc({
- "doctype": "Bank Transaction",
- "description":"1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
- "date": "2018-10-23",
- "deposit": 1700,
- "currency": "INR",
- "bank_account": "Checking Account - Citi Bank"
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": "1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
+ "date": "2018-10-23",
+ "deposit": 1700,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ }
+ ).insert()
doc.submit()
- doc = frappe.get_doc({
- "doctype": "Bank Transaction",
- "description":"Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
- "date": "2018-10-26",
- "withdrawal": 690,
- "currency": "INR",
- "bank_account": "Checking Account - Citi Bank"
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": "Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
+ "date": "2018-10-26",
+ "withdrawal": 690,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ }
+ ).insert()
doc.submit()
- doc = frappe.get_doc({
- "doctype": "Bank Transaction",
- "description":"Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
- "date": "2018-10-27",
- "deposit": 3900,
- "currency": "INR",
- "bank_account": "Checking Account - Citi Bank"
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": "Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
+ "date": "2018-10-27",
+ "deposit": 3900,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ }
+ ).insert()
doc.submit()
- doc = frappe.get_doc({
- "doctype": "Bank Transaction",
- "description":"I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
- "date": "2018-10-27",
- "withdrawal": 109080,
- "currency": "INR",
- "bank_account": "Checking Account - Citi Bank"
- }).insert()
+ doc = frappe.get_doc(
+ {
+ "doctype": "Bank Transaction",
+ "description": "I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
+ "date": "2018-10-27",
+ "withdrawal": 109080,
+ "currency": "INR",
+ "bank_account": "Checking Account - Citi Bank",
+ }
+ ).insert()
doc.submit()
def add_vouchers():
try:
- frappe.get_doc({
- "doctype": "Supplier",
- "supplier_group":"All Supplier Groups",
- "supplier_type": "Company",
- "supplier_name": "Conrad Electronic"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Supplier",
+ "supplier_group": "All Supplier Groups",
+ "supplier_type": "Company",
+ "supplier_name": "Conrad Electronic",
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
@@ -198,12 +267,14 @@ def add_vouchers():
pe.submit()
try:
- frappe.get_doc({
- "doctype": "Supplier",
- "supplier_group":"All Supplier Groups",
- "supplier_type": "Company",
- "supplier_name": "Mr G"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Supplier",
+ "supplier_group": "All Supplier Groups",
+ "supplier_type": "Company",
+ "supplier_name": "Mr G",
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
@@ -222,26 +293,30 @@ def add_vouchers():
pe.submit()
try:
- frappe.get_doc({
- "doctype": "Supplier",
- "supplier_group":"All Supplier Groups",
- "supplier_type": "Company",
- "supplier_name": "Poore Simon's"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Supplier",
+ "supplier_group": "All Supplier Groups",
+ "supplier_type": "Company",
+ "supplier_name": "Poore Simon's",
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
try:
- frappe.get_doc({
- "doctype": "Customer",
- "customer_group":"All Customer Groups",
- "customer_type": "Company",
- "customer_name": "Poore Simon's"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_group": "All Customer Groups",
+ "customer_type": "Company",
+ "customer_name": "Poore Simon's",
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
- pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save =1)
+ pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save=1)
pi.cash_bank_account = "_Test Bank - _TC"
pi.insert()
pi.submit()
@@ -261,33 +336,31 @@ def add_vouchers():
pe.submit()
try:
- frappe.get_doc({
- "doctype": "Customer",
- "customer_group":"All Customer Groups",
- "customer_type": "Company",
- "customer_name": "Fayva"
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_group": "All Customer Groups",
+ "customer_type": "Company",
+ "customer_name": "Fayva",
+ }
+ ).insert()
except frappe.DuplicateEntryError:
pass
- mode_of_payment = frappe.get_doc({
- "doctype": "Mode of Payment",
- "name": "Cash"
- })
+ mode_of_payment = frappe.get_doc({"doctype": "Mode of Payment", "name": "Cash"})
- if not frappe.db.get_value('Mode of Payment Account', {'company': "_Test Company", 'parent': "Cash"}):
- mode_of_payment.append("accounts", {
- "company": "_Test Company",
- "default_account": "_Test Bank - _TC"
- })
+ if not frappe.db.get_value(
+ "Mode of Payment Account", {"company": "_Test Company", "parent": "Cash"}
+ ):
+ mode_of_payment.append(
+ "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"}
+ )
mode_of_payment.save()
si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_save=1)
si.is_pos = 1
- si.append("payments", {
- "mode_of_payment": "Cash",
- "account": "_Test Bank - _TC",
- "amount": 109080
- })
+ si.append(
+ "payments", {"mode_of_payment": "Cash", "account": "_Test Bank - _TC", "amount": 109080}
+ )
si.insert()
si.submit()
diff --git a/erpnext/accounts/doctype/budget/budget.py b/erpnext/accounts/doctype/budget/budget.py
index 492bb365589..5527f9fb99f 100644
--- a/erpnext/accounts/doctype/budget/budget.py
+++ b/erpnext/accounts/doctype/budget/budget.py
@@ -14,13 +14,19 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (
from erpnext.accounts.utils import get_fiscal_year
-class BudgetError(frappe.ValidationError): pass
-class DuplicateBudgetError(frappe.ValidationError): pass
+class BudgetError(frappe.ValidationError):
+ pass
+
+
+class DuplicateBudgetError(frappe.ValidationError):
+ pass
+
class Budget(Document):
def autoname(self):
- self.name = make_autoname(self.get(frappe.scrub(self.budget_against))
- + "/" + self.fiscal_year + "/.###")
+ 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)):
@@ -35,34 +41,44 @@ class Budget(Document):
budget_against = self.get(budget_against_field)
accounts = [d.account for d in self.accounts] or []
- existing_budget = frappe.db.sql("""
+ existing_budget = frappe.db.sql(
+ """
select
b.name, ba.account from `tabBudget` b, `tabBudget Account` ba
where
ba.parent = b.name and b.docstatus < 2 and b.company = %s and %s=%s and
b.fiscal_year=%s and b.name != %s and ba.account in (%s) """
- % ('%s', budget_against_field, '%s', '%s', '%s', ','.join(['%s'] * len(accounts))),
- (self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts), as_dict=1)
+ % ("%s", budget_against_field, "%s", "%s", "%s", ",".join(["%s"] * len(accounts))),
+ (self.company, budget_against, self.fiscal_year, self.name) + tuple(accounts),
+ as_dict=1,
+ )
for d in existing_budget:
- frappe.throw(_("Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}")
- .format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year), DuplicateBudgetError)
+ frappe.throw(
+ _(
+ "Another Budget record '{0}' already exists against {1} '{2}' and account '{3}' for fiscal year {4}"
+ ).format(d.name, self.budget_against, budget_against, d.account, self.fiscal_year),
+ DuplicateBudgetError,
+ )
def validate_accounts(self):
account_list = []
- for d in self.get('accounts'):
+ for d in self.get("accounts"):
if d.account:
- account_details = frappe.db.get_value("Account", d.account,
- ["is_group", "company", "report_type"], as_dict=1)
+ account_details = frappe.db.get_value(
+ "Account", d.account, ["is_group", "company", "report_type"], as_dict=1
+ )
if account_details.is_group:
frappe.throw(_("Budget cannot be assigned against Group Account {0}").format(d.account))
elif account_details.company != self.company:
- frappe.throw(_("Account {0} does not belongs to company {1}")
- .format(d.account, self.company))
+ frappe.throw(_("Account {0} does not belongs to company {1}").format(d.account, self.company))
elif account_details.report_type != "Profit and Loss":
- frappe.throw(_("Budget cannot be assigned against {0}, as it's not an Income or Expense account")
- .format(d.account))
+ frappe.throw(
+ _("Budget cannot be assigned against {0}, as it's not an Income or Expense account").format(
+ d.account
+ )
+ )
if d.account in account_list:
frappe.throw(_("Account {0} has been entered multiple times").format(d.account))
@@ -70,51 +86,66 @@ class Budget(Document):
account_list.append(d.account)
def set_null_value(self):
- if self.budget_against == 'Cost Center':
+ if self.budget_against == "Cost Center":
self.project = None
else:
self.cost_center = None
def validate_applicable_for(self):
- if (self.applicable_on_material_request
- and not (self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses)):
- frappe.throw(_("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses"))
+ if self.applicable_on_material_request and not (
+ self.applicable_on_purchase_order and self.applicable_on_booking_actual_expenses
+ ):
+ frappe.throw(
+ _("Please enable Applicable on Purchase Order and Applicable on Booking Actual Expenses")
+ )
- elif (self.applicable_on_purchase_order
- and not (self.applicable_on_booking_actual_expenses)):
+ elif self.applicable_on_purchase_order and not (self.applicable_on_booking_actual_expenses):
frappe.throw(_("Please enable Applicable on Booking Actual Expenses"))
- elif not(self.applicable_on_material_request
- or self.applicable_on_purchase_order or self.applicable_on_booking_actual_expenses):
+ elif not (
+ self.applicable_on_material_request
+ or self.applicable_on_purchase_order
+ or self.applicable_on_booking_actual_expenses
+ ):
self.applicable_on_booking_actual_expenses = 1
+
def validate_expense_against_budget(args):
args = frappe._dict(args)
- if args.get('company') and not args.fiscal_year:
- args.fiscal_year = get_fiscal_year(args.get('posting_date'), company=args.get('company'))[0]
- frappe.flags.exception_approver_role = frappe.get_cached_value('Company',
- args.get('company'), 'exception_budget_approver_role')
+ if args.get("company") and not args.fiscal_year:
+ args.fiscal_year = get_fiscal_year(args.get("posting_date"), company=args.get("company"))[0]
+ frappe.flags.exception_approver_role = frappe.get_cached_value(
+ "Company", args.get("company"), "exception_budget_approver_role"
+ )
if not args.account:
args.account = args.get("expense_account")
- if not (args.get('account') and args.get('cost_center')) and args.item_code:
+ if not (args.get("account") and args.get("cost_center")) and args.item_code:
args.cost_center, args.account = get_item_details(args)
if not args.account:
return
- for budget_against in ['project', 'cost_center'] + get_accounting_dimensions():
- if (args.get(budget_against) and args.account
- and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})):
+ for budget_against in ["project", "cost_center"] + get_accounting_dimensions():
+ if (
+ args.get(budget_against)
+ and args.account
+ and frappe.db.get_value("Account", {"name": args.account, "root_type": "Expense"})
+ ):
doctype = frappe.unscrub(budget_against)
- if frappe.get_cached_value('DocType', doctype, 'is_tree'):
+ if frappe.get_cached_value("DocType", doctype, "is_tree"):
lft, rgt = frappe.db.get_value(doctype, args.get(budget_against), ["lft", "rgt"])
condition = """and exists(select name from `tab%s`
- where lft<=%s and rgt>=%s and name=b.%s)""" % (doctype, lft, rgt, budget_against) #nosec
+ where lft<=%s and rgt>=%s and name=b.%s)""" % (
+ doctype,
+ lft,
+ rgt,
+ budget_against,
+ ) # nosec
args.is_tree = True
else:
condition = "and b.%s=%s" % (budget_against, frappe.db.escape(args.get(budget_against)))
@@ -123,7 +154,8 @@ def validate_expense_against_budget(args):
args.budget_against_field = budget_against
args.budget_against_doctype = doctype
- budget_records = frappe.db.sql("""
+ budget_records = frappe.db.sql(
+ """
select
b.{budget_against_field} as budget_against, ba.budget_amount, b.monthly_distribution,
ifnull(b.applicable_on_material_request, 0) as for_material_request,
@@ -138,11 +170,17 @@ def validate_expense_against_budget(args):
b.name=ba.parent and b.fiscal_year=%s
and ba.account=%s and b.docstatus=1
{condition}
- """.format(condition=condition, budget_against_field=budget_against), (args.fiscal_year, args.account), as_dict=True) #nosec
+ """.format(
+ condition=condition, budget_against_field=budget_against
+ ),
+ (args.fiscal_year, args.account),
+ as_dict=True,
+ ) # nosec
if budget_records:
validate_budget_records(args, budget_records)
+
def validate_budget_records(args, budget_records):
for budget in budget_records:
if flt(budget.budget_amount):
@@ -150,88 +188,118 @@ def validate_budget_records(args, budget_records):
yearly_action, monthly_action = get_actions(args, budget)
if monthly_action in ["Stop", "Warn"]:
- budget_amount = get_accumulated_monthly_budget(budget.monthly_distribution,
- args.posting_date, args.fiscal_year, budget.budget_amount)
+ budget_amount = get_accumulated_monthly_budget(
+ budget.monthly_distribution, args.posting_date, args.fiscal_year, budget.budget_amount
+ )
args["month_end_date"] = get_last_day(args.posting_date)
- compare_expense_with_budget(args, budget_amount,
- _("Accumulated Monthly"), monthly_action, budget.budget_against, amount)
+ compare_expense_with_budget(
+ 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
+ )
- 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
- currency = frappe.get_cached_value('Company', args.company, 'default_currency')
+ currency = frappe.get_cached_value("Company", args.company, "default_currency")
msg = _("{0} Budget for Account {1} against {2} {3} is {4}. It will exceed by {5}").format(
- _(action_for), frappe.bold(args.account), args.budget_against_field,
- frappe.bold(budget_against),
- frappe.bold(fmt_money(budget_amount, currency=currency)),
- frappe.bold(fmt_money(diff, currency=currency)))
+ _(action_for),
+ frappe.bold(args.account),
+ args.budget_against_field,
+ frappe.bold(budget_against),
+ frappe.bold(fmt_money(budget_amount, currency=currency)),
+ frappe.bold(fmt_money(diff, currency=currency)),
+ )
- if (frappe.flags.exception_approver_role
- and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)):
+ if (
+ frappe.flags.exception_approver_role
+ and frappe.flags.exception_approver_role in frappe.get_roles(frappe.session.user)
+ ):
action = "Warn"
- if action=="Stop":
+ if action == "Stop":
frappe.throw(msg, BudgetError)
else:
- frappe.msgprint(msg, indicator='orange')
+ frappe.msgprint(msg, indicator="orange")
+
def get_actions(args, budget):
yearly_action = budget.action_if_annual_budget_exceeded
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded
- if args.get('doctype') == 'Material Request' and budget.for_material_request:
+ if args.get("doctype") == "Material Request" and budget.for_material_request:
yearly_action = budget.action_if_annual_budget_exceeded_on_mr
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_mr
- elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order:
+ elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
yearly_action = budget.action_if_annual_budget_exceeded_on_po
monthly_action = budget.action_if_accumulated_monthly_budget_exceeded_on_po
return yearly_action, monthly_action
+
def get_amount(args, budget):
amount = 0
- if args.get('doctype') == 'Material Request' and budget.for_material_request:
- amount = (get_requested_amount(args, budget)
- + get_ordered_amount(args, budget) + get_actual_expense(args))
+ if args.get("doctype") == "Material Request" and budget.for_material_request:
+ amount = (
+ get_requested_amount(args, budget) + get_ordered_amount(args, budget) + get_actual_expense(args)
+ )
- elif args.get('doctype') == 'Purchase Order' and budget.for_purchase_order:
+ elif args.get("doctype") == "Purchase Order" and budget.for_purchase_order:
amount = get_ordered_amount(args, budget) + get_actual_expense(args)
return amount
-def get_requested_amount(args, budget):
- item_code = args.get('item_code')
- condition = get_other_condition(args, budget, 'Material Request')
- data = frappe.db.sql(""" select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
+def get_requested_amount(args, budget):
+ item_code = args.get("item_code")
+ condition = get_other_condition(args, budget, "Material Request")
+
+ data = frappe.db.sql(
+ """ select ifnull((sum(child.stock_qty - child.ordered_qty) * rate), 0) as amount
from `tabMaterial Request Item` child, `tabMaterial Request` parent where parent.name = child.parent and
child.item_code = %s and parent.docstatus = 1 and child.stock_qty > child.ordered_qty and {0} and
- parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(condition), item_code, as_list=1)
+ parent.material_request_type = 'Purchase' and parent.status != 'Stopped'""".format(
+ condition
+ ),
+ item_code,
+ as_list=1,
+ )
return data[0][0] if data else 0
+
def get_ordered_amount(args, budget):
- item_code = args.get('item_code')
- condition = get_other_condition(args, budget, 'Purchase Order')
+ item_code = args.get("item_code")
+ condition = get_other_condition(args, budget, "Purchase Order")
- data = frappe.db.sql(""" select ifnull(sum(child.amount - child.billed_amt), 0) as amount
+ data = frappe.db.sql(
+ """ select ifnull(sum(child.amount - child.billed_amt), 0) as amount
from `tabPurchase Order Item` child, `tabPurchase Order` parent where
parent.name = child.parent and child.item_code = %s and parent.docstatus = 1 and child.amount > child.billed_amt
- and parent.status != 'Closed' and {0}""".format(condition), item_code, as_list=1)
+ and parent.status != 'Closed' and {0}""".format(
+ condition
+ ),
+ item_code,
+ as_list=1,
+ )
return data[0][0] if data else 0
+
def get_other_condition(args, budget, for_doc):
condition = "expense_account = '%s'" % (args.expense_account)
budget_against_field = args.get("budget_against_field")
@@ -239,41 +307,51 @@ def get_other_condition(args, budget, for_doc):
if budget_against_field and args.get(budget_against_field):
condition += " and child.%s = '%s'" % (budget_against_field, args.get(budget_against_field))
- 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('Fiscal Year', args.get('fiscal_year'),
- ['year_start_date', 'year_end_date'])
+ 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(
+ "Fiscal Year", args.get("fiscal_year"), ["year_start_date", "year_end_date"]
+ )
condition += """ and parent.%s
- between '%s' and '%s' """ %(date_field, start_date, end_date)
+ between '%s' and '%s' """ % (
+ date_field,
+ start_date,
+ end_date,
+ )
return condition
+
def get_actual_expense(args):
if not args.budget_against_doctype:
args.budget_against_doctype = frappe.unscrub(args.budget_against_field)
- budget_against_field = args.get('budget_against_field')
- condition1 = " and gle.posting_date <= %(month_end_date)s" \
- if args.get("month_end_date") else ""
+ budget_against_field = args.get("budget_against_field")
+ condition1 = " and gle.posting_date <= %(month_end_date)s" if args.get("month_end_date") else ""
if args.is_tree:
- lft_rgt = frappe.db.get_value(args.budget_against_doctype,
- args.get(budget_against_field), ["lft", "rgt"], as_dict=1)
+ lft_rgt = frappe.db.get_value(
+ args.budget_against_doctype, args.get(budget_against_field), ["lft", "rgt"], as_dict=1
+ )
args.update(lft_rgt)
condition2 = """and exists(select name from `tab{doctype}`
where lft>=%(lft)s and rgt<=%(rgt)s
- and name=gle.{budget_against_field})""".format(doctype=args.budget_against_doctype, #nosec
- budget_against_field=budget_against_field)
+ and name=gle.{budget_against_field})""".format(
+ doctype=args.budget_against_doctype, budget_against_field=budget_against_field # nosec
+ )
else:
condition2 = """and exists(select name from `tab{doctype}`
where name=gle.{budget_against} and
- gle.{budget_against} = %({budget_against})s)""".format(doctype=args.budget_against_doctype,
- budget_against = budget_against_field)
+ gle.{budget_against} = %({budget_against})s)""".format(
+ doctype=args.budget_against_doctype, budget_against=budget_against_field
+ )
- amount = flt(frappe.db.sql("""
+ amount = flt(
+ frappe.db.sql(
+ """
select sum(gle.debit) - sum(gle.credit)
from `tabGL Entry` gle
where gle.account=%(account)s
@@ -282,46 +360,59 @@ def get_actual_expense(args):
and gle.company=%(company)s
and gle.docstatus=1
{condition2}
- """.format(condition1=condition1, condition2=condition2), (args))[0][0]) #nosec
+ """.format(
+ condition1=condition1, condition2=condition2
+ ),
+ (args),
+ )[0][0]
+ ) # nosec
return amount
+
def get_accumulated_monthly_budget(monthly_distribution, posting_date, fiscal_year, annual_budget):
distribution = {}
if monthly_distribution:
- for d in frappe.db.sql("""select mdp.month, mdp.percentage_allocation
+ for d in frappe.db.sql(
+ """select mdp.month, mdp.percentage_allocation
from `tabMonthly Distribution Percentage` mdp, `tabMonthly Distribution` md
- where mdp.parent=md.name and md.fiscal_year=%s""", fiscal_year, as_dict=1):
- distribution.setdefault(d.month, d.percentage_allocation)
+ where mdp.parent=md.name and md.fiscal_year=%s""",
+ fiscal_year,
+ as_dict=1,
+ ):
+ distribution.setdefault(d.month, d.percentage_allocation)
dt = frappe.db.get_value("Fiscal Year", fiscal_year, "year_start_date")
accumulated_percentage = 0.0
- while(dt <= getdate(posting_date)):
+ while dt <= getdate(posting_date):
if monthly_distribution:
accumulated_percentage += distribution.get(getdate(dt).strftime("%B"), 0)
else:
- accumulated_percentage += 100.0/12
+ accumulated_percentage += 100.0 / 12
dt = add_months(dt, 1)
return annual_budget * accumulated_percentage / 100
+
def get_item_details(args):
cost_center, expense_account = None, None
- if not args.get('company'):
+ if not args.get("company"):
return cost_center, expense_account
if args.item_code:
- item_defaults = frappe.db.get_value('Item Default',
- {'parent': args.item_code, 'company': args.get('company')},
- ['buying_cost_center', 'expense_account'])
+ item_defaults = frappe.db.get_value(
+ "Item Default",
+ {"parent": args.item_code, "company": args.get("company")},
+ ["buying_cost_center", "expense_account"],
+ )
if item_defaults:
cost_center, expense_account = item_defaults
if not (cost_center and expense_account):
- for doctype in ['Item Group', 'Company']:
+ for doctype in ["Item Group", "Company"]:
data = get_expense_cost_center(doctype, args)
if not cost_center and data:
@@ -335,11 +426,15 @@ def get_item_details(args):
return cost_center, expense_account
+
def get_expense_cost_center(doctype, args):
- if doctype == 'Item Group':
- return frappe.db.get_value('Item Default',
- {'parent': args.get(frappe.scrub(doctype)), 'company': args.get('company')},
- ['buying_cost_center', 'expense_account'])
+ if doctype == "Item Group":
+ return frappe.db.get_value(
+ "Item Default",
+ {"parent": args.get(frappe.scrub(doctype)), "company": args.get("company")},
+ ["buying_cost_center", "expense_account"],
+ )
else:
- return frappe.db.get_value(doctype, args.get(frappe.scrub(doctype)),\
- ['cost_center', 'default_expense_account'])
+ return frappe.db.get_value(
+ doctype, args.get(frappe.scrub(doctype)), ["cost_center", "default_expense_account"]
+ )
diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py
index 9a83a0aa9a6..c48c7d97a2a 100644
--- a/erpnext/accounts/doctype/budget/test_budget.py
+++ b/erpnext/accounts/doctype/budget/test_budget.py
@@ -11,7 +11,8 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
-test_dependencies = ['Monthly Distribution']
+test_dependencies = ["Monthly Distribution"]
+
class TestBudget(unittest.TestCase):
def test_monthly_budget_crossed_ignore(self):
@@ -19,11 +20,18 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center")
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ submit=True,
+ )
- self.assertTrue(frappe.db.get_value("GL Entry",
- {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
+ self.assertTrue(
+ frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
+ )
budget.cancel()
jv.cancel()
@@ -33,10 +41,17 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center")
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -48,49 +63,65 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center")
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
- frappe.db.set_value('Company', budget.company, 'exception_budget_approver_role', 'Accounts User')
+ frappe.db.set_value("Company", budget.company, "exception_budget_approver_role", "Accounts User")
jv.submit()
- self.assertEqual(frappe.db.get_value('Journal Entry', jv.name, 'docstatus'), 1)
+ self.assertEqual(frappe.db.get_value("Journal Entry", jv.name, "docstatus"), 1)
jv.cancel()
- frappe.db.set_value('Company', budget.company, 'exception_budget_approver_role', '')
+ frappe.db.set_value("Company", budget.company, "exception_budget_approver_role", "")
budget.load_from_db()
budget.cancel()
def test_monthly_budget_crossed_for_mr(self):
- budget = make_budget(applicable_on_material_request=1,
- applicable_on_purchase_order=1, action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
- budget_against="Cost Center")
+ budget = make_budget(
+ applicable_on_material_request=1,
+ applicable_on_purchase_order=1,
+ action_if_accumulated_monthly_budget_exceeded_on_mr="Stop",
+ budget_against="Cost Center",
+ )
fiscal_year = get_fiscal_year(nowdate())[0]
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
- mr = frappe.get_doc({
- "doctype": "Material Request",
- "material_request_type": "Purchase",
- "transaction_date": nowdate(),
- "company": budget.company,
- "items": [{
- 'item_code': '_Test Item',
- 'qty': 1,
- 'uom': "_Test UOM",
- 'warehouse': '_Test Warehouse - _TC',
- 'schedule_date': nowdate(),
- 'rate': 100000,
- 'expense_account': '_Test Account Cost for Goods Sold - _TC',
- 'cost_center': '_Test Cost Center - _TC'
- }]
- })
+ mr = frappe.get_doc(
+ {
+ "doctype": "Material Request",
+ "material_request_type": "Purchase",
+ "transaction_date": nowdate(),
+ "company": budget.company,
+ "items": [
+ {
+ "item_code": "_Test Item",
+ "qty": 1,
+ "uom": "_Test UOM",
+ "warehouse": "_Test Warehouse - _TC",
+ "schedule_date": nowdate(),
+ "rate": 100000,
+ "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ }
+ ],
+ }
+ )
mr.set_missing_values()
@@ -100,11 +131,16 @@ class TestBudget(unittest.TestCase):
budget.cancel()
def test_monthly_budget_crossed_for_po(self):
- budget = make_budget(applicable_on_purchase_order=1,
- action_if_accumulated_monthly_budget_exceeded_on_po="Stop", budget_against="Cost Center")
+ budget = make_budget(
+ applicable_on_purchase_order=1,
+ action_if_accumulated_monthly_budget_exceeded_on_po="Stop",
+ budget_against="Cost Center",
+ )
fiscal_year = get_fiscal_year(nowdate())[0]
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
frappe.db.set_value("Budget", budget.name, "fiscal_year", fiscal_year)
po = create_purchase_order(transaction_date=nowdate(), do_not_submit=True)
@@ -122,12 +158,20 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Project")
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
project = frappe.get_value("Project", {"project_name": "_Test Project"})
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ "_Test Cost Center - _TC",
+ project=project,
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -139,8 +183,13 @@ class TestBudget(unittest.TestCase):
budget = make_budget(budget_against="Cost Center")
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 250000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -153,9 +202,14 @@ class TestBudget(unittest.TestCase):
project = frappe.get_value("Project", {"project_name": "_Test Project"})
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 250000, "_Test Cost Center - _TC",
- project=project, posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 250000,
+ "_Test Cost Center - _TC",
+ project=project,
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -169,14 +223,23 @@ class TestBudget(unittest.TestCase):
if month > 9:
month = 9
- for i in range(month+1):
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
+ for i in range(month + 1):
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 20000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ submit=True,
+ )
- self.assertTrue(frappe.db.get_value("GL Entry",
- {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
+ self.assertTrue(
+ frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
+ )
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
self.assertRaises(BudgetError, jv.cancel)
@@ -193,14 +256,23 @@ class TestBudget(unittest.TestCase):
project = frappe.get_value("Project", {"project_name": "_Test Project"})
for i in range(month + 1):
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True,
- project=project)
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 20000,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ submit=True,
+ project=project,
+ )
- self.assertTrue(frappe.db.get_value("GL Entry",
- {"voucher_type": "Journal Entry", "voucher_no": jv.name}))
+ self.assertTrue(
+ frappe.db.get_value("GL Entry", {"voucher_type": "Journal Entry", "voucher_no": jv.name})
+ )
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
self.assertRaises(BudgetError, jv.cancel)
@@ -212,10 +284,17 @@ class TestBudget(unittest.TestCase):
set_total_expense_zero(nowdate(), "cost_center", "_Test Cost Center 2 - _TC")
budget = make_budget(budget_against="Cost Center", cost_center="_Test Company - _TC")
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center 2 - _TC", posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ "_Test Cost Center 2 - _TC",
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -226,19 +305,28 @@ class TestBudget(unittest.TestCase):
cost_center = "_Test Cost Center 3 - _TC"
if not frappe.db.exists("Cost Center", cost_center):
- frappe.get_doc({
- 'doctype': 'Cost Center',
- 'cost_center_name': '_Test Cost Center 3',
- 'parent_cost_center': "_Test Company - _TC",
- 'company': '_Test Company',
- 'is_group': 0
- }).insert(ignore_permissions=True)
+ frappe.get_doc(
+ {
+ "doctype": "Cost Center",
+ "cost_center_name": "_Test Cost Center 3",
+ "parent_cost_center": "_Test Company - _TC",
+ "company": "_Test Company",
+ "is_group": 0,
+ }
+ ).insert(ignore_permissions=True)
budget = make_budget(budget_against="Cost Center", cost_center=cost_center)
- frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ frappe.db.set_value(
+ "Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop"
+ )
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, cost_center, posting_date=nowdate())
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 40000,
+ cost_center,
+ posting_date=nowdate(),
+ )
self.assertRaises(BudgetError, jv.submit)
@@ -255,14 +343,16 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
fiscal_year = get_fiscal_year(nowdate())[0]
- args = frappe._dict({
- "account": "_Test Account Cost for Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "monthly_end_date": posting_date,
- "company": "_Test Company",
- "fiscal_year": fiscal_year,
- "budget_against_field": budget_against_field,
- })
+ args = frappe._dict(
+ {
+ "account": "_Test Account Cost for Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "monthly_end_date": posting_date,
+ "company": "_Test Company",
+ "fiscal_year": fiscal_year,
+ "budget_against_field": budget_against_field,
+ }
+ )
if not args.get(budget_against_field):
args[budget_against_field] = budget_against
@@ -271,26 +361,42 @@ def set_total_expense_zero(posting_date, budget_against_field=None, budget_again
if existing_expense:
if budget_against_field == "cost_center":
- make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
+ make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ -existing_expense,
+ "_Test Cost Center - _TC",
+ posting_date=nowdate(),
+ submit=True,
+ )
elif budget_against_field == "project":
- make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", -existing_expense, "_Test Cost Center - _TC", submit=True, project=budget_against, posting_date=nowdate())
+ make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ -existing_expense,
+ "_Test Cost Center - _TC",
+ submit=True,
+ project=budget_against,
+ posting_date=nowdate(),
+ )
+
def make_budget(**args):
args = frappe._dict(args)
- budget_against=args.budget_against
- cost_center=args.cost_center
+ budget_against = args.budget_against
+ cost_center = args.cost_center
fiscal_year = get_fiscal_year(nowdate())[0]
if budget_against == "Project":
project_name = "{0}%".format("_Test Project/" + fiscal_year)
- budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", project_name)})
+ budget_list = frappe.get_all("Budget", fields=["name"], filters={"name": ("like", project_name)})
else:
cost_center_name = "{0}%".format(cost_center or "_Test Cost Center - _TC/" + fiscal_year)
- budget_list = frappe.get_all("Budget", fields=["name"], filters = {"name": ("like", cost_center_name)})
+ budget_list = frappe.get_all(
+ "Budget", fields=["name"], filters={"name": ("like", cost_center_name)}
+ )
for d in budget_list:
frappe.db.sql("delete from `tabBudget` where name = %(name)s", d)
frappe.db.sql("delete from `tabBudget Account` where parent = %(name)s", d)
@@ -300,7 +406,7 @@ def make_budget(**args):
if budget_against == "Project":
budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
else:
- budget.cost_center =cost_center or "_Test Cost Center - _TC"
+ budget.cost_center = cost_center or "_Test Cost Center - _TC"
monthly_distribution = frappe.get_doc("Monthly Distribution", "_Test Distribution")
monthly_distribution.fiscal_year = fiscal_year
@@ -312,20 +418,27 @@ def make_budget(**args):
budget.action_if_annual_budget_exceeded = "Stop"
budget.action_if_accumulated_monthly_budget_exceeded = "Ignore"
budget.budget_against = budget_against
- budget.append("accounts", {
- "account": "_Test Account Cost for Goods Sold - _TC",
- "budget_amount": 200000
- })
+ budget.append(
+ "accounts", {"account": "_Test Account Cost for Goods Sold - _TC", "budget_amount": 200000}
+ )
if args.applicable_on_material_request:
budget.applicable_on_material_request = 1
- budget.action_if_annual_budget_exceeded_on_mr = args.action_if_annual_budget_exceeded_on_mr or 'Warn'
- budget.action_if_accumulated_monthly_budget_exceeded_on_mr = args.action_if_accumulated_monthly_budget_exceeded_on_mr or 'Warn'
+ budget.action_if_annual_budget_exceeded_on_mr = (
+ args.action_if_annual_budget_exceeded_on_mr or "Warn"
+ )
+ budget.action_if_accumulated_monthly_budget_exceeded_on_mr = (
+ args.action_if_accumulated_monthly_budget_exceeded_on_mr or "Warn"
+ )
if args.applicable_on_purchase_order:
budget.applicable_on_purchase_order = 1
- budget.action_if_annual_budget_exceeded_on_po = args.action_if_annual_budget_exceeded_on_po or 'Warn'
- budget.action_if_accumulated_monthly_budget_exceeded_on_po = args.action_if_accumulated_monthly_budget_exceeded_on_po or 'Warn'
+ budget.action_if_annual_budget_exceeded_on_po = (
+ args.action_if_annual_budget_exceeded_on_po or "Warn"
+ )
+ budget.action_if_accumulated_monthly_budget_exceeded_on_po = (
+ args.action_if_accumulated_monthly_budget_exceeded_on_po or "Warn"
+ )
budget.insert()
budget.submit()
diff --git a/erpnext/accounts/doctype/c_form/c_form.py b/erpnext/accounts/doctype/c_form/c_form.py
index 61331d32d8e..0de75c78697 100644
--- a/erpnext/accounts/doctype/c_form/c_form.py
+++ b/erpnext/accounts/doctype/c_form/c_form.py
@@ -11,28 +11,42 @@ from frappe.utils import flt
class CForm(Document):
def validate(self):
"""Validate invoice that c-form is applicable
- and no other c-form is received for that"""
+ and no other c-form is received for that"""
- for d in self.get('invoices'):
+ for d in self.get("invoices"):
if d.invoice_no:
- inv = frappe.db.sql("""select c_form_applicable, c_form_no from
- `tabSales Invoice` where name = %s and docstatus = 1""", d.invoice_no)
+ inv = frappe.db.sql(
+ """select c_form_applicable, c_form_no from
+ `tabSales Invoice` where name = %s and docstatus = 1""",
+ d.invoice_no,
+ )
- if inv and inv[0][0] != 'Yes':
+ if inv and inv[0][0] != "Yes":
frappe.throw(_("C-form is not applicable for Invoice: {0}").format(d.invoice_no))
elif inv and inv[0][1] and inv[0][1] != self.name:
- frappe.throw(_("""Invoice {0} is tagged in another C-form: {1}.
+ frappe.throw(
+ _(
+ """Invoice {0} is tagged in another C-form: {1}.
If you want to change C-form no for this invoice,
- please remove invoice no from the previous c-form and then try again"""\
- .format(d.invoice_no, inv[0][1])))
+ please remove invoice no from the previous c-form and then try again""".format(
+ d.invoice_no, inv[0][1]
+ )
+ )
+ )
elif not inv:
- frappe.throw(_("Row {0}: Invoice {1} is invalid, it might be cancelled / does not exist. \
- Please enter a valid Invoice".format(d.idx, d.invoice_no)))
+ frappe.throw(
+ _(
+ "Row {0}: Invoice {1} is invalid, it might be cancelled / does not exist. \
+ Please enter a valid Invoice".format(
+ d.idx, d.invoice_no
+ )
+ )
+ )
def on_update(self):
- """ Update C-Form No on invoices"""
+ """Update C-Form No on invoices"""
self.set_total_invoiced_amount()
def on_submit(self):
@@ -43,30 +57,40 @@ class CForm(Document):
frappe.db.sql("""update `tabSales Invoice` set c_form_no=null where c_form_no=%s""", self.name)
def set_cform_in_sales_invoices(self):
- inv = [d.invoice_no for d in self.get('invoices')]
+ inv = [d.invoice_no for d in self.get("invoices")]
if inv:
- frappe.db.sql("""update `tabSales Invoice` set c_form_no=%s, modified=%s where name in (%s)""" %
- ('%s', '%s', ', '.join(['%s'] * len(inv))), tuple([self.name, self.modified] + inv))
+ frappe.db.sql(
+ """update `tabSales Invoice` set c_form_no=%s, modified=%s where name in (%s)"""
+ % ("%s", "%s", ", ".join(["%s"] * len(inv))),
+ tuple([self.name, self.modified] + inv),
+ )
- frappe.db.sql("""update `tabSales Invoice` set c_form_no = null, modified = %s
- where name not in (%s) and ifnull(c_form_no, '') = %s""" %
- ('%s', ', '.join(['%s']*len(inv)), '%s'), tuple([self.modified] + inv + [self.name]))
+ frappe.db.sql(
+ """update `tabSales Invoice` set c_form_no = null, modified = %s
+ where name not in (%s) and ifnull(c_form_no, '') = %s"""
+ % ("%s", ", ".join(["%s"] * len(inv)), "%s"),
+ tuple([self.modified] + inv + [self.name]),
+ )
else:
frappe.throw(_("Please enter atleast 1 invoice in the table"))
def set_total_invoiced_amount(self):
- total = sum(flt(d.grand_total) for d in self.get('invoices'))
- frappe.db.set(self, 'total_invoiced_amount', total)
+ total = sum(flt(d.grand_total) for d in self.get("invoices"))
+ frappe.db.set(self, "total_invoiced_amount", total)
@frappe.whitelist()
def get_invoice_details(self, invoice_no):
- """ Pull details from invoices for referrence """
+ """Pull details from invoices for referrence"""
if invoice_no:
- inv = frappe.db.get_value("Sales Invoice", invoice_no,
- ["posting_date", "territory", "base_net_total", "base_grand_total"], as_dict=True)
+ inv = frappe.db.get_value(
+ "Sales Invoice",
+ invoice_no,
+ ["posting_date", "territory", "base_net_total", "base_grand_total"],
+ as_dict=True,
+ )
return {
- 'invoice_date' : inv.posting_date,
- 'territory' : inv.territory,
- 'net_total' : inv.base_net_total,
- 'grand_total' : inv.base_grand_total
+ "invoice_date": inv.posting_date,
+ "territory": inv.territory,
+ "net_total": inv.base_net_total,
+ "grand_total": inv.base_grand_total,
}
diff --git a/erpnext/accounts/doctype/c_form/test_c_form.py b/erpnext/accounts/doctype/c_form/test_c_form.py
index fa34c255c66..87ad60fddac 100644
--- a/erpnext/accounts/doctype/c_form/test_c_form.py
+++ b/erpnext/accounts/doctype/c_form/test_c_form.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('C-Form')
+
class TestCForm(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py b/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py
index 4465ec6024b..79feb2dae23 100644
--- a/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py
+++ b/erpnext/accounts/doctype/cash_flow_mapper/default_cash_flow_mapper.py
@@ -1,26 +1,25 @@
-
DEFAULT_MAPPERS = [
- {
- 'doctype': 'Cash Flow Mapper',
- 'section_footer': 'Net cash generated by operating activities',
- 'section_header': 'Cash flows from operating activities',
- 'section_leader': 'Adjustments for',
- 'section_name': 'Operating Activities',
- 'position': 0,
- 'section_subtotal': 'Cash generated from operations',
- },
- {
- 'doctype': 'Cash Flow Mapper',
- 'position': 1,
- 'section_footer': 'Net cash used in investing activities',
- 'section_header': 'Cash flows from investing activities',
- 'section_name': 'Investing Activities'
- },
- {
- 'doctype': 'Cash Flow Mapper',
- 'position': 2,
- 'section_footer': 'Net cash used in financing activites',
- 'section_header': 'Cash flows from financing activities',
- 'section_name': 'Financing Activities',
- }
+ {
+ "doctype": "Cash Flow Mapper",
+ "section_footer": "Net cash generated by operating activities",
+ "section_header": "Cash flows from operating activities",
+ "section_leader": "Adjustments for",
+ "section_name": "Operating Activities",
+ "position": 0,
+ "section_subtotal": "Cash generated from operations",
+ },
+ {
+ "doctype": "Cash Flow Mapper",
+ "position": 1,
+ "section_footer": "Net cash used in investing activities",
+ "section_header": "Cash flows from investing activities",
+ "section_name": "Investing Activities",
+ },
+ {
+ "doctype": "Cash Flow Mapper",
+ "position": 2,
+ "section_footer": "Net cash used in financing activites",
+ "section_header": "Cash flows from financing activities",
+ "section_name": "Financing Activities",
+ },
]
diff --git a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py
index cd8381a4bd3..3bce4d51c7a 100644
--- a/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py
+++ b/erpnext/accounts/doctype/cash_flow_mapping/cash_flow_mapping.py
@@ -11,9 +11,11 @@ class CashFlowMapping(Document):
self.validate_checked_options()
def validate_checked_options(self):
- checked_fields = [d for d in self.meta.fields if d.fieldtype == 'Check' and self.get(d.fieldname) == 1]
+ checked_fields = [
+ d for d in self.meta.fields if d.fieldtype == "Check" and self.get(d.fieldname) == 1
+ ]
if len(checked_fields) > 1:
frappe.throw(
- frappe._('You can only select a maximum of one option from the list of check boxes.'),
- title='Error'
+ frappe._("You can only select a maximum of one option from the list of check boxes."),
+ title="Error",
)
diff --git a/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py b/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py
index abb25670467..19f2425b4ce 100644
--- a/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py
+++ b/erpnext/accounts/doctype/cash_flow_mapping/test_cash_flow_mapping.py
@@ -9,19 +9,16 @@ import frappe
class TestCashFlowMapping(unittest.TestCase):
def setUp(self):
if frappe.db.exists("Cash Flow Mapping", "Test Mapping"):
- frappe.delete_doc('Cash Flow Mappping', 'Test Mapping')
+ frappe.delete_doc("Cash Flow Mappping", "Test Mapping")
def tearDown(self):
- frappe.delete_doc('Cash Flow Mapping', 'Test Mapping')
+ frappe.delete_doc("Cash Flow Mapping", "Test Mapping")
def test_multiple_selections_not_allowed(self):
- doc = frappe.new_doc('Cash Flow Mapping')
- doc.mapping_name = 'Test Mapping'
- doc.label = 'Test label'
- doc.append(
- 'accounts',
- {'account': 'Accounts Receivable - _TC'}
- )
+ doc = frappe.new_doc("Cash Flow Mapping")
+ doc.mapping_name = "Test Mapping"
+ doc.label = "Test label"
+ doc.append("accounts", {"account": "Accounts Receivable - _TC"})
doc.is_working_capital = 1
doc.is_finance_cost = 1
diff --git a/erpnext/accounts/doctype/cashier_closing/cashier_closing.py b/erpnext/accounts/doctype/cashier_closing/cashier_closing.py
index 9fbd0c97c1e..60138077286 100644
--- a/erpnext/accounts/doctype/cashier_closing/cashier_closing.py
+++ b/erpnext/accounts/doctype/cashier_closing/cashier_closing.py
@@ -17,11 +17,14 @@ class CashierClosing(Document):
self.make_calculations()
def get_outstanding(self):
- values = frappe.db.sql("""
+ values = frappe.db.sql(
+ """
select sum(outstanding_amount)
from `tabSales Invoice`
where posting_date=%s and posting_time>=%s and posting_time<=%s and owner=%s
- """, (self.date, self.from_time, self.time, self.user))
+ """,
+ (self.date, self.from_time, self.time, self.user),
+ )
self.outstanding_amount = flt(values[0][0] if values else 0)
def make_calculations(self):
@@ -29,7 +32,9 @@ class CashierClosing(Document):
for i in self.payments:
total += flt(i.amount)
- self.net_amount = total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
+ self.net_amount = (
+ total + self.outstanding_amount + flt(self.expense) - flt(self.custody) + flt(self.returns)
+ )
def validate_time(self):
if self.from_time >= self.time:
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index aaacce4eb9d..01bf1c23e92 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -25,33 +25,41 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
class ChartofAccountsImporter(Document):
def validate(self):
if self.import_file:
- get_coa('Chart of Accounts Importer', 'All Accounts', file_name=self.import_file, for_validate=1)
+ get_coa(
+ "Chart of Accounts Importer", "All Accounts", file_name=self.import_file, for_validate=1
+ )
+
def validate_columns(data):
if not data:
- frappe.throw(_('No data found. Seems like you uploaded a blank file'))
+ frappe.throw(_("No data found. Seems like you uploaded a blank file"))
no_of_columns = max([len(d) for d in data])
if no_of_columns > 7:
- frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'),
- title=(_("Wrong Template")))
+ frappe.throw(
+ _("More columns found than expected. Please compare the uploaded file with standard template"),
+ title=(_("Wrong Template")),
+ )
+
@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.db.get_value(
+ "Company", {"name": 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.throw(msg, title=_('Wrong Company'))
+ frappe.bold("Allow Account Creation Against Child Company")
+ )
+ frappe.throw(msg, title=_("Wrong Company"))
- if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1):
+ if frappe.db.get_all("GL Entry", {"company": company}, "name", limit=1):
return False
+
@frappe.whitelist()
def import_coa(file_name, company):
# delete existing data for accounts
@@ -60,7 +68,7 @@ def import_coa(file_name, company):
# create accounts
file_doc, extension = get_file(file_name)
- if extension == 'csv':
+ if extension == "csv":
data = generate_data_from_csv(file_doc)
else:
data = generate_data_from_excel(file_doc, extension)
@@ -72,27 +80,33 @@ def import_coa(file_name, company):
# trigger on_update for company to reset default accounts
set_default_accounts(company)
+
def get_file(file_name):
file_doc = frappe.get_doc("File", {"file_url": file_name})
parts = file_doc.get_extension()
extension = parts[1]
extension = extension.lstrip(".")
- if extension not in ('csv', 'xlsx', 'xls'):
- frappe.throw(_("Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"))
+ if extension not in ("csv", "xlsx", "xls"):
+ frappe.throw(
+ _(
+ "Only CSV and Excel files can be used to for importing data. Please check the file format you are trying to upload"
+ )
+ )
+
+ return file_doc, extension
- return file_doc, extension
def generate_data_from_csv(file_doc, as_dict=False):
- ''' read csv file and return the generated nested tree '''
+ """read csv file and return the generated nested tree"""
file_path = file_doc.get_full_path()
data = []
- with open(file_path, 'r') as in_file:
+ with open(file_path, "r") as in_file:
csv_reader = list(csv.reader(in_file))
headers = csv_reader[0]
- del csv_reader[0] # delete top row and headers row
+ del csv_reader[0] # delete top row and headers row
for row in csv_reader:
if as_dict:
@@ -106,6 +120,7 @@ def generate_data_from_csv(file_doc, as_dict=False):
# convert csv data
return data
+
def generate_data_from_excel(file_doc, extension, as_dict=False):
content = file_doc.get_content()
@@ -123,20 +138,21 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
if not row[1]:
- row[1] = row[0]
- row[3] = row[2]
+ row[1] = row[0]
+ row[3] = row[2]
data.append(row)
return data
+
@frappe.whitelist()
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
- ''' called by tree view (to fetch node's children) '''
+ """called by tree view (to fetch node's children)"""
file_doc, extension = get_file(file_name)
- parent = None if parent==_('All Accounts') else parent
+ parent = None if parent == _("All Accounts") else parent
- if extension == 'csv':
+ if extension == "csv":
data = generate_data_from_csv(file_doc)
else:
data = generate_data_from_excel(file_doc, extension)
@@ -146,32 +162,33 @@ def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
if not for_validate:
forest = build_forest(data)
- accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form
+ accounts = build_tree_from_json(
+ "", chart_data=forest, from_coa_importer=True
+ ) # returns a list of dict in a tree render-able form
# filter out to show data for the selected node only
- accounts = [d for d in accounts if d['parent_account']==parent]
+ accounts = [d for d in accounts if d["parent_account"] == parent]
return accounts
else:
- return {
- 'show_import_button': 1
- }
+ return {"show_import_button": 1}
+
def build_forest(data):
- '''
- converts list of list into a nested tree
- if a = [[1,1], [1,2], [3,2], [4,4], [5,4]]
- tree = {
- 1: {
- 2: {
- 3: {}
- }
- },
- 4: {
- 5: {}
- }
- }
- '''
+ """
+ converts list of list into a nested tree
+ if a = [[1,1], [1,2], [3,2], [4,4], [5,4]]
+ tree = {
+ 1: {
+ 2: {
+ 3: {}
+ }
+ },
+ 4: {
+ 5: {}
+ }
+ }
+ """
# set the value of nested dictionary
def set_nested(d, path, value):
@@ -195,8 +212,11 @@ def build_forest(data):
elif account_name == child:
parent_account_list = return_parent(data, parent_account)
if not parent_account_list and parent_account:
- frappe.throw(_("The parent account {0} does not exists in the uploaded template").format(
- frappe.bold(parent_account)))
+ frappe.throw(
+ _("The parent account {0} does not exists in the uploaded template").format(
+ frappe.bold(parent_account)
+ )
+ )
return [child] + parent_account_list
charts_map, paths = {}, []
@@ -205,7 +225,15 @@ def build_forest(data):
error_messages = []
for i in data:
- account_name, parent_account, account_number, parent_account_number, is_group, account_type, root_type = i
+ (
+ account_name,
+ parent_account,
+ account_number,
+ parent_account_number,
+ is_group,
+ account_type,
+ root_type,
+ ) = i
if not account_name:
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
@@ -216,13 +244,17 @@ def build_forest(data):
account_name = "{} - {}".format(account_number, account_name)
charts_map[account_name] = {}
- charts_map[account_name]['account_name'] = name
- if account_number: charts_map[account_name]["account_number"] = account_number
- if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
- if account_type: charts_map[account_name]["account_type"] = account_type
- if root_type: charts_map[account_name]["root_type"] = root_type
+ charts_map[account_name]["account_name"] = name
+ if account_number:
+ charts_map[account_name]["account_number"] = account_number
+ if cint(is_group) == 1:
+ charts_map[account_name]["is_group"] = is_group
+ if account_type:
+ charts_map[account_name]["account_type"] = account_type
+ if root_type:
+ charts_map[account_name]["root_type"] = root_type
path = return_parent(data, account_name)[::-1]
- paths.append(path) # List of path is created
+ paths.append(path) # List of path is created
line_no += 1
if error_messages:
@@ -231,27 +263,32 @@ def build_forest(data):
out = {}
for path in paths:
for n, account_name in enumerate(path):
- set_nested(out, path[:n+1], charts_map[account_name]) # setting the value of nested dictionary.
+ set_nested(
+ out, path[: n + 1], charts_map[account_name]
+ ) # setting the value of nested dictionary.
return out
+
def build_response_as_excel(writer):
filename = frappe.generate_hash("", 10)
- with open(filename, 'wb') as f:
- f.write(cstr(writer.getvalue()).encode('utf-8'))
+ with open(filename, "wb") as f:
+ f.write(cstr(writer.getvalue()).encode("utf-8"))
f = open(filename)
reader = csv.reader(f)
from frappe.utils.xlsxutils import make_xlsx
+
xlsx_file = make_xlsx(reader, "Chart of Accounts Importer Template")
f.close()
os.remove(filename)
# write out response as a xlsx type
- frappe.response['filename'] = 'coa_importer_template.xlsx'
- frappe.response['filecontent'] = xlsx_file.getvalue()
- frappe.response['type'] = 'binary'
+ frappe.response["filename"] = "coa_importer_template.xlsx"
+ frappe.response["filecontent"] = xlsx_file.getvalue()
+ frappe.response["type"] = "binary"
+
@frappe.whitelist()
def download_template(file_type, template_type):
@@ -259,34 +296,46 @@ def download_template(file_type, template_type):
writer = get_template(template_type)
- if file_type == 'CSV':
+ if file_type == "CSV":
# download csv file
- frappe.response['result'] = cstr(writer.getvalue())
- frappe.response['type'] = 'csv'
- frappe.response['doctype'] = 'Chart of Accounts Importer'
+ frappe.response["result"] = cstr(writer.getvalue())
+ frappe.response["type"] = "csv"
+ frappe.response["doctype"] = "Chart of Accounts Importer"
else:
build_response_as_excel(writer)
+
def get_template(template_type):
- fields = ["Account Name", "Parent Account", "Account Number", "Parent Account Number", "Is Group", "Account Type", "Root Type"]
+ fields = [
+ "Account Name",
+ "Parent Account",
+ "Account Number",
+ "Parent Account Number",
+ "Is Group",
+ "Account Type",
+ "Root Type",
+ ]
writer = UnicodeWriter()
writer.writerow(fields)
- if template_type == 'Blank Template':
- for root_type in get_root_types():
- writer.writerow(['', '', '', 1, '', root_type])
+ if template_type == "Blank Template":
+ for root_type in get_root_types():
+ writer.writerow(["", "", "", 1, "", root_type])
for account in get_mandatory_group_accounts():
- writer.writerow(['', '', '', 1, account, "Asset"])
+ writer.writerow(["", "", "", 1, account, "Asset"])
for account_type in get_mandatory_account_types():
- writer.writerow(['', '', '', 0, account_type.get('account_type'), account_type.get('root_type')])
+ writer.writerow(
+ ["", "", "", 0, account_type.get("account_type"), account_type.get("root_type")]
+ )
else:
writer = get_sample_template(writer)
return writer
+
def get_sample_template(writer):
template = [
["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"],
@@ -316,7 +365,7 @@ def get_sample_template(writer):
@frappe.whitelist()
def validate_accounts(file_doc, extension):
- if extension == 'csv':
+ if extension == "csv":
accounts = generate_data_from_csv(file_doc, as_dict=True)
else:
accounts = generate_data_from_excel(file_doc, extension, as_dict=True)
@@ -325,7 +374,9 @@ def validate_accounts(file_doc, extension):
for account in accounts:
accounts_dict.setdefault(account["account_name"], account)
if "parent_account" not in account:
- msg = _("Please make sure the file you are using has 'Parent Account' column present in the header.")
+ msg = _(
+ "Please make sure the file you are using has 'Parent Account' column present in the header."
+ )
msg += "
"
msg += _("Alternatively, you can download the template and fill your data in.")
frappe.throw(msg, title=_("Parent Account Missing"))
@@ -336,77 +387,106 @@ def validate_accounts(file_doc, extension):
return [True, len(accounts)]
+
def validate_root(accounts):
- roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
+ roots = [accounts[d] for d in accounts if not accounts[d].get("parent_account")]
error_messages = []
for account in roots:
if not account.get("root_type") and account.get("account_name"):
- error_messages.append(_("Please enter Root Type for account- {0}").format(account.get("account_name")))
+ error_messages.append(
+ _("Please enter Root Type for account- {0}").format(account.get("account_name"))
+ )
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
- error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
+ error_messages.append(
+ _("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(
+ account.get("account_name")
+ )
+ )
validate_missing_roots(roots)
if error_messages:
frappe.throw("
".join(error_messages))
+
def validate_missing_roots(roots):
- root_types_added = set(d.get('root_type') for d in roots)
+ root_types_added = set(d.get("root_type") for d in roots)
missing = list(set(get_root_types()) - root_types_added)
if missing:
- frappe.throw(_("Please add Root Account for - {0}").format(' , '.join(missing)))
+ frappe.throw(_("Please add Root Account for - {0}").format(" , ".join(missing)))
+
def get_root_types():
- return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
+ return ("Asset", "Liability", "Expense", "Income", "Equity")
+
def get_report_type(root_type):
- if root_type in ('Asset', 'Liability', 'Equity'):
- return 'Balance Sheet'
+ if root_type in ("Asset", "Liability", "Equity"):
+ return "Balance Sheet"
else:
- return 'Profit and Loss'
+ return "Profit and Loss"
+
def get_mandatory_group_accounts():
- return ('Bank', 'Cash', 'Stock')
+ return ("Bank", "Cash", "Stock")
+
def get_mandatory_account_types():
return [
- {'account_type': 'Cost of Goods Sold', 'root_type': 'Expense'},
- {'account_type': 'Depreciation', 'root_type': 'Expense'},
- {'account_type': 'Fixed Asset', 'root_type': 'Asset'},
- {'account_type': 'Payable', 'root_type': 'Liability'},
- {'account_type': 'Receivable', 'root_type': 'Asset'},
- {'account_type': 'Stock Adjustment', 'root_type': 'Expense'},
- {'account_type': 'Bank', 'root_type': 'Asset'},
- {'account_type': 'Cash', 'root_type': 'Asset'},
- {'account_type': 'Stock', 'root_type': 'Asset'}
+ {"account_type": "Cost of Goods Sold", "root_type": "Expense"},
+ {"account_type": "Depreciation", "root_type": "Expense"},
+ {"account_type": "Fixed Asset", "root_type": "Asset"},
+ {"account_type": "Payable", "root_type": "Liability"},
+ {"account_type": "Receivable", "root_type": "Asset"},
+ {"account_type": "Stock Adjustment", "root_type": "Expense"},
+ {"account_type": "Bank", "root_type": "Asset"},
+ {"account_type": "Cash", "root_type": "Asset"},
+ {"account_type": "Stock", "root_type": "Asset"},
]
+
def unset_existing_data(company):
- linked = frappe.db.sql('''select fieldname from tabDocField
- where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)
+ linked = frappe.db.sql(
+ '''select fieldname from tabDocField
+ where fieldtype="Link" and options="Account" and parent="Company"''',
+ as_dict=True,
+ )
# remove accounts data from company
- update_values = {d.fieldname: '' for d in linked}
- frappe.db.set_value('Company', company, update_values, update_values)
+ update_values = {d.fieldname: "" for d in linked}
+ frappe.db.set_value("Company", company, update_values, update_values)
# remove accounts data from various doctypes
- for doctype in ["Account", "Party Account", "Mode of Payment Account", "Tax Withholding Account",
- "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
- frappe.db.sql('''delete from `tab{0}` where `company`="%s"''' # nosec
- .format(doctype) % (company))
+ for doctype in [
+ "Account",
+ "Party Account",
+ "Mode of Payment Account",
+ "Tax Withholding Account",
+ "Sales Taxes and Charges Template",
+ "Purchase Taxes and Charges Template",
+ ]:
+ frappe.db.sql(
+ '''delete from `tab{0}` where `company`="%s"'''.format(doctype) % (company) # nosec
+ )
+
def set_default_accounts(company):
from erpnext.setup.doctype.company.company import install_country_fixtures
- company = frappe.get_doc('Company', company)
- company.update({
- "default_receivable_account": frappe.db.get_value("Account",
- {"company": company.name, "account_type": "Receivable", "is_group": 0}),
- "default_payable_account": frappe.db.get_value("Account",
- {"company": company.name, "account_type": "Payable", "is_group": 0})
- })
+
+ company = frappe.get_doc("Company", company)
+ company.update(
+ {
+ "default_receivable_account": frappe.db.get_value(
+ "Account", {"company": company.name, "account_type": "Receivable", "is_group": 0}
+ ),
+ "default_payable_account": frappe.db.get_value(
+ "Account", {"company": company.name, "account_type": "Payable", "is_group": 0}
+ ),
+ }
+ )
company.save()
install_country_fixtures(company.name, company.country)
diff --git a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py
index 20cb42c109c..f8ac66444b4 100644
--- a/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py
+++ b/erpnext/accounts/doctype/cheque_print_template/cheque_print_template.py
@@ -10,17 +10,20 @@ from frappe.model.document import Document
class ChequePrintTemplate(Document):
pass
+
@frappe.whitelist()
def create_or_update_cheque_print_format(template_name):
if not frappe.db.exists("Print Format", template_name):
cheque_print = frappe.new_doc("Print Format")
- cheque_print.update({
- "doc_type": "Payment Entry",
- "standard": "No",
- "custom_format": 1,
- "print_format_type": "Jinja",
- "name": template_name
- })
+ cheque_print.update(
+ {
+ "doc_type": "Payment Entry",
+ "standard": "No",
+ "custom_format": 1,
+ "print_format_type": "Jinja",
+ "name": template_name,
+ }
+ )
else:
cheque_print = frappe.get_doc("Print Format", template_name)
@@ -69,10 +72,12 @@ def create_or_update_cheque_print_format(template_name):
{{doc.company}}
-"""%{
- "starting_position_from_top_edge": doc.starting_position_from_top_edge \
- if doc.cheque_size == "A4" else 0.0,
- "cheque_width": doc.cheque_width, "cheque_height": doc.cheque_height,
+""" % {
+ "starting_position_from_top_edge": doc.starting_position_from_top_edge
+ if doc.cheque_size == "A4"
+ else 0.0,
+ "cheque_width": doc.cheque_width,
+ "cheque_height": doc.cheque_height,
"acc_pay_dist_from_top_edge": doc.acc_pay_dist_from_top_edge,
"acc_pay_dist_from_left_edge": doc.acc_pay_dist_from_left_edge,
"message_to_show": doc.message_to_show if doc.message_to_show else _("Account Pay Only"),
@@ -89,7 +94,7 @@ def create_or_update_cheque_print_format(template_name):
"amt_in_figures_from_top_edge": doc.amt_in_figures_from_top_edge,
"amt_in_figures_from_left_edge": doc.amt_in_figures_from_left_edge,
"signatory_from_top_edge": doc.signatory_from_top_edge,
- "signatory_from_left_edge": doc.signatory_from_left_edge
+ "signatory_from_left_edge": doc.signatory_from_left_edge,
}
cheque_print.save(ignore_permissions=True)
diff --git a/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py b/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py
index 2b323a9bf62..9b003ceaa3e 100644
--- a/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py
+++ b/erpnext/accounts/doctype/cheque_print_template/test_cheque_print_template.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Cheque Print Template')
+
class TestChequePrintTemplate(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py
index 7ae0a72e3d1..2d18a2409a2 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.py
+++ b/erpnext/accounts/doctype/cost_center/cost_center.py
@@ -11,11 +11,14 @@ from erpnext.accounts.utils import validate_field_number
class CostCenter(NestedSet):
- nsm_parent_field = 'parent_cost_center'
+ nsm_parent_field = "parent_cost_center"
def autoname(self):
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.name = get_autoname_with_number(
+ self.cost_center_number, self.cost_center_name, None, self.company
+ )
def validate(self):
self.validate_mandatory()
@@ -27,15 +30,31 @@ class CostCenter(NestedSet):
if not self.distributed_cost_center:
frappe.throw(_("Please enter distributed cost center"))
if sum(x.percentage_allocation for x in self.distributed_cost_center) != 100:
- frappe.throw(_("Total percentage allocation for distributed cost center should be equal to 100"))
- if not self.get('__islocal'):
- if not cint(frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")) \
- and self.check_if_part_of_distributed_cost_center():
- frappe.throw(_("Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"))
+ frappe.throw(
+ _("Total percentage allocation for distributed cost center should be equal to 100")
+ )
+ if not self.get("__islocal"):
+ if (
+ not cint(
+ frappe.get_cached_value("Cost Center", {"name": self.name}, "enable_distributed_cost_center")
+ )
+ and self.check_if_part_of_distributed_cost_center()
+ ):
+ frappe.throw(
+ _(
+ "Cannot enable Distributed Cost Center for a Cost Center already allocated in another Distributed Cost Center"
+ )
+ )
if next((True for x in self.distributed_cost_center if x.cost_center == x.parent), False):
frappe.throw(_("Parent Cost Center cannot be added in Distributed Cost Center"))
- if check_if_distributed_cost_center_enabled(list(x.cost_center for x in self.distributed_cost_center)):
- frappe.throw(_("A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."))
+ if check_if_distributed_cost_center_enabled(
+ list(x.cost_center for x in self.distributed_cost_center)
+ ):
+ frappe.throw(
+ _(
+ "A Distributed Cost Center cannot be added in the Distributed Cost Center allocation table."
+ )
+ )
else:
self.distributed_cost_center = []
@@ -47,9 +66,12 @@ class CostCenter(NestedSet):
def validate_parent_cost_center(self):
if self.parent_cost_center:
- if not frappe.db.get_value('Cost Center', self.parent_cost_center, 'is_group'):
- frappe.throw(_("{0} is not a group node. Please select a group node as parent cost center").format(
- frappe.bold(self.parent_cost_center)))
+ if not frappe.db.get_value("Cost Center", self.parent_cost_center, "is_group"):
+ frappe.throw(
+ _("{0} is not a group node. Please select a group node as parent cost center").format(
+ frappe.bold(self.parent_cost_center)
+ )
+ )
@frappe.whitelist()
def convert_group_to_ledger(self):
@@ -65,9 +87,13 @@ class CostCenter(NestedSet):
@frappe.whitelist()
def convert_ledger_to_group(self):
if cint(self.enable_distributed_cost_center):
- frappe.throw(_("Cost Center with enabled distributed cost center can not be converted to group"))
+ frappe.throw(
+ _("Cost Center with enabled distributed cost center can not be converted to group")
+ )
if self.check_if_part_of_distributed_cost_center():
- frappe.throw(_("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group"))
+ frappe.throw(
+ _("Cost Center Already Allocated in a Distributed Cost Center cannot be converted to group")
+ )
if self.check_gle_exists():
frappe.throw(_("Cost Center with existing transactions can not be converted to group"))
self.is_group = 1
@@ -78,8 +104,11 @@ class CostCenter(NestedSet):
return frappe.db.get_value("GL Entry", {"cost_center": self.name})
def check_if_child_exists(self):
- return frappe.db.sql("select name from `tabCost Center` where \
- parent_cost_center = %s and docstatus != 2", self.name)
+ return frappe.db.sql(
+ "select name from `tabCost Center` where \
+ parent_cost_center = %s and docstatus != 2",
+ self.name,
+ )
def check_if_part_of_distributed_cost_center(self):
return frappe.db.get_value("Distributed Cost Center", {"cost_center": self.name})
@@ -87,6 +116,7 @@ class CostCenter(NestedSet):
def before_rename(self, olddn, newdn, merge=False):
# Add company abbr if not provided
from erpnext.setup.doctype.company.company import get_name_with_abbr
+
new_cost_center = get_name_with_abbr(newdn, self.company)
# Validate properties before merging
@@ -100,7 +130,9 @@ class CostCenter(NestedSet):
super(CostCenter, self).after_rename(olddn, newdn, merge)
if not merge:
- new_cost_center = frappe.db.get_value("Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1)
+ new_cost_center = frappe.db.get_value(
+ "Cost Center", newdn, ["cost_center_name", "cost_center_number"], as_dict=1
+ )
# exclude company abbr
new_parts = newdn.split(" - ")[:-1]
@@ -109,7 +141,9 @@ class CostCenter(NestedSet):
if len(new_parts) == 1:
new_parts = newdn.split(" ")
if new_cost_center.cost_center_number != new_parts[0]:
- validate_field_number("Cost Center", self.name, new_parts[0], self.company, "cost_center_number")
+ validate_field_number(
+ "Cost Center", self.name, new_parts[0], self.company, "cost_center_number"
+ )
self.cost_center_number = new_parts[0]
self.db_set("cost_center_number", new_parts[0])
new_parts = new_parts[1:]
@@ -120,14 +154,19 @@ class CostCenter(NestedSet):
self.cost_center_name = cost_center_name
self.db_set("cost_center_name", cost_center_name)
+
def on_doctype_update():
frappe.db.add_index("Cost Center", ["lft", "rgt"])
+
def get_name_with_number(new_account, account_number):
if account_number and not new_account[0].isdigit():
new_account = account_number + " - " + new_account
return new_account
+
def check_if_distributed_cost_center_enabled(cost_center_list):
- value_list = frappe.get_list("Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1)
+ value_list = frappe.get_list(
+ "Cost Center", {"name": ["in", cost_center_list]}, "enable_distributed_cost_center", as_list=1
+ )
return next((True for x in value_list if x[0]), False)
diff --git a/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py b/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py
index 0bae8fe1456..5059dc3cc0e 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py
+++ b/erpnext/accounts/doctype/cost_center/cost_center_dashboard.py
@@ -1,14 +1,8 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'cost_center',
- 'reports': [
- {
- 'label': _('Reports'),
- 'items': ['Budget Variance Report', 'General Ledger']
- }
- ]
+ "fieldname": "cost_center",
+ "reports": [{"label": _("Reports"), "items": ["Budget Variance Report", "General Ledger"]}],
}
diff --git a/erpnext/accounts/doctype/cost_center/test_cost_center.py b/erpnext/accounts/doctype/cost_center/test_cost_center.py
index f8615ec03a5..a5a2d15ca93 100644
--- a/erpnext/accounts/doctype/cost_center/test_cost_center.py
+++ b/erpnext/accounts/doctype/cost_center/test_cost_center.py
@@ -5,51 +5,53 @@ import unittest
import frappe
-test_records = frappe.get_test_records('Cost Center')
+test_records = frappe.get_test_records("Cost Center")
+
class TestCostCenter(unittest.TestCase):
def test_cost_center_creation_against_child_node(self):
- if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
+ if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}):
frappe.get_doc(test_records[1]).insert()
- cost_center = frappe.get_doc({
- 'doctype': 'Cost Center',
- 'cost_center_name': '_Test Cost Center 3',
- 'parent_cost_center': '_Test Cost Center 2 - _TC',
- 'is_group': 0,
- 'company': '_Test Company'
- })
+ cost_center = frappe.get_doc(
+ {
+ "doctype": "Cost Center",
+ "cost_center_name": "_Test Cost Center 3",
+ "parent_cost_center": "_Test Cost Center 2 - _TC",
+ "is_group": 0,
+ "company": "_Test Company",
+ }
+ )
self.assertRaises(frappe.ValidationError, cost_center.save)
def test_validate_distributed_cost_center(self):
- if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center - _TC'}):
+ if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center - _TC"}):
frappe.get_doc(test_records[0]).insert()
- if not frappe.db.get_value('Cost Center', {'name': '_Test Cost Center 2 - _TC'}):
+ if not frappe.db.get_value("Cost Center", {"name": "_Test Cost Center 2 - _TC"}):
frappe.get_doc(test_records[1]).insert()
- invalid_distributed_cost_center = frappe.get_doc({
- "company": "_Test Company",
- "cost_center_name": "_Test Distributed Cost Center",
- "doctype": "Cost Center",
- "is_group": 0,
- "parent_cost_center": "_Test Company - _TC",
- "enable_distributed_cost_center": 1,
- "distributed_cost_center": [{
- "cost_center": "_Test Cost Center - _TC",
- "percentage_allocation": 40
- }, {
- "cost_center": "_Test Cost Center 2 - _TC",
- "percentage_allocation": 50
- }
- ]
- })
+ invalid_distributed_cost_center = frappe.get_doc(
+ {
+ "company": "_Test Company",
+ "cost_center_name": "_Test Distributed Cost Center",
+ "doctype": "Cost Center",
+ "is_group": 0,
+ "parent_cost_center": "_Test Company - _TC",
+ "enable_distributed_cost_center": 1,
+ "distributed_cost_center": [
+ {"cost_center": "_Test Cost Center - _TC", "percentage_allocation": 40},
+ {"cost_center": "_Test Cost Center 2 - _TC", "percentage_allocation": 50},
+ ],
+ }
+ )
self.assertRaises(frappe.ValidationError, invalid_distributed_cost_center.save)
+
def create_cost_center(**args):
args = frappe._dict(args)
if args.cost_center_name:
diff --git a/erpnext/accounts/doctype/coupon_code/coupon_code.py b/erpnext/accounts/doctype/coupon_code/coupon_code.py
index ee32de1cd28..6a0cdf91c0a 100644
--- a/erpnext/accounts/doctype/coupon_code/coupon_code.py
+++ b/erpnext/accounts/doctype/coupon_code/coupon_code.py
@@ -15,7 +15,7 @@ class CouponCode(Document):
if not self.coupon_code:
if self.coupon_type == "Promotional":
- self.coupon_code =''.join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
+ self.coupon_code = "".join(i for i in self.coupon_name if not i.isdigit())[0:8].upper()
elif self.coupon_type == "Gift Card":
self.coupon_code = frappe.generate_hash()[:10].upper()
diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
index ca482c8c4ec..b897546b036 100644
--- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
+++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
@@ -7,92 +7,110 @@ import frappe
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
-test_dependencies = ['Item']
+test_dependencies = ["Item"]
+
def test_create_test_data():
frappe.set_user("Administrator")
# create test item
- if not frappe.db.exists("Item","_Test Tesla Car"):
- item = frappe.get_doc({
- "description": "_Test Tesla Car",
- "doctype": "Item",
- "has_batch_no": 0,
- "has_serial_no": 0,
- "inspection_required": 0,
- "is_stock_item": 1,
- "opening_stock":100,
- "is_sub_contracted_item": 0,
- "item_code": "_Test Tesla Car",
- "item_group": "_Test Item Group",
- "item_name": "_Test Tesla Car",
- "apply_warehouse_wise_reorder_level": 0,
- "warehouse":"Stores - _TC",
- "gst_hsn_code": "999800",
- "valuation_rate": 5000,
- "standard_rate":5000,
- "item_defaults": [{
- "company": "_Test Company",
- "default_warehouse": "Stores - _TC",
- "default_price_list":"_Test Price List",
- "expense_account": "Cost of Goods Sold - _TC",
- "buying_cost_center": "Main - _TC",
- "selling_cost_center": "Main - _TC",
- "income_account": "Sales - _TC"
- }],
- })
+ if not frappe.db.exists("Item", "_Test Tesla Car"):
+ item = frappe.get_doc(
+ {
+ "description": "_Test Tesla Car",
+ "doctype": "Item",
+ "has_batch_no": 0,
+ "has_serial_no": 0,
+ "inspection_required": 0,
+ "is_stock_item": 1,
+ "opening_stock": 100,
+ "is_sub_contracted_item": 0,
+ "item_code": "_Test Tesla Car",
+ "item_group": "_Test Item Group",
+ "item_name": "_Test Tesla Car",
+ "apply_warehouse_wise_reorder_level": 0,
+ "warehouse": "Stores - _TC",
+ "gst_hsn_code": "999800",
+ "valuation_rate": 5000,
+ "standard_rate": 5000,
+ "item_defaults": [
+ {
+ "company": "_Test Company",
+ "default_warehouse": "Stores - _TC",
+ "default_price_list": "_Test Price List",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "buying_cost_center": "Main - _TC",
+ "selling_cost_center": "Main - _TC",
+ "income_account": "Sales - _TC",
+ }
+ ],
+ }
+ )
item.insert()
# create test item price
- item_price = frappe.get_list('Item Price', filters={'item_code': '_Test Tesla Car', 'price_list': '_Test Price List'}, fields=['name'])
- if len(item_price)==0:
- item_price = frappe.get_doc({
- "doctype": "Item Price",
- "item_code": "_Test Tesla Car",
- "price_list": "_Test Price List",
- "price_list_rate": 5000
- })
+ item_price = frappe.get_list(
+ "Item Price",
+ filters={"item_code": "_Test Tesla Car", "price_list": "_Test Price List"},
+ fields=["name"],
+ )
+ if len(item_price) == 0:
+ item_price = frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "item_code": "_Test Tesla Car",
+ "price_list": "_Test Price List",
+ "price_list_rate": 5000,
+ }
+ )
item_price.insert()
# create test item pricing rule
if not frappe.db.exists("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}):
- item_pricing_rule = frappe.get_doc({
- "doctype": "Pricing Rule",
- "title": "_Test Pricing Rule for _Test Item",
- "apply_on": "Item Code",
- "items": [{
- "item_code": "_Test Tesla Car"
- }],
- "warehouse":"Stores - _TC",
- "coupon_code_based":1,
- "selling": 1,
- "rate_or_discount": "Discount Percentage",
- "discount_percentage": 30,
- "company": "_Test Company",
- "currency":"INR",
- "for_price_list":"_Test Price List"
- })
+ item_pricing_rule = frappe.get_doc(
+ {
+ "doctype": "Pricing Rule",
+ "title": "_Test Pricing Rule for _Test Item",
+ "apply_on": "Item Code",
+ "items": [{"item_code": "_Test Tesla Car"}],
+ "warehouse": "Stores - _TC",
+ "coupon_code_based": 1,
+ "selling": 1,
+ "rate_or_discount": "Discount Percentage",
+ "discount_percentage": 30,
+ "company": "_Test Company",
+ "currency": "INR",
+ "for_price_list": "_Test Price List",
+ }
+ )
item_pricing_rule.insert()
# create test item sales partner
- if not frappe.db.exists("Sales Partner","_Test Coupon Partner"):
- sales_partner = frappe.get_doc({
- "doctype": "Sales Partner",
- "partner_name":"_Test Coupon Partner",
- "commission_rate":2,
- "referral_code": "COPART"
- })
+ if not frappe.db.exists("Sales Partner", "_Test Coupon Partner"):
+ sales_partner = frappe.get_doc(
+ {
+ "doctype": "Sales Partner",
+ "partner_name": "_Test Coupon Partner",
+ "commission_rate": 2,
+ "referral_code": "COPART",
+ }
+ )
sales_partner.insert()
# create test item coupon code
if not frappe.db.exists("Coupon Code", "SAVE30"):
- pricing_rule = frappe.db.get_value("Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ['name'])
- coupon_code = frappe.get_doc({
- "doctype": "Coupon Code",
- "coupon_name":"SAVE30",
- "coupon_code":"SAVE30",
- "pricing_rule": pricing_rule,
- "valid_from": "2014-01-01",
- "maximum_use":1,
- "used":0
- })
+ pricing_rule = frappe.db.get_value(
+ "Pricing Rule", {"title": "_Test Pricing Rule for _Test Item"}, ["name"]
+ )
+ coupon_code = frappe.get_doc(
+ {
+ "doctype": "Coupon Code",
+ "coupon_name": "SAVE30",
+ "coupon_code": "SAVE30",
+ "pricing_rule": pricing_rule,
+ "valid_from": "2014-01-01",
+ "maximum_use": 1,
+ "used": 0,
+ }
+ )
coupon_code.insert()
+
class TestCouponCode(unittest.TestCase):
def setUp(self):
test_create_test_data()
@@ -103,15 +121,21 @@ class TestCouponCode(unittest.TestCase):
def test_sales_order_with_coupon_code(self):
frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
- so = make_sales_order(company='_Test Company', warehouse='Stores - _TC',
- customer="_Test Customer", selling_price_list="_Test Price List",
- item_code="_Test Tesla Car", rate=5000, qty=1,
- do_not_submit=True)
+ so = make_sales_order(
+ company="_Test Company",
+ warehouse="Stores - _TC",
+ customer="_Test Customer",
+ selling_price_list="_Test Price List",
+ item_code="_Test Tesla Car",
+ rate=5000,
+ qty=1,
+ do_not_submit=True,
+ )
self.assertEqual(so.items[0].rate, 5000)
- so.coupon_code='SAVE30'
- so.sales_partner='_Test Coupon Partner'
+ so.coupon_code = "SAVE30"
+ so.sales_partner = "_Test Coupon Partner"
so.save()
# check item price after coupon code is applied
diff --git a/erpnext/accounts/doctype/dunning/dunning.py b/erpnext/accounts/doctype/dunning/dunning.py
index 02377cd5659..b675bde58af 100644
--- a/erpnext/accounts/doctype/dunning/dunning.py
+++ b/erpnext/accounts/doctype/dunning/dunning.py
@@ -20,78 +20,99 @@ 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.db.get_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
def validate_amount(self):
amounts = calculate_interest_and_amount(
- self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days)
- if self.interest_amount != amounts.get('interest_amount'):
- self.interest_amount = flt(amounts.get('interest_amount'), self.precision('interest_amount'))
- if self.dunning_amount != amounts.get('dunning_amount'):
- self.dunning_amount = flt(amounts.get('dunning_amount'), self.precision('dunning_amount'))
- if self.grand_total != amounts.get('grand_total'):
- self.grand_total = flt(amounts.get('grand_total'), self.precision('grand_total'))
+ self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days
+ )
+ if self.interest_amount != amounts.get("interest_amount"):
+ self.interest_amount = flt(amounts.get("interest_amount"), self.precision("interest_amount"))
+ if self.dunning_amount != amounts.get("dunning_amount"):
+ self.dunning_amount = flt(amounts.get("dunning_amount"), self.precision("dunning_amount"))
+ if self.grand_total != amounts.get("grand_total"):
+ self.grand_total = flt(amounts.get("grand_total"), self.precision("grand_total"))
def on_submit(self):
self.make_gl_entries()
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")
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
def make_gl_entries(self):
if not self.dunning_amount:
return
gl_entries = []
- invoice_fields = ["project", "cost_center", "debit_to", "party_account_currency", "conversion_rate", "cost_center"]
+ invoice_fields = [
+ "project",
+ "cost_center",
+ "debit_to",
+ "party_account_currency",
+ "conversion_rate",
+ "cost_center",
+ ]
inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1)
accounting_dimensions = get_accounting_dimensions()
invoice_fields.extend(accounting_dimensions)
dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate)
- default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center')
+ default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center")
gl_entries.append(
- self.get_gl_dict({
- "account": inv.debit_to,
- "party_type": "Customer",
- "party": self.customer,
- "due_date": self.due_date,
- "against": self.income_account,
- "debit": dunning_in_company_currency,
- "debit_in_account_currency": self.dunning_amount,
- "against_voucher": self.name,
- "against_voucher_type": "Dunning",
- "cost_center": inv.cost_center or default_cost_center,
- "project": inv.project
- }, inv.party_account_currency, item=inv)
+ self.get_gl_dict(
+ {
+ "account": inv.debit_to,
+ "party_type": "Customer",
+ "party": self.customer,
+ "due_date": self.due_date,
+ "against": self.income_account,
+ "debit": dunning_in_company_currency,
+ "debit_in_account_currency": self.dunning_amount,
+ "against_voucher": self.name,
+ "against_voucher_type": "Dunning",
+ "cost_center": inv.cost_center or default_cost_center,
+ "project": inv.project,
+ },
+ inv.party_account_currency,
+ item=inv,
+ )
)
gl_entries.append(
- self.get_gl_dict({
- "account": self.income_account,
- "against": self.customer,
- "credit": dunning_in_company_currency,
- "cost_center": inv.cost_center or default_cost_center,
- "credit_in_account_currency": self.dunning_amount,
- "project": inv.project
- }, item=inv)
+ self.get_gl_dict(
+ {
+ "account": self.income_account,
+ "against": self.customer,
+ "credit": dunning_in_company_currency,
+ "cost_center": inv.cost_center or default_cost_center,
+ "credit_in_account_currency": self.dunning_amount,
+ "project": inv.project,
+ },
+ item=inv,
+ )
+ )
+ make_gl_entries(
+ gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False
)
- make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False)
def resolve_dunning(doc, state):
for reference in doc.references:
- if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0:
- dunnings = frappe.get_list('Dunning', filters={
- 'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}, ignore_permissions=True)
+ if reference.reference_doctype == "Sales Invoice" and reference.outstanding_amount <= 0:
+ dunnings = frappe.get_list(
+ "Dunning",
+ filters={"sales_invoice": reference.reference_name, "status": ("!=", "Resolved")},
+ ignore_permissions=True,
+ )
for dunning in dunnings:
- frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved')
+ frappe.db.set_value("Dunning", dunning.name, "status", "Resolved")
+
def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_fee, overdue_days):
interest_amount = 0
@@ -102,23 +123,26 @@ def calculate_interest_and_amount(outstanding_amount, rate_of_interest, dunning_
grand_total += flt(interest_amount)
dunning_amount = flt(interest_amount) + flt(dunning_fee)
return {
- 'interest_amount': interest_amount,
- 'grand_total': grand_total,
- 'dunning_amount': dunning_amount}
+ "interest_amount": interest_amount,
+ "grand_total": grand_total,
+ "dunning_amount": dunning_amount,
+ }
+
@frappe.whitelist()
def get_dunning_letter_text(dunning_type, doc, language=None):
if isinstance(doc, string_types):
doc = json.loads(doc)
if language:
- filters = {'parent': dunning_type, 'language': language}
+ filters = {"parent": dunning_type, "language": language}
else:
- filters = {'parent': dunning_type, 'is_default_language': 1}
- letter_text = frappe.db.get_value('Dunning Letter Text', filters,
- ['body_text', 'closing_text', 'language'], as_dict=1)
+ filters = {"parent": dunning_type, "is_default_language": 1}
+ letter_text = frappe.db.get_value(
+ "Dunning Letter Text", filters, ["body_text", "closing_text", "language"], as_dict=1
+ )
if letter_text:
return {
- 'body_text': frappe.render_template(letter_text.body_text, doc),
- 'closing_text': frappe.render_template(letter_text.closing_text, doc),
- 'language': letter_text.language
+ "body_text": frappe.render_template(letter_text.body_text, doc),
+ "closing_text": frappe.render_template(letter_text.closing_text, doc),
+ "language": letter_text.language,
}
diff --git a/erpnext/accounts/doctype/dunning/dunning_dashboard.py b/erpnext/accounts/doctype/dunning/dunning_dashboard.py
index ebe4efb0985..d1d40314104 100644
--- a/erpnext/accounts/doctype/dunning/dunning_dashboard.py
+++ b/erpnext/accounts/doctype/dunning/dunning_dashboard.py
@@ -1,18 +1,12 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'dunning',
- 'non_standard_fieldnames': {
- 'Journal Entry': 'reference_name',
- 'Payment Entry': 'reference_name'
+ "fieldname": "dunning",
+ "non_standard_fieldnames": {
+ "Journal Entry": "reference_name",
+ "Payment Entry": "reference_name",
},
- 'transactions': [
- {
- 'label': _('Payment'),
- 'items': ['Payment Entry', 'Journal Entry']
- }
- ]
+ "transactions": [{"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]}],
}
diff --git a/erpnext/accounts/doctype/dunning/test_dunning.py b/erpnext/accounts/doctype/dunning/test_dunning.py
index 27543a89fc0..e1fd1e984f5 100644
--- a/erpnext/accounts/doctype/dunning/test_dunning.py
+++ b/erpnext/accounts/doctype/dunning/test_dunning.py
@@ -30,31 +30,35 @@ class TestDunning(unittest.TestCase):
def test_dunning(self):
dunning = create_dunning()
amounts = calculate_interest_and_amount(
- dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
- self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44)
- self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44)
- self.assertEqual(round(amounts.get('grand_total'), 2), 120.44)
+ dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
+ )
+ self.assertEqual(round(amounts.get("interest_amount"), 2), 0.44)
+ self.assertEqual(round(amounts.get("dunning_amount"), 2), 20.44)
+ self.assertEqual(round(amounts.get("grand_total"), 2), 120.44)
def test_dunning_with_zero_interest_rate(self):
dunning = create_dunning_with_zero_interest_rate()
amounts = calculate_interest_and_amount(
- dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days)
- self.assertEqual(round(amounts.get('interest_amount'), 2), 0)
- self.assertEqual(round(amounts.get('dunning_amount'), 2), 20)
- self.assertEqual(round(amounts.get('grand_total'), 2), 120)
-
+ dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days
+ )
+ self.assertEqual(round(amounts.get("interest_amount"), 2), 0)
+ self.assertEqual(round(amounts.get("dunning_amount"), 2), 20)
+ self.assertEqual(round(amounts.get("grand_total"), 2), 120)
def test_gl_entries(self):
dunning = create_dunning()
dunning.submit()
- gl_entries = frappe.db.sql("""select account, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, debit, credit
from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s
- order by account asc""", dunning.name, as_dict=1)
+ order by account asc""",
+ dunning.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
- expected_values = dict((d[0], d) for d in [
- ['Debtors - _TC', 20.44, 0.0],
- ['Sales - _TC', 0.0, 20.44]
- ])
+ expected_values = dict(
+ (d[0], d) for d in [["Debtors - _TC", 20.44, 0.0], ["Sales - _TC", 0.0, 20.44]]
+ )
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
@@ -72,7 +76,7 @@ class TestDunning(unittest.TestCase):
pe.target_exchange_rate = 1
pe.insert()
pe.submit()
- si_doc = frappe.get_doc('Sales Invoice', dunning.sales_invoice)
+ si_doc = frappe.get_doc("Sales Invoice", dunning.sales_invoice)
self.assertEqual(si_doc.outstanding_amount, 0)
@@ -80,8 +84,9 @@ def create_dunning():
posting_date = add_days(today(), -20)
due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center(
- posting_date=posting_date, due_date=due_date, status='Overdue')
- dunning_type = frappe.get_doc("Dunning Type", 'First Notice')
+ posting_date=posting_date, due_date=due_date, status="Overdue"
+ )
+ dunning_type = frappe.get_doc("Dunning Type", "First Notice")
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
@@ -91,18 +96,20 @@ def create_dunning():
dunning.company = sales_invoice.company
dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date
- dunning.dunning_type = 'First Notice'
+ dunning.dunning_type = "First Notice"
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
+
def create_dunning_with_zero_interest_rate():
posting_date = add_days(today(), -20)
due_date = add_days(today(), -15)
sales_invoice = create_sales_invoice_against_cost_center(
- posting_date=posting_date, due_date=due_date, status='Overdue')
- dunning_type = frappe.get_doc("Dunning Type", 'First Notice with 0% Rate of Interest')
+ posting_date=posting_date, due_date=due_date, status="Overdue"
+ )
+ dunning_type = frappe.get_doc("Dunning Type", "First Notice with 0% Rate of Interest")
dunning = frappe.new_doc("Dunning")
dunning.sales_invoice = sales_invoice.name
dunning.customer_name = sales_invoice.customer_name
@@ -112,40 +119,44 @@ def create_dunning_with_zero_interest_rate():
dunning.company = sales_invoice.company
dunning.posting_date = nowdate()
dunning.due_date = sales_invoice.due_date
- dunning.dunning_type = 'First Notice with 0% Rate of Interest'
+ dunning.dunning_type = "First Notice with 0% Rate of Interest"
dunning.rate_of_interest = dunning_type.rate_of_interest
dunning.dunning_fee = dunning_type.dunning_fee
dunning.save()
return dunning
+
def create_dunning_type():
dunning_type = frappe.new_doc("Dunning Type")
- dunning_type.dunning_type = 'First Notice'
+ dunning_type.dunning_type = "First Notice"
dunning_type.start_day = 10
dunning_type.end_day = 20
dunning_type.dunning_fee = 20
dunning_type.rate_of_interest = 8
dunning_type.append(
- "dunning_letter_text", {
- 'language': 'en',
- 'body_text': 'We have still not received payment for our invoice ',
- 'closing_text': 'We kindly request that you pay the outstanding amount immediately, including interest and late fees.'
- }
+ "dunning_letter_text",
+ {
+ "language": "en",
+ "body_text": "We have still not received payment for our invoice ",
+ "closing_text": "We kindly request that you pay the outstanding amount immediately, including interest and late fees.",
+ },
)
dunning_type.save()
+
def create_dunning_type_with_zero_interest_rate():
dunning_type = frappe.new_doc("Dunning Type")
- dunning_type.dunning_type = 'First Notice with 0% Rate of Interest'
+ dunning_type.dunning_type = "First Notice with 0% Rate of Interest"
dunning_type.start_day = 10
dunning_type.end_day = 20
dunning_type.dunning_fee = 20
dunning_type.rate_of_interest = 0
dunning_type.append(
- "dunning_letter_text", {
- 'language': 'en',
- 'body_text': 'We have still not received payment for our invoice ',
- 'closing_text': 'We kindly request that you pay the outstanding amount immediately, and late fees.'
- }
+ "dunning_letter_text",
+ {
+ "language": "en",
+ "body_text": "We have still not received payment for our invoice ",
+ "closing_text": "We kindly request that you pay the outstanding amount immediately, and late fees.",
+ },
)
dunning_type.save()
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
index 1b13195ce98..2f81c5fb750 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -20,8 +20,9 @@ class ExchangeRateRevaluation(Document):
def set_total_gain_loss(self):
total_gain_loss = 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"))
+ 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"))
total_gain_loss += flt(d.gain_loss, d.precision("gain_loss"))
self.total_gain_loss = flt(total_gain_loss, self.precision("total_gain_loss"))
@@ -30,15 +31,15 @@ class ExchangeRateRevaluation(Document):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
def on_cancel(self):
- self.ignore_linked_doctypes = ('GL Entry')
+ self.ignore_linked_doctypes = "GL Entry"
@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")
+ total_debit = frappe.db.get_value(
+ "Journal Entry Account",
+ {"reference_type": "Exchange Rate Revaluation", "reference_name": self.name, "docstatus": 1},
+ "sum(debit) as sum",
+ )
total_amt = 0
for d in self.accounts:
@@ -54,28 +55,33 @@ class ExchangeRateRevaluation(Document):
accounts = []
self.validate_mandatory()
company_currency = erpnext.get_company_currency(self.company)
- precision = get_field_precision(frappe.get_meta("Exchange Rate Revaluation Account")
- .get_field("new_balance_in_base_currency"), company_currency)
+ 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
+ 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
- })
+ 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 not accounts:
self.throw_invalid_response_message(account_details)
@@ -84,7 +90,8 @@ class ExchangeRateRevaluation(Document):
def get_accounts_from_gle(self):
company_currency = erpnext.get_company_currency(self.company)
- accounts = frappe.db.sql_list("""
+ accounts = frappe.db.sql_list(
+ """
select name
from tabAccount
where is_group = 0
@@ -93,11 +100,14 @@ class ExchangeRateRevaluation(Document):
and account_type != 'Stock'
and company=%s
and account_currency != %s
- order by name""",(self.company, company_currency))
+ order by name""",
+ (self.company, company_currency),
+ )
account_details = []
if accounts:
- account_details = frappe.db.sql("""
+ 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,
@@ -109,7 +119,11 @@ class ExchangeRateRevaluation(Document):
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)
+ """
+ % (", ".join(["%s"] * len(accounts)), "%s"),
+ tuple(accounts + [self.posting_date]),
+ as_dict=1,
+ )
return account_details
@@ -125,77 +139,107 @@ class ExchangeRateRevaluation(Document):
if self.total_gain_loss == 0:
return
- unrealized_exchange_gain_loss_account = frappe.get_cached_value('Company', self.company,
- "unrealized_exchange_gain_loss_account")
+ unrealized_exchange_gain_loss_account = frappe.get_cached_value(
+ "Company", self.company, "unrealized_exchange_gain_loss_account"
+ )
if not unrealized_exchange_gain_loss_account:
- frappe.throw(_("Please set Unrealized Exchange Gain/Loss Account in Company {0}")
- .format(self.company))
+ frappe.throw(
+ _("Please set Unrealized Exchange Gain/Loss Account in Company {0}").format(self.company)
+ )
- journal_entry = frappe.new_doc('Journal Entry')
- journal_entry.voucher_type = 'Exchange Rate Revaluation'
+ journal_entry = frappe.new_doc("Journal Entry")
+ journal_entry.voucher_type = "Exchange Rate Revaluation"
journal_entry.company = self.company
journal_entry.posting_date = self.posting_date
journal_entry.multi_currency = 1
journal_entry_accounts = []
for d in self.accounts:
- dr_or_cr = "debit_in_account_currency" \
- if d.get("balance_in_account_currency") > 0 else "credit_in_account_currency"
+ dr_or_cr = (
+ "debit_in_account_currency"
+ if d.get("balance_in_account_currency") > 0
+ else "credit_in_account_currency"
+ )
- reverse_dr_or_cr = "debit_in_account_currency" \
- if dr_or_cr=="credit_in_account_currency" else "credit_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_entry_accounts.append({
- "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")),
- dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
- "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
+ journal_entry_accounts.append(
+ {
+ "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")
+ ),
+ dr_or_cr: flt(
+ abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
+ ),
+ "exchange_rate": flt(d.get("new_exchange_rate"), d.precision("new_exchange_rate")),
+ "reference_type": "Exchange Rate Revaluation",
+ "reference_name": self.name,
+ }
+ )
+ journal_entry_accounts.append(
+ {
+ "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")
+ ),
+ reverse_dr_or_cr: flt(
+ abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")
+ ),
+ "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
+ "reference_type": "Exchange Rate Revaluation",
+ "reference_name": self.name,
+ }
+ )
+
+ journal_entry_accounts.append(
+ {
+ "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,
+ "exchange_rate": 1,
"reference_type": "Exchange Rate Revaluation",
"reference_name": self.name,
- })
- journal_entry_accounts.append({
- "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")),
- reverse_dr_or_cr: flt(abs(d.get("balance_in_account_currency")), d.precision("balance_in_account_currency")),
- "exchange_rate": flt(d.get("current_exchange_rate"), d.precision("current_exchange_rate")),
- "reference_type": "Exchange Rate Revaluation",
- "reference_name": self.name
- })
-
- journal_entry_accounts.append({
- "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,
- "exchange_rate": 1,
- "reference_type": "Exchange Rate Revaluation",
- "reference_name": self.name,
- })
+ }
+ )
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()
+
@frappe.whitelist()
def get_account_details(account, company, posting_date, party_type=None, party=None):
- account_currency, account_type = frappe.db.get_value("Account", account,
- ["account_currency", "account_type"])
+ account_currency, account_type = frappe.db.get_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)
+ balance = get_balance_on(
+ account, date=posting_date, party_type=party_type, party=party, in_account_currency=False
+ )
if balance:
- balance_in_account_currency = get_balance_on(account, date=posting_date, party_type=party_type, party=party)
- current_exchange_rate = balance / balance_in_account_currency if balance_in_account_currency else 0
+ balance_in_account_currency = get_balance_on(
+ account, date=posting_date, party_type=party_type, party=party
+ )
+ current_exchange_rate = (
+ balance / balance_in_account_currency if balance_in_account_currency else 0
+ )
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 = {
@@ -204,7 +248,7 @@ def get_account_details(account, company, posting_date, party_type=None, party=N
"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
+ "new_balance_in_base_currency": new_balance_in_base_currency,
}
return account_details
diff --git a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py
index 0efe291d217..7eca9703c24 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation_dashboard.py
@@ -1,11 +1,2 @@
-
-
def get_data():
- return {
- 'fieldname': 'reference_name',
- 'transactions': [
- {
- 'items': ['Journal Entry']
- }
- ]
- }
+ return {"fieldname": "reference_name", "transactions": [{"items": ["Journal Entry"]}]}
diff --git a/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py b/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py
index 4a56cd39d00..24e6c0c8721 100644
--- a/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py
+++ b/erpnext/accounts/doctype/finance_book/finance_book_dashboard.py
@@ -1,24 +1,13 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'finance_book',
- 'non_standard_fieldnames': {
- 'Asset': 'default_finance_book',
- 'Company': 'default_finance_book'
- },
- 'transactions': [
- {
- 'label': _('Assets'),
- 'items': ['Asset', 'Asset Value Adjustment']
- },
- {
- 'items': ['Company']
- },
- {
- 'items': ['Journal Entry']
- }
- ]
+ "fieldname": "finance_book",
+ "non_standard_fieldnames": {"Asset": "default_finance_book", "Company": "default_finance_book"},
+ "transactions": [
+ {"label": _("Assets"), "items": ["Asset", "Asset Value Adjustment"]},
+ {"items": ["Company"]},
+ {"items": ["Journal Entry"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/finance_book/test_finance_book.py b/erpnext/accounts/doctype/finance_book/test_finance_book.py
index 8fbf72d7c1c..7b2575d2c32 100644
--- a/erpnext/accounts/doctype/finance_book/test_finance_book.py
+++ b/erpnext/accounts/doctype/finance_book/test_finance_book.py
@@ -13,31 +13,30 @@ class TestFinanceBook(unittest.TestCase):
finance_book = create_finance_book()
# create jv entry
- jv = make_journal_entry("_Test Bank - _TC",
- "_Test Receivable - _TC", 100, save=False)
+ jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False)
- jv.accounts[1].update({
- "party_type": "Customer",
- "party": "_Test Customer"
- })
+ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer"})
jv.finance_book = finance_book.finance_book_name
jv.submit()
# check the Finance Book in the GL Entry
- gl_entries = frappe.get_all("GL Entry", fields=["name", "finance_book"],
- filters={"voucher_type": "Journal Entry", "voucher_no": jv.name})
+ gl_entries = frappe.get_all(
+ "GL Entry",
+ fields=["name", "finance_book"],
+ filters={"voucher_type": "Journal Entry", "voucher_no": jv.name},
+ )
for gl_entry in gl_entries:
self.assertEqual(gl_entry.finance_book, finance_book.name)
+
def create_finance_book():
if not frappe.db.exists("Finance Book", "_Test Finance Book"):
- finance_book = frappe.get_doc({
- "doctype": "Finance Book",
- "finance_book_name": "_Test Finance Book"
- }).insert()
+ finance_book = frappe.get_doc(
+ {"doctype": "Finance Book", "finance_book_name": "_Test Finance Book"}
+ ).insert()
else:
finance_book = frappe.get_doc("Finance Book", "_Test Finance Book")
- return finance_book
\ No newline at end of file
+ return finance_book
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
index dd893f9fc80..069ab5ea843 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
@@ -9,7 +9,9 @@ from frappe.model.document import Document
from frappe.utils import add_days, add_years, cstr, getdate
-class FiscalYearIncorrectDate(frappe.ValidationError): pass
+class FiscalYearIncorrectDate(frappe.ValidationError):
+ pass
+
class FiscalYear(Document):
@frappe.whitelist()
@@ -22,19 +24,33 @@ class FiscalYear(Document):
# clear cache
frappe.clear_cache()
- msgprint(_("{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect.").format(self.name))
+ msgprint(
+ _(
+ "{0} is now the default Fiscal Year. Please refresh your browser for the change to take effect."
+ ).format(self.name)
+ )
def validate(self):
self.validate_dates()
self.validate_overlap()
if not self.is_new():
- year_start_end_dates = frappe.db.sql("""select year_start_date, year_end_date
- from `tabFiscal Year` where name=%s""", (self.name))
+ year_start_end_dates = frappe.db.sql(
+ """select year_start_date, year_end_date
+ from `tabFiscal Year` where name=%s""",
+ (self.name),
+ )
if year_start_end_dates:
- if getdate(self.year_start_date) != year_start_end_dates[0][0] or getdate(self.year_end_date) != year_start_end_dates[0][1]:
- frappe.throw(_("Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."))
+ if (
+ getdate(self.year_start_date) != year_start_end_dates[0][0]
+ or getdate(self.year_end_date) != year_start_end_dates[0][1]
+ ):
+ frappe.throw(
+ _(
+ "Cannot change Fiscal Year Start Date and Fiscal Year End Date once the Fiscal Year is saved."
+ )
+ )
def validate_dates(self):
if self.is_short_year:
@@ -43,14 +59,18 @@ class FiscalYear(Document):
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)
+ 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.throw(
+ _("Fiscal Year End Date should be one year after Fiscal Year Start Date"),
+ FiscalYearIncorrectDate,
+ )
def on_update(self):
check_duplicate_fiscal_year(self)
@@ -59,11 +79,16 @@ class FiscalYear(Document):
def on_trash(self):
global_defaults = frappe.get_doc("Global Defaults")
if global_defaults.current_fiscal_year == self.name:
- frappe.throw(_("You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings").format(self.name))
+ frappe.throw(
+ _(
+ "You cannot delete Fiscal Year {0}. Fiscal Year {0} is set as default in Global Settings"
+ ).format(self.name)
+ )
frappe.cache().delete_value("fiscal_years")
def validate_overlap(self):
- existing_fiscal_years = frappe.db.sql("""select name from `tabFiscal Year`
+ existing_fiscal_years = frappe.db.sql(
+ """select name from `tabFiscal Year`
where (
(%(year_start_date)s between year_start_date and year_end_date)
or (%(year_end_date)s between year_start_date and year_end_date)
@@ -73,13 +98,18 @@ class FiscalYear(Document):
{
"year_start_date": self.year_start_date,
"year_end_date": self.year_end_date,
- "name": self.name or "No Name"
- }, as_dict=True)
+ "name": self.name or "No Name",
+ },
+ as_dict=True,
+ )
if existing_fiscal_years:
for existing in existing_fiscal_years:
- company_for_existing = frappe.db.sql_list("""select company from `tabFiscal Year Company`
- where parent=%s""", existing.name)
+ company_for_existing = frappe.db.sql_list(
+ """select company from `tabFiscal Year Company`
+ where parent=%s""",
+ existing.name,
+ )
overlap = False
if not self.get("companies") or not company_for_existing:
@@ -90,20 +120,36 @@ class FiscalYear(Document):
overlap = True
if overlap:
- frappe.throw(_("Year start date or end date is overlapping with {0}. To avoid please set company")
- .format(existing.name), frappe.NameError)
+ frappe.throw(
+ _("Year start date or end date is overlapping with {0}. To avoid please set company").format(
+ existing.name
+ ),
+ frappe.NameError,
+ )
+
@frappe.whitelist()
def check_duplicate_fiscal_year(doc):
- year_start_end_dates = frappe.db.sql("""select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""", (doc.name))
+ year_start_end_dates = frappe.db.sql(
+ """select name, year_start_date, year_end_date from `tabFiscal Year` where name!=%s""",
+ (doc.name),
+ )
for fiscal_year, ysd, yed in year_start_end_dates:
- if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (not frappe.flags.in_test):
- frappe.throw(_("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(fiscal_year))
+ if (getdate(doc.year_start_date) == ysd and getdate(doc.year_end_date) == yed) and (
+ not frappe.flags.in_test
+ ):
+ frappe.throw(
+ _("Fiscal Year Start Date and Fiscal Year End Date are already set in Fiscal Year {0}").format(
+ fiscal_year
+ )
+ )
@frappe.whitelist()
def auto_create_fiscal_year():
- for d in frappe.db.sql("""select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""):
+ for d in frappe.db.sql(
+ """select name from `tabFiscal Year` where year_end_date = date_add(current_date, interval 3 day)"""
+ ):
try:
current_fy = frappe.get_doc("Fiscal Year", d[0])
@@ -114,16 +160,14 @@ def auto_create_fiscal_year():
start_year = cstr(new_fy.year_start_date.year)
end_year = cstr(new_fy.year_end_date.year)
- new_fy.year = start_year if start_year==end_year else (start_year + "-" + end_year)
+ new_fy.year = start_year if start_year == end_year else (start_year + "-" + end_year)
new_fy.auto_created = 1
new_fy.insert(ignore_permissions=True)
except frappe.NameError:
pass
+
def get_from_and_to_date(fiscal_year):
- fields = [
- "year_start_date as from_date",
- "year_end_date as to_date"
- ]
+ 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)
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py
index 3ede25f19d4..bc966916ef3 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year_dashboard.py
@@ -1,22 +1,15 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'fiscal_year',
- 'transactions': [
+ "fieldname": "fiscal_year",
+ "transactions": [
+ {"label": _("Budgets"), "items": ["Budget"]},
+ {"label": _("References"), "items": ["Period Closing Voucher"]},
{
- 'label': _('Budgets'),
- 'items': ['Budget']
+ "label": _("Target Details"),
+ "items": ["Sales Person", "Sales Partner", "Territory", "Monthly Distribution"],
},
- {
- 'label': _('References'),
- 'items': ['Period Closing Voucher']
- },
- {
- 'label': _('Target Details'),
- 'items': ['Sales Person', 'Sales Partner', 'Territory', 'Monthly Distribution']
- }
- ]
+ ],
}
diff --git a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
index 69e13a407de..6e946f74660 100644
--- a/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/test_fiscal_year.py
@@ -11,43 +11,48 @@ from erpnext.accounts.doctype.fiscal_year.fiscal_year import FiscalYearIncorrect
test_ignore = ["Company"]
-class TestFiscalYear(unittest.TestCase):
+class TestFiscalYear(unittest.TestCase):
def test_extra_year(self):
if frappe.db.exists("Fiscal Year", "_Test Fiscal Year 2000"):
frappe.delete_doc("Fiscal Year", "_Test Fiscal Year 2000")
- fy = frappe.get_doc({
- "doctype": "Fiscal Year",
- "year": "_Test Fiscal Year 2000",
- "year_end_date": "2002-12-31",
- "year_start_date": "2000-04-01"
- })
+ fy = frappe.get_doc(
+ {
+ "doctype": "Fiscal Year",
+ "year": "_Test Fiscal Year 2000",
+ "year_end_date": "2002-12-31",
+ "year_start_date": "2000-04-01",
+ }
+ )
self.assertRaises(FiscalYearIncorrectDate, fy.insert)
def test_record_generator():
test_records = [
- {
- "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"
- }
+ {
+ "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",
+ }
]
start = 2012
end = now_datetime().year + 5
for year in range(start, end):
- test_records.append({
- "doctype": "Fiscal Year",
- "year": f"_Test Fiscal Year {year}",
- "year_start_date": f"{year}-01-01",
- "year_end_date": f"{year}-12-31"
- })
+ test_records.append(
+ {
+ "doctype": "Fiscal Year",
+ "year": f"_Test Fiscal Year {year}",
+ "year_start_date": f"{year}-01-01",
+ "year_end_date": f"{year}-12-31",
+ }
+ )
return test_records
+
test_records = test_record_generator()
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index f184b95a92d..8a3f9341182 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -26,6 +26,8 @@ from erpnext.exceptions import (
)
exclude_from_linked_with = True
+
+
class GLEntry(Document):
def autoname(self):
"""
@@ -33,13 +35,15 @@ class GLEntry(Document):
name will be changed using autoname options (in a scheduled job)
"""
self.name = frappe.generate_hash(txt="", length=10)
+ if self.meta.autoname == "hash":
+ self.to_rename = 0
def validate(self):
self.flags.ignore_submit_comment = True
self.validate_and_set_fiscal_year()
self.pl_must_have_cost_center()
- if not self.flags.from_repost:
+ if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.check_mandatory()
self.validate_cost_center()
self.check_pl_account()
@@ -48,7 +52,7 @@ class GLEntry(Document):
def on_update(self):
adv_adj = self.flags.adv_adj
- if not self.flags.from_repost:
+ if not self.flags.from_repost and self.voucher_type != "Period Closing Voucher":
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
@@ -56,14 +60,18 @@ class GLEntry(Document):
validate_frozen_account(self.account, adv_adj)
# Update outstanding amt on against voucher
- if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees']
- and self.against_voucher and self.flags.update_outstanding == 'Yes'
- and not frappe.flags.is_reverse_depr_entry):
- update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
- self.against_voucher)
+ if (
+ self.against_voucher_type in ["Journal Entry", "Sales Invoice", "Purchase Invoice", "Fees"]
+ and self.against_voucher
+ and self.flags.update_outstanding == "Yes"
+ and not frappe.flags.is_reverse_depr_entry
+ ):
+ update_outstanding_amt(
+ self.account, self.party_type, self.party, self.against_voucher_type, self.against_voucher
+ )
def check_mandatory(self):
- mandatory = ['account','voucher_type','voucher_no','company']
+ mandatory = ["account", "voucher_type", "voucher_no", "company"]
for k in mandatory:
if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
@@ -71,29 +79,40 @@ class GLEntry(Document):
if not (self.party_type and self.party):
account_type = frappe.get_cached_value("Account", self.account, "account_type")
if account_type == "Receivable":
- frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
- .format(self.voucher_type, self.voucher_no, self.account))
+ frappe.throw(
+ _("{0} {1}: Customer is required against Receivable account {2}").format(
+ self.voucher_type, self.voucher_no, self.account
+ )
+ )
elif account_type == "Payable":
- frappe.throw(_("{0} {1}: Supplier is required against Payable account {2}")
- .format(self.voucher_type, self.voucher_no, self.account))
+ frappe.throw(
+ _("{0} {1}: Supplier is required against Payable account {2}").format(
+ self.voucher_type, self.voucher_no, self.account
+ )
+ )
# Zero value transaction is not allowed
if not (flt(self.debit, self.precision("debit")) or flt(self.credit, self.precision("credit"))):
- frappe.throw(_("{0} {1}: Either debit or credit amount is required for {2}")
- .format(self.voucher_type, self.voucher_no, self.account))
+ frappe.throw(
+ _("{0} {1}: Either debit or credit amount is required for {2}").format(
+ self.voucher_type, self.voucher_no, self.account
+ )
+ )
def pl_must_have_cost_center(self):
"""Validate that profit and loss type account GL entries have a cost center."""
- if self.cost_center or self.voucher_type == 'Period Closing Voucher':
+ if self.cost_center or self.voucher_type == "Period Closing Voucher":
return
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
- self.voucher_type, self.voucher_no, self.account)
+ self.voucher_type, self.voucher_no, self.account
+ )
msg += " "
- msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
- self.voucher_type)
+ msg += _(
+ "Please set the cost center field in {0} or setup a default Cost Center for the Company."
+ ).format(self.voucher_type)
frappe.throw(msg, title=_("Missing Cost Center"))
@@ -101,17 +120,31 @@ class GLEntry(Document):
account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
- if account_type == "Profit and Loss" \
- and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled:
+ if (
+ account_type == "Profit and Loss"
+ and self.company == dimension.company
+ and dimension.mandatory_for_pl
+ and not dimension.disabled
+ ):
if not self.get(dimension.fieldname):
- frappe.throw(_("Accounting Dimension {0} is required for 'Profit and Loss' account {1}.")
- .format(dimension.label, self.account))
+ frappe.throw(
+ _("Accounting Dimension {0} is required for 'Profit and Loss' account {1}.").format(
+ dimension.label, self.account
+ )
+ )
- if account_type == "Balance Sheet" \
- and self.company == dimension.company and dimension.mandatory_for_bs and not dimension.disabled:
+ if (
+ account_type == "Balance Sheet"
+ and self.company == dimension.company
+ and dimension.mandatory_for_bs
+ and not dimension.disabled
+ ):
if not self.get(dimension.fieldname):
- frappe.throw(_("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.")
- .format(dimension.label, self.account))
+ frappe.throw(
+ _("Accounting Dimension {0} is required for 'Balance Sheet' account {1}.").format(
+ dimension.label, self.account
+ )
+ )
def validate_allowed_dimensions(self):
dimension_filter_map = get_dimension_filter_map()
@@ -120,56 +153,97 @@ class GLEntry(Document):
account = key[1]
if self.account == account:
- if value['is_mandatory'] and not self.get(dimension):
- frappe.throw(_("{0} is mandatory for account {1}").format(
- frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError)
+ if value["is_mandatory"] and not self.get(dimension):
+ frappe.throw(
+ _("{0} is mandatory for account {1}").format(
+ frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)
+ ),
+ MandatoryAccountDimensionError,
+ )
- if value['allow_or_restrict'] == 'Allow':
- if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']:
- frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
- frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
+ if value["allow_or_restrict"] == "Allow":
+ if self.get(dimension) and self.get(dimension) not in value["allowed_dimensions"]:
+ frappe.throw(
+ _("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(self.get(dimension)),
+ frappe.bold(frappe.unscrub(dimension)),
+ frappe.bold(self.account),
+ ),
+ InvalidAccountDimensionError,
+ )
else:
- if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']:
- frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
- frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
+ if self.get(dimension) and self.get(dimension) in value["allowed_dimensions"]:
+ frappe.throw(
+ _("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(self.get(dimension)),
+ frappe.bold(frappe.unscrub(dimension)),
+ frappe.bold(self.account),
+ ),
+ InvalidAccountDimensionError,
+ )
def check_pl_account(self):
- if self.is_opening=='Yes' and \
- frappe.db.get_value("Account", self.account, "report_type")=="Profit and Loss":
- frappe.throw(_("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry")
- .format(self.voucher_type, self.voucher_no, self.account))
+ if (
+ self.is_opening == "Yes"
+ and frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss"
+ and not self.is_cancelled
+ ):
+ frappe.throw(
+ _("{0} {1}: 'Profit and Loss' type account {2} not allowed in Opening Entry").format(
+ self.voucher_type, self.voucher_no, self.account
+ )
+ )
def validate_account_details(self, adv_adj):
"""Account must be ledger, active and not freezed"""
- ret = frappe.db.sql("""select is_group, docstatus, company
- from tabAccount where name=%s""", self.account, as_dict=1)[0]
+ ret = frappe.db.sql(
+ """select is_group, docstatus, company
+ from tabAccount where name=%s""",
+ self.account,
+ as_dict=1,
+ )[0]
- if ret.is_group==1:
- frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''')
- .format(self.voucher_type, self.voucher_no, self.account))
+ if ret.is_group == 1:
+ frappe.throw(
+ _(
+ """{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions"""
+ ).format(self.voucher_type, self.voucher_no, self.account)
+ )
- if ret.docstatus==2:
- frappe.throw(_("{0} {1}: Account {2} is inactive")
- .format(self.voucher_type, self.voucher_no, self.account))
+ if ret.docstatus == 2:
+ frappe.throw(
+ _("{0} {1}: Account {2} is inactive").format(self.voucher_type, self.voucher_no, self.account)
+ )
if ret.company != self.company:
- frappe.throw(_("{0} {1}: Account {2} does not belong to Company {3}")
- .format(self.voucher_type, self.voucher_no, self.account, self.company))
+ frappe.throw(
+ _("{0} {1}: Account {2} does not belong to Company {3}").format(
+ self.voucher_type, self.voucher_no, self.account, self.company
+ )
+ )
def validate_cost_center(self):
- if not self.cost_center: return
+ if not self.cost_center:
+ return
- is_group, company = frappe.get_cached_value('Cost Center',
- self.cost_center, ['is_group', 'company'])
+ is_group, company = frappe.get_cached_value(
+ "Cost Center", self.cost_center, ["is_group", "company"]
+ )
if company != self.company:
- frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}")
- .format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
+ frappe.throw(
+ _("{0} {1}: Cost Center {2} does not belong to Company {3}").format(
+ self.voucher_type, self.voucher_no, self.cost_center, self.company
+ )
+ )
- if (self.voucher_type != 'Period Closing Voucher' and is_group):
- frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format(
- self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
+ if self.voucher_type != "Period Closing Voucher" and is_group:
+ frappe.throw(
+ _(
+ """{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions"""
+ ).format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center))
+ )
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
@@ -182,9 +256,12 @@ class GLEntry(Document):
self.account_currency = account_currency or company_currency
if account_currency != self.account_currency:
- frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}")
- .format(self.voucher_type, self.voucher_no, self.account,
- (account_currency or company_currency)), InvalidAccountCurrency)
+ frappe.throw(
+ _("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}").format(
+ self.voucher_type, self.voucher_no, self.account, (account_currency or company_currency)
+ ),
+ InvalidAccountCurrency,
+ )
if self.party_type and self.party:
validate_party_gle_currency(self.party_type, self.party, self.company, self.account_currency)
@@ -193,51 +270,80 @@ class GLEntry(Document):
if not self.fiscal_year:
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
+
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")
if balance_must_be:
- balance = frappe.db.sql("""select sum(debit) - sum(credit)
- from `tabGL Entry` where account = %s""", account)[0][0]
+ balance = frappe.db.sql(
+ """select sum(debit) - sum(credit)
+ from `tabGL Entry` where account = %s""",
+ account,
+ )[0][0]
- if (balance_must_be=="Debit" and flt(balance) < 0) or \
- (balance_must_be=="Credit" and flt(balance) > 0):
- frappe.throw(_("Balance for Account {0} must always be {1}").format(account, _(balance_must_be)))
+ if (balance_must_be == "Debit" and flt(balance) < 0) or (
+ balance_must_be == "Credit" and flt(balance) > 0
+ ):
+ frappe.throw(
+ _("Balance for Account {0} must always be {1}").format(account, _(balance_must_be))
+ )
-def update_outstanding_amt(account, party_type, party, against_voucher_type, against_voucher, on_cancel=False):
+
+def update_outstanding_amt(
+ account, party_type, party, against_voucher_type, against_voucher, on_cancel=False
+):
if party_type and party:
- party_condition = " and party_type={0} and party={1}"\
- .format(frappe.db.escape(party_type), frappe.db.escape(party))
+ party_condition = " and party_type={0} and party={1}".format(
+ frappe.db.escape(party_type), frappe.db.escape(party)
+ )
else:
party_condition = ""
if against_voucher_type == "Sales Invoice":
party_account = frappe.db.get_value(against_voucher_type, against_voucher, "debit_to")
- account_condition = "and account in ({0}, {1})".format(frappe.db.escape(account), frappe.db.escape(party_account))
+ account_condition = "and account in ({0}, {1})".format(
+ frappe.db.escape(account), frappe.db.escape(party_account)
+ )
else:
account_condition = " and account = {0}".format(frappe.db.escape(account))
# get final outstanding amt
- bal = flt(frappe.db.sql("""
+ bal = flt(
+ frappe.db.sql(
+ """
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry`
where against_voucher_type=%s and against_voucher=%s
and voucher_type != 'Invoice Discounting'
- {0} {1}""".format(party_condition, account_condition),
- (against_voucher_type, against_voucher))[0][0] or 0.0)
+ {0} {1}""".format(
+ party_condition, account_condition
+ ),
+ (against_voucher_type, against_voucher),
+ )[0][0]
+ or 0.0
+ )
- if against_voucher_type == 'Purchase Invoice':
+ if against_voucher_type == "Purchase Invoice":
bal = -bal
elif against_voucher_type == "Journal Entry":
- against_voucher_amount = flt(frappe.db.sql("""
+ against_voucher_amount = flt(
+ frappe.db.sql(
+ """
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabGL Entry` where voucher_type = 'Journal Entry' and voucher_no = %s
- and account = %s and (against_voucher is null or against_voucher='') {0}"""
- .format(party_condition), (against_voucher, account))[0][0])
+ and account = %s and (against_voucher is null or against_voucher='') {0}""".format(
+ party_condition
+ ),
+ (against_voucher, account),
+ )[0][0]
+ )
if not against_voucher_amount:
- frappe.throw(_("Against Journal Entry {0} is already adjusted against some other voucher")
- .format(against_voucher))
+ frappe.throw(
+ _("Against Journal Entry {0} is already adjusted against some other voucher").format(
+ against_voucher
+ )
+ )
bal = against_voucher_amount + bal
if against_voucher_amount < 0:
@@ -245,44 +351,51 @@ def update_outstanding_amt(account, party_type, party, against_voucher_type, aga
# Validation : Outstanding can not be negative for JV
if bal < 0 and not on_cancel:
- frappe.throw(_("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal)))
+ frappe.throw(
+ _("Outstanding for {0} cannot be less than zero ({1})").format(against_voucher, fmt_money(bal))
+ )
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)
+ frappe.db.set_value(against_voucher_type, against_voucher, "outstanding_amount", bal)
ref_doc.set_status(update=True)
def validate_frozen_account(account, adv_adj=None):
frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
- if frozen_account == 'Yes' and not adv_adj:
- frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,
- 'frozen_accounts_modifier')
+ if frozen_account == "Yes" and not adv_adj:
+ frozen_accounts_modifier = frappe.db.get_value(
+ "Accounts Settings", None, "frozen_accounts_modifier"
+ )
if not frozen_accounts_modifier:
frappe.throw(_("Account {0} is frozen").format(account))
elif frozen_accounts_modifier not in frappe.get_roles():
frappe.throw(_("Not authorized to edit frozen Account {0}").format(account))
+
def update_against_account(voucher_type, voucher_no):
- entries = frappe.db.get_all("GL Entry",
+ entries = frappe.db.get_all(
+ "GL Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
- fields=["name", "party", "against", "debit", "credit", "account", "company"])
+ fields=["name", "party", "against", "debit", "credit", "account", "company"],
+ )
if not entries:
return
company_currency = erpnext.get_company_currency(entries[0].company)
- precision = get_field_precision(frappe.get_meta("GL Entry")
- .get_field("debit"), company_currency)
+ precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
accounts_debited, accounts_credited = [], []
for d in entries:
- if flt(d.debit, precision) > 0: accounts_debited.append(d.party or d.account)
- if flt(d.credit, precision) > 0: accounts_credited.append(d.party or d.account)
+ if flt(d.debit, precision) > 0:
+ accounts_debited.append(d.party or d.account)
+ if flt(d.credit, precision) > 0:
+ accounts_credited.append(d.party or d.account)
for d in entries:
if flt(d.debit, precision) > 0:
@@ -293,14 +406,17 @@ def update_against_account(voucher_type, voucher_no):
if d.against != new_against:
frappe.db.set_value("GL Entry", d.name, "against", new_against)
+
def on_doctype_update():
frappe.db.add_index("GL Entry", ["against_voucher_type", "against_voucher"])
frappe.db.add_index("GL Entry", ["voucher_type", "voucher_no"])
+
def rename_gle_sle_docs():
for doctype in ["GL Entry", "Stock Ledger Entry"]:
rename_temporarily_named_docs(doctype)
+
def rename_temporarily_named_docs(doctype):
"""Rename temporarily named docs using autoname options"""
docs_to_rename = frappe.get_all(doctype, {"to_rename": "1"}, order_by="creation", limit=50000)
@@ -311,5 +427,5 @@ def rename_temporarily_named_docs(doctype):
frappe.db.sql(
"UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
(newname, oldname),
- auto_commit=True
+ auto_commit=True,
)
diff --git a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py
index 3de23946892..b188b09843a 100644
--- a/erpnext/accounts/doctype/gl_entry/test_gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/test_gl_entry.py
@@ -14,48 +14,68 @@ from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journ
class TestGLEntry(unittest.TestCase):
def test_round_off_entry(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "_Test Write Off - _TC")
- frappe.db.set_value("Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC")
+ frappe.db.set_value(
+ "Company", "_Test Company", "round_off_cost_center", "_Test Cost Center - _TC"
+ )
- jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 100, "_Test Cost Center - _TC", submit=False)
+ jv = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC",
+ "_Test Bank - _TC",
+ 100,
+ "_Test Cost Center - _TC",
+ submit=False,
+ )
jv.get("accounts")[0].debit = 100.01
jv.flags.ignore_validate = True
jv.submit()
- round_off_entry = frappe.db.sql("""select name from `tabGL Entry`
+ round_off_entry = frappe.db.sql(
+ """select name from `tabGL Entry`
where voucher_type='Journal Entry' and voucher_no = %s
and account='_Test Write Off - _TC' and cost_center='_Test Cost Center - _TC'
- and debit = 0 and credit = '.01'""", jv.name)
+ and debit = 0 and credit = '.01'""",
+ jv.name,
+ )
self.assertTrue(round_off_entry)
def test_rename_entries(self):
- je = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True)
+ je = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True
+ )
rename_gle_sle_docs()
naming_series = parse_naming_series(parts=frappe.get_meta("GL Entry").autoname.split(".")[:-1])
- je = make_journal_entry("_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True)
+ je = make_journal_entry(
+ "_Test Account Cost for Goods Sold - _TC", "_Test Bank - _TC", 100, submit=True
+ )
- gl_entries = frappe.get_all("GL Entry",
+ gl_entries = frappe.get_all(
+ "GL Entry",
fields=["name", "to_rename"],
filters={"voucher_type": "Journal Entry", "voucher_no": je.name},
- order_by="creation"
+ order_by="creation",
)
self.assertTrue(all(entry.to_rename == 1 for entry in gl_entries))
- old_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0]
+ old_naming_series_current_value = frappe.db.sql(
+ "SELECT current from tabSeries where name = %s", naming_series
+ )[0][0]
rename_gle_sle_docs()
- new_gl_entries = frappe.get_all("GL Entry",
+ new_gl_entries = frappe.get_all(
+ "GL Entry",
fields=["name", "to_rename"],
filters={"voucher_type": "Journal Entry", "voucher_no": je.name},
- order_by="creation"
+ order_by="creation",
)
self.assertTrue(all(entry.to_rename == 0 for entry in new_gl_entries))
self.assertTrue(all(new.name != old.name for new, old in zip(gl_entries, new_gl_entries)))
- new_naming_series_current_value = frappe.db.sql("SELECT current from tabSeries where name = %s", naming_series)[0][0]
+ new_naming_series_current_value = frappe.db.sql(
+ "SELECT current from tabSeries where name = %s", naming_series
+ )[0][0]
self.assertEqual(old_naming_series_current_value + 2, new_naming_series_current_value)
diff --git a/erpnext/accounts/doctype/gst_account/gst_account.json b/erpnext/accounts/doctype/gst_account/gst_account.json
index b6ec8844e18..be5124c2d4d 100644
--- a/erpnext/accounts/doctype/gst_account/gst_account.json
+++ b/erpnext/accounts/doctype/gst_account/gst_account.json
@@ -10,6 +10,7 @@
"sgst_account",
"igst_account",
"cess_account",
+ "utgst_account",
"is_reverse_charge_account"
],
"fields": [
@@ -64,12 +65,18 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Is Reverse Charge Account"
+ },
+ {
+ "fieldname": "utgst_account",
+ "fieldtype": "Link",
+ "label": "UTGST Account",
+ "options": "Account"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-09 12:30:25.889993",
+ "modified": "2022-04-07 12:59:14.039768",
"modified_by": "Administrator",
"module": "Accounts",
"name": "GST Account",
@@ -78,5 +85,6 @@
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
index 09c389de738..5bd4585a9a8 100644
--- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
@@ -33,19 +33,32 @@ class InvoiceDiscounting(AccountsController):
frappe.throw(_("Loan Start Date and Loan Period are mandatory to save the Invoice Discounting"))
def validate_invoices(self):
- discounted_invoices = [record.sales_invoice for record in
- frappe.get_all("Discounted Invoice",fields=["sales_invoice"], filters={"docstatus":1})]
+ discounted_invoices = [
+ record.sales_invoice
+ for record in frappe.get_all(
+ "Discounted Invoice", fields=["sales_invoice"], filters={"docstatus": 1}
+ )
+ ]
for record in self.invoices:
if record.sales_invoice in discounted_invoices:
- frappe.throw(_("Row({0}): {1} is already discounted in {2}")
- .format(record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent)))
+ frappe.throw(
+ _("Row({0}): {1} is already discounted in {2}").format(
+ record.idx, frappe.bold(record.sales_invoice), frappe.bold(record.parent)
+ )
+ )
- actual_outstanding = frappe.db.get_value("Sales Invoice", record.sales_invoice,"outstanding_amount")
- if record.outstanding_amount > actual_outstanding :
- frappe.throw(_
- ("Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}").format(
- record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)))
+ actual_outstanding = frappe.db.get_value(
+ "Sales Invoice", record.sales_invoice, "outstanding_amount"
+ )
+ if record.outstanding_amount > actual_outstanding:
+ frappe.throw(
+ _(
+ "Row({0}): Outstanding Amount cannot be greater than actual Outstanding Amount {1} in {2}"
+ ).format(
+ record.idx, frappe.bold(actual_outstanding), frappe.bold(record.sales_invoice)
+ )
+ )
def calculate_total_amount(self):
self.total_amount = sum(flt(d.outstanding_amount) for d in self.invoices)
@@ -73,24 +86,21 @@ class InvoiceDiscounting(AccountsController):
self.status = "Cancelled"
if cancel:
- self.db_set('status', self.status, update_modified = True)
+ self.db_set("status", self.status, update_modified=True)
def update_sales_invoice(self):
for d in self.invoices:
if self.docstatus == 1:
is_discounted = 1
else:
- discounted_invoice = frappe.db.exists({
- "doctype": "Discounted Invoice",
- "sales_invoice": d.sales_invoice,
- "docstatus": 1
- })
+ discounted_invoice = frappe.db.exists(
+ {"doctype": "Discounted Invoice", "sales_invoice": d.sales_invoice, "docstatus": 1}
+ )
is_discounted = 1 if discounted_invoice else 0
frappe.db.set_value("Sales Invoice", d.sales_invoice, "is_discounted", is_discounted)
def make_gl_entries(self):
- company_currency = frappe.get_cached_value('Company', self.company, "default_currency")
-
+ company_currency = frappe.get_cached_value("Company", self.company, "default_currency")
gl_entries = []
invoice_fields = ["debit_to", "party_account_currency", "conversion_rate", "cost_center"]
@@ -102,135 +112,182 @@ class InvoiceDiscounting(AccountsController):
inv = frappe.db.get_value("Sales Invoice", d.sales_invoice, invoice_fields, as_dict=1)
if d.outstanding_amount:
- outstanding_in_company_currency = flt(d.outstanding_amount * inv.conversion_rate,
- d.precision("outstanding_amount"))
- ar_credit_account_currency = frappe.get_cached_value("Account", self.accounts_receivable_credit, "currency")
+ outstanding_in_company_currency = flt(
+ d.outstanding_amount * inv.conversion_rate, d.precision("outstanding_amount")
+ )
+ ar_credit_account_currency = frappe.get_cached_value(
+ "Account", self.accounts_receivable_credit, "currency"
+ )
- gl_entries.append(self.get_gl_dict({
- "account": inv.debit_to,
- "party_type": "Customer",
- "party": d.customer,
- "against": self.accounts_receivable_credit,
- "credit": outstanding_in_company_currency,
- "credit_in_account_currency": outstanding_in_company_currency \
- if inv.party_account_currency==company_currency else d.outstanding_amount,
- "cost_center": inv.cost_center,
- "against_voucher": d.sales_invoice,
- "against_voucher_type": "Sales Invoice"
- }, inv.party_account_currency, item=inv))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": inv.debit_to,
+ "party_type": "Customer",
+ "party": d.customer,
+ "against": self.accounts_receivable_credit,
+ "credit": outstanding_in_company_currency,
+ "credit_in_account_currency": outstanding_in_company_currency
+ if inv.party_account_currency == company_currency
+ else d.outstanding_amount,
+ "cost_center": inv.cost_center,
+ "against_voucher": d.sales_invoice,
+ "against_voucher_type": "Sales Invoice",
+ },
+ inv.party_account_currency,
+ item=inv,
+ )
+ )
- gl_entries.append(self.get_gl_dict({
- "account": self.accounts_receivable_credit,
- "party_type": "Customer",
- "party": d.customer,
- "against": inv.debit_to,
- "debit": outstanding_in_company_currency,
- "debit_in_account_currency": outstanding_in_company_currency \
- if ar_credit_account_currency==company_currency else d.outstanding_amount,
- "cost_center": inv.cost_center,
- "against_voucher": d.sales_invoice,
- "against_voucher_type": "Sales Invoice"
- }, ar_credit_account_currency, item=inv))
+ gl_entries.append(
+ self.get_gl_dict(
+ {
+ "account": self.accounts_receivable_credit,
+ "party_type": "Customer",
+ "party": d.customer,
+ "against": inv.debit_to,
+ "debit": outstanding_in_company_currency,
+ "debit_in_account_currency": outstanding_in_company_currency
+ if ar_credit_account_currency == company_currency
+ else d.outstanding_amount,
+ "cost_center": inv.cost_center,
+ "against_voucher": d.sales_invoice,
+ "against_voucher_type": "Sales Invoice",
+ },
+ ar_credit_account_currency,
+ item=inv,
+ )
+ )
- make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding='No')
+ make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No")
@frappe.whitelist()
def create_disbursement_entry(self):
je = frappe.new_doc("Journal Entry")
- je.voucher_type = 'Journal Entry'
+ je.voucher_type = "Journal Entry"
je.company = self.company
- je.remark = 'Loan Disbursement entry against Invoice Discounting: ' + self.name
+ je.remark = "Loan Disbursement entry against Invoice Discounting: " + self.name
- je.append("accounts", {
- "account": self.bank_account,
- "debit_in_account_currency": flt(self.total_amount) - flt(self.bank_charges),
- "cost_center": erpnext.get_default_cost_center(self.company)
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.bank_account,
+ "debit_in_account_currency": flt(self.total_amount) - flt(self.bank_charges),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ },
+ )
if self.bank_charges:
- je.append("accounts", {
- "account": self.bank_charges_account,
- "debit_in_account_currency": flt(self.bank_charges),
- "cost_center": erpnext.get_default_cost_center(self.company)
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.bank_charges_account,
+ "debit_in_account_currency": flt(self.bank_charges),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ },
+ )
- je.append("accounts", {
- "account": self.short_term_loan,
- "credit_in_account_currency": flt(self.total_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.short_term_loan,
+ "credit_in_account_currency": flt(self.total_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ },
+ )
for d in self.invoices:
- je.append("accounts", {
- "account": self.accounts_receivable_discounted,
- "debit_in_account_currency": flt(d.outstanding_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name,
- "party_type": "Customer",
- "party": d.customer
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.accounts_receivable_discounted,
+ "debit_in_account_currency": flt(d.outstanding_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ "party_type": "Customer",
+ "party": d.customer,
+ },
+ )
- je.append("accounts", {
- "account": self.accounts_receivable_credit,
- "credit_in_account_currency": flt(d.outstanding_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name,
- "party_type": "Customer",
- "party": d.customer
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.accounts_receivable_credit,
+ "credit_in_account_currency": flt(d.outstanding_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ "party_type": "Customer",
+ "party": d.customer,
+ },
+ )
return je
@frappe.whitelist()
def close_loan(self):
je = frappe.new_doc("Journal Entry")
- je.voucher_type = 'Journal Entry'
+ je.voucher_type = "Journal Entry"
je.company = self.company
- je.remark = 'Loan Settlement entry against Invoice Discounting: ' + self.name
+ je.remark = "Loan Settlement entry against Invoice Discounting: " + self.name
- je.append("accounts", {
- "account": self.short_term_loan,
- "debit_in_account_currency": flt(self.total_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name,
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.short_term_loan,
+ "debit_in_account_currency": flt(self.total_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ },
+ )
- je.append("accounts", {
- "account": self.bank_account,
- "credit_in_account_currency": flt(self.total_amount),
- "cost_center": erpnext.get_default_cost_center(self.company)
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.bank_account,
+ "credit_in_account_currency": flt(self.total_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ },
+ )
if getdate(self.loan_end_date) > getdate(nowdate()):
for d in self.invoices:
- outstanding_amount = frappe.db.get_value("Sales Invoice", d.sales_invoice, "outstanding_amount")
+ outstanding_amount = frappe.db.get_value(
+ "Sales Invoice", d.sales_invoice, "outstanding_amount"
+ )
if flt(outstanding_amount) > 0:
- je.append("accounts", {
- "account": self.accounts_receivable_discounted,
- "credit_in_account_currency": flt(outstanding_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name,
- "party_type": "Customer",
- "party": d.customer
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.accounts_receivable_discounted,
+ "credit_in_account_currency": flt(outstanding_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ "party_type": "Customer",
+ "party": d.customer,
+ },
+ )
- je.append("accounts", {
- "account": self.accounts_receivable_unpaid,
- "debit_in_account_currency": flt(outstanding_amount),
- "cost_center": erpnext.get_default_cost_center(self.company),
- "reference_type": "Invoice Discounting",
- "reference_name": self.name,
- "party_type": "Customer",
- "party": d.customer
- })
+ je.append(
+ "accounts",
+ {
+ "account": self.accounts_receivable_unpaid,
+ "debit_in_account_currency": flt(outstanding_amount),
+ "cost_center": erpnext.get_default_cost_center(self.company),
+ "reference_type": "Invoice Discounting",
+ "reference_name": self.name,
+ "party_type": "Customer",
+ "party": d.customer,
+ },
+ )
return je
+
@frappe.whitelist()
def get_invoices(filters):
filters = frappe._dict(json.loads(filters))
@@ -250,7 +307,8 @@ def get_invoices(filters):
if cond:
where_condition += " and " + " and ".join(cond)
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
name as sales_invoice,
customer,
@@ -264,17 +322,26 @@ def get_invoices(filters):
%s
and not exists(select di.name from `tabDiscounted Invoice` di
where di.docstatus=1 and di.sales_invoice=si.name)
- """ % where_condition, filters, as_dict=1)
+ """
+ % where_condition,
+ filters,
+ as_dict=1,
+ )
+
def get_party_account_based_on_invoice_discounting(sales_invoice):
party_account = None
- invoice_discounting = frappe.db.sql("""
+ invoice_discounting = frappe.db.sql(
+ """
select par.accounts_receivable_discounted, par.accounts_receivable_unpaid, par.status
from `tabInvoice Discounting` par, `tabDiscounted Invoice` ch
where par.name=ch.parent
and par.docstatus=1
and ch.sales_invoice = %s
- """, (sales_invoice), as_dict=1)
+ """,
+ (sales_invoice),
+ as_dict=1,
+ )
if invoice_discounting:
if invoice_discounting[0].status == "Disbursed":
party_account = invoice_discounting[0].accounts_receivable_discounted
diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py
index 771846e508a..a442231d9a4 100644
--- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py
+++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting_dashboard.py
@@ -1,21 +1,12 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'reference_name',
- 'internal_links': {
- 'Sales Invoice': ['invoices', 'sales_invoice']
- },
- 'transactions': [
- {
- 'label': _('Reference'),
- 'items': ['Sales Invoice']
- },
- {
- 'label': _('Payment'),
- 'items': ['Payment Entry', 'Journal Entry']
- }
- ]
+ "fieldname": "reference_name",
+ "internal_links": {"Sales Invoice": ["invoices", "sales_invoice"]},
+ "transactions": [
+ {"label": _("Reference"), "items": ["Sales Invoice"]},
+ {"label": _("Payment"), "items": ["Payment Entry", "Journal Entry"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py
index d1d4be36f17..a85fdfcad7f 100644
--- a/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/test_invoice_discounting.py
@@ -14,52 +14,74 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_
class TestInvoiceDiscounting(unittest.TestCase):
def setUp(self):
- self.ar_credit = create_account(account_name="_Test Accounts Receivable Credit", parent_account = "Accounts Receivable - _TC", company="_Test Company")
- self.ar_discounted = create_account(account_name="_Test Accounts Receivable Discounted", parent_account = "Accounts Receivable - _TC", company="_Test Company")
- self.ar_unpaid = create_account(account_name="_Test Accounts Receivable Unpaid", parent_account = "Accounts Receivable - _TC", company="_Test Company")
- self.short_term_loan = create_account(account_name="_Test Short Term Loan", parent_account = "Source of Funds (Liabilities) - _TC", company="_Test Company")
- self.bank_account = create_account(account_name="_Test Bank 2", parent_account = "Bank Accounts - _TC", company="_Test Company")
- self.bank_charges_account = create_account(account_name="_Test Bank Charges Account", parent_account = "Expenses - _TC", company="_Test Company")
+ self.ar_credit = create_account(
+ account_name="_Test Accounts Receivable Credit",
+ parent_account="Accounts Receivable - _TC",
+ company="_Test Company",
+ )
+ self.ar_discounted = create_account(
+ account_name="_Test Accounts Receivable Discounted",
+ parent_account="Accounts Receivable - _TC",
+ company="_Test Company",
+ )
+ self.ar_unpaid = create_account(
+ account_name="_Test Accounts Receivable Unpaid",
+ parent_account="Accounts Receivable - _TC",
+ company="_Test Company",
+ )
+ self.short_term_loan = create_account(
+ account_name="_Test Short Term Loan",
+ parent_account="Source of Funds (Liabilities) - _TC",
+ company="_Test Company",
+ )
+ self.bank_account = create_account(
+ account_name="_Test Bank 2", parent_account="Bank Accounts - _TC", company="_Test Company"
+ )
+ self.bank_charges_account = create_account(
+ account_name="_Test Bank Charges Account",
+ parent_account="Expenses - _TC",
+ company="_Test Company",
+ )
frappe.db.set_value("Company", "_Test Company", "default_bank_account", self.bank_account)
def test_total_amount(self):
inv1 = create_sales_invoice(rate=200)
inv2 = create_sales_invoice(rate=500)
- inv_disc = create_invoice_discounting([inv1.name, inv2.name],
+ inv_disc = create_invoice_discounting(
+ [inv1.name, inv2.name],
do_not_submit=True,
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account,
- bank_account=self.bank_account
- )
+ bank_account=self.bank_account,
+ )
self.assertEqual(inv_disc.total_amount, 700)
def test_gl_entries_in_base_currency(self):
inv = create_sales_invoice(rate=200)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account,
- bank_account=self.bank_account
- )
+ bank_account=self.bank_account,
+ )
gle = get_gl_entries("Invoice Discounting", inv_disc.name)
- expected_gle = {
- inv.debit_to: [0.0, 200],
- self.ar_credit: [200, 0.0]
- }
+ expected_gle = {inv.debit_to: [0.0, 200], self.ar_credit: [200, 0.0]}
for i, gle in enumerate(gle):
self.assertEqual([gle.debit, gle.credit], expected_gle.get(gle.account))
def test_loan_on_submit(self):
inv = create_sales_invoice(rate=300)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
@@ -67,28 +89,33 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
start=nowdate(),
- period=60
- )
+ period=60,
+ )
self.assertEqual(inv_disc.status, "Sanctioned")
- self.assertEqual(inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period))
-
+ self.assertEqual(
+ inv_disc.loan_end_date, add_days(inv_disc.loan_start_date, inv_disc.loan_period)
+ )
def test_on_disbursed(self):
inv = create_sales_invoice(rate=500)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
short_term_loan=self.short_term_loan,
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
- bank_charges=100
- )
+ bank_charges=100,
+ )
je = inv_disc.create_disbursement_entry()
self.assertEqual(je.accounts[0].account, self.bank_account)
- self.assertEqual(je.accounts[0].debit_in_account_currency, flt(inv_disc.total_amount) - flt(inv_disc.bank_charges))
+ self.assertEqual(
+ je.accounts[0].debit_in_account_currency,
+ flt(inv_disc.total_amount) - flt(inv_disc.bank_charges),
+ )
self.assertEqual(je.accounts[1].account, self.bank_charges_account)
self.assertEqual(je.accounts[1].debit_in_account_currency, flt(inv_disc.bank_charges))
@@ -102,7 +129,6 @@ class TestInvoiceDiscounting(unittest.TestCase):
self.assertEqual(je.accounts[4].account, self.ar_credit)
self.assertEqual(je.accounts[4].credit_in_account_currency, flt(inv.outstanding_amount))
-
je.posting_date = nowdate()
je.submit()
@@ -114,7 +140,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
def test_on_close_after_loan_period(self):
inv = create_sales_invoice(rate=600)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
@@ -122,8 +149,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
start=nowdate(),
- period=60
- )
+ period=60,
+ )
je1 = inv_disc.create_disbursement_entry()
je1.posting_date = nowdate()
@@ -151,7 +178,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
def test_on_close_after_loan_period_after_inv_payment(self):
inv = create_sales_invoice(rate=600)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
@@ -159,8 +187,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
start=nowdate(),
- period=60
- )
+ period=60,
+ )
je1 = inv_disc.create_disbursement_entry()
je1.posting_date = nowdate()
@@ -183,7 +211,8 @@ class TestInvoiceDiscounting(unittest.TestCase):
def test_on_close_before_loan_period(self):
inv = create_sales_invoice(rate=700)
- inv_disc = create_invoice_discounting([inv.name],
+ inv_disc = create_invoice_discounting(
+ [inv.name],
accounts_receivable_credit=self.ar_credit,
accounts_receivable_discounted=self.ar_discounted,
accounts_receivable_unpaid=self.ar_unpaid,
@@ -191,7 +220,7 @@ class TestInvoiceDiscounting(unittest.TestCase):
bank_charges_account=self.bank_charges_account,
bank_account=self.bank_account,
start=add_days(nowdate(), -80),
- period=60
+ period=60,
)
je1 = inv_disc.create_disbursement_entry()
@@ -209,16 +238,17 @@ class TestInvoiceDiscounting(unittest.TestCase):
self.assertEqual(je2.accounts[1].credit_in_account_currency, flt(inv_disc.total_amount))
def test_make_payment_before_loan_period(self):
- #it has problem
+ # it has problem
inv = create_sales_invoice(rate=700)
- inv_disc = create_invoice_discounting([inv.name],
- accounts_receivable_credit=self.ar_credit,
- accounts_receivable_discounted=self.ar_discounted,
- accounts_receivable_unpaid=self.ar_unpaid,
- short_term_loan=self.short_term_loan,
- bank_charges_account=self.bank_charges_account,
- bank_account=self.bank_account
- )
+ inv_disc = create_invoice_discounting(
+ [inv.name],
+ accounts_receivable_credit=self.ar_credit,
+ accounts_receivable_discounted=self.ar_discounted,
+ accounts_receivable_unpaid=self.ar_unpaid,
+ short_term_loan=self.short_term_loan,
+ bank_charges_account=self.bank_charges_account,
+ bank_account=self.bank_account,
+ )
je = inv_disc.create_disbursement_entry()
inv_disc.reload()
je.posting_date = nowdate()
@@ -232,26 +262,31 @@ class TestInvoiceDiscounting(unittest.TestCase):
je_on_payment.submit()
self.assertEqual(je_on_payment.accounts[0].account, self.ar_discounted)
- self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount))
+ self.assertEqual(
+ je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
+ )
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
- self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount))
+ self.assertEqual(
+ je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
+ )
inv.reload()
self.assertEqual(inv.outstanding_amount, 0)
def test_make_payment_before_after_period(self):
- #it has problem
+ # it has problem
inv = create_sales_invoice(rate=700)
- inv_disc = create_invoice_discounting([inv.name],
- accounts_receivable_credit=self.ar_credit,
- accounts_receivable_discounted=self.ar_discounted,
- accounts_receivable_unpaid=self.ar_unpaid,
- short_term_loan=self.short_term_loan,
- bank_charges_account=self.bank_charges_account,
- bank_account=self.bank_account,
- loan_start_date=add_days(nowdate(), -10),
- period=5
- )
+ inv_disc = create_invoice_discounting(
+ [inv.name],
+ accounts_receivable_credit=self.ar_credit,
+ accounts_receivable_discounted=self.ar_discounted,
+ accounts_receivable_unpaid=self.ar_unpaid,
+ short_term_loan=self.short_term_loan,
+ bank_charges_account=self.bank_charges_account,
+ bank_account=self.bank_account,
+ loan_start_date=add_days(nowdate(), -10),
+ period=5,
+ )
je = inv_disc.create_disbursement_entry()
inv_disc.reload()
je.posting_date = nowdate()
@@ -269,9 +304,13 @@ class TestInvoiceDiscounting(unittest.TestCase):
je_on_payment.submit()
self.assertEqual(je_on_payment.accounts[0].account, self.ar_unpaid)
- self.assertEqual(je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount))
+ self.assertEqual(
+ je_on_payment.accounts[0].credit_in_account_currency, flt(inv.outstanding_amount)
+ )
self.assertEqual(je_on_payment.accounts[1].account, self.bank_account)
- self.assertEqual(je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount))
+ self.assertEqual(
+ je_on_payment.accounts[1].debit_in_account_currency, flt(inv.outstanding_amount)
+ )
inv.reload()
self.assertEqual(inv.outstanding_amount, 0)
@@ -287,17 +326,15 @@ def create_invoice_discounting(invoices, **args):
inv_disc.accounts_receivable_credit = args.accounts_receivable_credit
inv_disc.accounts_receivable_discounted = args.accounts_receivable_discounted
inv_disc.accounts_receivable_unpaid = args.accounts_receivable_unpaid
- inv_disc.short_term_loan=args.short_term_loan
- inv_disc.bank_charges_account=args.bank_charges_account
- inv_disc.bank_account=args.bank_account
+ inv_disc.short_term_loan = args.short_term_loan
+ inv_disc.bank_charges_account = args.bank_charges_account
+ inv_disc.bank_account = args.bank_account
inv_disc.loan_start_date = args.start or nowdate()
inv_disc.loan_period = args.period or 30
inv_disc.bank_charges = flt(args.bank_charges)
for d in invoices:
- inv_disc.append("invoices", {
- "sales_invoice": d
- })
+ inv_disc.append("invoices", {"sales_invoice": d})
inv_disc.insert()
if not args.do_not_submit:
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
index 77c9e95b759..b42d712d88a 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
@@ -2,7 +2,7 @@
"actions": [],
"allow_import": 1,
"allow_rename": 1,
- "creation": "2018-11-22 22:45:00.370913",
+ "creation": "2022-01-19 01:09:13.297137",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
@@ -10,6 +10,9 @@
"field_order": [
"title",
"company",
+ "column_break_3",
+ "disabled",
+ "section_break_5",
"taxes"
],
"fields": [
@@ -36,10 +39,24 @@
"label": "Company",
"options": "Company",
"reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled"
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break"
}
],
"links": [],
- "modified": "2021-03-08 19:50:21.416513",
+ "modified": "2022-01-18 21:11:23.105589",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template",
@@ -82,6 +99,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
index 0ceb6a0bc23..23f36ec6d8d 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
@@ -13,20 +13,28 @@ class ItemTaxTemplate(Document):
def autoname(self):
if self.company and self.title:
- abbr = frappe.get_cached_value('Company', self.company, 'abbr')
- self.name = '{0} - {1}'.format(self.title, abbr)
+ abbr = frappe.get_cached_value("Company", self.company, "abbr")
+ self.name = "{0} - {1}".format(self.title, abbr)
def validate_tax_accounts(self):
"""Check whether Tax Rate is not entered twice for same Tax Type"""
check_list = []
- for d in self.get('taxes'):
+ for d in self.get("taxes"):
if d.tax_type:
account_type = frappe.db.get_value("Account", d.tax_type, "account_type")
- if account_type not in ['Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation']:
+ if account_type not in [
+ "Tax",
+ "Chargeable",
+ "Income Account",
+ "Expense Account",
+ "Expenses Included In Valuation",
+ ]:
frappe.throw(
- _("Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable").format(
- d.idx))
+ _(
+ "Item Tax Row {0} must have account of type Tax or Income or Expense or Chargeable"
+ ).format(d.idx)
+ )
else:
if d.tax_type in check_list:
frappe.throw(_("{0} entered twice in Item Tax").format(d.tax_type))
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
index 71177c25318..5a2bd720dd3 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
@@ -1,26 +1,13 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'item_tax_template',
- 'transactions': [
- {
- 'label': _('Pre Sales'),
- 'items': ['Quotation', 'Supplier Quotation']
- },
- {
- 'label': _('Sales'),
- 'items': ['Sales Invoice', 'Sales Order', 'Delivery Note']
- },
- {
- 'label': _('Purchase'),
- 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
- },
- {
- 'label': _('Stock'),
- 'items': ['Item Groups', 'Item']
- }
- ]
+ "fieldname": "item_tax_template",
+ "transactions": [
+ {"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]},
+ {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]},
+ {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]},
+ {"label": _("Stock"), "items": ["Item Groups", "Item"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index d76641dc9bd..5c0d3265680 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -8,6 +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'];
},
refresh: function(frm) {
@@ -31,7 +32,7 @@ frappe.ui.form.on("Journal Entry", {
if(frm.doc.docstatus==1) {
frm.add_custom_button(__('Reverse Journal Entry'), function() {
return erpnext.journal_entry.reverse_journal_entry(frm);
- }, __('Make'));
+ }, __('Actions'));
}
if (frm.doc.__islocal) {
@@ -148,22 +149,6 @@ frappe.ui.form.on("Journal Entry", {
}
});
}
- else if(frm.doc.voucher_type=="Opening Entry") {
- return frappe.call({
- type:"GET",
- method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_opening_accounts",
- args: {
- "company": frm.doc.company
- },
- callback: function(r) {
- frappe.model.clear_table(frm.doc, "accounts");
- if(r.message) {
- update_jv_details(frm.doc, r.message);
- }
- cur_frm.set_value("is_opening", "Yes");
- }
- });
- }
}
},
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.json b/erpnext/accounts/doctype/journal_entry/journal_entry.json
index 20678d787b4..8e5ba3718f7 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.json
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.json
@@ -3,7 +3,7 @@
"allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
- "creation": "2013-03-25 10:53:52",
+ "creation": "2022-01-25 10:29:58.717206",
"doctype": "DocType",
"document_type": "Document",
"engine": "InnoDB",
@@ -13,6 +13,8 @@
"voucher_type",
"naming_series",
"finance_book",
+ "process_deferred_accounting",
+ "reversal_of",
"tax_withholding_category",
"column_break1",
"from_template",
@@ -135,7 +137,8 @@
"fieldname": "finance_book",
"fieldtype": "Link",
"label": "Finance Book",
- "options": "Finance Book"
+ "options": "Finance Book",
+ "read_only": 1
},
{
"fieldname": "2_add_edit_gl_entries",
@@ -515,13 +518,28 @@
"fieldname": "apply_tds",
"fieldtype": "Check",
"label": "Apply Tax Withholding Amount "
+ },
+ {
+ "depends_on": "eval:doc.docstatus",
+ "fieldname": "reversal_of",
+ "fieldtype": "Link",
+ "label": "Reversal Of",
+ "options": "Journal Entry",
+ "read_only": 1
+ },
+ {
+ "fieldname": "process_deferred_accounting",
+ "fieldtype": "Link",
+ "label": "Process Deferred Accounting",
+ "options": "Process Deferred Accounting",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 176,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-09 15:31:14.484029",
+ "modified": "2022-06-23 22:01:32.348337",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Journal Entry",
@@ -569,6 +587,7 @@
"search_fields": "voucher_type,posting_date, due_date, cheque_no",
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index 3aed3c89d53..73e673ea38b 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -19,17 +19,21 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category
)
from erpnext.accounts.party import get_party_account
from erpnext.accounts.utils import (
- check_if_stock_and_account_balance_synced,
get_account_currency,
get_balance_on,
get_stock_accounts,
get_stock_and_account_balance,
)
from erpnext.controllers.accounts_controller import AccountsController
-from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
+from erpnext.hr.doctype.expense_claim.expense_claim import (
+ get_outstanding_amount_for_claim,
+ update_reimbursed_amount,
+)
-class StockAccountInvalidTransaction(frappe.ValidationError): pass
+class StockAccountInvalidTransaction(frappe.ValidationError):
+ pass
+
class JournalEntry(AccountsController):
def __init__(self, *args, **kwargs):
@@ -39,11 +43,11 @@ class JournalEntry(AccountsController):
return self.voucher_type
def validate(self):
- if self.voucher_type == 'Opening Entry':
- self.is_opening = 'Yes'
+ if self.voucher_type == "Opening Entry":
+ self.is_opening = "Yes"
if not self.is_opening:
- self.is_opening='No'
+ self.is_opening = "No"
self.clearance_date = None
@@ -86,15 +90,14 @@ class JournalEntry(AccountsController):
self.update_expense_claim()
self.update_inter_company_jv()
self.update_invoice_discounting()
- check_if_stock_and_account_balance_synced(self.posting_date,
- self.company, self.doctype, self.name)
def on_cancel(self):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
from erpnext.payroll.doctype.salary_slip.salary_slip import unlink_ref_doc_from_salary_slip
+
unlink_ref_doc_from_payment_entries(self)
unlink_ref_doc_from_salary_slip(self.name)
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(1)
self.update_advance_paid()
self.update_expense_claim()
@@ -119,10 +122,13 @@ class JournalEntry(AccountsController):
frappe.get_doc(voucher_type, voucher_no).set_total_advance_paid()
def validate_inter_company_accounts(self):
- if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
+ if (
+ self.voucher_type == "Inter Company Journal Entry"
+ and self.inter_company_journal_entry_reference
+ ):
doc = frappe.get_doc("Journal Entry", self.inter_company_journal_entry_reference)
- account_currency = frappe.get_cached_value('Company', self.company, "default_currency")
- previous_account_currency = frappe.get_cached_value('Company', doc.company, "default_currency")
+ account_currency = frappe.get_cached_value("Company", self.company, "default_currency")
+ previous_account_currency = frappe.get_cached_value("Company", doc.company, "default_currency")
if account_currency == previous_account_currency:
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
@@ -130,91 +136,130 @@ class JournalEntry(AccountsController):
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
for account in stock_accounts:
- account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
- self.posting_date, self.company)
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
+ account, self.posting_date, self.company
+ )
if account_bal == stock_bal:
- frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
- .format(account), StockAccountInvalidTransaction)
+ frappe.throw(
+ _("Account: {0} can only be updated via Stock Transactions").format(account),
+ StockAccountInvalidTransaction,
+ )
def apply_tax_withholding(self):
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
- if not self.apply_tds or self.voucher_type not in ('Debit Note', 'Credit Note'):
+ if not self.apply_tds or self.voucher_type not in ("Debit Note", "Credit Note"):
return
- parties = [d.party for d in self.get('accounts') if d.party]
+ parties = [d.party for d in self.get("accounts") if d.party]
parties = list(set(parties))
if len(parties) > 1:
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
account_type_map = get_account_type_map(self.company)
- party_type = 'supplier' if self.voucher_type == 'Credit Note' else 'customer'
- doctype = 'Purchase Invoice' if self.voucher_type == 'Credit Note' else 'Sales Invoice'
- debit_or_credit = 'debit_in_account_currency' if self.voucher_type == 'Credit Note' else 'credit_in_account_currency'
- rev_debit_or_credit = 'credit_in_account_currency' if debit_or_credit == 'debit_in_account_currency' else 'debit_in_account_currency'
+ party_type = "supplier" if self.voucher_type == "Credit Note" else "customer"
+ doctype = "Purchase Invoice" if self.voucher_type == "Credit Note" else "Sales Invoice"
+ debit_or_credit = (
+ "debit_in_account_currency"
+ if self.voucher_type == "Credit Note"
+ else "credit_in_account_currency"
+ )
+ rev_debit_or_credit = (
+ "credit_in_account_currency"
+ if debit_or_credit == "debit_in_account_currency"
+ else "debit_in_account_currency"
+ )
party_account = get_party_account(party_type.title(), parties[0], self.company)
- net_total = sum(d.get(debit_or_credit) for d in self.get('accounts') if account_type_map.get(d.account)
- not in ('Tax', 'Chargeable'))
+ net_total = sum(
+ d.get(debit_or_credit)
+ for d in self.get("accounts")
+ if account_type_map.get(d.account) not in ("Tax", "Chargeable")
+ )
- party_amount = sum(d.get(rev_debit_or_credit) for d in self.get('accounts') if d.account == party_account)
+ party_amount = sum(
+ d.get(rev_debit_or_credit) for d in self.get("accounts") if d.account == party_account
+ )
- inv = frappe._dict({
- party_type: parties[0],
- 'doctype': doctype,
- 'company': self.company,
- 'posting_date': self.posting_date,
- 'net_total': net_total
- })
+ inv = frappe._dict(
+ {
+ party_type: parties[0],
+ "doctype": doctype,
+ "company": self.company,
+ "posting_date": self.posting_date,
+ "net_total": net_total,
+ }
+ )
- 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
accounts = []
- for d in self.get('accounts'):
- if d.get('account') == tax_withholding_details.get("account_head"):
- d.update({
- 'account': tax_withholding_details.get("account_head"),
- debit_or_credit: tax_withholding_details.get('tax_amount')
- })
+ for d in self.get("accounts"):
+ if d.get("account") == tax_withholding_details.get("account_head"):
+ d.update(
+ {
+ "account": tax_withholding_details.get("account_head"),
+ debit_or_credit: tax_withholding_details.get("tax_amount"),
+ }
+ )
- accounts.append(d.get('account'))
+ accounts.append(d.get("account"))
- if d.get('account') == party_account:
- d.update({
- rev_debit_or_credit: party_amount - tax_withholding_details.get('tax_amount')
- })
+ if d.get("account") == party_account:
+ d.update({rev_debit_or_credit: party_amount - tax_withholding_details.get("tax_amount")})
if not accounts or tax_withholding_details.get("account_head") not in accounts:
- self.append("accounts", {
- 'account': tax_withholding_details.get("account_head"),
- rev_debit_or_credit: tax_withholding_details.get('tax_amount'),
- 'against_account': parties[0]
- })
+ self.append(
+ "accounts",
+ {
+ "account": tax_withholding_details.get("account_head"),
+ rev_debit_or_credit: tax_withholding_details.get("tax_amount"),
+ "against_account": parties[0],
+ },
+ )
- to_remove = [d for d in self.get('accounts')
- if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")]
+ to_remove = [
+ d
+ for d in self.get("accounts")
+ if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")
+ ]
for d in to_remove:
self.remove(d)
def update_inter_company_jv(self):
- if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
- frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
- "inter_company_journal_entry_reference", self.name)
+ if (
+ self.voucher_type == "Inter Company Journal Entry"
+ and self.inter_company_journal_entry_reference
+ ):
+ frappe.db.set_value(
+ "Journal Entry",
+ self.inter_company_journal_entry_reference,
+ "inter_company_journal_entry_reference",
+ self.name,
+ )
def update_invoice_discounting(self):
def _validate_invoice_discounting_status(inv_disc, id_status, expected_status, row_id):
id_link = get_link_to_form("Invoice Discounting", inv_disc)
if id_status != expected_status:
- frappe.throw(_("Row #{0}: Status must be {1} for Invoice Discounting {2}").format(d.idx, expected_status, id_link))
+ frappe.throw(
+ _("Row #{0}: Status must be {1} for Invoice Discounting {2}").format(
+ d.idx, expected_status, id_link
+ )
+ )
- invoice_discounting_list = list(set([d.reference_name for d in self.accounts if d.reference_type=="Invoice Discounting"]))
+ invoice_discounting_list = list(
+ set([d.reference_name for d in self.accounts if d.reference_type == "Invoice Discounting"])
+ )
for inv_disc in invoice_discounting_list:
inv_disc_doc = frappe.get_doc("Invoice Discounting", inv_disc)
status = None
@@ -238,104 +283,147 @@ class JournalEntry(AccountsController):
if status:
inv_disc_doc.set_status(status=status)
-
def unlink_advance_entry_reference(self):
for d in self.get("accounts"):
if d.is_advance == "Yes" and d.reference_type in ("Sales Invoice", "Purchase Invoice"):
doc = frappe.get_doc(d.reference_type, d.reference_name)
doc.delink_advance_entries(self.name)
- d.reference_type = ''
- d.reference_name = ''
+ d.reference_type = ""
+ d.reference_name = ""
d.db_update()
def unlink_asset_reference(self):
for d in self.get("accounts"):
- if d.reference_type=="Asset" and d.reference_name:
+ if d.reference_type == "Asset" and 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 = asset.get("finance_books")[idx - 1]
finance_books.value_after_depreciation += s.depreciation_amount
finance_books.db_update()
asset.set_status()
def unlink_inter_company_jv(self):
- if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
- frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
- "inter_company_journal_entry_reference", "")
- frappe.db.set_value("Journal Entry", self.name,\
- "inter_company_journal_entry_reference", "")
+ if (
+ self.voucher_type == "Inter Company Journal Entry"
+ and self.inter_company_journal_entry_reference
+ ):
+ frappe.db.set_value(
+ "Journal Entry",
+ self.inter_company_journal_entry_reference,
+ "inter_company_journal_entry_reference",
+ "",
+ )
+ frappe.db.set_value("Journal Entry", self.name, "inter_company_journal_entry_reference", "")
def unlink_asset_adjustment_entry(self):
- frappe.db.sql(""" update `tabAsset Value Adjustment`
- set journal_entry = null where journal_entry = %s""", self.name)
+ frappe.db.sql(
+ """ update `tabAsset Value Adjustment`
+ set journal_entry = null where journal_entry = %s""",
+ self.name,
+ )
def validate_party(self):
for d in self.get("accounts"):
account_type = frappe.db.get_value("Account", d.account, "account_type")
if account_type in ["Receivable", "Payable"]:
if not (d.party_type and d.party):
- frappe.throw(_("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(d.idx, d.account))
+ frappe.throw(
+ _("Row {0}: Party Type and Party is required for Receivable / Payable account {1}").format(
+ d.idx, d.account
+ )
+ )
def check_credit_limit(self):
- customers = list(set(d.party for d in self.get("accounts")
- if d.party_type=="Customer" and d.party and flt(d.debit) > 0))
+ customers = list(
+ set(
+ d.party
+ for d in self.get("accounts")
+ if d.party_type == "Customer" and d.party and flt(d.debit) > 0
+ )
+ )
if customers:
from erpnext.selling.doctype.customer.customer import check_credit_limit
+
for customer in customers:
check_credit_limit(customer, self.company)
def validate_cheque_info(self):
- if self.voucher_type in ['Bank Entry']:
+ if self.voucher_type in ["Bank Entry"]:
if not self.cheque_no or not self.cheque_date:
- msgprint(_("Reference No & Reference Date is required for {0}").format(self.voucher_type),
- raise_exception=1)
+ msgprint(
+ _("Reference No & Reference Date is required for {0}").format(self.voucher_type),
+ raise_exception=1,
+ )
if self.cheque_date and not self.cheque_no:
msgprint(_("Reference No is mandatory if you entered Reference Date"), raise_exception=1)
def validate_entries_for_advance(self):
- for d in self.get('accounts'):
+ for d in self.get("accounts"):
if d.reference_type not in ("Sales Invoice", "Purchase Invoice", "Journal Entry"):
- if (d.party_type == 'Customer' and flt(d.credit) > 0) or \
- (d.party_type == 'Supplier' and flt(d.debit) > 0):
- if d.is_advance=="No":
- msgprint(_("Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry.").format(d.idx, d.account), alert=True)
+ if (d.party_type == "Customer" and flt(d.credit) > 0) or (
+ d.party_type == "Supplier" and flt(d.debit) > 0
+ ):
+ if d.is_advance == "No":
+ msgprint(
+ _(
+ "Row {0}: Please check 'Is Advance' against Account {1} if this is an advance entry."
+ ).format(d.idx, d.account),
+ alert=True,
+ )
elif d.reference_type in ("Sales Order", "Purchase Order") and d.is_advance != "Yes":
- frappe.throw(_("Row {0}: Payment against Sales/Purchase Order should always be marked as advance").format(d.idx))
+ frappe.throw(
+ _(
+ "Row {0}: Payment against Sales/Purchase Order should always be marked as advance"
+ ).format(d.idx)
+ )
if d.is_advance == "Yes":
- if d.party_type == 'Customer' and flt(d.debit) > 0:
+ if d.party_type == "Customer" and flt(d.debit) > 0:
frappe.throw(_("Row {0}: Advance against Customer must be credit").format(d.idx))
- elif d.party_type == 'Supplier' and flt(d.credit) > 0:
+ elif d.party_type == "Supplier" and flt(d.credit) > 0:
frappe.throw(_("Row {0}: Advance against Supplier must be debit").format(d.idx))
def validate_against_jv(self):
- for d in self.get('accounts'):
- if d.reference_type=="Journal Entry":
+ for d in self.get("accounts"):
+ if d.reference_type == "Journal Entry":
account_root_type = frappe.db.get_value("Account", d.account, "root_type")
if account_root_type == "Asset" and flt(d.debit) > 0:
- frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets credited")
- .format(d.idx, d.account))
+ frappe.throw(
+ _(
+ "Row #{0}: For {1}, you can select reference document only if account gets credited"
+ ).format(d.idx, d.account)
+ )
elif account_root_type == "Liability" and flt(d.credit) > 0:
- frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets debited")
- .format(d.idx, d.account))
+ frappe.throw(
+ _(
+ "Row #{0}: For {1}, you can select reference document only if account gets debited"
+ ).format(d.idx, d.account)
+ )
if d.reference_name == self.name:
frappe.throw(_("You can not enter current voucher in 'Against Journal Entry' column"))
- against_entries = frappe.db.sql("""select * from `tabJournal Entry Account`
+ against_entries = frappe.db.sql(
+ """select * from `tabJournal Entry Account`
where account = %s and docstatus = 1 and parent = %s
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
- """, (d.account, d.reference_name), as_dict=True)
+ """,
+ (d.account, d.reference_name),
+ as_dict=True,
+ )
if not against_entries:
- frappe.throw(_("Journal Entry {0} does not have account {1} or already matched against other voucher")
- .format(d.reference_name, d.account))
+ frappe.throw(
+ _(
+ "Journal Entry {0} does not have account {1} or already matched against other voucher"
+ ).format(d.reference_name, d.account)
+ )
else:
dr_or_cr = "debit" if d.credit > 0 else "credit"
valid = False
@@ -343,16 +431,19 @@ class JournalEntry(AccountsController):
if flt(jvd[dr_or_cr]) > 0:
valid = True
if not valid:
- frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry")
- .format(d.reference_name, dr_or_cr))
+ frappe.throw(
+ _("Against Journal Entry {0} does not have any unmatched {1} entry").format(
+ d.reference_name, dr_or_cr
+ )
+ )
def validate_reference_doc(self):
"""Validates reference document"""
field_dict = {
- 'Sales Invoice': ["Customer", "Debit To"],
- 'Purchase Invoice': ["Supplier", "Credit To"],
- 'Sales Order': ["Customer"],
- 'Purchase Order': ["Supplier"]
+ "Sales Invoice": ["Customer", "Debit To"],
+ "Purchase Invoice": ["Supplier", "Credit To"],
+ "Sales Order": ["Customer"],
+ "Purchase Order": ["Supplier"],
}
self.reference_totals = {}
@@ -365,55 +456,76 @@ class JournalEntry(AccountsController):
if not d.reference_name:
d.reference_type = None
if d.reference_type and d.reference_name and (d.reference_type in list(field_dict)):
- dr_or_cr = "credit_in_account_currency" \
- if d.reference_type in ("Sales Order", "Sales Invoice") else "debit_in_account_currency"
+ dr_or_cr = (
+ "credit_in_account_currency"
+ if d.reference_type in ("Sales Order", "Sales Invoice")
+ else "debit_in_account_currency"
+ )
# check debit or credit type Sales / Purchase Order
- if d.reference_type=="Sales Order" and flt(d.debit) > 0:
- frappe.throw(_("Row {0}: Debit entry can not be linked with a {1}").format(d.idx, d.reference_type))
+ if d.reference_type == "Sales Order" and flt(d.debit) > 0:
+ frappe.throw(
+ _("Row {0}: Debit entry can not be linked with a {1}").format(d.idx, d.reference_type)
+ )
if d.reference_type == "Purchase Order" and flt(d.credit) > 0:
- frappe.throw(_("Row {0}: Credit entry can not be linked with a {1}").format(d.idx, d.reference_type))
+ frappe.throw(
+ _("Row {0}: Credit entry can not be linked with a {1}").format(d.idx, d.reference_type)
+ )
# set totals
if not d.reference_name in self.reference_totals:
self.reference_totals[d.reference_name] = 0.0
- if self.voucher_type not in ('Deferred Revenue', 'Deferred Expense'):
+ if self.voucher_type not in ("Deferred Revenue", "Deferred Expense"):
self.reference_totals[d.reference_name] += flt(d.get(dr_or_cr))
self.reference_types[d.reference_name] = d.reference_type
self.reference_accounts[d.reference_name] = d.account
- against_voucher = frappe.db.get_value(d.reference_type, d.reference_name,
- [scrub(dt) for dt in field_dict.get(d.reference_type)])
+ against_voucher = frappe.db.get_value(
+ d.reference_type, d.reference_name, [scrub(dt) for dt in field_dict.get(d.reference_type)]
+ )
if not against_voucher:
frappe.throw(_("Row {0}: Invalid reference {1}").format(d.idx, d.reference_name))
# check if party and account match
if d.reference_type in ("Sales Invoice", "Purchase Invoice"):
- if self.voucher_type in ('Deferred Revenue', 'Deferred Expense') and d.reference_detail_no:
- debit_or_credit = 'Debit' if d.debit else 'Credit'
- party_account = get_deferred_booking_accounts(d.reference_type, d.reference_detail_no,
- debit_or_credit)
+ if self.voucher_type in ("Deferred Revenue", "Deferred Expense") and d.reference_detail_no:
+ debit_or_credit = "Debit" if d.debit else "Credit"
+ party_account = get_deferred_booking_accounts(
+ d.reference_type, d.reference_detail_no, debit_or_credit
+ )
+ against_voucher = ["", against_voucher[1]]
else:
if d.reference_type == "Sales Invoice":
- party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
+ party_account = (
+ get_party_account_based_on_invoice_discounting(d.reference_name) or against_voucher[1]
+ )
else:
party_account = against_voucher[1]
- if (against_voucher[0] != d.party or party_account != d.account):
- frappe.throw(_("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}")
- .format(d.idx, field_dict.get(d.reference_type)[0], field_dict.get(d.reference_type)[1],
- d.reference_type, d.reference_name))
+ if against_voucher[0] != cstr(d.party) or party_account != d.account:
+ frappe.throw(
+ _("Row {0}: Party / Account does not match with {1} / {2} in {3} {4}").format(
+ d.idx,
+ field_dict.get(d.reference_type)[0],
+ field_dict.get(d.reference_type)[1],
+ d.reference_type,
+ d.reference_name,
+ )
+ )
# check if party matches for Sales / Purchase Order
if d.reference_type in ("Sales Order", "Purchase Order"):
# set totals
if against_voucher != d.party:
- frappe.throw(_("Row {0}: {1} {2} does not match with {3}") \
- .format(d.idx, d.party_type, d.party, d.reference_type))
+ frappe.throw(
+ _("Row {0}: {1} {2} does not match with {3}").format(
+ d.idx, d.party_type, d.party, d.reference_type
+ )
+ )
self.validate_orders()
self.validate_invoices()
@@ -439,53 +551,79 @@ class JournalEntry(AccountsController):
account_currency = get_account_currency(account)
if account_currency == self.company_currency:
voucher_total = order.base_grand_total
- formatted_voucher_total = fmt_money(voucher_total, order.precision("base_grand_total"),
- currency=account_currency)
+ formatted_voucher_total = fmt_money(
+ voucher_total, order.precision("base_grand_total"), currency=account_currency
+ )
else:
voucher_total = order.grand_total
- formatted_voucher_total = fmt_money(voucher_total, order.precision("grand_total"),
- currency=account_currency)
+ formatted_voucher_total = fmt_money(
+ voucher_total, order.precision("grand_total"), currency=account_currency
+ )
if flt(voucher_total) < (flt(order.advance_paid) + total):
- frappe.throw(_("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(reference_type, reference_name, formatted_voucher_total))
+ frappe.throw(
+ _("Advance paid against {0} {1} cannot be greater than Grand Total {2}").format(
+ reference_type, reference_name, formatted_voucher_total
+ )
+ )
def validate_invoices(self):
"""Validate totals and docstatus for invoices"""
for reference_name, total in iteritems(self.reference_totals):
reference_type = self.reference_types[reference_name]
- if (reference_type in ("Sales Invoice", "Purchase Invoice") and
- self.voucher_type not in ['Debit Note', 'Credit Note']):
- invoice = frappe.db.get_value(reference_type, reference_name,
- ["docstatus", "outstanding_amount"], as_dict=1)
+ if reference_type in ("Sales Invoice", "Purchase Invoice") and self.voucher_type not in [
+ "Debit Note",
+ "Credit Note",
+ ]:
+ invoice = frappe.db.get_value(
+ reference_type, reference_name, ["docstatus", "outstanding_amount"], as_dict=1
+ )
if invoice.docstatus != 1:
frappe.throw(_("{0} {1} is not submitted").format(reference_type, reference_name))
if total and flt(invoice.outstanding_amount) < total:
- frappe.throw(_("Payment against {0} {1} cannot be greater than Outstanding Amount {2}")
- .format(reference_type, reference_name, invoice.outstanding_amount))
+ frappe.throw(
+ _("Payment against {0} {1} cannot be greater than Outstanding Amount {2}").format(
+ reference_type, reference_name, invoice.outstanding_amount
+ )
+ )
def set_against_account(self):
accounts_debited, accounts_credited = [], []
- for d in self.get("accounts"):
- 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)
+ if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
+ for d in self.get("accounts"):
+ if d.reference_type == "Sales Invoice":
+ field = "customer"
+ else:
+ field = "supplier"
- for d in self.get("accounts"):
- if flt(d.debit > 0): d.against_account = ", ".join(list(set(accounts_credited)))
- if flt(d.credit > 0): d.against_account = ", ".join(list(set(accounts_debited)))
+ 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):
+ 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):
+ d.against_account = ", ".join(list(set(accounts_credited)))
+ 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'):
+ 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))
+ 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
@@ -496,13 +634,16 @@ class JournalEntry(AccountsController):
self.total_debit = flt(self.total_debit) + flt(d.debit, d.precision("debit"))
self.total_credit = flt(self.total_credit) + flt(d.credit, d.precision("credit"))
- self.difference = flt(self.total_debit, self.precision("total_debit")) - \
- flt(self.total_credit, self.precision("total_credit"))
+ self.difference = flt(self.total_debit, self.precision("total_debit")) - flt(
+ self.total_credit, self.precision("total_credit")
+ )
def validate_multi_currency(self):
alternate_currency = []
for d in self.get("accounts"):
- account = frappe.db.get_value("Account", d.account, ["account_currency", "account_type"], as_dict=1)
+ account = frappe.db.get_value(
+ "Account", d.account, ["account_currency", "account_type"], as_dict=1
+ )
if account:
d.account_currency = account.account_currency
d.account_type = account.account_type
@@ -521,8 +662,12 @@ class JournalEntry(AccountsController):
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"))
+ 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"))
@@ -531,13 +676,28 @@ class JournalEntry(AccountsController):
for d in self.get("accounts"):
if d.account_currency == self.company_currency:
d.exchange_rate = 1
- elif not d.exchange_rate or d.exchange_rate == 1 or \
- (d.reference_type in ("Sales Invoice", "Purchase Invoice")
- and d.reference_name and self.posting_date):
+ elif (
+ not d.exchange_rate
+ or d.exchange_rate == 1
+ or (
+ d.reference_type in ("Sales Invoice", "Purchase Invoice")
+ and d.reference_name
+ and self.posting_date
+ )
+ ):
- # Modified to include the posting date for which to retreive the exchange rate
- d.exchange_rate = get_exchange_rate(self.posting_date, d.account, d.account_currency,
- self.company, d.reference_type, d.reference_name, d.debit, d.credit, d.exchange_rate)
+ # Modified to include the posting date for which to retreive the exchange rate
+ d.exchange_rate = get_exchange_rate(
+ self.posting_date,
+ d.account,
+ d.account_currency,
+ self.company,
+ d.reference_type,
+ d.reference_name,
+ d.debit,
+ d.credit,
+ d.exchange_rate,
+ )
if not d.exchange_rate:
frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
@@ -550,55 +710,76 @@ class JournalEntry(AccountsController):
if self.cheque_no:
if self.cheque_date:
- r.append(_('Reference #{0} dated {1}').format(self.cheque_no, formatdate(self.cheque_date)))
+ r.append(_("Reference #{0} dated {1}").format(self.cheque_no, formatdate(self.cheque_date)))
else:
msgprint(_("Please enter Reference date"), raise_exception=frappe.MandatoryError)
- for d in self.get('accounts'):
- if d.reference_type=="Sales Invoice" and d.credit:
- r.append(_("{0} against Sales Invoice {1}").format(fmt_money(flt(d.credit), currency = self.company_currency), \
- d.reference_name))
+ for d in self.get("accounts"):
+ if d.reference_type == "Sales Invoice" and d.credit:
+ r.append(
+ _("{0} against Sales Invoice {1}").format(
+ fmt_money(flt(d.credit), currency=self.company_currency), d.reference_name
+ )
+ )
- if d.reference_type=="Sales Order" and d.credit:
- r.append(_("{0} against Sales Order {1}").format(fmt_money(flt(d.credit), currency = self.company_currency), \
- d.reference_name))
+ if d.reference_type == "Sales Order" and d.credit:
+ r.append(
+ _("{0} against Sales Order {1}").format(
+ fmt_money(flt(d.credit), currency=self.company_currency), d.reference_name
+ )
+ )
if d.reference_type == "Purchase Invoice" and d.debit:
- bill_no = frappe.db.sql("""select bill_no, bill_date
- from `tabPurchase Invoice` where name=%s""", d.reference_name)
- if bill_no and bill_no[0][0] and bill_no[0][0].lower().strip() \
- not in ['na', 'not applicable', 'none']:
- r.append(_('{0} against Bill {1} dated {2}').format(fmt_money(flt(d.debit), currency=self.company_currency), bill_no[0][0],
- bill_no[0][1] and formatdate(bill_no[0][1].strftime('%Y-%m-%d'))))
+ bill_no = frappe.db.sql(
+ """select bill_no, bill_date
+ from `tabPurchase Invoice` where name=%s""",
+ d.reference_name,
+ )
+ if (
+ bill_no
+ and bill_no[0][0]
+ and bill_no[0][0].lower().strip() not in ["na", "not applicable", "none"]
+ ):
+ r.append(
+ _("{0} against Bill {1} dated {2}").format(
+ fmt_money(flt(d.debit), currency=self.company_currency),
+ bill_no[0][0],
+ bill_no[0][1] and formatdate(bill_no[0][1].strftime("%Y-%m-%d")),
+ )
+ )
if d.reference_type == "Purchase Order" and d.debit:
- r.append(_("{0} against Purchase Order {1}").format(fmt_money(flt(d.credit), currency = self.company_currency), \
- d.reference_name))
+ r.append(
+ _("{0} against Purchase Order {1}").format(
+ fmt_money(flt(d.credit), currency=self.company_currency), d.reference_name
+ )
+ )
if r:
- self.remark = ("\n").join(r) #User Remarks is not mandatory
+ self.remark = ("\n").join(r) # User Remarks is not mandatory
def set_print_format_fields(self):
bank_amount = party_amount = total_amount = 0.0
- currency = bank_account_currency = party_account_currency = pay_to_recd_from= None
+ currency = bank_account_currency = party_account_currency = pay_to_recd_from = None
party_type = None
- for d in self.get('accounts'):
- if d.party_type in ['Customer', 'Supplier'] and d.party:
+ for d in self.get("accounts"):
+ if d.party_type in ["Customer", "Supplier"] and d.party:
party_type = d.party_type
if not pay_to_recd_from:
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 += d.debit_in_account_currency or d.credit_in_account_currency
party_account_currency = d.account_currency
elif frappe.db.get_value("Account", d.account, "account_type") in ["Bank", "Cash"]:
- bank_amount += (d.debit_in_account_currency or d.credit_in_account_currency)
+ bank_amount += d.debit_in_account_currency or d.credit_in_account_currency
bank_account_currency = d.account_currency
if party_type and pay_to_recd_from:
- self.pay_to_recd_from = frappe.db.get_value(party_type, pay_to_recd_from,
- "customer_name" if party_type=="Customer" else "supplier_name")
+ self.pay_to_recd_from = frappe.db.get_value(
+ party_type, pay_to_recd_from, "customer_name" if party_type == "Customer" else "supplier_name"
+ )
if bank_amount:
total_amount = bank_amount
currency = bank_account_currency
@@ -612,6 +793,7 @@ class JournalEntry(AccountsController):
self.total_amount = amt
self.total_amount_currency = currency
from frappe.utils import money_in_words
+
self.total_amount_in_words = money_in_words(amt, currency)
def make_gl_entries(self, cancel=0, adv_adj=0):
@@ -625,38 +807,45 @@ class JournalEntry(AccountsController):
remarks = "\n".join(r)
gl_map.append(
- self.get_gl_dict({
- "account": d.account,
- "party_type": d.party_type,
- "due_date": self.due_date,
- "party": d.party,
- "against": d.against_account,
- "debit": flt(d.debit, d.precision("debit")),
- "credit": flt(d.credit, d.precision("credit")),
- "account_currency": d.account_currency,
- "debit_in_account_currency": flt(d.debit_in_account_currency, d.precision("debit_in_account_currency")),
- "credit_in_account_currency": flt(d.credit_in_account_currency, d.precision("credit_in_account_currency")),
- "against_voucher_type": d.reference_type,
- "against_voucher": d.reference_name,
- "remarks": remarks,
- "voucher_detail_no": d.reference_detail_no,
- "cost_center": d.cost_center,
- "project": d.project,
- "finance_book": self.finance_book
- }, item=d)
+ self.get_gl_dict(
+ {
+ "account": d.account,
+ "party_type": d.party_type,
+ "due_date": self.due_date,
+ "party": d.party,
+ "against": d.against_account,
+ "debit": flt(d.debit, d.precision("debit")),
+ "credit": flt(d.credit, d.precision("credit")),
+ "account_currency": d.account_currency,
+ "debit_in_account_currency": flt(
+ d.debit_in_account_currency, d.precision("debit_in_account_currency")
+ ),
+ "credit_in_account_currency": flt(
+ d.credit_in_account_currency, d.precision("credit_in_account_currency")
+ ),
+ "against_voucher_type": d.reference_type,
+ "against_voucher": d.reference_name,
+ "remarks": remarks,
+ "voucher_detail_no": d.reference_detail_no,
+ "cost_center": d.cost_center,
+ "project": d.project,
+ "finance_book": self.finance_book,
+ },
+ item=d,
+ )
)
- if self.voucher_type in ('Deferred Revenue', 'Deferred Expense'):
- update_outstanding = 'No'
+ if self.voucher_type in ("Deferred Revenue", "Deferred Expense"):
+ update_outstanding = "No"
else:
- update_outstanding = 'Yes'
+ update_outstanding = "Yes"
if gl_map:
make_gl_entries(gl_map, cancel=cancel, adv_adj=adv_adj, update_outstanding=update_outstanding)
@frappe.whitelist()
def get_balance(self):
- if not self.get('accounts'):
+ if not self.get("accounts"):
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
else:
self.total_debit, self.total_credit = 0, 0
@@ -665,18 +854,18 @@ class JournalEntry(AccountsController):
# If any row without amount, set the diff on that row
if diff:
blank_row = None
- for d in self.get('accounts'):
+ for d in self.get("accounts"):
if not d.credit_in_account_currency and not d.debit_in_account_currency and diff != 0:
blank_row = d
if not blank_row:
- blank_row = self.append('accounts', {})
+ blank_row = self.append("accounts", {})
blank_row.exchange_rate = 1
- if diff>0:
+ if diff > 0:
blank_row.credit_in_account_currency = diff
blank_row.credit = diff
- elif diff<0:
+ elif diff < 0:
blank_row.debit_in_account_currency = abs(diff)
blank_row.debit = abs(diff)
@@ -684,76 +873,97 @@ class JournalEntry(AccountsController):
@frappe.whitelist()
def get_outstanding_invoices(self):
- self.set('accounts', [])
+ self.set("accounts", [])
total = 0
for d in self.get_values():
total += flt(d.outstanding_amount, self.precision("credit", "accounts"))
- jd1 = self.append('accounts', {})
+ jd1 = self.append("accounts", {})
jd1.account = d.account
jd1.party = d.party
- if self.write_off_based_on == 'Accounts Receivable':
+ if self.write_off_based_on == "Accounts Receivable":
jd1.party_type = "Customer"
- jd1.credit_in_account_currency = flt(d.outstanding_amount, self.precision("credit", "accounts"))
+ jd1.credit_in_account_currency = flt(
+ d.outstanding_amount, self.precision("credit", "accounts")
+ )
jd1.reference_type = "Sales Invoice"
jd1.reference_name = cstr(d.name)
- elif self.write_off_based_on == 'Accounts Payable':
+ elif self.write_off_based_on == "Accounts Payable":
jd1.party_type = "Supplier"
jd1.debit_in_account_currency = flt(d.outstanding_amount, self.precision("debit", "accounts"))
jd1.reference_type = "Purchase Invoice"
jd1.reference_name = cstr(d.name)
- jd2 = self.append('accounts', {})
- if self.write_off_based_on == 'Accounts Receivable':
+ jd2 = self.append("accounts", {})
+ if self.write_off_based_on == "Accounts Receivable":
jd2.debit_in_account_currency = total
- elif self.write_off_based_on == 'Accounts Payable':
+ elif self.write_off_based_on == "Accounts Payable":
jd2.credit_in_account_currency = total
self.validate_total_debit_and_credit()
-
def get_values(self):
- cond = " and outstanding_amount <= {0}".format(self.write_off_amount) \
- if flt(self.write_off_amount) > 0 else ""
+ cond = (
+ " and outstanding_amount <= {0}".format(self.write_off_amount)
+ if flt(self.write_off_amount) > 0
+ else ""
+ )
- if self.write_off_based_on == 'Accounts Receivable':
- return frappe.db.sql("""select name, debit_to as account, customer as party, outstanding_amount
+ if self.write_off_based_on == "Accounts Receivable":
+ return frappe.db.sql(
+ """select name, debit_to as account, customer as party, outstanding_amount
from `tabSales Invoice` where docstatus = 1 and company = %s
- and outstanding_amount > 0 %s""" % ('%s', cond), self.company, as_dict=True)
- elif self.write_off_based_on == 'Accounts Payable':
- return frappe.db.sql("""select name, credit_to as account, supplier as party, outstanding_amount
+ and outstanding_amount > 0 %s"""
+ % ("%s", cond),
+ self.company,
+ as_dict=True,
+ )
+ elif self.write_off_based_on == "Accounts Payable":
+ return frappe.db.sql(
+ """select name, credit_to as account, supplier as party, outstanding_amount
from `tabPurchase Invoice` where docstatus = 1 and company = %s
- and outstanding_amount > 0 %s""" % ('%s', cond), self.company, as_dict=True)
+ and outstanding_amount > 0 %s"""
+ % ("%s", cond),
+ self.company,
+ as_dict=True,
+ )
def update_expense_claim(self):
for d in self.accounts:
- if d.reference_type=="Expense Claim" and d.reference_name:
+ if d.reference_type == "Expense Claim" and d.reference_name:
doc = frappe.get_doc("Expense Claim", d.reference_name)
if self.docstatus == 2:
update_reimbursed_amount(doc, -1 * d.debit)
else:
update_reimbursed_amount(doc, d.debit)
-
def validate_expense_claim(self):
for d in self.accounts:
- if d.reference_type=="Expense Claim":
- sanctioned_amount, reimbursed_amount = frappe.db.get_value("Expense Claim",
- d.reference_name, ("total_sanctioned_amount", "total_amount_reimbursed"))
- pending_amount = flt(sanctioned_amount) - flt(reimbursed_amount)
- if d.debit > pending_amount:
- frappe.throw(_("Row No {0}: Amount cannot be greater than Pending Amount against Expense Claim {1}. Pending Amount is {2}").format(d.idx, d.reference_name, pending_amount))
+ if d.reference_type == "Expense Claim":
+ outstanding_amt = get_outstanding_amount_for_claim(d.reference_name)
+ if d.debit > outstanding_amt:
+ frappe.throw(
+ _(
+ "Row No {0}: Amount cannot be greater than the Outstanding Amount against Expense Claim {1}. Outstanding Amount is {2}"
+ ).format(d.idx, d.reference_name, outstanding_amt)
+ )
def validate_credit_debit_note(self):
if self.stock_entry:
if frappe.db.get_value("Stock Entry", self.stock_entry, "docstatus") != 1:
frappe.throw(_("Stock Entry {0} is not submitted").format(self.stock_entry))
- if frappe.db.exists({"doctype": "Journal Entry", "stock_entry": self.stock_entry, "docstatus":1}):
- frappe.msgprint(_("Warning: Another {0} # {1} exists against stock entry {2}").format(self.voucher_type, self.name, self.stock_entry))
+ if frappe.db.exists(
+ {"doctype": "Journal Entry", "stock_entry": self.stock_entry, "docstatus": 1}
+ ):
+ frappe.msgprint(
+ _("Warning: Another {0} # {1} exists against stock entry {2}").format(
+ self.voucher_type, self.name, self.stock_entry
+ )
+ )
def validate_empty_accounts_table(self):
- if not self.get('accounts'):
+ if not self.get("accounts"):
frappe.throw(_("Accounts table cannot be blank."))
def set_account_and_party_balance(self):
@@ -764,54 +974,66 @@ class JournalEntry(AccountsController):
account_balance[d.account] = get_balance_on(account=d.account, date=self.posting_date)
if (d.party_type, d.party) not in party_balance:
- party_balance[(d.party_type, d.party)] = get_balance_on(party_type=d.party_type,
- party=d.party, date=self.posting_date, company=self.company)
+ party_balance[(d.party_type, d.party)] = get_balance_on(
+ party_type=d.party_type, party=d.party, date=self.posting_date, company=self.company
+ )
d.account_balance = account_balance[d.account]
d.party_balance = party_balance[(d.party_type, d.party)]
+
@frappe.whitelist()
def get_default_bank_cash_account(company, account_type=None, mode_of_payment=None, account=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import get_bank_cash_account
+
if mode_of_payment:
account = get_bank_cash_account(mode_of_payment, company).get("account")
if not account:
- '''
- Set the default account first. If the user hasn't set any default account then, he doesn't
- want us to set any random account. In this case set the account only if there is single
- account (of that type), otherwise return empty dict.
- '''
- if account_type=="Bank":
- account = frappe.get_cached_value('Company', company, "default_bank_account")
+ """
+ Set the default account first. If the user hasn't set any default account then, he doesn't
+ want us to set any random account. In this case set the account only if there is single
+ account (of that type), otherwise return empty dict.
+ """
+ if account_type == "Bank":
+ account = frappe.get_cached_value("Company", company, "default_bank_account")
if not account:
- account_list = frappe.get_all("Account", filters = {"company": company,
- "account_type": "Bank", "is_group": 0})
+ account_list = frappe.get_all(
+ "Account", filters={"company": company, "account_type": "Bank", "is_group": 0}
+ )
if len(account_list) == 1:
account = account_list[0].name
- elif account_type=="Cash":
- account = frappe.get_cached_value('Company', company, "default_cash_account")
+ elif account_type == "Cash":
+ account = frappe.get_cached_value("Company", company, "default_cash_account")
if not account:
- account_list = frappe.get_all("Account", filters = {"company": company,
- "account_type": "Cash", "is_group": 0})
+ account_list = frappe.get_all(
+ "Account", filters={"company": company, "account_type": "Cash", "is_group": 0}
+ )
if len(account_list) == 1:
account = account_list[0].name
if account:
- account_details = frappe.db.get_value("Account", account,
- ["account_currency", "account_type"], as_dict=1)
+ account_details = frappe.db.get_value(
+ "Account", account, ["account_currency", "account_type"], as_dict=1
+ )
+
+ return frappe._dict(
+ {
+ "account": account,
+ "balance": get_balance_on(account),
+ "account_currency": account_details.account_currency,
+ "account_type": account_details.account_type,
+ }
+ )
+ else:
+ return frappe._dict()
- return frappe._dict({
- "account": account,
- "balance": get_balance_on(account),
- "account_currency": account_details.account_currency,
- "account_type": account_details.account_type
- })
- else: return frappe._dict()
@frappe.whitelist()
-def get_payment_entry_against_order(dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None):
+def get_payment_entry_against_order(
+ dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None
+):
ref_doc = frappe.get_doc(dt, dn)
if flt(ref_doc.per_billed, 2) > 0:
@@ -835,22 +1057,28 @@ def get_payment_entry_against_order(dt, dn, amount=None, debit_in_account_curren
else:
amount = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
- return get_payment_entry(ref_doc, {
- "party_type": party_type,
- "party_account": party_account,
- "party_account_currency": party_account_currency,
- "amount_field_party": amount_field_party,
- "amount_field_bank": amount_field_bank,
- "amount": amount,
- "debit_in_account_currency": debit_in_account_currency,
- "remarks": 'Advance Payment received against {0} {1}'.format(dt, dn),
- "is_advance": "Yes",
- "bank_account": bank_account,
- "journal_entry": journal_entry
- })
+ return get_payment_entry(
+ ref_doc,
+ {
+ "party_type": party_type,
+ "party_account": party_account,
+ "party_account_currency": party_account_currency,
+ "amount_field_party": amount_field_party,
+ "amount_field_bank": amount_field_bank,
+ "amount": amount,
+ "debit_in_account_currency": debit_in_account_currency,
+ "remarks": "Advance Payment received against {0} {1}".format(dt, dn),
+ "is_advance": "Yes",
+ "bank_account": bank_account,
+ "journal_entry": journal_entry,
+ },
+ )
+
@frappe.whitelist()
-def get_payment_entry_against_invoice(dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None):
+def get_payment_entry_against_invoice(
+ dt, dn, amount=None, debit_in_account_currency=None, journal_entry=False, bank_account=None
+):
ref_doc = frappe.get_doc(dt, dn)
if dt == "Sales Invoice":
party_type = "Customer"
@@ -859,73 +1087,91 @@ def get_payment_entry_against_invoice(dt, dn, amount=None, debit_in_account_cur
party_type = "Supplier"
party_account = ref_doc.credit_to
- if (dt == "Sales Invoice" and ref_doc.outstanding_amount > 0) \
- or (dt == "Purchase Invoice" and ref_doc.outstanding_amount < 0):
- amount_field_party = "credit_in_account_currency"
- amount_field_bank = "debit_in_account_currency"
+ if (dt == "Sales Invoice" and ref_doc.outstanding_amount > 0) or (
+ dt == "Purchase Invoice" and ref_doc.outstanding_amount < 0
+ ):
+ amount_field_party = "credit_in_account_currency"
+ amount_field_bank = "debit_in_account_currency"
else:
amount_field_party = "debit_in_account_currency"
amount_field_bank = "credit_in_account_currency"
- return get_payment_entry(ref_doc, {
- "party_type": party_type,
- "party_account": party_account,
- "party_account_currency": ref_doc.party_account_currency,
- "amount_field_party": amount_field_party,
- "amount_field_bank": amount_field_bank,
- "amount": amount if amount else abs(ref_doc.outstanding_amount),
- "debit_in_account_currency": debit_in_account_currency,
- "remarks": 'Payment received against {0} {1}. {2}'.format(dt, dn, ref_doc.remarks),
- "is_advance": "No",
- "bank_account": bank_account,
- "journal_entry": journal_entry
- })
+ return get_payment_entry(
+ ref_doc,
+ {
+ "party_type": party_type,
+ "party_account": party_account,
+ "party_account_currency": ref_doc.party_account_currency,
+ "amount_field_party": amount_field_party,
+ "amount_field_bank": amount_field_bank,
+ "amount": amount if amount else abs(ref_doc.outstanding_amount),
+ "debit_in_account_currency": debit_in_account_currency,
+ "remarks": "Payment received against {0} {1}. {2}".format(dt, dn, ref_doc.remarks),
+ "is_advance": "No",
+ "bank_account": bank_account,
+ "journal_entry": journal_entry,
+ },
+ )
+
def get_payment_entry(ref_doc, args):
- cost_center = ref_doc.get("cost_center") or frappe.get_cached_value('Company', ref_doc.company, "cost_center")
+ cost_center = ref_doc.get("cost_center") or frappe.get_cached_value(
+ "Company", ref_doc.company, "cost_center"
+ )
exchange_rate = 1
if args.get("party_account"):
# Modified to include the posting date for which the exchange rate is required.
# Assumed to be the posting date in the reference document
- exchange_rate = get_exchange_rate(ref_doc.get("posting_date") or ref_doc.get("transaction_date"),
- args.get("party_account"), args.get("party_account_currency"),
- ref_doc.company, ref_doc.doctype, ref_doc.name)
+ exchange_rate = get_exchange_rate(
+ ref_doc.get("posting_date") or ref_doc.get("transaction_date"),
+ args.get("party_account"),
+ args.get("party_account_currency"),
+ ref_doc.company,
+ ref_doc.doctype,
+ ref_doc.name,
+ )
je = frappe.new_doc("Journal Entry")
- je.update({
- "voucher_type": "Bank Entry",
- "company": ref_doc.company,
- "remark": args.get("remarks")
- })
+ je.update(
+ {"voucher_type": "Bank Entry", "company": ref_doc.company, "remark": args.get("remarks")}
+ )
- party_row = je.append("accounts", {
- "account": args.get("party_account"),
- "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_currency": args.get("party_account_currency") or \
- get_account_currency(args.get("party_account")),
- "balance": get_balance_on(args.get("party_account")),
- "party_balance": get_balance_on(party=args.get("party"), party_type=args.get("party_type")),
- "exchange_rate": exchange_rate,
- args.get("amount_field_party"): args.get("amount"),
- "is_advance": args.get("is_advance"),
- "reference_type": ref_doc.doctype,
- "reference_name": ref_doc.name
- })
+ party_row = je.append(
+ "accounts",
+ {
+ "account": args.get("party_account"),
+ "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_currency": args.get("party_account_currency")
+ or get_account_currency(args.get("party_account")),
+ "balance": get_balance_on(args.get("party_account")),
+ "party_balance": get_balance_on(party=args.get("party"), party_type=args.get("party_type")),
+ "exchange_rate": exchange_rate,
+ args.get("amount_field_party"): args.get("amount"),
+ "is_advance": args.get("is_advance"),
+ "reference_type": ref_doc.doctype,
+ "reference_name": ref_doc.name,
+ },
+ )
bank_row = je.append("accounts")
# Make it bank_details
- bank_account = get_default_bank_cash_account(ref_doc.company, "Bank", account=args.get("bank_account"))
+ bank_account = get_default_bank_cash_account(
+ ref_doc.company, "Bank", account=args.get("bank_account")
+ )
if bank_account:
bank_row.update(bank_account)
# Modified to include the posting date for which the exchange rate is required.
# Assumed to be the posting date of the reference date
- bank_row.exchange_rate = get_exchange_rate(ref_doc.get("posting_date")
- or ref_doc.get("transaction_date"), bank_account["account"],
- bank_account["account_currency"], ref_doc.company)
+ bank_row.exchange_rate = get_exchange_rate(
+ ref_doc.get("posting_date") or ref_doc.get("transaction_date"),
+ bank_account["account"],
+ bank_account["account_currency"],
+ ref_doc.company,
+ )
bank_row.cost_center = cost_center
@@ -937,9 +1183,10 @@ def get_payment_entry(ref_doc, args):
bank_row.set(args.get("amount_field_bank"), amount * exchange_rate)
# Multi currency check again
- if party_row.account_currency != ref_doc.company_currency \
- or (bank_row.account_currency and bank_row.account_currency != ref_doc.company_currency):
- je.multi_currency = 1
+ if party_row.account_currency != ref_doc.company_currency or (
+ bank_row.account_currency and bank_row.account_currency != ref_doc.company_currency
+ ):
+ je.multi_currency = 1
je.set_amounts_in_company_currency()
je.set_total_debit_credit()
@@ -947,27 +1194,14 @@ def get_payment_entry(ref_doc, args):
return je if args.get("journal_entry") else je.as_dict()
-@frappe.whitelist()
-def get_opening_accounts(company):
- """get all balance sheet accounts for opening entry"""
- accounts = frappe.db.sql_list("""select
- name from tabAccount
- where
- is_group=0 and report_type='Balance Sheet' and company={0} and
- name not in (select distinct account from tabWarehouse where
- account is not null and account != '')
- order by name asc""".format(frappe.db.escape(company)))
-
- return [{"account": a, "balance": get_balance_on(a)} for a in accounts]
-
-
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
- if not frappe.db.has_column('Journal Entry', searchfield):
+ if not frappe.db.has_column("Journal Entry", searchfield):
return []
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
SELECT jv.name, jv.posting_date, jv.user_remark
FROM `tabJournal Entry` jv, `tabJournal Entry Account` jv_detail
WHERE jv_detail.parent = jv.name
@@ -981,14 +1215,17 @@ def get_against_jv(doctype, txt, searchfield, start, page_len, filters):
AND jv.`{0}` LIKE %(txt)s
ORDER BY jv.name DESC
LIMIT %(offset)s, %(limit)s
- """.format(searchfield), dict(
- account=filters.get("account"),
- party=cstr(filters.get("party")),
- txt="%{0}%".format(txt),
- offset=start,
- limit=page_len
- )
- )
+ """.format(
+ searchfield
+ ),
+ dict(
+ account=filters.get("account"),
+ party=cstr(filters.get("party")),
+ txt="%{0}%".format(txt),
+ offset=start,
+ limit=page_len,
+ ),
+ )
@frappe.whitelist()
@@ -1004,37 +1241,55 @@ def get_outstanding(args):
if args.get("doctype") == "Journal Entry":
condition = " and party=%(party)s" if args.get("party") else ""
- against_jv_amount = frappe.db.sql("""
+ against_jv_amount = frappe.db.sql(
+ """
select sum(debit_in_account_currency) - sum(credit_in_account_currency)
from `tabJournal Entry Account` where parent=%(docname)s and account=%(account)s {0}
- and (reference_type is null or reference_type = '')""".format(condition), args)
+ and (reference_type is null or reference_type = '')""".format(
+ condition
+ ),
+ args,
+ )
against_jv_amount = flt(against_jv_amount[0][0]) if against_jv_amount else 0
- amount_field = "credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency"
- return {
- amount_field: abs(against_jv_amount)
- }
+ amount_field = (
+ "credit_in_account_currency" if against_jv_amount > 0 else "debit_in_account_currency"
+ )
+ return {amount_field: abs(against_jv_amount)}
elif args.get("doctype") in ("Sales Invoice", "Purchase Invoice"):
party_type = "Customer" if args.get("doctype") == "Sales Invoice" else "Supplier"
- invoice = frappe.db.get_value(args["doctype"], args["docname"],
- ["outstanding_amount", "conversion_rate", scrub(party_type)], as_dict=1)
+ invoice = frappe.db.get_value(
+ args["doctype"],
+ args["docname"],
+ ["outstanding_amount", "conversion_rate", scrub(party_type)],
+ as_dict=1,
+ )
- exchange_rate = invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1
+ exchange_rate = (
+ invoice.conversion_rate if (args.get("account_currency") != company_currency) else 1
+ )
if args["doctype"] == "Sales Invoice":
- amount_field = "credit_in_account_currency" \
- if flt(invoice.outstanding_amount) > 0 else "debit_in_account_currency"
+ amount_field = (
+ "credit_in_account_currency"
+ if flt(invoice.outstanding_amount) > 0
+ else "debit_in_account_currency"
+ )
else:
- amount_field = "debit_in_account_currency" \
- if flt(invoice.outstanding_amount) > 0 else "credit_in_account_currency"
+ amount_field = (
+ "debit_in_account_currency"
+ if flt(invoice.outstanding_amount) > 0
+ else "credit_in_account_currency"
+ )
return {
amount_field: abs(flt(invoice.outstanding_amount)),
"exchange_rate": exchange_rate,
"party_type": party_type,
- "party": invoice.get(scrub(party_type))
+ "party": invoice.get(scrub(party_type)),
}
+
@frappe.whitelist()
def get_party_account_and_balance(company, party_type, party, cost_center=None):
if not frappe.has_permission("Account"):
@@ -1043,24 +1298,30 @@ def get_party_account_and_balance(company, party_type, party, cost_center=None):
account = get_party_account(party_type, party, company)
account_balance = get_balance_on(account=account, cost_center=cost_center)
- party_balance = get_balance_on(party_type=party_type, party=party, company=company, cost_center=cost_center)
+ party_balance = get_balance_on(
+ party_type=party_type, party=party, company=company, cost_center=cost_center
+ )
return {
"account": account,
"balance": account_balance,
"party_balance": party_balance,
- "account_currency": frappe.db.get_value("Account", account, "account_currency")
+ "account_currency": frappe.db.get_value("Account", account, "account_currency"),
}
@frappe.whitelist()
-def get_account_balance_and_party_type(account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None):
+def get_account_balance_and_party_type(
+ account, date, company, debit=None, credit=None, exchange_rate=None, cost_center=None
+):
"""Returns dict of account balance and party type to be set in Journal Entry on selection of account."""
if not frappe.has_permission("Account"):
frappe.msgprint(_("No Permission"), raise_exception=1)
company_currency = erpnext.get_company_currency(company)
- account_details = frappe.db.get_value("Account", account, ["account_type", "account_currency"], as_dict=1)
+ account_details = frappe.db.get_value(
+ "Account", account, ["account_type", "account_currency"], as_dict=1
+ )
if not account_details:
return
@@ -1077,11 +1338,17 @@ def get_account_balance_and_party_type(account, date, company, debit=None, credi
"party_type": party_type,
"account_type": account_details.account_type,
"account_currency": account_details.account_currency or company_currency,
-
# The date used to retreive the exchange rate here is the date passed in
# as an argument to this function. It is assumed to be the date on which the balance is sought
- "exchange_rate": get_exchange_rate(date, account, account_details.account_currency,
- company, debit=debit, credit=credit, exchange_rate=exchange_rate)
+ "exchange_rate": get_exchange_rate(
+ date,
+ account,
+ account_details.account_currency,
+ company,
+ debit=debit,
+ credit=credit,
+ exchange_rate=exchange_rate,
+ ),
}
# un-set party if not party type
@@ -1092,11 +1359,22 @@ def get_account_balance_and_party_type(account, date, company, debit=None, credi
@frappe.whitelist()
-def get_exchange_rate(posting_date, account=None, account_currency=None, company=None,
- reference_type=None, reference_name=None, debit=None, credit=None, exchange_rate=None):
+def get_exchange_rate(
+ posting_date,
+ account=None,
+ account_currency=None,
+ company=None,
+ reference_type=None,
+ reference_name=None,
+ debit=None,
+ credit=None,
+ exchange_rate=None,
+):
from erpnext.setup.utils import get_exchange_rate
- account_details = frappe.db.get_value("Account", account,
- ["account_type", "root_type", "account_currency", "company"], as_dict=1)
+
+ account_details = frappe.db.get_value(
+ "Account", account, ["account_type", "root_type", "account_currency", "company"], as_dict=1
+ )
if not account_details:
frappe.throw(_("Please select correct account"))
@@ -1115,7 +1393,7 @@ def get_exchange_rate(posting_date, account=None, account_currency=None, company
# The date used to retreive the exchange rate here is the date passed
# in as an argument to this function.
- elif (not exchange_rate or flt(exchange_rate)==1) and account_currency and posting_date:
+ elif (not exchange_rate or flt(exchange_rate) == 1) and account_currency and posting_date:
exchange_rate = get_exchange_rate(account_currency, company_currency, posting_date)
else:
exchange_rate = 1
@@ -1134,42 +1412,43 @@ def get_average_exchange_rate(account):
return exchange_rate
+
@frappe.whitelist()
def make_inter_company_journal_entry(name, voucher_type, company):
- journal_entry = frappe.new_doc('Journal Entry')
+ journal_entry = frappe.new_doc("Journal Entry")
journal_entry.voucher_type = voucher_type
journal_entry.company = company
journal_entry.posting_date = nowdate()
journal_entry.inter_company_journal_entry_reference = name
return journal_entry.as_dict()
+
@frappe.whitelist()
def make_reverse_journal_entry(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
- def update_accounts(source, target, source_parent):
- target.reference_type = "Journal Entry"
- target.reference_name = source_parent.name
+ def post_process(source, target):
+ target.reversal_of = source.name
- doclist = get_mapped_doc("Journal Entry", source_name, {
- "Journal Entry": {
- "doctype": "Journal Entry",
- "validation": {
- "docstatus": ["=", 1]
- }
- },
- "Journal Entry Account": {
- "doctype": "Journal Entry Account",
- "field_map": {
- "account_currency": "account_currency",
- "exchange_rate": "exchange_rate",
- "debit_in_account_currency": "credit_in_account_currency",
- "debit": "credit",
- "credit_in_account_currency": "debit_in_account_currency",
- "credit": "debit",
+ doclist = get_mapped_doc(
+ "Journal Entry",
+ source_name,
+ {
+ "Journal Entry": {"doctype": "Journal Entry", "validation": {"docstatus": ["=", 1]}},
+ "Journal Entry Account": {
+ "doctype": "Journal Entry Account",
+ "field_map": {
+ "account_currency": "account_currency",
+ "exchange_rate": "exchange_rate",
+ "debit_in_account_currency": "credit_in_account_currency",
+ "debit": "credit",
+ "credit_in_account_currency": "debit_in_account_currency",
+ "credit": "debit",
+ },
},
- "postprocess": update_accounts,
},
- }, target_doc)
+ target_doc,
+ post_process,
+ )
return doclist
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
index 481462b2aba..2cc5378e927 100644
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
@@ -39,14 +39,25 @@ class TestJournalEntry(unittest.TestCase):
test_voucher.submit()
if test_voucher.doctype == "Journal Entry":
- self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account`
+ self.assertTrue(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
where account = %s and docstatus = 1 and parent = %s""",
- ("_Test Receivable - _TC", test_voucher.name)))
+ ("_Test Receivable - _TC", test_voucher.name),
+ )
+ )
- self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account`
- where reference_type = %s and reference_name = %s""", (test_voucher.doctype, test_voucher.name)))
+ self.assertFalse(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
+ where reference_type = %s and reference_name = %s""",
+ (test_voucher.doctype, test_voucher.name),
+ )
+ )
- base_jv.get("accounts")[0].is_advance = "Yes" if (test_voucher.doctype in ["Sales Order", "Purchase Order"]) else "No"
+ base_jv.get("accounts")[0].is_advance = (
+ "Yes" if (test_voucher.doctype in ["Sales Order", "Purchase Order"]) else "No"
+ )
base_jv.get("accounts")[0].set("reference_type", test_voucher.doctype)
base_jv.get("accounts")[0].set("reference_name", test_voucher.name)
base_jv.insert()
@@ -54,18 +65,28 @@ class TestJournalEntry(unittest.TestCase):
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
- self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account`
- where reference_type = %s and reference_name = %s and {0}=400""".format(dr_or_cr),
- (submitted_voucher.doctype, submitted_voucher.name)))
+ self.assertTrue(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
+ where reference_type = %s and reference_name = %s and {0}=400""".format(
+ dr_or_cr
+ ),
+ (submitted_voucher.doctype, submitted_voucher.name),
+ )
+ )
if base_jv.get("accounts")[0].is_advance == "Yes":
self.advance_paid_testcase(base_jv, submitted_voucher, dr_or_cr)
self.cancel_against_voucher_testcase(submitted_voucher)
def advance_paid_testcase(self, base_jv, test_voucher, dr_or_cr):
- #Test advance paid field
- advance_paid = frappe.db.sql("""select advance_paid from `tab%s`
- where name=%s""" % (test_voucher.doctype, '%s'), (test_voucher.name))
+ # Test advance paid field
+ advance_paid = frappe.db.sql(
+ """select advance_paid from `tab%s`
+ where name=%s"""
+ % (test_voucher.doctype, "%s"),
+ (test_voucher.name),
+ )
payment_against_order = base_jv.get("accounts")[0].get(dr_or_cr)
self.assertTrue(flt(advance_paid[0][0]) == flt(payment_against_order))
@@ -74,13 +95,19 @@ class TestJournalEntry(unittest.TestCase):
if test_voucher.doctype == "Journal Entry":
# if test_voucher is a Journal Entry, test cancellation of test_voucher
test_voucher.cancel()
- self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account`
- where reference_type='Journal Entry' and reference_name=%s""", test_voucher.name))
+ self.assertFalse(
+ frappe.db.sql(
+ """select name from `tabJournal Entry Account`
+ where reference_type='Journal Entry' and reference_name=%s""",
+ test_voucher.name,
+ )
+ )
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
- frappe.db.set_value("Accounts Settings", "Accounts Settings",
- "unlink_advance_payment_on_cancelation_of_order", 0)
+ frappe.db.set_value(
+ "Accounts Settings", "Accounts Settings", "unlink_advance_payment_on_cancelation_of_order", 0
+ )
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
@@ -89,7 +116,10 @@ class TestJournalEntry(unittest.TestCase):
stock_account = get_inventory_account(company)
from erpnext.accounts.utils import get_stock_and_account_balance
- account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
+
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(
+ stock_account, nowdate(), company
+ )
diff = flt(account_bal) - flt(stock_bal)
if not diff:
@@ -98,19 +128,25 @@ class TestJournalEntry(unittest.TestCase):
jv = frappe.new_doc("Journal Entry")
jv.company = company
jv.posting_date = nowdate()
- jv.append("accounts", {
- "account": stock_account,
- "cost_center": "Main - TCP1",
- "debit_in_account_currency": 0 if diff > 0 else abs(diff),
- "credit_in_account_currency": diff if diff > 0 else 0
- })
+ jv.append(
+ "accounts",
+ {
+ "account": stock_account,
+ "cost_center": "Main - TCP1",
+ "debit_in_account_currency": 0 if diff > 0 else abs(diff),
+ "credit_in_account_currency": diff if diff > 0 else 0,
+ },
+ )
- jv.append("accounts", {
- "account": "Stock Adjustment - TCP1",
- "cost_center": "Main - TCP1",
- "debit_in_account_currency": diff if diff > 0 else 0,
- "credit_in_account_currency": 0 if diff > 0 else abs(diff)
- })
+ jv.append(
+ "accounts",
+ {
+ "account": "Stock Adjustment - TCP1",
+ "cost_center": "Main - TCP1",
+ "debit_in_account_currency": diff if diff > 0 else 0,
+ "credit_in_account_currency": 0 if diff > 0 else abs(diff),
+ },
+ )
jv.insert()
if account_bal == stock_bal:
@@ -121,16 +157,21 @@ class TestJournalEntry(unittest.TestCase):
jv.cancel()
def test_multi_currency(self):
- jv = make_journal_entry("_Test Bank USD - _TC",
- "_Test Bank - _TC", 100, exchange_rate=50, save=False)
+ jv = make_journal_entry(
+ "_Test Bank USD - _TC", "_Test Bank - _TC", 100, exchange_rate=50, save=False
+ )
jv.get("accounts")[1].credit_in_account_currency = 5000
jv.submit()
- gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""", jv.name, as_dict=1)
+ order by account asc""",
+ jv.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -140,33 +181,42 @@ class TestJournalEntry(unittest.TestCase):
"debit": 5000,
"debit_in_account_currency": 100,
"credit": 0,
- "credit_in_account_currency": 0
+ "credit_in_account_currency": 0,
},
"_Test Bank - _TC": {
"account_currency": "INR",
"debit": 0,
"debit_in_account_currency": 0,
"credit": 5000,
- "credit_in_account_currency": 5000
- }
+ "credit_in_account_currency": 5000,
+ },
}
- for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"):
+ for field in (
+ "account_currency",
+ "debit",
+ "debit_in_account_currency",
+ "credit",
+ "credit_in_account_currency",
+ ):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
# cancel
jv.cancel()
- gle = frappe.db.sql("""select name from `tabGL Entry`
- where voucher_type='Sales Invoice' and voucher_no=%s""", jv.name)
+ gle = frappe.db.sql(
+ """select name from `tabGL Entry`
+ where voucher_type='Sales Invoice' and voucher_no=%s""",
+ jv.name,
+ )
self.assertFalse(gle)
def test_reverse_journal_entry(self):
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
- jv = make_journal_entry("_Test Bank USD - _TC",
- "Sales - _TC", 100, exchange_rate=50, save=False)
+
+ jv = make_journal_entry("_Test Bank USD - _TC", "Sales - _TC", 100, exchange_rate=50, save=False)
jv.get("accounts")[1].credit_in_account_currency = 5000
jv.get("accounts")[1].exchange_rate = 1
@@ -176,15 +226,17 @@ class TestJournalEntry(unittest.TestCase):
rjv.posting_date = nowdate()
rjv.submit()
-
- gl_entries = frappe.db.sql("""select account, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""", rjv.name, as_dict=1)
+ order by account asc""",
+ rjv.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
-
expected_values = {
"_Test Bank USD - _TC": {
"account_currency": "USD",
@@ -199,44 +251,38 @@ class TestJournalEntry(unittest.TestCase):
"debit_in_account_currency": 5000,
"credit": 0,
"credit_in_account_currency": 0,
- }
+ },
}
- for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"):
+ for field in (
+ "account_currency",
+ "debit",
+ "debit_in_account_currency",
+ "credit",
+ "credit_in_account_currency",
+ ):
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][field], gle[field])
def test_disallow_change_in_account_currency_for_a_party(self):
# create jv in USD
- jv = make_journal_entry("_Test Bank USD - _TC",
- "_Test Receivable USD - _TC", 100, save=False)
+ jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False)
- jv.accounts[1].update({
- "party_type": "Customer",
- "party": "_Test Customer USD"
- })
+ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"})
jv.submit()
# create jv in USD, but account currency in INR
- jv = make_journal_entry("_Test Bank - _TC",
- "_Test Receivable - _TC", 100, save=False)
+ jv = make_journal_entry("_Test Bank - _TC", "_Test Receivable - _TC", 100, save=False)
- jv.accounts[1].update({
- "party_type": "Customer",
- "party": "_Test Customer USD"
- })
+ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"})
self.assertRaises(InvalidAccountCurrency, jv.submit)
# back in USD
- jv = make_journal_entry("_Test Bank USD - _TC",
- "_Test Receivable USD - _TC", 100, save=False)
+ jv = make_journal_entry("_Test Bank USD - _TC", "_Test Receivable USD - _TC", 100, save=False)
- jv.accounts[1].update({
- "party_type": "Customer",
- "party": "_Test Customer USD"
- })
+ jv.accounts[1].update({"party_type": "Customer", "party": "_Test Customer USD"})
jv.submit()
@@ -245,13 +291,27 @@ class TestJournalEntry(unittest.TestCase):
frappe.db.set_value("Account", "Buildings - _TC", "inter_company_account", 1)
frappe.db.set_value("Account", "Sales Expenses - _TC1", "inter_company_account", 1)
frappe.db.set_value("Account", "Buildings - _TC1", "inter_company_account", 1)
- jv = make_journal_entry("Sales Expenses - _TC", "Buildings - _TC", 100, posting_date=nowdate(), cost_center = "Main - _TC", save=False)
+ jv = make_journal_entry(
+ "Sales Expenses - _TC",
+ "Buildings - _TC",
+ 100,
+ posting_date=nowdate(),
+ cost_center="Main - _TC",
+ save=False,
+ )
jv.voucher_type = "Inter Company Journal Entry"
jv.multi_currency = 0
jv.insert()
jv.submit()
- jv1 = make_journal_entry("Sales Expenses - _TC1", "Buildings - _TC1", 100, posting_date=nowdate(), cost_center = "Main - _TC1", save=False)
+ jv1 = make_journal_entry(
+ "Sales Expenses - _TC1",
+ "Buildings - _TC1",
+ 100,
+ posting_date=nowdate(),
+ cost_center="Main - _TC1",
+ save=False,
+ )
jv1.inter_company_journal_entry_reference = jv.name
jv1.company = "_Test Company 1"
jv1.voucher_type = "Inter Company Journal Entry"
@@ -273,9 +333,12 @@ class TestJournalEntry(unittest.TestCase):
def test_jv_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center = cost_center, save=False)
+ jv = make_journal_entry(
+ "_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center=cost_center, save=False
+ )
jv.voucher_type = "Bank Entry"
jv.multi_currency = 0
jv.cheque_no = "112233"
@@ -284,17 +347,17 @@ class TestJournalEntry(unittest.TestCase):
jv.submit()
expected_values = {
- "_Test Cash - _TC": {
- "cost_center": cost_center
- },
- "_Test Bank - _TC": {
- "cost_center": cost_center
- }
+ "_Test Cash - _TC": {"cost_center": cost_center},
+ "_Test Bank - _TC": {"cost_center": cost_center},
}
- gl_entries = frappe.db.sql("""select account, cost_center, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, debit, credit
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""", jv.name, as_dict=1)
+ order by account asc""",
+ jv.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -305,11 +368,13 @@ class TestJournalEntry(unittest.TestCase):
from erpnext.projects.doctype.project.test_project import make_project
if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}):
- project = make_project({
- 'project_name': 'Journal Entry Project',
- 'project_template_name': 'Test Project Template',
- 'start_date': '2020-01-01'
- })
+ project = make_project(
+ {
+ "project_name": "Journal Entry Project",
+ "project_template_name": "Test Project Template",
+ "start_date": "2020-01-01",
+ }
+ )
project_name = project.name
else:
project_name = frappe.get_value("Project", {"project_name": "_Test Project"})
@@ -325,17 +390,17 @@ class TestJournalEntry(unittest.TestCase):
jv.submit()
expected_values = {
- "_Test Cash - _TC": {
- "project": project_name
- },
- "_Test Bank - _TC": {
- "project": project_name
- }
+ "_Test Cash - _TC": {"project": project_name},
+ "_Test Bank - _TC": {"project": project_name},
}
- gl_entries = frappe.db.sql("""select account, project, debit, credit
+ gl_entries = frappe.db.sql(
+ """select account, project, debit, credit
from `tabGL Entry` where voucher_type='Journal Entry' and voucher_no=%s
- order by account asc""", jv.name, as_dict=1)
+ order by account asc""",
+ jv.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -345,9 +410,12 @@ class TestJournalEntry(unittest.TestCase):
def test_jv_account_and_party_balance_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.utils import get_balance_on
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center = cost_center, save=False)
+ jv = make_journal_entry(
+ "_Test Cash - _TC", "_Test Bank - _TC", 100, cost_center=cost_center, save=False
+ )
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center)
jv.voucher_type = "Bank Entry"
jv.multi_currency = 0
@@ -360,7 +428,18 @@ class TestJournalEntry(unittest.TestCase):
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=cost_center)
self.assertEqual(expected_account_balance, account_balance)
-def make_journal_entry(account1, account2, amount, cost_center=None, posting_date=None, exchange_rate=1, save=True, submit=False, project=None):
+
+def make_journal_entry(
+ account1,
+ account2,
+ amount,
+ cost_center=None,
+ posting_date=None,
+ exchange_rate=1,
+ save=True,
+ submit=False,
+ project=None,
+):
if not cost_center:
cost_center = "_Test Cost Center - _TC"
@@ -369,23 +448,27 @@ def make_journal_entry(account1, account2, amount, cost_center=None, posting_dat
jv.company = "_Test Company"
jv.user_remark = "test"
jv.multi_currency = 1
- jv.set("accounts", [
- {
- "account": account1,
- "cost_center": cost_center,
- "project": project,
- "debit_in_account_currency": amount if amount > 0 else 0,
- "credit_in_account_currency": abs(amount) if amount < 0 else 0,
- "exchange_rate": exchange_rate
- }, {
- "account": account2,
- "cost_center": cost_center,
- "project": project,
- "credit_in_account_currency": amount if amount > 0 else 0,
- "debit_in_account_currency": abs(amount) if amount < 0 else 0,
- "exchange_rate": exchange_rate
- }
- ])
+ jv.set(
+ "accounts",
+ [
+ {
+ "account": account1,
+ "cost_center": cost_center,
+ "project": project,
+ "debit_in_account_currency": amount if amount > 0 else 0,
+ "credit_in_account_currency": abs(amount) if amount < 0 else 0,
+ "exchange_rate": exchange_rate,
+ },
+ {
+ "account": account2,
+ "cost_center": cost_center,
+ "project": project,
+ "credit_in_account_currency": amount if amount > 0 else 0,
+ "debit_in_account_currency": abs(amount) if amount < 0 else 0,
+ "exchange_rate": exchange_rate,
+ },
+ ],
+ )
if save or submit:
jv.insert()
@@ -394,4 +477,5 @@ def make_journal_entry(account1, account2, amount, cost_center=None, posting_dat
return jv
-test_records = frappe.get_test_records('Journal Entry')
+
+test_records = frappe.get_test_records("Journal Entry")
diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js
index 1c19c1d2255..cf5fbe12afe 100644
--- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js
+++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.js
@@ -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(){
diff --git a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py
index 2da72c20ad8..b8ef3545d33 100644
--- a/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py
+++ b/erpnext/accounts/doctype/journal_entry_template/journal_entry_template.py
@@ -9,6 +9,7 @@ from frappe.model.document import Document
class JournalEntryTemplate(Document):
pass
+
@frappe.whitelist()
def get_naming_series():
return frappe.get_meta("Journal Entry").get_field("naming_series").options
diff --git a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
index f460b9f7953..dcb43fb2cb3 100644
--- a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
+++ b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
@@ -8,6 +8,7 @@ from frappe.utils import today
exclude_from_linked_with = True
+
class LoyaltyPointEntry(Document):
pass
@@ -16,18 +17,28 @@ def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=No
if not expiry_date:
expiry_date = today()
- return frappe.db.sql('''
+ return frappe.db.sql(
+ """
select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice
from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s
and expiry_date>=%s and loyalty_points>0 and company=%s
order by expiry_date
- ''', (customer, loyalty_program, expiry_date, company), as_dict=1)
+ """,
+ (customer, loyalty_program, expiry_date, company),
+ as_dict=1,
+ )
+
def get_redemption_details(customer, loyalty_program, company):
- return frappe._dict(frappe.db.sql('''
+ return frappe._dict(
+ frappe.db.sql(
+ """
select redeem_against, sum(loyalty_points)
from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and loyalty_points<0 and company=%s
group by redeem_against
- ''', (customer, loyalty_program, company)))
+ """,
+ (customer, loyalty_program, company),
+ )
+ )
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
index 70da03b27f3..48a25ad6b81 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.py
@@ -12,39 +12,61 @@ class LoyaltyProgram(Document):
pass
-def get_loyalty_details(customer, loyalty_program, expiry_date=None, company=None, include_expired_entry=False):
+def get_loyalty_details(
+ customer, loyalty_program, expiry_date=None, company=None, include_expired_entry=False
+):
if not expiry_date:
expiry_date = today()
- condition = ''
+ condition = ""
if company:
condition = " and company=%s " % frappe.db.escape(company)
if not include_expired_entry:
condition += " and expiry_date>='%s' " % expiry_date
- loyalty_point_details = frappe.db.sql('''select sum(loyalty_points) as loyalty_points,
+ loyalty_point_details = frappe.db.sql(
+ """select sum(loyalty_points) as loyalty_points,
sum(purchase_amount) as total_spent from `tabLoyalty Point Entry`
where customer=%s and loyalty_program=%s and posting_date <= %s
{condition}
- group by customer'''.format(condition=condition),
- (customer, loyalty_program, expiry_date), as_dict=1)
+ group by customer""".format(
+ condition=condition
+ ),
+ (customer, loyalty_program, expiry_date),
+ as_dict=1,
+ )
if loyalty_point_details:
return loyalty_point_details[0]
else:
return {"loyalty_points": 0, "total_spent": 0}
-@frappe.whitelist()
-def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, \
- silent=False, include_expired_entry=False, current_transaction_amount=0):
- lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent)
- loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
- lp_details.update(get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry))
- tier_spent_level = sorted([d.as_dict() for d in loyalty_program.collection_rules],
- key=lambda rule:rule.min_spent, reverse=True)
+@frappe.whitelist()
+def get_loyalty_program_details_with_points(
+ customer,
+ loyalty_program=None,
+ expiry_date=None,
+ company=None,
+ silent=False,
+ include_expired_entry=False,
+ current_transaction_amount=0,
+):
+ lp_details = get_loyalty_program_details(
+ customer, loyalty_program, company=company, silent=silent
+ )
+ loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program)
+ lp_details.update(
+ get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry)
+ )
+
+ tier_spent_level = sorted(
+ [d.as_dict() for d in loyalty_program.collection_rules],
+ key=lambda rule: rule.min_spent,
+ reverse=True,
+ )
for i, d in enumerate(tier_spent_level):
- if i==0 or (lp_details.total_spent+current_transaction_amount) <= d.min_spent:
+ if i == 0 or (lp_details.total_spent + current_transaction_amount) <= d.min_spent:
lp_details.tier_name = d.tier_name
lp_details.collection_factor = d.collection_factor
else:
@@ -52,8 +74,16 @@ def get_loyalty_program_details_with_points(customer, loyalty_program=None, expi
return lp_details
+
@frappe.whitelist()
-def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None, company=None, silent=False, include_expired_entry=False):
+def get_loyalty_program_details(
+ customer,
+ loyalty_program=None,
+ expiry_date=None,
+ company=None,
+ silent=False,
+ include_expired_entry=False,
+):
lp_details = frappe._dict()
if not loyalty_program:
@@ -72,6 +102,7 @@ def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None
lp_details.update(loyalty_program.as_dict())
return lp_details
+
@frappe.whitelist()
def get_redeemption_factor(loyalty_program=None, customer=None):
customer_loyalty_program = None
@@ -98,13 +129,16 @@ def validate_loyalty_points(ref_doc, points_to_redeem):
else:
loyalty_program = frappe.db.get_value("Customer", ref_doc.customer, ["loyalty_program"])
- if loyalty_program and frappe.db.get_value("Loyalty Program", loyalty_program, ["company"]) !=\
- ref_doc.company:
+ if (
+ loyalty_program
+ and frappe.db.get_value("Loyalty Program", loyalty_program, ["company"]) != ref_doc.company
+ ):
frappe.throw(_("The Loyalty Program isn't valid for the selected company"))
if loyalty_program and points_to_redeem:
- loyalty_program_details = get_loyalty_program_details_with_points(ref_doc.customer, loyalty_program,
- posting_date, ref_doc.company)
+ loyalty_program_details = get_loyalty_program_details_with_points(
+ ref_doc.customer, loyalty_program, posting_date, ref_doc.company
+ )
if points_to_redeem > loyalty_program_details.loyalty_points:
frappe.throw(_("You don't have enought Loyalty Points to redeem"))
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py b/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py
index 7652e9691b4..3a4f908a1e9 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program_dashboard.py
@@ -1,11 +1,5 @@
-
-
def get_data():
return {
- 'fieldname': 'loyalty_program',
- 'transactions': [
- {
- 'items': ['Sales Invoice', 'Customer']
- }
- ]
+ "fieldname": "loyalty_program",
+ "transactions": [{"items": ["Sales Invoice", "Customer"]}],
}
diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
index 82c14324f5c..3641ac4428f 100644
--- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
@@ -19,19 +19,28 @@ class TestLoyaltyProgram(unittest.TestCase):
create_records()
def test_loyalty_points_earned_single_tier(self):
- frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
+ frappe.db.set_value(
+ "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
+ )
# create a new sales invoice
si_original = create_sales_invoice_record()
si_original.insert()
si_original.submit()
- customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
+ customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
earned_points = get_points_earned(si_original)
- lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
+ lpe = frappe.get_doc(
+ "Loyalty Point Entry",
+ {
+ "invoice_type": "Sales Invoice",
+ "invoice": si_original.name,
+ "customer": si_original.customer,
+ },
+ )
- self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program)
- self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier)
+ self.assertEqual(si_original.get("loyalty_program"), customer.loyalty_program)
+ self.assertEqual(lpe.get("loyalty_program_tier"), customer.loyalty_program_tier)
self.assertEqual(lpe.loyalty_points, earned_points)
# add redemption point
@@ -43,21 +52,31 @@ class TestLoyaltyProgram(unittest.TestCase):
earned_after_redemption = get_points_earned(si_redeem)
- lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name})
- lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
+ lpe_redeem = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "redeem_against": lpe.name},
+ )
+ lpe_earn = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "name": ["!=", lpe_redeem.name]},
+ )
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
- self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
+ self.assertEqual(lpe_redeem.loyalty_points, (-1 * earned_points))
# cancel and delete
for d in [si_redeem, si_original]:
d.cancel()
def test_loyalty_points_earned_multiple_tier(self):
- frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty")
+ frappe.db.set_value(
+ "Customer", "Test Loyalty Customer", "loyalty_program", "Test Multiple Loyalty"
+ )
# assign multiple tier program to the customer
- customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
- customer.loyalty_program = frappe.get_doc('Loyalty Program', {'loyalty_program_name': 'Test Multiple Loyalty'}).name
+ customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
+ customer.loyalty_program = frappe.get_doc(
+ "Loyalty Program", {"loyalty_program_name": "Test Multiple Loyalty"}
+ ).name
customer.save()
# create a new sales invoice
@@ -67,10 +86,17 @@ class TestLoyaltyProgram(unittest.TestCase):
earned_points = get_points_earned(si_original)
- lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
+ lpe = frappe.get_doc(
+ "Loyalty Point Entry",
+ {
+ "invoice_type": "Sales Invoice",
+ "invoice": si_original.name,
+ "customer": si_original.customer,
+ },
+ )
- self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program)
- self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier)
+ self.assertEqual(si_original.get("loyalty_program"), customer.loyalty_program)
+ self.assertEqual(lpe.get("loyalty_program_tier"), customer.loyalty_program_tier)
self.assertEqual(lpe.loyalty_points, earned_points)
# add redemption point
@@ -80,14 +106,20 @@ class TestLoyaltyProgram(unittest.TestCase):
si_redeem.insert()
si_redeem.submit()
- customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"})
+ customer = frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"})
earned_after_redemption = get_points_earned(si_redeem)
- lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name})
- lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]})
+ lpe_redeem = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "redeem_against": lpe.name},
+ )
+ lpe_earn = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "Sales Invoice", "invoice": si_redeem.name, "name": ["!=", lpe_redeem.name]},
+ )
self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption)
- self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points))
+ self.assertEqual(lpe_redeem.loyalty_points, (-1 * earned_points))
self.assertEqual(lpe_earn.loyalty_program_tier, customer.loyalty_program_tier)
# cancel and delete
@@ -95,23 +127,30 @@ class TestLoyaltyProgram(unittest.TestCase):
d.cancel()
def test_cancel_sales_invoice(self):
- ''' cancelling the sales invoice should cancel the earned points'''
- frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
+ """cancelling the sales invoice should cancel the earned points"""
+ frappe.db.set_value(
+ "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
+ )
# create a new sales invoice
si = create_sales_invoice_record()
si.insert()
si.submit()
- lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si.name, 'customer': si.customer})
+ lpe = frappe.get_doc(
+ "Loyalty Point Entry",
+ {"invoice_type": "Sales Invoice", "invoice": si.name, "customer": si.customer},
+ )
self.assertEqual(True, not (lpe is None))
# cancelling sales invoice
si.cancel()
- lpe = frappe.db.exists('Loyalty Point Entry', lpe.name)
+ lpe = frappe.db.exists("Loyalty Point Entry", lpe.name)
self.assertEqual(True, (lpe is None))
def test_sales_invoice_return(self):
- frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty")
+ frappe.db.set_value(
+ "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty"
+ )
# create a new sales invoice
si_original = create_sales_invoice_record(2)
si_original.conversion_rate = flt(1)
@@ -119,7 +158,14 @@ class TestLoyaltyProgram(unittest.TestCase):
si_original.submit()
earned_points = get_points_earned(si_original)
- lpe_original = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
+ lpe_original = frappe.get_doc(
+ "Loyalty Point Entry",
+ {
+ "invoice_type": "Sales Invoice",
+ "invoice": si_original.name,
+ "customer": si_original.customer,
+ },
+ )
self.assertEqual(lpe_original.loyalty_points, earned_points)
# create sales invoice return
@@ -131,10 +177,17 @@ class TestLoyaltyProgram(unittest.TestCase):
si_return.submit()
# fetch original invoice again as its status would have been updated
- si_original = frappe.get_doc('Sales Invoice', lpe_original.invoice)
+ si_original = frappe.get_doc("Sales Invoice", lpe_original.invoice)
earned_points = get_points_earned(si_original)
- lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer})
+ lpe_after_return = frappe.get_doc(
+ "Loyalty Point Entry",
+ {
+ "invoice_type": "Sales Invoice",
+ "invoice": si_original.name,
+ "customer": si_original.customer,
+ },
+ )
self.assertEqual(lpe_after_return.loyalty_points, earned_points)
self.assertEqual(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points))
@@ -143,144 +196,164 @@ class TestLoyaltyProgram(unittest.TestCase):
try:
d.cancel()
except frappe.TimestampMismatchError:
- frappe.get_doc('Sales Invoice', d.name).cancel()
+ frappe.get_doc("Sales Invoice", d.name).cancel()
def test_loyalty_points_for_dashboard(self):
- doc = frappe.get_doc('Customer', 'Test Loyalty Customer')
+ doc = frappe.get_doc("Customer", "Test Loyalty Customer")
company_wise_info = get_dashboard_info("Customer", doc.name, doc.loyalty_program)
for d in company_wise_info:
self.assertTrue(d.get("loyalty_points"))
+
def get_points_earned(self):
def get_returned_amount():
- returned_amount = frappe.db.sql("""
+ returned_amount = frappe.db.sql(
+ """
select sum(grand_total)
from `tabSales Invoice`
where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s
- """, self.name)
+ """,
+ self.name,
+ )
return abs(flt(returned_amount[0][0])) if returned_amount else 0
- lp_details = get_loyalty_program_details_with_points(self.customer, company=self.company,
- loyalty_program=self.loyalty_program, expiry_date=self.posting_date, include_expired_entry=True)
- if lp_details and getdate(lp_details.from_date) <= getdate(self.posting_date) and \
- (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)):
+ lp_details = get_loyalty_program_details_with_points(
+ self.customer,
+ company=self.company,
+ loyalty_program=self.loyalty_program,
+ expiry_date=self.posting_date,
+ include_expired_entry=True,
+ )
+ if (
+ lp_details
+ and getdate(lp_details.from_date) <= getdate(self.posting_date)
+ and (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date))
+ ):
returned_amount = get_returned_amount()
eligible_amount = flt(self.grand_total) - cint(self.loyalty_amount) - returned_amount
- points_earned = cint(eligible_amount/lp_details.collection_factor)
+ points_earned = cint(eligible_amount / lp_details.collection_factor)
return points_earned or 0
+
def create_sales_invoice_record(qty=1):
# return sales invoice doc object
- return frappe.get_doc({
- "doctype": "Sales Invoice",
- "customer": frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}).name,
- "company": '_Test Company',
- "due_date": today(),
- "posting_date": today(),
- "currency": "INR",
- "taxes_and_charges": "",
- "debit_to": "Debtors - _TC",
- "taxes": [],
- "items": [{
- 'doctype': 'Sales Invoice Item',
- 'item_code': frappe.get_doc('Item', {'item_name': 'Loyal Item'}).name,
- 'qty': qty,
- "rate": 10000,
- 'income_account': 'Sales - _TC',
- 'cost_center': 'Main - _TC',
- 'expense_account': 'Cost of Goods Sold - _TC'
- }]
- })
+ return frappe.get_doc(
+ {
+ "doctype": "Sales Invoice",
+ "customer": frappe.get_doc("Customer", {"customer_name": "Test Loyalty Customer"}).name,
+ "company": "_Test Company",
+ "due_date": today(),
+ "posting_date": today(),
+ "currency": "INR",
+ "taxes_and_charges": "",
+ "debit_to": "Debtors - _TC",
+ "taxes": [],
+ "items": [
+ {
+ "doctype": "Sales Invoice Item",
+ "item_code": frappe.get_doc("Item", {"item_name": "Loyal Item"}).name,
+ "qty": qty,
+ "rate": 10000,
+ "income_account": "Sales - _TC",
+ "cost_center": "Main - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ }
+ ],
+ }
+ )
+
def create_records():
# create a new loyalty Account
if not frappe.db.exists("Account", "Loyalty - _TC"):
- frappe.get_doc({
- "doctype": "Account",
- "account_name": "Loyalty",
- "parent_account": "Direct Expenses - _TC",
- "company": "_Test Company",
- "is_group": 0,
- "account_type": "Expense Account",
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Account",
+ "account_name": "Loyalty",
+ "parent_account": "Direct Expenses - _TC",
+ "company": "_Test Company",
+ "is_group": 0,
+ "account_type": "Expense Account",
+ }
+ ).insert()
# create a new loyalty program Single tier
- if not frappe.db.exists("Loyalty Program","Test Single Loyalty"):
- frappe.get_doc({
- "doctype": "Loyalty Program",
- "loyalty_program_name": "Test Single Loyalty",
- "auto_opt_in": 1,
- "from_date": today(),
- "loyalty_program_type": "Single Tier Program",
- "conversion_factor": 1,
- "expiry_duration": 10,
- "company": "_Test Company",
- "cost_center": "Main - _TC",
- "expense_account": "Loyalty - _TC",
- "collection_rules": [{
- 'tier_name': 'Silver',
- 'collection_factor': 1000,
- 'min_spent': 1000
- }]
- }).insert()
+ if not frappe.db.exists("Loyalty Program", "Test Single Loyalty"):
+ frappe.get_doc(
+ {
+ "doctype": "Loyalty Program",
+ "loyalty_program_name": "Test Single Loyalty",
+ "auto_opt_in": 1,
+ "from_date": today(),
+ "loyalty_program_type": "Single Tier Program",
+ "conversion_factor": 1,
+ "expiry_duration": 10,
+ "company": "_Test Company",
+ "cost_center": "Main - _TC",
+ "expense_account": "Loyalty - _TC",
+ "collection_rules": [{"tier_name": "Silver", "collection_factor": 1000, "min_spent": 1000}],
+ }
+ ).insert()
# create a new customer
- if not frappe.db.exists("Customer","Test Loyalty Customer"):
- frappe.get_doc({
- "customer_group": "_Test Customer Group",
- "customer_name": "Test Loyalty Customer",
- "customer_type": "Individual",
- "doctype": "Customer",
- "territory": "_Test Territory"
- }).insert()
+ if not frappe.db.exists("Customer", "Test Loyalty Customer"):
+ frappe.get_doc(
+ {
+ "customer_group": "_Test Customer Group",
+ "customer_name": "Test Loyalty Customer",
+ "customer_type": "Individual",
+ "doctype": "Customer",
+ "territory": "_Test Territory",
+ }
+ ).insert()
# create a new loyalty program Multiple tier
- if not frappe.db.exists("Loyalty Program","Test Multiple Loyalty"):
- frappe.get_doc({
- "doctype": "Loyalty Program",
- "loyalty_program_name": "Test Multiple Loyalty",
- "auto_opt_in": 1,
- "from_date": today(),
- "loyalty_program_type": "Multiple Tier Program",
- "conversion_factor": 1,
- "expiry_duration": 10,
- "company": "_Test Company",
- "cost_center": "Main - _TC",
- "expense_account": "Loyalty - _TC",
- "collection_rules": [
- {
- 'tier_name': 'Silver',
- 'collection_factor': 1000,
- 'min_spent': 10000
- },
- {
- 'tier_name': 'Gold',
- 'collection_factor': 1000,
- 'min_spent': 19000
- }
- ]
- }).insert()
+ if not frappe.db.exists("Loyalty Program", "Test Multiple Loyalty"):
+ frappe.get_doc(
+ {
+ "doctype": "Loyalty Program",
+ "loyalty_program_name": "Test Multiple Loyalty",
+ "auto_opt_in": 1,
+ "from_date": today(),
+ "loyalty_program_type": "Multiple Tier Program",
+ "conversion_factor": 1,
+ "expiry_duration": 10,
+ "company": "_Test Company",
+ "cost_center": "Main - _TC",
+ "expense_account": "Loyalty - _TC",
+ "collection_rules": [
+ {"tier_name": "Silver", "collection_factor": 1000, "min_spent": 10000},
+ {"tier_name": "Gold", "collection_factor": 1000, "min_spent": 19000},
+ ],
+ }
+ ).insert()
# create an item
if not frappe.db.exists("Item", "Loyal Item"):
- frappe.get_doc({
- "doctype": "Item",
- "item_code": "Loyal Item",
- "item_name": "Loyal Item",
- "item_group": "All Item Groups",
- "company": "_Test Company",
- "is_stock_item": 1,
- "opening_stock": 100,
- "valuation_rate": 10000,
- }).insert()
+ frappe.get_doc(
+ {
+ "doctype": "Item",
+ "item_code": "Loyal Item",
+ "item_name": "Loyal Item",
+ "item_group": "All Item Groups",
+ "company": "_Test Company",
+ "is_stock_item": 1,
+ "opening_stock": 100,
+ "valuation_rate": 10000,
+ }
+ ).insert()
# create item price
- if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}):
- frappe.get_doc({
- "doctype": "Item Price",
- "price_list": "Standard Selling",
- "item_code": "Loyal Item",
- "price_list_rate": 10000
- }).insert()
+ if not frappe.db.exists(
+ "Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}
+ ):
+ frappe.get_doc(
+ {
+ "doctype": "Item Price",
+ "price_list": "Standard Selling",
+ "item_code": "Loyal Item",
+ "price_list_rate": 10000,
+ }
+ ).insert()
diff --git a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
index f21d1b9baa9..d0373021a69 100644
--- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
+++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
@@ -19,23 +19,35 @@ class ModeofPayment(Document):
for entry in self.accounts:
accounts_list.append(entry.company)
- if len(accounts_list)!= len(set(accounts_list)):
+ if len(accounts_list) != len(set(accounts_list)):
frappe.throw(_("Same Company is entered more than once"))
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:
- frappe.throw(_("Account {0} does not match with Company {1} in Mode of Account: {2}")
- .format(entry.default_account, entry.company, self.name))
+ frappe.throw(
+ _("Account {0} does not match with Company {1} in Mode of Account: {2}").format(
+ entry.default_account, entry.company, self.name
+ )
+ )
def validate_pos_mode_of_payment(self):
if not self.enabled:
- pos_profiles = frappe.db.sql("""SELECT sip.parent FROM `tabSales Invoice Payment` sip
- WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""", (self.name))
+ pos_profiles = frappe.db.sql(
+ """SELECT sip.parent FROM `tabSales Invoice Payment` sip
+ WHERE sip.parenttype = 'POS Profile' and sip.mode_of_payment = %s""",
+ (self.name),
+ )
pos_profiles = list(map(lambda x: x[0], pos_profiles))
if pos_profiles:
- message = "POS Profile " + frappe.bold(", ".join(pos_profiles)) + " contains \
- Mode of Payment " + frappe.bold(str(self.name)) + ". Please remove them to disable this mode."
+ message = (
+ "POS Profile "
+ + frappe.bold(", ".join(pos_profiles))
+ + " contains \
+ Mode of Payment "
+ + frappe.bold(str(self.name))
+ + ". Please remove them to disable this mode."
+ )
frappe.throw(_(message), title="Not Allowed")
diff --git a/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py b/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py
index 2ff02a7c4dc..9733fb89e2f 100644
--- a/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py
+++ b/erpnext/accounts/doctype/mode_of_payment/test_mode_of_payment.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Mode of Payment')
+
class TestModeofPayment(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
index a8c5f68c110..1d19708eddf 100644
--- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
+++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
@@ -11,13 +11,25 @@ from frappe.utils import add_months, flt
class MonthlyDistribution(Document):
@frappe.whitelist()
def get_months(self):
- month_list = ['January','February','March','April','May','June','July','August','September',
- 'October','November','December']
- idx =1
+ month_list = [
+ "January",
+ "February",
+ "March",
+ "April",
+ "May",
+ "June",
+ "July",
+ "August",
+ "September",
+ "October",
+ "November",
+ "December",
+ ]
+ idx = 1
for m in month_list:
- mnth = self.append('percentages')
+ mnth = self.append("percentages")
mnth.month = m
- mnth.percentage_allocation = 100.0/12
+ mnth.percentage_allocation = 100.0 / 12
mnth.idx = idx
idx += 1
@@ -25,18 +37,15 @@ class MonthlyDistribution(Document):
total = sum(flt(d.percentage_allocation) for d in self.get("percentages"))
if flt(total, 2) != 100.0:
- frappe.throw(_("Percentage Allocation should be equal to 100%") + \
- " ({0}%)".format(str(flt(total, 2))))
+ frappe.throw(
+ _("Percentage Allocation should be equal to 100%") + " ({0}%)".format(str(flt(total, 2)))
+ )
+
def get_periodwise_distribution_data(distribution_id, period_list, periodicity):
- doc = frappe.get_doc('Monthly Distribution', distribution_id)
+ doc = frappe.get_doc("Monthly Distribution", distribution_id)
- months_to_add = {
- "Yearly": 12,
- "Half-Yearly": 6,
- "Quarterly": 3,
- "Monthly": 1
- }[periodicity]
+ months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity]
period_dict = {}
@@ -45,6 +54,7 @@ def get_periodwise_distribution_data(distribution_id, period_list, periodicity):
return period_dict
+
def get_percentage(doc, start_date, period):
percentage = 0
months = [start_date.strftime("%B").title()]
diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py
index 3e6575f2540..ba2cb671d40 100644
--- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py
+++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution_dashboard.py
@@ -1,22 +1,16 @@
-
from frappe import _
def get_data():
return {
- 'fieldname': 'monthly_distribution',
- 'non_standard_fieldnames': {
- 'Sales Person': 'distribution_id',
- 'Territory': 'distribution_id',
- 'Sales Partner': 'distribution_id',
+ "fieldname": "monthly_distribution",
+ "non_standard_fieldnames": {
+ "Sales Person": "distribution_id",
+ "Territory": "distribution_id",
+ "Sales Partner": "distribution_id",
},
- 'transactions': [
- {
- 'label': _('Target Details'),
- 'items': ['Sales Person', 'Territory', 'Sales Partner']
- },
- {
- 'items': ['Budget']
- }
- ]
+ "transactions": [
+ {"label": _("Target Details"), "items": ["Sales Person", "Territory", "Sales Partner"]},
+ {"items": ["Budget"]},
+ ],
}
diff --git a/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py
index 4a878b2aaf7..848b1f9de76 100644
--- a/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py
+++ b/erpnext/accounts/doctype/monthly_distribution/test_monthly_distribution.py
@@ -6,7 +6,8 @@ import unittest
import frappe
-test_records = frappe.get_test_records('Monthly Distribution')
+test_records = frappe.get_test_records("Monthly Distribution")
+
class TestMonthlyDistribution(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
index 9f22ab0d76c..7eb5c4234d1 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
@@ -49,7 +49,15 @@ frappe.ui.form.on('Opening Invoice Creation Tool', {
doc: frm.doc,
btn: $(btn_primary),
method: "make_invoices",
- freeze_message: __("Creating {0} Invoice", [frm.doc.invoice_type])
+ 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]));
+ }
+ }
});
});
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json
index bc9241802da..c367b360e1f 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.json
@@ -75,7 +75,7 @@
],
"hide_toolbar": 1,
"issingle": 1,
- "modified": "2019-07-25 14:57:33.187689",
+ "modified": "2022-01-04 16:25:06.053187",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool",
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index 222589180ee..0f0ab68dcb8 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -20,9 +20,9 @@ class OpeningInvoiceCreationTool(Document):
def onload(self):
"""Load the Opening Invoice summary"""
summary, max_count = self.get_opening_invoice_summary()
- self.set_onload('opening_invoices_summary', summary)
- self.set_onload('max_count', max_count)
- self.set_onload('temporary_opening_account', get_temporary_opening_account(self.company))
+ self.set_onload("opening_invoices_summary", summary)
+ self.set_onload("max_count", max_count)
+ self.set_onload("temporary_opening_account", get_temporary_opening_account(self.company))
def get_opening_invoice_summary(self):
def prepare_invoice_summary(doctype, invoices):
@@ -32,10 +32,7 @@ class OpeningInvoiceCreationTool(Document):
for invoice in invoices:
company = invoice.pop("company")
_summary = invoices_summary.get(company, {})
- _summary.update({
- "currency": company_wise_currency.get(company),
- doctype: invoice
- })
+ _summary.update({"currency": company_wise_currency.get(company), doctype: invoice})
invoices_summary.update({company: _summary})
if invoice.paid_amount:
@@ -44,17 +41,21 @@ class OpeningInvoiceCreationTool(Document):
outstanding_amount.append(invoice.outstanding_amount)
if paid_amount or outstanding_amount:
- max_count.update({
- doctype: {
- "max_paid": max(paid_amount) if paid_amount else 0.0,
- "max_due": max(outstanding_amount) if outstanding_amount else 0.0
+ max_count.update(
+ {
+ doctype: {
+ "max_paid": max(paid_amount) if paid_amount else 0.0,
+ "max_due": max(outstanding_amount) if outstanding_amount else 0.0,
+ }
}
- })
+ )
invoices_summary = {}
max_count = {}
fields = [
- "company", "count(name) as total_invoices", "sum(outstanding_amount) as outstanding_amount"
+ "company",
+ "count(name) as total_invoices",
+ "sum(outstanding_amount) as outstanding_amount",
]
companies = frappe.get_all("Company", fields=["name as company", "default_currency as currency"])
if not companies:
@@ -62,8 +63,9 @@ class OpeningInvoiceCreationTool(Document):
company_wise_currency = {row.company: row.currency for row in companies}
for doctype in ["Sales Invoice", "Purchase Invoice"]:
- invoices = frappe.get_all(doctype, filters=dict(is_opening="Yes", docstatus=1),
- fields=fields, group_by="company")
+ invoices = frappe.get_all(
+ doctype, filters=dict(is_opening="Yes", docstatus=1), fields=fields, group_by="company"
+ )
prepare_invoice_summary(doctype, invoices)
return invoices_summary, max_count
@@ -74,7 +76,9 @@ class OpeningInvoiceCreationTool(Document):
def set_missing_values(self, row):
row.qty = row.qty or 1.0
- row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(self.company)
+ row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(
+ self.company
+ )
row.party_type = "Customer" if self.invoice_type == "Sales" else "Supplier"
row.item_name = row.item_name or _("Opening Invoice Item")
row.posting_date = row.posting_date or nowdate()
@@ -85,7 +89,11 @@ class OpeningInvoiceCreationTool(Document):
if self.create_missing_party:
self.add_party(row.party_type, row.party)
else:
- frappe.throw(_("Row #{}: {} {} does not exist.").format(row.idx, frappe.bold(row.party_type), frappe.bold(row.party)))
+ frappe.throw(
+ _("Row #{}: {} {} does not exist.").format(
+ row.idx, frappe.bold(row.party_type), frappe.bold(row.party)
+ )
+ )
mandatory_error_msg = _("Row #{0}: {1} is required to create the Opening {2} Invoices")
for d in ("Party", "Outstanding Amount", "Temporary Opening Account"):
@@ -100,12 +108,22 @@ class OpeningInvoiceCreationTool(Document):
self.set_missing_values(row)
self.validate_mandatory_invoice_fields(row)
invoice = self.get_invoice_dict(row)
- company_details = frappe.get_cached_value('Company', self.company, ["default_currency", "default_letter_head"], as_dict=1) or {}
+ company_details = (
+ frappe.get_cached_value(
+ "Company", self.company, ["default_currency", "default_letter_head"], as_dict=1
+ )
+ or {}
+ )
+
+ default_currency = frappe.db.get_value(row.party_type, row.party, "default_currency")
+
if company_details:
- invoice.update({
- "currency": company_details.get("default_currency"),
- "letter_head": company_details.get("default_letter_head")
- })
+ invoice.update(
+ {
+ "currency": default_currency or company_details.get("default_currency"),
+ "letter_head": company_details.get("default_letter_head"),
+ }
+ )
invoices.append(invoice)
return invoices
@@ -127,46 +145,61 @@ class OpeningInvoiceCreationTool(Document):
def get_invoice_dict(self, row=None):
def get_item_dict():
- cost_center = row.get('cost_center') or frappe.get_cached_value('Company', self.company, "cost_center")
+ cost_center = row.get("cost_center") or frappe.get_cached_value(
+ "Company", self.company, "cost_center"
+ )
if not cost_center:
- frappe.throw(_("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company)))
+ frappe.throw(
+ _("Please set the Default Cost Center in {0} company.").format(frappe.bold(self.company))
+ )
- income_expense_account_field = "income_account" if row.party_type == "Customer" else "expense_account"
+ income_expense_account_field = (
+ "income_account" if row.party_type == "Customer" else "expense_account"
+ )
default_uom = frappe.db.get_single_value("Stock Settings", "stock_uom") or _("Nos")
rate = flt(row.outstanding_amount) / flt(row.qty)
- return frappe._dict({
- "uom": default_uom,
- "rate": rate or 0.0,
- "qty": row.qty,
- "conversion_factor": 1.0,
- "item_name": row.item_name or "Opening Invoice Item",
- "description": row.item_name or "Opening Invoice Item",
- income_expense_account_field: row.temporary_opening_account,
- "cost_center": cost_center
- })
+ item_dict = frappe._dict(
+ {
+ "uom": default_uom,
+ "rate": rate or 0.0,
+ "qty": row.qty,
+ "conversion_factor": 1.0,
+ "item_name": row.item_name or "Opening Invoice Item",
+ "description": row.item_name or "Opening Invoice Item",
+ income_expense_account_field: row.temporary_opening_account,
+ "cost_center": cost_center,
+ }
+ )
+
+ for dimension in get_accounting_dimensions():
+ item_dict.update({dimension: row.get(dimension)})
+
+ return item_dict
item = get_item_dict()
- invoice = frappe._dict({
- "items": [item],
- "is_opening": "Yes",
- "set_posting_time": 1,
- "company": self.company,
- "cost_center": self.cost_center,
- "due_date": row.due_date,
- "posting_date": row.posting_date,
- frappe.scrub(row.party_type): row.party,
- "is_pos": 0,
- "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
- "update_stock": 0
- })
+ invoice = frappe._dict(
+ {
+ "items": [item],
+ "is_opening": "Yes",
+ "set_posting_time": 1,
+ "company": self.company,
+ "cost_center": self.cost_center,
+ "due_date": row.due_date,
+ "posting_date": row.posting_date,
+ frappe.scrub(row.party_type): row.party,
+ "is_pos": 0,
+ "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
+ "update_stock": 0, # important: https://github.com/frappe/erpnext/pull/23559
+ "invoice_number": row.invoice_number,
+ "disable_rounded_total": 1,
+ }
+ )
accounting_dimension = get_accounting_dimensions()
for dimension in accounting_dimension:
- invoice.update({
- dimension: item.get(dimension)
- })
+ invoice.update({dimension: self.get(dimension) or item.get(dimension)})
return invoice
@@ -192,53 +225,65 @@ class OpeningInvoiceCreationTool(Document):
event="opening_invoice_creation",
job_name=self.name,
invoices=invoices,
- now=frappe.conf.developer_mode or frappe.flags.in_test
+ now=frappe.conf.developer_mode or frappe.flags.in_test,
)
+
def start_import(invoices):
errors = 0
names = []
for idx, d in enumerate(invoices):
try:
+ invoice_number = None
+ if d.invoice_number:
+ invoice_number = d.invoice_number
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
doc.flags.ignore_mandatory = True
- doc.insert()
+ doc.insert(set_name=invoice_number)
doc.submit()
frappe.db.commit()
names.append(doc.name)
except Exception:
errors += 1
frappe.db.rollback()
- message = "\n".join(["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()])
+ message = "\n".join(
+ ["Data:", dumps(d, default=str, indent=4), "--" * 50, "\nException:", traceback.format_exc()]
+ )
frappe.log_error(title="Error while creating Opening Invoice", message=message)
frappe.db.commit()
if errors:
- frappe.msgprint(_("You had {} errors while creating opening invoices. Check {} for more details")
- .format(errors, "Error Log"), indicator="red", title=_("Error Occured"))
+ frappe.msgprint(
+ _("You had {} errors while creating opening invoices. Check {} for more details").format(
+ errors, "Error Log"
+ ),
+ indicator="red",
+ title=_("Error Occured"),
+ )
return names
+
def publish(index, total, doctype):
- if total < 5: return
+ if total < 5:
+ return
frappe.publish_realtime(
"opening_invoice_creation_progress",
dict(
title=_("Opening Invoice Creation In Progress"),
- message=_('Creating {} out of {} {}').format(index + 1, total, doctype),
+ message=_("Creating {} out of {} {}").format(index + 1, total, doctype),
user=frappe.session.user,
- count=index+1,
- total=total
- ))
+ count=index + 1,
+ total=total,
+ ),
+ )
+
@frappe.whitelist()
def get_temporary_opening_account(company=None):
if not company:
return
- accounts = frappe.get_all("Account", filters={
- 'company': company,
- 'account_type': 'Temporary'
- })
+ accounts = frappe.get_all("Account", filters={"company": company, "account_type": "Temporary"})
if not accounts:
frappe.throw(_("Please add a Temporary Opening account in Chart of Accounts"))
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index c795e83c56b..1e22c64c8f2 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -1,51 +1,64 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-import unittest
-
import frappe
-from frappe.cache_manager import clear_doctype_cache
-from frappe.custom.doctype.property_setter.property_setter import make_property_setter
+from frappe.tests.utils import FrappeTestCase
+from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import (
+ create_dimension,
+ disable_dimension,
+)
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import (
get_temporary_opening_account,
)
-test_dependencies = ["Customer", "Supplier"]
+test_dependencies = ["Customer", "Supplier", "Accounting Dimension"]
-class TestOpeningInvoiceCreationTool(unittest.TestCase):
- def setUp(self):
+
+class TestOpeningInvoiceCreationTool(FrappeTestCase):
+ @classmethod
+ def setUpClass(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
make_company()
+ create_dimension()
+ return super().setUpClass()
- def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
+ def make_invoices(
+ self,
+ invoice_type="Sales",
+ company=None,
+ party_1=None,
+ party_2=None,
+ invoice_number=None,
+ department=None,
+ ):
doc = frappe.get_single("Opening Invoice Creation Tool")
- args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
- party_1=party_1, party_2=party_2)
+ args = get_opening_invoice_creation_dict(
+ invoice_type=invoice_type,
+ company=company,
+ party_1=party_1,
+ party_2=party_2,
+ invoice_number=invoice_number,
+ department=department,
+ )
doc.update(args)
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
- property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
- try:
- invoices = self.make_invoices(company="_Test Opening Invoice Company")
+ invoices = self.make_invoices(company="_Test Opening Invoice Company")
- self.assertEqual(len(invoices), 2)
- expected_value = {
- "keys": ["customer", "outstanding_amount", "status"],
- 0: ["_Test Customer", 300, "Overdue"],
- 1: ["_Test Customer 1", 250, "Overdue"],
- }
- self.check_expected_values(invoices, expected_value)
+ self.assertEqual(len(invoices), 2)
+ expected_value = {
+ "keys": ["customer", "outstanding_amount", "status"],
+ 0: ["_Test Customer", 300, "Overdue"],
+ 1: ["_Test Customer 1", 250, "Overdue"],
+ }
+ self.check_expected_values(invoices, expected_value)
- si = frappe.get_doc("Sales Invoice", invoices[0])
+ si = frappe.get_doc("Sales Invoice", invoices[0])
- # Check if update stock is not enabled
- self.assertEqual(si.update_stock, 0)
-
- finally:
- property_setter.delete()
- clear_doctype_cache("Sales Invoice")
+ # Check if update stock is not enabled
+ self.assertEqual(si.update_stock, 0)
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
@@ -70,15 +83,30 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
company = "_Test Opening Invoice Company"
party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
- old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
+ old_default_receivable_account = frappe.db.get_value(
+ "Company", company, "default_receivable_account"
+ )
frappe.db.set_value("Company", company, "default_receivable_account", "")
if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
- cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company",
- "is_group": 1, "company": "_Test Opening Invoice Company"})
+ cc = frappe.get_doc(
+ {
+ "doctype": "Cost Center",
+ "cost_center_name": "_Test Opening Invoice Company",
+ "is_group": 1,
+ "company": "_Test Opening Invoice Company",
+ }
+ )
cc.insert(ignore_mandatory=True)
- cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0,
- "company": "_Test Opening Invoice Company", "parent_cost_center": cc.name})
+ cc2 = frappe.get_doc(
+ {
+ "doctype": "Cost Center",
+ "cost_center_name": "Main",
+ "is_group": 0,
+ "company": "_Test Opening Invoice Company",
+ "parent_cost_center": cc.name,
+ }
+ )
cc2.insert()
frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
@@ -86,44 +114,86 @@ class TestOpeningInvoiceCreationTool(unittest.TestCase):
self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
# Check if missing debit account error raised
- error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]})
+ error_log = frappe.db.exists(
+ "Error Log",
+ {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]},
+ )
self.assertTrue(error_log)
# teardown
- frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
+ frappe.db.set_value(
+ "Company", company, "default_receivable_account", old_default_receivable_account
+ )
+
+ def test_renaming_of_invoice_using_invoice_number_field(self):
+ company = "_Test Opening Invoice Company"
+ party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
+ self.make_invoices(
+ company=company, party_1=party_1, party_2=party_2, invoice_number="TEST-NEW-INV-11"
+ )
+
+ sales_inv1 = frappe.get_all("Sales Invoice", filters={"customer": "Customer A"})[0].get("name")
+ sales_inv2 = frappe.get_all("Sales Invoice", filters={"customer": "Customer B"})[0].get("name")
+ self.assertEqual(sales_inv1, "TEST-NEW-INV-11")
+
+ # teardown
+ for inv in [sales_inv1, sales_inv2]:
+ doc = frappe.get_doc("Sales Invoice", inv)
+ doc.cancel()
+
+ def test_opening_invoice_with_accounting_dimension(self):
+ invoices = self.make_invoices(
+ invoice_type="Sales", company="_Test Opening Invoice Company", department="Sales - _TOIC"
+ )
+
+ expected_value = {
+ "keys": ["customer", "outstanding_amount", "status", "department"],
+ 0: ["_Test Customer", 300, "Overdue", "Sales - _TOIC"],
+ 1: ["_Test Customer 1", 250, "Overdue", "Sales - _TOIC"],
+ }
+ self.check_expected_values(invoices, expected_value, invoice_type="Sales")
+
+ def tearDown(self):
+ disable_dimension()
+
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
- invoice_dict = frappe._dict({
- "company": company,
- "invoice_type": args.get("invoice_type", "Sales"),
- "invoices": [
- {
- "qty": 1.0,
- "outstanding_amount": 300,
- "party": args.get("party_1") or "_Test {0}".format(party),
- "item_name": "Opening Item",
- "due_date": "2016-09-10",
- "posting_date": "2016-09-05",
- "temporary_opening_account": get_temporary_opening_account(company)
- },
- {
- "qty": 2.0,
- "outstanding_amount": 250,
- "party": args.get("party_2") or "_Test {0} 1".format(party),
- "item_name": "Opening Item",
- "due_date": "2016-09-10",
- "posting_date": "2016-09-05",
- "temporary_opening_account": get_temporary_opening_account(company)
- }
- ]
- })
+ invoice_dict = frappe._dict(
+ {
+ "company": company,
+ "invoice_type": args.get("invoice_type", "Sales"),
+ "invoices": [
+ {
+ "qty": 1.0,
+ "outstanding_amount": 300,
+ "party": args.get("party_1") or "_Test {0}".format(party),
+ "item_name": "Opening Item",
+ "due_date": "2016-09-10",
+ "posting_date": "2016-09-05",
+ "temporary_opening_account": get_temporary_opening_account(company),
+ "invoice_number": args.get("invoice_number"),
+ },
+ {
+ "qty": 2.0,
+ "outstanding_amount": 250,
+ "party": args.get("party_2") or "_Test {0} 1".format(party),
+ "item_name": "Opening Item",
+ "due_date": "2016-09-10",
+ "posting_date": "2016-09-05",
+ "temporary_opening_account": get_temporary_opening_account(company),
+ "invoice_number": None,
+ },
+ ],
+ }
+ )
invoice_dict.update(args)
return invoice_dict
+
def make_company():
if frappe.db.exists("Company", "_Test Opening Invoice Company"):
return frappe.get_doc("Company", "_Test Opening Invoice Company")
@@ -132,19 +202,22 @@ def make_company():
company.company_name = "_Test Opening Invoice Company"
company.abbr = "_TOIC"
company.default_currency = "INR"
- company.country = "India"
+ company.country = "Pakistan"
company.insert()
return company
+
def make_customer(customer=None):
customer_name = customer or "Opening Customer"
- customer = frappe.get_doc({
- "doctype": "Customer",
- "customer_name": customer_name,
- "customer_group": "All Customer Groups",
- "customer_type": "Company",
- "territory": "All Territories"
- })
+ customer = frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_name": customer_name,
+ "customer_group": "All Customer Groups",
+ "customer_type": "Company",
+ "territory": "All Territories",
+ }
+ )
if not frappe.db.exists("Customer", customer_name):
customer.insert(ignore_permissions=True)
return customer.name
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json
index 4ce8cb95b18..8149d594cb4 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool_item/opening_invoice_creation_tool_item.json
@@ -1,9 +1,11 @@
{
+ "actions": [],
"creation": "2017-08-29 04:26:36.159247",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "invoice_number",
"party_type",
"party",
"temporary_opening_account",
@@ -103,10 +105,18 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "description": "Reference number of the invoice from the previous system",
+ "fieldname": "invoice_number",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Invoice Number"
}
],
"istable": 1,
- "modified": "2019-07-25 15:00:00.460695",
+ "links": [],
+ "modified": "2022-01-04 18:40:15.927675",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Opening Invoice Creation Tool Item",
@@ -116,4 +126,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/accounts/doctype/party_account/party_account.json b/erpnext/accounts/doctype/party_account/party_account.json
index c9f15a6a470..69330577ab3 100644
--- a/erpnext/accounts/doctype/party_account/party_account.json
+++ b/erpnext/accounts/doctype/party_account/party_account.json
@@ -3,6 +3,7 @@
"creation": "2014-08-29 16:02:39.740505",
"doctype": "DocType",
"editable_grid": 1,
+ "engine": "InnoDB",
"field_order": [
"company",
"account"
@@ -11,6 +12,7 @@
{
"fieldname": "company",
"fieldtype": "Link",
+ "ignore_user_permissions": 1,
"in_list_view": 1,
"label": "Company",
"options": "Company",
@@ -27,7 +29,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-04-07 18:13:08.833822",
+ "modified": "2022-04-04 12:31:02.994197",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Party Account",
@@ -35,5 +37,6 @@
"permissions": [],
"quick_entry": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "states": []
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/party_link/party_link.py b/erpnext/accounts/doctype/party_link/party_link.py
index 249bf419c55..d766aad9176 100644
--- a/erpnext/accounts/doctype/party_link/party_link.py
+++ b/erpnext/accounts/doctype/party_link/party_link.py
@@ -8,32 +8,43 @@ from frappe.model.document import Document
class PartyLink(Document):
def validate(self):
- if self.primary_role not in ['Customer', 'Supplier']:
- frappe.throw(_("Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."),
- title=_("Invalid Primary Role"))
- existing_party_link = frappe.get_all('Party Link', {
- 'primary_party': self.secondary_party
- }, pluck="primary_role")
+ if self.primary_role not in ["Customer", "Supplier"]:
+ frappe.throw(
+ _(
+ "Allowed primary roles are 'Customer' and 'Supplier'. Please select one of these roles only."
+ ),
+ title=_("Invalid Primary Role"),
+ )
+
+ existing_party_link = frappe.get_all(
+ "Party Link", {"primary_party": self.secondary_party}, pluck="primary_role"
+ )
if existing_party_link:
- frappe.throw(_('{} {} is already linked with another {}')
- .format(self.secondary_role, self.secondary_party, existing_party_link[0]))
- existing_party_link = frappe.get_all('Party Link', {
- 'secondary_party': self.primary_party
- }, pluck="primary_role")
+ frappe.throw(
+ _("{} {} is already linked with another {}").format(
+ self.secondary_role, self.secondary_party, existing_party_link[0]
+ )
+ )
+
+ existing_party_link = frappe.get_all(
+ "Party Link", {"secondary_party": self.primary_party}, pluck="primary_role"
+ )
if existing_party_link:
- frappe.throw(_('{} {} is already linked with another {}')
- .format(self.primary_role, self.primary_party, existing_party_link[0]))
+ frappe.throw(
+ _("{} {} is already linked with another {}").format(
+ self.primary_role, self.primary_party, existing_party_link[0]
+ )
+ )
@frappe.whitelist()
def create_party_link(primary_role, primary_party, secondary_party):
- party_link = frappe.new_doc('Party Link')
+ party_link = frappe.new_doc("Party Link")
party_link.primary_role = primary_role
party_link.primary_party = primary_party
- party_link.secondary_role = 'Customer' if primary_role == 'Supplier' else 'Supplier'
+ party_link.secondary_role = "Customer" if primary_role == "Supplier" else "Supplier"
party_link.secondary_party = secondary_party
party_link.save(ignore_permissions=True)
return party_link
-
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index 727ef55b3c7..6be0920d2a8 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -196,8 +196,14 @@ frappe.ui.form.on('Payment Entry', {
frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency));
frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency);
- frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
- (frm.doc.paid_from_account_currency != company_currency));
+
+ if (frm.doc.payment_type == "Pay") {
+ frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
+ (frm.doc.paid_to_account_currency != company_currency));
+ } else {
+ frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges &&
+ (frm.doc.paid_from_account_currency != company_currency));
+ }
frm.toggle_display("base_received_amount", (
frm.doc.paid_to_account_currency != company_currency
@@ -220,10 +226,7 @@ frappe.ui.form.on('Payment Entry', {
(frm.doc.total_allocated_amount > party_amount)));
frm.toggle_display("set_exchange_gain_loss",
- (frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount &&
- ((frm.doc.paid_from_account_currency != company_currency ||
- frm.doc.paid_to_account_currency != company_currency) &&
- frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)));
+ frm.doc.paid_amount && frm.doc.received_amount && frm.doc.difference_amount);
frm.refresh_fields();
},
@@ -232,7 +235,8 @@ frappe.ui.form.on('Payment Entry', {
var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: "";
frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount",
- "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency);
+ "difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax",
+ "base_total_taxes_and_charges"], company_currency);
frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency);
frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency);
@@ -341,6 +345,8 @@ frappe.ui.form.on('Payment Entry', {
}
frm.set_party_account_based_on_party = true;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+
return frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details",
args: {
@@ -374,7 +380,11 @@ frappe.ui.form.on('Payment Entry', {
if (r.message.bank_account) {
frm.set_value("bank_account", r.message.bank_account);
}
- }
+ },
+ () => frm.events.set_current_exchange_rate(frm, "source_exchange_rate",
+ frm.doc.paid_from_account_currency, company_currency),
+ () => frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
+ frm.doc.paid_to_account_currency, company_currency)
]);
}
}
@@ -478,14 +488,14 @@ frappe.ui.form.on('Payment Entry', {
},
paid_from_account_currency: function(frm) {
- if(!frm.doc.paid_from_account_currency) return;
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ if(!frm.doc.paid_from_account_currency || !frm.doc.company) return;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.paid_from_account_currency == company_currency) {
frm.set_value("source_exchange_rate", 1);
} else if (frm.doc.paid_from){
if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) {
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frappe.call({
method: "erpnext.setup.utils.get_exchange_rate",
args: {
@@ -505,8 +515,8 @@ frappe.ui.form.on('Payment Entry', {
},
paid_to_account_currency: function(frm) {
- if(!frm.doc.paid_to_account_currency) return;
- var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+ if(!frm.doc.paid_to_account_currency || !frm.doc.company) return;
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
frm.events.set_current_exchange_rate(frm, "target_exchange_rate",
frm.doc.paid_to_account_currency, company_currency);
@@ -1101,7 +1111,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;
});
},
@@ -1192,7 +1202,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))
});
},
@@ -1224,6 +1234,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
@@ -1244,8 +1255,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) {
@@ -1254,9 +1265,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');
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index c8d1db91f54..3fc1adff2d3 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -66,7 +66,9 @@
"tax_withholding_category",
"section_break_56",
"taxes",
+ "section_break_60",
"base_total_taxes_and_charges",
+ "column_break_61",
"total_taxes_and_charges",
"deductions_or_loss_section",
"deductions",
@@ -715,12 +717,21 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Paid To Account Type"
+ },
+ {
+ "fieldname": "column_break_61",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_60",
+ "fieldtype": "Section Break",
+ "hide_border": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-11-24 18:58:24.919764",
+ "modified": "2022-02-23 20:08:39.559814",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -763,6 +774,7 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"title_field": "title",
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 61fa194aefd..904d7af757f 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -3,6 +3,7 @@
import json
+from functools import reduce
import frappe
from frappe import ValidationError, _, scrub, throw
@@ -29,7 +30,10 @@ from erpnext.controllers.accounts_controller import (
get_supplier_block_status,
validate_taxes_and_charges,
)
-from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
+from erpnext.hr.doctype.expense_claim.expense_claim import (
+ get_outstanding_amount_for_claim,
+ update_reimbursed_amount,
+)
from erpnext.setup.utils import get_exchange_rate
@@ -96,7 +100,7 @@ class PaymentEntry(AccountsController):
self.set_status()
def on_cancel(self):
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry")
self.make_gl_entries(cancel=1)
self.update_expense_claim()
self.update_outstanding_amounts()
@@ -109,6 +113,7 @@ class PaymentEntry(AccountsController):
def set_payment_req_status(self):
from erpnext.accounts.doctype.payment_request.payment_request import update_payment_req_status
+
update_payment_req_status(self, None)
def update_outstanding_amounts(self):
@@ -118,8 +123,11 @@ class PaymentEntry(AccountsController):
reference_names = []
for d in self.get("references"):
if (d.reference_doctype, d.reference_name, d.payment_term) in reference_names:
- frappe.throw(_("Row #{0}: Duplicate entry in References {1} {2}")
- .format(d.idx, d.reference_doctype, d.reference_name))
+ frappe.throw(
+ _("Row #{0}: Duplicate entry in References {1} {2}").format(
+ d.idx, d.reference_doctype, d.reference_name
+ )
+ )
reference_names.append((d.reference_doctype, d.reference_name, d.payment_term))
def set_bank_account_data(self):
@@ -135,20 +143,27 @@ class PaymentEntry(AccountsController):
self.set(field, bank_data.account)
def validate_payment_type_with_outstanding(self):
- total_outstanding = sum(d.allocated_amount for d in self.get('references'))
- if total_outstanding < 0 and self.party_type == 'Customer' and self.payment_type == 'Receive':
- frappe.throw(_("Cannot receive from customer against negative outstanding"), title=_("Incorrect Payment Type"))
+ total_outstanding = sum(d.allocated_amount for d in self.get("references"))
+ if total_outstanding < 0 and self.party_type == "Customer" and self.payment_type == "Receive":
+ frappe.throw(
+ _("Cannot receive from customer against negative outstanding"),
+ title=_("Incorrect Payment Type"),
+ )
def validate_allocated_amount(self):
for d in self.get("references"):
- if (flt(d.allocated_amount))> 0:
+ if (flt(d.allocated_amount)) > 0:
if flt(d.allocated_amount) > flt(d.outstanding_amount):
- frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
+ frappe.throw(
+ _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
+ )
# Check for negative outstanding invoices as well
if flt(d.allocated_amount) < 0:
if flt(d.allocated_amount) < flt(d.outstanding_amount):
- frappe.throw(_("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx))
+ frappe.throw(
+ _("Row #{0}: Allocated Amount cannot be greater than outstanding amount.").format(d.idx)
+ )
def delink_advance_entry_references(self):
for reference in self.references:
@@ -158,9 +173,14 @@ class PaymentEntry(AccountsController):
def set_missing_values(self):
if self.payment_type == "Internal Transfer":
- for field in ("party", "party_balance", "total_allocated_amount",
- "base_total_allocated_amount", "unallocated_amount"):
- self.set(field, None)
+ for field in (
+ "party",
+ "party_balance",
+ "total_allocated_amount",
+ "base_total_allocated_amount",
+ "unallocated_amount",
+ ):
+ self.set(field, None)
self.references = []
else:
if not self.party_type:
@@ -169,13 +189,16 @@ class PaymentEntry(AccountsController):
if not self.party:
frappe.throw(_("Party is mandatory"))
- _party_name = "title" if self.party_type in ("Student", "Shareholder") else self.party_type.lower() + "_name"
+ _party_name = (
+ "title" if self.party_type in ("Student", "Shareholder") else self.party_type.lower() + "_name"
+ )
self.party_name = frappe.db.get_value(self.party_type, self.party, _party_name)
if self.party:
if not self.party_balance:
- self.party_balance = get_balance_on(party_type=self.party_type,
- party=self.party, date=self.posting_date, company=self.company)
+ self.party_balance = get_balance_on(
+ party_type=self.party_type, party=self.party, date=self.posting_date, company=self.company
+ )
if not self.party_account:
party_account = get_party_account(self.party_type, self.party, self.company)
@@ -192,16 +215,20 @@ class PaymentEntry(AccountsController):
self.paid_to_account_currency = acc.account_currency
self.paid_to_account_balance = acc.account_balance
- self.party_account_currency = self.paid_from_account_currency \
- if self.payment_type=="Receive" else self.paid_to_account_currency
+ self.party_account_currency = (
+ self.paid_from_account_currency
+ if self.payment_type == "Receive"
+ else self.paid_to_account_currency
+ )
self.set_missing_ref_details()
def set_missing_ref_details(self, force=False):
for d in self.get("references"):
if d.allocated_amount:
- ref_details = get_reference_details(d.reference_doctype,
- d.reference_name, self.party_account_currency)
+ ref_details = get_reference_details(
+ d.reference_doctype, d.reference_name, self.party_account_currency
+ )
for field, value in iteritems(ref_details):
if d.exchange_gain_loss:
@@ -211,7 +238,7 @@ class PaymentEntry(AccountsController):
# refer -> `update_reference_in_payment_entry()` in utils.py
continue
- if field == 'exchange_rate' or not d.get(field) or force:
+ if field == "exchange_rate" or not d.get(field) or force:
d.db_set(field, value)
def validate_payment_type(self):
@@ -224,8 +251,9 @@ class PaymentEntry(AccountsController):
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)])
+ 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"):
@@ -253,8 +281,9 @@ class PaymentEntry(AccountsController):
self.source_exchange_rate = ref_doc.get("exchange_rate")
if not self.source_exchange_rate:
- self.source_exchange_rate = get_exchange_rate(self.paid_from_account_currency,
- self.company_currency, self.posting_date)
+ self.source_exchange_rate = get_exchange_rate(
+ self.paid_from_account_currency, self.company_currency, self.posting_date
+ )
def set_target_exchange_rate(self, ref_doc=None):
if self.paid_from_account_currency == self.paid_to_account_currency:
@@ -265,8 +294,9 @@ class PaymentEntry(AccountsController):
self.target_exchange_rate = ref_doc.get("exchange_rate")
if not self.target_exchange_rate:
- self.target_exchange_rate = get_exchange_rate(self.paid_to_account_currency,
- self.company_currency, self.posting_date)
+ self.target_exchange_rate = get_exchange_rate(
+ self.paid_to_account_currency, self.company_currency, self.posting_date
+ )
def validate_mandatory(self):
for field in ("paid_amount", "received_amount", "source_exchange_rate", "target_exchange_rate"):
@@ -275,7 +305,7 @@ class PaymentEntry(AccountsController):
def validate_reference_documents(self):
if self.party_type == "Student":
- valid_reference_doctypes = ("Fees")
+ valid_reference_doctypes = ("Fees", "Journal Entry")
elif self.party_type == "Customer":
valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning")
elif self.party_type == "Supplier":
@@ -283,16 +313,17 @@ class PaymentEntry(AccountsController):
elif self.party_type == "Employee":
valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity")
elif self.party_type == "Shareholder":
- valid_reference_doctypes = ("Journal Entry")
+ valid_reference_doctypes = "Journal Entry"
elif self.party_type == "Donor":
- valid_reference_doctypes = ("Donation")
+ valid_reference_doctypes = "Donation"
for d in self.get("references"):
if not d.allocated_amount:
continue
if d.reference_doctype not in valid_reference_doctypes:
- frappe.throw(_("Reference Doctype must be one of {0}")
- .format(comma_or(valid_reference_doctypes)))
+ frappe.throw(
+ _("Reference Doctype must be one of {0}").format(comma_or(valid_reference_doctypes))
+ )
elif d.reference_name:
if not frappe.db.exists(d.reference_doctype, d.reference_name):
@@ -302,28 +333,41 @@ class PaymentEntry(AccountsController):
if d.reference_doctype != "Journal Entry":
if self.party != ref_doc.get(scrub(self.party_type)):
- frappe.throw(_("{0} {1} is not associated with {2} {3}")
- .format(d.reference_doctype, d.reference_name, self.party_type, self.party))
+ frappe.throw(
+ _("{0} {1} is not associated with {2} {3}").format(
+ d.reference_doctype, d.reference_name, self.party_type, self.party
+ )
+ )
else:
self.validate_journal_entry()
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Expense Claim", "Fees"):
if self.party_type == "Customer":
- ref_party_account = get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to
+ ref_party_account = (
+ get_party_account_based_on_invoice_discounting(d.reference_name) or ref_doc.debit_to
+ )
elif self.party_type == "Student":
ref_party_account = ref_doc.receivable_account
- elif self.party_type=="Supplier":
+ elif self.party_type == "Supplier":
ref_party_account = ref_doc.credit_to
- elif self.party_type=="Employee":
+ elif self.party_type == "Employee":
ref_party_account = ref_doc.payable_account
if ref_party_account != self.party_account:
- frappe.throw(_("{0} {1} is associated with {2}, but Party Account is {3}")
- .format(d.reference_doctype, d.reference_name, ref_party_account, self.party_account))
+ frappe.throw(
+ _("{0} {1} is associated with {2}, but Party Account is {3}").format(
+ d.reference_doctype, d.reference_name, ref_party_account, self.party_account
+ )
+ )
+
+ if ref_doc.doctype == "Purchase Invoice" and ref_doc.get("on_hold"):
+ frappe.throw(
+ _("{0} {1} is on hold").format(d.reference_doctype, d.reference_name),
+ title=_("Invalid Invoice"),
+ )
if ref_doc.docstatus != 1:
- frappe.throw(_("{0} {1} must be submitted")
- .format(d.reference_doctype, d.reference_name))
+ frappe.throw(_("{0} {1} must be submitted").format(d.reference_doctype, d.reference_name))
def validate_paid_invoices(self):
no_oustanding_refs = {}
@@ -333,28 +377,45 @@ class PaymentEntry(AccountsController):
continue
if d.reference_doctype in ("Sales Invoice", "Purchase Invoice", "Fees"):
- outstanding_amount, is_return = frappe.get_cached_value(d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"])
+ outstanding_amount, is_return = frappe.get_cached_value(
+ d.reference_doctype, d.reference_name, ["outstanding_amount", "is_return"]
+ )
if outstanding_amount <= 0 and not is_return:
no_oustanding_refs.setdefault(d.reference_doctype, []).append(d)
for k, v in no_oustanding_refs.items():
frappe.msgprint(
- _("{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry.")
- .format(_(k), frappe.bold(", ".join(d.reference_name for d in v)), frappe.bold(_("negative outstanding amount")))
- + "
" + _("If this is undesirable please cancel the corresponding Payment Entry."),
- title=_("Warning"), indicator="orange")
+ _(
+ "{} - {} now have {} as they had no outstanding amount left before submitting the Payment Entry."
+ ).format(
+ _(k),
+ frappe.bold(", ".join(d.reference_name for d in v)),
+ frappe.bold(_("negative outstanding amount")),
+ )
+ + "
"
+ + _("If this is undesirable please cancel the corresponding Payment Entry."),
+ title=_("Warning"),
+ indicator="orange",
+ )
def validate_journal_entry(self):
for d in self.get("references"):
if d.allocated_amount and d.reference_doctype == "Journal Entry":
- je_accounts = frappe.db.sql("""select debit, credit from `tabJournal Entry Account`
+ je_accounts = frappe.db.sql(
+ """select debit, credit from `tabJournal Entry Account`
where account = %s and party=%s and docstatus = 1 and parent = %s
and (reference_type is null or reference_type in ("", "Sales Order", "Purchase Order"))
- """, (self.party_account, self.party, d.reference_name), as_dict=True)
+ """,
+ (self.party_account, self.party, d.reference_name),
+ as_dict=True,
+ )
if not je_accounts:
- frappe.throw(_("Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher")
- .format(d.idx, d.reference_name, self.party_account))
+ frappe.throw(
+ _(
+ "Row #{0}: Journal Entry {1} does not have account {2} or already matched against another voucher"
+ ).format(d.idx, d.reference_name, self.party_account)
+ )
else:
dr_or_cr = "debit" if self.payment_type == "Receive" else "credit"
valid = False
@@ -362,14 +423,17 @@ class PaymentEntry(AccountsController):
if flt(jvd[dr_or_cr]) > 0:
valid = True
if not valid:
- frappe.throw(_("Against Journal Entry {0} does not have any unmatched {1} entry")
- .format(d.reference_name, dr_or_cr))
+ frappe.throw(
+ _("Against Journal Entry {0} does not have any unmatched {1} entry").format(
+ d.reference_name, dr_or_cr
+ )
+ )
def update_payment_schedule(self, cancel=0):
invoice_payment_amount_map = {}
invoice_paid_amount_map = {}
- for ref in self.get('references'):
+ for ref in self.get("references"):
if ref.payment_term and ref.reference_name:
key = (ref.payment_term, ref.reference_name)
invoice_payment_amount_map.setdefault(key, 0.0)
@@ -377,58 +441,68 @@ class PaymentEntry(AccountsController):
if not invoice_paid_amount_map.get(key):
payment_schedule = frappe.get_all(
- 'Payment Schedule',
- filters={'parent': ref.reference_name},
- fields=['paid_amount', 'payment_amount', 'payment_term', 'discount', 'outstanding']
+ "Payment Schedule",
+ filters={"parent": ref.reference_name},
+ fields=["paid_amount", "payment_amount", "payment_term", "discount", "outstanding"],
)
for term in payment_schedule:
invoice_key = (term.payment_term, ref.reference_name)
invoice_paid_amount_map.setdefault(invoice_key, {})
- invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
- invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
+ invoice_paid_amount_map[invoice_key]["outstanding"] = term.outstanding
+ invoice_paid_amount_map[invoice_key]["discounted_amt"] = ref.total_amount * (
+ term.discount / 100
+ )
for idx, (key, allocated_amount) in enumerate(iteritems(invoice_payment_amount_map), 1):
if not invoice_paid_amount_map.get(key):
- frappe.throw(_('Payment term {0} not used in {1}').format(key[0], key[1]))
+ frappe.throw(_("Payment term {0} not used in {1}").format(key[0], key[1]))
- outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
- discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get('discounted_amt'))
+ outstanding = flt(invoice_paid_amount_map.get(key, {}).get("outstanding"))
+ discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get("discounted_amt"))
if cancel:
- frappe.db.sql("""
+ frappe.db.sql(
+ """
UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` - %s,
discounted_amount = `discounted_amount` - %s,
outstanding = `outstanding` + %s
WHERE parent = %s and payment_term = %s""",
- (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
+ (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
+ )
else:
if allocated_amount > outstanding:
- frappe.throw(_('Row #{0}: Cannot allocate more than {1} against payment term {2}').format(idx, outstanding, key[0]))
+ frappe.throw(
+ _("Row #{0}: Cannot allocate more than {1} against payment term {2}").format(
+ idx, outstanding, key[0]
+ )
+ )
if allocated_amount and outstanding:
- frappe.db.sql("""
+ frappe.db.sql(
+ """
UPDATE `tabPayment Schedule`
SET
paid_amount = `paid_amount` + %s,
discounted_amount = `discounted_amount` + %s,
outstanding = `outstanding` - %s
WHERE parent = %s and payment_term = %s""",
- (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
+ (allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]),
+ )
def set_status(self):
if self.docstatus == 2:
- self.status = 'Cancelled'
+ self.status = "Cancelled"
elif self.docstatus == 1:
- self.status = 'Submitted'
+ self.status = "Submitted"
else:
- self.status = 'Draft'
+ self.status = "Draft"
- self.db_set('status', self.status, update_modified = True)
+ self.db_set("status", self.status, update_modified=True)
def set_tax_withholding(self):
- if not self.party_type == 'Supplier':
+ if not self.party_type == "Supplier":
return
if not self.apply_tax_withholding_amount:
@@ -437,22 +511,24 @@ class PaymentEntry(AccountsController):
net_total = self.paid_amount
# Adding args as purchase invoice to get TDS amount
- args = frappe._dict({
- 'company': self.company,
- 'doctype': 'Payment Entry',
- 'supplier': self.party,
- 'posting_date': self.posting_date,
- 'net_total': net_total
- })
+ args = frappe._dict(
+ {
+ "company": self.company,
+ "doctype": "Payment Entry",
+ "supplier": self.party,
+ "posting_date": self.posting_date,
+ "net_total": net_total,
+ }
+ )
tax_withholding_details = get_party_tax_withholding_details(args, self.tax_withholding_category)
if not tax_withholding_details:
return
- tax_withholding_details.update({
- 'cost_center': self.cost_center or erpnext.get_default_cost_center(self.company)
- })
+ tax_withholding_details.update(
+ {"cost_center": self.cost_center or erpnext.get_default_cost_center(self.company)}
+ )
accounts = []
for d in self.taxes:
@@ -460,7 +536,7 @@ class PaymentEntry(AccountsController):
# Preserve user updated included in paid amount
if d.included_in_paid_amount:
- tax_withholding_details.update({'included_in_paid_amount': d.included_in_paid_amount})
+ tax_withholding_details.update({"included_in_paid_amount": d.included_in_paid_amount})
d.update(tax_withholding_details)
accounts.append(d.account_head)
@@ -468,8 +544,11 @@ class PaymentEntry(AccountsController):
if not accounts or tax_withholding_details.get("account_head") not in accounts:
self.append("taxes", tax_withholding_details)
- to_remove = [d for d in self.taxes
- if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")]
+ to_remove = [
+ d
+ for d in self.taxes
+ if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")
+ ]
for d in to_remove:
self.remove(d)
@@ -496,40 +575,53 @@ class PaymentEntry(AccountsController):
def set_received_amount(self):
self.base_received_amount = self.base_paid_amount
- if self.paid_from_account_currency == self.paid_to_account_currency \
- and not self.payment_type == 'Internal Transfer':
+ if (
+ self.paid_from_account_currency == self.paid_to_account_currency
+ and not self.payment_type == "Internal Transfer"
+ ):
self.received_amount = self.paid_amount
def set_amounts_after_tax(self):
applicable_tax = 0
base_applicable_tax = 0
- for tax in self.get('taxes'):
+ for tax in self.get("taxes"):
if not tax.included_in_paid_amount:
- amount = -1 * tax.tax_amount if tax.add_deduct_tax == 'Deduct' else tax.tax_amount
- base_amount = -1 * tax.base_tax_amount if tax.add_deduct_tax == 'Deduct' else tax.base_tax_amount
+ amount = -1 * tax.tax_amount if tax.add_deduct_tax == "Deduct" else tax.tax_amount
+ base_amount = (
+ -1 * tax.base_tax_amount if tax.add_deduct_tax == "Deduct" else tax.base_tax_amount
+ )
applicable_tax += amount
base_applicable_tax += base_amount
- self.paid_amount_after_tax = flt(flt(self.paid_amount) + flt(applicable_tax),
- self.precision("paid_amount_after_tax"))
- self.base_paid_amount_after_tax = flt(flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate),
- self.precision("base_paid_amount_after_tax"))
+ self.paid_amount_after_tax = flt(
+ flt(self.paid_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax")
+ )
+ self.base_paid_amount_after_tax = flt(
+ flt(self.paid_amount_after_tax) * flt(self.source_exchange_rate),
+ self.precision("base_paid_amount_after_tax"),
+ )
- self.received_amount_after_tax = flt(flt(self.received_amount) + flt(applicable_tax),
- self.precision("paid_amount_after_tax"))
- self.base_received_amount_after_tax = flt(flt(self.received_amount_after_tax) * flt(self.target_exchange_rate),
- self.precision("base_paid_amount_after_tax"))
+ self.received_amount_after_tax = flt(
+ flt(self.received_amount) + flt(applicable_tax), self.precision("paid_amount_after_tax")
+ )
+ self.base_received_amount_after_tax = flt(
+ flt(self.received_amount_after_tax) * flt(self.target_exchange_rate),
+ self.precision("base_paid_amount_after_tax"),
+ )
def set_amounts_in_company_currency(self):
self.base_paid_amount, self.base_received_amount, self.difference_amount = 0, 0, 0
if self.paid_amount:
- self.base_paid_amount = flt(flt(self.paid_amount) * flt(self.source_exchange_rate),
- self.precision("base_paid_amount"))
+ self.base_paid_amount = flt(
+ flt(self.paid_amount) * flt(self.source_exchange_rate), self.precision("base_paid_amount")
+ )
if self.received_amount:
- self.base_received_amount = flt(flt(self.received_amount) * flt(self.target_exchange_rate),
- self.precision("base_received_amount"))
+ self.base_received_amount = flt(
+ flt(self.received_amount) * flt(self.target_exchange_rate),
+ self.precision("base_received_amount"),
+ )
def set_total_allocated_amount(self):
if self.payment_type == "Internal Transfer":
@@ -539,8 +631,9 @@ class PaymentEntry(AccountsController):
for d in self.get("references"):
if d.allocated_amount:
total_allocated_amount += flt(d.allocated_amount)
- base_total_allocated_amount += flt(flt(d.allocated_amount) * flt(d.exchange_rate),
- self.precision("base_paid_amount"))
+ base_total_allocated_amount += flt(
+ flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("base_paid_amount")
+ )
self.total_allocated_amount = abs(total_allocated_amount)
self.base_total_allocated_amount = abs(base_total_allocated_amount)
@@ -550,22 +643,33 @@ class PaymentEntry(AccountsController):
if self.party:
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
- if 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):
- self.unallocated_amount = (self.base_received_amount + total_deductions -
- self.base_total_allocated_amount) / self.source_exchange_rate
+ if (
+ 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)
+ ):
+ self.unallocated_amount = (
+ self.base_received_amount + total_deductions - self.base_total_allocated_amount
+ ) / self.source_exchange_rate
self.unallocated_amount -= included_taxes
- elif 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):
- self.unallocated_amount = (self.base_paid_amount - (total_deductions +
- self.base_total_allocated_amount)) / self.target_exchange_rate
+ elif (
+ 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)
+ ):
+ self.unallocated_amount = (
+ self.base_paid_amount - (total_deductions + self.base_total_allocated_amount)
+ ) / self.target_exchange_rate
self.unallocated_amount -= included_taxes
def set_difference_amount(self):
- base_unallocated_amount = flt(self.unallocated_amount) * (flt(self.source_exchange_rate)
- if self.payment_type == "Receive" else flt(self.target_exchange_rate))
+ base_unallocated_amount = flt(self.unallocated_amount) * (
+ flt(self.source_exchange_rate)
+ if self.payment_type == "Receive"
+ else flt(self.target_exchange_rate)
+ )
base_party_amount = flt(self.base_total_allocated_amount) + flt(base_unallocated_amount)
@@ -579,14 +683,15 @@ class PaymentEntry(AccountsController):
total_deductions = sum(flt(d.amount) for d in self.get("deductions"))
included_taxes = self.get_included_taxes()
- self.difference_amount = flt(self.difference_amount - total_deductions - included_taxes,
- self.precision("difference_amount"))
+ self.difference_amount = flt(
+ self.difference_amount - total_deductions - included_taxes, self.precision("difference_amount")
+ )
def get_included_taxes(self):
included_taxes = 0
- for tax in self.get('taxes'):
+ for tax in self.get("taxes"):
if tax.included_in_paid_amount:
- if tax.add_deduct_tax == 'Add':
+ if tax.add_deduct_tax == "Add":
included_taxes += tax.base_tax_amount
else:
included_taxes -= tax.base_tax_amount
@@ -597,27 +702,41 @@ class PaymentEntry(AccountsController):
# Clear the reference document which doesn't have allocated amount on validate so that form can be loaded fast
def clear_unallocated_reference_document_rows(self):
self.set("references", self.get("references", {"allocated_amount": ["not in", [0, None, ""]]}))
- frappe.db.sql("""delete from `tabPayment Entry Reference`
- where parent = %s and allocated_amount = 0""", self.name)
+ frappe.db.sql(
+ """delete from `tabPayment Entry Reference`
+ where parent = %s and allocated_amount = 0""",
+ self.name,
+ )
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" and self.party_type == "Customer") or (
+ self.payment_type == "Receive" and self.party_type == "Supplier"
+ ):
- 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
+ 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)
+ 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)
+ 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:
@@ -638,33 +757,45 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction"))
def set_remarks(self):
- if self.custom_remarks: return
+ if self.custom_remarks:
+ return
- if self.payment_type=="Internal Transfer":
- remarks = [_("Amount {0} {1} transferred from {2} to {3}")
- .format(self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to)]
+ if self.payment_type == "Internal Transfer":
+ remarks = [
+ _("Amount {0} {1} transferred from {2} to {3}").format(
+ self.paid_from_account_currency, self.paid_amount, self.paid_from, self.paid_to
+ )
+ ]
else:
- remarks = [_("Amount {0} {1} {2} {3}").format(
- self.party_account_currency,
- self.paid_amount if self.payment_type=="Receive" else self.received_amount,
- _("received from") if self.payment_type=="Receive" else _("to"), self.party
- )]
+ remarks = [
+ _("Amount {0} {1} {2} {3}").format(
+ self.party_account_currency,
+ self.paid_amount if self.payment_type == "Receive" else self.received_amount,
+ _("received from") if self.payment_type == "Receive" else _("to"),
+ self.party,
+ )
+ ]
if self.reference_no:
- remarks.append(_("Transaction reference no {0} dated {1}")
- .format(self.reference_no, self.reference_date))
+ remarks.append(
+ _("Transaction reference no {0} dated {1}").format(self.reference_no, self.reference_date)
+ )
if self.payment_type in ["Receive", "Pay"]:
for d in self.get("references"):
if d.allocated_amount:
- remarks.append(_("Amount {0} {1} against {2} {3}").format(self.party_account_currency,
- d.allocated_amount, d.reference_doctype, d.reference_name))
+ remarks.append(
+ _("Amount {0} {1} against {2} {3}").format(
+ self.party_account_currency, d.allocated_amount, d.reference_doctype, d.reference_name
+ )
+ )
for d in self.get("deductions"):
if d.amount:
- remarks.append(_("Amount {0} {1} deducted against {2}")
- .format(self.company_currency, d.amount, d.account))
+ remarks.append(
+ _("Amount {0} {1} deducted against {2}").format(self.company_currency, d.amount, d.account)
+ )
self.set("remarks", "\n".join(remarks))
@@ -683,92 +814,110 @@ class PaymentEntry(AccountsController):
def add_party_gl_entries(self, gl_entries):
if self.party_account:
- if self.payment_type=="Receive":
+ if self.payment_type == "Receive":
against_account = self.paid_to
else:
against_account = self.paid_from
- party_gl_dict = self.get_gl_dict({
- "account": self.party_account,
- "party_type": self.party_type,
- "party": self.party,
- "against": against_account,
- "account_currency": self.party_account_currency,
- "cost_center": self.cost_center
- }, item=self)
+ party_gl_dict = self.get_gl_dict(
+ {
+ "account": self.party_account,
+ "party_type": self.party_type,
+ "party": self.party,
+ "against": against_account,
+ "account_currency": self.party_account_currency,
+ "cost_center": self.cost_center,
+ },
+ item=self,
+ )
- dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit"
+ dr_or_cr = (
+ "credit" if erpnext.get_party_account_type(self.party_type) == "Receivable" else "debit"
+ )
for d in self.get("references"):
cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
gle = party_gl_dict.copy()
- gle.update({
- "against_voucher_type": d.reference_doctype,
- "against_voucher": d.reference_name,
- "cost_center": cost_center
- })
+ gle.update(
+ {
+ "against_voucher_type": d.reference_doctype,
+ "against_voucher": d.reference_name,
+ "cost_center": cost_center,
+ }
+ )
- allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate),
- self.precision("paid_amount"))
+ allocated_amount_in_company_currency = flt(
+ flt(d.allocated_amount) * flt(d.exchange_rate), self.precision("paid_amount")
+ )
- gle.update({
- dr_or_cr + "_in_account_currency": d.allocated_amount,
- dr_or_cr: allocated_amount_in_company_currency
- })
+ gle.update(
+ {
+ dr_or_cr + "_in_account_currency": d.allocated_amount,
+ dr_or_cr: allocated_amount_in_company_currency,
+ }
+ )
gl_entries.append(gle)
if self.unallocated_amount:
exchange_rate = self.get_exchange_rate()
- base_unallocated_amount = (self.unallocated_amount * exchange_rate)
+ base_unallocated_amount = self.unallocated_amount * exchange_rate
gle = party_gl_dict.copy()
- gle.update({
- dr_or_cr + "_in_account_currency": self.unallocated_amount,
- dr_or_cr: base_unallocated_amount
- })
+ gle.update(
+ {
+ dr_or_cr + "_in_account_currency": self.unallocated_amount,
+ dr_or_cr: base_unallocated_amount,
+ }
+ )
gl_entries.append(gle)
def add_bank_gl_entries(self, gl_entries):
if self.payment_type in ("Pay", "Internal Transfer"):
gl_entries.append(
- self.get_gl_dict({
- "account": self.paid_from,
- "account_currency": self.paid_from_account_currency,
- "against": self.party if self.payment_type=="Pay" else self.paid_to,
- "credit_in_account_currency": self.paid_amount,
- "credit": self.base_paid_amount,
- "cost_center": self.cost_center,
- "post_net_value": True
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.paid_from,
+ "account_currency": self.paid_from_account_currency,
+ "against": self.party if self.payment_type == "Pay" else self.paid_to,
+ "credit_in_account_currency": self.paid_amount,
+ "credit": self.base_paid_amount,
+ "cost_center": self.cost_center,
+ "post_net_value": True,
+ },
+ item=self,
+ )
)
if self.payment_type in ("Receive", "Internal Transfer"):
gl_entries.append(
- self.get_gl_dict({
- "account": self.paid_to,
- "account_currency": self.paid_to_account_currency,
- "against": self.party if self.payment_type=="Receive" else self.paid_from,
- "debit_in_account_currency": self.received_amount,
- "debit": self.base_received_amount,
- "cost_center": self.cost_center
- }, item=self)
+ self.get_gl_dict(
+ {
+ "account": self.paid_to,
+ "account_currency": self.paid_to_account_currency,
+ "against": self.party if self.payment_type == "Receive" else self.paid_from,
+ "debit_in_account_currency": self.received_amount,
+ "debit": self.base_received_amount,
+ "cost_center": self.cost_center,
+ },
+ item=self,
+ )
)
def add_tax_gl_entries(self, gl_entries):
- for d in self.get('taxes'):
+ for d in self.get("taxes"):
account_currency = get_account_currency(d.account_head)
if account_currency != self.company_currency:
frappe.throw(_("Currency for {0} must be {1}").format(d.account_head, self.company_currency))
- if self.payment_type in ('Pay', 'Internal Transfer'):
+ if self.payment_type in ("Pay", "Internal Transfer"):
dr_or_cr = "debit" if d.add_deduct_tax == "Add" else "credit"
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
against = self.party or self.paid_from
- elif self.payment_type == 'Receive':
+ elif self.payment_type == "Receive":
dr_or_cr = "credit" if d.add_deduct_tax == "Add" else "debit"
rev_dr_or_cr = "credit" if dr_or_cr == "debit" else "debit"
against = self.party or self.paid_to
@@ -778,29 +927,46 @@ class PaymentEntry(AccountsController):
base_tax_amount = d.base_tax_amount
gl_entries.append(
- self.get_gl_dict({
- "account": d.account_head,
- "against": against,
- dr_or_cr: tax_amount,
- dr_or_cr + "_in_account_currency": base_tax_amount
- if account_currency==self.company_currency
- else d.tax_amount,
- "cost_center": d.cost_center,
- "post_net_value": True,
- }, account_currency, item=d))
+ self.get_gl_dict(
+ {
+ "account": d.account_head,
+ "against": against,
+ dr_or_cr: tax_amount,
+ dr_or_cr + "_in_account_currency": base_tax_amount
+ if account_currency == self.company_currency
+ else d.tax_amount,
+ "cost_center": d.cost_center,
+ "post_net_value": True,
+ },
+ account_currency,
+ item=d,
+ )
+ )
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({
- "account": payment_account,
- "against": against,
- rev_dr_or_cr: tax_amount,
- rev_dr_or_cr + "_in_account_currency": base_tax_amount
- if account_currency==self.company_currency
- else d.tax_amount,
- "cost_center": self.cost_center,
- "post_net_value": True,
- }, account_currency, item=d))
+ self.get_gl_dict(
+ {
+ "account": payment_account,
+ "against": against,
+ rev_dr_or_cr: tax_amount,
+ rev_dr_or_cr + "_in_account_currency": base_tax_amount
+ if account_currency == self.company_currency
+ else d.tax_amount,
+ "cost_center": self.cost_center,
+ "post_net_value": True,
+ },
+ account_currency,
+ item=d,
+ )
+ )
def add_deductions_gl_entries(self, gl_entries):
for d in self.get("deductions"):
@@ -810,33 +976,40 @@ class PaymentEntry(AccountsController):
frappe.throw(_("Currency for {0} must be {1}").format(d.account, self.company_currency))
gl_entries.append(
- self.get_gl_dict({
- "account": d.account,
- "account_currency": account_currency,
- "against": self.party or self.paid_from,
- "debit_in_account_currency": d.amount,
- "debit": d.amount,
- "cost_center": d.cost_center
- }, item=d)
+ self.get_gl_dict(
+ {
+ "account": d.account,
+ "account_currency": account_currency,
+ "against": self.party or self.paid_from,
+ "debit_in_account_currency": d.amount,
+ "debit": d.amount,
+ "cost_center": d.cost_center,
+ },
+ item=d,
+ )
)
def get_party_account_for_taxes(self):
- if self.payment_type == 'Receive':
+ if self.payment_type == "Receive":
return self.paid_to
- elif self.payment_type in ('Pay', 'Internal Transfer'):
+ elif self.payment_type in ("Pay", "Internal Transfer"):
return self.paid_from
def update_advance_paid(self):
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 ("Sales Order", "Purchase Order", "Employee Advance", "Gratuity"):
- frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid()
+ if d.allocated_amount and d.reference_doctype in (
+ "Sales Order",
+ "Purchase Order",
+ "Employee Advance",
+ "Gratuity",
+ ):
+ frappe.get_doc(d.reference_doctype, d.reference_name).set_total_advance_paid()
def update_expense_claim(self):
if self.payment_type in ("Pay") and self.party:
for d in self.get("references"):
- if d.reference_doctype=="Expense Claim" and d.reference_name:
+ if d.reference_doctype == "Expense Claim" and d.reference_name:
doc = frappe.get_doc("Expense Claim", d.reference_name)
if self.docstatus == 2:
update_reimbursed_amount(doc, -1 * d.allocated_amount)
@@ -846,7 +1019,7 @@ class PaymentEntry(AccountsController):
def update_donation(self, cancel=0):
if self.payment_type == "Receive" and self.party_type == "Donor" and self.party:
for d in self.get("references"):
- if d.reference_doctype=="Donation" and d.reference_name:
+ if d.reference_doctype == "Donation" and d.reference_name:
is_paid = 0 if cancel else 1
frappe.db.set_value("Donation", d.reference_name, "paid", is_paid)
@@ -856,31 +1029,29 @@ class PaymentEntry(AccountsController):
def calculate_deductions(self, tax_details):
return {
- "account": tax_details['tax']['account_head'],
- "cost_center": frappe.get_cached_value('Company', self.company, "cost_center"),
- "amount": self.total_allocated_amount * (tax_details['tax']['rate'] / 100)
+ "account": tax_details["tax"]["account_head"],
+ "cost_center": frappe.get_cached_value("Company", self.company, "cost_center"),
+ "amount": self.total_allocated_amount * (tax_details["tax"]["rate"] / 100),
}
def set_gain_or_loss(self, account_details=None):
if not self.difference_amount:
self.set_difference_amount()
- row = {
- 'amount': self.difference_amount
- }
+ row = {"amount": self.difference_amount}
if account_details:
row.update(account_details)
- if not row.get('amount'):
+ if not row.get("amount"):
# if no difference amount
return
- self.append('deductions', row)
+ self.append("deductions", row)
self.set_unallocated_amount()
def get_exchange_rate(self):
- return self.source_exchange_rate if self.payment_type=="Receive" else self.target_exchange_rate
+ return self.source_exchange_rate if self.payment_type == "Receive" else self.target_exchange_rate
def initialize_taxes(self):
for tax in self.get("taxes"):
@@ -895,7 +1066,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"))):
@@ -904,25 +1075,31 @@ class PaymentEntry(AccountsController):
cumulated_tax_fraction = 0
for i, tax in enumerate(self.get("taxes")):
tax.tax_fraction_for_current_item = self.get_current_tax_fraction(tax)
- if i==0:
+ if i == 0:
tax.grand_total_fraction_for_current_item = 1 + tax.tax_fraction_for_current_item
else:
- tax.grand_total_fraction_for_current_item = \
- self.get("taxes")[i-1].grand_total_fraction_for_current_item \
+ tax.grand_total_fraction_for_current_item = (
+ self.get("taxes")[i - 1].grand_total_fraction_for_current_item
+ tax.tax_fraction_for_current_item
+ )
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
self.base_total_taxes_and_charges = 0.0
- actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
- for tax in self.get("taxes") if tax.charge_type == "Actual"])
+ actual_tax_dict = dict(
+ [
+ [tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
+ for tax in self.get("taxes")
+ if tax.charge_type == "Actual"
+ ]
+ )
- for i, tax in enumerate(self.get('taxes')):
+ for i, tax in enumerate(self.get("taxes")):
current_tax_amount = self.get_current_tax_amount(tax)
if tax.charge_type == "Actual":
@@ -931,7 +1108,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
@@ -941,15 +1118,27 @@ class PaymentEntry(AccountsController):
if i == 0:
tax.total = flt(self.paid_amount_after_tax + current_tax_amount, self.precision("total", tax))
else:
- tax.total = flt(self.get('taxes')[i-1].total + current_tax_amount, self.precision("total", tax))
+ tax.total = flt(
+ 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
- self.total_taxes_and_charges += current_tax_amount
- self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate
+ if self.payment_type == "Pay":
+ 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
- if self.get('taxes'):
- self.paid_amount_after_tax = self.get('taxes')[-1].base_total
+ 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
def get_current_tax_amount(self, tax):
tax_rate = tax.rate
@@ -957,7 +1146,11 @@ class PaymentEntry(AccountsController):
# To set row_id by default as previous row.
if tax.charge_type in ["On Previous Row Amount", "On Previous Row Total"]:
if tax.idx == 1:
- frappe.throw(_("Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"))
+ frappe.throw(
+ _(
+ "Cannot select charge type as 'On Previous Row Amount' or 'On Previous Row Total' for first row"
+ )
+ )
if not tax.row_id:
tax.row_id = tax.idx - 1
@@ -967,12 +1160,10 @@ class PaymentEntry(AccountsController):
elif tax.charge_type == "On Paid Amount":
current_tax_amount = (tax_rate / 100.0) * self.paid_amount_after_tax
elif tax.charge_type == "On Previous Row Amount":
- current_tax_amount = (tax_rate / 100.0) * \
- self.get('taxes')[cint(tax.row_id) - 1].tax_amount
+ current_tax_amount = (tax_rate / 100.0) * self.get("taxes")[cint(tax.row_id) - 1].tax_amount
elif tax.charge_type == "On Previous Row Total":
- current_tax_amount = (tax_rate / 100.0) * \
- self.get('taxes')[cint(tax.row_id) - 1].total
+ current_tax_amount = (tax_rate / 100.0) * self.get("taxes")[cint(tax.row_id) - 1].total
return current_tax_amount
@@ -985,83 +1176,106 @@ class PaymentEntry(AccountsController):
if tax.charge_type == "On Paid Amount":
current_tax_fraction = tax_rate / 100.0
elif tax.charge_type == "On Previous Row Amount":
- current_tax_fraction = (tax_rate / 100.0) * \
- self.get("taxes")[cint(tax.row_id) - 1].tax_fraction_for_current_item
+ current_tax_fraction = (tax_rate / 100.0) * self.get("taxes")[
+ cint(tax.row_id) - 1
+ ].tax_fraction_for_current_item
elif tax.charge_type == "On Previous Row Total":
- current_tax_fraction = (tax_rate / 100.0) * \
- self.get("taxes")[cint(tax.row_id) - 1].grand_total_fraction_for_current_item
+ current_tax_fraction = (tax_rate / 100.0) * self.get("taxes")[
+ cint(tax.row_id) - 1
+ ].grand_total_fraction_for_current_item
if getattr(tax, "add_deduct_tax", None) and tax.add_deduct_tax == "Deduct":
current_tax_fraction *= -1.0
return current_tax_fraction
+
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):
- throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range))
+ throw(
+ _("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(
+ tax.idx, row_range
+ )
+ )
if cint(getattr(tax, "included_in_paid_amount", None)):
if tax.charge_type == "Actual":
# inclusive tax cannot be of type Actual
- throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx))
- elif tax.charge_type == "On Previous Row Amount" and \
- not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount):
+ throw(
+ _("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(
+ tax.idx
+ )
+ )
+ elif tax.charge_type == "On Previous Row Amount" and not cint(
+ doc.get("taxes")[cint(tax.row_id) - 1].included_in_paid_amount
+ ):
# referred row should also be inclusive
_on_previous_row_error(tax.row_id)
- elif tax.charge_type == "On Previous Row Total" and \
- not all([cint(t.included_in_paid_amount for t in doc.get("taxes")[:cint(tax.row_id) - 1])]):
+ elif tax.charge_type == "On Previous Row Total" and not all(
+ [cint(t.included_in_paid_amount for t in doc.get("taxes")[: cint(tax.row_id) - 1])]
+ ):
# all rows about the referred tax should be inclusive
_on_previous_row_error("1 - %d" % (cint(tax.row_id),))
elif tax.get("category") == "Valuation":
frappe.throw(_("Valuation type charges can not be marked as Inclusive"))
+
@frappe.whitelist()
def get_outstanding_reference_documents(args):
if isinstance(args, string_types):
args = json.loads(args)
- if args.get('party_type') == 'Member':
+ if args.get("party_type") == "Member":
return
# confirm that Supplier is not blocked
- if args.get('party_type') == 'Supplier':
- supplier_status = get_supplier_block_status(args['party'])
- if supplier_status['on_hold']:
- if supplier_status['hold_type'] == 'All':
+ if args.get("party_type") == "Supplier":
+ supplier_status = get_supplier_block_status(args["party"])
+ if supplier_status["on_hold"]:
+ if supplier_status["hold_type"] == "All":
return []
- elif supplier_status['hold_type'] == 'Payments':
- if not supplier_status['release_date'] or getdate(nowdate()) <= supplier_status['release_date']:
+ elif supplier_status["hold_type"] == "Payments":
+ if (
+ not supplier_status["release_date"] or getdate(nowdate()) <= supplier_status["release_date"]
+ ):
return []
party_account_currency = get_account_currency(args.get("party_account"))
- company_currency = frappe.get_cached_value('Company', args.get("company"), "default_currency")
+ company_currency = frappe.get_cached_value("Company", args.get("company"), "default_currency")
# Get positive outstanding sales /purchase invoices/ Fees
condition = ""
if args.get("voucher_type") and args.get("voucher_no"):
- condition = " and voucher_type={0} and voucher_no={1}"\
- .format(frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"]))
+ condition = " and voucher_type={0} and voucher_no={1}".format(
+ frappe.db.escape(args["voucher_type"]), frappe.db.escape(args["voucher_no"])
+ )
# Add cost center condition
if args.get("cost_center"):
condition += " and cost_center='%s'" % args.get("cost_center")
date_fields_dict = {
- 'posting_date': ['from_posting_date', 'to_posting_date'],
- 'due_date': ['from_due_date', 'to_due_date']
+ "posting_date": ["from_posting_date", "to_posting_date"],
+ "due_date": ["from_due_date", "to_due_date"],
}
for fieldname, date_fields in date_fields_dict.items():
if args.get(date_fields[0]) and args.get(date_fields[1]):
- condition += " and {0} between '{1}' and '{2}'".format(fieldname,
- args.get(date_fields[0]), args.get(date_fields[1]))
+ condition += " and {0} between '{1}' and '{2}'".format(
+ 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")))
- outstanding_invoices = get_outstanding_invoices(args.get("party_type"), args.get("party"),
- args.get("party_account"), filters=args, condition=condition)
+ outstanding_invoices = get_outstanding_invoices(
+ args.get("party_type"),
+ args.get("party"),
+ args.get("party_account"),
+ filters=args,
+ condition=condition,
+ )
outstanding_invoices = split_invoices_based_on_payment_terms(outstanding_invoices)
@@ -1072,28 +1286,44 @@ def get_outstanding_reference_documents(args):
d["exchange_rate"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "conversion_rate")
elif d.voucher_type == "Journal Entry":
d["exchange_rate"] = get_exchange_rate(
- party_account_currency, company_currency, d.posting_date
+ party_account_currency, company_currency, d.posting_date
)
if d.voucher_type in ("Purchase Invoice"):
d["bill_no"] = frappe.db.get_value(d.voucher_type, d.voucher_no, "bill_no")
- # Get all SO / PO which are not fully billed or aginst which full advance not paid
+ # Get all SO / PO which are not fully billed or against which full advance not paid
orders_to_be_billed = []
- if (args.get("party_type") != "Student"):
- orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
- args.get("party"), args.get("company"), party_account_currency, company_currency, filters=args)
+ if args.get("party_type") != "Student":
+ orders_to_be_billed = get_orders_to_be_billed(
+ args.get("posting_date"),
+ args.get("party_type"),
+ args.get("party"),
+ args.get("company"),
+ party_account_currency,
+ company_currency,
+ filters=args,
+ )
# Get negative outstanding sales /purchase invoices
negative_outstanding_invoices = []
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
- negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), args.get("party"),
- args.get("party_account"), party_account_currency, company_currency, condition=condition)
+ negative_outstanding_invoices = get_negative_outstanding_invoices(
+ args.get("party_type"),
+ args.get("party"),
+ args.get("party_account"),
+ party_account_currency,
+ company_currency,
+ condition=condition,
+ )
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
if not data:
- frappe.msgprint(_("No outstanding invoices found for the {0} {1} which qualify the filters you have specified.")
- .format(_(args.get("party_type")).lower(), frappe.bold(args.get("party"))))
+ frappe.msgprint(
+ _(
+ "No outstanding invoices found for the {0} {1} which qualify the filters you have specified."
+ ).format(_(args.get("party_type")).lower(), frappe.bold(args.get("party")))
+ )
return data
@@ -1101,53 +1331,75 @@ def get_outstanding_reference_documents(args):
def split_invoices_based_on_payment_terms(outstanding_invoices):
invoice_ref_based_on_payment_terms = {}
for idx, d in enumerate(outstanding_invoices):
- if d.voucher_type in ['Sales Invoice', 'Purchase Invoice']:
- payment_term_template = frappe.db.get_value(d.voucher_type, d.voucher_no, 'payment_terms_template')
+ if d.voucher_type in ["Sales Invoice", "Purchase Invoice"]:
+ payment_term_template = frappe.db.get_value(
+ d.voucher_type, d.voucher_no, "payment_terms_template"
+ )
if payment_term_template:
allocate_payment_based_on_payment_terms = frappe.db.get_value(
- 'Payment Terms Template', payment_term_template, 'allocate_payment_based_on_payment_terms')
+ "Payment Terms Template", payment_term_template, "allocate_payment_based_on_payment_terms"
+ )
if allocate_payment_based_on_payment_terms:
- payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': d.voucher_no}, fields=["*"])
+ payment_schedule = frappe.get_all(
+ "Payment Schedule", filters={"parent": d.voucher_no}, fields=["*"]
+ )
for payment_term in payment_schedule:
if payment_term.outstanding > 0.1:
invoice_ref_based_on_payment_terms.setdefault(idx, [])
- invoice_ref_based_on_payment_terms[idx].append(frappe._dict({
- 'due_date': d.due_date,
- 'currency': d.currency,
- 'voucher_no': d.voucher_no,
- 'voucher_type': d.voucher_type,
- 'posting_date': d.posting_date,
- 'invoice_amount': flt(d.invoice_amount),
- 'outstanding_amount': flt(d.outstanding_amount),
- 'payment_amount': payment_term.payment_amount,
- 'payment_term': payment_term.payment_term
- }))
+ invoice_ref_based_on_payment_terms[idx].append(
+ frappe._dict(
+ {
+ "due_date": d.due_date,
+ "currency": d.currency,
+ "voucher_no": d.voucher_no,
+ "voucher_type": d.voucher_type,
+ "posting_date": d.posting_date,
+ "invoice_amount": flt(d.invoice_amount),
+ "outstanding_amount": flt(d.outstanding_amount),
+ "payment_amount": payment_term.payment_amount,
+ "payment_term": payment_term.payment_term,
+ }
+ )
+ )
outstanding_invoices_after_split = []
if invoice_ref_based_on_payment_terms:
for idx, ref in invoice_ref_based_on_payment_terms.items():
- voucher_no = ref[0]['voucher_no']
- voucher_type = ref[0]['voucher_type']
+ voucher_no = ref[0]["voucher_no"]
+ voucher_type = ref[0]["voucher_type"]
- frappe.msgprint(_("Spliting {} {} into {} row(s) as per Payment Terms").format(
- voucher_type, voucher_no, len(ref)), alert=True)
+ frappe.msgprint(
+ _("Spliting {} {} into {} row(s) as per Payment Terms").format(
+ voucher_type, voucher_no, len(ref)
+ ),
+ alert=True,
+ )
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
- existing_row = list(filter(lambda x: x.get('voucher_no') == voucher_no, outstanding_invoices))
+ existing_row = list(filter(lambda x: x.get("voucher_no") == voucher_no, outstanding_invoices))
index = outstanding_invoices.index(existing_row[0])
outstanding_invoices.pop(index)
outstanding_invoices_after_split += outstanding_invoices
return outstanding_invoices_after_split
-def get_orders_to_be_billed(posting_date, party_type, party,
- company, party_account_currency, company_currency, cost_center=None, filters=None):
+
+def get_orders_to_be_billed(
+ posting_date,
+ party_type,
+ party,
+ company,
+ party_account_currency,
+ company_currency,
+ cost_center=None,
+ filters=None,
+):
if party_type == "Customer":
- voucher_type = 'Sales Order'
+ voucher_type = "Sales Order"
elif party_type == "Supplier":
- voucher_type = 'Purchase Order'
+ voucher_type = "Purchase Order"
elif party_type == "Employee":
voucher_type = None
@@ -1155,7 +1407,7 @@ def get_orders_to_be_billed(posting_date, party_type, party,
if voucher_type:
doc = frappe.get_doc({"doctype": voucher_type})
condition = ""
- if doc and hasattr(doc, 'cost_center'):
+ if doc and hasattr(doc, "cost_center"):
condition = " and cost_center='%s'" % cost_center
orders = []
@@ -1167,7 +1419,8 @@ def get_orders_to_be_billed(posting_date, party_type, party,
grand_total_field = "grand_total"
rounded_total_field = "rounded_total"
- orders = frappe.db.sql("""
+ orders = frappe.db.sql(
+ """
select
name as voucher_no,
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
@@ -1185,18 +1438,25 @@ def get_orders_to_be_billed(posting_date, party_type, party,
{condition}
order by
transaction_date, name
- """.format(**{
- "rounded_total_field": rounded_total_field,
- "grand_total_field": grand_total_field,
- "voucher_type": voucher_type,
- "party_type": scrub(party_type),
- "condition": condition
- }), (party, company), as_dict=True)
+ """.format(
+ **{
+ "rounded_total_field": rounded_total_field,
+ "grand_total_field": grand_total_field,
+ "voucher_type": voucher_type,
+ "party_type": scrub(party_type),
+ "condition": condition,
+ }
+ ),
+ (party, company),
+ as_dict=True,
+ )
order_list = []
for d in orders:
- if not (flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
- and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))):
+ if not (
+ flt(d.outstanding_amount) >= flt(filters.get("outstanding_amt_greater_than"))
+ and flt(d.outstanding_amount) <= flt(filters.get("outstanding_amt_less_than"))
+ ):
continue
d["voucher_type"] = voucher_type
@@ -1206,8 +1466,16 @@ def get_orders_to_be_billed(posting_date, party_type, party,
return order_list
-def get_negative_outstanding_invoices(party_type, party, party_account,
- party_account_currency, company_currency, cost_center=None, condition=None):
+
+def get_negative_outstanding_invoices(
+ party_type,
+ party,
+ party_account,
+ party_account_currency,
+ company_currency,
+ cost_center=None,
+ condition=None,
+):
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
supplier_condition = ""
if voucher_type == "Purchase Invoice":
@@ -1219,7 +1487,8 @@ def get_negative_outstanding_invoices(party_type, party, party_account,
grand_total_field = "grand_total"
rounded_total_field = "rounded_total"
- return frappe.db.sql("""
+ return frappe.db.sql(
+ """
select
"{voucher_type}" as voucher_type, name as voucher_no,
if({rounded_total_field}, {rounded_total_field}, {grand_total_field}) as invoice_amount,
@@ -1234,21 +1503,26 @@ def get_negative_outstanding_invoices(party_type, party, party_account,
{condition}
order by
posting_date, name
- """.format(**{
- "supplier_condition": supplier_condition,
- "condition": condition,
- "rounded_total_field": rounded_total_field,
- "grand_total_field": grand_total_field,
- "voucher_type": voucher_type,
- "party_type": scrub(party_type),
- "party_account": "debit_to" if party_type == "Customer" else "credit_to",
- "cost_center": cost_center
- }), (party, party_account), as_dict=True)
+ """.format(
+ **{
+ "supplier_condition": supplier_condition,
+ "condition": condition,
+ "rounded_total_field": rounded_total_field,
+ "grand_total_field": grand_total_field,
+ "voucher_type": voucher_type,
+ "party_type": scrub(party_type),
+ "party_account": "debit_to" if party_type == "Customer" else "credit_to",
+ "cost_center": cost_center,
+ }
+ ),
+ (party, party_account),
+ as_dict=True,
+ )
@frappe.whitelist()
def get_party_details(company, party_type, party, date, cost_center=None):
- bank_account = ''
+ bank_account = ""
if not frappe.db.exists(party_type, party):
frappe.throw(_("Invalid {0}: {1}").format(party_type, party))
@@ -1256,7 +1530,9 @@ def get_party_details(company, party_type, party, date, cost_center=None):
account_currency = get_account_currency(party_account)
account_balance = get_balance_on(party_account, date, cost_center=cost_center)
- _party_name = "title" if party_type in ("Student", "Shareholder") else party_type.lower() + "_name"
+ _party_name = (
+ "title" if party_type in ("Student", "Shareholder") else party_type.lower() + "_name"
+ )
party_name = frappe.db.get_value(party_type, party, _party_name)
party_balance = get_balance_on(party_type=party_type, party=party, cost_center=cost_center)
if party_type in ["Customer", "Supplier"]:
@@ -1268,61 +1544,68 @@ def get_party_details(company, party_type, party, date, cost_center=None):
"party_account_currency": account_currency,
"party_balance": party_balance,
"account_balance": account_balance,
- "bank_account": bank_account
+ "bank_account": bank_account,
}
@frappe.whitelist()
def get_account_details(account, date, cost_center=None):
- frappe.has_permission('Payment Entry', throw=True)
+ frappe.has_permission("Payment Entry", throw=True)
# to check if the passed account is accessible under reference doctype Payment Entry
- account_list = frappe.get_list('Account', {
- 'name': account
- }, reference_doctype='Payment Entry', limit=1)
+ account_list = frappe.get_list(
+ "Account", {"name": account}, reference_doctype="Payment Entry", limit=1
+ )
# There might be some user permissions which will allow account under certain doctypes
# except for Payment Entry, only in such case we should throw permission error
if not account_list:
- frappe.throw(_('Account: {0} is not permitted under Payment Entry').format(account))
+ frappe.throw(_("Account: {0} is not permitted under Payment Entry").format(account))
- account_balance = get_balance_on(account, date, cost_center=cost_center,
- ignore_account_permission=True)
+ account_balance = get_balance_on(
+ account, date, cost_center=cost_center, ignore_account_permission=True
+ )
- return frappe._dict({
- "account_currency": get_account_currency(account),
- "account_balance": account_balance,
- "account_type": frappe.db.get_value("Account", account, "account_type")
- })
+ return frappe._dict(
+ {
+ "account_currency": get_account_currency(account),
+ "account_balance": account_balance,
+ "account_type": frappe.db.get_value("Account", account, "account_type"),
+ }
+ )
@frappe.whitelist()
def get_company_defaults(company):
fields = ["write_off_account", "exchange_gain_loss_account", "cost_center"]
- ret = frappe.get_cached_value('Company', company, fields, as_dict=1)
+ ret = frappe.get_cached_value("Company", company, fields, as_dict=1)
for fieldname in fields:
if not ret[fieldname]:
- frappe.throw(_("Please set default {0} in Company {1}")
- .format(frappe.get_meta("Company").get_label(fieldname), company))
+ frappe.throw(
+ _("Please set default {0} in Company {1}").format(
+ frappe.get_meta("Company").get_label(fieldname), company
+ )
+ )
return ret
def get_outstanding_on_journal_entry(name):
res = frappe.db.sql(
- 'SELECT '
- 'CASE WHEN party_type IN ("Customer", "Student") '
- 'THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) '
- 'ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) '
- 'END as outstanding_amount '
- 'FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) '
- 'AND party_type IS NOT NULL '
- 'AND party_type != ""',
- (name, name), as_dict=1
- )
+ "SELECT "
+ 'CASE WHEN party_type IN ("Customer", "Student") '
+ "THEN ifnull(sum(debit_in_account_currency - credit_in_account_currency), 0) "
+ "ELSE ifnull(sum(credit_in_account_currency - debit_in_account_currency), 0) "
+ "END as outstanding_amount "
+ "FROM `tabGL Entry` WHERE (voucher_no=%s OR against_voucher=%s) "
+ "AND party_type IS NOT NULL "
+ 'AND party_type != ""',
+ (name, name),
+ as_dict=1,
+ )
- outstanding_amount = res[0].get('outstanding_amount', 0) if res else 0
+ outstanding_amount = res[0].get("outstanding_amount", 0) if res else 0
return outstanding_amount
@@ -1331,7 +1614,9 @@ def get_outstanding_on_journal_entry(name):
def get_reference_details(reference_doctype, reference_name, party_account_currency):
total_amount = outstanding_amount = exchange_rate = bill_no = None
ref_doc = frappe.get_doc(reference_doctype, reference_name)
- company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(ref_doc.company)
+ company_currency = ref_doc.get("company_currency") or erpnext.get_company_currency(
+ ref_doc.company
+ )
if reference_doctype == "Fees":
total_amount = ref_doc.get("grand_total")
@@ -1348,20 +1633,22 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
total_amount = ref_doc.get("total_amount")
if ref_doc.multi_currency:
- exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
+ exchange_rate = get_exchange_rate(
+ party_account_currency, company_currency, ref_doc.posting_date
+ )
else:
exchange_rate = 1
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
elif reference_doctype != "Journal Entry":
if ref_doc.doctype == "Expense Claim":
- total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
+ total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
total_amount = ref_doc.advance_amount
exchange_rate = ref_doc.get("exchange_rate")
if party_account_currency != ref_doc.currency:
total_amount = flt(total_amount) * flt(exchange_rate)
elif ref_doc.doctype == "Gratuity":
- total_amount = ref_doc.amount
+ total_amount = ref_doc.amount
if not total_amount:
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
@@ -1371,16 +1658,16 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
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.
- exchange_rate = ref_doc.get("conversion_rate") or \
- get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
+ exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate(
+ party_account_currency, company_currency, ref_doc.posting_date
+ )
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
elif reference_doctype == "Expense Claim":
- outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\
- - flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount"))
+ outstanding_amount = get_outstanding_amount_for_claim(ref_doc)
elif reference_doctype == "Employee Advance":
- outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount))
+ outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
if party_account_currency != ref_doc.currency:
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
@@ -1391,18 +1678,22 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
else:
# Get the exchange rate based on the posting date of the ref doc.
- exchange_rate = get_exchange_rate(party_account_currency,
- company_currency, ref_doc.posting_date)
+ exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
- return frappe._dict({
- "due_date": ref_doc.get("due_date"),
- "total_amount": flt(total_amount),
- "outstanding_amount": flt(outstanding_amount),
- "exchange_rate": flt(exchange_rate),
- "bill_no": bill_no
- })
+ return frappe._dict(
+ {
+ "due_date": ref_doc.get("due_date"),
+ "total_amount": flt(total_amount),
+ "outstanding_amount": flt(outstanding_amount),
+ "exchange_rate": flt(exchange_rate),
+ "bill_no": bill_no,
+ }
+ )
-def get_amounts_based_on_reference_doctype(reference_doctype, ref_doc, party_account_currency, company_currency, reference_name):
+
+def get_amounts_based_on_reference_doctype(
+ reference_doctype, ref_doc, party_account_currency, company_currency, reference_name
+):
total_amount = outstanding_amount = exchange_rate = None
if reference_doctype == "Fees":
total_amount = ref_doc.get("grand_total")
@@ -1415,35 +1706,46 @@ def get_amounts_based_on_reference_doctype(reference_doctype, ref_doc, party_acc
elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1:
total_amount = ref_doc.get("total_amount")
if ref_doc.multi_currency:
- exchange_rate = get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
+ exchange_rate = get_exchange_rate(
+ party_account_currency, company_currency, ref_doc.posting_date
+ )
else:
exchange_rate = 1
outstanding_amount = get_outstanding_on_journal_entry(reference_name)
return total_amount, outstanding_amount, exchange_rate
-def get_amounts_based_on_ref_doc(reference_doctype, ref_doc, party_account_currency, company_currency):
+
+def get_amounts_based_on_ref_doc(
+ reference_doctype, ref_doc, party_account_currency, company_currency
+):
total_amount = outstanding_amount = exchange_rate = None
if ref_doc.doctype == "Expense Claim":
- total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
+ total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges)
elif ref_doc.doctype == "Employee Advance":
- total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc)
+ total_amount, exchange_rate = get_total_amount_exchange_rate_for_employee_advance(
+ party_account_currency, ref_doc
+ )
if not total_amount:
total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency(
- party_account_currency, company_currency, ref_doc)
+ party_account_currency, company_currency, ref_doc
+ )
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
- exchange_rate = ref_doc.get("conversion_rate") or \
- get_exchange_rate(party_account_currency, company_currency, ref_doc.posting_date)
+ exchange_rate = ref_doc.get("conversion_rate") or get_exchange_rate(
+ party_account_currency, company_currency, ref_doc.posting_date
+ )
outstanding_amount, exchange_rate, bill_no = get_bill_no_and_update_amounts(
- reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency)
+ reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency
+ )
return total_amount, outstanding_amount, exchange_rate, bill_no
+
def get_total_amount_exchange_rate_for_employee_advance(party_account_currency, ref_doc):
total_amount = ref_doc.advance_amount
exchange_rate = ref_doc.get("exchange_rate")
@@ -1452,7 +1754,10 @@ def get_total_amount_exchange_rate_for_employee_advance(party_account_currency,
return total_amount, exchange_rate
-def get_total_amount_exchange_rate_base_on_currency(party_account_currency, company_currency, ref_doc):
+
+def get_total_amount_exchange_rate_base_on_currency(
+ party_account_currency, company_currency, ref_doc
+):
exchange_rate = None
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
@@ -1462,16 +1767,23 @@ def get_total_amount_exchange_rate_base_on_currency(party_account_currency, comp
return total_amount, exchange_rate
-def get_bill_no_and_update_amounts(reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency):
+
+def get_bill_no_and_update_amounts(
+ reference_doctype, ref_doc, total_amount, exchange_rate, party_account_currency, company_currency
+):
outstanding_amount = bill_no = None
if reference_doctype in ("Sales Invoice", "Purchase Invoice"):
outstanding_amount = ref_doc.get("outstanding_amount")
bill_no = ref_doc.get("bill_no")
elif reference_doctype == "Expense Claim":
- outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\
- - flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount"))
+ outstanding_amount = (
+ flt(ref_doc.get("total_sanctioned_amount"))
+ + flt(ref_doc.get("total_taxes_and_charges"))
+ - flt(ref_doc.get("total_amount_reimbursed"))
+ - flt(ref_doc.get("total_advance_amount"))
+ )
elif reference_doctype == "Employee Advance":
- outstanding_amount = (flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount))
+ outstanding_amount = flt(ref_doc.advance_amount) - flt(ref_doc.paid_amount)
if party_account_currency != ref_doc.currency:
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
@@ -1493,15 +1805,20 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
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)
- grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc)
+ grand_total, outstanding_amount = set_grand_total_and_outstanding_amount(
+ party_amount, dt, party_account_currency, doc
+ )
# bank or cash
bank = get_bank_cash_account(doc, bank_account)
paid_amount, received_amount = set_paid_amount_and_received_amount(
- dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc)
+ dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
+ )
- paid_amount, received_amount, discount_amount = apply_early_payment_discount(paid_amount, received_amount, doc)
+ paid_amount, received_amount, discount_amount = apply_early_payment_discount(
+ paid_amount, received_amount, doc
+ )
pe = frappe.new_doc("Payment Entry")
pe.payment_type = payment_type
@@ -1515,15 +1832,23 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.contact_email = doc.get("contact_email")
pe.ensure_supplier_is_not_blocked()
- pe.paid_from = party_account if payment_type=="Receive" else bank.account
- pe.paid_to = party_account if payment_type=="Pay" else bank.account
- pe.paid_from_account_currency = party_account_currency \
- if payment_type=="Receive" else bank.account_currency
- pe.paid_to_account_currency = party_account_currency if payment_type=="Pay" else bank.account_currency
+ pe.paid_from = party_account if payment_type == "Receive" else bank.account
+ pe.paid_to = party_account if payment_type == "Pay" else bank.account
+ pe.paid_from_account_currency = (
+ party_account_currency if payment_type == "Receive" else bank.account_currency
+ )
+ pe.paid_to_account_currency = (
+ party_account_currency if payment_type == "Pay" else bank.account_currency
+ )
pe.paid_amount = paid_amount
pe.received_amount = received_amount
pe.letter_head = doc.get("letter_head")
+ if dt in ["Purchase Order", "Sales Order", "Sales Invoice", "Purchase Invoice"]:
+ pe.project = doc.get("project") or reduce(
+ lambda prev, cur: prev or cur, [x.get("project") for x in doc.get("items")], None
+ ) # get first non-empty project from items
+
if pe.party_type in ["Customer", "Supplier"]:
bank_account = get_party_bank_account(pe.party_type, pe.party)
pe.set("bank_account", bank_account)
@@ -1531,44 +1856,57 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
# only Purchase Invoice can be blocked individually
if doc.doctype == "Purchase Invoice" and doc.invoice_is_blocked():
- frappe.msgprint(_('{0} is on hold till {1}').format(doc.name, doc.release_date))
+ 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('Payment Terms Template',
- {'name': doc.payment_terms_template}, 'allocate_payment_based_on_payment_terms')):
+ if doc.doctype in ("Sales Invoice", "Purchase Invoice") and frappe.get_value(
+ "Payment Terms Template",
+ {"name": doc.payment_terms_template},
+ "allocate_payment_based_on_payment_terms",
+ ):
- for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
- pe.append('references', reference)
+ for reference in get_reference_as_per_payment_terms(
+ doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount
+ ):
+ pe.append("references", reference)
else:
if dt == "Dunning":
- pe.append("references", {
- 'reference_doctype': 'Sales Invoice',
- 'reference_name': doc.get('sales_invoice'),
- "bill_no": doc.get("bill_no"),
- "due_date": doc.get("due_date"),
- 'total_amount': doc.get('outstanding_amount'),
- 'outstanding_amount': doc.get('outstanding_amount'),
- 'allocated_amount': doc.get('outstanding_amount')
- })
- pe.append("references", {
- 'reference_doctype': dt,
- 'reference_name': dn,
- "bill_no": doc.get("bill_no"),
- "due_date": doc.get("due_date"),
- 'total_amount': doc.get('dunning_amount'),
- 'outstanding_amount': doc.get('dunning_amount'),
- 'allocated_amount': doc.get('dunning_amount')
- })
+ pe.append(
+ "references",
+ {
+ "reference_doctype": "Sales Invoice",
+ "reference_name": doc.get("sales_invoice"),
+ "bill_no": doc.get("bill_no"),
+ "due_date": doc.get("due_date"),
+ "total_amount": doc.get("outstanding_amount"),
+ "outstanding_amount": doc.get("outstanding_amount"),
+ "allocated_amount": doc.get("outstanding_amount"),
+ },
+ )
+ pe.append(
+ "references",
+ {
+ "reference_doctype": dt,
+ "reference_name": dn,
+ "bill_no": doc.get("bill_no"),
+ "due_date": doc.get("due_date"),
+ "total_amount": doc.get("dunning_amount"),
+ "outstanding_amount": doc.get("dunning_amount"),
+ "allocated_amount": doc.get("dunning_amount"),
+ },
+ )
else:
- pe.append("references", {
- 'reference_doctype': dt,
- 'reference_name': dn,
- "bill_no": doc.get("bill_no"),
- "due_date": doc.get("due_date"),
- 'total_amount': grand_total,
- 'outstanding_amount': outstanding_amount,
- 'allocated_amount': outstanding_amount
- })
+ pe.append(
+ "references",
+ {
+ "reference_doctype": dt,
+ "reference_name": dn,
+ "bill_no": doc.get("bill_no"),
+ "due_date": doc.get("due_date"),
+ "total_amount": grand_total,
+ "outstanding_amount": outstanding_amount,
+ "allocated_amount": outstanding_amount,
+ },
+ )
pe.setup_party_account_field()
pe.set_missing_values()
@@ -1579,25 +1917,32 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount=
pe.set_exchange_rate(ref_doc=reference_doc)
pe.set_amounts()
if discount_amount:
- pe.set_gain_or_loss(account_details={
- 'account': frappe.get_cached_value('Company', pe.company, "default_discount_account"),
- 'cost_center': pe.cost_center or frappe.get_cached_value('Company', pe.company, "cost_center"),
- 'amount': discount_amount * (-1 if payment_type == "Pay" else 1)
- })
+ pe.set_gain_or_loss(
+ account_details={
+ "account": frappe.get_cached_value("Company", pe.company, "default_discount_account"),
+ "cost_center": pe.cost_center
+ or frappe.get_cached_value("Company", pe.company, "cost_center"),
+ "amount": discount_amount * (-1 if payment_type == "Pay" else 1),
+ }
+ )
pe.set_difference_amount()
return pe
+
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)
+ bank = get_default_bank_cash_account(
+ doc.company, "Bank", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
+ )
if not bank:
- bank = get_default_bank_cash_account(doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"),
- account=bank_account)
+ bank = get_default_bank_cash_account(
+ doc.company, "Cash", mode_of_payment=doc.get("mode_of_payment"), account=bank_account
+ )
return bank
+
def set_party_type(dt):
if dt in ("Sales Invoice", "Sales Order", "Dunning"):
party_type = "Customer"
@@ -1611,6 +1956,7 @@ def set_party_type(dt):
party_type = "Donor"
return party_type
+
def set_party_account(dt, dn, doc, party_type):
if dt == "Sales Invoice":
party_account = get_party_account_based_on_invoice_discounting(dn) or doc.debit_to
@@ -1628,6 +1974,7 @@ def set_party_account(dt, dn, doc, party_type):
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
return party_account
+
def set_party_account_currency(dt, party_account, doc):
if dt not in ("Sales Invoice", "Purchase Invoice"):
party_account_currency = get_account_currency(party_account)
@@ -1635,14 +1982,18 @@ def set_party_account_currency(dt, party_account, doc):
party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account)
return party_account_currency
+
def set_payment_type(dt, doc):
- if (dt in ("Sales Order", "Donation") or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
- or (dt=="Purchase Invoice" and doc.outstanding_amount < 0):
- payment_type = "Receive"
+ if (
+ dt in ("Sales Order", "Donation")
+ or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)
+ ) or (dt == "Purchase Invoice" and doc.outstanding_amount < 0):
+ payment_type = "Receive"
else:
payment_type = "Pay"
return payment_type
+
def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_currency, doc):
grand_total = outstanding_amount = 0
if party_amount:
@@ -1655,8 +2006,7 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
outstanding_amount = doc.outstanding_amount
elif dt in ("Expense Claim"):
grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges
- outstanding_amount = doc.grand_total \
- - doc.total_amount_reimbursed
+ outstanding_amount = doc.grand_total - doc.total_amount_reimbursed
elif dt == "Employee Advance":
grand_total = flt(doc.advance_amount)
outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount)
@@ -1683,7 +2033,10 @@ def set_grand_total_and_outstanding_amount(party_amount, dt, party_account_curre
outstanding_amount = grand_total - flt(doc.advance_paid)
return grand_total, outstanding_amount
-def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc):
+
+def set_paid_amount_and_received_amount(
+ dt, party_account_currency, bank, outstanding_amount, payment_type, bank_amount, doc
+):
paid_amount = received_amount = 0
if party_account_currency == bank.account_currency:
paid_amount = received_amount = abs(outstanding_amount)
@@ -1692,34 +2045,38 @@ def set_paid_amount_and_received_amount(dt, party_account_currency, bank, outsta
if bank_amount:
received_amount = bank_amount
else:
- received_amount = paid_amount * doc.get('conversion_rate', 1)
+ received_amount = paid_amount * doc.get("conversion_rate", 1)
if dt == "Employee Advance":
- received_amount = paid_amount * doc.get('exchange_rate', 1)
+ received_amount = paid_amount * doc.get("exchange_rate", 1)
else:
received_amount = abs(outstanding_amount)
if bank_amount:
paid_amount = bank_amount
else:
# if party account currency and bank currency is different then populate paid amount as well
- paid_amount = received_amount * doc.get('conversion_rate', 1)
+ paid_amount = received_amount * doc.get("conversion_rate", 1)
if dt == "Employee Advance":
- paid_amount = received_amount * doc.get('exchange_rate', 1)
+ paid_amount = received_amount * doc.get("exchange_rate", 1)
return paid_amount, received_amount
+
def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount = 0
- if doc.doctype in ['Sales Invoice', 'Purchase Invoice'] and doc.payment_schedule:
+ eligible_for_payments = ["Sales Order", "Sales Invoice", "Purchase Order", "Purchase Invoice"]
+ has_payment_schedule = hasattr(doc, "payment_schedule") and doc.payment_schedule
+
+ if doc.doctype in eligible_for_payments and has_payment_schedule:
for term in doc.payment_schedule:
if not term.discounted_amount and term.discount and getdate(nowdate()) <= term.discount_date:
- if term.discount_type == 'Percentage':
- discount_amount = flt(doc.get('grand_total')) * (term.discount / 100)
+ if term.discount_type == "Percentage":
+ discount_amount = flt(doc.get("grand_total")) * (term.discount / 100)
else:
discount_amount = term.discount
- discount_amount_in_foreign_currency = discount_amount * doc.get('conversion_rate', 1)
+ discount_amount_in_foreign_currency = discount_amount * doc.get("conversion_rate", 1)
- if doc.doctype == 'Sales Invoice':
+ if doc.doctype == "Sales Invoice":
paid_amount -= discount_amount
received_amount -= discount_amount_in_foreign_currency
else:
@@ -1729,38 +2086,46 @@ def apply_early_payment_discount(paid_amount, received_amount, doc):
total_discount += discount_amount
if total_discount:
- money = frappe.utils.fmt_money(total_discount, currency=doc.get('currency'))
+ money = frappe.utils.fmt_money(total_discount, currency=doc.get("currency"))
frappe.msgprint(_("Discount of {} applied as per Payment Term").format(money), alert=1)
return paid_amount, received_amount, total_discount
-def get_reference_as_per_payment_terms(payment_schedule, dt, dn, doc, grand_total, outstanding_amount):
+
+def get_reference_as_per_payment_terms(
+ payment_schedule, dt, dn, doc, grand_total, outstanding_amount
+):
references = []
for payment_term in payment_schedule:
- payment_term_outstanding = flt(payment_term.payment_amount - payment_term.paid_amount,
- payment_term.precision('payment_amount'))
+ payment_term_outstanding = flt(
+ payment_term.payment_amount - payment_term.paid_amount, payment_term.precision("payment_amount")
+ )
if payment_term_outstanding:
- references.append({
- 'reference_doctype': dt,
- 'reference_name': dn,
- 'bill_no': doc.get('bill_no'),
- 'due_date': doc.get('due_date'),
- 'total_amount': grand_total,
- 'outstanding_amount': outstanding_amount,
- 'payment_term': payment_term.payment_term,
- 'allocated_amount': payment_term_outstanding
- })
+ references.append(
+ {
+ "reference_doctype": dt,
+ "reference_name": dn,
+ "bill_no": doc.get("bill_no"),
+ "due_date": doc.get("due_date"),
+ "total_amount": grand_total,
+ "outstanding_amount": outstanding_amount,
+ "payment_term": payment_term.payment_term,
+ "allocated_amount": payment_term_outstanding,
+ }
+ )
return references
+
def get_paid_amount(dt, dn, party_type, party, account, due_date):
- if party_type=="Customer":
+ if party_type == "Customer":
dr_or_cr = "credit_in_account_currency - debit_in_account_currency"
else:
dr_or_cr = "debit_in_account_currency - credit_in_account_currency"
- paid_amount = frappe.db.sql("""
+ paid_amount = frappe.db.sql(
+ """
select ifnull(sum({dr_or_cr}), 0) as paid_amount
from `tabGL Entry`
where against_voucher_type = %s
@@ -1770,41 +2135,58 @@ def get_paid_amount(dt, dn, party_type, party, account, due_date):
and account = %s
and due_date = %s
and {dr_or_cr} > 0
- """.format(dr_or_cr=dr_or_cr), (dt, dn, party_type, party, account, due_date))
+ """.format(
+ dr_or_cr=dr_or_cr
+ ),
+ (dt, dn, party_type, party, account, due_date),
+ )
return paid_amount[0][0] if paid_amount else 0
+
@frappe.whitelist()
-def get_party_and_account_balance(company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None):
- return frappe._dict({
- "party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
- "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
- "paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center)
- })
+def get_party_and_account_balance(
+ company, date, paid_from=None, paid_to=None, ptype=None, pty=None, cost_center=None
+):
+ return frappe._dict(
+ {
+ "party_balance": get_balance_on(party_type=ptype, party=pty, cost_center=cost_center),
+ "paid_from_account_balance": get_balance_on(paid_from, date, cost_center=cost_center),
+ "paid_to_account_balance": get_balance_on(paid_to, date=date, cost_center=cost_center),
+ }
+ )
+
@frappe.whitelist()
def make_payment_order(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
+
def set_missing_values(source, target):
target.payment_order_type = "Payment Entry"
- target.append('references', dict(
- reference_doctype="Payment Entry",
- reference_name=source.name,
- bank_account=source.party_bank_account,
- amount=source.paid_amount,
- account=source.paid_to,
- supplier=source.party,
- mode_of_payment=source.mode_of_payment,
- ))
+ target.append(
+ "references",
+ dict(
+ reference_doctype="Payment Entry",
+ reference_name=source.name,
+ bank_account=source.party_bank_account,
+ amount=source.paid_amount,
+ account=source.paid_to,
+ supplier=source.party,
+ mode_of_payment=source.mode_of_payment,
+ ),
+ )
- doclist = get_mapped_doc("Payment Entry", source_name, {
- "Payment Entry": {
- "doctype": "Payment Order",
- "validation": {
- "docstatus": ["=", 1]
- },
- }
-
- }, target_doc, set_missing_values)
+ doclist = get_mapped_doc(
+ "Payment Entry",
+ source_name,
+ {
+ "Payment Entry": {
+ "doctype": "Payment Order",
+ "validation": {"docstatus": ["=", 1]},
+ }
+ },
+ target_doc,
+ set_missing_values,
+ )
return doclist
diff --git a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
index cc3528e9aaa..004c84c0221 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -4,6 +4,7 @@
import unittest
import frappe
+from frappe import qb
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.payment_entry.payment_entry import (
@@ -32,10 +33,9 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["Debtors - _TC", 0, 1000, so.name],
- ["_Test Cash - _TC", 1000.0, 0, None]
- ])
+ expected_gle = dict(
+ (d[0], d) for d in [["Debtors - _TC", 0, 1000, so.name], ["_Test Cash - _TC", 1000.0, 0, None]]
+ )
self.validate_gl_entries(pe.name, expected_gle)
@@ -48,9 +48,9 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(so_advance_paid, 0)
def test_payment_entry_for_blocked_supplier_invoice(self):
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Invoices'
+ supplier.hold_type = "Invoices"
supplier.save()
self.assertRaises(frappe.ValidationError, make_purchase_invoice)
@@ -59,32 +59,40 @@ class TestPaymentEntry(unittest.TestCase):
supplier.save()
def test_payment_entry_for_blocked_supplier_payments(self):
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Payments'
+ supplier.hold_type = "Payments"
supplier.save()
pi = make_purchase_invoice()
self.assertRaises(
- frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name,
- bank_account="_Test Bank - _TC")
+ frappe.ValidationError,
+ get_payment_entry,
+ dt="Purchase Invoice",
+ dn=pi.name,
+ bank_account="_Test Bank - _TC",
+ )
supplier.on_hold = 0
supplier.save()
def test_payment_entry_for_blocked_supplier_payments_today_date(self):
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Payments'
+ supplier.hold_type = "Payments"
supplier.release_date = nowdate()
supplier.save()
pi = make_purchase_invoice()
self.assertRaises(
- frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name,
- bank_account="_Test Bank - _TC")
+ frappe.ValidationError,
+ get_payment_entry,
+ dt="Purchase Invoice",
+ dn=pi.name,
+ bank_account="_Test Bank - _TC",
+ )
supplier.on_hold = 0
supplier.save()
@@ -93,15 +101,15 @@ class TestPaymentEntry(unittest.TestCase):
# this test is meant to fail only if something fails in the try block
with self.assertRaises(Exception):
try:
- supplier = frappe.get_doc('Supplier', '_Test Supplier')
+ supplier = frappe.get_doc("Supplier", "_Test Supplier")
supplier.on_hold = 1
- supplier.hold_type = 'Payments'
- supplier.release_date = '2018-03-01'
+ supplier.hold_type = "Payments"
+ supplier.release_date = "2018-03-01"
supplier.save()
pi = make_purchase_invoice()
- get_payment_entry('Purchase Invoice', pi.name, bank_account="_Test Bank - _TC")
+ get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
supplier.on_hold = 0
supplier.save()
@@ -111,8 +119,12 @@ class TestPaymentEntry(unittest.TestCase):
raise Exception
def test_payment_entry_against_si_usd_to_usd(self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
@@ -120,10 +132,13 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Receivable USD - _TC", 0, 5000, si.name],
- ["_Test Bank USD - _TC", 5000.0, 0, None]
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Receivable USD - _TC", 0, 5000, si.name],
+ ["_Test Bank USD - _TC", 5000.0, 0, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
@@ -136,8 +151,12 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(outstanding_amount, 100)
def test_payment_entry_against_pi(self):
- pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC",
- currency="USD", conversion_rate=50)
+ pi = make_purchase_invoice(
+ supplier="_Test Supplier USD",
+ debit_to="_Test Payable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
@@ -145,20 +164,26 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Payable USD - _TC", 12500, 0, pi.name],
- ["_Test Bank USD - _TC", 0, 12500, None]
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Payable USD - _TC", 12500, 0, pi.name],
+ ["_Test Bank USD - _TC", 0, 12500, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", pi.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 0)
-
def test_payment_against_sales_invoice_to_check_status(self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
@@ -167,28 +192,35 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- outstanding_amount, status = frappe.db.get_value("Sales Invoice", si.name, ["outstanding_amount", "status"])
+ outstanding_amount, status = frappe.db.get_value(
+ "Sales Invoice", si.name, ["outstanding_amount", "status"]
+ )
self.assertEqual(flt(outstanding_amount), 0)
- self.assertEqual(status, 'Paid')
+ self.assertEqual(status, "Paid")
pe.cancel()
- outstanding_amount, status = frappe.db.get_value("Sales Invoice", si.name, ["outstanding_amount", "status"])
+ outstanding_amount, status = frappe.db.get_value(
+ "Sales Invoice", si.name, ["outstanding_amount", "status"]
+ )
self.assertEqual(flt(outstanding_amount), 100)
- self.assertEqual(status, 'Unpaid')
+ self.assertEqual(status, "Unpaid")
def test_payment_entry_against_payment_terms(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template()
- si.payment_terms_template = 'Test Receivable Template'
+ si.payment_terms_template = "Test Receivable Template"
- si.append('taxes', {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 18
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 18,
+ },
+ )
si.save()
si.submit()
@@ -197,25 +229,28 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
si.load_from_db()
- self.assertEqual(pe.references[0].payment_term, 'Basic Amount Receivable')
- self.assertEqual(pe.references[1].payment_term, 'Tax Receivable')
+ self.assertEqual(pe.references[0].payment_term, "Basic Amount Receivable")
+ self.assertEqual(pe.references[1].payment_term, "Tax Receivable")
self.assertEqual(si.payment_schedule[0].paid_amount, 200.0)
self.assertEqual(si.payment_schedule[1].paid_amount, 36.0)
def test_payment_entry_against_payment_terms_with_discount(self):
si = create_sales_invoice(do_not_save=1, qty=1, rate=200)
create_payment_terms_template_with_discount()
- si.payment_terms_template = 'Test Discount Template'
+ si.payment_terms_template = "Test Discount Template"
- frappe.db.set_value('Company', si.company, 'default_discount_account', 'Write Off - _TC')
+ frappe.db.set_value("Company", si.company, "default_discount_account", "Write Off - _TC")
- si.append('taxes', {
- "charge_type": "On Net Total",
- "account_head": "_Test Account Service Tax - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "description": "Service Tax",
- "rate": 18
- })
+ si.append(
+ "taxes",
+ {
+ "charge_type": "On Net Total",
+ "account_head": "_Test Account Service Tax - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "description": "Service Tax",
+ "rate": 18,
+ },
+ )
si.save()
si.submit()
@@ -224,16 +259,19 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
si.load_from_db()
- self.assertEqual(pe.references[0].payment_term, '30 Credit Days with 10% Discount')
+ self.assertEqual(pe.references[0].payment_term, "30 Credit Days with 10% Discount")
self.assertEqual(si.payment_schedule[0].payment_amount, 236.0)
self.assertEqual(si.payment_schedule[0].paid_amount, 212.40)
self.assertEqual(si.payment_schedule[0].outstanding, 0)
self.assertEqual(si.payment_schedule[0].discounted_amount, 23.6)
-
def test_payment_against_purchase_invoice_to_check_status(self):
- pi = make_purchase_invoice(supplier="_Test Supplier USD", debit_to="_Test Payable USD - _TC",
- currency="USD", conversion_rate=50)
+ pi = make_purchase_invoice(
+ supplier="_Test Supplier USD",
+ debit_to="_Test Payable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
@@ -242,21 +280,27 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- outstanding_amount, status = frappe.db.get_value("Purchase Invoice", pi.name, ["outstanding_amount", "status"])
+ outstanding_amount, status = frappe.db.get_value(
+ "Purchase Invoice", pi.name, ["outstanding_amount", "status"]
+ )
self.assertEqual(flt(outstanding_amount), 0)
- self.assertEqual(status, 'Paid')
+ self.assertEqual(status, "Paid")
pe.cancel()
- outstanding_amount, status = frappe.db.get_value("Purchase Invoice", pi.name, ["outstanding_amount", "status"])
+ outstanding_amount, status = frappe.db.get_value(
+ "Purchase Invoice", pi.name, ["outstanding_amount", "status"]
+ )
self.assertEqual(flt(outstanding_amount), 250)
- self.assertEqual(status, 'Unpaid')
+ self.assertEqual(status, "Unpaid")
def test_payment_entry_against_ec(self):
- payable = frappe.get_cached_value('Company', "_Test Company", 'default_payable_account')
+ payable = frappe.get_cached_value("Company", "_Test Company", "default_payable_account")
ec = make_expense_claim(payable, 300, 300, "_Test Company", "Travel Expenses - _TC")
- pe = get_payment_entry("Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300)
+ pe = get_payment_entry(
+ "Expense Claim", ec.name, bank_account="_Test Bank USD - _TC", bank_amount=300
+ )
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
pe.source_exchange_rate = 1
@@ -264,68 +308,87 @@ class TestPaymentEntry(unittest.TestCase):
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- [payable, 300, 0, ec.name],
- ["_Test Bank USD - _TC", 0, 300, None]
- ])
+ expected_gle = dict(
+ (d[0], d) for d in [[payable, 300, 0, ec.name], ["_Test Bank USD - _TC", 0, 300, None]]
+ )
self.validate_gl_entries(pe.name, expected_gle)
- outstanding_amount = flt(frappe.db.get_value("Expense Claim", ec.name, "total_sanctioned_amount")) - \
- flt(frappe.db.get_value("Expense Claim", ec.name, "total_amount_reimbursed"))
+ outstanding_amount = flt(
+ frappe.db.get_value("Expense Claim", ec.name, "total_sanctioned_amount")
+ ) - flt(frappe.db.get_value("Expense Claim", ec.name, "total_amount_reimbursed"))
self.assertEqual(outstanding_amount, 0)
def test_payment_entry_against_si_usd_to_inr(self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
- pe = get_payment_entry("Sales Invoice", si.name, party_amount=20,
- bank_account="_Test Bank - _TC", bank_amount=900)
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
+ pe = get_payment_entry(
+ "Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank - _TC", bank_amount=900
+ )
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
self.assertEqual(pe.difference_amount, 100)
- pe.append("deductions", {
- "account": "_Test Exchange Gain/Loss - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "amount": 100
- })
+ pe.append(
+ "deductions",
+ {
+ "account": "_Test Exchange Gain/Loss - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "amount": 100,
+ },
+ )
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Receivable USD - _TC", 0, 1000, si.name],
- ["_Test Bank - _TC", 900, 0, None],
- ["_Test Exchange Gain/Loss - _TC", 100.0, 0, None],
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Receivable USD - _TC", 0, 1000, si.name],
+ ["_Test Bank - _TC", 900, 0, None],
+ ["_Test Exchange Gain/Loss - _TC", 100.0, 0, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
outstanding_amount = flt(frappe.db.get_value("Sales Invoice", si.name, "outstanding_amount"))
self.assertEqual(outstanding_amount, 80)
- def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency (self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50, do_not_save=1)
+ def test_payment_entry_against_si_usd_to_usd_with_deduction_in_base_currency(self):
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ do_not_save=1,
+ )
si.plc_conversion_rate = 50
si.save()
si.submit()
- pe = get_payment_entry("Sales Invoice", si.name, party_amount=20,
- bank_account="_Test Bank USD - _TC", bank_amount=900)
+ pe = get_payment_entry(
+ "Sales Invoice", si.name, party_amount=20, bank_account="_Test Bank USD - _TC", bank_amount=900
+ )
pe.source_exchange_rate = 45.263
pe.target_exchange_rate = 45.263
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
-
- pe.append("deductions", {
- "account": "_Test Exchange Gain/Loss - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "amount": 94.80
- })
+ pe.append(
+ "deductions",
+ {
+ "account": "_Test Exchange Gain/Loss - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "amount": 94.80,
+ },
+ )
pe.save()
@@ -359,8 +422,7 @@ class TestPaymentEntry(unittest.TestCase):
pe.set_amounts()
self.assertEqual(
- pe.source_exchange_rate, 65.1,
- "{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1)
+ pe.source_exchange_rate, 65.1, "{0} is not equal to {1}".format(pe.source_exchange_rate, 65.1)
)
def test_internal_transfer_usd_to_inr(self):
@@ -382,20 +444,26 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(pe.difference_amount, 500)
- pe.append("deductions", {
- "account": "_Test Exchange Gain/Loss - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "amount": 500
- })
+ pe.append(
+ "deductions",
+ {
+ "account": "_Test Exchange Gain/Loss - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "amount": 500,
+ },
+ )
pe.insert()
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Bank USD - _TC", 0, 5000, None],
- ["_Test Bank - _TC", 4500, 0, None],
- ["_Test Exchange Gain/Loss - _TC", 500.0, 0, None],
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Bank USD - _TC", 0, 5000, None],
+ ["_Test Bank - _TC", 4500, 0, None],
+ ["_Test Exchange Gain/Loss - _TC", 500.0, 0, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
@@ -435,10 +503,9 @@ class TestPaymentEntry(unittest.TestCase):
pe3.insert()
pe3.submit()
- expected_gle = dict((d[0], d) for d in [
- ["Debtors - _TC", 100, 0, si1.name],
- ["_Test Cash - _TC", 0, 100, None]
- ])
+ expected_gle = dict(
+ (d[0], d) for d in [["Debtors - _TC", 100, 0, si1.name], ["_Test Cash - _TC", 0, 100, None]]
+ )
self.validate_gl_entries(pe3.name, expected_gle)
@@ -462,12 +529,16 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(expected_gle[gle.account][3], gle.against_voucher)
def get_gle(self, voucher_no):
- return frappe.db.sql("""select account, debit, credit, against_voucher
+ return frappe.db.sql(
+ """select account, debit, credit, against_voucher
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
- order by account asc""", voucher_no, as_dict=1)
+ order by account asc""",
+ voucher_no,
+ as_dict=1,
+ )
def test_payment_entry_write_off_difference(self):
- si = create_sales_invoice()
+ si = create_sales_invoice()
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
@@ -477,11 +548,10 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(pe.unallocated_amount, 10)
pe.received_amount = pe.paid_amount = 95
- pe.append("deductions", {
- "account": "_Test Write Off - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "amount": 5
- })
+ pe.append(
+ "deductions",
+ {"account": "_Test Write Off - _TC", "cost_center": "_Test Cost Center - _TC", "amount": 5},
+ )
pe.save()
self.assertEqual(pe.unallocated_amount, 0)
@@ -489,27 +559,37 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["Debtors - _TC", 0, 100, si.name],
- ["_Test Cash - _TC", 95, 0, None],
- ["_Test Write Off - _TC", 5, 0, None]
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["Debtors - _TC", 0, 100, si.name],
+ ["_Test Cash - _TC", 95, 0, None],
+ ["_Test Write Off - _TC", 5, 0, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
def test_payment_entry_exchange_gain_loss(self):
- si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
- currency="USD", conversion_rate=50)
+ si = create_sales_invoice(
+ customer="_Test Customer USD",
+ debit_to="_Test Receivable USD - _TC",
+ currency="USD",
+ conversion_rate=50,
+ )
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank USD - _TC")
pe.reference_no = "1"
pe.reference_date = "2016-01-01"
pe.source_exchange_rate = 55
- pe.append("deductions", {
- "account": "_Test Exchange Gain/Loss - _TC",
- "cost_center": "_Test Cost Center - _TC",
- "amount": -500
- })
+ pe.append(
+ "deductions",
+ {
+ "account": "_Test Exchange Gain/Loss - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ "amount": -500,
+ },
+ )
pe.save()
self.assertEqual(pe.unallocated_amount, 0)
@@ -517,11 +597,14 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
- expected_gle = dict((d[0], d) for d in [
- ["_Test Receivable USD - _TC", 0, 5000, si.name],
- ["_Test Bank USD - _TC", 5500, 0, None],
- ["_Test Exchange Gain/Loss - _TC", 0, 500, None],
- ])
+ expected_gle = dict(
+ (d[0], d)
+ for d in [
+ ["_Test Receivable USD - _TC", 0, 5000, si.name],
+ ["_Test Bank USD - _TC", 5500, 0, None],
+ ["_Test Exchange Gain/Loss - _TC", 0, 500, None],
+ ]
+ )
self.validate_gl_entries(pe.name, expected_gle)
@@ -530,10 +613,11 @@ class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_against_sales_invoice_with_cost_centre(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
+ si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
self.assertEqual(pe.cost_center, si.cost_center)
@@ -546,18 +630,18 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
expected_values = {
- "_Test Bank - _TC": {
- "cost_center": cost_center
- },
- "Debtors - _TC": {
- "cost_center": cost_center
- }
+ "_Test Bank - _TC": {"cost_center": cost_center},
+ "Debtors - _TC": {"cost_center": cost_center},
}
- gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
- order by account asc""", pe.name, as_dict=1)
+ order by account asc""",
+ pe.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -566,10 +650,13 @@ class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_against_purchase_invoice_with_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC")
+ pi = make_purchase_invoice_against_cost_center(
+ cost_center=cost_center, credit_to="Creditors - _TC"
+ )
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
self.assertEqual(pe.cost_center, pi.cost_center)
@@ -582,18 +669,18 @@ class TestPaymentEntry(unittest.TestCase):
pe.submit()
expected_values = {
- "_Test Bank - _TC": {
- "cost_center": cost_center
- },
- "Creditors - _TC": {
- "cost_center": cost_center
- }
+ "_Test Bank - _TC": {"cost_center": cost_center},
+ "Creditors - _TC": {"cost_center": cost_center},
}
- gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit,
+ gl_entries = frappe.db.sql(
+ """select account, cost_center, account_currency, debit, credit,
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s
- order by account asc""", pe.name, as_dict=1)
+ order by account asc""",
+ pe.name,
+ as_dict=1,
+ )
self.assertTrue(gl_entries)
@@ -603,13 +690,16 @@ class TestPaymentEntry(unittest.TestCase):
def test_payment_entry_account_and_party_balance_with_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.utils import get_balance_on
+
cost_center = "_Test Cost Center for BS Account - _TC"
create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company")
- si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
+ si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC")
account_balance = get_balance_on(account="_Test Bank - _TC", cost_center=si.cost_center)
- party_balance = get_balance_on(party_type="Customer", party=si.customer, cost_center=si.cost_center)
+ party_balance = get_balance_on(
+ party_type="Customer", party=si.customer, cost_center=si.cost_center
+ )
party_account_balance = get_balance_on(si.debit_to, cost_center=si.cost_center)
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
@@ -633,56 +723,165 @@ class TestPaymentEntry(unittest.TestCase):
self.assertEqual(flt(expected_party_balance), party_balance)
self.assertEqual(flt(expected_party_account_balance), party_account_balance)
+ def test_multi_currency_payment_entry_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": 10,
+ "add_deduct_tax": "Add",
+ "description": "Test",
+ },
+ )
+
+ payment_entry.save()
+ self.assertEqual(payment_entry.base_total_taxes_and_charges, 10)
+ self.assertEqual(
+ 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()
+
+ pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank USD - _TC")
+ pe.reference_no = "1"
+ pe.reference_date = "2016-01-01"
+
+ # block invoice after creating payment entry
+ # since `get_payment_entry` will not attach blocked invoice to payment
+ pi.block_invoice()
+ with self.assertRaises(frappe.ValidationError) as err:
+ pe.save()
+
+ self.assertTrue("is on hold" in str(err.exception).lower())
+
+
+def create_payment_entry(**args):
+ payment_entry = frappe.new_doc("Payment Entry")
+ payment_entry.company = args.get("company") or "_Test Company"
+ payment_entry.payment_type = args.get("payment_type") or "Pay"
+ payment_entry.party_type = args.get("party_type") or "Supplier"
+ payment_entry.party = args.get("party") or "_Test Supplier"
+ payment_entry.paid_from = args.get("paid_from") or "_Test Bank - _TC"
+ payment_entry.paid_to = args.get("paid_to") or "Creditors - _TC"
+ payment_entry.paid_amount = args.get("paid_amount") or 1000
+
+ payment_entry.setup_party_account_field()
+ payment_entry.set_missing_values()
+ payment_entry.set_exchange_rate()
+ payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate
+ payment_entry.reference_no = "Test001"
+ payment_entry.reference_date = nowdate()
+
+ if args.get("save"):
+ payment_entry.save()
+ if args.get("submit"):
+ payment_entry.submit()
+
+ return payment_entry
+
+
def create_payment_terms_template():
- create_payment_term('Basic Amount Receivable')
- create_payment_term('Tax Receivable')
+ create_payment_term("Basic Amount Receivable")
+ create_payment_term("Tax Receivable")
- if not frappe.db.exists('Payment Terms Template', 'Test Receivable Template'):
- payment_term_template = frappe.get_doc({
- 'doctype': 'Payment Terms Template',
- 'template_name': 'Test Receivable Template',
- 'allocate_payment_based_on_payment_terms': 1,
- 'terms': [{
- 'doctype': 'Payment Terms Template Detail',
- 'payment_term': 'Basic Amount Receivable',
- 'invoice_portion': 84.746,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 1
- },
+ if not frappe.db.exists("Payment Terms Template", "Test Receivable Template"):
+ payment_term_template = frappe.get_doc(
{
- 'doctype': 'Payment Terms Template Detail',
- 'payment_term': 'Tax Receivable',
- 'invoice_portion': 15.254,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 2
- }]
- }).insert()
+ "doctype": "Payment Terms Template",
+ "template_name": "Test Receivable Template",
+ "allocate_payment_based_on_payment_terms": 1,
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "payment_term": "Basic Amount Receivable",
+ "invoice_portion": 84.746,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 1,
+ },
+ {
+ "doctype": "Payment Terms Template Detail",
+ "payment_term": "Tax Receivable",
+ "invoice_portion": 15.254,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 2,
+ },
+ ],
+ }
+ ).insert()
+
def create_payment_terms_template_with_discount():
- create_payment_term('30 Credit Days with 10% Discount')
+ create_payment_term("30 Credit Days with 10% Discount")
+
+ if not frappe.db.exists("Payment Terms Template", "Test Discount Template"):
+ payment_term_template = frappe.get_doc(
+ {
+ "doctype": "Payment Terms Template",
+ "template_name": "Test Discount Template",
+ "allocate_payment_based_on_payment_terms": 1,
+ "terms": [
+ {
+ "doctype": "Payment Terms Template Detail",
+ "payment_term": "30 Credit Days with 10% Discount",
+ "invoice_portion": 100,
+ "credit_days_based_on": "Day(s) after invoice date",
+ "credit_days": 2,
+ "discount": 10,
+ "discount_validity_based_on": "Day(s) after invoice date",
+ "discount_validity": 1,
+ }
+ ],
+ }
+ ).insert()
- if not frappe.db.exists('Payment Terms Template', 'Test Discount Template'):
- payment_term_template = frappe.get_doc({
- 'doctype': 'Payment Terms Template',
- 'template_name': 'Test Discount Template',
- 'allocate_payment_based_on_payment_terms': 1,
- 'terms': [{
- 'doctype': 'Payment Terms Template Detail',
- 'payment_term': '30 Credit Days with 10% Discount',
- 'invoice_portion': 100,
- 'credit_days_based_on': 'Day(s) after invoice date',
- 'credit_days': 2,
- 'discount': 10,
- 'discount_validity_based_on': 'Day(s) after invoice date',
- 'discount_validity': 1
- }]
- }).insert()
def create_payment_term(name):
- if not frappe.db.exists('Payment Term', name):
- frappe.get_doc({
- 'doctype': 'Payment Term',
- 'payment_term_name': name
- }).insert()
+ if not frappe.db.exists("Payment Term", name):
+ frappe.get_doc({"doctype": "Payment Term", "payment_term_name": name}).insert()
diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py
index 25dc4e6a60d..ab47b6151cd 100644
--- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py
+++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account.py
@@ -18,10 +18,13 @@ class PaymentGatewayAccount(Document):
def update_default_payment_gateway(self):
if self.is_default:
- frappe.db.sql("""update `tabPayment Gateway Account` set is_default = 0
- where is_default = 1 """)
+ frappe.db.sql(
+ """update `tabPayment Gateway Account` set is_default = 0
+ where is_default = 1 """
+ )
def set_as_default_if_not_set(self):
- if not frappe.db.get_value("Payment Gateway Account",
- {"is_default": 1, "name": ("!=", self.name)}, "name"):
+ if not frappe.db.get_value(
+ "Payment Gateway Account", {"is_default": 1, "name": ("!=", self.name)}, "name"
+ ):
self.is_default = 1
diff --git a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py
index bb0fc975cac..d0aaee88350 100644
--- a/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py
+++ b/erpnext/accounts/doctype/payment_gateway_account/payment_gateway_account_dashboard.py
@@ -1,17 +1,6 @@
-
-
def get_data():
return {
- 'fieldname': 'payment_gateway_account',
- 'non_standard_fieldnames': {
- 'Subscription Plan': 'payment_gateway'
- },
- 'transactions': [
- {
- 'items': ['Payment Request']
- },
- {
- 'items': ['Subscription Plan']
- }
- ]
+ "fieldname": "payment_gateway_account",
+ "non_standard_fieldnames": {"Subscription Plan": "payment_gateway"},
+ "transactions": [{"items": ["Payment Request"]}, {"items": ["Subscription Plan"]}],
}
diff --git a/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py b/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py
index 1895c12ad7c..7a8cdf73355 100644
--- a/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py
+++ b/erpnext/accounts/doctype/payment_gateway_account/test_payment_gateway_account.py
@@ -5,5 +5,6 @@ import unittest
# test_records = frappe.get_test_records('Payment Gateway Account')
+
class TestPaymentGatewayAccount(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js
index 9074defa577..7d85d89c452 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.js
+++ b/erpnext/accounts/doctype/payment_order/payment_order.js
@@ -12,7 +12,6 @@ frappe.ui.form.on('Payment Order', {
});
frm.set_df_property('references', 'cannot_add_rows', true);
- frm.set_df_property('references', 'cannot_delete_rows', true);
},
refresh: function(frm) {
if (frm.doc.docstatus == 0) {
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.py b/erpnext/accounts/doctype/payment_order/payment_order.py
index 50a58b8a0ab..3c45d20770d 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.py
+++ b/erpnext/accounts/doctype/payment_order/payment_order.py
@@ -18,9 +18,9 @@ class PaymentOrder(Document):
self.update_payment_status(cancel=True)
def update_payment_status(self, cancel=False):
- status = 'Payment Ordered'
+ status = "Payment Ordered"
if cancel:
- status = 'Initiated'
+ status = "Initiated"
if self.payment_order_type == "Payment Request":
ref_field = "status"
@@ -32,67 +32,67 @@ class PaymentOrder(Document):
for d in self.references:
frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status)
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_mop_query(doctype, txt, searchfield, start, page_len, filters):
- return frappe.db.sql(""" select mode_of_payment from `tabPayment Order Reference`
+ return frappe.db.sql(
+ """ select mode_of_payment from `tabPayment Order Reference`
where parent = %(parent)s and mode_of_payment like %(txt)s
- limit %(start)s, %(page_len)s""", {
- 'parent': filters.get("parent"),
- 'start': start,
- 'page_len': page_len,
- 'txt': "%%%s%%" % txt
- })
+ limit %(start)s, %(page_len)s""",
+ {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
+ )
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_supplier_query(doctype, txt, searchfield, start, page_len, filters):
- return frappe.db.sql(""" select supplier from `tabPayment Order Reference`
+ return frappe.db.sql(
+ """ select supplier from `tabPayment Order Reference`
where parent = %(parent)s and supplier like %(txt)s and
(payment_reference is null or payment_reference='')
- limit %(start)s, %(page_len)s""", {
- 'parent': filters.get("parent"),
- 'start': start,
- 'page_len': page_len,
- 'txt': "%%%s%%" % txt
- })
+ limit %(start)s, %(page_len)s""",
+ {"parent": filters.get("parent"), "start": start, "page_len": page_len, "txt": "%%%s%%" % txt},
+ )
+
@frappe.whitelist()
def make_payment_records(name, supplier, mode_of_payment=None):
- doc = frappe.get_doc('Payment Order', name)
+ doc = frappe.get_doc("Payment Order", name)
make_journal_entry(doc, supplier, mode_of_payment)
+
def make_journal_entry(doc, supplier, mode_of_payment=None):
- je = frappe.new_doc('Journal Entry')
+ je = frappe.new_doc("Journal Entry")
je.payment_order = doc.name
je.posting_date = nowdate()
- mode_of_payment_type = frappe._dict(frappe.get_all('Mode of Payment',
- fields = ["name", "type"], as_list=1))
+ mode_of_payment_type = frappe._dict(
+ frappe.get_all("Mode of Payment", fields=["name", "type"], as_list=1)
+ )
- je.voucher_type = 'Bank Entry'
- if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == 'Cash':
+ je.voucher_type = "Bank Entry"
+ if mode_of_payment and mode_of_payment_type.get(mode_of_payment) == "Cash":
je.voucher_type = "Cash Entry"
paid_amt = 0
- party_account = get_party_account('Supplier', supplier, doc.company)
+ party_account = get_party_account("Supplier", supplier, doc.company)
for d in doc.references:
- if (d.supplier == supplier
- and (not mode_of_payment or mode_of_payment == d.mode_of_payment)):
- je.append('accounts', {
- 'account': party_account,
- 'debit_in_account_currency': d.amount,
- 'party_type': 'Supplier',
- 'party': supplier,
- 'reference_type': d.reference_doctype,
- 'reference_name': d.reference_name
- })
+ if d.supplier == supplier and (not mode_of_payment or mode_of_payment == d.mode_of_payment):
+ je.append(
+ "accounts",
+ {
+ "account": party_account,
+ "debit_in_account_currency": d.amount,
+ "party_type": "Supplier",
+ "party": supplier,
+ "reference_type": d.reference_doctype,
+ "reference_name": d.reference_name,
+ },
+ )
paid_amt += d.amount
- je.append('accounts', {
- 'account': doc.account,
- 'credit_in_account_currency': paid_amt
- })
+ je.append("accounts", {"account": doc.account, "credit_in_account_currency": paid_amt})
je.flags.ignore_mandatory = True
je.save()
diff --git a/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py b/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py
index 02da9793895..f82886e7c26 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py
+++ b/erpnext/accounts/doctype/payment_order/payment_order_dashboard.py
@@ -1,11 +1,5 @@
-
-
def get_data():
return {
- 'fieldname': 'payment_order',
- 'transactions': [
- {
- 'items': ['Payment Entry', 'Journal Entry']
- }
- ]
+ "fieldname": "payment_order",
+ "transactions": [{"items": ["Payment Entry", "Journal Entry"]}],
}
diff --git a/erpnext/accounts/doctype/payment_order/test_payment_order.py b/erpnext/accounts/doctype/payment_order/test_payment_order.py
index 3f4d89b4eaf..0dcb1794b9a 100644
--- a/erpnext/accounts/doctype/payment_order/test_payment_order.py
+++ b/erpnext/accounts/doctype/payment_order/test_payment_order.py
@@ -26,7 +26,9 @@ class TestPaymentOrder(unittest.TestCase):
def test_payment_order_creation_against_payment_entry(self):
purchase_invoice = make_purchase_invoice()
- payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC")
+ payment_entry = get_payment_entry(
+ "Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC"
+ )
payment_entry.reference_no = "_Test_Payment_Order"
payment_entry.reference_date = getdate()
payment_entry.party_bank_account = "Checking Account - Citi Bank"
@@ -40,13 +42,16 @@ class TestPaymentOrder(unittest.TestCase):
self.assertEqual(reference_doc.supplier, "_Test Supplier")
self.assertEqual(reference_doc.amount, 250)
+
def create_payment_order_against_payment_entry(ref_doc, order_type):
- payment_order = frappe.get_doc(dict(
- doctype="Payment Order",
- company="_Test Company",
- payment_order_type=order_type,
- company_bank_account="Checking Account - Citi Bank"
- ))
+ payment_order = frappe.get_doc(
+ dict(
+ doctype="Payment Order",
+ company="_Test Company",
+ payment_order_type=order_type,
+ company_bank_account="Checking Account - Citi Bank",
+ )
+ )
doc = make_payment_order(ref_doc.name, payment_order)
doc.save()
doc.submit()
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index 648d2da754e..867fcc7f13e 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -1,4 +1,4 @@
-// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors
// For license information, please see license.txt
frappe.provide("erpnext.accounts");
@@ -38,6 +38,15 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
]
};
});
+
+ this.frm.set_query("cost_center", () => {
+ return {
+ "filters": {
+ "company": this.frm.doc.company,
+ "is_group": 0
+ }
+ }
+ });
},
refresh: function() {
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
index eb0c20f92d9..18d34850850 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
@@ -24,6 +24,7 @@
"invoice_limit",
"payment_limit",
"bank_cash_account",
+ "cost_center",
"sec_break1",
"invoices",
"column_break_15",
@@ -178,13 +179,19 @@
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "options": "Cost Center"
}
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"issingle": 1,
"links": [],
- "modified": "2021-10-04 20:27:11.114194",
+ "modified": "2022-04-29 15:37:10.246831",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
@@ -209,5 +216,6 @@
],
"sort_field": "modified",
"sort_order": "DESC",
+ "states": [],
"track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 548571d1d76..d6e07ab31e0 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -8,7 +8,11 @@ from frappe.model.document import Document
from frappe.utils import flt, getdate, nowdate, today
import erpnext
-from erpnext.accounts.utils import get_outstanding_invoices, reconcile_against_document
+from erpnext.accounts.utils import (
+ get_outstanding_invoices,
+ reconcile_against_document,
+ update_reference_in_payment_entry,
+)
from erpnext.controllers.accounts_controller import get_advance_payment_entries
@@ -32,30 +36,43 @@ class PaymentReconciliation(Document):
non_reconciled_payments = payment_entries + journal_entries + dr_or_cr_notes
if self.payment_limit:
- non_reconciled_payments = non_reconciled_payments[:self.payment_limit]
+ non_reconciled_payments = non_reconciled_payments[: self.payment_limit]
- non_reconciled_payments = sorted(non_reconciled_payments, key=lambda k: k['posting_date'] or getdate(nowdate()))
+ non_reconciled_payments = sorted(
+ non_reconciled_payments, key=lambda k: k["posting_date"] or getdate(nowdate())
+ )
self.add_payment_entries(non_reconciled_payments)
def get_payment_entries(self):
- order_doctype = "Sales Order" if self.party_type=="Customer" else "Purchase Order"
+ order_doctype = "Sales Order" if self.party_type == "Customer" else "Purchase Order"
condition = self.get_conditions(get_payments=True)
- payment_entries = get_advance_payment_entries(self.party_type, self.party,
- self.receivable_payable_account, order_doctype, against_all_orders=True, limit=self.payment_limit,
- condition=condition)
+ payment_entries = get_advance_payment_entries(
+ self.party_type,
+ self.party,
+ self.receivable_payable_account,
+ order_doctype,
+ against_all_orders=True,
+ limit=self.payment_limit,
+ condition=condition,
+ )
return payment_entries
def get_jv_entries(self):
condition = self.get_conditions()
- dr_or_cr = ("credit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable'
- else "debit_in_account_currency")
+ dr_or_cr = (
+ "credit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "debit_in_account_currency"
+ )
- bank_account_condition = "t2.against_account like %(bank_cash_account)s" \
- if self.bank_cash_account else "1=1"
+ bank_account_condition = (
+ "t2.against_account like %(bank_cash_account)s" if self.bank_cash_account else "1=1"
+ )
- journal_entries = frappe.db.sql("""
+ 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,
@@ -76,31 +93,42 @@ class PaymentReconciliation(Document):
ELSE {bank_account_condition}
END)
order by t1.posting_date
- """.format(**{
- "dr_or_cr": dr_or_cr,
- "bank_account_condition": bank_account_condition,
- "condition": condition
- }), {
+ """.format(
+ **{
+ "dr_or_cr": dr_or_cr,
+ "bank_account_condition": bank_account_condition,
+ "condition": condition,
+ }
+ ),
+ {
"party_type": self.party_type,
"party": self.party,
"account": self.receivable_payable_account,
- "bank_cash_account": "%%%s%%" % self.bank_cash_account
- }, as_dict=1)
+ "bank_cash_account": "%%%s%%" % self.bank_cash_account,
+ },
+ as_dict=1,
+ )
return list(journal_entries)
def get_dr_or_cr_notes(self):
condition = self.get_conditions(get_return_invoices=True)
- dr_or_cr = ("credit_in_account_currency"
- if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency")
+ dr_or_cr = (
+ "credit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "debit_in_account_currency"
+ )
- reconciled_dr_or_cr = ("debit_in_account_currency"
- if dr_or_cr == "credit_in_account_currency" else "credit_in_account_currency")
+ reconciled_dr_or_cr = (
+ "debit_in_account_currency"
+ if dr_or_cr == "credit_in_account_currency"
+ else "credit_in_account_currency"
+ )
- voucher_type = ('Sales Invoice'
- if self.party_type == 'Customer' else "Purchase Invoice")
+ voucher_type = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice"
- return frappe.db.sql(""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
+ return frappe.db.sql(
+ """ SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
(sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount, doc.posting_date,
account_currency as currency
FROM `tab{doc}` doc, `tabGL Entry` gl
@@ -117,106 +145,136 @@ class PaymentReconciliation(Document):
amount > 0
ORDER BY doc.posting_date
""".format(
- doc=voucher_type,
- dr_or_cr=dr_or_cr,
- reconciled_dr_or_cr=reconciled_dr_or_cr,
- party_type_field=frappe.scrub(self.party_type),
- condition=condition or ""),
+ doc=voucher_type,
+ dr_or_cr=dr_or_cr,
+ reconciled_dr_or_cr=reconciled_dr_or_cr,
+ party_type_field=frappe.scrub(self.party_type),
+ condition=condition or "",
+ ),
{
- 'party': self.party,
- 'party_type': self.party_type,
- 'voucher_type': voucher_type,
- 'account': self.receivable_payable_account
- }, as_dict=1)
+ "party": self.party,
+ "party_type": self.party_type,
+ "voucher_type": voucher_type,
+ "account": self.receivable_payable_account,
+ },
+ as_dict=1,
+ )
def add_payment_entries(self, non_reconciled_payments):
- self.set('payments', [])
+ self.set("payments", [])
for payment in non_reconciled_payments:
- row = self.append('payments', {})
+ row = self.append("payments", {})
row.update(payment)
def get_invoice_entries(self):
- #Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
+ # Fetch JVs, Sales and Purchase Invoices for 'invoices' to reconcile against
condition = self.get_conditions(get_invoices=True)
- non_reconciled_invoices = get_outstanding_invoices(self.party_type, self.party,
- self.receivable_payable_account, condition=condition)
+ non_reconciled_invoices = get_outstanding_invoices(
+ self.party_type, self.party, self.receivable_payable_account, condition=condition
+ )
if self.invoice_limit:
- non_reconciled_invoices = non_reconciled_invoices[:self.invoice_limit]
+ non_reconciled_invoices = non_reconciled_invoices[: self.invoice_limit]
self.add_invoice_entries(non_reconciled_invoices)
def add_invoice_entries(self, non_reconciled_invoices):
- #Populate 'invoices' with JVs and Invoices to reconcile against
- self.set('invoices', [])
+ # Populate 'invoices' with JVs and Invoices to reconcile against
+ self.set("invoices", [])
for entry in non_reconciled_invoices:
- inv = self.append('invoices', {})
- inv.invoice_type = entry.get('voucher_type')
- inv.invoice_number = entry.get('voucher_no')
- inv.invoice_date = entry.get('posting_date')
- inv.amount = flt(entry.get('invoice_amount'))
- inv.currency = entry.get('currency')
- inv.outstanding_amount = flt(entry.get('outstanding_amount'))
+ inv = self.append("invoices", {})
+ inv.invoice_type = entry.get("voucher_type")
+ inv.invoice_number = entry.get("voucher_no")
+ inv.invoice_date = entry.get("posting_date")
+ inv.amount = flt(entry.get("invoice_amount"))
+ inv.currency = entry.get("currency")
+ inv.outstanding_amount = flt(entry.get("outstanding_amount"))
+
+ def get_difference_amount(self, allocated_entry):
+ if allocated_entry.get("reference_type") != "Payment Entry":
+ return
+
+ dr_or_cr = (
+ "credit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "debit_in_account_currency"
+ )
+
+ row = self.get_payment_details(allocated_entry, dr_or_cr)
+
+ doc = frappe.get_doc(allocated_entry.reference_type, allocated_entry.reference_name)
+ update_reference_in_payment_entry(row, doc, do_not_save=True)
+
+ return doc.difference_amount
@frappe.whitelist()
def allocate_entries(self, args):
self.validate_entries()
entries = []
- for pay in args.get('payments'):
- pay.update({'unreconciled_amount': pay.get('amount')})
- for inv in args.get('invoices'):
- if pay.get('amount') >= inv.get('outstanding_amount'):
- res = self.get_allocated_entry(pay, inv, inv['outstanding_amount'])
- pay['amount'] = flt(pay.get('amount')) - flt(inv.get('outstanding_amount'))
- inv['outstanding_amount'] = 0
+ for pay in args.get("payments"):
+ pay.update({"unreconciled_amount": pay.get("amount")})
+ for inv in args.get("invoices"):
+ if pay.get("amount") >= inv.get("outstanding_amount"):
+ res = self.get_allocated_entry(pay, inv, inv["outstanding_amount"])
+ pay["amount"] = flt(pay.get("amount")) - flt(inv.get("outstanding_amount"))
+ inv["outstanding_amount"] = 0
else:
- res = self.get_allocated_entry(pay, inv, pay['amount'])
- inv['outstanding_amount'] = flt(inv.get('outstanding_amount')) - flt(pay.get('amount'))
- pay['amount'] = 0
- if pay.get('amount') == 0:
+ res = self.get_allocated_entry(pay, inv, pay["amount"])
+ inv["outstanding_amount"] = flt(inv.get("outstanding_amount")) - flt(pay.get("amount"))
+ pay["amount"] = 0
+
+ res.difference_amount = self.get_difference_amount(res)
+
+ if pay.get("amount") == 0:
entries.append(res)
break
- elif inv.get('outstanding_amount') == 0:
+ elif inv.get("outstanding_amount") == 0:
entries.append(res)
continue
+
else:
break
- self.set('allocation', [])
+ self.set("allocation", [])
for entry in entries:
- if entry['allocated_amount'] != 0:
- row = self.append('allocation', {})
+ if entry["allocated_amount"] != 0:
+ row = self.append("allocation", {})
row.update(entry)
def get_allocated_entry(self, pay, inv, allocated_amount):
- return frappe._dict({
- 'reference_type': pay.get('reference_type'),
- 'reference_name': pay.get('reference_name'),
- 'reference_row': pay.get('reference_row'),
- 'invoice_type': inv.get('invoice_type'),
- 'invoice_number': inv.get('invoice_number'),
- 'unreconciled_amount': pay.get('unreconciled_amount'),
- 'amount': pay.get('amount'),
- 'allocated_amount': allocated_amount,
- 'difference_amount': pay.get('difference_amount')
- })
+ return frappe._dict(
+ {
+ "reference_type": pay.get("reference_type"),
+ "reference_name": pay.get("reference_name"),
+ "reference_row": pay.get("reference_row"),
+ "invoice_type": inv.get("invoice_type"),
+ "invoice_number": inv.get("invoice_number"),
+ "unreconciled_amount": pay.get("unreconciled_amount"),
+ "amount": pay.get("amount"),
+ "allocated_amount": allocated_amount,
+ "difference_amount": pay.get("difference_amount"),
+ }
+ )
@frappe.whitelist()
def reconcile(self):
self.validate_allocation()
- dr_or_cr = ("credit_in_account_currency"
- if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit_in_account_currency")
+ dr_or_cr = (
+ "credit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "debit_in_account_currency"
+ )
entry_list = []
dr_or_cr_notes = []
- for row in self.get('allocation'):
+ for row in self.get("allocation"):
reconciled_entry = []
if row.invoice_number and row.allocated_amount:
- if row.reference_type in ['Sales Invoice', 'Purchase Invoice']:
+ if row.reference_type in ["Sales Invoice", "Purchase Invoice"]:
reconciled_entry = dr_or_cr_notes
else:
reconciled_entry = entry_list
@@ -233,23 +291,25 @@ class PaymentReconciliation(Document):
self.get_unreconciled_entries()
def get_payment_details(self, row, dr_or_cr):
- return frappe._dict({
- 'voucher_type': row.get('reference_type'),
- 'voucher_no' : row.get('reference_name'),
- 'voucher_detail_no' : row.get('reference_row'),
- 'against_voucher_type' : row.get('invoice_type'),
- 'against_voucher' : row.get('invoice_number'),
- 'account' : self.receivable_payable_account,
- 'party_type': self.party_type,
- 'party': self.party,
- 'is_advance' : row.get('is_advance'),
- 'dr_or_cr' : dr_or_cr,
- 'unreconciled_amount': flt(row.get('unreconciled_amount')),
- 'unadjusted_amount' : flt(row.get('amount')),
- 'allocated_amount' : flt(row.get('allocated_amount')),
- 'difference_amount': flt(row.get('difference_amount')),
- 'difference_account': row.get('difference_account')
- })
+ return frappe._dict(
+ {
+ "voucher_type": row.get("reference_type"),
+ "voucher_no": row.get("reference_name"),
+ "voucher_detail_no": row.get("reference_row"),
+ "against_voucher_type": row.get("invoice_type"),
+ "against_voucher": row.get("invoice_number"),
+ "account": self.receivable_payable_account,
+ "party_type": self.party_type,
+ "party": self.party,
+ "is_advance": row.get("is_advance"),
+ "dr_or_cr": dr_or_cr,
+ "unreconciled_amount": flt(row.get("unreconciled_amount")),
+ "unadjusted_amount": flt(row.get("amount")),
+ "allocated_amount": flt(row.get("allocated_amount")),
+ "difference_amount": flt(row.get("difference_amount")),
+ "difference_account": row.get("difference_account"),
+ }
+ )
def check_mandatory_to_fetch(self):
for fieldname in ["company", "party_type", "party", "receivable_payable_account"]:
@@ -267,7 +327,9 @@ class PaymentReconciliation(Document):
unreconciled_invoices = frappe._dict()
for inv in self.get("invoices"):
- unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(inv.invoice_number, inv.outstanding_amount)
+ unreconciled_invoices.setdefault(inv.invoice_type, {}).setdefault(
+ inv.invoice_number, inv.outstanding_amount
+ )
invoices_to_reconcile = []
for row in self.get("allocation"):
@@ -275,13 +337,19 @@ class PaymentReconciliation(Document):
invoices_to_reconcile.append(row.invoice_number)
if flt(row.amount) - flt(row.allocated_amount) < 0:
- frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}")
- .format(row.idx, row.allocated_amount, row.amount))
+ frappe.throw(
+ _(
+ "Row {0}: Allocated amount {1} must be less than or equal to remaining payment amount {2}"
+ ).format(row.idx, row.allocated_amount, row.amount)
+ )
invoice_outstanding = unreconciled_invoices.get(row.invoice_type, {}).get(row.invoice_number)
if flt(row.allocated_amount) - invoice_outstanding > 0.009:
- frappe.throw(_("Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}")
- .format(row.idx, row.allocated_amount, invoice_outstanding))
+ frappe.throw(
+ _(
+ "Row {0}: Allocated amount {1} must be less than or equal to invoice outstanding amount {2}"
+ ).format(row.idx, row.allocated_amount, invoice_outstanding)
+ )
if not invoices_to_reconcile:
frappe.throw(_("No records found in Allocation table"))
@@ -289,79 +357,134 @@ class PaymentReconciliation(Document):
def get_conditions(self, get_invoices=False, get_payments=False, get_return_invoices=False):
condition = " and company = '{0}' ".format(self.company)
+ if self.get("cost_center") and (get_invoices or get_payments or get_return_invoices):
+ condition = " and cost_center = '{0}' ".format(self.cost_center)
+
if get_invoices:
- condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date)) if self.from_invoice_date else ""
- condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date)) if self.to_invoice_date else ""
- dr_or_cr = ("debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable'
- else "credit_in_account_currency")
+ condition += (
+ " and posting_date >= {0}".format(frappe.db.escape(self.from_invoice_date))
+ if self.from_invoice_date
+ else ""
+ )
+ condition += (
+ " and posting_date <= {0}".format(frappe.db.escape(self.to_invoice_date))
+ if self.to_invoice_date
+ else ""
+ )
+ dr_or_cr = (
+ "debit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "credit_in_account_currency"
+ )
if self.minimum_invoice_amount:
- condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_invoice_amount))
+ condition += " and {dr_or_cr} >= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.minimum_invoice_amount)
+ )
if self.maximum_invoice_amount:
- condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_invoice_amount))
+ condition += " and {dr_or_cr} <= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.maximum_invoice_amount)
+ )
elif get_return_invoices:
condition = " and doc.company = '{0}' ".format(self.company)
- condition += " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else ""
- condition += " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else ""
- dr_or_cr = ("gl.debit_in_account_currency" if erpnext.get_party_account_type(self.party_type) == 'Receivable'
- else "gl.credit_in_account_currency")
+ condition += (
+ " and doc.posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
+ if self.from_payment_date
+ else ""
+ )
+ condition += (
+ " and doc.posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
+ if self.to_payment_date
+ else ""
+ )
+ dr_or_cr = (
+ "debit_in_account_currency"
+ if erpnext.get_party_account_type(self.party_type) == "Receivable"
+ else "credit_in_account_currency"
+ )
if self.minimum_invoice_amount:
- condition += " and `{0}` >= {1}".format(dr_or_cr, flt(self.minimum_payment_amount))
+ condition += " and gl.{dr_or_cr} >= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.minimum_payment_amount)
+ )
if self.maximum_invoice_amount:
- condition += " and `{0}` <= {1}".format(dr_or_cr, flt(self.maximum_payment_amount))
+ condition += " and gl.{dr_or_cr} <= {amount}".format(
+ dr_or_cr=dr_or_cr, amount=flt(self.maximum_payment_amount)
+ )
else:
- condition += " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date)) if self.from_payment_date else ""
- condition += " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date)) if self.to_payment_date else ""
+ condition += (
+ " and posting_date >= {0}".format(frappe.db.escape(self.from_payment_date))
+ if self.from_payment_date
+ else ""
+ )
+ condition += (
+ " and posting_date <= {0}".format(frappe.db.escape(self.to_payment_date))
+ if self.to_payment_date
+ else ""
+ )
if self.minimum_payment_amount:
- condition += " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount)) if get_payments \
+ condition += (
+ " and unallocated_amount >= {0}".format(flt(self.minimum_payment_amount))
+ if get_payments
else " and total_debit >= {0}".format(flt(self.minimum_payment_amount))
+ )
if self.maximum_payment_amount:
- condition += " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount)) if get_payments \
+ condition += (
+ " and unallocated_amount <= {0}".format(flt(self.maximum_payment_amount))
+ if get_payments
else " and total_debit <= {0}".format(flt(self.maximum_payment_amount))
+ )
return condition
+
def reconcile_dr_cr_note(dr_cr_notes, company):
for inv in dr_cr_notes:
- voucher_type = ('Credit Note'
- if inv.voucher_type == 'Sales Invoice' else 'Debit Note')
+ voucher_type = "Credit Note" if inv.voucher_type == "Sales Invoice" else "Debit Note"
- reconcile_dr_or_cr = ('debit_in_account_currency'
- if inv.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency')
+ reconcile_dr_or_cr = (
+ "debit_in_account_currency"
+ if inv.dr_or_cr == "credit_in_account_currency"
+ else "credit_in_account_currency"
+ )
company_currency = erpnext.get_company_currency(company)
- jv = frappe.get_doc({
- "doctype": "Journal Entry",
- "voucher_type": voucher_type,
- "posting_date": today(),
- "company": company,
- "multi_currency": 1 if inv.currency != company_currency else 0,
- "accounts": [
- {
- 'account': inv.account,
- 'party': inv.party,
- 'party_type': inv.party_type,
- inv.dr_or_cr: abs(inv.allocated_amount),
- 'reference_type': inv.against_voucher_type,
- 'reference_name': inv.against_voucher,
- 'cost_center': erpnext.get_default_cost_center(company)
- },
- {
- 'account': inv.account,
- 'party': inv.party,
- 'party_type': inv.party_type,
- reconcile_dr_or_cr: (abs(inv.allocated_amount)
- if abs(inv.unadjusted_amount) > abs(inv.allocated_amount) else abs(inv.unadjusted_amount)),
- 'reference_type': inv.voucher_type,
- 'reference_name': inv.voucher_no,
- 'cost_center': erpnext.get_default_cost_center(company)
- }
- ]
- })
+ jv = frappe.get_doc(
+ {
+ "doctype": "Journal Entry",
+ "voucher_type": voucher_type,
+ "posting_date": today(),
+ "company": company,
+ "multi_currency": 1 if inv.currency != company_currency else 0,
+ "accounts": [
+ {
+ "account": inv.account,
+ "party": inv.party,
+ "party_type": inv.party_type,
+ inv.dr_or_cr: abs(inv.allocated_amount),
+ "reference_type": inv.against_voucher_type,
+ "reference_name": inv.against_voucher,
+ "cost_center": erpnext.get_default_cost_center(company),
+ },
+ {
+ "account": inv.account,
+ "party": inv.party,
+ "party_type": inv.party_type,
+ reconcile_dr_or_cr: (
+ abs(inv.allocated_amount)
+ if abs(inv.unadjusted_amount) > abs(inv.allocated_amount)
+ else abs(inv.unadjusted_amount)
+ ),
+ "reference_type": inv.voucher_type,
+ "reference_name": inv.voucher_no,
+ "cost_center": erpnext.get_default_cost_center(company),
+ },
+ ],
+ }
+ )
jv.flags.ignore_mandatory = True
jv.submit()
diff --git a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
index 2271f48a2b9..d2374b77a63 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/test_payment_reconciliation.py
@@ -1,9 +1,96 @@
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
-# import frappe
import unittest
+import frappe
+from frappe.utils import add_days, getdate
+
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+
class TestPaymentReconciliation(unittest.TestCase):
- pass
+ @classmethod
+ def setUpClass(cls):
+ make_customer()
+ make_invoice_and_payment()
+
+ def test_payment_reconciliation(self):
+ payment_reco = frappe.get_doc("Payment Reconciliation")
+ payment_reco.company = "_Test Company"
+ payment_reco.party_type = "Customer"
+ payment_reco.party = "_Test Payment Reco Customer"
+ payment_reco.receivable_payable_account = "Debtors - _TC"
+ payment_reco.from_invoice_date = add_days(getdate(), -1)
+ payment_reco.to_invoice_date = getdate()
+ payment_reco.from_payment_date = add_days(getdate(), -1)
+ payment_reco.to_payment_date = getdate()
+ payment_reco.maximum_invoice_amount = 1000
+ payment_reco.maximum_payment_amount = 1000
+ payment_reco.invoice_limit = 10
+ payment_reco.payment_limit = 10
+ payment_reco.bank_cash_account = "_Test Bank - _TC"
+ payment_reco.cost_center = "_Test Cost Center - _TC"
+ payment_reco.get_unreconciled_entries()
+
+ self.assertEqual(len(payment_reco.get("invoices")), 1)
+ self.assertEqual(len(payment_reco.get("payments")), 1)
+
+ payment_entry = payment_reco.get("payments")[0].reference_name
+ invoice = payment_reco.get("invoices")[0].invoice_number
+
+ payment_reco.allocate_entries(
+ {
+ "payments": [payment_reco.get("payments")[0].as_dict()],
+ "invoices": [payment_reco.get("invoices")[0].as_dict()],
+ }
+ )
+ payment_reco.reconcile()
+
+ payment_entry_doc = frappe.get_doc("Payment Entry", payment_entry)
+ self.assertEqual(payment_entry_doc.get("references")[0].reference_name, invoice)
+
+
+def make_customer():
+ if not frappe.db.get_value("Customer", "_Test Payment Reco Customer"):
+ frappe.get_doc(
+ {
+ "doctype": "Customer",
+ "customer_name": "_Test Payment Reco Customer",
+ "customer_type": "Individual",
+ "customer_group": "_Test Customer Group",
+ "territory": "_Test Territory",
+ }
+ ).insert()
+
+
+def make_invoice_and_payment():
+ si = create_sales_invoice(
+ customer="_Test Payment Reco Customer", qty=1, rate=690, do_not_save=True
+ )
+ si.cost_center = "_Test Cost Center - _TC"
+ si.save()
+ si.submit()
+
+ pe = frappe.get_doc(
+ {
+ "doctype": "Payment Entry",
+ "payment_type": "Receive",
+ "party_type": "Customer",
+ "party": "_Test Payment Reco Customer",
+ "company": "_Test Company",
+ "paid_from_account_currency": "INR",
+ "paid_to_account_currency": "INR",
+ "source_exchange_rate": 1,
+ "target_exchange_rate": 1,
+ "reference_no": "1",
+ "reference_date": getdate(),
+ "received_amount": 690,
+ "paid_amount": 690,
+ "paid_from": "Debtors - _TC",
+ "paid_to": "_Test Bank - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ }
+ )
+ pe.insert()
+ pe.submit()
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 1a833a4008e..f05dd5ba496 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -24,7 +24,7 @@ from erpnext.erpnext_integrations.stripe_integration import create_stripe_subscr
class PaymentRequest(Document):
def validate(self):
if self.get("__islocal"):
- self.status = 'Draft'
+ self.status = "Draft"
self.validate_reference_document()
self.validate_payment_request_amount()
self.validate_currency()
@@ -35,51 +35,67 @@ 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 = 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"):
+ if hasattr(ref_doc, "order_type") and 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("Account", self.payment_account, "account_currency"):
+ if self.payment_account and ref_doc.currency != frappe.db.get_value(
+ "Account", self.payment_account, "account_currency"
+ ):
frappe.throw(_("Transaction currency must be same as Payment Gateway currency"))
def validate_subscription_details(self):
if self.is_a_subscription:
amount = 0
for subscription_plan in self.subscription_plans:
- payment_gateway = frappe.db.get_value("Subscription Plan", subscription_plan.plan, "payment_gateway")
+ payment_gateway = frappe.db.get_value(
+ "Subscription Plan", subscription_plan.plan, "payment_gateway"
+ )
if payment_gateway != self.payment_gateway_account:
- frappe.throw(_('The payment gateway account in plan {0} is different from the payment gateway account in this payment request').format(subscription_plan.name))
+ frappe.throw(
+ _(
+ "The payment gateway account in plan {0} is different from the payment gateway account in this payment request"
+ ).format(subscription_plan.name)
+ )
rate = get_plan_rate(subscription_plan.plan, quantity=subscription_plan.qty)
amount += rate
if amount != self.grand_total:
- frappe.msgprint(_("The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document.").format(self.grand_total, amount))
+ frappe.msgprint(
+ _(
+ "The amount of {0} set in this payment request is different from the calculated amount of all payment plans: {1}. Make sure this is correct before submitting the document."
+ ).format(self.grand_total, amount)
+ )
def on_submit(self):
- if self.payment_request_type == 'Outward':
- self.db_set('status', 'Initiated')
+ if self.payment_request_type == "Outward":
+ self.db_set("status", "Initiated")
return
- elif self.payment_request_type == 'Inward':
- self.db_set('status', 'Requested')
+ elif self.payment_request_type == "Inward":
+ self.db_set("status", "Requested")
send_mail = self.payment_gateway_validation() if self.payment_gateway else None
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") \
- or self.flags.mute_email:
+ if (
+ hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart"
+ ) or self.flags.mute_email:
send_mail = False
if send_mail and self.payment_channel != "Phone":
@@ -101,23 +117,27 @@ class PaymentRequest(Document):
request_amount=request_amount,
sender=self.email_to,
currency=self.currency,
- payment_gateway=self.payment_gateway
+ payment_gateway=self.payment_gateway,
)
controller.validate_transaction_currency(self.currency)
controller.request_for_payment(**payment_record)
def get_request_amount(self):
- data_of_completed_requests = frappe.get_all("Integration Request", filters={
- 'reference_doctype': self.doctype,
- 'reference_docname': self.name,
- 'status': 'Completed'
- }, pluck="data")
+ data_of_completed_requests = frappe.get_all(
+ "Integration Request",
+ filters={
+ "reference_doctype": self.doctype,
+ "reference_docname": self.name,
+ "status": "Completed",
+ },
+ pluck="data",
+ )
if not data_of_completed_requests:
return self.grand_total
- request_amounts = sum(json.loads(d).get('request_amount') for d in data_of_completed_requests)
+ request_amounts = sum(json.loads(d).get("request_amount") for d in data_of_completed_requests)
return request_amounts
def on_cancel(self):
@@ -126,8 +146,9 @@ class PaymentRequest(Document):
def make_invoice(self):
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"):
+ if hasattr(ref_doc, "order_type") and getattr(ref_doc, "order_type") == "Shopping Cart":
from erpnext.selling.doctype.sales_order.sales_order import make_sales_invoice
+
si = make_sales_invoice(self.reference_name, ignore_permissions=True)
si.allocate_advances_automatically = True
si = si.insert(ignore_permissions=True)
@@ -136,7 +157,7 @@ class PaymentRequest(Document):
def payment_gateway_validation(self):
try:
controller = get_payment_gateway_controller(self.payment_gateway)
- if hasattr(controller, 'on_payment_request_submission'):
+ if hasattr(controller, "on_payment_request_submission"):
return controller.on_payment_request_submission(self)
else:
return True
@@ -148,36 +169,45 @@ class PaymentRequest(Document):
self.payment_url = self.get_payment_url()
if self.payment_url:
- self.db_set('payment_url', self.payment_url)
+ self.db_set("payment_url", self.payment_url)
- if self.payment_url or not self.payment_gateway_account \
- or (self.payment_gateway_account and self.payment_channel == "Phone"):
- self.db_set('status', 'Initiated')
+ if (
+ self.payment_url
+ or not self.payment_gateway_account
+ or (self.payment_gateway_account and self.payment_channel == "Phone")
+ ):
+ self.db_set("status", "Initiated")
def get_payment_url(self):
if self.reference_doctype != "Fees":
- data = frappe.db.get_value(self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1)
+ data = frappe.db.get_value(
+ self.reference_doctype, self.reference_name, ["company", "customer_name"], as_dict=1
+ )
else:
- data = frappe.db.get_value(self.reference_doctype, self.reference_name, ["student_name"], as_dict=1)
+ data = frappe.db.get_value(
+ self.reference_doctype, self.reference_name, ["student_name"], as_dict=1
+ )
data.update({"company": frappe.defaults.get_defaults().company})
controller = get_payment_gateway_controller(self.payment_gateway)
controller.validate_transaction_currency(self.currency)
- if hasattr(controller, 'validate_minimum_transaction_amount'):
+ if hasattr(controller, "validate_minimum_transaction_amount"):
controller.validate_minimum_transaction_amount(self.currency, self.grand_total)
- return controller.get_payment_url(**{
- "amount": flt(self.grand_total, self.precision("grand_total")),
- "title": data.company.encode("utf-8"),
- "description": self.subject.encode("utf-8"),
- "reference_doctype": "Payment Request",
- "reference_docname": self.name,
- "payer_email": self.email_to or frappe.session.user,
- "payer_name": frappe.safe_encode(data.customer_name),
- "order_id": self.name,
- "currency": self.currency
- })
+ return controller.get_payment_url(
+ **{
+ "amount": flt(self.grand_total, self.precision("grand_total")),
+ "title": data.company.encode("utf-8"),
+ "description": self.subject.encode("utf-8"),
+ "reference_doctype": "Payment Request",
+ "reference_docname": self.name,
+ "payer_email": self.email_to or frappe.session.user,
+ "payer_name": frappe.safe_encode(data.customer_name),
+ "order_id": self.name,
+ "currency": self.currency,
+ }
+ )
def set_as_paid(self):
if self.payment_channel == "Phone":
@@ -202,32 +232,47 @@ class PaymentRequest(Document):
else:
party_account = get_party_account("Customer", ref_doc.get("customer"), ref_doc.company)
- party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(party_account)
+ party_account_currency = ref_doc.get("party_account_currency") or get_account_currency(
+ party_account
+ )
bank_amount = self.grand_total
- if party_account_currency == ref_doc.company_currency and party_account_currency != self.currency:
+ if (
+ party_account_currency == ref_doc.company_currency and party_account_currency != self.currency
+ ):
party_amount = ref_doc.base_grand_total
else:
party_amount = self.grand_total
- payment_entry = get_payment_entry(self.reference_doctype, self.reference_name, party_amount=party_amount,
- bank_account=self.payment_account, bank_amount=bank_amount)
+ payment_entry = get_payment_entry(
+ self.reference_doctype,
+ self.reference_name,
+ party_amount=party_amount,
+ bank_account=self.payment_account,
+ bank_amount=bank_amount,
+ )
- payment_entry.update({
- "reference_no": self.name,
- "reference_date": nowdate(),
- "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(self.reference_doctype,
- self.reference_name, self.name)
- })
+ payment_entry.update(
+ {
+ "reference_no": self.name,
+ "reference_date": nowdate(),
+ "remarks": "Payment Entry against {0} {1} via Payment Request {2}".format(
+ self.reference_doctype, self.reference_name, self.name
+ ),
+ }
+ )
if payment_entry.difference_amount:
company_details = get_company_defaults(ref_doc.company)
- payment_entry.append("deductions", {
- "account": company_details.exchange_gain_loss_account,
- "cost_center": company_details.cost_center,
- "amount": payment_entry.difference_amount
- })
+ payment_entry.append(
+ "deductions",
+ {
+ "account": company_details.exchange_gain_loss_account,
+ "cost_center": company_details.cost_center,
+ "amount": payment_entry.difference_amount,
+ },
+ )
if submit:
payment_entry.insert(ignore_permissions=True)
@@ -243,16 +288,23 @@ class PaymentRequest(Document):
"subject": self.subject,
"message": self.get_message(),
"now": True,
- "attachments": [frappe.attach_print(self.reference_doctype, self.reference_name,
- file_name=self.reference_name, print_format=self.print_format)]}
- enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
+ "attachments": [
+ frappe.attach_print(
+ self.reference_doctype,
+ self.reference_name,
+ file_name=self.reference_name,
+ print_format=self.print_format,
+ )
+ ],
+ }
+ enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
def get_message(self):
"""return message with payment gateway link"""
context = {
"doc": frappe.get_doc(self.reference_doctype, self.reference_name),
- "payment_url": self.payment_url
+ "payment_url": self.payment_url,
}
if self.message:
@@ -266,22 +318,26 @@ class PaymentRequest(Document):
def check_if_payment_entry_exists(self):
if self.status == "Paid":
- if frappe.get_all("Payment Entry Reference",
+ if frappe.get_all(
+ "Payment Entry Reference",
filters={"reference_name": self.reference_name, "docstatus": ["<", 2]},
fields=["parent"],
- limit=1):
- frappe.throw(_("Payment Entry already exists"), title=_('Error'))
+ limit=1,
+ ):
+ frappe.throw(_("Payment Entry already exists"), title=_("Error"))
def make_communication_entry(self):
"""Make communication entry"""
- comm = frappe.get_doc({
- "doctype":"Communication",
- "subject": self.subject,
- "content": self.get_message(),
- "sent_or_received": "Sent",
- "reference_doctype": self.reference_doctype,
- "reference_name": self.reference_name
- })
+ comm = frappe.get_doc(
+ {
+ "doctype": "Communication",
+ "subject": self.subject,
+ "content": self.get_message(),
+ "sent_or_received": "Sent",
+ "reference_doctype": self.reference_doctype,
+ "reference_name": self.reference_name,
+ }
+ )
comm.insert(ignore_permissions=True)
def get_payment_success_url(self):
@@ -298,16 +354,17 @@ class PaymentRequest(Document):
self.set_as_paid()
# if shopping cart enabled and in session
- if (shopping_cart_settings.enabled and hasattr(frappe.local, "session")
- and frappe.local.session.user != "Guest") and self.payment_channel != "Phone":
+ if (
+ shopping_cart_settings.enabled
+ and hasattr(frappe.local, "session")
+ and frappe.local.session.user != "Guest"
+ ) and self.payment_channel != "Phone":
success_url = shopping_cart_settings.payment_success_url
if success_url:
- redirect_to = ({
- "Orders": "/orders",
- "Invoices": "/invoices",
- "My Account": "/me"
- }).get(success_url, "/me")
+ redirect_to = ({"Orders": "/orders", "Invoices": "/invoices", "My Account": "/me"}).get(
+ success_url, "/me"
+ )
else:
redirect_to = get_url("/orders/{0}".format(self.reference_name))
@@ -317,6 +374,7 @@ class PaymentRequest(Document):
if payment_provider == "stripe":
return create_stripe_subscription(gateway_controller, data)
+
@frappe.whitelist(allow_guest=True)
def make_payment_request(**args):
"""Make payment request"""
@@ -329,55 +387,68 @@ def make_payment_request(**args):
grand_total = get_amount(ref_doc, gateway_account.get("payment_account"))
if args.loyalty_points and args.dt == "Sales Order":
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
+
loyalty_amount = validate_loyalty_points(ref_doc, int(args.loyalty_points))
- frappe.db.set_value("Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False)
- frappe.db.set_value("Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False)
+ frappe.db.set_value(
+ "Sales Order", args.dn, "loyalty_points", int(args.loyalty_points), update_modified=False
+ )
+ frappe.db.set_value(
+ "Sales Order", args.dn, "loyalty_amount", loyalty_amount, update_modified=False
+ )
grand_total = grand_total - loyalty_amount
- bank_account = (get_party_bank_account(args.get('party_type'), args.get('party'))
- if args.get('party_type') else '')
+ bank_account = (
+ get_party_bank_account(args.get("party_type"), args.get("party"))
+ if args.get("party_type")
+ 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)})
+ existing_payment_request = frappe.db.get_value(
+ "Payment Request",
+ {"reference_doctype": args.dt, "reference_name": args.dn, "docstatus": ("!=", 2)},
+ )
if existing_payment_request:
- frappe.db.set_value("Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False)
+ frappe.db.set_value(
+ "Payment Request", existing_payment_request, "grand_total", grand_total, update_modified=False
+ )
pr = frappe.get_doc("Payment Request", existing_payment_request)
else:
if args.order_type != "Shopping Cart":
- existing_payment_request_amount = \
- get_existing_payment_request_amount(args.dt, args.dn)
+ 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({
- "payment_gateway_account": gateway_account.get("name"),
- "payment_gateway": gateway_account.get("payment_gateway"),
- "payment_account": gateway_account.get("payment_account"),
- "payment_channel": gateway_account.get("payment_channel"),
- "payment_request_type": args.get("payment_request_type"),
- "currency": ref_doc.currency,
- "grand_total": grand_total,
- "mode_of_payment": args.mode_of_payment,
- "email_to": args.recipient_id or ref_doc.owner,
- "subject": _("Payment Request for {0}").format(args.dn),
- "message": gateway_account.get("message") or get_dummy_message(ref_doc),
- "reference_doctype": args.dt,
- "reference_name": args.dn,
- "party_type": args.get("party_type") or "Customer",
- "party": args.get("party") or ref_doc.get("customer"),
- "bank_account": bank_account
- })
+ pr.update(
+ {
+ "payment_gateway_account": gateway_account.get("name"),
+ "payment_gateway": gateway_account.get("payment_gateway"),
+ "payment_account": gateway_account.get("payment_account"),
+ "payment_channel": gateway_account.get("payment_channel"),
+ "payment_request_type": args.get("payment_request_type"),
+ "currency": ref_doc.currency,
+ "grand_total": grand_total,
+ "mode_of_payment": args.mode_of_payment,
+ "email_to": args.recipient_id or ref_doc.owner,
+ "subject": _("Payment Request for {0}").format(args.dn),
+ "message": gateway_account.get("message") or get_dummy_message(ref_doc),
+ "reference_doctype": args.dt,
+ "reference_name": args.dn,
+ "party_type": args.get("party_type") or "Customer",
+ "party": args.get("party") or ref_doc.get("customer"),
+ "bank_account": bank_account,
+ }
+ )
if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True
- pr.insert(ignore_permissions=True)
if args.submit_doc:
+ pr.insert(ignore_permissions=True)
pr.submit()
if args.order_type == "Shopping Cart":
@@ -390,11 +461,15 @@ def make_payment_request(**args):
return pr.as_dict()
+
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)
+ if ref_doc.party_account_currency == ref_doc.currency:
+ grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid)
+ else:
+ grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) / ref_doc.conversion_rate
elif dt in ["Sales Invoice", "Purchase Invoice"]:
if ref_doc.party_account_currency == ref_doc.currency:
@@ -411,18 +486,20 @@ def get_amount(ref_doc, payment_account=None):
elif dt == "Fees":
grand_total = ref_doc.outstanding_amount
- if grand_total > 0 :
+ if grand_total > 0:
return grand_total
else:
frappe.throw(_("Payment Entry is already created"))
+
def get_existing_payment_request_amount(ref_dt, ref_dn):
"""
Get the existing payment request which are unpaid or partially paid for payment channel other than Phone
and get the summation of existing paid payment request for Phone payment channel.
"""
- existing_payment_request_amount = frappe.db.sql("""
+ existing_payment_request_amount = frappe.db.sql(
+ """
select sum(grand_total)
from `tabPayment Request`
where
@@ -432,9 +509,12 @@ def get_existing_payment_request_amount(ref_dt, ref_dn):
and (status != 'Paid'
or (payment_channel = 'Phone'
and status = 'Paid'))
- """, (ref_dt, ref_dn))
+ """,
+ (ref_dt, ref_dn),
+ )
return flt(existing_payment_request_amount[0][0]) if existing_payment_request_amount else 0
+
def get_gateway_details(args):
"""return gateway and payment account of default payment gateway"""
if args.get("payment_gateway_account"):
@@ -448,58 +528,74 @@ def get_gateway_details(args):
return gateway_account
+
def get_payment_gateway_account(args):
- return frappe.db.get_value("Payment Gateway Account", args,
+ return frappe.db.get_value(
+ "Payment Gateway Account",
+ args,
["name", "payment_gateway", "payment_account", "message"],
- as_dict=1)
+ as_dict=1,
+ )
+
@frappe.whitelist()
def get_print_format_list(ref_doctype):
print_format_list = ["Standard"]
- print_format_list.extend([p.name for p in frappe.get_all("Print Format",
- filters={"doc_type": ref_doctype})])
+ print_format_list.extend(
+ [p.name for p in frappe.get_all("Print Format", filters={"doc_type": ref_doctype})]
+ )
+
+ return {"print_format": print_format_list}
- return {
- "print_format": print_format_list
- }
@frappe.whitelist(allow_guest=True)
def resend_payment_email(docname):
return frappe.get_doc("Payment Request", docname).send_email()
+
@frappe.whitelist()
def make_payment_entry(docname):
doc = frappe.get_doc("Payment Request", docname)
return doc.create_payment_entry(submit=False).as_dict()
+
def update_payment_req_status(doc, method):
from erpnext.accounts.doctype.payment_entry.payment_entry import get_reference_details
for ref in doc.references:
- payment_request_name = frappe.db.get_value("Payment Request",
- {"reference_doctype": ref.reference_doctype, "reference_name": ref.reference_name,
- "docstatus": 1})
+ payment_request_name = frappe.db.get_value(
+ "Payment Request",
+ {
+ "reference_doctype": ref.reference_doctype,
+ "reference_name": ref.reference_name,
+ "docstatus": 1,
+ },
+ )
if payment_request_name:
- ref_details = get_reference_details(ref.reference_doctype, ref.reference_name, doc.party_account_currency)
- pay_req_doc = frappe.get_doc('Payment Request', payment_request_name)
+ ref_details = get_reference_details(
+ ref.reference_doctype, ref.reference_name, doc.party_account_currency
+ )
+ pay_req_doc = frappe.get_doc("Payment Request", payment_request_name)
status = pay_req_doc.status
if status != "Paid" and not ref_details.outstanding_amount:
- status = 'Paid'
+ status = "Paid"
elif status != "Partially Paid" and ref_details.outstanding_amount != ref_details.total_amount:
- status = 'Partially Paid'
+ status = "Partially Paid"
elif ref_details.outstanding_amount == ref_details.total_amount:
- if pay_req_doc.payment_request_type == 'Outward':
- status = 'Initiated'
- elif pay_req_doc.payment_request_type == 'Inward':
- status = 'Requested'
+ if pay_req_doc.payment_request_type == "Outward":
+ status = "Initiated"
+ elif pay_req_doc.payment_request_type == "Inward":
+ status = "Requested"
+
+ pay_req_doc.db_set("status", status)
- pay_req_doc.db_set('status', status)
def get_dummy_message(doc):
- return frappe.render_template("""{% if doc.contact_person -%}
+ return frappe.render_template(
+ """{% if doc.contact_person -%}
Dear {{ doc.contact_person }},
{%- else %}Hello,
{% endif %} @@ -511,12 +607,19 @@ def get_dummy_message(doc):{{ _("If you have any questions, please get back to us.") }}
{{ _("Thank you for your business!") }}
-""", dict(doc=doc, payment_url = '{{ payment_url }}')) +""", + dict(doc=doc, payment_url="{{ payment_url }}"), + ) + @frappe.whitelist() def get_subscription_details(reference_doctype, reference_name): if reference_doctype == "Sales Invoice": - subscriptions = frappe.db.sql("""SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""",reference_name, as_dict=1) + subscriptions = frappe.db.sql( + """SELECT parent as sub_name FROM `tabSubscription Invoice` WHERE invoice=%s""", + reference_name, + as_dict=1, + ) subscription_plans = [] for subscription in subscriptions: plans = frappe.get_doc("Subscription", subscription.sub_name).plans @@ -524,38 +627,50 @@ def get_subscription_details(reference_doctype, reference_name): subscription_plans.append(plan) return subscription_plans + @frappe.whitelist() def make_payment_order(source_name, target_doc=None): from frappe.model.mapper import get_mapped_doc + def set_missing_values(source, target): target.payment_order_type = "Payment Request" - target.append('references', { - 'reference_doctype': source.reference_doctype, - 'reference_name': source.reference_name, - 'amount': source.grand_total, - 'supplier': source.party, - 'payment_request': source_name, - 'mode_of_payment': source.mode_of_payment, - 'bank_account': source.bank_account, - 'account': source.account - }) + target.append( + "references", + { + "reference_doctype": source.reference_doctype, + "reference_name": source.reference_name, + "amount": source.grand_total, + "supplier": source.party, + "payment_request": source_name, + "mode_of_payment": source.mode_of_payment, + "bank_account": source.bank_account, + "account": source.account, + }, + ) - doclist = get_mapped_doc("Payment Request", source_name, { - "Payment Request": { - "doctype": "Payment Order", - } - }, target_doc, set_missing_values) + doclist = get_mapped_doc( + "Payment Request", + source_name, + { + "Payment Request": { + "doctype": "Payment Order", + } + }, + target_doc, + set_missing_values, + ) return doclist + def validate_payment(doc, method=None): if doc.reference_doctype != "Payment Request" or ( - frappe.db.get_value(doc.reference_doctype, doc.reference_docname, 'status') - != "Paid" + frappe.db.get_value(doc.reference_doctype, doc.reference_docname, "status") != "Paid" ): return frappe.throw( - _("The Payment Request {0} is already paid, cannot process payment twice") - .format(doc.reference_docname) + _("The Payment Request {0} is already paid, cannot process payment twice").format( + doc.reference_docname + ) ) diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py index f679ccfe4ff..355784716a0 100644 --- a/erpnext/accounts/doctype/payment_request/test_payment_request.py +++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py @@ -12,10 +12,7 @@ from erpnext.setup.utils import get_exchange_rate test_dependencies = ["Currency Exchange", "Journal Entry", "Contact", "Address"] -payment_gateway = { - "doctype": "Payment Gateway", - "gateway": "_Test Gateway" -} +payment_gateway = {"doctype": "Payment Gateway", "gateway": "_Test Gateway"} payment_method = [ { @@ -23,30 +20,38 @@ payment_method = [ "is_default": 1, "payment_gateway": "_Test Gateway", "payment_account": "_Test Bank - _TC", - "currency": "INR" + "currency": "INR", }, { "doctype": "Payment Gateway Account", "payment_gateway": "_Test Gateway", "payment_account": "_Test Bank USD - _TC", - "currency": "USD" - } + "currency": "USD", + }, ] + class TestPaymentRequest(unittest.TestCase): def setUp(self): if not frappe.db.get_value("Payment Gateway", payment_gateway["gateway"], "name"): frappe.get_doc(payment_gateway).insert(ignore_permissions=True) for method in payment_method: - if not frappe.db.get_value("Payment Gateway Account", {"payment_gateway": method["payment_gateway"], - "currency": method["currency"]}, "name"): + if not frappe.db.get_value( + "Payment Gateway Account", + {"payment_gateway": method["payment_gateway"], "currency": method["currency"]}, + "name", + ): frappe.get_doc(method).insert(ignore_permissions=True) def test_payment_request_linkings(self): so_inr = make_sales_order(currency="INR") - pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", - payment_gateway_account="_Test Gateway - INR") + pr = make_payment_request( + dt="Sales Order", + dn=so_inr.name, + recipient_id="saurabh@erpnext.com", + payment_gateway_account="_Test Gateway - INR", + ) self.assertEqual(pr.reference_doctype, "Sales Order") self.assertEqual(pr.reference_name, so_inr.name) @@ -55,45 +60,75 @@ class TestPaymentRequest(unittest.TestCase): conversion_rate = get_exchange_rate("USD", "INR") si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate) - pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", - payment_gateway_account="_Test Gateway - USD") + pr = make_payment_request( + dt="Sales Invoice", + dn=si_usd.name, + recipient_id="saurabh@erpnext.com", + payment_gateway_account="_Test Gateway - USD", + ) self.assertEqual(pr.reference_doctype, "Sales Invoice") self.assertEqual(pr.reference_name, si_usd.name) self.assertEqual(pr.currency, "USD") def test_payment_entry(self): - frappe.db.set_value("Company", "_Test Company", - "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC") + frappe.db.set_value( + "Company", "_Test Company", "exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" + ) frappe.db.set_value("Company", "_Test Company", "write_off_account", "_Test Write Off - _TC") frappe.db.set_value("Company", "_Test Company", "cost_center", "_Test Cost Center - _TC") so_inr = make_sales_order(currency="INR") - pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com", - mute_email=1, payment_gateway_account="_Test Gateway - INR", submit_doc=1, return_doc=1) + pr = make_payment_request( + dt="Sales Order", + dn=so_inr.name, + recipient_id="saurabh@erpnext.com", + mute_email=1, + payment_gateway_account="_Test Gateway - INR", + submit_doc=1, + return_doc=1, + ) pe = pr.set_as_paid() so_inr = frappe.get_doc("Sales Order", so_inr.name) self.assertEqual(so_inr.advance_paid, 1000) - si_usd = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si_usd = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) - pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", - mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1) + pr = make_payment_request( + dt="Sales Invoice", + dn=si_usd.name, + recipient_id="saurabh@erpnext.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + submit_doc=1, + return_doc=1, + ) pe = pr.set_as_paid() - expected_gle = dict((d[0], d) for d in [ - ["_Test Receivable USD - _TC", 0, 5000, si_usd.name], - [pr.payment_account, 6290.0, 0, None], - ["_Test Exchange Gain/Loss - _TC", 0, 1290, None] - ]) + expected_gle = dict( + (d[0], d) + for d in [ + ["_Test Receivable USD - _TC", 0, 5000, si_usd.name], + [pr.payment_account, 6290.0, 0, None], + ["_Test Exchange Gain/Loss - _TC", 0, 1290, None], + ] + ) - gl_entries = frappe.db.sql("""select account, debit, credit, against_voucher + gl_entries = frappe.db.sql( + """select account, debit, credit, against_voucher from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s - order by account asc""", pe.name, as_dict=1) + order by account asc""", + pe.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -104,35 +139,49 @@ class TestPaymentRequest(unittest.TestCase): self.assertEqual(expected_gle[gle.account][3], gle.against_voucher) def test_status(self): - si_usd = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si_usd = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) - pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com", - mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1) + pr = make_payment_request( + dt="Sales Invoice", + dn=si_usd.name, + recipient_id="saurabh@erpnext.com", + mute_email=1, + payment_gateway_account="_Test Gateway - USD", + submit_doc=1, + return_doc=1, + ) pe = pr.create_payment_entry() pr.load_from_db() - self.assertEqual(pr.status, 'Paid') + self.assertEqual(pr.status, "Paid") pe.cancel() pr.load_from_db() - self.assertEqual(pr.status, 'Requested') + self.assertEqual(pr.status, "Requested") def test_multiple_payment_entries_against_sales_order(self): # Make Sales Order, grand_total = 1000 so = make_sales_order() # Payment Request amount = 200 - pr1 = make_payment_request(dt="Sales Order", dn=so.name, - recipient_id="nabin@erpnext.com", return_doc=1) + pr1 = make_payment_request( + dt="Sales Order", dn=so.name, recipient_id="nabin@erpnext.com", return_doc=1 + ) pr1.grand_total = 200 + pr1.insert() pr1.submit() # Make a 2nd Payment Request - pr2 = make_payment_request(dt="Sales Order", dn=so.name, - recipient_id="nabin@erpnext.com", return_doc=1) + pr2 = make_payment_request( + dt="Sales Order", dn=so.name, recipient_id="nabin@erpnext.com", return_doc=1 + ) self.assertEqual(pr2.grand_total, 800) diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json index 6ed7a3154e5..dde9980ce53 100644 --- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json +++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json @@ -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 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py b/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py index 7f5b96c8a8a..8df97bf6c77 100644 --- a/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py +++ b/erpnext/accounts/doctype/payment_term/payment_term_dashboard.py @@ -1,21 +1,12 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'payment_term', - 'transactions': [ - { - 'label': _('Sales'), - 'items': ['Sales Invoice', 'Sales Order', 'Quotation'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Invoice', 'Purchase Order'] - }, - { - 'items': ['Payment Terms Template'] - } - ] + "fieldname": "payment_term", + "transactions": [ + {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Quotation"]}, + {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order"]}, + {"items": ["Payment Terms Template"]}, + ], } diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py index 3a6999c5799..ea3b76c5243 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py @@ -16,10 +16,12 @@ class PaymentTermsTemplate(Document): def validate_invoice_portion(self): total_portion = 0 for term in self.terms: - total_portion += flt(term.get('invoice_portion', 0)) + total_portion += flt(term.get("invoice_portion", 0)) if flt(total_portion, 2) != 100.00: - frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red') + frappe.msgprint( + _("Combined invoice portion must equal 100%"), raise_exception=1, indicator="red" + ) def check_duplicate_terms(self): terms = [] @@ -27,8 +29,9 @@ class PaymentTermsTemplate(Document): term_info = (term.payment_term, term.credit_days, term.credit_months, term.due_date_based_on) if term_info in terms: frappe.msgprint( - _('The Payment Term at row {0} is possibly a duplicate.').format(term.idx), - raise_exception=1, indicator='red' + _("The Payment Term at row {0} is possibly a duplicate.").format(term.idx), + raise_exception=1, + indicator="red", ) else: terms.append(term_info) diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py index aa5de2ca3f8..34ac773febc 100644 --- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py +++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template_dashboard.py @@ -1,32 +1,19 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'payment_terms_template', - 'non_standard_fieldnames': { - 'Customer Group': 'payment_terms', - 'Supplier Group': 'payment_terms', - 'Supplier': 'payment_terms', - 'Customer': 'payment_terms' + "fieldname": "payment_terms_template", + "non_standard_fieldnames": { + "Customer Group": "payment_terms", + "Supplier Group": "payment_terms", + "Supplier": "payment_terms", + "Customer": "payment_terms", }, - 'transactions': [ - { - 'label': _('Sales'), - 'items': ['Sales Invoice', 'Sales Order', 'Quotation'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Invoice', 'Purchase Order'] - }, - { - 'label': _('Party'), - 'items': ['Customer', 'Supplier'] - }, - { - 'label': _('Group'), - 'items': ['Customer Group', 'Supplier Group'] - } - ] + "transactions": [ + {"label": _("Sales"), "items": ["Sales Invoice", "Sales Order", "Quotation"]}, + {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order"]}, + {"label": _("Party"), "items": ["Customer", "Supplier"]}, + {"label": _("Group"), "items": ["Customer Group", "Supplier Group"]}, + ], } diff --git a/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py index 8529ef5c3d5..9717f2009a0 100644 --- a/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py +++ b/erpnext/accounts/doctype/payment_terms_template/test_payment_terms_template.py @@ -8,64 +8,76 @@ import frappe class TestPaymentTermsTemplate(unittest.TestCase): def tearDown(self): - frappe.delete_doc('Payment Terms Template', '_Test Payment Terms Template For Test', force=1) + frappe.delete_doc("Payment Terms Template", "_Test Payment Terms Template For Test", force=1) def test_create_template(self): - template = frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': '_Test Payment Terms Template For Test', - 'terms': [{ - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 30 - }] - }) + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test Payment Terms Template For Test", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 30, + } + ], + } + ) self.assertRaises(frappe.ValidationError, template.insert) - template.append('terms', { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 0 - }) + template.append( + "terms", + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 0, + }, + ) template.insert() def test_credit_days(self): - template = frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': '_Test Payment Terms Template For Test', - 'terms': [{ - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 100.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': -30 - }] - }) + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test Payment Terms Template For Test", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 100.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": -30, + } + ], + } + ) self.assertRaises(frappe.ValidationError, template.insert) def test_duplicate_terms(self): - template = frappe.get_doc({ - 'doctype': 'Payment Terms Template', - 'template_name': '_Test Payment Terms Template For Test', - 'terms': [ - { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 30 - }, - { - 'doctype': 'Payment Terms Template Detail', - 'invoice_portion': 50.00, - 'credit_days_based_on': 'Day(s) after invoice date', - 'credit_days': 30 - } - - ] - }) + template = frappe.get_doc( + { + "doctype": "Payment Terms Template", + "template_name": "_Test Payment Terms Template For Test", + "terms": [ + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 30, + }, + { + "doctype": "Payment Terms Template Detail", + "invoice_portion": 50.00, + "credit_days_based_on": "Day(s) after invoice date", + "credit_days": 30, + }, + ], + } + ) self.assertRaises(frappe.ValidationError, template.insert) diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json index 84c941ecc10..54a76b34196 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.json @@ -10,10 +10,11 @@ "fiscal_year", "amended_from", "company", - "cost_center_wise_pnl", "column_break1", "closing_account_head", - "remarks" + "remarks", + "gle_processing_status", + "error_message" ], "fields": [ { @@ -86,17 +87,26 @@ "reqd": 1 }, { - "default": "0", - "fieldname": "cost_center_wise_pnl", - "fieldtype": "Check", - "label": "Book Cost Center Wise Profit/Loss" + "depends_on": "eval:doc.docstatus!=0", + "fieldname": "gle_processing_status", + "fieldtype": "Select", + "label": "GL Entry Processing Status", + "options": "In Progress\nCompleted\nFailed", + "read_only": 1 + }, + { + "depends_on": "eval:doc.gle_processing_status=='Failed'", + "fieldname": "error_message", + "fieldtype": "Text", + "label": "Error Message", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 1, "is_submittable": 1, "links": [], - "modified": "2021-05-20 15:27:37.210458", + "modified": "2022-07-20 14:51:04.714154", "modified_by": "Administrator", "module": "Accounts", "name": "Period Closing Voucher", diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py index d8a024248be..866a94d04b1 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py @@ -8,7 +8,6 @@ from frappe.utils import flt from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, - get_dimensions, ) from erpnext.accounts.utils import get_account_currency from erpnext.controllers.accounts_controller import AccountsController @@ -20,119 +19,200 @@ class PeriodClosingVoucher(AccountsController): self.validate_posting_date() def on_submit(self): + self.db_set("gle_processing_status", "In Progress") self.make_gl_entries() def on_cancel(self): - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') - from erpnext.accounts.general_ledger import make_reverse_gl_entries - make_reverse_gl_entries(voucher_type="Period Closing Voucher", voucher_no=self.name) + self.db_set("gle_processing_status", "In Progress") + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + gle_count = frappe.db.count( + "GL Entry", + {"voucher_type": "Period Closing Voucher", "voucher_no": self.name, "is_cancelled": 0}, + ) + if gle_count > 5000: + frappe.enqueue( + make_reverse_gl_entries, + voucher_type="Period Closing Voucher", + voucher_no=self.name, + queue="long", + ) + frappe.msgprint( + _("The GL Entries will be cancelled in the background, it can take a few minutes."), alert=True + ) + else: + 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") if closing_account_type not in ["Liability", "Equity"]: - frappe.throw(_("Closing Account {0} must be of type Liability / Equity") - .format(self.closing_account_head)) + frappe.throw( + _("Closing Account {0} must be of type Liability / Equity").format(self.closing_account_head) + ) account_currency = get_account_currency(self.closing_account_head) - company_currency = frappe.get_cached_value('Company', self.company, "default_currency") + company_currency = frappe.get_cached_value("Company", self.company, "default_currency") if account_currency != company_currency: frappe.throw(_("Currency of the Closing Account must be {0}").format(company_currency)) def validate_posting_date(self): from erpnext.accounts.utils import get_fiscal_year, validate_fiscal_year - validate_fiscal_year(self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self) + validate_fiscal_year( + self.posting_date, self.fiscal_year, self.company, label=_("Posting Date"), doc=self + ) - self.year_start_date = get_fiscal_year(self.posting_date, self.fiscal_year, company=self.company)[1] + self.year_start_date = get_fiscal_year( + self.posting_date, self.fiscal_year, company=self.company + )[1] - pce = frappe.db.sql("""select name from `tabPeriod Closing Voucher` - where posting_date > %s and fiscal_year = %s and docstatus = 1""", - (self.posting_date, self.fiscal_year)) + pce = frappe.db.sql( + """select name from `tabPeriod Closing Voucher` + where posting_date > %s and fiscal_year = %s and docstatus = 1 and company = %s""", + (self.posting_date, self.fiscal_year, self.company), + ) if pce and pce[0][0]: - frappe.throw(_("Another Period Closing Entry {0} has been made after {1}") - .format(pce[0][0], self.posting_date)) + frappe.throw( + _("Another Period Closing Entry {0} has been made after {1}").format( + pce[0][0], self.posting_date + ) + ) def make_gl_entries(self): gl_entries = self.get_gl_entries() if gl_entries: - from erpnext.accounts.general_ledger import make_gl_entries - make_gl_entries(gl_entries) + if len(gl_entries) > 5000: + frappe.enqueue(process_gl_entries, gl_entries=gl_entries, queue="long") + frappe.msgprint( + _("The GL Entries will be processed in the background, it can take a few minutes."), + alert=True, + ) + else: + process_gl_entries(gl_entries) def get_gl_entries(self): gl_entries = [] - pl_accounts = self.get_pl_balances() - for acc in pl_accounts: + # pl account + for acc in self.get_pl_balances_based_on_dimensions(group_by_account=True): if flt(acc.bal_in_company_currency): - gl_entries.append(self.get_gl_dict({ - "account": acc.account, - "cost_center": acc.cost_center, - "finance_book": acc.finance_book, - "account_currency": acc.account_currency, - "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, - "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, - "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, - "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0 - }, item=acc)) + gl_entries.append(self.get_gle_for_pl_account(acc)) - if gl_entries: - gle_for_net_pl_bal = self.get_pnl_gl_entry(pl_accounts) - gl_entries += gle_for_net_pl_bal + # closing liability account + for acc in self.get_pl_balances_based_on_dimensions(group_by_account=False): + if flt(acc.bal_in_company_currency): + gl_entries.append(self.get_gle_for_closing_account(acc)) return gl_entries - def get_pnl_gl_entry(self, pl_accounts): - company_cost_center = frappe.db.get_value("Company", self.company, "cost_center") - gl_entries = [] + def get_gle_for_pl_account(self, acc): + gl_entry = self.get_gl_dict( + { + "account": acc.account, + "cost_center": acc.cost_center, + "finance_book": acc.finance_book, + "account_currency": acc.account_currency, + "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) + if flt(acc.bal_in_account_currency) < 0 + else 0, + "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, + "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) + if flt(acc.bal_in_account_currency) > 0 + else 0, + "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, + }, + item=acc, + ) + self.update_default_dimensions(gl_entry, acc) + return gl_entry - for acc in pl_accounts: - if flt(acc.bal_in_company_currency): - cost_center = acc.cost_center if self.cost_center_wise_pnl else company_cost_center - gl_entry = self.get_gl_dict({ - "account": self.closing_account_head, - "cost_center": cost_center, - "finance_book": acc.finance_book, - "account_currency": acc.account_currency, - "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) > 0 else 0, - "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, - "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) if flt(acc.bal_in_account_currency) < 0 else 0, - "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0 - }, item=acc) + def get_gle_for_closing_account(self, acc): + gl_entry = self.get_gl_dict( + { + "account": self.closing_account_head, + "cost_center": acc.cost_center, + "finance_book": acc.finance_book, + "account_currency": acc.account_currency, + "debit_in_account_currency": abs(flt(acc.bal_in_account_currency)) + if flt(acc.bal_in_account_currency) > 0 + else 0, + "debit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) > 0 else 0, + "credit_in_account_currency": abs(flt(acc.bal_in_account_currency)) + if flt(acc.bal_in_account_currency) < 0 + else 0, + "credit": abs(flt(acc.bal_in_company_currency)) if flt(acc.bal_in_company_currency) < 0 else 0, + }, + item=acc, + ) + self.update_default_dimensions(gl_entry, acc) + return gl_entry - self.update_default_dimensions(gl_entry) - - gl_entries.append(gl_entry) - - return gl_entries - - def update_default_dimensions(self, gl_entry): + def update_default_dimensions(self, gl_entry, acc): if not self.accounting_dimensions: self.accounting_dimensions = get_accounting_dimensions() - _, default_dimensions = get_dimensions() for dimension in self.accounting_dimensions: - gl_entry.update({ - dimension: default_dimensions.get(self.company, {}).get(dimension) - }) + gl_entry.update({dimension: acc.get(dimension)}) - def get_pl_balances(self): + def get_pl_balances_based_on_dimensions(self, group_by_account=False): """Get balance for dimension-wise pl accounts""" - dimension_fields = ['t1.cost_center', 't1.finance_book'] + dimension_fields = ["t1.cost_center", "t1.finance_book"] self.accounting_dimensions = get_accounting_dimensions() for dimension in self.accounting_dimensions: - dimension_fields.append('t1.{0}'.format(dimension)) + dimension_fields.append("t1.{0}".format(dimension)) - return frappe.db.sql(""" + if group_by_account: + dimension_fields.append("t1.account") + + return frappe.db.sql( + """ select - t1.account, t2.account_currency, {dimension_fields}, + t2.account_currency, + {dimension_fields}, sum(t1.debit_in_account_currency) - sum(t1.credit_in_account_currency) as bal_in_account_currency, sum(t1.debit) - sum(t1.credit) as bal_in_company_currency from `tabGL Entry` t1, `tabAccount` t2 - where t1.account = t2.name and t2.report_type = 'Profit and Loss' - and t2.docstatus < 2 and t2.company = %s - and t1.posting_date between %s and %s - group by t1.account, {dimension_fields} - """.format(dimension_fields = ', '.join(dimension_fields)), (self.company, self.get("year_start_date"), self.posting_date), as_dict=1) + where + t1.is_cancelled = 0 + and t1.account = t2.name + and t2.report_type = 'Profit and Loss' + and t2.docstatus < 2 + and t2.company = %s + and t1.posting_date between %s and %s + group by {dimension_fields} + """.format( + dimension_fields=", ".join(dimension_fields) + ), + (self.company, self.get("year_start_date"), self.posting_date), + as_dict=1, + ) + + +def process_gl_entries(gl_entries): + from erpnext.accounts.general_ledger import make_gl_entries + + try: + make_gl_entries(gl_entries, merge_entries=False) + frappe.db.set_value( + "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Completed" + ) + except Exception as e: + frappe.db.rollback() + frappe.log_error(e) + frappe.db.set_value( + "Period Closing Voucher", gl_entries[0].get("voucher_no"), "gle_processing_status", "Failed" + ) + + +def make_reverse_gl_entries(voucher_type, voucher_no): + from erpnext.accounts.general_ledger import make_reverse_gl_entries + + try: + make_reverse_gl_entries(voucher_type=voucher_type, voucher_no=voucher_no) + frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Completed") + except Exception as e: + frappe.db.rollback() + frappe.log_error(e) + frappe.db.set_value("Period Closing Voucher", voucher_no, "gle_processing_status", "Failed") diff --git a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py index 030b4caf7ca..93869ed6c04 100644 --- a/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py +++ b/erpnext/accounts/doctype/period_closing_voucher/test_period_closing_voucher.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import unittest import frappe @@ -19,7 +18,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") company = create_company() - cost_center = create_cost_center('Test Cost Center 1') + cost_center = create_cost_center("Test Cost Center 1") jv1 = make_journal_entry( amount=400, @@ -27,7 +26,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): account2="Sales - TPC", cost_center=cost_center, posting_date=now(), - save=False + save=False, ) jv1.company = company jv1.save() @@ -39,7 +38,7 @@ class TestPeriodClosingVoucher(unittest.TestCase): account2="Cash - TPC", cost_center=cost_center, posting_date=now(), - save=False + save=False, ) jv2.company = company jv2.save() @@ -49,15 +48,19 @@ class TestPeriodClosingVoucher(unittest.TestCase): surplus_account = pcv.closing_account_head expected_gle = ( - ('Cost of Goods Sold - TPC', 0.0, 600.0), - (surplus_account, 600.0, 400.0), - ('Sales - TPC', 400.0, 0.0) + ("Cost of Goods Sold - TPC", 0.0, 600.0), + (surplus_account, 200.0, 0.0), + ("Sales - TPC", 400.0, 0.0), ) - pcv_gle = frappe.db.sql(""" + pcv_gle = frappe.db.sql( + """ select account, debit, credit from `tabGL Entry` where voucher_no=%s order by account - """, (pcv.name)) - + """, + (pcv.name), + ) + pcv.reload() + self.assertEqual(pcv.gle_processing_status, "Completed") self.assertEqual(pcv_gle, expected_gle) def test_cost_center_wise_posting(self): @@ -75,19 +78,23 @@ class TestPeriodClosingVoucher(unittest.TestCase): income_account="Sales - TPC", expense_account="Cost of Goods Sold - TPC", rate=400, - debit_to="Debtors - TPC" + debit_to="Debtors - TPC", + currency="USD", + customer="_Test Customer USD", ) + create_sales_invoice( company=company, cost_center=cost_center2, income_account="Sales - TPC", expense_account="Cost of Goods Sold - TPC", rate=200, - debit_to="Debtors - TPC" + debit_to="Debtors - TPC", + currency="USD", + customer="_Test Customer USD", ) pcv = self.make_period_closing_voucher(submit=False) - pcv.cost_center_wise_pnl = 1 pcv.save() pcv.submit() surplus_account = pcv.closing_account_head @@ -95,18 +102,31 @@ class TestPeriodClosingVoucher(unittest.TestCase): expected_gle = ( (surplus_account, 0.0, 400.0, cost_center1), (surplus_account, 0.0, 200.0, cost_center2), - ('Sales - TPC', 400.0, 0.0, cost_center1), - ('Sales - TPC', 200.0, 0.0, cost_center2), + ("Sales - TPC", 400.0, 0.0, cost_center1), + ("Sales - TPC", 200.0, 0.0, cost_center2), ) - pcv_gle = frappe.db.sql(""" + pcv_gle = frappe.db.sql( + """ select account, debit, credit, cost_center from `tabGL Entry` where voucher_no=%s order by account, cost_center - """, (pcv.name)) + """, + (pcv.name), + ) self.assertEqual(pcv_gle, expected_gle) + pcv.reload() + pcv.cancel() + + self.assertFalse( + frappe.db.get_value( + "GL Entry", + {"voucher_type": "Period Closing Voucher", "voucher_no": pcv.name, "is_cancelled": 0}, + ) + ) + def test_period_closing_with_finance_book_entries(self): frappe.db.sql("delete from `tabGL Entry` where company='Test PCV Company'") @@ -114,20 +134,23 @@ class TestPeriodClosingVoucher(unittest.TestCase): surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") - create_sales_invoice( + si = create_sales_invoice( company=company, income_account="Sales - TPC", expense_account="Cost of Goods Sold - TPC", cost_center=cost_center, rate=400, - debit_to="Debtors - TPC" + debit_to="Debtors - TPC", + currency="USD", + customer="_Test Customer USD", ) + jv = make_journal_entry( account1="Cash - TPC", account2="Sales - TPC", amount=400, cost_center=cost_center, - posting_date=now() + posting_date=now(), ) jv.company = company jv.finance_book = create_finance_book().name @@ -140,69 +163,84 @@ class TestPeriodClosingVoucher(unittest.TestCase): expected_gle = ( (surplus_account, 0.0, 400.0, None), (surplus_account, 0.0, 400.0, jv.finance_book), - ('Sales - TPC', 400.0, 0.0, None), - ('Sales - TPC', 400.0, 0.0, jv.finance_book) + ("Sales - TPC", 400.0, 0.0, None), + ("Sales - TPC", 400.0, 0.0, jv.finance_book), ) - pcv_gle = frappe.db.sql(""" + pcv_gle = frappe.db.sql( + """ select account, debit, credit, finance_book from `tabGL Entry` where voucher_no=%s order by account, finance_book - """, (pcv.name)) + """, + (pcv.name), + ) self.assertEqual(pcv_gle, expected_gle) def make_period_closing_voucher(self, submit=True): surplus_account = create_account() cost_center = create_cost_center("Test Cost Center 1") - pcv = frappe.get_doc({ - "doctype": "Period Closing Voucher", - "transaction_date": today(), - "posting_date": today(), - "company": "Test PCV Company", - "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0], - "cost_center": cost_center, - "closing_account_head": surplus_account, - "remarks": "test" - }) + pcv = frappe.get_doc( + { + "doctype": "Period Closing Voucher", + "transaction_date": today(), + "posting_date": today(), + "company": "Test PCV Company", + "fiscal_year": get_fiscal_year(today(), company="Test PCV Company")[0], + "cost_center": cost_center, + "closing_account_head": surplus_account, + "remarks": "test", + } + ) pcv.insert() if submit: pcv.submit() return pcv + def create_company(): - company = frappe.get_doc({ - 'doctype': 'Company', - 'company_name': "Test PCV Company", - 'country': 'United States', - 'default_currency': 'USD' - }) - company.insert(ignore_if_duplicate = True) + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": "Test PCV Company", + "country": "United States", + "default_currency": "USD", + } + ) + company.insert(ignore_if_duplicate=True) return company.name + def create_account(): - account = frappe.get_doc({ - "account_name": "Reserve and Surplus", - "is_group": 0, - "company": "Test PCV Company", - "root_type": "Liability", - "report_type": "Balance Sheet", - "account_currency": "USD", - "parent_account": "Current Liabilities - TPC", - "doctype": "Account" - }).insert(ignore_if_duplicate = True) + account = frappe.get_doc( + { + "account_name": "Reserve and Surplus", + "is_group": 0, + "company": "Test PCV Company", + "root_type": "Liability", + "report_type": "Balance Sheet", + "account_currency": "USD", + "parent_account": "Current Liabilities - TPC", + "doctype": "Account", + } + ).insert(ignore_if_duplicate=True) return account.name + def create_cost_center(cc_name): - costcenter = frappe.get_doc({ - "company": "Test PCV Company", - "cost_center_name": cc_name, - "doctype": "Cost Center", - "parent_cost_center": "Test PCV Company - TPC" - }) - costcenter.insert(ignore_if_duplicate = True) + costcenter = frappe.get_doc( + { + "company": "Test PCV Company", + "cost_center_name": cc_name, + "doctype": "Cost Center", + "parent_cost_center": "Test PCV Company - TPC", + } + ) + costcenter.insert(ignore_if_duplicate=True) return costcenter.name + test_dependencies = ["Customer", "Cost Center"] test_records = frappe.get_test_records("Period Closing Voucher") diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js index 264d4a68b00..1d596c1bfbb 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js @@ -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) { @@ -64,13 +73,15 @@ frappe.ui.form.on('POS Closing Entry', { pos_opening_entry(frm) { if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) { reset_values(frm); - frm.trigger("set_opening_amounts"); - frm.trigger("get_pos_invoices"); + frappe.run_serially([ + () => frm.trigger("set_opening_amounts"), + () => frm.trigger("get_pos_invoices") + ]); } }, set_opening_amounts(frm) { - frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry) + return frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry) .then(({ balance_details }) => { balance_details.forEach(detail => { frm.add_child("payment_reconciliation", { @@ -83,7 +94,7 @@ frappe.ui.form.on('POS Closing Entry', { }, get_pos_invoices(frm) { - frappe.call({ + return frappe.call({ method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices', args: { start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), @@ -100,7 +111,9 @@ frappe.ui.form.on('POS Closing Entry', { }); }, - before_save: function(frm) { + before_save: async function(frm) { + frappe.dom.freeze(__('Processing Sales! Please Wait...')); + frm.set_value("grand_total", 0); frm.set_value("net_total", 0); frm.set_value("total_quantity", 0); @@ -110,17 +123,23 @@ frappe.ui.form.on('POS Closing Entry', { row.expected_amount = row.opening_amount; } - for (let row of frm.doc.pos_transactions) { - frappe.db.get_doc("POS Invoice", row.pos_invoice).then(doc => { - frm.doc.grand_total += flt(doc.grand_total); - frm.doc.net_total += flt(doc.net_total); - frm.doc.total_quantity += flt(doc.total_qty); - refresh_payments(doc, frm); - refresh_taxes(doc, frm); - refresh_fields(frm); - set_html_data(frm); - }); + const pos_inv_promises = frm.doc.pos_transactions.map( + row => frappe.db.get_doc("POS Invoice", row.pos_invoice) + ); + + const pos_invoices = await Promise.all(pos_inv_promises); + + for (let doc of pos_invoices) { + frm.doc.grand_total += flt(doc.grand_total); + frm.doc.net_total += flt(doc.net_total); + frm.doc.total_quantity += flt(doc.total_qty); + refresh_payments(doc, frm); + refresh_taxes(doc, frm); + refresh_fields(frm); + set_html_data(frm); } + + frappe.dom.unfreeze(); } }); diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json index d6e35c6a50d..9d15e6cf357 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json @@ -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 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py index 07059cb7aa3..655c4ec0035 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py @@ -15,6 +15,9 @@ 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")) @@ -23,21 +26,33 @@ class POSClosingEntry(StatusUpdater): def validate_pos_invoices(self): invalid_rows = [] for d in self.pos_transactions: - invalid_row = {'idx': d.idx} - pos_invoice = frappe.db.get_values("POS Invoice", d.pos_invoice, - ["consolidated_invoice", "pos_profile", "docstatus", "owner"], as_dict=1)[0] + invalid_row = {"idx": d.idx} + pos_invoice = frappe.db.get_values( + "POS Invoice", + d.pos_invoice, + ["consolidated_invoice", "pos_profile", "docstatus", "owner"], + as_dict=1, + )[0] if pos_invoice.consolidated_invoice: - invalid_row.setdefault('msg', []).append(_('POS Invoice is {}').format(frappe.bold("already consolidated"))) + invalid_row.setdefault("msg", []).append( + _("POS Invoice is {}").format(frappe.bold("already consolidated")) + ) invalid_rows.append(invalid_row) continue if pos_invoice.pos_profile != self.pos_profile: - invalid_row.setdefault('msg', []).append(_("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile))) + invalid_row.setdefault("msg", []).append( + _("POS Profile doesn't matches {}").format(frappe.bold(self.pos_profile)) + ) if pos_invoice.docstatus != 1: - invalid_row.setdefault('msg', []).append(_('POS Invoice is not {}').format(frappe.bold("submitted"))) + invalid_row.setdefault("msg", []).append( + _("POS Invoice is not {}").format(frappe.bold("submitted")) + ) if pos_invoice.owner != self.user: - invalid_row.setdefault('msg', []).append(_("POS Invoice isn't created by user {}").format(frappe.bold(self.owner))) + invalid_row.setdefault("msg", []).append( + _("POS Invoice isn't created by user {}").format(frappe.bold(self.owner)) + ) - if invalid_row.get('msg'): + if invalid_row.get("msg"): invalid_rows.append(invalid_row) if not invalid_rows: @@ -45,16 +60,18 @@ class POSClosingEntry(StatusUpdater): error_list = [] for row in invalid_rows: - for msg in row.get('msg'): - error_list.append(_("Row #{}: {}").format(row.get('idx'), msg)) + for msg in row.get("msg"): + error_list.append(_("Row #{}: {}").format(row.get("idx"), msg)) frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True) @frappe.whitelist() def get_payment_reconciliation_details(self): - currency = frappe.get_cached_value('Company', self.company, "default_currency") - return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", - {"data": self, "currency": currency}) + currency = frappe.get_cached_value("Company", self.company, "default_currency") + return frappe.render_template( + "erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", + {"data": self, "currency": currency}, + ) def on_submit(self): consolidate_pos_invoices(closing_entry=self) @@ -72,29 +89,38 @@ class POSClosingEntry(StatusUpdater): opening_entry.set_status() opening_entry.save() + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_cashiers(doctype, txt, searchfield, start, page_len, filters): - cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user'], as_list=1) + cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=["user"], as_list=1) return [c for c in cashiers_list] + @frappe.whitelist() def get_pos_invoices(start, end, pos_profile, user): - data = frappe.db.sql(""" + data = frappe.db.sql( + """ select name, timestamp(posting_date, posting_time) as "timestamp" from `tabPOS Invoice` where owner = %s and docstatus = 1 and pos_profile = %s and ifnull(consolidated_invoice,'') = '' - """, (user, pos_profile), as_dict=1) + """, + (user, pos_profile), + as_dict=1, + ) - data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)) + data = list( + filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data) + ) # need to get taxes and payments so can't avoid get_doc data = [frappe.get_doc("POS Invoice", d.name).as_dict() for d in data] return data + def make_closing_entry_from_opening(opening_entry): closing_entry = frappe.new_doc("POS Closing Entry") closing_entry.pos_opening_entry = opening_entry.name @@ -107,26 +133,38 @@ def make_closing_entry_from_opening(opening_entry): closing_entry.net_total = 0 closing_entry.total_quantity = 0 - invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, - closing_entry.pos_profile, closing_entry.user) + invoices = get_pos_invoices( + closing_entry.period_start_date, + closing_entry.period_end_date, + closing_entry.pos_profile, + closing_entry.user, + ) pos_transactions = [] taxes = [] payments = [] for detail in opening_entry.balance_details: - payments.append(frappe._dict({ - 'mode_of_payment': detail.mode_of_payment, - 'opening_amount': detail.opening_amount, - 'expected_amount': detail.opening_amount - })) + payments.append( + frappe._dict( + { + "mode_of_payment": detail.mode_of_payment, + "opening_amount": detail.opening_amount, + "expected_amount": detail.opening_amount, + } + ) + ) for d in invoices: - pos_transactions.append(frappe._dict({ - 'pos_invoice': d.name, - 'posting_date': d.posting_date, - 'grand_total': d.grand_total, - 'customer': d.customer - })) + pos_transactions.append( + frappe._dict( + { + "pos_invoice": d.name, + "posting_date": d.posting_date, + "grand_total": d.grand_total, + "customer": d.customer, + } + ) + ) closing_entry.grand_total += flt(d.grand_total) closing_entry.net_total += flt(d.net_total) closing_entry.total_quantity += flt(d.total_qty) @@ -134,24 +172,22 @@ def make_closing_entry_from_opening(opening_entry): for t in d.taxes: existing_tax = [tx for tx in taxes if tx.account_head == t.account_head and tx.rate == t.rate] if existing_tax: - existing_tax[0].amount += flt(t.tax_amount); + existing_tax[0].amount += flt(t.tax_amount) else: - taxes.append(frappe._dict({ - 'account_head': t.account_head, - 'rate': t.rate, - 'amount': t.tax_amount - })) + taxes.append( + frappe._dict({"account_head": t.account_head, "rate": t.rate, "amount": t.tax_amount}) + ) for p in d.payments: existing_pay = [pay for pay in payments if pay.mode_of_payment == p.mode_of_payment] if existing_pay: - existing_pay[0].expected_amount += flt(p.amount); + existing_pay[0].expected_amount += flt(p.amount) else: - payments.append(frappe._dict({ - 'mode_of_payment': p.mode_of_payment, - 'opening_amount': 0, - 'expected_amount': p.amount - })) + payments.append( + frappe._dict( + {"mode_of_payment": p.mode_of_payment, "opening_amount": 0, "expected_amount": p.amount} + ) + ) closing_entry.set("pos_transactions", pos_transactions) closing_entry.set("payment_reconciliation", payments) diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py index c40cd363d8f..2d4acc4d047 100644 --- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py +++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py @@ -28,24 +28,20 @@ class TestPOSClosingEntry(unittest.TestCase): opening_entry = create_opening_entry(pos_profile, test_user.name) pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) - pos_inv1.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500 - }) + pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) pos_inv2.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) payment = pcv_doc.payment_reconciliation[0] - self.assertEqual(payment.mode_of_payment, 'Cash') + self.assertEqual(payment.mode_of_payment, "Cash") for d in pcv_doc.payment_reconciliation: - if d.mode_of_payment == 'Cash': + if d.mode_of_payment == "Cash": d.closing_amount = 6700 pcv_doc.submit() @@ -58,24 +54,20 @@ class TestPOSClosingEntry(unittest.TestCase): opening_entry = create_opening_entry(pos_profile, test_user.name) pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) - pos_inv1.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500 - }) + pos_inv1.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3500}) pos_inv1.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) pos_inv2.submit() pcv_doc = make_closing_entry_from_opening(opening_entry) payment = pcv_doc.payment_reconciliation[0] - self.assertEqual(payment.mode_of_payment, 'Cash') + self.assertEqual(payment.mode_of_payment, "Cash") for d in pcv_doc.payment_reconciliation: - if d.mode_of_payment == 'Cash': + if d.mode_of_payment == "Cash": d.closing_amount = 6700 pcv_doc.submit() @@ -91,22 +83,19 @@ class TestPOSClosingEntry(unittest.TestCase): si_doc.load_from_db() pos_inv1.load_from_db() self.assertEqual(si_doc.docstatus, 2) - self.assertEqual(pos_inv1.status, 'Paid') + self.assertEqual(pos_inv1.status, "Paid") def init_user_and_profile(**args): - user = 'test@example.com' - test_user = frappe.get_doc('User', user) + user = "test@example.com" + test_user = frappe.get_doc("User", user) roles = ("Accounts Manager", "Accounts User", "Sales Manager") test_user.add_roles(*roles) frappe.set_user(user) pos_profile = make_pos_profile(**args) - pos_profile.append('applicable_for_users', { - 'default': 1, - 'user': user - }) + pos_profile.append("applicable_for_users", {"default": 1, "user": user}) pos_profile.save() diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json index 0c6e7edeb02..6458756957d 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json @@ -264,7 +264,6 @@ "print_hide": 1 }, { - "allow_on_submit": 1, "default": "0", "fieldname": "is_return", "fieldtype": "Check", @@ -1573,7 +1572,7 @@ "icon": "fa fa-file-text", "is_submittable": 1, "links": [], - "modified": "2021-10-05 12:11:53.871828", + "modified": "2022-09-27 13:00:24.166684", "modified_by": "Administrator", "module": "Accounts", "name": "POS Invoice", @@ -1623,6 +1622,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "title", "track_changes": 1, diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py index 814372f6b35..6f1434d429b 100644 --- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py @@ -16,7 +16,12 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_multi_mode_option, ) from erpnext.accounts.party import get_due_date, get_party_account -from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos +from erpnext.stock.doctype.batch.batch import get_batch_qty, get_pos_reserved_batch_qty +from erpnext.stock.doctype.serial_no.serial_no import ( + get_delivered_serial_nos, + get_pos_reserved_serial_nos, + get_serial_nos, +) class POSInvoice(SalesInvoice): @@ -25,7 +30,9 @@ class POSInvoice(SalesInvoice): def validate(self): if not cint(self.is_pos): - frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))) + frappe.throw( + _("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment")) + ) # run on validate method of selling controller super(SalesInvoice, self).validate() @@ -42,7 +49,6 @@ class POSInvoice(SalesInvoice): self.validate_serialised_or_batched_item() self.validate_stock_availablility() self.validate_return_items_qty() - self.validate_non_stock_items() self.set_status() self.set_account_for_mode_of_payment() self.validate_pos() @@ -50,11 +56,12 @@ class POSInvoice(SalesInvoice): self.validate_loyalty_transaction() if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import validate_coupon_code + validate_coupon_code(self.coupon_code) def on_submit(self): # create the loyalty point ledger entry if the customer is enrolled in any loyalty program - if self.loyalty_program: + if not self.is_return and self.loyalty_program: self.make_loyalty_point_entry() elif self.is_return and self.return_against and self.loyalty_program: against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) @@ -67,28 +74,32 @@ class POSInvoice(SalesInvoice): if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count - update_coupon_code_count(self.coupon_code,'used') + + update_coupon_code_count(self.coupon_code, "used") def before_cancel(self): - if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1: + if ( + self.consolidated_invoice + and frappe.db.get_value("Sales Invoice", self.consolidated_invoice, "docstatus") == 1 + ): pos_closing_entry = frappe.get_all( "POS Invoice Reference", ignore_permissions=True, - filters={ 'pos_invoice': self.name }, + filters={"pos_invoice": self.name}, pluck="parent", - limit=1 + limit=1, ) frappe.throw( - _('You need to cancel POS Closing Entry {} to be able to cancel this document.').format( + _("You need to cancel POS Closing Entry {} to be able to cancel this document.").format( get_link_to_form("POS Closing Entry", pos_closing_entry[0]) ), - title=_('Not Allowed') + title=_("Not Allowed"), ) def on_cancel(self): # run on cancel method of selling controller super(SalesInvoice, self).on_cancel() - if self.loyalty_program: + if not self.is_return and self.loyalty_program: self.delete_loyalty_point_entry() elif self.is_return and self.return_against and self.loyalty_program: against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) @@ -97,16 +108,22 @@ class POSInvoice(SalesInvoice): if self.coupon_code: from erpnext.accounts.doctype.pricing_rule.utils import update_coupon_code_count - update_coupon_code_count(self.coupon_code,'cancelled') + + update_coupon_code_count(self.coupon_code, "cancelled") def check_phone_payments(self): for pay in self.payments: if pay.type == "Phone" and pay.amount >= 0: - paid_amt = frappe.db.get_value("Payment Request", + paid_amt = frappe.db.get_value( + "Payment Request", filters=dict( - reference_doctype="POS Invoice", reference_name=self.name, - mode_of_payment=pay.mode_of_payment, status="Paid"), - fieldname="grand_total") + reference_doctype="POS Invoice", + reference_name=self.name, + mode_of_payment=pay.mode_of_payment, + status="Paid", + ), + fieldname="grand_total", + ) if paid_amt and pay.amount != paid_amt: return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment)) @@ -120,49 +137,122 @@ class POSInvoice(SalesInvoice): reserved_serial_nos = get_pos_reserved_serial_nos(filters) invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos] - bold_invalid_serial_nos = frappe.bold(', '.join(invalid_serial_nos)) + bold_invalid_serial_nos = frappe.bold(", ".join(invalid_serial_nos)) if len(invalid_serial_nos) == 1: - frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no.") - .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable")) + frappe.throw( + _( + "Row #{}: Serial No. {} has already been transacted into another POS Invoice. Please select valid serial no." + ).format(item.idx, bold_invalid_serial_nos), + title=_("Item Unavailable"), + ) elif invalid_serial_nos: - frappe.throw(_("Row #{}: Serial Nos. {} has already been transacted into another POS Invoice. Please select valid serial no.") - .format(item.idx, bold_invalid_serial_nos), title=_("Item Unavailable")) + frappe.throw( + _( + "Row #{}: Serial Nos. {} have already been transacted into another POS Invoice. Please select valid serial no." + ).format(item.idx, bold_invalid_serial_nos), + title=_("Item Unavailable"), + ) + + def validate_pos_reserved_batch_qty(self, item): + filters = {"item_code": item.item_code, "warehouse": item.warehouse, "batch_no": item.batch_no} + + available_batch_qty = get_batch_qty(item.batch_no, item.warehouse, item.item_code) + reserved_batch_qty = get_pos_reserved_batch_qty(filters) + + bold_item_name = frappe.bold(item.item_name) + bold_extra_batch_qty_needed = frappe.bold( + abs(available_batch_qty - reserved_batch_qty - item.qty) + ) + bold_invalid_batch_no = frappe.bold(item.batch_no) + + if (available_batch_qty - reserved_batch_qty) == 0: + frappe.throw( + _( + "Row #{}: Batch No. {} of item {} has no stock available. Please select valid batch no." + ).format(item.idx, bold_invalid_batch_no, bold_item_name), + title=_("Item Unavailable"), + ) + elif (available_batch_qty - reserved_batch_qty - item.qty) < 0: + frappe.throw( + _( + "Row #{}: Batch No. {} of item {} has less than required stock available, {} more required" + ).format( + item.idx, bold_invalid_batch_no, bold_item_name, bold_extra_batch_qty_needed + ), + title=_("Item Unavailable"), + ) def validate_delivered_serial_nos(self, item): - serial_nos = get_serial_nos(item.serial_no) - delivered_serial_nos = frappe.db.get_list('Serial No', { - 'item_code': item.item_code, - 'name': ['in', serial_nos], - 'sales_invoice': ['is', 'set'] - }, pluck='name') + delivered_serial_nos = get_delivered_serial_nos(item.serial_no) if delivered_serial_nos: - bold_delivered_serial_nos = frappe.bold(', '.join(delivered_serial_nos)) - frappe.throw(_("Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no.") - .format(item.idx, bold_delivered_serial_nos), title=_("Item Unavailable")) + bold_delivered_serial_nos = frappe.bold(", ".join(delivered_serial_nos)) + frappe.throw( + _( + "Row #{}: Serial No. {} has already been transacted into another Sales Invoice. Please select valid serial no." + ).format(item.idx, bold_delivered_serial_nos), + title=_("Item Unavailable"), + ) + + def validate_invalid_serial_nos(self, item): + serial_nos = get_serial_nos(item.serial_no) + error_msg = [] + invalid_serials, msg = "", "" + for serial_no in serial_nos: + if not frappe.db.exists("Serial No", serial_no): + invalid_serials = invalid_serials + (", " if invalid_serials else "") + serial_no + msg = _("Row #{}: Following Serial numbers for item {} are Invalid: {}").format( + item.idx, frappe.bold(item.get("item_code")), frappe.bold(invalid_serials) + ) + if invalid_serials: + error_msg.append(msg) + + if error_msg: + frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) def validate_stock_availablility(self): - if self.is_return or self.docstatus != 1: + if self.is_return: return - allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') - for d in self.get('items'): + if self.docstatus == 0 and not frappe.db.get_value( + "POS Profile", self.pos_profile, "validate_stock_on_save" + ): + return + + allow_negative_stock = frappe.db.get_single_value("Stock Settings", "allow_negative_stock") + + for d in self.get("items"): if d.serial_no: self.validate_pos_reserved_serial_nos(d) self.validate_delivered_serial_nos(d) + self.validate_invalid_serial_nos(d) + elif d.batch_no: + self.validate_pos_reserved_batch_qty(d) else: if allow_negative_stock: return - available_stock = get_stock_availability(d.item_code, d.warehouse) + available_stock, is_stock_item = get_stock_availability(d.item_code, d.warehouse) - item_code, warehouse, qty = frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty) - if 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): - frappe.throw(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.') - .format(d.idx, item_code, warehouse, available_stock), title=_("Item Unavailable")) + item_code, warehouse, qty = ( + frappe.bold(d.item_code), + frappe.bold(d.warehouse), + frappe.bold(d.qty), + ) + 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 is_stock_item and flt(available_stock) < flt(d.qty): + frappe.throw( + _( + "Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}." + ).format(d.idx, item_code, warehouse, available_stock), + title=_("Item Unavailable"), + ) def validate_serialised_or_batched_item(self): error_msg = [] @@ -176,16 +266,21 @@ class POSInvoice(SalesInvoice): item_code = frappe.bold(d.item_code) serial_nos = get_serial_nos(d.serial_no) if serialized and batched and (no_batch_selected or no_serial_selected): - msg = (_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.') - .format(d.idx, item_code)) + msg = _( + "Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction." + ).format(d.idx, item_code) elif serialized and no_serial_selected: - msg = (_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.') - .format(d.idx, item_code)) + msg = _( + "Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction." + ).format(d.idx, item_code) elif batched and no_batch_selected: - msg = (_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.') - .format(d.idx, item_code)) + msg = _( + "Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction." + ).format(d.idx, item_code) elif serialized and not no_serial_selected and len(serial_nos) != d.qty: - msg = (_("Row #{}: You must select {} serial numbers for item {}.").format(d.idx, frappe.bold(cint(d.qty)), item_code)) + msg = _("Row #{}: You must select {} serial numbers for item {}.").format( + d.idx, frappe.bold(cint(d.qty)), item_code + ) if msg: error_msg.append(msg) @@ -194,18 +289,22 @@ class POSInvoice(SalesInvoice): frappe.throw(error_msg, title=_("Invalid Item"), as_list=True) def validate_return_items_qty(self): - if not self.get("is_return"): return + if not self.get("is_return"): + return for d in self.get("items"): if d.get("qty") > 0: frappe.throw( - _("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.") - .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item") + _( + "Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return." + ).format(d.idx, frappe.bold(d.item_code)), + title=_("Invalid Item"), ) if d.get("serial_no"): serial_nos = get_serial_nos(d.serial_no) for sr in serial_nos: - serial_no_exists = frappe.db.sql(""" + serial_no_exists = frappe.db.sql( + """ SELECT name FROM `tabPOS Invoice Item` WHERE @@ -215,39 +314,43 @@ class POSInvoice(SalesInvoice): or serial_no like %s or serial_no like %s ) - """, (self.return_against, sr, sr+'\n%', '%\n'+sr, '%\n'+sr+'\n%')) + """, + (self.return_against, sr, sr + "\n%", "%\n" + sr, "%\n" + sr + "\n%"), + ) if not serial_no_exists: bold_return_against = frappe.bold(self.return_against) bold_serial_no = frappe.bold(sr) frappe.throw( - _("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}") - .format(d.idx, bold_serial_no, bold_return_against) + _( + "Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}" + ).format(d.idx, bold_serial_no, bold_return_against) ) - def validate_non_stock_items(self): - for d in self.get("items"): - is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item") - if not is_stock_item: - if not frappe.db.exists('Product Bundle', d.item_code): - frappe.throw(_("Row #{}: Item {} is a non stock item. You can only include stock items in a POS Invoice.") - .format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) - def validate_mode_of_payment(self): if len(self.payments) == 0: frappe.throw(_("At least one mode of payment is required for POS invoice.")) def validate_change_account(self): - if self.change_amount and self.account_for_change_amount and \ - frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company: - frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company)) + if ( + self.change_amount + and self.account_for_change_amount + and frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company + ): + frappe.throw( + _("The selected change account {} doesn't belongs to Company {}.").format( + self.account_for_change_amount, self.company + ) + ) def validate_change_amount(self): grand_total = flt(self.rounded_total) or flt(self.grand_total) base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total) if not flt(self.change_amount) and grand_total < flt(self.paid_amount): self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount)) - self.base_change_amount = flt(self.base_paid_amount) - base_grand_total + flt(self.base_write_off_amount) + self.base_change_amount = ( + flt(self.base_paid_amount) - base_grand_total + flt(self.base_write_off_amount) + ) if flt(self.change_amount) and not self.account_for_change_amount: frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1) @@ -267,8 +370,12 @@ class POSInvoice(SalesInvoice): frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total)) def validate_loyalty_transaction(self): - if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center): - expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"]) + if self.redeem_loyalty_points and ( + not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center + ): + expense_account, cost_center = frappe.db.get_value( + "Loyalty Program", self.loyalty_program, ["expense_account", "cost_center"] + ) if not self.loyalty_redemption_account: self.loyalty_redemption_account = expense_account if not self.loyalty_redemption_cost_center: @@ -279,8 +386,8 @@ class POSInvoice(SalesInvoice): def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): - if self.get('amended_from'): - self.status = 'Draft' + if self.get("amended_from"): + self.status = "Draft" return if not status: @@ -289,19 +396,35 @@ class POSInvoice(SalesInvoice): elif self.docstatus == 1: if self.consolidated_invoice: self.status = "Consolidated" - elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed': + elif ( + flt(self.outstanding_amount) > 0 + and getdate(self.due_date) < getdate(nowdate()) + and self.is_discounted + and self.get_discounting_status() == "Disbursed" + ): self.status = "Overdue and Discounted" elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()): self.status = "Overdue" - elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed': + elif ( + flt(self.outstanding_amount) > 0 + and getdate(self.due_date) >= getdate(nowdate()) + and self.is_discounted + and self.get_discounting_status() == "Disbursed" + ): self.status = "Unpaid and Discounted" elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()): self.status = "Unpaid" - elif flt(self.outstanding_amount) <= 0 and self.is_return == 0 and frappe.db.get_value('POS Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): + elif ( + flt(self.outstanding_amount) <= 0 + and self.is_return == 0 + and frappe.db.get_value( + "POS Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} + ) + ): self.status = "Credit Note Issued" elif self.is_return == 1: self.status = "Return" - elif flt(self.outstanding_amount)<=0: + elif flt(self.outstanding_amount) <= 0: self.status = "Paid" else: self.status = "Submitted" @@ -309,22 +432,23 @@ class POSInvoice(SalesInvoice): self.status = "Draft" if update: - self.db_set('status', self.status, update_modified = update_modified) + self.db_set("status", self.status, update_modified=update_modified) def set_pos_fields(self, for_validate=False): """Set retail related fields from POS Profiles""" from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details + if not self.pos_profile: pos_profile = get_pos_profile(self.company) or {} if not pos_profile: frappe.throw(_("No POS Profile found. Please create a New POS Profile first")) - self.pos_profile = pos_profile.get('name') + self.pos_profile = pos_profile.get("name") profile = {} if self.pos_profile: - profile = frappe.get_doc('POS Profile', self.pos_profile) + profile = frappe.get_doc("POS Profile", self.pos_profile) - if not self.get('payments') and not for_validate: + if not self.get("payments") and not for_validate: update_multi_mode_option(self, profile) if self.is_return and not for_validate: @@ -334,36 +458,55 @@ class POSInvoice(SalesInvoice): if not for_validate and not self.customer: self.customer = profile.customer - self.ignore_pricing_rule = profile.ignore_pricing_rule - self.account_for_change_amount = profile.get('account_for_change_amount') or self.account_for_change_amount - self.set_warehouse = profile.get('warehouse') or self.set_warehouse + self.account_for_change_amount = ( + profile.get("account_for_change_amount") or self.account_for_change_amount + ) + self.set_warehouse = profile.get("warehouse") or self.set_warehouse - for fieldname in ('currency', 'letter_head', 'tc_name', - 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges', - 'write_off_cost_center', 'apply_discount_on', 'cost_center', 'tax_category', - 'ignore_pricing_rule', 'company_address', 'update_stock'): - if not for_validate: - self.set(fieldname, profile.get(fieldname)) + for fieldname in ( + "currency", + "letter_head", + "tc_name", + "company", + "select_print_heading", + "write_off_account", + "taxes_and_charges", + "write_off_cost_center", + "apply_discount_on", + "cost_center", + "tax_category", + "ignore_pricing_rule", + "company_address", + "update_stock", + ): + if not for_validate: + self.set(fieldname, profile.get(fieldname)) if self.customer: customer_price_list, customer_group, customer_currency = frappe.db.get_value( - "Customer", self.customer, ['default_price_list', 'customer_group', 'default_currency'] + "Customer", self.customer, ["default_price_list", "customer_group", "default_currency"] ) - customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list') - selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list') - if customer_currency != profile.get('currency'): - self.set('currency', customer_currency) + customer_group_price_list = frappe.db.get_value( + "Customer Group", customer_group, "default_price_list" + ) + selling_price_list = ( + customer_price_list or customer_group_price_list or profile.get("selling_price_list") + ) + if customer_currency != profile.get("currency"): + self.set("currency", customer_currency) else: - selling_price_list = profile.get('selling_price_list') + selling_price_list = profile.get("selling_price_list") if selling_price_list: - self.set('selling_price_list', selling_price_list) + self.set("selling_price_list", selling_price_list) # set pos values in items for item in self.get("items"): - if item.get('item_code'): - profile_details = get_pos_profile_item_details(profile.get("company"), frappe._dict(item.as_dict()), profile) + if item.get("item_code"): + profile_details = get_pos_profile_item_details( + profile.get("company"), frappe._dict(item.as_dict()), profile + ) for fname, val in iteritems(profile_details): if (not for_validate) or (for_validate and not item.get(fname)): item.set(fname, val) @@ -377,7 +520,9 @@ class POSInvoice(SalesInvoice): self.set_taxes() if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') + self.account_for_change_amount = frappe.get_cached_value( + "Company", self.company, "default_cash_account" + ) return profile @@ -387,32 +532,33 @@ 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.db.get_value( + "Account", self.debit_to, "account_currency", cache=True + ) if not self.due_date and self.customer: self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company) super(SalesInvoice, self).set_missing_values(for_validate) print_format = profile.get("print_format") if profile else None - if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')): - print_format = 'POS Invoice' + if not print_format and not cint(frappe.db.get_value("Print Format", "POS Invoice", "disabled")): + print_format = "POS Invoice" if profile: return { "print_format": print_format, "campaign": profile.get("campaign"), - "allow_print_before_pay": profile.get("allow_print_before_pay") + "allow_print_before_pay": profile.get("allow_print_before_pay"), } @frappe.whitelist() def reset_mode_of_payments(self): if self.pos_profile: - pos_profile = frappe.get_cached_doc('POS Profile', self.pos_profile) + pos_profile = frappe.get_cached_doc("POS Profile", self.pos_profile) update_multi_mode_option(self, pos_profile) self.paid_amount = 0 def set_account_for_mode_of_payment(self): - self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default] for pay in self.payments: if not pay.account: pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") @@ -430,6 +576,7 @@ class POSInvoice(SalesInvoice): pay_req = self.get_existing_payment_request(pay) if not pay_req: pay_req = self.get_new_payment_request(pay) + pay_req.insert() pay_req.submit() else: pay_req.request_phone_payment() @@ -437,9 +584,13 @@ class POSInvoice(SalesInvoice): return pay_req def get_new_payment_request(self, mop): - payment_gateway_account = frappe.db.get_value("Payment Gateway Account", { - "payment_account": mop.account, - }, ["name"]) + payment_gateway_account = frappe.db.get_value( + "Payment Gateway Account", + { + "payment_account": mop.account, + }, + ["name"], + ) args = { "dt": "POS Invoice", @@ -450,38 +601,50 @@ class POSInvoice(SalesInvoice): "payment_request_type": "Inward", "party_type": "Customer", "party": self.customer, - "return_doc": True + "return_doc": True, } return make_payment_request(**args) def get_existing_payment_request(self, pay): - payment_gateway_account = frappe.db.get_value("Payment Gateway Account", { - "payment_account": pay.account, - }, ["name"]) + payment_gateway_account = frappe.db.get_value( + "Payment Gateway Account", + { + "payment_account": pay.account, + }, + ["name"], + ) args = { - 'doctype': 'Payment Request', - 'reference_doctype': 'POS Invoice', - 'reference_name': self.name, - 'payment_gateway_account': payment_gateway_account, - 'email_to': self.contact_mobile + "doctype": "Payment Request", + "reference_doctype": "POS Invoice", + "reference_name": self.name, + "payment_gateway_account": payment_gateway_account, + "email_to": self.contact_mobile, } pr = frappe.db.exists(args) if pr: - return frappe.get_doc('Payment Request', pr[0][0]) + return frappe.get_doc("Payment Request", pr[0][0]) + @frappe.whitelist() def get_stock_availability(item_code, warehouse): - if frappe.db.get_value('Item', item_code, 'is_stock_item'): + if frappe.db.get_value("Item", item_code, "is_stock_item"): + is_stock_item = True bin_qty = get_bin_qty(item_code, warehouse) pos_sales_qty = get_pos_reserved_qty(item_code, warehouse) - return bin_qty - pos_sales_qty + return bin_qty - pos_sales_qty, is_stock_item else: - if frappe.db.exists('Product Bundle', item_code): - return get_bundle_availability(item_code, warehouse) + is_stock_item = True + if frappe.db.exists("Product Bundle", item_code): + return get_bundle_availability(item_code, warehouse), is_stock_item + else: + is_stock_item = False + # Is a service item or non_stock item + return 0, is_stock_item + def get_bundle_availability(bundle_item_code, warehouse): - product_bundle = frappe.get_doc('Product Bundle', bundle_item_code) + product_bundle = frappe.get_doc("Product Bundle", bundle_item_code) bundle_bin_qty = 1000000 for item in product_bundle.items: @@ -490,36 +653,51 @@ 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) return bundle_bin_qty - pos_sales_qty + def get_bin_qty(item_code, warehouse): - bin_qty = frappe.db.sql("""select actual_qty from `tabBin` + bin_qty = frappe.db.sql( + """select actual_qty from `tabBin` where item_code = %s and warehouse = %s - limit 1""", (item_code, warehouse), as_dict=1) + limit 1""", + (item_code, warehouse), + as_dict=1, + ) return bin_qty[0].actual_qty or 0 if bin_qty else 0 + def get_pos_reserved_qty(item_code, warehouse): - reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty + reserved_qty = frappe.db.sql( + """select sum(p_item.qty) as qty from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item where p.name = p_item.parent and ifnull(p.consolidated_invoice, '') = '' and p_item.docstatus = 1 and p_item.item_code = %s and p_item.warehouse = %s - """, (item_code, warehouse), as_dict=1) + """, + (item_code, warehouse), + as_dict=1, + ) return reserved_qty[0].qty or 0 if reserved_qty else 0 + @frappe.whitelist() def make_sales_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("POS Invoice", source_name, target_doc) + @frappe.whitelist() def make_merge_log(invoices): import json @@ -530,35 +708,46 @@ def make_merge_log(invoices): invoices = json.loads(invoices) if len(invoices) == 0: - frappe.throw(_('Atleast one invoice has to be selected.')) + frappe.throw(_("Atleast one invoice has to be selected.")) merge_log = frappe.new_doc("POS Invoice Merge Log") merge_log.posting_date = getdate(nowdate()) for inv in invoices: - inv_data = frappe.db.get_values("POS Invoice", inv.get('name'), - ["customer", "posting_date", "grand_total"], as_dict=1)[0] + inv_data = frappe.db.get_values( + "POS Invoice", inv.get("name"), ["customer", "posting_date", "grand_total"], as_dict=1 + )[0] merge_log.customer = inv_data.customer - merge_log.append("pos_invoices", { - 'pos_invoice': inv.get('name'), - 'customer': inv_data.customer, - 'posting_date': inv_data.posting_date, - 'grand_total': inv_data.grand_total - }) + merge_log.append( + "pos_invoices", + { + "pos_invoice": inv.get("name"), + "customer": inv_data.customer, + "posting_date": inv_data.posting_date, + "grand_total": inv_data.grand_total, + }, + ) - if merge_log.get('pos_invoices'): + if merge_log.get("pos_invoices"): return merge_log.as_dict() + def add_return_modes(doc, pos_profile): def append_payment(payment_mode): - payment = doc.append('payments', {}) + payment = doc.append("payments", {}) payment.default = payment_mode.default payment.mode_of_payment = payment_mode.parent payment.account = payment_mode.default_account payment.type = payment_mode.type - for pos_payment_method in pos_profile.get('payments'): + for pos_payment_method in pos_profile.get("payments"): pos_payment_method = pos_payment_method.as_dict() mode_of_payment = pos_payment_method.mode_of_payment - if pos_payment_method.allow_in_returns and not [d for d in doc.get('payments') if d.mode_of_payment == mode_of_payment]: + if pos_payment_method.allow_in_returns and not [ + d for d in doc.get("payments") if d.mode_of_payment == mode_of_payment + ]: payment_mode = get_mode_of_payment_info(mode_of_payment, doc.company) append_payment(payment_mode[0]) + + +def on_doctype_update(): + frappe.db.add_index("POS Invoice", ["return_against"]) diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py index 66963335376..70f128e0e39 100644 --- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py +++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py @@ -35,29 +35,34 @@ class TestPOSInvoice(unittest.TestCase): w2 = frappe.get_doc(w.doctype, w.name) import time + time.sleep(1) w.save() import time + time.sleep(1) self.assertRaises(frappe.TimestampMismatchError, w2.save) def test_change_naming_series(self): inv = create_pos_invoice(do_not_submit=1) - inv.naming_series = 'TEST-' + inv.naming_series = "TEST-" self.assertRaises(frappe.CannotChangeConstantError, inv.save) def test_discount_and_inclusive_tax(self): inv = create_pos_invoice(qty=100, rate=50, do_not_save=1) - inv.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) + inv.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) inv.insert() self.assertEqual(inv.net_total, 4385.96) @@ -66,7 +71,7 @@ class TestPOSInvoice(unittest.TestCase): inv.reload() inv.discount_amount = 100 - inv.apply_discount_on = 'Net Total' + inv.apply_discount_on = "Net Total" inv.payment_schedule = [] inv.save() @@ -77,7 +82,7 @@ class TestPOSInvoice(unittest.TestCase): inv.reload() inv.discount_amount = 100 - inv.apply_discount_on = 'Grand Total' + inv.apply_discount_on = "Grand Total" inv.payment_schedule = [] inv.save() @@ -93,14 +98,17 @@ class TestPOSInvoice(unittest.TestCase): item_row_copy.qty = qty inv.append("items", item_row_copy) - inv.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 19 - }) + inv.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 19, + }, + ) inv.insert() self.assertEqual(inv.net_total, 4600) @@ -115,10 +123,10 @@ class TestPOSInvoice(unittest.TestCase): item_row = inv.get("items")[0] add_items = [ - (54, '_Test Account Excise Duty @ 12 - _TC'), - (288, '_Test Account Excise Duty @ 15 - _TC'), - (144, '_Test Account Excise Duty @ 20 - _TC'), - (430, '_Test Item Tax Template 1 - _TC') + (54, "_Test Account Excise Duty @ 12 - _TC"), + (288, "_Test Account Excise Duty @ 15 - _TC"), + (144, "_Test Account Excise Duty @ 20 - _TC"), + (430, "_Test Item Tax Template 1 - _TC"), ] for qty, item_tax_template in add_items: item_row_copy = copy.deepcopy(item_row) @@ -126,30 +134,39 @@ class TestPOSInvoice(unittest.TestCase): item_row_copy.item_tax_template = item_tax_template inv.append("items", item_row_copy) - inv.append("taxes", { - "account_head": "_Test Account Excise Duty - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "rate": 11 - }) - inv.append("taxes", { - "account_head": "_Test Account Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 0 - }) - inv.append("taxes", { - "account_head": "_Test Account S&H Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "S&H Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 3 - }) + inv.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 11, + }, + ) + inv.append( + "taxes", + { + "account_head": "_Test Account Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 0, + }, + ) + inv.append( + "taxes", + { + "account_head": "_Test Account S&H Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "S&H Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 3, + }, + ) inv.insert() self.assertEqual(inv.net_total, 4600) @@ -179,14 +196,17 @@ class TestPOSInvoice(unittest.TestCase): inv.apply_discount_on = "Net Total" inv.discount_amount = 75.0 - inv.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 24 - }) + inv.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 24, + }, + ) inv.insert() self.assertEqual(inv.total, 975) @@ -198,11 +218,15 @@ class TestPOSInvoice(unittest.TestCase): self.assertEqual(inv.grand_total, 1116.0) def test_pos_returns_with_repayment(self): - pos = create_pos_invoice(qty = 10, do_not_save=True) + pos = create_pos_invoice(qty=10, do_not_save=True) - pos.set('payments', []) - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1}) + pos.set("payments", []) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500} + ) + pos.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500, "default": 1} + ) pos.insert() pos.submit() @@ -211,25 +235,39 @@ class TestPOSInvoice(unittest.TestCase): pos_return.insert() pos_return.submit() - self.assertEqual(pos_return.get('payments')[0].amount, -500) - self.assertEqual(pos_return.get('payments')[1].amount, -500) + self.assertEqual(pos_return.get("payments")[0].amount, -500) + self.assertEqual(pos_return.get("payments")[1].amount, -500) def test_pos_return_for_serialized_item(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos 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') + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) serial_nos = get_serial_nos(se.get("items")[0].serial_no) - 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, do_not_save=1) + 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, + do_not_save=1, + ) pos.get("items")[0].serial_no = serial_nos[0] - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1}) + pos.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} + ) pos.insert() pos.submit() @@ -238,24 +276,39 @@ class TestPOSInvoice(unittest.TestCase): pos_return.insert() pos_return.submit() - self.assertEqual(pos_return.get('items')[0].serial_no, serial_nos[0]) + self.assertEqual(pos_return.get("items")[0].serial_no, serial_nos[0]) def test_partial_pos_returns(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos 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') + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) serial_nos = get_serial_nos(se.get("items")[0].serial_no) - 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, qty=2, rate=1000, do_not_save=1) + 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, + qty=2, + rate=1000, + do_not_save=1, + ) pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1] - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1}) + pos.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 1000, "default": 1} + ) pos.insert() pos.submit() @@ -263,24 +316,34 @@ class TestPOSInvoice(unittest.TestCase): pos_return1 = make_sales_return(pos.name) # partial return 1 - pos_return1.get('items')[0].qty = -1 - pos_return1.get('items')[0].serial_no = serial_nos[0] + pos_return1.get("items")[0].qty = -1 + pos_return1.get("items")[0].serial_no = serial_nos[0] pos_return1.insert() pos_return1.submit() # partial return 2 pos_return2 = make_sales_return(pos.name) - self.assertEqual(pos_return2.get('items')[0].qty, -1) - self.assertEqual(pos_return2.get('items')[0].serial_no, serial_nos[1]) + self.assertEqual(pos_return2.get("items")[0].qty, -1) + self.assertEqual(pos_return2.get("items")[0].serial_no, serial_nos[1]) def test_pos_change_amount(self): - pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC", - income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, - cost_center = "Main - _TC", do_not_save=True) + pos = create_pos_invoice( + company="_Test Company", + debit_to="Debtors - _TC", + income_account="Sales - _TC", + expense_account="Cost of Goods Sold - _TC", + rate=105, + cost_center="Main - _TC", + do_not_save=True, + ) - pos.set('payments', []) - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1}) + pos.set("payments", []) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 50} + ) + pos.append( + "payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 60, "default": 1} + ) pos.insert() pos.submit() @@ -298,29 +361,53 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos 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') + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) serial_nos = get_serial_nos(se.get("items")[0].serial_no) - 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, do_not_save=1) + 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, + do_not_save=1, + ) pos.get("items")[0].serial_no = serial_nos[0] - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} + ) pos.insert() pos.submit() - 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, do_not_save=1) + 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, + do_not_save=1, + ) pos2.get("items")[0].serial_no = serial_nos[0] - pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) + pos2.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} + ) pos2.insert() self.assertRaises(frappe.ValidationError, pos2.submit) @@ -329,31 +416,85 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos 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') + se = make_serialized_item( + company="_Test Company", + target_warehouse="Stores - _TC", + cost_center="Main - _TC", + expense_account="Cost of Goods Sold - _TC", + ) serial_nos = get_serial_nos(se.get("items")[0].serial_no) - si = create_sales_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, do_not_save=1) + si = create_sales_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, + do_not_save=1, + ) si.get("items")[0].serial_no = serial_nos[0] + si.update_stock = 1 si.insert() si.submit() - 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, do_not_save=1) + 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, + do_not_save=1, + ) pos2.get("items")[0].serial_no = serial_nos[0] - pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) + pos2.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 1000} + ) pos2.insert() self.assertRaises(frappe.ValidationError, pos2.submit) + def test_invalid_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 + "wrong" + + 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=2, + do_not_save=1, + ) + + pos.get("items")[0].has_serial_no = 1 + pos.get("items")[0].serial_no = serial_nos + pos.insert() + + self.assertRaises(frappe.ValidationError, pos.submit) + def test_loyalty_points(self): from erpnext.accounts.doctype.loyalty_program.loyalty_program import ( get_loyalty_program_details_with_points, @@ -361,20 +502,31 @@ class TestPOSInvoice(unittest.TestCase): from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records create_records() - frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") - before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty") + frappe.db.set_value( + "Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty" + ) + before_lp_details = get_loyalty_program_details_with_points( + "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" + ) inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000) - lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'POS Invoice', 'invoice': inv.name, 'customer': inv.customer}) - after_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) + lpe = frappe.get_doc( + "Loyalty Point Entry", + {"invoice_type": "POS Invoice", "invoice": inv.name, "customer": inv.customer}, + ) + after_lp_details = get_loyalty_program_details_with_points( + inv.customer, company=inv.company, loyalty_program=inv.loyalty_program + ) - self.assertEqual(inv.get('loyalty_program'), "Test Single Loyalty") + self.assertEqual(inv.get("loyalty_program"), "Test Single Loyalty") self.assertEqual(lpe.loyalty_points, 10) self.assertEqual(after_lp_details.loyalty_points, before_lp_details.loyalty_points + 10) inv.cancel() - after_cancel_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) + after_cancel_lp_details = get_loyalty_program_details_with_points( + inv.customer, company=inv.company, loyalty_program=inv.loyalty_program + ) self.assertEqual(after_cancel_lp_details.loyalty_points, before_lp_details.loyalty_points) def test_loyalty_points_redeemption(self): @@ -385,17 +537,24 @@ class TestPOSInvoice(unittest.TestCase): # add 10 loyalty points create_pos_invoice(customer="Test Loyalty Customer", rate=10000) - before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty") + before_lp_details = get_loyalty_program_details_with_points( + "Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty" + ) inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) inv.redeem_loyalty_points = 1 inv.loyalty_points = before_lp_details.loyalty_points inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor - inv.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 10000 - inv.loyalty_amount}) + inv.append( + "payments", + {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 10000 - inv.loyalty_amount}, + ) inv.paid_amount = 10000 inv.submit() - after_redeem_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) + after_redeem_lp_details = get_loyalty_program_details_with_points( + inv.customer, company=inv.company, loyalty_program=inv.loyalty_program + ) self.assertEqual(after_redeem_lp_details.loyalty_points, 9) def test_merging_into_sales_invoice_with_discount(self): @@ -409,21 +568,19 @@ class TestPOSInvoice(unittest.TestCase): frappe.db.sql("delete from `tabPOS Invoice`") test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 270 - }) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 270}) pos_inv.submit() pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 3200}) pos_inv2.submit() consolidate_pos_invoices() pos_inv.load_from_db() - rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") + rounded_total = frappe.db.get_value( + "Sales Invoice", pos_inv.consolidated_invoice, "rounded_total" + ) self.assertEqual(rounded_total, 3470) def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self): @@ -437,38 +594,42 @@ class TestPOSInvoice(unittest.TestCase): frappe.db.sql("delete from `tabPOS Invoice`") test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(rate=300, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 - }) - pos_inv.append('taxes', { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) pos_inv.submit() pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1) pos_inv2.additional_discount_percentage = 10 - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 540 - }) - pos_inv2.append('taxes', { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 540}) + pos_inv2.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) pos_inv2.submit() consolidate_pos_invoices() pos_inv.load_from_db() - rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") + rounded_total = frappe.db.get_value( + "Sales Invoice", pos_inv.consolidated_invoice, "rounded_total" + ) self.assertEqual(rounded_total, 840) def test_merging_with_validate_selling_price(self): @@ -488,39 +649,194 @@ class TestPOSInvoice(unittest.TestCase): frappe.db.sql("delete from `tabPOS Invoice`") test_user, pos_profile = init_user_and_profile() pos_inv = create_pos_invoice(item=item, rate=300, do_not_submit=1) - pos_inv.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 - }) - pos_inv.append('taxes', { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) + pos_inv.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 300}) + pos_inv.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) self.assertRaises(frappe.ValidationError, pos_inv.submit) pos_inv2 = create_pos_invoice(item=item, rate=400, do_not_submit=1) - pos_inv2.append('payments', { - 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400 - }) - pos_inv2.append('taxes', { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) + pos_inv2.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 400}) + pos_inv2.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) pos_inv2.submit() consolidate_pos_invoices() pos_inv2.load_from_db() - rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") + rounded_total = frappe.db.get_value( + "Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total" + ) self.assertEqual(rounded_total, 400) + def test_pos_batch_item_qty_validation(self): + from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( + create_batch_item_with_batch, + ) + + create_batch_item_with_batch("_BATCH ITEM", "TestBatch 01") + item = frappe.get_doc("Item", "_BATCH ITEM") + batch = frappe.get_doc("Batch", "TestBatch 01") + batch.submit() + item.batch_no = "TestBatch 01" + item.save() + + se = make_stock_entry( + target="_Test Warehouse - _TC", + item_code="_BATCH ITEM", + qty=2, + basic_rate=100, + batch_no="TestBatch 01", + ) + + pos_inv1 = create_pos_invoice(item=item.name, rate=300, qty=1, do_not_submit=1) + pos_inv1.items[0].batch_no = "TestBatch 01" + pos_inv1.save() + pos_inv1.submit() + + pos_inv2 = create_pos_invoice(item=item.name, rate=300, qty=2, do_not_submit=1) + pos_inv2.items[0].batch_no = "TestBatch 01" + pos_inv2.save() + + self.assertRaises(frappe.ValidationError, pos_inv2.submit) + + # teardown + pos_inv1.reload() + pos_inv1.cancel() + pos_inv1.delete() + pos_inv2.reload() + pos_inv2.delete() + se.cancel() + batch.reload() + batch.cancel() + batch.delete() + + def test_ignore_pricing_rule(self): + from erpnext.accounts.doctype.pricing_rule.test_pricing_rule import make_pricing_rule + + item_price = frappe.get_doc( + { + "doctype": "Item Price", + "item_code": "_Test Item", + "price_list": "_Test Price List", + "price_list_rate": "450", + } + ) + item_price.insert() + pr = make_pricing_rule(selling=1, priority=5, discount_percentage=10) + pr.save() + + try: + pos_inv = create_pos_invoice(qty=1, do_not_submit=1) + pos_inv.items[0].rate = 300 + pos_inv.save() + self.assertEquals(pos_inv.items[0].discount_percentage, 10) + # rate shouldn't change + self.assertEquals(pos_inv.items[0].rate, 405) + + pos_inv.ignore_pricing_rule = 1 + pos_inv.save() + self.assertEquals(pos_inv.ignore_pricing_rule, 1) + # rate should reset since pricing rules are ignored + self.assertEquals(pos_inv.items[0].rate, 450) + + pos_inv.items[0].rate = 300 + pos_inv.save() + self.assertEquals(pos_inv.items[0].rate, 300) + + finally: + item_price.delete() + pos_inv.delete() + pr.delete() + + def test_delivered_serial_no_case(self): + from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( + init_user_and_profile, + ) + from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.savepoint("before_test_delivered_serial_no_case") + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + dn = create_delivery_note(item_code="_Test Serialized Item With Series", serial_no=serial_no) + + delivery_document_no = frappe.db.get_value("Serial No", serial_no, "delivery_document_no") + self.assertEquals(delivery_document_no, dn.name) + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + do_not_submit=True, + ) + + self.assertRaises(frappe.ValidationError, pos_inv.submit) + + finally: + frappe.db.rollback(save_point="before_test_delivered_serial_no_case") + frappe.set_user("Administrator") + + def test_returned_serial_no_case(self): + from erpnext.accounts.doctype.pos_invoice_merge_log.test_pos_invoice_merge_log import ( + init_user_and_profile, + ) + from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos + from erpnext.stock.doctype.serial_no.test_serial_no import get_serial_nos + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item + + frappe.db.savepoint("before_test_returned_serial_no_case") + try: + se = make_serialized_item() + serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] + + init_user_and_profile() + + pos_inv = create_pos_invoice( + item_code="_Test Serialized Item With Series", + serial_no=serial_no, + qty=1, + rate=100, + ) + + pos_return = make_sales_return(pos_inv.name) + pos_return.flags.ignore_validate = True + pos_return.insert() + pos_return.submit() + + pos_reserved_serial_nos = get_pos_reserved_serial_nos( + {"item_code": "_Test Serialized Item With Series", "warehouse": "_Test Warehouse - _TC"} + ) + self.assertTrue(serial_no not in pos_reserved_serial_nos) + + finally: + frappe.db.rollback(save_point="before_test_returned_serial_no_case") + frappe.set_user("Administrator") + + def create_pos_invoice(**args): args = frappe._dict(args) pos_profile = None @@ -543,22 +859,26 @@ def create_pos_invoice(**args): pos_inv.debit_to = args.debit_to or "Debtors - _TC" pos_inv.is_return = args.is_return pos_inv.return_against = args.return_against - pos_inv.currency=args.currency or "INR" + pos_inv.currency = args.currency or "INR" pos_inv.conversion_rate = args.conversion_rate or 1 pos_inv.account_for_change_amount = args.account_for_change_amount or "Cash - _TC" pos_inv.set_missing_values() - pos_inv.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 1, - "rate": args.rate if args.get("rate") is not None else 100, - "income_account": args.income_account or "Sales - _TC", - "expense_account": args.expense_account or "Cost of Goods Sold - _TC", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no - }) + pos_inv.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 1, + "rate": args.rate if args.get("rate") is not None else 100, + "income_account": args.income_account or "Sales - _TC", + "expense_account": args.expense_account or "Cost of Goods Sold - _TC", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "serial_no": args.serial_no, + "batch_no": args.batch_no, + }, + ) if not args.do_not_save: pos_inv.insert() @@ -570,3 +890,10 @@ def create_pos_invoice(**args): pos_inv.payment_schedule = [] return pos_inv + + +def make_batch_item(item_name): + from erpnext.stock.doctype.item.test_item import make_item + + if not frappe.db.exists(item_name): + return make_item(item_name, dict(has_batch_no=1, create_new_batch=1, is_stock_item=1)) diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json index d7620870780..a0594556474 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json @@ -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 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py index 0cd19549f60..f6002163f0f 100644 --- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py +++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py @@ -5,12 +5,11 @@ import json import frappe -import six 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 flt, getdate, nowdate +from frappe.utils import cint, flt, get_time, getdate, nowdate, nowtime from frappe.utils.background_jobs import enqueue from frappe.utils.scheduler import is_scheduler_inactive @@ -21,32 +20,44 @@ class POSInvoiceMergeLog(Document): self.validate_pos_invoice_status() def validate_customer(self): - if self.merge_invoices_based_on == 'Customer Group': + if self.merge_invoices_based_on == "Customer Group": return for d in self.pos_invoices: if d.customer != self.customer: - frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer)) + frappe.throw( + _("Row #{}: POS Invoice {} is not against customer {}").format( + d.idx, d.pos_invoice, self.customer + ) + ) def validate_pos_invoice_status(self): for d in self.pos_invoices: status, docstatus, is_return, return_against = frappe.db.get_value( - 'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against']) + "POS Invoice", d.pos_invoice, ["status", "docstatus", "is_return", "return_against"] + ) bold_pos_invoice = frappe.bold(d.pos_invoice) bold_status = frappe.bold(status) if docstatus != 1: frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, bold_pos_invoice)) if status == "Consolidated": - frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status)) - if is_return and return_against and return_against not in [d.pos_invoice for d in self.pos_invoices]: + frappe.throw( + _("Row #{}: POS Invoice {} has been {}").format(d.idx, bold_pos_invoice, bold_status) + ) + if ( + is_return + and return_against + and return_against not in [d.pos_invoice for d in self.pos_invoices] + ): bold_return_against = frappe.bold(return_against) - return_against_status = frappe.db.get_value('POS Invoice', return_against, "status") + return_against_status = frappe.db.get_value("POS Invoice", return_against, "status") if return_against_status != "Consolidated": # if return entry is not getting merged in the current pos closing and if it is not consolidated bold_unconsolidated = frappe.bold("not Consolidated") - msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}.") - .format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated)) + msg = _("Row #{}: Original Invoice {} of return invoice {} is {}.").format( + d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated + ) msg += " " msg += _("Original invoice should be consolidated before or along with the return invoice.") msg += "| {{ _("Date") }} | @@ -64,10 +64,10 @@{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }} | - {{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }} + {{ row.get('account', '') and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }} | - {{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }} + {{ row.get('account', '') and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }} | {% endif %}
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 088c190f451..29f2e98e779 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -51,6 +51,13 @@ frappe.ui.form.on('Process Statement Of Accounts', {
}
}
});
+ frm.set_query("account", function() {
+ return {
+ filters: {
+ 'company': frm.doc.company
+ }
+ };
+ });
if(frm.doc.__islocal){
frm.set_value('from_date', frappe.datetime.add_months(frappe.datetime.get_today(), -1));
frm.set_value('to_date', frappe.datetime.get_today());
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index 09aa72352e4..01f716daa21 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -23,130 +23,166 @@ from erpnext.accounts.report.general_ledger.general_ledger import execute as get
class ProcessStatementOfAccounts(Document):
def validate(self):
if not self.subject:
- self.subject = 'Statement Of Accounts for {{ customer.name }}'
+ self.subject = "Statement Of Accounts for {{ customer.name }}"
if not self.body:
- self.body = 'Hello {{ customer.name }}, PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}.' + self.body = "Hello {{ customer.name }}, PFA your Statement Of Accounts from {{ doc.from_date }} to {{ doc.to_date }}." validate_template(self.subject) validate_template(self.body) if not self.customers: - frappe.throw(_('Customers not selected.')) + frappe.throw(_("Customers not selected.")) if self.enable_auto_email: - self.to_date = self.start_date - self.from_date = add_months(self.to_date, -1 * self.filter_duration) + if self.start_date and getdate(self.start_date) >= getdate(today()): + self.to_date = self.start_date + self.from_date = add_months(self.to_date, -1 * self.filter_duration) def get_report_pdf(doc, consolidated=True): statement_dict = {} - ageing = '' + ageing = "" base_template_path = "frappe/www/printview.html" - template_path = "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" + template_path = ( + "erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html" + ) for entry in doc.customers: if doc.include_ageing: - ageing_filters = frappe._dict({ - 'company': doc.company, - 'report_date': doc.to_date, - 'ageing_based_on': doc.ageing_based_on, - 'range1': 30, - 'range2': 60, - 'range3': 90, - 'range4': 120, - 'customer': entry.customer - }) + ageing_filters = frappe._dict( + { + "company": doc.company, + "report_date": doc.to_date, + "ageing_based_on": doc.ageing_based_on, + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, + "customer": entry.customer, + } + ) col1, ageing = get_ageing(ageing_filters) if ageing: - ageing[0]['ageing_based_on'] = doc.ageing_based_on + ageing[0]["ageing_based_on"] = doc.ageing_based_on - tax_id = frappe.get_doc('Customer', entry.customer).tax_id - presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \ - or doc.currency or get_company_currency(doc.company) + tax_id = frappe.get_doc("Customer", entry.customer).tax_id + presentation_currency = ( + get_party_account_currency("Customer", entry.customer, doc.company) + or doc.currency + or get_company_currency(doc.company) + ) if doc.letter_head: from frappe.www.printview import get_letter_head + letter_head = get_letter_head(doc, 0) - filters= frappe._dict({ - 'from_date': doc.from_date, - 'to_date': doc.to_date, - 'company': doc.company, - 'finance_book': doc.finance_book if doc.finance_book else None, - 'account': doc.account if doc.account else None, - 'party_type': 'Customer', - 'party': [entry.customer], - 'presentation_currency': presentation_currency, - 'group_by': doc.group_by, - 'currency': doc.currency, - 'cost_center': [cc.cost_center_name for cc in doc.cost_center], - 'project': [p.project_name for p in doc.project], - 'show_opening_entries': 0, - 'include_default_book_entries': 0, - 'tax_id': tax_id if tax_id else None - }) + filters = frappe._dict( + { + "from_date": doc.from_date, + "to_date": doc.to_date, + "company": doc.company, + "finance_book": doc.finance_book if doc.finance_book else None, + "account": [doc.account] if doc.account else None, + "party_type": "Customer", + "party": [entry.customer], + "presentation_currency": presentation_currency, + "group_by": doc.group_by, + "currency": doc.currency, + "cost_center": [cc.cost_center_name for cc in doc.cost_center], + "project": [p.project_name for p in doc.project], + "show_opening_entries": 0, + "include_default_book_entries": 0, + "tax_id": tax_id if tax_id else None, + } + ) col, res = get_soa(filters) for x in [0, -2, -1]: - res[x]['account'] = res[x]['account'].replace("'","") + res[x]["account"] = res[x]["account"].replace("'", "") if len(res) == 3: continue - html = frappe.render_template(template_path, \ - {"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None, + html = frappe.render_template( + template_path, + { + "filters": filters, + "data": res, + "ageing": ageing[0] if (doc.include_ageing and ageing) else None, "letter_head": letter_head if doc.letter_head else None, - "terms_and_conditions": frappe.db.get_value('Terms and Conditions', doc.terms_and_conditions, 'terms') - if doc.terms_and_conditions else None}) + "terms_and_conditions": frappe.db.get_value( + "Terms and Conditions", doc.terms_and_conditions, "terms" + ) + if doc.terms_and_conditions + else None, + }, + ) - html = frappe.render_template(base_template_path, {"body": html, \ - "css": get_print_style(), "title": "Statement For " + entry.customer}) + html = frappe.render_template( + base_template_path, + {"body": html, "css": get_print_style(), "title": "Statement For " + entry.customer}, + ) statement_dict[entry.customer] = html if not bool(statement_dict): return False elif consolidated: - result = ''.join(list(statement_dict.values())) - return get_pdf(result, {'orientation': doc.orientation}) + result = "".join(list(statement_dict.values())) + return get_pdf(result, {"orientation": doc.orientation}) else: for customer, statement_html in statement_dict.items(): - statement_dict[customer]=get_pdf(statement_html, {'orientation': doc.orientation}) + statement_dict[customer] = get_pdf(statement_html, {"orientation": doc.orientation}) return statement_dict + def get_customers_based_on_territory_or_customer_group(customer_collection, collection_name): fields_dict = { - 'Customer Group': 'customer_group', - 'Territory': 'territory', + "Customer Group": "customer_group", + "Territory": "territory", } collection = frappe.get_doc(customer_collection, collection_name) - selected = [customer.name for customer in frappe.get_list(customer_collection, filters=[ - ['lft', '>=', collection.lft], - ['rgt', '<=', collection.rgt] - ], - fields=['name'], - order_by='lft asc, rgt desc' - )] - return frappe.get_list('Customer', fields=['name', 'email_id'], \ - filters=[[fields_dict[customer_collection], 'IN', selected]]) + selected = [ + customer.name + for customer in frappe.get_list( + customer_collection, + filters=[["lft", ">=", collection.lft], ["rgt", "<=", collection.rgt]], + fields=["name"], + order_by="lft asc, rgt desc", + ) + ] + return frappe.get_list( + "Customer", + fields=["name", "email_id"], + filters=[[fields_dict[customer_collection], "IN", selected]], + ) + def get_customers_based_on_sales_person(sales_person): - lft, rgt = frappe.db.get_value("Sales Person", - sales_person, ["lft", "rgt"]) - records = frappe.db.sql(""" + lft, rgt = frappe.db.get_value("Sales Person", sales_person, ["lft", "rgt"]) + records = frappe.db.sql( + """ select distinct parent, parenttype from `tabSales Team` steam where parenttype = 'Customer' and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) - """, (lft, rgt), as_dict=1) + """, + (lft, rgt), + as_dict=1, + ) sales_person_records = frappe._dict() for d in records: sales_person_records.setdefault(d.parenttype, set()).add(d.parent) - if sales_person_records.get('Customer'): - return frappe.get_list('Customer', fields=['name', 'email_id'], \ - filters=[['name', 'in', list(sales_person_records['Customer'])]]) + if sales_person_records.get("Customer"): + return frappe.get_list( + "Customer", + fields=["name", "email_id"], + filters=[["name", "in", list(sales_person_records["Customer"])]], + ) else: return [] + def get_recipients_and_cc(customer, doc): recipients = [] for clist in doc.customers: @@ -155,65 +191,72 @@ def get_recipients_and_cc(customer, doc): if doc.primary_mandatory and clist.primary_email: recipients.append(clist.primary_email) cc = [] - if doc.cc_to != '': + if doc.cc_to != "": try: - cc=[frappe.get_value('User', doc.cc_to, 'email')] + cc = [frappe.get_value("User", doc.cc_to, "email")] except Exception: pass return recipients, cc + def get_context(customer, doc): template_doc = copy.deepcopy(doc) del template_doc.customers template_doc.from_date = format_date(template_doc.from_date) template_doc.to_date = format_date(template_doc.to_date) return { - 'doc': template_doc, - 'customer': frappe.get_doc('Customer', customer), - 'frappe': frappe.utils + "doc": template_doc, + "customer": frappe.get_doc("Customer", customer), + "frappe": frappe.utils, } + @frappe.whitelist() def fetch_customers(customer_collection, collection_name, primary_mandatory): customer_list = [] customers = [] - if customer_collection == 'Sales Person': + if customer_collection == "Sales Person": customers = get_customers_based_on_sales_person(collection_name) if not bool(customers): - frappe.throw(_('No Customers found with selected options.')) + frappe.throw(_("No Customers found with selected options.")) else: - if customer_collection == 'Sales Partner': - customers = frappe.get_list('Customer', fields=['name', 'email_id'], \ - filters=[['default_sales_partner', '=', collection_name]]) + if customer_collection == "Sales Partner": + customers = frappe.get_list( + "Customer", + fields=["name", "email_id"], + filters=[["default_sales_partner", "=", collection_name]], + ) else: - customers = get_customers_based_on_territory_or_customer_group(customer_collection, collection_name) + customers = get_customers_based_on_territory_or_customer_group( + customer_collection, collection_name + ) for customer in customers: - primary_email = customer.get('email_id') or '' + primary_email = customer.get("email_id") or "" billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False) if int(primary_mandatory): - if (primary_email == ''): + if primary_email == "": continue - elif (billing_email == '') and (primary_email == ''): + elif (billing_email == "") and (primary_email == ""): continue - customer_list.append({ - 'name': customer.name, - 'primary_email': primary_email, - 'billing_email': billing_email - }) + customer_list.append( + {"name": customer.name, "primary_email": primary_email, "billing_email": billing_email} + ) return customer_list + @frappe.whitelist() def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True): - """ Returns first email from Contact Email table as a Billing email - when Is Billing Contact checked - and Primary email- email with Is Primary checked """ + """Returns first email from Contact Email table as a Billing email + when Is Billing Contact checked + and Primary email- email with Is Primary checked""" - billing_email = frappe.db.sql(""" + billing_email = frappe.db.sql( + """ SELECT email.email_id FROM @@ -231,42 +274,43 @@ def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=Tr and link.link_name=%s and contact.is_billing_contact=1 ORDER BY - contact.creation desc""", customer_name) + contact.creation desc""", + customer_name, + ) if len(billing_email) == 0 or (billing_email[0][0] is None): if billing_and_primary: frappe.throw(_("No billing email found for customer: {0}").format(customer_name)) else: - return '' + return "" if billing_and_primary: - primary_email = frappe.get_value('Customer', customer_name, 'email_id') + primary_email = frappe.get_value("Customer", customer_name, "email_id") if primary_email is None and int(primary_mandatory): frappe.throw(_("No primary email found for customer: {0}").format(customer_name)) - return [primary_email or '', billing_email[0][0]] + return [primary_email or "", billing_email[0][0]] else: - return billing_email[0][0] or '' + return billing_email[0][0] or "" + @frappe.whitelist() def download_statements(document_name): - doc = frappe.get_doc('Process Statement Of Accounts', document_name) + doc = frappe.get_doc("Process Statement Of Accounts", document_name) report = get_report_pdf(doc) if report: - frappe.local.response.filename = doc.name + '.pdf' + frappe.local.response.filename = doc.name + ".pdf" frappe.local.response.filecontent = report frappe.local.response.type = "download" + @frappe.whitelist() def send_emails(document_name, from_scheduler=False): - doc = frappe.get_doc('Process Statement Of Accounts', document_name) + doc = frappe.get_doc("Process Statement Of Accounts", document_name) report = get_report_pdf(doc, consolidated=False) if report: for customer, report_pdf in report.items(): - attachments = [{ - 'fname': customer + '.pdf', - 'fcontent': report_pdf - }] + attachments = [{"fname": customer + ".pdf", "fcontent": report_pdf}] recipients, cc = get_recipients_and_cc(customer, doc) context = get_context(customer, doc) @@ -274,7 +318,7 @@ def send_emails(document_name, from_scheduler=False): message = frappe.render_template(doc.body, context) frappe.enqueue( - queue='short', + queue="short", method=frappe.sendmail, recipients=recipients, sender=frappe.session.user, @@ -282,28 +326,34 @@ def send_emails(document_name, from_scheduler=False): subject=subject, message=message, now=True, - reference_doctype='Process Statement Of Accounts', + reference_doctype="Process Statement Of Accounts", reference_name=document_name, - attachments=attachments + attachments=attachments, ) if doc.enable_auto_email and from_scheduler: new_to_date = getdate(today()) - if doc.frequency == 'Weekly': + if doc.frequency == "Weekly": new_to_date = add_days(new_to_date, 7) else: - new_to_date = add_months(new_to_date, 1 if doc.frequency == 'Monthly' else 3) + new_to_date = add_months(new_to_date, 1 if doc.frequency == "Monthly" else 3) new_from_date = add_months(new_to_date, -1 * doc.filter_duration) - doc.add_comment('Comment', 'Emails sent on: ' + frappe.utils.format_datetime(frappe.utils.now())) - doc.db_set('to_date', new_to_date, commit=True) - doc.db_set('from_date', new_from_date, commit=True) + doc.add_comment( + "Comment", "Emails sent on: " + frappe.utils.format_datetime(frappe.utils.now()) + ) + doc.db_set("to_date", new_to_date, commit=True) + doc.db_set("from_date", new_from_date, commit=True) return True else: return False + @frappe.whitelist() def send_auto_email(): - selected = frappe.get_list('Process Statement Of Accounts', filters={'to_date': format_date(today()), 'enable_auto_email': 1}) + selected = frappe.get_list( + "Process Statement Of Accounts", + filters={"to_date": format_date(today()), "enable_auto_email": 1}, + ) for entry in selected: send_emails(entry.name, from_scheduler=True) return True diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py index 5fbe93ee68d..fac9be7bdb1 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py @@ -6,29 +6,73 @@ import frappe from frappe import _ from frappe.model.document import Document -pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group', - 'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from', - 'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier', - 'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules'] +pricing_rule_fields = [ + "apply_on", + "mixed_conditions", + "is_cumulative", + "other_item_code", + "other_item_group", + "apply_rule_on_other", + "other_brand", + "selling", + "buying", + "applicable_for", + "valid_from", + "valid_upto", + "customer", + "customer_group", + "territory", + "sales_partner", + "campaign", + "supplier", + "supplier_group", + "company", + "currency", + "apply_multiple_pricing_rules", +] -other_fields = ['min_qty', 'max_qty', 'min_amt', - 'max_amt', 'priority','warehouse', 'threshold_percentage', 'rule_description'] +other_fields = [ + "min_qty", + "max_qty", + "min_amt", + "max_amt", + "priority", + "warehouse", + "threshold_percentage", + "rule_description", +] -price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discount_on_rate', - 'rate', 'discount_amount', 'discount_percentage', 'validate_applied_rule', 'apply_multiple_pricing_rules'] +price_discount_fields = [ + "rate_or_discount", + "apply_discount_on", + "apply_discount_on_rate", + "rate", + "discount_amount", + "discount_percentage", + "validate_applied_rule", + "apply_multiple_pricing_rules", +] + +product_discount_fields = [ + "free_item", + "free_qty", + "free_item_uom", + "free_item_rate", + "same_item", + "is_recursive", + "apply_multiple_pricing_rules", +] -product_discount_fields = ['free_item', 'free_qty', 'free_item_uom', - 'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules'] class TransactionExists(frappe.ValidationError): pass + class PromotionalScheme(Document): def validate(self): if not self.selling and not self.buying: frappe.throw(_("Either 'Selling' or 'Buying' must be selected"), title=_("Mandatory")) - if not (self.price_discount_slabs - or self.product_discount_slabs): + if not (self.price_discount_slabs or self.product_discount_slabs): frappe.throw(_("Price or product discount slabs are required")) self.validate_applicable_for() @@ -39,7 +83,7 @@ class PromotionalScheme(Document): applicable_for = frappe.scrub(self.applicable_for) if not self.get(applicable_for): - msg = (f'The field {frappe.bold(self.applicable_for)} is required') + msg = f"The field {frappe.bold(self.applicable_for)} is required" frappe.throw(_(msg)) def validate_pricing_rules(self): @@ -53,28 +97,28 @@ class PromotionalScheme(Document): if self._doc_before_save.applicable_for == self.applicable_for: return - docnames = frappe.get_all('Pricing Rule', - filters= {'promotional_scheme': self.name}) + docnames = frappe.get_all("Pricing Rule", filters={"promotional_scheme": self.name}) for docname in docnames: - if frappe.db.exists('Pricing Rule Detail', - {'pricing_rule': docname.name, 'docstatus': ('<', 2)}): + if frappe.db.exists( + "Pricing Rule Detail", {"pricing_rule": docname.name, "docstatus": ("<", 2)} + ): raise_for_transaction_exists(self.name) if docnames and not transaction_exists: for docname in docnames: - frappe.delete_doc('Pricing Rule', docname.name) + frappe.delete_doc("Pricing Rule", docname.name) def on_update(self): - pricing_rules = frappe.get_all( - 'Pricing Rule', - fields = ["promotional_scheme_id", "name", "creation"], - filters = { - 'promotional_scheme': self.name, - 'applicable_for': self.applicable_for - }, - order_by = 'creation asc', - ) or {} + pricing_rules = ( + frappe.get_all( + "Pricing Rule", + fields=["promotional_scheme_id", "name", "creation"], + filters={"promotional_scheme": self.name, "applicable_for": self.applicable_for}, + order_by="creation asc", + ) + or {} + ) self.update_pricing_rules(pricing_rules) def update_pricing_rules(self, pricing_rules): @@ -83,7 +127,7 @@ class PromotionalScheme(Document): names = [] for rule in pricing_rules: names.append(rule.name) - rules[rule.get('promotional_scheme_id')] = names + rules[rule.get("promotional_scheme_id")] = names docs = get_pricing_rules(self, rules) @@ -100,34 +144,38 @@ class PromotionalScheme(Document): frappe.msgprint(_("New {0} pricing rules are created").format(count)) def on_trash(self): - for rule in frappe.get_all('Pricing Rule', - {'promotional_scheme': self.name}): - frappe.delete_doc('Pricing Rule', rule.name) + for rule in frappe.get_all("Pricing Rule", {"promotional_scheme": self.name}): + frappe.delete_doc("Pricing Rule", rule.name) + def raise_for_transaction_exists(name): - msg = (f"""You can't change the {frappe.bold(_('Applicable For'))} - because transactions are present against the Promotional Scheme {frappe.bold(name)}. """) - msg += 'Kindly disable this Promotional Scheme and create new for new Applicable For.' + msg = f"""You can't change the {frappe.bold(_('Applicable For'))} + because transactions are present against the Promotional Scheme {frappe.bold(name)}. """ + msg += "Kindly disable this Promotional Scheme and create new for new Applicable For." frappe.throw(_(msg), TransactionExists) + def get_pricing_rules(doc, rules=None): if rules is None: rules = {} new_doc = [] - for child_doc, fields in {'price_discount_slabs': price_discount_fields, - 'product_discount_slabs': product_discount_fields}.items(): + for child_doc, fields in { + "price_discount_slabs": price_discount_fields, + "product_discount_slabs": product_discount_fields, + }.items(): if doc.get(child_doc): new_doc.extend(_get_pricing_rules(doc, child_doc, fields, rules)) return new_doc + def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): if rules is None: rules = {} new_doc = [] args = get_args_for_pricing_rule(doc) - applicable_for = frappe.scrub(doc.get('applicable_for')) + applicable_for = frappe.scrub(doc.get("applicable_for")) for idx, d in enumerate(doc.get(child_doc)): if d.name in rules: @@ -138,15 +186,23 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): else: for applicable_for_value in args.get(applicable_for): docname = get_pricing_rule_docname(d, applicable_for, applicable_for_value) - pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, - d, docname, applicable_for, applicable_for_value) + pr = prepare_pricing_rule( + args, doc, child_doc, discount_fields, d, docname, applicable_for, applicable_for_value + ) new_doc.append(pr) elif args.get(applicable_for): applicable_for_values = args.get(applicable_for) or [] for applicable_for_value in applicable_for_values: - pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, - d, applicable_for=applicable_for, value= applicable_for_value) + pr = prepare_pricing_rule( + args, + doc, + child_doc, + discount_fields, + d, + applicable_for=applicable_for, + value=applicable_for_value, + ) new_doc.append(pr) else: @@ -155,20 +211,24 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): return new_doc -def get_pricing_rule_docname(row: dict, applicable_for: str = None, applicable_for_value: str = None) -> str: - fields = ['promotional_scheme_id', 'name'] - filters = { - 'promotional_scheme_id': row.name - } + +def get_pricing_rule_docname( + row: dict, applicable_for: str = None, applicable_for_value: str = None +) -> str: + fields = ["promotional_scheme_id", "name"] + filters = {"promotional_scheme_id": row.name} if applicable_for: fields.append(applicable_for) filters[applicable_for] = applicable_for_value - docname = frappe.get_all('Pricing Rule', fields = fields, filters = filters) - return docname[0].name if docname else '' + docname = frappe.get_all("Pricing Rule", fields=fields, filters=filters) + return docname[0].name if docname else "" -def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None): + +def prepare_pricing_rule( + args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None +): if docname: pr = frappe.get_doc("Pricing Rule", docname) else: @@ -182,33 +242,31 @@ def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, return set_args(temp_args, pr, doc, child_doc, discount_fields, d) + def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): pr.update(args) - for field in (other_fields + discount_fields): + for field in other_fields + discount_fields: pr.set(field, child_doc_fields.get(field)) pr.promotional_scheme_id = child_doc_fields.name pr.promotional_scheme = doc.name pr.disable = child_doc_fields.disable if child_doc_fields.disable else doc.disable - pr.price_or_product_discount = ('Price' - if child_doc == 'price_discount_slabs' else 'Product') + pr.price_or_product_discount = "Price" if child_doc == "price_discount_slabs" else "Product" - for field in ['items', 'item_groups', 'brands']: + for field in ["items", "item_groups", "brands"]: if doc.get(field): pr.set(field, []) - apply_on = frappe.scrub(doc.get('apply_on')) + apply_on = frappe.scrub(doc.get("apply_on")) for d in doc.get(field): - pr.append(field, { - apply_on: d.get(apply_on), - 'uom': d.uom - }) + pr.append(field, {apply_on: d.get(apply_on), "uom": d.uom}) return pr + def get_args_for_pricing_rule(doc): - args = { 'promotional_scheme': doc.name } - applicable_for = frappe.scrub(doc.get('applicable_for')) + args = {"promotional_scheme": doc.name} + applicable_for = frappe.scrub(doc.get("applicable_for")) for d in pricing_rule_fields: if d == applicable_for: diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py index 6d079242682..70e87404a35 100644 --- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py +++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme_dashboard.py @@ -3,11 +3,6 @@ from frappe import _ def get_data(): return { - 'fieldname': 'promotional_scheme', - 'transactions': [ - { - 'label': _('Reference'), - 'items': ['Pricing Rule'] - } - ] + "fieldname": "promotional_scheme", + "transactions": [{"label": _("Reference"), "items": ["Pricing Rule"]}], } diff --git a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py index 49192a45f87..b3b9d7b2086 100644 --- a/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py +++ b/erpnext/accounts/doctype/promotional_scheme/test_promotional_scheme.py @@ -11,96 +11,111 @@ from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_orde class TestPromotionalScheme(unittest.TestCase): def setUp(self): - if frappe.db.exists('Promotional Scheme', '_Test Scheme'): - frappe.delete_doc('Promotional Scheme', '_Test Scheme') + if frappe.db.exists("Promotional Scheme", "_Test Scheme"): + frappe.delete_doc("Promotional Scheme", "_Test Scheme") def test_promotional_scheme(self): - ps = make_promotional_scheme(applicable_for='Customer', customer='_Test Customer') - price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"], - filters = {'promotional_scheme': ps.name}) - self.assertTrue(len(price_rules),1) - price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) - self.assertTrue(price_doc_details.customer, '_Test Customer') + ps = make_promotional_scheme(applicable_for="Customer", customer="_Test Customer") + price_rules = frappe.get_all( + "Pricing Rule", + fields=["promotional_scheme_id", "name", "creation"], + filters={"promotional_scheme": ps.name}, + ) + self.assertTrue(len(price_rules), 1) + price_doc_details = frappe.db.get_value( + "Pricing Rule", price_rules[0].name, ["customer", "min_qty", "discount_percentage"], as_dict=1 + ) + self.assertTrue(price_doc_details.customer, "_Test Customer") self.assertTrue(price_doc_details.min_qty, 4) self.assertTrue(price_doc_details.discount_percentage, 20) ps.price_discount_slabs[0].min_qty = 6 - ps.append('customer', { - 'customer': "_Test Customer 2"}) + ps.append("customer", {"customer": "_Test Customer 2"}) ps.save() - price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], - filters = {'promotional_scheme': ps.name}) + price_rules = frappe.get_all( + "Pricing Rule", + fields=["promotional_scheme_id", "name"], + filters={"promotional_scheme": ps.name}, + ) self.assertTrue(len(price_rules), 2) - price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[1].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) - self.assertTrue(price_doc_details.customer, '_Test Customer 2') + price_doc_details = frappe.db.get_value( + "Pricing Rule", price_rules[1].name, ["customer", "min_qty", "discount_percentage"], as_dict=1 + ) + self.assertTrue(price_doc_details.customer, "_Test Customer 2") self.assertTrue(price_doc_details.min_qty, 6) self.assertTrue(price_doc_details.discount_percentage, 20) - price_doc_details = frappe.db.get_value('Pricing Rule', price_rules[0].name, ['customer', 'min_qty', 'discount_percentage'], as_dict = 1) - self.assertTrue(price_doc_details.customer, '_Test Customer') + price_doc_details = frappe.db.get_value( + "Pricing Rule", price_rules[0].name, ["customer", "min_qty", "discount_percentage"], as_dict=1 + ) + self.assertTrue(price_doc_details.customer, "_Test Customer") self.assertTrue(price_doc_details.min_qty, 6) - frappe.delete_doc('Promotional Scheme', ps.name) - price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name"], - filters = {'promotional_scheme': ps.name}) + frappe.delete_doc("Promotional Scheme", ps.name) + price_rules = frappe.get_all( + "Pricing Rule", + fields=["promotional_scheme_id", "name"], + filters={"promotional_scheme": ps.name}, + ) self.assertEqual(price_rules, []) def test_promotional_scheme_without_applicable_for(self): ps = make_promotional_scheme() - price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertTrue(len(price_rules), 1) - frappe.delete_doc('Promotional Scheme', ps.name) + frappe.delete_doc("Promotional Scheme", ps.name) - price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertEqual(price_rules, []) def test_change_applicable_for_in_promotional_scheme(self): ps = make_promotional_scheme() - price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertTrue(len(price_rules), 1) - so = make_sales_order(qty=5, currency='USD', do_not_save=True) + so = make_sales_order(qty=5, currency="USD", do_not_save=True) so.set_missing_values() so.save() self.assertEqual(price_rules[0].name, so.pricing_rules[0].pricing_rule) - ps.applicable_for = 'Customer' - ps.append('customer', { - 'customer': '_Test Customer' - }) + ps.applicable_for = "Customer" + ps.append("customer", {"customer": "_Test Customer"}) self.assertRaises(TransactionExists, ps.save) - frappe.delete_doc('Sales Order', so.name) - frappe.delete_doc('Promotional Scheme', ps.name) - price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) + frappe.delete_doc("Sales Order", so.name) + frappe.delete_doc("Promotional Scheme", ps.name) + price_rules = frappe.get_all("Pricing Rule", filters={"promotional_scheme": ps.name}) self.assertEqual(price_rules, []) + def make_promotional_scheme(**args): args = frappe._dict(args) - ps = frappe.new_doc('Promotional Scheme') - ps.name = '_Test Scheme' - ps.append('items',{ - 'item_code': '_Test Item' - }) + ps = frappe.new_doc("Promotional Scheme") + ps.name = "_Test Scheme" + ps.append("items", {"item_code": "_Test Item"}) ps.selling = 1 - ps.append('price_discount_slabs',{ - 'min_qty': 4, - 'validate_applied_rule': 0, - 'discount_percentage': 20, - 'rule_description': 'Test' - }) + ps.append( + "price_discount_slabs", + { + "min_qty": 4, + "validate_applied_rule": 0, + "discount_percentage": 20, + "rule_description": "Test", + }, + ) - ps.company = '_Test Company' + ps.company = "_Test Company" if args.applicable_for: ps.applicable_for = args.applicable_for - ps.append(frappe.scrub(args.applicable_for), { - frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for)) - }) + ps.append( + frappe.scrub(args.applicable_for), + {frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for))}, + ) ps.save() diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index 19c0d8aaf3d..aed8cb56d1b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -30,6 +30,9 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ onload: function() { this._super(); + // Ignore linked advances + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry']; + if(!this.frm.doc.__islocal) { // show credit_to in print format if(!this.frm.doc.supplier && this.frm.doc.credit_to) { @@ -42,8 +45,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ if (this.frm.doc.supplier && this.frm.doc.__islocal) { this.frm.trigger('supplier'); } - - erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, refresh: function(doc) { @@ -276,6 +277,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference) return; + if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return; + erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", { posting_date: this.frm.doc.posting_date, @@ -536,7 +539,7 @@ frappe.ui.form.on("Purchase Invoice", { }, add_custom_buttons: function(frm) { - if (frm.doc.per_received < 100) { + if (frm.doc.docstatus == 1 && frm.doc.per_received < 100) { frm.add_custom_button(__('Purchase Receipt'), () => { frm.events.make_purchase_receipt(frm); }, __('Create')); diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json index bd0116443ff..ff3d5950808 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json @@ -83,6 +83,8 @@ "section_break_51", "taxes_and_charges", "taxes", + "tax_withheld_vouchers_section", + "tax_withheld_vouchers", "sec_tax_breakup", "other_charges_calculation", "totals", @@ -511,7 +513,6 @@ "fieldname": "ignore_pricing_rule", "fieldtype": "Check", "label": "Ignore Pricing Rule", - "no_copy": 1, "permlevel": 1, "print_hide": 1 }, @@ -1417,13 +1418,25 @@ "label": "Advance Tax", "options": "Advance Tax", "read_only": 1 + }, + { + "fieldname": "tax_withheld_vouchers_section", + "fieldtype": "Section Break", + "label": "Tax Withheld Vouchers" + }, + { + "fieldname": "tax_withheld_vouchers", + "fieldtype": "Table", + "label": "Tax Withheld Vouchers", + "options": "Tax Withheld Vouchers", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 204, "is_submittable": 1, "links": [], - "modified": "2021-11-25 13:31:02.716727", + "modified": "2022-09-13 23:39:54.525037", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice", @@ -1483,6 +1496,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "supplier", "title_field": "title", "track_changes": 1 diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 33fbd748c7b..6476845de78 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -33,6 +33,7 @@ from erpnext.accounts.utils import get_account_currency, get_fiscal_year from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account from erpnext.buying.utils import check_on_hold_or_closed_status +from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.buying_controller import BuyingController from erpnext.stock import get_warehouse_account_map from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( @@ -40,25 +41,26 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( update_billed_amount_based_on_po, ) -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} + class PurchaseInvoice(BuyingController): def __init__(self, *args, **kwargs): super(PurchaseInvoice, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Purchase Invoice Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'po_detail', - 'target_field': 'billed_amt', - 'target_parent_dt': 'Purchase Order', - 'target_parent_field': 'per_billed', - 'target_ref_field': 'amount', - 'source_field': 'amount', - 'percent_join_field': 'purchase_order', - 'overflow_type': 'billing' - }] + self.status_updater = [ + { + "source_dt": "Purchase Invoice Item", + "target_dt": "Purchase Order Item", + "join_field": "po_detail", + "target_field": "billed_amt", + "target_parent_dt": "Purchase Order", + "target_parent_field": "per_billed", + "target_ref_field": "amount", + "source_field": "amount", + "percent_join_field": "purchase_order", + "overflow_type": "billing", + } + ] def onload(self): super(PurchaseInvoice, self).onload() @@ -67,15 +69,14 @@ class PurchaseInvoice(BuyingController): def before_save(self): if not self.on_hold: - self.release_date = '' - + self.release_date = "" def invoice_is_blocked(self): return self.on_hold and (not self.release_date or self.release_date > getdate(nowdate())) def validate(self): if not self.is_opening: - self.is_opening = 'No' + self.is_opening = "No" self.validate_posting_time() @@ -87,14 +88,14 @@ class PurchaseInvoice(BuyingController): self.validate_supplier_invoice() # validate cash purchase - if (self.is_paid == 1): + if self.is_paid == 1: self.validate_cash() # validate service stop date to lie in between start and end date validate_service_stop_date(self) - if self._action=="submit" and self.update_stock: - self.make_batches('warehouse') + if self._action == "submit" and self.update_stock: + self.make_batches("warehouse") self.validate_release_date() self.check_conversion_rate() @@ -105,79 +106,86 @@ class PurchaseInvoice(BuyingController): self.validate_uom_is_integer("uom", "qty") self.validate_uom_is_integer("stock_uom", "stock_qty") self.set_expense_account(for_validate=True) + self.validate_expense_account() self.set_against_expense_account() self.validate_write_off_account() self.validate_multiple_billing("Purchase Receipt", "pr_detail", "amount", "items") self.create_remarks() self.set_status() self.validate_purchase_receipt_if_update_stock() - validate_inter_company_party(self.doctype, self.supplier, self.company, self.inter_company_invoice_reference) + validate_inter_company_party( + self.doctype, self.supplier, self.company, self.inter_company_invoice_reference + ) self.reset_default_field_value("set_warehouse", "items", "warehouse") self.reset_default_field_value("rejected_warehouse", "items", "rejected_warehouse") self.reset_default_field_value("set_from_warehouse", "items", "from_warehouse") def validate_release_date(self): if self.release_date and getdate(nowdate()) >= getdate(self.release_date): - frappe.throw(_('Release date must be in the future')) + frappe.throw(_("Release date must be in the future")) def validate_cash(self): if not self.cash_bank_account and flt(self.paid_amount): frappe.throw(_("Cash or Bank Account is mandatory for making payment entry")) - if (flt(self.paid_amount) + flt(self.write_off_amount) - - flt(self.get("rounded_total") or self.grand_total) - > 1/(10**(self.precision("base_grand_total") + 1))): + if flt(self.paid_amount) + flt(self.write_off_amount) - flt( + self.get("rounded_total") or self.grand_total + ) > 1 / (10 ** (self.precision("base_grand_total") + 1)): frappe.throw(_("""Paid amount + Write Off Amount can not be greater than Grand Total""")) def create_remarks(self): if not self.remarks: if self.bill_no and self.bill_date: - self.remarks = _("Against Supplier Invoice {0} dated {1}").format(self.bill_no, - formatdate(self.bill_date)) + self.remarks = _("Against Supplier Invoice {0} dated {1}").format( + self.bill_no, formatdate(self.bill_date) + ) else: self.remarks = _("No Remarks") def set_missing_values(self, for_validate=False): if not self.credit_to: self.credit_to = get_party_account("Supplier", self.supplier, self.company) - self.party_account_currency = frappe.db.get_value("Account", self.credit_to, "account_currency", cache=True) + self.party_account_currency = frappe.db.get_value( + "Account", self.credit_to, "account_currency", cache=True + ) if not self.due_date: - self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company, self.bill_date) + self.due_date = get_due_date( + self.posting_date, "Supplier", self.supplier, self.company, self.bill_date + ) tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") if tds_category and not for_validate: self.apply_tds = 1 self.tax_withholding_category = tds_category + self.set_onload("supplier_tds", tds_category) super(PurchaseInvoice, self).set_missing_values(for_validate) - def check_conversion_rate(self): - default_currency = erpnext.get_company_currency(self.company) - if not default_currency: - throw(_('Please enter default currency in Company Master')) - if (self.currency == default_currency and flt(self.conversion_rate) != 1.00) or not self.conversion_rate or (self.currency != default_currency and flt(self.conversion_rate) == 1.00): - throw(_("Conversion rate cannot be 0 or 1")) - def validate_credit_to_acc(self): if not self.credit_to: self.credit_to = get_party_account("Supplier", self.supplier, self.company) if not self.credit_to: self.raise_missing_debit_credit_account_error("Supplier", self.supplier) - account = frappe.db.get_value("Account", self.credit_to, - ["account_type", "report_type", "account_currency"], as_dict=True) + account = frappe.db.get_value( + "Account", self.credit_to, ["account_type", "report_type", "account_currency"], as_dict=True + ) if account.report_type != "Balance Sheet": frappe.throw( - _("Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account.") - .format(frappe.bold("Credit To")), title=_("Invalid Account") + _( + "Please ensure {} account is a Balance Sheet account. You can change the parent account to a Balance Sheet account or select a different account." + ).format(frappe.bold("Credit To")), + title=_("Invalid Account"), ) if self.supplier and account.account_type != "Payable": frappe.throw( - _("Please ensure {} account is a Payable account. Change the account type to Payable or select a different account.") - .format(frappe.bold("Credit To")), title=_("Invalid Account") + _( + "Please ensure {} account {} is a Payable account. Change the account type to Payable or select a different account." + ).format(frappe.bold("Credit To"), frappe.bold(self.credit_to)), + title=_("Invalid Account"), ) self.party_account_currency = account.account_currency @@ -185,51 +193,61 @@ class PurchaseInvoice(BuyingController): def check_on_hold_or_closed_status(self): check_list = [] - for d in self.get('items'): + for d in self.get("items"): if d.purchase_order and not d.purchase_order in check_list and not d.purchase_receipt: check_list.append(d.purchase_order) - check_on_hold_or_closed_status('Purchase Order', d.purchase_order) + check_on_hold_or_closed_status("Purchase Order", d.purchase_order) def validate_with_previous_doc(self): - super(PurchaseInvoice, self).validate_with_previous_doc({ - "Purchase Order": { - "ref_dn_field": "purchase_order", - "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], - }, - "Purchase Order Item": { - "ref_dn_field": "po_detail", - "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - "Purchase Receipt": { - "ref_dn_field": "purchase_receipt", - "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], - }, - "Purchase Receipt Item": { - "ref_dn_field": "pr_detail", - "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]], - "is_child_table": True + super(PurchaseInvoice, self).validate_with_previous_doc( + { + "Purchase Order": { + "ref_dn_field": "purchase_order", + "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], + }, + "Purchase Order Item": { + "ref_dn_field": "po_detail", + "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + "Purchase Receipt": { + "ref_dn_field": "purchase_receipt", + "compare_fields": [["supplier", "="], ["company", "="], ["currency", "="]], + }, + "Purchase Receipt Item": { + "ref_dn_field": "pr_detail", + "compare_fields": [["project", "="], ["item_code", "="], ["uom", "="]], + "is_child_table": True, + }, } - }) + ) - if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')) and not self.is_return: - self.validate_rate_with_reference_doc([ - ["Purchase Order", "purchase_order", "po_detail"], - ["Purchase Receipt", "purchase_receipt", "pr_detail"] - ]) + if ( + cint(frappe.db.get_single_value("Buying Settings", "maintain_same_rate")) and not self.is_return + ): + self.validate_rate_with_reference_doc( + [ + ["Purchase Order", "purchase_order", "po_detail"], + ["Purchase Receipt", "purchase_receipt", "pr_detail"], + ] + ) def validate_warehouse(self, for_validate=True): if self.update_stock and for_validate: - for d in self.get('items'): - if not d.warehouse: - frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}"). - format(d.idx, d.item_code, self.company)) + stock_items = self.get_stock_items() + for d in self.get("items"): + if not d.warehouse and d.item_code in stock_items: + frappe.throw( + _( + "Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}" + ).format(d.idx, d.item_code, self.company) + ) super(PurchaseInvoice, self).validate_warehouse() def validate_item_code(self): - for d in self.get('items'): + for d in self.get("items"): if not d.item_code: frappe.msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True) @@ -257,51 +275,82 @@ class PurchaseInvoice(BuyingController): if item.item_code: asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") - if auto_accounting_for_stock and item.item_code in stock_items \ - and self.is_opening == 'No' and not item.is_fixed_asset \ - and (not item.po_detail or - not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier")): + if ( + auto_accounting_for_stock + and item.item_code in stock_items + and self.is_opening == "No" + and not item.is_fixed_asset + and ( + not item.po_detail + or not frappe.db.get_value("Purchase Order Item", item.po_detail, "delivered_by_supplier") + ) + ): if self.update_stock and (not item.from_warehouse): - if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]: - msg = _("Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account").format( - item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse)) + if ( + for_validate + and item.expense_account + and item.expense_account != warehouse_account[item.warehouse]["account"] + ): + msg = _( + "Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account" + ).format( + item.idx, + frappe.bold(warehouse_account[item.warehouse]["account"]), + frappe.bold(item.expense_account), + frappe.bold(item.warehouse), + ) frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = warehouse_account[item.warehouse]["account"] else: # check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not if item.purchase_receipt: - negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry` + negative_expense_booked_in_pr = frappe.db.sql( + """select name from `tabGL Entry` where voucher_type='Purchase Receipt' and voucher_no=%s and account = %s""", - (item.purchase_receipt, stock_not_billed_account)) + (item.purchase_receipt, stock_not_billed_account), + ) if negative_expense_booked_in_pr: - if for_validate and item.expense_account and item.expense_account != stock_not_billed_account: - msg = _("Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}").format( - item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt)) + if ( + for_validate and item.expense_account and item.expense_account != stock_not_billed_account + ): + msg = _( + "Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}" + ).format( + item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt) + ) frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = stock_not_billed_account else: # If no purchase receipt present then book expense in 'Stock Received But Not Billed' # This is done in cases when Purchase Invoice is created before Purchase Receipt - if for_validate and item.expense_account and item.expense_account != stock_not_billed_account: - msg = _("Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.").format( - item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code)) + if ( + for_validate and item.expense_account and item.expense_account != stock_not_billed_account + ): + msg = _( + "Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}." + ).format( + item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code) + ) msg += " " - msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice") + msg += _( + "This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice" + ) frappe.msgprint(msg, title=_("Expense Head Changed")) item.expense_account = stock_not_billed_account elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): - asset_category_account = get_asset_category_account('fixed_asset_account', item=item.item_code, - company = self.company) + asset_category_account = get_asset_category_account( + "fixed_asset_account", item=item.item_code, company=self.company + ) if not asset_category_account: - form_link = get_link_to_form('Asset Category', asset_category) + form_link = get_link_to_form("Asset Category", asset_category) throw( _("Please set Fixed Asset Account in {} against {}.").format(form_link, self.company), - title=_("Missing Account") + title=_("Missing Account"), ) item.expense_account = asset_category_account elif item.is_fixed_asset and item.pr_detail: @@ -309,6 +358,10 @@ class PurchaseInvoice(BuyingController): elif not item.expense_account and for_validate: throw(_("Expense account is mandatory for item {0}").format(item.item_code or item.item_name)) + def validate_expense_account(self): + for item in self.get("items"): + validate_account_head(item.idx, item.expense_account, self.company, "Expense") + def set_against_expense_account(self): against_accounts = [] for item in self.get("items"): @@ -318,32 +371,44 @@ class PurchaseInvoice(BuyingController): self.against_expense_account = ",".join(against_accounts) def po_required(self): - if frappe.db.get_value("Buying Settings", None, "po_required") == 'Yes': + if frappe.db.get_value("Buying Settings", None, "po_required") == "Yes": - if frappe.get_value('Supplier', self.supplier, 'allow_purchase_invoice_creation_without_purchase_order'): + if frappe.get_value( + "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_order" + ): return - for d in self.get('items'): + for d in self.get("items"): if not d.purchase_order: msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code)) msg += " " msg += _("To submit the invoice without purchase order please set {0} as {1} in {2}").format( - frappe.bold(_('Purchase Order Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')) + frappe.bold(_("Purchase Order Required")), + frappe.bold("No"), + get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"), + ) throw(msg, title=_("Mandatory Purchase Order")) def pr_required(self): stock_items = self.get_stock_items() - if frappe.db.get_value("Buying Settings", None, "pr_required") == 'Yes': + if frappe.db.get_value("Buying Settings", None, "pr_required") == "Yes": - if frappe.get_value('Supplier', self.supplier, 'allow_purchase_invoice_creation_without_purchase_receipt'): + if frappe.get_value( + "Supplier", self.supplier, "allow_purchase_invoice_creation_without_purchase_receipt" + ): return - for d in self.get('items'): + for d in self.get("items"): if not d.purchase_receipt and d.item_code in stock_items: msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code)) msg += " " - msg += _("To submit the invoice without purchase receipt please set {0} as {1} in {2}").format( - frappe.bold(_('Purchase Receipt Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings')) + msg += _( + "To submit the invoice without purchase receipt please set {0} as {1} in {2}" + ).format( + frappe.bold(_("Purchase Receipt Required")), + frappe.bold("No"), + get_link_to_form("Buying Settings", "Buying Settings", "Buying Settings"), + ) throw(msg, title=_("Mandatory Purchase Receipt")) def validate_write_off_account(self): @@ -351,56 +416,65 @@ class PurchaseInvoice(BuyingController): throw(_("Please enter Write Off Account")) def check_prev_docstatus(self): - for d in self.get('items'): + for d in self.get("items"): if d.purchase_order: - submitted = frappe.db.sql("select name from `tabPurchase Order` where docstatus = 1 and name = %s", d.purchase_order) + submitted = frappe.db.sql( + "select name from `tabPurchase Order` where docstatus = 1 and name = %s", d.purchase_order + ) if not submitted: frappe.throw(_("Purchase Order {0} is not submitted").format(d.purchase_order)) if d.purchase_receipt: - submitted = frappe.db.sql("select name from `tabPurchase Receipt` where docstatus = 1 and name = %s", d.purchase_receipt) + submitted = frappe.db.sql( + "select name from `tabPurchase Receipt` where docstatus = 1 and name = %s", d.purchase_receipt + ) if not submitted: frappe.throw(_("Purchase Receipt {0} is not submitted").format(d.purchase_receipt)) def update_status_updater_args(self): if cint(self.update_stock): - self.status_updater.append({ - 'source_dt': 'Purchase Invoice Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'po_detail', - 'target_field': 'received_qty', - 'target_parent_dt': 'Purchase Order', - 'target_parent_field': 'per_received', - 'target_ref_field': 'qty', - 'source_field': 'received_qty', - 'second_source_dt': 'Purchase Receipt Item', - 'second_source_field': 'received_qty', - 'second_join_field': 'purchase_order_item', - 'percent_join_field':'purchase_order', - 'overflow_type': 'receipt', - 'extra_cond': """ and exists(select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""" - }) + self.status_updater.append( + { + "source_dt": "Purchase Invoice Item", + "target_dt": "Purchase Order Item", + "join_field": "po_detail", + "target_field": "received_qty", + "target_parent_dt": "Purchase Order", + "target_parent_field": "per_received", + "target_ref_field": "qty", + "source_field": "received_qty", + "second_source_dt": "Purchase Receipt Item", + "second_source_field": "received_qty", + "second_join_field": "purchase_order_item", + "percent_join_field": "purchase_order", + "overflow_type": "receipt", + "extra_cond": """ and exists(select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and update_stock = 1)""", + } + ) if cint(self.is_return): - self.status_updater.append({ - 'source_dt': 'Purchase Invoice Item', - 'target_dt': 'Purchase Order Item', - 'join_field': 'po_detail', - 'target_field': 'returned_qty', - 'source_field': '-1 * qty', - 'second_source_dt': 'Purchase Receipt Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'purchase_order_item', - 'overflow_type': 'receipt', - 'extra_cond': """ and exists (select name from `tabPurchase Invoice` - where name=`tabPurchase Invoice Item`.parent and update_stock=1 and is_return=1)""" - }) + self.status_updater.append( + { + "source_dt": "Purchase Invoice Item", + "target_dt": "Purchase Order Item", + "join_field": "po_detail", + "target_field": "returned_qty", + "source_field": "-1 * qty", + "second_source_dt": "Purchase Receipt Item", + "second_source_field": "-1 * qty", + "second_join_field": "purchase_order_item", + "overflow_type": "receipt", + "extra_cond": """ and exists (select name from `tabPurchase Invoice` + where name=`tabPurchase Invoice Item`.parent and update_stock=1 and is_return=1)""", + } + ) def validate_purchase_receipt_if_update_stock(self): if self.update_stock: for item in self.get("items"): if item.purchase_receipt: - frappe.throw(_("Stock cannot be updated against Purchase Receipt {0}") - .format(item.purchase_receipt)) + frappe.throw( + _("Stock cannot be updated against Purchase Receipt {0}").format(item.purchase_receipt) + ) def on_submit(self): super(PurchaseInvoice, self).on_submit() @@ -409,8 +483,9 @@ class PurchaseInvoice(BuyingController): self.update_status_updater_args() self.update_prevdoc_status() - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total + ) if not self.is_return: self.update_against_document_in_jv() @@ -425,6 +500,7 @@ class PurchaseInvoice(BuyingController): self.update_stock_ledger() self.set_consumed_qty_in_po() from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit + update_serial_nos_after_submit(self, "items") # this sequence because outstanding may get -negative @@ -447,13 +523,32 @@ class PurchaseInvoice(BuyingController): update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" if self.docstatus == 1: - make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost) + make_gl_entries( + gl_entries, + update_outstanding=update_outstanding, + merge_entries=False, + from_repost=from_repost, + ) elif self.docstatus == 2: + provisional_entries = [a for a in gl_entries if a.voucher_type == "Purchase Receipt"] make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + if provisional_entries: + for entry in provisional_entries: + frappe.db.set_value( + "GL Entry", + {"voucher_type": "Purchase Receipt", "voucher_detail_no": entry.voucher_detail_no}, + "is_cancelled", + 1, + ) if update_outstanding == "No": - update_outstanding_amt(self.credit_to, "Supplier", self.supplier, - self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name) + update_outstanding_amt( + self.credit_to, + "Supplier", + self.supplier, + self.doctype, + self.return_against if cint(self.is_return) and self.return_against else self.name, + ) elif self.docstatus == 2 and cint(self.update_stock) and self.auto_accounting_for_stock: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) @@ -472,7 +567,6 @@ class PurchaseInvoice(BuyingController): self.make_supplier_gl_entry(gl_entries) self.make_item_gl_entries(gl_entries) - self.make_discount_gl_entries(gl_entries) if self.check_asset_cwip_enabled(): self.get_asset_gl_entry(gl_entries) @@ -502,28 +596,41 @@ class PurchaseInvoice(BuyingController): def make_supplier_gl_entry(self, gl_entries): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total - grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total + grand_total = ( + self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total + ) + base_grand_total = flt( + self.base_rounded_total + if (self.base_rounding_adjustment and self.base_rounded_total) + else self.base_grand_total, + self.precision("base_grand_total"), + ) if grand_total and not self.is_internal_transfer(): - # Did not use base_grand_total to book rounding loss gle - grand_total_in_company_currency = flt(grand_total * self.conversion_rate, - self.precision("grand_total")) - gl_entries.append( - self.get_gl_dict({ + # Did not use base_grand_total to book rounding loss gle + gl_entries.append( + self.get_gl_dict( + { "account": self.credit_to, "party_type": "Supplier", "party": self.supplier, "due_date": self.due_date, "against": self.against_expense_account, - "credit": grand_total_in_company_currency, - "credit_in_account_currency": grand_total_in_company_currency \ - if self.party_account_currency==self.company_currency else grand_total, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, + "credit": base_grand_total, + "credit_in_account_currency": base_grand_total + if self.party_account_currency == self.company_currency + else grand_total, + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, "against_voucher_type": self.doctype, "project": self.project, - "cost_center": self.cost_center - }, self.party_account_currency, item=self) + "cost_center": self.cost_center, + }, + self.party_account_currency, + item=self, ) + ) def make_item_gl_entries(self, gl_entries): # item gl entries @@ -535,13 +642,30 @@ class PurchaseInvoice(BuyingController): voucher_wise_stock_value = {} if self.update_stock: - for d in frappe.get_all('Stock Ledger Entry', - fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}): - voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference) + stock_ledger_entries = frappe.get_all( + "Stock Ledger Entry", + fields=["voucher_detail_no", "stock_value_difference", "warehouse"], + filters={"voucher_no": self.name, "voucher_type": self.doctype, "is_cancelled": 0}, + ) + for d in stock_ledger_entries: + voucher_wise_stock_value.setdefault( + (d.voucher_detail_no, d.warehouse), d.stock_value_difference + ) - valuation_tax_accounts = [d.account_head for d in self.get("taxes") - if d.category in ('Valuation', 'Total and Valuation') - and flt(d.base_tax_amount_after_discount_amount)] + valuation_tax_accounts = [ + d.account_head + for d in self.get("taxes") + if d.category in ("Valuation", "Total and Valuation") + and flt(d.base_tax_amount_after_discount_amount) + ] + + provisional_accounting_for_non_stock_items = cint( + frappe.db.get_value( + "Company", self.company, "enable_provisional_accounting_for_non_stock_items" + ) + ) + + purchase_receipt_doc_map = {} for item in self.get("items"): if flt(item.base_net_amount): @@ -551,167 +675,249 @@ class PurchaseInvoice(BuyingController): if self.update_stock and self.auto_accounting_for_stock and item.item_code in stock_items: # warehouse account - warehouse_debit_amount = self.make_stock_adjustment_entry(gl_entries, - item, voucher_wise_stock_value, account_currency) + warehouse_debit_amount = self.make_stock_adjustment_entry( + gl_entries, item, voucher_wise_stock_value, account_currency + ) if item.from_warehouse: - gl_entries.append(self.get_gl_dict({ - "account": warehouse_account[item.warehouse]['account'], - "against": warehouse_account[item.from_warehouse]["account"], - "cost_center": item.cost_center, - "project": item.project or self.project, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": warehouse_debit_amount, - }, warehouse_account[item.warehouse]["account_currency"], item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": warehouse_account[item.warehouse]["account"], + "against": warehouse_account[item.from_warehouse]["account"], + "cost_center": item.cost_center, + "project": item.project or self.project, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": warehouse_debit_amount, + }, + warehouse_account[item.warehouse]["account_currency"], + item=item, + ) + ) # Intentionally passed negative debit amount to avoid incorrect GL Entry validation - gl_entries.append(self.get_gl_dict({ - "account": warehouse_account[item.from_warehouse]['account'], - "against": warehouse_account[item.warehouse]["account"], - "cost_center": item.cost_center, - "project": item.project or self.project, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")), - }, warehouse_account[item.from_warehouse]["account_currency"], item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": warehouse_account[item.from_warehouse]["account"], + "against": warehouse_account[item.warehouse]["account"], + "cost_center": item.cost_center, + "project": item.project or self.project, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")), + }, + warehouse_account[item.from_warehouse]["account_currency"], + item=item, + ) + ) # Do not book expense for transfer within same company transfer if not self.is_internal_transfer(): gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": flt(item.base_net_amount, item.precision("base_net_amount")), - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "cost_center": item.cost_center, - "project": item.project - }, account_currency, item=item) + self.get_gl_dict( + { + "account": item.expense_account, + "against": self.supplier, + "debit": flt(item.base_net_amount, item.precision("base_net_amount")), + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project, + }, + account_currency, + item=item, + ) ) else: if not self.is_internal_transfer(): gl_entries.append( - self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "debit": warehouse_debit_amount, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) + self.get_gl_dict( + { + "account": item.expense_account, + "against": self.supplier, + "debit": warehouse_debit_amount, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + account_currency, + item=item, + ) ) # Amount added through landed-cost-voucher if landed_cost_entries: for account, amount in iteritems(landed_cost_entries[(item.item_code, item.name)]): - gl_entries.append(self.get_gl_dict({ - "account": account, - "against": item.expense_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(amount["base_amount"]), - "credit_in_account_currency": flt(amount["amount"]), - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": account, + "against": item.expense_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(amount["base_amount"]), + "credit_in_account_currency": flt(amount["amount"]), + "project": item.project or self.project, + }, + item=item, + ) + ) # sub-contracting warehouse if flt(item.rm_supp_cost): supplier_warehouse_account = warehouse_account[self.supplier_warehouse]["account"] if not supplier_warehouse_account: - frappe.throw(_("Please set account in Warehouse {0}") - .format(self.supplier_warehouse)) - gl_entries.append(self.get_gl_dict({ - "account": supplier_warehouse_account, - "against": item.expense_account, - "cost_center": item.cost_center, - "project": item.project or self.project, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(item.rm_supp_cost) - }, warehouse_account[self.supplier_warehouse]["account_currency"], item=item)) + frappe.throw(_("Please set account in Warehouse {0}").format(self.supplier_warehouse)) + gl_entries.append( + self.get_gl_dict( + { + "account": supplier_warehouse_account, + "against": item.expense_account, + "cost_center": item.cost_center, + "project": item.project or self.project, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.rm_supp_cost), + }, + warehouse_account[self.supplier_warehouse]["account_currency"], + item=item, + ) + ) - elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)): - expense_account = (item.expense_account - if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) + elif not item.is_fixed_asset or ( + item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category) + ): + expense_account = ( + item.expense_account + if (not item.enable_deferred_expense or self.is_return) + else item.deferred_expense_account + ) if not item.is_fixed_asset: - dummy, amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting) + dummy, amount = self.get_amount_and_base_amount(item, None) else: amount = flt(item.base_net_amount + item.item_tax_amount, item.precision("base_net_amount")) - auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items')) - - if auto_accounting_for_non_stock_items: - service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed") - + if provisional_accounting_for_non_stock_items: if item.purchase_receipt: + provisional_account = frappe.db.get_value( + "Purchase Receipt Item", item.pr_detail, "provisional_expense_account" + ) or self.get_company_default("default_provisional_account") + purchase_receipt_doc = purchase_receipt_doc_map.get(item.purchase_receipt) + + if not purchase_receipt_doc: + purchase_receipt_doc = frappe.get_doc("Purchase Receipt", item.purchase_receipt) + purchase_receipt_doc_map[item.purchase_receipt] = purchase_receipt_doc + # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - expense_booked_in_pr = frappe.db.get_value('GL Entry', {'is_cancelled': 0, - 'voucher_type': 'Purchase Receipt', 'voucher_no': item.purchase_receipt, 'voucher_detail_no': item.pr_detail, - 'account':service_received_but_not_billed_account}, ['name']) + expense_booked_in_pr = frappe.db.get_value( + "GL Entry", + { + "is_cancelled": 0, + "voucher_type": "Purchase Receipt", + "voucher_no": item.purchase_receipt, + "voucher_detail_no": item.pr_detail, + "account": provisional_account, + }, + ["name"], + ) if expense_booked_in_pr: - expense_account = service_received_but_not_billed_account + # Intentionally passing purchase invoice item to handle partial billing + purchase_receipt_doc.add_provisional_gl_entry( + item, gl_entries, self.posting_date, provisional_account, reverse=1 + ) if not self.is_internal_transfer(): - gl_entries.append(self.get_gl_dict({ - "account": expense_account, - "against": self.supplier, - "debit": amount, - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": expense_account, + "against": self.supplier, + "debit": amount, + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + account_currency, + item=item, + ) + ) # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: - expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") + expenses_included_in_asset_valuation = self.get_company_default( + "expenses_included_in_asset_valuation" + ) # Amount added through landed-cost-voucher - gl_entries.append(self.get_gl_dict({ - "account": expenses_included_in_asset_valuation, - "against": expense_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": expenses_included_in_asset_valuation, + "against": expense_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.landed_cost_voucher_amount), + "project": item.project or self.project, + }, + item=item, + ) + ) - gl_entries.append(self.get_gl_dict({ - "account": expense_account, - "against": expenses_included_in_asset_valuation, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": expense_account, + "against": expenses_included_in_asset_valuation, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": flt(item.landed_cost_voucher_amount), + "project": item.project or self.project, + }, + item=item, + ) + ) # update gross amount of asset bought through this document - assets = frappe.db.get_all('Asset', - filters={ 'purchase_invoice': self.name, 'item_code': item.item_code } + assets = frappe.db.get_all( + "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} ) for asset in assets: frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) - frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) + frappe.db.set_value( + "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) + ) - if self.auto_accounting_for_stock and self.is_opening == "No" and \ - item.item_code in stock_items and item.item_tax_amount: - # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - if item.purchase_receipt and valuation_tax_accounts: - negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry` + if ( + self.auto_accounting_for_stock + and self.is_opening == "No" + and item.item_code in stock_items + and item.item_tax_amount + ): + # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt + if item.purchase_receipt and valuation_tax_accounts: + negative_expense_booked_in_pr = frappe.db.sql( + """select name from `tabGL Entry` where voucher_type='Purchase Receipt' and voucher_no=%s and account in %s""", - (item.purchase_receipt, valuation_tax_accounts)) + (item.purchase_receipt, valuation_tax_accounts), + ) - if not negative_expense_booked_in_pr: - gl_entries.append( - self.get_gl_dict({ + if not negative_expense_booked_in_pr: + gl_entries.append( + self.get_gl_dict( + { "account": self.stock_received_but_not_billed, "against": self.supplier, "debit": flt(item.item_tax_amount, item.precision("item_tax_amount")), "remarks": self.remarks or _("Accounting Entry for Stock"), "cost_center": self.cost_center, - "project": item.project or self.project - }, item=item) + "project": item.project or self.project, + }, + item=item, ) + ) - self.negative_expense_to_be_booked += flt(item.item_tax_amount, \ - item.precision("item_tax_amount")) + self.negative_expense_to_be_booked += flt( + item.item_tax_amount, item.precision("item_tax_amount") + ) def get_asset_gl_entry(self, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") @@ -719,125 +925,179 @@ class PurchaseInvoice(BuyingController): for item in self.get("items"): if item.is_fixed_asset: - asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) + asset_amount = flt(item.net_amount) + flt(item.item_tax_amount / self.conversion_rate) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) - item_exp_acc_type = frappe.db.get_value('Account', item.expense_account, 'account_type') - if (not item.expense_account or item_exp_acc_type not in ['Asset Received But Not Billed', 'Fixed Asset']): + item_exp_acc_type = frappe.db.get_value("Account", item.expense_account, "account_type") + if not item.expense_account or item_exp_acc_type not in [ + "Asset Received But Not Billed", + "Fixed Asset", + ]: item.expense_account = arbnb_account if not self.update_stock: arbnb_currency = get_account_currency(item.expense_account) - gl_entries.append(self.get_gl_dict({ - "account": item.expense_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "debit": base_asset_amount, - "debit_in_account_currency": (base_asset_amount - if arbnb_currency == self.company_currency else asset_amount), - "cost_center": item.cost_center, - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": item.expense_account, + "against": self.supplier, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "debit": base_asset_amount, + "debit_in_account_currency": ( + base_asset_amount if arbnb_currency == self.company_currency else asset_amount + ), + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + item=item, + ) + ) if item.item_tax_amount: asset_eiiav_currency = get_account_currency(eiiav_account) - gl_entries.append(self.get_gl_dict({ - "account": eiiav_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "cost_center": item.cost_center, - "project": item.project or self.project, - "credit": item.item_tax_amount, - "credit_in_account_currency": (item.item_tax_amount - if asset_eiiav_currency == self.company_currency else - item.item_tax_amount / self.conversion_rate) - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": eiiav_account, + "against": self.supplier, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "cost_center": item.cost_center, + "project": item.project or self.project, + "credit": item.item_tax_amount, + "credit_in_account_currency": ( + item.item_tax_amount + if asset_eiiav_currency == self.company_currency + else item.item_tax_amount / self.conversion_rate + ), + }, + item=item, + ) + ) else: - cwip_account = get_asset_account("capital_work_in_progress_account", - asset_category=item.asset_category,company=self.company) + cwip_account = get_asset_account( + "capital_work_in_progress_account", asset_category=item.asset_category, company=self.company + ) cwip_account_currency = get_account_currency(cwip_account) - gl_entries.append(self.get_gl_dict({ - "account": cwip_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "debit": base_asset_amount, - "debit_in_account_currency": (base_asset_amount - if cwip_account_currency == self.company_currency else asset_amount), - "cost_center": self.cost_center, - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": cwip_account, + "against": self.supplier, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "debit": base_asset_amount, + "debit_in_account_currency": ( + base_asset_amount if cwip_account_currency == self.company_currency else asset_amount + ), + "cost_center": self.cost_center, + "project": item.project or self.project, + }, + item=item, + ) + ) if item.item_tax_amount and not cint(erpnext.is_perpetual_inventory_enabled(self.company)): asset_eiiav_currency = get_account_currency(eiiav_account) - gl_entries.append(self.get_gl_dict({ - "account": eiiav_account, - "against": self.supplier, - "remarks": self.get("remarks") or _("Accounting Entry for Asset"), - "cost_center": item.cost_center, - "credit": item.item_tax_amount, - "project": item.project or self.project, - "credit_in_account_currency": (item.item_tax_amount - if asset_eiiav_currency == self.company_currency else - item.item_tax_amount / self.conversion_rate) - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": eiiav_account, + "against": self.supplier, + "remarks": self.get("remarks") or _("Accounting Entry for Asset"), + "cost_center": item.cost_center, + "credit": item.item_tax_amount, + "project": item.project or self.project, + "credit_in_account_currency": ( + item.item_tax_amount + if asset_eiiav_currency == self.company_currency + else item.item_tax_amount / self.conversion_rate + ), + }, + item=item, + ) + ) # When update stock is checked # Assets are bought through this document then it will be linked to this document if self.update_stock: if flt(item.landed_cost_voucher_amount): - gl_entries.append(self.get_gl_dict({ - "account": eiiav_account, - "against": cwip_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "credit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": eiiav_account, + "against": cwip_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "credit": flt(item.landed_cost_voucher_amount), + "project": item.project or self.project, + }, + item=item, + ) + ) - gl_entries.append(self.get_gl_dict({ - "account": cwip_account, - "against": eiiav_account, - "cost_center": item.cost_center, - "remarks": self.get("remarks") or _("Accounting Entry for Stock"), - "debit": flt(item.landed_cost_voucher_amount), - "project": item.project or self.project - }, item=item)) + gl_entries.append( + self.get_gl_dict( + { + "account": cwip_account, + "against": eiiav_account, + "cost_center": item.cost_center, + "remarks": self.get("remarks") or _("Accounting Entry for Stock"), + "debit": flt(item.landed_cost_voucher_amount), + "project": item.project or self.project, + }, + item=item, + ) + ) # update gross amount of assets bought through this document - assets = frappe.db.get_all('Asset', - filters={ 'purchase_invoice': self.name, 'item_code': item.item_code } + assets = frappe.db.get_all( + "Asset", filters={"purchase_invoice": self.name, "item_code": item.item_code} ) for asset in assets: frappe.db.set_value("Asset", asset.name, "gross_purchase_amount", flt(item.valuation_rate)) - frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate)) + frappe.db.set_value( + "Asset", asset.name, "purchase_receipt_amount", flt(item.valuation_rate) + ) return gl_entries - def make_stock_adjustment_entry(self, gl_entries, item, voucher_wise_stock_value, account_currency): + def make_stock_adjustment_entry( + self, gl_entries, item, voucher_wise_stock_value, account_currency + ): net_amt_precision = item.precision("base_net_amount") val_rate_db_precision = 6 if cint(item.precision("valuation_rate")) <= 6 else 9 - warehouse_debit_amount = flt(flt(item.valuation_rate, val_rate_db_precision) - * flt(item.qty) * flt(item.conversion_factor), net_amt_precision) + warehouse_debit_amount = flt( + flt(item.valuation_rate, val_rate_db_precision) * flt(item.qty) * flt(item.conversion_factor), + net_amt_precision, + ) # Stock ledger value is not matching with the warehouse amount - if (self.update_stock and voucher_wise_stock_value.get(item.name) and - warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)): + if ( + self.update_stock + and voucher_wise_stock_value.get((item.name, item.warehouse)) + and warehouse_debit_amount + != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) + ): cost_of_goods_sold_account = self.get_company_default("default_expense_account") stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision) stock_adjustment_amt = warehouse_debit_amount - stock_amount gl_entries.append( - self.get_gl_dict({ - "account": cost_of_goods_sold_account, - "against": item.expense_account, - "debit": stock_adjustment_amt, - "remarks": self.get("remarks") or _("Stock Adjustment"), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) + self.get_gl_dict( + { + "account": cost_of_goods_sold_account, + "against": item.expense_account, + "debit": stock_adjustment_amt, + "remarks": self.get("remarks") or _("Stock Adjustment"), + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + account_currency, + item=item, + ) ) warehouse_debit_amount = stock_amount @@ -849,31 +1109,42 @@ class PurchaseInvoice(BuyingController): valuation_tax = {} for tax in self.get("taxes"): - amount, base_amount = self.get_tax_amounts(tax, self.enable_discount_accounting) + amount, base_amount = self.get_tax_amounts(tax, None) if tax.category in ("Total", "Valuation and Total") and flt(base_amount): account_currency = get_account_currency(tax.account_head) dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit" gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "against": self.supplier, - dr_or_cr: base_amount, - dr_or_cr + "_in_account_currency": base_amount - if account_currency==self.company_currency + self.get_gl_dict( + { + "account": tax.account_head, + "against": self.supplier, + dr_or_cr: base_amount, + dr_or_cr + "_in_account_currency": base_amount + if account_currency == self.company_currency else amount, - "cost_center": tax.cost_center - }, account_currency, item=tax) + "cost_center": tax.cost_center, + }, + account_currency, + item=tax, + ) ) # accumulate valuation tax - if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(base_amount) \ - and not self.is_internal_transfer(): + if ( + self.is_opening == "No" + and tax.category in ("Valuation", "Valuation and Total") + and flt(base_amount) + and not self.is_internal_transfer() + ): if self.auto_accounting_for_stock and not tax.cost_center: - frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category))) + frappe.throw( + _("Cost Center is required in row {0} in Taxes table for type {1}").format( + tax.idx, _(tax.category) + ) + ) valuation_tax.setdefault(tax.name, 0) - valuation_tax[tax.name] += \ - (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount) + valuation_tax[tax.name] += (tax.add_deduct_tax == "Add" and 1 or -1) * flt(base_amount) if self.is_opening == "No" and self.negative_expense_to_be_booked and valuation_tax: # credit valuation tax amount in "Expenses Included In Valuation" @@ -887,17 +1158,22 @@ class PurchaseInvoice(BuyingController): if i == len(valuation_tax): applicable_amount = amount_including_divisional_loss else: - applicable_amount = self.negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount) + applicable_amount = self.negative_expense_to_be_booked * ( + valuation_tax[tax.name] / total_valuation_amount + ) amount_including_divisional_loss -= applicable_amount gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "cost_center": tax.cost_center, - "against": self.supplier, - "credit": applicable_amount, - "remarks": self.remarks or _("Accounting Entry for Stock"), - }, item=tax) + self.get_gl_dict( + { + "account": tax.account_head, + "cost_center": tax.cost_center, + "against": self.supplier, + "credit": applicable_amount, + "remarks": self.remarks or _("Accounting Entry for Stock"), + }, + item=tax, + ) ) i += 1 @@ -906,18 +1182,24 @@ class PurchaseInvoice(BuyingController): for tax in self.get("taxes"): if valuation_tax.get(tax.name): gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "cost_center": tax.cost_center, - "against": self.supplier, - "credit": valuation_tax[tax.name], - "remarks": self.remarks or _("Accounting Entry for Stock") - }, item=tax)) + self.get_gl_dict( + { + "account": tax.account_head, + "cost_center": tax.cost_center, + "against": self.supplier, + "credit": valuation_tax[tax.name], + "remarks": self.remarks or _("Accounting Entry for Stock"), + }, + item=tax, + ) + ) @property def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): - self._enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + self._enable_discount_accounting = cint( + frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + ) return self._enable_discount_accounting @@ -925,13 +1207,18 @@ class PurchaseInvoice(BuyingController): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) gl_entries.append( - self.get_gl_dict({ - "account": self.unrealized_profit_loss_account, - "against": self.supplier, - "credit": flt(self.total_taxes_and_charges), - "credit_in_account_currency": flt(self.base_total_taxes_and_charges), - "cost_center": self.cost_center - }, account_currency, item=self)) + self.get_gl_dict( + { + "account": self.unrealized_profit_loss_account, + "against": self.supplier, + "credit": flt(self.total_taxes_and_charges), + "credit_in_account_currency": flt(self.base_total_taxes_and_charges), + "cost_center": self.cost_center, + }, + account_currency, + item=self, + ) + ) def make_payment_gl_entries(self, gl_entries): # Make Cash GL Entries @@ -939,30 +1226,42 @@ class PurchaseInvoice(BuyingController): bank_account_currency = get_account_currency(self.cash_bank_account) # CASH, make payment entries gl_entries.append( - self.get_gl_dict({ - "account": self.credit_to, - "party_type": "Supplier", - "party": self.supplier, - "against": self.cash_bank_account, - "debit": self.base_paid_amount, - "debit_in_account_currency": self.base_paid_amount \ - if self.party_account_currency==self.company_currency else self.paid_amount, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": self.credit_to, + "party_type": "Supplier", + "party": self.supplier, + "against": self.cash_bank_account, + "debit": self.base_paid_amount, + "debit_in_account_currency": self.base_paid_amount + if self.party_account_currency == self.company_currency + else self.paid_amount, + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.cash_bank_account, - "against": self.supplier, - "credit": self.base_paid_amount, - "credit_in_account_currency": self.base_paid_amount \ - if bank_account_currency==self.company_currency else self.paid_amount, - "cost_center": self.cost_center - }, bank_account_currency, item=self) + self.get_gl_dict( + { + "account": self.cash_bank_account, + "against": self.supplier, + "credit": self.base_paid_amount, + "credit_in_account_currency": self.base_paid_amount + if bank_account_currency == self.company_currency + else self.paid_amount, + "cost_center": self.cost_center, + }, + bank_account_currency, + item=self, + ) ) def make_write_off_gl_entry(self, gl_entries): @@ -972,48 +1271,66 @@ class PurchaseInvoice(BuyingController): write_off_account_currency = get_account_currency(self.write_off_account) gl_entries.append( - self.get_gl_dict({ - "account": self.credit_to, - "party_type": "Supplier", - "party": self.supplier, - "against": self.write_off_account, - "debit": self.base_write_off_amount, - "debit_in_account_currency": self.base_write_off_amount \ - if self.party_account_currency==self.company_currency else self.write_off_amount, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": self.credit_to, + "party_type": "Supplier", + "party": self.supplier, + "against": self.write_off_account, + "debit": self.base_write_off_amount, + "debit_in_account_currency": self.base_write_off_amount + if self.party_account_currency == self.company_currency + else self.write_off_amount, + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.write_off_account, - "against": self.supplier, - "credit": flt(self.base_write_off_amount), - "credit_in_account_currency": self.base_write_off_amount \ - if write_off_account_currency==self.company_currency else self.write_off_amount, - "cost_center": self.cost_center or self.write_off_cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.write_off_account, + "against": self.supplier, + "credit": flt(self.base_write_off_amount), + "credit_in_account_currency": self.base_write_off_amount + if write_off_account_currency == self.company_currency + else self.write_off_amount, + "cost_center": self.cost_center or self.write_off_cost_center, + }, + item=self, + ) ) def make_gle_for_rounding_adjustment(self, gl_entries): # if rounding adjustment in small and conversion rate is also small then # base_rounding_adjustment may become zero due to small precision # eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2 - # then base_rounding_adjustment becomes zero and error is thrown in GL Entry - if not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment: - round_off_account, round_off_cost_center = \ - get_round_off_account_and_cost_center(self.company) + # then base_rounding_adjustment becomes zero and error is thrown in GL Entry + if ( + not self.is_internal_transfer() and self.rounding_adjustment and self.base_rounding_adjustment + ): + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Purchase Invoice", self.name + ) gl_entries.append( - self.get_gl_dict({ - "account": round_off_account, - "against": self.supplier, - "debit_in_account_currency": self.rounding_adjustment, - "debit": self.base_rounding_adjustment, - "cost_center": self.cost_center or round_off_cost_center, - }, item=self)) + self.get_gl_dict( + { + "account": round_off_account, + "against": self.supplier, + "debit_in_account_currency": self.rounding_adjustment, + "debit": self.base_rounding_adjustment, + "cost_center": self.cost_center or round_off_cost_center, + }, + item=self, + ) + ) def on_cancel(self): check_if_return_invoice_linked_with_payment_entry(self) @@ -1044,10 +1361,10 @@ class PurchaseInvoice(BuyingController): self.repost_future_sle_and_gle() self.update_project() - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") self.update_advance_tax_references(cancel=1) def update_project(self): @@ -1068,19 +1385,22 @@ class PurchaseInvoice(BuyingController): if cint(frappe.db.get_single_value("Accounts Settings", "check_supplier_invoice_uniqueness")): fiscal_year = get_fiscal_year(self.posting_date, company=self.company, as_dict=True) - pi = frappe.db.sql('''select name from `tabPurchase Invoice` + pi = frappe.db.sql( + """select name from `tabPurchase Invoice` where bill_no = %(bill_no)s and supplier = %(supplier)s and name != %(name)s and docstatus < 2 - and posting_date between %(year_start_date)s and %(year_end_date)s''', { - "bill_no": self.bill_no, - "supplier": self.supplier, - "name": self.name, - "year_start_date": fiscal_year.year_start_date, - "year_end_date": fiscal_year.year_end_date - }) + and posting_date between %(year_start_date)s and %(year_end_date)s""", + { + "bill_no": self.bill_no, + "supplier": self.supplier, + "name": self.name, + "year_start_date": fiscal_year.year_start_date, + "year_end_date": fiscal_year.year_end_date, + }, + ) if pi: pi = pi[0][0] @@ -1090,16 +1410,26 @@ class PurchaseInvoice(BuyingController): updated_pr = [] for d in self.get("items"): if d.pr_detail: - billed_amt = frappe.db.sql("""select sum(amount) from `tabPurchase Invoice Item` - where pr_detail=%s and docstatus=1""", d.pr_detail) + billed_amt = frappe.db.sql( + """select sum(amount) from `tabPurchase Invoice Item` + where pr_detail=%s and docstatus=1""", + d.pr_detail, + ) billed_amt = billed_amt and billed_amt[0][0] or 0 - frappe.db.set_value("Purchase Receipt Item", d.pr_detail, "billed_amt", billed_amt, update_modified=update_modified) + frappe.db.set_value( + "Purchase Receipt Item", + d.pr_detail, + "billed_amt", + billed_amt, + update_modified=update_modified, + ) updated_pr.append(d.purchase_receipt) elif d.po_detail: updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified) for pr in set(updated_pr): from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage + pr_doc = frappe.get_doc("Purchase Receipt", pr) update_billing_percentage(pr_doc, update_modified=update_modified) @@ -1107,25 +1437,29 @@ class PurchaseInvoice(BuyingController): self.due_date = None def block_invoice(self, hold_comment=None, release_date=None): - self.db_set('on_hold', 1) - self.db_set('hold_comment', cstr(hold_comment)) - self.db_set('release_date', release_date) + self.db_set("on_hold", 1) + self.db_set("hold_comment", cstr(hold_comment)) + self.db_set("release_date", release_date) def unblock_invoice(self): - self.db_set('on_hold', 0) - self.db_set('release_date', None) + self.db_set("on_hold", 0) + self.db_set("release_date", None) def set_tax_withholding(self): if not self.apply_tds: return - if self.apply_tds and not self.get('tax_withholding_category'): - self.tax_withholding_category = frappe.db.get_value('Supplier', self.supplier, 'tax_withholding_category') + if self.apply_tds and not self.get("tax_withholding_category"): + self.tax_withholding_category = frappe.db.get_value( + "Supplier", self.supplier, "tax_withholding_category" + ) if not self.tax_withholding_category: return - tax_withholding_details, advance_taxes = get_party_tax_withholding_details(self, self.tax_withholding_category) + tax_withholding_details, advance_taxes, voucher_wise_amount = get_party_tax_withholding_details( + self, self.tax_withholding_category + ) # Adjust TDS paid on advances self.allocate_advance_tds(tax_withholding_details, advance_taxes) @@ -1143,37 +1477,59 @@ class PurchaseInvoice(BuyingController): if not accounts or tax_withholding_details.get("account_head") not in accounts: self.append("taxes", tax_withholding_details) - to_remove = [d for d in self.taxes - if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")] + to_remove = [ + d + for d in self.taxes + if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head") + ] for d in to_remove: self.remove(d) + ## Add pending vouchers on which tax was withheld + self.set("tax_withheld_vouchers", []) + + for voucher_no, voucher_details in voucher_wise_amount.items(): + self.append( + "tax_withheld_vouchers", + { + "voucher_name": voucher_no, + "voucher_type": voucher_details.get("voucher_type"), + "taxable_amount": voucher_details.get("amount"), + }, + ) + # calculate totals again after applying TDS self.calculate_taxes_and_totals() def allocate_advance_tds(self, tax_withholding_details, advance_taxes): - self.set('advance_tax', []) + self.set("advance_tax", []) for tax in advance_taxes: allocated_amount = 0 pending_amount = flt(tax.tax_amount - tax.allocated_amount) - if flt(tax_withholding_details.get('tax_amount')) >= pending_amount: - tax_withholding_details['tax_amount'] -= pending_amount + if flt(tax_withholding_details.get("tax_amount")) >= pending_amount: + tax_withholding_details["tax_amount"] -= pending_amount allocated_amount = pending_amount - elif flt(tax_withholding_details.get('tax_amount')) and flt(tax_withholding_details.get('tax_amount')) < pending_amount: - allocated_amount = tax_withholding_details['tax_amount'] - tax_withholding_details['tax_amount'] = 0 + elif ( + flt(tax_withholding_details.get("tax_amount")) + and flt(tax_withholding_details.get("tax_amount")) < pending_amount + ): + allocated_amount = tax_withholding_details["tax_amount"] + tax_withholding_details["tax_amount"] = 0 - self.append('advance_tax', { - 'reference_type': 'Payment Entry', - 'reference_name': tax.parent, - 'reference_detail': tax.name, - 'account_head': tax.account_head, - 'allocated_amount': allocated_amount - }) + self.append( + "advance_tax", + { + "reference_type": "Payment Entry", + "reference_name": tax.parent, + "reference_detail": tax.name, + "account_head": tax.account_head, + "allocated_amount": allocated_amount, + }, + ) def update_advance_tax_references(self, cancel=0): - for tax in self.get('advance_tax'): + for tax in self.get("advance_tax"): at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") if cancel: @@ -1187,8 +1543,8 @@ class PurchaseInvoice(BuyingController): def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): - if self.get('amended_from'): - self.status = 'Draft' + if self.get("amended_from"): + self.status = "Draft" return outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount")) @@ -1199,19 +1555,25 @@ class PurchaseInvoice(BuyingController): status = "Cancelled" elif self.docstatus == 1: if self.is_internal_transfer(): - self.status = 'Internal Transfer' + self.status = "Internal Transfer" elif is_overdue(self, total): self.status = "Overdue" elif 0 < outstanding_amount < total: self.status = "Partly Paid" elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" - #Check if outstanding amount is 0 due to debit note issued against invoice - elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): + # Check if outstanding amount is 0 due to debit note issued against invoice + elif ( + outstanding_amount <= 0 + and self.is_return == 0 + and frappe.db.get_value( + "Purchase Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} + ) + ): self.status = "Debit Note Issued" elif self.is_return == 1: self.status = "Return" - elif outstanding_amount<=0: + elif outstanding_amount <= 0: self.status = "Paid" else: self.status = "Submitted" @@ -1219,76 +1581,86 @@ class PurchaseInvoice(BuyingController): self.status = "Draft" if update: - self.db_set('status', self.status, update_modified = update_modified) + self.db_set("status", self.status, update_modified=update_modified) + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Purchase Invoices'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Purchase Invoices"), + } + ) return list_context + @erpnext.allow_regional def make_regional_gl_entries(gl_entries, doc): return gl_entries + @frappe.whitelist() def make_debit_note(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Purchase Invoice", source_name, target_doc) + @frappe.whitelist() def make_stock_entry(source_name, target_doc=None): - doc = get_mapped_doc("Purchase Invoice", source_name, { - "Purchase Invoice": { - "doctype": "Stock Entry", - "validation": { - "docstatus": ["=", 1] - } - }, - "Purchase Invoice Item": { - "doctype": "Stock Entry Detail", - "field_map": { - "stock_qty": "transfer_qty", - "batch_no": "batch_no" + doc = get_mapped_doc( + "Purchase Invoice", + source_name, + { + "Purchase Invoice": {"doctype": "Stock Entry", "validation": {"docstatus": ["=", 1]}}, + "Purchase Invoice Item": { + "doctype": "Stock Entry Detail", + "field_map": {"stock_qty": "transfer_qty", "batch_no": "batch_no"}, }, - } - }, target_doc) + }, + target_doc, + ) return doc + @frappe.whitelist() def change_release_date(name, release_date=None): - if frappe.db.exists('Purchase Invoice', name): - pi = frappe.get_doc('Purchase Invoice', name) - pi.db_set('release_date', release_date) + if frappe.db.exists("Purchase Invoice", name): + pi = frappe.get_doc("Purchase Invoice", name) + pi.db_set("release_date", release_date) @frappe.whitelist() def unblock_invoice(name): - if frappe.db.exists('Purchase Invoice', name): - pi = frappe.get_doc('Purchase Invoice', name) + if frappe.db.exists("Purchase Invoice", name): + pi = frappe.get_doc("Purchase Invoice", name) pi.unblock_invoice() @frappe.whitelist() def block_invoice(name, release_date, hold_comment=None): - if frappe.db.exists('Purchase Invoice', name): - pi = frappe.get_doc('Purchase Invoice', name) + if frappe.db.exists("Purchase Invoice", name): + pi = frappe.get_doc("Purchase Invoice", name) pi.block_invoice(hold_comment, release_date) + @frappe.whitelist() def make_inter_company_sales_invoice(source_name, target_doc=None): from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction + return make_inter_company_transaction("Purchase Invoice", source_name, target_doc) + def on_doctype_update(): frappe.db.add_index("Purchase Invoice", ["supplier", "is_return", "return_against"]) + @frappe.whitelist() def make_purchase_receipt(source_name, target_doc=None): def update_item(obj, target, source_parent): @@ -1296,33 +1668,39 @@ def make_purchase_receipt(source_name, target_doc=None): target.received_qty = flt(obj.qty) - flt(obj.received_qty) target.stock_qty = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.conversion_factor) target.amount = (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) - target.base_amount = (flt(obj.qty) - flt(obj.received_qty)) * \ - flt(obj.rate) * flt(source_parent.conversion_rate) + target.base_amount = ( + (flt(obj.qty) - flt(obj.received_qty)) * flt(obj.rate) * flt(source_parent.conversion_rate) + ) - doc = get_mapped_doc("Purchase Invoice", source_name, { - "Purchase Invoice": { - "doctype": "Purchase Receipt", - "validation": { - "docstatus": ["=", 1], - } - }, - "Purchase Invoice Item": { - "doctype": "Purchase Receipt Item", - "field_map": { - "name": "purchase_invoice_item", - "parent": "purchase_invoice", - "bom": "bom", - "purchase_order": "purchase_order", - "po_detail": "purchase_order_item", - "material_request": "material_request", - "material_request_item": "material_request_item" + doc = get_mapped_doc( + "Purchase Invoice", + source_name, + { + "Purchase Invoice": { + "doctype": "Purchase Receipt", + "validation": { + "docstatus": ["=", 1], + }, }, - "postprocess": update_item, - "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty) + "Purchase Invoice Item": { + "doctype": "Purchase Receipt Item", + "field_map": { + "name": "purchase_invoice_item", + "parent": "purchase_invoice", + "bom": "bom", + "purchase_order": "purchase_order", + "po_detail": "purchase_order_item", + "material_request": "material_request", + "material_request_item": "material_request_item", + }, + "postprocess": update_item, + "condition": lambda doc: abs(doc.received_qty) < abs(doc.qty), + }, + "Purchase Taxes and Charges": {"doctype": "Purchase Taxes and Charges"}, }, - "Purchase Taxes and Charges": { - "doctype": "Purchase Taxes and Charges" - } - }, target_doc) + target_doc, + ) + + doc.set_onload("ignore_price_list", True) return doc diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py index f1878b480ed..10dd0ef6e25 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_dashboard.py @@ -1,38 +1,28 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'purchase_invoice', - 'non_standard_fieldnames': { - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name', - 'Payment Request': 'reference_name', - 'Landed Cost Voucher': 'receipt_document', - 'Purchase Invoice': 'return_against', - 'Auto Repeat': 'reference_document' + "fieldname": "purchase_invoice", + "non_standard_fieldnames": { + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", + "Payment Request": "reference_name", + "Landed Cost Voucher": "receipt_document", + "Purchase Invoice": "return_against", + "Auto Repeat": "reference_document", }, - 'internal_links': { - 'Purchase Order': ['items', 'purchase_order'], - 'Purchase Receipt': ['items', 'purchase_receipt'], + "internal_links": { + "Purchase Order": ["items", "purchase_order"], + "Purchase Receipt": ["items", "purchase_receipt"], }, - 'transactions': [ + "transactions": [ + {"label": _("Payment"), "items": ["Payment Entry", "Payment Request", "Journal Entry"]}, { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Payment Request', 'Journal Entry'] + "label": _("Reference"), + "items": ["Purchase Order", "Purchase Receipt", "Asset", "Landed Cost Voucher"], }, - { - 'label': _('Reference'), - 'items': ['Purchase Order', 'Purchase Receipt', 'Asset', 'Landed Cost Voucher'] - }, - { - 'label': _('Returns'), - 'items': ['Purchase Invoice'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + {"label": _("Returns"), "items": ["Purchase Invoice"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + ], } diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 52c3d52046a..60611b0edbb 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -2,7 +2,6 @@ # License: GNU General Public License v3. See license.txt - import unittest import frappe @@ -11,22 +10,29 @@ from frappe.utils import add_days, cint, flt, getdate, nowdate, today import erpnext from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry +from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice +from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order from erpnext.buying.doctype.supplier.test_supplier import create_supplier from erpnext.controllers.accounts_controller import get_payment_terms from erpnext.controllers.buying_controller import QtyMismatchError from erpnext.exceptions import InvalidCurrency from erpnext.projects.doctype.project.test_project import make_project from erpnext.stock.doctype.item.test_item import create_item +from erpnext.stock.doctype.purchase_receipt.purchase_receipt import ( + make_purchase_invoice as create_purchase_invoice_from_receipt, +) from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( get_taxes, make_purchase_receipt, ) from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction +from erpnext.stock.tests.test_utils import StockTestMixin test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"] test_ignore = ["Serial No"] -class TestPurchaseInvoice(unittest.TestCase): + +class TestPurchaseInvoice(unittest.TestCase, StockTestMixin): @classmethod def setUpClass(self): unlink_payment_on_cancel_of_invoice() @@ -38,16 +44,18 @@ class TestPurchaseInvoice(unittest.TestCase): def test_purchase_invoice_received_qty(self): """ - 1. Test if received qty is validated against accepted + rejected - 2. Test if received qty is auto set on save + 1. Test if received qty is validated against accepted + rejected + 2. Test if received qty is auto set on save """ pi = make_purchase_invoice( qty=1, rejected_qty=1, received_qty=3, item_code="_Test Item Home Desktop 200", - rejected_warehouse = "_Test Rejected Warehouse - _TC", - update_stock=True, do_not_save=True) + rejected_warehouse="_Test Rejected Warehouse - _TC", + update_stock=True, + do_not_save=True, + ) self.assertRaises(QtyMismatchError, pi.save) pi.items[0].received_qty = 0 @@ -74,18 +82,26 @@ class TestPurchaseInvoice(unittest.TestCase): "_Test Account CST - _TC": [29.88, 0], "_Test Account VAT - _TC": [156.25, 0], "_Test Account Discount - _TC": [0, 168.03], - "Round Off - _TC": [0, 0.3] + "Round Off - _TC": [0, 0.3], } - gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name, as_dict=1) + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` + where voucher_type = 'Purchase Invoice' and voucher_no = %s""", + pi.name, + as_dict=1, + ) for d in gl_entries: self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account)) def test_gl_entries_with_perpetual_inventory(self): - pi = make_purchase_invoice(company="_Test Company with perpetual inventory", - warehouse= "Stores - TCP1", cost_center = "Main - TCP1", - expense_account ="_Test Account Cost for Goods Sold - TCP1", - get_taxes_and_charges=True, qty=10) + pi = make_purchase_invoice( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + get_taxes_and_charges=True, + qty=10, + ) self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1) @@ -99,6 +115,7 @@ class TestPurchaseInvoice(unittest.TestCase): def test_payment_entry_unlink_against_purchase_invoice(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry + unlink_payment_on_cancel_of_invoice(0) pi_doc = make_purchase_invoice() @@ -114,7 +131,7 @@ class TestPurchaseInvoice(unittest.TestCase): pe.save(ignore_permissions=True) pe.submit() - pi_doc = frappe.get_doc('Purchase Invoice', pi_doc.name) + pi_doc = frappe.get_doc("Purchase Invoice", pi_doc.name) pi_doc.load_from_db() self.assertTrue(pi_doc.status, "Paid") @@ -122,7 +139,7 @@ class TestPurchaseInvoice(unittest.TestCase): unlink_payment_on_cancel_of_invoice() def test_purchase_invoice_for_blocked_supplier(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 supplier.save() @@ -132,9 +149,9 @@ class TestPurchaseInvoice(unittest.TestCase): supplier.save() def test_purchase_invoice_for_blocked_supplier_invoice(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Invoices' + supplier.hold_type = "Invoices" supplier.save() self.assertRaises(frappe.ValidationError, make_purchase_invoice) @@ -143,31 +160,40 @@ class TestPurchaseInvoice(unittest.TestCase): supplier.save() def test_purchase_invoice_for_blocked_supplier_payment(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' + supplier.hold_type = "Payments" supplier.save() pi = make_purchase_invoice() self.assertRaises( - frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC") + frappe.ValidationError, + get_payment_entry, + dt="Purchase Invoice", + dn=pi.name, + bank_account="_Test Bank - _TC", + ) supplier.on_hold = 0 supplier.save() def test_purchase_invoice_for_blocked_supplier_payment_today_date(self): - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' + supplier.hold_type = "Payments" supplier.release_date = nowdate() supplier.save() pi = make_purchase_invoice() self.assertRaises( - frappe.ValidationError, get_payment_entry, dt='Purchase Invoice', dn=pi.name, - bank_account="_Test Bank - _TC") + frappe.ValidationError, + get_payment_entry, + dt="Purchase Invoice", + dn=pi.name, + bank_account="_Test Bank - _TC", + ) supplier.on_hold = 0 supplier.save() @@ -176,15 +202,15 @@ class TestPurchaseInvoice(unittest.TestCase): # this test is meant to fail only if something fails in the try block with self.assertRaises(Exception): try: - supplier = frappe.get_doc('Supplier', '_Test Supplier') + supplier = frappe.get_doc("Supplier", "_Test Supplier") supplier.on_hold = 1 - supplier.hold_type = 'Payments' - supplier.release_date = '2018-03-01' + supplier.hold_type = "Payments" + supplier.release_date = "2018-03-01" supplier.save() pi = make_purchase_invoice() - get_payment_entry('Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC") + get_payment_entry("Purchase Invoice", dn=pi.name, bank_account="_Test Bank - _TC") supplier.on_hold = 0 supplier.save() @@ -198,7 +224,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi.release_date = nowdate() self.assertRaises(frappe.ValidationError, pi.save) - pi.release_date = '' + pi.release_date = "" pi.save() def test_purchase_invoice_temporary_blocked(self): @@ -207,7 +233,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi.save() pi.submit() - pe = get_payment_entry('Purchase Invoice', dn=pi.name, bank_account="_Test Bank - _TC") + pe = get_payment_entry("Purchase Invoice", dn=pi.name, bank_account="_Test Bank - _TC") self.assertRaises(frappe.ValidationError, pe.save) @@ -223,9 +249,24 @@ class TestPurchaseInvoice(unittest.TestCase): def test_gl_entries_with_perpetual_inventory_against_pr(self): - pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,) + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + supplier_warehouse="Work In Progress - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + get_taxes_and_charges=True, + ) - pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True") + pi = make_purchase_invoice( + company="_Test Company with perpetual inventory", + supplier_warehouse="Work In Progress - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + get_taxes_and_charges=True, + qty=10, + do_not_save="True", + ) for d in pi.items: d.purchase_receipt = pr.name @@ -238,72 +279,35 @@ class TestPurchaseInvoice(unittest.TestCase): self.check_gle_for_pi(pi.name) def check_gle_for_pi(self, pi): - gl_entries = frappe.db.sql("""select account, sum(debit) as debit, sum(credit) as credit + gl_entries = frappe.db.sql( + """select account, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - group by account""", pi, as_dict=1) + group by account""", + pi, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - ["Creditors - TCP1", 0, 720], - ["Stock Received But Not Billed - TCP1", 500.0, 0], - ["_Test Account Shipping Charges - TCP1", 100.0, 0.0], - ["_Test Account VAT - TCP1", 120.0, 0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + ["Creditors - TCP1", 0, 720], + ["Stock Received But Not Billed - TCP1", 500.0, 0], + ["_Test Account Shipping Charges - TCP1", 100.0, 0.0], + ["_Test Account VAT - TCP1", 120.0, 0], + ] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) - def test_purchase_invoice_with_discount_accounting_enabled(self): - enable_discount_accounting() - - discount_account = create_account(account_name="Discount Account", - parent_account="Indirect Expenses - _TC", company="_Test Company") - pi = make_purchase_invoice(discount_account=discount_account, rate=45) - - expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 225.0, nowdate()], - ["Discount Account - _TC", 0.0, 25.0, nowdate()] - ] - - check_gl_entries(self, pi.name, expected_gle, nowdate()) - enable_discount_accounting(enable=0) - - def test_additional_discount_for_purchase_invoice_with_discount_accounting_enabled(self): - enable_discount_accounting() - additional_discount_account = create_account(account_name="Discount Account", - parent_account="Indirect Expenses - _TC", company="_Test Company") - - pi = make_purchase_invoice(do_not_save=1, parent_cost_center="Main - _TC") - pi.apply_discount_on = "Grand Total" - pi.additional_discount_account = additional_discount_account - pi.additional_discount_percentage = 10 - pi.disable_rounded_total = 1 - pi.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account VAT - _TC", - "cost_center": "Main - _TC", - "description": "Test", - "rate": 10 - }) - pi.submit() - - expected_gle = [ - ["_Test Account Cost for Goods Sold - _TC", 250.0, 0.0, nowdate()], - ["_Test Account VAT - _TC", 25.0, 0.0, nowdate()], - ["Creditors - _TC", 0.0, 247.5, nowdate()], - ["Discount Account - _TC", 0.0, 27.5, nowdate()] - ] - - check_gl_entries(self, pi.name, expected_gle, nowdate()) - def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() - pi.naming_series = 'TEST-' + pi.naming_series = "TEST-" self.assertRaises(frappe.CannotChangeConstantError, pi.save) @@ -312,25 +316,33 @@ class TestPurchaseInvoice(unittest.TestCase): pi.load_from_db() self.assertTrue(pi.status, "Draft") - pi.naming_series = 'TEST-' + pi.naming_series = "TEST-" self.assertRaises(frappe.CannotChangeConstantError, pi.save) def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self): - pi = make_purchase_invoice(item_code = "_Test Non Stock Item", - company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", - cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") + pi = make_purchase_invoice( + item_code="_Test Non Stock Item", + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) self.assertTrue(pi.status, "Unpaid") - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) expected_values = [ ["_Test Account Cost for Goods Sold - TCP1", 250.0, 0], - ["Creditors - TCP1", 0, 250] + ["Creditors - TCP1", 0, 250], ] for i, gle in enumerate(gl_entries): @@ -345,7 +357,7 @@ class TestPurchaseInvoice(unittest.TestCase): expected_values = [ ["_Test Item Home Desktop 100", 90, 59], - ["_Test Item Home Desktop 200", 135, 177] + ["_Test Item Home Desktop 200", 135, 177], ] for i, item in enumerate(pi.get("items")): self.assertEqual(item.item_code, expected_values[i][0]) @@ -377,10 +389,7 @@ class TestPurchaseInvoice(unittest.TestCase): wrapper.insert() wrapper.load_from_db() - expected_values = [ - ["_Test FG Item", 90, 59], - ["_Test Item Home Desktop 200", 135, 177] - ] + expected_values = [["_Test FG Item", 90, 59], ["_Test Item Home Desktop 200", 135, 177]] for i, item in enumerate(wrapper.get("items")): self.assertEqual(item.item_code, expected_values[i][0]) self.assertEqual(item.item_tax_amount, expected_values[i][1]) @@ -417,14 +426,17 @@ class TestPurchaseInvoice(unittest.TestCase): pi = frappe.copy_doc(test_records[0]) pi.disable_rounded_total = 1 pi.allocate_advances_automatically = 0 - pi.append("advances", { - "reference_type": "Journal Entry", - "reference_name": jv.name, - "reference_row": jv.get("accounts")[0].name, - "advance_amount": 400, - "allocated_amount": 300, - "remarks": jv.remark - }) + pi.append( + "advances", + { + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.get("accounts")[0].name, + "advance_amount": 400, + "allocated_amount": 300, + "remarks": jv.remark, + }, + ) pi.insert() self.assertEqual(pi.outstanding_amount, 1212.30) @@ -437,14 +449,24 @@ class TestPurchaseInvoice(unittest.TestCase): pi.submit() pi.load_from_db() - self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` + self.assertTrue( + frappe.db.sql( + """select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' - and reference_name=%s and debit_in_account_currency=300""", pi.name)) + and reference_name=%s and debit_in_account_currency=300""", + pi.name, + ) + ) pi.cancel() - self.assertFalse(frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_type='Purchase Invoice' and reference_name=%s""", pi.name)) + self.assertFalse( + frappe.db.sql( + """select name from `tabJournal Entry Account` + where reference_type='Purchase Invoice' and reference_name=%s""", + pi.name, + ) + ) def test_invoice_with_advance_and_multi_payment_terms(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( @@ -458,20 +480,26 @@ class TestPurchaseInvoice(unittest.TestCase): pi = frappe.copy_doc(test_records[0]) pi.disable_rounded_total = 1 pi.allocate_advances_automatically = 0 - pi.append("advances", { - "reference_type": "Journal Entry", - "reference_name": jv.name, - "reference_row": jv.get("accounts")[0].name, - "advance_amount": 400, - "allocated_amount": 300, - "remarks": jv.remark - }) + pi.append( + "advances", + { + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.get("accounts")[0].name, + "advance_amount": 400, + "allocated_amount": 300, + "remarks": jv.remark, + }, + ) pi.insert() - pi.update({ - "payment_schedule": get_payment_terms("_Test Payment Term Template", - pi.posting_date, pi.grand_total, pi.base_grand_total) - }) + pi.update( + { + "payment_schedule": get_payment_terms( + "_Test Payment Term Template", pi.posting_date, pi.grand_total, pi.base_grand_total + ) + } + ) pi.save() pi.submit() @@ -485,7 +513,9 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertTrue( frappe.db.sql( "select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' and " - "reference_name=%s and debit_in_account_currency=300", pi.name) + "reference_name=%s and debit_in_account_currency=300", + pi.name, + ) ) self.assertEqual(pi.outstanding_amount, 1212.30) @@ -495,49 +525,76 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertFalse( frappe.db.sql( "select name from `tabJournal Entry Account` where reference_type='Purchase Invoice' and " - "reference_name=%s", pi.name) + "reference_name=%s", + pi.name, + ) ) def test_total_purchase_cost_for_project(self): if not frappe.db.exists("Project", {"project_name": "_Test Project for Purchase"}): - project = make_project({'project_name':'_Test Project for Purchase'}) + project = make_project({"project_name": "_Test Project for Purchase"}) else: project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"}) - existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount) + existing_purchase_cost = frappe.db.sql( + """select sum(base_net_amount) from `tabPurchase Invoice Item` where project = '{0}' - and docstatus=1""".format(project.name)) + and docstatus=1""".format( + project.name + ) + ) existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0 pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name) - self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), - existing_purchase_cost + 15000) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_purchase_cost"), + existing_purchase_cost + 15000, + ) pi1 = make_purchase_invoice(qty=10, project=project.name) - self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), - existing_purchase_cost + 15500) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_purchase_cost"), + existing_purchase_cost + 15500, + ) pi1.cancel() - self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), - existing_purchase_cost + 15000) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_purchase_cost"), + existing_purchase_cost + 15000, + ) pi.cancel() - self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost) + self.assertEqual( + frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost + ) def test_return_purchase_invoice_with_perpetual_inventory(self): - pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", - cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") - - return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2, - company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1", - cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") + pi = make_purchase_invoice( + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) + return_pi = make_purchase_invoice( + is_return=1, + return_against=pi.name, + qty=-2, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) # check gl entries for return - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type=%s and voucher_no=%s - order by account desc""", ("Purchase Invoice", return_pi.name), as_dict=1) + order by account desc""", + ("Purchase Invoice", return_pi.name), + as_dict=1, + ) self.assertTrue(gl_entries) @@ -550,14 +607,96 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][0], gle.debit) self.assertEqual(expected_values[gle.account][1], gle.credit) - def test_multi_currency_gle(self): - pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", - currency="USD", conversion_rate=50) + def test_standalone_return_using_pi(self): + from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + item = self.make_item().name + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + + make_stock_entry(item_code=item, target=warehouse, qty=50, rate=120) + + return_pi = make_purchase_invoice( + is_return=1, + item=item, + qty=-10, + update_stock=1, + rate=100, + company=company, + warehouse=warehouse, + cost_center="Main - TCP1", + ) + + # assert that stock consumption is with actual rate + self.assertGLEs( + return_pi, + [{"credit": 1200, "debit": 0}], + gle_filters={"account": "Stock In Hand - TCP1"}, + ) + + # assert loss booked in COGS + self.assertGLEs( + return_pi, + [{"credit": 0, "debit": 200}], + gle_filters={"account": "Cost of Goods Sold - TCP1"}, + ) + + def test_return_with_lcv(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import ( + create_landed_cost_voucher, + ) + + item = self.make_item().name + company = "_Test Company with perpetual inventory" + warehouse = "Stores - TCP1" + cost_center = "Main - TCP1" + + pi = make_purchase_invoice( + item=item, + company=company, + warehouse=warehouse, + cost_center=cost_center, + update_stock=1, + qty=10, + rate=100, + ) + + # Create landed cost voucher - will increase valuation of received item by 10 + create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company, charges=100) + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + # assert that stock consumption is with actual in rate + self.assertGLEs( + return_pi, + [{"credit": 1100, "debit": 0}], + gle_filters={"account": "Stock In Hand - TCP1"}, + ) + + # assert loss booked in COGS + self.assertGLEs( + return_pi, + [{"credit": 0, "debit": 100}], + gle_filters={"account": "Cost of Goods Sold - TCP1"}, + ) + + def test_multi_currency_gle(self): + pi = make_purchase_invoice( + supplier="_Test Supplier USD", + credit_to="_Test Payable USD - _TC", + currency="USD", + conversion_rate=50, + ) + + gl_entries = frappe.db.sql( + """select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -567,53 +706,74 @@ class TestPurchaseInvoice(unittest.TestCase): "debit": 0, "debit_in_account_currency": 0, "credit": 12500, - "credit_in_account_currency": 250 + "credit_in_account_currency": 250, }, "_Test Account Cost for Goods Sold - _TC": { "account_currency": "INR", "debit": 12500, "debit_in_account_currency": 12500, "credit": 0, - "credit_in_account_currency": 0 - } + "credit_in_account_currency": 0, + }, } - for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): + for field in ( + "account_currency", + "debit", + "debit_in_account_currency", + "credit", + "credit_in_account_currency", + ): for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][field], gle[field]) - # Check for valid currency - pi1 = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", - do_not_save=True) + pi1 = make_purchase_invoice( + supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC", do_not_save=True + ) self.assertRaises(InvalidCurrency, pi1.save) # cancel pi.cancel() - gle = frappe.db.sql("""select name from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", pi.name) + gle = frappe.db.sql( + """select name from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + pi.name, + ) self.assertFalse(gle) def test_purchase_invoice_update_stock_gl_entry_with_perpetual_inventory(self): - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") + pi = make_purchase_invoice( + update_stock=1, + posting_date=frappe.utils.nowdate(), + posting_time=frappe.utils.nowtime(), + cash_bank_account="Cash - TCP1", + company="_Test Company with perpetual inventory", + supplier_warehouse="Work In Progress - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) stock_in_hand_account = get_inventory_account(pi.company, pi.get("items")[0].warehouse) - expected_gl_entries = dict((d[0], d) for d in [ - [pi.credit_to, 0.0, 250.0], - [stock_in_hand_account, 250.0, 0.0] - ]) + expected_gl_entries = dict( + (d[0], d) for d in [[pi.credit_to, 0.0, 250.0], [stock_in_hand_account, 250.0, 0.0]] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gl_entries[gle.account][0], gle.account) @@ -622,22 +782,39 @@ class TestPurchaseInvoice(unittest.TestCase): def test_purchase_invoice_for_is_paid_and_update_stock_gl_entry_with_perpetual_inventory(self): - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime(), cash_bank_account="Cash - TCP1", is_paid=1, company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1") + pi = make_purchase_invoice( + update_stock=1, + posting_date=frappe.utils.nowdate(), + posting_time=frappe.utils.nowtime(), + cash_bank_account="Cash - TCP1", + is_paid=1, + company="_Test Company with perpetual inventory", + supplier_warehouse="Work In Progress - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + expense_account="_Test Account Cost for Goods Sold - TCP1", + ) - gl_entries = frappe.db.sql("""select account, account_currency, sum(debit) as debit, + gl_entries = frappe.db.sql( + """select account, account_currency, sum(debit) as debit, sum(credit) as credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - group by account, voucher_no order by account asc;""", pi.name, as_dict=1) + group by account, voucher_no order by account asc;""", + pi.name, + as_dict=1, + ) stock_in_hand_account = get_inventory_account(pi.company, pi.get("items")[0].warehouse) self.assertTrue(gl_entries) - expected_gl_entries = dict((d[0], d) for d in [ - [pi.credit_to, 250.0, 250.0], - [stock_in_hand_account, 250.0, 0.0], - ["Cash - TCP1", 0.0, 250.0] - ]) + expected_gl_entries = dict( + (d[0], d) + for d in [ + [pi.credit_to, 250.0, 250.0], + [stock_in_hand_account, 250.0, 0.0], + ["Cash - TCP1", 0.0, 250.0], + ] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gl_entries[gle.account][0], gle.account) @@ -645,31 +822,36 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_gl_entries[gle.account][2], gle.credit) def test_auto_batch(self): - item_code = frappe.db.get_value('Item', - {'has_batch_no': 1, 'create_new_batch':1}, 'name') + item_code = frappe.db.get_value("Item", {"has_batch_no": 1, "create_new_batch": 1}, "name") if not item_code: - doc = frappe.get_doc({ - 'doctype': 'Item', - 'is_stock_item': 1, - 'item_code': 'test batch item', - 'item_group': 'Products', - 'has_batch_no': 1, - 'create_new_batch': 1 - }).insert(ignore_permissions=True) + doc = frappe.get_doc( + { + "doctype": "Item", + "is_stock_item": 1, + "item_code": "test batch item", + "item_group": "Products", + "has_batch_no": 1, + "create_new_batch": 1, + } + ).insert(ignore_permissions=True) item_code = doc.name - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime(), item_code=item_code) + pi = make_purchase_invoice( + update_stock=1, + posting_date=frappe.utils.nowdate(), + posting_time=frappe.utils.nowtime(), + item_code=item_code, + ) - self.assertTrue(frappe.db.get_value('Batch', - {'item': item_code, 'reference_name': pi.name})) + self.assertTrue(frappe.db.get_value("Batch", {"item": item_code, "reference_name": pi.name})) def test_update_stock_and_purchase_return(self): actual_qty_0 = get_qty_after_transaction() - pi = make_purchase_invoice(update_stock=1, posting_date=frappe.utils.nowdate(), - posting_time=frappe.utils.nowtime()) + pi = make_purchase_invoice( + update_stock=1, posting_date=frappe.utils.nowdate(), posting_time=frappe.utils.nowtime() + ) actual_qty_1 = get_qty_after_transaction() self.assertEqual(actual_qty_0 + 5, actual_qty_1) @@ -696,13 +878,20 @@ class TestPurchaseInvoice(unittest.TestCase): from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry - update_backflush_based_on('BOM') - make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100) - make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC", - qty=100, basic_rate=100) + update_backflush_based_on("BOM") + make_stock_entry( + item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100 + ) + make_stock_entry( + item_code="_Test Item Home Desktop 100", + target="_Test Warehouse 1 - _TC", + qty=100, + basic_rate=100, + ) - pi = make_purchase_invoice(item_code="_Test FG Item", qty=10, rate=500, - update_stock=1, is_subcontracted="Yes") + pi = make_purchase_invoice( + item_code="_Test FG Item", qty=10, rate=500, update_stock=1, is_subcontracted="Yes" + ) self.assertEqual(len(pi.get("supplied_items")), 2) @@ -710,15 +899,26 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2)) def test_rejected_serial_no(self): - pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1, - rejected_qty=1, rate=500, update_stock=1, rejected_warehouse = "_Test Rejected Warehouse - _TC", - allow_zero_valuation_rate=1) + pi = make_purchase_invoice( + item_code="_Test Serialized Item With Series", + received_qty=2, + qty=1, + rejected_qty=1, + rate=500, + update_stock=1, + rejected_warehouse="_Test Rejected Warehouse - _TC", + allow_zero_valuation_rate=1, + ) - self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"), - pi.get("items")[0].warehouse) + self.assertEqual( + frappe.db.get_value("Serial No", pi.get("items")[0].serial_no, "warehouse"), + pi.get("items")[0].warehouse, + ) - self.assertEqual(frappe.db.get_value("Serial No", pi.get("items")[0].rejected_serial_no, - "warehouse"), pi.get("items")[0].rejected_warehouse) + self.assertEqual( + frappe.db.get_value("Serial No", pi.get("items")[0].rejected_serial_no, "warehouse"), + pi.get("items")[0].rejected_warehouse, + ) def test_outstanding_amount_after_advance_jv_cancelation(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( @@ -726,85 +926,95 @@ class TestPurchaseInvoice(unittest.TestCase): ) jv = frappe.copy_doc(jv_test_records[1]) - jv.accounts[0].is_advance = 'Yes' + jv.accounts[0].is_advance = "Yes" jv.insert() jv.submit() pi = frappe.copy_doc(test_records[0]) - pi.append("advances", { - "reference_type": "Journal Entry", - "reference_name": jv.name, - "reference_row": jv.get("accounts")[0].name, - "advance_amount": 400, - "allocated_amount": 300, - "remarks": jv.remark - }) + pi.append( + "advances", + { + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.get("accounts")[0].name, + "advance_amount": 400, + "allocated_amount": 300, + "remarks": jv.remark, + }, + ) pi.insert() pi.submit() pi.load_from_db() - #check outstanding after advance allocation + # check outstanding after advance allocation self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total - pi.total_advance)) - #added to avoid Document has been modified exception + # added to avoid Document has been modified exception jv = frappe.get_doc("Journal Entry", jv.name) jv.cancel() pi.load_from_db() - #check outstanding after advance cancellation + # check outstanding after advance cancellation self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total + pi.total_advance)) def test_outstanding_amount_after_advance_payment_entry_cancelation(self): - pe = frappe.get_doc({ - "doctype": "Payment Entry", - "payment_type": "Pay", - "party_type": "Supplier", - "party": "_Test Supplier", - "company": "_Test Company", - "paid_from_account_currency": "INR", - "paid_to_account_currency": "INR", - "source_exchange_rate": 1, - "target_exchange_rate": 1, - "reference_no": "1", - "reference_date": nowdate(), - "received_amount": 300, - "paid_amount": 300, - "paid_from": "_Test Cash - _TC", - "paid_to": "_Test Payable - _TC" - }) + pe = frappe.get_doc( + { + "doctype": "Payment Entry", + "payment_type": "Pay", + "party_type": "Supplier", + "party": "_Test Supplier", + "company": "_Test Company", + "paid_from_account_currency": "INR", + "paid_to_account_currency": "INR", + "source_exchange_rate": 1, + "target_exchange_rate": 1, + "reference_no": "1", + "reference_date": nowdate(), + "received_amount": 300, + "paid_amount": 300, + "paid_from": "_Test Cash - _TC", + "paid_to": "_Test Payable - _TC", + } + ) pe.insert() pe.submit() pi = frappe.copy_doc(test_records[0]) pi.is_pos = 0 - pi.append("advances", { - "doctype": "Purchase Invoice Advance", - "reference_type": "Payment Entry", - "reference_name": pe.name, - "advance_amount": 300, - "allocated_amount": 300, - "remarks": pe.remarks - }) + pi.append( + "advances", + { + "doctype": "Purchase Invoice Advance", + "reference_type": "Payment Entry", + "reference_name": pe.name, + "advance_amount": 300, + "allocated_amount": 300, + "remarks": pe.remarks, + }, + ) pi.insert() pi.submit() pi.load_from_db() - #check outstanding after advance allocation + # check outstanding after advance allocation self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total - pi.total_advance)) - #added to avoid Document has been modified exception + # added to avoid Document has been modified exception pe = frappe.get_doc("Payment Entry", pe.name) pe.cancel() pi.load_from_db() - #check outstanding after advance cancellation + # check outstanding after advance cancellation self.assertEqual(flt(pi.outstanding_amount), flt(pi.rounded_total + pi.total_advance)) def test_purchase_invoice_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule - shipping_rule = create_shipping_rule(shipping_rule_type = "Buying", shipping_rule_name = "Shipping Rule - Purchase Invoice Test") + shipping_rule = create_shipping_rule( + shipping_rule_type="Buying", shipping_rule_name="Shipping Rule - Purchase Invoice Test" + ) pi = frappe.copy_doc(test_records[0]) @@ -820,16 +1030,20 @@ class TestPurchaseInvoice(unittest.TestCase): def test_make_pi_without_terms(self): pi = make_purchase_invoice(do_not_save=1) - self.assertFalse(pi.get('payment_schedule')) + self.assertFalse(pi.get("payment_schedule")) pi.insert() - self.assertTrue(pi.get('payment_schedule')) + self.assertTrue(pi.get("payment_schedule")) def test_duplicate_due_date_in_terms(self): pi = make_purchase_invoice(do_not_save=1) - pi.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50)) - pi.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50)) + pi.append( + "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) + ) + pi.append( + "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) + ) self.assertRaises(frappe.ValidationError, pi.insert) @@ -837,12 +1051,13 @@ class TestPurchaseInvoice(unittest.TestCase): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import get_outstanding_amount - pi = make_purchase_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1) + pi = make_purchase_invoice(item_code="_Test Item", qty=(5 * -1), rate=500, is_return=1) pi.load_from_db() self.assertTrue(pi.status, "Return") - outstanding_amount = get_outstanding_amount(pi.doctype, - pi.name, "Creditors - _TC", pi.supplier, "Supplier") + outstanding_amount = get_outstanding_amount( + pi.doctype, pi.name, "Creditors - _TC", pi.supplier, "Supplier" + ) self.assertEqual(pi.outstanding_amount, outstanding_amount) @@ -857,30 +1072,33 @@ class TestPurchaseInvoice(unittest.TestCase): pe.insert() pe.submit() - pi_doc = frappe.get_doc('Purchase Invoice', pi.name) + pi_doc = frappe.get_doc("Purchase Invoice", pi.name) self.assertEqual(pi_doc.outstanding_amount, 0) def test_purchase_invoice_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - pi = make_purchase_invoice_against_cost_center(cost_center=cost_center, credit_to="Creditors - _TC") + pi = make_purchase_invoice_against_cost_center( + cost_center=cost_center, credit_to="Creditors - _TC" + ) self.assertEqual(pi.cost_center, cost_center) expected_values = { - "Creditors - _TC": { - "cost_center": cost_center - }, - "_Test Account Cost for Goods Sold - _TC": { - "cost_center": cost_center - } + "Creditors - _TC": {"cost_center": cost_center}, + "_Test Account Cost for Goods Sold - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -889,21 +1107,21 @@ class TestPurchaseInvoice(unittest.TestCase): def test_purchase_invoice_without_cost_center(self): cost_center = "_Test Cost Center - _TC" - pi = make_purchase_invoice(credit_to="Creditors - _TC") + pi = make_purchase_invoice(credit_to="Creditors - _TC") expected_values = { - "Creditors - _TC": { - "cost_center": None - }, - "_Test Account Cost for Goods Sold - _TC": { - "cost_center": cost_center - } + "Creditors - _TC": {"cost_center": None}, + "_Test Account Cost for Goods Sold - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -911,36 +1129,40 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) def test_purchase_invoice_with_project_link(self): - project = make_project({ - 'project_name': 'Purchase Invoice Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2020-01-01' - }) - item_project = make_project({ - 'project_name': 'Purchase Invoice Item Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2019-06-01' - }) + project = make_project( + { + "project_name": "Purchase Invoice Project", + "project_template_name": "Test Project Template", + "start_date": "2020-01-01", + } + ) + item_project = make_project( + { + "project_name": "Purchase Invoice Item Project", + "project_template_name": "Test Project Template", + "start_date": "2019-06-01", + } + ) - pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1) + pi = make_purchase_invoice(credit_to="Creditors - _TC", do_not_save=1) pi.items[0].project = item_project.name pi.project = project.name pi.submit() expected_values = { - "Creditors - _TC": { - "project": project.name - }, - "_Test Account Cost for Goods Sold - _TC": { - "project": item_project.name - } + "Creditors - _TC": {"project": project.name}, + "_Test Account Cost for Goods Sold - _TC": {"project": item_project.name}, } - gl_entries = frappe.db.sql("""select account, cost_center, project, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, project, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - order by account asc""", pi.name, as_dict=1) + order by account asc""", + pi.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -948,10 +1170,11 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account]["project"], gle.project) def test_deferred_expense_via_journal_entry(self): - deferred_account = create_account(account_name="Deferred Expense", - parent_account="Current Assets - _TC", company="_Test Company") + deferred_account = create_account( + account_name="Deferred Expense", parent_account="Current Assets - _TC", company="_Test Company" + ) - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 1 acc_settings.submit_journal_entries = 1 acc_settings.save() @@ -963,7 +1186,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi = make_purchase_invoice(item=item.name, qty=1, rate=100, do_not_save=True) pi.set_posting_time = 1 - pi.posting_date = '2019-03-15' + pi.posting_date = "2019-01-10" pi.items[0].enable_deferred_expense = 1 pi.items[0].service_start_date = "2019-01-10" pi.items[0].service_end_date = "2019-03-15" @@ -971,14 +1194,16 @@ class TestPurchaseInvoice(unittest.TestCase): pi.save() pi.submit() - pda1 = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Expense", - company="_Test Company" - )) + pda1 = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Expense", + company="_Test Company", + ) + ) pda1.insert() pda1.submit() @@ -989,13 +1214,17 @@ class TestPurchaseInvoice(unittest.TestCase): ["_Test Account Cost for Goods Sold - _TC", 0.0, 43.08, "2019-02-28"], [deferred_account, 43.08, 0.0, "2019-02-28"], ["_Test Account Cost for Goods Sold - _TC", 0.0, 23.07, "2019-03-15"], - [deferred_account, 23.07, 0.0, "2019-03-15"] + [deferred_account, 23.07, 0.0, "2019-03-15"], ] - gl_entries = gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + gl_entries = gl_entries = frappe.db.sql( + """select account, debit, credit, posting_date from `tabGL Entry` where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s - order by posting_date asc, account asc""", (pi.items[0].name, pi.posting_date), as_dict=1) + order by posting_date asc, account asc""", + (pi.items[0].name, pi.posting_date), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) @@ -1003,108 +1232,139 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_gle[i][2], gle.debit) self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") acc_settings.book_deferred_entries_via_journal_entry = 0 acc_settings.submit_journal_entriessubmit_journal_entries = 0 acc_settings.save() def test_gain_loss_with_advance_entry(self): unlink_enabled = frappe.db.get_value( - "Accounts Settings", "Accounts Settings", - "unlink_payment_on_cancel_of_invoice") + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" + ) frappe.db.set_value( - "Accounts Settings", "Accounts Settings", - "unlink_payment_on_cancel_of_invoice", 1) + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1 + ) original_account = frappe.db.get_value("Company", "_Test Company", "exchange_gain_loss_account") - frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC") + frappe.db.set_value( + "Company", "_Test Company", "exchange_gain_loss_account", "Exchange Gain/Loss - _TC" + ) - pay = frappe.get_doc({ - 'doctype': 'Payment Entry', - 'company': '_Test Company', - 'payment_type': 'Pay', - 'party_type': 'Supplier', - 'party': '_Test Supplier USD', - 'paid_to': '_Test Payable USD - _TC', - 'paid_from': 'Cash - _TC', - 'paid_amount': 70000, - 'target_exchange_rate': 70, - 'received_amount': 1000, - }) + pay = frappe.get_doc( + { + "doctype": "Payment Entry", + "company": "_Test Company", + "payment_type": "Pay", + "party_type": "Supplier", + "party": "_Test Supplier USD", + "paid_to": "_Test Payable USD - _TC", + "paid_from": "Cash - _TC", + "paid_amount": 70000, + "target_exchange_rate": 70, + "received_amount": 1000, + } + ) pay.insert() pay.submit() - pi = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", - conversion_rate=75, rate=500, do_not_save=1, qty=1) + pi = make_purchase_invoice( + supplier="_Test Supplier USD", + currency="USD", + conversion_rate=75, + rate=500, + do_not_save=1, + qty=1, + ) pi.cost_center = "_Test Cost Center - _TC" pi.advances = [] - pi.append("advances", { - "reference_type": "Payment Entry", - "reference_name": pay.name, - "advance_amount": 1000, - "remarks": pay.remarks, - "allocated_amount": 500, - "ref_exchange_rate": 70 - }) + pi.append( + "advances", + { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 1000, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70, + }, + ) pi.save() pi.submit() expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 37500.0], ["_Test Payable USD - _TC", -35000.0], - ["Exchange Gain/Loss - _TC", -2500.0] + ["Exchange Gain/Loss - _TC", -2500.0], ] - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select account, sum(debit - credit) as balance from `tabGL Entry` where voucher_no=%s group by account - order by account asc""", (pi.name), as_dict=1) + order by account asc""", + (pi.name), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.balance) - pi_2 = make_purchase_invoice(supplier='_Test Supplier USD', currency="USD", - conversion_rate=73, rate=500, do_not_save=1, qty=1) + pi_2 = make_purchase_invoice( + supplier="_Test Supplier USD", + currency="USD", + conversion_rate=73, + rate=500, + do_not_save=1, + qty=1, + ) pi_2.cost_center = "_Test Cost Center - _TC" pi_2.advances = [] - pi_2.append("advances", { - "reference_type": "Payment Entry", - "reference_name": pay.name, - "advance_amount": 500, - "remarks": pay.remarks, - "allocated_amount": 500, - "ref_exchange_rate": 70 - }) + pi_2.append( + "advances", + { + "reference_type": "Payment Entry", + "reference_name": pay.name, + "advance_amount": 500, + "remarks": pay.remarks, + "allocated_amount": 500, + "ref_exchange_rate": 70, + }, + ) pi_2.save() pi_2.submit() expected_gle = [ ["_Test Account Cost for Goods Sold - _TC", 36500.0], ["_Test Payable USD - _TC", -35000.0], - ["Exchange Gain/Loss - _TC", -1500.0] + ["Exchange Gain/Loss - _TC", -1500.0], ] - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select account, sum(debit - credit) as balance from `tabGL Entry` where voucher_no=%s - group by account order by account asc""", (pi_2.name), as_dict=1) + group by account order by account asc""", + (pi_2.name), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) self.assertEqual(expected_gle[i][1], gle.balance) - expected_gle = [ - ["_Test Payable USD - _TC", 70000.0], - ["Cash - _TC", -70000.0] - ] + expected_gle = [["_Test Payable USD - _TC", 70000.0], ["Cash - _TC", -70000.0]] - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select account, sum(debit - credit) as balance from `tabGL Entry` where voucher_no=%s and is_cancelled=0 - group by account order by account asc""", (pay.name), as_dict=1) + group by account order by account asc""", + (pay.name), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) @@ -1119,46 +1379,57 @@ class TestPurchaseInvoice(unittest.TestCase): pay.reload() pay.cancel() - frappe.db.set_value("Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled) + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled + ) frappe.db.set_value("Company", "_Test Company", "exchange_gain_loss_account", original_account) def test_purchase_invoice_advance_taxes(self): from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice - from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order # create a new supplier to test - supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier', - tax_withholding_category = 'TDS - 194 - Dividends - Individual') + supplier = create_supplier( + supplier_name="_Test TDS Advance Supplier", + tax_withholding_category="TDS - 194 - Dividends - Individual", + ) # Update tax withholding category with current fiscal year and rate details - update_tax_witholding_category('_Test Company', 'TDS Payable - _TC') + update_tax_witholding_category("_Test Company", "TDS Payable - _TC") # Create Purchase Order with TDS applied - po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item', - posting_date='2021-09-15') + po = create_purchase_order( + do_not_save=1, + supplier=supplier.name, + rate=3000, + item="_Test Non Stock Item", + posting_date="2021-09-15", + ) po.save() po.submit() # Create Payment Entry Against the order - payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name) - payment_entry.paid_from = 'Cash - _TC' + payment_entry = get_payment_entry(dt="Purchase Order", dn=po.name) + payment_entry.paid_from = "Cash - _TC" payment_entry.apply_tax_withholding_amount = 1 - payment_entry.tax_withholding_category = 'TDS - 194 - Dividends - Individual' + payment_entry.tax_withholding_category = "TDS - 194 - Dividends - Individual" payment_entry.save() payment_entry.submit() # Check GLE for Payment Entry expected_gle = [ - ['Cash - _TC', 0, 27000], - ['Creditors - _TC', 30000, 0], - ['TDS Payable - _TC', 0, 3000], + ["Cash - _TC", 0, 27000], + ["Creditors - _TC", 30000, 0], + ["TDS Payable - _TC", 0, 3000], ] - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Payment Entry' and voucher_no=%s - order by account asc""", (payment_entry.name), as_dict=1) + order by account asc""", + (payment_entry.name), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) @@ -1168,23 +1439,24 @@ class TestPurchaseInvoice(unittest.TestCase): # Create Purchase Invoice against Purchase Order purchase_invoice = get_mapped_purchase_invoice(po.name) purchase_invoice.allocate_advances_automatically = 1 - purchase_invoice.items[0].item_code = '_Test Non Stock Item' - purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC' + purchase_invoice.items[0].item_code = "_Test Non Stock Item" + purchase_invoice.items[0].expense_account = "_Test Account Cost for Goods Sold - _TC" purchase_invoice.save() purchase_invoice.submit() # Check GLE for Purchase Invoice # Zero net effect on final TDS Payable on invoice - expected_gle = [ - ['_Test Account Cost for Goods Sold - _TC', 30000], - ['Creditors - _TC', -30000] - ] + expected_gle = [["_Test Account Cost for Goods Sold - _TC", 30000], ["Creditors - _TC", -30000]] - gl_entries = frappe.db.sql("""select account, sum(debit - credit) as amount + gl_entries = frappe.db.sql( + """select account, sum(debit - credit) as amount from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s group by account - order by account asc""", (purchase_invoice.name), as_dict=1) + order by account asc""", + (purchase_invoice.name), + as_dict=1, + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_gle[i][0], gle.account) @@ -1198,11 +1470,126 @@ class TestPurchaseInvoice(unittest.TestCase): payment_entry.load_from_db() self.assertEqual(payment_entry.taxes[0].allocated_amount, 0) + def test_provisional_accounting_entry(self): + create_item("_Test Non Stock Item", is_stock_item=0) + + provisional_account = create_account( + account_name="Provision Account", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) + + company = frappe.get_doc("Company", "_Test Company") + company.enable_provisional_accounting_for_non_stock_items = 1 + company.default_provisional_account = provisional_account + company.save() + + pr = make_purchase_receipt( + item_code="_Test Non Stock Item", posting_date=add_days(nowdate(), -2) + ) + + pi = create_purchase_invoice_from_receipt(pr.name) + pi.set_posting_time = 1 + pi.posting_date = add_days(pr.posting_date, -1) + pi.items[0].expense_account = "Cost of Goods Sold - _TC" + pi.save() + pi.submit() + + self.assertEquals(pr.items[0].provisional_expense_account, "Provision Account - _TC") + + # Check GLE for Purchase Invoice + expected_gle = [ + ["Cost of Goods Sold - _TC", 250, 0, add_days(pr.posting_date, -1)], + ["Creditors - _TC", 0, 250, add_days(pr.posting_date, -1)], + ] + + check_gl_entries(self, pi.name, expected_gle, pi.posting_date) + + expected_gle_for_purchase_receipt = [ + ["Provision Account - _TC", 250, 0, pr.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 0, 250, pr.posting_date], + ["Provision Account - _TC", 0, 250, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date], + ] + + check_gl_entries(self, pr.name, expected_gle_for_purchase_receipt, pr.posting_date) + + # Cancel purchase invoice to check reverse provisional entry cancellation + pi.cancel() + + expected_gle_for_purchase_receipt_post_pi_cancel = [ + ["Provision Account - _TC", 0, 250, pi.posting_date], + ["_Test Account Cost for Goods Sold - _TC", 250, 0, pi.posting_date], + ] + + check_gl_entries( + self, pr.name, expected_gle_for_purchase_receipt_post_pi_cancel, pr.posting_date + ) + + company.enable_provisional_accounting_for_non_stock_items = 0 + company.save() + + def test_item_less_defaults(self): + + pi = frappe.new_doc("Purchase Invoice") + pi.supplier = "_Test Supplier" + pi.company = "_Test Company" + pi.append( + "items", + { + "item_name": "Opening item", + "qty": 1, + "uom": "Tonne", + "stock_uom": "Kg", + "rate": 1000, + "expense_account": "Stock Received But Not Billed - _TC", + }, + ) + + pi.save() + self.assertEqual(pi.items[0].conversion_factor, 1000) + + def test_batch_expiry_for_purchase_invoice(self): + from erpnext.controllers.sales_and_purchase_return import make_return_doc + + item = self.make_item( + "_Test Batch Item For Return Check", + { + "is_purchase_item": 1, + "is_stock_item": 1, + "has_batch_no": 1, + "create_new_batch": 1, + "batch_number_series": "TBIRC.#####", + }, + ) + + pi = make_purchase_invoice( + qty=1, + item_code=item.name, + update_stock=True, + ) + + pi.load_from_db() + batch_no = pi.items[0].batch_no + self.assertTrue(batch_no) + + frappe.db.set_value("Batch", batch_no, "expiry_date", add_days(nowdate(), -1)) + + return_pi = make_return_doc(pi.doctype, pi.name) + return_pi.save().submit() + + self.assertTrue(return_pi.docstatus == 1) + + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): - gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + gl_entries = frappe.db.sql( + """select account, debit, credit, posting_date from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s and posting_date >= %s - order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1) + order by posting_date asc, account asc""", + (voucher_no, posting_date), + as_dict=1, + ) for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) @@ -1210,45 +1597,55 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + def update_tax_witholding_category(company, account): from erpnext.accounts.utils import get_fiscal_year - fiscal_year = get_fiscal_year(fiscal_year='_Test Fiscal Year 2021') + fiscal_year = get_fiscal_year(date=nowdate()) - if not frappe.db.get_value('Tax Withholding Rate', - {'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]), - 'to_date': ('<=', fiscal_year[2])}): - tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual') - tds_category.set('rates', []) + if not frappe.db.get_value( + "Tax Withholding Rate", + { + "parent": "TDS - 194 - Dividends - Individual", + "from_date": (">=", fiscal_year[1]), + "to_date": ("<=", fiscal_year[2]), + }, + ): + tds_category = frappe.get_doc("Tax Withholding Category", "TDS - 194 - Dividends - Individual") + tds_category.set("rates", []) - tds_category.append('rates', { - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 2500, - 'cumulative_threshold': 0 - }) + tds_category.append( + "rates", + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 2500, + "cumulative_threshold": 0, + }, + ) tds_category.save() - if not frappe.db.get_value('Tax Withholding Account', - {'parent': 'TDS - 194 - Dividends - Individual', 'account': account}): - tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual') - tds_category.append('accounts', { - 'company': company, - 'account': account - }) + if not frappe.db.get_value( + "Tax Withholding Account", {"parent": "TDS - 194 - Dividends - Individual", "account": account} + ): + tds_category = frappe.get_doc("Tax Withholding Category", "TDS - 194 - Dividends - Individual") + tds_category.append("accounts", {"company": company, "account": account}) tds_category.save() + def unlink_payment_on_cancel_of_invoice(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.unlink_payment_on_cancellation_of_invoice = enable accounts_settings.save() + def enable_discount_accounting(enable=1): accounts_settings = frappe.get_doc("Accounts Settings") accounts_settings.enable_discount_accounting = enable accounts_settings.save() + def make_purchase_invoice(**args): pi = frappe.new_doc("Purchase Invoice") args = frappe._dict(args) @@ -1261,7 +1658,7 @@ def make_purchase_invoice(**args): pi.is_paid = 1 if args.cash_bank_account: - pi.cash_bank_account=args.cash_bank_account + pi.cash_bank_account = args.cash_bank_account pi.company = args.company or "_Test Company" pi.supplier = args.supplier or "_Test Supplier" @@ -1273,27 +1670,30 @@ def make_purchase_invoice(**args): pi.supplier_warehouse = args.supplier_warehouse or "_Test Warehouse 1 - _TC" pi.cost_center = args.parent_cost_center - pi.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 5, - "received_qty": args.received_qty or 0, - "rejected_qty": args.rejected_qty or 0, - "rate": args.rate or 50, - "price_list_rate": args.price_list_rate or 50, - "expense_account": args.expense_account or '_Test Account Cost for Goods Sold - _TC', - "discount_account": args.discount_account or None, - "discount_amount": args.discount_amount or 0, - "conversion_factor": 1.0, - "serial_no": args.serial_no, - "stock_uom": args.uom or "_Test UOM", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "project": args.project, - "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "", - "asset_location": args.location or "", - "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0 - }) + pi.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 5, + "received_qty": args.received_qty or 0, + "rejected_qty": args.rejected_qty or 0, + "rate": args.rate or 50, + "price_list_rate": args.price_list_rate or 50, + "expense_account": args.expense_account or "_Test Account Cost for Goods Sold - _TC", + "discount_account": args.discount_account or None, + "discount_amount": args.discount_amount or 0, + "conversion_factor": 1.0, + "serial_no": args.serial_no, + "stock_uom": args.uom or "_Test UOM", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "project": args.project, + "rejected_warehouse": args.rejected_warehouse or "", + "rejected_serial_no": args.rejected_serial_no or "", + "asset_location": args.location or "", + "allow_zero_valuation_rate": args.get("allow_zero_valuation_rate") or 0, + }, + ) if args.get_taxes_and_charges: taxes = get_taxes() @@ -1306,6 +1706,7 @@ def make_purchase_invoice(**args): pi.submit() return pi + def make_purchase_invoice_against_cost_center(**args): pi = frappe.new_doc("Purchase Invoice") args = frappe._dict(args) @@ -1318,7 +1719,7 @@ def make_purchase_invoice_against_cost_center(**args): pi.is_paid = 1 if args.cash_bank_account: - pi.cash_bank_account=args.cash_bank_account + pi.cash_bank_account = args.cash_bank_account pi.company = args.company or "_Test Company" pi.cost_center = args.cost_center or "_Test Cost Center - _TC" @@ -1332,25 +1733,29 @@ def make_purchase_invoice_against_cost_center(**args): if args.supplier_warehouse: pi.supplier_warehouse = "_Test Warehouse 1 - _TC" - pi.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 5, - "received_qty": args.received_qty or 0, - "rejected_qty": args.rejected_qty or 0, - "rate": args.rate or 50, - "conversion_factor": 1.0, - "serial_no": args.serial_no, - "stock_uom": "_Test UOM", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "project": args.project, - "rejected_warehouse": args.rejected_warehouse or "", - "rejected_serial_no": args.rejected_serial_no or "" - }) + pi.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 5, + "received_qty": args.received_qty or 0, + "rejected_qty": args.rejected_qty or 0, + "rate": args.rate or 50, + "conversion_factor": 1.0, + "serial_no": args.serial_no, + "stock_uom": "_Test UOM", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "project": args.project, + "rejected_warehouse": args.rejected_warehouse or "", + "rejected_serial_no": args.rejected_serial_no or "", + }, + ) if not args.do_not_save: pi.insert() if not args.do_not_submit: pi.submit() return pi -test_records = frappe.get_test_records('Purchase Invoice') + +test_records = frappe.get_test_records("Purchase Invoice") diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py index f5eb404d0a4..70d29bfda25 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.py @@ -16,5 +16,5 @@ class PurchaseTaxesandChargesTemplate(Document): def autoname(self): if self.company and self.title: - abbr = frappe.get_cached_value('Company', self.company, 'abbr') - self.name = '{0} - {1}'.format(self.title, abbr) + abbr = frappe.get_cached_value("Company", self.company, "abbr") + self.name = "{0} - {1}".format(self.title, abbr) diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py index 95a7a1c889b..1f0ea211f20 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template_dashboard.py @@ -1,21 +1,17 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'taxes_and_charges', - 'non_standard_fieldnames': { - 'Tax Rule': 'purchase_tax_template', + "fieldname": "taxes_and_charges", + "non_standard_fieldnames": { + "Tax Rule": "purchase_tax_template", }, - 'transactions': [ + "transactions": [ { - 'label': _('Transactions'), - 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'] + "label": _("Transactions"), + "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"], }, - { - 'label': _('References'), - 'items': ['Supplier Quotation', 'Tax Rule'] - } - ] + {"label": _("References"), "items": ["Supplier Quotation", "Tax Rule"]}, + ], } diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py index b5b4a67d759..1d02f055048 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/test_purchase_taxes_and_charges_template.py @@ -5,5 +5,6 @@ import unittest # test_records = frappe.get_test_records('Purchase Taxes and Charges Template') + class TestPurchaseTaxesandChargesTemplate(unittest.TestCase): pass diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 8a6d3cd5935..c765e4f07ff 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -34,7 +34,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte var me = this; this._super(); - this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry']; + this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', + 'POS Closing Entry', 'Journal Entry', 'Payment Entry']; + if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) { // show debit_to in print format this.frm.set_df_property("debit_to", "print_hide", 0); @@ -51,7 +53,6 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte me.frm.refresh_fields(); } erpnext.queries.setup_warehouse_query(this.frm); - erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype); }, refresh: function(doc, dt, dn) { @@ -281,6 +282,9 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte } var me = this; if(this.frm.updating_party_details) return; + + if (this.frm.doc.__onload && this.frm.doc.__onload.load_after_mapping) return; + erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details", { posting_date: this.frm.doc.posting_date, @@ -470,9 +474,21 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte let row = frappe.get_doc(d.doctype, d.name) set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail) }); - frm.trigger("calculate_timesheet_totals"); + this.frm.trigger("calculate_timesheet_totals"); } + }, + + is_cash_or_non_trade_discount() { + this.frm.set_df_property("additional_discount_account", "hidden", 1 - this.frm.doc.is_cash_or_non_trade_discount); + this.frm.set_df_property("additional_discount_account", "reqd", this.frm.doc.is_cash_or_non_trade_discount); + + if (!this.frm.doc.is_cash_or_non_trade_discount) { + this.frm.set_value("additional_discount_account", ""); + } + + this.calculate_taxes_and_totals(); } + }); // for backward compatibility: combine new and previous states @@ -880,27 +896,44 @@ frappe.ui.form.on('Sales Invoice', { set_timesheet_data: function(frm, timesheets) { frm.clear_table("timesheets") - timesheets.forEach(timesheet => { + timesheets.forEach(async (timesheet) => { if (frm.doc.currency != timesheet.currency) { - frappe.call({ - method: "erpnext.setup.utils.get_exchange_rate", - args: { - from_currency: timesheet.currency, - to_currency: frm.doc.currency - }, - callback: function(r) { - if (r.message) { - exchange_rate = r.message; - frm.events.append_time_log(frm, timesheet, exchange_rate); - } - } - }); + const exchange_rate = await frm.events.get_exchange_rate( + frm, timesheet.currency, frm.doc.currency + ) + frm.events.append_time_log(frm, timesheet, exchange_rate) } else { frm.events.append_time_log(frm, timesheet, 1.0); } }); }, + async get_exchange_rate(frm, from_currency, to_currency) { + if ( + frm.exchange_rates + && frm.exchange_rates[from_currency] + && frm.exchange_rates[from_currency][to_currency] + ) { + return frm.exchange_rates[from_currency][to_currency]; + } + + return frappe.call({ + method: "erpnext.setup.utils.get_exchange_rate", + args: { + from_currency, + to_currency + }, + callback: function(r) { + if (r.message) { + // cache exchange rates + frm.exchange_rates = frm.exchange_rates || {}; + frm.exchange_rates[from_currency] = frm.exchange_rates[from_currency] || {}; + frm.exchange_rates[from_currency][to_currency] = r.message; + } + } + }); + }, + append_time_log: function(frm, time_log, exchange_rate) { const row = frm.add_child("timesheets"); row.activity_type = time_log.activity_type; @@ -911,7 +944,7 @@ frappe.ui.form.on('Sales Invoice', { row.billing_hours = time_log.billing_hours; row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate); row.timesheet_detail = time_log.name; - row.project_name = time_log.project_name; + row.project_name = time_log.project_name; frm.refresh_field("timesheets"); frm.trigger("calculate_timesheet_totals"); diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json index 545abf77e6b..cd3434a7756 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json @@ -2,7 +2,7 @@ "actions": [], "allow_import": 1, "autoname": "naming_series:", - "creation": "2013-05-24 19:29:05", + "creation": "2022-01-25 10:29:57.771398", "doctype": "DocType", "engine": "InnoDB", "field_order": [ @@ -106,6 +106,7 @@ "loyalty_redemption_cost_center", "section_break_49", "apply_discount_on", + "is_cash_or_non_trade_discount", "base_discount_amount", "additional_discount_account", "column_break_51", @@ -413,7 +414,7 @@ }, { "default": "0", - "depends_on": "eval: doc.is_return && doc.return_against", + "depends_on": "eval: doc.is_return", "fieldname": "update_billed_amount_in_sales_order", "fieldtype": "Check", "hide_days": 1, @@ -650,8 +651,6 @@ "hide_days": 1, "hide_seconds": 1, "label": "Ignore Pricing Rule", - "no_copy": 1, - "permlevel": 1, "print_hide": 1 }, { @@ -1974,9 +1973,10 @@ }, { "default": "0", + "description": "Issue a debit note with 0 qty against an existing Sales Invoice", "fieldname": "is_debit_note", "fieldtype": "Check", - "label": "Is Debit Note" + "label": "Is Rate Adjustment Entry (Debit Note)" }, { "default": "0", @@ -1988,7 +1988,7 @@ { "fieldname": "additional_discount_account", "fieldtype": "Link", - "label": "Additional Discount Account", + "label": "Discount Account", "options": "Account" }, { @@ -2026,6 +2026,13 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 + }, + { + "default": "0", + "depends_on": "eval: doc.apply_discount_on == \"Grand Total\"", + "fieldname": "is_cash_or_non_trade_discount", + "fieldtype": "Check", + "label": "Is Cash or Non Trade Discount" } ], "icon": "fa fa-file-text", @@ -2038,7 +2045,7 @@ "link_fieldname": "consolidated_invoice" } ], - "modified": "2021-10-21 20:19:38.667508", + "modified": "2022-09-16 17:44:22.227332", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice", @@ -2089,8 +2096,9 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "title", "track_changes": 1, "track_seen": 1 -} +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 176d47897d6..1badb809341 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -38,35 +38,41 @@ from erpnext.assets.doctype.asset.depreciation import ( get_gl_entries_on_asset_regain, make_depreciation_entry, ) +from erpnext.controllers.accounts_controller import validate_account_head from erpnext.controllers.selling_controller import SellingController from erpnext.healthcare.utils import manage_invoice_submit_cancel from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data from erpnext.setup.doctype.company.company import update_company_current_month_sales from erpnext.stock.doctype.batch.batch import set_batch_nos from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so -from erpnext.stock.doctype.serial_no.serial_no import get_delivery_note_serial_no, get_serial_nos +from erpnext.stock.doctype.serial_no.serial_no import ( + get_delivery_note_serial_no, + get_serial_nos, + update_serial_nos_after_submit, +) + +form_grid_templates = {"items": "templates/form_grid/item_grid.html"} -form_grid_templates = { - "items": "templates/form_grid/item_grid.html" -} class SalesInvoice(SellingController): def __init__(self, *args, **kwargs): super(SalesInvoice, self).__init__(*args, **kwargs) - self.status_updater = [{ - 'source_dt': 'Sales Invoice Item', - 'target_field': 'billed_amt', - 'target_ref_field': 'amount', - 'target_dt': 'Sales Order Item', - 'join_field': 'so_detail', - 'target_parent_dt': 'Sales Order', - 'target_parent_field': 'per_billed', - 'source_field': 'amount', - 'percent_join_field': 'sales_order', - 'status_field': 'billing_status', - 'keyword': 'Billed', - 'overflow_type': 'billing' - }] + self.status_updater = [ + { + "source_dt": "Sales Invoice Item", + "target_field": "billed_amt", + "target_ref_field": "amount", + "target_dt": "Sales Order Item", + "join_field": "so_detail", + "target_parent_dt": "Sales Order", + "target_parent_field": "per_billed", + "source_field": "amount", + "percent_join_field": "sales_order", + "status_field": "billing_status", + "keyword": "Billed", + "overflow_type": "billing", + } + ] def set_indicator(self): """Set indicator for portal""" @@ -109,7 +115,12 @@ class SalesInvoice(SellingController): self.validate_fixed_asset() self.set_income_account_for_fixed_assets() self.validate_item_cost_centers() - validate_inter_company_party(self.doctype, self.customer, self.company, self.inter_company_invoice_reference) + self.validate_income_account() + self.check_conversion_rate() + + validate_inter_company_party( + self.doctype, self.customer, self.company, self.inter_company_invoice_reference + ) if cint(self.is_pos): self.validate_pos() @@ -125,15 +136,21 @@ class SalesInvoice(SellingController): validate_service_stop_date(self) if not self.is_opening: - self.is_opening = 'No' + self.is_opening = "No" - if self._action != 'submit' and self.update_stock and not self.is_return: - set_batch_nos(self, 'warehouse', True) + if self._action != "submit" and self.update_stock and not self.is_return: + set_batch_nos(self, "warehouse", True) if self.redeem_loyalty_points: - lp = frappe.get_doc('Loyalty Program', self.loyalty_program) - self.loyalty_redemption_account = lp.expense_account if not self.loyalty_redemption_account else self.loyalty_redemption_account - self.loyalty_redemption_cost_center = lp.cost_center if not self.loyalty_redemption_cost_center else self.loyalty_redemption_cost_center + lp = frappe.get_doc("Loyalty Program", self.loyalty_program) + self.loyalty_redemption_account = ( + lp.expense_account if not self.loyalty_redemption_account else self.loyalty_redemption_account + ) + self.loyalty_redemption_cost_center = ( + lp.cost_center + if not self.loyalty_redemption_cost_center + else self.loyalty_redemption_cost_center + ) self.set_against_income_account() self.validate_c_form() @@ -150,11 +167,16 @@ class SalesInvoice(SellingController): if self.is_pos and not self.is_return: self.verify_payment_amount_is_positive() - #validate amount in mode of payments for returned invoices for pos must be negative + # validate amount in mode of payments for returned invoices for pos must be negative if self.is_pos and self.is_return: self.verify_payment_amount_is_negative() - if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated: + if ( + self.redeem_loyalty_points + and self.loyalty_program + and self.loyalty_points + and not self.is_consolidated + ): validate_loyalty_points(self, self.loyalty_points) self.reset_default_field_value("set_warehouse", "items", "warehouse") @@ -167,14 +189,28 @@ class SalesInvoice(SellingController): if self.update_stock: frappe.throw(_("'Update Stock' cannot be checked for fixed asset sale")) - elif asset.status in ("Scrapped", "Cancelled") or (asset.status == "Sold" and not self.is_return): - frappe.throw(_("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format(d.idx, d.asset, asset.status)) + elif asset.status in ("Scrapped", "Cancelled") or ( + asset.status == "Sold" and not self.is_return + ): + frappe.throw( + _("Row #{0}: Asset {1} cannot be submitted, it is already {2}").format( + d.idx, d.asset, asset.status + ) + ) def validate_item_cost_centers(self): for item in self.items: cost_center_company = frappe.get_cached_value("Cost Center", item.cost_center, "company") if cost_center_company != self.company: - frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company))) + frappe.throw( + _("Row #{0}: Cost Center {1} does not belong to company {2}").format( + frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company) + ) + ) + + def validate_income_account(self): + for item in self.get("items"): + validate_account_head(item.idx, item.income_account, self.company, "Income") def set_tax_withholding(self): tax_withholding_details = get_party_tax_withholding_details(self) @@ -193,8 +229,11 @@ class SalesInvoice(SellingController): if not accounts or tax_withholding_account not in accounts: self.append("taxes", tax_withholding_details) - to_remove = [d for d in self.taxes - if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account] + to_remove = [ + d + for d in self.taxes + if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account + ] for d in to_remove: self.remove(d) @@ -209,8 +248,9 @@ class SalesInvoice(SellingController): self.validate_pos_paid_amount() if not self.auto_repeat: - frappe.get_doc('Authorization Control').validate_approving_authority(self.doctype, - self.company, self.base_grand_total, self) + frappe.get_doc("Authorization Control").validate_approving_authority( + self.doctype, self.company, self.base_grand_total, self + ) self.check_prev_docstatus() @@ -227,6 +267,8 @@ class SalesInvoice(SellingController): # because updating reserved qty in bin depends upon updated delivered qty in SO if self.update_stock == 1: self.update_stock_ledger() + if self.is_return and self.update_stock: + update_serial_nos_after_submit(self, "items") # this sequence because outstanding may get -ve self.make_gl_entries() @@ -246,7 +288,9 @@ class SalesInvoice(SellingController): self.update_time_sheet(self.name) - if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": + if ( + frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" + ): update_company_current_month_sales(self.company) self.update_project() update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) @@ -254,7 +298,9 @@ class SalesInvoice(SellingController): # create the loyalty point ledger entry if the customer is enrolled in any loyalty program if not self.is_return and not self.is_consolidated and self.loyalty_program: self.make_loyalty_point_entry() - elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program: + elif ( + self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program + ): against_si_doc = frappe.get_doc("Sales Invoice", self.return_against) against_si_doc.delete_loyalty_point_entry() against_si_doc.make_loyalty_point_entry() @@ -262,7 +308,7 @@ class SalesInvoice(SellingController): self.apply_loyalty_points() # Healthcare Service Invoice. - domain_settings = frappe.get_doc('Domain Settings') + domain_settings = frappe.get_doc("Domain Settings") active_domains = [d.domain for d in domain_settings.active_domains] if "Healthcare" in active_domains: @@ -271,6 +317,9 @@ class SalesInvoice(SellingController): self.process_common_party_accounting() def validate_pos_return(self): + if self.is_consolidated: + # pos return is already validated in pos invoice + return if self.is_pos and self.is_return: total_amount_in_payments = 0 @@ -287,16 +336,16 @@ class SalesInvoice(SellingController): def check_if_consolidated_invoice(self): # since POS Invoice extends Sales Invoice, we explicitly check if doctype is Sales Invoice if self.doctype == "Sales Invoice" and self.is_consolidated: - invoice_or_credit_note = "consolidated_credit_note" if self.is_return else "consolidated_invoice" - pos_closing_entry = frappe.get_all( - "POS Invoice Merge Log", - filters={ invoice_or_credit_note: self.name }, - pluck="pos_closing_entry" + invoice_or_credit_note = ( + "consolidated_credit_note" if self.is_return else "consolidated_invoice" ) - if pos_closing_entry: + pos_closing_entry = frappe.get_all( + "POS Invoice Merge Log", filters={invoice_or_credit_note: self.name}, pluck="pos_closing_entry" + ) + if pos_closing_entry and pos_closing_entry[0]: msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format( frappe.bold("Consolidated Sales Invoice"), - get_link_to_form("POS Closing Entry", pos_closing_entry[0]) + get_link_to_form("POS Closing Entry", pos_closing_entry[0]), ) frappe.throw(msg, title=_("Not Allowed")) @@ -338,14 +387,18 @@ class SalesInvoice(SellingController): if self.update_stock == 1: self.repost_future_sle_and_gle() - frappe.db.set(self, 'status', 'Cancelled') + frappe.db.set(self, "status", "Cancelled") - if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": + if ( + frappe.db.get_single_value("Selling Settings", "sales_update_frequency") == "Each Transaction" + ): update_company_current_month_sales(self.company) self.update_project() if not self.is_return and not self.is_consolidated and self.loyalty_program: self.delete_loyalty_point_entry() - elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program: + elif ( + self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program + ): against_si_doc = frappe.get_doc("Sales Invoice", self.return_against) against_si_doc.delete_loyalty_point_entry() against_si_doc.make_loyalty_point_entry() @@ -353,56 +406,62 @@ class SalesInvoice(SellingController): unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference) # Healthcare Service Invoice. - domain_settings = frappe.get_doc('Domain Settings') + domain_settings = frappe.get_doc("Domain Settings") active_domains = [d.domain for d in domain_settings.active_domains] if "Healthcare" in active_domains: manage_invoice_submit_cancel(self, "on_cancel") self.unlink_sales_invoice_from_timesheets() - self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation') + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry", "Repost Item Valuation") def update_status_updater_args(self): if cint(self.update_stock): - self.status_updater.append({ - 'source_dt':'Sales Invoice Item', - 'target_dt':'Sales Order Item', - 'target_parent_dt':'Sales Order', - 'target_parent_field':'per_delivered', - 'target_field':'delivered_qty', - 'target_ref_field':'qty', - 'source_field':'qty', - 'join_field':'so_detail', - 'percent_join_field':'sales_order', - 'status_field':'delivery_status', - 'keyword':'Delivered', - 'second_source_dt': 'Delivery Note Item', - 'second_source_field': 'qty', - 'second_join_field': 'so_detail', - 'overflow_type': 'delivery', - 'extra_cond': """ and exists(select name from `tabSales Invoice` - where name=`tabSales Invoice Item`.parent and update_stock = 1)""" - }) + self.status_updater.append( + { + "source_dt": "Sales Invoice Item", + "target_dt": "Sales Order Item", + "target_parent_dt": "Sales Order", + "target_parent_field": "per_delivered", + "target_field": "delivered_qty", + "target_ref_field": "qty", + "source_field": "qty", + "join_field": "so_detail", + "percent_join_field": "sales_order", + "status_field": "delivery_status", + "keyword": "Delivered", + "second_source_dt": "Delivery Note Item", + "second_source_field": "qty", + "second_join_field": "so_detail", + "overflow_type": "delivery", + "extra_cond": """ and exists(select name from `tabSales Invoice` + where name=`tabSales Invoice Item`.parent and update_stock = 1)""", + } + ) if cint(self.is_return): - self.status_updater.append({ - 'source_dt': 'Sales Invoice Item', - 'target_dt': 'Sales Order Item', - 'join_field': 'so_detail', - 'target_field': 'returned_qty', - 'target_parent_dt': 'Sales Order', - 'source_field': '-1 * qty', - 'second_source_dt': 'Delivery Note Item', - 'second_source_field': '-1 * qty', - 'second_join_field': 'so_detail', - 'extra_cond': """ and exists (select name from `tabSales Invoice` where name=`tabSales Invoice Item`.parent and update_stock=1 and is_return=1)""" - }) + self.status_updater.append( + { + "source_dt": "Sales Invoice Item", + "target_dt": "Sales Order Item", + "join_field": "so_detail", + "target_field": "returned_qty", + "target_parent_dt": "Sales Order", + "source_field": "-1 * qty", + "second_source_dt": "Delivery Note Item", + "second_source_field": "-1 * qty", + "second_join_field": "so_detail", + "extra_cond": """ and exists (select name from `tabSales Invoice` where name=`tabSales Invoice Item`.parent and update_stock=1 and is_return=1)""", + } + ) def check_credit_limit(self): from erpnext.selling.doctype.customer.customer import check_credit_limit validate_against_credit_limit = False - bypass_credit_limit_check_at_sales_order = frappe.db.get_value("Customer Credit Limit", - filters={'parent': self.customer, 'parenttype': 'Customer', 'company': self.company}, - fieldname=["bypass_credit_limit_check"]) + bypass_credit_limit_check_at_sales_order = frappe.db.get_value( + "Customer Credit Limit", + filters={"parent": self.customer, "parenttype": "Customer", "company": self.company}, + fieldname=["bypass_credit_limit_check"], + ) if bypass_credit_limit_check_at_sales_order: validate_against_credit_limit = True @@ -416,7 +475,7 @@ class SalesInvoice(SellingController): def unlink_sales_invoice_from_timesheets(self): for row in self.timesheets: - timesheet = frappe.get_doc('Timesheet', row.time_sheet) + timesheet = frappe.get_doc("Timesheet", row.time_sheet) for time_log in timesheet.time_logs: if time_log.sales_invoice == self.name: time_log.sales_invoice = None @@ -432,15 +491,17 @@ class SalesInvoice(SellingController): 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.db.get_value( + "Account", self.debit_to, "account_currency", cache=True + ) if not self.due_date and self.customer: self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company) super(SalesInvoice, self).set_missing_values(for_validate) print_format = pos.get("print_format") if pos else None - if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')): - print_format = 'POS Invoice' + if not print_format and not cint(frappe.db.get_value("Print Format", "POS Invoice", "disabled")): + print_format = "POS Invoice" if pos: return { @@ -448,7 +509,7 @@ class SalesInvoice(SellingController): "allow_edit_rate": pos.get("allow_user_to_edit_rate"), "allow_edit_discount": pos.get("allow_user_to_edit_discount"), "campaign": pos.get("campaign"), - "allow_print_before_pay": pos.get("allow_print_before_pay") + "allow_print_before_pay": pos.get("allow_print_before_pay"), } def update_time_sheet(self, sales_invoice): @@ -464,9 +525,11 @@ class SalesInvoice(SellingController): def update_time_sheet_detail(self, timesheet, args, sales_invoice): for data in timesheet.time_logs: - if (self.project and args.timesheet_detail == data.name) or \ - (not self.project and not data.sales_invoice) or \ - (not sales_invoice and data.sales_invoice == self.name): + if ( + (self.project and args.timesheet_detail == data.name) + or (not self.project and not data.sales_invoice) + or (not sales_invoice and data.sales_invoice == self.name) + ): data.sales_invoice = sales_invoice def on_update(self): @@ -476,7 +539,7 @@ class SalesInvoice(SellingController): paid_amount = 0.0 base_paid_amount = 0.0 for data in self.payments: - data.base_amount = flt(data.amount*self.conversion_rate, self.precision("base_paid_amount")) + data.base_amount = flt(data.amount * self.conversion_rate, self.precision("base_paid_amount")) paid_amount += data.amount base_paid_amount += data.base_amount @@ -487,7 +550,7 @@ class SalesInvoice(SellingController): for data in self.timesheets: if data.time_sheet: status = frappe.db.get_value("Timesheet", data.time_sheet, "status") - if status not in ['Submitted', 'Payslip']: + if status not in ["Submitted", "Payslip"]: frappe.throw(_("Timesheet {0} is already completed or cancelled").format(data.time_sheet)) def set_pos_fields(self, for_validate=False): @@ -496,20 +559,23 @@ class SalesInvoice(SellingController): return if not self.account_for_change_amount: - self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account') + self.account_for_change_amount = frappe.get_cached_value( + "Company", self.company, "default_cash_account" + ) from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details + if not self.pos_profile and not self.flags.ignore_pos_profile: pos_profile = get_pos_profile(self.company) or {} if not pos_profile: return - self.pos_profile = pos_profile.get('name') + self.pos_profile = pos_profile.get("name") pos = {} if self.pos_profile: - pos = frappe.get_doc('POS Profile', self.pos_profile) + pos = frappe.get_doc("POS Profile", self.pos_profile) - if not self.get('payments') and not for_validate: + if not self.get("payments") and not for_validate: update_multi_mode_option(self, pos) if pos: @@ -522,35 +588,52 @@ class SalesInvoice(SellingController): if not for_validate: self.ignore_pricing_rule = pos.ignore_pricing_rule - if pos.get('account_for_change_amount'): - self.account_for_change_amount = pos.get('account_for_change_amount') + if pos.get("account_for_change_amount"): + self.account_for_change_amount = pos.get("account_for_change_amount") - for fieldname in ('currency', 'letter_head', 'tc_name', - 'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges', - 'write_off_cost_center', 'apply_discount_on', 'cost_center'): - if (not for_validate) or (for_validate and not self.get(fieldname)): - self.set(fieldname, pos.get(fieldname)) + for fieldname in ( + "currency", + "letter_head", + "tc_name", + "company", + "select_print_heading", + "write_off_account", + "taxes_and_charges", + "write_off_cost_center", + "apply_discount_on", + "cost_center", + ): + if (not for_validate) or (for_validate and not self.get(fieldname)): + self.set(fieldname, pos.get(fieldname)) if pos.get("company_address"): self.company_address = pos.get("company_address") if self.customer: - customer_price_list, customer_group = frappe.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) - customer_group_price_list = frappe.get_value("Customer Group", customer_group, 'default_price_list') - selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list') + customer_price_list, customer_group = frappe.get_value( + "Customer", self.customer, ["default_price_list", "customer_group"] + ) + customer_group_price_list = frappe.get_value( + "Customer Group", customer_group, "default_price_list" + ) + selling_price_list = ( + customer_price_list or customer_group_price_list or pos.get("selling_price_list") + ) else: - selling_price_list = pos.get('selling_price_list') + selling_price_list = pos.get("selling_price_list") if selling_price_list: - self.set('selling_price_list', selling_price_list) + self.set("selling_price_list", selling_price_list) if not for_validate: self.update_stock = cint(pos.get("update_stock")) # set pos values in items for item in self.get("items"): - if item.get('item_code'): - profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos, update_data=True) + if item.get("item_code"): + profile_details = get_pos_profile_item_details( + pos, frappe._dict(item.as_dict()), pos, update_data=True + ) for fname, val in iteritems(profile_details): if (not for_validate) or (for_validate and not item.get(fname)): item.set(fname, val) @@ -574,19 +657,29 @@ class SalesInvoice(SellingController): if not self.debit_to: self.raise_missing_debit_credit_account_error("Customer", self.customer) - account = frappe.get_cached_value("Account", self.debit_to, - ["account_type", "report_type", "account_currency"], as_dict=True) + account = frappe.get_cached_value( + "Account", self.debit_to, ["account_type", "report_type", "account_currency"], as_dict=True + ) if not account: frappe.throw(_("Debit To is required"), title=_("Account Missing")) if account.report_type != "Balance Sheet": - msg = _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " " - msg += _("You can change the parent account to a Balance Sheet account or select a different account.") + msg = ( + _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " " + ) + msg += _( + "You can change the parent account to a Balance Sheet account or select a different account." + ) frappe.throw(msg, title=_("Invalid Account")) if self.customer and account.account_type != "Receivable": - msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " " + msg = ( + _("Please ensure {} account {} is a Receivable account.").format( + frappe.bold("Debit To"), frappe.bold(self.debit_to) + ) + + " " + ) msg += _("Change the account type to Receivable or select a different account.") frappe.throw(msg, title=_("Invalid Account")) @@ -595,52 +688,60 @@ class SalesInvoice(SellingController): def clear_unallocated_mode_of_payments(self): self.set("payments", self.get("payments", {"amount": ["not in", [0, None, ""]]})) - frappe.db.sql("""delete from `tabSales Invoice Payment` where parent = %s - and amount = 0""", self.name) + frappe.db.sql( + """delete from `tabSales Invoice Payment` where parent = %s + and amount = 0""", + self.name, + ) def validate_with_previous_doc(self): - super(SalesInvoice, self).validate_with_previous_doc({ - "Sales Order": { - "ref_dn_field": "sales_order", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]] - }, - "Sales Order Item": { - "ref_dn_field": "so_detail", - "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - "Delivery Note": { - "ref_dn_field": "delivery_note", - "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]] - }, - "Delivery Note Item": { - "ref_dn_field": "dn_detail", - "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], - "is_child_table": True, - "allow_duplicate_prev_row_id": True - }, - }) + super(SalesInvoice, self).validate_with_previous_doc( + { + "Sales Order": { + "ref_dn_field": "sales_order", + "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + }, + "Sales Order Item": { + "ref_dn_field": "so_detail", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + "Delivery Note": { + "ref_dn_field": "delivery_note", + "compare_fields": [["customer", "="], ["company", "="], ["project", "="], ["currency", "="]], + }, + "Delivery Note Item": { + "ref_dn_field": "dn_detail", + "compare_fields": [["item_code", "="], ["uom", "="], ["conversion_factor", "="]], + "is_child_table": True, + "allow_duplicate_prev_row_id": True, + }, + } + ) - if cint(frappe.db.get_single_value('Selling Settings', 'maintain_same_sales_rate')) and not self.is_return: - self.validate_rate_with_reference_doc([ - ["Sales Order", "sales_order", "so_detail"], - ["Delivery Note", "delivery_note", "dn_detail"] - ]) + if ( + cint(frappe.db.get_single_value("Selling Settings", "maintain_same_sales_rate")) + and not self.is_return + ): + self.validate_rate_with_reference_doc( + [["Sales Order", "sales_order", "so_detail"], ["Delivery Note", "delivery_note", "dn_detail"]] + ) def set_against_income_account(self): """Set against account for debit to account""" against_acc = [] - for d in self.get('items'): + for d in self.get("items"): if d.income_account and d.income_account not in against_acc: against_acc.append(d.income_account) - self.against_income_account = ','.join(against_acc) + self.against_income_account = ",".join(against_acc) def add_remarks(self): if not self.remarks: if self.po_no and self.po_date: - self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no, - formatdate(self.po_date)) + self.remarks = _("Against Customer Order {0} dated {1}").format( + self.po_no, formatdate(self.po_date) + ) else: self.remarks = _("No Remarks") @@ -656,36 +757,41 @@ class SalesInvoice(SellingController): if self.is_return: return - prev_doc_field_map = {'Sales Order': ['so_required', 'is_pos'],'Delivery Note': ['dn_required', 'update_stock']} + prev_doc_field_map = { + "Sales Order": ["so_required", "is_pos"], + "Delivery Note": ["dn_required", "update_stock"], + } for key, value in iteritems(prev_doc_field_map): - if frappe.db.get_single_value('Selling Settings', value[0]) == 'Yes': + if frappe.db.get_single_value("Selling Settings", value[0]) == "Yes": - if frappe.get_value('Customer', self.customer, value[0]): + if frappe.get_value("Customer", self.customer, value[0]): continue - for d in self.get('items'): - if (d.item_code and not d.get(key.lower().replace(' ', '_')) and not self.get(value[1])): + for d in self.get("items"): + if d.item_code and not d.get(key.lower().replace(" ", "_")) and not self.get(value[1]): msgprint(_("{0} is mandatory for Item {1}").format(key, d.item_code), raise_exception=1) - def validate_proj_cust(self): """check for does customer belong to same project as entered..""" if self.project and self.customer: - res = frappe.db.sql("""select name from `tabProject` + res = frappe.db.sql( + """select name from `tabProject` where name = %s and (customer = %s or customer is null or customer = '')""", - (self.project, self.customer)) + (self.project, self.customer), + ) if not res: - throw(_("Customer {0} does not belong to project {1}").format(self.customer,self.project)) + throw(_("Customer {0} does not belong to project {1}").format(self.customer, self.project)) def validate_pos(self): if self.is_return: invoice_total = self.rounded_total or self.grand_total - if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > \ - 1.0/(10.0**(self.precision("grand_total") + 1.0)): - frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total")) + if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > 1.0 / ( + 10.0 ** (self.precision("grand_total") + 1.0) + ): + frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total")) def validate_item_code(self): - for d in self.get('items'): + for d in self.get("items"): if not d.item_code and self.is_opening == "No": msgprint(_("Item Code required at Row No {0}").format(d.idx), raise_exception=True) @@ -693,17 +799,24 @@ class SalesInvoice(SellingController): super(SalesInvoice, self).validate_warehouse() for d in self.get_item_list(): - if not d.warehouse and d.item_code and frappe.get_cached_value("Item", d.item_code, "is_stock_item"): + if ( + not d.warehouse + and d.item_code + and frappe.get_cached_value("Item", d.item_code, "is_stock_item") + ): frappe.throw(_("Warehouse required for stock Item {0}").format(d.item_code)) def validate_delivery_note(self): for d in self.get("items"): if d.delivery_note: - msgprint(_("Stock cannot be updated against Delivery Note {0}").format(d.delivery_note), raise_exception=1) + msgprint( + _("Stock cannot be updated against Delivery Note {0}").format(d.delivery_note), + raise_exception=1, + ) def validate_write_off_account(self): if flt(self.write_off_amount) and not self.write_off_account: - self.write_off_account = frappe.get_cached_value('Company', self.company, 'write_off_account') + self.write_off_account = frappe.get_cached_value("Company", self.company, "write_off_account") if flt(self.write_off_amount) and not self.write_off_account: msgprint(_("Please enter Write Off Account"), raise_exception=1) @@ -713,18 +826,23 @@ class SalesInvoice(SellingController): msgprint(_("Please enter Account for Change Amount"), raise_exception=1) def validate_c_form(self): - """ Blank C-form no if C-form applicable marked as 'No'""" - if self.amended_from and self.c_form_applicable == 'No' and self.c_form_no: - frappe.db.sql("""delete from `tabC-Form Invoice Detail` where invoice_no = %s - and parent = %s""", (self.amended_from, self.c_form_no)) + """Blank C-form no if C-form applicable marked as 'No'""" + if self.amended_from and self.c_form_applicable == "No" and self.c_form_no: + frappe.db.sql( + """delete from `tabC-Form Invoice Detail` where invoice_no = %s + and parent = %s""", + (self.amended_from, self.c_form_no), + ) - frappe.db.set(self, 'c_form_no', '') + frappe.db.set(self, "c_form_no", "") def validate_c_form_on_cancel(self): - """ Display message if C-Form no exists on cancellation of Sales Invoice""" - if self.c_form_applicable == 'Yes' and self.c_form_no: - msgprint(_("Please remove this Invoice {0} from C-Form {1}") - .format(self.name, self.c_form_no), raise_exception = 1) + """Display message if C-Form no exists on cancellation of Sales Invoice""" + if self.c_form_applicable == "Yes" and self.c_form_no: + msgprint( + _("Please remove this Invoice {0} from C-Form {1}").format(self.name, self.c_form_no), + raise_exception=1, + ) def validate_dropship_item(self): for item in self.items: @@ -733,27 +851,36 @@ class SalesInvoice(SellingController): frappe.throw(_("Could not update stock, invoice contains drop shipping item.")) def update_current_stock(self): - for d in self.get('items'): + for d in self.get("items"): if d.item_code and d.warehouse: - bin = frappe.db.sql("select actual_qty from `tabBin` where item_code = %s and warehouse = %s", (d.item_code, d.warehouse), as_dict = 1) - d.actual_qty = bin and flt(bin[0]['actual_qty']) or 0 + bin = frappe.db.sql( + "select actual_qty from `tabBin` where item_code = %s and warehouse = %s", + (d.item_code, d.warehouse), + as_dict=1, + ) + d.actual_qty = bin and flt(bin[0]["actual_qty"]) or 0 - for d in self.get('packed_items'): - bin = frappe.db.sql("select actual_qty, projected_qty from `tabBin` where item_code = %s and warehouse = %s", (d.item_code, d.warehouse), as_dict = 1) - d.actual_qty = bin and flt(bin[0]['actual_qty']) or 0 - d.projected_qty = bin and flt(bin[0]['projected_qty']) or 0 + for d in self.get("packed_items"): + bin = frappe.db.sql( + "select actual_qty, projected_qty from `tabBin` where item_code = %s and warehouse = %s", + (d.item_code, d.warehouse), + as_dict=1, + ) + d.actual_qty = bin and flt(bin[0]["actual_qty"]) or 0 + d.projected_qty = bin and flt(bin[0]["projected_qty"]) or 0 def update_packing_list(self): if cint(self.update_stock) == 1: from erpnext.stock.doctype.packed_item.packed_item import make_packing_list + make_packing_list(self) else: - self.set('packed_items', []) + self.set("packed_items", []) def set_billing_hours_and_amount(self): if not self.project: for timesheet in self.timesheets: - ts_doc = frappe.get_doc('Timesheet', timesheet.time_sheet) + ts_doc = frappe.get_doc("Timesheet", timesheet.time_sheet) if not timesheet.billing_hours and ts_doc.total_billable_hours: timesheet.billing_hours = ts_doc.total_billable_hours @@ -768,17 +895,20 @@ class SalesInvoice(SellingController): @frappe.whitelist() def add_timesheet_data(self): - self.set('timesheets', []) + self.set("timesheets", []) if self.project: for data in get_projectwise_timesheet_data(self.project): - self.append('timesheets', { - 'time_sheet': data.time_sheet, - 'billing_hours': data.billing_hours, - 'billing_amount': data.billing_amount, - 'timesheet_detail': data.name, - 'activity_type': data.activity_type, - 'description': data.description - }) + self.append( + "timesheets", + { + "time_sheet": data.time_sheet, + "billing_hours": data.billing_hours, + "billing_amount": data.billing_amount, + "timesheet_detail": data.name, + "activity_type": data.activity_type, + "description": data.description, + }, + ) self.calculate_billing_amount_for_timesheet() @@ -790,13 +920,19 @@ class SalesInvoice(SellingController): self.total_billing_hours = timesheet_sum("billing_hours") def get_warehouse(self): - user_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile` - where ifnull(user,'') = %s and company = %s""", (frappe.session['user'], self.company)) + user_pos_profile = frappe.db.sql( + """select name, warehouse from `tabPOS Profile` + where ifnull(user,'') = %s and company = %s""", + (frappe.session["user"], self.company), + ) warehouse = user_pos_profile[0][1] if user_pos_profile else None if not warehouse: - global_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile` - where (user is null or user = '') and company = %s""", self.company) + global_pos_profile = frappe.db.sql( + """select name, warehouse from `tabPOS Profile` + where (user is null or user = '') and company = %s""", + self.company, + ) if global_pos_profile: warehouse = global_pos_profile[0][1] @@ -810,14 +946,16 @@ class SalesInvoice(SellingController): for d in self.get("items"): if d.is_fixed_asset: if not disposal_account: - disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center(self.company) + disposal_account, depreciation_cost_center = get_disposal_account_and_cost_center( + self.company + ) d.income_account = disposal_account if not d.cost_center: d.cost_center = depreciation_cost_center def check_prev_docstatus(self): - for d in self.get('items'): + for d in self.get("items"): if d.sales_order and frappe.db.get_value("Sales Order", d.sales_order, "docstatus") != 1: frappe.throw(_("Sales Order {0} is not submitted").format(d.sales_order)) @@ -833,22 +971,35 @@ class SalesInvoice(SellingController): if gl_entries: # if POS and amount is written off, updating outstanding amt after posting all gl entries - update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or - cint(self.redeem_loyalty_points)) else "Yes" + update_outstanding = ( + "No" + if (cint(self.is_pos) or self.write_off_account or cint(self.redeem_loyalty_points)) + else "Yes" + ) if self.docstatus == 1: - make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost) + make_gl_entries( + gl_entries, + update_outstanding=update_outstanding, + merge_entries=False, + from_repost=from_repost, + ) elif self.docstatus == 2: make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) if update_outstanding == "No": from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt - update_outstanding_amt(self.debit_to, "Customer", self.customer, - self.doctype, self.return_against if cint(self.is_return) and self.return_against else self.name) - elif self.docstatus == 2 and cint(self.update_stock) \ - and cint(auto_accounting_for_stock): - make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) + update_outstanding_amt( + self.debit_to, + "Customer", + self.customer, + self.doctype, + self.return_against if cint(self.is_return) and self.return_against else self.name, + ) + + elif self.docstatus == 2 and cint(self.update_stock) and cint(auto_accounting_for_stock): + make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) def get_gl_entries(self, warehouse_account=None): from erpnext.accounts.general_ledger import merge_similar_entries @@ -878,27 +1029,40 @@ class SalesInvoice(SellingController): def make_customer_gl_entry(self, gl_entries): # Checked both rounding_adjustment and rounded_total # because rounded_total had value even before introcution of posting GLE based on rounded total - grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total - if grand_total and not self.is_internal_transfer(): - # Didnot use base_grand_total to book rounding loss gle - grand_total_in_company_currency = flt(grand_total * self.conversion_rate, - self.precision("grand_total")) + grand_total = ( + self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total + ) + base_grand_total = flt( + self.base_rounded_total + if (self.base_rounding_adjustment and self.base_rounded_total) + else self.base_grand_total, + self.precision("base_grand_total"), + ) + if grand_total and not self.is_internal_transfer(): + # Did not use base_grand_total to book rounding loss gle gl_entries.append( - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "due_date": self.due_date, - "against": self.against_income_account, - "debit": grand_total_in_company_currency, - "debit_in_account_currency": grand_total_in_company_currency \ - if self.party_account_currency==self.company_currency else grand_total, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "due_date": self.due_date, + "against": self.against_income_account, + "debit": base_grand_total, + "debit_in_account_currency": base_grand_total + if self.party_account_currency == self.company_currency + else grand_total, + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) def make_tax_gl_entries(self, gl_entries): @@ -908,29 +1072,39 @@ class SalesInvoice(SellingController): if flt(tax.base_tax_amount_after_discount_amount): account_currency = get_account_currency(tax.account_head) gl_entries.append( - self.get_gl_dict({ - "account": tax.account_head, - "against": self.customer, - "credit": flt(base_amount, - tax.precision("tax_amount_after_discount_amount")), - "credit_in_account_currency": (flt(base_amount, - tax.precision("base_tax_amount_after_discount_amount")) if account_currency==self.company_currency else - flt(amount, tax.precision("tax_amount_after_discount_amount"))), - "cost_center": tax.cost_center - }, account_currency, item=tax) + self.get_gl_dict( + { + "account": tax.account_head, + "against": self.customer, + "credit": flt(base_amount, tax.precision("tax_amount_after_discount_amount")), + "credit_in_account_currency": ( + flt(base_amount, tax.precision("base_tax_amount_after_discount_amount")) + if account_currency == self.company_currency + else flt(amount, tax.precision("tax_amount_after_discount_amount")) + ), + "cost_center": tax.cost_center, + }, + account_currency, + item=tax, + ) ) def make_internal_transfer_gl_entries(self, gl_entries): if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges): account_currency = get_account_currency(self.unrealized_profit_loss_account) gl_entries.append( - self.get_gl_dict({ - "account": self.unrealized_profit_loss_account, - "against": self.customer, - "debit": flt(self.total_taxes_and_charges), - "debit_in_account_currency": flt(self.base_total_taxes_and_charges), - "cost_center": self.cost_center - }, account_currency, item=self)) + self.get_gl_dict( + { + "account": self.unrealized_profit_loss_account, + "against": self.customer, + "debit": flt(self.total_taxes_and_charges), + "debit_in_account_currency": flt(self.base_total_taxes_and_charges), + "cost_center": self.cost_center, + }, + account_currency, + item=self, + ) + ) def make_item_gl_entries(self, gl_entries): # income account gl entries @@ -940,22 +1114,24 @@ class SalesInvoice(SellingController): asset = self.get_asset(item) if self.is_return: - fixed_asset_gl_entries = get_gl_entries_on_asset_regain(asset, - item.base_net_amount, item.finance_book) - asset.db_set("disposal_date", None) - if asset.calculate_depreciation: self.reverse_depreciation_entry_made_after_sale(asset) self.reset_depreciation_schedule(asset) - else: - fixed_asset_gl_entries = get_gl_entries_on_asset_disposal(asset, - item.base_net_amount, item.finance_book) - asset.db_set("disposal_date", self.posting_date) + fixed_asset_gl_entries = get_gl_entries_on_asset_regain( + asset, item.base_net_amount, item.finance_book + ) + asset.db_set("disposal_date", None) + else: if asset.calculate_depreciation: self.depreciate_asset(asset) + fixed_asset_gl_entries = get_gl_entries_on_asset_disposal( + asset, item.base_net_amount, item.finance_book + ) + asset.db_set("disposal_date", self.posting_date) + for gle in fixed_asset_gl_entries: gle["against"] = self.customer gl_entries.append(self.get_gl_dict(gle, item=item)) @@ -965,47 +1141,57 @@ class SalesInvoice(SellingController): else: # Do not book income for transfer within same company if not self.is_internal_transfer(): - income_account = (item.income_account - if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account) + income_account = ( + item.income_account + if (not item.enable_deferred_revenue or self.is_return) + else item.deferred_revenue_account + ) amount, base_amount = self.get_amount_and_base_amount(item, self.enable_discount_accounting) account_currency = get_account_currency(income_account) gl_entries.append( - self.get_gl_dict({ - "account": income_account, - "against": self.customer, - "credit": flt(base_amount, item.precision("base_net_amount")), - "credit_in_account_currency": (flt(base_amount, item.precision("base_net_amount")) - if account_currency==self.company_currency - else flt(amount, item.precision("net_amount"))), - "cost_center": item.cost_center, - "project": item.project or self.project - }, account_currency, item=item) + self.get_gl_dict( + { + "account": income_account, + "against": self.customer, + "credit": flt(base_amount, item.precision("base_net_amount")), + "credit_in_account_currency": ( + flt(base_amount, item.precision("base_net_amount")) + if account_currency == self.company_currency + else flt(amount, item.precision("net_amount")) + ), + "cost_center": item.cost_center, + "project": item.project or self.project, + }, + account_currency, + item=item, + ) ) # expense account gl entries - if cint(self.update_stock) and \ - erpnext.is_perpetual_inventory_enabled(self.company): + if cint(self.update_stock) and erpnext.is_perpetual_inventory_enabled(self.company): gl_entries += super(SalesInvoice, self).get_gl_entries() def get_asset(self, item): - if item.get('asset'): + if item.get("asset"): asset = frappe.get_doc("Asset", item.asset) else: - frappe.throw(_( - "Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name), - title=_("Missing Asset") + frappe.throw( + _("Row #{0}: You must select an Asset for Item {1}.").format(item.idx, item.item_name), + title=_("Missing Asset"), ) self.check_finance_books(item, asset) return asset def check_finance_books(self, item, asset): - if (len(asset.finance_books) > 1 and not item.finance_book - and asset.finance_books[0].finance_book): - frappe.throw(_("Select finance book for the item {0} at row {1}") - .format(item.item_code, item.idx)) + if ( + len(asset.finance_books) > 1 and not item.finance_book and asset.finance_books[0].finance_book + ): + frappe.throw( + _("Select finance book for the item {0} at row {1}").format(item.item_code, item.idx) + ) def depreciate_asset(self, asset): asset.flags.ignore_validate_update_after_submit = True @@ -1013,6 +1199,7 @@ class SalesInvoice(SellingController): asset.save() make_depreciation_entry(asset.name, self.posting_date) + asset.load_from_db() def reset_depreciation_schedule(self, asset): asset.flags.ignore_validate_update_after_submit = True @@ -1022,17 +1209,16 @@ class SalesInvoice(SellingController): self.modify_depreciation_schedule_for_asset_repairs(asset) asset.save() + asset.load_from_db() def modify_depreciation_schedule_for_asset_repairs(self, asset): asset_repairs = frappe.get_all( - 'Asset Repair', - filters = {'asset': asset.name}, - fields = ['name', 'increase_in_asset_life'] + "Asset Repair", filters={"asset": asset.name}, fields=["name", "increase_in_asset_life"] ) for repair in asset_repairs: if repair.increase_in_asset_life: - asset_repair = frappe.get_doc('Asset Repair', repair.name) + asset_repair = frappe.get_doc("Asset Repair", repair.name) asset_repair.modify_depreciation_schedule() asset.prepare_depreciation_data() @@ -1042,8 +1228,8 @@ class SalesInvoice(SellingController): posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice() row = -1 - finance_book = asset.get('schedules')[0].get('finance_book') - for schedule in asset.get('schedules'): + finance_book = asset.get("schedules")[0].get("finance_book") + for schedule in asset.get("schedules"): if schedule.finance_book != finance_book: row = 0 finance_book = schedule.finance_book @@ -1051,8 +1237,9 @@ class SalesInvoice(SellingController): row += 1 if schedule.schedule_date == posting_date_of_original_invoice: - if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice) \ - or self.sale_happens_in_the_future(posting_date_of_original_invoice): + if not self.sale_was_made_on_original_schedule_date( + asset, schedule, row, posting_date_of_original_invoice + ) or self.sale_happens_in_the_future(posting_date_of_original_invoice): reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry) reverse_journal_entry.posting_date = nowdate() @@ -1067,14 +1254,17 @@ class SalesInvoice(SellingController): asset.save() def get_posting_date_of_sales_invoice(self): - return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date') + return frappe.db.get_value("Sales Invoice", self.return_against, "posting_date") # if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone - def sale_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_invoice): - for finance_book in asset.get('finance_books'): + def sale_was_made_on_original_schedule_date( + self, asset, schedule, row, posting_date_of_original_invoice + ): + for finance_book in asset.get("finance_books"): if schedule.finance_book == finance_book.finance_book: - orginal_schedule_date = add_months(finance_book.depreciation_start_date, - row * cint(finance_book.frequency_of_depreciation)) + orginal_schedule_date = add_months( + finance_book.depreciation_start_date, row * cint(finance_book.frequency_of_depreciation) + ) if orginal_schedule_date == posting_date_of_original_invoice: return True @@ -1095,7 +1285,9 @@ class SalesInvoice(SellingController): @property def enable_discount_accounting(self): if not hasattr(self, "_enable_discount_accounting"): - self._enable_discount_accounting = cint(frappe.db.get_single_value('Accounts Settings', 'enable_discount_accounting')) + self._enable_discount_accounting = cint( + frappe.db.get_single_value("Accounts Settings", "enable_discount_accounting") + ) return self._enable_discount_accounting @@ -1103,36 +1295,46 @@ class SalesInvoice(SellingController): if self.is_return: asset.set_status() else: - asset.set_status("Sold" if self.docstatus==1 else None) + asset.set_status("Sold" if self.docstatus == 1 else None) def make_loyalty_point_redemption_gle(self, gl_entries): if cint(self.redeem_loyalty_points): gl_entries.append( - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "against": "Expense account - " + cstr(self.loyalty_redemption_account) + " for the Loyalty Program", - "credit": self.loyalty_amount, - "against_voucher": self.return_against if cint(self.is_return) else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "against": "Expense account - " + + cstr(self.loyalty_redemption_account) + + " for the Loyalty Program", + "credit": self.loyalty_amount, + "against_voucher": self.return_against if cint(self.is_return) else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + }, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.loyalty_redemption_account, - "cost_center": self.cost_center or self.loyalty_redemption_cost_center, - "against": self.customer, - "debit": self.loyalty_amount, - "remark": "Loyalty Points redeemed by the customer" - }, item=self) + self.get_gl_dict( + { + "account": self.loyalty_redemption_account, + "cost_center": self.cost_center or self.loyalty_redemption_cost_center, + "against": self.customer, + "debit": self.loyalty_amount, + "remark": "Loyalty Points redeemed by the customer", + }, + item=self, + ) ) def make_pos_gl_entries(self, gl_entries): if cint(self.is_pos): - skip_change_gl_entries = not cint(frappe.db.get_single_value('Accounts Settings', 'post_change_gl_entries')) + skip_change_gl_entries = not cint( + frappe.db.get_single_value("Accounts Settings", "post_change_gl_entries") + ) for payment_mode in self.payments: if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount: @@ -1141,32 +1343,42 @@ class SalesInvoice(SellingController): if payment_mode.amount: # POS, make payment entries gl_entries.append( - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "against": payment_mode.account, - "credit": payment_mode.base_amount, - "credit_in_account_currency": payment_mode.base_amount \ - if self.party_account_currency==self.company_currency \ + self.get_gl_dict( + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "against": payment_mode.account, + "credit": payment_mode.base_amount, + "credit_in_account_currency": payment_mode.base_amount + if self.party_account_currency == self.company_currency else payment_mode.amount, - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center - }, self.party_account_currency, item=self) + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + }, + self.party_account_currency, + item=self, + ) ) payment_mode_account_currency = get_account_currency(payment_mode.account) gl_entries.append( - self.get_gl_dict({ - "account": payment_mode.account, - "against": self.customer, - "debit": payment_mode.base_amount, - "debit_in_account_currency": payment_mode.base_amount \ - if payment_mode_account_currency==self.company_currency \ + self.get_gl_dict( + { + "account": payment_mode.account, + "against": self.customer, + "debit": payment_mode.base_amount, + "debit_in_account_currency": payment_mode.base_amount + if payment_mode_account_currency == self.company_currency else payment_mode.amount, - "cost_center": self.cost_center - }, payment_mode_account_currency, item=self) + "cost_center": self.cost_center, + }, + payment_mode_account_currency, + item=self, + ) ) if not skip_change_gl_entries: @@ -1176,28 +1388,38 @@ class SalesInvoice(SellingController): if self.change_amount: if self.account_for_change_amount: gl_entries.append( - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "against": self.account_for_change_amount, - "debit": flt(self.base_change_amount), - "debit_in_account_currency": flt(self.base_change_amount) \ - if self.party_account_currency==self.company_currency else flt(self.change_amount), - "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "against": self.account_for_change_amount, + "debit": flt(self.base_change_amount), + "debit_in_account_currency": flt(self.base_change_amount) + if self.party_account_currency == self.company_currency + else flt(self.change_amount), + "against_voucher": self.return_against + if cint(self.is_return) and self.return_against + else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.account_for_change_amount, - "against": self.customer, - "credit": self.base_change_amount, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.account_for_change_amount, + "against": self.customer, + "credit": self.base_change_amount, + "cost_center": self.cost_center, + }, + item=self, + ) ) else: frappe.throw(_("Select change amount account"), title="Mandatory Field") @@ -1206,61 +1428,86 @@ class SalesInvoice(SellingController): # write off entries, applicable if only pos if self.write_off_account and flt(self.write_off_amount, self.precision("write_off_amount")): write_off_account_currency = get_account_currency(self.write_off_account) - default_cost_center = frappe.get_cached_value('Company', self.company, 'cost_center') + default_cost_center = frappe.get_cached_value("Company", self.company, "cost_center") gl_entries.append( - self.get_gl_dict({ - "account": self.debit_to, - "party_type": "Customer", - "party": self.customer, - "against": self.write_off_account, - "credit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")), - "credit_in_account_currency": (flt(self.base_write_off_amount, - self.precision("base_write_off_amount")) if self.party_account_currency==self.company_currency - else flt(self.write_off_amount, self.precision("write_off_amount"))), - "against_voucher": self.return_against if cint(self.is_return) else self.name, - "against_voucher_type": self.doctype, - "cost_center": self.cost_center, - "project": self.project - }, self.party_account_currency, item=self) + self.get_gl_dict( + { + "account": self.debit_to, + "party_type": "Customer", + "party": self.customer, + "against": self.write_off_account, + "credit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")), + "credit_in_account_currency": ( + flt(self.base_write_off_amount, self.precision("base_write_off_amount")) + if self.party_account_currency == self.company_currency + else flt(self.write_off_amount, self.precision("write_off_amount")) + ), + "against_voucher": self.return_against if cint(self.is_return) else self.name, + "against_voucher_type": self.doctype, + "cost_center": self.cost_center, + "project": self.project, + }, + self.party_account_currency, + item=self, + ) ) gl_entries.append( - self.get_gl_dict({ - "account": self.write_off_account, - "against": self.customer, - "debit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")), - "debit_in_account_currency": (flt(self.base_write_off_amount, - self.precision("base_write_off_amount")) if write_off_account_currency==self.company_currency - else flt(self.write_off_amount, self.precision("write_off_amount"))), - "cost_center": self.cost_center or self.write_off_cost_center or default_cost_center - }, write_off_account_currency, item=self) + self.get_gl_dict( + { + "account": self.write_off_account, + "against": self.customer, + "debit": flt(self.base_write_off_amount, self.precision("base_write_off_amount")), + "debit_in_account_currency": ( + flt(self.base_write_off_amount, self.precision("base_write_off_amount")) + if write_off_account_currency == self.company_currency + else flt(self.write_off_amount, self.precision("write_off_amount")) + ), + "cost_center": self.cost_center or self.write_off_cost_center or default_cost_center, + }, + write_off_account_currency, + item=self, + ) ) def make_gle_for_rounding_adjustment(self, gl_entries): - if flt(self.rounding_adjustment, self.precision("rounding_adjustment")) and self.base_rounding_adjustment \ - and not self.is_internal_transfer(): - round_off_account, round_off_cost_center = \ - get_round_off_account_and_cost_center(self.company) + if ( + flt(self.rounding_adjustment, self.precision("rounding_adjustment")) + and self.base_rounding_adjustment + and not self.is_internal_transfer() + ): + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + self.company, "Sales Invoice", self.name + ) gl_entries.append( - self.get_gl_dict({ - "account": round_off_account, - "against": self.customer, - "credit_in_account_currency": flt(self.rounding_adjustment, - self.precision("rounding_adjustment")), - "credit": flt(self.base_rounding_adjustment, - self.precision("base_rounding_adjustment")), - "cost_center": self.cost_center or round_off_cost_center, - }, item=self)) + self.get_gl_dict( + { + "account": round_off_account, + "against": self.customer, + "credit_in_account_currency": flt( + self.rounding_adjustment, self.precision("rounding_adjustment") + ), + "credit": flt(self.base_rounding_adjustment, self.precision("base_rounding_adjustment")), + "cost_center": self.cost_center or round_off_cost_center, + }, + item=self, + ) + ) def update_billing_status_in_dn(self, update_modified=True): updated_delivery_notes = [] for d in self.get("items"): if d.dn_detail: - billed_amt = frappe.db.sql("""select sum(amount) from `tabSales Invoice Item` - where dn_detail=%s and docstatus=1""", d.dn_detail) + billed_amt = frappe.db.sql( + """select sum(amount) from `tabSales Invoice Item` + where dn_detail=%s and docstatus=1""", + d.dn_detail, + ) billed_amt = billed_amt and billed_amt[0][0] or 0 - frappe.db.set_value("Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified) + frappe.db.set_value( + "Delivery Note Item", d.dn_detail, "billed_amt", billed_amt, update_modified=update_modified + ) updated_delivery_notes.append(d.delivery_note) elif d.so_detail: updated_delivery_notes += update_billed_amount_based_on_so(d.so_detail, update_modified) @@ -1275,7 +1522,7 @@ class SalesInvoice(SellingController): self.due_date = None def update_serial_no(self, in_cancel=False): - """ update Sales Invoice refrence in Serial No """ + """update Sales Invoice refrence in Serial No""" invoice = None if (in_cancel or self.is_return) else self.name if in_cancel and self.is_return: invoice = self.return_against @@ -1285,26 +1532,25 @@ class SalesInvoice(SellingController): continue for serial_no in get_serial_nos(item.serial_no): - if serial_no and frappe.db.get_value('Serial No', serial_no, 'item_code') == item.item_code: - frappe.db.set_value('Serial No', serial_no, 'sales_invoice', invoice) + if serial_no and frappe.db.get_value("Serial No", serial_no, "item_code") == item.item_code: + frappe.db.set_value("Serial No", serial_no, "sales_invoice", invoice) def validate_serial_numbers(self): """ - validate serial number agains Delivery Note and Sales Invoice + validate serial number agains Delivery Note and Sales Invoice """ self.set_serial_no_against_delivery_note() self.validate_serial_against_delivery_note() def set_serial_no_against_delivery_note(self): for item in self.items: - if item.serial_no and item.delivery_note and \ - item.qty != len(get_serial_nos(item.serial_no)): + if item.serial_no and item.delivery_note and item.qty != len(get_serial_nos(item.serial_no)): item.serial_no = get_delivery_note_serial_no(item.item_code, item.qty, item.delivery_note) def validate_serial_against_delivery_note(self): """ - validate if the serial numbers in Sales Invoice Items are same as in - Delivery Note Item + validate if the serial numbers in Sales Invoice Items are same as in + Delivery Note Item """ for item in self.items: @@ -1323,14 +1569,18 @@ class SalesInvoice(SellingController): serial_no_msg = ", ".join(frappe.bold(d) for d in serial_no_diff) msg = _("Row #{0}: The following Serial Nos are not present in Delivery Note {1}:").format( - item.idx, dn_link) + item.idx, dn_link + ) msg += " " + serial_no_msg frappe.throw(msg=msg, title=_("Serial Nos Mismatch")) if item.serial_no and cint(item.qty) != len(si_serial_nos): - frappe.throw(_("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format( - item.idx, item.qty, item.item_code, len(si_serial_nos))) + frappe.throw( + _("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format( + item.idx, item.qty, item.item_code, len(si_serial_nos) + ) + ) def update_project(self): if self.project: @@ -1338,7 +1588,6 @@ class SalesInvoice(SellingController): project.update_billed_amount() project.db_update() - def verify_payment_amount_is_positive(self): for entry in self.payments: if entry.amount < 0: @@ -1354,63 +1603,90 @@ class SalesInvoice(SellingController): returned_amount = self.get_returned_amount() current_amount = flt(self.grand_total) - cint(self.loyalty_amount) eligible_amount = current_amount - returned_amount - lp_details = get_loyalty_program_details_with_points(self.customer, company=self.company, - current_transaction_amount=current_amount, loyalty_program=self.loyalty_program, - expiry_date=self.posting_date, include_expired_entry=True) - if lp_details and getdate(lp_details.from_date) <= getdate(self.posting_date) and \ - (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)): + lp_details = get_loyalty_program_details_with_points( + self.customer, + company=self.company, + current_transaction_amount=current_amount, + loyalty_program=self.loyalty_program, + expiry_date=self.posting_date, + include_expired_entry=True, + ) + if ( + lp_details + and getdate(lp_details.from_date) <= getdate(self.posting_date) + and (not lp_details.to_date or getdate(lp_details.to_date) >= getdate(self.posting_date)) + ): collection_factor = lp_details.collection_factor if lp_details.collection_factor else 1.0 - points_earned = cint(eligible_amount/collection_factor) + points_earned = cint(eligible_amount / collection_factor) - doc = frappe.get_doc({ - "doctype": "Loyalty Point Entry", - "company": self.company, - "loyalty_program": lp_details.loyalty_program, - "loyalty_program_tier": lp_details.tier_name, - "customer": self.customer, - "invoice_type": self.doctype, - "invoice": self.name, - "loyalty_points": points_earned, - "purchase_amount": eligible_amount, - "expiry_date": add_days(self.posting_date, lp_details.expiry_duration), - "posting_date": self.posting_date - }) + doc = frappe.get_doc( + { + "doctype": "Loyalty Point Entry", + "company": self.company, + "loyalty_program": lp_details.loyalty_program, + "loyalty_program_tier": lp_details.tier_name, + "customer": self.customer, + "invoice_type": self.doctype, + "invoice": self.name, + "loyalty_points": points_earned, + "purchase_amount": eligible_amount, + "expiry_date": add_days(self.posting_date, lp_details.expiry_duration), + "posting_date": self.posting_date, + } + ) doc.flags.ignore_permissions = 1 doc.save() self.set_loyalty_program_tier() # valdite the redemption and then delete the loyalty points earned on cancel of the invoice def delete_loyalty_point_entry(self): - lp_entry = frappe.db.sql("select name from `tabLoyalty Point Entry` where invoice=%s", - (self.name), as_dict=1) + lp_entry = frappe.db.sql( + "select name from `tabLoyalty Point Entry` where invoice=%s", (self.name), as_dict=1 + ) - if not lp_entry: return - against_lp_entry = frappe.db.sql('''select name, invoice from `tabLoyalty Point Entry` - where redeem_against=%s''', (lp_entry[0].name), as_dict=1) + if not lp_entry: + return + against_lp_entry = frappe.db.sql( + """select name, invoice from `tabLoyalty Point Entry` + where redeem_against=%s""", + (lp_entry[0].name), + as_dict=1, + ) if against_lp_entry: invoice_list = ", ".join([d.invoice for d in against_lp_entry]) frappe.throw( - _('''{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}''') - .format(self.doctype, self.doctype, invoice_list) + _( + """{} can't be cancelled since the Loyalty Points earned has been redeemed. First cancel the {} No {}""" + ).format(self.doctype, self.doctype, invoice_list) ) else: - frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name)) + frappe.db.sql("""delete from `tabLoyalty Point Entry` where invoice=%s""", (self.name)) # Set loyalty program self.set_loyalty_program_tier() def set_loyalty_program_tier(self): - lp_details = get_loyalty_program_details_with_points(self.customer, company=self.company, - loyalty_program=self.loyalty_program, include_expired_entry=True) + lp_details = get_loyalty_program_details_with_points( + self.customer, + company=self.company, + loyalty_program=self.loyalty_program, + include_expired_entry=True, + ) frappe.db.set_value("Customer", self.customer, "loyalty_program_tier", lp_details.tier_name) def get_returned_amount(self): - returned_amount = frappe.db.sql(""" - select sum(grand_total) - from `tabSales Invoice` - where docstatus=1 and is_return=1 and ifnull(return_against, '')=%s - """, self.name) - return abs(flt(returned_amount[0][0])) if returned_amount else 0 + from frappe.query_builder.functions import Coalesce, Sum + + doc = frappe.qb.DocType(self.doctype) + returned_amount = ( + frappe.qb.from_(doc) + .select(Sum(doc.grand_total)) + .where( + (doc.docstatus == 1) & (doc.is_return == 1) & (Coalesce(doc.return_against, "") == self.name) + ) + ).run() + + return abs(returned_amount[0][0]) if returned_amount[0][0] else 0 # redeem the loyalty points. def apply_loyalty_points(self): @@ -1418,7 +1694,10 @@ class SalesInvoice(SellingController): get_loyalty_point_entries, get_redemption_details, ) - loyalty_point_entries = get_loyalty_point_entries(self.customer, self.loyalty_program, self.company, self.posting_date) + + loyalty_point_entries = get_loyalty_point_entries( + self.customer, self.loyalty_program, self.company, self.posting_date + ) redemption_details = get_redemption_details(self.customer, self.loyalty_program, self.company) points_to_redeem = self.loyalty_points @@ -1432,24 +1711,26 @@ class SalesInvoice(SellingController): redeemed_points = points_to_redeem else: redeemed_points = available_points - doc = frappe.get_doc({ - "doctype": "Loyalty Point Entry", - "company": self.company, - "loyalty_program": self.loyalty_program, - "loyalty_program_tier": lp_entry.loyalty_program_tier, - "customer": self.customer, - "invoice_type": self.doctype, - "invoice": self.name, - "redeem_against": lp_entry.name, - "loyalty_points": -1*redeemed_points, - "purchase_amount": self.grand_total, - "expiry_date": lp_entry.expiry_date, - "posting_date": self.posting_date - }) + doc = frappe.get_doc( + { + "doctype": "Loyalty Point Entry", + "company": self.company, + "loyalty_program": self.loyalty_program, + "loyalty_program_tier": lp_entry.loyalty_program_tier, + "customer": self.customer, + "invoice_type": self.doctype, + "invoice": self.name, + "redeem_against": lp_entry.name, + "loyalty_points": -1 * redeemed_points, + "purchase_amount": self.grand_total, + "expiry_date": lp_entry.expiry_date, + "posting_date": self.posting_date, + } + ) doc.flags.ignore_permissions = 1 doc.save() points_to_redeem -= redeemed_points - if points_to_redeem < 1: # since points_to_redeem is integer + if points_to_redeem < 1: # since points_to_redeem is integer break # Healthcare @@ -1457,44 +1738,47 @@ class SalesInvoice(SellingController): def set_healthcare_services(self, checked_values): self.set("items", []) from erpnext.stock.get_item_details import get_item_details + for checked_item in checked_values: item_line = self.append("items", {}) - price_list, price_list_currency = frappe.db.get_values("Price List", {"selling": 1}, ['name', 'currency'])[0] + price_list, price_list_currency = frappe.db.get_values( + "Price List", {"selling": 1}, ["name", "currency"] + )[0] args = { - 'doctype': "Sales Invoice", - 'item_code': checked_item['item'], - 'company': self.company, - 'customer': frappe.db.get_value("Patient", self.patient, "customer"), - 'selling_price_list': price_list, - 'price_list_currency': price_list_currency, - 'plc_conversion_rate': 1.0, - 'conversion_rate': 1.0 + "doctype": "Sales Invoice", + "item_code": checked_item["item"], + "company": self.company, + "customer": frappe.db.get_value("Patient", self.patient, "customer"), + "selling_price_list": price_list, + "price_list_currency": price_list_currency, + "plc_conversion_rate": 1.0, + "conversion_rate": 1.0, } item_details = get_item_details(args) - item_line.item_code = checked_item['item'] + item_line.item_code = checked_item["item"] item_line.qty = 1 - if checked_item['qty']: - item_line.qty = checked_item['qty'] - if checked_item['rate']: - item_line.rate = checked_item['rate'] + if checked_item["qty"]: + item_line.qty = checked_item["qty"] + if checked_item["rate"]: + item_line.rate = checked_item["rate"] else: item_line.rate = item_details.price_list_rate item_line.amount = float(item_line.rate) * float(item_line.qty) - if checked_item['income_account']: - item_line.income_account = checked_item['income_account'] - if checked_item['dt']: - item_line.reference_dt = checked_item['dt'] - if checked_item['dn']: - item_line.reference_dn = checked_item['dn'] - if checked_item['description']: - item_line.description = checked_item['description'] + if checked_item["income_account"]: + item_line.income_account = checked_item["income_account"] + if checked_item["dt"]: + item_line.reference_dt = checked_item["dt"] + if checked_item["dn"]: + item_line.reference_dn = checked_item["dn"] + if checked_item["description"]: + item_line.description = checked_item["description"] - self.set_missing_values(for_validate = True) + self.set_missing_values(for_validate=True) def set_status(self, update=False, status=None, update_modified=True): if self.is_new(): - if self.get('amended_from'): - self.status = 'Draft' + if self.get("amended_from"): + self.status = "Draft" return outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount")) @@ -1505,7 +1789,7 @@ class SalesInvoice(SellingController): status = "Cancelled" elif self.docstatus == 1: if self.is_internal_transfer(): - self.status = 'Internal Transfer' + self.status = "Internal Transfer" elif is_overdue(self, total): self.status = "Overdue" elif 0 < outstanding_amount < total: @@ -1513,11 +1797,17 @@ class SalesInvoice(SellingController): elif outstanding_amount > 0 and getdate(self.due_date) >= getdate(): self.status = "Unpaid" # Check if outstanding amount is 0 due to credit note issued against invoice - elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): + elif ( + outstanding_amount <= 0 + and self.is_return == 0 + and frappe.db.get_value( + "Sales Invoice", {"is_return": 1, "return_against": self.name, "docstatus": 1} + ) + ): self.status = "Credit Note Issued" elif self.is_return == 1: self.status = "Return" - elif outstanding_amount<=0: + elif outstanding_amount <= 0: self.status = "Paid" else: self.status = "Submitted" @@ -1533,34 +1823,29 @@ class SalesInvoice(SellingController): self.status = "Draft" if update: - self.db_set('status', self.status, update_modified = update_modified) + self.db_set("status", self.status, update_modified=update_modified) def get_total_in_party_account_currency(doc): - total_fieldname = ( - "grand_total" - if doc.disable_rounded_total - else "rounded_total" - ) + total_fieldname = "grand_total" if doc.disable_rounded_total else "rounded_total" if doc.party_account_currency != doc.currency: total_fieldname = "base_" + total_fieldname return flt(doc.get(total_fieldname), doc.precision(total_fieldname)) + def is_overdue(doc, total): outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount")) if outstanding_amount <= 0: return today = getdate() - if doc.get('is_pos') or not doc.get('payment_schedule'): + if doc.get("is_pos") or not doc.get("payment_schedule"): return getdate(doc.due_date) < today # calculate payable amount till date payment_amount_field = ( - "base_payment_amount" - if doc.party_account_currency != doc.currency - else "payment_amount" + "base_payment_amount" if doc.party_account_currency != doc.currency else "payment_amount" ) payable_amount = sum( @@ -1575,7 +1860,8 @@ def is_overdue(doc, total): def get_discounting_status(sales_invoice): status = None - invoice_discounting_list = frappe.db.sql(""" + invoice_discounting_list = frappe.db.sql( + """ select status from `tabInvoice Discounting` id, `tabDiscounted Invoice` d where @@ -1583,7 +1869,9 @@ def get_discounting_status(sales_invoice): and d.sales_invoice=%s and id.docstatus=1 and status in ('Disbursed', 'Settled') - """, sales_invoice) + """, + sales_invoice, + ) for d in invoice_discounting_list: status = d[0] @@ -1592,6 +1880,7 @@ def get_discounting_status(sales_invoice): return status + def validate_inter_company_party(doctype, party, company, inter_company_reference): if not party: return @@ -1620,10 +1909,19 @@ def validate_inter_company_party(doctype, party, company, inter_company_referenc frappe.throw(_("Invalid Company for Inter Company Transaction.")) elif frappe.db.get_value(partytype, {"name": party, internal: 1}, "name") == party: - companies = frappe.get_all("Allowed To Transact With", fields=["company"], filters={"parenttype": partytype, "parent": party}) + companies = frappe.get_all( + "Allowed To Transact With", + fields=["company"], + filters={"parenttype": partytype, "parent": party}, + ) companies = [d.company for d in companies] if not company in companies: - frappe.throw(_("{0} not allowed to transact with {1}. Please change the Company.").format(partytype, company)) + frappe.throw( + _("{0} not allowed to transact with {1}. Please change the Company.").format( + partytype, company + ) + ) + def update_linked_doc(doctype, name, inter_company_reference): @@ -1633,8 +1931,8 @@ def update_linked_doc(doctype, name, inter_company_reference): ref_field = "inter_company_order_reference" if inter_company_reference: - frappe.db.set_value(doctype, inter_company_reference,\ - ref_field, name) + frappe.db.set_value(doctype, inter_company_reference, ref_field, name) + def unlink_inter_company_doc(doctype, name, inter_company_reference): @@ -1649,48 +1947,57 @@ def unlink_inter_company_doc(doctype, name, inter_company_reference): frappe.db.set_value(doctype, name, ref_field, "") frappe.db.set_value(ref_doc, inter_company_reference, ref_field, "") + def get_list_context(context=None): from erpnext.controllers.website_list_for_contact import get_list_context + list_context = get_list_context(context) - list_context.update({ - 'show_sidebar': True, - 'show_search': True, - 'no_breadcrumbs': True, - 'title': _('Invoices'), - }) + list_context.update( + { + "show_sidebar": True, + "show_search": True, + "no_breadcrumbs": True, + "title": _("Invoices"), + } + ) return list_context + @frappe.whitelist() def get_bank_cash_account(mode_of_payment, company): - account = frappe.db.get_value("Mode of Payment Account", - {"parent": mode_of_payment, "company": company}, "default_account") + account = frappe.db.get_value( + "Mode of Payment Account", {"parent": mode_of_payment, "company": company}, "default_account" + ) if not account: - frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") - .format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) - return { - "account": account - } + frappe.throw( + _("Please set default Cash or Bank account in Mode of Payment {0}").format( + get_link_to_form("Mode of Payment", mode_of_payment) + ), + title=_("Missing Account"), + ) + return {"account": account} + @frappe.whitelist() def make_maintenance_schedule(source_name, target_doc=None): - doclist = get_mapped_doc("Sales Invoice", source_name, { - "Sales Invoice": { - "doctype": "Maintenance Schedule", - "validation": { - "docstatus": ["=", 1] - } + doclist = get_mapped_doc( + "Sales Invoice", + source_name, + { + "Sales Invoice": {"doctype": "Maintenance Schedule", "validation": {"docstatus": ["=", 1]}}, + "Sales Invoice Item": { + "doctype": "Maintenance Schedule Item", + }, }, - "Sales Invoice Item": { - "doctype": "Maintenance Schedule Item", - }, - }, target_doc) + target_doc, + ) return doclist + @frappe.whitelist() def make_delivery_note(source_name, target_doc=None): def set_missing_values(source, target): - target.ignore_pricing_rule = 1 target.run_method("set_missing_values") target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") @@ -1702,82 +2009,104 @@ def make_delivery_note(source_name, target_doc=None): target_doc.base_amount = target_doc.qty * flt(source_doc.base_rate) target_doc.amount = target_doc.qty * flt(source_doc.rate) - doclist = get_mapped_doc("Sales Invoice", source_name, { - "Sales Invoice": { - "doctype": "Delivery Note", - "validation": { - "docstatus": ["=", 1] - } - }, - "Sales Invoice Item": { - "doctype": "Delivery Note Item", - "field_map": { - "name": "si_detail", - "parent": "against_sales_invoice", - "serial_no": "serial_no", - "sales_order": "against_sales_order", - "so_detail": "so_detail", - "cost_center": "cost_center" + doclist = get_mapped_doc( + "Sales Invoice", + source_name, + { + "Sales Invoice": {"doctype": "Delivery Note", "validation": {"docstatus": ["=", 1]}}, + "Sales Invoice Item": { + "doctype": "Delivery Note Item", + "field_map": { + "name": "si_detail", + "parent": "against_sales_invoice", + "serial_no": "serial_no", + "sales_order": "against_sales_order", + "so_detail": "so_detail", + "cost_center": "cost_center", + }, + "postprocess": update_item, + "condition": lambda doc: doc.delivered_by_supplier != 1, }, - "postprocess": update_item, - "condition": lambda doc: doc.delivered_by_supplier!=1 - }, - "Sales Taxes and Charges": { - "doctype": "Sales Taxes and Charges", - "add_if_empty": True - }, - "Sales Team": { - "doctype": "Sales Team", - "field_map": { - "incentives": "incentives" + "Sales Taxes and Charges": {"doctype": "Sales Taxes and Charges", "add_if_empty": True}, + "Sales Team": { + "doctype": "Sales Team", + "field_map": {"incentives": "incentives"}, + "add_if_empty": True, }, - "add_if_empty": True - } - }, target_doc, set_missing_values) + }, + target_doc, + set_missing_values, + ) + doclist.set_onload("ignore_price_list", True) return doclist + @frappe.whitelist() def make_sales_return(source_name, target_doc=None): from erpnext.controllers.sales_and_purchase_return import make_return_doc + return make_return_doc("Sales Invoice", source_name, target_doc) + def set_account_for_mode_of_payment(self): for data in self.payments: if not data.account: data.account = get_bank_cash_account(data.mode_of_payment, self.company).get("account") + def get_inter_company_details(doc, doctype): if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"]: - parties = frappe.db.get_all("Supplier", fields=["name"], filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company}) + parties = frappe.db.get_all( + "Supplier", + fields=["name"], + filters={"disabled": 0, "is_internal_supplier": 1, "represents_company": doc.company}, + ) company = frappe.get_cached_value("Customer", doc.customer, "represents_company") if not parties: - frappe.throw(_('No Supplier found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company))) + frappe.throw( + _("No Supplier found for Inter Company Transactions which represents company {0}").format( + frappe.bold(doc.company) + ) + ) party = get_internal_party(parties, "Supplier", doc) else: - parties = frappe.db.get_all("Customer", fields=["name"], filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company}) + parties = frappe.db.get_all( + "Customer", + fields=["name"], + filters={"disabled": 0, "is_internal_customer": 1, "represents_company": doc.company}, + ) company = frappe.get_cached_value("Supplier", doc.supplier, "represents_company") if not parties: - frappe.throw(_('No Customer found for Inter Company Transactions which represents company {0}').format(frappe.bold(doc.company))) + frappe.throw( + _("No Customer found for Inter Company Transactions which represents company {0}").format( + frappe.bold(doc.company) + ) + ) party = get_internal_party(parties, "Customer", doc) - return { - "party": party, - "company": company - } + return {"party": party, "company": company} + def get_internal_party(parties, link_doctype, doc): if len(parties) == 1: - party = parties[0].name + party = parties[0].name else: # If more than one Internal Supplier/Customer, get supplier/customer on basis of address - if doc.get('company_address') or doc.get('shipping_address'): - party = frappe.db.get_value("Dynamic Link", {"parent": doc.get('company_address') or doc.get('shipping_address'), - "parenttype": "Address", "link_doctype": link_doctype}, "link_name") + if doc.get("company_address") or doc.get("shipping_address"): + party = frappe.db.get_value( + "Dynamic Link", + { + "parent": doc.get("company_address") or doc.get("shipping_address"), + "parenttype": "Address", + "link_doctype": link_doctype, + }, + "link_name", + ) if not party: party = parties[0].name @@ -1786,11 +2115,18 @@ def get_internal_party(parties, link_doctype, doc): return party + def validate_inter_company_transaction(doc, doctype): details = get_inter_company_details(doc, doctype) - price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list - valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1}) + price_list = ( + doc.selling_price_list + if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] + else doc.buying_price_list + ) + valid_price_list = frappe.db.get_value( + "Price List", {"name": price_list, "buying": 1, "selling": 1} + ) if not valid_price_list and not doc.is_internal_transfer(): frappe.throw(_("Selected Price List should have buying and selling fields checked.")) @@ -1800,28 +2136,34 @@ def validate_inter_company_transaction(doc, doctype): frappe.throw(_("No {0} found for Inter Company Transactions.").format(partytype)) company = details.get("company") - default_currency = frappe.get_cached_value('Company', company, "default_currency") + default_currency = frappe.get_cached_value("Company", company, "default_currency") if default_currency != doc.currency: - frappe.throw(_("Company currencies of both the companies should match for Inter Company Transactions.")) + frappe.throw( + _("Company currencies of both the companies should match for Inter Company Transactions.") + ) return + @frappe.whitelist() def make_inter_company_purchase_invoice(source_name, target_doc=None): return make_inter_company_transaction("Sales Invoice", source_name, target_doc) + def make_inter_company_transaction(doctype, source_name, target_doc=None): if doctype in ["Sales Invoice", "Sales Order"]: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order" target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item" - source_document_warehouse_field = 'target_warehouse' - target_document_warehouse_field = 'from_warehouse' + source_document_warehouse_field = "target_warehouse" + target_document_warehouse_field = "from_warehouse" + received_items = get_received_items(source_name, target_doctype, target_detail_field) else: source_doc = frappe.get_doc(doctype, source_name) target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order" - source_document_warehouse_field = 'from_warehouse' - target_document_warehouse_field = 'target_warehouse' + source_document_warehouse_field = "from_warehouse" + target_document_warehouse_field = "target_warehouse" + received_items = {} validate_inter_company_transaction(source_doc, doctype) details = get_inter_company_details(source_doc, doctype) @@ -1833,7 +2175,7 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): def update_details(source_doc, target_doc, source_parent): target_doc.inter_company_invoice_reference = source_doc.name if target_doc.doctype in ["Purchase Invoice", "Purchase Order"]: - currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency') + currency = frappe.db.get_value("Supplier", details.get("party"), "default_currency") target_doc.company = details.get("company") target_doc.supplier = details.get("party") target_doc.is_internal_supplier = 1 @@ -1841,130 +2183,203 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None): target_doc.buying_price_list = source_doc.selling_price_list # Invert Addresses - update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address) - update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address) + update_address(target_doc, "supplier_address", "address_display", source_doc.company_address) + update_address( + target_doc, "shipping_address", "shipping_address_display", source_doc.customer_address + ) if currency: target_doc.currency = currency - update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company, - doctype=target_doc.doctype, party_address=target_doc.supplier_address, - company_address=target_doc.shipping_address) + update_taxes( + target_doc, + party=target_doc.supplier, + party_type="Supplier", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.supplier_address, + company_address=target_doc.shipping_address, + ) else: - currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency') + currency = frappe.db.get_value("Customer", details.get("party"), "default_currency") target_doc.company = details.get("company") target_doc.customer = details.get("party") target_doc.selling_price_list = source_doc.buying_price_list - update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address) - update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address) - update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address) + update_address( + target_doc, "company_address", "company_address_display", source_doc.supplier_address + ) + update_address( + target_doc, "shipping_address_name", "shipping_address", source_doc.shipping_address + ) + update_address(target_doc, "customer_address", "address_display", source_doc.shipping_address) if currency: target_doc.currency = currency - update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company, - doctype=target_doc.doctype, party_address=target_doc.customer_address, - company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name) + update_taxes( + target_doc, + party=target_doc.customer, + party_type="Customer", + company=target_doc.company, + doctype=target_doc.doctype, + party_address=target_doc.customer_address, + company_address=target_doc.company_address, + shipping_address_name=target_doc.shipping_address_name, + ) + + def update_item(source, target, source_parent): + target.qty = flt(source.qty) - received_items.get(source.name, 0.0) item_field_map = { "doctype": target_doctype + " Item", - "field_no_map": [ - "income_account", - "expense_account", - "cost_center", - "warehouse" - ], + "field_no_map": ["income_account", "expense_account", "cost_center", "warehouse"], "field_map": { - 'rate': 'rate', - } + "rate": "rate", + }, + "postprocess": update_item, + "condition": lambda doc: doc.qty > 0, } if doctype in ["Sales Invoice", "Sales Order"]: - item_field_map["field_map"].update({ - "name": target_detail_field, - }) + item_field_map["field_map"].update( + { + "name": target_detail_field, + } + ) - if source_doc.get('update_stock'): - item_field_map["field_map"].update({ - source_document_warehouse_field: target_document_warehouse_field, - 'batch_no': 'batch_no', - 'serial_no': 'serial_no' - }) + if source_doc.get("update_stock"): + item_field_map["field_map"].update( + { + source_document_warehouse_field: target_document_warehouse_field, + "batch_no": "batch_no", + "serial_no": "serial_no", + } + ) - doclist = get_mapped_doc(doctype, source_name, { - doctype: { - "doctype": target_doctype, - "postprocess": update_details, - "set_target_warehouse": "set_from_warehouse", - "field_no_map": [ - "taxes_and_charges", - "set_warehouse", - "shipping_address" - ] + doclist = get_mapped_doc( + doctype, + source_name, + { + doctype: { + "doctype": target_doctype, + "postprocess": update_details, + "set_target_warehouse": "set_from_warehouse", + "field_no_map": ["taxes_and_charges", "set_warehouse", "shipping_address"], + }, + doctype + " Item": item_field_map, }, - doctype +" Item": item_field_map - - }, target_doc, set_missing_values) + target_doc, + set_missing_values, + ) return doclist + +def get_received_items(reference_name, doctype, reference_fieldname): + target_doctypes = frappe.get_all( + doctype, + filters={"inter_company_invoice_reference": reference_name, "docstatus": 1}, + as_list=True, + ) + + if target_doctypes: + target_doctypes = list(target_doctypes[0]) + + received_items_map = frappe._dict( + frappe.get_all( + doctype + " Item", + filters={"parent": ("in", target_doctypes)}, + fields=[reference_fieldname, "qty"], + as_list=1, + ) + ) + + return received_items_map + + def set_purchase_references(doc): # add internal PO or PR links if any if doc.is_internal_transfer(): - if doc.doctype == 'Purchase Receipt': + if doc.doctype == "Purchase Receipt": so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference) if so_item_map: - pd_item_map, parent_child_map, warehouse_map = \ - get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item') + pd_item_map, parent_child_map, warehouse_map = get_pd_details( + "Purchase Order Item", so_item_map, "sales_order_item" + ) update_pr_items(doc, so_item_map, pd_item_map, parent_child_map, warehouse_map) - elif doc.doctype == 'Purchase Invoice': + elif doc.doctype == "Purchase Invoice": dn_item_map, so_item_map = get_sales_invoice_details(doc.inter_company_invoice_reference) # First check for Purchase receipt if list(dn_item_map.values()): - pd_item_map, parent_child_map, warehouse_map = \ - get_pd_details('Purchase Receipt Item', dn_item_map, 'delivery_note_item') + pd_item_map, parent_child_map, warehouse_map = get_pd_details( + "Purchase Receipt Item", dn_item_map, "delivery_note_item" + ) - update_pi_items(doc, 'pr_detail', 'purchase_receipt', - dn_item_map, pd_item_map, parent_child_map, warehouse_map) + update_pi_items( + doc, + "pr_detail", + "purchase_receipt", + dn_item_map, + pd_item_map, + parent_child_map, + warehouse_map, + ) if list(so_item_map.values()): - pd_item_map, parent_child_map, warehouse_map = \ - get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item') + pd_item_map, parent_child_map, warehouse_map = get_pd_details( + "Purchase Order Item", so_item_map, "sales_order_item" + ) - update_pi_items(doc, 'po_detail', 'purchase_order', - so_item_map, pd_item_map, parent_child_map, warehouse_map) + update_pi_items( + doc, "po_detail", "purchase_order", so_item_map, pd_item_map, parent_child_map, warehouse_map + ) -def update_pi_items(doc, detail_field, parent_field, sales_item_map, - purchase_item_map, parent_child_map, warehouse_map): - for item in doc.get('items'): + +def update_pi_items( + doc, + detail_field, + parent_field, + sales_item_map, + purchase_item_map, + parent_child_map, + warehouse_map, +): + for item in doc.get("items"): item.set(detail_field, purchase_item_map.get(sales_item_map.get(item.sales_invoice_item))) item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item))) if doc.update_stock: item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item)) + def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map): - for item in doc.get('items'): + for item in doc.get("items"): item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item)) item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item)) item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item)) + def get_delivery_note_details(internal_reference): - si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'], - filters={'parent': internal_reference}) + si_item_details = frappe.get_all( + "Delivery Note Item", fields=["name", "so_detail"], filters={"parent": internal_reference} + ) return {d.name: d.so_detail for d in si_item_details if d.so_detail} + def get_sales_invoice_details(internal_reference): dn_item_map = {} so_item_map = {} - si_item_details = frappe.get_all('Sales Invoice Item', fields=['name', 'so_detail', - 'dn_detail'], filters={'parent': internal_reference}) + si_item_details = frappe.get_all( + "Sales Invoice Item", + fields=["name", "so_detail", "dn_detail"], + filters={"parent": internal_reference}, + ) for d in si_item_details: if d.dn_detail: @@ -1974,13 +2389,17 @@ def get_sales_invoice_details(internal_reference): return dn_item_map, so_item_map + def get_pd_details(doctype, sd_detail_map, sd_detail_field): pd_item_map = {} accepted_warehouse_map = {} parent_child_map = {} - pd_item_details = frappe.get_all(doctype, - fields=[sd_detail_field, 'name', 'warehouse', 'parent'], filters={sd_detail_field: ('in', list(sd_detail_map.values()))}) + pd_item_details = frappe.get_all( + doctype, + fields=[sd_detail_field, "name", "warehouse", "parent"], + filters={sd_detail_field: ("in", list(sd_detail_map.values()))}, + ) for d in pd_item_details: pd_item_map.setdefault(d.get(sd_detail_field), d.name) @@ -1989,16 +2408,33 @@ def get_pd_details(doctype, sd_detail_map, sd_detail_field): return pd_item_map, parent_child_map, accepted_warehouse_map -def update_taxes(doc, party=None, party_type=None, company=None, doctype=None, party_address=None, - company_address=None, shipping_address_name=None, master_doctype=None): + +def update_taxes( + doc, + party=None, + party_type=None, + company=None, + doctype=None, + party_address=None, + company_address=None, + shipping_address_name=None, + master_doctype=None, +): # Update Party Details - party_details = get_party_details(party=party, party_type=party_type, company=company, - doctype=doctype, party_address=party_address, company_address=company_address, - shipping_address=shipping_address_name) + party_details = get_party_details( + party=party, + party_type=party_type, + company=company, + doctype=doctype, + party_address=party_address, + company_address=company_address, + shipping_address=shipping_address_name, + ) # Update taxes and charges if any - doc.taxes_and_charges = party_details.get('taxes_and_charges') - doc.set('taxes', party_details.get('taxes')) + doc.taxes_and_charges = party_details.get("taxes_and_charges") + doc.set("taxes", party_details.get("taxes")) + def update_address(doc, address_field, address_display_field, address_name): doc.set(address_field, address_name) @@ -2009,53 +2445,61 @@ def update_address(doc, address_field, address_display_field, address_name): doc.set(address_display_field, get_address_display(doc.get(address_field))) + @frappe.whitelist() def get_loyalty_programs(customer): - ''' sets applicable loyalty program to the customer or returns a list of applicable programs ''' + """sets applicable loyalty program to the customer or returns a list of applicable programs""" from erpnext.selling.doctype.customer.customer import get_loyalty_programs - customer = frappe.get_doc('Customer', customer) - if customer.loyalty_program: return [customer.loyalty_program] + customer = frappe.get_doc("Customer", customer) + if customer.loyalty_program: + return [customer.loyalty_program] lp_details = get_loyalty_programs(customer) if len(lp_details) == 1: - frappe.db.set(customer, 'loyalty_program', lp_details[0]) + frappe.db.set(customer, "loyalty_program", lp_details[0]) return lp_details else: return lp_details + def on_doctype_update(): frappe.db.add_index("Sales Invoice", ["customer", "is_return", "return_against"]) + @frappe.whitelist() def create_invoice_discounting(source_name, target_doc=None): invoice = frappe.get_doc("Sales Invoice", source_name) invoice_discounting = frappe.new_doc("Invoice Discounting") invoice_discounting.company = invoice.company - invoice_discounting.append("invoices", { - "sales_invoice": source_name, - "customer": invoice.customer, - "posting_date": invoice.posting_date, - "outstanding_amount": invoice.outstanding_amount - }) + invoice_discounting.append( + "invoices", + { + "sales_invoice": source_name, + "customer": invoice.customer, + "posting_date": invoice.posting_date, + "outstanding_amount": invoice.outstanding_amount, + }, + ) return invoice_discounting + def update_multi_mode_option(doc, pos_profile): def append_payment(payment_mode): - payment = doc.append('payments', {}) + payment = doc.append("payments", {}) payment.default = payment_mode.default payment.mode_of_payment = payment_mode.mop payment.account = payment_mode.default_account payment.type = payment_mode.type - doc.set('payments', []) + doc.set("payments", []) invalid_modes = [] - mode_of_payments = [d.mode_of_payment for d in pos_profile.get('payments')] + mode_of_payments = [d.mode_of_payment for d in pos_profile.get("payments")] mode_of_payments_info = get_mode_of_payments_info(mode_of_payments, doc.company) - for row in pos_profile.get('payments'): + for row in pos_profile.get("payments"): payment_mode = mode_of_payments_info.get(row.mode_of_payment) if not payment_mode: invalid_modes.append(get_link_to_form("Mode of Payment", row.mode_of_payment)) @@ -2071,12 +2515,17 @@ def update_multi_mode_option(doc, pos_profile): msg = _("Please set default Cash or Bank account in Mode of Payments {}") frappe.throw(msg.format(", ".join(invalid_modes)), title=_("Missing Account")) + def get_all_mode_of_payments(doc): - return frappe.db.sql(""" + return frappe.db.sql( + """ select mpa.default_account, mpa.parent, mp.type as type from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""", - {'company': doc.company}, as_dict=1) + {"company": doc.company}, + as_dict=1, + ) + def get_mode_of_payments_info(mode_of_payments, company): data = frappe.db.sql( @@ -2092,16 +2541,24 @@ def get_mode_of_payments_info(mode_of_payments, company): mp.name in %s group by mp.name - """, (company, mode_of_payments), as_dict=1) + """, + (company, mode_of_payments), + as_dict=1, + ) + + return {row.get("mop"): row for row in data} - return {row.get('mop'): row for row in data} def get_mode_of_payment_info(mode_of_payment, company): - return frappe.db.sql(""" + return frappe.db.sql( + """ select mpa.default_account, mpa.parent, mp.type as type from `tabMode of Payment Account` mpa,`tabMode of Payment` mp where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", - (company, mode_of_payment), as_dict=1) + (company, mode_of_payment), + as_dict=1, + ) + @frappe.whitelist() def create_dunning(source_name, target_doc=None): @@ -2111,41 +2568,58 @@ def create_dunning(source_name, target_doc=None): calculate_interest_and_amount, get_dunning_letter_text, ) + def set_missing_values(source, target): target.sales_invoice = source_name target.outstanding_amount = source.outstanding_amount overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days target.overdue_days = overdue_days - if frappe.db.exists('Dunning Type', {'start_day': [ - '<', overdue_days], 'end_day': ['>=', overdue_days]}): - dunning_type = frappe.get_doc('Dunning Type', {'start_day': [ - '<', overdue_days], 'end_day': ['>=', overdue_days]}) + if frappe.db.exists( + "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]} + ): + dunning_type = frappe.get_doc( + "Dunning Type", {"start_day": ["<", overdue_days], "end_day": [">=", overdue_days]} + ) target.dunning_type = dunning_type.name target.rate_of_interest = dunning_type.rate_of_interest target.dunning_fee = dunning_type.dunning_fee - letter_text = get_dunning_letter_text(dunning_type = dunning_type.name, doc = target.as_dict()) + letter_text = get_dunning_letter_text(dunning_type=dunning_type.name, doc=target.as_dict()) if letter_text: - target.body_text = letter_text.get('body_text') - target.closing_text = letter_text.get('closing_text') - target.language = letter_text.get('language') - amounts = calculate_interest_and_amount(target.posting_date, target.outstanding_amount, - target.rate_of_interest, target.dunning_fee, target.overdue_days) - target.interest_amount = amounts.get('interest_amount') - target.dunning_amount = amounts.get('dunning_amount') - target.grand_total = amounts.get('grand_total') + target.body_text = letter_text.get("body_text") + target.closing_text = letter_text.get("closing_text") + target.language = letter_text.get("language") + amounts = calculate_interest_and_amount( + target.posting_date, + target.outstanding_amount, + target.rate_of_interest, + target.dunning_fee, + target.overdue_days, + ) + target.interest_amount = amounts.get("interest_amount") + target.dunning_amount = amounts.get("dunning_amount") + target.grand_total = amounts.get("grand_total") - doclist = get_mapped_doc("Sales Invoice", source_name, { - "Sales Invoice": { - "doctype": "Dunning", - } - }, target_doc, set_missing_values) + doclist = get_mapped_doc( + "Sales Invoice", + source_name, + { + "Sales Invoice": { + "doctype": "Dunning", + } + }, + target_doc, + set_missing_values, + ) return doclist + def check_if_return_invoice_linked_with_payment_entry(self): # If a Return invoice is linked with payment entry along with other invoices, # the cancellation of the Return causes allocated amount to be greater than paid - if not frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'): + if not frappe.db.get_single_value( + "Accounts Settings", "unlink_payment_on_cancellation_of_invoice" + ): return payment_entries = [] @@ -2154,7 +2628,8 @@ def check_if_return_invoice_linked_with_payment_entry(self): else: invoice = self.name - payment_entries = frappe.db.sql_list(""" + payment_entries = frappe.db.sql_list( + """ SELECT t1.name FROM @@ -2164,7 +2639,9 @@ def check_if_return_invoice_linked_with_payment_entry(self): and t1.docstatus = 1 and t2.reference_name = %s and t2.allocated_amount < 0 - """, invoice) + """, + invoice, + ) links_to_pe = [] if payment_entries: @@ -2173,7 +2650,9 @@ def check_if_return_invoice_linked_with_payment_entry(self): if len(payment_entry.references) > 1: links_to_pe.append(payment_entry.name) if links_to_pe: - payment_entries_link = [get_link_to_form('Payment Entry', name, label=name) for name in links_to_pe] + payment_entries_link = [ + get_link_to_form("Payment Entry", name, label=name) for name in links_to_pe + ] message = _("Please cancel and amend the Payment Entry") message += " " + ", ".join(payment_entries_link) + " " message += _("to unallocate the amount of this Return Invoice before cancelling it.") diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py index 104d4f9b8aa..0a765f3f46f 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_dashboard.py @@ -1,37 +1,36 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'sales_invoice', - 'non_standard_fieldnames': { - 'Delivery Note': 'against_sales_invoice', - 'Journal Entry': 'reference_name', - 'Payment Entry': 'reference_name', - 'Payment Request': 'reference_name', - 'Sales Invoice': 'return_against', - 'Auto Repeat': 'reference_document', + "fieldname": "sales_invoice", + "non_standard_fieldnames": { + "Delivery Note": "against_sales_invoice", + "Journal Entry": "reference_name", + "Payment Entry": "reference_name", + "Payment Request": "reference_name", + "Sales Invoice": "return_against", + "Auto Repeat": "reference_document", + "Purchase Invoice": "inter_company_invoice_reference", }, - 'internal_links': { - 'Sales Order': ['items', 'sales_order'] + "internal_links": { + "Sales Order": ["items", "sales_order"], + "Timesheet": ["timesheets", "time_sheet"], }, - 'transactions': [ + "transactions": [ { - 'label': _('Payment'), - 'items': ['Payment Entry', 'Payment Request', 'Journal Entry', 'Invoice Discounting', 'Dunning'] + "label": _("Payment"), + "items": [ + "Payment Entry", + "Payment Request", + "Journal Entry", + "Invoice Discounting", + "Dunning", + ], }, - { - 'label': _('Reference'), - 'items': ['Timesheet', 'Delivery Note', 'Sales Order'] - }, - { - 'label': _('Returns'), - 'items': ['Sales Invoice'] - }, - { - 'label': _('Subscription'), - 'items': ['Auto Repeat'] - }, - ] + {"label": _("Reference"), "items": ["Timesheet", "Delivery Note", "Sales Order"]}, + {"label": _("Returns"), "items": ["Sales Invoice"]}, + {"label": _("Subscription"), "items": ["Auto Repeat"]}, + {"label": _("Internal Transfers"), "items": ["Purchase Invoice"]}, + ], } diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index ebe2a969b46..2df095a41a0 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -19,6 +19,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_comp from erpnext.accounts.utils import PaymentEntryUnlinkError from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries from erpnext.assets.doctype.asset.test_asset import create_asset, create_asset_data +from erpnext.controllers.accounts_controller import update_invoice_status from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency from erpnext.regional.india.utils import get_ewb_data @@ -57,23 +58,25 @@ class TestSalesInvoice(unittest.TestCase): w2 = frappe.get_doc(w.doctype, w.name) import time + time.sleep(1) w.save() import time + time.sleep(1) self.assertRaises(frappe.TimestampMismatchError, w2.save) def test_sales_invoice_change_naming_series(self): si = frappe.copy_doc(test_records[2]) si.insert() - si.naming_series = 'TEST-' + si.naming_series = "TEST-" self.assertRaises(frappe.CannotChangeConstantError, si.save) si = frappe.copy_doc(test_records[1]) si.insert() - si.naming_series = 'TEST-' + si.naming_series = "TEST-" self.assertRaises(frappe.CannotChangeConstantError, si.save) @@ -89,15 +92,21 @@ class TestSalesInvoice(unittest.TestCase): si.insert() expected_values = { - "keys": ["price_list_rate", "discount_percentage", "rate", "amount", - "base_price_list_rate", "base_rate", "base_amount"], + "keys": [ + "price_list_rate", + "discount_percentage", + "rate", + "amount", + "base_price_list_rate", + "base_rate", + "base_amount", + ], "_Test Item Home Desktop 100": [50, 0, 50, 500, 50, 50, 500], "_Test Item Home Desktop 200": [150, 0, 150, 750, 150, 150, 750], } # check if children are saved - self.assertEqual(len(si.get("items")), - len(expected_values)-1) + self.assertEqual(len(si.get("items")), len(expected_values) - 1) # check if item values are calculated for d in si.get("items"): @@ -118,7 +127,7 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account S&H Education Cess - _TC": [1.4, 1619.2], "_Test Account CST - _TC": [32.38, 1651.58], "_Test Account VAT - _TC": [156.25, 1807.83], - "_Test Account Discount - _TC": [-180.78, 1627.05] + "_Test Account Discount - _TC": [-180.78, 1627.05], } for d in si.get("taxes"): @@ -148,7 +157,7 @@ class TestSalesInvoice(unittest.TestCase): pe.submit() unlink_payment_on_cancel_of_invoice(0) - si = frappe.get_doc('Sales Invoice', si.name) + si = frappe.get_doc("Sales Invoice", si.name) self.assertRaises(frappe.LinkExistsError, si.cancel) unlink_payment_on_cancel_of_invoice() @@ -159,25 +168,30 @@ class TestSalesInvoice(unittest.TestCase): si2 = create_sales_invoice(rate=300) si3 = create_sales_invoice(qty=-1, rate=300, is_return=1) - pe = get_payment_entry("Sales Invoice", si1.name, bank_account="_Test Bank - _TC") - pe.append('references', { - 'reference_doctype': 'Sales Invoice', - 'reference_name': si2.name, - 'total_amount': si2.grand_total, - 'outstanding_amount': si2.outstanding_amount, - 'allocated_amount': si2.outstanding_amount - }) + pe.append( + "references", + { + "reference_doctype": "Sales Invoice", + "reference_name": si2.name, + "total_amount": si2.grand_total, + "outstanding_amount": si2.outstanding_amount, + "allocated_amount": si2.outstanding_amount, + }, + ) - pe.append('references', { - 'reference_doctype': 'Sales Invoice', - 'reference_name': si3.name, - 'total_amount': si3.grand_total, - 'outstanding_amount': si3.outstanding_amount, - 'allocated_amount': si3.outstanding_amount - }) + pe.append( + "references", + { + "reference_doctype": "Sales Invoice", + "reference_name": si3.name, + "total_amount": si3.grand_total, + "outstanding_amount": si3.outstanding_amount, + "allocated_amount": si3.outstanding_amount, + }, + ) - pe.reference_no = 'Test001' + pe.reference_no = "Test001" pe.reference_date = nowdate() pe.save() pe.submit() @@ -188,7 +202,6 @@ class TestSalesInvoice(unittest.TestCase): si1.load_from_db() self.assertRaises(PaymentEntryUnlinkError, si1.cancel) - def test_sales_invoice_calculation_export_currency(self): si = frappe.copy_doc(test_records[2]) si.currency = "USD" @@ -203,14 +216,21 @@ class TestSalesInvoice(unittest.TestCase): si.insert() expected_values = { - "keys": ["price_list_rate", "discount_percentage", "rate", "amount", - "base_price_list_rate", "base_rate", "base_amount"], + "keys": [ + "price_list_rate", + "discount_percentage", + "rate", + "amount", + "base_price_list_rate", + "base_rate", + "base_amount", + ], "_Test Item Home Desktop 100": [1, 0, 1, 10, 50, 50, 500], "_Test Item Home Desktop 200": [3, 0, 3, 15, 150, 150, 750], } # check if children are saved - self.assertEqual(len(si.get("items")), len(expected_values)-1) + self.assertEqual(len(si.get("items")), len(expected_values) - 1) # check if item values are calculated for d in si.get("items"): @@ -233,7 +253,7 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account S&H Education Cess - _TC": [1.5, 1619.5, 0.03, 32.39], "_Test Account CST - _TC": [32.5, 1652, 0.65, 33.04], "_Test Account VAT - _TC": [156.5, 1808.5, 3.13, 36.17], - "_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55] + "_Test Account Discount - _TC": [-181.0, 1627.5, -3.62, 32.55], } for d in si.get("taxes"): @@ -245,22 +265,28 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_with_discount_and_inclusive_tax(self): si = create_sales_invoice(qty=100, rate=50, do_not_save=True) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 14, - 'included_in_print_rate': 1 - }) - si.append("taxes", { - "charge_type": "On Item Quantity", - "account_head": "_Test Account Education Cess - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "CESS", - "rate": 5, - 'included_in_print_rate': 1 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 14, + "included_in_print_rate": 1, + }, + ) + si.append( + "taxes", + { + "charge_type": "On Item Quantity", + "account_head": "_Test Account Education Cess - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "CESS", + "rate": 5, + "included_in_print_rate": 1, + }, + ) si.insert() # with inclusive tax @@ -272,7 +298,7 @@ class TestSalesInvoice(unittest.TestCase): # additional discount si.discount_amount = 100 - si.apply_discount_on = 'Net Total' + si.apply_discount_on = "Net Total" si.payment_schedule = [] si.save() @@ -285,7 +311,7 @@ class TestSalesInvoice(unittest.TestCase): # additional discount on grand total si.discount_amount = 100 - si.apply_discount_on = 'Grand Total' + si.apply_discount_on = "Grand Total" si.payment_schedule = [] si.save() @@ -297,14 +323,17 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_discount_amount(self): si = frappe.copy_doc(test_records[3]) si.discount_amount = 104.94 - si.append("taxes", { - "charge_type": "On Previous Row Amount", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 10, - "row_id": 8, - }) + si.append( + "taxes", + { + "charge_type": "On Previous Row Amount", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 10, + "row_id": 8, + }, + ) si.insert() expected_values = [ @@ -320,7 +349,7 @@ class TestSalesInvoice(unittest.TestCase): "net_rate": 46.54, "net_amount": 465.37, "base_net_rate": 46.54, - "base_net_amount": 465.37 + "base_net_amount": 465.37, }, { "item_code": "_Test Item Home Desktop 200", @@ -334,12 +363,12 @@ class TestSalesInvoice(unittest.TestCase): "net_rate": 139.62, "net_amount": 698.08, "base_net_rate": 139.62, - "base_net_amount": 698.08 - } + "base_net_amount": 698.08, + }, ] # check if children are saved - self.assertEqual(len(si.get("items")), len(expected_values)) + self.assertEqual(len(si.get("items")), len(expected_values)) # check if item values are calculated for i, d in enumerate(si.get("items")): @@ -361,7 +390,7 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account Customs Duty - _TC": [125, 116.35, 1585.40], "_Test Account Shipping Charges - _TC": [100, 100, 1685.40], "_Test Account Discount - _TC": [-180.33, -168.54, 1516.86], - "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01] + "_Test Account Service Tax - _TC": [-18.03, -16.85, 1500.01], } for d in si.get("taxes"): @@ -376,38 +405,48 @@ class TestSalesInvoice(unittest.TestCase): frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC") si = frappe.copy_doc(test_records[3]) si.discount_amount = 104.94 - si.append("taxes", { - "doctype": "Sales Taxes and Charges", - "charge_type": "On Previous Row Amount", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 10, - "row_id": 8 - }) + si.append( + "taxes", + { + "doctype": "Sales Taxes and Charges", + "charge_type": "On Previous Row Amount", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 10, + "row_id": 8, + }, + ) si.insert() si.submit() - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 1500, 0.0], - [test_records[3]["items"][0]["income_account"], 0.0, 1163.45], - [test_records[3]["taxes"][0]["account_head"], 0.0, 130.31], - [test_records[3]["taxes"][1]["account_head"], 0.0, 2.61], - [test_records[3]["taxes"][2]["account_head"], 0.0, 1.30], - [test_records[3]["taxes"][3]["account_head"], 0.0, 25.95], - [test_records[3]["taxes"][4]["account_head"], 0.0, 145.43], - [test_records[3]["taxes"][5]["account_head"], 0.0, 116.35], - [test_records[3]["taxes"][6]["account_head"], 0.0, 100], - [test_records[3]["taxes"][7]["account_head"], 168.54, 0.0], - ["_Test Account Service Tax - _TC", 16.85, 0.0], - ["Round Off - _TC", 0.01, 0.0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 1500, 0.0], + [test_records[3]["items"][0]["income_account"], 0.0, 1163.45], + [test_records[3]["taxes"][0]["account_head"], 0.0, 130.31], + [test_records[3]["taxes"][1]["account_head"], 0.0, 2.61], + [test_records[3]["taxes"][2]["account_head"], 0.0, 1.30], + [test_records[3]["taxes"][3]["account_head"], 0.0, 25.95], + [test_records[3]["taxes"][4]["account_head"], 0.0, 145.43], + [test_records[3]["taxes"][5]["account_head"], 0.0, 116.35], + [test_records[3]["taxes"][6]["account_head"], 0.0, 100], + [test_records[3]["taxes"][7]["account_head"], 168.54, 0.0], + ["_Test Account Service Tax - _TC", 16.85, 0.0], + ["Round Off - _TC", 0.01, 0.0], + ] + ) for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) @@ -417,8 +456,11 @@ class TestSalesInvoice(unittest.TestCase): # cancel si.cancel() - gle = frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) + gle = frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + si.name, + ) self.assertTrue(gle) @@ -430,14 +472,17 @@ class TestSalesInvoice(unittest.TestCase): item_row_copy.qty = qty si.append("items", item_row_copy) - si.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 19 - }) + si.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 19, + }, + ) si.insert() self.assertEqual(si.net_total, 4600) @@ -452,10 +497,10 @@ class TestSalesInvoice(unittest.TestCase): item_row = si.get("items")[0] add_items = [ - (54, '_Test Account Excise Duty @ 12 - _TC'), - (288, '_Test Account Excise Duty @ 15 - _TC'), - (144, '_Test Account Excise Duty @ 20 - _TC'), - (430, '_Test Item Tax Template 1 - _TC') + (54, "_Test Account Excise Duty @ 12 - _TC"), + (288, "_Test Account Excise Duty @ 15 - _TC"), + (144, "_Test Account Excise Duty @ 20 - _TC"), + (430, "_Test Item Tax Template 1 - _TC"), ] for qty, item_tax_template in add_items: item_row_copy = copy.deepcopy(item_row) @@ -463,30 +508,39 @@ class TestSalesInvoice(unittest.TestCase): item_row_copy.item_tax_template = item_tax_template si.append("items", item_row_copy) - si.append("taxes", { - "account_head": "_Test Account Excise Duty - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "rate": 11 - }) - si.append("taxes", { - "account_head": "_Test Account Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 0 - }) - si.append("taxes", { - "account_head": "_Test Account S&H Education Cess - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "S&H Education Cess", - "doctype": "Sales Taxes and Charges", - "rate": 3 - }) + si.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "rate": 11, + }, + ) + si.append( + "taxes", + { + "account_head": "_Test Account Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 0, + }, + ) + si.append( + "taxes", + { + "account_head": "_Test Account S&H Education Cess - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "S&H Education Cess", + "doctype": "Sales Taxes and Charges", + "rate": 3, + }, + ) si.insert() self.assertEqual(si.net_total, 4600) @@ -516,14 +570,17 @@ class TestSalesInvoice(unittest.TestCase): si.apply_discount_on = "Net Total" si.discount_amount = 75.0 - si.append("taxes", { - "account_head": "_Test Account VAT - _TC", - "charge_type": "On Net Total", - "cost_center": "_Test Cost Center - _TC", - "description": "VAT", - "doctype": "Sales Taxes and Charges", - "rate": 24 - }) + si.append( + "taxes", + { + "account_head": "_Test Account VAT - _TC", + "charge_type": "On Net Total", + "cost_center": "_Test Cost Center - _TC", + "description": "VAT", + "doctype": "Sales Taxes and Charges", + "rate": 24, + }, + ) si.insert() self.assertEqual(si.total, 975) @@ -537,7 +594,7 @@ class TestSalesInvoice(unittest.TestCase): def test_inclusive_rate_validations(self): si = frappe.copy_doc(test_records[2]) for i, tax in enumerate(si.get("taxes")): - tax.idx = i+1 + tax.idx = i + 1 si.get("items")[0].price_list_rate = 62.5 si.get("items")[0].price_list_rate = 191 @@ -557,14 +614,43 @@ class TestSalesInvoice(unittest.TestCase): si.insert() expected_values = { - "keys": ["price_list_rate", "discount_percentage", "rate", "amount", - "base_price_list_rate", "base_rate", "base_amount", "net_rate", "net_amount"], - "_Test Item Home Desktop 100": [62.5, 0, 62.5, 625.0, 62.5, 62.5, 625.0, 50, 499.97600115194473], - "_Test Item Home Desktop 200": [190.66, 0, 190.66, 953.3, 190.66, 190.66, 953.3, 150, 749.9968530500239], + "keys": [ + "price_list_rate", + "discount_percentage", + "rate", + "amount", + "base_price_list_rate", + "base_rate", + "base_amount", + "net_rate", + "net_amount", + ], + "_Test Item Home Desktop 100": [ + 62.5, + 0, + 62.5, + 625.0, + 62.5, + 62.5, + 625.0, + 50, + 499.97600115194473, + ], + "_Test Item Home Desktop 200": [ + 190.66, + 0, + 190.66, + 953.3, + 190.66, + 190.66, + 953.3, + 150, + 749.9968530500239, + ], } # check if children are saved - self.assertEqual(len(si.get("items")), len(expected_values)-1) + self.assertEqual(len(si.get("items")), len(expected_values) - 1) # check if item values are calculated for d in si.get("items"): @@ -585,7 +671,7 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account VAT - _TC": [156.25, 1578.30], "_Test Account Customs Duty - _TC": [125, 1703.30], "_Test Account Shipping Charges - _TC": [100, 1803.30], - "_Test Account Discount - _TC": [-180.33, 1622.97] + "_Test Account Discount - _TC": [-180.33, 1622.97], } for d in si.get("taxes"): @@ -623,7 +709,7 @@ class TestSalesInvoice(unittest.TestCase): "net_rate": 40, "net_amount": 399.9808009215558, "base_net_rate": 2000, - "base_net_amount": 19999 + "base_net_amount": 19999, }, { "item_code": "_Test Item Home Desktop 200", @@ -637,8 +723,8 @@ class TestSalesInvoice(unittest.TestCase): "net_rate": 118.01, "net_amount": 590.0531205155963, "base_net_rate": 5900.5, - "base_net_amount": 29502.5 - } + "base_net_amount": 29502.5, + }, ] # check if children are saved @@ -663,8 +749,8 @@ class TestSalesInvoice(unittest.TestCase): "_Test Account CST - _TC": [1104, 56312.0, 22.08, 1126.24], "_Test Account VAT - _TC": [6187.5, 62499.5, 123.75, 1249.99], "_Test Account Customs Duty - _TC": [4950.0, 67449.5, 99.0, 1348.99], - "_Test Account Shipping Charges - _TC": [ 100, 67549.5, 2, 1350.99], - "_Test Account Discount - _TC": [ -6755, 60794.5, -135.10, 1215.89] + "_Test Account Shipping Charges - _TC": [100, 67549.5, 2, 1350.99], + "_Test Account Discount - _TC": [-6755, 60794.5, -135.10, 1215.89], } for d in si.get("taxes"): @@ -676,7 +762,6 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.rounding_adjustment, 0.01) self.assertEqual(si.base_rounding_adjustment, 0.50) - def test_outstanding(self): w = self.make() self.assertEqual(w.outstanding_amount, w.base_rounded_total) @@ -696,11 +781,11 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 162.0) - link_data = get_dynamic_link_map().get('Sales Invoice', []) + link_data = get_dynamic_link_map().get("Sales Invoice", []) link_doctypes = [d.parent for d in link_data] # test case for dynamic link order - self.assertTrue(link_doctypes.index('GL Entry') > link_doctypes.index('Journal Entry Account')) + self.assertTrue(link_doctypes.index("GL Entry") > link_doctypes.index("Journal Entry Account")) jv.cancel() self.assertEqual(frappe.db.get_value("Sales Invoice", w.name, "outstanding_amount"), 562.0) @@ -710,18 +795,25 @@ class TestSalesInvoice(unittest.TestCase): si.insert() si.submit() - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 630.0, 0.0], - [test_records[1]["items"][0]["income_account"], 0.0, 500.0], - [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0], - [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0], - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 630.0, 0.0], + [test_records[1]["items"][0]["income_account"], 0.0, 500.0], + [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0], + [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0], + ] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][0], gle.account) @@ -731,25 +823,49 @@ class TestSalesInvoice(unittest.TestCase): # cancel si.cancel() - gle = frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) + gle = frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + si.name, + ) self.assertTrue(gle) def test_pos_gl_entry_with_perpetual_inventory(self): - make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", - expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") + make_pos_profile( + company="_Test Company with perpetual inventory", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + write_off_account="_Test Write Off - TCP1", + ) - pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1") + pr = make_purchase_receipt( + company="_Test Company with perpetual inventory", + item_code="_Test FG Item", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + ) - pos = create_sales_invoice(company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", - income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1", do_not_save=True) + pos = create_sales_invoice( + company="_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", + item_code="_Test FG Item", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + do_not_save=True, + ) pos.is_pos = 1 pos.update_stock = 1 - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 50}) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50} + ) + pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 50}) taxes = get_taxes_and_charges() pos.taxes = [] @@ -769,20 +885,19 @@ class TestSalesInvoice(unittest.TestCase): pos_profile = make_pos_profile() pos_profile.payments = [] - pos_profile.append('payments', { - 'default': 1, - 'mode_of_payment': 'Cash' - }) + pos_profile.append("payments", {"default": 1, "mode_of_payment": "Cash"}) pos_profile.save() - pos = create_sales_invoice(qty = 10, do_not_save=True) + pos = create_sales_invoice(qty=10, do_not_save=True) pos.is_pos = 1 pos.pos_profile = pos_profile.name - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - _TC", "amount": 500} + ) + pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - _TC", "amount": 500}) pos.insert() pos.submit() @@ -791,46 +906,123 @@ class TestSalesInvoice(unittest.TestCase): pos_return.insert() pos_return.submit() - self.assertEqual(pos_return.get('payments')[0].amount, -1000) + self.assertEqual(pos_return.get("payments")[0].amount, -1000) def test_pos_change_amount(self): - make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", - expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") + make_pos_profile( + company="_Test Company with perpetual inventory", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + write_off_account="_Test Write Off - TCP1", + ) - make_purchase_receipt(company= "_Test Company with perpetual inventory", - item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1") + make_purchase_receipt( + company="_Test Company with perpetual inventory", + item_code="_Test FG Item", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + ) - pos = create_sales_invoice(company= "_Test Company with perpetual inventory", - debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", - income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", - cost_center = "Main - TCP1", do_not_save=True) + pos = create_sales_invoice( + company="_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", + item_code="_Test FG Item", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + do_not_save=True, + ) pos.is_pos = 1 pos.update_stock = 1 - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60}) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50} + ) + pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 60}) - pos.change_amount = 5.0 + pos.write_off_outstanding_amount_automatically = 1 pos.insert() pos.submit() self.assertEqual(pos.grand_total, 100.0) - self.assertEqual(pos.write_off_amount, -5) + self.assertEqual(pos.write_off_amount, 0) + + def test_auto_write_off_amount(self): + make_pos_profile( + company="_Test Company with perpetual inventory", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + write_off_account="_Test Write Off - TCP1", + ) + + make_purchase_receipt( + company="_Test Company with perpetual inventory", + item_code="_Test FG Item", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + ) + + pos = create_sales_invoice( + company="_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", + item_code="_Test FG Item", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + do_not_save=True, + ) + + pos.is_pos = 1 + pos.update_stock = 1 + + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50} + ) + pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 40}) + + pos.write_off_outstanding_amount_automatically = 1 + pos.insert() + pos.submit() + + self.assertEqual(pos.grand_total, 100.0) + self.assertEqual(pos.write_off_amount, 10) def test_pos_with_no_gl_entry_for_change_amount(self): - frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0) + frappe.db.set_value("Accounts Settings", None, "post_change_gl_entries", 0) - make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1", - expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1") + make_pos_profile( + company="_Test Company with perpetual inventory", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + write_off_account="_Test Write Off - TCP1", + ) - make_purchase_receipt(company= "_Test Company with perpetual inventory", - item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1") + make_purchase_receipt( + company="_Test Company with perpetual inventory", + item_code="_Test FG Item", + warehouse="Stores - TCP1", + cost_center="Main - TCP1", + ) - pos = create_sales_invoice(company= "_Test Company with perpetual inventory", - debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", - income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", - cost_center = "Main - TCP1", do_not_save=True) + pos = create_sales_invoice( + company="_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", + item_code="_Test FG Item", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + do_not_save=True, + ) pos.is_pos = 1 pos.update_stock = 1 @@ -840,8 +1032,10 @@ class TestSalesInvoice(unittest.TestCase): for tax in taxes: pos.append("taxes", tax) - pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50}) - pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60}) + pos.append( + "payments", {"mode_of_payment": "Bank Draft", "account": "_Test Bank - TCP1", "amount": 50} + ) + pos.append("payments", {"mode_of_payment": "Cash", "account": "Cash - TCP1", "amount": 60}) pos.insert() pos.submit() @@ -851,40 +1045,50 @@ class TestSalesInvoice(unittest.TestCase): self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True) - frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 1) + frappe.db.set_value("Accounts Settings", None, "post_change_gl_entries", 1) def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False): if validate_without_change_gle: cash_amount -= pos.change_amount # check stock ledger entries - sle = frappe.db.sql("""select * from `tabStock Ledger Entry` + sle = frappe.db.sql( + """select * from `tabStock Ledger Entry` where voucher_type = 'Sales Invoice' and voucher_no = %s""", - si.name, as_dict=1)[0] + si.name, + as_dict=1, + )[0] self.assertTrue(sle) - self.assertEqual([sle.item_code, sle.warehouse, sle.actual_qty], - ['_Test FG Item', 'Stores - TCP1', -1.0]) + self.assertEqual( + [sle.item_code, sle.warehouse, sle.actual_qty], ["_Test FG Item", "Stores - TCP1", -1.0] + ) # check gl entries - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc, debit asc, credit asc""", si.name, as_dict=1) + order by account asc, debit asc, credit asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) - stock_in_hand = get_inventory_account('_Test Company with perpetual inventory') - expected_gl_entries = sorted([ - [si.debit_to, 100.0, 0.0], - [pos.items[0].income_account, 0.0, 89.09], - ['Round Off - TCP1', 0.0, 0.01], - [pos.taxes[0].account_head, 0.0, 10.69], - [pos.taxes[1].account_head, 0.0, 0.21], - [stock_in_hand, 0.0, abs(sle.stock_value_difference)], - [pos.items[0].expense_account, abs(sle.stock_value_difference), 0.0], - [si.debit_to, 0.0, 50.0], - [si.debit_to, 0.0, cash_amount], - ["_Test Bank - TCP1", 50, 0.0], - ["Cash - TCP1", cash_amount, 0.0] - ]) + stock_in_hand = get_inventory_account("_Test Company with perpetual inventory") + expected_gl_entries = sorted( + [ + [si.debit_to, 100.0, 0.0], + [pos.items[0].income_account, 0.0, 89.09], + ["Round Off - TCP1", 0.0, 0.01], + [pos.taxes[0].account_head, 0.0, 10.69], + [pos.taxes[1].account_head, 0.0, 0.21], + [stock_in_hand, 0.0, abs(sle.stock_value_difference)], + [pos.items[0].expense_account, abs(sle.stock_value_difference), 0.0], + [si.debit_to, 0.0, 50.0], + [si.debit_to, 0.0, cash_amount], + ["_Test Bank - TCP1", 50, 0.0], + ["Cash - TCP1", cash_amount, 0.0], + ] + ) for i, gle in enumerate(sorted(gl_entries, key=lambda gle: gle.account)): self.assertEqual(expected_gl_entries[i][0], gle.account) @@ -892,8 +1096,11 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_gl_entries[i][2], gle.credit) si.cancel() - gle = frappe.db.sql("""select * from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) + gle = frappe.db.sql( + """select * from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + si.name, + ) self.assertTrue(gle) @@ -913,21 +1120,29 @@ class TestSalesInvoice(unittest.TestCase): self.assertRaises(frappe.ValidationError, si.submit) def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self): - si = create_sales_invoice(company="_Test Company with perpetual inventory", debit_to = "Debtors - TCP1", - income_account="Sales - TCP1", cost_center = "Main - TCP1", do_not_save=True) + si = create_sales_invoice( + company="_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", + income_account="Sales - TCP1", + cost_center="Main - TCP1", + do_not_save=True, + ) si.get("items")[0].item_code = None si.insert() si.submit() - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - ["Debtors - TCP1", 100.0, 0.0], - ["Sales - TCP1", 0.0, 100.0] - ]) + expected_values = dict( + (d[0], d) for d in [["Debtors - TCP1", 100.0, 0.0], ["Sales - TCP1", 0.0, 100.0]] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) @@ -936,25 +1151,32 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self): si = create_sales_invoice(item="_Test Non Stock Item") - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 100.0, 0.0], - [test_records[1]["items"][0]["income_account"], 0.0, 100.0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 100.0, 0.0], + [test_records[1]["items"][0]["income_account"], 0.0, 100.0], + ] + ) for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) - def _insert_purchase_receipt(self): from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( test_records as pr_test_records, ) + pr = frappe.copy_doc(pr_test_records[0]) pr.naming_series = "_T-Purchase Receipt-" pr.insert() @@ -964,6 +1186,7 @@ class TestSalesInvoice(unittest.TestCase): from erpnext.stock.doctype.delivery_note.test_delivery_note import ( test_records as dn_test_records, ) + dn = frappe.copy_doc(dn_test_records[0]) dn.naming_series = "_T-Delivery Note-" dn.insert() @@ -981,24 +1204,37 @@ class TestSalesInvoice(unittest.TestCase): si = frappe.copy_doc(test_records[0]) si.allocate_advances_automatically = 0 - si.append("advances", { - "doctype": "Sales Invoice Advance", - "reference_type": "Journal Entry", - "reference_name": jv.name, - "reference_row": jv.get("accounts")[0].name, - "advance_amount": 400, - "allocated_amount": 300, - "remarks": jv.remark - }) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.get("accounts")[0].name, + "advance_amount": 400, + "allocated_amount": 300, + "remarks": jv.remark, + }, + ) si.insert() si.submit() si.load_from_db() - self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_name=%s""", si.name)) + self.assertTrue( + frappe.db.sql( + """select name from `tabJournal Entry Account` + where reference_name=%s""", + si.name, + ) + ) - self.assertTrue(frappe.db.sql("""select name from `tabJournal Entry Account` - where reference_name=%s and credit_in_account_currency=300""", si.name)) + self.assertTrue( + frappe.db.sql( + """select name from `tabJournal Entry Account` + where reference_name=%s and credit_in_account_currency=300""", + si.name, + ) + ) self.assertEqual(si.outstanding_amount, 262.0) @@ -1020,29 +1256,34 @@ class TestSalesInvoice(unittest.TestCase): si.submit() self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse")) - self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], - "delivery_document_no"), si.name) + self.assertEqual( + frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no"), si.name + ) return si def test_serialized_cancel(self): from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos + si = self.test_serialized() si.cancel() serial_nos = get_serial_nos(si.get("items")[0].serial_no) - self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC") - self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], - "delivery_document_no")) + self.assertEqual( + frappe.db.get_value("Serial No", serial_nos[0], "warehouse"), "_Test Warehouse - _TC" + ) + self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "delivery_document_no")) self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice")) def test_serialize_status(self): - serial_no = frappe.get_doc({ - "doctype": "Serial No", - "item_code": "_Test Serialized Item With Series", - "serial_no": make_autoname("SR", "Serial No") - }) + serial_no = frappe.get_doc( + { + "doctype": "Serial No", + "item_code": "_Test Serialized Item With Series", + "serial_no": make_autoname("SR", "Serial No"), + } + ) serial_no.save() si = frappe.copy_doc(test_records[0]) @@ -1056,8 +1297,8 @@ class TestSalesInvoice(unittest.TestCase): def test_serial_numbers_against_delivery_note(self): """ - check if the sales invoice item serial numbers and the delivery note items - serial numbers are same + check if the sales invoice item serial numbers and the delivery note items + serial numbers are same """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos @@ -1077,40 +1318,84 @@ class TestSalesInvoice(unittest.TestCase): def test_return_sales_invoice(self): make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100) - actual_qty_0 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1") + actual_qty_0 = get_qty_after_transaction(item_code="_Test Item", warehouse="Stores - TCP1") - si = create_sales_invoice(qty = 5, rate=500, update_stock=1, company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1") + si = create_sales_invoice( + qty=5, + rate=500, + update_stock=1, + company="_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", + item_code="_Test Item", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) - - actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1") + actual_qty_1 = get_qty_after_transaction(item_code="_Test Item", warehouse="Stores - TCP1") self.assertEqual(actual_qty_0 - 5, actual_qty_1) # outgoing_rate - outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Sales Invoice", - "voucher_no": si.name}, "stock_value_difference") / 5 + outgoing_rate = ( + frappe.db.get_value( + "Stock Ledger Entry", + {"voucher_type": "Sales Invoice", "voucher_no": si.name}, + "stock_value_difference", + ) + / 5 + ) # return entry - si1 = create_sales_invoice(is_return=1, return_against=si.name, qty=-2, rate=500, update_stock=1, company= "_Test Company with perpetual inventory", debit_to="Debtors - TCP1", item_code= "_Test Item", warehouse="Stores - TCP1", income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", cost_center = "Main - TCP1") + si1 = create_sales_invoice( + is_return=1, + return_against=si.name, + qty=-2, + rate=500, + update_stock=1, + company="_Test Company with perpetual inventory", + debit_to="Debtors - TCP1", + item_code="_Test Item", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) - actual_qty_2 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1") + actual_qty_2 = get_qty_after_transaction(item_code="_Test Item", warehouse="Stores - TCP1") self.assertEqual(actual_qty_1 + 2, actual_qty_2) - incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", + incoming_rate, stock_value_difference = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_type": "Sales Invoice", "voucher_no": si1.name}, - ["incoming_rate", "stock_value_difference"]) + ["incoming_rate", "stock_value_difference"], + ) self.assertEqual(flt(incoming_rate, 3), abs(flt(outgoing_rate, 3))) - stock_in_hand_account = get_inventory_account('_Test Company with perpetual inventory', si1.items[0].warehouse) + stock_in_hand_account = get_inventory_account( + "_Test Company with perpetual inventory", si1.items[0].warehouse + ) # Check gl entry - gle_warehouse_amount = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice", - "voucher_no": si1.name, "account": stock_in_hand_account}, "debit") + gle_warehouse_amount = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Sales Invoice", "voucher_no": si1.name, "account": stock_in_hand_account}, + "debit", + ) self.assertEqual(gle_warehouse_amount, stock_value_difference) - party_credited = frappe.db.get_value("GL Entry", {"voucher_type": "Sales Invoice", - "voucher_no": si1.name, "account": "Debtors - TCP1", "party": "_Test Customer"}, "credit") + party_credited = frappe.db.get_value( + "GL Entry", + { + "voucher_type": "Sales Invoice", + "voucher_no": si1.name, + "account": "Debtors - TCP1", + "party": "_Test Customer", + }, + "credit", + ) self.assertEqual(party_credited, 1000) @@ -1121,63 +1406,87 @@ class TestSalesInvoice(unittest.TestCase): def test_gle_made_when_asset_is_returned(self): create_asset_data() - pi = frappe.new_doc('Purchase Invoice') - pi.supplier = '_Test Supplier' - pi.append('items', { - 'item_code': 'Macbook Pro', - 'qty': 1 - }) + pi = frappe.new_doc("Purchase Invoice") + pi.supplier = "_Test Supplier" + pi.append("items", {"item_code": "Macbook Pro", "qty": 1}) pi.set_missing_values() asset = create_asset(item_code="Macbook Pro") si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000) - return_si = create_sales_invoice(is_return=1, return_against=si.name, item_code="Macbook Pro", asset=asset.name, qty=-1, rate=90000) + return_si = create_sales_invoice( + is_return=1, + return_against=si.name, + item_code="Macbook Pro", + asset=asset.name, + qty=-1, + rate=90000, + ) disposal_account = frappe.get_cached_value("Company", "_Test Company", "disposal_account") # Asset value is 100,000 but it was sold for 90,000, so there should be a loss of 10,000 loss_for_si = frappe.get_all( "GL Entry", - filters = { - "voucher_no": si.name, - "account": disposal_account - }, - fields = ["credit", "debit"] + filters={"voucher_no": si.name, "account": disposal_account}, + fields=["credit", "debit"], )[0] loss_for_return_si = frappe.get_all( "GL Entry", - filters = { - "voucher_no": return_si.name, - "account": disposal_account - }, - fields = ["credit", "debit"] + filters={"voucher_no": return_si.name, "account": disposal_account}, + fields=["credit", "debit"], )[0] - self.assertEqual(loss_for_si['credit'], loss_for_return_si['debit']) - self.assertEqual(loss_for_si['debit'], loss_for_return_si['credit']) + self.assertEqual(loss_for_si["credit"], loss_for_return_si["debit"]) + self.assertEqual(loss_for_si["debit"], loss_for_return_si["credit"]) def test_incoming_rate_for_stand_alone_credit_note(self): - return_si = create_sales_invoice(is_return=1, update_stock=1, qty=-1, rate=90000, incoming_rate=10, - company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', debit_to='Debtors - TCP1', - income_account='Sales - TCP1', expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1') + return_si = create_sales_invoice( + is_return=1, + update_stock=1, + qty=-1, + rate=90000, + incoming_rate=10, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + debit_to="Debtors - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) - incoming_rate = frappe.db.get_value('Stock Ledger Entry', {'voucher_no': return_si.name}, 'incoming_rate') - debit_amount = frappe.db.get_value('GL Entry', - {'voucher_no': return_si.name, 'account': 'Stock In Hand - TCP1'}, 'debit') + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": return_si.name}, "incoming_rate" + ) + debit_amount = frappe.db.get_value( + "GL Entry", {"voucher_no": return_si.name, "account": "Stock In Hand - TCP1"}, "debit" + ) self.assertEqual(debit_amount, 10.0) self.assertEqual(incoming_rate, 10.0) def test_incoming_rate_for_stand_alone_credit_note(self): - return_si = create_sales_invoice(is_return=1, update_stock=1, qty=-1, rate=90000, incoming_rate=10, - company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', debit_to='Debtors - TCP1', - income_account='Sales - TCP1', expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1') + return_si = create_sales_invoice( + is_return=1, + update_stock=1, + qty=-1, + rate=90000, + incoming_rate=10, + company="_Test Company with perpetual inventory", + warehouse="Stores - TCP1", + debit_to="Debtors - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + ) - incoming_rate = frappe.db.get_value('Stock Ledger Entry', {'voucher_no': return_si.name}, 'incoming_rate') - debit_amount = frappe.db.get_value('GL Entry', - {'voucher_no': return_si.name, 'account': 'Stock In Hand - TCP1'}, 'debit') + incoming_rate = frappe.db.get_value( + "Stock Ledger Entry", {"voucher_no": return_si.name}, "incoming_rate" + ) + debit_amount = frappe.db.get_value( + "GL Entry", {"voucher_no": return_si.name, "account": "Stock In Hand - TCP1"}, "debit" + ) self.assertEqual(debit_amount, 10.0) self.assertEqual(incoming_rate, 10.0) @@ -1189,16 +1498,25 @@ class TestSalesInvoice(unittest.TestCase): si.insert() expected_values = { - "keys": ["price_list_rate", "discount_percentage", "rate", "amount", - "base_price_list_rate", "base_rate", "base_amount", - "net_rate", "base_net_rate", "net_amount", "base_net_amount"], + "keys": [ + "price_list_rate", + "discount_percentage", + "rate", + "amount", + "base_price_list_rate", + "base_rate", + "base_amount", + "net_rate", + "base_net_rate", + "net_amount", + "base_net_amount", + ], "_Test Item Home Desktop 100": [50, 0, 50, 500, 50, 50, 500, 25, 25, 250, 250], "_Test Item Home Desktop 200": [150, 0, 150, 750, 150, 150, 750, 75, 75, 375, 375], } # check if children are saved - self.assertEqual(len(si.get("items")), - len(expected_values)-1) + self.assertEqual(len(si.get("items")), len(expected_values) - 1) # check if item values are calculated for d in si.get("items"): @@ -1213,16 +1531,19 @@ class TestSalesInvoice(unittest.TestCase): # check tax calculation expected_values = { - "keys": ["tax_amount", "tax_amount_after_discount_amount", - "base_tax_amount_after_discount_amount"], + "keys": [ + "tax_amount", + "tax_amount_after_discount_amount", + "base_tax_amount_after_discount_amount", + ], "_Test Account Shipping Charges - _TC": [100, 100, 100], "_Test Account Customs Duty - _TC": [62.5, 62.5, 62.5], "_Test Account Excise Duty - _TC": [70, 70, 70], "_Test Account Education Cess - _TC": [1.4, 1.4, 1.4], - "_Test Account S&H Education Cess - _TC": [.7, 0.7, 0.7], + "_Test Account S&H Education Cess - _TC": [0.7, 0.7, 0.7], "_Test Account CST - _TC": [17.19, 17.19, 17.19], "_Test Account VAT - _TC": [78.13, 78.13, 78.13], - "_Test Account Discount - _TC": [-95.49, -95.49, -95.49] + "_Test Account Discount - _TC": [-95.49, -95.49, -95.49], } for d in si.get("taxes"): @@ -1230,19 +1551,26 @@ class TestSalesInvoice(unittest.TestCase): if expected_values.get(d.account_head): self.assertEqual(d.get(k), expected_values[d.account_head][i]) - self.assertEqual(si.total_taxes_and_charges, 234.43) self.assertEqual(si.base_grand_total, 859.43) self.assertEqual(si.grand_total, 859.43) def test_multi_currency_gle(self): - si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) - gl_entries = frappe.db.sql("""select account, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -1252,59 +1580,99 @@ class TestSalesInvoice(unittest.TestCase): "debit": 5000, "debit_in_account_currency": 100, "credit": 0, - "credit_in_account_currency": 0 + "credit_in_account_currency": 0, }, "Sales - _TC": { "account_currency": "INR", "debit": 0, "debit_in_account_currency": 0, "credit": 5000, - "credit_in_account_currency": 5000 - } + "credit_in_account_currency": 5000, + }, } - for field in ("account_currency", "debit", "debit_in_account_currency", "credit", "credit_in_account_currency"): + for field in ( + "account_currency", + "debit", + "debit_in_account_currency", + "credit", + "credit_in_account_currency", + ): for i, gle in enumerate(gl_entries): self.assertEqual(expected_values[gle.account][field], gle[field]) # cancel si.cancel() - gle = frappe.db.sql("""select name from `tabGL Entry` - where voucher_type='Sales Invoice' and voucher_no=%s""", si.name) + gle = frappe.db.sql( + """select name from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no=%s""", + si.name, + ) self.assertTrue(gle) + def test_invoice_exchange_rate(self): + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=1, + do_not_save=1, + ) + + self.assertRaises(frappe.ValidationError, si.save) + def test_invalid_currency(self): # Customer currency = USD # Transaction currency cannot be INR - si1 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - do_not_save=True) + si1 = create_sales_invoice( + customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", do_not_save=True + ) self.assertRaises(InvalidCurrency, si1.save) # Transaction currency cannot be EUR - si2 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="EUR", conversion_rate=80, do_not_save=True) + si2 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="EUR", + conversion_rate=80, + do_not_save=True, + ) self.assertRaises(InvalidCurrency, si2.save) # Transaction currency only allowed in USD - si3 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC", - currency="USD", conversion_rate=50) + si3 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=50, + ) # Party Account currency must be in USD, as there is existing GLE with USD - si4 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable - _TC", - currency="USD", conversion_rate=50, do_not_submit=True) + si4 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable - _TC", + currency="USD", + conversion_rate=50, + do_not_submit=True, + ) self.assertRaises(InvalidAccountCurrency, si4.submit) # Party Account currency must be in USD, force customer currency as there is no GLE si3.cancel() - si5 = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable - _TC", - currency="USD", conversion_rate=50, do_not_submit=True) + si5 = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable - _TC", + currency="USD", + conversion_rate=50, + do_not_submit=True, + ) self.assertRaises(InvalidAccountCurrency, si5.submit) @@ -1312,12 +1680,12 @@ class TestSalesInvoice(unittest.TestCase): si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True) price_list_rate = flt(100) * flt(si.plc_conversion_rate) si.items[0].price_list_rate = price_list_rate - si.items[0].margin_type = 'Percentage' + si.items[0].margin_type = "Percentage" si.items[0].margin_rate_or_amount = 25 si.items[0].discount_amount = 0.0 si.items[0].discount_percentage = 0.0 si.save() - self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) + self.assertEqual(si.get("items")[0].rate, flt((price_list_rate * 25) / 100 + price_list_rate)) def test_outstanding_amount_after_advance_jv_cancelation(self): from erpnext.accounts.doctype.journal_entry.test_journal_entry import ( @@ -1325,89 +1693,107 @@ class TestSalesInvoice(unittest.TestCase): ) jv = frappe.copy_doc(jv_test_records[0]) - jv.accounts[0].is_advance = 'Yes' + jv.accounts[0].is_advance = "Yes" jv.insert() jv.submit() si = frappe.copy_doc(test_records[0]) - si.append("advances", { - "doctype": "Sales Invoice Advance", - "reference_type": "Journal Entry", - "reference_name": jv.name, - "reference_row": jv.get("accounts")[0].name, - "advance_amount": 400, - "allocated_amount": 300, - "remarks": jv.remark - }) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.get("accounts")[0].name, + "advance_amount": 400, + "allocated_amount": 300, + "remarks": jv.remark, + }, + ) si.insert() si.submit() si.load_from_db() - #check outstanding after advance allocation - self.assertEqual(flt(si.outstanding_amount), - flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount"))) + # check outstanding after advance allocation + self.assertEqual( + flt(si.outstanding_amount), + flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")), + ) - #added to avoid Document has been modified exception + # added to avoid Document has been modified exception jv = frappe.get_doc("Journal Entry", jv.name) jv.cancel() si.load_from_db() - #check outstanding after advance cancellation - self.assertEqual(flt(si.outstanding_amount), - flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount"))) + # check outstanding after advance cancellation + self.assertEqual( + flt(si.outstanding_amount), + flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")), + ) def test_outstanding_amount_after_advance_payment_entry_cancelation(self): - pe = frappe.get_doc({ - "doctype": "Payment Entry", - "payment_type": "Receive", - "party_type": "Customer", - "party": "_Test Customer", - "company": "_Test Company", - "paid_from_account_currency": "INR", - "paid_to_account_currency": "INR", - "source_exchange_rate": 1, - "target_exchange_rate": 1, - "reference_no": "1", - "reference_date": nowdate(), - "received_amount": 300, - "paid_amount": 300, - "paid_from": "_Test Receivable - _TC", - "paid_to": "_Test Cash - _TC" - }) + pe = frappe.get_doc( + { + "doctype": "Payment Entry", + "payment_type": "Receive", + "party_type": "Customer", + "party": "_Test Customer", + "company": "_Test Company", + "paid_from_account_currency": "INR", + "paid_to_account_currency": "INR", + "source_exchange_rate": 1, + "target_exchange_rate": 1, + "reference_no": "1", + "reference_date": nowdate(), + "received_amount": 300, + "paid_amount": 300, + "paid_from": "_Test Receivable - _TC", + "paid_to": "_Test Cash - _TC", + } + ) pe.insert() pe.submit() si = frappe.copy_doc(test_records[0]) si.is_pos = 0 - si.append("advances", { - "doctype": "Sales Invoice Advance", - "reference_type": "Payment Entry", - "reference_name": pe.name, - "advance_amount": 300, - "allocated_amount": 300, - "remarks": pe.remarks - }) + si.append( + "advances", + { + "doctype": "Sales Invoice Advance", + "reference_type": "Payment Entry", + "reference_name": pe.name, + "advance_amount": 300, + "allocated_amount": 300, + "remarks": pe.remarks, + }, + ) si.insert() si.submit() si.load_from_db() - #check outstanding after advance allocation - self.assertEqual(flt(si.outstanding_amount), - flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount"))) + # check outstanding after advance allocation + self.assertEqual( + flt(si.outstanding_amount), + flt(si.rounded_total - si.total_advance, si.precision("outstanding_amount")), + ) - #added to avoid Document has been modified exception + # added to avoid Document has been modified exception pe = frappe.get_doc("Payment Entry", pe.name) pe.cancel() si.load_from_db() - #check outstanding after advance cancellation - self.assertEqual(flt(si.outstanding_amount), - flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount"))) + # check outstanding after advance cancellation + self.assertEqual( + flt(si.outstanding_amount), + flt(si.rounded_total + si.total_advance, si.precision("outstanding_amount")), + ) def test_multiple_uom_in_selling(self): - frappe.db.sql("""delete from `tabItem Price` - where price_list='_Test Price List' and item_code='_Test Item'""") + frappe.db.sql( + """delete from `tabItem Price` + where price_list='_Test Price List' and item_code='_Test Item'""" + ) item_price = frappe.new_doc("Item Price") item_price.price_list = "_Test Price List" item_price.item_code = "_Test Item" @@ -1421,9 +1807,18 @@ class TestSalesInvoice(unittest.TestCase): si.save() expected_values = { - "keys": ["price_list_rate", "stock_uom", "uom", "conversion_factor", "rate", "amount", - "base_price_list_rate", "base_rate", "base_amount"], - "_Test Item": [1000, "_Test UOM", "_Test UOM 1", 10.0, 1000, 1000, 1000, 1000, 1000] + "keys": [ + "price_list_rate", + "stock_uom", + "uom", + "conversion_factor", + "rate", + "amount", + "base_price_list_rate", + "base_rate", + "base_amount", + ], + "_Test Item": [1000, "_Test UOM", "_Test UOM 1", 10.0, 1000, 1000, 1000, 1000, 1000], } # check if the conversion_factor and price_list_rate is calculated according to uom @@ -1438,23 +1833,10 @@ class TestSalesInvoice(unittest.TestCase): itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si) expected_itemised_tax = { - "_Test Item": { - "Service Tax": { - "tax_rate": 10.0, - "tax_amount": 1000.0 - } - }, - "_Test Item 2": { - "Service Tax": { - "tax_rate": 10.0, - "tax_amount": 500.0 - } - } - } - expected_itemised_taxable_amount = { - "_Test Item": 10000.0, - "_Test Item 2": 5000.0 + "_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}}, + "_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}}, } + expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0} self.assertEqual(itemised_tax, expected_itemised_tax) self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount) @@ -1469,23 +1851,10 @@ class TestSalesInvoice(unittest.TestCase): itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si) expected_itemised_tax = { - "_Test Item": { - "Service Tax": { - "tax_rate": 10.0, - "tax_amount": 1000.0 - } - }, - "_Test Item 2": { - "Service Tax": { - "tax_rate": 10.0, - "tax_amount": 500.0 - } - } - } - expected_itemised_taxable_amount = { - "_Test Item": 10000.0, - "_Test Item 2": 5000.0 + "_Test Item": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 1000.0}}, + "_Test Item 2": {"Service Tax": {"tax_rate": 10.0, "tax_amount": 500.0}}, } + expected_itemised_taxable_amount = {"_Test Item": 10000.0, "_Test Item 2": 5000.0} self.assertEqual(itemised_tax, expected_itemised_tax) self.assertEqual(itemised_taxable_amount, expected_itemised_taxable_amount) @@ -1494,59 +1863,73 @@ class TestSalesInvoice(unittest.TestCase): def create_si_to_test_tax_breakup(self): si = create_sales_invoice(qty=100, rate=50, do_not_save=True) - si.append("items", { - "item_code": "_Test Item", - "gst_hsn_code": "999800", - "warehouse": "_Test Warehouse - _TC", - "qty": 100, - "rate": 50, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC" - }) - si.append("items", { - "item_code": "_Test Item 2", - "gst_hsn_code": "999800", - "warehouse": "_Test Warehouse - _TC", - "qty": 100, - "rate": 50, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC" - }) + si.append( + "items", + { + "item_code": "_Test Item", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": 100, + "rate": 50, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) + si.append( + "items", + { + "item_code": "_Test Item 2", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": 100, + "rate": 50, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "cost_center": "_Test Cost Center - _TC", - "description": "Service Tax", - "rate": 10 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "cost_center": "_Test Cost Center - _TC", + "description": "Service Tax", + "rate": 10, + }, + ) si.insert() return si def test_company_monthly_sales(self): - existing_current_month_sales = frappe.get_cached_value('Company', "_Test Company", "total_monthly_sales") + existing_current_month_sales = frappe.get_cached_value( + "Company", "_Test Company", "total_monthly_sales" + ) si = create_sales_invoice() - current_month_sales = frappe.get_cached_value('Company', "_Test Company", "total_monthly_sales") + current_month_sales = frappe.get_cached_value("Company", "_Test Company", "total_monthly_sales") self.assertEqual(current_month_sales, existing_current_month_sales + si.base_grand_total) si.cancel() - current_month_sales = frappe.get_cached_value('Company', "_Test Company", "total_monthly_sales") + current_month_sales = frappe.get_cached_value("Company", "_Test Company", "total_monthly_sales") self.assertEqual(current_month_sales, existing_current_month_sales) def test_rounding_adjustment(self): si = create_sales_invoice(rate=24900, do_not_save=True) for tax in ["Tax 1", "Tax2"]: - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account Service Tax - _TC", - "description": tax, - "rate": 14, - "cost_center": "_Test Cost Center - _TC", - "included_in_print_rate": 1 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account Service Tax - _TC", + "description": tax, + "rate": 14, + "cost_center": "_Test Cost Center - _TC", + "included_in_print_rate": 1, + }, + ) si.save() si.submit() self.assertEqual(si.net_total, 19453.13) @@ -1554,16 +1937,23 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.total_taxes_and_charges, 5446.88) self.assertEqual(si.rounding_adjustment, -0.01) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 24900, 0.0], - ["_Test Account Service Tax - _TC", 0.0, 5446.88], - ["Sales - _TC", 0.0, 19453.13], - ["Round Off - _TC", 0.01, 0.0] - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 24900, 0.0], + ["_Test Account Service Tax - _TC", 0.0, 5446.88], + ["Sales - _TC", 0.0, 19453.13], + ["Round Off - _TC", 0.01, 0.0], + ] + ) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) @@ -1573,24 +1963,30 @@ class TestSalesInvoice(unittest.TestCase): def test_rounding_adjustment_2(self): si = create_sales_invoice(rate=400, do_not_save=True) for rate in [400, 600, 100]: - si.append("items", { - "item_code": "_Test Item", - "gst_hsn_code": "999800", - "warehouse": "_Test Warehouse - _TC", - "qty": 1, - "rate": rate, - "income_account": "Sales - _TC", - "cost_center": "_Test Cost Center - _TC" - }) + si.append( + "items", + { + "item_code": "_Test Item", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": 1, + "rate": rate, + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]: - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": tax_account, - "description": tax_account, - "rate": 9, - "cost_center": "_Test Cost Center - _TC", - "included_in_print_rate": 1 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": tax_account, + "description": tax_account, + "rate": 9, + "cost_center": "_Test Cost Center - _TC", + "included_in_print_rate": 1, + }, + ) si.save() si.submit() self.assertEqual(si.net_total, 1271.19) @@ -1598,26 +1994,121 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.total_taxes_and_charges, 228.82) self.assertEqual(si.rounding_adjustment, -0.01) - expected_values = dict((d[0], d) for d in [ - [si.debit_to, 1500, 0.0], - ["_Test Account Service Tax - _TC", 0.0, 114.41], - ["_Test Account VAT - _TC", 0.0, 114.41], - ["Sales - _TC", 0.0, 1271.18] - ]) + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 1500, 0.0], + ["_Test Account Service Tax - _TC", 0.0, 114.41], + ["_Test Account VAT - _TC", 0.0, 114.41], + ["Sales - _TC", 0.0, 1271.18], + ] + ) - gl_entries = frappe.db.sql("""select account, debit, credit + gl_entries = frappe.db.sql( + """select account, debit, credit from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) for gle in gl_entries: self.assertEqual(expected_values[gle.account][0], gle.account) self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) + def test_rounding_adjustment_3(self): + from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import ( + create_dimension, + disable_dimension, + ) + + create_dimension() + + si = create_sales_invoice(do_not_save=True) + si.items = [] + for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: + si.append( + "items", + { + "item_code": "_Test Item", + "gst_hsn_code": "999800", + "warehouse": "_Test Warehouse - _TC", + "qty": d[1], + "rate": d[0], + "income_account": "Sales - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) + for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]: + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": tax_account, + "description": tax_account, + "rate": 6, + "cost_center": "_Test Cost Center - _TC", + "included_in_print_rate": 1, + }, + ) + + si.cost_center = "_Test Cost Center 2 - _TC" + si.location = "Block 1" + + si.save() + si.submit() + self.assertEqual(si.net_total, 4007.16) + self.assertEqual(si.grand_total, 4488.02) + self.assertEqual(si.total_taxes_and_charges, 480.86) + self.assertEqual(si.rounding_adjustment, -0.02) + + expected_values = dict( + (d[0], d) + for d in [ + [si.debit_to, 4488.0, 0.0], + ["_Test Account Service Tax - _TC", 0.0, 240.43], + ["_Test Account VAT - _TC", 0.0, 240.43], + ["Sales - _TC", 0.0, 4007.15], + ["Round Off - _TC", 0.01, 0], + ] + ) + + gl_entries = frappe.db.sql( + """select account, debit, credit + from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s + order by account asc""", + si.name, + as_dict=1, + ) + + debit_credit_diff = 0 + for gle in gl_entries: + self.assertEqual(expected_values[gle.account][0], gle.account) + self.assertEqual(expected_values[gle.account][1], gle.debit) + self.assertEqual(expected_values[gle.account][2], gle.credit) + debit_credit_diff += gle.debit - gle.credit + + self.assertEqual(debit_credit_diff, 0) + + round_off_gle = frappe.db.get_value( + "GL Entry", + {"voucher_type": "Sales Invoice", "voucher_no": si.name, "account": "Round Off - _TC"}, + ["cost_center", "location"], + as_dict=1, + ) + + self.assertEqual(round_off_gle.cost_center, "_Test Cost Center 2 - _TC") + self.assertEqual(round_off_gle.location, "Block 1") + + disable_dimension() + def test_sales_invoice_with_shipping_rule(self): from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule - shipping_rule = create_shipping_rule(shipping_rule_type = "Selling", shipping_rule_name = "Shipping Rule - Sales Invoice Test") + shipping_rule = create_shipping_rule( + shipping_rule_type="Selling", shipping_rule_name="Shipping Rule - Sales Invoice Test" + ) si = frappe.copy_doc(test_records[2]) @@ -1630,29 +2121,32 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(si.total_taxes_and_charges, 468.85) self.assertEqual(si.grand_total, 1718.85) - - def test_create_invoice_without_terms(self): si = create_sales_invoice(do_not_save=1) - self.assertFalse(si.get('payment_schedule')) + self.assertFalse(si.get("payment_schedule")) si.insert() - self.assertTrue(si.get('payment_schedule')) + self.assertTrue(si.get("payment_schedule")) def test_duplicate_due_date_in_terms(self): si = create_sales_invoice(do_not_save=1) - si.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50)) - si.append('payment_schedule', dict(due_date='2017-01-01', invoice_portion=50.00, payment_amount=50)) + si.append( + "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) + ) + si.append( + "payment_schedule", dict(due_date="2017-01-01", invoice_portion=50.00, payment_amount=50) + ) self.assertRaises(frappe.ValidationError, si.insert) def test_credit_note(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry - si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1) + si = create_sales_invoice(item_code="_Test Item", qty=(5 * -1), rate=500, is_return=1) - outstanding_amount = get_outstanding_amount(si.doctype, - si.name, "Debtors - _TC", si.customer, "Customer") + outstanding_amount = get_outstanding_amount( + si.doctype, si.name, "Debtors - _TC", si.customer, "Customer" + ) self.assertEqual(si.outstanding_amount, outstanding_amount) @@ -1667,30 +2161,31 @@ class TestSalesInvoice(unittest.TestCase): pe.insert() pe.submit() - si_doc = frappe.get_doc('Sales Invoice', si.name) + si_doc = frappe.get_doc("Sales Invoice", si.name) self.assertEqual(si_doc.outstanding_amount, 0) def test_sales_invoice_with_cost_center(self): from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center + cost_center = "_Test Cost Center for BS Account - _TC" create_cost_center(cost_center_name="_Test Cost Center for BS Account", company="_Test Company") - si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") + si = create_sales_invoice_against_cost_center(cost_center=cost_center, debit_to="Debtors - _TC") self.assertEqual(si.cost_center, cost_center) expected_values = { - "Debtors - _TC": { - "cost_center": cost_center - }, - "Sales - _TC": { - "cost_center": cost_center - } + "Debtors - _TC": {"cost_center": cost_center}, + "Sales - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -1700,16 +2195,20 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_with_project_link(self): from erpnext.projects.doctype.project.test_project import make_project - project = make_project({ - 'project_name': 'Sales Invoice Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2020-01-01' - }) - item_project = make_project({ - 'project_name': 'Sales Invoice Item Project', - 'project_template_name': 'Test Project Template', - 'start_date': '2019-06-01' - }) + project = make_project( + { + "project_name": "Sales Invoice Project", + "project_template_name": "Test Project Template", + "start_date": "2020-01-01", + } + ) + item_project = make_project( + { + "project_name": "Sales Invoice Item Project", + "project_template_name": "Test Project Template", + "start_date": "2019-06-01", + } + ) sales_invoice = create_sales_invoice(do_not_save=1) sales_invoice.items[0].project = item_project.name @@ -1718,18 +2217,18 @@ class TestSalesInvoice(unittest.TestCase): sales_invoice.submit() expected_values = { - "Debtors - _TC": { - "project": project.name - }, - "Sales - _TC": { - "project": item_project.name - } + "Debtors - _TC": {"project": project.name}, + "Sales - _TC": {"project": item_project.name}, } - gl_entries = frappe.db.sql("""select account, cost_center, project, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, project, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", sales_invoice.name, as_dict=1) + order by account asc""", + sales_invoice.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -1738,21 +2237,21 @@ class TestSalesInvoice(unittest.TestCase): def test_sales_invoice_without_cost_center(self): cost_center = "_Test Cost Center - _TC" - si = create_sales_invoice(debit_to="Debtors - _TC") + si = create_sales_invoice(debit_to="Debtors - _TC") expected_values = { - "Debtors - _TC": { - "cost_center": None - }, - "Sales - _TC": { - "cost_center": cost_center - } + "Debtors - _TC": {"cost_center": None}, + "Sales - _TC": {"cost_center": cost_center}, } - gl_entries = frappe.db.sql("""select account, cost_center, account_currency, debit, credit, + gl_entries = frappe.db.sql( + """select account, cost_center, account_currency, debit, credit, debit_in_account_currency, credit_in_account_currency from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s - order by account asc""", si.name, as_dict=1) + order by account asc""", + si.name, + as_dict=1, + ) self.assertTrue(gl_entries) @@ -1760,8 +2259,11 @@ class TestSalesInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center) def test_deferred_revenue(self): - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") + deferred_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) item = create_item("_Test Item for Deferred Accounting") item.enable_deferred_revenue = 1 @@ -1777,14 +2279,16 @@ class TestSalesInvoice(unittest.TestCase): si.save() si.submit() - pda1 = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company" - )) + pda1 = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company", + ) + ) pda1.insert() pda1.submit() @@ -1795,58 +2299,28 @@ class TestSalesInvoice(unittest.TestCase): [deferred_account, 43.08, 0.0, "2019-02-28"], ["Sales - _TC", 0.0, 43.08, "2019-02-28"], [deferred_account, 23.07, 0.0, "2019-03-15"], - ["Sales - _TC", 0.0, 23.07, "2019-03-15"] + ["Sales - _TC", 0.0, 23.07, "2019-03-15"], ] check_gl_entries(self, si.name, expected_gle, "2019-01-30") - def test_deferred_revenue_post_account_freeze_upto_by_admin(self): - frappe.set_user("Administrator") - - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) - frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None) - - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") - - item = create_item("_Test Item for Deferred Accounting") - item.enable_deferred_revenue = 1 - item.deferred_revenue_account = deferred_account - item.no_of_months = 12 - item.save() - - si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True) + def test_deferred_revenue_missing_account(self): + si = create_sales_invoice(posting_date="2019-01-10", do_not_submit=True) si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2019-01-10" si.items[0].service_end_date = "2019-03-15" - si.items[0].deferred_revenue_account = deferred_account - si.save() - si.submit() - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31')) - frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager') - - pda1 = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date=nowdate(), - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company" - )) - - pda1.insert() - self.assertRaises(frappe.ValidationError, pda1.submit) - - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) - frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None) + self.assertRaises(frappe.ValidationError, si.save) def test_fixed_deferred_revenue(self): - deferred_account = create_account(account_name="Deferred Revenue", - parent_account="Current Liabilities - _TC", company="_Test Company") + deferred_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - acc_settings.book_deferred_entries_based_on = 'Months' + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") + acc_settings.book_deferred_entries_based_on = "Months" acc_settings.save() item = create_item("_Test Item for Deferred Accounting") @@ -1855,7 +2329,9 @@ class TestSalesInvoice(unittest.TestCase): item.no_of_months = 12 item.save() - si = create_sales_invoice(item=item.name, posting_date="2019-01-16", rate=50000, do_not_submit=True) + si = create_sales_invoice( + item=item.name, posting_date="2019-01-16", rate=50000, do_not_submit=True + ) si.items[0].enable_deferred_revenue = 1 si.items[0].service_start_date = "2019-01-16" si.items[0].service_end_date = "2019-03-31" @@ -1863,14 +2339,16 @@ class TestSalesInvoice(unittest.TestCase): si.save() si.submit() - pda1 = frappe.get_doc(dict( - doctype='Process Deferred Accounting', - posting_date='2019-03-31', - start_date="2019-01-01", - end_date="2019-03-31", - type="Income", - company="_Test Company" - )) + pda1 = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date="2019-03-31", + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company", + ) + ) pda1.insert() pda1.submit() @@ -1881,13 +2359,13 @@ class TestSalesInvoice(unittest.TestCase): [deferred_account, 20000.0, 0.0, "2019-02-28"], ["Sales - _TC", 0.0, 20000.0, "2019-02-28"], [deferred_account, 20000.0, 0.0, "2019-03-31"], - ["Sales - _TC", 0.0, 20000.0, "2019-03-31"] + ["Sales - _TC", 0.0, 20000.0, "2019-03-31"], ] check_gl_entries(self, si.name, expected_gle, "2019-01-30") - acc_settings = frappe.get_doc('Accounts Settings', 'Accounts Settings') - acc_settings.book_deferred_entries_based_on = 'Days' + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") + acc_settings.book_deferred_entries_based_on = "Days" acc_settings.save() def test_inter_company_transaction(self): @@ -1896,45 +2374,47 @@ class TestSalesInvoice(unittest.TestCase): create_internal_customer( customer_name="_Test Internal Customer", represents_company="_Test Company 1", - allowed_to_interact_with="Wind Power LLC" + allowed_to_interact_with="Wind Power LLC", ) if not frappe.db.exists("Supplier", "_Test Internal Supplier"): - supplier = frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": "_Test Internal Supplier", - "doctype": "Supplier", - "is_internal_supplier": 1, - "represents_company": "Wind Power LLC" - }) + supplier = frappe.get_doc( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": "_Test Internal Supplier", + "doctype": "Supplier", + "is_internal_supplier": 1, + "represents_company": "Wind Power LLC", + } + ) - supplier.append("companies", { - "company": "_Test Company 1" - }) + supplier.append("companies", {"company": "_Test Company 1"}) supplier.insert() si = create_sales_invoice( - company = "Wind Power LLC", - customer = "_Test Internal Customer", - debit_to = "Debtors - WP", - warehouse = "Stores - WP", - income_account = "Sales - WP", - expense_account = "Cost of Goods Sold - WP", - cost_center = "Main - WP", - currency = "USD", - do_not_save = 1 + company="Wind Power LLC", + customer="_Test Internal Customer", + debit_to="Debtors - WP", + warehouse="Stores - WP", + income_account="Sales - WP", + expense_account="Cost of Goods Sold - WP", + cost_center="Main - WP", + currency="USD", + do_not_save=1, ) si.selling_price_list = "_Test Price List Rest of the World" si.submit() target_doc = make_inter_company_transaction("Sales Invoice", si.name) - target_doc.items[0].update({ - "expense_account": "Cost of Goods Sold - _TC1", - "cost_center": "Main - _TC1", - "warehouse": "Stores - _TC1" - }) + target_doc.items[0].update( + { + "expense_account": "Cost of Goods Sold - _TC1", + "cost_center": "Main - _TC1", + "warehouse": "Stores - _TC1", + } + ) target_doc.submit() self.assertEqual(target_doc.company, "_Test Company 1") @@ -1944,9 +2424,9 @@ class TestSalesInvoice(unittest.TestCase): se = make_stock_entry( item_code="138-CMS Shoe", target="Finished Goods - _TC", - company = "_Test Company", + company="_Test Company", qty=1, - basic_rate=500 + basic_rate=500, ) si = frappe.copy_doc(test_records[0]) @@ -1958,8 +2438,9 @@ class TestSalesInvoice(unittest.TestCase): si.insert() si.submit() - sles = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": si.name}, - fields=["name", "actual_qty"]) + sles = frappe.get_all( + "Stock Ledger Entry", filters={"voucher_no": si.name}, fields=["name", "actual_qty"] + ) # check if both SLEs are created self.assertEqual(len(sles), 2) @@ -1973,82 +2454,92 @@ class TestSalesInvoice(unittest.TestCase): ## Create internal transfer account from erpnext.selling.doctype.customer.test_customer import create_internal_customer - account = create_account(account_name="Unrealized Profit", - parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory") + account = create_account( + account_name="Unrealized Profit", + parent_account="Current Liabilities - TCP1", + company="_Test Company with perpetual inventory", + ) - frappe.db.set_value('Company', '_Test Company with perpetual inventory', - 'unrealized_profit_loss_account', account) + frappe.db.set_value( + "Company", "_Test Company with perpetual inventory", "unrealized_profit_loss_account", account + ) - customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory", - "_Test Company with perpetual inventory") + customer = create_internal_customer( + "_Test Internal Customer 2", + "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory", + ) - create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory", - "_Test Company with perpetual inventory") + create_internal_supplier( + "_Test Internal Supplier 2", + "_Test Company with perpetual inventory", + "_Test Company with perpetual inventory", + ) si = create_sales_invoice( - company = "_Test Company with perpetual inventory", - customer = customer, - debit_to = "Debtors - TCP1", - warehouse = "Stores - TCP1", - income_account = "Sales - TCP1", - expense_account = "Cost of Goods Sold - TCP1", - cost_center = "Main - TCP1", - currency = "INR", - do_not_save = 1 + company="_Test Company with perpetual inventory", + customer=customer, + debit_to="Debtors - TCP1", + warehouse="Stores - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + cost_center="Main - TCP1", + currency="INR", + do_not_save=1, ) si.selling_price_list = "_Test Price List Rest of the World" si.update_stock = 1 - si.items[0].target_warehouse = 'Work In Progress - TCP1' + si.items[0].target_warehouse = "Work In Progress - TCP1" # Add stock to stores for succesful stock transfer make_stock_entry( - target="Stores - TCP1", - company = "_Test Company with perpetual inventory", - qty=1, - basic_rate=100 + target="Stores - TCP1", company="_Test Company with perpetual inventory", qty=1, basic_rate=100 ) add_taxes(si) si.save() rate = 0.0 - for d in si.get('items'): - rate = get_incoming_rate({ - "item_code": d.item_code, - "warehouse": d.warehouse, - "posting_date": si.posting_date, - "posting_time": si.posting_time, - "qty": -1 * flt(d.get('stock_qty')), - "serial_no": d.serial_no, - "company": si.company, - "voucher_type": 'Sales Invoice', - "voucher_no": si.name, - "allow_zero_valuation": d.get("allow_zero_valuation") - }, raise_error_if_no_rate=False) + for d in si.get("items"): + rate = get_incoming_rate( + { + "item_code": d.item_code, + "warehouse": d.warehouse, + "posting_date": si.posting_date, + "posting_time": si.posting_time, + "qty": -1 * flt(d.get("stock_qty")), + "serial_no": d.serial_no, + "company": si.company, + "voucher_type": "Sales Invoice", + "voucher_no": si.name, + "allow_zero_valuation": d.get("allow_zero_valuation"), + }, + raise_error_if_no_rate=False, + ) rate = flt(rate, 2) si.submit() target_doc = make_inter_company_transaction("Sales Invoice", si.name) - target_doc.company = '_Test Company with perpetual inventory' - target_doc.items[0].warehouse = 'Finished Goods - TCP1' + target_doc.company = "_Test Company with perpetual inventory" + target_doc.items[0].warehouse = "Finished Goods - TCP1" add_taxes(target_doc) target_doc.save() target_doc.submit() - tax_amount = flt(rate * (12/100), 2) + tax_amount = flt(rate * (12 / 100), 2) si_gl_entries = [ ["_Test Account Excise Duty - TCP1", 0.0, tax_amount, nowdate()], - ["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()] + ["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()], ] check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1)) pi_gl_entries = [ - ["_Test Account Excise Duty - TCP1", tax_amount , 0.0, nowdate()], - ["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()] + ["_Test Account Excise Duty - TCP1", tax_amount, 0.0, nowdate()], + ["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()], ] # Sale and Purchase both should be at valuation rate @@ -2064,44 +2555,48 @@ class TestSalesInvoice(unittest.TestCase): data = get_ewb_data("Sales Invoice", [si.name]) - self.assertEqual(data['version'], '1.0.0421') - self.assertEqual(data['billLists'][0]['fromGstin'], '27AAECE4835E1ZR') - self.assertEqual(data['billLists'][0]['fromTrdName'], '_Test Company') - self.assertEqual(data['billLists'][0]['toTrdName'], '_Test Customer') - self.assertEqual(data['billLists'][0]['vehicleType'], 'R') - self.assertEqual(data['billLists'][0]['totalValue'], 60000) - self.assertEqual(data['billLists'][0]['cgstValue'], 5400) - self.assertEqual(data['billLists'][0]['sgstValue'], 5400) - self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234') - self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000) - self.assertEqual(data['billLists'][0]['actualFromStateCode'],7) - self.assertEqual(data['billLists'][0]['fromStateCode'],27) + self.assertEqual(data["version"], "1.0.0421") + self.assertEqual(data["billLists"][0]["fromGstin"], "27AAECE4835E1ZR") + self.assertEqual(data["billLists"][0]["fromTrdName"], "_Test Company") + self.assertEqual(data["billLists"][0]["toTrdName"], "_Test Customer") + self.assertEqual(data["billLists"][0]["vehicleType"], "R") + self.assertEqual(data["billLists"][0]["totalValue"], 60000) + self.assertEqual(data["billLists"][0]["cgstValue"], 5400) + self.assertEqual(data["billLists"][0]["sgstValue"], 5400) + self.assertEqual(data["billLists"][0]["vehicleNo"], "KA12KA1234") + self.assertEqual(data["billLists"][0]["itemList"][0]["taxableAmount"], 60000) + self.assertEqual(data["billLists"][0]["actualFromStateCode"], 7) + self.assertEqual(data["billLists"][0]["fromStateCode"], 27) def test_einvoice_submission_without_irn(self): # init - einvoice_settings = frappe.get_doc('E Invoice Settings') + einvoice_settings = frappe.get_doc("E Invoice Settings") einvoice_settings.enable = 1 einvoice_settings.applicable_from = nowdate() - einvoice_settings.append('credentials', { - 'company': '_Test Company', - 'gstin': '27AAECE4835E1ZR', - 'username': 'test', - 'password': 'test' - }) + einvoice_settings.append( + "credentials", + { + "company": "_Test Company", + "gstin": "27AAECE4835E1ZR", + "username": "test", + "password": "test", + }, + ) einvoice_settings.save() country = frappe.flags.country - frappe.flags.country = 'India' + frappe.flags.country = "India" si = make_sales_invoice_for_ewaybill() self.assertRaises(frappe.ValidationError, si.submit) - si.irn = 'test_irn' + si.irn = "test_irn" si.submit() # reset - einvoice_settings = frappe.get_doc('E Invoice Settings') + einvoice_settings = frappe.get_doc("E Invoice Settings") einvoice_settings.enable = 0 + einvoice_settings.save() frappe.flags.country = country def test_einvoice_json(self): @@ -2112,45 +2607,265 @@ class TestSalesInvoice(unittest.TestCase): si.save() einvoice = make_einvoice(si) - self.assertTrue(einvoice['EwbDtls']) + self.assertTrue(einvoice["EwbDtls"]) validate_totals(einvoice) - si.apply_discount_on = 'Net Total' + si.apply_discount_on = "Net Total" si.save() einvoice = make_einvoice(si) validate_totals(einvoice) - [d.set('included_in_print_rate', 1) for d in si.taxes] + [d.set("included_in_print_rate", 1) for d in si.taxes] si.save() einvoice = make_einvoice(si) validate_totals(einvoice) + def test_einvoice_discounts(self): + from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals + + frappe.db.set_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice", False) + + # Normal Itemized Discount + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "" + si.items[0].price_list_rate = 12 + si.items[0].discount_percentage = 16.6666666667 + si.items[0].rate = 10 + + si.items[1].price_list_rate = 15 + si.items[1].discount_amount = 5 + si.items[1].rate = 10 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 4000) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 2100) + self.assertEqual(einvoice["ItemList"][2]["Discount"], 222) + self.assertEqual(einvoice["ItemList"][3]["Discount"], 5555) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12) + self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15) + self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20) + self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10) + + # Invoice Discount on net total + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Net Total" + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 253.61) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 66.57) + self.assertEqual(einvoice["ItemList"][2]["Discount"], 243.11) + self.assertEqual(einvoice["ItemList"][3]["Discount"], 5613.71) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12) + self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15) + self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20) + self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10) + + # Invoice Discount on grand total (Itemized Discount) + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Grand Total" + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 214.93) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 56.42) + self.assertEqual(einvoice["ItemList"][2]["Discount"], 239.89) + self.assertEqual(einvoice["ItemList"][3]["Discount"], 5604.75) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12) + self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15) + self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20) + self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10) + + # Invoice Discount on grand total (Cash/Non-Trade Discount) + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Grand Total" + si.is_cash_or_non_trade_discount = 1 + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][2]["Discount"], 222.0) + self.assertEqual(einvoice["ItemList"][3]["Discount"], 5555.0) + self.assertEqual(einvoice["ValDtls"]["Discount"], 400) + + self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12) + self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15) + self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 20) + self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 10) + + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "" + si.items[1].price_list_rate = 15 + si.items[1].discount_amount = -5 + si.items[1].rate = 20 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][1]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20) + + def test_einvoice_without_discounts(self): + from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals + + frappe.db.set_single_value("E Invoice Settings", "dont_show_discounts_in_e_invoice", True) + + # Normal Itemized Discount + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "" + si.items[0].price_list_rate = 12 + si.items[0].discount_percentage = 16.6666666667 + si.items[0].rate = 10 + + si.items[1].price_list_rate = 15 + si.items[1].discount_amount = 5 + si.items[1].rate = 10 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][2]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][3]["Discount"], 0) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 10) + self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 10) + self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 18) + self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 5) + + # Invoice Discount on net total + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Net Total" + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][2]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][3]["Discount"], 0) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 11.87) + self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 14.84) + self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 17.81) + self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 4.95) + + # Invoice Discount on grand total (Itemized Discount) + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Grand Total" + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][2]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][3]["Discount"], 0) + self.assertEqual(einvoice["ValDtls"]["Discount"], 0) + + self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 11.89) + self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 14.87) + self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 17.84) + self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 4.96) + + # Invoice Discount on grand total (Cash/Non-Trade Discount) + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "Grand Total" + si.is_cash_or_non_trade_discount = 1 + si.discount_amount = 400 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][0]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][1]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][2]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][3]["Discount"], 0) + self.assertEqual(einvoice["ValDtls"]["Discount"], 400) + + self.assertEqual(einvoice["ItemList"][0]["UnitPrice"], 12) + self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 15) + self.assertEqual(einvoice["ItemList"][2]["UnitPrice"], 18) + self.assertEqual(einvoice["ItemList"][3]["UnitPrice"], 5) + + si = get_sales_invoice_for_e_invoice() + si.apply_discount_on = "" + si.items[1].price_list_rate = 15 + si.items[1].discount_amount = -5 + si.items[1].rate = 20 + si.save() + + einvoice = make_einvoice(si) + validate_totals(einvoice) + + self.assertEqual(einvoice["ItemList"][1]["Discount"], 0) + self.assertEqual(einvoice["ItemList"][1]["UnitPrice"], 20) + def test_item_tax_net_range(self): item = create_item("T Shirt") - item.set('taxes', []) - item.append("taxes", { - "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", - "minimum_net_rate": 0, - "maximum_net_rate": 500 - }) + item.set("taxes", []) + item.append( + "taxes", + { + "item_tax_template": "_Test Account Excise Duty @ 10 - _TC", + "minimum_net_rate": 0, + "maximum_net_rate": 500, + }, + ) - item.append("taxes", { - "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", - "minimum_net_rate": 501, - "maximum_net_rate": 1000 - }) + item.append( + "taxes", + { + "item_tax_template": "_Test Account Excise Duty @ 12 - _TC", + "minimum_net_rate": 501, + "maximum_net_rate": 1000, + }, + ) item.save() - sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True) - self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC") + sales_invoice = create_sales_invoice(item="T Shirt", rate=700, do_not_submit=True) + self.assertEqual( + sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC" + ) # Apply discount - sales_invoice.apply_discount_on = 'Net Total' + sales_invoice.apply_discount_on = "Net Total" sales_invoice.discount_amount = 300 sales_invoice.save() - self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC") + self.assertEqual( + sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC" + ) def test_sales_invoice_with_discount_accounting_enabled(self): from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import ( @@ -2159,14 +2874,17 @@ class TestSalesInvoice(unittest.TestCase): enable_discount_accounting() - discount_account = create_account(account_name="Discount Account", - parent_account="Indirect Expenses - _TC", company="_Test Company") + discount_account = create_account( + account_name="Discount Account", + parent_account="Indirect Expenses - _TC", + company="_Test Company", + ) si = create_sales_invoice(discount_account=discount_account, discount_percentage=10, rate=90) expected_gle = [ ["Debtors - _TC", 90.0, 0.0, nowdate()], ["Discount Account - _TC", 10.0, 0.0, nowdate()], - ["Sales - _TC", 0.0, 100.0, nowdate()] + ["Sales - _TC", 0.0, 100.0, nowdate()], ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) @@ -2178,27 +2896,33 @@ class TestSalesInvoice(unittest.TestCase): ) enable_discount_accounting() - additional_discount_account = create_account(account_name="Discount Account", - parent_account="Indirect Expenses - _TC", company="_Test Company") + additional_discount_account = create_account( + account_name="Discount Account", + parent_account="Indirect Expenses - _TC", + company="_Test Company", + ) - si = create_sales_invoice(parent_cost_center='Main - _TC', do_not_save=1) + si = create_sales_invoice(parent_cost_center="Main - _TC", do_not_save=1) si.apply_discount_on = "Grand Total" si.additional_discount_account = additional_discount_account si.additional_discount_percentage = 20 - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "_Test Account VAT - _TC", - "cost_center": "Main - _TC", - "description": "Test", - "rate": 10 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "description": "Test", + "rate": 10, + }, + ) si.submit() expected_gle = [ ["_Test Account VAT - _TC", 0.0, 10.0, nowdate()], ["Debtors - _TC", 88, 0.0, nowdate()], ["Discount Account - _TC", 22.0, 0.0, nowdate()], - ["Sales - _TC", 0.0, 100.0, nowdate()] + ["Sales - _TC", 0.0, 100.0, nowdate()], ] check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1)) @@ -2206,20 +2930,22 @@ class TestSalesInvoice(unittest.TestCase): def test_asset_depreciation_on_sale_with_pro_rata(self): """ - Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. + Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale. """ create_asset_data() asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1) post_depreciation_entries(getdate("2021-09-30")) - create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")) + create_sales_invoice( + item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30") + ) asset.load_from_db() expected_values = [ - ["2020-06-30", 1311.48, 1311.48], - ["2021-06-30", 20000.0, 21311.48], - ["2021-09-30", 5041.1, 26352.58] + ["2020-06-30", 1366.12, 1366.12], + ["2021-06-30", 20000.0, 21366.12], + ["2021-09-30", 5041.1, 26407.22], ] for i, schedule in enumerate(asset.schedules): @@ -2230,23 +2956,28 @@ class TestSalesInvoice(unittest.TestCase): def test_asset_depreciation_on_sale_without_pro_rata(self): """ - Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale. + Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale. """ create_asset_data() - asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, - available_for_use_date=getdate("2019-12-31"), total_number_of_depreciations=3, - expected_value_after_useful_life=10000, depreciation_start_date=getdate("2020-12-31"), submit=1) + asset = create_asset( + item_code="Macbook Pro", + calculate_depreciation=1, + available_for_use_date=getdate("2019-12-31"), + total_number_of_depreciations=3, + expected_value_after_useful_life=10000, + depreciation_start_date=getdate("2020-12-31"), + submit=1, + ) post_depreciation_entries(getdate("2021-09-30")) - create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31")) + create_sales_invoice( + item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31") + ) asset.load_from_db() - expected_values = [ - ["2020-12-31", 30000, 30000], - ["2021-12-31", 30000, 60000] - ] + expected_values = [["2020-12-31", 30000, 30000], ["2021-12-31", 30000, 60000]] for i, schedule in enumerate(asset.schedules): self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date) @@ -2261,18 +2992,20 @@ class TestSalesInvoice(unittest.TestCase): asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1) post_depreciation_entries(getdate("2021-09-30")) - si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30")) + si = create_sales_invoice( + item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30") + ) return_si = make_return_doc("Sales Invoice", si.name) return_si.submit() asset.load_from_db() expected_values = [ - ["2020-06-30", 1311.48, 1311.48, True], - ["2021-06-30", 20000.0, 21311.48, True], - ["2022-06-30", 20000.0, 41311.48, False], - ["2023-06-30", 20000.0, 61311.48, False], - ["2024-06-30", 20000.0, 81311.48, False], - ["2025-06-06", 18688.52, 100000.0, False] + ["2020-06-30", 1366.12, 1366.12, True], + ["2021-06-30", 20000.0, 21366.12, True], + ["2022-06-30", 20000.0, 41366.12, False], + ["2023-06-30", 20000.0, 61366.12, False], + ["2024-06-30", 20000.0, 81366.12, False], + ["2025-06-06", 18633.88, 100000.0, False], ] for i, schedule in enumerate(asset.schedules): @@ -2297,30 +3030,34 @@ class TestSalesInvoice(unittest.TestCase): party_link = create_party_link("Supplier", supplier, customer) # enable common party accounting - frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 1) + frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 1) # create a sales invoice si = create_sales_invoice(customer=customer, parent_cost_center="_Test Cost Center - _TC") # check outstanding of sales invoice si.reload() - self.assertEqual(si.status, 'Paid') + self.assertEqual(si.status, "Paid") self.assertEqual(flt(si.outstanding_amount), 0.0) # check creation of journal entry - jv = frappe.get_all('Journal Entry Account', { - 'account': si.debit_to, - 'party_type': 'Customer', - 'party': si.customer, - 'reference_type': si.doctype, - 'reference_name': si.name - }, pluck='credit_in_account_currency') + jv = frappe.get_all( + "Journal Entry Account", + { + "account": si.debit_to, + "party_type": "Customer", + "party": si.customer, + "reference_type": si.doctype, + "reference_name": si.name, + }, + pluck="credit_in_account_currency", + ) self.assertTrue(jv) self.assertEqual(jv[0], si.grand_total) party_link.delete() - frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0) + frappe.db.set_value("Accounts Settings", None, "enable_common_party_accounting", 0) def test_payment_statuses(self): from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry @@ -2330,16 +3067,14 @@ class TestSalesInvoice(unittest.TestCase): # Test Overdue si = create_sales_invoice(do_not_submit=True) si.payment_schedule = [] - si.append("payment_schedule", { - "due_date": add_days(today, -5), - "invoice_portion": 50, - "payment_amount": si.grand_total / 2 - }) - si.append("payment_schedule", { - "due_date": add_days(today, 5), - "invoice_portion": 50, - "payment_amount": si.grand_total / 2 - }) + si.append( + "payment_schedule", + {"due_date": add_days(today, -5), "invoice_portion": 50, "payment_amount": si.grand_total / 2}, + ) + si.append( + "payment_schedule", + {"due_date": add_days(today, 5), "invoice_portion": 50, "payment_amount": si.grand_total / 2}, + ) si.submit() self.assertEqual(si.status, "Overdue") @@ -2370,15 +3105,63 @@ class TestSalesInvoice(unittest.TestCase): si.reload() self.assertEqual(si.status, "Paid") + def test_update_invoice_status(self): + today = nowdate() + + # Sales Invoice without Payment Schedule + si = create_sales_invoice(posting_date=add_days(today, -5)) + + # Sales Invoice with Payment Schedule + si_with_payment_schedule = create_sales_invoice(do_not_submit=True) + si_with_payment_schedule.extend( + "payment_schedule", + [ + { + "due_date": add_days(today, -5), + "invoice_portion": 50, + "payment_amount": si_with_payment_schedule.grand_total / 2, + }, + { + "due_date": add_days(today, 5), + "invoice_portion": 50, + "payment_amount": si_with_payment_schedule.grand_total / 2, + }, + ], + ) + si_with_payment_schedule.submit() + + for invoice in (si, si_with_payment_schedule): + invoice.db_set("status", "Unpaid") + update_invoice_status() + invoice.reload() + self.assertEqual(invoice.status, "Overdue") + + invoice.db_set("status", "Unpaid and Discounted") + update_invoice_status() + invoice.reload() + self.assertEqual(invoice.status, "Overdue and Discounted") + def test_sales_commission(self): - si = frappe.copy_doc(test_records[0]) - item = copy.deepcopy(si.get('items')[0]) - item.update({ - "qty": 1, - "rate": 500, - "grant_commission": 1 - }) - si.append("items", item) + si = frappe.copy_doc(test_records[2]) + + frappe.db.set_value("Item", si.get("items")[0].item_code, "grant_commission", 1) + frappe.db.set_value("Item", si.get("items")[1].item_code, "grant_commission", 0) + + item = copy.deepcopy(si.get("items")[0]) + item.update( + { + "qty": 1, + "rate": 500, + } + ) + + item = copy.deepcopy(si.get("items")[1]) + item.update( + { + "qty": 1, + "rate": 500, + } + ) # Test valid values for commission_rate, total_commission in ((0, 0), (10, 50), (100, 500)): @@ -2394,7 +3177,7 @@ class TestSalesInvoice(unittest.TestCase): self.assertRaises(frappe.ValidationError, si.save) def test_sales_invoice_submission_post_account_freezing_date(self): - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1)) + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", add_days(getdate(), 1)) si = create_sales_invoice(do_not_save=True) si.posting_date = add_days(getdate(), 1) si.save() @@ -2403,17 +3186,19 @@ class TestSalesInvoice(unittest.TestCase): si.posting_date = getdate() si.submit() - frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None) + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) def test_over_billing_case_against_delivery_note(self): - ''' - Test a case where duplicating the item with qty = 1 in the invoice - allows overbilling even if it is disabled - ''' + """ + Test a case where duplicating the item with qty = 1 in the invoice + allows overbilling even if it is disabled + """ from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note - over_billing_allowance = frappe.db.get_single_value('Accounts Settings', 'over_billing_allowance') - frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', 0) + over_billing_allowance = frappe.db.get_single_value( + "Accounts Settings", "over_billing_allowance" + ) + frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) dn = create_delivery_note() dn.submit() @@ -2421,7 +3206,7 @@ class TestSalesInvoice(unittest.TestCase): si = make_sales_invoice(dn.name) # make a copy of first item and add it to invoice item_copy = frappe.copy_doc(si.items[0]) - si.append('items', item_copy) + si.append("items", item_copy) si.save() with self.assertRaises(frappe.ValidationError) as err: @@ -2429,120 +3214,338 @@ class TestSalesInvoice(unittest.TestCase): self.assertTrue("cannot overbill" in str(err.exception).lower()) - frappe.db.set_value('Accounts Settings', None, 'over_billing_allowance', over_billing_allowance) + frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", over_billing_allowance) + + def test_multi_currency_deferred_revenue_via_journal_entry(self): + deferred_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - _TC", + company="_Test Company", + ) + + acc_settings = frappe.get_single("Accounts Settings") + acc_settings.book_deferred_entries_via_journal_entry = 1 + acc_settings.submit_journal_entries = 1 + acc_settings.save() + + item = create_item("_Test Item for Deferred Accounting") + item.enable_deferred_expense = 1 + item.deferred_revenue_account = deferred_account + item.save() + + si = create_sales_invoice( + customer="_Test Customer USD", + currency="USD", + item=item.name, + qty=1, + rate=100, + conversion_rate=60, + do_not_save=True, + ) + + si.set_posting_time = 1 + si.posting_date = "2019-01-01" + si.debit_to = "_Test Receivable USD - _TC" + si.items[0].enable_deferred_revenue = 1 + si.items[0].service_start_date = "2019-01-01" + si.items[0].service_end_date = "2019-03-30" + si.items[0].deferred_expense_account = deferred_account + si.save() + si.submit() + + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", getdate("2019-01-31")) + + pda1 = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2019-01-01", + end_date="2019-03-31", + type="Income", + company="_Test Company", + ) + ) + + pda1.insert() + pda1.submit() + + expected_gle = [ + ["Sales - _TC", 0.0, 2089.89, "2019-01-28"], + [deferred_account, 2089.89, 0.0, "2019-01-28"], + ["Sales - _TC", 0.0, 1887.64, "2019-02-28"], + [deferred_account, 1887.64, 0.0, "2019-02-28"], + ["Sales - _TC", 0.0, 2022.47, "2019-03-15"], + [deferred_account, 2022.47, 0.0, "2019-03-15"], + ] + + gl_entries = gl_entries = frappe.db.sql( + """select account, debit, credit, posting_date + from `tabGL Entry` + where voucher_type='Journal Entry' and voucher_detail_no=%s and posting_date <= %s + order by posting_date asc, account asc""", + (si.items[0].name, si.posting_date), + as_dict=1, + ) + + for i, gle in enumerate(gl_entries): + self.assertEqual(expected_gle[i][0], gle.account) + self.assertEqual(expected_gle[i][1], gle.credit) + self.assertEqual(expected_gle[i][2], gle.debit) + self.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + + acc_settings = frappe.get_single("Accounts Settings") + acc_settings.book_deferred_entries_via_journal_entry = 0 + acc_settings.submit_journal_entries = 0 + acc_settings.save() + + frappe.db.set_value("Accounts Settings", None, "acc_frozen_upto", None) + + def test_standalone_serial_no_return(self): + si = create_sales_invoice( + item_code="_Test Serialized Item With Series", update_stock=True, is_return=True, qty=-1 + ) + si.reload() + self.assertTrue(si.items[0].serial_no) + + def test_sales_invoice_with_disabled_account(self): + try: + account = frappe.get_doc("Account", "VAT 5% - _TC") + account.disabled = 1 + account.save() + + si = create_sales_invoice(do_not_save=True) + si.posting_date = add_days(getdate(), 1) + si.taxes = [] + + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "VAT 5% - _TC", + "cost_center": "Main - _TC", + "description": "VAT @ 5.0", + "rate": 9, + }, + ) + si.save() + + with self.assertRaises(frappe.ValidationError) as err: + si.submit() + + self.assertTrue( + "Cannot create accounting entries against disabled accounts" in str(err.exception) + ) + + finally: + account.disabled = 0 + account.save() + + def test_gain_loss_with_advance_entry(self): + from erpnext.accounts.doctype.journal_entry.test_journal_entry import make_journal_entry + + unlink_enabled = frappe.db.get_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice" + ) + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", 1 + ) + + jv = make_journal_entry("_Test Receivable USD - _TC", "_Test Bank - _TC", -7000, save=False) + + jv.accounts[0].exchange_rate = 70 + jv.accounts[0].credit_in_account_currency = 100 + jv.accounts[0].party_type = "Customer" + jv.accounts[0].party = "_Test Customer USD" + + jv.save() + jv.submit() + + si = create_sales_invoice( + customer="_Test Customer USD", + debit_to="_Test Receivable USD - _TC", + currency="USD", + conversion_rate=75, + do_not_save=1, + rate=100, + ) + + si.append( + "advances", + { + "reference_type": "Journal Entry", + "reference_name": jv.name, + "reference_row": jv.accounts[0].name, + "advance_amount": 100, + "allocated_amount": 100, + "ref_exchange_rate": 70, + }, + ) + si.save() + si.submit() + + expected_gle = [ + ["_Test Receivable USD - _TC", 7500.0, 500], + ["Exchange Gain/Loss - _TC", 500.0, 0.0], + ["Sales - _TC", 0.0, 7500.0], + ] + + check_gl_entries(self, si.name, expected_gle, nowdate()) + + frappe.db.set_value( + "Accounts Settings", "Accounts Settings", "unlink_payment_on_cancel_of_invoice", unlink_enabled + ) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() - si.naming_series = 'INV-2020-.#####' + si.naming_series = "INV-2020-.#####" si.items = [] - si.append("items", { - "item_code": "_Test Item", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 2000, - "rate": 12, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }) + si.append( + "items", + { + "item_code": "_Test Item", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 2000, + "rate": 12, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) - si.append("items", { - "item_code": "_Test Item 2", - "uom": "Nos", - "warehouse": "_Test Warehouse - _TC", - "qty": 420, - "rate": 15, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": "_Test Cost Center - _TC", - }) + si.append( + "items", + { + "item_code": "_Test Item 2", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 420, + "rate": 15, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) + + si.append( + "items", + { + "item_code": "_Test Item", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 111, + "price_list_rate": 20, + "discount_percentage": 10, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) + + si.append( + "items", + { + "item_code": "_Test Item 2", + "uom": "Nos", + "warehouse": "_Test Warehouse - _TC", + "qty": 1111, + "price_list_rate": 10, + "rate": 5, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": "_Test Cost Center - _TC", + }, + ) return si def make_test_address_for_ewaybill(): - if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Address for Eway bill", - "address_type": "Billing", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+910000000000", - "gstin": "27AAECE4835E1ZR", - "gst_state": "Maharashtra", - "gst_state_number": "27", - "pincode": "401108" - }).insert() + if not frappe.db.exists("Address", "_Test Address for Eway bill-Billing"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test Address for Eway bill", + "address_type": "Billing", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+910000000000", + "gstin": "27AAECE4835E1ZR", + "gst_state": "Maharashtra", + "gst_state_number": "27", + "pincode": "401108", + } + ).insert() - address.append("links", { - "link_doctype": "Company", - "link_name": "_Test Company" - }) + address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"}) address.save() - if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'): - address = frappe.get_doc({ - "address_line1": "_Test Address Line 1", - "address_title": "_Test Customer-Address for Eway bill", - "address_type": "Shipping", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 1, - "phone": "+910000000000", - "gstin": "27AACCM7806M1Z3", - "gst_state": "Maharashtra", - "gst_state_number": "27", - "pincode": "410038" - }).insert() + if not frappe.db.exists("Address", "_Test Customer-Address for Eway bill-Shipping"): + address = frappe.get_doc( + { + "address_line1": "_Test Address Line 1", + "address_title": "_Test Customer-Address for Eway bill", + "address_type": "Shipping", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 1, + "phone": "+910000000000", + "gstin": "27AACCM7806M1Z3", + "gst_state": "Maharashtra", + "gst_state_number": "27", + "pincode": "410038", + } + ).insert() - address.append("links", { - "link_doctype": "Customer", - "link_name": "_Test Customer" - }) + address.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"}) address.save() - if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'): - address = frappe.get_doc({ - "address_line1": "_Test Dispatch Address Line 1", - "address_title": "_Test Dispatch-Address for Eway bill", - "address_type": "Shipping", - "city": "_Test City", - "state": "Test State", - "country": "India", - "doctype": "Address", - "is_primary_address": 0, - "phone": "+910000000000", - "gstin": "07AAACC1206D1ZI", - "gst_state": "Delhi", - "gst_state_number": "07", - "pincode": "1100101" - }).insert() + if not frappe.db.exists("Address", "_Test Dispatch-Address for Eway bill-Shipping"): + address = frappe.get_doc( + { + "address_line1": "_Test Dispatch Address Line 1", + "address_title": "_Test Dispatch-Address for Eway bill", + "address_type": "Shipping", + "city": "_Test City", + "state": "Test State", + "country": "India", + "doctype": "Address", + "is_primary_address": 0, + "phone": "+910000000000", + "gstin": "07AAACC1206D1ZI", + "gst_state": "Delhi", + "gst_state_number": "07", + "pincode": "1100101", + } + ).insert() - address.append("links", { - "link_doctype": "Company", - "link_name": "_Test Company" - }) + address.append("links", {"link_doctype": "Company", "link_name": "_Test Company"}) address.save() + def make_test_transporter_for_ewaybill(): - if not frappe.db.exists('Supplier', '_Test Transporter'): - frappe.get_doc({ - "doctype": "Supplier", - "supplier_name": "_Test Transporter", - "country": "India", - "supplier_group": "_Test Supplier Group", - "supplier_type": "Company", - "is_transporter": 1 - }).insert() + if not frappe.db.exists("Supplier", "_Test Transporter"): + frappe.get_doc( + { + "doctype": "Supplier", + "supplier_name": "_Test Transporter", + "country": "India", + "supplier_group": "_Test Supplier Group", + "supplier_type": "Company", + "is_transporter": 1, + } + ).insert() + def make_sales_invoice_for_ewaybill(): make_test_address_for_ewaybill() @@ -2553,20 +3556,23 @@ def make_sales_invoice_for_ewaybill(): gst_account = frappe.get_all( "GST Account", fields=["cgst_account", "sgst_account", "igst_account"], - filters = {"company": "_Test Company"} + filters={"company": "_Test Company"}, ) if not gst_account: - gst_settings.append("gst_accounts", { - "company": "_Test Company", - "cgst_account": "Output Tax CGST - _TC", - "sgst_account": "Output Tax SGST - _TC", - "igst_account": "Output Tax IGST - _TC", - }) + gst_settings.append( + "gst_accounts", + { + "company": "_Test Company", + "cgst_account": "Output Tax CGST - _TC", + "sgst_account": "Output Tax SGST - _TC", + "igst_account": "Output Tax IGST - _TC", + }, + ) gst_settings.save() - si = create_sales_invoice(do_not_save=1, rate='60000') + si = create_sales_invoice(do_not_save=1, rate="60000") si.distance = 2000 si.company_address = "_Test Address for Eway bill-Billing" @@ -2574,32 +3580,43 @@ def make_sales_invoice_for_ewaybill(): si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping" si.vehicle_no = "KA12KA1234" si.gst_category = "Registered Regular" - si.mode_of_transport = 'Road' - si.transporter = '_Test Transporter' + si.mode_of_transport = "Road" + si.transporter = "_Test Transporter" - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax CGST - _TC", - "cost_center": "Main - _TC", - "description": "CGST @ 9.0", - "rate": 9 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax CGST - _TC", + "cost_center": "Main - _TC", + "description": "CGST @ 9.0", + "rate": 9, + }, + ) - si.append("taxes", { - "charge_type": "On Net Total", - "account_head": "Output Tax SGST - _TC", - "cost_center": "Main - _TC", - "description": "SGST @ 9.0", - "rate": 9 - }) + si.append( + "taxes", + { + "charge_type": "On Net Total", + "account_head": "Output Tax SGST - _TC", + "cost_center": "Main - _TC", + "description": "SGST @ 9.0", + "rate": 9, + }, + ) return si + def check_gl_entries(doc, voucher_no, expected_gle, posting_date): - gl_entries = frappe.db.sql("""select account, debit, credit, posting_date + gl_entries = frappe.db.sql( + """select account, debit, credit, posting_date from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s and posting_date > %s - order by posting_date asc, account asc""", (voucher_no, posting_date), as_dict=1) + order by posting_date asc, account asc""", + (voucher_no, posting_date), + as_dict=1, + ) for i, gle in enumerate(gl_entries): doc.assertEqual(expected_gle[i][0], gle.account) @@ -2607,6 +3624,7 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date): doc.assertEqual(expected_gle[i][2], gle.credit) doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date) + def create_sales_invoice(**args): si = frappe.new_doc("Sales Invoice") args = frappe._dict(args) @@ -2621,32 +3639,35 @@ def create_sales_invoice(**args): si.is_pos = args.is_pos si.is_return = args.is_return si.return_against = args.return_against - si.currency=args.currency or "INR" + si.currency = args.currency or "INR" si.conversion_rate = args.conversion_rate or 1 si.naming_series = args.naming_series or "T-SINV-" si.cost_center = args.parent_cost_center - si.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "item_name": args.item_name or "_Test Item", - "description": args.description or "_Test Item", - "gst_hsn_code": "999800", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 1, - "uom": args.uom or "Nos", - "stock_uom": args.uom or "Nos", - "rate": args.rate if args.get("rate") is not None else 100, - "price_list_rate": args.price_list_rate if args.get("price_list_rate") is not None else 100, - "income_account": args.income_account or "Sales - _TC", - "expense_account": args.expense_account or "Cost of Goods Sold - _TC", - "asset": args.asset or None, - "discount_account": args.discount_account or None, - "discount_amount": args.discount_amount or 0, - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no, - "conversion_factor": 1, - "incoming_rate": args.incoming_rate or 0 - }) + si.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "item_name": args.item_name or "_Test Item", + "description": args.description or "_Test Item", + "gst_hsn_code": "999800", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 1, + "uom": args.uom or "Nos", + "stock_uom": args.uom or "Nos", + "rate": args.rate if args.get("rate") is not None else 100, + "price_list_rate": args.price_list_rate if args.get("price_list_rate") is not None else 100, + "income_account": args.income_account or "Sales - _TC", + "expense_account": args.expense_account or "Cost of Goods Sold - _TC", + "asset": args.asset or None, + "discount_account": args.discount_account or None, + "discount_amount": args.discount_amount or 0, + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "serial_no": args.serial_no, + "conversion_factor": 1, + "incoming_rate": args.incoming_rate or 0, + }, + ) if not args.do_not_save: si.insert() @@ -2659,6 +3680,7 @@ def create_sales_invoice(**args): return si + def create_sales_invoice_against_cost_center(**args): si = frappe.new_doc("Sales Invoice") args = frappe._dict(args) @@ -2674,20 +3696,23 @@ def create_sales_invoice_against_cost_center(**args): si.is_pos = args.is_pos si.is_return = args.is_return si.return_against = args.return_against - si.currency=args.currency or "INR" + si.currency = args.currency or "INR" si.conversion_rate = args.conversion_rate or 1 - si.append("items", { - "item_code": args.item or args.item_code or "_Test Item", - "gst_hsn_code": "999800", - "warehouse": args.warehouse or "_Test Warehouse - _TC", - "qty": args.qty or 1, - "rate": args.rate or 100, - "income_account": "Sales - _TC", - "expense_account": "Cost of Goods Sold - _TC", - "cost_center": args.cost_center or "_Test Cost Center - _TC", - "serial_no": args.serial_no - }) + si.append( + "items", + { + "item_code": args.item or args.item_code or "_Test Item", + "gst_hsn_code": "999800", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + "qty": args.qty or 1, + "rate": args.rate or 100, + "income_account": "Sales - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "cost_center": args.cost_center or "_Test Cost Center - _TC", + "serial_no": args.serial_no, + }, + ) if not args.do_not_save: si.insert() @@ -2702,59 +3727,69 @@ def create_sales_invoice_against_cost_center(**args): test_dependencies = ["Journal Entry", "Contact", "Address"] -test_records = frappe.get_test_records('Sales Invoice') +test_records = frappe.get_test_records("Sales Invoice") + def get_outstanding_amount(against_voucher_type, against_voucher, account, party, party_type): - bal = flt(frappe.db.sql(""" + bal = flt( + frappe.db.sql( + """ select sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where against_voucher_type=%s and against_voucher=%s and account = %s and party = %s and party_type = %s""", - (against_voucher_type, against_voucher, account, party, party_type))[0][0] or 0.0) + (against_voucher_type, against_voucher, account, party, party_type), + )[0][0] + or 0.0 + ) - if against_voucher_type == 'Purchase Invoice': + if against_voucher_type == "Purchase Invoice": bal = bal * -1 return bal + def get_taxes_and_charges(): - return [{ - "account_head": "_Test Account Excise Duty - TCP1", - "charge_type": "On Net Total", - "cost_center": "Main - TCP1", - "description": "Excise Duty", - "doctype": "Sales Taxes and Charges", - "idx": 1, - "included_in_print_rate": 1, - "parentfield": "taxes", - "rate": 12 - }, - { - "account_head": "_Test Account Education Cess - TCP1", - "charge_type": "On Previous Row Amount", - "cost_center": "Main - TCP1", - "description": "Education Cess", - "doctype": "Sales Taxes and Charges", - "idx": 2, - "included_in_print_rate": 1, - "parentfield": "taxes", - "rate": 2, - "row_id": 1 - }] + return [ + { + "account_head": "_Test Account Excise Duty - TCP1", + "charge_type": "On Net Total", + "cost_center": "Main - TCP1", + "description": "Excise Duty", + "doctype": "Sales Taxes and Charges", + "idx": 1, + "included_in_print_rate": 1, + "parentfield": "taxes", + "rate": 12, + }, + { + "account_head": "_Test Account Education Cess - TCP1", + "charge_type": "On Previous Row Amount", + "cost_center": "Main - TCP1", + "description": "Education Cess", + "doctype": "Sales Taxes and Charges", + "idx": 2, + "included_in_print_rate": 1, + "parentfield": "taxes", + "rate": 2, + "row_id": 1, + }, + ] + def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with): if not frappe.db.exists("Supplier", supplier_name): - supplier = frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": supplier_name, - "doctype": "Supplier", - "is_internal_supplier": 1, - "represents_company": represents_company - }) + supplier = frappe.get_doc( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": supplier_name, + "doctype": "Supplier", + "is_internal_supplier": 1, + "represents_company": represents_company, + } + ) - supplier.append("companies", { - "company": allowed_to_interact_with - }) + supplier.append("companies", {"company": allowed_to_interact_with}) supplier.insert() supplier_name = supplier.name @@ -2763,11 +3798,15 @@ def create_internal_supplier(supplier_name, represents_company, allowed_to_inter return supplier_name + def add_taxes(doc): - doc.append('taxes', { - 'account_head': '_Test Account Excise Duty - TCP1', - "charge_type": "On Net Total", - "cost_center": "Main - TCP1", - "description": "Excise Duty", - "rate": 12 - }) + doc.append( + "taxes", + { + "account_head": "_Test Account Excise Duty - TCP1", + "charge_type": "On Net Total", + "cost_center": "Main - TCP1", + "description": "Excise Duty", + "rate": 12, + }, + ) diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index a9412d86396..7bb564b066a 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -279,7 +279,6 @@ "label": "Discount (%) on Price List Rate with Margin", "oldfieldname": "adj_rate", "oldfieldtype": "Float", - "precision": "2", "print_hide": 1 }, { @@ -832,6 +831,7 @@ }, { "default": "0", + "fetch_from": "item_code.grant_commission", "fieldname": "grant_commission", "fieldtype": "Check", "label": "Grant Commission", @@ -841,7 +841,7 @@ "idx": 1, "istable": 1, "links": [], - "modified": "2021-10-05 12:24:54.968907", + "modified": "2022-08-26 12:06:31.205417", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", @@ -851,3 +851,4 @@ "sort_field": "modified", "sort_order": "DESC" } + diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py index b5909447dc8..d9009bae4c0 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py @@ -21,13 +21,14 @@ class SalesTaxesandChargesTemplate(Document): def autoname(self): if self.company and self.title: - abbr = frappe.get_cached_value('Company', self.company, 'abbr') - self.name = '{0} - {1}'.format(self.title, abbr) + abbr = frappe.get_cached_value("Company", self.company, "abbr") + self.name = "{0} - {1}".format(self.title, abbr) def set_missing_values(self): for data in self.taxes: - if data.charge_type == 'On Net Total' and flt(data.rate) == 0.0: - data.rate = frappe.db.get_value('Account', data.account_head, 'tax_rate') + if data.charge_type == "On Net Total" and flt(data.rate) == 0.0: + data.rate = frappe.db.get_value("Account", data.account_head, "tax_rate") + def valdiate_taxes_and_charges_template(doc): # default should not be disabled @@ -35,9 +36,13 @@ def valdiate_taxes_and_charges_template(doc): # doc.is_default = 1 if doc.is_default == 1: - frappe.db.sql("""update `tab{0}` set is_default = 0 - where is_default = 1 and name != %s and company = %s""".format(doc.doctype), - (doc.name, doc.company)) + frappe.db.sql( + """update `tab{0}` set is_default = 0 + where is_default = 1 and name != %s and company = %s""".format( + doc.doctype + ), + (doc.name, doc.company), + ) validate_disabled(doc) @@ -46,14 +51,31 @@ def valdiate_taxes_and_charges_template(doc): for tax in doc.get("taxes"): validate_taxes_and_charges(tax) - validate_account_head(tax, doc) + validate_account_head(tax.idx, tax.account_head, doc.company) validate_cost_center(tax, doc) validate_inclusive_tax(tax, doc) + def validate_disabled(doc): if doc.is_default and doc.disabled: frappe.throw(_("Disabled template must not be default template")) + def validate_for_tax_category(doc): - if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}): - frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category))) + if not doc.tax_category: + return + + if frappe.db.exists( + doc.doctype, + { + "company": doc.company, + "tax_category": doc.tax_category, + "disabled": 0, + "name": ["!=", doc.name], + }, + ): + frappe.throw( + _( + "A template with tax category {0} already exists. Only one template is allowed with each tax category" + ).format(frappe.bold(doc.tax_category)) + ) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py index 5b9fbafed8d..6432acaae93 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template_dashboard.py @@ -1,23 +1,16 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'taxes_and_charges', - 'non_standard_fieldnames': { - 'Tax Rule': 'sales_tax_template', - 'Subscription': 'sales_tax_template', - 'Restaurant': 'default_tax_template' + "fieldname": "taxes_and_charges", + "non_standard_fieldnames": { + "Tax Rule": "sales_tax_template", + "Subscription": "sales_tax_template", + "Restaurant": "default_tax_template", }, - 'transactions': [ - { - 'label': _('Transactions'), - 'items': ['Sales Invoice', 'Sales Order', 'Delivery Note'] - }, - { - 'label': _('References'), - 'items': ['POS Profile', 'Subscription', 'Restaurant', 'Tax Rule'] - } - ] + "transactions": [ + {"label": _("Transactions"), "items": ["Sales Invoice", "Sales Order", "Delivery Note"]}, + {"label": _("References"), "items": ["POS Profile", "Subscription", "Restaurant", "Tax Rule"]}, + ], } diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py index 7b13c6c6925..972b773501a 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/test_sales_taxes_and_charges_template.py @@ -5,7 +5,8 @@ import unittest import frappe -test_records = frappe.get_test_records('Sales Taxes and Charges Template') +test_records = frappe.get_test_records("Sales Taxes and Charges Template") + class TestSalesTaxesandChargesTemplate(unittest.TestCase): pass diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.py b/erpnext/accounts/doctype/share_transfer/share_transfer.py index b543ad8204d..4f49843c1eb 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.py @@ -10,95 +10,115 @@ from frappe.model.naming import make_autoname from frappe.utils import nowdate -class ShareDontExists(ValidationError): pass +class ShareDontExists(ValidationError): + pass + class ShareTransfer(Document): def on_submit(self): - if self.transfer_type == 'Issue': + if self.transfer_type == "Issue": shareholder = self.get_company_shareholder() - shareholder.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares, - 'is_company': 1, - 'current_state': 'Issued' - }) + shareholder.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + "is_company": 1, + "current_state": "Issued", + }, + ) shareholder.save() doc = self.get_shareholder_doc(self.to_shareholder) - doc.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares - }) + doc.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + }, + ) doc.save() - elif self.transfer_type == 'Purchase': + elif self.transfer_type == "Purchase": self.remove_shares(self.from_shareholder) self.remove_shares(self.get_company_shareholder().name) - elif self.transfer_type == 'Transfer': + elif self.transfer_type == "Transfer": self.remove_shares(self.from_shareholder) doc = self.get_shareholder_doc(self.to_shareholder) - doc.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares - }) + doc.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + }, + ) doc.save() def on_cancel(self): - if self.transfer_type == 'Issue': + if self.transfer_type == "Issue": compnay_shareholder = self.get_company_shareholder() self.remove_shares(compnay_shareholder.name) self.remove_shares(self.to_shareholder) - elif self.transfer_type == 'Purchase': + elif self.transfer_type == "Purchase": compnay_shareholder = self.get_company_shareholder() from_shareholder = self.get_shareholder_doc(self.from_shareholder) - from_shareholder.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares - }) + from_shareholder.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + }, + ) from_shareholder.save() - compnay_shareholder.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares - }) + compnay_shareholder.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + }, + ) compnay_shareholder.save() - elif self.transfer_type == 'Transfer': + elif self.transfer_type == "Transfer": self.remove_shares(self.to_shareholder) from_shareholder = self.get_shareholder_doc(self.from_shareholder) - from_shareholder.append('share_balance', { - 'share_type': self.share_type, - 'from_no': self.from_no, - 'to_no': self.to_no, - 'rate': self.rate, - 'amount': self.amount, - 'no_of_shares': self.no_of_shares - }) + from_shareholder.append( + "share_balance", + { + "share_type": self.share_type, + "from_no": self.from_no, + "to_no": self.to_no, + "rate": self.rate, + "amount": self.amount, + "no_of_shares": self.no_of_shares, + }, + ) from_shareholder.save() def validate(self): @@ -106,90 +126,96 @@ class ShareTransfer(Document): self.basic_validations() self.folio_no_validation() - if self.transfer_type == 'Issue': + if self.transfer_type == "Issue": # validate share doesn't exist in company ret_val = self.share_exists(self.get_company_shareholder().name) - if ret_val in ('Complete', 'Partial'): - frappe.throw(_('The shares already exist'), frappe.DuplicateEntryError) + if ret_val in ("Complete", "Partial"): + frappe.throw(_("The shares already exist"), frappe.DuplicateEntryError) else: # validate share exists with from_shareholder ret_val = self.share_exists(self.from_shareholder) - if ret_val in ('Outside', 'Partial'): - frappe.throw(_("The shares don't exist with the {0}") - .format(self.from_shareholder), ShareDontExists) + if ret_val in ("Outside", "Partial"): + frappe.throw( + _("The shares don't exist with the {0}").format(self.from_shareholder), ShareDontExists + ) def basic_validations(self): - if self.transfer_type == 'Purchase': - self.to_shareholder = '' + if self.transfer_type == "Purchase": + self.to_shareholder = "" if not self.from_shareholder: - frappe.throw(_('The field From Shareholder cannot be blank')) + frappe.throw(_("The field From Shareholder cannot be blank")) if not self.from_folio_no: self.to_folio_no = self.autoname_folio(self.to_shareholder) if not self.asset_account: - frappe.throw(_('The field Asset Account cannot be blank')) - elif (self.transfer_type == 'Issue'): - self.from_shareholder = '' + frappe.throw(_("The field Asset Account cannot be blank")) + elif self.transfer_type == "Issue": + self.from_shareholder = "" if not self.to_shareholder: - frappe.throw(_('The field To Shareholder cannot be blank')) + frappe.throw(_("The field To Shareholder cannot be blank")) if not self.to_folio_no: self.to_folio_no = self.autoname_folio(self.to_shareholder) if not self.asset_account: - frappe.throw(_('The field Asset Account cannot be blank')) + frappe.throw(_("The field Asset Account cannot be blank")) else: if not self.from_shareholder or not self.to_shareholder: - frappe.throw(_('The fields From Shareholder and To Shareholder cannot be blank')) + frappe.throw(_("The fields From Shareholder and To Shareholder cannot be blank")) if not self.to_folio_no: self.to_folio_no = self.autoname_folio(self.to_shareholder) if not self.equity_or_liability_account: - frappe.throw(_('The field Equity/Liability Account cannot be blank')) + frappe.throw(_("The field Equity/Liability Account cannot be blank")) if self.from_shareholder == self.to_shareholder: - frappe.throw(_('The seller and the buyer cannot be the same')) + frappe.throw(_("The seller and the buyer cannot be the same")) if self.no_of_shares != self.to_no - self.from_no + 1: - frappe.throw(_('The number of shares and the share numbers are inconsistent')) + frappe.throw(_("The number of shares and the share numbers are inconsistent")) if not self.amount: self.amount = self.rate * self.no_of_shares if self.amount != self.rate * self.no_of_shares: - frappe.throw(_('There are inconsistencies between the rate, no of shares and the amount calculated')) + frappe.throw( + _("There are inconsistencies between the rate, no of shares and the amount calculated") + ) def share_exists(self, shareholder): doc = self.get_shareholder_doc(shareholder) for entry in doc.share_balance: - if entry.share_type != self.share_type or \ - entry.from_no > self.to_no or \ - entry.to_no < self.from_no: - continue # since query lies outside bounds - elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: #both inside - return 'Complete' # absolute truth! + if ( + entry.share_type != self.share_type or entry.from_no > self.to_no or entry.to_no < self.from_no + ): + continue # since query lies outside bounds + elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: # both inside + return "Complete" # absolute truth! elif entry.from_no <= self.from_no <= self.to_no: - return 'Partial' + return "Partial" elif entry.from_no <= self.to_no <= entry.to_no: - return 'Partial' + return "Partial" - return 'Outside' + return "Outside" def folio_no_validation(self): - shareholder_fields = ['from_shareholder', 'to_shareholder'] + shareholder_fields = ["from_shareholder", "to_shareholder"] for shareholder_field in shareholder_fields: shareholder_name = self.get(shareholder_field) if not shareholder_name: continue doc = self.get_shareholder_doc(shareholder_name) if doc.company != self.company: - frappe.throw(_('The shareholder does not belong to this company')) + frappe.throw(_("The shareholder does not belong to this company")) if not doc.folio_no: - doc.folio_no = self.from_folio_no \ - if (shareholder_field == 'from_shareholder') else self.to_folio_no + doc.folio_no = ( + self.from_folio_no if (shareholder_field == "from_shareholder") else self.to_folio_no + ) doc.save() else: - if doc.folio_no and doc.folio_no != (self.from_folio_no if (shareholder_field == 'from_shareholder') else self.to_folio_no): - frappe.throw(_('The folio numbers are not matching')) + if doc.folio_no and doc.folio_no != ( + self.from_folio_no if (shareholder_field == "from_shareholder") else self.to_folio_no + ): + frappe.throw(_("The folio numbers are not matching")) def autoname_folio(self, shareholder, is_company=False): if is_company: doc = self.get_company_shareholder() else: doc = self.get_shareholder_doc(shareholder) - doc.folio_no = make_autoname('FN.#####') + doc.folio_no = make_autoname("FN.#####") doc.save() return doc.folio_no @@ -197,106 +223,120 @@ class ShareTransfer(Document): # query = {'from_no': share_starting_no, 'to_no': share_ending_no} # Shares exist for sure # Iterate over all entries and modify entry if in entry - doc = frappe.get_doc('Shareholder', shareholder) + doc = frappe.get_doc("Shareholder", shareholder) current_entries = doc.share_balance new_entries = [] for entry in current_entries: # use spaceage logic here - if entry.share_type != self.share_type or \ - entry.from_no > self.to_no or \ - entry.to_no < self.from_no: + if ( + entry.share_type != self.share_type or entry.from_no > self.to_no or entry.to_no < self.from_no + ): new_entries.append(entry) - continue # since query lies outside bounds + continue # since query lies outside bounds elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: - #split + # split if entry.from_no == self.from_no: if entry.to_no == self.to_no: - pass #nothing to append + pass # nothing to append else: - new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate)) + new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate)) else: if entry.to_no == self.to_no: - new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate)) + new_entries.append( + self.return_share_balance_entry(entry.from_no, self.from_no - 1, entry.rate) + ) else: - new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate)) - new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate)) + new_entries.append( + self.return_share_balance_entry(entry.from_no, self.from_no - 1, entry.rate) + ) + new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate)) elif entry.from_no >= self.from_no and entry.to_no <= self.to_no: # split and check - pass #nothing to append + pass # nothing to append elif self.from_no <= entry.from_no <= self.to_no and entry.to_no >= self.to_no: - new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate)) + new_entries.append(self.return_share_balance_entry(self.to_no + 1, entry.to_no, entry.rate)) elif self.from_no <= entry.to_no <= self.to_no and entry.from_no <= self.from_no: - new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate)) + new_entries.append( + self.return_share_balance_entry(entry.from_no, self.from_no - 1, entry.rate) + ) else: new_entries.append(entry) doc.share_balance = [] for entry in new_entries: - doc.append('share_balance', entry) + doc.append("share_balance", entry) doc.save() def return_share_balance_entry(self, from_no, to_no, rate): # return an entry as a dict return { - 'share_type' : self.share_type, - 'from_no' : from_no, - 'to_no' : to_no, - 'rate' : rate, - 'amount' : self.rate * (to_no - from_no + 1), - 'no_of_shares' : to_no - from_no + 1 + "share_type": self.share_type, + "from_no": from_no, + "to_no": to_no, + "rate": rate, + "amount": self.rate * (to_no - from_no + 1), + "no_of_shares": to_no - from_no + 1, } def get_shareholder_doc(self, shareholder): # Get Shareholder doc based on the Shareholder name if shareholder: - query_filters = {'name': shareholder} + query_filters = {"name": shareholder} - name = frappe.db.get_value('Shareholder', {'name': shareholder}, 'name') + name = frappe.db.get_value("Shareholder", {"name": shareholder}, "name") - return frappe.get_doc('Shareholder', name) + return frappe.get_doc("Shareholder", name) def get_company_shareholder(self): # Get company doc or create one if not present - company_shareholder = frappe.db.get_value('Shareholder', - { - 'company': self.company, - 'is_company': 1 - }, 'name') + company_shareholder = frappe.db.get_value( + "Shareholder", {"company": self.company, "is_company": 1}, "name" + ) if company_shareholder: - return frappe.get_doc('Shareholder', company_shareholder) + return frappe.get_doc("Shareholder", company_shareholder) else: - shareholder = frappe.get_doc({ - 'doctype': 'Shareholder', - 'title': self.company, - 'company': self.company, - 'is_company': 1 - }) + shareholder = frappe.get_doc( + {"doctype": "Shareholder", "title": self.company, "company": self.company, "is_company": 1} + ) shareholder.insert() return shareholder + @frappe.whitelist() -def make_jv_entry( company, account, amount, payment_account,\ - credit_applicant_type, credit_applicant, debit_applicant_type, debit_applicant): - journal_entry = frappe.new_doc('Journal Entry') - journal_entry.voucher_type = 'Journal Entry' +def make_jv_entry( + company, + account, + amount, + payment_account, + credit_applicant_type, + credit_applicant, + debit_applicant_type, + debit_applicant, +): + journal_entry = frappe.new_doc("Journal Entry") + journal_entry.voucher_type = "Journal Entry" journal_entry.company = company journal_entry.posting_date = nowdate() account_amt_list = [] - account_amt_list.append({ - "account": account, - "debit_in_account_currency": amount, - "party_type": debit_applicant_type, - "party": debit_applicant, - }) - account_amt_list.append({ - "account": payment_account, - "credit_in_account_currency": amount, - "party_type": credit_applicant_type, - "party": credit_applicant, - }) + account_amt_list.append( + { + "account": account, + "debit_in_account_currency": amount, + "party_type": debit_applicant_type, + "party": debit_applicant, + } + ) + account_amt_list.append( + { + "account": payment_account, + "credit_in_account_currency": amount, + "party_type": credit_applicant_type, + "party": credit_applicant, + } + ) journal_entry.set("accounts", account_amt_list) return journal_entry.as_dict() diff --git a/erpnext/accounts/doctype/share_transfer/test_share_transfer.py b/erpnext/accounts/doctype/share_transfer/test_share_transfer.py index bc3a52167db..97310743605 100644 --- a/erpnext/accounts/doctype/share_transfer/test_share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/test_share_transfer.py @@ -9,6 +9,7 @@ from erpnext.accounts.doctype.share_transfer.share_transfer import ShareDontExis test_dependencies = ["Share Type", "Shareholder"] + class TestShareTransfer(unittest.TestCase): def setUp(self): frappe.db.sql("delete from `tabShare Transfer`") @@ -26,7 +27,7 @@ class TestShareTransfer(unittest.TestCase): "rate": 10, "company": "_Test Company", "asset_account": "Cash - _TC", - "equity_or_liability_account": "Creditors - _TC" + "equity_or_liability_account": "Creditors - _TC", }, { "doctype": "Share Transfer", @@ -40,7 +41,7 @@ class TestShareTransfer(unittest.TestCase): "no_of_shares": 100, "rate": 15, "company": "_Test Company", - "equity_or_liability_account": "Creditors - _TC" + "equity_or_liability_account": "Creditors - _TC", }, { "doctype": "Share Transfer", @@ -54,7 +55,7 @@ class TestShareTransfer(unittest.TestCase): "no_of_shares": 300, "rate": 20, "company": "_Test Company", - "equity_or_liability_account": "Creditors - _TC" + "equity_or_liability_account": "Creditors - _TC", }, { "doctype": "Share Transfer", @@ -68,7 +69,7 @@ class TestShareTransfer(unittest.TestCase): "no_of_shares": 200, "rate": 15, "company": "_Test Company", - "equity_or_liability_account": "Creditors - _TC" + "equity_or_liability_account": "Creditors - _TC", }, { "doctype": "Share Transfer", @@ -82,42 +83,46 @@ class TestShareTransfer(unittest.TestCase): "rate": 25, "company": "_Test Company", "asset_account": "Cash - _TC", - "equity_or_liability_account": "Creditors - _TC" - } + "equity_or_liability_account": "Creditors - _TC", + }, ] for d in share_transfers: st = frappe.get_doc(d) st.submit() def test_invalid_share_transfer(self): - doc = frappe.get_doc({ - "doctype": "Share Transfer", - "transfer_type": "Transfer", - "date": "2018-01-05", - "from_shareholder": "SH-00003", - "to_shareholder": "SH-00002", - "share_type": "Equity", - "from_no": 1, - "to_no": 100, - "no_of_shares": 100, - "rate": 15, - "company": "_Test Company", - "equity_or_liability_account": "Creditors - _TC" - }) + doc = frappe.get_doc( + { + "doctype": "Share Transfer", + "transfer_type": "Transfer", + "date": "2018-01-05", + "from_shareholder": "SH-00003", + "to_shareholder": "SH-00002", + "share_type": "Equity", + "from_no": 1, + "to_no": 100, + "no_of_shares": 100, + "rate": 15, + "company": "_Test Company", + "equity_or_liability_account": "Creditors - _TC", + } + ) self.assertRaises(ShareDontExists, doc.insert) - doc = frappe.get_doc({ - "doctype": "Share Transfer", - "transfer_type": "Purchase", - "date": "2018-01-02", - "from_shareholder": "SH-00001", - "share_type": "Equity", - "from_no": 1, - "to_no": 200, - "no_of_shares": 200, - "rate": 15, - "company": "_Test Company", - "asset_account": "Cash - _TC", - "equity_or_liability_account": "Creditors - _TC" - }) + doc = frappe.get_doc( + { + "doctype": "Share Transfer", + "transfer_type": "Purchase", + "date": "2018-01-02", + "from_shareholder": "SH-00001", + "share_type": "Equity", + "from_no": 1, + "to_no": 200, + "no_of_shares": 200, + "rate": 15, + "company": "_Test Company", + "asset_account": "Cash - _TC", + "equity_or_liability_account": "Creditors - _TC", + } + ) self.assertRaises(ShareDontExists, doc.insert) diff --git a/erpnext/accounts/doctype/share_type/share_type_dashboard.py b/erpnext/accounts/doctype/share_type/share_type_dashboard.py index fdb417ed6d0..19604b332a3 100644 --- a/erpnext/accounts/doctype/share_type/share_type_dashboard.py +++ b/erpnext/accounts/doctype/share_type/share_type_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'share_type', - 'transactions': [ - { - 'label': _('References'), - 'items': ['Share Transfer', 'Shareholder'] - } - ] + "fieldname": "share_type", + "transactions": [{"label": _("References"), "items": ["Share Transfer", "Shareholder"]}], } diff --git a/erpnext/accounts/doctype/shareholder/shareholder.py b/erpnext/accounts/doctype/shareholder/shareholder.py index 8a0fa85a692..b0e2493f7a6 100644 --- a/erpnext/accounts/doctype/shareholder/shareholder.py +++ b/erpnext/accounts/doctype/shareholder/shareholder.py @@ -15,7 +15,7 @@ class Shareholder(Document): load_address_and_contact(self) def on_trash(self): - delete_contact_and_address('Shareholder', self.name) + delete_contact_and_address("Shareholder", self.name) def before_save(self): for entry in self.share_balance: diff --git a/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py b/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py index 44d5ec684fb..fa9d431c19e 100644 --- a/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py +++ b/erpnext/accounts/doctype/shareholder/shareholder_dashboard.py @@ -1,14 +1,6 @@ - - def get_data(): return { - 'fieldname': 'shareholder', - 'non_standard_fieldnames': { - 'Share Transfer': 'to_shareholder' - }, - 'transactions': [ - { - 'items': ['Share Transfer'] - } - ] + "fieldname": "shareholder", + "non_standard_fieldnames": {"Share Transfer": "to_shareholder"}, + "transactions": [{"items": ["Share Transfer"]}], } diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index 7e5129911e4..1d79503a05e 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -12,9 +12,17 @@ from frappe.utils import flt, fmt_money import erpnext -class OverlappingConditionError(frappe.ValidationError): pass -class FromGreaterThanToError(frappe.ValidationError): pass -class ManyBlankToValuesError(frappe.ValidationError): pass +class OverlappingConditionError(frappe.ValidationError): + pass + + +class FromGreaterThanToError(frappe.ValidationError): + pass + + +class ManyBlankToValuesError(frappe.ValidationError): + pass + class ShippingRule(Document): def validate(self): @@ -35,15 +43,19 @@ class ShippingRule(Document): if not d.to_value: zero_to_values.append(d) elif d.from_value >= d.to_value: - throw(_("From value must be less than to value in row {0}").format(d.idx), FromGreaterThanToError) + throw( + _("From value must be less than to value in row {0}").format(d.idx), FromGreaterThanToError + ) # check if more than two or more rows has To Value = 0 if len(zero_to_values) >= 2: - throw(_('There can only be one Shipping Rule Condition with 0 or blank value for "To Value"'), - ManyBlankToValuesError) + throw( + _('There can only be one Shipping Rule Condition with 0 or blank value for "To Value"'), + ManyBlankToValuesError, + ) def apply(self, doc): - '''Apply shipping rule on given doc. Called from accounts controller''' + """Apply shipping rule on given doc. Called from accounts controller""" shipping_amount = 0.0 by_value = False @@ -52,15 +64,15 @@ class ShippingRule(Document): # validate country only if there is address self.validate_countries(doc) - if self.calculate_based_on == 'Net Total': + if self.calculate_based_on == "Net Total": value = doc.base_net_total by_value = True - elif self.calculate_based_on == 'Net Weight': + elif self.calculate_based_on == "Net Weight": value = doc.total_net_weight by_value = True - elif self.calculate_based_on == 'Fixed': + elif self.calculate_based_on == "Fixed": shipping_amount = self.shipping_amount # shipping amount by value, apply conditions @@ -75,7 +87,9 @@ class ShippingRule(Document): def get_shipping_amount_from_rules(self, value): for condition in self.get("conditions"): - if not condition.to_value or (flt(condition.from_value) <= flt(value) <= flt(condition.to_value)): + if not condition.to_value or ( + flt(condition.from_value) <= flt(value) <= flt(condition.to_value) + ): return condition.shipping_amount return 0.0 @@ -83,27 +97,31 @@ class ShippingRule(Document): def validate_countries(self, doc): # validate applicable countries if self.countries: - shipping_country = doc.get_shipping_address().get('country') + shipping_country = doc.get_shipping_address().get("country") if not shipping_country: - frappe.throw(_('Shipping Address does not have country, which is required for this Shipping Rule')) + frappe.throw( + _("Shipping Address does not have country, which is required for this Shipping Rule") + ) if shipping_country not in [d.country for d in self.countries]: - frappe.throw(_('Shipping rule not applicable for country {0} in Shipping Address').format(shipping_country)) + frappe.throw( + _("Shipping rule not applicable for country {0} in Shipping Address").format(shipping_country) + ) def add_shipping_rule_to_tax_table(self, doc, shipping_amount): shipping_charge = { "charge_type": "Actual", "account_head": self.account, - "cost_center": self.cost_center + "cost_center": self.cost_center, } if self.shipping_rule_type == "Selling": # check if not applied on purchase - if not doc.meta.get_field('taxes').options == 'Sales Taxes and Charges': - frappe.throw(_('Shipping rule only applicable for Selling')) + if not doc.meta.get_field("taxes").options == "Sales Taxes and Charges": + frappe.throw(_("Shipping rule only applicable for Selling")) shipping_charge["doctype"] = "Sales Taxes and Charges" else: # check if not applied on sales - if not doc.meta.get_field('taxes').options == 'Purchase Taxes and Charges': - frappe.throw(_('Shipping rule only applicable for Buying')) + if not doc.meta.get_field("taxes").options == "Purchase Taxes and Charges": + frappe.throw(_("Shipping rule only applicable for Buying")) shipping_charge["doctype"] = "Purchase Taxes and Charges" shipping_charge["category"] = "Valuation and Total" @@ -127,19 +145,19 @@ class ShippingRule(Document): def validate_overlapping_shipping_rule_conditions(self): def overlap_exists_between(num_range1, num_range2): """ - num_range1 and num_range2 are two ranges - ranges are represented as a tuple e.g. range 100 to 300 is represented as (100, 300) - if condition num_range1 = 100 to 300 - then condition num_range2 can only be like 50 to 99 or 301 to 400 - hence, non-overlapping condition = (x1 <= x2 < y1 <= y2) or (y1 <= y2 < x1 <= x2) + num_range1 and num_range2 are two ranges + ranges are represented as a tuple e.g. range 100 to 300 is represented as (100, 300) + if condition num_range1 = 100 to 300 + then condition num_range2 can only be like 50 to 99 or 301 to 400 + hence, non-overlapping condition = (x1 <= x2 < y1 <= y2) or (y1 <= y2 < x1 <= x2) """ (x1, x2), (y1, y2) = num_range1, num_range2 separate = (x1 <= x2 <= y1 <= y2) or (y1 <= y2 <= x1 <= x2) - return (not separate) + return not separate overlaps = [] for i in range(0, len(self.conditions)): - for j in range(i+1, len(self.conditions)): + for j in range(i + 1, len(self.conditions)): d1, d2 = self.conditions[i], self.conditions[j] if d1.as_dict() != d2.as_dict(): # in our case, to_value can be zero, hence pass the from_value if so @@ -153,7 +171,12 @@ class ShippingRule(Document): msgprint(_("Overlapping conditions found between:")) messages = [] for d1, d2 in overlaps: - messages.append("%s-%s = %s " % (d1.from_value, d1.to_value, fmt_money(d1.shipping_amount, currency=company_currency)) + - _("and") + " %s-%s = %s" % (d2.from_value, d2.to_value, fmt_money(d2.shipping_amount, currency=company_currency))) + messages.append( + "%s-%s = %s " + % (d1.from_value, d1.to_value, fmt_money(d1.shipping_amount, currency=company_currency)) + + _("and") + + " %s-%s = %s" + % (d2.from_value, d2.to_value, fmt_money(d2.shipping_amount, currency=company_currency)) + ) msgprint("\n".join(messages), raise_exception=OverlappingConditionError) diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py index ef2a053227b..60ce120c54f 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule_dashboard.py @@ -1,25 +1,13 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'shipping_rule', - 'non_standard_fieldnames': { - 'Payment Entry': 'party_name' - }, - 'transactions': [ - { - 'label': _('Pre Sales'), - 'items': ['Quotation', 'Supplier Quotation'] - }, - { - 'label': _('Sales'), - 'items': ['Sales Order', 'Delivery Note', 'Sales Invoice'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'] - } - ] + "fieldname": "shipping_rule", + "non_standard_fieldnames": {"Payment Entry": "party_name"}, + "transactions": [ + {"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]}, + {"label": _("Sales"), "items": ["Sales Order", "Delivery Note", "Sales Invoice"]}, + {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]}, + ], } diff --git a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py index c06dae09701..a24e834c572 100644 --- a/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/test_shipping_rule.py @@ -11,18 +11,19 @@ from erpnext.accounts.doctype.shipping_rule.shipping_rule import ( OverlappingConditionError, ) -test_records = frappe.get_test_records('Shipping Rule') +test_records = frappe.get_test_records("Shipping Rule") + class TestShippingRule(unittest.TestCase): def test_from_greater_than_to(self): shipping_rule = frappe.copy_doc(test_records[0]) - shipping_rule.name = test_records[0].get('name') + shipping_rule.name = test_records[0].get("name") shipping_rule.get("conditions")[0].from_value = 101 self.assertRaises(FromGreaterThanToError, shipping_rule.insert) def test_many_zero_to_values(self): shipping_rule = frappe.copy_doc(test_records[0]) - shipping_rule.name = test_records[0].get('name') + shipping_rule.name = test_records[0].get("name") shipping_rule.get("conditions")[0].to_value = 0 self.assertRaises(ManyBlankToValuesError, shipping_rule.insert) @@ -35,48 +36,58 @@ class TestShippingRule(unittest.TestCase): ((50, 150), (50, 150)), ]: shipping_rule = frappe.copy_doc(test_records[0]) - shipping_rule.name = test_records[0].get('name') + shipping_rule.name = test_records[0].get("name") shipping_rule.get("conditions")[0].from_value = range_a[0] shipping_rule.get("conditions")[0].to_value = range_a[1] shipping_rule.get("conditions")[1].from_value = range_b[0] shipping_rule.get("conditions")[1].to_value = range_b[1] self.assertRaises(OverlappingConditionError, shipping_rule.insert) + def create_shipping_rule(shipping_rule_type, shipping_rule_name): if frappe.db.exists("Shipping Rule", shipping_rule_name): return frappe.get_doc("Shipping Rule", shipping_rule_name) sr = frappe.new_doc("Shipping Rule") - sr.account = "_Test Account Shipping Charges - _TC" - sr.calculate_based_on = "Net Total" + sr.account = "_Test Account Shipping Charges - _TC" + sr.calculate_based_on = "Net Total" sr.company = "_Test Company" sr.cost_center = "_Test Cost Center - _TC" sr.label = shipping_rule_name sr.name = shipping_rule_name sr.shipping_rule_type = shipping_rule_type - sr.append("conditions", { + sr.append( + "conditions", + { "doctype": "Shipping Rule Condition", "from_value": 0, "parentfield": "conditions", "shipping_amount": 50.0, - "to_value": 100 - }) - sr.append("conditions", { + "to_value": 100, + }, + ) + sr.append( + "conditions", + { "doctype": "Shipping Rule Condition", "from_value": 101, "parentfield": "conditions", "shipping_amount": 100.0, - "to_value": 200 - }) - sr.append("conditions", { + "to_value": 200, + }, + ) + sr.append( + "conditions", + { "doctype": "Shipping Rule Condition", "from_value": 201, "parentfield": "conditions", "shipping_amount": 200.0, - "to_value": 2000 - }) + "to_value": 2000, + }, + ) sr.insert(ignore_permissions=True) sr.submit() return sr diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py index 5942ce5d637..62ffc79fee8 100644 --- a/erpnext/accounts/doctype/subscription/subscription.py +++ b/erpnext/accounts/doctype/subscription/subscription.py @@ -1,4 +1,3 @@ - # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors # For license information, please see license.txt @@ -24,6 +23,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( get_accounting_dimensions, ) from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate +from erpnext.accounts.party import get_party_account_currency class Subscription(Document): @@ -60,7 +60,11 @@ class Subscription(Document): """ _current_invoice_start = None - if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date): + if ( + self.is_new_subscription() + and self.trial_period_end + and getdate(self.trial_period_end) > getdate(self.start_date) + ): _current_invoice_start = add_days(self.trial_period_end, 1) elif self.trial_period_start and self.is_trialling(): _current_invoice_start = self.trial_period_start @@ -102,7 +106,7 @@ class Subscription(Document): if self.follow_calendar_months: billing_info = self.get_billing_cycle_and_interval() - billing_interval_count = billing_info[0]['billing_interval_count'] + billing_interval_count = billing_info[0]["billing_interval_count"] calendar_months = get_calendar_months(billing_interval_count) calendar_month = 0 current_invoice_end_month = getdate(_current_invoice_end).month @@ -112,12 +116,13 @@ class Subscription(Document): if month <= current_invoice_end_month: calendar_month = month - if cint(calendar_month - billing_interval_count) <= 0 and \ - getdate(date).month != 1: + if cint(calendar_month - billing_interval_count) <= 0 and getdate(date).month != 1: calendar_month = 12 current_invoice_end_year -= 1 - _current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' + cstr(calendar_month) + '-01') + _current_invoice_end = get_last_day( + cstr(current_invoice_end_year) + "-" + cstr(calendar_month) + "-01" + ) if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date): _current_invoice_end = self.end_date @@ -131,7 +136,7 @@ class Subscription(Document): same billing interval """ if billing_cycle_data and len(billing_cycle_data) != 1: - frappe.throw(_('You can only have Plans with the same billing cycle in a Subscription')) + frappe.throw(_("You can only have Plans with the same billing cycle in a Subscription")) def get_billing_cycle_and_interval(self): """ @@ -141,10 +146,11 @@ class Subscription(Document): """ plan_names = [plan.plan for plan in self.plans] billing_info = frappe.db.sql( - 'select distinct `billing_interval`, `billing_interval_count` ' - 'from `tabSubscription Plan` ' - 'where name in %s', - (plan_names,), as_dict=1 + "select distinct `billing_interval`, `billing_interval_count` " + "from `tabSubscription Plan` " + "where name in %s", + (plan_names,), + as_dict=1, ) return billing_info @@ -161,19 +167,19 @@ class Subscription(Document): if billing_info: data = dict() - interval = billing_info[0]['billing_interval'] - interval_count = billing_info[0]['billing_interval_count'] - if interval not in ['Day', 'Week']: - data['days'] = -1 - if interval == 'Day': - data['days'] = interval_count - 1 - elif interval == 'Month': - data['months'] = interval_count - elif interval == 'Year': - data['years'] = interval_count + interval = billing_info[0]["billing_interval"] + interval_count = billing_info[0]["billing_interval_count"] + if interval not in ["Day", "Week"]: + data["days"] = -1 + if interval == "Day": + data["days"] = interval_count - 1 + elif interval == "Month": + data["months"] = interval_count + elif interval == "Year": + data["years"] = interval_count # todo: test week - elif interval == 'Week': - data['days'] = interval_count * 7 - 1 + elif interval == "Week": + data["days"] = interval_count * 7 - 1 return data @@ -184,27 +190,27 @@ class Subscription(Document): Used when the `Subscription` needs to decide what to do after the current generated invoice is past it's due date and grace period. """ - subscription_settings = frappe.get_single('Subscription Settings') - if self.status == 'Past Due Date' and self.is_past_grace_period(): - self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid' + subscription_settings = frappe.get_single("Subscription Settings") + if self.status == "Past Due Date" and self.is_past_grace_period(): + self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid" def set_subscription_status(self): """ Sets the status of the `Subscription` """ if self.is_trialling(): - self.status = 'Trialling' - elif self.status == 'Active' and self.end_date and getdate() > getdate(self.end_date): - self.status = 'Completed' + self.status = "Trialling" + elif self.status == "Active" and self.end_date and getdate() > getdate(self.end_date): + self.status = "Completed" elif self.is_past_grace_period(): - subscription_settings = frappe.get_single('Subscription Settings') - self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid' + subscription_settings = frappe.get_single("Subscription Settings") + self.status = "Cancelled" if cint(subscription_settings.cancel_after_grace) else "Unpaid" elif self.current_invoice_is_past_due() and not self.is_past_grace_period(): - self.status = 'Past Due Date' + self.status = "Past Due Date" elif not self.has_outstanding_invoice(): - self.status = 'Active' + self.status = "Active" elif self.is_new_subscription(): - self.status = 'Active' + self.status = "Active" self.save() def is_trialling(self): @@ -231,7 +237,7 @@ class Subscription(Document): """ current_invoice = self.get_current_invoice() if self.current_invoice_is_past_due(current_invoice): - subscription_settings = frappe.get_single('Subscription Settings') + subscription_settings = frappe.get_single("Subscription Settings") grace_period = cint(subscription_settings.grace_period) return getdate() > add_days(current_invoice.due_date, grace_period) @@ -252,15 +258,15 @@ class Subscription(Document): """ Returns the most recent generated invoice. """ - doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' + doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" if len(self.invoices): current = self.invoices[-1] - if frappe.db.exists(doctype, current.get('invoice')): - doc = frappe.get_doc(doctype, current.get('invoice')) + if frappe.db.exists(doctype, current.get("invoice")): + doc = frappe.get_doc(doctype, current.get("invoice")) return doc else: - frappe.throw(_('Invoice {0} no longer exists').format(current.get('invoice'))) + frappe.throw(_("Invoice {0} no longer exists").format(current.get("invoice"))) def is_new_subscription(self): """ @@ -273,7 +279,7 @@ class Subscription(Document): self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) self.validate_end_date() self.validate_to_follow_calendar_months() - self.cost_center = erpnext.get_default_cost_center(self.get('company')) + self.cost_center = erpnext.get_default_cost_center(self.get("company")) def validate_trial_period(self): """ @@ -281,30 +287,34 @@ class Subscription(Document): """ if self.trial_period_start and self.trial_period_end: if getdate(self.trial_period_end) < getdate(self.trial_period_start): - frappe.throw(_('Trial Period End Date Cannot be before Trial Period Start Date')) + frappe.throw(_("Trial Period End Date Cannot be before Trial Period Start Date")) if self.trial_period_start and not self.trial_period_end: - frappe.throw(_('Both Trial Period Start Date and Trial Period End Date must be set')) + frappe.throw(_("Both Trial Period Start Date and Trial Period End Date must be set")) if self.trial_period_start and getdate(self.trial_period_start) > getdate(self.start_date): - frappe.throw(_('Trial Period Start date cannot be after Subscription Start Date')) + frappe.throw(_("Trial Period Start date cannot be after Subscription Start Date")) def validate_end_date(self): billing_cycle_info = self.get_billing_cycle_data() end_date = add_to_date(self.start_date, **billing_cycle_info) if self.end_date and getdate(self.end_date) <= getdate(end_date): - frappe.throw(_('Subscription End Date must be after {0} as per the subscription plan').format(end_date)) + frappe.throw( + _("Subscription End Date must be after {0} as per the subscription plan").format(end_date) + ) def validate_to_follow_calendar_months(self): if self.follow_calendar_months: billing_info = self.get_billing_cycle_and_interval() if not self.end_date: - frappe.throw(_('Subscription End Date is mandatory to follow calendar months')) + frappe.throw(_("Subscription End Date is mandatory to follow calendar months")) - if billing_info[0]['billing_interval'] != 'Month': - frappe.throw(_('Billing Interval in Subscription Plan must be Month to follow calendar months')) + if billing_info[0]["billing_interval"] != "Month": + frappe.throw( + _("Billing Interval in Subscription Plan must be Month to follow calendar months") + ) def after_insert(self): # todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype? @@ -316,13 +326,10 @@ class Subscription(Document): saves the `Subscription`. """ - doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' + doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" invoice = self.create_invoice(prorate) - self.append('invoices', { - 'document_type': doctype, - 'invoice': invoice.name - }) + self.append("invoices", {"document_type": doctype, "invoice": invoice.name}) self.save() @@ -332,52 +339,58 @@ class Subscription(Document): """ Creates a `Invoice`, submits it and returns it """ - doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' + doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" invoice = frappe.new_doc(doctype) # For backward compatibility # Earlier subscription didn't had any company field - company = self.get('company') or get_default_company() + company = self.get("company") or get_default_company() if not company: - frappe.throw(_("Company is mandatory was generating invoice. Please set default company in Global Defaults")) + frappe.throw( + _("Company is mandatory was generating invoice. Please set default company in Global Defaults") + ) invoice.company = company invoice.set_posting_time = 1 - invoice.posting_date = self.current_invoice_start if self.generate_invoice_at_period_start \ + invoice.posting_date = ( + self.current_invoice_start + if self.generate_invoice_at_period_start else self.current_invoice_end + ) invoice.cost_center = self.cost_center - if doctype == 'Sales Invoice': + if doctype == "Sales Invoice": invoice.customer = self.party else: invoice.supplier = self.party - if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'): + if frappe.db.get_value("Supplier", self.party, "tax_withholding_category"): invoice.apply_tds = 1 - ## Add dimensions in invoice for subscription: + # Add party currency to invoice + invoice.currency = get_party_account_currency(self.party_type, self.party, self.company) + + # Add dimensions in invoice for subscription: accounting_dimensions = get_accounting_dimensions() for dimension in accounting_dimensions: if self.get(dimension): - invoice.update({ - dimension: self.get(dimension) - }) + invoice.update({dimension: self.get(dimension)}) # Subscription is better suited for service items. I won't update `update_stock` # for that reason items_list = self.get_items_from_plans(self.plans, prorate) for item in items_list: - item['cost_center'] = self.cost_center - invoice.append('items', item) + item["cost_center"] = self.cost_center + invoice.append("items", item) # Taxes - tax_template = '' + tax_template = "" - if doctype == 'Sales Invoice' and self.sales_tax_template: + if doctype == "Sales Invoice" and self.sales_tax_template: tax_template = self.sales_tax_template - if doctype == 'Purchase Invoice' and self.purchase_tax_template: + if doctype == "Purchase Invoice" and self.purchase_tax_template: tax_template = self.purchase_tax_template if tax_template: @@ -387,11 +400,11 @@ class Subscription(Document): # Due date if self.days_until_due: invoice.append( - 'payment_schedule', + "payment_schedule", { - 'due_date': add_days(invoice.posting_date, cint(self.days_until_due)), - 'invoice_portion': 100 - } + "due_date": add_days(invoice.posting_date, cint(self.days_until_due)), + "invoice_portion": 100, + }, ) # Discounts @@ -403,7 +416,7 @@ class Subscription(Document): if self.additional_discount_percentage or self.additional_discount_amount: discount_on = self.apply_additional_discount - invoice.apply_discount_on = discount_on if discount_on else 'Grand Total' + invoice.apply_discount_on = discount_on if discount_on else "Grand Total" # Subscription period invoice.from_date = self.current_invoice_start @@ -423,44 +436,62 @@ class Subscription(Document): Returns the `Item`s linked to `Subscription Plan` """ if prorate: - prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start, - self.generate_invoice_at_period_start) + prorate_factor = get_prorata_factor( + self.current_invoice_end, self.current_invoice_start, self.generate_invoice_at_period_start + ) items = [] party = self.party for plan in plans: - plan_doc = frappe.get_doc('Subscription Plan', plan.plan) + plan_doc = frappe.get_doc("Subscription Plan", plan.plan) item_code = plan_doc.item - if self.party == 'Customer': - deferred_field = 'enable_deferred_revenue' + if self.party == "Customer": + deferred_field = "enable_deferred_revenue" else: - deferred_field = 'enable_deferred_expense' + deferred_field = "enable_deferred_expense" - deferred = frappe.db.get_value('Item', item_code, deferred_field) + deferred = frappe.db.get_value("Item", item_code, deferred_field) if not prorate: - item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party, - self.current_invoice_start, self.current_invoice_end), 'cost_center': plan_doc.cost_center} + item = { + "item_code": item_code, + "qty": plan.qty, + "rate": get_plan_rate( + plan.plan, plan.qty, party, self.current_invoice_start, self.current_invoice_end + ), + "cost_center": plan_doc.cost_center, + } else: - item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party, - self.current_invoice_start, self.current_invoice_end, prorate_factor), 'cost_center': plan_doc.cost_center} + item = { + "item_code": item_code, + "qty": plan.qty, + "rate": get_plan_rate( + plan.plan, + plan.qty, + party, + self.current_invoice_start, + self.current_invoice_end, + prorate_factor, + ), + "cost_center": plan_doc.cost_center, + } if deferred: - item.update({ - deferred_field: deferred, - 'service_start_date': self.current_invoice_start, - 'service_end_date': self.current_invoice_end - }) + item.update( + { + deferred_field: deferred, + "service_start_date": self.current_invoice_start, + "service_end_date": self.current_invoice_end, + } + ) accounting_dimensions = get_accounting_dimensions() for dimension in accounting_dimensions: if plan_doc.get(dimension): - item.update({ - dimension: plan_doc.get(dimension) - }) + item.update({dimension: plan_doc.get(dimension)}) items.append(item) @@ -473,9 +504,9 @@ class Subscription(Document): 1. `process_for_active` 2. `process_for_past_due` """ - if self.status == 'Active': + if self.status == "Active": self.process_for_active() - elif self.status in ['Past Due Date', 'Unpaid']: + elif self.status in ["Past Due Date", "Unpaid"]: self.process_for_past_due_date() self.set_subscription_status() @@ -483,8 +514,10 @@ class Subscription(Document): self.save() def is_postpaid_to_invoice(self): - return getdate() > getdate(self.current_invoice_end) or \ - (getdate() >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) + return getdate() > getdate(self.current_invoice_end) or ( + getdate() >= getdate(self.current_invoice_end) + and getdate(self.current_invoice_end) == getdate(self.current_invoice_start) + ) def is_prepaid_to_invoice(self): if not self.generate_invoice_at_period_start: @@ -500,9 +533,13 @@ class Subscription(Document): invoice = self.get_current_invoice() if not (_current_start_date and _current_end_date): - _current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True) + _current_start_date, _current_end_date = self.update_subscription_period( + date=add_days(self.current_invoice_end, 1), return_date=True + ) - if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date): + if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate( + _current_end_date + ): return True return False @@ -517,10 +554,11 @@ class Subscription(Document): 3. Change the `Subscription` status to 'Cancelled' """ - if not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \ - and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): + if not self.is_current_invoice_generated( + self.current_invoice_start, self.current_invoice_end + ) and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): - prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') + prorate = frappe.db.get_single_value("Subscription Settings", "prorate") self.generate_invoice(prorate) if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice(): @@ -536,7 +574,7 @@ class Subscription(Document): if self.end_date and getdate() < getdate(self.end_date): return - self.status = 'Cancelled' + self.status = "Cancelled" if not self.cancelation_date: self.cancelation_date = nowdate() @@ -551,19 +589,21 @@ class Subscription(Document): """ current_invoice = self.get_current_invoice() if not current_invoice: - frappe.throw(_('Current invoice {0} is missing').format(current_invoice.invoice)) + frappe.throw(_("Current invoice {0} is missing").format(current_invoice.invoice)) else: if not self.has_outstanding_invoice(): - self.status = 'Active' + self.status = "Active" else: self.set_status_grace_period() # Generate invoices periodically even if current invoice are unpaid - if self.generate_new_invoices_past_due_date and not \ - self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \ - and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): + if ( + self.generate_new_invoices_past_due_date + and not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) + and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()) + ): - prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') + prorate = frappe.db.get_single_value("Subscription Settings", "prorate") self.generate_invoice(prorate) if getdate() > getdate(self.current_invoice_end): @@ -574,18 +614,19 @@ class Subscription(Document): """ Return `True` if the given invoice is paid """ - return invoice.status == 'Paid' + return invoice.status == "Paid" def has_outstanding_invoice(self): """ Returns `True` if the most recent invoice for the `Subscription` is not paid """ - doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' + doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" current_invoice = self.get_current_invoice() invoice_list = [d.invoice for d in self.invoices] - outstanding_invoices = frappe.get_all(doctype, fields=['name'], - filters={'status': ('!=', 'Paid'), 'name': ('in', invoice_list)}) + outstanding_invoices = frappe.get_all( + doctype, fields=["name"], filters={"status": ("!=", "Paid"), "name": ("in", invoice_list)} + ) if outstanding_invoices: return True @@ -597,10 +638,12 @@ class Subscription(Document): This sets the subscription as cancelled. It will stop invoices from being generated but it will not affect already created invoices. """ - if self.status != 'Cancelled': - to_generate_invoice = True if self.status == 'Active' and not self.generate_invoice_at_period_start else False - to_prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') - self.status = 'Cancelled' + if self.status != "Cancelled": + to_generate_invoice = ( + True if self.status == "Active" and not self.generate_invoice_at_period_start else False + ) + to_prorate = frappe.db.get_single_value("Subscription Settings", "prorate") + self.status = "Cancelled" self.cancelation_date = nowdate() if to_generate_invoice: self.generate_invoice(prorate=to_prorate) @@ -612,19 +655,20 @@ class Subscription(Document): subscription and the `Subscription` will lose all the history of generated invoices it has. """ - if self.status == 'Cancelled': - self.status = 'Active' - self.db_set('start_date', nowdate()) + if self.status == "Cancelled": + self.status = "Active" + self.db_set("start_date", nowdate()) self.update_subscription_period(nowdate()) self.invoices = [] self.save() else: - frappe.throw(_('You cannot restart a Subscription that is not cancelled.')) + frappe.throw(_("You cannot restart a Subscription that is not cancelled.")) def get_precision(self): invoice = self.get_current_invoice() if invoice: - return invoice.precision('grand_total') + return invoice.precision("grand_total") + def get_calendar_months(billing_interval): calendar_months = [] @@ -635,6 +679,7 @@ def get_calendar_months(billing_interval): return calendar_months + def get_prorata_factor(period_end, period_start, is_prepaid): if is_prepaid: prorate_factor = 1 @@ -659,7 +704,7 @@ def get_all_subscriptions(): """ Returns all `Subscription` documents """ - return frappe.db.get_all('Subscription', {'status': ('!=','Cancelled')}) + return frappe.db.get_all("Subscription", {"status": ("!=", "Cancelled")}) def process(data): @@ -668,7 +713,7 @@ def process(data): """ if data: try: - subscription = frappe.get_doc('Subscription', data['name']) + subscription = frappe.get_doc("Subscription", data["name"]) subscription.process() frappe.db.commit() except frappe.ValidationError: @@ -684,7 +729,7 @@ def cancel_subscription(name): Cancels a `Subscription`. This will stop the `Subscription` from further invoicing the `Subscriber` but all already outstanding invoices will not be affected. """ - subscription = frappe.get_doc('Subscription', name) + subscription = frappe.get_doc("Subscription", name) subscription.cancel_subscription() @@ -694,7 +739,7 @@ def restart_subscription(name): Restarts a cancelled `Subscription`. The `Subscription` will 'forget' the history of all invoices it has generated """ - subscription = frappe.get_doc('Subscription', name) + subscription = frappe.get_doc("Subscription", name) subscription.restart_subscription() @@ -703,5 +748,5 @@ def get_subscription_updates(name): """ Use this to get the latest state of the given `Subscription` """ - subscription = frappe.get_doc('Subscription', name) + subscription = frappe.get_doc("Subscription", name) subscription.process() diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py index 9dd370bd472..eb17daa282f 100644 --- a/erpnext/accounts/doctype/subscription/test_subscription.py +++ b/erpnext/accounts/doctype/subscription/test_subscription.py @@ -18,81 +18,111 @@ from erpnext.accounts.doctype.subscription.subscription import get_prorata_facto test_dependencies = ("UOM", "Item Group", "Item") + def create_plan(): - if not frappe.db.exists('Subscription Plan', '_Test Plan Name'): - plan = frappe.new_doc('Subscription Plan') - plan.plan_name = '_Test Plan Name' - plan.item = '_Test Non Stock Item' + if not frappe.db.exists("Subscription Plan", "_Test Plan Name"): + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = "_Test Plan Name" + plan.item = "_Test Non Stock Item" plan.price_determination = "Fixed Rate" plan.cost = 900 - plan.billing_interval = 'Month' + plan.billing_interval = "Month" plan.billing_interval_count = 1 plan.insert() - if not frappe.db.exists('Subscription Plan', '_Test Plan Name 2'): - plan = frappe.new_doc('Subscription Plan') - plan.plan_name = '_Test Plan Name 2' - plan.item = '_Test Non Stock Item' + if not frappe.db.exists("Subscription Plan", "_Test Plan Name 2"): + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = "_Test Plan Name 2" + plan.item = "_Test Non Stock Item" plan.price_determination = "Fixed Rate" plan.cost = 1999 - plan.billing_interval = 'Month' + plan.billing_interval = "Month" plan.billing_interval_count = 1 plan.insert() - if not frappe.db.exists('Subscription Plan', '_Test Plan Name 3'): - plan = frappe.new_doc('Subscription Plan') - plan.plan_name = '_Test Plan Name 3' - plan.item = '_Test Non Stock Item' + if not frappe.db.exists("Subscription Plan", "_Test Plan Name 3"): + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = "_Test Plan Name 3" + plan.item = "_Test Non Stock Item" plan.price_determination = "Fixed Rate" plan.cost = 1999 - plan.billing_interval = 'Day' + plan.billing_interval = "Day" plan.billing_interval_count = 14 plan.insert() # Defined a quarterly Subscription Plan - if not frappe.db.exists('Subscription Plan', '_Test Plan Name 4'): - plan = frappe.new_doc('Subscription Plan') - plan.plan_name = '_Test Plan Name 4' - plan.item = '_Test Non Stock Item' + if not frappe.db.exists("Subscription Plan", "_Test Plan Name 4"): + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = "_Test Plan Name 4" + plan.item = "_Test Non Stock Item" plan.price_determination = "Monthly Rate" plan.cost = 20000 - plan.billing_interval = 'Month' + plan.billing_interval = "Month" plan.billing_interval_count = 3 plan.insert() - if not frappe.db.exists('Supplier', '_Test Supplier'): - supplier = frappe.new_doc('Supplier') - supplier.supplier_name = '_Test Supplier' - supplier.supplier_group = 'All Supplier Groups' + if not frappe.db.exists("Subscription Plan", "_Test Plan Multicurrency"): + plan = frappe.new_doc("Subscription Plan") + plan.plan_name = "_Test Plan Multicurrency" + plan.item = "_Test Non Stock Item" + plan.price_determination = "Fixed Rate" + plan.cost = 50 + plan.currency = "USD" + plan.billing_interval = "Month" + plan.billing_interval_count = 1 + plan.insert() + + +def create_parties(): + if not frappe.db.exists("Supplier", "_Test Supplier"): + supplier = frappe.new_doc("Supplier") + supplier.supplier_name = "_Test Supplier" + supplier.supplier_group = "All Supplier Groups" supplier.insert() + if not frappe.db.exists("Customer", "_Test Subscription Customer"): + customer = frappe.new_doc("Customer") + customer.customer_name = "_Test Subscription Customer" + customer.billing_currency = "USD" + customer.append( + "accounts", {"company": "_Test Company", "account": "_Test Receivable USD - _TC"} + ) + customer.insert() + + class TestSubscription(unittest.TestCase): def setUp(self): create_plan() + create_parties() def test_create_subscription_with_trial_with_correct_period(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.trial_period_start = nowdate() subscription.trial_period_end = add_months(nowdate(), 1) - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() self.assertEqual(subscription.trial_period_start, nowdate()) self.assertEqual(subscription.trial_period_end, add_months(nowdate(), 1)) - self.assertEqual(add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start)) - self.assertEqual(add_to_date(subscription.current_invoice_start, months=1, days=-1), get_date_str(subscription.current_invoice_end)) + self.assertEqual( + add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start) + ) + self.assertEqual( + add_to_date(subscription.current_invoice_start, months=1, days=-1), + get_date_str(subscription.current_invoice_end), + ) self.assertEqual(subscription.invoices, []) - self.assertEqual(subscription.status, 'Trialling') + self.assertEqual(subscription.status, "Trialling") subscription.delete() def test_create_subscription_without_trial_with_correct_period(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() self.assertEqual(subscription.trial_period_start, None) @@ -101,190 +131,190 @@ class TestSubscription(unittest.TestCase): self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) # No invoice is created self.assertEqual(len(subscription.invoices), 0) - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") subscription.delete() def test_create_subscription_trial_with_wrong_dates(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.trial_period_end = nowdate() subscription.trial_period_start = add_days(nowdate(), 30) - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) self.assertRaises(frappe.ValidationError, subscription.save) subscription.delete() def test_create_subscription_multi_with_different_billing_fails(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.trial_period_end = nowdate() subscription.trial_period_start = add_days(nowdate(), 30) - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.append('plans', {'plan': '_Test Plan Name 3', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.append("plans", {"plan": "_Test Plan Name 3", "qty": 1}) self.assertRaises(frappe.ValidationError, subscription.save) subscription.delete() def test_invoice_is_generated_at_end_of_billing_period(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.start_date = '2018-01-01' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.start_date = "2018-01-01" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.insert() - self.assertEqual(subscription.status, 'Active') - self.assertEqual(subscription.current_invoice_start, '2018-01-01') - self.assertEqual(subscription.current_invoice_end, '2018-01-31') + self.assertEqual(subscription.status, "Active") + self.assertEqual(subscription.current_invoice_start, "2018-01-01") + self.assertEqual(subscription.current_invoice_end, "2018-01-31") subscription.process() self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.current_invoice_start, '2018-01-01') + self.assertEqual(subscription.current_invoice_start, "2018-01-01") subscription.process() - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") subscription.delete() def test_status_goes_back_to_active_after_invoice_is_paid(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice self.assertEqual(len(subscription.invoices), 1) # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") subscription.get_current_invoice() current_invoice = subscription.get_current_invoice() self.assertIsNotNone(current_invoice) - current_invoice.db_set('outstanding_amount', 0) - current_invoice.db_set('status', 'Paid') + current_invoice.db_set("outstanding_amount", 0) + current_invoice.db_set("status", "Paid") subscription.process() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1)) self.assertEqual(len(subscription.invoices), 1) subscription.delete() def test_subscription_cancel_after_grace_period(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") default_grace_period_action = settings.cancel_after_grace settings.cancel_after_grace = 1 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") - subscription.process() # generate first invoice + subscription.process() # generate first invoice # This should change status to Cancelled since grace period is 0 # And is backdated subscription so subscription will be cancelled after processing - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") settings.cancel_after_grace = default_grace_period_action settings.save() subscription.delete() def test_subscription_unpaid_after_grace_period(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") default_grace_period_action = settings.cancel_after_grace settings.cancel_after_grace = 0 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") settings.cancel_after_grace = default_grace_period_action settings.save() subscription.delete() def test_subscription_invoice_days_until_due(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.days_until_due = 10 subscription.start_date = add_months(nowdate(), -1) subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") subscription.delete() def test_subscription_is_past_due_doesnt_change_within_grace_period(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") grace_period = settings.grace_period settings.grace_period = 1000 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.start_date = add_days(nowdate(), -1000) subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice - self.assertEqual(subscription.status, 'Past Due Date') + self.assertEqual(subscription.status, "Past Due Date") subscription.process() # Grace period is 1000 days so status should remain as Past Due Date - self.assertEqual(subscription.status, 'Past Due Date') + self.assertEqual(subscription.status, "Past Due Date") subscription.process() - self.assertEqual(subscription.status, 'Past Due Date') + self.assertEqual(subscription.status, "Past Due Date") subscription.process() - self.assertEqual(subscription.status, 'Past Due Date') + self.assertEqual(subscription.status, "Past Due Date") settings.grace_period = grace_period settings.save() subscription.delete() def test_subscription_remains_active_during_invoice_period(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() - subscription.process() # no changes expected + subscription.process() # no changes expected - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.current_invoice_start, nowdate()) self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) self.assertEqual(len(subscription.invoices), 0) - subscription.process() # no changes expected still - self.assertEqual(subscription.status, 'Active') + subscription.process() # no changes expected still + self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.current_invoice_start, nowdate()) self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) self.assertEqual(len(subscription.invoices), 0) - subscription.process() # no changes expected yet still - self.assertEqual(subscription.status, 'Active') + subscription.process() # no changes expected yet still + self.assertEqual(subscription.status, "Active") self.assertEqual(subscription.current_invoice_start, nowdate()) self.assertEqual(subscription.current_invoice_end, add_to_date(nowdate(), months=1, days=-1)) self.assertEqual(len(subscription.invoices), 0) @@ -292,30 +322,30 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_cancelation(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.cancel_subscription() - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") subscription.delete() def test_subscription_cancellation_invoices(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") to_prorate = settings.prorate settings.prorate = 1 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") subscription.cancel_subscription() # Invoice must have been generated @@ -323,33 +353,39 @@ class TestSubscription(unittest.TestCase): invoice = subscription.get_current_invoice() diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) - prorate_factor = flt(diff/plan_days) + plan_days = flt( + date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 + ) + prorate_factor = flt(diff / plan_days) self.assertEqual( flt( - get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start, - subscription.generate_invoice_at_period_start), - 2), - flt(prorate_factor, 2) + get_prorata_factor( + subscription.current_invoice_end, + subscription.current_invoice_start, + subscription.generate_invoice_at_period_start, + ), + 2, + ), + flt(prorate_factor, 2), ) self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") subscription.delete() settings.prorate = to_prorate settings.save() def test_subscription_cancellation_invoices_with_prorata_false(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") to_prorate = settings.prorate settings.prorate = 0 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.cancel_subscription() invoice = subscription.get_current_invoice() @@ -362,21 +398,23 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_cancellation_invoices_with_prorata_true(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") to_prorate = settings.prorate settings.prorate = 1 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.cancel_subscription() invoice = subscription.get_current_invoice() diff = flt(date_diff(nowdate(), subscription.current_invoice_start) + 1) - plan_days = flt(date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1) + plan_days = flt( + date_diff(subscription.current_invoice_end, subscription.current_invoice_start) + 1 + ) prorate_factor = flt(diff / plan_days) self.assertEqual(flt(invoice.grand_total, 2), flt(prorate_factor * 900, 2)) @@ -387,30 +425,30 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subcription_cancellation_and_process(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") default_grace_period_action = settings.cancel_after_grace settings.cancel_after_grace = 1 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice invoices = len(subscription.invoices) subscription.cancel_subscription() - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") self.assertEqual(len(subscription.invoices), invoices) subscription.process() - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") self.assertEqual(len(subscription.invoices), invoices) subscription.process() - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") self.assertEqual(len(subscription.invoices), invoices) settings.cancel_after_grace = default_grace_period_action @@ -418,36 +456,36 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_restart_and_process(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") default_grace_period_action = settings.cancel_after_grace settings.grace_period = 0 settings.cancel_after_grace = 0 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice # Status is unpaid as Days until Due is zero and grace period is Zero - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") subscription.cancel_subscription() - self.assertEqual(subscription.status, 'Cancelled') + self.assertEqual(subscription.status, "Cancelled") subscription.restart_subscription() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") self.assertEqual(len(subscription.invoices), 0) subscription.process() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") self.assertEqual(len(subscription.invoices), 0) subscription.process() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") self.assertEqual(len(subscription.invoices), 0) settings.cancel_after_grace = default_grace_period_action @@ -455,42 +493,42 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_unpaid_back_to_active(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") default_grace_period_action = settings.cancel_after_grace settings.cancel_after_grace = 0 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) - subscription.start_date = '2018-01-01' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) + subscription.start_date = "2018-01-01" subscription.insert() - subscription.process() # generate first invoice + subscription.process() # generate first invoice # This should change status to Unpaid since grace period is 0 - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") invoice = subscription.get_current_invoice() - invoice.db_set('outstanding_amount', 0) - invoice.db_set('status', 'Paid') + invoice.db_set("outstanding_amount", 0) + invoice.db_set("status", "Paid") subscription.process() - self.assertEqual(subscription.status, 'Active') + self.assertEqual(subscription.status, "Active") # A new invoice is generated subscription.process() - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") settings.cancel_after_grace = default_grace_period_action settings.save() subscription.delete() def test_restart_active_subscription(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() self.assertRaises(frappe.ValidationError, subscription.restart_subscription) @@ -498,44 +536,44 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_invoice_discount_percentage(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.additional_discount_percentage = 10 - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.cancel_subscription() invoice = subscription.get_current_invoice() self.assertEqual(invoice.additional_discount_percentage, 10) - self.assertEqual(invoice.apply_discount_on, 'Grand Total') + self.assertEqual(invoice.apply_discount_on, "Grand Total") subscription.delete() def test_subscription_invoice_discount_amount(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.additional_discount_amount = 11 - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.cancel_subscription() invoice = subscription.get_current_invoice() self.assertEqual(invoice.discount_amount, 11) - self.assertEqual(invoice.apply_discount_on, 'Grand Total') + self.assertEqual(invoice.apply_discount_on, "Grand Total") subscription.delete() def test_prepaid_subscriptions(self): # Create a non pre-billed subscription, processing should not create # invoices. - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.process() @@ -550,16 +588,16 @@ class TestSubscription(unittest.TestCase): self.assertEqual(len(subscription.invoices), 1) def test_prepaid_subscriptions_with_prorate_true(self): - settings = frappe.get_single('Subscription Settings') + settings = frappe.get_single("Subscription Settings") to_prorate = settings.prorate settings.prorate = 1 settings.save() - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Customer' - subscription.party = '_Test Customer' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Customer" subscription.generate_invoice_at_period_start = True - subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) + subscription.append("plans", {"plan": "_Test Plan Name", "qty": 1}) subscription.save() subscription.process() subscription.cancel_subscription() @@ -579,38 +617,38 @@ class TestSubscription(unittest.TestCase): subscription.delete() def test_subscription_with_follow_calendar_months(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Supplier' - subscription.party = '_Test Supplier' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Supplier" + subscription.party = "_Test Supplier" subscription.generate_invoice_at_period_start = 1 subscription.follow_calendar_months = 1 # select subscription start date as '2018-01-15' - subscription.start_date = '2018-01-15' - subscription.end_date = '2018-07-15' - subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1}) + subscription.start_date = "2018-01-15" + subscription.end_date = "2018-07-15" + subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) subscription.save() # even though subscription starts at '2018-01-15' and Billing interval is Month and count 3 # First invoice will end at '2018-03-31' instead of '2018-04-14' - self.assertEqual(get_date_str(subscription.current_invoice_end), '2018-03-31') + self.assertEqual(get_date_str(subscription.current_invoice_end), "2018-03-31") def test_subscription_generate_invoice_past_due(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Supplier' - subscription.party = '_Test Supplier' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Supplier" + subscription.party = "_Test Supplier" subscription.generate_invoice_at_period_start = 1 subscription.generate_new_invoices_past_due_date = 1 # select subscription start date as '2018-01-15' - subscription.start_date = '2018-01-01' - subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1}) + subscription.start_date = "2018-01-01" + subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) subscription.save() # Process subscription and create first invoice # Subscription status will be unpaid since due date has already passed subscription.process() self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") # Now the Subscription is unpaid # Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in @@ -620,20 +658,39 @@ class TestSubscription(unittest.TestCase): self.assertEqual(len(subscription.invoices), 2) def test_subscription_without_generate_invoice_past_due(self): - subscription = frappe.new_doc('Subscription') - subscription.party_type = 'Supplier' - subscription.party = '_Test Supplier' + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Supplier" + subscription.party = "_Test Supplier" subscription.generate_invoice_at_period_start = 1 # select subscription start date as '2018-01-15' - subscription.start_date = '2018-01-01' - subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1}) + subscription.start_date = "2018-01-01" + subscription.append("plans", {"plan": "_Test Plan Name 4", "qty": 1}) subscription.save() # Process subscription and create first invoice # Subscription status will be unpaid since due date has already passed subscription.process() self.assertEqual(len(subscription.invoices), 1) - self.assertEqual(subscription.status, 'Unpaid') + self.assertEqual(subscription.status, "Unpaid") subscription.process() self.assertEqual(len(subscription.invoices), 1) + + def test_multicurrency_subscription(self): + subscription = frappe.new_doc("Subscription") + subscription.party_type = "Customer" + subscription.party = "_Test Subscription Customer" + subscription.generate_invoice_at_period_start = 1 + subscription.company = "_Test Company" + # select subscription start date as '2018-01-15' + subscription.start_date = "2018-01-01" + subscription.append("plans", {"plan": "_Test Plan Multicurrency", "qty": 1}) + subscription.save() + + subscription.process() + self.assertEqual(len(subscription.invoices), 1) + self.assertEqual(subscription.status, "Unpaid") + + # Check the currency of the created invoice + currency = frappe.db.get_value("Sales Invoice", subscription.invoices[0].invoice, "currency") + self.assertEqual(currency, "USD") diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json index 878ae098891..563df79eec7 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.json +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.json @@ -75,7 +75,8 @@ "fieldname": "cost", "fieldtype": "Currency", "in_list_view": 1, - "label": "Cost" + "label": "Cost", + "options": "currency" }, { "depends_on": "eval:doc.price_determination==\"Based On Price List\"", @@ -147,7 +148,7 @@ } ], "links": [], - "modified": "2021-08-13 10:53:44.205774", + "modified": "2021-12-10 15:24:15.794477", "modified_by": "Administrator", "module": "Accounts", "name": "Subscription Plan", diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py index 1285343d196..a95e0a9c2da 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan.py @@ -16,10 +16,13 @@ class SubscriptionPlan(Document): def validate_interval_count(self): if self.billing_interval_count < 1: - frappe.throw(_('Billing Interval Count cannot be less than 1')) + frappe.throw(_("Billing Interval Count cannot be less than 1")) + @frappe.whitelist() -def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1): +def get_plan_rate( + plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1 +): plan = frappe.get_doc("Subscription Plan", plan) if plan.price_determination == "Fixed Rate": return plan.cost * prorate_factor @@ -30,13 +33,19 @@ def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=Non else: customer_group = None - price = get_price(item_code=plan.item, price_list=plan.price_list, customer_group=customer_group, company=None, qty=quantity) + price = get_price( + item_code=plan.item, + price_list=plan.price_list, + customer_group=customer_group, + company=None, + qty=quantity, + ) if not price: return 0 else: return price.price_list_rate * prorate_factor - elif plan.price_determination == 'Monthly Rate': + elif plan.price_determination == "Monthly Rate": start_date = getdate(start_date) end_date = getdate(end_date) @@ -44,15 +53,21 @@ def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=Non cost = plan.cost * no_of_months # Adjust cost if start or end date is not month start or end - prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') + prorate = frappe.db.get_single_value("Subscription Settings", "prorate") if prorate: - prorate_factor = flt(date_diff(start_date, get_first_day(start_date)) / date_diff( - get_last_day(start_date), get_first_day(start_date)), 1) + prorate_factor = flt( + date_diff(start_date, get_first_day(start_date)) + / date_diff(get_last_day(start_date), get_first_day(start_date)), + 1, + ) - prorate_factor += flt(date_diff(get_last_day(end_date), end_date) / date_diff( - get_last_day(end_date), get_first_day(end_date)), 1) + prorate_factor += flt( + date_diff(get_last_day(end_date), end_date) + / date_diff(get_last_day(end_date), get_first_day(end_date)), + 1, + ) - cost -= (plan.cost * prorate_factor) + cost -= plan.cost * prorate_factor return cost diff --git a/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py b/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py index 15df62daa23..7df76cde80c 100644 --- a/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py +++ b/erpnext/accounts/doctype/subscription_plan/subscription_plan_dashboard.py @@ -1,18 +1,9 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'subscription_plan', - 'non_standard_fieldnames': { - 'Payment Request': 'plan', - 'Subscription': 'plan' - }, - 'transactions': [ - { - 'label': _('References'), - 'items': ['Payment Request', 'Subscription'] - } - ] + "fieldname": "subscription_plan", + "non_standard_fieldnames": {"Payment Request": "plan", "Subscription": "plan"}, + "transactions": [{"label": _("References"), "items": ["Payment Request", "Subscription"]}], } diff --git a/erpnext/accounts/doctype/tax_category/tax_category.json b/erpnext/accounts/doctype/tax_category/tax_category.json index f7145af44c3..44a339f31df 100644 --- a/erpnext/accounts/doctype/tax_category/tax_category.json +++ b/erpnext/accounts/doctype/tax_category/tax_category.json @@ -2,12 +2,13 @@ "actions": [], "allow_rename": 1, "autoname": "field:title", - "creation": "2018-11-22 23:38:39.668804", + "creation": "2022-01-19 01:09:28.920486", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", "field_order": [ - "title" + "title", + "disabled" ], "fields": [ { @@ -18,14 +19,21 @@ "label": "Title", "reqd": 1, "unique": 1 + }, + { + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" } ], "index_web_pages_for_search": 1, "links": [], - "modified": "2021-03-03 11:50:38.748872", + "modified": "2022-01-18 21:13:41.161017", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Category", + "naming_rule": "By fieldname", "owner": "Administrator", "permissions": [ { @@ -65,5 +73,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py b/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py index c9d52da78ad..17a275ebc34 100644 --- a/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py +++ b/erpnext/accounts/doctype/tax_category/tax_category_dashboard.py @@ -1,30 +1,14 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'tax_category', - 'transactions': [ - { - 'label': _('Pre Sales'), - 'items': ['Quotation', 'Supplier Quotation'] - }, - { - 'label': _('Sales'), - 'items': ['Sales Invoice', 'Delivery Note', 'Sales Order'] - }, - { - 'label': _('Purchase'), - 'items': ['Purchase Invoice', 'Purchase Receipt'] - }, - { - 'label': _('Party'), - 'items': ['Customer', 'Supplier'] - }, - { - 'label': _('Taxes'), - 'items': ['Item', 'Tax Rule'] - } - ] + "fieldname": "tax_category", + "transactions": [ + {"label": _("Pre Sales"), "items": ["Quotation", "Supplier Quotation"]}, + {"label": _("Sales"), "items": ["Sales Invoice", "Delivery Note", "Sales Order"]}, + {"label": _("Purchase"), "items": ["Purchase Invoice", "Purchase Receipt"]}, + {"label": _("Party"), "items": ["Customer", "Supplier"]}, + {"label": _("Taxes"), "items": ["Item", "Tax Rule"]}, + ], } diff --git a/erpnext/accounts/doctype/tax_rule/tax_rule.py b/erpnext/accounts/doctype/tax_rule/tax_rule.py index ce64d222856..a21d48415fd 100644 --- a/erpnext/accounts/doctype/tax_rule/tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/tax_rule.py @@ -16,9 +16,17 @@ from six import iteritems from erpnext.setup.doctype.customer_group.customer_group import get_parent_customer_groups -class IncorrectCustomerGroup(frappe.ValidationError): pass -class IncorrectSupplierType(frappe.ValidationError): pass -class ConflictingTaxRule(frappe.ValidationError): pass +class IncorrectCustomerGroup(frappe.ValidationError): + pass + + +class IncorrectSupplierType(frappe.ValidationError): + pass + + +class ConflictingTaxRule(frappe.ValidationError): + pass + class TaxRule(Document): def __setup__(self): @@ -31,7 +39,7 @@ class TaxRule(Document): self.validate_use_for_shopping_cart() def validate_tax_template(self): - if self.tax_type== "Sales": + if self.tax_type == "Sales": self.purchase_tax_template = self.supplier = self.supplier_group = None if self.customer: self.customer_group = None @@ -51,28 +59,28 @@ class TaxRule(Document): def validate_filters(self): filters = { - "tax_type": self.tax_type, - "customer": self.customer, - "customer_group": self.customer_group, - "supplier": self.supplier, - "supplier_group": self.supplier_group, - "item": self.item, - "item_group": self.item_group, - "billing_city": self.billing_city, - "billing_county": self.billing_county, - "billing_state": self.billing_state, - "billing_zipcode": self.billing_zipcode, - "billing_country": self.billing_country, - "shipping_city": self.shipping_city, - "shipping_county": self.shipping_county, - "shipping_state": self.shipping_state, - "shipping_zipcode": self.shipping_zipcode, - "shipping_country": self.shipping_country, - "tax_category": self.tax_category, - "company": self.company + "tax_type": self.tax_type, + "customer": self.customer, + "customer_group": self.customer_group, + "supplier": self.supplier, + "supplier_group": self.supplier_group, + "item": self.item, + "item_group": self.item_group, + "billing_city": self.billing_city, + "billing_county": self.billing_county, + "billing_state": self.billing_state, + "billing_zipcode": self.billing_zipcode, + "billing_country": self.billing_country, + "shipping_city": self.shipping_city, + "shipping_county": self.shipping_county, + "shipping_state": self.shipping_state, + "shipping_zipcode": self.shipping_zipcode, + "shipping_country": self.shipping_country, + "tax_category": self.tax_category, + "company": self.company, } - conds="" + conds = "" for d in filters: if conds: conds += " and " @@ -82,85 +90,112 @@ class TaxRule(Document): conds += """ and ((from_date > '{from_date}' and from_date < '{to_date}') or (to_date > '{from_date}' and to_date < '{to_date}') or ('{from_date}' > from_date and '{from_date}' < to_date) or - ('{from_date}' = from_date and '{to_date}' = to_date))""".format(from_date=self.from_date, to_date=self.to_date) + ('{from_date}' = from_date and '{to_date}' = to_date))""".format( + from_date=self.from_date, to_date=self.to_date + ) elif self.from_date and not self.to_date: - conds += """ and to_date > '{from_date}'""".format(from_date = self.from_date) + conds += """ and to_date > '{from_date}'""".format(from_date=self.from_date) elif self.to_date and not self.from_date: - conds += """ and from_date < '{to_date}'""".format(to_date = self.to_date) + conds += """ and from_date < '{to_date}'""".format(to_date=self.to_date) - tax_rule = frappe.db.sql("select name, priority \ - from `tabTax Rule` where {0} and name != '{1}'".format(conds, self.name), as_dict=1) + tax_rule = frappe.db.sql( + "select name, priority \ + from `tabTax Rule` where {0} and name != '{1}'".format( + conds, self.name + ), + as_dict=1, + ) if tax_rule: if tax_rule[0].priority == self.priority: frappe.throw(_("Tax Rule Conflicts with {0}").format(tax_rule[0].name), ConflictingTaxRule) def validate_use_for_shopping_cart(self): - '''If shopping cart is enabled and no tax rule exists for shopping cart, enable this one''' - if (not self.use_for_shopping_cart - and cint(frappe.db.get_single_value('E Commerce Settings', 'enabled')) - and not frappe.db.get_value('Tax Rule', {'use_for_shopping_cart': 1, 'name': ['!=', self.name]})): + """If shopping cart is enabled and no tax rule exists for shopping cart, enable this one""" + if ( + not self.use_for_shopping_cart + and cint(frappe.db.get_single_value("E Commerce Settings", "enabled")) + and not frappe.db.get_value("Tax Rule", {"use_for_shopping_cart": 1, "name": ["!=", self.name]}) + ): self.use_for_shopping_cart = 1 - frappe.msgprint(_("Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart")) + frappe.msgprint( + _( + "Enabling 'Use for Shopping Cart', as Shopping Cart is enabled and there should be at least one Tax Rule for Shopping Cart" + ) + ) + @frappe.whitelist() def get_party_details(party, party_type, args=None): out = {} billing_address, shipping_address = None, None if args: - if args.get('billing_address'): - billing_address = frappe.get_doc('Address', args.get('billing_address')) - if args.get('shipping_address'): - shipping_address = frappe.get_doc('Address', args.get('shipping_address')) + if args.get("billing_address"): + billing_address = frappe.get_doc("Address", args.get("billing_address")) + if args.get("shipping_address"): + shipping_address = frappe.get_doc("Address", args.get("shipping_address")) else: billing_address_name = get_default_address(party_type, party) - shipping_address_name = get_default_address(party_type, party, 'is_shipping_address') + shipping_address_name = get_default_address(party_type, party, "is_shipping_address") if billing_address_name: - billing_address = frappe.get_doc('Address', billing_address_name) + billing_address = frappe.get_doc("Address", billing_address_name) if shipping_address_name: - shipping_address = frappe.get_doc('Address', shipping_address_name) + shipping_address = frappe.get_doc("Address", shipping_address_name) if billing_address: - out["billing_city"]= billing_address.city - out["billing_county"]= billing_address.county - out["billing_state"]= billing_address.state - out["billing_zipcode"]= billing_address.pincode - out["billing_country"]= billing_address.country + out["billing_city"] = billing_address.city + out["billing_county"] = billing_address.county + out["billing_state"] = billing_address.state + out["billing_zipcode"] = billing_address.pincode + out["billing_country"] = billing_address.country if shipping_address: - out["shipping_city"]= shipping_address.city - out["shipping_county"]= shipping_address.county - out["shipping_state"]= shipping_address.state - out["shipping_zipcode"]= shipping_address.pincode - out["shipping_country"]= shipping_address.country + out["shipping_city"] = shipping_address.city + out["shipping_county"] = shipping_address.county + out["shipping_state"] = shipping_address.state + out["shipping_zipcode"] = shipping_address.pincode + out["shipping_country"] = shipping_address.country return out + def get_tax_template(posting_date, args): """Get matching tax rule""" args = frappe._dict(args) - conditions = ["""(from_date is null or from_date <= '{0}') - and (to_date is null or to_date >= '{0}')""".format(posting_date)] + conditions = [ + """(from_date is null or from_date <= '{0}') + and (to_date is null or to_date >= '{0}')""".format( + posting_date + ) + ] - conditions.append("ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category"))))) - if 'tax_category' in args.keys(): - del args['tax_category'] + conditions.append( + "ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category")))) + ) + if "tax_category" in args.keys(): + del args["tax_category"] for key, value in iteritems(args): - if key=="use_for_shopping_cart": + if key == "use_for_shopping_cart": conditions.append("use_for_shopping_cart = {0}".format(1 if value else 0)) - elif key == 'customer_group': - if not value: value = get_root_of("Customer Group") + elif key == "customer_group": + if not value: + value = get_root_of("Customer Group") customer_group_condition = get_customer_group_condition(value) conditions.append("ifnull({0}, '') in ('', {1})".format(key, customer_group_condition)) else: conditions.append("ifnull({0}, '') in ('', {1})".format(key, frappe.db.escape(cstr(value)))) - tax_rule = frappe.db.sql("""select * from `tabTax Rule` - where {0}""".format(" and ".join(conditions)), as_dict = True) + tax_rule = frappe.db.sql( + """select * from `tabTax Rule` + where {0}""".format( + " and ".join(conditions) + ), + as_dict=True, + ) if not tax_rule: return None @@ -168,24 +203,30 @@ def get_tax_template(posting_date, args): for rule in tax_rule: rule.no_of_keys_matched = 0 for key in args: - if rule.get(key): rule.no_of_keys_matched += 1 + if rule.get(key): + rule.no_of_keys_matched += 1 - rule = sorted(tax_rule, - key = functools.cmp_to_key(lambda b, a: - cmp(a.no_of_keys_matched, b.no_of_keys_matched) or - cmp(a.priority, b.priority)))[0] + rule = sorted( + tax_rule, + key=functools.cmp_to_key( + lambda b, a: cmp(a.no_of_keys_matched, b.no_of_keys_matched) or cmp(a.priority, b.priority) + ), + )[0] tax_template = rule.sales_tax_template or rule.purchase_tax_template doctype = "{0} Taxes and Charges Template".format(rule.tax_type) - if frappe.db.get_value(doctype, tax_template, 'disabled')==1: + if frappe.db.get_value(doctype, tax_template, "disabled") == 1: return None return tax_template + def get_customer_group_condition(customer_group): condition = "" - customer_groups = ["%s"%(frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group)] + customer_groups = [ + "%s" % (frappe.db.escape(d.name)) for d in get_parent_customer_groups(customer_group) + ] if customer_groups: - condition = ",".join(['%s'] * len(customer_groups))%(tuple(customer_groups)) + condition = ",".join(["%s"] * len(customer_groups)) % (tuple(customer_groups)) return condition diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py index 44344bb7635..2cb9564875f 100644 --- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py +++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py @@ -9,7 +9,7 @@ from erpnext.accounts.doctype.tax_rule.tax_rule import ConflictingTaxRule, get_t from erpnext.crm.doctype.opportunity.opportunity import make_quotation from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity -test_records = frappe.get_test_records('Tax Rule') +test_records = frappe.get_test_records("Tax Rule") from six import iteritems @@ -27,40 +27,70 @@ class TestTaxRule(unittest.TestCase): frappe.db.sql("delete from `tabTax Rule`") def test_conflict(self): - tax_rule1 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1) + tax_rule1 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + ) tax_rule1.save() - tax_rule2 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1) + tax_rule2 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + ) self.assertRaises(ConflictingTaxRule, tax_rule2.save) def test_conflict_with_non_overlapping_dates(self): - tax_rule1 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01") + tax_rule1 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + from_date="2015-01-01", + ) tax_rule1.save() - tax_rule2 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, to_date = "2013-01-01") + tax_rule2 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + to_date="2013-01-01", + ) tax_rule2.save() self.assertTrue(tax_rule2.name) def test_for_parent_customer_group(self): - tax_rule1 = make_tax_rule(customer_group= "All Customer Groups", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01") + tax_rule1 = make_tax_rule( + customer_group="All Customer Groups", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + from_date="2015-01-01", + ) tax_rule1.save() - self.assertEqual(get_tax_template("2015-01-01", {"customer_group" : "Commercial", "use_for_shopping_cart":1}), - "_Test Sales Taxes and Charges Template - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer_group": "Commercial", "use_for_shopping_cart": 1}), + "_Test Sales Taxes and Charges Template - _TC", + ) def test_conflict_with_overlapping_dates(self): - tax_rule1 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-01", to_date = "2015-01-05") + tax_rule1 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + from_date="2015-01-01", + to_date="2015-01-05", + ) tax_rule1.save() - tax_rule2 = make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority = 1, from_date = "2015-01-03", to_date = "2015-01-09") + tax_rule2 = make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + from_date="2015-01-03", + to_date="2015-01-09", + ) self.assertRaises(ConflictingTaxRule, tax_rule2.save) @@ -68,93 +98,186 @@ class TestTaxRule(unittest.TestCase): tax_rule = make_tax_rule() self.assertEqual(tax_rule.purchase_tax_template, None) - def test_select_tax_rule_based_on_customer(self): - make_tax_rule(customer= "_Test Customer", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - make_tax_rule(customer= "_Test Customer 1", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1) + make_tax_rule( + customer="_Test Customer 1", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + save=1, + ) - make_tax_rule(customer= "_Test Customer 2", - sales_tax_template = "_Test Sales Taxes and Charges Template 2 - _TC", save=1) + make_tax_rule( + customer="_Test Customer 2", + sales_tax_template="_Test Sales Taxes and Charges Template 2 - _TC", + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer 2"}), - "_Test Sales Taxes and Charges Template 2 - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer 2"}), + "_Test Sales Taxes and Charges Template 2 - _TC", + ) def test_select_tax_rule_based_on_tax_category(self): - make_tax_rule(customer="_Test Customer", tax_category="_Test Tax Category 1", - sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + tax_category="_Test Tax Category 1", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + save=1, + ) - make_tax_rule(customer="_Test Customer", tax_category="_Test Tax Category 2", - sales_tax_template="_Test Sales Taxes and Charges Template 2 - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + tax_category="_Test Tax Category 2", + sales_tax_template="_Test Sales Taxes and Charges Template 2 - _TC", + save=1, + ) self.assertFalse(get_tax_template("2015-01-01", {"customer": "_Test Customer"})) - self.assertEqual(get_tax_template("2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 1"}), - "_Test Sales Taxes and Charges Template 1 - _TC") - self.assertEqual(get_tax_template("2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 2"}), - "_Test Sales Taxes and Charges Template 2 - _TC") + self.assertEqual( + get_tax_template( + "2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 1"} + ), + "_Test Sales Taxes and Charges Template 1 - _TC", + ) + self.assertEqual( + get_tax_template( + "2015-01-01", {"customer": "_Test Customer", "tax_category": "_Test Tax Category 2"} + ), + "_Test Sales Taxes and Charges Template 2 - _TC", + ) - make_tax_rule(customer="_Test Customer", tax_category="", - sales_tax_template="_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + tax_category="", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer": "_Test Customer"}), - "_Test Sales Taxes and Charges Template - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer"}), + "_Test Sales Taxes and Charges Template - _TC", + ) def test_select_tax_rule_based_on_better_match(self): - make_tax_rule(customer= "_Test Customer", billing_city = "Test City", billing_state = "Test State", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City", + billing_state="Test State", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - make_tax_rule(customer= "_Test Customer", billing_city = "Test City1", billing_state = "Test State", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City1", + billing_state="Test State", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City", "billing_state": "Test State"}), - "_Test Sales Taxes and Charges Template - _TC") + self.assertEqual( + get_tax_template( + "2015-01-01", + {"customer": "_Test Customer", "billing_city": "Test City", "billing_state": "Test State"}, + ), + "_Test Sales Taxes and Charges Template - _TC", + ) def test_select_tax_rule_based_on_state_match(self): - make_tax_rule(customer= "_Test Customer", shipping_state = "Test State", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + shipping_state="Test State", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - make_tax_rule(customer= "_Test Customer", shipping_state = "Test State12", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", priority=2, save=1) + make_tax_rule( + customer="_Test Customer", + shipping_state="Test State12", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + priority=2, + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "shipping_state": "Test State"}), - "_Test Sales Taxes and Charges Template - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer", "shipping_state": "Test State"}), + "_Test Sales Taxes and Charges Template - _TC", + ) def test_select_tax_rule_based_on_better_priority(self): - make_tax_rule(customer= "_Test Customer", billing_city = "Test City", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", priority=1, save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + priority=1, + save=1, + ) - make_tax_rule(customer= "_Test Customer", billing_city = "Test City", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", priority=2, save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + priority=2, + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City"}), - "_Test Sales Taxes and Charges Template 1 - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer", "billing_city": "Test City"}), + "_Test Sales Taxes and Charges Template 1 - _TC", + ) def test_select_tax_rule_based_cross_matching_keys(self): - make_tax_rule(customer= "_Test Customer", billing_city = "Test City", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - make_tax_rule(customer= "_Test Customer 1", billing_city = "Test City 1", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1) + make_tax_rule( + customer="_Test Customer 1", + billing_city="Test City 1", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}), - None) + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer", "billing_city": "Test City 1"}), + None, + ) def test_select_tax_rule_based_cross_partially_keys(self): - make_tax_rule(customer= "_Test Customer", billing_city = "Test City", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="Test City", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) - make_tax_rule(billing_city = "Test City 1", - sales_tax_template = "_Test Sales Taxes and Charges Template 1 - _TC", save=1) + make_tax_rule( + billing_city="Test City 1", + sales_tax_template="_Test Sales Taxes and Charges Template 1 - _TC", + save=1, + ) - self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}), - "_Test Sales Taxes and Charges Template 1 - _TC") + self.assertEqual( + get_tax_template("2015-01-01", {"customer": "_Test Customer", "billing_city": "Test City 1"}), + "_Test Sales Taxes and Charges Template 1 - _TC", + ) def test_taxes_fetch_via_tax_rule(self): - make_tax_rule(customer= "_Test Customer", billing_city = "_Test City", - sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) + make_tax_rule( + customer="_Test Customer", + billing_city="_Test City", + sales_tax_template="_Test Sales Taxes and Charges Template - _TC", + save=1, + ) # create opportunity for customer opportunity = make_opportunity(with_items=1) @@ -169,7 +292,6 @@ class TestTaxRule(unittest.TestCase): self.assertTrue(len(quotation.taxes) > 0) - def make_tax_rule(**args): args = frappe._dict(args) diff --git a/erpnext/accounts/print_format/gst_pos_invoice/__init__.py b/erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py similarity index 100% rename from erpnext/accounts/print_format/gst_pos_invoice/__init__.py rename to erpnext/accounts/doctype/tax_withheld_vouchers/__init__.py diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json new file mode 100644 index 00000000000..ce8c0c37086 --- /dev/null +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.json @@ -0,0 +1,49 @@ +{ + "actions": [], + "autoname": "autoincrement", + "creation": "2022-09-13 16:18:59.404842", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "voucher_type", + "voucher_name", + "taxable_amount" + ], + "fields": [ + { + "fieldname": "voucher_type", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Voucher Type", + "options": "DocType" + }, + { + "fieldname": "voucher_name", + "fieldtype": "Dynamic Link", + "in_list_view": 1, + "label": "Voucher Name", + "options": "voucher_type" + }, + { + "fieldname": "taxable_amount", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Taxable Amount", + "options": "Company:company:default_currency" + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2022-09-13 23:40:41.479208", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Tax Withheld Vouchers", + "naming_rule": "Autoincrement", + "owner": "Administrator", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py new file mode 100644 index 00000000000..ea54c5403a8 --- /dev/null +++ b/erpnext/accounts/doctype/tax_withheld_vouchers/tax_withheld_vouchers.py @@ -0,0 +1,9 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class TaxWithheldVouchers(Document): + pass diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py index f33aa31e161..7f79724f3bc 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py @@ -15,7 +15,7 @@ class TaxWithholdingCategory(Document): def validate_dates(self): last_date = None - for d in self.get('rates'): + for d in self.get("rates"): if getdate(d.from_date) >= getdate(d.to_date): frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx)) @@ -24,18 +24,25 @@ class TaxWithholdingCategory(Document): frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx)) def validate_thresholds(self): - for d in self.get('rates'): - if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold: - frappe.throw(_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(d.idx)) + for d in self.get("rates"): + if ( + d.cumulative_threshold and d.single_threshold and d.cumulative_threshold < d.single_threshold + ): + frappe.throw( + _("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format( + d.idx + ) + ) + def get_party_details(inv): - party_type, party = '', '' + party_type, party = "", "" - if inv.doctype == 'Sales Invoice': - party_type = 'Customer' + if inv.doctype == "Sales Invoice": + party_type = "Customer" party = inv.customer else: - party_type = 'Supplier' + party_type = "Supplier" party = inv.supplier if not party: @@ -43,65 +50,71 @@ def get_party_details(inv): return party_type, party + def get_party_tax_withholding_details(inv, tax_withholding_category=None): - pan_no = '' + pan_no = "" parties = [] party_type, party = get_party_details(inv) has_pan_field = frappe.get_meta(party_type).has_field("pan") if not tax_withholding_category: if has_pan_field: - fields = ['tax_withholding_category', 'pan'] + fields = ["tax_withholding_category", "pan"] else: - fields = ['tax_withholding_category'] + fields = ["tax_withholding_category"] tax_withholding_details = frappe.db.get_value(party_type, party, fields, as_dict=1) - tax_withholding_category = tax_withholding_details.get('tax_withholding_category') - pan_no = tax_withholding_details.get('pan') + tax_withholding_category = tax_withholding_details.get("tax_withholding_category") + pan_no = tax_withholding_details.get("pan") if not tax_withholding_category: return # if tax_withholding_category passed as an argument but not pan_no if not pan_no and has_pan_field: - pan_no = frappe.db.get_value(party_type, party, 'pan') + pan_no = frappe.db.get_value(party_type, party, "pan") # Get others suppliers with the same PAN No if pan_no: - parties = frappe.get_all(party_type, filters={ 'pan': pan_no }, pluck='name') + parties = frappe.get_all(party_type, filters={"pan": pan_no}, pluck="name") if not parties: parties.append(party) - posting_date = inv.get('posting_date') or inv.get('transaction_date') + posting_date = inv.get("posting_date") or inv.get("transaction_date") tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company) if not tax_details: - frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}') - .format(tax_withholding_category, inv.company)) + frappe.throw( + _("Please set associated account in Tax Withholding Category {0} against Company {1}").format( + tax_withholding_category, inv.company + ) + ) - if party_type == 'Customer' and not tax_details.cumulative_threshold: + if party_type == "Customer" and not tax_details.cumulative_threshold: # TCS is only chargeable on sum of invoiced value - frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.') - .format(tax_withholding_category, inv.company, party)) + frappe.throw( + _( + "Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value." + ).format(tax_withholding_category, inv.company, party) + ) - tax_amount, tax_deducted, tax_deducted_on_advances = get_tax_amount( - party_type, parties, - inv, tax_details, - posting_date, pan_no + tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount = get_tax_amount( + party_type, parties, inv, tax_details, posting_date, pan_no ) - if party_type == 'Supplier': + if party_type == "Supplier": tax_row = get_tax_row_for_tds(tax_details, tax_amount) else: tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted) - if inv.doctype == 'Purchase Invoice': - return tax_row, tax_deducted_on_advances + if inv.doctype == "Purchase Invoice": + return tax_row, tax_deducted_on_advances, voucher_wise_amount else: return tax_row + def get_tax_withholding_details(tax_withholding_category, posting_date, company): tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category) @@ -109,19 +122,24 @@ def get_tax_withholding_details(tax_withholding_category, posting_date, company) for account_detail in tax_withholding.accounts: if company == account_detail.company: - return frappe._dict({ - "tax_withholding_category": tax_withholding_category, - "account_head": account_detail.account, - "rate": tax_rate_detail.tax_withholding_rate, - "from_date": tax_rate_detail.from_date, - "to_date": tax_rate_detail.to_date, - "threshold": tax_rate_detail.single_threshold, - "cumulative_threshold": tax_rate_detail.cumulative_threshold, - "description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category, - "consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount, - "tax_on_excess_amount": tax_withholding.tax_on_excess_amount, - "round_off_tax_amount": tax_withholding.round_off_tax_amount - }) + return frappe._dict( + { + "tax_withholding_category": tax_withholding_category, + "account_head": account_detail.account, + "rate": tax_rate_detail.tax_withholding_rate, + "from_date": tax_rate_detail.from_date, + "to_date": tax_rate_detail.to_date, + "threshold": tax_rate_detail.single_threshold, + "cumulative_threshold": tax_rate_detail.cumulative_threshold, + "description": tax_withholding.category_name + if tax_withholding.category_name + else tax_withholding_category, + "consider_party_ledger_amount": tax_withholding.consider_party_ledger_amount, + "tax_on_excess_amount": tax_withholding.tax_on_excess_amount, + "round_off_tax_amount": tax_withholding.round_off_tax_amount, + } + ) + def get_tax_withholding_rates(tax_withholding, posting_date): # returns the row that matches with the fiscal year from posting date @@ -131,13 +149,14 @@ def get_tax_withholding_rates(tax_withholding, posting_date): frappe.throw(_("No Tax Withholding data found for the current posting date.")) + def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted): row = { "category": "Total", "charge_type": "Actual", "tax_amount": tax_amount, "description": tax_details.description, - "account_head": tax_details.account_head + "account_head": tax_details.account_head, } if tax_deducted: @@ -147,20 +166,20 @@ def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted): taxes_excluding_tcs = [d for d in inv.taxes if d.account_head != tax_details.account_head] if taxes_excluding_tcs: # chargeable amount is the total amount after other charges are applied - row.update({ - "charge_type": "On Previous Row Total", - "row_id": len(taxes_excluding_tcs), - "rate": tax_details.rate - }) + row.update( + { + "charge_type": "On Previous Row Total", + "row_id": len(taxes_excluding_tcs), + "rate": tax_details.rate, + } + ) else: # if only TCS is to be charged, then net total is chargeable amount - row.update({ - "charge_type": "On Net Total", - "rate": tax_details.rate - }) + row.update({"charge_type": "On Net Total", "rate": tax_details.rate}) return row + def get_tax_row_for_tds(tax_details, tax_amount): return { "category": "Total", @@ -168,29 +187,41 @@ def get_tax_row_for_tds(tax_details, tax_amount): "tax_amount": tax_amount, "add_deduct_tax": "Deduct", "description": tax_details.description, - "account_head": tax_details.account_head + "account_head": tax_details.account_head, } + def get_lower_deduction_certificate(tax_details, pan_no): - ldc_name = frappe.db.get_value('Lower Deduction Certificate', + ldc_name = frappe.db.get_value( + "Lower Deduction Certificate", { - 'pan_no': pan_no, - 'tax_withholding_category': tax_details.tax_withholding_category, - 'valid_from': ('>=', tax_details.from_date), - 'valid_upto': ('<=', tax_details.to_date) - }, 'name') + "pan_no": pan_no, + "tax_withholding_category": tax_details.tax_withholding_category, + "valid_from": (">=", tax_details.from_date), + "valid_upto": ("<=", tax_details.to_date), + }, + "name", + ) if ldc_name: - return frappe.get_doc('Lower Deduction Certificate', ldc_name) + return frappe.get_doc("Lower Deduction Certificate", ldc_name) + def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None): - vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type) - advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date, - to_date=tax_details.to_date, party_type=party_type) + vouchers, voucher_wise_amount = get_invoice_vouchers( + parties, tax_details, inv.company, party_type=party_type + ) + advance_vouchers = get_advance_vouchers( + parties, + company=inv.company, + from_date=tax_details.from_date, + to_date=tax_details.to_date, + party_type=party_type, + ) taxable_vouchers = vouchers + advance_vouchers tax_deducted_on_advances = 0 - if inv.doctype == 'Purchase Invoice': + if inv.doctype == "Purchase Invoice": tax_deducted_on_advances = get_taxes_deducted_on_advances_allocated(inv, tax_details) tax_deducted = 0 @@ -198,18 +229,21 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N tax_deducted = get_deducted_tax(taxable_vouchers, tax_details) tax_amount = 0 - if party_type == 'Supplier': + + if party_type == "Supplier": ldc = get_lower_deduction_certificate(tax_details, pan_no) if tax_deducted: net_total = inv.net_total if ldc: - tax_amount = get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total) + tax_amount = get_tds_amount_from_ldc( + ldc, parties, pan_no, tax_details, posting_date, net_total + ) else: tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0 else: tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers) - elif party_type == 'Customer': + elif party_type == "Customer": if tax_deducted: # if already TCS is charged, then amount will be calculated based on 'Previous Row Total' tax_amount = 0 @@ -221,83 +255,108 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N if cint(tax_details.round_off_tax_amount): tax_amount = round(tax_amount) - return tax_amount, tax_deducted, tax_deducted_on_advances + return tax_amount, tax_deducted, tax_deducted_on_advances, voucher_wise_amount -def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'): - dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit' - doctype = 'Purchase Invoice' if party_type == 'Supplier' else 'Sales Invoice' + +def get_invoice_vouchers(parties, tax_details, company, party_type="Supplier"): + doctype = "Purchase Invoice" if party_type == "Supplier" else "Sales Invoice" + voucher_wise_amount = {} + vouchers = [] filters = { - 'company': company, - frappe.scrub(party_type): ['in', parties], - 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)], - 'is_opening': 'No', - 'docstatus': 1 + "company": company, + frappe.scrub(party_type): ["in", parties], + "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], + "is_opening": "No", + "docstatus": 1, } - if not tax_details.get('consider_party_ledger_amount') and doctype != "Sales Invoice": - filters.update({ - 'apply_tds': 1, - 'tax_withholding_category': tax_details.get('tax_withholding_category') - }) + if not tax_details.get("consider_party_ledger_amount") and doctype != "Sales Invoice": + filters.update( + {"apply_tds": 1, "tax_withholding_category": tax_details.get("tax_withholding_category")} + ) - invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""] + invoices_details = frappe.get_all(doctype, filters=filters, fields=["name", "base_net_total"]) - journal_entries = frappe.db.sql(""" - SELECT j.name + for d in invoices_details: + vouchers.append(d.name) + voucher_wise_amount.update({d.name: {"amount": d.base_net_total, "voucher_type": doctype}}) + + journal_entries_details = frappe.db.sql( + """ + SELECT j.name, ja.credit - ja.debit AS amount FROM `tabJournal Entry` j, `tabJournal Entry Account` ja WHERE - j.docstatus = 1 + j.name = ja.parent + AND j.docstatus = 1 AND j.is_opening = 'No' AND j.posting_date between %s and %s - AND ja.{dr_or_cr} > 0 AND ja.party in %s - """.format(dr_or_cr=dr_or_cr), (tax_details.from_date, tax_details.to_date, tuple(parties)), as_list=1) + AND j.apply_tds = 1 + AND j.tax_withholding_category = %s + """, + ( + tax_details.from_date, + tax_details.to_date, + tuple(parties), + tax_details.get("tax_withholding_category"), + ), + as_dict=1, + ) - if journal_entries: - journal_entries = journal_entries[0] + if journal_entries_details: + for d in journal_entries_details: + vouchers.append(d.name) + voucher_wise_amount.update({d.name: {"amount": d.amount, "voucher_type": "Journal Entry"}}) - return invoices + journal_entries + return vouchers, voucher_wise_amount -def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type='Supplier'): + +def get_advance_vouchers( + parties, company=None, from_date=None, to_date=None, party_type="Supplier" +): # for advance vouchers, debit and credit is reversed - dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit' + dr_or_cr = "debit" if party_type == "Supplier" else "credit" filters = { - dr_or_cr: ['>', 0], - 'is_opening': 'No', - 'is_cancelled': 0, - 'party_type': party_type, - 'party': ['in', parties], - 'against_voucher': ['is', 'not set'] + dr_or_cr: [">", 0], + "is_opening": "No", + "is_cancelled": 0, + "party_type": party_type, + "party": ["in", parties], } - if company: - filters['company'] = company - if from_date and to_date: - filters['posting_date'] = ['between', (from_date, to_date)] + if party_type == "Customer": + filters.update({"against_voucher": ["is", "not set"]}) + + if company: + filters["company"] = company + if from_date and to_date: + filters["posting_date"] = ["between", (from_date, to_date)] + + return frappe.get_all("GL Entry", filters=filters, distinct=1, pluck="voucher_no") or [""] - return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""] def get_taxes_deducted_on_advances_allocated(inv, tax_details): - advances = [d.reference_name for d in inv.get('advances')] tax_info = [] - if advances: - pe = frappe.qb.DocType("Payment Entry").as_("pe") - at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + if inv.get("advances"): + advances = [d.reference_name for d in inv.get("advances")] - tax_info = frappe.qb.from_(at).inner_join(pe).on( - pe.name == at.parent - ).select( - at.parent, at.name, at.tax_amount, at.allocated_amount - ).where( - pe.tax_withholding_category == tax_details.get('tax_withholding_category') - ).where( - at.parent.isin(advances) - ).where( - at.account_head == tax_details.account_head - ).run(as_dict=True) + if advances: + pe = frappe.qb.DocType("Payment Entry").as_("pe") + at = frappe.qb.DocType("Advance Taxes and Charges").as_("at") + + tax_info = ( + frappe.qb.from_(at) + .inner_join(pe) + .on(pe.name == at.parent) + .select(at.parent, at.name, at.tax_amount, at.allocated_amount) + .where(pe.tax_withholding_category == tax_details.get("tax_withholding_category")) + .where(at.parent.isin(advances)) + .where(at.account_head == tax_details.account_head) + .run(as_dict=True) + ) return tax_info @@ -305,59 +364,72 @@ def get_taxes_deducted_on_advances_allocated(inv, tax_details): def get_deducted_tax(taxable_vouchers, tax_details): # check if TDS / TCS account is already charged on taxable vouchers filters = { - 'is_cancelled': 0, - 'credit': ['>', 0], - 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)], - 'account': tax_details.account_head, - 'voucher_no': ['in', taxable_vouchers], + "is_cancelled": 0, + "credit": [">", 0], + "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], + "account": tax_details.account_head, + "voucher_no": ["in", taxable_vouchers], } field = "credit" - entries = frappe.db.get_all('GL Entry', filters, pluck=field) + entries = frappe.db.get_all("GL Entry", filters, pluck=field) return sum(entries) + def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): tds_amount = 0 - invoice_filters = { - 'name': ('in', vouchers), - 'docstatus': 1, - 'apply_tds': 1 - } + supp_credit_amt = 0.0 + supp_jv_credit_amt = 0.0 - field = 'sum(net_total)' + invoice_filters = {"name": ("in", vouchers), "docstatus": 1, "apply_tds": 1} + + field = "sum(net_total)" if cint(tax_details.consider_party_ledger_amount): - invoice_filters.pop('apply_tds', None) - field = 'sum(grand_total)' + invoice_filters.pop("apply_tds", None) + field = "sum(grand_total)" - supp_credit_amt = frappe.db.get_value('Purchase Invoice', invoice_filters, field) or 0.0 + if vouchers: + supp_credit_amt = frappe.db.get_value("Purchase Invoice", invoice_filters, field) or 0.0 - supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', { - 'parent': ('in', vouchers), 'docstatus': 1, - 'party': ('in', parties), 'reference_type': ('!=', 'Purchase Invoice') - }, 'sum(credit_in_account_currency)') or 0.0 + supp_jv_credit_amt = ( + frappe.db.get_value( + "Journal Entry Account", + { + "parent": ("in", vouchers), + "docstatus": 1, + "party": ("in", parties), + "reference_type": ("!=", "Purchase Invoice"), + }, + "sum(credit_in_account_currency)", + ) + ) or 0.0 supp_credit_amt += supp_jv_credit_amt supp_credit_amt += inv.net_total - debit_note_amount = get_debit_note_amount(parties, tax_details.from_date, tax_details.to_date, inv.company) - supp_credit_amt -= debit_note_amount + threshold = tax_details.get("threshold", 0) + cumulative_threshold = tax_details.get("cumulative_threshold", 0) - threshold = tax_details.get('threshold', 0) - cumulative_threshold = tax_details.get('cumulative_threshold', 0) - - if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)): - if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint(tax_details.tax_on_excess_amount): + if (threshold and inv.net_total >= threshold) or ( + cumulative_threshold and supp_credit_amt >= cumulative_threshold + ): + if (cumulative_threshold and supp_credit_amt >= cumulative_threshold) and cint( + tax_details.tax_on_excess_amount + ): # Get net total again as TDS is calculated on net total # Grand is used to just check for threshold breach - net_total = frappe.db.get_value('Purchase Invoice', invoice_filters, 'sum(net_total)') or 0.0 + net_total = frappe.db.get_value("Purchase Invoice", invoice_filters, "sum(net_total)") or 0.0 net_total += inv.net_total supp_credit_amt = net_total - cumulative_threshold if ldc and is_valid_certificate( - ldc.valid_from, ldc.valid_upto, - inv.get('posting_date') or inv.get('transaction_date'), tax_deducted, - inv.net_total, ldc.certificate_limit + ldc.valid_from, + ldc.valid_upto, + inv.get("posting_date") or inv.get("transaction_date"), + tax_deducted, + inv.net_total, + ldc.certificate_limit, ): tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details) else: @@ -365,98 +437,115 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): return tds_amount + def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers): tcs_amount = 0 + invoiced_amt = 0 + advance_amt = 0 # sum of debit entries made from sales invoices - invoiced_amt = frappe.db.get_value('GL Entry', { - 'is_cancelled': 0, - 'party': ['in', parties], - 'company': inv.company, - 'voucher_no': ['in', vouchers], - }, 'sum(debit)') or 0.0 + if vouchers: + invoiced_amt = ( + frappe.db.get_value( + "GL Entry", + { + "is_cancelled": 0, + "party": ["in", parties], + "company": inv.company, + "voucher_no": ["in", vouchers], + }, + "sum(debit)", + ) + or 0.0 + ) # sum of credit entries made from PE / JV with unset 'against voucher' - advance_amt = frappe.db.get_value('GL Entry', { - 'is_cancelled': 0, - 'party': ['in', parties], - 'company': inv.company, - 'voucher_no': ['in', adv_vouchers], - }, 'sum(credit)') or 0.0 + if advance_amt: + advance_amt = ( + frappe.db.get_value( + "GL Entry", + { + "is_cancelled": 0, + "party": ["in", parties], + "company": inv.company, + "voucher_no": ["in", adv_vouchers], + }, + "sum(credit)", + ) + or 0.0 + ) # sum of credit entries made from sales invoice - credit_note_amt = sum(frappe.db.get_all('GL Entry', { - 'is_cancelled': 0, - 'credit': ['>', 0], - 'party': ['in', parties], - 'posting_date': ['between', (tax_details.from_date, tax_details.to_date)], - 'company': inv.company, - 'voucher_type': 'Sales Invoice', - }, pluck='credit')) + credit_note_amt = sum( + frappe.db.get_all( + "GL Entry", + { + "is_cancelled": 0, + "credit": [">", 0], + "party": ["in", parties], + "posting_date": ["between", (tax_details.from_date, tax_details.to_date)], + "company": inv.company, + "voucher_type": "Sales Invoice", + }, + pluck="credit", + ) + ) - cumulative_threshold = tax_details.get('cumulative_threshold', 0) + cumulative_threshold = tax_details.get("cumulative_threshold", 0) current_invoice_total = get_invoice_total_without_tcs(inv, tax_details) total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt - if ((cumulative_threshold and total_invoiced_amt >= cumulative_threshold)): + if cumulative_threshold and total_invoiced_amt >= cumulative_threshold: chargeable_amt = total_invoiced_amt - cumulative_threshold tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0 return tcs_amount + def get_invoice_total_without_tcs(inv, tax_details): tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head] tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0 return inv.grand_total - tcs_tax_row_amount + def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total): tds_amount = 0 - limit_consumed = frappe.db.get_value('Purchase Invoice', { - 'supplier': ('in', parties), - 'apply_tds': 1, - 'docstatus': 1 - }, 'sum(net_total)') + limit_consumed = frappe.db.get_value( + "Purchase Invoice", + {"supplier": ("in", parties), "apply_tds": 1, "docstatus": 1}, + "sum(net_total)", + ) if is_valid_certificate( - ldc.valid_from, ldc.valid_upto, - posting_date, limit_consumed, - net_total, ldc.certificate_limit + ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total, ldc.certificate_limit ): - tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details) + tds_amount = get_ltds_amount( + net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details + ) return tds_amount -def get_debit_note_amount(suppliers, from_date, to_date, company=None): - - filters = { - 'supplier': ['in', suppliers], - 'is_return': 1, - 'docstatus': 1, - 'posting_date': ['between', (from_date, to_date)] - } - fields = ['abs(sum(net_total)) as net_total'] - - if company: - filters['company'] = company - - return frappe.get_all('Purchase Invoice', filters, fields)[0].get('net_total') or 0.0 def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details): if current_amount < (certificate_limit - deducted_amount): - return current_amount * rate/100 + return current_amount * rate / 100 else: - ltds_amount = (certificate_limit - deducted_amount) + ltds_amount = certificate_limit - deducted_amount tds_amount = current_amount - ltds_amount - return ltds_amount * rate/100 + tds_amount * tax_details.rate/100 + return ltds_amount * rate / 100 + tds_amount * tax_details.rate / 100 -def is_valid_certificate(valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit): + +def is_valid_certificate( + valid_from, valid_upto, posting_date, deducted_amount, current_amount, certificate_limit +): valid = False - if ((getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto)) and - certificate_limit > deducted_amount): + if ( + getdate(valid_from) <= getdate(posting_date) <= getdate(valid_upto) + ) and certificate_limit > deducted_amount: valid = True return valid diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py index 46d0c2e487e..8a510ea023f 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py +++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category_dashboard.py @@ -1,11 +1,2 @@ - - def get_data(): - return { - 'fieldname': 'tax_withholding_category', - 'transactions': [ - { - 'items': ['Supplier'] - } - ] - } + return {"fieldname": "tax_withholding_category", "transactions": [{"items": ["Supplier"]}]} diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py index a3fcf7da7a5..e80fe11ab30 100644 --- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py +++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py @@ -10,6 +10,7 @@ from erpnext.accounts.utils import get_fiscal_year test_dependencies = ["Supplier Group", "Customer Group"] + class TestTaxWithholdingCategory(unittest.TestCase): @classmethod def setUpClass(self): @@ -21,18 +22,20 @@ class TestTaxWithholdingCategory(unittest.TestCase): cancel_invoices() def test_cumulative_threshold_tds(self): - frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS") + frappe.db.set_value( + "Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS" + ) invoices = [] # create invoices for lower than single threshold tax rate for _ in range(2): - pi = create_purchase_invoice(supplier = "Test TDS Supplier") + pi = create_purchase_invoice(supplier="Test TDS Supplier") pi.submit() invoices.append(pi) # create another invoice whose total when added to previously created invoice, # surpasses cumulative threshhold - pi = create_purchase_invoice(supplier = "Test TDS Supplier") + pi = create_purchase_invoice(supplier="Test TDS Supplier") pi.submit() # assert equal tax deduction on total invoice amount uptil now @@ -41,21 +44,23 @@ class TestTaxWithholdingCategory(unittest.TestCase): invoices.append(pi) # TDS is already deducted, so from onward system will deduct the TDS on every invoice - pi = create_purchase_invoice(supplier = "Test TDS Supplier", rate=5000) + pi = create_purchase_invoice(supplier="Test TDS Supplier", rate=5000) pi.submit() # assert equal tax deduction on total invoice amount uptil now self.assertEqual(pi.taxes_and_charges_deducted, 500) invoices.append(pi) - #delete invoices to avoid clashing - for d in invoices: + # delete invoices to avoid clashing + for d in reversed(invoices): d.cancel() def test_single_threshold_tds(self): invoices = [] - frappe.db.set_value("Supplier", "Test TDS Supplier1", "tax_withholding_category", "Single Threshold TDS") - pi = create_purchase_invoice(supplier = "Test TDS Supplier1", rate = 20000) + frappe.db.set_value( + "Supplier", "Test TDS Supplier1", "tax_withholding_category", "Single Threshold TDS" + ) + pi = create_purchase_invoice(supplier="Test TDS Supplier1", rate=20000) pi.submit() invoices.append(pi) @@ -63,7 +68,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi.grand_total, 18000) # check gl entry for the purchase invoice - gl_entries = frappe.db.get_all('GL Entry', filters={'voucher_no': pi.name}, fields=["*"]) + gl_entries = frappe.db.get_all("GL Entry", filters={"voucher_no": pi.name}, fields=["*"]) self.assertEqual(len(gl_entries), 3) for d in gl_entries: if d.account == pi.credit_to: @@ -75,7 +80,7 @@ class TestTaxWithholdingCategory(unittest.TestCase): else: raise ValueError("Account head does not match.") - pi = create_purchase_invoice(supplier = "Test TDS Supplier1") + pi = create_purchase_invoice(supplier="Test TDS Supplier1") pi.submit() invoices.append(pi) @@ -83,22 +88,24 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi.taxes_and_charges_deducted, 1000) # delete invoices to avoid clashing - for d in invoices: + for d in reversed(invoices): d.cancel() def test_tax_withholding_category_checks(self): invoices = [] - frappe.db.set_value("Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category") + frappe.db.set_value( + "Supplier", "Test TDS Supplier3", "tax_withholding_category", "New TDS Category" + ) # First Invoice with no tds check - pi = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000, do_not_save=True) + pi = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000, do_not_save=True) pi.apply_tds = 0 pi.save() pi.submit() invoices.append(pi) # Second Invoice will apply TDS checked - pi1 = create_purchase_invoice(supplier = "Test TDS Supplier3", rate = 20000) + pi1 = create_purchase_invoice(supplier="Test TDS Supplier3", rate=20000) pi1.submit() invoices.append(pi1) @@ -107,85 +114,92 @@ class TestTaxWithholdingCategory(unittest.TestCase): # TDS should be applied only on 1000 self.assertEqual(pi1.taxes[0].tax_amount, 1000) - for d in invoices: + for d in reversed(invoices): d.cancel() - def test_cumulative_threshold_tcs(self): - frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") + frappe.db.set_value( + "Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS" + ) invoices = [] # create invoices for lower than single threshold tax rate for _ in range(2): - si = create_sales_invoice(customer = "Test TCS Customer") + si = create_sales_invoice(customer="Test TCS Customer") si.submit() invoices.append(si) # create another invoice whose total when added to previously created invoice, # surpasses cumulative threshhold - si = create_sales_invoice(customer = "Test TCS Customer", rate=12000) + si = create_sales_invoice(customer="Test TCS Customer", rate=12000) si.submit() # assert tax collection on total invoice amount created until now - tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC']) + tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC"]) self.assertEqual(tcs_charged, 200) self.assertEqual(si.grand_total, 12200) invoices.append(si) # TCS is already collected once, so going forward system will collect TCS on every invoice - si = create_sales_invoice(customer = "Test TCS Customer", rate=5000) + si = create_sales_invoice(customer="Test TCS Customer", rate=5000) si.submit() - tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC') + tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == "TCS - _TC") self.assertEqual(tcs_charged, 500) invoices.append(si) - #delete invoices to avoid clashing - for d in invoices: + # cancel invoices to avoid clashing + for d in reversed(invoices): d.cancel() def test_tds_calculation_on_net_total(self): - frappe.db.set_value("Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS") + frappe.db.set_value( + "Supplier", "Test TDS Supplier4", "tax_withholding_category", "Cumulative Threshold TDS" + ) invoices = [] - pi = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000, do_not_save=True) - pi.append('taxes', { - "category": "Total", - "charge_type": "Actual", - "account_head": '_Test Account VAT - _TC', - "cost_center": 'Main - _TC', - "tax_amount": 1000, - "description": "Test", - "add_deduct_tax": "Add" - - }) + pi = create_purchase_invoice(supplier="Test TDS Supplier4", rate=20000, do_not_save=True) + pi.append( + "taxes", + { + "category": "Total", + "charge_type": "Actual", + "account_head": "_Test Account VAT - _TC", + "cost_center": "Main - _TC", + "tax_amount": 1000, + "description": "Test", + "add_deduct_tax": "Add", + }, + ) pi.save() pi.submit() invoices.append(pi) # Second Invoice will apply TDS checked - pi1 = create_purchase_invoice(supplier = "Test TDS Supplier4", rate = 20000) + pi1 = create_purchase_invoice(supplier="Test TDS Supplier4", rate=20000) pi1.submit() invoices.append(pi1) self.assertEqual(pi1.taxes[0].tax_amount, 4000) - #delete invoices to avoid clashing - for d in invoices: + # cancel invoices to avoid clashing + for d in reversed(invoices): d.cancel() def test_multi_category_single_supplier(self): - frappe.db.set_value("Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category") + frappe.db.set_value( + "Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category" + ) invoices = [] - pi = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 500, do_not_save=True) + pi = create_purchase_invoice(supplier="Test TDS Supplier5", rate=500, do_not_save=True) pi.tax_withholding_category = "Test Service Category" pi.save() pi.submit() invoices.append(pi) # Second Invoice will apply TDS checked - pi1 = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 2500, do_not_save=True) + pi1 = create_purchase_invoice(supplier="Test TDS Supplier5", rate=2500, do_not_save=True) pi1.tax_withholding_category = "Test Goods Category" pi1.save() pi1.submit() @@ -193,258 +207,356 @@ class TestTaxWithholdingCategory(unittest.TestCase): self.assertEqual(pi1.taxes[0].tax_amount, 250) - #delete invoices to avoid clashing - for d in invoices: + # cancel invoices to avoid clashing + for d in reversed(invoices): d.cancel() -def cancel_invoices(): - purchase_invoices = frappe.get_all("Purchase Invoice", { - 'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']], - 'docstatus': 1 - }, pluck="name") + def test_tax_withholding_category_voucher_display(self): + frappe.db.set_value( + "Supplier", "Test TDS Supplier6", "tax_withholding_category", "Test Multi Invoice Category" + ) + invoices = [] - sales_invoices = frappe.get_all("Sales Invoice", { - 'customer': 'Test TCS Customer', - 'docstatus': 1 - }, pluck="name") + pi = create_purchase_invoice(supplier="Test TDS Supplier6", rate=4000, do_not_save=True) + pi.apply_tds = 1 + pi.tax_withholding_category = "Test Multi Invoice Category" + pi.save() + pi.submit() + invoices.append(pi) + + pi1 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=2000, do_not_save=True) + pi1.apply_tds = 1 + pi1.is_return = 1 + pi1.items[0].qty = -1 + pi1.tax_withholding_category = "Test Multi Invoice Category" + pi1.save() + pi1.submit() + invoices.append(pi1) + + pi2 = create_purchase_invoice(supplier="Test TDS Supplier6", rate=9000, do_not_save=True) + pi2.apply_tds = 1 + pi2.tax_withholding_category = "Test Multi Invoice Category" + pi2.save() + pi2.submit() + invoices.append(pi2) + + pi2.load_from_db() + + self.assertTrue(pi2.taxes[0].tax_amount, 1100) + + self.assertTrue(pi2.tax_withheld_vouchers[0].voucher_name == pi1.name) + self.assertTrue(pi2.tax_withheld_vouchers[0].taxable_amount == pi1.net_total) + self.assertTrue(pi2.tax_withheld_vouchers[1].voucher_name == pi.name) + self.assertTrue(pi2.tax_withheld_vouchers[1].taxable_amount == pi.net_total) + + # cancel invoices to avoid clashing + for d in reversed(invoices): + d.cancel() + + +def cancel_invoices(): + purchase_invoices = frappe.get_all( + "Purchase Invoice", + { + "supplier": ["in", ["Test TDS Supplier", "Test TDS Supplier1", "Test TDS Supplier2"]], + "docstatus": 1, + }, + pluck="name", + ) + + sales_invoices = frappe.get_all( + "Sales Invoice", {"customer": "Test TCS Customer", "docstatus": 1}, pluck="name" + ) for d in purchase_invoices: - frappe.get_doc('Purchase Invoice', d).cancel() + frappe.get_doc("Purchase Invoice", d).cancel() for d in sales_invoices: - frappe.get_doc('Sales Invoice', d).cancel() + frappe.get_doc("Sales Invoice", d).cancel() + def create_purchase_invoice(**args): # return sales invoice doc object - item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name") + item = frappe.db.get_value("Item", {"item_name": "TDS Item"}, "name") args = frappe._dict(args) - pi = frappe.get_doc({ - "doctype": "Purchase Invoice", - "posting_date": today(), - "apply_tds": 0 if args.do_not_apply_tds else 1, - "supplier": args.supplier, - "company": '_Test Company', - "taxes_and_charges": "", - "currency": "INR", - "credit_to": "Creditors - _TC", - "taxes": [], - "items": [{ - 'doctype': 'Purchase Invoice Item', - 'item_code': item, - 'qty': args.qty or 1, - 'rate': args.rate or 10000, - 'cost_center': 'Main - _TC', - 'expense_account': 'Stock Received But Not Billed - _TC' - }] - }) + pi = frappe.get_doc( + { + "doctype": "Purchase Invoice", + "posting_date": today(), + "apply_tds": 0 if args.do_not_apply_tds else 1, + "supplier": args.supplier, + "company": "_Test Company", + "taxes_and_charges": "", + "currency": "INR", + "credit_to": "Creditors - _TC", + "taxes": [], + "items": [ + { + "doctype": "Purchase Invoice Item", + "item_code": item, + "qty": args.qty or 1, + "rate": args.rate or 10000, + "cost_center": "Main - _TC", + "expense_account": "Stock Received But Not Billed - _TC", + } + ], + } + ) pi.save() return pi + def create_sales_invoice(**args): # return sales invoice doc object - item = frappe.db.get_value('Item', {'item_name': 'TCS Item'}, "name") + item = frappe.db.get_value("Item", {"item_name": "TCS Item"}, "name") args = frappe._dict(args) - si = frappe.get_doc({ - "doctype": "Sales Invoice", - "posting_date": today(), - "customer": args.customer, - "company": '_Test Company', - "taxes_and_charges": "", - "currency": "INR", - "debit_to": "Debtors - _TC", - "taxes": [], - "items": [{ - 'doctype': 'Sales Invoice Item', - 'item_code': item, - 'qty': args.qty or 1, - 'rate': args.rate or 10000, - 'cost_center': 'Main - _TC', - 'expense_account': 'Cost of Goods Sold - _TC', - 'warehouse': args.warehouse or '_Test Warehouse - _TC' - }] - }) + si = frappe.get_doc( + { + "doctype": "Sales Invoice", + "posting_date": today(), + "customer": args.customer, + "company": "_Test Company", + "taxes_and_charges": "", + "currency": "INR", + "debit_to": "Debtors - _TC", + "taxes": [], + "items": [ + { + "doctype": "Sales Invoice Item", + "item_code": item, + "qty": args.qty or 1, + "rate": args.rate or 10000, + "cost_center": "Main - _TC", + "expense_account": "Cost of Goods Sold - _TC", + "warehouse": args.warehouse or "_Test Warehouse - _TC", + } + ], + } + ) si.save() return si + def create_records(): # create a new suppliers - for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', - 'Test TDS Supplier4', 'Test TDS Supplier5']: - if frappe.db.exists('Supplier', name): + for name in [ + "Test TDS Supplier", + "Test TDS Supplier1", + "Test TDS Supplier2", + "Test TDS Supplier3", + "Test TDS Supplier4", + "Test TDS Supplier5", + "Test TDS Supplier6", + ]: + if frappe.db.exists("Supplier", name): continue - frappe.get_doc({ - "supplier_group": "_Test Supplier Group", - "supplier_name": name, - "doctype": "Supplier", - }).insert() + frappe.get_doc( + { + "supplier_group": "_Test Supplier Group", + "supplier_name": name, + "doctype": "Supplier", + } + ).insert() - for name in ['Test TCS Customer']: - if frappe.db.exists('Customer', name): + for name in ["Test TCS Customer"]: + if frappe.db.exists("Customer", name): continue - frappe.get_doc({ - "customer_group": "_Test Customer Group", - "customer_name": name, - "doctype": "Customer" - }).insert() + frappe.get_doc( + {"customer_group": "_Test Customer Group", "customer_name": name, "doctype": "Customer"} + ).insert() # create item - if not frappe.db.exists('Item', "TDS Item"): - frappe.get_doc({ - "doctype": "Item", - "item_code": "TDS Item", - "item_name": "TDS Item", - "item_group": "All Item Groups", - "is_stock_item": 0, - }).insert() + if not frappe.db.exists("Item", "TDS Item"): + frappe.get_doc( + { + "doctype": "Item", + "item_code": "TDS Item", + "item_name": "TDS Item", + "item_group": "All Item Groups", + "is_stock_item": 0, + } + ).insert() - if not frappe.db.exists('Item', "TCS Item"): - frappe.get_doc({ - "doctype": "Item", - "item_code": "TCS Item", - "item_name": "TCS Item", - "item_group": "All Item Groups", - "is_stock_item": 1 - }).insert() + if not frappe.db.exists("Item", "TCS Item"): + frappe.get_doc( + { + "doctype": "Item", + "item_code": "TCS Item", + "item_name": "TCS Item", + "item_group": "All Item Groups", + "is_stock_item": 1, + } + ).insert() # create tds account if not frappe.db.exists("Account", "TDS - _TC"): - frappe.get_doc({ - 'doctype': 'Account', - 'company': '_Test Company', - 'account_name': 'TDS', - 'parent_account': 'Tax Assets - _TC', - 'report_type': 'Balance Sheet', - 'root_type': 'Asset' - }).insert() + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "TDS", + "parent_account": "Tax Assets - _TC", + "report_type": "Balance Sheet", + "root_type": "Asset", + } + ).insert() # create tcs account if not frappe.db.exists("Account", "TCS - _TC"): - frappe.get_doc({ - 'doctype': 'Account', - 'company': '_Test Company', - 'account_name': 'TCS', - 'parent_account': 'Duties and Taxes - _TC', - 'report_type': 'Balance Sheet', - 'root_type': 'Liability' - }).insert() + frappe.get_doc( + { + "doctype": "Account", + "company": "_Test Company", + "account_name": "TCS", + "parent_account": "Duties and Taxes - _TC", + "report_type": "Balance Sheet", + "root_type": "Liability", + } + ).insert() + def create_tax_with_holding_category(): fiscal_year = get_fiscal_year(today(), company="_Test Company") # Cumulative threshold if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "Cumulative Threshold TDS", - "category_name": "10% TDS", - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 0, - 'cumulative_threshold': 30000.00 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TDS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Cumulative Threshold TDS", + "category_name": "10% TDS", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 30000.00, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "Cumulative Threshold TCS", - "category_name": "10% TCS", - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 0, - 'cumulative_threshold': 30000.00 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TCS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Cumulative Threshold TCS", + "category_name": "10% TCS", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 30000.00, + } + ], + "accounts": [{"company": "_Test Company", "account": "TCS - _TC"}], + } + ).insert() # Single thresold if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "Single Threshold TDS", - "category_name": "10% TDS", - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 20000.00, - 'cumulative_threshold': 0 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TDS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Single Threshold TDS", + "category_name": "10% TDS", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 20000.00, + "cumulative_threshold": 0, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() if not frappe.db.exists("Tax Withholding Category", "New TDS Category"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "New TDS Category", - "category_name": "New TDS Category", - "round_off_tax_amount": 1, - "consider_party_ledger_amount": 1, - "tax_on_excess_amount": 1, - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 0, - 'cumulative_threshold': 30000 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TDS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "New TDS Category", + "category_name": "New TDS Category", + "round_off_tax_amount": 1, + "consider_party_ledger_amount": 1, + "tax_on_excess_amount": 1, + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 0, + "cumulative_threshold": 30000, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() if not frappe.db.exists("Tax Withholding Category", "Test Service Category"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "Test Service Category", - "category_name": "Test Service Category", - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 2000, - 'cumulative_threshold': 2000 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TDS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Test Service Category", + "category_name": "Test Service Category", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 2000, + "cumulative_threshold": 2000, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() if not frappe.db.exists("Tax Withholding Category", "Test Goods Category"): - frappe.get_doc({ - "doctype": "Tax Withholding Category", - "name": "Test Goods Category", - "category_name": "Test Goods Category", - "rates": [{ - 'from_date': fiscal_year[1], - 'to_date': fiscal_year[2], - 'tax_withholding_rate': 10, - 'single_threshold': 2000, - 'cumulative_threshold': 2000 - }], - "accounts": [{ - 'company': '_Test Company', - 'account': 'TDS - _TC' - }] - }).insert() + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Test Goods Category", + "category_name": "Test Goods Category", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 2000, + "cumulative_threshold": 2000, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() + + if not frappe.db.exists("Tax Withholding Category", "Test Multi Invoice Category"): + frappe.get_doc( + { + "doctype": "Tax Withholding Category", + "name": "Test Multi Invoice Category", + "category_name": "Test Multi Invoice Category", + "rates": [ + { + "from_date": fiscal_year[1], + "to_date": fiscal_year[2], + "tax_withholding_rate": 10, + "single_threshold": 5000, + "cumulative_threshold": 10000, + } + ], + "accounts": [{"company": "_Test Company", "account": "TDS - _TC"}], + } + ).insert() diff --git a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json index d2c505c6300..e032bb307b0 100644 --- a/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json +++ b/erpnext/accounts/doctype/tax_withholding_rate/tax_withholding_rate.json @@ -28,14 +28,14 @@ { "columns": 2, "fieldname": "single_threshold", - "fieldtype": "Currency", + "fieldtype": "Float", "in_list_view": 1, "label": "Single Transaction Threshold" }, { "columns": 3, "fieldname": "cumulative_threshold", - "fieldtype": "Currency", + "fieldtype": "Float", "in_list_view": 1, "label": "Cumulative Transaction Threshold" }, @@ -59,7 +59,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2021-08-31 11:42:12.213977", + "modified": "2022-01-13 12:04:42.904263", "modified_by": "Administrator", "module": "Accounts", "name": "Tax Withholding Rate", @@ -68,5 +68,6 @@ "quick_entry": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 1836db6477f..7525369d4f6 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -14,23 +14,59 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import ( from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget -class ClosedAccountingPeriod(frappe.ValidationError): pass +class ClosedAccountingPeriod(frappe.ValidationError): + pass -def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False): + +def make_gl_entries( + gl_map, + cancel=False, + adv_adj=False, + merge_entries=True, + update_outstanding="Yes", + from_repost=False, +): if gl_map: if not cancel: validate_accounting_period(gl_map) + validate_disabled_accounts(gl_map) gl_map = process_gl_map(gl_map, merge_entries) if gl_map and len(gl_map) > 1: save_entries(gl_map, adv_adj, update_outstanding, from_repost) # Post GL Map proccess there may no be any GL Entries elif gl_map: - frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction.")) + frappe.throw( + _( + "Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction." + ) + ) else: make_reverse_gl_entries(gl_map, adv_adj=adv_adj, update_outstanding=update_outstanding) + +def validate_disabled_accounts(gl_map): + accounts = [d.account for d in gl_map if d.account] + + Account = frappe.qb.DocType("Account") + + disabled_accounts = ( + frappe.qb.from_(Account) + .where(Account.name.isin(accounts) & Account.disabled == 1) + .select(Account.name, Account.disabled) + ).run(as_dict=True) + + if disabled_accounts: + account_list = " " + account_list += ", ".join([frappe.bold(d.name) for d in disabled_accounts]) + frappe.throw( + _("Cannot create accounting entries against disabled accounts: {0}").format(account_list), + title=_("Disabled Account Selected"), + ) + + def validate_accounting_period(gl_map): - accounting_periods = frappe.db.sql(""" SELECT + accounting_periods = frappe.db.sql( + """ SELECT ap.name as name FROM `tabAccounting Period` ap, `tabClosed Document` cd @@ -40,15 +76,23 @@ def validate_accounting_period(gl_map): AND cd.closed = 1 AND cd.document_type = %(voucher_type)s AND %(date)s between ap.start_date and ap.end_date - """, { - 'date': gl_map[0].posting_date, - 'company': gl_map[0].company, - 'voucher_type': gl_map[0].voucher_type - }, as_dict=1) + """, + { + "date": gl_map[0].posting_date, + "company": gl_map[0].company, + "voucher_type": gl_map[0].voucher_type, + }, + as_dict=1, + ) if accounting_periods: - frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}") - .format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod) + frappe.throw( + _( + "You cannot create or cancel any accounting entries with in the closed Accounting Period {0}" + ).format(frappe.bold(accounting_periods[0].name)), + ClosedAccountingPeriod, + ) + def process_gl_map(gl_map, merge_entries=True, precision=None): if merge_entries: @@ -60,8 +104,9 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): entry.debit = 0.0 if flt(entry.debit_in_account_currency) < 0: - entry.credit_in_account_currency = \ - flt(entry.credit_in_account_currency) - flt(entry.debit_in_account_currency) + entry.credit_in_account_currency = flt(entry.credit_in_account_currency) - flt( + entry.debit_in_account_currency + ) entry.debit_in_account_currency = 0.0 if flt(entry.credit) < 0: @@ -69,46 +114,54 @@ def process_gl_map(gl_map, merge_entries=True, precision=None): entry.credit = 0.0 if flt(entry.credit_in_account_currency) < 0: - entry.debit_in_account_currency = \ - flt(entry.debit_in_account_currency) - flt(entry.credit_in_account_currency) + entry.debit_in_account_currency = flt(entry.debit_in_account_currency) - flt( + entry.credit_in_account_currency + ) entry.credit_in_account_currency = 0.0 update_net_values(entry) return gl_map + def update_net_values(entry): # In some scenarios net value needs to be shown in the ledger # This method updates net values as debit or credit if entry.post_net_value and entry.debit and entry.credit: if entry.debit > entry.credit: entry.debit = entry.debit - entry.credit - entry.debit_in_account_currency = entry.debit_in_account_currency \ - - entry.credit_in_account_currency + entry.debit_in_account_currency = ( + entry.debit_in_account_currency - entry.credit_in_account_currency + ) entry.credit = 0 entry.credit_in_account_currency = 0 else: entry.credit = entry.credit - entry.debit - entry.credit_in_account_currency = entry.credit_in_account_currency \ - - entry.debit_in_account_currency + entry.credit_in_account_currency = ( + entry.credit_in_account_currency - entry.debit_in_account_currency + ) entry.debit = 0 entry.debit_in_account_currency = 0 + def merge_similar_entries(gl_map, precision=None): merged_gl_map = [] accounting_dimensions = get_accounting_dimensions() + for entry in gl_map: # if there is already an entry in this account then just add it # to that entry same_head = check_if_in_list(entry, merged_gl_map, accounting_dimensions) if same_head: - same_head.debit = flt(same_head.debit) + flt(entry.debit) - same_head.debit_in_account_currency = \ - flt(same_head.debit_in_account_currency) + flt(entry.debit_in_account_currency) + same_head.debit = flt(same_head.debit) + flt(entry.debit) + same_head.debit_in_account_currency = flt(same_head.debit_in_account_currency) + flt( + entry.debit_in_account_currency + ) same_head.credit = flt(same_head.credit) + flt(entry.credit) - same_head.credit_in_account_currency = \ - flt(same_head.credit_in_account_currency) + flt(entry.credit_in_account_currency) + same_head.credit_in_account_currency = flt(same_head.credit_in_account_currency) + flt( + entry.credit_in_account_currency + ) else: merged_gl_map.append(entry) @@ -119,14 +172,25 @@ def merge_similar_entries(gl_map, precision=None): precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency) # filter zero debit and credit entries - merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map) + merged_gl_map = filter( + lambda x: flt(x.debit, precision) != 0 or flt(x.credit, precision) != 0, merged_gl_map + ) merged_gl_map = list(merged_gl_map) return merged_gl_map + def check_if_in_list(gle, gl_map, dimensions=None): - account_head_fieldnames = ['voucher_detail_no', 'party', 'against_voucher', - 'cost_center', 'against_voucher_type', 'party_type', 'project', 'finance_book'] + account_head_fieldnames = [ + "voucher_detail_no", + "party", + "against_voucher", + "cost_center", + "against_voucher_type", + "party_type", + "project", + "finance_book", + ] if dimensions: account_head_fieldnames = account_head_fieldnames + dimensions @@ -145,6 +209,7 @@ def check_if_in_list(gle, gl_map, dimensions=None): if same_head: return e + def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): if not from_repost: validate_cwip_accounts(gl_map) @@ -157,36 +222,53 @@ def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False): for entry in gl_map: make_entry(entry, adv_adj, update_outstanding, from_repost) + def make_entry(args, adv_adj, update_outstanding, from_repost=False): gle = frappe.new_doc("GL Entry") gle.update(args) gle.flags.ignore_permissions = 1 gle.flags.from_repost = from_repost gle.flags.adv_adj = adv_adj - gle.flags.update_outstanding = update_outstanding or 'Yes' + gle.flags.update_outstanding = update_outstanding or "Yes" + gle.flags.notify_update = False gle.submit() - if not from_repost: + if not from_repost and gle.voucher_type != "Period Closing Voucher": validate_expense_against_budget(args) + def validate_cwip_accounts(gl_map): """Validate that CWIP account are not used in Journal Entry""" if gl_map and gl_map[0].voucher_type != "Journal Entry": return - cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting")) + cwip_enabled = any( + cint(ac.enable_cwip_accounting) + for ac in frappe.db.get_all("Asset Category", "enable_cwip_accounting") + ) if cwip_enabled: - cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount - where account_type = 'Capital Work in Progress' and is_group=0""")] + cwip_accounts = [ + d[0] + for d in frappe.db.sql( + """select name from tabAccount + where account_type = 'Capital Work in Progress' and is_group=0""" + ) + ] for entry in gl_map: if entry.account in cwip_accounts: frappe.throw( - _("Account: {0} is capital Work in progress and can not be updated by Journal Entry").format(entry.account)) + _( + "Account: {0} is capital Work in progress and can not be updated by Journal Entry" + ).format(entry.account) + ) + def round_off_debit_credit(gl_map): - precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), - currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) + precision = get_field_precision( + frappe.get_meta("GL Entry").get_field("debit"), + currency=frappe.get_cached_value("Company", gl_map[0].company, "default_currency"), + ) debit_credit_diff = 0.0 for entry in gl_map: @@ -199,17 +281,23 @@ def round_off_debit_credit(gl_map): if gl_map[0]["voucher_type"] in ("Journal Entry", "Payment Entry"): allowance = 5.0 / (10**precision) else: - allowance = .5 + allowance = 0.5 if abs(debit_credit_diff) > allowance: - frappe.throw(_("Debit and Credit not equal for {0} #{1}. Difference is {2}.") - .format(gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff)) + frappe.throw( + _("Debit and Credit not equal for {0} #{1}. Difference is {2}.").format( + gl_map[0].voucher_type, gl_map[0].voucher_no, debit_credit_diff + ) + ) elif abs(debit_credit_diff) >= (1.0 / (10**precision)): make_round_off_gle(gl_map, debit_credit_diff, precision) + def make_round_off_gle(gl_map, debit_credit_diff, precision): - round_off_account, round_off_cost_center = get_round_off_account_and_cost_center(gl_map[0].company) + round_off_account, round_off_cost_center = get_round_off_account_and_cost_center( + gl_map[0].company, gl_map[0].voucher_type, gl_map[0].voucher_no + ) round_off_account_exists = False round_off_gle = frappe._dict() for d in gl_map: @@ -221,35 +309,67 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): debit_credit_diff += flt(d.credit) round_off_account_exists = True - if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)): + if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)): gl_map.remove(round_off_gle) return if not round_off_gle: - for k in ["voucher_type", "voucher_no", "company", - "posting_date", "remarks"]: - round_off_gle[k] = gl_map[0][k] + for k in ["voucher_type", "voucher_no", "company", "posting_date", "remarks"]: + round_off_gle[k] = gl_map[0][k] - round_off_gle.update({ - "account": round_off_account, - "debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, - "credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0, - "debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, - "credit": debit_credit_diff if debit_credit_diff > 0 else 0, - "cost_center": round_off_cost_center, - "party_type": None, - "party": None, - "is_opening": "No", - "against_voucher_type": None, - "against_voucher": None - }) + round_off_gle.update( + { + "account": round_off_account, + "debit_in_account_currency": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, + "credit_in_account_currency": debit_credit_diff if debit_credit_diff > 0 else 0, + "debit": abs(debit_credit_diff) if debit_credit_diff < 0 else 0, + "credit": debit_credit_diff if debit_credit_diff > 0 else 0, + "cost_center": round_off_cost_center, + "party_type": None, + "party": None, + "is_opening": "No", + "against_voucher_type": None, + "against_voucher": None, + } + ) + + update_accounting_dimensions(round_off_gle) if not round_off_account_exists: gl_map.append(round_off_gle) -def get_round_off_account_and_cost_center(company): - round_off_account, round_off_cost_center = frappe.get_cached_value('Company', company, - ["round_off_account", "round_off_cost_center"]) or [None, None] + +def update_accounting_dimensions(round_off_gle): + dimensions = get_accounting_dimensions() + meta = frappe.get_meta(round_off_gle["voucher_type"]) + has_all_dimensions = True + + for dimension in dimensions: + if not meta.has_field(dimension): + has_all_dimensions = False + + if dimensions and has_all_dimensions: + dimension_values = frappe.db.get_value( + round_off_gle["voucher_type"], round_off_gle["voucher_no"], dimensions, as_dict=1 + ) + + for dimension in dimensions: + round_off_gle[dimension] = dimension_values.get(dimension) + + +def get_round_off_account_and_cost_center(company, voucher_type, voucher_no): + round_off_account, round_off_cost_center = frappe.get_cached_value( + "Company", company, ["round_off_account", "round_off_cost_center"] + ) or [None, None] + + meta = frappe.get_meta(voucher_type) + + # Give first preference to parent cost center for round off GLE + if meta.has_field("cost_center"): + parent_cost_center = frappe.db.get_value(voucher_type, voucher_no, "cost_center") + if parent_cost_center: + round_off_cost_center = parent_cost_center + if not round_off_account: frappe.throw(_("Please mention Round Off Account in Company")) @@ -258,68 +378,78 @@ def get_round_off_account_and_cost_center(company): return round_off_account, round_off_cost_center -def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, - adv_adj=False, update_outstanding="Yes"): + +def make_reverse_gl_entries( + gl_entries=None, voucher_type=None, voucher_no=None, adv_adj=False, update_outstanding="Yes" +): """ - Get original gl entries of the voucher - and make reverse gl entries by swapping debit and credit + Get original gl entries of the voucher + and make reverse gl entries by swapping debit and credit """ if not gl_entries: - gl_entries = frappe.get_all("GL Entry", - fields = ["*"], - filters = { - "voucher_type": voucher_type, - "voucher_no": voucher_no, - "is_cancelled": 0 - }) + gl_entries = frappe.get_all( + "GL Entry", + fields=["*"], + filters={"voucher_type": voucher_type, "voucher_no": voucher_no, "is_cancelled": 0}, + ) if gl_entries: validate_accounting_period(gl_entries) check_freezing_date(gl_entries[0]["posting_date"], adv_adj) - set_as_cancel(gl_entries[0]['voucher_type'], gl_entries[0]['voucher_no']) + set_as_cancel(gl_entries[0]["voucher_type"], gl_entries[0]["voucher_no"]) for entry in gl_entries: - entry['name'] = None - debit = entry.get('debit', 0) - credit = entry.get('credit', 0) + entry["name"] = None + debit = entry.get("debit", 0) + credit = entry.get("credit", 0) - debit_in_account_currency = entry.get('debit_in_account_currency', 0) - credit_in_account_currency = entry.get('credit_in_account_currency', 0) + debit_in_account_currency = entry.get("debit_in_account_currency", 0) + credit_in_account_currency = entry.get("credit_in_account_currency", 0) - entry['debit'] = credit - entry['credit'] = debit - entry['debit_in_account_currency'] = credit_in_account_currency - entry['credit_in_account_currency'] = debit_in_account_currency + entry["debit"] = credit + entry["credit"] = debit + entry["debit_in_account_currency"] = credit_in_account_currency + entry["credit_in_account_currency"] = debit_in_account_currency - entry['remarks'] = "On cancellation of " + entry['voucher_no'] - entry['is_cancelled'] = 1 + entry["remarks"] = "On cancellation of " + entry["voucher_no"] + entry["is_cancelled"] = 1 - if entry['debit'] or entry['credit']: + if entry["debit"] or entry["credit"]: make_entry(entry, adv_adj, "Yes") def check_freezing_date(posting_date, adv_adj=False): """ - Nobody can do GL Entries where posting date is before freezing date - except authorized person + Nobody can do GL Entries where posting date is before freezing date + except authorized person - Administrator has all the roles so this check will be bypassed if any role is allowed to post - Hence stop admin to bypass if accounts are freezed + Administrator has all the roles so this check will be bypassed if any role is allowed to post + Hence stop admin to bypass if accounts are freezed """ if not adv_adj: - acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto') + acc_frozen_upto = frappe.db.get_value("Accounts Settings", None, "acc_frozen_upto") if acc_frozen_upto: - frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier') - if getdate(posting_date) <= getdate(acc_frozen_upto) \ - and (frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == 'Administrator'): - frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto))) + frozen_accounts_modifier = frappe.db.get_value( + "Accounts Settings", None, "frozen_accounts_modifier" + ) + if getdate(posting_date) <= getdate(acc_frozen_upto) and ( + frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == "Administrator" + ): + frappe.throw( + _("You are not authorized to add or update entries before {0}").format( + formatdate(acc_frozen_upto) + ) + ) + def set_as_cancel(voucher_type, voucher_no): """ - Set is_cancelled=1 in all original gl entries for the voucher + Set is_cancelled=1 in all original gl entries for the voucher """ - frappe.db.sql("""UPDATE `tabGL Entry` SET is_cancelled = 1, + frappe.db.sql( + """UPDATE `tabGL Entry` SET is_cancelled = 1, modified=%s, modified_by=%s where voucher_type=%s and voucher_no=%s and is_cancelled = 0""", - (now(), frappe.session.user, voucher_type, voucher_no)) + (now(), frappe.session.user, voucher_type, voucher_no), + ) diff --git a/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py b/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py index 19b550feea7..02e3e933330 100644 --- a/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py +++ b/erpnext/accounts/notification/notification_for_new_fiscal_year/notification_for_new_fiscal_year.py @@ -1,5 +1,3 @@ - - def get_context(context): # do your magic here pass diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index a1c34a87ba8..c2ba0b43c6e 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -34,125 +34,255 @@ from erpnext.accounts.utils import get_fiscal_year from erpnext.exceptions import InvalidAccountCurrency, PartyDisabled, PartyFrozen -class DuplicatePartyAccountError(frappe.ValidationError): pass +class DuplicatePartyAccountError(frappe.ValidationError): + pass + @frappe.whitelist() -def get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, - bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, fetch_payment_terms_template=True, - party_address=None, company_address=None, shipping_address=None, pos_profile=None): +def get_party_details( + party=None, + account=None, + party_type="Customer", + company=None, + posting_date=None, + bill_date=None, + price_list=None, + currency=None, + doctype=None, + ignore_permissions=False, + fetch_payment_terms_template=True, + party_address=None, + company_address=None, + shipping_address=None, + pos_profile=None, +): if not party: return {} if not frappe.db.exists(party_type, party): frappe.throw(_("{0}: {1} does not exists").format(party_type, party)) - return _get_party_details(party, account, party_type, - company, posting_date, bill_date, price_list, currency, doctype, ignore_permissions, - fetch_payment_terms_template, party_address, company_address, shipping_address, pos_profile) + return _get_party_details( + party, + account, + party_type, + company, + posting_date, + bill_date, + price_list, + currency, + doctype, + ignore_permissions, + fetch_payment_terms_template, + party_address, + company_address, + shipping_address, + pos_profile, + ) -def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, - bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, - fetch_payment_terms_template=True, party_address=None, company_address=None, shipping_address=None, pos_profile=None): - party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) + +def _get_party_details( + party=None, + account=None, + party_type="Customer", + company=None, + posting_date=None, + bill_date=None, + price_list=None, + currency=None, + doctype=None, + ignore_permissions=False, + fetch_payment_terms_template=True, + party_address=None, + company_address=None, + shipping_address=None, + pos_profile=None, +): + party_details = frappe._dict( + set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype) + ) party = party_details[party_type.lower()] - if not ignore_permissions and not (frappe.has_permission(party_type, "read", party) or frappe.has_permission(party_type, "select", party)): + if not ignore_permissions and not ( + frappe.has_permission(party_type, "read", party) + or frappe.has_permission(party_type, "select", party) + ): frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError) party = frappe.get_doc(party_type, party) - currency = party.default_currency if party.get("default_currency") else get_company_currency(company) + currency = party.get("default_currency") or currency or get_company_currency(company) - party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address) + party_address, shipping_address = set_address_details( + party_details, + party, + party_type, + doctype, + company, + party_address, + company_address, + shipping_address, + ) set_contact_details(party_details, party, party_type) set_other_values(party_details, party, party_type) set_price_list(party_details, party, party_type, price_list, pos_profile) - party_details["tax_category"] = get_address_tax_category(party.get("tax_category"), - party_address, shipping_address if party_type != "Supplier" else party_address) + party_details["tax_category"] = get_address_tax_category( + party.get("tax_category"), + party_address, + shipping_address if party_type != "Supplier" else party_address, + ) - tax_template = set_taxes(party.name, party_type, posting_date, company, - customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, - billing_address=party_address, shipping_address=shipping_address) + tax_template = set_taxes( + party.name, + party_type, + posting_date, + company, + customer_group=party_details.customer_group, + supplier_group=party_details.supplier_group, + tax_category=party_details.tax_category, + billing_address=party_address, + shipping_address=shipping_address, + ) if tax_template: - party_details['taxes_and_charges'] = tax_template + party_details["taxes_and_charges"] = tax_template if cint(fetch_payment_terms_template): - party_details["payment_terms_template"] = get_payment_terms_template(party.name, party_type, company) + party_details["payment_terms_template"] = get_payment_terms_template( + party.name, party_type, company + ) if not party_details.get("currency"): party_details["currency"] = currency # sales team - if party_type=="Customer": - party_details["sales_team"] = [{ - "sales_person": d.sales_person, - "allocated_percentage": d.allocated_percentage or None, - "commission_rate": d.commission_rate - } for d in party.get("sales_team")] + if party_type == "Customer": + party_details["sales_team"] = [ + { + "sales_person": d.sales_person, + "allocated_percentage": d.allocated_percentage or None, + "commission_rate": d.commission_rate, + } + for d in party.get("sales_team") + ] # supplier tax withholding category if party_type == "Supplier" and party: - party_details["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category") + party_details["supplier_tds"] = frappe.get_value( + party_type, party.name, "tax_withholding_category" + ) return party_details -def set_address_details(party_details, party, party_type, doctype=None, company=None, party_address=None, company_address=None, shipping_address=None): - billing_address_field = "customer_address" if party_type == "Lead" \ - else party_type.lower() + "_address" - party_details[billing_address_field] = party_address or get_default_address(party_type, party.name) + +def set_address_details( + party_details, + party, + party_type, + doctype=None, + company=None, + party_address=None, + company_address=None, + shipping_address=None, +): + billing_address_field = ( + "customer_address" if party_type == "Lead" else party_type.lower() + "_address" + ) + party_details[billing_address_field] = party_address or get_default_address( + party_type, party.name + ) if doctype: - party_details.update(get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])) + party_details.update( + get_fetch_values(doctype, billing_address_field, party_details[billing_address_field]) + ) # address display party_details.address_display = get_address_display(party_details[billing_address_field]) # shipping address if party_type in ["Customer", "Lead"]: - party_details.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name) + party_details.shipping_address_name = shipping_address or get_party_shipping_address( + party_type, party.name + ) party_details.shipping_address = get_address_display(party_details["shipping_address_name"]) if doctype: - party_details.update(get_fetch_values(doctype, 'shipping_address_name', party_details.shipping_address_name)) + party_details.update( + get_fetch_values(doctype, "shipping_address_name", party_details.shipping_address_name) + ) if company_address: - party_details.update({'company_address': company_address}) + party_details.company_address = company_address else: party_details.update(get_company_address(company)) - if doctype and doctype in ['Delivery Note', 'Sales Invoice', 'Sales Order']: + if doctype and doctype in ["Delivery Note", "Sales Invoice", "Sales Order", "Quotation"]: if party_details.company_address: - party_details.update(get_fetch_values(doctype, 'company_address', party_details.company_address)) + party_details.update( + get_fetch_values(doctype, "company_address", party_details.company_address) + ) get_regional_address_details(party_details, doctype, company) elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: + if shipping_address: + party_details.update( + { + "shipping_address": shipping_address, + "shipping_address_display": get_address_display(shipping_address), + **get_fetch_values(doctype, "shipping_address", shipping_address), + } + ) + if party_details.company_address: - party_details["shipping_address"] = shipping_address or party_details["company_address"] - party_details.shipping_address_display = get_address_display(party_details["shipping_address"]) - party_details.update(get_fetch_values(doctype, 'shipping_address', party_details.shipping_address)) + # billing address + party_details.update( + { + "billing_address": party_details.company_address, + "billing_address_display": ( + party_details.company_address_display or get_address_display(party_details.company_address) + ), + **get_fetch_values(doctype, "billing_address", party_details.company_address), + } + ) + + # shipping address - if not already set + if not party_details.shipping_address: + party_details.update( + { + "shipping_address": party_details.billing_address, + "shipping_address_display": party_details.billing_address_display, + **get_fetch_values(doctype, "shipping_address", party_details.billing_address), + } + ) + get_regional_address_details(party_details, doctype, company) return party_details.get(billing_address_field), party_details.shipping_address_name + @erpnext.allow_regional def get_regional_address_details(party_details, doctype, company): pass + def set_contact_details(party_details, party, party_type): party_details.contact_person = get_default_contact(party_type, party.name) if not party_details.contact_person: - party_details.update({ - "contact_person": None, - "contact_display": None, - "contact_email": None, - "contact_mobile": None, - "contact_phone": None, - "contact_designation": None, - "contact_department": None - }) + party_details.update( + { + "contact_person": None, + "contact_display": None, + "contact_email": None, + "contact_mobile": None, + "contact_phone": None, + "contact_designation": None, + "contact_department": None, + } + ) else: party_details.update(get_contact_details(party_details.contact_person)) + def set_other_values(party_details, party, party_type): # copy - if party_type=="Customer": + if party_type == "Customer": to_copy = ["customer_name", "customer_group", "territory", "language"] else: to_copy = ["supplier_name", "supplier_group", "language"] @@ -160,112 +290,121 @@ def set_other_values(party_details, party, party_type): party_details[f] = party.get(f) # fields prepended with default in Customer doctype - for f in ['currency'] \ - + (['sales_partner', 'commission_rate'] if party_type=="Customer" else []): + for f in ["currency"] + ( + ["sales_partner", "commission_rate"] if party_type == "Customer" else [] + ): if party.get("default_" + f): party_details[f] = party.get("default_" + f) + def get_default_price_list(party): """Return default price list for party (Document object)""" if party.get("default_price_list"): return party.default_price_list if party.doctype == "Customer": - price_list = frappe.get_cached_value("Customer Group", - party.customer_group, "default_price_list") - if price_list: - return price_list + return frappe.db.get_value("Customer Group", party.customer_group, "default_price_list") - return None def set_price_list(party_details, party, party_type, given_price_list, pos=None): # price list - price_list = get_permitted_documents('Price List') + price_list = get_permitted_documents("Price List") # if there is only one permitted document based on user permissions, set it if price_list and len(price_list) == 1: price_list = price_list[0] - elif pos and party_type == 'Customer': - customer_price_list = frappe.get_value('Customer', party.name, 'default_price_list') + elif pos and party_type == "Customer": + customer_price_list = frappe.get_value("Customer", party.name, "default_price_list") if customer_price_list: price_list = customer_price_list else: - pos_price_list = frappe.get_value('POS Profile', pos, 'selling_price_list') + pos_price_list = frappe.get_value("POS Profile", pos, "selling_price_list") price_list = pos_price_list or given_price_list else: price_list = get_default_price_list(party) or given_price_list if price_list: - party_details.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True) + party_details.price_list_currency = frappe.db.get_value( + "Price List", price_list, "currency", cache=True + ) - party_details["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list + party_details[ + "selling_price_list" if party.doctype == "Customer" else "buying_price_list" + ] = price_list -def set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype): +def set_account_and_due_date( + party, account, party_type, company, posting_date, bill_date, doctype +): if doctype not in ["POS Invoice", "Sales Invoice", "Purchase Invoice"]: # not an invoice - return { - party_type.lower(): party - } + return {party_type.lower(): party} if party: account = get_party_account(party_type, party, company) - account_fieldname = "debit_to" if party_type=="Customer" else "credit_to" + account_fieldname = "debit_to" if party_type == "Customer" else "credit_to" out = { party_type.lower(): party, - account_fieldname : account, - "due_date": get_due_date(posting_date, party_type, party, company, bill_date) + account_fieldname: account, + "due_date": get_due_date(posting_date, party_type, party, company, bill_date), } return out + @frappe.whitelist() def get_party_account(party_type, party=None, company=None): """Returns the account for the given `party`. - Will first search in party (Customer / Supplier) record, if not found, - will search in group (Customer Group / Supplier Group), - finally will return default.""" + Will first search in party (Customer / Supplier) record, if not found, + will search in group (Customer Group / Supplier Group), + finally will return default.""" if not company: frappe.throw(_("Please select a Company")) - if not party and party_type in ['Customer', 'Supplier']: - default_account_name = "default_receivable_account" \ - if party_type=="Customer" else "default_payable_account" + if not party and party_type in ["Customer", "Supplier"]: + default_account_name = ( + "default_receivable_account" if party_type == "Customer" else "default_payable_account" + ) - return frappe.get_cached_value('Company', company, default_account_name) + return frappe.get_cached_value("Company", company, default_account_name) - account = frappe.db.get_value("Party Account", - {"parenttype": party_type, "parent": party, "company": company}, "account") + account = frappe.db.get_value( + "Party Account", {"parenttype": party_type, "parent": party, "company": company}, "account" + ) - if not account and party_type in ['Customer', 'Supplier']: - party_group_doctype = "Customer Group" if party_type=="Customer" else "Supplier Group" + if not account and party_type in ["Customer", "Supplier"]: + party_group_doctype = "Customer Group" if party_type == "Customer" else "Supplier Group" group = frappe.get_cached_value(party_type, party, scrub(party_group_doctype)) - account = frappe.db.get_value("Party Account", - {"parenttype": party_group_doctype, "parent": group, "company": company}, "account") + account = frappe.db.get_value( + "Party Account", + {"parenttype": party_group_doctype, "parent": group, "company": company}, + "account", + ) - if not account and party_type in ['Customer', 'Supplier']: - default_account_name = "default_receivable_account" \ - if party_type=="Customer" else "default_payable_account" - account = frappe.get_cached_value('Company', company, default_account_name) + if not account and party_type in ["Customer", "Supplier"]: + default_account_name = ( + "default_receivable_account" if party_type == "Customer" else "default_payable_account" + ) + account = frappe.get_cached_value("Company", company, default_account_name) existing_gle_currency = get_party_gle_currency(party_type, party, company) if existing_gle_currency: if account: account_currency = frappe.db.get_value("Account", account, "account_currency", cache=True) if (account and account_currency != existing_gle_currency) or not account: - account = get_party_gle_account(party_type, party, company) + account = get_party_gle_account(party_type, party, company) return account + @frappe.whitelist() def get_party_bank_account(party_type, party): - return frappe.db.get_value('Bank Account', { - 'party_type': party_type, - 'party': party, - 'is_default': 1 - }) + return frappe.db.get_value( + "Bank Account", {"party_type": party_type, "party": party, "is_default": 1} + ) + def get_party_account_currency(party_type, party, company): def generator(): @@ -274,27 +413,38 @@ def get_party_account_currency(party_type, party, company): return frappe.local_cache("party_account_currency", (party_type, party, company), generator) + def get_party_gle_currency(party_type, party, company): def generator(): - existing_gle_currency = frappe.db.sql("""select account_currency from `tabGL Entry` + existing_gle_currency = frappe.db.sql( + """select account_currency from `tabGL Entry` where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s - limit 1""", { "company": company, "party_type": party_type, "party": party }) + limit 1""", + {"company": company, "party_type": party_type, "party": party}, + ) return existing_gle_currency[0][0] if existing_gle_currency else None - return frappe.local_cache("party_gle_currency", (party_type, party, company), generator, - regenerate_if_none=True) + return frappe.local_cache( + "party_gle_currency", (party_type, party, company), generator, regenerate_if_none=True + ) + def get_party_gle_account(party_type, party, company): def generator(): - existing_gle_account = frappe.db.sql("""select account from `tabGL Entry` + existing_gle_account = frappe.db.sql( + """select account from `tabGL Entry` where docstatus=1 and company=%(company)s and party_type=%(party_type)s and party=%(party)s - limit 1""", { "company": company, "party_type": party_type, "party": party }) + limit 1""", + {"company": company, "party_type": party_type, "party": party}, + ) return existing_gle_account[0][0] if existing_gle_account else None - return frappe.local_cache("party_gle_account", (party_type, party, company), generator, - regenerate_if_none=True) + return frappe.local_cache( + "party_gle_account", (party_type, party, company), generator, regenerate_if_none=True + ) + def validate_party_gle_currency(party_type, party, company, party_account_currency=None): """Validate party account currency with existing GL Entry's currency""" @@ -304,32 +454,58 @@ def validate_party_gle_currency(party_type, party, company, party_account_curren existing_gle_currency = get_party_gle_currency(party_type, party, company) if existing_gle_currency and party_account_currency != existing_gle_currency: - frappe.throw(_("{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}.") - .format(frappe.bold(party_type), frappe.bold(party), frappe.bold(existing_gle_currency), frappe.bold(company)), InvalidAccountCurrency) + frappe.throw( + _( + "{0} {1} has accounting entries in currency {2} for company {3}. Please select a receivable or payable account with currency {2}." + ).format( + frappe.bold(party_type), + frappe.bold(party), + frappe.bold(existing_gle_currency), + frappe.bold(company), + ), + InvalidAccountCurrency, + ) + def validate_party_accounts(doc): + from erpnext.controllers.accounts_controller import validate_account_head companies = [] for account in doc.get("accounts"): if account.company in companies: - frappe.throw(_("There can only be 1 Account per Company in {0} {1}") - .format(doc.doctype, doc.name), DuplicatePartyAccountError) + frappe.throw( + _("There can only be 1 Account per Company in {0} {1}").format(doc.doctype, doc.name), + DuplicatePartyAccountError, + ) else: companies.append(account.company) - party_account_currency = frappe.db.get_value("Account", account.account, "account_currency", cache=True) + party_account_currency = frappe.db.get_value( + "Account", account.account, "account_currency", cache=True + ) if frappe.db.get_default("Company"): - company_default_currency = frappe.get_cached_value('Company', - frappe.db.get_default("Company"), "default_currency") + company_default_currency = frappe.get_cached_value( + "Company", frappe.db.get_default("Company"), "default_currency" + ) else: - company_default_currency = frappe.db.get_value('Company', account.company, "default_currency") + company_default_currency = frappe.db.get_value("Company", account.company, "default_currency") validate_party_gle_currency(doc.doctype, doc.name, account.company, party_account_currency) if doc.get("default_currency") and party_account_currency and company_default_currency: - if doc.default_currency != party_account_currency and doc.default_currency != company_default_currency: - frappe.throw(_("Billing currency must be equal to either default company's currency or party account currency")) + if ( + doc.default_currency != party_account_currency + and doc.default_currency != company_default_currency + ): + frappe.throw( + _( + "Billing currency must be equal to either default company's currency or party account currency" + ) + ) + + # validate if account is mapped for same company + validate_account_head(account.idx, account.account, account.company) @frappe.whitelist() @@ -341,18 +517,23 @@ def get_due_date(posting_date, party_type, party, company=None, bill_date=None): template_name = get_payment_terms_template(party, party_type, company) if template_name: - due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") + due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime( + "%Y-%m-%d" + ) else: if party_type == "Supplier": supplier_group = frappe.get_cached_value(party_type, party, "supplier_group") template_name = frappe.get_cached_value("Supplier Group", supplier_group, "payment_terms") if template_name: - due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") + due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime( + "%Y-%m-%d" + ) # If due date is calculated from bill_date, check this condition if getdate(due_date) < getdate(posting_date): due_date = posting_date return due_date + def get_due_date_from_template(template_name, posting_date, bill_date): """ Inspects all `Payment Term`s from the a `Payment Terms Template` and returns the due @@ -362,40 +543,55 @@ def get_due_date_from_template(template_name, posting_date, bill_date): """ due_date = getdate(bill_date or posting_date) - template = frappe.get_doc('Payment Terms Template', template_name) + template = frappe.get_doc("Payment Terms Template", template_name) for term in template.terms: - if term.due_date_based_on == 'Day(s) after invoice date': + if term.due_date_based_on == "Day(s) after invoice date": due_date = max(due_date, add_days(due_date, term.credit_days)) - elif term.due_date_based_on == 'Day(s) after the end of the invoice month': + elif term.due_date_based_on == "Day(s) after the end of the invoice month": due_date = max(due_date, add_days(get_last_day(due_date), term.credit_days)) else: due_date = max(due_date, add_months(get_last_day(due_date), term.credit_months)) return due_date -def validate_due_date(posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None): + +def validate_due_date( + posting_date, due_date, party_type, party, company=None, bill_date=None, template_name=None +): if getdate(due_date) < getdate(posting_date): frappe.throw(_("Due Date cannot be before Posting / Supplier Invoice Date")) else: - if not template_name: return + if not template_name: + return - default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime("%Y-%m-%d") + default_due_date = get_due_date_from_template(template_name, posting_date, bill_date).strftime( + "%Y-%m-%d" + ) if not default_due_date: return if default_due_date != posting_date and getdate(due_date) > getdate(default_due_date): - is_credit_controller = frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles() + is_credit_controller = ( + frappe.db.get_single_value("Accounts Settings", "credit_controller") in frappe.get_roles() + ) if is_credit_controller: - msgprint(_("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)") - .format(date_diff(due_date, default_due_date))) + msgprint( + _("Note: Due / Reference Date exceeds allowed customer credit days by {0} day(s)").format( + date_diff(due_date, default_due_date) + ) + ) else: - frappe.throw(_("Due / Reference Date cannot be after {0}") - .format(formatdate(default_due_date))) + frappe.throw( + _("Due / Reference Date cannot be after {0}").format(formatdate(default_due_date)) + ) + @frappe.whitelist() def get_address_tax_category(tax_category=None, billing_address=None, shipping_address=None): - addr_tax_category_from = frappe.db.get_single_value("Accounts Settings", "determine_address_tax_category_from") + addr_tax_category_from = frappe.db.get_single_value( + "Accounts Settings", "determine_address_tax_category_from" + ) if addr_tax_category_from == "Shipping Address": if shipping_address: tax_category = frappe.db.get_value("Address", shipping_address, "tax_category") or tax_category @@ -405,36 +601,48 @@ def get_address_tax_category(tax_category=None, billing_address=None, shipping_a return cstr(tax_category) + @frappe.whitelist() -def set_taxes(party, party_type, posting_date, company, customer_group=None, supplier_group=None, tax_category=None, - billing_address=None, shipping_address=None, use_for_shopping_cart=None): +def set_taxes( + party, + party_type, + posting_date, + company, + customer_group=None, + supplier_group=None, + tax_category=None, + billing_address=None, + shipping_address=None, + use_for_shopping_cart=None, +): from erpnext.accounts.doctype.tax_rule.tax_rule import get_party_details, get_tax_template - args = { - party_type.lower(): party, - "company": company - } + + args = {party_type.lower(): party, "company": company} if tax_category: - args['tax_category'] = tax_category + args["tax_category"] = tax_category if customer_group: - args['customer_group'] = customer_group + args["customer_group"] = customer_group if supplier_group: - args['supplier_group'] = supplier_group + args["supplier_group"] = supplier_group if billing_address or shipping_address: - args.update(get_party_details(party, party_type, {"billing_address": billing_address, \ - "shipping_address": shipping_address })) + args.update( + get_party_details( + party, party_type, {"billing_address": billing_address, "shipping_address": shipping_address} + ) + ) else: args.update(get_party_details(party, party_type)) if party_type in ("Customer", "Lead"): args.update({"tax_type": "Sales"}) - if party_type=='Lead': - args['customer'] = None - del args['lead'] + if party_type == "Lead": + args["customer"] = None + del args["lead"] else: args.update({"tax_type": "Purchase"}) @@ -449,25 +657,27 @@ def get_payment_terms_template(party_name, party_type, company=None): if party_type not in ("Customer", "Supplier"): return template = None - if party_type == 'Customer': - customer = frappe.get_cached_value("Customer", party_name, - fieldname=['payment_terms', "customer_group"], as_dict=1) + if party_type == "Customer": + customer = frappe.get_cached_value( + "Customer", party_name, fieldname=["payment_terms", "customer_group"], as_dict=1 + ) template = customer.payment_terms if not template and customer.customer_group: - template = frappe.get_cached_value("Customer Group", - customer.customer_group, 'payment_terms') + template = frappe.get_cached_value("Customer Group", customer.customer_group, "payment_terms") else: - supplier = frappe.get_cached_value("Supplier", party_name, - fieldname=['payment_terms', "supplier_group"], as_dict=1) + supplier = frappe.get_cached_value( + "Supplier", party_name, fieldname=["payment_terms", "supplier_group"], as_dict=1 + ) template = supplier.payment_terms if not template and supplier.supplier_group: - template = frappe.get_cached_value("Supplier Group", supplier.supplier_group, 'payment_terms') + template = frappe.get_cached_value("Supplier Group", supplier.supplier_group, "payment_terms") if not template and company: - template = frappe.get_cached_value('Company', company, fieldname='payment_terms') + template = frappe.get_cached_value("Company", company, fieldname="payment_terms") return template + def validate_party_frozen_disabled(party_type, party_name): if frappe.flags.ignore_party_validation: @@ -479,7 +689,9 @@ def validate_party_frozen_disabled(party_type, party_name): if party.disabled: frappe.throw(_("{0} {1} is disabled").format(party_type, party_name), PartyDisabled) elif party.get("is_frozen"): - frozen_accounts_modifier = frappe.db.get_single_value( 'Accounts Settings', 'frozen_accounts_modifier') + frozen_accounts_modifier = frappe.db.get_single_value( + "Accounts Settings", "frozen_accounts_modifier" + ) if not frozen_accounts_modifier in frappe.get_roles(): frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen) @@ -487,99 +699,124 @@ def validate_party_frozen_disabled(party_type, party_name): if frappe.db.get_value("Employee", party_name, "status") != "Active": frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True) + def get_timeline_data(doctype, name): - '''returns timeline data for the past one year''' + """returns timeline data for the past one year""" from frappe.desk.form.load import get_communication_data out = {} - fields = 'creation, count(*)' - after = add_years(None, -1).strftime('%Y-%m-%d') - group_by='group by Date(creation)' + fields = "creation, count(*)" + after = add_years(None, -1).strftime("%Y-%m-%d") + group_by = "group by Date(creation)" - data = get_communication_data(doctype, name, after=after, group_by='group by creation', - fields='C.creation as creation, count(C.name)',as_dict=False) + data = get_communication_data( + doctype, + name, + after=after, + group_by="group by creation", + fields="C.creation as creation, count(C.name)", + as_dict=False, + ) # fetch and append data from Activity Log - data += frappe.db.sql("""select {fields} + data += frappe.db.sql( + """select {fields} from `tabActivity Log` where (reference_doctype=%(doctype)s and reference_name=%(name)s) or (timeline_doctype in (%(doctype)s) and timeline_name=%(name)s) or (reference_doctype in ("Quotation", "Opportunity") and timeline_name=%(name)s) and status!='Success' and creation > {after} {group_by} order by creation desc - """.format(fields=fields, group_by=group_by, after=after), { - "doctype": doctype, - "name": name - }, as_dict=False) + """.format( + fields=fields, group_by=group_by, after=after + ), + {"doctype": doctype, "name": name}, + as_dict=False, + ) timeline_items = dict(data) for date, count in iteritems(timeline_items): timestamp = get_timestamp(date) - out.update({ timestamp: count }) + out.update({timestamp: count}) return out + def get_dashboard_info(party_type, party, loyalty_program=None): current_fiscal_year = get_fiscal_year(nowdate(), as_dict=True) - doctype = "Sales Invoice" if party_type=="Customer" else "Purchase Invoice" + doctype = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice" - companies = frappe.get_all(doctype, filters={ - 'docstatus': 1, - party_type.lower(): party - }, distinct=1, fields=['company']) + companies = frappe.get_all( + doctype, filters={"docstatus": 1, party_type.lower(): party}, distinct=1, fields=["company"] + ) company_wise_info = [] - company_wise_grand_total = frappe.get_all(doctype, + company_wise_grand_total = frappe.get_all( + doctype, filters={ - 'docstatus': 1, + "docstatus": 1, party_type.lower(): party, - 'posting_date': ('between', [current_fiscal_year.year_start_date, current_fiscal_year.year_end_date]) - }, - group_by="company", - fields=["company", "sum(grand_total) as grand_total", "sum(base_grand_total) as base_grand_total"] - ) + "posting_date": ( + "between", + [current_fiscal_year.year_start_date, current_fiscal_year.year_end_date], + ), + }, + group_by="company", + fields=[ + "company", + "sum(grand_total) as grand_total", + "sum(base_grand_total) as base_grand_total", + ], + ) loyalty_point_details = [] if party_type == "Customer": - loyalty_point_details = frappe._dict(frappe.get_all("Loyalty Point Entry", - filters={ - 'customer': party, - 'expiry_date': ('>=', getdate()), + loyalty_point_details = frappe._dict( + frappe.get_all( + "Loyalty Point Entry", + filters={ + "customer": party, + "expiry_date": (">=", getdate()), }, group_by="company", fields=["company", "sum(loyalty_points) as loyalty_points"], - as_list =1 - )) + as_list=1, + ) + ) company_wise_billing_this_year = frappe._dict() for d in company_wise_grand_total: company_wise_billing_this_year.setdefault( - d.company,{ - "grand_total": d.grand_total, - "base_grand_total": d.base_grand_total - }) + d.company, {"grand_total": d.grand_total, "base_grand_total": d.base_grand_total} + ) - - company_wise_total_unpaid = frappe._dict(frappe.db.sql(""" + company_wise_total_unpaid = frappe._dict( + frappe.db.sql( + """ select company, sum(debit_in_account_currency) - sum(credit_in_account_currency) from `tabGL Entry` where party_type = %s and party=%s and is_cancelled = 0 - group by company""", (party_type, party))) + group by company""", + (party_type, party), + ) + ) for d in companies: - company_default_currency = frappe.db.get_value("Company", d.company, 'default_currency') + company_default_currency = frappe.db.get_value("Company", d.company, "default_currency") party_account_currency = get_party_account_currency(party_type, party, d.company) - if party_account_currency==company_default_currency: - billing_this_year = flt(company_wise_billing_this_year.get(d.company,{}).get("base_grand_total")) + if party_account_currency == company_default_currency: + billing_this_year = flt( + company_wise_billing_this_year.get(d.company, {}).get("base_grand_total") + ) else: - billing_this_year = flt(company_wise_billing_this_year.get(d.company,{}).get("grand_total")) + billing_this_year = flt(company_wise_billing_this_year.get(d.company, {}).get("grand_total")) total_unpaid = flt(company_wise_total_unpaid.get(d.company)) @@ -602,6 +839,7 @@ def get_dashboard_info(party_type, party, loyalty_program=None): return company_wise_info + def get_party_shipping_address(doctype, name): """ Returns an Address name (best guess) for the given doctype and name for which `address_type == 'Shipping'` is true. @@ -614,50 +852,59 @@ def get_party_shipping_address(doctype, name): :return: String """ out = frappe.db.sql( - 'SELECT dl.parent ' - 'from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name ' - 'where ' - 'dl.link_doctype=%s ' - 'and dl.link_name=%s ' + "SELECT dl.parent " + "from `tabDynamic Link` dl join `tabAddress` ta on dl.parent=ta.name " + "where " + "dl.link_doctype=%s " + "and dl.link_name=%s " 'and dl.parenttype="Address" ' - 'and ifnull(ta.disabled, 0) = 0 and' + "and ifnull(ta.disabled, 0) = 0 and" '(ta.address_type="Shipping" or ta.is_shipping_address=1) ' - 'order by ta.is_shipping_address desc, ta.address_type desc limit 1', - (doctype, name) + "order by ta.is_shipping_address desc, ta.address_type desc limit 1", + (doctype, name), ) if out: return out[0][0] else: - return '' + return "" -def get_partywise_advanced_payment_amount(party_type, posting_date = None, future_payment=0, company=None): + +def get_partywise_advanced_payment_amount( + party_type, posting_date=None, future_payment=0, company=None +): cond = "1=1" if posting_date: if future_payment: - cond = "posting_date <= '{0}' OR DATE(creation) <= '{0}' """.format(posting_date) + cond = "posting_date <= '{0}' OR DATE(creation) <= '{0}' " "".format(posting_date) else: cond = "posting_date <= '{0}'".format(posting_date) if company: cond += "and company = {0}".format(frappe.db.escape(company)) - data = frappe.db.sql(""" SELECT party, sum({0}) as amount + data = frappe.db.sql( + """ SELECT party, sum({0}) as amount FROM `tabGL Entry` WHERE party_type = %s and against_voucher is null and is_cancelled = 0 - and {1} GROUP BY party""" - .format(("credit") if party_type == "Customer" else "debit", cond) , party_type) + and {1} GROUP BY party""".format( + ("credit") if party_type == "Customer" else "debit", cond + ), + party_type, + ) if data: return frappe._dict(data) + def get_default_contact(doctype, name): """ - Returns default contact for the given doctype and name. - Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact. + Returns default contact for the given doctype and name. + Can be ordered by `contact_type` to either is_primary_contact or is_billing_contact. """ - out = frappe.db.sql(""" + out = frappe.db.sql( + """ SELECT dl.parent, c.is_primary_contact, c.is_billing_contact FROM `tabDynamic Link` dl INNER JOIN tabContact c ON c.name = dl.parent @@ -666,7 +913,9 @@ def get_default_contact(doctype, name): dl.link_name=%s AND dl.parenttype = "Contact" ORDER BY is_primary_contact DESC, is_billing_contact DESC - """, (doctype, name)) + """, + (doctype, name), + ) if out: try: return out[0][0] @@ -674,3 +923,18 @@ def get_default_contact(doctype, name): return None else: return None + + +def add_party_account(party_type, party, company, account): + doc = frappe.get_doc(party_type, party) + account_exists = False + for d in doc.get("accounts"): + if d.account == account: + account_exists = True + + if not account_exists: + accounts = {"company": company, "account": account} + + doc.append("accounts", accounts) + + doc.save() diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html index e6580493095..605ce8383e4 100644 --- a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html +++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html @@ -1,7 +1,8 @@ {%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%} -{%- set einvoice = json.loads(doc.signed_einvoice) -%}
+ {% if doc.signed_einvoice %}
+ {%- set einvoice = json.loads(doc.signed_einvoice) -%}
{% if letter_head and not no_letterhead %}
{{ letter_head }}
@@ -170,4 +171,10 @@
|
|---|
\n\t{{ doc.company }}
\n\t{% if doc.company_address_display %}\n\t\t{% set company_address = doc.company_address_display.replace(\"\\n\", \" \").replace(\"
\", \" \") %}\n\t\t{% if \"GSTIN\" not in company_address %}\n\t\t\t{{ company_address }}\n\t\t\t{{ _(\"GSTIN\") }}:{{ doc.company_gstin }}\n\t\t{% else %}\n\t\t\t{{ company_address.replace(\"GSTIN\", \"
GSTIN\") }}\n\t\t{% endif %}\n\t{% endif %}\n\t
\n\t{% if doc.docstatus == 0 %}\n\t\t{{ doc.status + \" \"+ (doc.select_print_heading or _(\"Invoice\")) }}
\n\t{% else %}\n\t\t{{ doc.select_print_heading or _(\"Invoice\") }}
\n\t{% endif %}\n
\n\t{{ _(\"Receipt No\") }}: {{ doc.name }}
\n\t{{ _(\"Date\") }}: {{ doc.get_formatted(\"posting_date\") }}
\n\t{% if doc.grand_total > 50000 %}\n\t\t{% set customer_address = doc.address_display.replace(\"\\n\", \" \").replace(\"
\", \" \") %}\n\t\t{{ _(\"Customer\") }}:
\n\t\t{{ doc.customer_name }}
\n\t\t{{ customer_address }}\n\t{% endif %}\n
| {{ _(\"Item\") }} | \n\t\t\t{{ _(\"Qty\") }} | \n\t\t\t{{ _(\"Amount\") }} | \n\t\t
|---|---|---|
| \n\t\t\t\t{{ item.item_code }}\n\t\t\t\t{%- if item.item_name != item.item_code -%}\n\t\t\t\t\t {{ item.item_name }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.gst_hsn_code -%}\n\t\t\t\t\t {{ _(\"HSN/SAC\") }}: {{ item.gst_hsn_code }}\n\t\t\t\t{%- endif -%}\n\t\t\t\t{%- if item.serial_no -%}\n\t\t\t\t\t {{ _(\"Serial No\") }}: {{ item.serial_no }}\n\t\t\t\t{%- endif -%}\n\t\t\t | \n\t\t\t{{ item.qty }} @ {{ item.rate }} | \n\t\t\t{{ item.get_formatted(\"amount\") }} | \n\t\t
| \n\t\t\t\t\t{{ _(\"Total Excl. Tax\") }}\n\t\t\t\t | \n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"net_total\", doc) }}\n\t\t\t\t | \n\t\t\t{% else %}\n\t\t\t\t\n\t\t\t\t\t{{ _(\"Total\") }}\n\t\t\t\t | \n\t\t\t\t\n\t\t\t\t\t{{ doc.get_formatted(\"total\", doc) }}\n\t\t\t\t | \n\t\t\t{% endif %}\n\t\t
| \n\t\t\t\t\t{{ row.description }}\n\t\t\t\t | \n\t\t\t\t\n\t\t\t\t\t{{ row.get_formatted(\"tax_amount\", doc) }}\n\t\t\t\t | \n\t\t\t||
| \n\t\t\t\t{{ _(\"Discount\") }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"discount_amount\") }}\n\t\t\t | \n\t\t||
| \n\t\t\t\t{{ _(\"Grand Total\") }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"grand_total\") }}\n\t\t\t | \n\t\t||
| \n\t\t\t\t{{ _(\"Rounded Total\") }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"rounded_total\") }}\n\t\t\t | \n\t\t||
| \n\t\t\t\t{{ _(\"Paid Amount\") }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"paid_amount\") }}\n\t\t\t | \n\t\t||
| \n\t\t\t\t{{ _(\"Change Amount\") }}\n\t\t\t | \n\t\t\t\n\t\t\t\t{{ doc.get_formatted(\"change_amount\") }}\n\t\t\t | \n\t\t
{{ doc.terms or \"\" }}
\n{{ _(\"Thank you, please visit again.\") }}
", - "idx": 0, - "line_breaks": 0, - "modified": "2020-04-29 16:39:12.936215", - "modified_by": "Administrator", - "module": "Accounts", - "name": "GST POS Invoice", - "owner": "Administrator", - "print_format_builder": 0, - "print_format_type": "Jinja", - "raw_printing": 0, - "show_section_headings": 0, - "standard": "Yes" -} \ No newline at end of file diff --git a/erpnext/accounts/report/account_balance/account_balance.py b/erpnext/accounts/report/account_balance/account_balance.py index a2c70a45f99..824a965cdcf 100644 --- a/erpnext/accounts/report/account_balance/account_balance.py +++ b/erpnext/accounts/report/account_balance/account_balance.py @@ -14,6 +14,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(filters): columns = [ { @@ -21,7 +22,7 @@ def get_columns(filters): "fieldtype": "Link", "fieldname": "account", "options": "Account", - "width": 100 + "width": 100, }, { "label": _("Currency"), @@ -29,19 +30,20 @@ def get_columns(filters): "fieldname": "currency", "options": "Currency", "hidden": 1, - "width": 50 + "width": 50, }, { "label": _("Balance"), "fieldtype": "Currency", "fieldname": "balance", "options": "currency", - "width": 100 - } + "width": 100, + }, ] return columns + def get_conditions(filters): conditions = {} @@ -57,12 +59,14 @@ def get_conditions(filters): return conditions + def get_data(filters): data = [] conditions = get_conditions(filters) - accounts = frappe.db.get_all("Account", fields=["name", "account_currency"], - filters=conditions, order_by='name') + accounts = frappe.db.get_all( + "Account", fields=["name", "account_currency"], filters=conditions, order_by="name" + ) for d in accounts: balance = get_balance_on(d.name, date=filters.report_date) diff --git a/erpnext/accounts/report/account_balance/test_account_balance.py b/erpnext/accounts/report/account_balance/test_account_balance.py index 50b1a679b6c..13fa05d4743 100644 --- a/erpnext/accounts/report/account_balance/test_account_balance.py +++ b/erpnext/accounts/report/account_balance/test_account_balance.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -14,9 +13,9 @@ class TestAccountBalance(unittest.TestCase): frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") filters = { - 'company': '_Test Company 2', - 'report_date': getdate(), - 'root_type': 'Income', + "company": "_Test Company 2", + "report_date": getdate(), + "root_type": "Income", } make_sales_invoice() @@ -25,42 +24,45 @@ class TestAccountBalance(unittest.TestCase): expected_data = [ { - "account": 'Direct Income - _TC2', - "currency": 'EUR', + "account": "Direct Income - _TC2", + "currency": "EUR", "balance": -100.0, }, { - "account": 'Income - _TC2', - "currency": 'EUR', + "account": "Income - _TC2", + "currency": "EUR", "balance": -100.0, }, { - "account": 'Indirect Income - _TC2', - "currency": 'EUR', + "account": "Indirect Income - _TC2", + "currency": "EUR", "balance": 0.0, }, { - "account": 'Sales - _TC2', - "currency": 'EUR', + "account": "Sales - _TC2", + "currency": "EUR", "balance": -100.0, }, { - "account": 'Service - _TC2', - "currency": 'EUR', + "account": "Service - _TC2", + "currency": "EUR", "balance": 0.0, - } + }, ] self.assertEqual(expected_data, report[1]) + def make_sales_invoice(): frappe.set_user("Administrator") - create_sales_invoice(company="_Test Company 2", - customer = '_Test Customer 2', - currency = 'EUR', - warehouse = 'Finished Goods - _TC2', - debit_to = 'Debtors - _TC2', - income_account = 'Sales - _TC2', - expense_account = 'Cost of Goods Sold - _TC2', - cost_center = 'Main - _TC2') + create_sales_invoice( + company="_Test Company 2", + customer="_Test Customer 2", + currency="EUR", + warehouse="Finished Goods - _TC2", + debit_to="Debtors - _TC2", + income_account="Sales - _TC2", + expense_account="Cost of Goods Sold - _TC2", + cost_center="Main - _TC2", + ) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 81c60bb337d..f6961eb95fa 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -53,6 +53,22 @@ frappe.query_reports["Accounts Payable"] = { } } }, + { + "fieldname": "party_account", + "label": __("Payable Account"), + "fieldtype": "Link", + "options": "Account", + get_query: () => { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + 'company': company, + 'account_type': 'Payable', + 'is_group': 0 + } + }; + } + }, { "fieldname": "ageing_based_on", "label": __("Ageing Based On"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html index f4fd06ba037..f2bf9424f72 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html @@ -42,7 +42,7 @@ {% if(filters.show_future_payments) { %} {% var balance_row = data.slice(-1).pop(); - var start = filters.based_on_payment_terms ? 13 : 11; + var start = report.columns.findIndex((elem) => (elem.fieldname == 'age')); var range1 = report.columns[start].label; var range2 = report.columns[start+1].label; var range3 = report.columns[start+2].label; diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js index 570029851e8..748bcde4354 100644 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.js +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.js @@ -66,6 +66,22 @@ frappe.query_reports["Accounts Receivable"] = { } } }, + { + "fieldname": "party_account", + "label": __("Receivable Account"), + "fieldtype": "Link", + "options": "Account", + get_query: () => { + var company = frappe.query_report.get_filter_value('company'); + return { + filters: { + 'company': company, + 'account_type': 'Receivable', + 'is_group': 0 + } + }; + } + }, { "fieldname": "ageing_based_on", "label": __("Ageing Based On"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index a990f23cd6b..c9567f23a34 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -29,6 +29,7 @@ from erpnext.accounts.utils import get_currency_precision # 9. Report amounts are in "Party Currency" if party is selected, or company currency for multi-party # 10. This reports is based on all GL Entries that are made against account_type "Receivable" or "Payable" + def execute(filters=None): args = { "party_type": "Customer", @@ -36,18 +37,23 @@ def execute(filters=None): } return ReceivablePayableReport(filters).run(args) + class ReceivablePayableReport(object): def __init__(self, filters=None): self.filters = frappe._dict(filters or {}) self.filters.report_date = getdate(self.filters.report_date or nowdate()) - self.age_as_on = getdate(nowdate()) \ - if self.filters.report_date > getdate(nowdate()) \ + self.age_as_on = ( + getdate(nowdate()) + if self.filters.report_date > getdate(nowdate()) else self.filters.report_date + ) def run(self, args): self.filters.update(args) self.set_defaults() - self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + self.party_naming_by = frappe.db.get_value( + args.get("naming_by")[0], None, args.get("naming_by")[1] + ) self.get_columns() self.get_data() self.get_chart_data() @@ -55,8 +61,10 @@ class ReceivablePayableReport(object): def set_defaults(self): if not self.filters.get("company"): - self.filters.company = frappe.db.get_single_value('Global Defaults', 'default_company') - self.company_currency = frappe.get_cached_value('Company', self.filters.get("company"), "default_currency") + self.filters.company = frappe.db.get_single_value("Global Defaults", "default_company") + self.company_currency = frappe.get_cached_value( + "Company", self.filters.get("company"), "default_currency" + ) self.currency_precision = get_currency_precision() or 2 self.dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" self.party_type = self.filters.party_type @@ -64,8 +72,8 @@ class ReceivablePayableReport(object): self.invoices = set() self.skip_total_row = 0 - if self.filters.get('group_by_party'): - self.previous_party='' + if self.filters.get("group_by_party"): + self.previous_party = "" self.total_row_map = {} self.skip_total_row = 1 @@ -73,7 +81,7 @@ class ReceivablePayableReport(object): self.get_gl_entries() self.get_sales_invoices_or_customers_based_on_sales_person() self.voucher_balance = OrderedDict() - self.init_voucher_balance() # invoiced, paid, credit_note, outstanding + self.init_voucher_balance() # invoiced, paid, credit_note, outstanding # Build delivery note map against all sales invoices self.build_delivery_note_map() @@ -100,64 +108,73 @@ class ReceivablePayableReport(object): key = (gle.voucher_type, gle.voucher_no, gle.party) if not key in self.voucher_balance: self.voucher_balance[key] = frappe._dict( - voucher_type = gle.voucher_type, - voucher_no = gle.voucher_no, - party = gle.party, - posting_date = gle.posting_date, - account_currency = gle.account_currency, - remarks = gle.remarks if self.filters.get("show_remarks") else None, - invoiced = 0.0, - paid = 0.0, - credit_note = 0.0, - outstanding = 0.0, - invoiced_in_account_currency = 0.0, - paid_in_account_currency = 0.0, - credit_note_in_account_currency = 0.0, - outstanding_in_account_currency = 0.0 + voucher_type=gle.voucher_type, + voucher_no=gle.voucher_no, + party=gle.party, + party_account=gle.account, + posting_date=gle.posting_date, + account_currency=gle.account_currency, + remarks=gle.remarks if self.filters.get("show_remarks") else None, + invoiced=0.0, + paid=0.0, + credit_note=0.0, + outstanding=0.0, + invoiced_in_account_currency=0.0, + paid_in_account_currency=0.0, + credit_note_in_account_currency=0.0, + outstanding_in_account_currency=0.0, ) self.get_invoices(gle) - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): self.init_subtotal_row(gle.party) - if self.filters.get('group_by_party'): - self.init_subtotal_row('Total') + if self.filters.get("group_by_party"): + self.init_subtotal_row("Total") def get_invoices(self, gle): - if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): + if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"): if self.filters.get("sales_person"): - if gle.voucher_no in self.sales_person_records.get("Sales Invoice", []) \ - or gle.party in self.sales_person_records.get("Customer", []): - self.invoices.add(gle.voucher_no) + if gle.voucher_no in self.sales_person_records.get( + "Sales Invoice", [] + ) or gle.party in self.sales_person_records.get("Customer", []): + self.invoices.add(gle.voucher_no) else: self.invoices.add(gle.voucher_no) def init_subtotal_row(self, party): if not self.total_row_map.get(party): - self.total_row_map.setdefault(party, { - 'party': party, - 'bold': 1 - }) + self.total_row_map.setdefault(party, {"party": party, "bold": 1}) for field in self.get_currency_fields(): self.total_row_map[party][field] = 0.0 def get_currency_fields(self): - return ['invoiced', 'paid', 'credit_note', 'outstanding', 'range1', - 'range2', 'range3', 'range4', 'range5'] + return [ + "invoiced", + "paid", + "credit_note", + "outstanding", + "range1", + "range2", + "range3", + "range4", + "range5", + ] def update_voucher_balance(self, gle): # get the row where this balance needs to be updated # if its a payment, it will return the linked invoice or will be considered as advance row = self.get_voucher_balance(gle) - if not row: return + if not row: + return # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) gle_balance_in_account_currency = self.get_gle_balance_in_account_currency(gle) if gle_balance > 0: - if gle.voucher_type in ('Journal Entry', 'Payment Entry') and gle.against_voucher: + if gle.voucher_type in ("Journal Entry", "Payment Entry") and gle.against_voucher: # debit against sales / purchase invoice row.paid -= gle_balance row.paid_in_account_currency -= gle_balance_in_account_currency @@ -177,7 +194,7 @@ class ReceivablePayableReport(object): row.paid_in_account_currency -= gle_balance_in_account_currency if gle.cost_center: - row.cost_center = str(gle.cost_center) + row.cost_center = str(gle.cost_center) def update_sub_total_row(self, row, party): total_row = self.total_row_map.get(party) @@ -191,14 +208,16 @@ class ReceivablePayableReport(object): if sub_total_row: self.data.append(sub_total_row) self.data.append({}) - self.update_sub_total_row(sub_total_row, 'Total') + self.update_sub_total_row(sub_total_row, "Total") def get_voucher_balance(self, gle): if self.filters.get("sales_person"): against_voucher = gle.against_voucher or gle.voucher_no - if not (gle.party in self.sales_person_records.get("Customer", []) or \ - against_voucher in self.sales_person_records.get("Sales Invoice", [])): - return + if not ( + gle.party in self.sales_person_records.get("Customer", []) + or against_voucher in self.sales_person_records.get("Sales Invoice", []) + ): + return voucher_balance = None if gle.against_voucher: @@ -208,13 +227,15 @@ class ReceivablePayableReport(object): # If payment is made against credit note # and credit note is made against a Sales Invoice # then consider the payment against original sales invoice. - if gle.against_voucher_type in ('Sales Invoice', 'Purchase Invoice'): + if gle.against_voucher_type in ("Sales Invoice", "Purchase Invoice"): if gle.against_voucher in self.return_entries: return_against = self.return_entries.get(gle.against_voucher) if return_against: against_voucher = return_against - voucher_balance = self.voucher_balance.get((gle.against_voucher_type, against_voucher, gle.party)) + voucher_balance = self.voucher_balance.get( + (gle.against_voucher_type, against_voucher, gle.party) + ) if not voucher_balance: # no invoice, this is an invoice / stand-alone payment / credit note @@ -227,13 +248,18 @@ class ReceivablePayableReport(object): # as we can use this to filter out invoices without outstanding for key, row in self.voucher_balance.items(): row.outstanding = flt(row.invoiced - row.paid - row.credit_note, self.currency_precision) - row.outstanding_in_account_currency = flt(row.invoiced_in_account_currency - row.paid_in_account_currency - \ - row.credit_note_in_account_currency, self.currency_precision) + row.outstanding_in_account_currency = flt( + row.invoiced_in_account_currency + - row.paid_in_account_currency + - row.credit_note_in_account_currency, + self.currency_precision, + ) row.invoice_grand_total = row.invoiced - if (abs(row.outstanding) > 1.0/10 ** self.currency_precision) and \ - (abs(row.outstanding_in_account_currency) > 1.0/10 ** self.currency_precision): + if (abs(row.outstanding) > 1.0 / 10**self.currency_precision) and ( + abs(row.outstanding_in_account_currency) > 1.0 / 10**self.currency_precision + ): # non-zero oustanding, we must consider this row if self.is_invoice(row) and self.filters.based_on_payment_terms: @@ -254,10 +280,10 @@ class ReceivablePayableReport(object): else: self.append_row(row) - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): self.append_subtotal_row(self.previous_party) if self.data: - self.data.append(self.total_row_map.get('Total')) + self.data.append(self.total_row_map.get("Total")) def append_row(self, row): self.allocate_future_payments(row) @@ -265,7 +291,7 @@ class ReceivablePayableReport(object): self.set_party_details(row) self.set_ageing(row) - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): self.update_sub_total_row(row, row.party) if self.previous_party and (self.previous_party != row.party): self.append_subtotal_row(self.previous_party) @@ -279,39 +305,49 @@ class ReceivablePayableReport(object): invoice_details.pop("due_date", None) row.update(invoice_details) - if row.voucher_type == 'Sales Invoice': + if row.voucher_type == "Sales Invoice": if self.filters.show_delivery_notes: self.set_delivery_notes(row) if self.filters.show_sales_person and row.sales_team: row.sales_person = ", ".join(row.sales_team) - del row['sales_team'] + del row["sales_team"] def set_delivery_notes(self, row): delivery_notes = self.delivery_notes.get(row.voucher_no, []) if delivery_notes: - row.delivery_notes = ', '.join(delivery_notes) + row.delivery_notes = ", ".join(delivery_notes) def build_delivery_note_map(self): if self.invoices and self.filters.show_delivery_notes: self.delivery_notes = frappe._dict() # delivery note link inside sales invoice - si_against_dn = frappe.db.sql(""" + si_against_dn = frappe.db.sql( + """ select parent, delivery_note from `tabSales Invoice Item` where docstatus=1 and parent in (%s) - """ % (','.join(['%s'] * len(self.invoices))), tuple(self.invoices), as_dict=1) + """ + % (",".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in si_against_dn: if d.delivery_note: self.delivery_notes.setdefault(d.parent, set()).add(d.delivery_note) - dn_against_si = frappe.db.sql(""" + dn_against_si = frappe.db.sql( + """ select distinct parent, against_sales_invoice from `tabDelivery Note Item` where against_sales_invoice in (%s) - """ % (','.join(['%s'] * len(self.invoices))), tuple(self.invoices) , as_dict=1) + """ + % (",".join(["%s"] * len(self.invoices))), + tuple(self.invoices), + as_dict=1, + ) for d in dn_against_si: self.delivery_notes.setdefault(d.against_sales_invoice, set()).add(d.parent) @@ -319,39 +355,55 @@ class ReceivablePayableReport(object): def get_invoice_details(self): self.invoice_details = frappe._dict() if self.party_type == "Customer": - si_list = frappe.db.sql(""" + si_list = frappe.db.sql( + """ select name, due_date, po_no from `tabSales Invoice` where posting_date <= %s - """,self.filters.report_date, as_dict=1) + """, + self.filters.report_date, + as_dict=1, + ) for d in si_list: self.invoice_details.setdefault(d.name, d) # Get Sales Team if self.filters.show_sales_person: - sales_team = frappe.db.sql(""" + sales_team = frappe.db.sql( + """ select parent, sales_person from `tabSales Team` where parenttype = 'Sales Invoice' - """, as_dict=1) + """, + as_dict=1, + ) for d in sales_team: - self.invoice_details.setdefault(d.parent, {})\ - .setdefault('sales_team', []).append(d.sales_person) + self.invoice_details.setdefault(d.parent, {}).setdefault("sales_team", []).append( + d.sales_person + ) if self.party_type == "Supplier": - for pi in frappe.db.sql(""" + for pi in frappe.db.sql( + """ select name, due_date, bill_no, bill_date from `tabPurchase Invoice` where posting_date <= %s - """, self.filters.report_date, as_dict=1): + """, + self.filters.report_date, + as_dict=1, + ): self.invoice_details.setdefault(pi.name, pi) # Invoices booked via Journal Entries - journal_entries = frappe.db.sql(""" + journal_entries = frappe.db.sql( + """ select name, due_date, bill_no, bill_date from `tabJournal Entry` where posting_date <= %s - """, self.filters.report_date, as_dict=1) + """, + self.filters.report_date, + as_dict=1, + ) for je in journal_entries: if je.bill_no: @@ -372,17 +424,18 @@ class ReceivablePayableReport(object): # update "paid" and "oustanding" for this term if not term.paid: - self.allocate_closing_to_term(row, term, 'paid') + self.allocate_closing_to_term(row, term, "paid") # update "credit_note" and "oustanding" for this term if term.outstanding: - self.allocate_closing_to_term(row, term, 'credit_note') + self.allocate_closing_to_term(row, term, "credit_note") - row.payment_terms = sorted(row.payment_terms, key=lambda x: x['due_date']) + row.payment_terms = sorted(row.payment_terms, key=lambda x: x["due_date"]) def get_payment_terms(self, row): # build payment_terms for row - payment_terms_details = frappe.db.sql(""" + payment_terms_details = frappe.db.sql( + """ select si.name, si.party_account_currency, si.currency, si.conversion_rate, ps.due_date, ps.payment_term, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount @@ -391,8 +444,12 @@ class ReceivablePayableReport(object): si.name = ps.parent and si.name = %s order by ps.paid_amount desc, due_date - """.format(row.voucher_type), row.voucher_no, as_dict = 1) - + """.format( + row.voucher_type + ), + row.voucher_no, + as_dict=1, + ) original_row = frappe._dict(row) row.payment_terms = [] @@ -406,23 +463,29 @@ class ReceivablePayableReport(object): self.append_payment_term(row, d, term) def append_payment_term(self, row, d, term): - if (self.filters.get("customer") or self.filters.get("supplier")) and d.currency == d.party_account_currency: + if ( + self.filters.get("customer") or self.filters.get("supplier") + ) and d.currency == d.party_account_currency: invoiced = d.payment_amount else: invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision) - row.payment_terms.append(term.update({ - "due_date": d.due_date, - "invoiced": invoiced, - "invoice_grand_total": row.invoiced, - "payment_term": d.description or d.payment_term, - "paid": d.paid_amount + d.discounted_amount, - "credit_note": 0.0, - "outstanding": invoiced - d.paid_amount - d.discounted_amount - })) + row.payment_terms.append( + term.update( + { + "due_date": d.due_date, + "invoiced": invoiced, + "invoice_grand_total": row.invoiced, + "payment_term": d.description or d.payment_term, + "paid": d.paid_amount + d.discounted_amount, + "credit_note": 0.0, + "outstanding": invoiced - d.paid_amount - d.discounted_amount, + } + ) + ) if d.paid_amount: - row['paid'] -= d.paid_amount + d.discounted_amount + row["paid"] -= d.paid_amount + d.discounted_amount def allocate_closing_to_term(self, row, term, key): if row[key]: @@ -437,7 +500,7 @@ class ReceivablePayableReport(object): def allocate_extra_payments_or_credits(self, row): # allocate extra payments / credits additional_row = None - for key in ('paid', 'credit_note'): + for key in ("paid", "credit_note"): if row[key] > 0: if not additional_row: additional_row = frappe._dict(row) @@ -445,7 +508,9 @@ class ReceivablePayableReport(object): additional_row[key] = row[key] if additional_row: - additional_row.outstanding = additional_row.invoiced - additional_row.paid - additional_row.credit_note + additional_row.outstanding = ( + additional_row.invoiced - additional_row.paid - additional_row.credit_note + ) self.append_row(additional_row) def get_future_payments(self): @@ -459,7 +524,8 @@ class ReceivablePayableReport(object): self.future_payments.setdefault((d.invoice_no, d.party), []).append(d) def get_future_payments_from_payment_entry(self): - return frappe.db.sql(""" + return frappe.db.sql( + """ select ref.reference_name as invoice_no, payment_entry.party, @@ -475,16 +541,23 @@ class ReceivablePayableReport(object): payment_entry.docstatus < 2 and payment_entry.posting_date > %s and payment_entry.party_type = %s - """, (self.filters.report_date, self.party_type), as_dict=1) + """, + (self.filters.report_date, self.party_type), + as_dict=1, + ) def get_future_payments_from_journal_entry(self): - if self.filters.get('party'): - amount_field = ("jea.debit_in_account_currency - jea.credit_in_account_currency" - if self.party_type == 'Supplier' else "jea.credit_in_account_currency - jea.debit_in_account_currency") + if self.filters.get("party"): + amount_field = ( + "jea.debit_in_account_currency - jea.credit_in_account_currency" + if self.party_type == "Supplier" + else "jea.credit_in_account_currency - jea.debit_in_account_currency" + ) else: - amount_field = ("jea.debit - " if self.party_type == 'Supplier' else "jea.credit") + amount_field = "jea.debit - " if self.party_type == "Supplier" else "jea.credit" - return frappe.db.sql(""" + return frappe.db.sql( + """ select jea.reference_name as invoice_no, jea.party, @@ -503,7 +576,12 @@ class ReceivablePayableReport(object): and jea.reference_name is not null and jea.reference_name != '' group by je.name, jea.reference_name having future_amount > 0 - """.format(amount_field), (self.filters.report_date, self.party_type), as_dict=1) + """.format( + amount_field + ), + (self.filters.report_date, self.party_type), + as_dict=1, + ) def allocate_future_payments(self, row): # future payments are captured in additional columns @@ -525,22 +603,21 @@ class ReceivablePayableReport(object): future.future_amount = 0 row.remaining_balance = row.outstanding - row.future_amount - row.setdefault('future_ref', []).append(cstr(future.future_ref) + '/' + cstr(future.future_date)) + row.setdefault("future_ref", []).append( + cstr(future.future_ref) + "/" + cstr(future.future_date) + ) if row.future_ref: - row.future_ref = ', '.join(row.future_ref) + row.future_ref = ", ".join(row.future_ref) def get_return_entries(self): doctype = "Sales Invoice" if self.party_type == "Customer" else "Purchase Invoice" - filters={ - 'is_return': 1, - 'docstatus': 1 - } + filters = {"is_return": 1, "docstatus": 1} party_field = scrub(self.filters.party_type) if self.filters.get(party_field): filters.update({party_field: self.filters.get(party_field)}) self.return_entries = frappe._dict( - frappe.get_all(doctype, filters, ['name', 'return_against'], as_list=1) + frappe.get_all(doctype, filters, ["name", "return_against"], as_list=1) ) def set_ageing(self, row): @@ -571,16 +648,26 @@ class ReceivablePayableReport(object): row.age = (getdate(self.age_as_on) - getdate(entry_date)).days or 0 index = None - if not (self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4): - self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = 30, 60, 90, 120 + if not ( + self.filters.range1 and self.filters.range2 and self.filters.range3 and self.filters.range4 + ): + self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4 = ( + 30, + 60, + 90, + 120, + ) - for i, days in enumerate([self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4]): + for i, days in enumerate( + [self.filters.range1, self.filters.range2, self.filters.range3, self.filters.range4] + ): if cint(row.age) <= cint(days): index = i break - if index is None: index = 4 - row['range' + str(index+1)] = row.outstanding + if index is None: + index = 4 + row["range" + str(index + 1)] = row.outstanding def get_gl_entries(self): # get all the GL entries filtered by the given filters @@ -605,7 +692,8 @@ class ReceivablePayableReport(object): remarks = ", remarks" if self.filters.get("show_remarks") else "" - self.gl_entries = frappe.db.sql(""" + self.gl_entries = frappe.db.sql( + """ select name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center, against_voucher_type, against_voucher, account_currency, {0}, {1} {remarks} @@ -616,20 +704,27 @@ class ReceivablePayableReport(object): and is_cancelled = 0 and party_type=%s and (party is not null and party != '') - {2} {3} {4}""" - .format(select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True) + {2} {3} {4}""".format( + select_fields, doc_currency_fields, date_condition, conditions, order_by, remarks=remarks + ), + values, + as_dict=True, + ) def get_sales_invoices_or_customers_based_on_sales_person(self): if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value("Sales Person", self.filters.get("sales_person"), ["lft", "rgt"]) - records = frappe.db.sql(""" + records = frappe.db.sql( + """ select distinct parent, parenttype from `tabSales Team` steam where parenttype in ('Customer', 'Sales Invoice') and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) - """, (lft, rgt), as_dict=1) + """, + (lft, rgt), + as_dict=1, + ) self.sales_person_records = frappe._dict() for d in records: @@ -642,10 +737,10 @@ class ReceivablePayableReport(object): self.add_common_filters(conditions, values, party_type_field) - if party_type_field=="customer": + if party_type_field == "customer": self.add_customer_filters(conditions, values) - elif party_type_field=="supplier": + elif party_type_field == "supplier": self.add_supplier_filters(conditions, values) if self.filters.cost_center: @@ -656,13 +751,16 @@ class ReceivablePayableReport(object): def get_cost_center_conditions(self, conditions): lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) - cost_center_list = [center.name for center in frappe.get_list("Cost Center", filters = {'lft': (">=", lft), 'rgt': ("<=", rgt)})] + cost_center_list = [ + center.name + for center in frappe.get_list("Cost Center", filters={"lft": (">=", lft), "rgt": ("<=", rgt)}) + ] cost_center_string = '", "'.join(cost_center_list) conditions.append('cost_center in ("{0}")'.format(cost_center_string)) def get_order_by_condition(self): - if self.filters.get('group_by_party'): + if self.filters.get("group_by_party"): return "order by party, posting_date" else: return "order by posting_date, party" @@ -680,21 +778,28 @@ class ReceivablePayableReport(object): conditions.append("party=%s") values.append(self.filters.get(party_type_field)) - # get GL with "receivable" or "payable" account_type - account_type = "Receivable" if self.party_type == "Customer" else "Payable" - accounts = [d.name for d in frappe.get_all("Account", - filters={"account_type": account_type, "company": self.filters.company})] - - if accounts: - conditions.append("account in (%s)" % ','.join(['%s'] *len(accounts))) - values += accounts + if self.filters.party_account: + conditions.append("account =%s") + values.append(self.filters.party_account) + else: + # get GL with "receivable" or "payable" account_type + account_type = "Receivable" if self.party_type == "Customer" else "Payable" + accounts = [ + d.name + for d in frappe.get_all( + "Account", filters={"account_type": account_type, "company": self.filters.company} + ) + ] + if accounts: + conditions.append("account in (%s)" % ",".join(["%s"] * len(accounts))) + values += accounts def add_customer_filters(self, conditions, values): if self.filters.get("customer_group"): - conditions.append(self.get_hierarchical_filters('Customer Group', 'customer_group')) + conditions.append(self.get_hierarchical_filters("Customer Group", "customer_group")) if self.filters.get("territory"): - conditions.append(self.get_hierarchical_filters('Territory', 'territory')) + conditions.append(self.get_hierarchical_filters("Territory", "territory")) if self.filters.get("payment_terms_template"): conditions.append("party in (select name from tabCustomer where payment_terms=%s)") @@ -706,8 +811,10 @@ class ReceivablePayableReport(object): def add_supplier_filters(self, conditions, values): if self.filters.get("supplier_group"): - conditions.append("""party in (select name from tabSupplier - where supplier_group=%s)""") + conditions.append( + """party in (select name from tabSupplier + where supplier_group=%s)""" + ) values.append(self.filters.get("supplier_group")) if self.filters.get("payment_terms_template"): @@ -720,7 +827,8 @@ class ReceivablePayableReport(object): return """party in (select name from tabCustomer where exists(select name from `tab{doctype}` where lft >= {lft} and rgt <= {rgt} and name=tabCustomer.{key}))""".format( - doctype=doctype, lft=lft, rgt=rgt, key=key) + doctype=doctype, lft=lft, rgt=rgt, key=key + ) def add_accounting_dimensions_filters(self, conditions, values): accounting_dimensions = get_accounting_dimensions(as_list=False) @@ -728,9 +836,10 @@ class ReceivablePayableReport(object): if accounting_dimensions: for dimension in accounting_dimensions: if self.filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - self.filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - self.filters.get(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + self.filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, self.filters.get(dimension.fieldname) + ) conditions.append("{0} in %s".format(dimension.fieldname)) values.append(tuple(self.filters.get(dimension.fieldname))) @@ -740,123 +849,176 @@ class ReceivablePayableReport(object): def get_gle_balance_in_account_currency(self, gle): # get the balance of the GL (debit - credit) or reverse balance based on report type - return gle.get(self.dr_or_cr + '_in_account_currency') - self.get_reverse_balance_in_account_currency(gle) + return gle.get( + self.dr_or_cr + "_in_account_currency" + ) - self.get_reverse_balance_in_account_currency(gle) def get_reverse_balance_in_account_currency(self, gle): - return gle.get('debit_in_account_currency' if self.dr_or_cr=='credit' else 'credit_in_account_currency') + return gle.get( + "debit_in_account_currency" if self.dr_or_cr == "credit" else "credit_in_account_currency" + ) def get_reverse_balance(self, gle): # get "credit" balance if report type is "debit" and vice versa - return gle.get('debit' if self.dr_or_cr=='credit' else 'credit') + return gle.get("debit" if self.dr_or_cr == "credit" else "credit") def is_invoice(self, gle): - if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): + if gle.voucher_type in ("Sales Invoice", "Purchase Invoice"): return True def get_party_details(self, party): if not party in self.party_details: - if self.party_type == 'Customer': - self.party_details[party] = frappe.db.get_value('Customer', party, ['customer_name', - 'territory', 'customer_group', 'customer_primary_contact'], as_dict=True) + if self.party_type == "Customer": + self.party_details[party] = frappe.db.get_value( + "Customer", + party, + ["customer_name", "territory", "customer_group", "customer_primary_contact"], + as_dict=True, + ) else: - self.party_details[party] = frappe.db.get_value('Supplier', party, ['supplier_name', - 'supplier_group'], as_dict=True) + self.party_details[party] = frappe.db.get_value( + "Supplier", party, ["supplier_name", "supplier_group"], as_dict=True + ) return self.party_details[party] - def get_columns(self): self.columns = [] - self.add_column('Posting Date', fieldtype='Date') - self.add_column(label=_(self.party_type), fieldname='party', - fieldtype='Link', options=self.party_type, width=180) + self.add_column("Posting Date", fieldtype="Date") + self.add_column( + label=_(self.party_type), + fieldname="party", + fieldtype="Link", + options=self.party_type, + width=180, + ) + + self.add_column( + label="Receivable Account" if self.party_type == "Customer" else "Payable Account", + fieldname="party_account", + fieldtype="Link", + options="Account", + width=180, + ) if self.party_naming_by == "Naming Series": - self.add_column(_('{0} Name').format(self.party_type), - fieldname = scrub(self.party_type) + '_name', fieldtype='Data') + self.add_column( + _("{0} Name").format(self.party_type), + fieldname=scrub(self.party_type) + "_name", + fieldtype="Data", + ) - if self.party_type == 'Customer': - self.add_column(_("Customer Contact"), fieldname='customer_primary_contact', - fieldtype='Link', options='Contact') + if self.party_type == "Customer": + self.add_column( + _("Customer Contact"), + fieldname="customer_primary_contact", + fieldtype="Link", + options="Contact", + ) - self.add_column(label=_('Cost Center'), fieldname='cost_center', fieldtype='Data') - self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data') - self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link', - options='voucher_type', width=180) + self.add_column(label=_("Cost Center"), fieldname="cost_center", fieldtype="Data") + self.add_column(label=_("Voucher Type"), fieldname="voucher_type", fieldtype="Data") + self.add_column( + label=_("Voucher No"), + fieldname="voucher_no", + fieldtype="Dynamic Link", + options="voucher_type", + width=180, + ) if self.filters.show_remarks: - self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200), + self.add_column(label=_("Remarks"), fieldname="remarks", fieldtype="Text", width=200), - self.add_column(label='Due Date', fieldtype='Date') + self.add_column(label="Due Date", fieldtype="Date") if self.party_type == "Supplier": - self.add_column(label=_('Bill No'), fieldname='bill_no', fieldtype='Data') - self.add_column(label=_('Bill Date'), fieldname='bill_date', fieldtype='Date') + self.add_column(label=_("Bill No"), fieldname="bill_no", fieldtype="Data") + self.add_column(label=_("Bill Date"), fieldname="bill_date", fieldtype="Date") if self.filters.based_on_payment_terms: - self.add_column(label=_('Payment Term'), fieldname='payment_term', fieldtype='Data') - self.add_column(label=_('Invoice Grand Total'), fieldname='invoice_grand_total') + self.add_column(label=_("Payment Term"), fieldname="payment_term", fieldtype="Data") + self.add_column(label=_("Invoice Grand Total"), fieldname="invoice_grand_total") - self.add_column(_('Invoiced Amount'), fieldname='invoiced') - self.add_column(_('Paid Amount'), fieldname='paid') + self.add_column(_("Invoiced Amount"), fieldname="invoiced") + self.add_column(_("Paid Amount"), fieldname="paid") if self.party_type == "Customer": - self.add_column(_('Credit Note'), fieldname='credit_note') + self.add_column(_("Credit Note"), fieldname="credit_note") else: # note: fieldname is still `credit_note` - self.add_column(_('Debit Note'), fieldname='credit_note') - self.add_column(_('Outstanding Amount'), fieldname='outstanding') + self.add_column(_("Debit Note"), fieldname="credit_note") + self.add_column(_("Outstanding Amount"), fieldname="outstanding") self.setup_ageing_columns() - self.add_column(label=_('Currency'), fieldname='currency', fieldtype='Link', options='Currency', width=80) + self.add_column( + label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", width=80 + ) if self.filters.show_future_payments: - self.add_column(label=_('Future Payment Ref'), fieldname='future_ref', fieldtype='Data') - self.add_column(label=_('Future Payment Amount'), fieldname='future_amount') - self.add_column(label=_('Remaining Balance'), fieldname='remaining_balance') + self.add_column(label=_("Future Payment Ref"), fieldname="future_ref", fieldtype="Data") + self.add_column(label=_("Future Payment Amount"), fieldname="future_amount") + self.add_column(label=_("Remaining Balance"), fieldname="remaining_balance") - if self.filters.party_type == 'Customer': - self.add_column(label=_('Customer LPO'), fieldname='po_no', fieldtype='Data') + if self.filters.party_type == "Customer": + self.add_column(label=_("Customer LPO"), fieldname="po_no", fieldtype="Data") # comma separated list of linked delivery notes if self.filters.show_delivery_notes: - self.add_column(label=_('Delivery Notes'), fieldname='delivery_notes', fieldtype='Data') - self.add_column(label=_('Territory'), fieldname='territory', fieldtype='Link', - options='Territory') - self.add_column(label=_('Customer Group'), fieldname='customer_group', fieldtype='Link', - options='Customer Group') + self.add_column(label=_("Delivery Notes"), fieldname="delivery_notes", fieldtype="Data") + self.add_column( + label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory" + ) + self.add_column( + label=_("Customer Group"), + fieldname="customer_group", + fieldtype="Link", + options="Customer Group", + ) if self.filters.show_sales_person: - self.add_column(label=_('Sales Person'), fieldname='sales_person', fieldtype='Data') + self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") if self.filters.party_type == "Supplier": - self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', - options='Supplier Group') + self.add_column( + label=_("Supplier Group"), + fieldname="supplier_group", + fieldtype="Link", + options="Supplier Group", + ) - def add_column(self, label, fieldname=None, fieldtype='Currency', options=None, width=120): - if not fieldname: fieldname = scrub(label) - if fieldtype=='Currency': options='currency' - if fieldtype=='Date': width = 90 + def add_column(self, label, fieldname=None, fieldtype="Currency", options=None, width=120): + if not fieldname: + fieldname = scrub(label) + if fieldtype == "Currency": + options = "currency" + if fieldtype == "Date": + width = 90 - self.columns.append(dict( - label=label, - fieldname=fieldname, - fieldtype=fieldtype, - options=options, - width=width - )) + self.columns.append( + dict(label=label, fieldname=fieldname, fieldtype=fieldtype, options=options, width=width) + ) def setup_ageing_columns(self): # for charts self.ageing_column_labels = [] - self.add_column(label=_('Age (Days)'), fieldname='age', fieldtype='Int', width=80) + self.add_column(label=_("Age (Days)"), fieldname="age", fieldtype="Int", width=80) - for i, label in enumerate(["0-{range1}".format(range1=self.filters["range1"]), - "{range1}-{range2}".format(range1=cint(self.filters["range1"])+ 1, range2=self.filters["range2"]), - "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), - "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), - "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]): - self.add_column(label=label, fieldname='range' + str(i+1)) - self.ageing_column_labels.append(label) + for i, label in enumerate( + [ + "0-{range1}".format(range1=self.filters["range1"]), + "{range1}-{range2}".format( + range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"] + ), + "{range2}-{range3}".format( + range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"] + ), + "{range3}-{range4}".format( + range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"] + ), + "{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")), + ] + ): + self.add_column(label=label, fieldname="range" + str(i + 1)) + self.ageing_column_labels.append(label) def get_chart_data(self): rows = [] @@ -865,14 +1027,9 @@ class ReceivablePayableReport(object): if not cint(row.bold): values = [row.range1, row.range2, row.range3, row.range4, row.range5] precision = cint(frappe.db.get_default("float_precision")) or 2 - rows.append({ - 'values': [flt(val, precision) for val in values] - }) + rows.append({"values": [flt(val, precision) for val in values]}) self.chart = { - "data": { - 'labels': self.ageing_column_labels, - 'datasets': rows - }, - "type": 'percentage' + "data": {"labels": self.ageing_column_labels, "datasets": rows}, + "type": "percentage", } diff --git a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py index b5408bd4c8f..f38890e980c 100644 --- a/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/test_accounts_receivable.py @@ -1,4 +1,3 @@ - import unittest import frappe @@ -15,13 +14,13 @@ class TestAccountsReceivable(unittest.TestCase): frappe.db.sql("delete from `tabGL Entry` where company='_Test Company 2'") filters = { - 'company': '_Test Company 2', - 'based_on_payment_terms': 1, - 'report_date': today(), - 'range1': 30, - 'range2': 60, - 'range3': 90, - 'range4': 120 + "company": "_Test Company 2", + "based_on_payment_terms": 1, + "report_date": today(), + "range1": 30, + "range2": 60, + "range3": 90, + "range4": 120, } # check invoice grand total and invoiced column's value for 3 payment terms @@ -31,8 +30,8 @@ class TestAccountsReceivable(unittest.TestCase): expected_data = [[100, 30], [100, 50], [100, 20]] for i in range(3): - row = report[1][i-1] - self.assertEqual(expected_data[i-1], [row.invoice_grand_total, row.invoiced]) + row = report[1][i - 1] + self.assertEqual(expected_data[i - 1], [row.invoice_grand_total, row.invoiced]) # check invoice grand total, invoiced, paid and outstanding column's value after payment make_payment(name) @@ -41,41 +40,65 @@ class TestAccountsReceivable(unittest.TestCase): expected_data_after_payment = [[100, 50, 10, 40], [100, 20, 0, 20]] for i in range(2): - row = report[1][i-1] - self.assertEqual(expected_data_after_payment[i-1], - [row.invoice_grand_total, row.invoiced, row.paid, row.outstanding]) + row = report[1][i - 1] + self.assertEqual( + expected_data_after_payment[i - 1], + [row.invoice_grand_total, row.invoiced, row.paid, row.outstanding], + ) # check invoice grand total, invoiced, paid and outstanding column's value after credit note make_credit_note(name) report = execute(filters) - expected_data_after_credit_note = [100, 0, 0, 40, -40] + expected_data_after_credit_note = [100, 0, 0, 40, -40, "Debtors - _TC2"] row = report[1][0] - self.assertEqual(expected_data_after_credit_note, - [row.invoice_grand_total, row.invoiced, row.paid, row.credit_note, row.outstanding]) + self.assertEqual( + expected_data_after_credit_note, + [ + row.invoice_grand_total, + row.invoiced, + row.paid, + row.credit_note, + row.outstanding, + row.party_account, + ], + ) + def make_sales_invoice(): frappe.set_user("Administrator") - si = create_sales_invoice(company="_Test Company 2", - customer = '_Test Customer 2', - currency = 'EUR', - warehouse = 'Finished Goods - _TC2', - debit_to = 'Debtors - _TC2', - income_account = 'Sales - _TC2', - expense_account = 'Cost of Goods Sold - _TC2', - cost_center = 'Main - _TC2', - do_not_save=1) + si = create_sales_invoice( + company="_Test Company 2", + customer="_Test Customer 2", + currency="EUR", + warehouse="Finished Goods - _TC2", + debit_to="Debtors - _TC2", + income_account="Sales - _TC2", + expense_account="Cost of Goods Sold - _TC2", + cost_center="Main - _TC2", + do_not_save=1, + ) - si.append('payment_schedule', dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30)) - si.append('payment_schedule', dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50)) - si.append('payment_schedule', dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20)) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 30)), invoice_portion=30.00, payment_amount=30), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 60)), invoice_portion=50.00, payment_amount=50), + ) + si.append( + "payment_schedule", + dict(due_date=getdate(add_days(today(), 90)), invoice_portion=20.00, payment_amount=20), + ) si.submit() return si.name + def make_payment(docname): pe = get_payment_entry("Sales Invoice", docname, bank_account="Cash - _TC2", party_amount=40) pe.paid_from = "Debtors - _TC2" @@ -84,14 +107,16 @@ def make_payment(docname): def make_credit_note(docname): - create_sales_invoice(company="_Test Company 2", - customer = '_Test Customer 2', - currency = 'EUR', - qty = -1, - warehouse = 'Finished Goods - _TC2', - debit_to = 'Debtors - _TC2', - income_account = 'Sales - _TC2', - expense_account = 'Cost of Goods Sold - _TC2', - cost_center = 'Main - _TC2', - is_return = 1, - return_against = docname) + create_sales_invoice( + company="_Test Company 2", + customer="_Test Customer 2", + currency="EUR", + qty=-1, + warehouse="Finished Goods - _TC2", + debit_to="Debtors - _TC2", + income_account="Sales - _TC2", + expense_account="Cost of Goods Sold - _TC2", + cost_center="Main - _TC2", + is_return=1, + return_against=docname, + ) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 305cddb102a..715cd6476e8 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -117,6 +117,11 @@ frappe.query_reports["Accounts Receivable Summary"] = { "label": __("Show Future Payments"), "fieldtype": "Check", }, + { + "fieldname":"show_gl_balance", + "label": __("Show GL Balance"), + "fieldtype": "Check", + }, ], onload: function(report) { diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index a95bcf83ef7..85baa82e83f 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -4,7 +4,7 @@ import frappe from frappe import _, scrub -from frappe.utils import cint +from frappe.utils import cint, flt from six import iteritems from erpnext.accounts.party import get_partywise_advanced_payment_amount @@ -19,10 +19,13 @@ def execute(filters=None): return AccountsReceivableSummary(filters).run(args) + class AccountsReceivableSummary(ReceivablePayableReport): def run(self, args): - self.party_type = args.get('party_type') - self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + self.party_type = args.get("party_type") + self.party_naming_by = frappe.db.get_value( + args.get("naming_by")[0], None, args.get("naming_by")[1] + ) self.get_columns() self.get_data(args) return self.columns, self.data @@ -34,8 +37,18 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.get_party_total(args) - party_advance_amount = get_partywise_advanced_payment_amount(self.party_type, - self.filters.report_date, self.filters.show_future_payments, self.filters.company) or {} + party_advance_amount = ( + get_partywise_advanced_payment_amount( + self.party_type, + self.filters.report_date, + self.filters.show_future_payments, + self.filters.company, + ) + or {} + ) + + if self.filters.show_gl_balance: + gl_balance_map = get_gl_balance(self.filters.report_date) for party, party_dict in iteritems(self.party_total): if party_dict.outstanding == 0: @@ -45,7 +58,9 @@ class AccountsReceivableSummary(ReceivablePayableReport): row.party = party if self.party_naming_by == "Naming Series": - row.party_name = frappe.get_cached_value(self.party_type, party, scrub(self.party_type) + "_name") + row.party_name = frappe.get_cached_value( + self.party_type, party, scrub(self.party_type) + "_name" + ) row.update(party_dict) @@ -56,6 +71,10 @@ class AccountsReceivableSummary(ReceivablePayableReport): # but in summary report advance shown in separate column row.paid -= row.advance + if self.filters.show_gl_balance: + row.gl_balance = gl_balance_map.get(party) + row.diff = flt(row.outstanding) - flt(row.gl_balance) + self.data.append(row) def get_party_total(self, args): @@ -74,24 +93,29 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.set_party_details(d) def init_party_total(self, row): - self.party_total.setdefault(row.party, frappe._dict({ - "invoiced": 0.0, - "paid": 0.0, - "credit_note": 0.0, - "outstanding": 0.0, - "range1": 0.0, - "range2": 0.0, - "range3": 0.0, - "range4": 0.0, - "range5": 0.0, - "total_due": 0.0, - "sales_person": [] - })) + self.party_total.setdefault( + row.party, + frappe._dict( + { + "invoiced": 0.0, + "paid": 0.0, + "credit_note": 0.0, + "outstanding": 0.0, + "range1": 0.0, + "range2": 0.0, + "range3": 0.0, + "range4": 0.0, + "range5": 0.0, + "total_due": 0.0, + "sales_person": [], + } + ), + ) def set_party_details(self, row): self.party_total[row.party].currency = row.currency - for key in ('territory', 'customer_group', 'supplier_group'): + for key in ("territory", "customer_group", "supplier_group"): if row.get(key): self.party_total[row.party][key] = row.get(key) @@ -100,44 +124,84 @@ class AccountsReceivableSummary(ReceivablePayableReport): def get_columns(self): self.columns = [] - self.add_column(label=_(self.party_type), fieldname='party', - fieldtype='Link', options=self.party_type, width=180) + self.add_column( + label=_(self.party_type), + fieldname="party", + fieldtype="Link", + options=self.party_type, + width=180, + ) if self.party_naming_by == "Naming Series": - self.add_column(_('{0} Name').format(self.party_type), - fieldname = 'party_name', fieldtype='Data') + self.add_column(_("{0} Name").format(self.party_type), fieldname="party_name", fieldtype="Data") - credit_debit_label = "Credit Note" if self.party_type == 'Customer' else "Debit Note" + credit_debit_label = "Credit Note" if self.party_type == "Customer" else "Debit Note" - self.add_column(_('Advance Amount'), fieldname='advance') - self.add_column(_('Invoiced Amount'), fieldname='invoiced') - self.add_column(_('Paid Amount'), fieldname='paid') - self.add_column(_(credit_debit_label), fieldname='credit_note') - self.add_column(_('Outstanding Amount'), fieldname='outstanding') + self.add_column(_("Advance Amount"), fieldname="advance") + self.add_column(_("Invoiced Amount"), fieldname="invoiced") + self.add_column(_("Paid Amount"), fieldname="paid") + self.add_column(_(credit_debit_label), fieldname="credit_note") + self.add_column(_("Outstanding Amount"), fieldname="outstanding") + + if self.filters.show_gl_balance: + self.add_column(_("GL Balance"), fieldname="gl_balance") + self.add_column(_("Difference"), fieldname="diff") self.setup_ageing_columns() if self.party_type == "Customer": - self.add_column(label=_('Territory'), fieldname='territory', fieldtype='Link', - options='Territory') - self.add_column(label=_('Customer Group'), fieldname='customer_group', fieldtype='Link', - options='Customer Group') + self.add_column( + label=_("Territory"), fieldname="territory", fieldtype="Link", options="Territory" + ) + self.add_column( + label=_("Customer Group"), + fieldname="customer_group", + fieldtype="Link", + options="Customer Group", + ) if self.filters.show_sales_person: - self.add_column(label=_('Sales Person'), fieldname='sales_person', fieldtype='Data') + self.add_column(label=_("Sales Person"), fieldname="sales_person", fieldtype="Data") else: - self.add_column(label=_('Supplier Group'), fieldname='supplier_group', fieldtype='Link', - options='Supplier Group') + self.add_column( + label=_("Supplier Group"), + fieldname="supplier_group", + fieldtype="Link", + options="Supplier Group", + ) - self.add_column(label=_('Currency'), fieldname='currency', fieldtype='Link', - options='Currency', width=80) + self.add_column( + label=_("Currency"), fieldname="currency", fieldtype="Link", options="Currency", width=80 + ) def setup_ageing_columns(self): - for i, label in enumerate(["0-{range1}".format(range1=self.filters["range1"]), - "{range1}-{range2}".format(range1=cint(self.filters["range1"])+ 1, range2=self.filters["range2"]), - "{range2}-{range3}".format(range2=cint(self.filters["range2"])+ 1, range3=self.filters["range3"]), - "{range3}-{range4}".format(range3=cint(self.filters["range3"])+ 1, range4=self.filters["range4"]), - "{range4}-{above}".format(range4=cint(self.filters["range4"])+ 1, above=_("Above"))]): - self.add_column(label=label, fieldname='range' + str(i+1)) + for i, label in enumerate( + [ + "0-{range1}".format(range1=self.filters["range1"]), + "{range1}-{range2}".format( + range1=cint(self.filters["range1"]) + 1, range2=self.filters["range2"] + ), + "{range2}-{range3}".format( + range2=cint(self.filters["range2"]) + 1, range3=self.filters["range3"] + ), + "{range3}-{range4}".format( + range3=cint(self.filters["range3"]) + 1, range4=self.filters["range4"] + ), + "{range4}-{above}".format(range4=cint(self.filters["range4"]) + 1, above=_("Above")), + ] + ): + self.add_column(label=label, fieldname="range" + str(i + 1)) # Add column for total due amount - self.add_column(label="Total Amount Due", fieldname='total_due') + self.add_column(label="Total Amount Due", fieldname="total_due") + + +def get_gl_balance(report_date): + return frappe._dict( + frappe.db.get_all( + "GL Entry", + fields=["party", "sum(debit - credit)"], + filters={"posting_date": ("<=", report_date), "is_cancelled": 0}, + group_by="party", + as_list=1, + ) + ) diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py index 98f5b74eaac..57d80492ae0 100644 --- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py +++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py @@ -11,34 +11,44 @@ def execute(filters=None): columns, data = get_columns(), get_data(filters) return columns, data + def get_data(filters): data = [] - depreciation_accounts = frappe.db.sql_list(""" select name from tabAccount - where ifnull(account_type, '') = 'Depreciation' """) + depreciation_accounts = frappe.db.sql_list( + """ select name from tabAccount + where ifnull(account_type, '') = 'Depreciation' """ + ) - filters_data = [["company", "=", filters.get('company')], - ["posting_date", ">=", filters.get('from_date')], - ["posting_date", "<=", filters.get('to_date')], + filters_data = [ + ["company", "=", filters.get("company")], + ["posting_date", ">=", filters.get("from_date")], + ["posting_date", "<=", filters.get("to_date")], ["against_voucher_type", "=", "Asset"], - ["account", "in", depreciation_accounts]] + ["account", "in", depreciation_accounts], + ] if filters.get("asset"): filters_data.append(["against_voucher", "=", filters.get("asset")]) if filters.get("asset_category"): - assets = frappe.db.sql_list("""select name from tabAsset - where asset_category = %s and docstatus=1""", filters.get("asset_category")) + assets = frappe.db.sql_list( + """select name from tabAsset + where asset_category = %s and docstatus=1""", + filters.get("asset_category"), + ) filters_data.append(["against_voucher", "in", assets]) if filters.get("finance_book"): - filters_data.append(["finance_book", "in", ['', filters.get('finance_book')]]) + filters_data.append(["finance_book", "in", ["", filters.get("finance_book")]]) - gl_entries = frappe.get_all('GL Entry', - filters= filters_data, - fields = ["against_voucher", "debit_in_account_currency as debit", "voucher_no", "posting_date"], - order_by= "against_voucher, posting_date") + gl_entries = frappe.get_all( + "GL Entry", + filters=filters_data, + fields=["against_voucher", "debit_in_account_currency as debit", "voucher_no", "posting_date"], + order_by="against_voucher, posting_date", + ) if not gl_entries: return data @@ -55,29 +65,40 @@ def get_data(filters): asset_data.accumulated_depreciation_amount += d.debit row = frappe._dict(asset_data) - row.update({ - "depreciation_amount": d.debit, - "depreciation_date": d.posting_date, - "amount_after_depreciation": (flt(row.gross_purchase_amount) - - flt(row.accumulated_depreciation_amount)), - "depreciation_entry": d.voucher_no - }) + row.update( + { + "depreciation_amount": d.debit, + "depreciation_date": d.posting_date, + "amount_after_depreciation": ( + flt(row.gross_purchase_amount) - flt(row.accumulated_depreciation_amount) + ), + "depreciation_entry": d.voucher_no, + } + ) data.append(row) return data + def get_assets_details(assets): assets_details = {} - fields = ["name as asset", "gross_purchase_amount", - "asset_category", "status", "depreciation_method", "purchase_date"] + fields = [ + "name as asset", + "gross_purchase_amount", + "asset_category", + "status", + "depreciation_method", + "purchase_date", + ] - for d in frappe.get_all("Asset", fields = fields, filters = {'name': ('in', assets)}): + for d in frappe.get_all("Asset", fields=fields, filters={"name": ("in", assets)}): assets_details.setdefault(d.asset, d) return assets_details + def get_columns(): return [ { @@ -85,68 +106,58 @@ def get_columns(): "fieldname": "asset", "fieldtype": "Link", "options": "Asset", - "width": 120 + "width": 120, }, { "label": _("Depreciation Date"), "fieldname": "depreciation_date", "fieldtype": "Date", - "width": 120 + "width": 120, }, { "label": _("Purchase Amount"), "fieldname": "gross_purchase_amount", "fieldtype": "Currency", - "width": 120 + "width": 120, }, { "label": _("Depreciation Amount"), "fieldname": "depreciation_amount", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Accumulated Depreciation Amount"), "fieldname": "accumulated_depreciation_amount", "fieldtype": "Currency", - "width": 210 + "width": 210, }, { "label": _("Amount After Depreciation"), "fieldname": "amount_after_depreciation", "fieldtype": "Currency", - "width": 180 + "width": 180, }, { "label": _("Depreciation Entry"), "fieldname": "depreciation_entry", "fieldtype": "Link", "options": "Journal Entry", - "width": 140 + "width": 140, }, { "label": _("Asset Category"), "fieldname": "asset_category", "fieldtype": "Link", "options": "Asset Category", - "width": 120 - }, - { - "label": _("Current Status"), - "fieldname": "status", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"label": _("Current Status"), "fieldname": "status", "fieldtype": "Data", "width": 120}, { "label": _("Depreciation Method"), "fieldname": "depreciation_method", "fieldtype": "Data", - "width": 130 + "width": 130, }, - { - "label": _("Purchase Date"), - "fieldname": "purchase_date", - "fieldtype": "Date", - "width": 120 - } + {"label": _("Purchase Date"), "fieldname": "purchase_date", "fieldtype": "Date", "width": 120}, ] diff --git a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py index 0f9435f4a57..ad9b1ba58eb 100644 --- a/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py +++ b/erpnext/accounts/report/asset_depreciations_and_balances/asset_depreciations_and_balances.py @@ -24,18 +24,33 @@ def get_data(filters): # row.asset_category = asset_category row.update(asset_category) - row.cost_as_on_to_date = (flt(row.cost_as_on_from_date) + flt(row.cost_of_new_purchase) - - flt(row.cost_of_sold_asset) - flt(row.cost_of_scrapped_asset)) + row.cost_as_on_to_date = ( + flt(row.cost_as_on_from_date) + + flt(row.cost_of_new_purchase) + - flt(row.cost_of_sold_asset) + - flt(row.cost_of_scrapped_asset) + ) - row.update(next(asset for asset in assets if asset["asset_category"] == asset_category.get("asset_category", ""))) - row.accumulated_depreciation_as_on_to_date = (flt(row.accumulated_depreciation_as_on_from_date) + - flt(row.depreciation_amount_during_the_period) - flt(row.depreciation_eliminated_during_the_period)) + row.update( + next( + asset + for asset in assets + if asset["asset_category"] == asset_category.get("asset_category", "") + ) + ) + row.accumulated_depreciation_as_on_to_date = ( + flt(row.accumulated_depreciation_as_on_from_date) + + flt(row.depreciation_amount_during_the_period) + - flt(row.depreciation_eliminated_during_the_period) + ) - row.net_asset_value_as_on_from_date = (flt(row.cost_as_on_from_date) - - flt(row.accumulated_depreciation_as_on_from_date)) + row.net_asset_value_as_on_from_date = flt(row.cost_as_on_from_date) - flt( + row.accumulated_depreciation_as_on_from_date + ) - row.net_asset_value_as_on_to_date = (flt(row.cost_as_on_to_date) - - flt(row.accumulated_depreciation_as_on_to_date)) + row.net_asset_value_as_on_to_date = flt(row.cost_as_on_to_date) - flt( + row.accumulated_depreciation_as_on_to_date + ) data.append(row) @@ -43,7 +58,8 @@ def get_data(filters): def get_asset_categories(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT asset_category, ifnull(sum(case when purchase_date < %(from_date)s then case when ifnull(disposal_date, 0) = 0 or disposal_date >= %(from_date)s then @@ -84,10 +100,15 @@ def get_asset_categories(filters): from `tabAsset` where docstatus=1 and company=%(company)s and purchase_date <= %(to_date)s group by asset_category - """, {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1) + """, + {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + as_dict=1, + ) + def get_assets(filters): - return frappe.db.sql(""" + return frappe.db.sql( + """ SELECT results.asset_category, sum(results.accumulated_depreciation_as_on_from_date) as accumulated_depreciation_as_on_from_date, sum(results.depreciation_eliminated_during_the_period) as depreciation_eliminated_during_the_period, @@ -130,7 +151,10 @@ def get_assets(filters): where a.docstatus=1 and a.company=%(company)s and a.purchase_date <= %(to_date)s group by a.asset_category) as results group by results.asset_category - """, {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, as_dict=1) + """, + {"to_date": filters.to_date, "from_date": filters.from_date, "company": filters.company}, + as_dict=1, + ) def get_columns(filters): @@ -140,72 +164,72 @@ def get_columns(filters): "fieldname": "asset_category", "fieldtype": "Link", "options": "Asset Category", - "width": 120 + "width": 120, }, { "label": _("Cost as on") + " " + formatdate(filters.day_before_from_date), "fieldname": "cost_as_on_from_date", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Cost of New Purchase"), "fieldname": "cost_of_new_purchase", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Cost of Sold Asset"), "fieldname": "cost_of_sold_asset", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Cost of Scrapped Asset"), "fieldname": "cost_of_scrapped_asset", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Cost as on") + " " + formatdate(filters.to_date), "fieldname": "cost_as_on_to_date", "fieldtype": "Currency", - "width": 140 + "width": 140, }, { "label": _("Accumulated Depreciation as on") + " " + formatdate(filters.day_before_from_date), "fieldname": "accumulated_depreciation_as_on_from_date", "fieldtype": "Currency", - "width": 270 + "width": 270, }, { "label": _("Depreciation Amount during the period"), "fieldname": "depreciation_amount_during_the_period", "fieldtype": "Currency", - "width": 240 + "width": 240, }, { "label": _("Depreciation Eliminated due to disposal of assets"), "fieldname": "depreciation_eliminated_during_the_period", "fieldtype": "Currency", - "width": 300 + "width": 300, }, { "label": _("Accumulated Depreciation as on") + " " + formatdate(filters.to_date), "fieldname": "accumulated_depreciation_as_on_to_date", "fieldtype": "Currency", - "width": 270 + "width": 270, }, { "label": _("Net Asset value as on") + " " + formatdate(filters.day_before_from_date), "fieldname": "net_asset_value_as_on_from_date", "fieldtype": "Currency", - "width": 200 + "width": 200, }, { "label": _("Net Asset value as on") + " " + formatdate(filters.to_date), "fieldname": "net_asset_value_as_on_to_date", "fieldtype": "Currency", - "width": 200 - } + "width": 200, + }, ] diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py index dc1f7aae42e..7b1e9793266 100644 --- a/erpnext/accounts/report/balance_sheet/balance_sheet.py +++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py @@ -15,26 +15,53 @@ from erpnext.accounts.report.financial_statements import ( def execute(filters=None): - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, - filters.period_start_date, filters.period_end_date, filters.filter_based_on, - filters.periodicity, company=filters.company) + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) - currency = filters.presentation_currency or frappe.get_cached_value('Company', filters.company, "default_currency") + currency = filters.presentation_currency or frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) - asset = get_data(filters.company, "Asset", "Debit", period_list, - only_current_fiscal_year=False, filters=filters, - accumulated_values=filters.accumulated_values) + asset = get_data( + filters.company, + "Asset", + "Debit", + period_list, + only_current_fiscal_year=False, + filters=filters, + accumulated_values=filters.accumulated_values, + ) - liability = get_data(filters.company, "Liability", "Credit", period_list, - only_current_fiscal_year=False, filters=filters, - accumulated_values=filters.accumulated_values) + liability = get_data( + filters.company, + "Liability", + "Credit", + period_list, + only_current_fiscal_year=False, + filters=filters, + accumulated_values=filters.accumulated_values, + ) - equity = get_data(filters.company, "Equity", "Credit", period_list, - only_current_fiscal_year=False, filters=filters, - accumulated_values=filters.accumulated_values) + equity = get_data( + filters.company, + "Equity", + "Credit", + period_list, + only_current_fiscal_year=False, + filters=filters, + accumulated_values=filters.accumulated_values, + ) - provisional_profit_loss, total_credit = get_provisional_profit_loss(asset, liability, equity, - period_list, filters.company, currency) + provisional_profit_loss, total_credit = get_provisional_profit_loss( + asset, liability, equity, period_list, filters.company, currency + ) message, opening_balance = check_opening_balance(asset, liability, equity) @@ -42,19 +69,19 @@ def execute(filters=None): data.extend(asset or []) data.extend(liability or []) data.extend(equity or []) - if opening_balance and round(opening_balance,2) !=0: - unclosed ={ + if opening_balance and round(opening_balance, 2) != 0: + unclosed = { "account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", "account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", "warn_if_negative": True, - "currency": currency + "currency": currency, } for period in period_list: unclosed[period.key] = opening_balance if provisional_profit_loss: provisional_profit_loss[period.key] = provisional_profit_loss[period.key] - opening_balance - unclosed["total"]=opening_balance + unclosed["total"] = opening_balance data.append(unclosed) if provisional_profit_loss: @@ -62,26 +89,32 @@ def execute(filters=None): if total_credit: data.append(total_credit) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, company=filters.company) + columns = get_columns( + filters.periodicity, period_list, filters.accumulated_values, company=filters.company + ) chart = get_chart_data(filters, columns, asset, liability, equity) - report_summary = get_report_summary(period_list, asset, liability, equity, provisional_profit_loss, - total_credit, currency, filters) + report_summary = get_report_summary( + period_list, asset, liability, equity, provisional_profit_loss, total_credit, currency, filters + ) return columns, data, message, chart, report_summary -def get_provisional_profit_loss(asset, liability, equity, period_list, company, currency=None, consolidated=False): + +def get_provisional_profit_loss( + asset, liability, equity, period_list, company, currency=None, consolidated=False +): provisional_profit_loss = {} total_row = {} if asset and (liability or equity): - total = total_row_total=0 - currency = currency or frappe.get_cached_value('Company', company, "default_currency") + total = total_row_total = 0 + currency = currency or frappe.get_cached_value("Company", company, "default_currency") total_row = { "account_name": "'" + _("Total (Credit)") + "'", "account": "'" + _("Total (Credit)") + "'", "warn_if_negative": True, - "currency": currency + "currency": currency, } has_value = False @@ -106,41 +139,54 @@ def get_provisional_profit_loss(asset, liability, equity, period_list, company, total_row["total"] = total_row_total if has_value: - provisional_profit_loss.update({ - "account_name": "'" + _("Provisional Profit / Loss (Credit)") + "'", - "account": "'" + _("Provisional Profit / Loss (Credit)") + "'", - "warn_if_negative": True, - "currency": currency - }) + provisional_profit_loss.update( + { + "account_name": "'" + _("Provisional Profit / Loss (Credit)") + "'", + "account": "'" + _("Provisional Profit / Loss (Credit)") + "'", + "warn_if_negative": True, + "currency": currency, + } + ) return provisional_profit_loss, total_row + def check_opening_balance(asset, liability, equity): # Check if previous year balance sheet closed opening_balance = 0 float_precision = cint(frappe.db.get_default("float_precision")) or 2 if asset: - opening_balance = flt(asset[0].get("opening_balance", 0), float_precision) + opening_balance = flt(asset[-1].get("opening_balance", 0), float_precision) if liability: - opening_balance -= flt(liability[0].get("opening_balance", 0), float_precision) + opening_balance -= flt(liability[-1].get("opening_balance", 0), float_precision) if equity: - opening_balance -= flt(equity[0].get("opening_balance", 0), float_precision) + opening_balance -= flt(equity[-1].get("opening_balance", 0), float_precision) opening_balance = flt(opening_balance, float_precision) if opening_balance: - return _("Previous Financial Year is not closed"),opening_balance - return None,None + return _("Previous Financial Year is not closed"), opening_balance + return None, None -def get_report_summary(period_list, asset, liability, equity, provisional_profit_loss, total_credit, currency, - filters, consolidated=False): + +def get_report_summary( + period_list, + asset, + liability, + equity, + provisional_profit_loss, + total_credit, + currency, + filters, + consolidated=False, +): net_asset, net_liability, net_equity, net_provisional_profit_loss = 0.0, 0.0, 0.0, 0.0 - if filters.get('accumulated_values'): + if filters.get("accumulated_values"): period_list = [period_list[-1]] # from consolidated financial statement - if filters.get('accumulated_in_group_company'): + if filters.get("accumulated_in_group_company"): period_list = get_filtered_list_for_consolidated_report(filters, period_list) for period in period_list: @@ -155,33 +201,24 @@ def get_report_summary(period_list, asset, liability, equity, provisional_profit net_provisional_profit_loss += provisional_profit_loss.get(key) return [ - { - "value": net_asset, - "label": "Total Asset", - "datatype": "Currency", - "currency": currency - }, + {"value": net_asset, "label": "Total Asset", "datatype": "Currency", "currency": currency}, { "value": net_liability, "label": "Total Liability", "datatype": "Currency", - "currency": currency - }, - { - "value": net_equity, - "label": "Total Equity", - "datatype": "Currency", - "currency": currency + "currency": currency, }, + {"value": net_equity, "label": "Total Equity", "datatype": "Currency", "currency": currency}, { "value": net_provisional_profit_loss, "label": "Provisional Profit / Loss (Credit)", "indicator": "Green" if net_provisional_profit_loss > 0 else "Red", "datatype": "Currency", - "currency": currency - } + "currency": currency, + }, ] + def get_chart_data(filters, columns, asset, liability, equity): labels = [d.get("label") for d in columns[2:]] @@ -197,18 +234,13 @@ def get_chart_data(filters, columns, asset, liability, equity): datasets = [] if asset_data: - datasets.append({'name': _('Assets'), 'values': asset_data}) + datasets.append({"name": _("Assets"), "values": asset_data}) if liability_data: - datasets.append({'name': _('Liabilities'), 'values': liability_data}) + datasets.append({"name": _("Liabilities"), "values": liability_data}) if equity_data: - datasets.append({'name': _('Equity'), 'values': equity_data}) + datasets.append({"name": _("Equity"), "values": equity_data}) - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - } - } + chart = {"data": {"labels": labels, "datasets": datasets}} if not filters.accumulated_values: chart["type"] = "bar" diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py index b456e89f344..9d2deea523b 100644 --- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py +++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py @@ -8,86 +8,88 @@ from frappe.utils import getdate, nowdate def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() data = get_entries(filters) return columns, data + def get_columns(): - columns = [{ + columns = [ + { "label": _("Payment Document Type"), "fieldname": "payment_document_type", "fieldtype": "Link", "options": "Doctype", - "width": 130 + "width": 130, }, { "label": _("Payment Entry"), "fieldname": "payment_entry", "fieldtype": "Dynamic Link", "options": "payment_document_type", - "width": 140 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 100 - }, - { - "label": _("Cheque/Reference No"), - "fieldname": "cheque_no", - "width": 120 - }, - { - "label": _("Clearance Date"), - "fieldname": "clearance_date", - "fieldtype": "Date", - "width": 100 + "width": 140, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, + {"label": _("Cheque/Reference No"), "fieldname": "cheque_no", "width": 120}, + {"label": _("Clearance Date"), "fieldname": "clearance_date", "fieldtype": "Date", "width": 100}, { "label": _("Against Account"), "fieldname": "against", "fieldtype": "Link", "options": "Account", - "width": 170 + "width": 170, }, - { - "label": _("Amount"), - "fieldname": "amount", - "width": 120 - }] + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 120}, + ] return columns + def get_conditions(filters): conditions = "" - if filters.get("from_date"): conditions += " and posting_date>=%(from_date)s" - if filters.get("to_date"): conditions += " and posting_date<=%(to_date)s" + if filters.get("from_date"): + conditions += " and posting_date>=%(from_date)s" + if filters.get("to_date"): + conditions += " and posting_date<=%(to_date)s" return conditions + def get_entries(filters): conditions = get_conditions(filters) - journal_entries = frappe.db.sql("""SELECT + journal_entries = frappe.db.sql( + """SELECT "Journal Entry", jv.name, jv.posting_date, jv.cheque_no, jv.clearance_date, jvd.against_account, jvd.debit - jvd.credit FROM `tabJournal Entry Account` jvd, `tabJournal Entry` jv WHERE jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s {0} - order by posting_date DESC, jv.name DESC""".format(conditions), filters, as_list=1) + order by posting_date DESC, jv.name DESC""".format( + conditions + ), + filters, + as_list=1, + ) - payment_entries = frappe.db.sql("""SELECT + payment_entries = frappe.db.sql( + """SELECT "Payment Entry", name, posting_date, reference_no, clearance_date, party, if(paid_from=%(account)s, paid_amount * -1, received_amount) FROM `tabPayment Entry` WHERE docstatus=1 and (paid_from = %(account)s or paid_to = %(account)s) {0} - order by posting_date DESC, name DESC""".format(conditions), filters, as_list=1) + order by posting_date DESC, name DESC""".format( + conditions + ), + filters, + as_list=1, + ) return sorted(journal_entries + payment_entries, key=lambda k: k[2] or getdate(nowdate())) diff --git a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py index 6c401fb8f3b..c41d0d10ffe 100644 --- a/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py +++ b/erpnext/accounts/report/bank_reconciliation_statement/bank_reconciliation_statement.py @@ -4,121 +4,134 @@ import frappe from frappe import _ -from frappe.utils import flt, getdate, nowdate +from frappe.query_builder.custom import ConstantColumn +from frappe.query_builder.functions import Sum +from frappe.utils import flt, getdate +from pypika import CustomFunction + +from erpnext.accounts.utils import get_balance_on def execute(filters=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns() - if not filters.get("account"): return columns, [] + if not filters.get("account"): + return columns, [] account_currency = frappe.db.get_value("Account", filters.account, "account_currency") data = get_entries(filters) - from erpnext.accounts.utils import get_balance_on balance_as_per_system = get_balance_on(filters["account"], filters["report_date"]) - total_debit, total_credit = 0,0 + total_debit, total_credit = 0, 0 for d in data: total_debit += flt(d.debit) total_credit += flt(d.credit) amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters) - bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \ + bank_bal = ( + flt(balance_as_per_system) + - flt(total_debit) + + flt(total_credit) + amounts_not_reflected_in_system + ) data += [ - get_balance_row(_("Bank Statement balance as per General Ledger"), balance_as_per_system, account_currency), + get_balance_row( + _("Bank Statement balance as per General Ledger"), balance_as_per_system, account_currency + ), {}, { "payment_entry": _("Outstanding Cheques and Deposits to clear"), "debit": total_debit, "credit": total_credit, - "account_currency": account_currency + "account_currency": account_currency, }, - get_balance_row(_("Cheques and Deposits incorrectly cleared"), amounts_not_reflected_in_system, - account_currency), + get_balance_row( + _("Cheques and Deposits incorrectly cleared"), amounts_not_reflected_in_system, account_currency + ), {}, - get_balance_row(_("Calculated Bank Statement balance"), bank_bal, account_currency) + get_balance_row(_("Calculated Bank Statement balance"), bank_bal, account_currency), ] return columns, data + def get_columns(): return [ - { - "fieldname": "posting_date", - "label": _("Posting Date"), - "fieldtype": "Date", - "width": 90 - }, + {"fieldname": "posting_date", "label": _("Posting Date"), "fieldtype": "Date", "width": 90}, { "fieldname": "payment_document", "label": _("Payment Document Type"), "fieldtype": "Data", - "width": 220 + "width": 220, }, { "fieldname": "payment_entry", "label": _("Payment Document"), "fieldtype": "Dynamic Link", "options": "payment_document", - "width": 220 + "width": 220, }, { "fieldname": "debit", "label": _("Debit"), "fieldtype": "Currency", "options": "account_currency", - "width": 120 + "width": 120, }, { "fieldname": "credit", "label": _("Credit"), "fieldtype": "Currency", "options": "account_currency", - "width": 120 + "width": 120, }, { "fieldname": "against_account", "label": _("Against Account"), "fieldtype": "Link", "options": "Account", - "width": 200 - }, - { - "fieldname": "reference_no", - "label": _("Reference"), - "fieldtype": "Data", - "width": 100 - }, - { - "fieldname": "ref_date", - "label": _("Ref Date"), - "fieldtype": "Date", - "width": 110 - }, - { - "fieldname": "clearance_date", - "label": _("Clearance Date"), - "fieldtype": "Date", - "width": 110 + "width": 200, }, + {"fieldname": "reference_no", "label": _("Reference"), "fieldtype": "Data", "width": 100}, + {"fieldname": "ref_date", "label": _("Ref Date"), "fieldtype": "Date", "width": 110}, + {"fieldname": "clearance_date", "label": _("Clearance Date"), "fieldtype": "Date", "width": 110}, { "fieldname": "account_currency", "label": _("Currency"), "fieldtype": "Link", "options": "Currency", - "width": 100 - } + "width": 100, + }, ] + def get_entries(filters): - journal_entries = frappe.db.sql(""" + journal_entries = get_journal_entries(filters) + + payment_entries = get_payment_entries(filters) + + loan_entries = get_loan_entries(filters) + + pos_entries = [] + if filters.include_pos_transactions: + pos_entries = get_pos_entries(filters) + + return sorted( + list(payment_entries) + list(journal_entries + list(pos_entries) + list(loan_entries)), + key=lambda k: getdate(k["posting_date"]), + ) + + +def get_journal_entries(filters): + return frappe.db.sql( + """ select "Journal Entry" as payment_document, jv.posting_date, jv.name as payment_entry, jvd.debit_in_account_currency as debit, jvd.credit_in_account_currency as credit, jvd.against_account, @@ -128,9 +141,15 @@ def get_entries(filters): where jvd.parent = jv.name and jv.docstatus=1 and jvd.account = %(account)s and jv.posting_date <= %(report_date)s and ifnull(jv.clearance_date, '4000-01-01') > %(report_date)s - and ifnull(jv.is_opening, 'No') = 'No'""", filters, as_dict=1) + and ifnull(jv.is_opening, 'No') = 'No'""", + filters, + as_dict=1, + ) - payment_entries = frappe.db.sql(""" + +def get_payment_entries(filters): + return frappe.db.sql( + """ select "Payment Entry" as payment_document, name as payment_entry, reference_no, reference_date as ref_date, @@ -143,11 +162,15 @@ def get_entries(filters): (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1 and posting_date <= %(report_date)s and ifnull(clearance_date, '4000-01-01') > %(report_date)s - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) - pos_entries = [] - if filters.include_pos_transactions: - pos_entries = frappe.db.sql(""" + +def get_pos_entries(filters): + return frappe.db.sql( + """ select "Sales Invoice Payment" as payment_document, sip.name as payment_entry, sip.amount as debit, si.posting_date, si.debit_to as against_account, sip.clearance_date, @@ -159,30 +182,112 @@ def get_entries(filters): ifnull(sip.clearance_date, '4000-01-01') > %(report_date)s order by si.posting_date ASC, si.name DESC - """, filters, as_dict=1) + """, + filters, + as_dict=1, + ) + + +def get_loan_entries(filters): + loan_docs = [] + for doctype in ["Loan Disbursement", "Loan Repayment"]: + loan_doc = frappe.qb.DocType(doctype) + ifnull = CustomFunction("IFNULL", ["value", "default"]) + + if doctype == "Loan Disbursement": + amount_field = (loan_doc.disbursed_amount).as_("credit") + posting_date = (loan_doc.disbursement_date).as_("posting_date") + account = loan_doc.disbursement_account + salary_condition = loan_doc.docstatus == 1 + else: + amount_field = (loan_doc.amount_paid).as_("debit") + posting_date = (loan_doc.posting_date).as_("posting_date") + account = loan_doc.payment_account + salary_condition = loan_doc.repay_from_salary == 0 + + query = ( + frappe.qb.from_(loan_doc) + .select( + ConstantColumn(doctype).as_("payment_document"), + (loan_doc.name).as_("payment_entry"), + (loan_doc.reference_number).as_("reference_no"), + (loan_doc.reference_date).as_("ref_date"), + amount_field, + posting_date, + ) + .where(loan_doc.docstatus == 1) + .where(salary_condition) + .where(account == filters.get("account")) + .where(posting_date <= getdate(filters.get("report_date"))) + .where(ifnull(loan_doc.clearance_date, "4000-01-01") > getdate(filters.get("report_date"))) + ) + + entries = query.run(as_dict=1) + loan_docs.extend(entries) + + return loan_docs - return sorted(list(payment_entries)+list(journal_entries+list(pos_entries)), - key=lambda k: k['posting_date'] or getdate(nowdate())) def get_amounts_not_reflected_in_system(filters): - je_amount = frappe.db.sql(""" + je_amount = frappe.db.sql( + """ select sum(jvd.debit_in_account_currency - jvd.credit_in_account_currency) from `tabJournal Entry Account` jvd, `tabJournal Entry` jv where jvd.parent = jv.name and jv.docstatus=1 and jvd.account=%(account)s and jv.posting_date > %(report_date)s and jv.clearance_date <= %(report_date)s - and ifnull(jv.is_opening, 'No') = 'No' """, filters) + and ifnull(jv.is_opening, 'No') = 'No' """, + filters, + ) je_amount = flt(je_amount[0][0]) if je_amount else 0.0 - pe_amount = frappe.db.sql(""" + pe_amount = frappe.db.sql( + """ select sum(if(paid_from=%(account)s, paid_amount, received_amount)) from `tabPayment Entry` where (paid_from=%(account)s or paid_to=%(account)s) and docstatus=1 - and posting_date > %(report_date)s and clearance_date <= %(report_date)s""", filters) + and posting_date > %(report_date)s and clearance_date <= %(report_date)s""", + filters, + ) pe_amount = flt(pe_amount[0][0]) if pe_amount else 0.0 - return je_amount + pe_amount + loan_amount = get_loan_amount(filters) + + return je_amount + pe_amount + loan_amount + + +def get_loan_amount(filters): + total_amount = 0 + for doctype in ["Loan Disbursement", "Loan Repayment"]: + loan_doc = frappe.qb.DocType(doctype) + ifnull = CustomFunction("IFNULL", ["value", "default"]) + + if doctype == "Loan Disbursement": + amount_field = Sum(loan_doc.disbursed_amount) + posting_date = (loan_doc.disbursement_date).as_("posting_date") + account = loan_doc.disbursement_account + salary_condition = loan_doc.docstatus == 1 + else: + amount_field = Sum(loan_doc.amount_paid) + posting_date = (loan_doc.posting_date).as_("posting_date") + account = loan_doc.payment_account + salary_condition = loan_doc.repay_from_salary == 0 + amount = ( + frappe.qb.from_(loan_doc) + .select(amount_field) + .where(loan_doc.docstatus == 1) + .where(salary_condition) + .where(account == filters.get("account")) + .where(posting_date > getdate(filters.get("report_date"))) + .where(ifnull(loan_doc.clearance_date, "4000-01-01") <= getdate(filters.get("report_date"))) + .run()[0][0] + ) + + total_amount += flt(amount) + + return total_amount + def get_balance_row(label, amount, account_currency): if amount > 0: @@ -190,12 +295,12 @@ def get_balance_row(label, amount, account_currency): "payment_entry": label, "debit": amount, "credit": 0, - "account_currency": account_currency + "account_currency": account_currency, } else: return { "payment_entry": label, "debit": 0, "credit": abs(amount), - "account_currency": account_currency + "account_currency": account_currency, } diff --git a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py index 1d7463c8920..62bee82590b 100644 --- a/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py +++ b/erpnext/accounts/report/billed_items_to_be_received/billed_items_to_be_received.py @@ -12,97 +12,70 @@ def execute(filters=None): return columns, data + def get_data(report_filters): filters = get_report_filters(report_filters) fields = get_report_fields() - return frappe.get_all('Purchase Invoice', - fields= fields, filters=filters) + return frappe.get_all("Purchase Invoice", fields=fields, filters=filters) + def get_report_filters(report_filters): - filters = [['Purchase Invoice','company','=',report_filters.get('company')], - ['Purchase Invoice','posting_date','<=',report_filters.get('posting_date')], ['Purchase Invoice','docstatus','=',1], - ['Purchase Invoice','per_received','<',100], ['Purchase Invoice','update_stock','=',0]] + filters = [ + ["Purchase Invoice", "company", "=", report_filters.get("company")], + ["Purchase Invoice", "posting_date", "<=", report_filters.get("posting_date")], + ["Purchase Invoice", "docstatus", "=", 1], + ["Purchase Invoice", "per_received", "<", 100], + ["Purchase Invoice", "update_stock", "=", 0], + ] - if report_filters.get('purchase_invoice'): - filters.append(['Purchase Invoice','per_received','in',[report_filters.get('purchase_invoice')]]) + if report_filters.get("purchase_invoice"): + filters.append( + ["Purchase Invoice", "per_received", "in", [report_filters.get("purchase_invoice")]] + ) return filters + def get_report_fields(): fields = [] - for p_field in ['name', 'supplier', 'company', 'posting_date', 'currency']: - fields.append('`tabPurchase Invoice`.`{}`'.format(p_field)) + for p_field in ["name", "supplier", "company", "posting_date", "currency"]: + fields.append("`tabPurchase Invoice`.`{}`".format(p_field)) - for c_field in ['item_code', 'item_name', 'uom', 'qty', 'received_qty', 'rate', 'amount']: - fields.append('`tabPurchase Invoice Item`.`{}`'.format(c_field)) + for c_field in ["item_code", "item_name", "uom", "qty", "received_qty", "rate", "amount"]: + fields.append("`tabPurchase Invoice Item`.`{}`".format(c_field)) return fields + def get_columns(): return [ { - 'label': _('Purchase Invoice'), - 'fieldname': 'name', - 'fieldtype': 'Link', - 'options': 'Purchase Invoice', - 'width': 170 + "label": _("Purchase Invoice"), + "fieldname": "name", + "fieldtype": "Link", + "options": "Purchase Invoice", + "width": 170, }, { - 'label': _('Supplier'), - 'fieldname': 'supplier', - 'fieldtype': 'Link', - 'options': 'Supplier', - 'width': 120 + "label": _("Supplier"), + "fieldname": "supplier", + "fieldtype": "Link", + "options": "Supplier", + "width": 120, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 100}, { - 'label': _('Posting Date'), - 'fieldname': 'posting_date', - 'fieldtype': 'Date', - 'width': 100 + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, }, - { - 'label': _('Item Code'), - 'fieldname': 'item_code', - 'fieldtype': 'Link', - 'options': 'Item', - 'width': 100 - }, - { - 'label': _('Item Name'), - 'fieldname': 'item_name', - 'fieldtype': 'Data', - 'width': 100 - }, - { - 'label': _('UOM'), - 'fieldname': 'uom', - 'fieldtype': 'Link', - 'options': 'UOM', - 'width': 100 - }, - { - 'label': _('Invoiced Qty'), - 'fieldname': 'qty', - 'fieldtype': 'Float', - 'width': 100 - }, - { - 'label': _('Received Qty'), - 'fieldname': 'received_qty', - 'fieldtype': 'Float', - 'width': 100 - }, - { - 'label': _('Rate'), - 'fieldname': 'rate', - 'fieldtype': 'Currency', - 'width': 100 - }, - { - 'label': _('Amount'), - 'fieldname': 'amount', - 'fieldtype': 'Currency', - 'width': 100 - } + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 100}, + {"label": _("UOM"), "fieldname": "uom", "fieldtype": "Link", "options": "UOM", "width": 100}, + {"label": _("Invoiced Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 100}, + {"label": _("Received Qty"), "fieldname": "received_qty", "fieldtype": "Float", "width": 100}, + {"label": _("Rate"), "fieldname": "rate", "fieldtype": "Currency", "width": 100}, + {"label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 100}, ] diff --git a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py index ead67766785..95159476f2d 100644 --- a/erpnext/accounts/report/budget_variance_report/budget_variance_report.py +++ b/erpnext/accounts/report/budget_variance_report/budget_variance_report.py @@ -31,22 +31,28 @@ def execute(filters=None): if dimension_items: data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, 0) else: - DCC_allocation = frappe.db.sql('''SELECT parent, sum(percentage_allocation) as percentage_allocation + DCC_allocation = frappe.db.sql( + """SELECT parent, sum(percentage_allocation) as percentage_allocation FROM `tabDistributed Cost Center` WHERE cost_center IN %(dimension)s AND parent NOT IN %(dimension)s - GROUP BY parent''',{'dimension':[dimension]}) + GROUP BY parent""", + {"dimension": [dimension]}, + ) if DCC_allocation: - filters['budget_against_filter'] = [DCC_allocation[0][0]] + filters["budget_against_filter"] = [DCC_allocation[0][0]] ddc_cam_map = get_dimension_account_month_map(filters) dimension_items = ddc_cam_map.get(DCC_allocation[0][0]) if dimension_items: - data = get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1]) + data = get_final_data( + dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation[0][1] + ) chart = get_chart_data(filters, columns, data) return columns, data, None, chart + def get_final_data(dimension, dimension_items, filters, period_month_ranges, data, DCC_allocation): for account, monthwise_data in iteritems(dimension_items): row = [dimension, account] @@ -66,16 +72,16 @@ def get_final_data(dimension, dimension_items, filters, period_month_ranges, dat period_data[0] += last_total if DCC_allocation: - period_data[0] = period_data[0]*(DCC_allocation/100) - period_data[1] = period_data[1]*(DCC_allocation/100) + period_data[0] = period_data[0] * (DCC_allocation / 100) + period_data[1] = period_data[1] * (DCC_allocation / 100) - if(filters.get("show_cumulative")): + if filters.get("show_cumulative"): last_total = period_data[0] - period_data[1] period_data[2] = period_data[0] - period_data[1] row += period_data totals[2] = totals[0] - totals[1] - if filters["period"] != "Yearly" : + if filters["period"] != "Yearly": row += totals data.append(row) @@ -85,19 +91,19 @@ def get_final_data(dimension, dimension_items, filters, period_month_ranges, dat def get_columns(filters): columns = [ { - 'label': _(filters.get("budget_against")), - 'fieldtype': 'Link', - 'fieldname': 'budget_against', - 'options': filters.get('budget_against'), - 'width': 150 + "label": _(filters.get("budget_against")), + "fieldtype": "Link", + "fieldname": "budget_against", + "options": filters.get("budget_against"), + "width": 150, }, { - 'label': _('Account'), - 'fieldname': 'Account', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 150 - } + "label": _("Account"), + "fieldname": "Account", + "fieldtype": "Link", + "options": "Account", + "width": 150, + }, ] group_months = False if filters["period"] == "Monthly" else True @@ -110,45 +116,34 @@ def get_columns(filters): labels = [ _("Budget") + " " + str(year[0]), _("Actual ") + " " + str(year[0]), - _("Variance ") + " " + str(year[0]) + _("Variance ") + " " + str(year[0]), ] for label in labels: - columns.append({ - 'label': label, - 'fieldtype': 'Float', - 'fieldname': frappe.scrub(label), - 'width': 150 - }) + columns.append( + {"label": label, "fieldtype": "Float", "fieldname": frappe.scrub(label), "width": 150} + ) else: for label in [ _("Budget") + " (%s)" + " " + str(year[0]), _("Actual") + " (%s)" + " " + str(year[0]), - _("Variance") + " (%s)" + " " + str(year[0]) + _("Variance") + " (%s)" + " " + str(year[0]), ]: if group_months: label = label % ( - formatdate(from_date, format_string="MMM") - + "-" - + formatdate(to_date, format_string="MMM") + formatdate(from_date, format_string="MMM") + "-" + formatdate(to_date, format_string="MMM") ) else: label = label % formatdate(from_date, format_string="MMM") - columns.append({ - 'label': label, - 'fieldtype': 'Float', - 'fieldname': frappe.scrub(label), - 'width': 150 - }) + columns.append( + {"label": label, "fieldtype": "Float", "fieldname": frappe.scrub(label), "width": 150} + ) if filters["period"] != "Yearly": for label in [_("Total Budget"), _("Total Actual"), _("Total Variance")]: - columns.append({ - 'label': label, - 'fieldtype': 'Float', - 'fieldname': frappe.scrub(label), - 'width': 150 - }) + columns.append( + {"label": label, "fieldtype": "Float", "fieldname": frappe.scrub(label), "width": 150} + ) return columns else: @@ -170,8 +165,11 @@ def get_cost_centers(filters): where company = %s {order_by} - """.format(tab=filters.get("budget_against"), order_by=order_by), - filters.get("company")) + """.format( + tab=filters.get("budget_against"), order_by=order_by + ), + filters.get("company"), + ) else: return frappe.db.sql_list( """ @@ -179,7 +177,10 @@ def get_cost_centers(filters): name from `tab{tab}` - """.format(tab=filters.get("budget_against"))) # nosec + """.format( + tab=filters.get("budget_against") + ) + ) # nosec # Get dimension & target details @@ -187,8 +188,9 @@ def get_dimension_target_details(filters): budget_against = frappe.scrub(filters.get("budget_against")) cond = "" if filters.get("budget_against_filter"): - cond += """ and b.{budget_against} in (%s)""".format( - budget_against=budget_against) % ", ".join(["%s"] * len(filters.get("budget_against_filter"))) + cond += """ and b.{budget_against} in (%s)""".format(budget_against=budget_against) % ", ".join( + ["%s"] * len(filters.get("budget_against_filter")) + ) return frappe.db.sql( """ @@ -222,7 +224,9 @@ def get_dimension_target_details(filters): filters.company, ] + (filters.get("budget_against_filter") or []) - ), as_dict=True) + ), + as_dict=True, + ) # Get target distribution details of accounts of cost center @@ -243,13 +247,14 @@ def get_target_distribution_details(filters): order by md.fiscal_year """, - (filters.from_fiscal_year, filters.to_fiscal_year), as_dict=1): - target_details.setdefault(d.name, {}).setdefault( - d.month, flt(d.percentage_allocation) - ) + (filters.from_fiscal_year, filters.to_fiscal_year), + as_dict=1, + ): + target_details.setdefault(d.name, {}).setdefault(d.month, flt(d.percentage_allocation)) return target_details + # Get actual details from gl entry def get_actual_details(name, filters): budget_against = frappe.scrub(filters.get("budget_against")) @@ -260,7 +265,9 @@ def get_actual_details(name, filters): cond = """ and lft >= "{lft}" and rgt <= "{rgt}" - """.format(lft=cc_lft, rgt=cc_rgt) + """.format( + lft=cc_lft, rgt=cc_rgt + ) ac_details = frappe.db.sql( """ @@ -294,8 +301,12 @@ def get_actual_details(name, filters): group by gl.name order by gl.fiscal_year - """.format(tab=filters.budget_against, budget_against=budget_against, cond=cond), - (filters.from_fiscal_year, filters.to_fiscal_year, name), as_dict=1) + """.format( + tab=filters.budget_against, budget_against=budget_against, cond=cond + ), + (filters.from_fiscal_year, filters.to_fiscal_year, name), + as_dict=1, + ) cc_actual_details = {} for d in ac_details: @@ -303,6 +314,7 @@ def get_actual_details(name, filters): return cc_actual_details + def get_dimension_account_month_map(filters): dimension_target_details = get_dimension_target_details(filters) tdd = get_target_distribution_details(filters) @@ -314,17 +326,13 @@ def get_dimension_account_month_map(filters): for month_id in range(1, 13): month = datetime.date(2013, month_id, 1).strftime("%B") - cam_map.setdefault(ccd.budget_against, {}).setdefault( - ccd.account, {} - ).setdefault(ccd.fiscal_year, {}).setdefault( - month, frappe._dict({"target": 0.0, "actual": 0.0}) - ) + cam_map.setdefault(ccd.budget_against, {}).setdefault(ccd.account, {}).setdefault( + ccd.fiscal_year, {} + ).setdefault(month, frappe._dict({"target": 0.0, "actual": 0.0})) tav_dict = cam_map[ccd.budget_against][ccd.account][ccd.fiscal_year][month] month_percentage = ( - tdd.get(ccd.monthly_distribution, {}).get(month, 0) - if ccd.monthly_distribution - else 100.0 / 12 + tdd.get(ccd.monthly_distribution, {}).get(month, 0) if ccd.monthly_distribution else 100.0 / 12 ) tav_dict.target = flt(ccd.budget_amount) * month_percentage / 100 @@ -347,13 +355,12 @@ def get_fiscal_years(filters): where name between %(from_fiscal_year)s and %(to_fiscal_year)s """, - { - "from_fiscal_year": filters["from_fiscal_year"], - "to_fiscal_year": filters["to_fiscal_year"] - }) + {"from_fiscal_year": filters["from_fiscal_year"], "to_fiscal_year": filters["to_fiscal_year"]}, + ) return fiscal_year + def get_chart_data(filters, columns, data): if not data: @@ -366,12 +373,13 @@ def get_chart_data(filters, columns, data): for year in fiscal_year: for from_date, to_date in get_period_date_ranges(filters["period"], year[0]): - if filters['period'] == 'Yearly': + if filters["period"] == "Yearly": labels.append(year[0]) else: if group_months: - label = formatdate(from_date, format_string="MMM") + "-" \ - + formatdate(to_date, format_string="MMM") + label = ( + formatdate(from_date, format_string="MMM") + "-" + formatdate(to_date, format_string="MMM") + ) labels.append(label) else: label = formatdate(from_date, format_string="MMM") @@ -386,16 +394,16 @@ def get_chart_data(filters, columns, data): for i in range(no_of_columns): budget_values[i] += values[index] - actual_values[i] += values[index+1] + actual_values[i] += values[index + 1] index += 3 return { - 'data': { - 'labels': labels, - 'datasets': [ - {'name': 'Budget', 'chartType': 'bar', 'values': budget_values}, - {'name': 'Actual Expense', 'chartType': 'bar', 'values': actual_values} - ] + "data": { + "labels": labels, + "datasets": [ + {"name": "Budget", "chartType": "bar", "values": budget_values}, + {"name": "Actual Expense", "chartType": "bar", "values": actual_values}, + ], }, - 'type' : 'bar' + "type": "bar", } diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py index 75365b81f22..ee924f86a6a 100644 --- a/erpnext/accounts/report/cash_flow/cash_flow.py +++ b/erpnext/accounts/report/cash_flow/cash_flow.py @@ -20,65 +20,103 @@ from erpnext.accounts.utils import get_fiscal_year def execute(filters=None): - if cint(frappe.db.get_single_value('Accounts Settings', 'use_custom_cash_flow')): + if cint(frappe.db.get_single_value("Accounts Settings", "use_custom_cash_flow")): from erpnext.accounts.report.cash_flow.custom_cash_flow import execute as execute_custom + return execute_custom(filters=filters) - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, - filters.period_start_date, filters.period_end_date, filters.filter_based_on, - filters.periodicity, company=filters.company) + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) cash_flow_accounts = get_cash_flow_accounts() # compute net profit / loss - income = get_data(filters.company, "Income", "Credit", period_list, filters=filters, - accumulated_values=filters.accumulated_values, ignore_closing_entries=True, ignore_accumulated_values_for_fy= True) - expense = get_data(filters.company, "Expense", "Debit", period_list, filters=filters, - accumulated_values=filters.accumulated_values, ignore_closing_entries=True, ignore_accumulated_values_for_fy= True) + income = get_data( + filters.company, + "Income", + "Credit", + period_list, + filters=filters, + accumulated_values=filters.accumulated_values, + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, + ) + expense = get_data( + filters.company, + "Expense", + "Debit", + period_list, + filters=filters, + accumulated_values=filters.accumulated_values, + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, + ) net_profit_loss = get_net_profit_loss(income, expense, period_list, filters.company) data = [] summary_data = {} - company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") for cash_flow_account in cash_flow_accounts: section_data = [] - data.append({ - "account_name": cash_flow_account['section_header'], - "parent_account": None, - "indent": 0.0, - "account": cash_flow_account['section_header'] - }) + data.append( + { + "account_name": cash_flow_account["section_header"], + "parent_account": None, + "indent": 0.0, + "account": cash_flow_account["section_header"], + } + ) if len(data) == 1: # add first net income in operations section if net_profit_loss: - net_profit_loss.update({ - "indent": 1, - "parent_account": cash_flow_accounts[0]['section_header'] - }) + net_profit_loss.update( + {"indent": 1, "parent_account": cash_flow_accounts[0]["section_header"]} + ) data.append(net_profit_loss) section_data.append(net_profit_loss) - for account in cash_flow_account['account_types']: - account_data = get_account_type_based_data(filters.company, - account['account_type'], period_list, filters.accumulated_values, filters) - account_data.update({ - "account_name": account['label'], - "account": account['label'], - "indent": 1, - "parent_account": cash_flow_account['section_header'], - "currency": company_currency - }) + for account in cash_flow_account["account_types"]: + account_data = get_account_type_based_data( + filters.company, account["account_type"], period_list, filters.accumulated_values, filters + ) + account_data.update( + { + "account_name": account["label"], + "account": account["label"], + "indent": 1, + "parent_account": cash_flow_account["section_header"], + "currency": company_currency, + } + ) data.append(account_data) section_data.append(account_data) - add_total_row_account(data, section_data, cash_flow_account['section_footer'], - period_list, company_currency, summary_data, filters) + add_total_row_account( + data, + section_data, + cash_flow_account["section_footer"], + period_list, + company_currency, + summary_data, + filters, + ) - add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) + add_total_row_account( + data, data, _("Net Change in Cash"), period_list, company_currency, summary_data, filters + ) + columns = get_columns( + filters.periodicity, period_list, filters.accumulated_values, filters.company + ) chart = get_chart_data(columns, data) @@ -86,6 +124,7 @@ def execute(filters=None): return columns, data, None, chart, report_summary + def get_cash_flow_accounts(): operation_accounts = { "section_name": "Operations", @@ -95,39 +134,37 @@ def get_cash_flow_accounts(): {"account_type": "Depreciation", "label": _("Depreciation")}, {"account_type": "Receivable", "label": _("Net Change in Accounts Receivable")}, {"account_type": "Payable", "label": _("Net Change in Accounts Payable")}, - {"account_type": "Stock", "label": _("Net Change in Inventory")} - ] + {"account_type": "Stock", "label": _("Net Change in Inventory")}, + ], } investing_accounts = { "section_name": "Investing", "section_footer": _("Net Cash from Investing"), "section_header": _("Cash Flow from Investing"), - "account_types": [ - {"account_type": "Fixed Asset", "label": _("Net Change in Fixed Asset")} - ] + "account_types": [{"account_type": "Fixed Asset", "label": _("Net Change in Fixed Asset")}], } financing_accounts = { "section_name": "Financing", "section_footer": _("Net Cash from Financing"), "section_header": _("Cash Flow from Financing"), - "account_types": [ - {"account_type": "Equity", "label": _("Net Change in Equity")} - ] + "account_types": [{"account_type": "Equity", "label": _("Net Change in Equity")}], } # combine all cash flow accounts for iteration return [operation_accounts, investing_accounts, financing_accounts] + def get_account_type_based_data(company, account_type, period_list, accumulated_values, filters): data = {} total = 0 for period in period_list: start_date = get_start_date(period, accumulated_values, company) - amount = get_account_type_based_gl_data(company, start_date, - period['to_date'], account_type, filters) + amount = get_account_type_based_gl_data( + company, start_date, period["to_date"], account_type, filters + ) if amount and account_type == "Depreciation": amount *= -1 @@ -138,31 +175,42 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_ data["total"] = total return data + def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None): cond = "" filters = frappe._dict(filters or {}) if filters.include_default_book_entries: - company_fb = frappe.db.get_value("Company", company, 'default_finance_book') + company_fb = frappe.db.get_value("Company", company, "default_finance_book") cond = """ AND (finance_book in (%s, %s, '') OR finance_book IS NULL) - """ %(frappe.db.escape(filters.finance_book), frappe.db.escape(company_fb)) + """ % ( + frappe.db.escape(filters.finance_book), + frappe.db.escape(company_fb), + ) else: - cond = " AND (finance_book in (%s, '') OR finance_book IS NULL)" %(frappe.db.escape(cstr(filters.finance_book))) + cond = " AND (finance_book in (%s, '') OR finance_book IS NULL)" % ( + frappe.db.escape(cstr(filters.finance_book)) + ) - - gl_sum = frappe.db.sql_list(""" + gl_sum = frappe.db.sql_list( + """ select sum(credit) - sum(debit) from `tabGL Entry` where company=%s and posting_date >= %s and posting_date <= %s and voucher_type != 'Period Closing Voucher' and account in ( SELECT name FROM tabAccount WHERE account_type = %s) {cond} - """.format(cond=cond), (company, start_date, end_date, account_type)) + """.format( + cond=cond + ), + (company, start_date, end_date, account_type), + ) return gl_sum[0] if gl_sum and gl_sum[0] else 0 + def get_start_date(period, accumulated_values, company): - if not accumulated_values and period.get('from_date'): - return period['from_date'] + if not accumulated_values and period.get("from_date"): + return period["from_date"] start_date = period["year_start_date"] if accumulated_values: @@ -170,23 +218,26 @@ def get_start_date(period, accumulated_values, company): return start_date -def add_total_row_account(out, data, label, period_list, currency, summary_data, filters, consolidated=False): + +def add_total_row_account( + out, data, label, period_list, currency, summary_data, filters, consolidated=False +): total_row = { "account_name": "'" + _("{0}").format(label) + "'", "account": "'" + _("{0}").format(label) + "'", - "currency": currency + "currency": currency, } summary_data[label] = 0 # from consolidated financial statement - if filters.get('accumulated_in_group_company'): + if filters.get("accumulated_in_group_company"): period_list = get_filtered_list_for_consolidated_report(filters, period_list) for row in data: if row.get("parent_account"): for period in period_list: - key = period if consolidated else period['key'] + key = period if consolidated else period["key"] total_row.setdefault(key, 0.0) total_row[key] += row.get(key, 0.0) summary_data[label] += row.get(key) @@ -203,12 +254,7 @@ def get_report_summary(summary_data, currency): for label, value in iteritems(summary_data): report_summary.append( - { - "value": value, - "label": label, - "datatype": "Currency", - "currency": currency - } + {"value": value, "label": label, "datatype": "Currency", "currency": currency} ) return report_summary @@ -216,16 +262,17 @@ def get_report_summary(summary_data, currency): def get_chart_data(columns, data): labels = [d.get("label") for d in columns[2:]] - datasets = [{'name':account.get('account').replace("'", ""), 'values': [account.get('total')]} for account in data if account.get('parent_account') == None and account.get('currency')] + datasets = [ + { + "name": account.get("account").replace("'", ""), + "values": [account.get(d.get("fieldname")) for d in columns[2:]], + } + for account in data + if account.get("parent_account") == None and account.get("currency") + ] datasets = datasets[:-1] - chart = { - "data": { - 'labels': labels, - 'datasets': datasets - }, - "type": "bar" - } + chart = {"data": {"labels": labels, "datasets": datasets}, "type": "bar"} chart["fieldtype"] = "Currency" diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py index 45d147e7a21..b165c88c068 100644 --- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py +++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py @@ -4,7 +4,8 @@ import frappe from frappe import _ -from frappe.utils import add_to_date +from frappe.query_builder.functions import Sum +from frappe.utils import add_to_date, flt, get_date_str from erpnext.accounts.report.financial_statements import get_columns, get_data, get_period_list from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import ( @@ -13,41 +14,59 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement def get_mapper_for(mappers, position): - mapper_list = list(filter(lambda x: x['position'] == position, mappers)) + mapper_list = list(filter(lambda x: x["position"] == position, mappers)) return mapper_list[0] if mapper_list else [] def get_mappers_from_db(): return frappe.get_all( - 'Cash Flow Mapper', + "Cash Flow Mapper", fields=[ - 'section_name', 'section_header', 'section_leader', 'section_subtotal', - 'section_footer', 'name', 'position'], - order_by='position' + "section_name", + "section_header", + "section_leader", + "section_subtotal", + "section_footer", + "name", + "position", + ], + order_by="position", ) def get_accounts_in_mappers(mapping_names): - return frappe.db.sql(''' - select cfma.name, cfm.label, cfm.is_working_capital, cfm.is_income_tax_liability, - cfm.is_income_tax_expense, cfm.is_finance_cost, cfm.is_finance_cost_adjustment - from `tabCash Flow Mapping Accounts` cfma - join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name - where cfma.parent in (%s) - order by cfm.is_working_capital - ''', (', '.join('"%s"' % d for d in mapping_names))) + cfm = frappe.qb.DocType("Cash Flow Mapping") + cfma = frappe.qb.DocType("Cash Flow Mapping Accounts") + result = ( + frappe.qb.select( + cfma.name, + cfm.label, + cfm.is_working_capital, + cfm.is_income_tax_liability, + cfm.is_income_tax_expense, + cfm.is_finance_cost, + cfm.is_finance_cost_adjustment, + cfma.account, + ) + .from_(cfm) + .join(cfma) + .on(cfm.name == cfma.parent) + .where(cfma.parent.isin(mapping_names)) + ).run() + + return result def setup_mappers(mappers): cash_flow_accounts = [] for mapping in mappers: - mapping['account_types'] = [] - mapping['tax_liabilities'] = [] - mapping['tax_expenses'] = [] - mapping['finance_costs'] = [] - mapping['finance_costs_adjustments'] = [] - doc = frappe.get_doc('Cash Flow Mapper', mapping['name']) + mapping["account_types"] = [] + mapping["tax_liabilities"] = [] + mapping["tax_expenses"] = [] + mapping["finance_costs"] = [] + mapping["finance_costs_adjustments"] = [] + doc = frappe.get_doc("Cash Flow Mapper", mapping["name"]) mapping_names = [item.name for item in doc.accounts] if not mapping_names: @@ -57,96 +76,123 @@ def setup_mappers(mappers): account_types = [ dict( - name=account[0], label=account[1], is_working_capital=account[2], - is_income_tax_liability=account[3], is_income_tax_expense=account[4] - ) for account in accounts if not account[3]] + name=account[0], + account_name=account[7], + label=account[1], + is_working_capital=account[2], + is_income_tax_liability=account[3], + is_income_tax_expense=account[4], + ) + for account in accounts + if not account[3] + ] finance_costs_adjustments = [ dict( - name=account[0], label=account[1], is_finance_cost=account[5], - is_finance_cost_adjustment=account[6] - ) for account in accounts if account[6]] + name=account[0], + account_name=account[7], + label=account[1], + is_finance_cost=account[5], + is_finance_cost_adjustment=account[6], + ) + for account in accounts + if account[6] + ] tax_liabilities = [ dict( - name=account[0], label=account[1], is_income_tax_liability=account[3], - is_income_tax_expense=account[4] - ) for account in accounts if account[3]] + name=account[0], + account_name=account[7], + label=account[1], + is_income_tax_liability=account[3], + is_income_tax_expense=account[4], + ) + for account in accounts + if account[3] + ] tax_expenses = [ dict( - name=account[0], label=account[1], is_income_tax_liability=account[3], - is_income_tax_expense=account[4] - ) for account in accounts if account[4]] + name=account[0], + account_name=account[7], + label=account[1], + is_income_tax_liability=account[3], + is_income_tax_expense=account[4], + ) + for account in accounts + if account[4] + ] finance_costs = [ - dict( - name=account[0], label=account[1], is_finance_cost=account[5]) - for account in accounts if account[5]] + dict(name=account[0], account_name=account[7], label=account[1], is_finance_cost=account[5]) + for account in accounts + if account[5] + ] account_types_labels = sorted( set( - (d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense']) - for d in account_types + (d["label"], d["is_working_capital"], d["is_income_tax_liability"], d["is_income_tax_expense"]) + for d in account_types ), - key=lambda x: x[1] + key=lambda x: x[1], ) fc_adjustment_labels = sorted( set( - [(d['label'], d['is_finance_cost'], d['is_finance_cost_adjustment']) - for d in finance_costs_adjustments if d['is_finance_cost_adjustment']] + [ + (d["label"], d["is_finance_cost"], d["is_finance_cost_adjustment"]) + for d in finance_costs_adjustments + if d["is_finance_cost_adjustment"] + ] ), - key=lambda x: x[2] + key=lambda x: x[2], ) unique_liability_labels = sorted( set( - [(d['label'], d['is_income_tax_liability'], d['is_income_tax_expense']) - for d in tax_liabilities] + [ + (d["label"], d["is_income_tax_liability"], d["is_income_tax_expense"]) + for d in tax_liabilities + ] ), - key=lambda x: x[0] + key=lambda x: x[0], ) unique_expense_labels = sorted( set( - [(d['label'], d['is_income_tax_liability'], d['is_income_tax_expense']) - for d in tax_expenses] + [(d["label"], d["is_income_tax_liability"], d["is_income_tax_expense"]) for d in tax_expenses] ), - key=lambda x: x[0] + key=lambda x: x[0], ) unique_finance_costs_labels = sorted( - set( - [(d['label'], d['is_finance_cost']) for d in finance_costs] - ), - key=lambda x: x[0] + set([(d["label"], d["is_finance_cost"]) for d in finance_costs]), key=lambda x: x[0] ) for label in account_types_labels: - names = [d['name'] for d in account_types if d['label'] == label[0]] + names = [d["account_name"] for d in account_types if d["label"] == label[0]] m = dict(label=label[0], names=names, is_working_capital=label[1]) - mapping['account_types'].append(m) + mapping["account_types"].append(m) for label in fc_adjustment_labels: - names = [d['name'] for d in finance_costs_adjustments if d['label'] == label[0]] + names = [d["account_name"] for d in finance_costs_adjustments if d["label"] == label[0]] m = dict(label=label[0], names=names) - mapping['finance_costs_adjustments'].append(m) + mapping["finance_costs_adjustments"].append(m) for label in unique_liability_labels: - names = [d['name'] for d in tax_liabilities if d['label'] == label[0]] + names = [d["account_name"] for d in tax_liabilities if d["label"] == label[0]] m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2]) - mapping['tax_liabilities'].append(m) + mapping["tax_liabilities"].append(m) for label in unique_expense_labels: - names = [d['name'] for d in tax_expenses if d['label'] == label[0]] + names = [d["account_name"] for d in tax_expenses if d["label"] == label[0]] m = dict(label=label[0], names=names, tax_liability=label[1], tax_expense=label[2]) - mapping['tax_expenses'].append(m) + mapping["tax_expenses"].append(m) for label in unique_finance_costs_labels: - names = [d['name'] for d in finance_costs if d['label'] == label[0]] + names = [d["account_name"] for d in finance_costs if d["label"] == label[0]] m = dict(label=label[0], names=names, is_finance_cost=label[1]) - mapping['finance_costs'].append(m) + mapping["finance_costs"].append(m) cash_flow_accounts.append(mapping) @@ -154,119 +200,145 @@ def setup_mappers(mappers): def add_data_for_operating_activities( - filters, company_currency, profit_data, period_list, light_mappers, mapper, data): + filters, company_currency, profit_data, period_list, light_mappers, mapper, data +): has_added_working_capital_header = False section_data = [] - data.append({ - "account_name": mapper['section_header'], - "parent_account": None, - "indent": 0.0, - "account": mapper['section_header'] - }) + data.append( + { + "account_name": mapper["section_header"], + "parent_account": None, + "indent": 0.0, + "account": mapper["section_header"], + } + ) if profit_data: - profit_data.update({ - "indent": 1, - "parent_account": get_mapper_for(light_mappers, position=1)['section_header'] - }) + profit_data.update( + {"indent": 1, "parent_account": get_mapper_for(light_mappers, position=1)["section_header"]} + ) data.append(profit_data) section_data.append(profit_data) - data.append({ - "account_name": mapper["section_leader"], - "parent_account": None, - "indent": 1.0, - "account": mapper["section_leader"] - }) - - for account in mapper['account_types']: - if account['is_working_capital'] and not has_added_working_capital_header: - data.append({ - "account_name": 'Movement in working capital', + data.append( + { + "account_name": mapper["section_leader"], "parent_account": None, "indent": 1.0, - "account": "" - }) + "account": mapper["section_leader"], + } + ) + + for account in mapper["account_types"]: + if account["is_working_capital"] and not has_added_working_capital_header: + data.append( + { + "account_name": "Movement in working capital", + "parent_account": None, + "indent": 1.0, + "account": "", + } + ) has_added_working_capital_header = True account_data = _get_account_type_based_data( - filters, account['names'], period_list, filters.accumulated_values) + filters, account["names"], period_list, filters.accumulated_values + ) - if not account['is_working_capital']: + if not account["is_working_capital"]: for key in account_data: - if key != 'total': + if key != "total": account_data[key] *= -1 - if account_data['total'] != 0: - account_data.update({ - "account_name": account['label'], - "account": account['names'], - "indent": 1.0, - "parent_account": mapper['section_header'], - "currency": company_currency - }) + if account_data["total"] != 0: + account_data.update( + { + "account_name": account["label"], + "account": account["names"], + "indent": 1.0, + "parent_account": mapper["section_header"], + "currency": company_currency, + } + ) data.append(account_data) section_data.append(account_data) _add_total_row_account( - data, section_data, mapper['section_subtotal'], period_list, company_currency, indent=1) + data, section_data, mapper["section_subtotal"], period_list, company_currency, indent=1 + ) # calculate adjustment for tax paid and add to data - if not mapper['tax_liabilities']: - mapper['tax_liabilities'] = [ - dict(label='Income tax paid', names=[''], tax_liability=1, tax_expense=0)] + if not mapper["tax_liabilities"]: + mapper["tax_liabilities"] = [ + dict(label="Income tax paid", names=[""], tax_liability=1, tax_expense=0) + ] - for account in mapper['tax_liabilities']: + for account in mapper["tax_liabilities"]: tax_paid = calculate_adjustment( - filters, mapper['tax_liabilities'], mapper['tax_expenses'], - filters.accumulated_values, period_list) + filters, + mapper["tax_liabilities"], + mapper["tax_expenses"], + filters.accumulated_values, + period_list, + ) if tax_paid: - tax_paid.update({ - 'parent_account': mapper['section_header'], - 'currency': company_currency, - 'account_name': account['label'], - 'indent': 1.0 - }) + tax_paid.update( + { + "parent_account": mapper["section_header"], + "currency": company_currency, + "account_name": account["label"], + "indent": 1.0, + } + ) data.append(tax_paid) section_data.append(tax_paid) - if not mapper['finance_costs_adjustments']: - mapper['finance_costs_adjustments'] = [dict(label='Interest Paid', names=[''])] + if not mapper["finance_costs_adjustments"]: + mapper["finance_costs_adjustments"] = [dict(label="Interest Paid", names=[""])] - for account in mapper['finance_costs_adjustments']: + for account in mapper["finance_costs_adjustments"]: interest_paid = calculate_adjustment( - filters, mapper['finance_costs_adjustments'], mapper['finance_costs'], - filters.accumulated_values, period_list + filters, + mapper["finance_costs_adjustments"], + mapper["finance_costs"], + filters.accumulated_values, + period_list, ) if interest_paid: - interest_paid.update({ - 'parent_account': mapper['section_header'], - 'currency': company_currency, - 'account_name': account['label'], - 'indent': 1.0 - }) + interest_paid.update( + { + "parent_account": mapper["section_header"], + "currency": company_currency, + "account_name": account["label"], + "indent": 1.0, + } + ) data.append(interest_paid) section_data.append(interest_paid) _add_total_row_account( - data, section_data, mapper['section_footer'], period_list, company_currency) + data, section_data, mapper["section_footer"], period_list, company_currency + ) -def calculate_adjustment(filters, non_expense_mapper, expense_mapper, use_accumulated_values, period_list): - liability_accounts = [d['names'] for d in non_expense_mapper] - expense_accounts = [d['names'] for d in expense_mapper] +def calculate_adjustment( + filters, non_expense_mapper, expense_mapper, use_accumulated_values, period_list +): + liability_accounts = [d["names"] for d in non_expense_mapper] + expense_accounts = [d["names"] for d in expense_mapper] - non_expense_closing = _get_account_type_based_data( - filters, liability_accounts, period_list, 0) + non_expense_closing = _get_account_type_based_data(filters, liability_accounts, period_list, 0) non_expense_opening = _get_account_type_based_data( - filters, liability_accounts, period_list, use_accumulated_values, opening_balances=1) + filters, liability_accounts, period_list, use_accumulated_values, opening_balances=1 + ) expense_data = _get_account_type_based_data( - filters, expense_accounts, period_list, use_accumulated_values) + filters, expense_accounts, period_list, use_accumulated_values + ) data = _calculate_adjustment(non_expense_closing, non_expense_opening, expense_data) return data @@ -276,7 +348,9 @@ def _calculate_adjustment(non_expense_closing, non_expense_opening, expense_data account_data = {} for month in non_expense_opening.keys(): if non_expense_opening[month] and non_expense_closing[month]: - account_data[month] = non_expense_opening[month] - expense_data[month] + non_expense_closing[month] + account_data[month] = ( + non_expense_opening[month] - expense_data[month] + non_expense_closing[month] + ) elif expense_data[month]: account_data[month] = expense_data[month] @@ -284,32 +358,39 @@ def _calculate_adjustment(non_expense_closing, non_expense_opening, expense_data def add_data_for_other_activities( - filters, company_currency, profit_data, period_list, light_mappers, mapper_list, data): + filters, company_currency, profit_data, period_list, light_mappers, mapper_list, data +): for mapper in mapper_list: section_data = [] - data.append({ - "account_name": mapper['section_header'], - "parent_account": None, - "indent": 0.0, - "account": mapper['section_header'] - }) + data.append( + { + "account_name": mapper["section_header"], + "parent_account": None, + "indent": 0.0, + "account": mapper["section_header"], + } + ) - for account in mapper['account_types']: - account_data = _get_account_type_based_data(filters, - account['names'], period_list, filters.accumulated_values) - if account_data['total'] != 0: - account_data.update({ - "account_name": account['label'], - "account": account['names'], - "indent": 1, - "parent_account": mapper['section_header'], - "currency": company_currency - }) + for account in mapper["account_types"]: + account_data = _get_account_type_based_data( + filters, account["names"], period_list, filters.accumulated_values + ) + if account_data["total"] != 0: + account_data.update( + { + "account_name": account["label"], + "account": account["names"], + "indent": 1, + "parent_account": mapper["section_header"], + "currency": company_currency, + } + ) data.append(account_data) section_data.append(account_data) - _add_total_row_account(data, section_data, mapper['section_footer'], - period_list, company_currency) + _add_total_row_account( + data, section_data, mapper["section_footer"], period_list, company_currency + ) def compute_data(filters, company_currency, profit_data, period_list, light_mappers, full_mapper): @@ -318,13 +399,18 @@ def compute_data(filters, company_currency, profit_data, period_list, light_mapp operating_activities_mapper = get_mapper_for(light_mappers, position=1) other_mappers = [ get_mapper_for(light_mappers, position=2), - get_mapper_for(light_mappers, position=3) + get_mapper_for(light_mappers, position=3), ] if operating_activities_mapper: add_data_for_operating_activities( - filters, company_currency, profit_data, period_list, light_mappers, - operating_activities_mapper, data + filters, + company_currency, + profit_data, + period_list, + light_mappers, + operating_activities_mapper, + data, ) if all(other_mappers): @@ -336,10 +422,17 @@ def compute_data(filters, company_currency, profit_data, period_list, light_mapp def execute(filters=None): - if not filters.periodicity: filters.periodicity = "Monthly" - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, - filters.period_start_date, filters.period_end_date, filters.filter_based_on, - filters.periodicity, company=filters.company) + if not filters.periodicity: + filters.periodicity = "Monthly" + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + company=filters.company, + ) mappers = get_mappers_from_db() @@ -347,43 +440,72 @@ def execute(filters=None): # compute net profit / loss income = get_data( - filters.company, "Income", "Credit", period_list, filters=filters, - accumulated_values=filters.accumulated_values, ignore_closing_entries=True, - ignore_accumulated_values_for_fy=True + filters.company, + "Income", + "Credit", + period_list, + filters=filters, + accumulated_values=filters.accumulated_values, + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, ) expense = get_data( - filters.company, "Expense", "Debit", period_list, filters=filters, - accumulated_values=filters.accumulated_values, ignore_closing_entries=True, - ignore_accumulated_values_for_fy=True + filters.company, + "Expense", + "Debit", + period_list, + filters=filters, + accumulated_values=filters.accumulated_values, + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, ) net_profit_loss = get_net_profit_loss(income, expense, period_list, filters.company) - company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") + company_currency = frappe.get_cached_value("Company", filters.company, "default_currency") - data = compute_data(filters, company_currency, net_profit_loss, period_list, mappers, cash_flow_accounts) + data = compute_data( + filters, company_currency, net_profit_loss, period_list, mappers, cash_flow_accounts + ) _add_total_row_account(data, data, _("Net Change in Cash"), period_list, company_currency) - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) + columns = get_columns( + filters.periodicity, period_list, filters.accumulated_values, filters.company + ) return columns, data -def _get_account_type_based_data(filters, account_names, period_list, accumulated_values, opening_balances=0): +def _get_account_type_based_data( + filters, account_names, period_list, accumulated_values, opening_balances=0 +): + if not account_names or not account_names[0] or not type(account_names[0]) == str: + # only proceed if account_names is a list of account names + return {} + from erpnext.accounts.report.cash_flow.cash_flow import get_start_date company = filters.company data = {} total = 0 + GLEntry = frappe.qb.DocType("GL Entry") + Account = frappe.qb.DocType("Account") + for period in period_list: start_date = get_start_date(period, accumulated_values, company) - accounts = ', '.join('"%s"' % d for d in account_names) + + account_subquery = ( + frappe.qb.from_(Account) + .where((Account.name.isin(account_names)) | (Account.parent_account.isin(account_names))) + .select(Account.name) + .as_("account_subquery") + ) if opening_balances: date_info = dict(date=start_date) - months_map = {'Monthly': -1, 'Quarterly': -3, 'Half-Yearly': -6} - years_map = {'Yearly': -1} + months_map = {"Monthly": -1, "Quarterly": -3, "Half-Yearly": -6} + years_map = {"Yearly": -1} if months_map.get(filters.periodicity): date_info.update(months=months_map[filters.periodicity]) @@ -391,36 +513,35 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate date_info.update(years=years_map[filters.periodicity]) if accumulated_values: - start, end = add_to_date(start_date, years=-1), add_to_date(period['to_date'], years=-1) + start, end = add_to_date(start_date, years=-1), add_to_date(period["to_date"], years=-1) else: start, end = add_to_date(**date_info), add_to_date(**date_info) - gl_sum = frappe.db.sql_list(""" - select sum(credit) - sum(debit) - from `tabGL Entry` - where company=%s and posting_date >= %s and posting_date <= %s - and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE name IN (%s) - OR parent_account IN (%s)) - """, (company, start, end, accounts, accounts)) - else: - gl_sum = frappe.db.sql_list(""" - select sum(credit) - sum(debit) - from `tabGL Entry` - where company=%s and posting_date >= %s and posting_date <= %s - and voucher_type != 'Period Closing Voucher' - and account in ( SELECT name FROM tabAccount WHERE name IN (%s) - OR parent_account IN (%s)) - """, (company, start_date if accumulated_values else period['from_date'], - period['to_date'], accounts, accounts)) + start, end = get_date_str(start), get_date_str(end) - if gl_sum and gl_sum[0]: - amount = gl_sum[0] else: - amount = 0 + start, end = start_date if accumulated_values else period["from_date"], period["to_date"] + start, end = get_date_str(start), get_date_str(end) - total += amount - data.setdefault(period["key"], amount) + result = ( + frappe.qb.from_(GLEntry) + .select(Sum(GLEntry.credit) - Sum(GLEntry.debit)) + .where( + (GLEntry.company == company) + & (GLEntry.posting_date >= start) + & (GLEntry.posting_date <= end) + & (GLEntry.voucher_type != "Period Closing Voucher") + & (GLEntry.account.isin(account_subquery)) + ) + ).run() + + if result and result[0]: + gl_sum = result[0][0] + else: + gl_sum = 0 + + total += flt(gl_sum) + data.setdefault(period["key"], flt(gl_sum)) data["total"] = total return data @@ -431,7 +552,7 @@ def _add_total_row_account(out, data, label, period_list, currency, indent=0.0): "indent": indent, "account_name": "'" + _("{0}").format(label) + "'", "account": "'" + _("{0}").format(label) + "'", - "currency": currency + "currency": currency, } for row in data: if row.get("parent_account"): diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py index 758e3e93379..330e442a808 100644 --- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py +++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py @@ -42,26 +42,32 @@ from erpnext.accounts.report.utils import convert, convert_to_presentation_curre def execute(filters=None): columns, data, message, chart = [], [], [], [] - if not filters.get('company'): + if not filters.get("company"): return columns, data, message, chart - fiscal_year = get_fiscal_year_data(filters.get('from_fiscal_year'), filters.get('to_fiscal_year')) + fiscal_year = get_fiscal_year_data(filters.get("from_fiscal_year"), filters.get("to_fiscal_year")) companies_column, companies = get_companies(filters) columns = get_columns(companies_column, filters) - if filters.get('report') == "Balance Sheet": - data, message, chart, report_summary = get_balance_sheet_data(fiscal_year, companies, columns, filters) - elif filters.get('report') == "Profit and Loss Statement": - data, message, chart, report_summary = get_profit_loss_data(fiscal_year, companies, columns, filters) + if filters.get("report") == "Balance Sheet": + data, message, chart, report_summary = get_balance_sheet_data( + fiscal_year, companies, columns, filters + ) + elif filters.get("report") == "Profit and Loss Statement": + data, message, chart, report_summary = get_profit_loss_data( + fiscal_year, companies, columns, filters + ) else: - if cint(frappe.db.get_single_value('Accounts Settings', 'use_custom_cash_flow')): + if cint(frappe.db.get_single_value("Accounts Settings", "use_custom_cash_flow")): from erpnext.accounts.report.cash_flow.custom_cash_flow import execute as execute_custom + return execute_custom(filters=filters) data, report_summary = get_cash_flow_data(fiscal_year, companies, filters) return columns, data, message, chart, report_summary + def get_balance_sheet_data(fiscal_year, companies, columns, filters): asset = get_data(companies, "Asset", "Debit", fiscal_year, filters=filters) @@ -75,24 +81,27 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters): data.extend(equity or []) company_currency = get_company_currency(filters) - provisional_profit_loss, total_credit = get_provisional_profit_loss(asset, liability, equity, - companies, filters.get('company'), company_currency, True) + provisional_profit_loss, total_credit = get_provisional_profit_loss( + asset, liability, equity, companies, filters.get("company"), company_currency, True + ) - message, opening_balance = prepare_companywise_opening_balance(asset, liability, equity, companies) + message, opening_balance = prepare_companywise_opening_balance( + asset, liability, equity, companies + ) if opening_balance: unclosed = { "account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", "account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'", "warn_if_negative": True, - "currency": company_currency + "currency": company_currency, } for company in companies: unclosed[company] = opening_balance.get(company) if provisional_profit_loss and provisional_profit_loss.get(company): - provisional_profit_loss[company] = ( - flt(provisional_profit_loss[company]) - flt(opening_balance.get(company)) + provisional_profit_loss[company] = flt(provisional_profit_loss[company]) - flt( + opening_balance.get(company) ) unclosed["total"] = opening_balance.get(company) @@ -103,13 +112,23 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters): if total_credit: data.append(total_credit) - report_summary = get_bs_summary(companies, asset, liability, equity, provisional_profit_loss, total_credit, - company_currency, filters, True) + report_summary = get_bs_summary( + companies, + asset, + liability, + equity, + provisional_profit_loss, + total_credit, + company_currency, + filters, + True, + ) chart = get_chart_data(filters, columns, asset, liability, equity) return data, message, chart, report_summary + def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, companies): opening_balance = {} for company in companies: @@ -119,29 +138,36 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, for data in [asset_data, liability_data, equity_data]: if data: account_name = get_root_account_name(data[0].root_type, company) - opening_value += (get_opening_balance(account_name, data, company) or 0.0) + opening_value += get_opening_balance(account_name, data, company) or 0.0 opening_balance[company] = opening_value if opening_balance: return _("Previous Financial Year is not closed"), opening_balance - return '', {} + return "", {} + def get_opening_balance(account_name, data, company): for row in data: - if row.get('account_name') == account_name: - return row.get('company_wise_opening_bal', {}).get(company, 0.0) + if row.get("account_name") == account_name: + return row.get("company_wise_opening_bal", {}).get(company, 0.0) + def get_root_account_name(root_type, company): return frappe.get_all( - 'Account', - fields=['account_name'], - filters = {'root_type': root_type, 'is_group': 1, - 'company': company, 'parent_account': ('is', 'not set')}, - as_list=1 + "Account", + fields=["account_name"], + filters={ + "root_type": root_type, + "is_group": 1, + "company": company, + "parent_account": ("is", "not set"), + }, + as_list=1, )[0][0] + def get_profit_loss_data(fiscal_year, companies, columns, filters): income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters) company_currency = get_company_currency(filters) @@ -154,20 +180,26 @@ def get_profit_loss_data(fiscal_year, companies, columns, filters): chart = get_pl_chart_data(filters, columns, income, expense, net_profit_loss) - report_summary = get_pl_summary(companies, '', income, expense, net_profit_loss, company_currency, filters, True) + report_summary = get_pl_summary( + companies, "", income, expense, net_profit_loss, company_currency, filters, True + ) return data, None, chart, report_summary + def get_income_expense_data(companies, fiscal_year, filters): company_currency = get_company_currency(filters) income = get_data(companies, "Income", "Credit", fiscal_year, filters, True) expense = get_data(companies, "Expense", "Debit", fiscal_year, filters, True) - net_profit_loss = get_net_profit_loss(income, expense, companies, filters.company, company_currency, True) + net_profit_loss = get_net_profit_loss( + income, expense, companies, filters.company, company_currency, True + ) return income, expense, net_profit_loss + def get_cash_flow_data(fiscal_year, companies, filters): cash_flow_accounts = get_cash_flow_accounts() @@ -179,50 +211,67 @@ def get_cash_flow_data(fiscal_year, companies, filters): for cash_flow_account in cash_flow_accounts: section_data = [] - data.append({ - "account_name": cash_flow_account['section_header'], - "parent_account": None, - "indent": 0.0, - "account": cash_flow_account['section_header'] - }) + data.append( + { + "account_name": cash_flow_account["section_header"], + "parent_account": None, + "indent": 0.0, + "account": cash_flow_account["section_header"], + } + ) if len(data) == 1: # add first net income in operations section if net_profit_loss: - net_profit_loss.update({ - "indent": 1, - "parent_account": cash_flow_accounts[0]['section_header'] - }) + net_profit_loss.update( + {"indent": 1, "parent_account": cash_flow_accounts[0]["section_header"]} + ) data.append(net_profit_loss) section_data.append(net_profit_loss) - for account in cash_flow_account['account_types']: - account_data = get_account_type_based_data(account['account_type'], companies, fiscal_year, filters) - account_data.update({ - "account_name": account['label'], - "account": account['label'], - "indent": 1, - "parent_account": cash_flow_account['section_header'], - "currency": company_currency - }) + for account in cash_flow_account["account_types"]: + account_data = get_account_type_based_data( + account["account_type"], companies, fiscal_year, filters + ) + account_data.update( + { + "account_name": account["label"], + "account": account["label"], + "indent": 1, + "parent_account": cash_flow_account["section_header"], + "currency": company_currency, + } + ) data.append(account_data) section_data.append(account_data) - add_total_row_account(data, section_data, cash_flow_account['section_footer'], - companies, company_currency, summary_data, filters, True) + add_total_row_account( + data, + section_data, + cash_flow_account["section_footer"], + companies, + company_currency, + summary_data, + filters, + True, + ) - add_total_row_account(data, data, _("Net Change in Cash"), companies, company_currency, summary_data, filters, True) + add_total_row_account( + data, data, _("Net Change in Cash"), companies, company_currency, summary_data, filters, True + ) report_summary = get_cash_flow_summary(summary_data, company_currency) return data, report_summary + def get_account_type_based_data(account_type, companies, fiscal_year, filters): data = {} total = 0 for company in companies: - amount = get_account_type_based_gl_data(company, - fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters) + amount = get_account_type_based_gl_data( + company, fiscal_year.year_start_date, fiscal_year.year_end_date, account_type, filters + ) if amount and account_type == "Depreciation": amount *= -1 @@ -233,6 +282,7 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters): data["total"] = total return data + def get_columns(companies, filters): columns = [ { @@ -240,14 +290,15 @@ def get_columns(companies, filters): "label": _("Account"), "fieldtype": "Link", "options": "Account", - "width": 300 - }, { + "width": 300, + }, + { "fieldname": "currency", "label": _("Currency"), "fieldtype": "Link", "options": "Currency", - "hidden": 1 - } + "hidden": 1, + }, ] for company in companies: @@ -256,69 +307,96 @@ def get_columns(companies, filters): if not currency: currency = erpnext.get_company_currency(company) - columns.append({ - "fieldname": company, - "label": f'{company} ({currency})', - "fieldtype": "Currency", - "options": "currency", - "width": 150, - "apply_currency_formatter": apply_currency_formatter, - "company_name": company - }) + columns.append( + { + "fieldname": company, + "label": f"{company} ({currency})", + "fieldtype": "Currency", + "options": "currency", + "width": 150, + "apply_currency_formatter": apply_currency_formatter, + "company_name": company, + } + ) return columns -def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False): - accounts, accounts_by_name, parent_children_map = get_account_heads(root_type, - companies, filters) - if not accounts: return [] +def get_data( + companies, root_type, balance_must_be, fiscal_year, filters=None, ignore_closing_entries=False +): + accounts, accounts_by_name, parent_children_map = get_account_heads(root_type, companies, filters) + + if not accounts: + return [] company_currency = get_company_currency(filters) - if filters.filter_based_on == 'Fiscal Year': - start_date = fiscal_year.year_start_date if filters.report != 'Balance Sheet' else None + if filters.filter_based_on == "Fiscal Year": + start_date = fiscal_year.year_start_date if filters.report != "Balance Sheet" else None end_date = fiscal_year.year_end_date else: - start_date = filters.period_start_date if filters.report != 'Balance Sheet' else None + start_date = filters.period_start_date if filters.report != "Balance Sheet" else None end_date = filters.period_end_date filters.end_date = end_date gl_entries_by_account = {} - for root in frappe.db.sql("""select lft, rgt from tabAccount - where root_type=%s and ifnull(parent_account, '') = ''""", root_type, as_dict=1): + for root in frappe.db.sql( + """select lft, rgt from tabAccount + where root_type=%s and ifnull(parent_account, '') = ''""", + root_type, + as_dict=1, + ): - set_gl_entries_by_account(start_date, - end_date, root.lft, root.rgt, filters, - gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False) + set_gl_entries_by_account( + start_date, + end_date, + root.lft, + root.rgt, + filters, + gl_entries_by_account, + accounts_by_name, + accounts, + ignore_closing_entries=False, + ) calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year) accumulate_values_into_parents(accounts, accounts_by_name, companies) - out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters) + out = prepare_data( + accounts, start_date, end_date, balance_must_be, companies, company_currency, filters + ) - out = filter_out_zero_value_rows(out, parent_children_map, show_zero_values=filters.get("show_zero_values")) + out = filter_out_zero_value_rows( + out, parent_children_map, show_zero_values=filters.get("show_zero_values") + ) if out: add_total_row(out, root_type, balance_must_be, companies, company_currency) return out + def get_company_currency(filters=None): - return (filters.get('presentation_currency') - or frappe.get_cached_value('Company', filters.company, "default_currency")) + return filters.get("presentation_currency") or frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) + def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year): - start_date = (fiscal_year.year_start_date - if filters.filter_based_on == 'Fiscal Year' else filters.period_start_date) + start_date = ( + fiscal_year.year_start_date + if filters.filter_based_on == "Fiscal Year" + else filters.period_start_date + ) for entries in gl_entries_by_account.values(): for entry in entries: if entry.account_number: - account_name = entry.account_number + ' - ' + entry.account_name + account_name = entry.account_number + " - " + entry.account_name else: - account_name = entry.account_name + account_name = entry.account_name d = accounts_by_name.get(account_name) @@ -326,48 +404,56 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters debit, credit = 0, 0 for company in companies: # check if posting date is within the period - if (entry.company == company or (filters.get('accumulated_in_group_company')) - and entry.company in companies.get(company)): + if ( + entry.company == company + or (filters.get("accumulated_in_group_company")) + and entry.company in companies.get(company) + ): parent_company_currency = erpnext.get_company_currency(d.company) child_company_currency = erpnext.get_company_currency(entry.company) debit, credit = flt(entry.debit), flt(entry.credit) - if (not filters.get('presentation_currency') + if ( + not filters.get("presentation_currency") and entry.company != company and parent_company_currency != child_company_currency - and filters.get('accumulated_in_group_company')): + and filters.get("accumulated_in_group_company") + ): debit = convert(debit, parent_company_currency, child_company_currency, filters.end_date) credit = convert(credit, parent_company_currency, child_company_currency, filters.end_date) d[company] = d.get(company, 0.0) + flt(debit) - flt(credit) if entry.posting_date < getdate(start_date): - d['company_wise_opening_bal'][company] += (flt(debit) - flt(credit)) + d["company_wise_opening_bal"][company] += flt(debit) - flt(credit) if entry.posting_date < getdate(start_date): d["opening_balance"] = d.get("opening_balance", 0.0) + flt(debit) - flt(credit) + def accumulate_values_into_parents(accounts, accounts_by_name, companies): """accumulate children's values in parent accounts""" for d in reversed(accounts): if d.parent_account: account = d.parent_account_name - # if not accounts_by_name.get(account): - # continue - for company in companies: - accounts_by_name[account][company] = \ - accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0) + accounts_by_name[account][company] = accounts_by_name[account].get(company, 0.0) + d.get( + company, 0.0 + ) - accounts_by_name[account]['company_wise_opening_bal'][company] += d.get('company_wise_opening_bal', {}).get(company, 0.0) + accounts_by_name[account]["company_wise_opening_bal"][company] += d.get( + "company_wise_opening_bal", {} + ).get(company, 0.0) + + accounts_by_name[account]["opening_balance"] = accounts_by_name[account].get( + "opening_balance", 0.0 + ) + d.get("opening_balance", 0.0) - accounts_by_name[account]["opening_balance"] = \ - accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) def get_account_heads(root_type, companies, filters): - accounts = get_accounts(root_type, filters) + accounts = get_accounts(root_type, companies) if not accounts: return None, None, None @@ -378,32 +464,34 @@ def get_account_heads(root_type, companies, filters): return accounts, accounts_by_name, parent_children_map + def update_parent_account_names(accounts): """Update parent_account_name in accounts list. - parent_name is `name` of parent account which could have other prefix - of account_number and suffix of company abbr. This function adds key called - `parent_account_name` which does not have such prefix/suffix. + parent_name is `name` of parent account which could have other prefix + of account_number and suffix of company abbr. This function adds key called + `parent_account_name` which does not have such prefix/suffix. """ name_to_account_map = {} for d in accounts: if d.account_number: - account_name = d.account_number + ' - ' + d.account_name + account_name = d.account_number + " - " + d.account_name else: - account_name = d.account_name + account_name = d.account_name name_to_account_map[d.name] = account_name for account in accounts: if account.parent_account: - account["parent_account_name"] = name_to_account_map[account.parent_account] + account["parent_account_name"] = name_to_account_map.get(account.parent_account) return accounts + def get_companies(filters): companies = {} - all_companies = get_subsidiary_companies(filters.get('company')) - companies.setdefault(filters.get('company'), all_companies) + all_companies = get_subsidiary_companies(filters.get("company")) + companies.setdefault(filters.get("company"), all_companies) for d in all_companies: if d not in companies: @@ -412,40 +500,77 @@ def get_companies(filters): return all_companies, companies + def get_subsidiary_companies(company): - lft, rgt = frappe.get_cached_value('Company', - company, ["lft", "rgt"]) + lft, rgt = frappe.get_cached_value("Company", company, ["lft", "rgt"]) - return frappe.db.sql_list("""select name from `tabCompany` - where lft >= {0} and rgt <= {1} order by lft, rgt""".format(lft, rgt)) + return frappe.db.sql_list( + """select name from `tabCompany` + where lft >= {0} and rgt <= {1} order by lft, rgt""".format( + lft, rgt + ) + ) -def get_accounts(root_type, filters): - return frappe.db.sql(""" select name, is_group, company, - parent_account, lft, rgt, root_type, report_type, account_name, account_number - from - `tabAccount` where company = %s and root_type = %s - """ , (filters.get('company'), root_type), as_dict=1) -def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters): +def get_accounts(root_type, companies): + accounts = [] + added_accounts = [] + + for company in companies: + for account in frappe.get_all( + "Account", + fields=[ + "name", + "is_group", + "company", + "parent_account", + "lft", + "rgt", + "root_type", + "report_type", + "account_name", + "account_number", + ], + filters={"company": company, "root_type": root_type}, + ): + if account.account_name not in added_accounts: + accounts.append(account) + if account.account_number: + account_key = account.account_number + "-" + account.account_name + else: + account_key = account.account_name + added_accounts.append(account_key) + + return accounts + + +def prepare_data( + accounts, start_date, end_date, balance_must_be, companies, company_currency, filters +): data = [] for d in accounts: # add to output has_value = False total = 0 - row = frappe._dict({ - "account_name": ('%s - %s' %(_(d.account_number), _(d.account_name)) - if d.account_number else _(d.account_name)), - "account": _(d.name), - "parent_account": _(d.parent_account), - "indent": flt(d.indent), - "year_start_date": start_date, - "root_type": d.root_type, - "year_end_date": end_date, - "currency": filters.presentation_currency, - "company_wise_opening_bal": d.company_wise_opening_bal, - "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1) - }) + row = frappe._dict( + { + "account_name": ( + "%s - %s" % (_(d.account_number), _(d.account_name)) + if d.account_number + else _(d.account_name) + ), + "account": _(d.name), + "parent_account": _(d.parent_account), + "indent": flt(d.indent), + "year_start_date": start_date, + "root_type": d.root_type, + "year_end_date": end_date, + "currency": filters.presentation_currency, + "company_wise_opening_bal": d.company_wise_opening_bal, + "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1), + } + ) for company in companies: if d.get(company) and balance_must_be == "Credit": @@ -466,32 +591,49 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com return data -def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account, - accounts_by_name, accounts, ignore_closing_entries=False): + +def set_gl_entries_by_account( + from_date, + to_date, + root_lft, + root_rgt, + filters, + gl_entries_by_account, + accounts_by_name, + accounts, + ignore_closing_entries=False, +): """Returns a dict like { "account": [gl entries], ... }""" - company_lft, company_rgt = frappe.get_cached_value('Company', - filters.get('company'), ["lft", "rgt"]) + company_lft, company_rgt = frappe.get_cached_value( + "Company", filters.get("company"), ["lft", "rgt"] + ) additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters) - companies = frappe.db.sql(""" select name, default_currency from `tabCompany` - where lft >= %(company_lft)s and rgt <= %(company_rgt)s""", { + companies = frappe.db.sql( + """ select name, default_currency from `tabCompany` + where lft >= %(company_lft)s and rgt <= %(company_rgt)s""", + { "company_lft": company_lft, "company_rgt": company_rgt, - }, as_dict=1) + }, + as_dict=1, + ) - currency_info = frappe._dict({ - 'report_date': to_date, - 'presentation_currency': filters.get('presentation_currency') - }) + currency_info = frappe._dict( + {"report_date": to_date, "presentation_currency": filters.get("presentation_currency")} + ) for d in companies: - gl_entries = frappe.db.sql("""select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, + gl_entries = frappe.db.sql( + """select gl.posting_date, gl.account, gl.debit, gl.credit, gl.is_opening, gl.company, gl.fiscal_year, gl.debit_in_account_currency, gl.credit_in_account_currency, gl.account_currency, acc.account_name, acc.account_number from `tabGL Entry` gl, `tabAccount` acc where acc.name = gl.account and gl.company = %(company)s and gl.is_cancelled = 0 {additional_conditions} and gl.posting_date <= %(to_date)s and acc.lft >= %(lft)s and acc.rgt <= %(rgt)s - order by gl.account, gl.posting_date""".format(additional_conditions=additional_conditions), + order by gl.account, gl.posting_date""".format( + additional_conditions=additional_conditions + ), { "from_date": from_date, "to_date": to_date, @@ -499,29 +641,47 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g "rgt": root_rgt, "company": d.name, "finance_book": filters.get("finance_book"), - "company_fb": frappe.db.get_value("Company", d.name, 'default_finance_book') + "company_fb": frappe.db.get_value("Company", d.name, "default_finance_book"), }, - as_dict=True) + as_dict=True, + ) - if filters and filters.get('presentation_currency') != d.default_currency: - currency_info['company'] = d.name - currency_info['company_currency'] = d.default_currency - convert_to_presentation_currency(gl_entries, currency_info, filters.get('company')) + if filters and filters.get("presentation_currency") != d.default_currency: + currency_info["company"] = d.name + currency_info["company_currency"] = d.default_currency + convert_to_presentation_currency(gl_entries, currency_info, filters.get("company")) for entry in gl_entries: if entry.account_number: - account_name = entry.account_number + ' - ' + entry.account_name + account_name = entry.account_number + " - " + entry.account_name else: - account_name = entry.account_name + account_name = entry.account_name validate_entries(account_name, entry, accounts_by_name, accounts) gl_entries_by_account.setdefault(account_name, []).append(entry) return gl_entries_by_account + def get_account_details(account): - return frappe.get_cached_value('Account', account, ['name', 'report_type', 'root_type', 'company', - 'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1) + return frappe.get_cached_value( + "Account", + account, + [ + "name", + "report_type", + "root_type", + "company", + "is_group", + "account_name", + "account_number", + "parent_account", + "lft", + "rgt", + ], + as_dict=1, + ) + def validate_entries(key, entry, accounts_by_name, accounts): # If an account present in the child company and not in the parent company @@ -531,15 +691,17 @@ def validate_entries(key, entry, accounts_by_name, accounts): if args.parent_account: parent_args = get_account_details(args.parent_account) - args.update({ - 'lft': parent_args.lft + 1, - 'rgt': parent_args.rgt - 1, - 'indent': 3, - 'root_type': parent_args.root_type, - 'report_type': parent_args.report_type, - 'parent_account_name': parent_args.account_name, - 'company_wise_opening_bal': defaultdict(float) - }) + args.update( + { + "lft": parent_args.lft + 1, + "rgt": parent_args.rgt - 1, + "indent": 3, + "root_type": parent_args.root_type, + "report_type": parent_args.report_type, + "parent_account_name": parent_args.account_name, + "company_wise_opening_bal": defaultdict(float), + } + ) accounts_by_name.setdefault(key, args) @@ -550,7 +712,8 @@ def validate_entries(key, entry, accounts_by_name, accounts): idx = index break - accounts.insert(idx+1, args) + accounts.insert(idx + 1, args) + def get_additional_conditions(from_date, ignore_closing_entries, filters): additional_conditions = [] @@ -562,17 +725,20 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters): additional_conditions.append("gl.posting_date >= %(from_date)s") if filters.get("include_default_book_entries"): - additional_conditions.append("(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)") + additional_conditions.append( + "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + ) else: additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else "" + def add_total_row(out, root_type, balance_must_be, companies, company_currency): total_row = { "account_name": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", "account": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", - "currency": company_currency + "currency": company_currency, } for row in out: @@ -591,15 +757,16 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency): # blank row after Total out.append({}) + def filter_accounts(accounts, depth=10): parent_children_map = {} accounts_by_name = {} for d in accounts: if d.account_number: - account_name = d.account_number + ' - ' + d.account_name + account_name = d.account_number + " - " + d.account_name else: - account_name = d.account_name - d['company_wise_opening_bal'] = defaultdict(float) + account_name = d.account_name + d["company_wise_opening_bal"] = defaultdict(float) accounts_by_name[account_name] = d parent_children_map.setdefault(d.parent_account or None, []).append(d) @@ -609,7 +776,7 @@ def filter_accounts(accounts, depth=10): def add_to_list(parent, level): if level < depth: children = parent_children_map.get(parent) or [] - sort_accounts(children, is_root=True if parent==None else False) + sort_accounts(children, is_root=True if parent == None else False) for child in children: child.indent = level diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py index 29546198464..3beaa2bfe74 100644 --- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py +++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py @@ -15,14 +15,16 @@ class PartyLedgerSummaryReport(object): self.filters.to_date = getdate(self.filters.to_date or nowdate()) if not self.filters.get("company"): - self.filters["company"] = frappe.db.get_single_value('Global Defaults', 'default_company') + self.filters["company"] = frappe.db.get_single_value("Global Defaults", "default_company") def run(self, args): if self.filters.from_date > self.filters.to_date: frappe.throw(_("From Date must be before To Date")) self.filters.party_type = args.get("party_type") - self.party_naming_by = frappe.db.get_value(args.get("naming_by")[0], None, args.get("naming_by")[1]) + self.party_naming_by = frappe.db.get_value( + args.get("naming_by")[0], None, args.get("naming_by")[1] + ) self.get_gl_entries() self.get_return_invoices() @@ -33,21 +35,25 @@ class PartyLedgerSummaryReport(object): return columns, data def get_columns(self): - columns = [{ - "label": _(self.filters.party_type), - "fieldtype": "Link", - "fieldname": "party", - "options": self.filters.party_type, - "width": 200 - }] + columns = [ + { + "label": _(self.filters.party_type), + "fieldtype": "Link", + "fieldname": "party", + "options": self.filters.party_type, + "width": 200, + } + ] if self.party_naming_by == "Naming Series": - columns.append({ - "label": _(self.filters.party_type + "Name"), - "fieldtype": "Data", - "fieldname": "party_name", - "width": 110 - }) + columns.append( + { + "label": _(self.filters.party_type + "Name"), + "fieldtype": "Data", + "fieldname": "party_name", + "width": 110, + } + ) credit_or_debit_note = "Credit Note" if self.filters.party_type == "Customer" else "Debit Note" @@ -57,40 +63,42 @@ class PartyLedgerSummaryReport(object): "fieldname": "opening_balance", "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "label": _("Invoiced Amount"), "fieldname": "invoiced_amount", "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "label": _("Paid Amount"), "fieldname": "paid_amount", "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "label": _(credit_or_debit_note), "fieldname": "return_amount", "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, ] for account in self.party_adjustment_accounts: - columns.append({ - "label": account, - "fieldname": "adj_" + scrub(account), - "fieldtype": "Currency", - "options": "currency", - "width": 120, - "is_adjustment": 1 - }) + columns.append( + { + "label": account, + "fieldname": "adj_" + scrub(account), + "fieldtype": "Currency", + "options": "currency", + "width": 120, + "is_adjustment": 1, + } + ) columns += [ { @@ -98,36 +106,43 @@ class PartyLedgerSummaryReport(object): "fieldname": "closing_balance", "fieldtype": "Currency", "options": "currency", - "width": 120 + "width": 120, }, { "label": _("Currency"), "fieldname": "currency", "fieldtype": "Link", "options": "Currency", - "width": 50 - } + "width": 50, + }, ] return columns def get_data(self): - company_currency = frappe.get_cached_value('Company', self.filters.get("company"), "default_currency") + company_currency = frappe.get_cached_value( + "Company", self.filters.get("company"), "default_currency" + ) invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit" self.party_data = frappe._dict({}) for gle in self.gl_entries: - self.party_data.setdefault(gle.party, frappe._dict({ - "party": gle.party, - "party_name": gle.party_name, - "opening_balance": 0, - "invoiced_amount": 0, - "paid_amount": 0, - "return_amount": 0, - "closing_balance": 0, - "currency": company_currency - })) + self.party_data.setdefault( + gle.party, + frappe._dict( + { + "party": gle.party, + "party_name": gle.party_name, + "opening_balance": 0, + "invoiced_amount": 0, + "paid_amount": 0, + "return_amount": 0, + "closing_balance": 0, + "currency": company_currency, + } + ), + ) amount = gle.get(invoice_dr_or_cr) - gle.get(reverse_dr_or_cr) self.party_data[gle.party].closing_balance += amount @@ -144,8 +159,16 @@ class PartyLedgerSummaryReport(object): out = [] for party, row in iteritems(self.party_data): - if row.opening_balance or row.invoiced_amount or row.paid_amount or row.return_amount or row.closing_amount: - total_party_adjustment = sum(amount for amount in itervalues(self.party_adjustment_details.get(party, {}))) + if ( + row.opening_balance + or row.invoiced_amount + or row.paid_amount + or row.return_amount + or row.closing_amount + ): + total_party_adjustment = sum( + amount for amount in itervalues(self.party_adjustment_details.get(party, {})) + ) row.paid_amount -= total_party_adjustment adjustments = self.party_adjustment_details.get(party, {}) @@ -166,7 +189,8 @@ class PartyLedgerSummaryReport(object): join_field = ", p.supplier_name as party_name" join = "left join `tabSupplier` p on gle.party = p.name" - self.gl_entries = frappe.db.sql(""" + self.gl_entries = frappe.db.sql( + """ select gle.posting_date, gle.party, gle.voucher_type, gle.voucher_no, gle.against_voucher_type, gle.against_voucher, gle.debit, gle.credit, gle.is_opening {join_field} @@ -176,7 +200,12 @@ class PartyLedgerSummaryReport(object): gle.docstatus < 2 and gle.is_cancelled = 0 and gle.party_type=%(party_type)s and ifnull(gle.party, '') != '' and gle.posting_date <= %(to_date)s {conditions} order by gle.posting_date - """.format(join=join, join_field=join_field, conditions=conditions), self.filters, as_dict=True) + """.format( + join=join, join_field=join_field, conditions=conditions + ), + self.filters, + as_dict=True, + ) def prepare_conditions(self): conditions = [""] @@ -192,57 +221,88 @@ class PartyLedgerSummaryReport(object): if self.filters.party_type == "Customer": if self.filters.get("customer_group"): - lft, rgt = frappe.db.get_value("Customer Group", - self.filters.get("customer_group"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value( + "Customer Group", self.filters.get("customer_group"), ["lft", "rgt"] + ) - conditions.append("""party in (select name from tabCustomer + conditions.append( + """party in (select name from tabCustomer where exists(select name from `tabCustomer Group` where lft >= {0} and rgt <= {1} - and name=tabCustomer.customer_group))""".format(lft, rgt)) + and name=tabCustomer.customer_group))""".format( + lft, rgt + ) + ) if self.filters.get("territory"): - lft, rgt = frappe.db.get_value("Territory", - self.filters.get("territory"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value("Territory", self.filters.get("territory"), ["lft", "rgt"]) - conditions.append("""party in (select name from tabCustomer + conditions.append( + """party in (select name from tabCustomer where exists(select name from `tabTerritory` where lft >= {0} and rgt <= {1} - and name=tabCustomer.territory))""".format(lft, rgt)) + and name=tabCustomer.territory))""".format( + lft, rgt + ) + ) if self.filters.get("payment_terms_template"): - conditions.append("party in (select name from tabCustomer where payment_terms=%(payment_terms_template)s)") + conditions.append( + "party in (select name from tabCustomer where payment_terms=%(payment_terms_template)s)" + ) if self.filters.get("sales_partner"): - conditions.append("party in (select name from tabCustomer where default_sales_partner=%(sales_partner)s)") + conditions.append( + "party in (select name from tabCustomer where default_sales_partner=%(sales_partner)s)" + ) if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) + lft, rgt = frappe.db.get_value( + "Sales Person", self.filters.get("sales_person"), ["lft", "rgt"] + ) - conditions.append("""exists(select name from `tabSales Team` steam where + conditions.append( + """exists(select name from `tabSales Team` steam where steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1}) and ((steam.parent = voucher_no and steam.parenttype = voucher_type) or (steam.parent = against_voucher and steam.parenttype = against_voucher_type) - or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)) + or (steam.parent = party and steam.parenttype = 'Customer')))""".format( + lft, rgt + ) + ) if self.filters.party_type == "Supplier": if self.filters.get("supplier_group"): - conditions.append("""party in (select name from tabSupplier - where supplier_group=%(supplier_group)s)""") + conditions.append( + """party in (select name from tabSupplier + where supplier_group=%(supplier_group)s)""" + ) return " and ".join(conditions) def get_return_invoices(self): doctype = "Sales Invoice" if self.filters.party_type == "Customer" else "Purchase Invoice" - self.return_invoices = [d.name for d in frappe.get_all(doctype, filters={"is_return": 1, "docstatus": 1, - "posting_date": ["between", [self.filters.from_date, self.filters.to_date]]})] + self.return_invoices = [ + d.name + for d in frappe.get_all( + doctype, + filters={ + "is_return": 1, + "docstatus": 1, + "posting_date": ["between", [self.filters.from_date, self.filters.to_date]], + }, + ) + ] def get_party_adjustment_amounts(self): conditions = self.prepare_conditions() - income_or_expense = "Expense Account" if self.filters.party_type == "Customer" else "Income Account" + income_or_expense = ( + "Expense Account" if self.filters.party_type == "Customer" else "Income Account" + ) invoice_dr_or_cr = "debit" if self.filters.party_type == "Customer" else "credit" reverse_dr_or_cr = "credit" if self.filters.party_type == "Customer" else "debit" - round_off_account = frappe.get_cached_value('Company', self.filters.company, "round_off_account") + round_off_account = frappe.get_cached_value("Company", self.filters.company, "round_off_account") - gl_entries = frappe.db.sql(""" + gl_entries = frappe.db.sql( + """ select posting_date, account, party, voucher_type, voucher_no, debit, credit from @@ -258,7 +318,12 @@ class PartyLedgerSummaryReport(object): where gle.party_type=%(party_type)s and ifnull(party, '') != '' and gle.posting_date between %(from_date)s and %(to_date)s and gle.docstatus < 2 {conditions} ) - """.format(conditions=conditions, income_or_expense=income_or_expense), self.filters, as_dict=True) + """.format( + conditions=conditions, income_or_expense=income_or_expense + ), + self.filters, + as_dict=True, + ) self.party_adjustment_details = {} self.party_adjustment_accounts = set() @@ -300,6 +365,7 @@ class PartyLedgerSummaryReport(object): self.party_adjustment_details[party].setdefault(account, 0) self.party_adjustment_details[party][account] += amount + def execute(filters=None): args = { "party_type": "Customer", diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/__init__.py b/erpnext/accounts/report/deferred_revenue_and_expense/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js new file mode 100644 index 00000000000..0056b9e8f56 --- /dev/null +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.js @@ -0,0 +1,114 @@ +// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt +/* eslint-disable */ + +function get_filters() { + let filters = [ + { + "fieldname":"company", + "label": __("Company"), + "fieldtype": "Link", + "options": "Company", + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 + }, + { + "fieldname":"filter_based_on", + "label": __("Filter Based On"), + "fieldtype": "Select", + "options": ["Fiscal Year", "Date Range"], + "default": ["Fiscal Year"], + "reqd": 1, + on_change: function() { + let filter_based_on = frappe.query_report.get_filter_value('filter_based_on'); + frappe.query_report.toggle_filter_display('from_fiscal_year', filter_based_on === 'Date Range'); + frappe.query_report.toggle_filter_display('to_fiscal_year', filter_based_on === 'Date Range'); + frappe.query_report.toggle_filter_display('period_start_date', filter_based_on === 'Fiscal Year'); + frappe.query_report.toggle_filter_display('period_end_date', filter_based_on === 'Fiscal Year'); + + frappe.query_report.refresh(); + } + }, + { + "fieldname":"period_start_date", + "label": __("Start Date"), + "fieldtype": "Date", + "hidden": 1, + "reqd": 1 + }, + { + "fieldname":"period_end_date", + "label": __("End Date"), + "fieldtype": "Date", + "hidden": 1, + "reqd": 1 + }, + { + "fieldname":"from_fiscal_year", + "label": __("Start Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "reqd": 1 + }, + { + "fieldname":"to_fiscal_year", + "label": __("End Year"), + "fieldtype": "Link", + "options": "Fiscal Year", + "default": frappe.defaults.get_user_default("fiscal_year"), + "reqd": 1 + }, + { + "fieldname": "periodicity", + "label": __("Periodicity"), + "fieldtype": "Select", + "options": [ + { "value": "Monthly", "label": __("Monthly") }, + { "value": "Quarterly", "label": __("Quarterly") }, + { "value": "Half-Yearly", "label": __("Half-Yearly") }, + { "value": "Yearly", "label": __("Yearly") } + ], + "default": "Monthly", + "reqd": 1 + }, + { + "fieldname": "type", + "label": __("Invoice Type"), + "fieldtype": "Select", + "options": [ + { "value": "Revenue", "label": __("Revenue") }, + { "value": "Expense", "label": __("Expense") } + ], + "default": "Revenue", + "reqd": 1 + }, + { + "fieldname" : "with_upcoming_postings", + "label": __("Show with upcoming revenue/expense"), + "fieldtype": "Check", + "default": 1 + } + ] + + return filters; +} + +frappe.query_reports["Deferred Revenue and Expense"] = { + "filters": get_filters(), + "formatter": function(value, row, column, data, default_formatter){ + return default_formatter(value, row, column, data); + }, + onload: function(report){ + let fiscal_year = frappe.defaults.get_user_default("fiscal_year"); + + frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) { + var fy = frappe.model.get_doc("Fiscal Year", fiscal_year); + frappe.query_report.set_filter_value({ + period_start_date: fy.year_start_date, + period_end_date: fy.year_end_date + }); + }); + } +}; + diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.json b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.json new file mode 100644 index 00000000000..c7dfb3b7142 --- /dev/null +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.json @@ -0,0 +1,32 @@ +{ + "add_total_row": 0, + "columns": [], + "creation": "2021-12-10 19:27:14.654220", + "disable_prepared_report": 0, + "disabled": 0, + "docstatus": 0, + "doctype": "Report", + "filters": [], + "idx": 0, + "is_standard": "Yes", + "modified": "2021-12-10 19:27:14.654220", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Deferred Revenue and Expense", + "owner": "Administrator", + "prepared_report": 0, + "ref_doctype": "GL Entry", + "report_name": "Deferred Revenue and Expense", + "report_type": "Script Report", + "roles": [ + { + "role": "Accounts User" + }, + { + "role": "Accounts Manager" + }, + { + "role": "Auditor" + } + ] +} \ No newline at end of file diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py new file mode 100644 index 00000000000..1eb257ac853 --- /dev/null +++ b/erpnext/accounts/report/deferred_revenue_and_expense/deferred_revenue_and_expense.py @@ -0,0 +1,440 @@ +# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors +# License: MIT. See LICENSE + +import frappe +from frappe import _, qb +from frappe.query_builder import Column, functions +from frappe.utils import add_days, date_diff, flt, get_first_day, get_last_day, rounded + +from erpnext.accounts.report.financial_statements import get_period_list + + +class Deferred_Item(object): + """ + Helper class for processing items with deferred revenue/expense + """ + + def __init__(self, item, inv, gle_entries): + self.name = item + self.parent = inv.name + self.item_name = gle_entries[0].item_name + self.service_start_date = gle_entries[0].service_start_date + self.service_end_date = gle_entries[0].service_end_date + self.base_net_amount = gle_entries[0].base_net_amount + self.filters = inv.filters + self.period_list = inv.period_list + + if gle_entries[0].deferred_revenue_account: + self.type = "Deferred Sale Item" + self.deferred_account = gle_entries[0].deferred_revenue_account + elif gle_entries[0].deferred_expense_account: + self.type = "Deferred Purchase Item" + self.deferred_account = gle_entries[0].deferred_expense_account + + self.gle_entries = [] + # holds period wise total for item + self.period_total = [] + self.last_entry_date = self.service_start_date + + if gle_entries: + self.gle_entries = gle_entries + for x in self.gle_entries: + if self.get_amount(x): + self.last_entry_date = x.gle_posting_date + + def report_data(self): + """ + Generate report data for output + """ + ret_data = frappe._dict({"name": self.item_name}) + for period in self.period_total: + ret_data[period.key] = period.total + ret_data.indent = 1 + return ret_data + + def get_amount(self, entry): + """ + For a given GL/Journal posting, get balance based on item type + """ + if self.type == "Deferred Sale Item": + return entry.debit - entry.credit + elif self.type == "Deferred Purchase Item": + return -(entry.credit - entry.debit) + return 0 + + def get_item_total(self): + """ + Helper method - calculate booked amount. Includes simulated postings as well + """ + total = 0 + for gle_posting in self.gle_entries: + total += self.get_amount(gle_posting) + + return total + + def calculate_amount(self, start_date, end_date): + """ + start_date, end_date - datetime.datetime.date + return - estimated amount to post for given period + Calculated based on already booked amount and item service period + """ + total_months = ( + (self.service_end_date.year - self.service_start_date.year) * 12 + + (self.service_end_date.month - self.service_start_date.month) + + 1 + ) + + prorate = date_diff(self.service_end_date, self.service_start_date) / date_diff( + get_last_day(self.service_end_date), get_first_day(self.service_start_date) + ) + + actual_months = rounded(total_months * prorate, 1) + + already_booked_amount = self.get_item_total() + base_amount = self.base_net_amount / actual_months + + if base_amount + already_booked_amount > self.base_net_amount: + base_amount = self.base_net_amount - already_booked_amount + + if not (get_first_day(start_date) == start_date and get_last_day(end_date) == end_date): + partial_month = flt(date_diff(end_date, start_date)) / flt( + date_diff(get_last_day(end_date), get_first_day(start_date)) + ) + base_amount *= rounded(partial_month, 1) + + return base_amount + + def make_dummy_gle(self, name, date, amount): + """ + return - frappe._dict() of a dummy gle entry + """ + entry = frappe._dict( + {"name": name, "gle_posting_date": date, "debit": 0, "credit": 0, "posted": "not"} + ) + if self.type == "Deferred Sale Item": + entry.debit = amount + elif self.type == "Deferred Purchase Item": + entry.credit = amount + return entry + + def simulate_future_posting(self): + """ + simulate future posting by creating dummy gl entries. starts from the last posting date. + """ + if self.service_start_date != self.service_end_date: + if add_days(self.last_entry_date, 1) < self.period_list[-1].to_date: + self.estimate_for_period_list = get_period_list( + self.filters.from_fiscal_year, + self.filters.to_fiscal_year, + add_days(self.last_entry_date, 1), + self.period_list[-1].to_date, + "Date Range", + "Monthly", + company=self.filters.company, + ) + for period in self.estimate_for_period_list: + amount = self.calculate_amount(period.from_date, period.to_date) + gle = self.make_dummy_gle(period.key, period.to_date, amount) + self.gle_entries.append(gle) + + def calculate_item_revenue_expense_for_period(self): + """ + calculate item postings for each period and update period_total list + """ + for period in self.period_list: + period_sum = 0 + actual = 0 + for posting in self.gle_entries: + # if period.from_date <= posting.posting_date <= period.to_date: + if period.from_date <= posting.gle_posting_date <= period.to_date: + period_sum += self.get_amount(posting) + if posting.posted == "posted": + actual += self.get_amount(posting) + + self.period_total.append( + frappe._dict({"key": period.key, "total": period_sum, "actual": actual}) + ) + return self.period_total + + +class Deferred_Invoice(object): + def __init__(self, invoice, items, filters, period_list): + """ + Helper class for processing invoices with deferred revenue/expense items + invoice - string : invoice name + items - list : frappe._dict() with item details. Refer Deferred_Item for required fields + """ + self.name = invoice + self.posting_date = items[0].posting_date + self.filters = filters + self.period_list = period_list + # holds period wise total for invoice + self.period_total = [] + + if items[0].deferred_revenue_account: + self.type = "Sales" + elif items[0].deferred_expense_account: + self.type = "Purchase" + + self.items = [] + # for each uniq items + self.uniq_items = set([x.item for x in items]) + for item in self.uniq_items: + self.items.append(Deferred_Item(item, self, [x for x in items if x.item == item])) + + def calculate_invoice_revenue_expense_for_period(self): + """ + calculate deferred revenue/expense for all items in invoice + """ + # initialize period_total list for invoice + for period in self.period_list: + self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0})) + + for item in self.items: + item_total = item.calculate_item_revenue_expense_for_period() + # update invoice total + for idx, period in enumerate(self.period_list, 0): + self.period_total[idx].total += item_total[idx].total + self.period_total[idx].actual += item_total[idx].actual + return self.period_total + + def estimate_future(self): + """ + create dummy GL entries for upcoming months for all items in invoice + """ + [item.simulate_future_posting() for item in self.items] + + def report_data(self): + """ + generate report data for invoice, includes invoice total + """ + ret_data = [] + inv_total = frappe._dict({"name": self.name}) + for x in self.period_total: + inv_total[x.key] = x.total + inv_total.indent = 0 + ret_data.append(inv_total) + list(map(lambda item: ret_data.append(item.report_data()), self.items)) + return ret_data + + +class Deferred_Revenue_and_Expense_Report(object): + def __init__(self, filters=None): + """ + Initialize deferred revenue/expense report with user provided filters or system defaults, if none is provided + """ + + # If no filters are provided, get user defaults + if not filters: + fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + self.filters = frappe._dict( + { + "company": frappe.defaults.get_user_default("Company"), + "filter_based_on": "Fiscal Year", + "period_start_date": fiscal_year.year_start_date, + "period_end_date": fiscal_year.year_end_date, + "from_fiscal_year": fiscal_year.year, + "to_fiscal_year": fiscal_year.year, + "periodicity": "Monthly", + "type": "Revenue", + "with_upcoming_postings": True, + } + ) + else: + self.filters = frappe._dict(filters) + + self.period_list = None + self.deferred_invoices = [] + # holds period wise total for report + self.period_total = [] + + def get_period_list(self): + """ + Figure out selected period based on filters + """ + self.period_list = get_period_list( + self.filters.from_fiscal_year, + self.filters.to_fiscal_year, + self.filters.period_start_date, + self.filters.period_end_date, + self.filters.filter_based_on, + self.filters.periodicity, + company=self.filters.company, + ) + + def get_invoices(self): + """ + Get all sales and purchase invoices which has deferred revenue/expense items + """ + gle = qb.DocType("GL Entry") + # column doesn't have an alias option + posted = Column("posted") + + if self.filters.type == "Revenue": + inv = qb.DocType("Sales Invoice") + inv_item = qb.DocType("Sales Invoice Item") + deferred_flag_field = inv_item["enable_deferred_revenue"] + deferred_account_field = inv_item["deferred_revenue_account"] + + elif self.filters.type == "Expense": + inv = qb.DocType("Purchase Invoice") + inv_item = qb.DocType("Purchase Invoice Item") + deferred_flag_field = inv_item["enable_deferred_expense"] + deferred_account_field = inv_item["deferred_expense_account"] + + query = ( + qb.from_(inv_item) + .join(inv) + .on(inv.name == inv_item.parent) + .join(gle) + .on((inv_item.name == gle.voucher_detail_no) & (deferred_account_field == gle.account)) + .select( + inv.name.as_("doc"), + inv.posting_date, + inv_item.name.as_("item"), + inv_item.item_name, + inv_item.service_start_date, + inv_item.service_end_date, + inv_item.base_net_amount, + deferred_account_field, + gle.posting_date.as_("gle_posting_date"), + functions.Sum(gle.debit).as_("debit"), + functions.Sum(gle.credit).as_("credit"), + posted, + ) + .where( + (inv.docstatus == 1) + & (deferred_flag_field == 1) + & ( + ( + (self.period_list[0].from_date >= inv_item.service_start_date) + & (inv_item.service_end_date >= self.period_list[0].from_date) + ) + | ( + (inv_item.service_start_date >= self.period_list[0].from_date) + & (inv_item.service_start_date <= self.period_list[-1].to_date) + ) + ) + ) + .groupby(inv.name, inv_item.name, gle.posting_date) + .orderby(gle.posting_date) + ) + self.invoices = query.run(as_dict=True) + + uniq_invoice = set([x.doc for x in self.invoices]) + for inv in uniq_invoice: + self.deferred_invoices.append( + Deferred_Invoice( + inv, [x for x in self.invoices if x.doc == inv], self.filters, self.period_list + ) + ) + + def estimate_future(self): + """ + For all Invoices estimate upcoming postings + """ + for x in self.deferred_invoices: + x.estimate_future() + + def calculate_revenue_and_expense(self): + """ + calculate the deferred revenue/expense for all invoices + """ + # initialize period_total list for report + for period in self.period_list: + self.period_total.append(frappe._dict({"key": period.key, "total": 0, "actual": 0})) + + for inv in self.deferred_invoices: + inv_total = inv.calculate_invoice_revenue_expense_for_period() + # calculate total for whole report + for idx, period in enumerate(self.period_list, 0): + self.period_total[idx].total += inv_total[idx].total + self.period_total[idx].actual += inv_total[idx].actual + + def get_columns(self): + columns = [] + columns.append({"label": _("Name"), "fieldname": "name", "fieldtype": "Data", "read_only": 1}) + for period in self.period_list: + columns.append( + { + "label": _(period.label), + "fieldname": period.key, + "fieldtype": "Currency", + "read_only": 1, + } + ) + return columns + + def generate_report_data(self): + """ + Generate report data for all invoices. Adds total rows for revenue and expense + """ + ret = [] + + for inv in self.deferred_invoices: + ret += inv.report_data() + + # empty row for padding + ret += [{}] + + # add total row + if ret is not []: + if self.filters.type == "Revenue": + total_row = frappe._dict({"name": "Total Deferred Income"}) + elif self.filters.type == "Expense": + total_row = frappe._dict({"name": "Total Deferred Expense"}) + + for idx, period in enumerate(self.period_list, 0): + total_row[period.key] = self.period_total[idx].total + ret.append(total_row) + + return ret + + def prepare_chart(self): + chart = { + "data": { + "labels": [period.label for period in self.period_list], + "datasets": [ + { + "name": "Actual Posting", + "chartType": "bar", + "values": [x.actual for x in self.period_total], + } + ], + }, + "type": "axis-mixed", + "height": 500, + "axisOptions": {"xAxisMode": "Tick", "xIsSeries": True}, + "barOptions": {"stacked": False, "spaceRatio": 0.5}, + } + + if self.filters.with_upcoming_postings: + chart["data"]["datasets"].append( + {"name": "Expected", "chartType": "line", "values": [x.total for x in self.period_total]} + ) + + return chart + + def run(self, *args, **kwargs): + """ + Run report and generate data + """ + self.deferred_invoices.clear() + self.get_period_list() + self.get_invoices() + + if self.filters.with_upcoming_postings: + self.estimate_future() + self.calculate_revenue_and_expense() + + +def execute(filters=None): + report = Deferred_Revenue_and_Expense_Report(filters=filters) + report.run() + + columns = report.get_columns() + data = report.generate_report_data() + message = [] + chart = report.prepare_chart() + + return columns, data, message, chart diff --git a/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py new file mode 100644 index 00000000000..023ff225eea --- /dev/null +++ b/erpnext/accounts/report/deferred_revenue_and_expense/test_deferred_revenue_and_expense.py @@ -0,0 +1,350 @@ +import unittest + +import frappe +from frappe import qb +from frappe.utils import nowdate + +from erpnext.accounts.doctype.account.test_account import create_account +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice +from erpnext.accounts.report.deferred_revenue_and_expense.deferred_revenue_and_expense import ( + Deferred_Revenue_and_Expense_Report, +) +from erpnext.buying.doctype.supplier.test_supplier import create_supplier +from erpnext.stock.doctype.item.test_item import create_item + + +class TestDeferredRevenueAndExpense(unittest.TestCase): + @classmethod + def setUpClass(self): + clear_accounts_and_items() + create_company() + self.maxDiff = None + + def clear_old_entries(self): + sinv = qb.DocType("Sales Invoice") + sinv_item = qb.DocType("Sales Invoice Item") + pinv = qb.DocType("Purchase Invoice") + pinv_item = qb.DocType("Purchase Invoice Item") + + # delete existing invoices with deferred items + deferred_invoices = ( + qb.from_(sinv) + .join(sinv_item) + .on(sinv.name == sinv_item.parent) + .select(sinv.name) + .where(sinv_item.enable_deferred_revenue == 1) + .run() + ) + if deferred_invoices: + qb.from_(sinv).delete().where(sinv.name.isin(deferred_invoices)).run() + + deferred_invoices = ( + qb.from_(pinv) + .join(pinv_item) + .on(pinv.name == pinv_item.parent) + .select(pinv.name) + .where(pinv_item.enable_deferred_expense == 1) + .run() + ) + if deferred_invoices: + qb.from_(pinv).delete().where(pinv.name.isin(deferred_invoices)).run() + + def test_deferred_revenue(self): + self.clear_old_entries() + + # created deferred expense accounts, if not found + deferred_revenue_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - _CD", + company="_Test Company DR", + ) + + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") + acc_settings.book_deferred_entries_based_on = "Months" + acc_settings.save() + + customer = frappe.new_doc("Customer") + customer.customer_name = "_Test Customer DR" + customer.type = "Individual" + customer.insert() + + item = create_item( + "_Test Internet Subscription", + is_stock_item=0, + warehouse="All Warehouses - _CD", + company="_Test Company DR", + ) + item.enable_deferred_revenue = 1 + item.deferred_revenue_account = deferred_revenue_account + item.no_of_months = 3 + item.save() + + si = create_sales_invoice( + item=item.name, + company="_Test Company DR", + customer="_Test Customer DR", + debit_to="Debtors - _CD", + posting_date="2021-05-01", + parent_cost_center="Main - _CD", + cost_center="Main - _CD", + do_not_save=True, + rate=300, + price_list_rate=300, + ) + + si.items[0].income_account = "Sales - _CD" + si.items[0].enable_deferred_revenue = 1 + si.items[0].service_start_date = "2021-05-01" + si.items[0].service_end_date = "2021-08-01" + si.items[0].deferred_revenue_account = deferred_revenue_account + si.items[0].income_account = "Sales - _CD" + si.save() + si.submit() + + pda = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2021-05-01", + end_date="2021-08-01", + type="Income", + company="_Test Company DR", + ) + ) + pda.insert() + pda.submit() + + # execute report + fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + self.filters = frappe._dict( + { + "company": frappe.defaults.get_user_default("Company"), + "filter_based_on": "Date Range", + "period_start_date": "2021-05-01", + "period_end_date": "2021-08-01", + "from_fiscal_year": fiscal_year.year, + "to_fiscal_year": fiscal_year.year, + "periodicity": "Monthly", + "type": "Revenue", + "with_upcoming_postings": False, + } + ) + + report = Deferred_Revenue_and_Expense_Report(filters=self.filters) + report.run() + expected = [ + {"key": "may_2021", "total": 100.0, "actual": 100.0}, + {"key": "jun_2021", "total": 100.0, "actual": 100.0}, + {"key": "jul_2021", "total": 100.0, "actual": 100.0}, + {"key": "aug_2021", "total": 0, "actual": 0}, + ] + self.assertEqual(report.period_total, expected) + + def test_deferred_expense(self): + self.clear_old_entries() + + # created deferred expense accounts, if not found + deferred_expense_account = create_account( + account_name="Deferred Expense", + parent_account="Current Assets - _CD", + company="_Test Company DR", + ) + + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") + acc_settings.book_deferred_entries_based_on = "Months" + acc_settings.save() + + supplier = create_supplier( + supplier_name="_Test Furniture Supplier", supplier_group="Local", supplier_type="Company" + ) + supplier.save() + + item = create_item( + "_Test Office Desk", + is_stock_item=0, + warehouse="All Warehouses - _CD", + company="_Test Company DR", + ) + item.enable_deferred_expense = 1 + item.deferred_expense_account = deferred_expense_account + item.no_of_months_exp = 3 + item.save() + + pi = make_purchase_invoice( + item=item.name, + company="_Test Company DR", + supplier="_Test Furniture Supplier", + is_return=False, + update_stock=False, + posting_date=frappe.utils.datetime.date(2021, 5, 1), + parent_cost_center="Main - _CD", + cost_center="Main - _CD", + do_not_save=True, + rate=300, + price_list_rate=300, + warehouse="All Warehouses - _CD", + qty=1, + ) + pi.set_posting_time = True + pi.items[0].enable_deferred_expense = 1 + pi.items[0].service_start_date = "2021-05-01" + pi.items[0].service_end_date = "2021-08-01" + pi.items[0].deferred_expense_account = deferred_expense_account + pi.items[0].expense_account = "Office Maintenance Expenses - _CD" + pi.save() + pi.submit() + + pda = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2021-05-01", + end_date="2021-08-01", + type="Expense", + company="_Test Company DR", + ) + ) + pda.insert() + pda.submit() + + # execute report + fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + self.filters = frappe._dict( + { + "company": frappe.defaults.get_user_default("Company"), + "filter_based_on": "Date Range", + "period_start_date": "2021-05-01", + "period_end_date": "2021-08-01", + "from_fiscal_year": fiscal_year.year, + "to_fiscal_year": fiscal_year.year, + "periodicity": "Monthly", + "type": "Expense", + "with_upcoming_postings": False, + } + ) + + report = Deferred_Revenue_and_Expense_Report(filters=self.filters) + report.run() + expected = [ + {"key": "may_2021", "total": -100.0, "actual": -100.0}, + {"key": "jun_2021", "total": -100.0, "actual": -100.0}, + {"key": "jul_2021", "total": -100.0, "actual": -100.0}, + {"key": "aug_2021", "total": 0, "actual": 0}, + ] + self.assertEqual(report.period_total, expected) + + def test_zero_months(self): + self.clear_old_entries() + # created deferred expense accounts, if not found + deferred_revenue_account = create_account( + account_name="Deferred Revenue", + parent_account="Current Liabilities - _CD", + company="_Test Company DR", + ) + + acc_settings = frappe.get_doc("Accounts Settings", "Accounts Settings") + acc_settings.book_deferred_entries_based_on = "Months" + acc_settings.save() + + customer = frappe.new_doc("Customer") + customer.customer_name = "_Test Customer DR" + customer.type = "Individual" + customer.insert() + + item = create_item( + "_Test Internet Subscription", + is_stock_item=0, + warehouse="All Warehouses - _CD", + company="_Test Company DR", + ) + item.enable_deferred_revenue = 1 + item.deferred_revenue_account = deferred_revenue_account + item.no_of_months = 0 + item.save() + + si = create_sales_invoice( + item=item.name, + company="_Test Company DR", + customer="_Test Customer DR", + debit_to="Debtors - _CD", + posting_date="2021-05-01", + parent_cost_center="Main - _CD", + cost_center="Main - _CD", + do_not_save=True, + rate=300, + price_list_rate=300, + ) + + si.items[0].enable_deferred_revenue = 1 + si.items[0].income_account = "Sales - _CD" + si.items[0].deferred_revenue_account = deferred_revenue_account + si.items[0].income_account = "Sales - _CD" + si.save() + si.submit() + + pda = frappe.get_doc( + dict( + doctype="Process Deferred Accounting", + posting_date=nowdate(), + start_date="2021-05-01", + end_date="2021-08-01", + type="Income", + company="_Test Company DR", + ) + ) + pda.insert() + pda.submit() + + # execute report + fiscal_year = frappe.get_doc("Fiscal Year", frappe.defaults.get_user_default("fiscal_year")) + self.filters = frappe._dict( + { + "company": frappe.defaults.get_user_default("Company"), + "filter_based_on": "Date Range", + "period_start_date": "2021-05-01", + "period_end_date": "2021-08-01", + "from_fiscal_year": fiscal_year.year, + "to_fiscal_year": fiscal_year.year, + "periodicity": "Monthly", + "type": "Revenue", + "with_upcoming_postings": False, + } + ) + + report = Deferred_Revenue_and_Expense_Report(filters=self.filters) + report.run() + expected = [ + {"key": "may_2021", "total": 300.0, "actual": 300.0}, + {"key": "jun_2021", "total": 0, "actual": 0}, + {"key": "jul_2021", "total": 0, "actual": 0}, + {"key": "aug_2021", "total": 0, "actual": 0}, + ] + self.assertEqual(report.period_total, expected) + + +def create_company(): + company = frappe.db.exists("Company", "_Test Company DR") + if not company: + company = frappe.new_doc("Company") + company.company_name = "_Test Company DR" + company.default_currency = "INR" + company.chart_of_accounts = "Standard" + company.insert() + + +def clear_accounts_and_items(): + item = qb.DocType("Item") + account = qb.DocType("Account") + customer = qb.DocType("Customer") + supplier = qb.DocType("Supplier") + + qb.from_(account).delete().where( + (account.account_name == "Deferred Revenue") + | (account.account_name == "Deferred Expense") & (account.company == "_Test Company DR") + ).run() + qb.from_(item).delete().where( + (item.item_code == "_Test Internet Subscription") | (item.item_code == "_Test Office Rent") + ).run() + qb.from_(customer).delete().where(customer.customer_name == "_Test Customer DR").run() + qb.from_(supplier).delete().where(supplier.supplier_name == "_Test Furniture Supplier").run() diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py index 004d09250ab..59914dc29ac 100644 --- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py +++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py @@ -13,6 +13,7 @@ def execute(filters=None): data = get_ordered_to_be_billed_data(args) return columns, data + def get_column(): return [ { @@ -20,90 +21,76 @@ def get_column(): "fieldname": "name", "fieldtype": "Link", "options": "Delivery Note", - "width": 160 - }, - { - "label": _("Date"), - "fieldname": "date", - "fieldtype": "Date", - "width": 100 + "width": 160, }, + {"label": _("Date"), "fieldname": "date", "fieldtype": "Date", "width": 100}, { "label": _("Customer"), "fieldname": "customer", "fieldtype": "Link", "options": "Customer", - "width": 120 - }, - { - "label": _("Customer Name"), - "fieldname": "customer_name", - "fieldtype": "Data", - "width": 120 + "width": 120, }, + {"label": _("Customer Name"), "fieldname": "customer_name", "fieldtype": "Data", "width": 120}, { "label": _("Item Code"), "fieldname": "item_code", "fieldtype": "Link", "options": "Item", - "width": 120 + "width": 120, }, { "label": _("Amount"), "fieldname": "amount", "fieldtype": "Currency", "width": 100, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", }, { "label": _("Billed Amount"), "fieldname": "billed_amount", "fieldtype": "Currency", "width": 100, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", }, { "label": _("Returned Amount"), "fieldname": "returned_amount", "fieldtype": "Currency", "width": 120, - "options": "Company:company:default_currency" + "options": "Company:company:default_currency", }, { "label": _("Pending Amount"), "fieldname": "pending_amount", "fieldtype": "Currency", "width": 120, - "options": "Company:company:default_currency" - }, - { - "label": _("Item Name"), - "fieldname": "item_name", - "fieldtype": "Data", - "width": 120 - }, - { - "label": _("Description"), - "fieldname": "description", - "fieldtype": "Data", - "width": 120 + "options": "Company:company:default_currency", }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 120}, { "label": _("Project"), "fieldname": "project", "fieldtype": "Link", "options": "Project", - "width": 120 + "width": 120, }, { "label": _("Company"), "fieldname": "company", "fieldtype": "Link", "options": "Company", - "width": 120 - } + "width": 120, + }, ] + def get_args(): - return {'doctype': 'Delivery Note', 'party': 'customer', - 'date': 'posting_date', 'order': 'name', 'order_by': 'desc'} + return { + "doctype": "Delivery Note", + "party": "customer", + "date": "posting_date", + "order": "name", + "order_by": "desc", + } diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js index 6a0394861b8..ea05a35b259 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js @@ -39,12 +39,14 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "label": __("From Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_start_date"), + "reqd": 1 }, { "fieldname": "to_date", "label": __("To Date"), "fieldtype": "Date", "default": frappe.defaults.get_user_default("year_end_date"), + "reqd": 1 }, { "fieldname": "finance_book", @@ -56,6 +58,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { "fieldname": "dimension", "label": __("Select Dimension"), "fieldtype": "Select", + "default": "Cost Center", "options": get_accounting_dimension_options(), "reqd": 1, }, @@ -70,7 +73,7 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { }); function get_accounting_dimension_options() { - let options =["", "Cost Center", "Project"]; + let options =["Cost Center", "Project"]; frappe.db.get_list('Accounting Dimension', {fields:['document_type']}).then((res) => { res.forEach((dimension) => { diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py index c69bb3f70c5..9bc4c4b71da 100644 --- a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py +++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py @@ -16,21 +16,24 @@ from erpnext.accounts.report.trial_balance.trial_balance import validate_filters def execute(filters=None): - validate_filters(filters) - dimension_items_list = get_dimension_items_list(filters.dimension, filters.company) - if not dimension_items_list: + validate_filters(filters) + dimension_list = get_dimensions(filters) + + if not dimension_list: return [], [] - dimension_items_list = [''.join(d) for d in dimension_items_list] - columns = get_columns(dimension_items_list) - data = get_data(filters, dimension_items_list) + columns = get_columns(dimension_list) + data = get_data(filters, dimension_list) return columns, data -def get_data(filters, dimension_items_list): + +def get_data(filters, dimension_list): company_currency = erpnext.get_company_currency(filters.company) - acc = frappe.db.sql(""" + + acc = frappe.db.sql( + """ select name, account_number, parent_account, lft, rgt, root_type, report_type, account_name, include_in_gross, account_type, is_group @@ -38,88 +41,104 @@ def get_data(filters, dimension_items_list): `tabAccount` where company=%s - order by lft""", (filters.company), as_dict=True) + order by lft""", + (filters.company), + as_dict=True, + ) if not acc: return None accounts, accounts_by_name, parent_children_map = filter_accounts(acc) - min_lft, max_rgt = frappe.db.sql("""select min(lft), max(rgt) from `tabAccount` - where company=%s""", (filters.company))[0] + min_lft, max_rgt = frappe.db.sql( + """select min(lft), max(rgt) from `tabAccount` + where company=%s""", + (filters.company), + )[0] - account = frappe.db.sql_list("""select name from `tabAccount` - where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company)) + account = frappe.db.sql_list( + """select name from `tabAccount` + where lft >= %s and rgt <= %s and company = %s""", + (min_lft, max_rgt, filters.company), + ) gl_entries_by_account = {} - set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account) - format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list) - accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list) - out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list) + set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account) + format_gl_entries( + gl_entries_by_account, accounts_by_name, dimension_list, frappe.scrub(filters.get("dimension")) + ) + accumulate_values_into_parents(accounts, accounts_by_name, dimension_list) + out = prepare_data(accounts, filters, company_currency, dimension_list) out = filter_out_zero_value_rows(out, parent_children_map) return out -def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account): - for item in dimension_items_list: - condition = get_condition(filters.from_date, item, filters.dimension) - if account: - condition += " and account in ({})"\ - .format(", ".join([frappe.db.escape(d) for d in account])) - gl_filters = { - "company": filters.get("company"), - "from_date": filters.get("from_date"), - "to_date": filters.get("to_date"), - "finance_book": cstr(filters.get("finance_book")) - } +def set_gl_entries_by_account(dimension_list, filters, account, gl_entries_by_account): + condition = get_condition(filters.get("dimension")) - gl_filters['item'] = ''.join(item) + if account: + condition += " and account in ({})".format(", ".join([frappe.db.escape(d) for d in account])) - if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.db.get_value("Company", - filters.company, 'default_finance_book') + gl_filters = { + "company": filters.get("company"), + "from_date": filters.get("from_date"), + "to_date": filters.get("to_date"), + "finance_book": cstr(filters.get("finance_book")), + } - for key, value in filters.items(): - if value: - gl_filters.update({ - key: value - }) + gl_filters["dimensions"] = set(dimension_list) - gl_entries = frappe.db.sql(""" + if filters.get("include_default_book_entries"): + gl_filters["company_fb"] = frappe.db.get_value( + "Company", filters.company, "default_finance_book" + ) + + gl_entries = frappe.db.sql( + """ select - posting_date, account, debit, credit, is_opening, fiscal_year, + posting_date, account, {dimension}, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` where company=%(company)s {condition} + and posting_date >= %(from_date)s and posting_date <= %(to_date)s and is_cancelled = 0 order by account, posting_date""".format( - condition=condition), - gl_filters, as_dict=True) #nosec + dimension=frappe.scrub(filters.get("dimension")), condition=condition + ), + gl_filters, + as_dict=True, + ) # nosec - for entry in gl_entries: - entry['dimension_item'] = ''.join(item) - gl_entries_by_account.setdefault(entry.account, []).append(entry) + for entry in gl_entries: + gl_entries_by_account.setdefault(entry.account, []).append(entry) -def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list): + +def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_list, dimension_type): for entries in itervalues(gl_entries_by_account): for entry in entries: d = accounts_by_name.get(entry.account) if not d: frappe.msgprint( - _("Could not retrieve information for {0}.").format(entry.account), title="Error", - raise_exception=1 + _("Could not retrieve information for {0}.").format(entry.account), + title="Error", + raise_exception=1, ) - for item in dimension_items_list: - if item == entry.dimension_item: - d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit) -def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list): + for dimension in dimension_list: + if dimension == entry.get(dimension_type): + d[frappe.scrub(dimension)] = ( + d.get(frappe.scrub(dimension), 0.0) + flt(entry.debit) - flt(entry.credit) + ) + + +def prepare_data(accounts, filters, company_currency, dimension_list): data = [] for d in accounts: @@ -132,17 +151,18 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen "from_date": filters.from_date, "to_date": filters.to_date, "currency": company_currency, - "account_name": ('{} - {}'.format(d.account_number, d.account_name) - if d.account_number else d.account_name) + "account_name": ( + "{} - {}".format(d.account_number, d.account_name) if d.account_number else d.account_name + ), } - for item in dimension_items_list: - row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3) + for dimension in dimension_list: + row[frappe.scrub(dimension)] = flt(d.get(frappe.scrub(dimension), 0.0), 3) - if abs(row[frappe.scrub(item)]) >= 0.005: + if abs(row[frappe.scrub(dimension)]) >= 0.005: # ignore zero values has_value = True - total += flt(d.get(frappe.scrub(item), 0.0), 3) + total += flt(d.get(frappe.scrub(dimension), 0.0), 3) row["has_value"] = has_value row["total"] = total @@ -150,68 +170,72 @@ def prepare_data(accounts, filters, parent_children_map, company_currency, dimen return data -def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list): + +def accumulate_values_into_parents(accounts, accounts_by_name, dimension_list): """accumulate children's values in parent accounts""" for d in reversed(accounts): if d.parent_account: - for item in dimension_items_list: - accounts_by_name[d.parent_account][frappe.scrub(item)] = \ - accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0) + for dimension in dimension_list: + accounts_by_name[d.parent_account][frappe.scrub(dimension)] = accounts_by_name[ + d.parent_account + ].get(frappe.scrub(dimension), 0.0) + d.get(frappe.scrub(dimension), 0.0) -def get_condition(from_date, item, dimension): + +def get_condition(dimension): conditions = [] - if from_date: - conditions.append("posting_date >= %(from_date)s") - if dimension: - if dimension not in ['Cost Center', 'Project']: - if dimension in ['Customer', 'Supplier']: - dimension = 'Party' - else: - dimension = 'Voucher No' - txt = "{0} = %(item)s".format(frappe.scrub(dimension)) - conditions.append(txt) + conditions.append("{0} in %(dimensions)s".format(frappe.scrub(dimension))) return " and {}".format(" and ".join(conditions)) if conditions else "" -def get_dimension_items_list(dimension, company): - meta = frappe.get_meta(dimension, cached=False) - fieldnames = [d.fieldname for d in meta.get("fields")] - filters = {} - if 'company' in fieldnames: - filters['company'] = company - return frappe.get_all(dimension, filters, as_list=True) -def get_columns(dimension_items_list, accumulated_values=1, company=None): - columns = [{ - "fieldname": "account", - "label": _("Account"), - "fieldtype": "Link", - "options": "Account", - "width": 300 - }] - if company: - columns.append({ +def get_dimensions(filters): + meta = frappe.get_meta(filters.get("dimension"), cached=False) + query_filters = {} + + if meta.has_field("company"): + query_filters = {"company": filters.get("company")} + + return frappe.get_all(filters.get("dimension"), filters=query_filters, pluck="name") + + +def get_columns(dimension_list): + columns = [ + { + "fieldname": "account", + "label": _("Account"), + "fieldtype": "Link", + "options": "Account", + "width": 300, + }, + { "fieldname": "currency", "label": _("Currency"), "fieldtype": "Link", "options": "Currency", - "hidden": 1 - }) - for item in dimension_items_list: - columns.append({ - "fieldname": frappe.scrub(item), - "label": item, - "fieldtype": "Currency", - "options": "currency", - "width": 150 - }) - columns.append({ + "hidden": 1, + }, + ] + + for dimension in dimension_list: + columns.append( + { + "fieldname": frappe.scrub(dimension), + "label": dimension, + "fieldtype": "Currency", + "options": "currency", + "width": 150, + } + ) + + columns.append( + { "fieldname": "total", "label": "Total", "fieldtype": "Currency", "options": "currency", - "width": 150 - }) + "width": 150, + } + ) return columns diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 7bba2fcbe7d..2a3fbc9bdd7 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -1,4 +1,3 @@ - # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors # License: GNU General Public License v3. See license.txt @@ -21,12 +20,22 @@ from erpnext.accounts.report.utils import convert_to_presentation_currency, get_ from erpnext.accounts.utils import get_fiscal_year -def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_end_date, filter_based_on, periodicity, accumulated_values=False, - company=None, reset_period_on_fy_change=True, ignore_fiscal_year=False): +def get_period_list( + from_fiscal_year, + to_fiscal_year, + period_start_date, + period_end_date, + filter_based_on, + periodicity, + accumulated_values=False, + company=None, + reset_period_on_fy_change=True, + ignore_fiscal_year=False, +): """Get a list of dict {"from_date": from_date, "to_date": to_date, "key": key, "label": label} - Periodicity can be (Yearly, Quarterly, Monthly)""" + Periodicity can be (Yearly, Quarterly, Monthly)""" - if filter_based_on == 'Fiscal Year': + if filter_based_on == "Fiscal Year": fiscal_year = get_fiscal_year_data(from_fiscal_year, to_fiscal_year) validate_fiscal_year(fiscal_year, from_fiscal_year, to_fiscal_year) year_start_date = getdate(fiscal_year.year_start_date) @@ -36,12 +45,7 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ year_start_date = getdate(period_start_date) year_end_date = getdate(period_end_date) - months_to_add = { - "Yearly": 12, - "Half-Yearly": 6, - "Quarterly": 3, - "Monthly": 1 - }[periodicity] + months_to_add = {"Yearly": 12, "Half-Yearly": 6, "Quarterly": 3, "Monthly": 1}[periodicity] period_list = [] @@ -49,11 +53,9 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ months = get_months(year_start_date, year_end_date) for i in range(cint(math.ceil(months / months_to_add))): - period = frappe._dict({ - "from_date": start_date - }) + period = frappe._dict({"from_date": start_date}) - if i==0 and filter_based_on == 'Date Range': + if i == 0 and filter_based_on == "Date Range": to_date = add_months(get_first_day(start_date), months_to_add) else: to_date = add_months(start_date, months_to_add) @@ -93,32 +95,38 @@ def get_period_list(from_fiscal_year, to_fiscal_year, period_start_date, period_ else: label = get_label(periodicity, period_list[0].from_date, opts["to_date"]) - opts.update({ - "key": key.replace(" ", "_").replace("-", "_"), - "label": label, - "year_start_date": year_start_date, - "year_end_date": year_end_date - }) + opts.update( + { + "key": key.replace(" ", "_").replace("-", "_"), + "label": label, + "year_start_date": year_start_date, + "year_end_date": year_end_date, + } + ) return period_list def get_fiscal_year_data(from_fiscal_year, to_fiscal_year): - fiscal_year = frappe.db.sql("""select min(year_start_date) as year_start_date, + fiscal_year = frappe.db.sql( + """select min(year_start_date) as year_start_date, max(year_end_date) as year_end_date from `tabFiscal Year` where name between %(from_fiscal_year)s and %(to_fiscal_year)s""", - {'from_fiscal_year': from_fiscal_year, 'to_fiscal_year': to_fiscal_year}, as_dict=1) + {"from_fiscal_year": from_fiscal_year, "to_fiscal_year": to_fiscal_year}, + as_dict=1, + ) return fiscal_year[0] if fiscal_year else {} def validate_fiscal_year(fiscal_year, from_fiscal_year, to_fiscal_year): - if not fiscal_year.get('year_start_date') or not fiscal_year.get('year_end_date'): + if not fiscal_year.get("year_start_date") or not fiscal_year.get("year_end_date"): frappe.throw(_("Start Year and End Year are mandatory")) - if getdate(fiscal_year.get('year_end_date')) < getdate(fiscal_year.get('year_start_date')): + if getdate(fiscal_year.get("year_end_date")) < getdate(fiscal_year.get("year_start_date")): frappe.throw(_("End Year cannot be before Start Year")) + def validate_dates(from_date, to_date): if not from_date or not to_date: frappe.throw(_("From Date and To Date are mandatory")) @@ -126,6 +134,7 @@ def validate_dates(from_date, to_date): if to_date < from_date: frappe.throw(_("To Date cannot be less than From Date")) + def get_months(start_date, end_date): diff = (12 * end_date.year + end_date.month) - (12 * start_date.year + start_date.month) return diff + 1 @@ -144,9 +153,17 @@ def get_label(periodicity, from_date, to_date): def get_data( - company, root_type, balance_must_be, period_list, filters=None, - accumulated_values=1, only_current_fiscal_year=True, ignore_closing_entries=False, - ignore_accumulated_values_for_fy=False , total = True): + company, + root_type, + balance_must_be, + period_list, + filters=None, + accumulated_values=1, + only_current_fiscal_year=True, + ignore_closing_entries=False, + ignore_accumulated_values_for_fy=False, + total=True, +): accounts = get_accounts(company, root_type) if not accounts: @@ -157,19 +174,31 @@ def get_data( company_currency = get_appropriate_currency(company, filters) gl_entries_by_account = {} - for root in frappe.db.sql("""select lft, rgt from tabAccount - where root_type=%s and ifnull(parent_account, '') = ''""", root_type, as_dict=1): + for root in frappe.db.sql( + """select lft, rgt from tabAccount + where root_type=%s and ifnull(parent_account, '') = ''""", + root_type, + as_dict=1, + ): set_gl_entries_by_account( company, period_list[0]["year_start_date"] if only_current_fiscal_year else None, period_list[-1]["to_date"], - root.lft, root.rgt, filters, - gl_entries_by_account, ignore_closing_entries=ignore_closing_entries + root.lft, + root.rgt, + filters, + gl_entries_by_account, + ignore_closing_entries=ignore_closing_entries, ) calculate_values( - accounts_by_name, gl_entries_by_account, period_list, accumulated_values, ignore_accumulated_values_for_fy) + accounts_by_name, + gl_entries_by_account, + period_list, + accumulated_values, + ignore_accumulated_values_for_fy, + ) accumulate_values_into_parents(accounts, accounts_by_name, period_list) out = prepare_data(accounts, balance_must_be, period_list, company_currency) out = filter_out_zero_value_rows(out, parent_children_map) @@ -184,26 +213,32 @@ def get_appropriate_currency(company, filters=None): if filters and filters.get("presentation_currency"): return filters["presentation_currency"] else: - return frappe.get_cached_value('Company', company, "default_currency") + return frappe.get_cached_value("Company", company, "default_currency") def calculate_values( - accounts_by_name, gl_entries_by_account, period_list, accumulated_values, ignore_accumulated_values_for_fy): + accounts_by_name, + gl_entries_by_account, + period_list, + accumulated_values, + ignore_accumulated_values_for_fy, +): for entries in itervalues(gl_entries_by_account): for entry in entries: d = accounts_by_name.get(entry.account) if not d: frappe.msgprint( - _("Could not retrieve information for {0}.").format(entry.account), title="Error", - raise_exception=1 + _("Could not retrieve information for {0}.").format(entry.account), + title="Error", + raise_exception=1, ) for period in period_list: # check if posting date is within the period if entry.posting_date <= period.to_date: - if (accumulated_values or entry.posting_date >= period.from_date) and \ - (not ignore_accumulated_values_for_fy or - entry.fiscal_year == period.to_date_fiscal_year): + if (accumulated_values or entry.posting_date >= period.from_date) and ( + not ignore_accumulated_values_for_fy or entry.fiscal_year == period.to_date_fiscal_year + ): d[period.key] = d.get(period.key, 0.0) + flt(entry.debit) - flt(entry.credit) if entry.posting_date < period_list[0].year_start_date: @@ -215,11 +250,13 @@ def accumulate_values_into_parents(accounts, accounts_by_name, period_list): for d in reversed(accounts): if d.parent_account: for period in period_list: - accounts_by_name[d.parent_account][period.key] = \ - accounts_by_name[d.parent_account].get(period.key, 0.0) + d.get(period.key, 0.0) + accounts_by_name[d.parent_account][period.key] = accounts_by_name[d.parent_account].get( + period.key, 0.0 + ) + d.get(period.key, 0.0) - accounts_by_name[d.parent_account]["opening_balance"] = \ - accounts_by_name[d.parent_account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0) + accounts_by_name[d.parent_account]["opening_balance"] = accounts_by_name[d.parent_account].get( + "opening_balance", 0.0 + ) + d.get("opening_balance", 0.0) def prepare_data(accounts, balance_must_be, period_list, company_currency): @@ -231,20 +268,25 @@ def prepare_data(accounts, balance_must_be, period_list, company_currency): # add to output has_value = False total = 0 - row = frappe._dict({ - "account": _(d.name), - "parent_account": _(d.parent_account) if d.parent_account else '', - "indent": flt(d.indent), - "year_start_date": year_start_date, - "year_end_date": year_end_date, - "currency": company_currency, - "include_in_gross": d.include_in_gross, - "account_type": d.account_type, - "is_group": d.is_group, - "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be=="Debit" else -1), - "account_name": ('%s - %s' %(_(d.account_number), _(d.account_name)) - if d.account_number else _(d.account_name)) - }) + row = frappe._dict( + { + "account": _(d.name), + "parent_account": _(d.parent_account) if d.parent_account else "", + "indent": flt(d.indent), + "year_start_date": year_start_date, + "year_end_date": year_end_date, + "currency": company_currency, + "include_in_gross": d.include_in_gross, + "account_type": d.account_type, + "is_group": d.is_group, + "opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1), + "account_name": ( + "%s - %s" % (_(d.account_number), _(d.account_name)) + if d.account_number + else _(d.account_name) + ), + } + ) for period in period_list: if d.get(period.key) and balance_must_be == "Credit": # change sign based on Debit or Credit, since calculation is done using (debit - credit) @@ -285,7 +327,8 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency total_row = { "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), - "currency": company_currency + "currency": company_currency, + "opening_balance": 0.0, } for row in out: @@ -297,6 +340,7 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency total_row.setdefault("total", 0.0) total_row["total"] += flt(row["total"]) + total_row["opening_balance"] += row["opening_balance"] row["total"] = "" if "total" in total_row: @@ -307,10 +351,14 @@ def add_total_row(out, root_type, balance_must_be, period_list, company_currency def get_accounts(company, root_type): - return frappe.db.sql(""" + return frappe.db.sql( + """ select name, account_number, parent_account, lft, rgt, root_type, report_type, account_name, include_in_gross, account_type, is_group, lft, rgt from `tabAccount` - where company=%s and root_type=%s order by lft""", (company, root_type), as_dict=True) + where company=%s and root_type=%s order by lft""", + (company, root_type), + as_dict=True, + ) def filter_accounts(accounts, depth=20): @@ -325,7 +373,7 @@ def filter_accounts(accounts, depth=20): def add_to_list(parent, level): if level < depth: children = parent_children_map.get(parent) or [] - sort_accounts(children, is_root=True if parent==None else False) + sort_accounts(children, is_root=True if parent == None else False) for child in children: child.indent = level @@ -341,7 +389,7 @@ def sort_accounts(accounts, is_root=False, key="name"): """Sort root types as Asset, Liability, Equity, Income, Expense""" def compare_accounts(a, b): - if re.split(r'\W+', a[key])[0].isdigit(): + if re.split(r"\W+", a[key])[0].isdigit(): # if chart of accounts is numbered, then sort by number return cmp(a[key], b[key]) elif is_root: @@ -358,40 +406,50 @@ def sort_accounts(accounts, is_root=False, key="name"): return cmp(a[key], b[key]) return 1 - accounts.sort(key = functools.cmp_to_key(compare_accounts)) + accounts.sort(key=functools.cmp_to_key(compare_accounts)) + def set_gl_entries_by_account( - company, from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account, ignore_closing_entries=False): + company, + from_date, + to_date, + root_lft, + root_rgt, + filters, + gl_entries_by_account, + ignore_closing_entries=False, +): """Returns a dict like { "account": [gl entries], ... }""" additional_conditions = get_additional_conditions(from_date, ignore_closing_entries, filters) - accounts = frappe.db.sql_list("""select name from `tabAccount` - where lft >= %s and rgt <= %s and company = %s""", (root_lft, root_rgt, company)) + accounts = frappe.db.sql_list( + """select name from `tabAccount` + where lft >= %s and rgt <= %s and company = %s""", + (root_lft, root_rgt, company), + ) if accounts: - additional_conditions += " and account in ({})"\ - .format(", ".join(frappe.db.escape(d) for d in accounts)) + additional_conditions += " and account in ({})".format( + ", ".join(frappe.db.escape(d) for d in accounts) + ) gl_filters = { "company": company, "from_date": from_date, "to_date": to_date, - "finance_book": cstr(filters.get("finance_book")) + "finance_book": cstr(filters.get("finance_book")), } if filters.get("include_default_book_entries"): - gl_filters["company_fb"] = frappe.db.get_value("Company", - company, 'default_finance_book') + gl_filters["company_fb"] = frappe.db.get_value("Company", company, "default_finance_book") for key, value in filters.items(): if value: - gl_filters.update({ - key: value - }) + gl_filters.update({key: value}) distributed_cost_center_query = "" - if filters and filters.get('cost_center'): + if filters and filters.get("cost_center"): distributed_cost_center_query = """ UNION ALL SELECT posting_date, @@ -416,19 +474,26 @@ def set_gl_entries_by_account( AND posting_date <= %(to_date)s AND is_cancelled = 0 AND cost_center = DCC_allocation.parent - """.format(additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", '')) + """.format( + additional_conditions=additional_conditions.replace("and cost_center in %(cost_center)s ", "") + ) - gl_entries = frappe.db.sql("""select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` + gl_entries = frappe.db.sql( + """select posting_date, account, debit, credit, is_opening, fiscal_year, debit_in_account_currency, credit_in_account_currency, account_currency from `tabGL Entry` where company=%(company)s {additional_conditions} and posting_date <= %(to_date)s and is_cancelled = 0 {distributed_cost_center_query}""".format( additional_conditions=additional_conditions, - distributed_cost_center_query=distributed_cost_center_query), gl_filters, as_dict=True) #nosec + distributed_cost_center_query=distributed_cost_center_query, + ), + gl_filters, + as_dict=True, + ) # nosec - if filters and filters.get('presentation_currency'): - convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get('company')) + if filters and filters.get("presentation_currency"): + convert_to_presentation_currency(gl_entries, get_currency(filters), filters.get("company")) for entry in gl_entries: gl_entries_by_account.setdefault(entry.account, []).append(entry) @@ -459,25 +524,29 @@ def get_additional_conditions(from_date, ignore_closing_entries, filters): additional_conditions.append("cost_center in %(cost_center)s") if filters.get("include_default_book_entries"): - additional_conditions.append("(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)") + additional_conditions.append( + "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + ) else: additional_conditions.append("(finance_book in (%(finance_book)s, '') OR finance_book IS NULL)") if accounting_dimensions: for dimension in accounting_dimensions: if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) additional_conditions.append("{0} in %({0})s".format(dimension.fieldname)) else: - additional_conditions.append("{0} in (%({0})s)".format(dimension.fieldname)) + additional_conditions.append("{0} in %({0})s".format(dimension.fieldname)) return " and {}".format(" and ".join(additional_conditions)) if additional_conditions else "" + def get_cost_centers_with_children(cost_centers): if not isinstance(cost_centers, list): - cost_centers = [d.strip() for d in cost_centers.strip().split(',') if d] + cost_centers = [d.strip() for d in cost_centers.strip().split(",") if d] all_cost_centers = [] for d in cost_centers: @@ -490,45 +559,50 @@ def get_cost_centers_with_children(cost_centers): return list(set(all_cost_centers)) + def get_columns(periodicity, period_list, accumulated_values=1, company=None): - columns = [{ - "fieldname": "account", - "label": _("Account"), - "fieldtype": "Link", - "options": "Account", - "width": 300 - }] - if company: - columns.append({ - "fieldname": "currency", - "label": _("Currency"), + columns = [ + { + "fieldname": "account", + "label": _("Account"), "fieldtype": "Link", - "options": "Currency", - "hidden": 1 - }) + "options": "Account", + "width": 300, + } + ] + if company: + columns.append( + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1, + } + ) for period in period_list: - columns.append({ - "fieldname": period.key, - "label": period.label, - "fieldtype": "Currency", - "options": "currency", - "width": 150 - }) - if periodicity!="Yearly": - if not accumulated_values: - columns.append({ - "fieldname": "total", - "label": _("Total"), + columns.append( + { + "fieldname": period.key, + "label": period.label, "fieldtype": "Currency", - "width": 150 - }) + "options": "currency", + "width": 150, + } + ) + if periodicity != "Yearly": + if not accumulated_values: + columns.append( + {"fieldname": "total", "label": _("Total"), "fieldtype": "Currency", "width": 150} + ) return columns + def get_filtered_list_for_consolidated_report(filters, period_list): filtered_summary_list = [] for period in period_list: - if period == filters.get('company'): + if period == filters.get("company"): filtered_summary_list.append(period) return filtered_summary_list diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js index b2968761c63..010284c2ea5 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.js +++ b/erpnext/accounts/report/general_ledger/general_ledger.js @@ -167,7 +167,7 @@ frappe.query_reports["General Ledger"] = { "fieldname": "include_dimensions", "label": __("Consider Accounting Dimensions"), "fieldtype": "Check", - "default": 0 + "default": 1 }, { "fieldname": "show_opening_entries", diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py index 9aad52137b4..98c81086ab2 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -21,20 +21,20 @@ from erpnext.accounts.utils import get_account_currency # to cache translations TRANSLATIONS = frappe._dict() + def execute(filters=None): if not filters: return [], [] account_details = {} - if filters and filters.get('print_in_account_currency') and \ - not filters.get('account'): + if filters and filters.get("print_in_account_currency") and not filters.get("account"): frappe.throw(_("Select an account to print in account currency")) for acc in frappe.db.sql("""select name, is_group from tabAccount""", as_dict=1): account_details.setdefault(acc.name, acc) - if filters.get('party'): + if filters.get("party"): filters.party = frappe.parse_json(filters.get("party")) validate_filters(filters, account_details) @@ -51,46 +51,45 @@ def execute(filters=None): return columns, res + def update_translations(): TRANSLATIONS.update( - dict( - OPENING = _('Opening'), - TOTAL = _('Total'), - CLOSING_TOTAL = _('Closing (Opening + Total)') - ) + dict(OPENING=_("Opening"), TOTAL=_("Total"), CLOSING_TOTAL=_("Closing (Opening + Total)")) ) + def validate_filters(filters, account_details): if not filters.get("company"): frappe.throw(_("{0} is mandatory").format(_("Company"))) if not filters.get("from_date") and not filters.get("to_date"): - frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) + frappe.throw( + _("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date"))) + ) - if filters.get('account'): - filters.account = frappe.parse_json(filters.get('account')) + if filters.get("account"): + filters.account = frappe.parse_json(filters.get("account")) for account in filters.account: if not account_details.get(account): frappe.throw(_("Account {0} does not exists").format(account)) - if (filters.get("account") and filters.get("group_by") == 'Group by Account'): - filters.account = frappe.parse_json(filters.get('account')) + if filters.get("account") and filters.get("group_by") == "Group by Account": + filters.account = frappe.parse_json(filters.get("account")) for account in filters.account: if account_details[account].is_group == 0: frappe.throw(_("Can not filter based on Child Account, if grouped by Account")) - if (filters.get("voucher_no") - and filters.get("group_by") in ['Group by Voucher']): + if filters.get("voucher_no") and filters.get("group_by") in ["Group by Voucher"]: frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher")) if filters.from_date > filters.to_date: frappe.throw(_("From Date must be before To Date")) - if filters.get('project'): - filters.project = frappe.parse_json(filters.get('project')) + if filters.get("project"): + filters.project = frappe.parse_json(filters.get("project")) - if filters.get('cost_center'): - filters.cost_center = frappe.parse_json(filters.get('cost_center')) + if filters.get("cost_center"): + filters.cost_center = frappe.parse_json(filters.get("cost_center")) def validate_party(filters): @@ -101,9 +100,12 @@ def validate_party(filters): if not frappe.db.exists(party_type, d): frappe.throw(_("Invalid {0}: {1}").format(party_type, d)) + def set_account_currency(filters): - if filters.get("account") or (filters.get('party') and len(filters.party) == 1): - filters["company_currency"] = frappe.get_cached_value('Company', filters.company, "default_currency") + if filters.get("account") or (filters.get("party") and len(filters.party) == 1): + filters["company_currency"] = frappe.get_cached_value( + "Company", filters.company, "default_currency" + ) account_currency = None if filters.get("account"): @@ -122,17 +124,19 @@ def set_account_currency(filters): elif filters.get("party"): gle_currency = frappe.db.get_value( - "GL Entry", { - "party_type": filters.party_type, "party": filters.party[0], "company": filters.company - }, - "account_currency" + "GL Entry", + {"party_type": filters.party_type, "party": filters.party[0], "company": filters.company}, + "account_currency", ) if gle_currency: account_currency = gle_currency else: - account_currency = (None if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] else - frappe.db.get_value(filters.party_type, filters.party[0], "default_currency")) + account_currency = ( + None + if filters.party_type in ["Employee", "Student", "Shareholder", "Member"] + else frappe.db.get_value(filters.party_type, filters.party[0], "default_currency") + ) filters["account_currency"] = account_currency or filters.company_currency if filters.account_currency != filters.company_currency and not filters.presentation_currency: @@ -140,6 +144,7 @@ def set_account_currency(filters): return filters + def get_result(filters, account_details): accounting_dimensions = [] if filters.get("include_dimensions"): @@ -147,13 +152,13 @@ def get_result(filters, account_details): gl_entries = get_gl_entries(filters, accounting_dimensions) - data = get_data_with_opening_closing(filters, account_details, - accounting_dimensions, gl_entries) + data = get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries) result = get_result_as_list(data, filters) return result + def get_gl_entries(filters, accounting_dimensions): currency_map = get_currency(filters) select_fields = """, debit, credit, debit_in_account_currency, @@ -170,15 +175,16 @@ def get_gl_entries(filters, accounting_dimensions): order_by_statement = "order by account, posting_date, creation" if filters.get("include_default_book_entries"): - filters['company_fb'] = frappe.db.get_value("Company", - filters.get("company"), 'default_finance_book') + filters["company_fb"] = frappe.db.get_value( + "Company", filters.get("company"), "default_finance_book" + ) dimension_fields = "" if accounting_dimensions: - dimension_fields = ', '.join(accounting_dimensions) + ',' + dimension_fields = ", ".join(accounting_dimensions) + "," distributed_cost_center_query = "" - if filters and filters.get('cost_center'): + if filters and filters.get("cost_center"): select_fields_with_percentage = """, debit*(DCC_allocation.percentage_allocation/100) as debit, credit*(DCC_allocation.percentage_allocation/100) as credit, debit_in_account_currency*(DCC_allocation.percentage_allocation/100) as debit_in_account_currency, @@ -211,7 +217,11 @@ def get_gl_entries(filters, accounting_dimensions): {conditions} AND posting_date <= %(to_date)s AND cost_center = DCC_allocation.parent - """.format(dimension_fields=dimension_fields,select_fields_with_percentage=select_fields_with_percentage, conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", '')) + """.format( + dimension_fields=dimension_fields, + select_fields_with_percentage=select_fields_with_percentage, + conditions=get_conditions(filters).replace("and cost_center in %(cost_center)s ", ""), + ) gl_entries = frappe.db.sql( """ @@ -226,13 +236,18 @@ def get_gl_entries(filters, accounting_dimensions): {distributed_cost_center_query} {order_by_statement} """.format( - dimension_fields=dimension_fields, select_fields=select_fields, conditions=get_conditions(filters), distributed_cost_center_query=distributed_cost_center_query, - order_by_statement=order_by_statement + dimension_fields=dimension_fields, + select_fields=select_fields, + conditions=get_conditions(filters), + distributed_cost_center_query=distributed_cost_center_query, + order_by_statement=order_by_statement, ), - filters, as_dict=1) + filters, + as_dict=1, + ) - if filters.get('presentation_currency'): - return convert_to_presentation_currency(gl_entries, currency_map, filters.get('company')) + if filters.get("presentation_currency"): + return convert_to_presentation_currency(gl_entries, currency_map, filters.get("company")) else: return gl_entries @@ -260,18 +275,23 @@ def get_conditions(filters): if filters.get("party"): conditions.append("party in %(party)s") - if not (filters.get("account") or filters.get("party") or - filters.get("group_by") in ["Group by Account", "Group by Party"]): - conditions.append("posting_date >=%(from_date)s") + if not ( + filters.get("account") + or filters.get("party") + or filters.get("group_by") in ["Group by Account", "Group by Party"] + ): + conditions.append("(posting_date >=%(from_date)s or is_opening = 'Yes')") - conditions.append("(posting_date <=%(to_date)s or is_opening = 'Yes')") + conditions.append("(posting_date <=%(to_date)s)") if filters.get("project"): conditions.append("project in %(project)s") if filters.get("finance_book"): if filters.get("include_default_book_entries"): - conditions.append("(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)") + conditions.append( + "(finance_book in (%(finance_book)s, %(company_fb)s, '') OR finance_book IS NULL)" + ) else: conditions.append("finance_book in (%(finance_book)s)") @@ -279,6 +299,7 @@ def get_conditions(filters): conditions.append("is_cancelled = 0") from frappe.desk.reportview import build_match_conditions + match_conditions = build_match_conditions("GL Entry") if match_conditions: @@ -291,18 +312,20 @@ def get_conditions(filters): for dimension in accounting_dimensions: if not dimension.disabled: if filters.get(dimension.fieldname): - if frappe.get_cached_value('DocType', dimension.document_type, 'is_tree'): - filters[dimension.fieldname] = get_dimension_with_children(dimension.document_type, - filters.get(dimension.fieldname)) + if frappe.get_cached_value("DocType", dimension.document_type, "is_tree"): + filters[dimension.fieldname] = get_dimension_with_children( + dimension.document_type, filters.get(dimension.fieldname) + ) conditions.append("{0} in %({0})s".format(dimension.fieldname)) else: - conditions.append("{0} in (%({0})s)".format(dimension.fieldname)) + conditions.append("{0} in %({0})s".format(dimension.fieldname)) return "and {}".format(" and ".join(conditions)) if conditions else "" + def get_accounts_with_children(accounts): if not isinstance(accounts, list): - accounts = [d.strip() for d in accounts.strip().split(',') if d] + accounts = [d.strip() for d in accounts.strip().split(",") if d] all_accounts = [] for d in accounts: @@ -315,6 +338,7 @@ def get_accounts_with_children(accounts): return list(set(all_accounts)) + def get_data_with_opening_closing(filters, account_details, accounting_dimensions, gl_entries): data = [] @@ -325,7 +349,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension # Opening for filtered account data.append(totals.opening) - if filters.get("group_by") != 'Group by Voucher (Consolidated)': + if filters.get("group_by") != "Group by Voucher (Consolidated)": for acc, acc_dict in iteritems(gle_map): # acc if acc_dict.entries: @@ -354,6 +378,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension return data + def get_totals_dict(): def _get_debit_credit_dict(label): return _dict( @@ -361,25 +386,28 @@ def get_totals_dict(): debit=0.0, credit=0.0, debit_in_account_currency=0.0, - credit_in_account_currency=0.0 + credit_in_account_currency=0.0, ) + return _dict( - opening = _get_debit_credit_dict(TRANSLATIONS.OPENING), - total = _get_debit_credit_dict(TRANSLATIONS.TOTAL), - closing = _get_debit_credit_dict(TRANSLATIONS.CLOSING_TOTAL) + opening=_get_debit_credit_dict(TRANSLATIONS.OPENING), + total=_get_debit_credit_dict(TRANSLATIONS.TOTAL), + closing=_get_debit_credit_dict(TRANSLATIONS.CLOSING_TOTAL), ) + def group_by_field(group_by): - if group_by == 'Group by Party': - return 'party' - elif group_by in ['Group by Voucher (Consolidated)', 'Group by Account']: - return 'account' + if group_by == "Group by Party": + return "party" + elif group_by in ["Group by Voucher (Consolidated)", "Group by Account"]: + return "account" else: - return 'voucher_no' + return "voucher_no" + def initialize_gle_map(gl_entries, filters): gle_map = OrderedDict() - group_by = group_by_field(filters.get('group_by')) + group_by = group_by_field(filters.get("group_by")) for gle in gl_entries: gle_map.setdefault(gle.get(group_by), _dict(totals=get_totals_dict(), entries=[])) @@ -390,11 +418,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): totals = get_totals_dict() entries = [] consolidated_gle = OrderedDict() - group_by = group_by_field(filters.get('group_by')) - group_by_voucher_consolidated = filters.get("group_by") == 'Group by Voucher (Consolidated)' + group_by = group_by_field(filters.get("group_by")) + group_by_voucher_consolidated = filters.get("group_by") == "Group by Voucher (Consolidated)" - if filters.get('show_net_values_in_party_account'): - account_type_map = get_account_type_map(filters.get('company')) + if filters.get("show_net_values_in_party_account"): + account_type_map = get_account_type_map(filters.get("company")) def update_value_in_dict(data, key, gle): data[key].debit += gle.debit @@ -403,26 +431,28 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): data[key].debit_in_account_currency += gle.debit_in_account_currency data[key].credit_in_account_currency += gle.credit_in_account_currency - if filters.get('show_net_values_in_party_account') and \ - account_type_map.get(data[key].account) in ('Receivable', 'Payable'): + if filters.get("show_net_values_in_party_account") and account_type_map.get( + data[key].account + ) in ("Receivable", "Payable"): net_value = data[key].debit - data[key].credit - net_value_in_account_currency = data[key].debit_in_account_currency \ - - data[key].credit_in_account_currency + net_value_in_account_currency = ( + data[key].debit_in_account_currency - data[key].credit_in_account_currency + ) if net_value < 0: - dr_or_cr = 'credit' - rev_dr_or_cr = 'debit' + dr_or_cr = "credit" + rev_dr_or_cr = "debit" else: - dr_or_cr = 'debit' - rev_dr_or_cr = 'credit' + dr_or_cr = "debit" + rev_dr_or_cr = "credit" data[key][dr_or_cr] = abs(net_value) - data[key][dr_or_cr+'_in_account_currency'] = abs(net_value_in_account_currency) + data[key][dr_or_cr + "_in_account_currency"] = abs(net_value_in_account_currency) data[key][rev_dr_or_cr] = 0 - data[key][rev_dr_or_cr+'_in_account_currency'] = 0 + data[key][rev_dr_or_cr + "_in_account_currency"] = 0 if data[key].against_voucher and gle.against_voucher: - data[key].against_voucher += ', ' + gle.against_voucher + data[key].against_voucher += ", " + gle.against_voucher from_date, to_date = getdate(filters.from_date), getdate(filters.to_date) show_opening_entries = filters.get("show_opening_entries") @@ -430,28 +460,36 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): for gle in gl_entries: group_by_value = gle.get(group_by) - if (gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries)): + if gle.posting_date < from_date or (cstr(gle.is_opening) == "Yes" and not show_opening_entries): if not group_by_voucher_consolidated: - update_value_in_dict(gle_map[group_by_value].totals, 'opening', gle) - update_value_in_dict(gle_map[group_by_value].totals, 'closing', gle) + update_value_in_dict(gle_map[group_by_value].totals, "opening", gle) + update_value_in_dict(gle_map[group_by_value].totals, "closing", gle) - update_value_in_dict(totals, 'opening', gle) - update_value_in_dict(totals, 'closing', gle) + update_value_in_dict(totals, "opening", gle) + update_value_in_dict(totals, "closing", gle) - elif gle.posting_date <= to_date: + elif gle.posting_date <= to_date or (cstr(gle.is_opening) == "Yes" and show_opening_entries): if not group_by_voucher_consolidated: - update_value_in_dict(gle_map[group_by_value].totals, 'total', gle) - update_value_in_dict(gle_map[group_by_value].totals, 'closing', gle) - update_value_in_dict(totals, 'total', gle) - update_value_in_dict(totals, 'closing', gle) + update_value_in_dict(gle_map[group_by_value].totals, "total", gle) + update_value_in_dict(gle_map[group_by_value].totals, "closing", gle) + update_value_in_dict(totals, "total", gle) + update_value_in_dict(totals, "closing", gle) gle_map[group_by_value].entries.append(gle) elif group_by_voucher_consolidated: - keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")] - for dim in accounting_dimensions: - keylist.append(gle.get(dim)) - keylist.append(gle.get("cost_center")) + keylist = [ + gle.get("voucher_type"), + gle.get("voucher_no"), + gle.get("account"), + gle.get("party_type"), + gle.get("party"), + ] + if filters.get("include_dimensions"): + for dim in accounting_dimensions: + keylist.append(gle.get(dim)) + keylist.append(gle.get("cost_center")) + key = tuple(keylist) if key not in consolidated_gle: consolidated_gle.setdefault(key, gle) @@ -459,47 +497,58 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): update_value_in_dict(consolidated_gle, key, gle) for key, value in consolidated_gle.items(): - update_value_in_dict(totals, 'total', value) - update_value_in_dict(totals, 'closing', value) + update_value_in_dict(totals, "total", value) + update_value_in_dict(totals, "closing", value) entries.append(value) return totals, entries + def get_account_type_map(company): - account_type_map = frappe._dict(frappe.get_all('Account', fields=['name', 'account_type'], - filters={'company': company}, as_list=1)) + account_type_map = frappe._dict( + frappe.get_all( + "Account", fields=["name", "account_type"], filters={"company": company}, as_list=1 + ) + ) return account_type_map + def get_result_as_list(data, filters): balance, balance_in_account_currency = 0, 0 inv_details = get_supplier_invoice_details() for d in data: - if not d.get('posting_date'): + if not d.get("posting_date"): balance, balance_in_account_currency = 0, 0 - balance = get_balance(d, balance, 'debit', 'credit') - d['balance'] = balance + balance = get_balance(d, balance, "debit", "credit") + d["balance"] = balance - d['account_currency'] = filters.account_currency - d['bill_no'] = inv_details.get(d.get('against_voucher'), '') + d["account_currency"] = filters.account_currency + d["bill_no"] = inv_details.get(d.get("against_voucher"), "") return data + def get_supplier_invoice_details(): inv_details = {} - for d in frappe.db.sql(""" select name, bill_no from `tabPurchase Invoice` - where docstatus = 1 and bill_no is not null and bill_no != '' """, as_dict=1): + for d in frappe.db.sql( + """ select name, bill_no from `tabPurchase Invoice` + where docstatus = 1 and bill_no is not null and bill_no != '' """, + as_dict=1, + ): inv_details[d.name] = d.bill_no return inv_details + def get_balance(row, balance, debit_field, credit_field): - balance += (row.get(debit_field, 0) - row.get(credit_field, 0)) + balance += row.get(debit_field, 0) - row.get(credit_field, 0) return balance + def get_columns(filters): if filters.get("presentation_currency"): currency = filters["presentation_currency"] @@ -516,116 +565,75 @@ def get_columns(filters): "fieldname": "gl_entry", "fieldtype": "Link", "options": "GL Entry", - "hidden": 1 - }, - { - "label": _("Posting Date"), - "fieldname": "posting_date", - "fieldtype": "Date", - "width": 90 + "hidden": 1, }, + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 90}, { "label": _("Account"), "fieldname": "account", "fieldtype": "Link", "options": "Account", - "width": 180 + "width": 180, }, { "label": _("Debit ({0})").format(currency), "fieldname": "debit", "fieldtype": "Float", - "width": 100 + "width": 100, }, { "label": _("Credit ({0})").format(currency), "fieldname": "credit", "fieldtype": "Float", - "width": 100 + "width": 100, }, { "label": _("Balance ({0})").format(currency), "fieldname": "balance", "fieldtype": "Float", - "width": 130 - } + "width": 130, + }, ] - columns.extend([ - { - "label": _("Voucher Type"), - "fieldname": "voucher_type", - "width": 120 - }, - { - "label": _("Voucher No"), - "fieldname": "voucher_no", - "fieldtype": "Dynamic Link", - "options": "voucher_type", - "width": 180 - }, - { - "label": _("Against Account"), - "fieldname": "against", - "width": 120 - }, - { - "label": _("Party Type"), - "fieldname": "party_type", - "width": 100 - }, - { - "label": _("Party"), - "fieldname": "party", - "width": 100 - }, - { - "label": _("Project"), - "options": "Project", - "fieldname": "project", - "width": 100 - } - ]) + columns.extend( + [ + {"label": _("Voucher Type"), "fieldname": "voucher_type", "width": 120}, + { + "label": _("Voucher No"), + "fieldname": "voucher_no", + "fieldtype": "Dynamic Link", + "options": "voucher_type", + "width": 180, + }, + {"label": _("Against Account"), "fieldname": "against", "width": 120}, + {"label": _("Party Type"), "fieldname": "party_type", "width": 100}, + {"label": _("Party"), "fieldname": "party", "width": 100}, + {"label": _("Project"), "options": "Project", "fieldname": "project", "width": 100}, + ] + ) if filters.get("include_dimensions"): - for dim in get_accounting_dimensions(as_list = False): - columns.append({ - "label": _(dim.label), - "options": dim.label, - "fieldname": dim.fieldname, - "width": 100 - }) + for dim in get_accounting_dimensions(as_list=False): + columns.append( + {"label": _(dim.label), "options": dim.label, "fieldname": dim.fieldname, "width": 100} + ) + columns.append( + {"label": _("Cost Center"), "options": "Cost Center", "fieldname": "cost_center", "width": 100} + ) - columns.extend([ - { - "label": _("Cost Center"), - "options": "Cost Center", - "fieldname": "cost_center", - "width": 100 - }, - { - "label": _("Against Voucher Type"), - "fieldname": "against_voucher_type", - "width": 100 - }, - { - "label": _("Against Voucher"), - "fieldname": "against_voucher", - "fieldtype": "Dynamic Link", - "options": "against_voucher_type", - "width": 100 - }, - { - "label": _("Supplier Invoice No"), - "fieldname": "bill_no", - "fieldtype": "Data", - "width": 100 - }, - { - "label": _("Remarks"), - "fieldname": "remarks", - "width": 400 - } - ]) + columns.extend( + [ + {"label": _("Against Voucher Type"), "fieldname": "against_voucher_type", "width": 100}, + { + "label": _("Against Voucher"), + "fieldname": "against_voucher", + "fieldtype": "Dynamic Link", + "options": "against_voucher_type", + "width": 100, + }, + {"label": _("Supplier Invoice No"), "fieldname": "bill_no", "fieldtype": "Data", "width": 100}, + {"label": _("Remarks"), "fieldname": "remarks", "width": 400}, + ] + ) return columns diff --git a/erpnext/accounts/report/general_ledger/test_general_ledger.py b/erpnext/accounts/report/general_ledger/test_general_ledger.py new file mode 100644 index 00000000000..b10e7696187 --- /dev/null +++ b/erpnext/accounts/report/general_ledger/test_general_ledger.py @@ -0,0 +1,152 @@ +# Copyright (c) 2022, Frappe Technologies Pvt. Ltd. and Contributors +# MIT License. See license.txt + +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import today + +from erpnext.accounts.report.general_ledger.general_ledger import execute + + +class TestGeneralLedger(FrappeTestCase): + def test_foreign_account_balance_after_exchange_rate_revaluation(self): + """ + Checks the correctness of balance after exchange rate revaluation + """ + # create a new account with USD currency + account_name = "Test USD Account for Revalutation" + company = "_Test Company" + account = frappe.get_doc( + { + "account_name": account_name, + "is_group": 0, + "company": company, + "root_type": "Asset", + "report_type": "Balance Sheet", + "account_currency": "USD", + "inter_company_account": 0, + "parent_account": "Bank Accounts - _TC", + "account_type": "Bank", + "doctype": "Account", + } + ) + account.insert(ignore_if_duplicate=True) + # create a JV to debit 1000 USD at 75 exchange rate + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = company + jv.multi_currency = 1 + jv.cost_center = "_Test Cost Center - _TC" + jv.set( + "accounts", + [ + { + "account": account.name, + "debit_in_account_currency": 1000, + "credit_in_account_currency": 0, + "exchange_rate": 75, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "Cash - _TC", + "debit_in_account_currency": 0, + "credit_in_account_currency": 75000, + "cost_center": "_Test Cost Center - _TC", + }, + ], + ) + jv.save() + jv.submit() + # create a JV to credit 900 USD at 100 exchange rate + jv = frappe.new_doc("Journal Entry") + jv.posting_date = today() + jv.company = company + jv.multi_currency = 1 + jv.cost_center = "_Test Cost Center - _TC" + jv.set( + "accounts", + [ + { + "account": account.name, + "debit_in_account_currency": 0, + "credit_in_account_currency": 900, + "exchange_rate": 100, + "cost_center": "_Test Cost Center - _TC", + }, + { + "account": "Cash - _TC", + "debit_in_account_currency": 90000, + "credit_in_account_currency": 0, + "cost_center": "_Test Cost Center - _TC", + }, + ], + ) + jv.save() + jv.submit() + + # create an exchange rate revaluation entry at 77 exchange rate + revaluation = frappe.new_doc("Exchange Rate Revaluation") + revaluation.posting_date = today() + revaluation.company = company + revaluation.set( + "accounts", + [ + { + "account": account.name, + "account_currency": "USD", + "new_exchange_rate": 77, + "new_balance_in_base_currency": 7700, + "balance_in_base_currency": -15000, + "balance_in_account_currency": 100, + "current_exchange_rate": -150, + } + ], + ) + revaluation.save() + revaluation.submit() + + # post journal entry to revaluate + frappe.db.set_value( + "Company", company, "unrealized_exchange_gain_loss_account", "_Test Exchange Gain/Loss - _TC" + ) + revaluation_jv = revaluation.make_jv_entry() + revaluation_jv = frappe.get_doc(revaluation_jv) + revaluation_jv.cost_center = "_Test Cost Center - _TC" + for acc in revaluation_jv.get("accounts"): + acc.cost_center = "_Test Cost Center - _TC" + revaluation_jv.save() + revaluation_jv.submit() + + # check the balance of the account + balance = frappe.db.sql( + """ + select sum(debit_in_account_currency) - sum(credit_in_account_currency) + from `tabGL Entry` + where account = %s + group by account + """, + account.name, + ) + + self.assertEqual(balance[0][0], 100) + + # check if general ledger shows correct balance + columns, data = execute( + frappe._dict( + { + "company": company, + "from_date": today(), + "to_date": today(), + "account": [account.name], + "group_by": "Group by Voucher (Consolidated)", + } + ) + ) + + self.assertEqual(data[1]["account"], account.name) + self.assertEqual(data[1]["debit"], 1000) + self.assertEqual(data[1]["credit"], 0) + self.assertEqual(data[2]["debit"], 0) + self.assertEqual(data[2]["credit"], 900) + self.assertEqual(data[3]["debit"], 100) + self.assertEqual(data[3]["credit"], 100) diff --git a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py index b18b940fd2b..cd5f3667071 100644 --- a/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py +++ b/erpnext/accounts/report/gross_and_net_profit_report/gross_and_net_profit_report.py @@ -12,30 +12,57 @@ from erpnext.accounts.report.financial_statements import get_columns, get_data, def execute(filters=None): - period_list = get_period_list(filters.from_fiscal_year, filters.to_fiscal_year, filters.period_start_date, - filters.period_end_date, filters.filter_based_on, filters.periodicity, filters.accumulated_values, filters.company) + period_list = get_period_list( + filters.from_fiscal_year, + filters.to_fiscal_year, + filters.period_start_date, + filters.period_end_date, + filters.filter_based_on, + filters.periodicity, + filters.accumulated_values, + filters.company, + ) columns, data = [], [] - income = get_data(filters.company, "Income", "Credit", period_list, filters = filters, + income = get_data( + filters.company, + "Income", + "Credit", + period_list, + filters=filters, accumulated_values=filters.accumulated_values, - ignore_closing_entries=True, ignore_accumulated_values_for_fy= True, total= False) + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, + total=False, + ) - expense = get_data(filters.company, "Expense", "Debit", period_list, filters=filters, + expense = get_data( + filters.company, + "Expense", + "Debit", + period_list, + filters=filters, accumulated_values=filters.accumulated_values, - ignore_closing_entries=True, ignore_accumulated_values_for_fy= True, total= False) - - columns = get_columns(filters.periodicity, period_list, filters.accumulated_values, filters.company) + ignore_closing_entries=True, + ignore_accumulated_values_for_fy=True, + total=False, + ) + columns = get_columns( + filters.periodicity, period_list, filters.accumulated_values, filters.company + ) gross_income = get_revenue(income, period_list) gross_expense = get_revenue(expense, period_list) - if(len(gross_income)==0 and len(gross_expense)== 0): - data.append({ - "account_name": "'" + _("Nothing is included in gross") + "'", - "account": "'" + _("Nothing is included in gross") + "'" - }) + if len(gross_income) == 0 and len(gross_expense) == 0: + data.append( + { + "account_name": "'" + _("Nothing is included in gross") + "'", + "account": "'" + _("Nothing is included in gross") + "'", + } + ) return columns, data # to avoid error eg: gross_income[0] : list index out of range @@ -44,10 +71,12 @@ def execute(filters=None): if not gross_expense: gross_expense = [{}] - data.append({ - "account_name": "'" + _("Included in Gross Profit") + "'", - "account": "'" + _("Included in Gross Profit") + "'" - }) + data.append( + { + "account_name": "'" + _("Included in Gross Profit") + "'", + "account": "'" + _("Included in Gross Profit") + "'", + } + ) data.append({}) data.extend(gross_income or []) @@ -56,7 +85,14 @@ def execute(filters=None): data.extend(gross_expense or []) data.append({}) - gross_profit = get_profit(gross_income, gross_expense, period_list, filters.company, 'Gross Profit',filters.presentation_currency) + gross_profit = get_profit( + gross_income, + gross_expense, + period_list, + filters.company, + "Gross Profit", + filters.presentation_currency, + ) data.append(gross_profit) non_gross_income = get_revenue(income, period_list, 0) @@ -67,28 +103,40 @@ def execute(filters=None): data.append({}) data.extend(non_gross_expense or []) - net_profit = get_net_profit(non_gross_income, gross_income, gross_expense, non_gross_expense, period_list, filters.company,filters.presentation_currency) + net_profit = get_net_profit( + non_gross_income, + gross_income, + gross_expense, + non_gross_expense, + period_list, + filters.company, + filters.presentation_currency, + ) data.append({}) data.append(net_profit) return columns, data -def get_revenue(data, period_list, include_in_gross=1): - revenue = [item for item in data if item['include_in_gross']==include_in_gross or item['is_group']==1] - data_to_be_removed =True +def get_revenue(data, period_list, include_in_gross=1): + revenue = [ + item for item in data if item["include_in_gross"] == include_in_gross or item["is_group"] == 1 + ] + + data_to_be_removed = True while data_to_be_removed: revenue, data_to_be_removed = remove_parent_with_no_child(revenue, period_list) revenue = adjust_account(revenue, period_list) return copy.deepcopy(revenue) + def remove_parent_with_no_child(data, period_list): data_to_be_removed = False for parent in data: - if 'is_group' in parent and parent.get("is_group") == 1: + if "is_group" in parent and parent.get("is_group") == 1: have_child = False for child in data: - if 'parent_account' in child and child.get("parent_account") == parent.get("account"): + if "parent_account" in child and child.get("parent_account") == parent.get("account"): have_child = True break @@ -98,34 +146,39 @@ def remove_parent_with_no_child(data, period_list): return data, data_to_be_removed -def adjust_account(data, period_list, consolidated= False): - leaf_nodes = [item for item in data if item['is_group'] == 0] + +def adjust_account(data, period_list, consolidated=False): + leaf_nodes = [item for item in data if item["is_group"] == 0] totals = {} for node in leaf_nodes: set_total(node, node["total"], data, totals) for d in data: for period in period_list: key = period if consolidated else period.key - d[key] = totals[d["account"]] - d['total'] = totals[d["account"]] + d["total"] = totals[d["account"]] return data + def set_total(node, value, complete_list, totals): - if not totals.get(node['account']): + if not totals.get(node["account"]): totals[node["account"]] = 0 totals[node["account"]] += value - parent = node['parent_account'] - if not parent == '': - return set_total(next(item for item in complete_list if item['account'] == parent), value, complete_list, totals) + parent = node["parent_account"] + if not parent == "": + return set_total( + next(item for item in complete_list if item["account"] == parent), value, complete_list, totals + ) -def get_profit(gross_income, gross_expense, period_list, company, profit_type, currency=None, consolidated=False): +def get_profit( + gross_income, gross_expense, period_list, company, profit_type, currency=None, consolidated=False +): profit_loss = { "account_name": "'" + _(profit_type) + "'", "account": "'" + _(profit_type) + "'", "warn_if_negative": True, - "currency": currency or frappe.get_cached_value('Company', company, "default_currency") + "currency": currency or frappe.get_cached_value("Company", company, "default_currency"), } has_value = False @@ -137,17 +190,27 @@ def get_profit(gross_income, gross_expense, period_list, company, profit_type, c profit_loss[key] = gross_income_for_period - gross_expense_for_period if profit_loss[key]: - has_value=True + has_value = True if has_value: return profit_loss -def get_net_profit(non_gross_income, gross_income, gross_expense, non_gross_expense, period_list, company, currency=None, consolidated=False): + +def get_net_profit( + non_gross_income, + gross_income, + gross_expense, + non_gross_expense, + period_list, + company, + currency=None, + consolidated=False, +): profit_loss = { "account_name": "'" + _("Net Profit") + "'", "account": "'" + _("Net Profit") + "'", "warn_if_negative": True, - "currency": currency or frappe.get_cached_value('Company', company, "default_currency") + "currency": currency or frappe.get_cached_value("Company", company, "default_currency"), } has_value = False @@ -165,7 +228,7 @@ def get_net_profit(non_gross_income, gross_income, gross_expense, non_gross_expe profit_loss[key] = flt(total_income) - flt(total_expense) if profit_loss[key]: - has_value=True + has_value = True if has_value: return profit_loss diff --git a/erpnext/accounts/report/gross_profit/gross_profit.js b/erpnext/accounts/report/gross_profit/gross_profit.js index 685f2d6176b..4000d3baf29 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.js +++ b/erpnext/accounts/report/gross_profit/gross_profit.js @@ -8,20 +8,22 @@ frappe.query_reports["Gross Profit"] = { "label": __("Company"), "fieldtype": "Link", "options": "Company", - "reqd": 1, - "default": frappe.defaults.get_user_default("Company") + "default": frappe.defaults.get_user_default("Company"), + "reqd": 1 }, { "fieldname":"from_date", "label": __("From Date"), "fieldtype": "Date", - "default": frappe.defaults.get_user_default("year_start_date") + "default": frappe.defaults.get_user_default("year_start_date"), + "reqd": 1 }, { "fieldname":"to_date", "label": __("To Date"), "fieldtype": "Date", - "default": frappe.defaults.get_user_default("year_end_date") + "default": frappe.defaults.get_user_default("year_end_date"), + "reqd": 1 }, { "fieldname":"sales_invoice", @@ -42,9 +44,14 @@ frappe.query_reports["Gross Profit"] = { "parent_field": "parent_invoice", "initial_depth": 3, "formatter": function(value, row, column, data, default_formatter) { + if (column.fieldname == "sales_invoice" && column.options == "Item" && data && data.indent == 0) { + column._options = "Sales Invoice"; + } else { + column._options = "Item"; + } value = default_formatter(value, row, column, data); - if (data && (data.indent == 0.0 || row[1].content == "Total")) { + if (data && (data.indent == 0.0 || (row[1] && row[1].content == "Total"))) { value = $(`${value}`); var $value = $(value).css("font-weight", "bold"); value = $value.wrap("").parent().html(); diff --git a/erpnext/accounts/report/gross_profit/gross_profit.json b/erpnext/accounts/report/gross_profit/gross_profit.json index 76c560ad247..0730ffd77e5 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.json +++ b/erpnext/accounts/report/gross_profit/gross_profit.json @@ -1,5 +1,5 @@ { - "add_total_row": 0, + "add_total_row": 1, "columns": [], "creation": "2013-02-25 17:03:34", "disable_prepared_report": 0, @@ -9,7 +9,7 @@ "filters": [], "idx": 3, "is_standard": "Yes", - "modified": "2021-11-13 19:14:23.730198", + "modified": "2022-02-11 10:18:36.956558", "modified_by": "Administrator", "module": "Accounts", "name": "Gross Profit", diff --git a/erpnext/accounts/report/gross_profit/gross_profit.py b/erpnext/accounts/report/gross_profit/gross_profit.py index a8b5a0e28bd..7f6e2b99c89 100644 --- a/erpnext/accounts/report/gross_profit/gross_profit.py +++ b/erpnext/accounts/report/gross_profit/gross_profit.py @@ -11,38 +11,125 @@ from erpnext.stock.utils import get_incoming_rate def execute(filters=None): - if not filters: filters = frappe._dict() - filters.currency = frappe.get_cached_value('Company', filters.company, "default_currency") + if not filters: + filters = frappe._dict() + filters.currency = frappe.get_cached_value("Company", filters.company, "default_currency") gross_profit_data = GrossProfitGenerator(filters) data = [] - group_wise_columns = frappe._dict({ - "invoice": ["invoice_or_item", "customer", "customer_group", "posting_date","item_code", "item_name","item_group", "brand", "description", - "warehouse", "qty", "base_rate", "buying_rate", "base_amount", - "buying_amount", "gross_profit", "gross_profit_percent", "project"], - "item_code": ["item_code", "item_name", "brand", "description", "qty", "base_rate", - "buying_rate", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], - "warehouse": ["warehouse", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "brand": ["brand", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "item_group": ["item_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "customer": ["customer", "customer_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "customer_group": ["customer_group", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "sales_person": ["sales_person", "allocated_amount", "qty", "base_rate", "buying_rate", "base_amount", "buying_amount", - "gross_profit", "gross_profit_percent"], - "project": ["project", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], - "territory": ["territory", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"] - }) + group_wise_columns = frappe._dict( + { + "invoice": [ + "invoice_or_item", + "customer", + "customer_group", + "posting_date", + "item_code", + "item_name", + "item_group", + "brand", + "description", + "warehouse", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + "project", + ], + "item_code": [ + "item_code", + "item_name", + "brand", + "description", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "warehouse": [ + "warehouse", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "brand": [ + "brand", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "item_group": [ + "item_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "customer": [ + "customer", + "customer_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "customer_group": [ + "customer_group", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "sales_person": [ + "sales_person", + "allocated_amount", + "qty", + "base_rate", + "buying_rate", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + "project": ["project", "base_amount", "buying_amount", "gross_profit", "gross_profit_percent"], + "territory": [ + "territory", + "base_amount", + "buying_amount", + "gross_profit", + "gross_profit_percent", + ], + } + ) columns = get_columns(group_wise_columns, filters) - if filters.group_by == 'Invoice': + if filters.group_by == "Invoice": get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data) else: @@ -50,11 +137,14 @@ def execute(filters=None): return columns, data -def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_wise_columns, data): + +def get_data_when_grouped_by_invoice( + columns, gross_profit_data, filters, group_wise_columns, data +): column_names = get_column_names() # to display item as Item Code: Item Name - columns[0] = 'Sales Invoice:Link/Item:300' + columns[0] = "Sales Invoice:Link/Item:300" # removing Item Code and Item Name columns del columns[4:6] @@ -69,80 +159,207 @@ def get_data_when_grouped_by_invoice(columns, gross_profit_data, filters, group_ data.append(row) + def get_data_when_not_grouped_by_invoice(gross_profit_data, filters, group_wise_columns, data): - for idx, src in enumerate(gross_profit_data.grouped_data): + for src in gross_profit_data.grouped_data: row = [] for col in group_wise_columns.get(scrub(filters.group_by)): row.append(src.get(col)) row.append(filters.currency) - if idx == len(gross_profit_data.grouped_data)-1: - row[0] = "Total" data.append(row) + def get_columns(group_wise_columns, filters): columns = [] - column_map = frappe._dict({ - "parent": _("Sales Invoice") + ":Link/Sales Invoice:120", - "invoice_or_item": _("Sales Invoice") + ":Link/Sales Invoice:120", - "posting_date": _("Posting Date") + ":Date:100", - "posting_time": _("Posting Time") + ":Data:100", - "item_code": _("Item Code") + ":Link/Item:100", - "item_name": _("Item Name") + ":Data:100", - "item_group": _("Item Group") + ":Link/Item Group:100", - "brand": _("Brand") + ":Link/Brand:100", - "description": _("Description") +":Data:100", - "warehouse": _("Warehouse") + ":Link/Warehouse:100", - "qty": _("Qty") + ":Float:80", - "base_rate": _("Avg. Selling Rate") + ":Currency/currency:100", - "buying_rate": _("Valuation Rate") + ":Currency/currency:100", - "base_amount": _("Selling Amount") + ":Currency/currency:100", - "buying_amount": _("Buying Amount") + ":Currency/currency:100", - "gross_profit": _("Gross Profit") + ":Currency/currency:100", - "gross_profit_percent": _("Gross Profit %") + ":Percent:100", - "project": _("Project") + ":Link/Project:100", - "sales_person": _("Sales person"), - "allocated_amount": _("Allocated Amount") + ":Currency/currency:100", - "customer": _("Customer") + ":Link/Customer:100", - "customer_group": _("Customer Group") + ":Link/Customer Group:100", - "territory": _("Territory") + ":Link/Territory:100" - }) + column_map = frappe._dict( + { + "parent": { + "label": _("Sales Invoice"), + "fieldname": "parent_invoice", + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 120, + }, + "invoice_or_item": { + "label": _("Sales Invoice"), + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 120, + }, + "posting_date": { + "label": _("Posting Date"), + "fieldname": "posting_date", + "fieldtype": "Date", + "width": 100, + }, + "posting_time": { + "label": _("Posting Time"), + "fieldname": "posting_time", + "fieldtype": "Data", + "width": 100, + }, + "item_code": { + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 100, + }, + "item_name": { + "label": _("Item Name"), + "fieldname": "item_name", + "fieldtype": "Data", + "width": 100, + }, + "item_group": { + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 100, + }, + "brand": {"label": _("Brand"), "fieldtype": "Link", "options": "Brand", "width": 100}, + "description": { + "label": _("Description"), + "fieldname": "description", + "fieldtype": "Data", + "width": 100, + }, + "warehouse": { + "label": _("Warehouse"), + "fieldname": "warehouse", + "fieldtype": "Link", + "options": "warehouse", + "width": 100, + }, + "qty": {"label": _("Qty"), "fieldname": "qty", "fieldtype": "Float", "width": 80}, + "base_rate": { + "label": _("Avg. Selling Rate"), + "fieldname": "avg._selling_rate", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "buying_rate": { + "label": _("Valuation Rate"), + "fieldname": "valuation_rate", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "base_amount": { + "label": _("Selling Amount"), + "fieldname": "selling_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "buying_amount": { + "label": _("Buying Amount"), + "fieldname": "buying_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "gross_profit": { + "label": _("Gross Profit"), + "fieldname": "gross_profit", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "gross_profit_percent": { + "label": _("Gross Profit Percent"), + "fieldname": "gross_profit_%", + "fieldtype": "Percent", + "width": 100, + }, + "project": { + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 100, + }, + "sales_person": { + "label": _("Sales Person"), + "fieldname": "sales_person", + "fieldtype": "Data", + "width": 100, + }, + "allocated_amount": { + "label": _("Allocated Amount"), + "fieldname": "allocated_amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + "customer": { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 100, + }, + "customer_group": { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "customer", + "width": 100, + }, + "territory": { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "territory", + "width": 100, + }, + } + ) for col in group_wise_columns.get(scrub(filters.group_by)): columns.append(column_map.get(col)) - columns.append({ - "fieldname": "currency", - "label" : _("Currency"), - "fieldtype": "Link", - "options": "Currency", - "hidden": 1 - }) + columns.append( + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Link", + "options": "Currency", + "hidden": 1, + } + ) return columns + def get_column_names(): - return frappe._dict({ - 'invoice_or_item': 'sales_invoice', - 'customer': 'customer', - 'customer_group': 'customer_group', - 'posting_date': 'posting_date', - 'item_code': 'item_code', - 'item_name': 'item_name', - 'item_group': 'item_group', - 'brand': 'brand', - 'description': 'description', - 'warehouse': 'warehouse', - 'qty': 'qty', - 'base_rate': 'avg._selling_rate', - 'buying_rate': 'valuation_rate', - 'base_amount': 'selling_amount', - 'buying_amount': 'buying_amount', - 'gross_profit': 'gross_profit', - 'gross_profit_percent': 'gross_profit_%', - 'project': 'project' - }) + return frappe._dict( + { + "invoice_or_item": "sales_invoice", + "customer": "customer", + "customer_group": "customer_group", + "posting_date": "posting_date", + "item_code": "item_code", + "item_name": "item_name", + "item_group": "item_group", + "brand": "brand", + "description": "description", + "warehouse": "warehouse", + "qty": "qty", + "base_rate": "avg._selling_rate", + "buying_rate": "valuation_rate", + "base_amount": "selling_amount", + "buying_amount": "buying_amount", + "gross_profit": "gross_profit", + "gross_profit_percent": "gross_profit_%", + "project": "project", + } + ) + class GrossProfitGenerator(object): def __init__(self, filters=None): @@ -151,7 +368,7 @@ class GrossProfitGenerator(object): self.filters = frappe._dict(filters) self.load_invoice_items() - if filters.group_by == 'Invoice': + if filters.group_by == "Invoice": self.group_items_by_invoice() self.load_stock_ledger_entries() @@ -173,7 +390,7 @@ class GrossProfitGenerator(object): buying_amount = 0 for row in reversed(self.si_list): - if self.skip_row(row, self.product_bundles): + if self.skip_row(row): continue row.base_amount = flt(row.base_net_amount, self.currency_precision) @@ -182,17 +399,19 @@ class GrossProfitGenerator(object): if row.update_stock: product_bundles = self.product_bundles.get(row.parenttype, {}).get(row.parent, frappe._dict()) elif row.dn_detail: - product_bundles = self.product_bundles.get("Delivery Note", {})\ - .get(row.delivery_note, frappe._dict()) + product_bundles = self.product_bundles.get("Delivery Note", {}).get( + row.delivery_note, frappe._dict() + ) row.item_row = row.dn_detail # get buying amount if row.item_code in product_bundles: - row.buying_amount = flt(self.get_buying_amount_from_product_bundle(row, - product_bundles[row.item_code]), self.currency_precision) + row.buying_amount = flt( + self.get_buying_amount_from_product_bundle(row, product_bundles[row.item_code]), + self.currency_precision, + ) else: - row.buying_amount = flt(self.get_buying_amount(row, row.item_code), - self.currency_precision) + row.buying_amount = flt(self.get_buying_amount(row, row.item_code), self.currency_precision) if grouped_by_invoice: if row.indent == 1.0: @@ -212,7 +431,9 @@ class GrossProfitGenerator(object): # calculate gross profit row.gross_profit = flt(row.base_amount - row.buying_amount, self.currency_precision) if row.base_amount: - row.gross_profit_percent = flt((row.gross_profit / row.base_amount) * 100.0, self.currency_precision) + row.gross_profit_percent = flt( + (row.gross_profit / row.base_amount) * 100.0, self.currency_precision + ) else: row.gross_profit_percent = 0.0 @@ -223,20 +444,10 @@ class GrossProfitGenerator(object): self.get_average_rate_based_on_group_by() def get_average_rate_based_on_group_by(self): - # sum buying / selling totals for group - self.totals = frappe._dict( - qty=0, - base_amount=0, - buying_amount=0, - gross_profit=0, - gross_profit_percent=0, - base_rate=0, - buying_rate=0 - ) for key in list(self.grouped): if self.filters.get("group_by") != "Invoice": for i, row in enumerate(self.grouped[key]): - if i==0: + if i == 0: new_row = row else: new_row.qty += flt(row.qty) @@ -244,55 +455,53 @@ class GrossProfitGenerator(object): new_row.base_amount += flt(row.base_amount, self.currency_precision) new_row = self.set_average_rate(new_row) self.grouped_data.append(new_row) - self.add_to_totals(new_row) else: for i, row in enumerate(self.grouped[key]): if row.indent == 1.0: - if row.parent in self.returned_invoices \ - and row.item_code in self.returned_invoices[row.parent]: + if ( + row.parent in self.returned_invoices and row.item_code in self.returned_invoices[row.parent] + ): returned_item_rows = self.returned_invoices[row.parent][row.item_code] for returned_item_row in returned_item_rows: row.qty += flt(returned_item_row.qty) row.base_amount += flt(returned_item_row.base_amount, self.currency_precision) row.buying_amount = flt(flt(row.qty) * flt(row.buying_rate), self.currency_precision) - if (flt(row.qty) or row.base_amount): + if flt(row.qty) or row.base_amount: row = self.set_average_rate(row) self.grouped_data.append(row) - self.add_to_totals(row) - - self.set_average_gross_profit(self.totals) - - if self.filters.get("group_by") == "Invoice": - self.totals.indent = 0.0 - self.totals.parent_invoice = "" - self.totals.invoice_or_item = "Total" - self.si_list.append(self.totals) - else: - self.grouped_data.append(self.totals) def is_not_invoice_row(self, row): - return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get("group_by") != "Invoice" + return (self.filters.get("group_by") == "Invoice" and row.indent != 0.0) or self.filters.get( + "group_by" + ) != "Invoice" def set_average_rate(self, new_row): self.set_average_gross_profit(new_row) - new_row.buying_rate = flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 - new_row.base_rate = flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + new_row.buying_rate = ( + flt(new_row.buying_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + ) + new_row.base_rate = ( + flt(new_row.base_amount / new_row.qty, self.float_precision) if new_row.qty else 0 + ) return new_row def set_average_gross_profit(self, new_row): new_row.gross_profit = flt(new_row.base_amount - new_row.buying_amount, self.currency_precision) - new_row.gross_profit_percent = flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) \ - if new_row.base_amount else 0 - new_row.buying_rate = flt(new_row.buying_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 - new_row.base_rate = flt(new_row.base_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 - - def add_to_totals(self, new_row): - for key in self.totals: - if new_row.get(key): - self.totals[key] += new_row[key] + new_row.gross_profit_percent = ( + flt(((new_row.gross_profit / new_row.base_amount) * 100.0), self.currency_precision) + if new_row.base_amount + else 0 + ) + new_row.buying_rate = ( + flt(new_row.buying_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 + ) + new_row.base_rate = ( + flt(new_row.base_amount / flt(new_row.qty), self.float_precision) if flt(new_row.qty) else 0 + ) def get_returned_invoice_items(self): - returned_invoices = frappe.db.sql(""" + returned_invoices = frappe.db.sql( + """ select si.name, si_item.item_code, si_item.stock_qty as qty, si_item.base_net_amount as base_amount, si.return_against from @@ -301,24 +510,27 @@ class GrossProfitGenerator(object): si.name = si_item.parent and si.docstatus = 1 and si.is_return = 1 - """, as_dict=1) + """, + as_dict=1, + ) self.returned_invoices = frappe._dict() for inv in returned_invoices: - self.returned_invoices.setdefault(inv.return_against, frappe._dict())\ - .setdefault(inv.item_code, []).append(inv) + self.returned_invoices.setdefault(inv.return_against, frappe._dict()).setdefault( + inv.item_code, [] + ).append(inv) - def skip_row(self, row, product_bundles): + def skip_row(self, row): if self.filters.get("group_by") != "Invoice": if not row.get(scrub(self.filters.get("group_by", ""))): return True - elif row.get("is_return") == 1: - return True + + return False def get_buying_amount_from_product_bundle(self, row, product_bundle): buying_amount = 0.0 for packed_item in product_bundle: - if packed_item.get("parent_detail_docname")==row.item_row: + if packed_item.get("parent_detail_docname") == row.item_row: buying_amount += self.get_buying_amount(row, packed_item.item_code) return flt(buying_amount, self.currency_precision) @@ -328,7 +540,7 @@ class GrossProfitGenerator(object): # stock_ledger_entries should already be filtered by item_code and warehouse and # sorted by posting_date desc, posting_time desc if item_code in self.non_stock_items and (row.project or row.cost_center): - #Issue 6089-Get last purchasing rate for non-stock item + # Issue 6089-Get last purchasing rate for non-stock item item_rate = self.get_last_purchase_rate(item_code, row) return flt(row.qty) * item_rate @@ -341,15 +553,17 @@ class GrossProfitGenerator(object): for i, sle in enumerate(my_sle): # find the stock valution rate from stock ledger entry - if sle.voucher_type == parenttype and parent == sle.voucher_no and \ - sle.voucher_detail_no == row.item_row: - previous_stock_value = len(my_sle) > i+1 and \ - flt(my_sle[i+1].stock_value) or 0.0 + if ( + sle.voucher_type == parenttype + and parent == sle.voucher_no + and sle.voucher_detail_no == row.item_row + ): + previous_stock_value = len(my_sle) > i + 1 and flt(my_sle[i + 1].stock_value) or 0.0 - if previous_stock_value: - return (previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) - else: - return flt(row.qty) * self.get_average_buying_rate(row, item_code) + if previous_stock_value: + return abs(previous_stock_value - flt(sle.stock_value)) * flt(row.qty) / abs(flt(sle.qty)) + else: + return flt(row.qty) * self.get_average_buying_rate(row, item_code) else: return flt(row.qty) * self.get_average_buying_rate(row, item_code) @@ -358,33 +572,43 @@ class GrossProfitGenerator(object): def get_average_buying_rate(self, row, item_code): args = row if not item_code in self.average_buying_rate: - args.update({ - 'voucher_type': row.parenttype, - 'voucher_no': row.parent, - 'allow_zero_valuation': True, - 'company': self.filters.company - }) + args.update( + { + "voucher_type": row.parenttype, + "voucher_no": row.parent, + "allow_zero_valuation": True, + "company": self.filters.company, + } + ) average_buying_rate = get_incoming_rate(args) - self.average_buying_rate[item_code] = flt(average_buying_rate) + self.average_buying_rate[item_code] = flt(average_buying_rate) return self.average_buying_rate[item_code] def get_last_purchase_rate(self, item_code, row): - condition = '' - if row.project: - condition += " AND a.project=%s" % (frappe.db.escape(row.project)) - elif row.cost_center: - condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center)) - if self.filters.to_date: - condition += " AND modified='%s'" % (self.filters.to_date) + purchase_invoice = frappe.qb.DocType("Purchase Invoice") + purchase_invoice_item = frappe.qb.DocType("Purchase Invoice Item") - last_purchase_rate = frappe.db.sql(""" - select (a.base_rate / a.conversion_factor) - from `tabPurchase Invoice Item` a - where a.item_code = %s and a.docstatus=1 - {0} - order by a.modified desc limit 1""".format(condition), item_code) + query = ( + frappe.qb.from_(purchase_invoice_item) + .inner_join(purchase_invoice) + .on(purchase_invoice.name == purchase_invoice_item.parent) + .select(purchase_invoice_item.base_rate / purchase_invoice_item.conversion_factor) + .where(purchase_invoice.docstatus == 1) + .where(purchase_invoice.posting_date <= self.filters.to_date) + .where(purchase_invoice_item.item_code == item_code) + ) + + if row.project: + query.where(purchase_invoice_item.project == row.project) + + if row.cost_center: + query.where(purchase_invoice_item.cost_center == row.cost_center) + + query.orderby(purchase_invoice.posting_date, order=frappe.qb.desc) + query.limit(1) + last_purchase_rate = query.run() return flt(last_purchase_rate[0][0]) if last_purchase_rate else 0 @@ -397,7 +621,7 @@ class GrossProfitGenerator(object): if self.filters.to_date: conditions += " and posting_date <= %(to_date)s" - if self.filters.group_by=="Sales Person": + if self.filters.group_by == "Sales Person": sales_person_cols = ", sales.sales_person, sales.allocated_amount, sales.incentives" sales_team_table = "left join `tabSales Team` sales on sales.parent = `tabSales Invoice`.name" else: @@ -410,7 +634,8 @@ class GrossProfitGenerator(object): if self.filters.get("item_code"): conditions += " and `tabSales Invoice Item`.item_code = %(item_code)s" - self.si_list = frappe.db.sql(""" + self.si_list = frappe.db.sql( + """ select `tabSales Invoice Item`.parenttype, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.posting_time, @@ -432,13 +657,19 @@ class GrossProfitGenerator(object): where `tabSales Invoice`.docstatus=1 and `tabSales Invoice`.is_opening!='Yes' {conditions} {match_cond} order by - `tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""" - .format(conditions=conditions, sales_person_cols=sales_person_cols, - sales_team_table=sales_team_table, match_cond = get_match_cond('Sales Invoice')), self.filters, as_dict=1) + `tabSales Invoice`.posting_date desc, `tabSales Invoice`.posting_time desc""".format( + conditions=conditions, + sales_person_cols=sales_person_cols, + sales_team_table=sales_team_table, + match_cond=get_match_cond("Sales Invoice"), + ), + self.filters, + as_dict=1, + ) def group_items_by_invoice(self): """ - Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. + Turns list of Sales Invoice Items to a tree of Sales Invoices with their Items as children. """ parents = [] @@ -461,94 +692,96 @@ class GrossProfitGenerator(object): row.parent_invoice = row.parent row.invoice_or_item = row.item_code - if frappe.db.exists('Product Bundle', row.item_code): + if frappe.db.exists("Product Bundle", row.item_code): self.add_bundle_items(row, index) def get_invoice_row(self, row): - return frappe._dict({ - 'parent_invoice': "", - 'indent': 0.0, - 'invoice_or_item': row.parent, - 'parent': None, - 'posting_date': row.posting_date, - 'posting_time': row.posting_time, - 'project': row.project, - 'update_stock': row.update_stock, - 'customer': row.customer, - 'customer_group': row.customer_group, - 'item_code': None, - 'item_name': None, - 'description': None, - 'warehouse': None, - 'item_group': None, - 'brand': None, - 'dn_detail': None, - 'delivery_note': None, - 'qty': None, - 'item_row': None, - 'is_return': row.is_return, - 'cost_center': row.cost_center, - 'base_net_amount': frappe.db.get_value('Sales Invoice', row.parent, 'base_net_total') - }) + return frappe._dict( + { + "parent_invoice": "", + "indent": 0.0, + "invoice_or_item": row.parent, + "parent": None, + "posting_date": row.posting_date, + "posting_time": row.posting_time, + "project": row.project, + "update_stock": row.update_stock, + "customer": row.customer, + "customer_group": row.customer_group, + "item_code": None, + "item_name": None, + "description": None, + "warehouse": None, + "item_group": None, + "brand": None, + "dn_detail": None, + "delivery_note": None, + "qty": None, + "item_row": None, + "is_return": row.is_return, + "cost_center": row.cost_center, + "base_net_amount": frappe.db.get_value("Sales Invoice", row.parent, "base_net_total"), + } + ) def add_bundle_items(self, product_bundle, index): bundle_items = self.get_bundle_items(product_bundle) for i, item in enumerate(bundle_items): bundle_item = self.get_bundle_item_row(product_bundle, item) - self.si_list.insert((index+i+1), bundle_item) + self.si_list.insert((index + i + 1), bundle_item) def get_bundle_items(self, product_bundle): return frappe.get_all( - 'Product Bundle Item', - filters = { - 'parent': product_bundle.item_code - }, - fields = ['item_code', 'qty'] + "Product Bundle Item", filters={"parent": product_bundle.item_code}, fields=["item_code", "qty"] ) def get_bundle_item_row(self, product_bundle, item): item_name, description, item_group, brand = self.get_bundle_item_details(item.item_code) - return frappe._dict({ - 'parent_invoice': product_bundle.item_code, - 'indent': product_bundle.indent + 1, - 'parent': None, - 'invoice_or_item': item.item_code, - 'posting_date': product_bundle.posting_date, - 'posting_time': product_bundle.posting_time, - 'project': product_bundle.project, - 'customer': product_bundle.customer, - 'customer_group': product_bundle.customer_group, - 'item_code': item.item_code, - 'item_name': item_name, - 'description': description, - 'warehouse': product_bundle.warehouse, - 'item_group': item_group, - 'brand': brand, - 'dn_detail': product_bundle.dn_detail, - 'delivery_note': product_bundle.delivery_note, - 'qty': (flt(product_bundle.qty) * flt(item.qty)), - 'item_row': None, - 'is_return': product_bundle.is_return, - 'cost_center': product_bundle.cost_center - }) + return frappe._dict( + { + "parent_invoice": product_bundle.item_code, + "indent": product_bundle.indent + 1, + "parent": None, + "invoice_or_item": item.item_code, + "posting_date": product_bundle.posting_date, + "posting_time": product_bundle.posting_time, + "project": product_bundle.project, + "customer": product_bundle.customer, + "customer_group": product_bundle.customer_group, + "item_code": item.item_code, + "item_name": item_name, + "description": description, + "warehouse": product_bundle.warehouse, + "item_group": item_group, + "brand": brand, + "dn_detail": product_bundle.dn_detail, + "delivery_note": product_bundle.delivery_note, + "qty": (flt(product_bundle.qty) * flt(item.qty)), + "item_row": None, + "is_return": product_bundle.is_return, + "cost_center": product_bundle.cost_center, + } + ) def get_bundle_item_details(self, item_code): return frappe.db.get_value( - 'Item', - item_code, - ['item_name', 'description', 'item_group', 'brand'] + "Item", item_code, ["item_name", "description", "item_group", "brand"] ) def load_stock_ledger_entries(self): - res = frappe.db.sql("""select item_code, voucher_type, voucher_no, + res = frappe.db.sql( + """select item_code, voucher_type, voucher_no, voucher_detail_no, stock_value, warehouse, actual_qty as qty from `tabStock Ledger Entry` where company=%(company)s and is_cancelled = 0 order by item_code desc, warehouse desc, posting_date desc, - posting_time desc, creation desc""", self.filters, as_dict=True) + posting_time desc, creation desc""", + self.filters, + as_dict=True, + ) self.sle = {} for r in res: if (r.item_code, r.warehouse) not in self.sle: @@ -559,12 +792,18 @@ class GrossProfitGenerator(object): def load_product_bundle(self): self.product_bundles = {} - for d in frappe.db.sql("""select parenttype, parent, parent_item, + for d in frappe.db.sql( + """select parenttype, parent, parent_item, item_code, warehouse, -1*qty as total_qty, parent_detail_docname - from `tabPacked Item` where docstatus=1""", as_dict=True): - self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault(d.parent, - frappe._dict()).setdefault(d.parent_item, []).append(d) + from `tabPacked Item` where docstatus=1""", + as_dict=True, + ): + self.product_bundles.setdefault(d.parenttype, frappe._dict()).setdefault( + d.parent, frappe._dict() + ).setdefault(d.parent_item, []).append(d) def load_non_stock_items(self): - self.non_stock_items = frappe.db.sql_list("""select name from tabItem - where is_stock_item=0""") + self.non_stock_items = frappe.db.sql_list( + """select name from tabItem + where is_stock_item=0""" + ) diff --git a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py index 2f23c8ed1d6..8db72de22f3 100644 --- a/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py +++ b/erpnext/accounts/report/inactive_sales_items/inactive_sales_items.py @@ -12,6 +12,7 @@ def execute(filters=None): data = get_data(filters) return columns, data + def get_columns(): columns = [ { @@ -19,53 +20,36 @@ def get_columns(): "fieldtype": "Link", "label": _("Territory"), "options": "Territory", - "width": 100 + "width": 100, }, { "fieldname": "item_group", "fieldtype": "Link", "label": _("Item Group"), "options": "Item Group", - "width": 150 + "width": 150, }, - { - "fieldname": "item", - "fieldtype": "Link", - "options": "Item", - "label": "Item", - "width": 150 - }, - { - "fieldname": "item_name", - "fieldtype": "Data", - "label": _("Item Name"), - "width": 150 - }, - + {"fieldname": "item", "fieldtype": "Link", "options": "Item", "label": "Item", "width": 150}, + {"fieldname": "item_name", "fieldtype": "Data", "label": _("Item Name"), "width": 150}, { "fieldname": "customer", "fieldtype": "Link", "label": _("Customer"), "options": "Customer", - "width": 100 + "width": 100, }, { "fieldname": "last_order_date", "fieldtype": "Date", "label": _("Last Order Date"), - "width": 100 - }, - { - "fieldname": "qty", - "fieldtype": "Float", - "label": _("Quantity"), - "width": 100 + "width": 100, }, + {"fieldname": "qty", "fieldtype": "Float", "label": _("Quantity"), "width": 100}, { "fieldname": "days_since_last_order", "fieldtype": "Int", "label": _("Days Since Last Order"), - "width": 100 + "width": 100, }, ] @@ -84,19 +68,21 @@ def get_data(filters): "territory": territory.name, "item_group": item.item_group, "item": item.item_code, - "item_name": item.item_name + "item_name": item.item_name, } - if sales_invoice_data.get((territory.name,item.item_code)): - item_obj = sales_invoice_data[(territory.name,item.item_code)] - if item_obj.days_since_last_order > cint(filters['days']): - row.update({ - "territory": item_obj.territory, - "customer": item_obj.customer, - "last_order_date": item_obj.last_order_date, - "qty": item_obj.qty, - "days_since_last_order": item_obj.days_since_last_order - }) + if sales_invoice_data.get((territory.name, item.item_code)): + item_obj = sales_invoice_data[(territory.name, item.item_code)] + if item_obj.days_since_last_order > cint(filters["days"]): + row.update( + { + "territory": item_obj.territory, + "customer": item_obj.customer, + "last_order_date": item_obj.last_order_date, + "qty": item_obj.qty, + "days_since_last_order": item_obj.days_since_last_order, + } + ) else: continue @@ -111,45 +97,49 @@ def get_sales_details(filters): date_field = "s.transaction_date" if filters["based_on"] == "Sales Order" else "s.posting_date" - sales_data = frappe.db.sql(""" + sales_data = frappe.db.sql( + """ select s.territory, s.customer, si.item_group, si.item_code, si.qty, {date_field} as last_order_date, DATEDIFF(CURDATE(), {date_field}) as days_since_last_order from `tab{doctype}` s, `tab{doctype} Item` si where s.name = si.parent and s.docstatus = 1 - order by days_since_last_order """ #nosec - .format(date_field = date_field, doctype = filters['based_on']), as_dict=1) + order by days_since_last_order """.format( # nosec + date_field=date_field, doctype=filters["based_on"] + ), + as_dict=1, + ) for d in sales_data: - item_details_map.setdefault((d.territory,d.item_code), d) + item_details_map.setdefault((d.territory, d.item_code), d) return item_details_map + def get_territories(filters): filter_dict = {} if filters.get("territory"): - filter_dict.update({'name': filters['territory']}) + filter_dict.update({"name": filters["territory"]}) territories = frappe.get_all("Territory", fields=["name"], filters=filter_dict) return territories + def get_items(filters): - filters_dict = { - "disabled": 0, - "is_stock_item": 1 - } + filters_dict = {"disabled": 0, "is_stock_item": 1} if filters.get("item_group"): - filters_dict.update({ - "item_group": filters["item_group"] - }) + filters_dict.update({"item_group": filters["item_group"]}) if filters.get("item"): - filters_dict.update({ - "name": filters["item"] - }) + filters_dict.update({"name": filters["item"]}) - items = frappe.get_all("Item", fields=["name", "item_group", "item_name", "item_code"], filters=filters_dict, order_by="name") + items = frappe.get_all( + "Item", + fields=["name", "item_group", "item_name", "item_code"], + filters=filters_dict, + order_by="name", + ) return items diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py index aaed58d070d..c04b9c71252 100644 --- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py +++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py @@ -21,8 +21,10 @@ from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history impo def execute(filters=None): return _execute(filters) + def _execute(filters=None, additional_table_columns=None, additional_query_columns=None): - if not filters: filters = {} + if not filters: + filters = {} columns = get_columns(additional_table_columns, filters) company_currency = erpnext.get_company_currency(filters.company) @@ -30,18 +32,23 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum item_list = get_items(filters, additional_query_columns) aii_account_map = get_aii_accounts() if item_list: - itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency, - doctype='Purchase Invoice', tax_doctype='Purchase Taxes and Charges') + itemised_tax, tax_columns = get_tax_accounts( + item_list, + columns, + company_currency, + doctype="Purchase Invoice", + tax_doctype="Purchase Taxes and Charges", + ) po_pr_map = get_purchase_receipts_against_purchase_order(item_list) data = [] total_row_map = {} skip_total_row = 0 - prev_group_by_value = '' + prev_group_by_value = "" - if filters.get('group_by'): - grand_total = get_grand_total(filters, 'Purchase Invoice') + if filters.get("group_by"): + grand_total = get_grand_total(filters, "Purchase Invoice") item_details = get_item_details() @@ -57,71 +64,81 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum elif d.po_detail: purchase_receipt = ", ".join(po_pr_map.get(d.po_detail, [])) - expense_account = d.unrealized_profit_loss_account or d.expense_account \ - or aii_account_map.get(d.company) + expense_account = ( + d.unrealized_profit_loss_account or d.expense_account or aii_account_map.get(d.company) + ) row = { - 'item_code': d.item_code, - 'item_name': item_record.item_name if item_record else d.item_name, - 'item_group': item_record.item_group if item_record else d.item_group, - 'description': d.description, - 'invoice': d.parent, - 'posting_date': d.posting_date, - 'supplier': d.supplier, - 'supplier_name': d.supplier_name + "item_code": d.item_code, + "item_name": item_record.item_name if item_record else d.item_name, + "item_group": item_record.item_group if item_record else d.item_group, + "description": d.description, + "invoice": d.parent, + "posting_date": d.posting_date, + "supplier": d.supplier, + "supplier_name": d.supplier_name, } if additional_query_columns: for col in additional_query_columns: - row.update({ - col: d.get(col) - }) + row.update({col: d.get(col)}) - row.update({ - 'credit_to': d.credit_to, - 'mode_of_payment': d.mode_of_payment, - 'project': d.project, - 'company': d.company, - 'purchase_order': d.purchase_order, - 'purchase_receipt': d.purchase_receipt, - 'expense_account': expense_account, - 'stock_qty': d.stock_qty, - 'stock_uom': d.stock_uom, - 'rate': d.base_net_amount / d.stock_qty, - 'amount': d.base_net_amount - }) + row.update( + { + "credit_to": d.credit_to, + "mode_of_payment": d.mode_of_payment, + "project": d.project, + "company": d.company, + "purchase_order": d.purchase_order, + "purchase_receipt": d.purchase_receipt, + "expense_account": expense_account, + "stock_qty": d.stock_qty, + "stock_uom": d.stock_uom, + "rate": d.base_net_amount / d.stock_qty, + "amount": d.base_net_amount, + } + ) total_tax = 0 for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) - row.update({ - frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0), - frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0), - }) - total_tax += flt(item_tax.get('tax_amount')) + row.update( + { + frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0), + frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0), + } + ) + total_tax += flt(item_tax.get("tax_amount")) - row.update({ - 'total_tax': total_tax, - 'total': d.base_net_amount + total_tax, - 'currency': company_currency - }) + row.update( + {"total_tax": total_tax, "total": d.base_net_amount + total_tax, "currency": company_currency} + ) - if filters.get('group_by'): - row.update({'percent_gt': flt(row['total']/grand_total) * 100}) + if filters.get("group_by"): + row.update({"percent_gt": flt(row["total"] / grand_total) * 100}) group_by_field, subtotal_display_field = get_group_by_and_display_fields(filters) - data, prev_group_by_value = add_total_row(data, filters, prev_group_by_value, d, total_row_map, - group_by_field, subtotal_display_field, grand_total, tax_columns) - add_sub_total_row(row, total_row_map, d.get(group_by_field, ''), tax_columns) + data, prev_group_by_value = add_total_row( + data, + filters, + prev_group_by_value, + d, + total_row_map, + group_by_field, + subtotal_display_field, + grand_total, + tax_columns, + ) + add_sub_total_row(row, total_row_map, d.get(group_by_field, ""), tax_columns) data.append(row) - if filters.get('group_by') and item_list: - total_row = total_row_map.get(prev_group_by_value or d.get('item_name')) - total_row['percent_gt'] = flt(total_row['total']/grand_total * 100) + if filters.get("group_by") and item_list: + total_row = total_row_map.get(prev_group_by_value or d.get("item_name")) + total_row["percent_gt"] = flt(total_row["total"] / grand_total * 100) data.append(total_row) data.append({}) - add_sub_total_row(total_row, total_row_map, 'total_row', tax_columns) - data.append(total_row_map.get('total_row')) + add_sub_total_row(total_row, total_row_map, "total_row", tax_columns) + data.append(total_row_map.get("total_row")) skip_total_row = 1 return columns, data, None, None, None, skip_total_row @@ -131,195 +148,180 @@ def get_columns(additional_table_columns, filters): columns = [] - if filters.get('group_by') != ('Item'): + if filters.get("group_by") != ("Item"): columns.extend( [ { - 'label': _('Item Code'), - 'fieldname': 'item_code', - 'fieldtype': 'Link', - 'options': 'Item', - 'width': 120 + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + ] + ) + + if filters.get("group_by") not in ("Item", "Item Group"): + columns.extend( + [ { - 'label': _('Item Name'), - 'fieldname': 'item_name', - 'fieldtype': 'Data', - 'width': 120 + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 120, } ] ) - if filters.get('group_by') not in ('Item', 'Item Group'): - columns.extend([ + columns.extend( + [ + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 150}, { - 'label': _('Item Group'), - 'fieldname': 'item_group', - 'fieldtype': 'Link', - 'options': 'Item Group', - 'width': 120 - } - ]) - - columns.extend([ - { - 'label': _('Description'), - 'fieldname': 'description', - 'fieldtype': 'Data', - 'width': 150 - }, - { - 'label': _('Invoice'), - 'fieldname': 'invoice', - 'fieldtype': 'Link', - 'options': 'Purchase Invoice', - 'width': 120 - }, - { - 'label': _('Posting Date'), - 'fieldname': 'posting_date', - 'fieldtype': 'Date', - 'width': 120 - } - ]) - - if filters.get('group_by') != 'Supplier': - columns.extend([ - { - 'label': _('Supplier'), - 'fieldname': 'supplier', - 'fieldtype': 'Link', - 'options': 'Supplier', - 'width': 120 + "label": _("Invoice"), + "fieldname": "invoice", + "fieldtype": "Link", + "options": "Purchase Invoice", + "width": 120, }, - { - 'label': _('Supplier Name'), - 'fieldname': 'supplier_name', - 'fieldtype': 'Data', - 'width': 120 - } - ]) + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, + ] + ) + + if filters.get("group_by") != "Supplier": + columns.extend( + [ + { + "label": _("Supplier"), + "fieldname": "supplier", + "fieldtype": "Link", + "options": "Supplier", + "width": 120, + }, + {"label": _("Supplier Name"), "fieldname": "supplier_name", "fieldtype": "Data", "width": 120}, + ] + ) if additional_table_columns: columns += additional_table_columns columns += [ { - 'label': _('Payable Account'), - 'fieldname': 'credit_to', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 80 + "label": _("Payable Account"), + "fieldname": "credit_to", + "fieldtype": "Link", + "options": "Account", + "width": 80, }, { - 'label': _('Mode Of Payment'), - 'fieldname': 'mode_of_payment', - 'fieldtype': 'Link', - 'options': 'Mode of Payment', - 'width': 120 + "label": _("Mode Of Payment"), + "fieldname": "mode_of_payment", + "fieldtype": "Link", + "options": "Mode of Payment", + "width": 120, }, { - 'label': _('Project'), - 'fieldname': 'project', - 'fieldtype': 'Link', - 'options': 'Project', - 'width': 80 + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 80, }, { - 'label': _('Company'), - 'fieldname': 'company', - 'fieldtype': 'Link', - 'options': 'Company', - 'width': 80 + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 80, }, { - 'label': _('Purchase Order'), - 'fieldname': 'purchase_order', - 'fieldtype': 'Link', - 'options': 'Purchase Order', - 'width': 100 + "label": _("Purchase Order"), + "fieldname": "purchase_order", + "fieldtype": "Link", + "options": "Purchase Order", + "width": 100, }, { - 'label': _("Purchase Receipt"), - 'fieldname': 'Purchase Receipt', - 'fieldtype': 'Link', - 'options': 'Purchase Receipt', - 'width': 100 + "label": _("Purchase Receipt"), + "fieldname": "Purchase Receipt", + "fieldtype": "Link", + "options": "Purchase Receipt", + "width": 100, }, { - 'label': _('Expense Account'), - 'fieldname': 'expense_account', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 100 + "label": _("Expense Account"), + "fieldname": "expense_account", + "fieldtype": "Link", + "options": "Account", + "width": 100, + }, + {"label": _("Stock Qty"), "fieldname": "stock_qty", "fieldtype": "Float", "width": 100}, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 100, }, { - 'label': _('Stock Qty'), - 'fieldname': 'stock_qty', - 'fieldtype': 'Float', - 'width': 100 + "label": _("Rate"), + "fieldname": "rate", + "fieldtype": "Float", + "options": "currency", + "width": 100, }, { - 'label': _('Stock UOM'), - 'fieldname': 'stock_uom', - 'fieldtype': 'Link', - 'options': 'UOM', - 'width': 100 + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, }, - { - 'label': _('Rate'), - 'fieldname': 'rate', - 'fieldtype': 'Float', - 'options': 'currency', - 'width': 100 - }, - { - 'label': _('Amount'), - 'fieldname': 'amount', - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 100 - } ] - if filters.get('group_by'): - columns.append({ - 'label': _('% Of Grand Total'), - 'fieldname': 'percent_gt', - 'fieldtype': 'Float', - 'width': 80 - }) + if filters.get("group_by"): + columns.append( + {"label": _("% Of Grand Total"), "fieldname": "percent_gt", "fieldtype": "Float", "width": 80} + ) return columns + def get_conditions(filters): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("supplier", " and `tabPurchase Invoice`.supplier = %(supplier)s"), ("item_code", " and `tabPurchase Invoice Item`.item_code = %(item_code)s"), ("from_date", " and `tabPurchase Invoice`.posting_date>=%(from_date)s"), ("to_date", " and `tabPurchase Invoice`.posting_date<=%(to_date)s"), - ("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s")): - if filters.get(opts[0]): - conditions += opts[1] + ("mode_of_payment", " and ifnull(mode_of_payment, '') = %(mode_of_payment)s"), + ): + if filters.get(opts[0]): + conditions += opts[1] if not filters.get("group_by"): - conditions += "ORDER BY `tabPurchase Invoice`.posting_date desc, `tabPurchase Invoice Item`.item_code desc" + conditions += ( + "ORDER BY `tabPurchase Invoice`.posting_date desc, `tabPurchase Invoice Item`.item_code desc" + ) else: - conditions += get_group_by_conditions(filters, 'Purchase Invoice') + conditions += get_group_by_conditions(filters, "Purchase Invoice") return conditions + def get_items(filters, additional_query_columns): conditions = get_conditions(filters) if additional_query_columns: - additional_query_columns = ', ' + ', '.join(additional_query_columns) + additional_query_columns = ", " + ", ".join(additional_query_columns) else: - additional_query_columns = '' + additional_query_columns = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ select `tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`, `tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company, @@ -335,22 +337,35 @@ def get_items(filters, additional_query_columns): from `tabPurchase Invoice`, `tabPurchase Invoice Item` where `tabPurchase Invoice`.name = `tabPurchase Invoice Item`.`parent` and `tabPurchase Invoice`.docstatus = 1 %s - """.format(additional_query_columns) % (conditions), filters, as_dict=1) + """.format( + additional_query_columns + ) + % (conditions), + filters, + as_dict=1, + ) + def get_aii_accounts(): return dict(frappe.db.sql("select name, stock_received_but_not_billed from tabCompany")) + def get_purchase_receipts_against_purchase_order(item_list): po_pr_map = frappe._dict() po_item_rows = list(set(d.po_detail for d in item_list)) if po_item_rows: - purchase_receipts = frappe.db.sql(""" + purchase_receipts = frappe.db.sql( + """ select parent, purchase_order_item from `tabPurchase Receipt Item` where docstatus=1 and purchase_order_item in (%s) group by purchase_order_item, parent - """ % (', '.join(['%s']*len(po_item_rows))), tuple(po_item_rows), as_dict=1) + """ + % (", ".join(["%s"] * len(po_item_rows))), + tuple(po_item_rows), + as_dict=1, + ) for pr in purchase_receipts: po_pr_map.setdefault(pr.po_detail, []).append(pr.parent) diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py index 9b35538bb68..dd9c0736128 100644 --- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py +++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py @@ -18,13 +18,20 @@ from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history impo def execute(filters=None): return _execute(filters) -def _execute(filters=None, additional_table_columns=None, additional_query_columns=None): - if not filters: filters = {} + +def _execute( + filters=None, + additional_table_columns=None, + additional_query_columns=None, + additional_conditions=None, +): + if not filters: + filters = {} columns = get_columns(additional_table_columns, filters) - company_currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency') + company_currency = frappe.get_cached_value("Company", filters.get("company"), "default_currency") - item_list = get_items(filters, additional_query_columns) + item_list = get_items(filters, additional_query_columns, additional_conditions) if item_list: itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency) @@ -34,10 +41,10 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum data = [] total_row_map = {} skip_total_row = 0 - prev_group_by_value = '' + prev_group_by_value = "" - if filters.get('group_by'): - grand_total = get_grand_total(filters, 'Sales Invoice') + if filters.get("group_by"): + grand_total = get_grand_total(filters, "Sales Invoice") customer_details = get_customer_details() item_details = get_item_details() @@ -56,289 +63,291 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum delivery_note = d.parent row = { - 'item_code': d.item_code, - 'item_name': item_record.item_name if item_record else d.item_name, - 'item_group': item_record.item_group if item_record else d.item_group, - 'description': d.description, - 'invoice': d.parent, - 'posting_date': d.posting_date, - 'customer': d.customer, - 'customer_name': customer_record.customer_name, - 'customer_group': customer_record.customer_group, + "item_code": d.item_code, + "item_name": item_record.item_name if item_record else d.item_name, + "item_group": item_record.item_group if item_record else d.item_group, + "description": d.description, + "invoice": d.parent, + "posting_date": d.posting_date, + "customer": d.customer, + "customer_name": customer_record.customer_name, + "customer_group": customer_record.customer_group, } if additional_query_columns: for col in additional_query_columns: - row.update({ - col: d.get(col) - }) + row.update({col: d.get(col)}) - row.update({ - 'debit_to': d.debit_to, - 'mode_of_payment': ", ".join(mode_of_payments.get(d.parent, [])), - 'territory': d.territory, - 'project': d.project, - 'company': d.company, - 'sales_order': d.sales_order, - 'delivery_note': d.delivery_note, - 'income_account': d.unrealized_profit_loss_account if d.is_internal_customer == 1 else d.income_account, - 'cost_center': d.cost_center, - 'stock_qty': d.stock_qty, - 'stock_uom': d.stock_uom - }) + row.update( + { + "debit_to": d.debit_to, + "mode_of_payment": ", ".join(mode_of_payments.get(d.parent, [])), + "territory": d.territory, + "project": d.project, + "company": d.company, + "sales_order": d.sales_order, + "delivery_note": d.delivery_note, + "income_account": d.unrealized_profit_loss_account + if d.is_internal_customer == 1 + else d.income_account, + "cost_center": d.cost_center, + "stock_qty": d.stock_qty, + "stock_uom": d.stock_uom, + } + ) if d.stock_uom != d.uom and d.stock_qty: - row.update({ - 'rate': (d.base_net_rate * d.qty)/d.stock_qty, - 'amount': d.base_net_amount - }) + row.update({"rate": (d.base_net_rate * d.qty) / d.stock_qty, "amount": d.base_net_amount}) else: - row.update({ - 'rate': d.base_net_rate, - 'amount': d.base_net_amount - }) + row.update({"rate": d.base_net_rate, "amount": d.base_net_amount}) total_tax = 0 + total_other_charges = 0 for tax in tax_columns: item_tax = itemised_tax.get(d.name, {}).get(tax, {}) - row.update({ - frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0), - frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0), - }) - total_tax += flt(item_tax.get('tax_amount')) + row.update( + { + frappe.scrub(tax + " Rate"): item_tax.get("tax_rate", 0), + frappe.scrub(tax + " Amount"): item_tax.get("tax_amount", 0), + } + ) + if item_tax.get("is_other_charges"): + total_other_charges += flt(item_tax.get("tax_amount")) + else: + total_tax += flt(item_tax.get("tax_amount")) - row.update({ - 'total_tax': total_tax, - 'total': d.base_net_amount + total_tax, - 'currency': company_currency - }) + row.update( + { + "total_tax": total_tax, + "total_other_charges": total_other_charges, + "total": d.base_net_amount + total_tax, + "currency": company_currency, + } + ) - if filters.get('group_by'): - row.update({'percent_gt': flt(row['total']/grand_total) * 100}) + if filters.get("group_by"): + row.update({"percent_gt": flt(row["total"] / grand_total) * 100}) group_by_field, subtotal_display_field = get_group_by_and_display_fields(filters) - data, prev_group_by_value = add_total_row(data, filters, prev_group_by_value, d, total_row_map, - group_by_field, subtotal_display_field, grand_total, tax_columns) - add_sub_total_row(row, total_row_map, d.get(group_by_field, ''), tax_columns) + data, prev_group_by_value = add_total_row( + data, + filters, + prev_group_by_value, + d, + total_row_map, + group_by_field, + subtotal_display_field, + grand_total, + tax_columns, + ) + add_sub_total_row(row, total_row_map, d.get(group_by_field, ""), tax_columns) data.append(row) - if filters.get('group_by') and item_list: - total_row = total_row_map.get(prev_group_by_value or d.get('item_name')) - total_row['percent_gt'] = flt(total_row['total']/grand_total * 100) + if filters.get("group_by") and item_list: + total_row = total_row_map.get(prev_group_by_value or d.get("item_name")) + total_row["percent_gt"] = flt(total_row["total"] / grand_total * 100) data.append(total_row) data.append({}) - add_sub_total_row(total_row, total_row_map, 'total_row', tax_columns) - data.append(total_row_map.get('total_row')) + add_sub_total_row(total_row, total_row_map, "total_row", tax_columns) + data.append(total_row_map.get("total_row")) skip_total_row = 1 return columns, data, None, None, None, skip_total_row + def get_columns(additional_table_columns, filters): columns = [] - if filters.get('group_by') != ('Item'): + if filters.get("group_by") != ("Item"): columns.extend( [ { - 'label': _('Item Code'), - 'fieldname': 'item_code', - 'fieldtype': 'Link', - 'options': 'Item', - 'width': 120 + "label": _("Item Code"), + "fieldname": "item_code", + "fieldtype": "Link", + "options": "Item", + "width": 120, }, + {"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 120}, + ] + ) + + if filters.get("group_by") not in ("Item", "Item Group"): + columns.extend( + [ { - 'label': _('Item Name'), - 'fieldname': 'item_name', - 'fieldtype': 'Data', - 'width': 120 + "label": _("Item Group"), + "fieldname": "item_group", + "fieldtype": "Link", + "options": "Item Group", + "width": 120, } ] ) - if filters.get('group_by') not in ('Item', 'Item Group'): - columns.extend([ + columns.extend( + [ + {"label": _("Description"), "fieldname": "description", "fieldtype": "Data", "width": 150}, { - 'label': _('Item Group'), - 'fieldname': 'item_group', - 'fieldtype': 'Link', - 'options': 'Item Group', - 'width': 120 - } - ]) - - columns.extend([ - { - 'label': _('Description'), - 'fieldname': 'description', - 'fieldtype': 'Data', - 'width': 150 - }, - { - 'label': _('Invoice'), - 'fieldname': 'invoice', - 'fieldtype': 'Link', - 'options': 'Sales Invoice', - 'width': 120 - }, - { - 'label': _('Posting Date'), - 'fieldname': 'posting_date', - 'fieldtype': 'Date', - 'width': 120 - } - ]) - - if filters.get('group_by') != 'Customer': - columns.extend([ - { - 'label': _('Customer Group'), - 'fieldname': 'customer_group', - 'fieldtype': 'Link', - 'options': 'Customer Group', - 'width': 120 - } - ]) - - if filters.get('group_by') not in ('Customer', 'Customer Group'): - columns.extend([ - { - 'label': _('Customer'), - 'fieldname': 'customer', - 'fieldtype': 'Link', - 'options': 'Customer', - 'width': 120 + "label": _("Invoice"), + "fieldname": "invoice", + "fieldtype": "Link", + "options": "Sales Invoice", + "width": 120, }, - { - 'label': _('Customer Name'), - 'fieldname': 'customer_name', - 'fieldtype': 'Data', - 'width': 120 - } - ]) + {"label": _("Posting Date"), "fieldname": "posting_date", "fieldtype": "Date", "width": 120}, + ] + ) + + if filters.get("group_by") != "Customer": + columns.extend( + [ + { + "label": _("Customer Group"), + "fieldname": "customer_group", + "fieldtype": "Link", + "options": "Customer Group", + "width": 120, + } + ] + ) + + if filters.get("group_by") not in ("Customer", "Customer Group"): + columns.extend( + [ + { + "label": _("Customer"), + "fieldname": "customer", + "fieldtype": "Link", + "options": "Customer", + "width": 120, + }, + {"label": _("Customer Name"), "fieldname": "customer_name", "fieldtype": "Data", "width": 120}, + ] + ) if additional_table_columns: columns += additional_table_columns columns += [ { - 'label': _('Receivable Account'), - 'fieldname': 'debit_to', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 80 + "label": _("Receivable Account"), + "fieldname": "debit_to", + "fieldtype": "Link", + "options": "Account", + "width": 80, }, { - 'label': _('Mode Of Payment'), - 'fieldname': 'mode_of_payment', - 'fieldtype': 'Data', - 'width': 120 - } + "label": _("Mode Of Payment"), + "fieldname": "mode_of_payment", + "fieldtype": "Data", + "width": 120, + }, ] - if filters.get('group_by') != 'Territory': - columns.extend([ - { - 'label': _('Territory'), - 'fieldname': 'territory', - 'fieldtype': 'Link', - 'options': 'Territory', - 'width': 80 - } - ]) - + if filters.get("group_by") != "Territory": + columns.extend( + [ + { + "label": _("Territory"), + "fieldname": "territory", + "fieldtype": "Link", + "options": "Territory", + "width": 80, + } + ] + ) columns += [ { - 'label': _('Project'), - 'fieldname': 'project', - 'fieldtype': 'Link', - 'options': 'Project', - 'width': 80 + "label": _("Project"), + "fieldname": "project", + "fieldtype": "Link", + "options": "Project", + "width": 80, }, { - 'label': _('Company'), - 'fieldname': 'company', - 'fieldtype': 'Link', - 'options': 'Company', - 'width': 80 + "label": _("Company"), + "fieldname": "company", + "fieldtype": "Link", + "options": "Company", + "width": 80, }, { - 'label': _('Sales Order'), - 'fieldname': 'sales_order', - 'fieldtype': 'Link', - 'options': 'Sales Order', - 'width': 100 + "label": _("Sales Order"), + "fieldname": "sales_order", + "fieldtype": "Link", + "options": "Sales Order", + "width": 100, }, { - 'label': _("Delivery Note"), - 'fieldname': 'delivery_note', - 'fieldtype': 'Link', - 'options': 'Delivery Note', - 'width': 100 + "label": _("Delivery Note"), + "fieldname": "delivery_note", + "fieldtype": "Link", + "options": "Delivery Note", + "width": 100, }, { - 'label': _('Income Account'), - 'fieldname': 'income_account', - 'fieldtype': 'Link', - 'options': 'Account', - 'width': 100 + "label": _("Income Account"), + "fieldname": "income_account", + "fieldtype": "Link", + "options": "Account", + "width": 100, }, { - 'label': _("Cost Center"), - 'fieldname': 'cost_center', - 'fieldtype': 'Link', - 'options': 'Cost Center', - 'width': 100 + "label": _("Cost Center"), + "fieldname": "cost_center", + "fieldtype": "Link", + "options": "Cost Center", + "width": 100, + }, + {"label": _("Stock Qty"), "fieldname": "stock_qty", "fieldtype": "Float", "width": 100}, + { + "label": _("Stock UOM"), + "fieldname": "stock_uom", + "fieldtype": "Link", + "options": "UOM", + "width": 100, }, { - 'label': _('Stock Qty'), - 'fieldname': 'stock_qty', - 'fieldtype': 'Float', - 'width': 100 + "label": _("Rate"), + "fieldname": "rate", + "fieldtype": "Float", + "options": "currency", + "width": 100, }, { - 'label': _('Stock UOM'), - 'fieldname': 'stock_uom', - 'fieldtype': 'Link', - 'options': 'UOM', - 'width': 100 + "label": _("Amount"), + "fieldname": "amount", + "fieldtype": "Currency", + "options": "currency", + "width": 100, }, - { - 'label': _('Rate'), - 'fieldname': 'rate', - 'fieldtype': 'Float', - 'options': 'currency', - 'width': 100 - }, - { - 'label': _('Amount'), - 'fieldname': 'amount', - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 100 - } ] - if filters.get('group_by'): - columns.append({ - 'label': _('% Of Grand Total'), - 'fieldname': 'percent_gt', - 'fieldtype': 'Float', - 'width': 80 - }) + if filters.get("group_by"): + columns.append( + {"label": _("% Of Grand Total"), "fieldname": "percent_gt", "fieldtype": "Float", "width": 80} + ) return columns -def get_conditions(filters): + +def get_conditions(filters, additional_conditions=None): conditions = "" - for opts in (("company", " and company=%(company)s"), + for opts in ( + ("company", " and company=%(company)s"), ("customer", " and `tabSales Invoice`.customer = %(customer)s"), ("item_code", " and `tabSales Invoice Item`.item_code = %(item_code)s"), ("from_date", " and `tabSales Invoice`.posting_date>=%(from_date)s"), - ("to_date", " and `tabSales Invoice`.posting_date<=%(to_date)s")): - if filters.get(opts[0]): - conditions += opts[1] + ("to_date", " and `tabSales Invoice`.posting_date<=%(to_date)s"), + ): + if filters.get(opts[0]): + conditions += opts[1] + + if additional_conditions: + conditions += additional_conditions if filters.get("mode_of_payment"): conditions += """ and exists(select name from `tabSales Invoice Payment` @@ -346,41 +355,45 @@ def get_conditions(filters): and ifnull(`tabSales Invoice Payment`.mode_of_payment, '') = %(mode_of_payment)s)""" if filters.get("warehouse"): - conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s""" - + conditions += """and ifnull(`tabSales Invoice Item`.warehouse, '') = %(warehouse)s""" if filters.get("brand"): - conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s""" + conditions += """and ifnull(`tabSales Invoice Item`.brand, '') = %(brand)s""" if filters.get("item_group"): - conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s""" + conditions += """and ifnull(`tabSales Invoice Item`.item_group, '') = %(item_group)s""" if not filters.get("group_by"): - conditions += "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc" + conditions += ( + "ORDER BY `tabSales Invoice`.posting_date desc, `tabSales Invoice Item`.item_group desc" + ) else: - conditions += get_group_by_conditions(filters, 'Sales Invoice') + conditions += get_group_by_conditions(filters, "Sales Invoice") return conditions -def get_group_by_conditions(filters, doctype): - if filters.get("group_by") == 'Invoice': - return "ORDER BY `tab{0} Item`.parent desc".format(doctype) - elif filters.get("group_by") == 'Item': - return "ORDER BY `tab{0} Item`.`item_code`".format(doctype) - elif filters.get("group_by") == 'Item Group': - return "ORDER BY `tab{0} Item`.{1}".format(doctype, frappe.scrub(filters.get('group_by'))) - elif filters.get("group_by") in ('Customer', 'Customer Group', 'Territory', 'Supplier'): - return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get('group_by'))) -def get_items(filters, additional_query_columns): - conditions = get_conditions(filters) +def get_group_by_conditions(filters, doctype): + if filters.get("group_by") == "Invoice": + return "ORDER BY `tab{0} Item`.parent desc".format(doctype) + elif filters.get("group_by") == "Item": + return "ORDER BY `tab{0} Item`.`item_code`".format(doctype) + elif filters.get("group_by") == "Item Group": + return "ORDER BY `tab{0} Item`.{1}".format(doctype, frappe.scrub(filters.get("group_by"))) + elif filters.get("group_by") in ("Customer", "Customer Group", "Territory", "Supplier"): + return "ORDER BY `tab{0}`.{1}".format(doctype, frappe.scrub(filters.get("group_by"))) + + +def get_items(filters, additional_query_columns, additional_conditions=None): + conditions = get_conditions(filters, additional_conditions) if additional_query_columns: - additional_query_columns = ', ' + ', '.join(additional_query_columns) + additional_query_columns = ", " + ", ".join(additional_query_columns) else: - additional_query_columns = '' + additional_query_columns = "" - return frappe.db.sql(""" + return frappe.db.sql( + """ select `tabSales Invoice Item`.name, `tabSales Invoice Item`.parent, `tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to, @@ -399,47 +412,75 @@ def get_items(filters, additional_query_columns): from `tabSales Invoice`, `tabSales Invoice Item` where `tabSales Invoice`.name = `tabSales Invoice Item`.parent and `tabSales Invoice`.docstatus = 1 {1} - """.format(additional_query_columns or '', conditions), filters, as_dict=1) #nosec + """.format( + additional_query_columns or "", conditions + ), + filters, + as_dict=1, + ) # nosec + def get_delivery_notes_against_sales_order(item_list): so_dn_map = frappe._dict() so_item_rows = list(set([d.so_detail for d in item_list])) if so_item_rows: - delivery_notes = frappe.db.sql(""" + delivery_notes = frappe.db.sql( + """ select parent, so_detail from `tabDelivery Note Item` where docstatus=1 and so_detail in (%s) group by so_detail, parent - """ % (', '.join(['%s']*len(so_item_rows))), tuple(so_item_rows), as_dict=1) + """ + % (", ".join(["%s"] * len(so_item_rows))), + tuple(so_item_rows), + as_dict=1, + ) for dn in delivery_notes: so_dn_map.setdefault(dn.so_detail, []).append(dn.parent) return so_dn_map + def get_grand_total(filters, doctype): - return frappe.db.sql(""" SELECT + return frappe.db.sql( + """ SELECT SUM(`tab{0}`.base_grand_total) FROM `tab{0}` WHERE `tab{0}`.docstatus = 1 and posting_date between %s and %s - """.format(doctype), (filters.get('from_date'), filters.get('to_date')))[0][0] #nosec + """.format( + doctype + ), + (filters.get("from_date"), filters.get("to_date")), + )[0][ + 0 + ] # nosec -def get_deducted_taxes(): - return frappe.db.sql_list("select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'") -def get_tax_accounts(item_list, columns, company_currency, - doctype='Sales Invoice', tax_doctype='Sales Taxes and Charges'): +def get_tax_accounts( + item_list, + columns, + company_currency, + doctype="Sales Invoice", + tax_doctype="Sales Taxes and Charges", +): import json + item_row_map = {} tax_columns = [] invoice_item_row = {} itemised_tax = {} + add_deduct_tax = "charge_type" - tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field('tax_amount'), - currency=company_currency) or 2 + tax_amount_precision = ( + get_field_precision( + frappe.get_meta(tax_doctype).get_field("tax_amount"), currency=company_currency + ) + or 2 + ) for d in item_list: invoice_item_row.setdefault(d.parent, []).append(d) @@ -448,12 +489,13 @@ def get_tax_accounts(item_list, columns, company_currency, conditions = "" if doctype == "Purchase Invoice": conditions = " and category in ('Total', 'Valuation and Total') and base_tax_amount_after_discount_amount != 0" + add_deduct_tax = "add_deduct_tax" - deducted_tax = get_deducted_taxes() - tax_details = frappe.db.sql(""" + tax_details = frappe.db.sql( + """ select - name, parent, description, item_wise_tax_detail, - charge_type, base_tax_amount_after_discount_amount + name, parent, description, item_wise_tax_detail, account_head, + charge_type, {add_deduct_tax}, base_tax_amount_after_discount_amount from `tab%s` where parenttype = %s and docstatus = 1 @@ -461,10 +503,33 @@ def get_tax_accounts(item_list, columns, company_currency, and parent in (%s) %s order by description - """ % (tax_doctype, '%s', ', '.join(['%s']*len(invoice_item_row)), conditions), - tuple([doctype] + list(invoice_item_row))) + """.format( + add_deduct_tax=add_deduct_tax + ) + % (tax_doctype, "%s", ", ".join(["%s"] * len(invoice_item_row)), conditions), + tuple([doctype] + list(invoice_item_row)), + ) - for name, parent, description, item_wise_tax_detail, charge_type, tax_amount in tax_details: + account_doctype = frappe.qb.DocType("Account") + + query = ( + frappe.qb.from_(account_doctype) + .select(account_doctype.name) + .where((account_doctype.account_type == "Tax")) + ) + + tax_accounts = query.run() + + for ( + name, + parent, + description, + item_wise_tax_detail, + account_head, + charge_type, + add_deduct_tax, + tax_amount, + ) in tax_details: description = handle_html(description) if description not in tax_columns and tax_amount: # as description is text editor earlier and markup can break the column convention in reports @@ -483,151 +548,200 @@ def get_tax_accounts(item_list, columns, company_currency, tax_rate = tax_data tax_amount = 0 - if charge_type == 'Actual' and not tax_rate: - tax_rate = 'NA' + if charge_type == "Actual" and not tax_rate: + tax_rate = "NA" - item_net_amount = sum([flt(d.base_net_amount) - for d in item_row_map.get(parent, {}).get(item_code, [])]) + item_net_amount = sum( + [flt(d.base_net_amount) for d in item_row_map.get(parent, {}).get(item_code, [])] + ) for d in item_row_map.get(parent, {}).get(item_code, []): - item_tax_amount = flt((tax_amount * d.base_net_amount) / item_net_amount) \ - if item_net_amount else 0 + item_tax_amount = ( + flt((tax_amount * d.base_net_amount) / item_net_amount) if item_net_amount else 0 + ) if item_tax_amount: tax_value = flt(item_tax_amount, tax_amount_precision) - tax_value = (tax_value * -1 - if (doctype == 'Purchase Invoice' and name in deducted_tax) else tax_value) + tax_value = ( + tax_value * -1 + if (doctype == "Purchase Invoice" and add_deduct_tax == "Deduct") + else tax_value + ) - itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ - 'tax_rate': tax_rate, - 'tax_amount': tax_value - }) + itemised_tax.setdefault(d.name, {})[description] = frappe._dict( + { + "tax_rate": tax_rate, + "tax_amount": tax_value, + "is_other_charges": 0 if tuple([account_head]) in tax_accounts else 1, + } + ) except ValueError: continue - elif charge_type == 'Actual' and tax_amount: + elif charge_type == "Actual" and tax_amount: for d in invoice_item_row.get(parent, []): - itemised_tax.setdefault(d.name, {})[description] = frappe._dict({ - 'tax_rate': 'NA', - 'tax_amount': flt((tax_amount * d.base_net_amount) / d.base_net_total, - tax_amount_precision) - }) + itemised_tax.setdefault(d.name, {})[description] = frappe._dict( + { + "tax_rate": "NA", + "tax_amount": flt((tax_amount * d.base_net_amount) / d.base_net_total, tax_amount_precision), + } + ) tax_columns.sort() for desc in tax_columns: - columns.append({ - 'label': _(desc + ' Rate'), - 'fieldname': frappe.scrub(desc + ' Rate'), - 'fieldtype': 'Float', - 'width': 100 - }) + columns.append( + { + "label": _(desc + " Rate"), + "fieldname": frappe.scrub(desc + " Rate"), + "fieldtype": "Float", + "width": 100, + } + ) - columns.append({ - 'label': _(desc + ' Amount'), - 'fieldname': frappe.scrub(desc + ' Amount'), - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 100 - }) + columns.append( + { + "label": _(desc + " Amount"), + "fieldname": frappe.scrub(desc + " Amount"), + "fieldtype": "Currency", + "options": "currency", + "width": 100, + } + ) columns += [ { - 'label': _('Total Tax'), - 'fieldname': 'total_tax', - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 100 + "label": _("Total Tax"), + "fieldname": "total_tax", + "fieldtype": "Currency", + "options": "currency", + "width": 100, }, { - 'label': _('Total'), - 'fieldname': 'total', - 'fieldtype': 'Currency', - 'options': 'currency', - 'width': 100 + "label": _("Total Other Charges"), + "fieldname": "total_other_charges", + "fieldtype": "Currency", + "options": "currency", + "width": 100, }, { - 'fieldname': 'currency', - 'label': _('Currency'), - 'fieldtype': 'Currency', - 'width': 80, - 'hidden': 1 - } + "label": _("Total"), + "fieldname": "total", + "fieldtype": "Currency", + "options": "currency", + "width": 100, + }, + { + "fieldname": "currency", + "label": _("Currency"), + "fieldtype": "Currency", + "width": 80, + "hidden": 1, + }, ] return itemised_tax, tax_columns -def add_total_row(data, filters, prev_group_by_value, item, total_row_map, - group_by_field, subtotal_display_field, grand_total, tax_columns): - if prev_group_by_value != item.get(group_by_field, ''): + +def add_total_row( + data, + filters, + prev_group_by_value, + item, + total_row_map, + group_by_field, + subtotal_display_field, + grand_total, + tax_columns, +): + if prev_group_by_value != item.get(group_by_field, ""): if prev_group_by_value: total_row = total_row_map.get(prev_group_by_value) data.append(total_row) data.append({}) - add_sub_total_row(total_row, total_row_map, 'total_row', tax_columns) + add_sub_total_row(total_row, total_row_map, "total_row", tax_columns) - prev_group_by_value = item.get(group_by_field, '') + prev_group_by_value = item.get(group_by_field, "") - total_row_map.setdefault(item.get(group_by_field, ''), { - subtotal_display_field: get_display_value(filters, group_by_field, item), - 'stock_qty': 0.0, - 'amount': 0.0, - 'bold': 1, - 'total_tax': 0.0, - 'total': 0.0, - 'percent_gt': 0.0 - }) + total_row_map.setdefault( + item.get(group_by_field, ""), + { + subtotal_display_field: get_display_value(filters, group_by_field, item), + "stock_qty": 0.0, + "amount": 0.0, + "bold": 1, + "total_tax": 0.0, + "total": 0.0, + "percent_gt": 0.0, + }, + ) - total_row_map.setdefault('total_row', { - subtotal_display_field: 'Total', - 'stock_qty': 0.0, - 'amount': 0.0, - 'bold': 1, - 'total_tax': 0.0, - 'total': 0.0, - 'percent_gt': 0.0 - }) + total_row_map.setdefault( + "total_row", + { + subtotal_display_field: "Total", + "stock_qty": 0.0, + "amount": 0.0, + "bold": 1, + "total_tax": 0.0, + "total": 0.0, + "percent_gt": 0.0, + }, + ) return data, prev_group_by_value + def get_display_value(filters, group_by_field, item): - if filters.get('group_by') == 'Item': - if item.get('item_code') != item.get('item_name'): - value = cstr(item.get('item_code')) + "{message}
Please Review this grant application
Please Review this grant application
Thanks,
ERPNext Team. - """.format(gst_document_link=" ERPNext GST Document ") + """.format( + gst_document_link=" ERPNext GST Document " + ) try: sendmail_to_system_managers("[Important] ERPNext GST updates", message) diff --git a/erpnext/patches/v8_7/sync_india_custom_fields.py b/erpnext/patches/v8_7/sync_india_custom_fields.py index b5d58dc2eb8..e1b9a732dea 100644 --- a/erpnext/patches/v8_7/sync_india_custom_fields.py +++ b/erpnext/patches/v8_7/sync_india_custom_fields.py @@ -1,36 +1,42 @@ - import frappe from erpnext.regional.india.setup import make_custom_fields def execute(): - company = frappe.get_all('Company', filters = {'country': 'India'}) + company = frappe.get_all("Company", filters={"country": "India"}) if not company: return - frappe.reload_doc('Payroll', 'doctype', 'payroll_period') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_declaration') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_proof_submission') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_declaration_category') - frappe.reload_doc('Payroll', 'doctype', 'employee_tax_exemption_proof_submission_detail') + frappe.reload_doc("Payroll", "doctype", "payroll_period") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_declaration") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_proof_submission") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_declaration_category") + frappe.reload_doc("Payroll", "doctype", "employee_tax_exemption_proof_submission_detail") - frappe.reload_doc('accounts', 'doctype', 'tax_category') + frappe.reload_doc("accounts", "doctype", "tax_category") for doctype in ["Sales Invoice", "Delivery Note", "Purchase Invoice"]: - frappe.db.sql("""delete from `tabCustom Field` where dt = %s - and fieldname in ('port_code', 'shipping_bill_number', 'shipping_bill_date')""", doctype) + frappe.db.sql( + """delete from `tabCustom Field` where dt = %s + and fieldname in ('port_code', 'shipping_bill_number', 'shipping_bill_date')""", + doctype, + ) make_custom_fields() - frappe.db.sql(""" + frappe.db.sql( + """ update `tabCustom Field` set reqd = 0, `default` = '' where fieldname = 'reason_for_issuing_document' - """) + """ + ) - frappe.db.sql(""" + frappe.db.sql( + """ update tabAddress set gst_state_number=concat("0", gst_state_number) where ifnull(gst_state_number, '') != '' and gst_state_number<10 - """) + """ + ) diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py index bf8bd05fcc0..18bd3b7733c 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -30,12 +30,17 @@ class AdditionalSalary(Document): frappe.throw(_("Amount should not be less than zero")) def validate_salary_structure(self): - if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): - frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + if not frappe.db.exists("Salary Structure Assignment", {"employee": self.employee}): + frappe.throw( + _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format( + self.employee + ) + ) def validate_recurring_additional_salary_overlap(self): if self.is_recurring: - additional_salaries = frappe.db.sql(""" + additional_salaries = frappe.db.sql( + """ SELECT name FROM `tabAdditional Salary` @@ -47,22 +52,28 @@ class AdditionalSalary(Document): AND salary_component = %s AND to_date >= %s AND from_date <= %s""", - (self.employee, self.name, self.salary_component, self.from_date, self.to_date), as_dict = 1) + (self.employee, self.name, self.salary_component, self.from_date, self.to_date), + as_dict=1, + ) additional_salaries = [salary.name for salary in additional_salaries] if additional_salaries and len(additional_salaries): - frappe.throw(_("Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}").format( - bold(comma_and(additional_salaries)), - bold(self.salary_component), - bold(formatdate(self.from_date)), - bold(formatdate(self.to_date) - ))) - + frappe.throw( + _( + "Additional Salary: {0} already exist for Salary Component: {1} for period {2} and {3}" + ).format( + bold(comma_and(additional_salaries)), + bold(self.salary_component), + bold(formatdate(self.from_date)), + bold(formatdate(self.to_date)), + ) + ) def validate_dates(self): - date_of_joining, relieving_date = frappe.db.get_value("Employee", self.employee, - ["date_of_joining", "relieving_date"]) + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", self.employee, ["date_of_joining", "relieving_date"] + ) if getdate(self.from_date) > getdate(self.to_date): frappe.throw(_("From Date can not be greater than To Date.")) @@ -81,19 +92,27 @@ class AdditionalSalary(Document): def validate_employee_referral(self): if self.ref_doctype == "Employee Referral": - referral_details = frappe.db.get_value("Employee Referral", self.ref_docname, - ["is_applicable_for_referral_bonus", "status"], as_dict=1) + referral_details = frappe.db.get_value( + "Employee Referral", + self.ref_docname, + ["is_applicable_for_referral_bonus", "status"], + as_dict=1, + ) if not referral_details.is_applicable_for_referral_bonus: - frappe.throw(_("Employee Referral {0} is not applicable for referral bonus.").format( - self.ref_docname)) + frappe.throw( + _("Employee Referral {0} is not applicable for referral bonus.").format(self.ref_docname) + ) if self.type == "Deduction": frappe.throw(_("Earning Salary Component is required for Employee Referral Bonus.")) if referral_details.status != "Accepted": - frappe.throw(_("Additional Salary for referral bonus can only be created against Employee Referral with status {0}").format( - frappe.bold("Accepted"))) + frappe.throw( + _( + "Additional Salary for referral bonus can only be created against Employee Referral with status {0}" + ).format(frappe.bold("Accepted")) + ) def update_return_amount_in_employee_advance(self): if self.ref_doctype == "Employee Advance" and self.ref_docname: @@ -105,6 +124,8 @@ class AdditionalSalary(Document): return_amount += self.amount frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount) + advance = frappe.get_doc("Employee Advance", self.ref_docname) + advance.set_status(update=True) def update_employee_referral(self, cancel=False): if self.ref_doctype == "Employee Referral": @@ -123,28 +144,54 @@ class AdditionalSalary(Document): no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 return amount_per_day * no_of_days + @frappe.whitelist() def get_additional_salaries(employee, start_date, end_date, component_type): - comp_type = 'Earning' if component_type == 'earnings' else 'Deduction' + from frappe.query_builder import Criterion - additional_sal = frappe.qb.DocType('Additional Salary') - component_field = additional_sal.salary_component.as_('component') - overwrite_field = additional_sal.overwrite_salary_structure_amount.as_('overwrite') + comp_type = "Earning" if component_type == "earnings" else "Deduction" - additional_salary_list = frappe.qb.from_( - additional_sal - ).select( - additional_sal.name, component_field, additional_sal.type, - additional_sal.amount, additional_sal.is_recurring, overwrite_field, - additional_sal.deduct_full_tax_on_selected_payroll_date - ).where( - (additional_sal.employee == employee) - & (additional_sal.docstatus == 1) - & (additional_sal.type == comp_type) - ).where( - additional_sal.payroll_date[start_date: end_date] - | ((additional_sal.from_date <= end_date) & (additional_sal.to_date >= end_date)) - ).run(as_dict=True) + additional_sal = frappe.qb.DocType("Additional Salary") + component_field = additional_sal.salary_component.as_("component") + overwrite_field = additional_sal.overwrite_salary_structure_amount.as_("overwrite") + + additional_salary_list = ( + frappe.qb.from_(additional_sal) + .select( + additional_sal.name, + component_field, + additional_sal.type, + additional_sal.amount, + additional_sal.is_recurring, + overwrite_field, + additional_sal.deduct_full_tax_on_selected_payroll_date, + ) + .where( + (additional_sal.employee == employee) + & (additional_sal.docstatus == 1) + & (additional_sal.type == comp_type) + ) + .where( + Criterion.any( + [ + Criterion.all( + [ # is recurring and additional salary dates fall within the payroll period + additional_sal.is_recurring == 1, + additional_sal.from_date <= end_date, + additional_sal.to_date >= end_date, + ] + ), + Criterion.all( + [ # is not recurring and additional salary's payroll date falls within the payroll period + additional_sal.is_recurring == 0, + additional_sal.payroll_date[start_date:end_date], + ] + ), + ] + ) + ) + .run(as_dict=True) + ) additional_salaries = [] components_to_overwrite = [] @@ -152,8 +199,12 @@ def get_additional_salaries(employee, start_date, end_date, component_type): for d in additional_salary_list: if d.overwrite: if d.component in components_to_overwrite: - frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component {0} between {1} and {2}.").format( - frappe.bold(d.component), start_date, end_date), title=_("Error")) + frappe.throw( + _( + "Multiple Additional Salaries with overwrite property exist for Salary Component {0} between {1} and {2}." + ).format(frappe.bold(d.component), start_date, end_date), + title=_("Error"), + ) components_to_overwrite.append(d.component) diff --git a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py index 84de912e431..bd739368a0a 100644 --- a/erpnext/payroll/doctype/additional_salary/test_additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/test_additional_salary.py @@ -4,7 +4,8 @@ import unittest import frappe -from frappe.utils import add_days, nowdate +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, add_months, nowdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee @@ -16,40 +17,87 @@ from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure -class TestAdditionalSalary(unittest.TestCase): - +class TestAdditionalSalary(FrappeTestCase): def setUp(self): setup_test() - def tearDown(self): - for dt in ["Salary Slip", "Additional Salary", "Salary Structure Assignment", "Salary Structure"]: - frappe.db.sql("delete from `tab%s`" % dt) - def test_recurring_additional_salary(self): amount = 0 salary_component = None emp_id = make_employee("test_additional@salary.com") frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(nowdate(), 1800)) - salary_structure = make_salary_structure("Test Salary Structure Additional Salary", "Monthly", employee=emp_id) + salary_structure = make_salary_structure( + "Test Salary Structure Additional Salary", "Monthly", employee=emp_id + ) add_sal = get_additional_salary(emp_id) - ss = make_employee_salary_slip("test_additional@salary.com", "Monthly", salary_structure=salary_structure.name) + ss = make_employee_salary_slip( + "test_additional@salary.com", "Monthly", salary_structure=salary_structure.name + ) for earning in ss.earnings: if earning.salary_component == "Recurring Salary Component": amount = earning.amount salary_component = earning.salary_component + break self.assertEqual(amount, add_sal.amount) self.assertEqual(salary_component, add_sal.salary_component) -def get_additional_salary(emp_id): + def test_non_recurring_additional_salary(self): + amount = 0 + salary_component = None + date = nowdate() + + emp_id = make_employee("test_additional@salary.com") + frappe.db.set_value("Employee", emp_id, "relieving_date", add_days(date, 1800)) + salary_structure = make_salary_structure( + "Test Salary Structure Additional Salary", "Monthly", employee=emp_id + ) + add_sal = get_additional_salary(emp_id, recurring=False, payroll_date=date) + + ss = make_employee_salary_slip( + "test_additional@salary.com", "Monthly", salary_structure=salary_structure.name + ) + + amount, salary_component = None, None + for earning in ss.earnings: + if earning.salary_component == "Recurring Salary Component": + amount = earning.amount + salary_component = earning.salary_component + break + + self.assertEqual(amount, add_sal.amount) + self.assertEqual(salary_component, add_sal.salary_component) + + # should not show up in next months + ss.posting_date = add_months(date, 1) + ss.start_date = ss.end_date = None + ss.earnings = [] + ss.deductions = [] + ss.save() + + amount, salary_component = None, None + for earning in ss.earnings: + if earning.salary_component == "Recurring Salary Component": + amount = earning.amount + salary_component = earning.salary_component + break + + self.assertIsNone(amount) + self.assertIsNone(salary_component) + + +def get_additional_salary(emp_id, recurring=True, payroll_date=None): create_salary_component("Recurring Salary Component") add_sal = frappe.new_doc("Additional Salary") add_sal.employee = emp_id add_sal.salary_component = "Recurring Salary Component" - add_sal.is_recurring = 1 + + add_sal.is_recurring = 1 if recurring else 0 add_sal.from_date = add_days(nowdate(), -50) add_sal.to_date = add_days(nowdate(), 180) + add_sal.payroll_date = payroll_date + add_sal.amount = 5000 add_sal.currency = erpnext.get_default_currency() add_sal.save() diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py index eda50150ebd..8df1bb6e87e 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.py @@ -5,7 +5,7 @@ import frappe from frappe import _ from frappe.model.document import Document -from frappe.utils import add_days, cint, cstr, date_diff, getdate, rounded +from frappe.utils import add_days, cstr, date_diff, flt, getdate, rounded from erpnext.hr.utils import ( get_holiday_dates_for_employee, @@ -27,26 +27,40 @@ class EmployeeBenefitApplication(Document): validate_active_employee(self.employee) self.validate_duplicate_on_payroll_period() if not self.max_benefits: - self.max_benefits = get_max_benefits_remaining(self.employee, self.date, self.payroll_period) + self.max_benefits = flt( + get_max_benefits_remaining(self.employee, self.date, self.payroll_period), + self.precision("max_benefits"), + ) if self.max_benefits and self.max_benefits > 0: self.validate_max_benefit_for_component() self.validate_prev_benefit_claim() - if self.remaining_benefit > 0: + if self.remaining_benefit and self.remaining_benefit > 0: self.validate_remaining_benefit_amount() else: - frappe.throw(_("As per your assigned Salary Structure you cannot apply for benefits").format(self.employee)) + frappe.throw( + _("As per your assigned Salary Structure you cannot apply for benefits").format(self.employee) + ) def validate_prev_benefit_claim(self): if self.employee_benefits: for benefit in self.employee_benefits: if benefit.pay_against_benefit_claim == 1: payroll_period = frappe.get_doc("Payroll Period", self.payroll_period) - benefit_claimed = get_previous_claimed_amount(self.employee, payroll_period, component = benefit.earning_component) - benefit_given = get_sal_slip_total_benefit_given(self.employee, payroll_period, component = benefit.earning_component) + benefit_claimed = get_previous_claimed_amount( + self.employee, payroll_period, component=benefit.earning_component + ) + benefit_given = get_sal_slip_total_benefit_given( + self.employee, payroll_period, component=benefit.earning_component + ) benefit_claim_remining = benefit_claimed - benefit_given if benefit_claimed > 0 and benefit_claim_remining > benefit.amount: - frappe.throw(_("An amount of {0} already claimed for the component {1}, set the amount equal or greater than {2}").format( - benefit_claimed, benefit.earning_component, benefit_claim_remining)) + frappe.throw( + _( + "An amount of {0} already claimed for the component {1}, set the amount equal or greater than {2}" + ).format( + benefit_claimed, benefit.earning_component, benefit_claim_remining + ) + ) def validate_remaining_benefit_amount(self): # check salary structure earnings have flexi component (sum of max_benefit_amount) @@ -65,52 +79,80 @@ class EmployeeBenefitApplication(Document): if salary_structure.earnings: for earnings in salary_structure.earnings: if earnings.is_flexible_benefit == 1 and earnings.salary_component not in benefit_components: - pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value("Salary Component", earnings.salary_component, ["pay_against_benefit_claim", "max_benefit_amount"]) + pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value( + "Salary Component", + earnings.salary_component, + ["pay_against_benefit_claim", "max_benefit_amount"], + ) if pay_against_benefit_claim != 1: pro_rata_amount += max_benefit_amount else: non_pro_rata_amount += max_benefit_amount - if pro_rata_amount == 0 and non_pro_rata_amount == 0: - frappe.throw(_("Please add the remaining benefits {0} to any of the existing component").format(self.remaining_benefit)) + if pro_rata_amount == 0 and non_pro_rata_amount == 0: + frappe.throw( + _("Please add the remaining benefits {0} to any of the existing component").format( + self.remaining_benefit + ) + ) elif non_pro_rata_amount > 0 and non_pro_rata_amount < rounded(self.remaining_benefit): - frappe.throw(_("You can claim only an amount of {0}, the rest amount {1} should be in the application as pro-rata component").format( - non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount)) + frappe.throw( + _( + "You can claim only an amount of {0}, the rest amount {1} should be in the application as pro-rata component" + ).format(non_pro_rata_amount, self.remaining_benefit - non_pro_rata_amount) + ) elif non_pro_rata_amount == 0: - frappe.throw(_("Please add the remaining benefits {0} to the application as pro-rata component").format( - self.remaining_benefit)) + frappe.throw( + _("Please add the remaining benefits {0} to the application as pro-rata component").format( + self.remaining_benefit + ) + ) def validate_max_benefit_for_component(self): if self.employee_benefits: max_benefit_amount = 0 for employee_benefit in self.employee_benefits: self.validate_max_benefit(employee_benefit.earning_component) - max_benefit_amount += employee_benefit.amount + max_benefit_amount += flt(employee_benefit.amount) if max_benefit_amount > self.max_benefits: - frappe.throw(_("Maximum benefit amount of employee {0} exceeds {1}").format(self.employee, self.max_benefits)) + frappe.throw( + _("Maximum benefit amount of employee {0} exceeds {1}").format( + self.employee, self.max_benefits + ) + ) def validate_max_benefit(self, earning_component_name): - max_benefit_amount = frappe.db.get_value("Salary Component", earning_component_name, "max_benefit_amount") + max_benefit_amount = frappe.db.get_value( + "Salary Component", earning_component_name, "max_benefit_amount" + ) benefit_amount = 0 for employee_benefit in self.employee_benefits: if employee_benefit.earning_component == earning_component_name: - benefit_amount += employee_benefit.amount - prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given(self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name) + benefit_amount += flt(employee_benefit.amount) + + prev_sal_slip_flexi_amount = get_sal_slip_total_benefit_given( + self.employee, frappe.get_doc("Payroll Period", self.payroll_period), earning_component_name + ) benefit_amount += prev_sal_slip_flexi_amount if rounded(benefit_amount, 2) > max_benefit_amount: - frappe.throw(_("Maximum benefit amount of component {0} exceeds {1}").format(earning_component_name, max_benefit_amount)) + frappe.throw( + _("Maximum benefit amount of component {0} exceeds {1}").format( + earning_component_name, max_benefit_amount + ) + ) def validate_duplicate_on_payroll_period(self): application = frappe.db.exists( "Employee Benefit Application", - { - 'employee': self.employee, - 'payroll_period': self.payroll_period, - 'docstatus': 1 - } + {"employee": self.employee, "payroll_period": self.payroll_period, "docstatus": 1}, ) if application: - frappe.throw(_("Employee {0} already submited an apllication {1} for the payroll period {2}").format(self.employee, application, self.payroll_period)) + frappe.throw( + _("Employee {0} already submited an apllication {1} for the payroll period {2}").format( + self.employee, application, self.payroll_period + ) + ) + @frappe.whitelist() def get_max_benefits(employee, on_date): @@ -121,6 +163,7 @@ def get_max_benefits(employee, on_date): return max_benefits return False + @frappe.whitelist() def get_max_benefits_remaining(employee, on_date, payroll_period): max_benefits = get_max_benefits(employee, on_date) @@ -141,9 +184,14 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): sal_struct = frappe.get_doc("Salary Structure", sal_struct_name) for sal_struct_row in sal_struct.get("earnings"): salary_component = frappe.get_doc("Salary Component", sal_struct_row.salary_component) - if salary_component.depends_on_payment_days == 1 and salary_component.pay_against_benefit_claim != 1: + if ( + salary_component.depends_on_payment_days == 1 + and salary_component.pay_against_benefit_claim != 1 + ): have_depends_on_payment_days = True - benefit_amount = get_benefit_amount_based_on_pro_rata(sal_struct, salary_component.max_benefit_amount) + benefit_amount = get_benefit_amount_based_on_pro_rata( + sal_struct, salary_component.max_benefit_amount + ) amount_per_day = benefit_amount / payroll_period_days per_day_amount_total += amount_per_day @@ -159,71 +207,115 @@ def get_max_benefits_remaining(employee, on_date, payroll_period): return max_benefits - prev_sal_slip_flexi_total return max_benefits + def calculate_lwp(employee, start_date, holidays, working_days): lwp = 0 holidays = "','".join(holidays) + for d in range(working_days): - dt = add_days(cstr(getdate(start_date)), d) - leave = frappe.db.sql(""" - select t1.name, t1.half_day - from `tabLeave Application` t1, `tabLeave Type` t2 - where t2.name = t1.leave_type - and t2.is_lwp = 1 - and t1.docstatus = 1 - and t1.employee = %(employee)s - and CASE WHEN t2.include_holiday != 1 THEN %(dt)s not in ('{0}') and %(dt)s between from_date and to_date - WHEN t2.include_holiday THEN %(dt)s between from_date and to_date - END - """.format(holidays), {"employee": employee, "dt": dt}) - if leave: - lwp = cint(leave[0][1]) and (lwp + 0.5) or (lwp + 1) + date = add_days(cstr(getdate(start_date)), d) + + LeaveApplication = frappe.qb.DocType("Leave Application") + LeaveType = frappe.qb.DocType("Leave Type") + + is_half_day = ( + frappe.qb.terms.Case() + .when( + ( + (LeaveApplication.half_day_date == date) + | (LeaveApplication.from_date == LeaveApplication.to_date) + ), + LeaveApplication.half_day, + ) + .else_(0) + ).as_("is_half_day") + + query = ( + frappe.qb.from_(LeaveApplication) + .inner_join(LeaveType) + .on((LeaveType.name == LeaveApplication.leave_type)) + .select(LeaveApplication.name, is_half_day) + .where( + (LeaveType.is_lwp == 1) + & (LeaveApplication.docstatus == 1) + & (LeaveApplication.status == "Approved") + & (LeaveApplication.employee == employee) + & ((LeaveApplication.from_date <= date) & (date <= LeaveApplication.to_date)) + ) + ) + + # if it's a holiday only include if leave type has "include holiday" enabled + if date in holidays: + query = query.where((LeaveType.include_holiday == "1")) + leaves = query.run(as_dict=True) + + if leaves: + lwp += 0.5 if leaves[0].is_half_day else 1 + return lwp -def get_benefit_component_amount(employee, start_date, end_date, salary_component, sal_struct, payroll_frequency, payroll_period): + +def get_benefit_component_amount( + employee, start_date, end_date, salary_component, sal_struct, payroll_frequency, payroll_period +): if not payroll_period: - frappe.msgprint(_("Start and end dates not in a valid Payroll Period, cannot calculate {0}") - .format(salary_component)) + frappe.msgprint( + _("Start and end dates not in a valid Payroll Period, cannot calculate {0}").format( + salary_component + ) + ) return False # Considering there is only one application for a year - benefit_application = frappe.db.sql(""" + benefit_application = frappe.db.sql( + """ select name from `tabEmployee Benefit Application` where payroll_period=%(payroll_period)s and employee=%(employee)s and docstatus = 1 - """, { - 'employee': employee, - 'payroll_period': payroll_period.name - }) + """, + {"employee": employee, "payroll_period": payroll_period.name}, + ) current_benefit_amount = 0.0 - component_max_benefit, depends_on_payment_days = frappe.db.get_value("Salary Component", - salary_component, ["max_benefit_amount", "depends_on_payment_days"]) + component_max_benefit, depends_on_payment_days = frappe.db.get_value( + "Salary Component", salary_component, ["max_benefit_amount", "depends_on_payment_days"] + ) benefit_amount = 0 if benefit_application: - benefit_amount = frappe.db.get_value("Employee Benefit Application Detail", - {"parent": benefit_application[0][0], "earning_component": salary_component}, "amount") + benefit_amount = frappe.db.get_value( + "Employee Benefit Application Detail", + {"parent": benefit_application[0][0], "earning_component": salary_component}, + "amount", + ) elif component_max_benefit: benefit_amount = get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit) current_benefit_amount = 0 if benefit_amount: - total_sub_periods = get_period_factor(employee, - start_date, end_date, payroll_frequency, payroll_period, depends_on_payment_days)[0] + total_sub_periods = get_period_factor( + employee, start_date, end_date, payroll_frequency, payroll_period, depends_on_payment_days + )[0] current_benefit_amount = benefit_amount / total_sub_periods return current_benefit_amount + def get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit): max_benefits_total = 0 benefit_amount = 0 for d in sal_struct.get("earnings"): if d.is_flexible_benefit == 1: - component = frappe.db.get_value("Salary Component", d.salary_component, ["max_benefit_amount", "pay_against_benefit_claim"], as_dict=1) + component = frappe.db.get_value( + "Salary Component", + d.salary_component, + ["max_benefit_amount", "pay_against_benefit_claim"], + as_dict=1, + ) if not component.pay_against_benefit_claim: max_benefits_total += component.max_benefit_amount @@ -234,34 +326,46 @@ def get_benefit_amount_based_on_pro_rata(sal_struct, component_max_benefit): return benefit_amount + @frappe.whitelist() @frappe.validate_and_sanitize_search_inputs def get_earning_components(doctype, txt, searchfield, start, page_len, filters): if len(filters) < 2: return {} - salary_structure = get_assigned_salary_structure(filters['employee'], filters['date']) + salary_structure = get_assigned_salary_structure(filters["employee"], filters["date"]) if salary_structure: - return frappe.db.sql(""" + return frappe.db.sql( + """ select salary_component from `tabSalary Detail` where parent = %s and is_flexible_benefit = 1 order by name - """, salary_structure) + """, + salary_structure, + ) else: - frappe.throw(_("Salary Structure not found for employee {0} and date {1}") - .format(filters['employee'], filters['date'])) + frappe.throw( + _("Salary Structure not found for employee {0} and date {1}").format( + filters["employee"], filters["date"] + ) + ) + @frappe.whitelist() def get_earning_components_max_benefits(employee, date, earning_component): salary_structure = get_assigned_salary_structure(employee, date) - amount = frappe.db.sql(""" + amount = frappe.db.sql( + """ select amount from `tabSalary Detail` where parent = %s and is_flexible_benefit = 1 and salary_component = %s order by name - """, salary_structure, earning_component) + """, + salary_structure, + earning_component, + ) return amount if amount else 0 diff --git a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py index 02149adfce5..f2b86b3ff37 100644 --- a/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py +++ b/erpnext/payroll/doctype/employee_benefit_application/test_employee_benefit_application.py @@ -3,6 +3,82 @@ import unittest +import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, date_diff, get_year_ending, get_year_start, getdate -class TestEmployeeBenefitApplication(unittest.TestCase): - pass +from erpnext.hr.doctype.employee.test_employee import make_employee +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list +from erpnext.hr.tests.test_utils import get_first_sunday +from erpnext.hr.utils import get_holiday_dates_for_employee +from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import ( + calculate_lwp, +) +from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import ( + create_payroll_period, +) +from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( + make_holiday_list, + make_leave_application, +) +from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip +from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + +class TestEmployeeBenefitApplication(FrappeTestCase): + def setUp(self): + date = getdate() + make_holiday_list(from_date=get_year_start(date), to_date=get_year_ending(date)) + + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_employee_benefit_application(self): + payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company") + employee = make_employee("test_employee_benefits@salary.com", company="_Test Company") + first_sunday = get_first_sunday("Salary Slip Test Holiday List") + + leave_application = make_leave_application( + employee, + add_days(first_sunday, 1), + add_days(first_sunday, 3), + "Leave Without Pay", + half_day=1, + half_day_date=add_days(first_sunday, 1), + submit=True, + ) + + frappe.db.set_value("Leave Type", "Leave Without Pay", "include_holiday", 0) + salary_structure = make_salary_structure( + "Test Employee Benefits", + "Monthly", + other_details={"max_benefits": 100000}, + include_flexi_benefits=True, + employee=employee, + payroll_period=payroll_period, + ) + salary_slip = make_salary_slip(salary_structure.name, employee=employee, posting_date=getdate()) + salary_slip.insert() + salary_slip.submit() + + application = make_employee_benefit_application( + employee, payroll_period.name, date=leave_application.to_date + ) + self.assertEqual(application.employee_benefits[0].max_benefit_amount, 15000) + + holidays = get_holiday_dates_for_employee(employee, payroll_period.start_date, application.date) + working_days = date_diff(application.date, payroll_period.start_date) + 1 + lwp = calculate_lwp(employee, payroll_period.start_date, holidays, working_days) + self.assertEqual(lwp, 2.5) + + +def make_employee_benefit_application(employee, payroll_period, date): + frappe.db.delete("Employee Benefit Application") + + return frappe.get_doc( + { + "doctype": "Employee Benefit Application", + "employee": employee, + "date": date, + "payroll_period": payroll_period, + "employee_benefits": [{"earning_component": "Medical Allowance", "amount": 1500}], + } + ).insert() diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py index 801ce4ba367..31f26b25e73 100644 --- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py +++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.py @@ -23,9 +23,15 @@ class EmployeeBenefitClaim(Document): max_benefits = get_max_benefits(self.employee, self.claim_date) if not max_benefits or max_benefits <= 0: frappe.throw(_("Employee {0} has no maximum benefit amount").format(self.employee)) - payroll_period = get_payroll_period(self.claim_date, self.claim_date, frappe.db.get_value("Employee", self.employee, "company")) + payroll_period = get_payroll_period( + self.claim_date, self.claim_date, frappe.db.get_value("Employee", self.employee, "company") + ) if not payroll_period: - frappe.throw(_("{0} is not in a valid Payroll Period").format(frappe.format(self.claim_date, dict(fieldtype='Date')))) + frappe.throw( + _("{0} is not in a valid Payroll Period").format( + frappe.format(self.claim_date, dict(fieldtype="Date")) + ) + ) self.validate_max_benefit_for_component(payroll_period) self.validate_max_benefit_for_sal_struct(max_benefits) self.validate_benefit_claim_amount(max_benefits, payroll_period) @@ -36,21 +42,31 @@ class EmployeeBenefitClaim(Document): claimed_amount = self.claimed_amount claimed_amount += get_previous_claimed_amount(self.employee, payroll_period) if max_benefits < claimed_amount: - frappe.throw(_("Maximum benefit of employee {0} exceeds {1} by the sum {2} of previous claimed\ - amount").format(self.employee, max_benefits, claimed_amount-max_benefits)) + frappe.throw( + _( + "Maximum benefit of employee {0} exceeds {1} by the sum {2} of previous claimed\ + amount" + ).format(self.employee, max_benefits, claimed_amount - max_benefits) + ) def validate_max_benefit_for_sal_struct(self, max_benefits): if self.claimed_amount > max_benefits: - frappe.throw(_("Maximum benefit amount of employee {0} exceeds {1}").format(self.employee, max_benefits)) + frappe.throw( + _("Maximum benefit amount of employee {0} exceeds {1}").format(self.employee, max_benefits) + ) def validate_max_benefit_for_component(self, payroll_period): if self.max_amount_eligible: claimed_amount = self.claimed_amount - claimed_amount += get_previous_claimed_amount(self.employee, - payroll_period, component = self.earning_component) + claimed_amount += get_previous_claimed_amount( + self.employee, payroll_period, component=self.earning_component + ) if claimed_amount > self.max_amount_eligible: - frappe.throw(_("Maximum amount eligible for the component {0} exceeds {1}") - .format(self.earning_component, self.max_amount_eligible)) + frappe.throw( + _("Maximum amount eligible for the component {0} exceeds {1}").format( + self.earning_component, self.max_amount_eligible + ) + ) def validate_non_pro_rata_benefit_claim(self, max_benefits, payroll_period): claimed_amount = self.claimed_amount @@ -64,30 +80,39 @@ class EmployeeBenefitClaim(Document): sal_struct = frappe.get_doc("Salary Structure", sal_struct_name) pro_rata_amount = get_benefit_pro_rata_ratio_amount(self.employee, self.claim_date, sal_struct) - claimed_amount += get_previous_claimed_amount(self.employee, payroll_period, non_pro_rata = True) + claimed_amount += get_previous_claimed_amount(self.employee, payroll_period, non_pro_rata=True) if max_benefits < pro_rata_amount + claimed_amount: - frappe.throw(_("Maximum benefit of employee {0} exceeds {1} by the sum {2} of benefit application pro-rata component\ - amount and previous claimed amount").format(self.employee, max_benefits, pro_rata_amount+claimed_amount-max_benefits)) + frappe.throw( + _( + "Maximum benefit of employee {0} exceeds {1} by the sum {2} of benefit application pro-rata component\ + amount and previous claimed amount" + ).format( + self.employee, max_benefits, pro_rata_amount + claimed_amount - max_benefits + ) + ) def get_pro_rata_amount_in_application(self, payroll_period): application = frappe.db.exists( "Employee Benefit Application", - { - 'employee': self.employee, - 'payroll_period': payroll_period, - 'docstatus': 1 - } + {"employee": self.employee, "payroll_period": payroll_period, "docstatus": 1}, ) if application: - return frappe.db.get_value("Employee Benefit Application", application, "pro_rata_dispensed_amount") + return frappe.db.get_value( + "Employee Benefit Application", application, "pro_rata_dispensed_amount" + ) return False + def get_benefit_pro_rata_ratio_amount(employee, on_date, sal_struct): total_pro_rata_max = 0 benefit_amount_total = 0 for sal_struct_row in sal_struct.get("earnings"): try: - pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value("Salary Component", sal_struct_row.salary_component, ["pay_against_benefit_claim", "max_benefit_amount"]) + pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value( + "Salary Component", + sal_struct_row.salary_component, + ["pay_against_benefit_claim", "max_benefit_amount"], + ) except TypeError: # show the error in tests? frappe.throw(_("Unable to find Salary Component {0}").format(sal_struct_row.salary_component)) @@ -95,7 +120,11 @@ def get_benefit_pro_rata_ratio_amount(employee, on_date, sal_struct): total_pro_rata_max += max_benefit_amount if total_pro_rata_max > 0: for sal_struct_row in sal_struct.get("earnings"): - pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value("Salary Component", sal_struct_row.salary_component, ["pay_against_benefit_claim", "max_benefit_amount"]) + pay_against_benefit_claim, max_benefit_amount = frappe.db.get_value( + "Salary Component", + sal_struct_row.salary_component, + ["pay_against_benefit_claim", "max_benefit_amount"], + ) if sal_struct_row.is_flexible_benefit == 1 and pay_against_benefit_claim != 1: component_max = max_benefit_amount @@ -105,6 +134,7 @@ def get_benefit_pro_rata_ratio_amount(employee, on_date, sal_struct): benefit_amount_total += benefit_amount return benefit_amount_total + def get_benefit_claim_amount(employee, start_date, end_date, salary_component=None): query = """ select sum(claimed_amount) @@ -119,41 +149,54 @@ def get_benefit_claim_amount(employee, start_date, end_date, salary_component=No if salary_component: query += " and earning_component = %(earning_component)s" - claimed_amount = flt(frappe.db.sql(query, { - 'employee': employee, - 'start_date': start_date, - 'end_date': end_date, - 'earning_component': salary_component - })[0][0]) + claimed_amount = flt( + frappe.db.sql( + query, + { + "employee": employee, + "start_date": start_date, + "end_date": end_date, + "earning_component": salary_component, + }, + )[0][0] + ) return claimed_amount + def get_total_benefit_dispensed(employee, sal_struct, sal_slip_start_date, payroll_period): pro_rata_amount = 0 claimed_amount = 0 application = frappe.db.exists( "Employee Benefit Application", - { - 'employee': employee, - 'payroll_period': payroll_period.name, - 'docstatus': 1 - } + {"employee": employee, "payroll_period": payroll_period.name, "docstatus": 1}, ) if application: application_obj = frappe.get_doc("Employee Benefit Application", application) - pro_rata_amount = application_obj.pro_rata_dispensed_amount + application_obj.max_benefits - application_obj.remaining_benefit + pro_rata_amount = ( + application_obj.pro_rata_dispensed_amount + + application_obj.max_benefits + - application_obj.remaining_benefit + ) else: pro_rata_amount = get_benefit_pro_rata_ratio_amount(employee, sal_slip_start_date, sal_struct) - claimed_amount += get_benefit_claim_amount(employee, payroll_period.start_date, payroll_period.end_date) + claimed_amount += get_benefit_claim_amount( + employee, payroll_period.start_date, payroll_period.end_date + ) return claimed_amount + pro_rata_amount -def get_last_payroll_period_benefits(employee, sal_slip_start_date, sal_slip_end_date, payroll_period, sal_struct): + +def get_last_payroll_period_benefits( + employee, sal_slip_start_date, sal_slip_end_date, payroll_period, sal_struct +): max_benefits = get_max_benefits(employee, payroll_period.end_date) if not max_benefits: max_benefits = 0 - remaining_benefit = max_benefits - get_total_benefit_dispensed(employee, sal_struct, sal_slip_start_date, payroll_period) + remaining_benefit = max_benefits - get_total_benefit_dispensed( + employee, sal_struct, sal_slip_start_date, payroll_period + ) if remaining_benefit > 0: have_remaining = True # Set the remaining benefits to flexi non pro-rata component in the salary structure @@ -162,7 +205,9 @@ def get_last_payroll_period_benefits(employee, sal_slip_start_date, sal_slip_end if d.is_flexible_benefit == 1: salary_component = frappe.get_doc("Salary Component", d.salary_component) if salary_component.pay_against_benefit_claim == 1: - claimed_amount = get_benefit_claim_amount(employee, payroll_period.start_date, sal_slip_end_date, d.salary_component) + claimed_amount = get_benefit_claim_amount( + employee, payroll_period.start_date, sal_slip_end_date, d.salary_component + ) amount_fit_to_component = salary_component.max_benefit_amount - claimed_amount if amount_fit_to_component > 0: if remaining_benefit > amount_fit_to_component: @@ -171,19 +216,23 @@ def get_last_payroll_period_benefits(employee, sal_slip_start_date, sal_slip_end else: amount = remaining_benefit have_remaining = False - current_claimed_amount = get_benefit_claim_amount(employee, sal_slip_start_date, sal_slip_end_date, d.salary_component) + current_claimed_amount = get_benefit_claim_amount( + employee, sal_slip_start_date, sal_slip_end_date, d.salary_component + ) amount += current_claimed_amount struct_row = {} salary_components_dict = {} - struct_row['depends_on_payment_days'] = salary_component.depends_on_payment_days - struct_row['salary_component'] = salary_component.name - struct_row['abbr'] = salary_component.salary_component_abbr - struct_row['do_not_include_in_total'] = salary_component.do_not_include_in_total - struct_row['is_tax_applicable'] = salary_component.is_tax_applicable, - struct_row['is_flexible_benefit'] = salary_component.is_flexible_benefit, - struct_row['variable_based_on_taxable_salary'] = salary_component.variable_based_on_taxable_salary - salary_components_dict['amount'] = amount - salary_components_dict['struct_row'] = struct_row + struct_row["depends_on_payment_days"] = salary_component.depends_on_payment_days + struct_row["salary_component"] = salary_component.name + struct_row["abbr"] = salary_component.salary_component_abbr + struct_row["do_not_include_in_total"] = salary_component.do_not_include_in_total + struct_row["is_tax_applicable"] = (salary_component.is_tax_applicable,) + struct_row["is_flexible_benefit"] = (salary_component.is_flexible_benefit,) + struct_row[ + "variable_based_on_taxable_salary" + ] = salary_component.variable_based_on_taxable_salary + salary_components_dict["amount"] = amount + salary_components_dict["struct_row"] = struct_row salary_components_array.append(salary_components_dict) if not have_remaining: break diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py index a37e22425f7..7686185349f 100644 --- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.py +++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.py @@ -15,13 +15,17 @@ class EmployeeIncentive(Document): self.validate_salary_structure() def validate_salary_structure(self): - if not frappe.db.exists('Salary Structure Assignment', {'employee': self.employee}): - frappe.throw(_("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format(self.employee)) + if not frappe.db.exists("Salary Structure Assignment", {"employee": self.employee}): + frappe.throw( + _("There is no Salary Structure assigned to {0}. First assign a Salary Stucture.").format( + self.employee + ) + ) def on_submit(self): - company = frappe.db.get_value('Employee', self.employee, 'company') + company = frappe.db.get_value("Employee", self.employee, "company") - additional_salary = frappe.new_doc('Additional Salary') + additional_salary = frappe.new_doc("Additional Salary") additional_salary.employee = self.employee additional_salary.currency = self.currency additional_salary.salary_component = self.salary_component diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py index 9b5eab636f1..3d1d96598fc 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.py @@ -20,7 +20,9 @@ class EmployeeTaxExemptionDeclaration(Document): def validate(self): validate_active_employee(self.employee) validate_tax_declaration(self.declarations) - validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) + validate_duplicate_exemption_for_payroll_period( + self.doctype, self.name, self.payroll_period, self.employee + ) self.set_total_declared_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() @@ -31,7 +33,9 @@ class EmployeeTaxExemptionDeclaration(Document): self.total_declared_amount += flt(d.amount) def set_total_exemption_amount(self): - self.total_exemption_amount = get_total_exemption_amount(self.declarations) + self.total_exemption_amount = flt( + get_total_exemption_amount(self.declarations), self.precision("total_exemption_amount") + ) def calculate_hra_exemption(self): self.salary_structure_hra, self.annual_hra_exemption, self.monthly_hra_exemption = 0, 0, 0 @@ -39,21 +43,36 @@ class EmployeeTaxExemptionDeclaration(Document): hra_exemption = calculate_annual_eligible_hra_exemption(self) if hra_exemption: self.total_exemption_amount += hra_exemption["annual_exemption"] - self.salary_structure_hra = hra_exemption["hra_amount"] - self.annual_hra_exemption = hra_exemption["annual_exemption"] - self.monthly_hra_exemption = hra_exemption["monthly_exemption"] + self.total_exemption_amount = flt( + self.total_exemption_amount, self.precision("total_exemption_amount") + ) + self.salary_structure_hra = flt( + hra_exemption["hra_amount"], self.precision("salary_structure_hra") + ) + self.annual_hra_exemption = flt( + hra_exemption["annual_exemption"], self.precision("annual_hra_exemption") + ) + self.monthly_hra_exemption = flt( + hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption") + ) + @frappe.whitelist() def make_proof_submission(source_name, target_doc=None): - doclist = get_mapped_doc("Employee Tax Exemption Declaration", source_name, { - "Employee Tax Exemption Declaration": { - "doctype": "Employee Tax Exemption Proof Submission", - "field_no_map": ["monthly_house_rent", "monthly_hra_exemption"] + doclist = get_mapped_doc( + "Employee Tax Exemption Declaration", + source_name, + { + "Employee Tax Exemption Declaration": { + "doctype": "Employee Tax Exemption Proof Submission", + "field_no_map": ["monthly_house_rent", "monthly_hra_exemption"], + }, + "Employee Tax Exemption Declaration Category": { + "doctype": "Employee Tax Exemption Proof Submission Detail", + "add_if_empty": True, + }, }, - "Employee Tax Exemption Declaration Category": { - "doctype": "Employee Tax Exemption Proof Submission Detail", - "add_if_empty": True - } - }, target_doc) + target_doc, + ) return doclist diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py index fc28afdc3e5..2d8df350118 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py @@ -4,127 +4,487 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_months, getdate import erpnext from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.utils import DuplicateDeclarationError -class TestEmployeeTaxExemptionDeclaration(unittest.TestCase): +class TestEmployeeTaxExemptionDeclaration(FrappeTestCase): def setUp(self): - make_employee("employee@taxexepmtion.com") - make_employee("employee1@taxexepmtion.com") - create_payroll_period() + make_employee("employee@taxexemption.com", company="_Test Company") + make_employee("employee1@taxexemption.com", company="_Test Company") + create_payroll_period(company="_Test Company") create_exemption_category() - frappe.db.sql("""delete from `tabEmployee Tax Exemption Declaration`""") + frappe.db.delete("Employee Tax Exemption Declaration") + frappe.db.delete("Salary Structure Assignment") def test_duplicate_category_in_declaration(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 100000), - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 50000) - ] - }) + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=100000, + ), + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=50000, + ), + ], + } + ) self.assertRaises(frappe.ValidationError, declaration.save) def test_duplicate_entry_for_payroll_period(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 100000), - dict(exemption_sub_category = "_Test1 Sub Category", - exemption_category = "_Test Category", - amount = 50000), - ] - }).insert() + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=100000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=50000, + ), + ], + } + ).insert() - duplicate_declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 100000) - ] - }) + duplicate_declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=100000, + ) + ], + } + ) self.assertRaises(DuplicateDeclarationError, duplicate_declaration.insert) - duplicate_declaration.employee = frappe.get_value("Employee", {"user_id":"employee1@taxexepmtion.com"}, "name") + duplicate_declaration.employee = frappe.get_value( + "Employee", {"user_id": "employee1@taxexemption.com"}, "name" + ) self.assertTrue(duplicate_declaration.insert) def test_exemption_amount(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Declaration", - "employee": frappe.get_value("Employee", {"user_id":"employee@taxexepmtion.com"}, "name"), - "company": erpnext.get_default_company(), - "payroll_period": "_Test Payroll Period", - "currency": erpnext.get_default_currency(), - "declarations": [ - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 80000), - dict(exemption_sub_category = "_Test1 Sub Category", - exemption_category = "_Test Category", - amount = 60000), - ] - }).insert() + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name"), + "company": erpnext.get_default_company(), + "payroll_period": "_Test Payroll Period", + "currency": erpnext.get_default_currency(), + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=80000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() self.assertEqual(declaration.total_exemption_amount, 100000) + def test_india_hra_exemption(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + + setup_hra_exemption_prerequisites("Monthly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "_Test Company", + "payroll_period": "_Test Payroll Period", + "currency": "INR", + "monthly_house_rent": 50000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=80000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Monthly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 3000) + self.assertEqual(declaration.annual_hra_exemption, 36000) + # 100000 Standard Exemption + 36000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 136000) + + # reset + frappe.flags.country = current_country + + def test_india_hra_exemption_with_daily_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + + setup_hra_exemption_prerequisites("Daily") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "_Test Company", + "payroll_period": "_Test Payroll Period", + "currency": "INR", + "monthly_house_rent": 170000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Daily HRA received = 3000 + # should set HRA exemption as per (rent - 10% of Basic Salary), that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 17916.67) + self.assertEqual(declaration.annual_hra_exemption, 215000) + # 50000 Standard Exemption + 215000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 265000) + + # reset + frappe.flags.country = current_country + + def test_india_hra_exemption_with_weekly_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + + setup_hra_exemption_prerequisites("Weekly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "_Test Company", + "payroll_period": "_Test Payroll Period", + "currency": "INR", + "monthly_house_rent": 170000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Weekly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 13000) + self.assertEqual(declaration.annual_hra_exemption, 156000) + # 50000 Standard Exemption + 156000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 206000) + + # reset + frappe.flags.country = current_country + + def test_india_hra_exemption_with_fortnightly_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + + setup_hra_exemption_prerequisites("Fortnightly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "_Test Company", + "payroll_period": "_Test Payroll Period", + "currency": "INR", + "monthly_house_rent": 170000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Fortnightly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 6500) + self.assertEqual(declaration.annual_hra_exemption, 78000) + # 50000 Standard Exemption + 78000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 128000) + + # reset + frappe.flags.country = current_country + + def test_india_hra_exemption_with_bimonthly_payroll_frequency(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + + setup_hra_exemption_prerequisites("Bimonthly") + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "_Test Company", + "payroll_period": "_Test Payroll Period", + "currency": "INR", + "monthly_house_rent": 50000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=80000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Bimonthly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 1500) + self.assertEqual(declaration.annual_hra_exemption, 18000) + # 100000 Standard Exemption + 18000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 118000) + + # reset + frappe.flags.country = current_country + + def test_india_hra_exemption_with_multiple_salary_structure_assignments(self): + from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab + from erpnext.payroll.doctype.salary_structure.test_salary_structure import ( + create_salary_structure_assignment, + make_salary_structure, + ) + + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + + employee = make_employee("employee@taxexemption2.com", company="_Test Company") + payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company") + + create_tax_slab( + payroll_period, + allow_tax_exemption=True, + currency="INR", + effective_date=getdate("2019-04-01"), + company="_Test Company", + ) + + frappe.db.set_value( + "Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"} + ) + + # salary structure with base 50000, HRA 3000 + make_salary_structure( + "Monthly Structure for HRA Exemption 1", + "Monthly", + employee=employee, + company="_Test Company", + currency="INR", + payroll_period=payroll_period.name, + from_date=payroll_period.start_date, + ) + + # salary structure with base 70000, HRA = base * 0.2 = 14000 + salary_structure = make_salary_structure( + "Monthly Structure for HRA Exemption 2", + "Monthly", + employee=employee, + company="_Test Company", + currency="INR", + payroll_period=payroll_period.name, + from_date=payroll_period.start_date, + dont_submit=True, + ) + for component_row in salary_structure.earnings: + if component_row.salary_component == "HRA": + component_row.amount = 0 + component_row.amount_based_on_formula = 1 + component_row.formula = "base * 0.2" + break + + salary_structure.submit() + + create_salary_structure_assignment( + employee, + salary_structure.name, + from_date=add_months(payroll_period.start_date, 6), + company="_Test Company", + currency="INR", + payroll_period=payroll_period.name, + base=70000, + allow_duplicate=True, + ) + + declaration = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Declaration", + "employee": employee, + "company": "_Test Company", + "payroll_period": payroll_period.name, + "currency": "INR", + "monthly_house_rent": 50000, + "rented_in_metro_city": 1, + "declarations": [ + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + amount=60000, + ), + ], + } + ).insert() + + # Monthly HRA received = 50000 * 6 months + 70000 * 6 months + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(declaration.monthly_hra_exemption, 8500) + self.assertEqual(declaration.annual_hra_exemption, 102000) + # 50000 Standard Exemption + 102000 HRA exemption + self.assertEqual(declaration.total_exemption_amount, 152000) + + # reset + frappe.flags.country = current_country + + def create_payroll_period(**args): args = frappe._dict(args) name = args.name or "_Test Payroll Period" if not frappe.db.exists("Payroll Period", name): from datetime import date - payroll_period = frappe.get_doc(dict( - doctype = 'Payroll Period', - name = name, - company = args.company or erpnext.get_default_company(), - start_date = args.start_date or date(date.today().year, 1, 1), - end_date = args.end_date or date(date.today().year, 12, 31) - )).insert() + + payroll_period = frappe.get_doc( + dict( + doctype="Payroll Period", + name=name, + company=args.company or erpnext.get_default_company(), + start_date=args.start_date or date(date.today().year, 1, 1), + end_date=args.end_date or date(date.today().year, 12, 31), + ) + ).insert() return payroll_period else: return frappe.get_doc("Payroll Period", name) + def create_exemption_category(): if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"): - category = frappe.get_doc({ - "doctype": "Employee Tax Exemption Category", - "name": "_Test Category", - "deduction_component": "Income Tax", - "max_amount": 100000 - }).insert() + category = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Category", + "name": "_Test Category", + "deduction_component": "Income Tax", + "max_amount": 100000, + } + ).insert() if not frappe.db.exists("Employee Tax Exemption Sub Category", "_Test Sub Category"): - frappe.get_doc({ - "doctype": "Employee Tax Exemption Sub Category", - "name": "_Test Sub Category", - "exemption_category": "_Test Category", - "max_amount": 100000, - "is_active": 1 - }).insert() + frappe.get_doc( + { + "doctype": "Employee Tax Exemption Sub Category", + "name": "_Test Sub Category", + "exemption_category": "_Test Category", + "max_amount": 100000, + "is_active": 1, + } + ).insert() if not frappe.db.exists("Employee Tax Exemption Sub Category", "_Test1 Sub Category"): - frappe.get_doc({ - "doctype": "Employee Tax Exemption Sub Category", - "name": "_Test1 Sub Category", - "exemption_category": "_Test Category", - "max_amount": 50000, - "is_active": 1 - }).insert() + frappe.get_doc( + { + "doctype": "Employee Tax Exemption Sub Category", + "name": "_Test1 Sub Category", + "exemption_category": "_Test Category", + "max_amount": 50000, + "is_active": 1, + } + ).insert() + + +def setup_hra_exemption_prerequisites(frequency, employee=None): + from erpnext.payroll.doctype.salary_slip.test_salary_slip import create_tax_slab + from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure + + payroll_period = create_payroll_period(name="_Test Payroll Period", company="_Test Company") + if not employee: + employee = frappe.get_value("Employee", {"user_id": "employee@taxexemption.com"}, "name") + + create_tax_slab( + payroll_period, + allow_tax_exemption=True, + currency="INR", + effective_date=getdate("2019-04-01"), + company="_Test Company", + ) + + make_salary_structure( + f"{frequency} Structure for HRA Exemption", + frequency, + employee=employee, + company="_Test Company", + currency="INR", + payroll_period=payroll_period, + ) + + frappe.db.set_value( + "Company", "_Test Company", {"basic_component": "Basic Salary", "hra_component": "HRA"} + ) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py index 56e73b37dff..b3b66b9e7b1 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.py @@ -21,7 +21,9 @@ class EmployeeTaxExemptionProofSubmission(Document): self.set_total_actual_amount() self.set_total_exemption_amount() self.calculate_hra_exemption() - validate_duplicate_exemption_for_payroll_period(self.doctype, self.name, self.payroll_period, self.employee) + validate_duplicate_exemption_for_payroll_period( + self.doctype, self.name, self.payroll_period, self.employee + ) def set_total_actual_amount(self): self.total_actual_amount = flt(self.get("house_rent_payment_amount")) @@ -29,7 +31,9 @@ class EmployeeTaxExemptionProofSubmission(Document): self.total_actual_amount += flt(d.amount) def set_total_exemption_amount(self): - self.exemption_amount = get_total_exemption_amount(self.tax_exemption_proofs) + self.exemption_amount = flt( + get_total_exemption_amount(self.tax_exemption_proofs), self.precision("exemption_amount") + ) def calculate_hra_exemption(self): self.monthly_hra_exemption, self.monthly_house_rent, self.total_eligible_hra_exemption = 0, 0, 0 @@ -37,6 +41,13 @@ class EmployeeTaxExemptionProofSubmission(Document): hra_exemption = calculate_hra_exemption_for_period(self) if hra_exemption: self.exemption_amount += hra_exemption["total_eligible_hra_exemption"] - self.monthly_hra_exemption = hra_exemption["monthly_exemption"] - self.monthly_house_rent = hra_exemption["monthly_house_rent"] - self.total_eligible_hra_exemption = hra_exemption["total_eligible_hra_exemption"] + self.exemption_amount = flt(self.exemption_amount, self.precision("exemption_amount")) + self.monthly_hra_exemption = flt( + hra_exemption["monthly_exemption"], self.precision("monthly_hra_exemption") + ) + self.monthly_house_rent = flt( + hra_exemption["monthly_house_rent"], self.precision("monthly_house_rent") + ) + self.total_eligible_hra_exemption = flt( + hra_exemption["total_eligible_hra_exemption"], self.precision("total_eligible_hra_exemption") + ) diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py index f2aa64c2878..416cf316c97 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/test_employee_tax_exemption_proof_submission.py @@ -4,55 +4,133 @@ import unittest import frappe +from frappe.tests.utils import FrappeTestCase +from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.payroll.doctype.employee_tax_exemption_declaration.test_employee_tax_exemption_declaration import ( create_exemption_category, create_payroll_period, + setup_hra_exemption_prerequisites, ) -class TestEmployeeTaxExemptionProofSubmission(unittest.TestCase): - def setup(self): - make_employee("employee@proofsubmission.com") - create_payroll_period() +class TestEmployeeTaxExemptionProofSubmission(FrappeTestCase): + def setUp(self): + make_employee("employee@proofsubmission.com", company="_Test Company") + create_payroll_period(company="_Test Company") create_exemption_category() - frappe.db.sql("""delete from `tabEmployee Tax Exemption Proof Submission`""") + frappe.db.delete("Employee Tax Exemption Proof Submission") + frappe.db.delete("Salary Structure Assignment") def test_exemption_amount_lesser_than_category_max(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "employee": frappe.get_value("Employee", {"user_id":"employee@proofsubmission.com"}, "name"), - "payroll_period": "Test Payroll Period", - "tax_exemption_proofs": [dict(exemption_sub_category = "_Test Sub Category", - type_of_proof = "Test Proof", - exemption_category = "_Test Category", - amount = 150000)] - }) - self.assertRaises(frappe.ValidationError, declaration.save) - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "payroll_period": "Test Payroll Period", - "employee": frappe.get_value("Employee", {"user_id":"employee@proofsubmission.com"}, "name"), - "tax_exemption_proofs": [dict(exemption_sub_category = "_Test Sub Category", - type_of_proof = "Test Proof", - exemption_category = "_Test Category", - amount = 100000)] - }) - self.assertTrue(declaration.save) - self.assertTrue(declaration.submit) + proof = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), + "payroll_period": "Test Payroll Period", + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + type_of_proof="Test Proof", + exemption_category="_Test Category", + amount=150000, + ) + ], + } + ) + self.assertRaises(frappe.ValidationError, proof.save) + proof = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "payroll_period": "Test Payroll Period", + "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + type_of_proof="Test Proof", + exemption_category="_Test Category", + amount=100000, + ) + ], + } + ) + self.assertTrue(proof.save) + self.assertTrue(proof.submit) def test_duplicate_category_in_proof_submission(self): - declaration = frappe.get_doc({ - "doctype": "Employee Tax Exemption Proof Submission", - "employee": frappe.get_value("Employee", {"user_id":"employee@proofsubmission.com"}, "name"), - "payroll_period": "Test Payroll Period", - "tax_exemption_proofs": [dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - type_of_proof = "Test Proof", - amount = 100000), - dict(exemption_sub_category = "_Test Sub Category", - exemption_category = "_Test Category", - amount = 50000), - ] - }) - self.assertRaises(frappe.ValidationError, declaration.save) + proof = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name"), + "payroll_period": "Test Payroll Period", + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + type_of_proof="Test Proof", + amount=100000, + ), + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + amount=50000, + ), + ], + } + ) + self.assertRaises(frappe.ValidationError, proof.save) + + def test_india_hra_exemption(self): + # set country + current_country = frappe.flags.country + frappe.flags.country = "India" + + employee = frappe.get_value("Employee", {"user_id": "employee@proofsubmission.com"}, "name") + setup_hra_exemption_prerequisites("Monthly", employee) + payroll_period = frappe.db.get_value( + "Payroll Period", "_Test Payroll Period", ["start_date", "end_date"], as_dict=True + ) + + proof = frappe.get_doc( + { + "doctype": "Employee Tax Exemption Proof Submission", + "employee": employee, + "company": "_Test Company", + "payroll_period": "_Test Payroll Period", + "currency": "INR", + "house_rent_payment_amount": 600000, + "rented_in_metro_city": 1, + "rented_from_date": payroll_period.start_date, + "rented_to_date": payroll_period.end_date, + "tax_exemption_proofs": [ + dict( + exemption_sub_category="_Test Sub Category", + exemption_category="_Test Category", + type_of_proof="Test Proof", + amount=100000, + ), + dict( + exemption_sub_category="_Test1 Sub Category", + exemption_category="_Test Category", + type_of_proof="Test Proof", + amount=50000, + ), + ], + } + ).insert() + + self.assertEqual(proof.monthly_house_rent, 50000) + + # Monthly HRA received = 3000 + # should set HRA exemption as per actual annual HRA because that's the minimum + self.assertEqual(proof.monthly_hra_exemption, 3000) + self.assertEqual(proof.total_eligible_hra_exemption, 36000) + + # total exemptions + house rent payment amount + self.assertEqual(proof.total_actual_amount, 750000) + + # 100000 Standard Exemption + 36000 HRA exemption + self.assertEqual(proof.exemption_amount, 136000) + + # reset + frappe.flags.country = current_country diff --git a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py index 4ac11f7112d..fb75d6706c3 100644 --- a/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py +++ b/erpnext/payroll/doctype/employee_tax_exemption_sub_category/employee_tax_exemption_sub_category.py @@ -10,7 +10,12 @@ from frappe.utils import flt class EmployeeTaxExemptionSubCategory(Document): def validate(self): - category_max_amount = frappe.db.get_value("Employee Tax Exemption Category", self.exemption_category, "max_amount") + category_max_amount = frappe.db.get_value( + "Employee Tax Exemption Category", self.exemption_category, "max_amount" + ) if flt(self.max_amount) > flt(category_max_amount): - frappe.throw(_("Max Exemption Amount cannot be greater than maximum exemption amount {0} of Tax Exemption Category {1}") - .format(category_max_amount, self.exemption_category)) + frappe.throw( + _( + "Max Exemption Amount cannot be greater than maximum exemption amount {0} of Tax Exemption Category {1}" + ).format(category_max_amount, self.exemption_category) + ) diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json index 5cffd7eebf9..0d823cac06c 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.json +++ b/erpnext/payroll/doctype/gratuity/gratuity.json @@ -90,9 +90,8 @@ "fieldtype": "Select", "in_list_view": 1, "label": "Status", - "options": "Draft\nUnpaid\nPaid", - "read_only": 1, - "reqd": 1 + "options": "Draft\nUnpaid\nPaid\nSubmitted\nCancelled", + "read_only": 1 }, { "depends_on": "eval: doc.pay_via_salary_slip == 0", @@ -196,7 +195,7 @@ "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2020-11-02 18:21:11.971488", + "modified": "2022-05-27 13:56:14.349183", "modified_by": "Administrator", "module": "Payroll", "name": "Gratuity", diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py index 939634a9310..91740ae8c6c 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity.py +++ b/erpnext/payroll/doctype/gratuity/gratuity.py @@ -27,7 +27,7 @@ class Gratuity(AccountsController): self.create_gl_entries() def on_cancel(self): - self.ignore_linked_doctypes = ['GL Entry'] + self.ignore_linked_doctypes = ["GL Entry"] self.create_gl_entries(cancel=True) def create_gl_entries(self, cancel=False): @@ -39,28 +39,34 @@ class Gratuity(AccountsController): # payable entry if self.amount: gl_entry.append( - self.get_gl_dict({ - "account": self.payable_account, - "credit": self.amount, - "credit_in_account_currency": self.amount, - "against": self.expense_account, - "party_type": "Employee", - "party": self.employee, - "against_voucher_type": self.doctype, - "against_voucher": self.name, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.payable_account, + "credit": self.amount, + "credit_in_account_currency": self.amount, + "against": self.expense_account, + "party_type": "Employee", + "party": self.employee, + "against_voucher_type": self.doctype, + "against_voucher": self.name, + "cost_center": self.cost_center, + }, + item=self, + ) ) # expense entries gl_entry.append( - self.get_gl_dict({ - "account": self.expense_account, - "debit": self.amount, - "debit_in_account_currency": self.amount, - "against": self.payable_account, - "cost_center": self.cost_center - }, item=self) + self.get_gl_dict( + { + "account": self.expense_account, + "debit": self.amount, + "debit_in_account_currency": self.amount, + "against": self.payable_account, + "cost_center": self.cost_center, + }, + item=self, + ) ) else: frappe.throw(_("Total Amount can not be zero")) @@ -69,7 +75,7 @@ class Gratuity(AccountsController): def create_additional_salary(self): if self.pay_via_salary_slip: - additional_salary = frappe.new_doc('Additional Salary') + additional_salary = frappe.new_doc("Additional Salary") additional_salary.employee = self.employee additional_salary.salary_component = self.salary_component additional_salary.overwrite_salary_structure_amount = 0 @@ -81,19 +87,22 @@ class Gratuity(AccountsController): additional_salary.submit() def set_total_advance_paid(self): - paid_amount = frappe.db.sql(""" + paid_amount = frappe.db.sql( + """ select ifnull(sum(debit_in_account_currency), 0) as paid_amount from `tabGL Entry` where against_voucher_type = 'Gratuity' and against_voucher = %s and party_type = 'Employee' and party = %s - """, (self.name, self.employee), as_dict=1)[0].paid_amount + """, + (self.name, self.employee), + as_dict=1, + )[0].paid_amount if flt(paid_amount) > self.amount: frappe.throw(_("Row {0}# Paid Amount cannot be greater than Total amount")) - self.db_set("paid_amount", paid_amount) if self.amount == self.paid_amount: self.db_set("status", "Paid") @@ -104,69 +113,97 @@ def calculate_work_experience_and_amount(employee, gratuity_rule): current_work_experience = calculate_work_experience(employee, gratuity_rule) or 0 gratuity_amount = calculate_gratuity_amount(employee, gratuity_rule, current_work_experience) or 0 - return {'current_work_experience': current_work_experience, "amount": gratuity_amount} + return {"current_work_experience": current_work_experience, "amount": gratuity_amount} + def calculate_work_experience(employee, gratuity_rule): - total_working_days_per_year, minimum_year_for_gratuity = frappe.db.get_value("Gratuity Rule", gratuity_rule, ["total_working_days_per_year", "minimum_year_for_gratuity"]) + total_working_days_per_year, minimum_year_for_gratuity = frappe.db.get_value( + "Gratuity Rule", gratuity_rule, ["total_working_days_per_year", "minimum_year_for_gratuity"] + ) - date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) + date_of_joining, relieving_date = frappe.db.get_value( + "Employee", employee, ["date_of_joining", "relieving_date"] + ) if not relieving_date: - frappe.throw(_("Please set Relieving Date for employee: {0}").format(bold(get_link_to_form("Employee", employee)))) + frappe.throw( + _("Please set Relieving Date for employee: {0}").format( + bold(get_link_to_form("Employee", employee)) + ) + ) - method = frappe.db.get_value("Gratuity Rule", gratuity_rule, "work_experience_calculation_function") - employee_total_workings_days = calculate_employee_total_workings_days(employee, date_of_joining, relieving_date) + method = frappe.db.get_value( + "Gratuity Rule", gratuity_rule, "work_experience_calculation_function" + ) + employee_total_workings_days = calculate_employee_total_workings_days( + employee, date_of_joining, relieving_date + ) - current_work_experience = employee_total_workings_days/total_working_days_per_year or 1 - current_work_experience = get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity, employee) + current_work_experience = employee_total_workings_days / total_working_days_per_year or 1 + current_work_experience = get_work_experience_using_method( + method, current_work_experience, minimum_year_for_gratuity, employee + ) return current_work_experience -def calculate_employee_total_workings_days(employee, date_of_joining, relieving_date ): + +def calculate_employee_total_workings_days(employee, date_of_joining, relieving_date): employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days payroll_based_on = frappe.db.get_value("Payroll Settings", None, "payroll_based_on") or "Leave" if payroll_based_on == "Leave": total_lwp = get_non_working_days(employee, relieving_date, "On Leave") employee_total_workings_days -= total_lwp - elif payroll_based_on == "Attendance": + elif payroll_based_on == "Attendance": total_absents = get_non_working_days(employee, relieving_date, "Absent") employee_total_workings_days -= total_absents return employee_total_workings_days -def get_work_experience_using_method(method, current_work_experience, minimum_year_for_gratuity, employee): + +def get_work_experience_using_method( + method, current_work_experience, minimum_year_for_gratuity, employee +): if method == "Round off Work Experience": current_work_experience = round(current_work_experience) else: current_work_experience = floor(current_work_experience) if current_work_experience < minimum_year_for_gratuity: - frappe.throw(_("Employee: {0} have to complete minimum {1} years for gratuity").format(bold(employee), minimum_year_for_gratuity)) + frappe.throw( + _("Employee: {0} have to complete minimum {1} years for gratuity").format( + bold(employee), minimum_year_for_gratuity + ) + ) return current_work_experience + def get_non_working_days(employee, relieving_date, status): - filters={ - "docstatus": 1, - "status": status, - "employee": employee, - "attendance_date": ("<=", get_datetime(relieving_date)) - } + filters = { + "docstatus": 1, + "status": status, + "employee": employee, + "attendance_date": ("<=", get_datetime(relieving_date)), + } if status == "On Leave": - lwp_leave_types = frappe.get_list("Leave Type", filters = {"is_lwp":1}) + lwp_leave_types = frappe.get_list("Leave Type", filters={"is_lwp": 1}) lwp_leave_types = [leave_type.name for leave_type in lwp_leave_types] - filters["leave_type"] = ("IN", lwp_leave_types) + filters["leave_type"] = ("IN", lwp_leave_types) - - record = frappe.get_all("Attendance", filters=filters, fields = ["COUNT(name) as total_lwp"]) + record = frappe.get_all("Attendance", filters=filters, fields=["COUNT(name) as total_lwp"]) return record[0].total_lwp if len(record) else 0 + def calculate_gratuity_amount(employee, gratuity_rule, experience): applicable_earnings_component = get_applicable_components(gratuity_rule) - total_applicable_components_amount = get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule) + total_applicable_components_amount = get_total_applicable_component_amount( + employee, applicable_earnings_component, gratuity_rule + ) - calculate_gratuity_amount_based_on = frappe.db.get_value("Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on") + calculate_gratuity_amount_based_on = frappe.db.get_value( + "Gratuity Rule", gratuity_rule, "calculate_gratuity_amount_based_on" + ) gratuity_amount = 0 slabs = get_gratuity_rule_slabs(gratuity_rule) slab_found = False @@ -174,49 +211,78 @@ def calculate_gratuity_amount(employee, gratuity_rule, experience): for slab in slabs: if calculate_gratuity_amount_based_on == "Current Slab": - slab_found, gratuity_amount = calculate_amount_based_on_current_slab(slab.from_year, slab.to_year, - experience, total_applicable_components_amount, slab.fraction_of_applicable_earnings) + slab_found, gratuity_amount = calculate_amount_based_on_current_slab( + slab.from_year, + slab.to_year, + experience, + total_applicable_components_amount, + slab.fraction_of_applicable_earnings, + ) if slab_found: - break + break elif calculate_gratuity_amount_based_on == "Sum of all previous slabs": if slab.to_year == 0 and slab.from_year == 0: - gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + gratuity_amount += ( + year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + ) slab_found = True break - if experience > slab.to_year and experience > slab.from_year and slab.to_year !=0: - gratuity_amount += (slab.to_year - slab.from_year) * total_applicable_components_amount * slab.fraction_of_applicable_earnings - year_left -= (slab.to_year - slab.from_year) + if experience > slab.to_year and experience > slab.from_year and slab.to_year != 0: + gratuity_amount += ( + (slab.to_year - slab.from_year) + * total_applicable_components_amount + * slab.fraction_of_applicable_earnings + ) + year_left -= slab.to_year - slab.from_year slab_found = True elif slab.from_year <= experience and (experience < slab.to_year or slab.to_year == 0): - gratuity_amount += year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + gratuity_amount += ( + year_left * total_applicable_components_amount * slab.fraction_of_applicable_earnings + ) slab_found = True if not slab_found: - frappe.throw(_("No Suitable Slab found for Calculation of gratuity amount in Gratuity Rule: {0}").format(bold(gratuity_rule))) + frappe.throw( + _("No Suitable Slab found for Calculation of gratuity amount in Gratuity Rule: {0}").format( + bold(gratuity_rule) + ) + ) return gratuity_amount + def get_applicable_components(gratuity_rule): - applicable_earnings_component = frappe.get_all("Gratuity Applicable Component", filters= {'parent': gratuity_rule}, fields=["salary_component"]) + applicable_earnings_component = frappe.get_all( + "Gratuity Applicable Component", filters={"parent": gratuity_rule}, fields=["salary_component"] + ) if len(applicable_earnings_component) == 0: - frappe.throw(_("No Applicable Earnings Component found for Gratuity Rule: {0}").format(bold(get_link_to_form("Gratuity Rule",gratuity_rule)))) - applicable_earnings_component = [component.salary_component for component in applicable_earnings_component] + frappe.throw( + _("No Applicable Earnings Component found for Gratuity Rule: {0}").format( + bold(get_link_to_form("Gratuity Rule", gratuity_rule)) + ) + ) + applicable_earnings_component = [ + component.salary_component for component in applicable_earnings_component + ] return applicable_earnings_component + def get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule): - sal_slip = get_last_salary_slip(employee) + sal_slip = get_last_salary_slip(employee) if not sal_slip: frappe.throw(_("No Salary Slip is found for Employee: {0}").format(bold(employee))) - component_and_amounts = frappe.get_all("Salary Detail", + component_and_amounts = frappe.get_all( + "Salary Detail", filters={ "docstatus": 1, - 'parent': sal_slip, + "parent": sal_slip, "parentfield": "earnings", - 'salary_component': ('in', applicable_earnings_component) + "salary_component": ("in", applicable_earnings_component), }, - fields=["amount"]) + fields=["amount"], + ) total_applicable_components_amount = 0 if not len(component_and_amounts): frappe.throw(_("No Applicable Component is present in last month salary slip")) @@ -224,30 +290,44 @@ def get_total_applicable_component_amount(employee, applicable_earnings_componen total_applicable_components_amount += data.amount return total_applicable_components_amount -def calculate_amount_based_on_current_slab(from_year, to_year, experience, total_applicable_components_amount, fraction_of_applicable_earnings): - slab_found = False; gratuity_amount = 0 + +def calculate_amount_based_on_current_slab( + from_year, + to_year, + experience, + total_applicable_components_amount, + fraction_of_applicable_earnings, +): + slab_found = False + gratuity_amount = 0 if experience >= from_year and (to_year == 0 or experience < to_year): - gratuity_amount = total_applicable_components_amount * experience * fraction_of_applicable_earnings + gratuity_amount = ( + total_applicable_components_amount * experience * fraction_of_applicable_earnings + ) if fraction_of_applicable_earnings: slab_found = True return slab_found, gratuity_amount + def get_gratuity_rule_slabs(gratuity_rule): - return frappe.get_all("Gratuity Rule Slab", filters= {'parent': gratuity_rule}, fields = ["*"], order_by="idx") + return frappe.get_all( + "Gratuity Rule Slab", filters={"parent": gratuity_rule}, fields=["*"], order_by="idx" + ) + def get_salary_structure(employee): - return frappe.get_list("Salary Structure Assignment", filters = { - "employee": employee, 'docstatus': 1 - }, + return frappe.get_list( + "Salary Structure Assignment", + filters={"employee": employee, "docstatus": 1}, fields=["from_date", "salary_structure"], - order_by = "from_date desc")[0].salary_structure + order_by="from_date desc", + )[0].salary_structure + def get_last_salary_slip(employee): - salary_slips = frappe.get_list("Salary Slip", filters = { - "employee": employee, 'docstatus': 1 - }, - order_by = "start_date desc" + salary_slips = frappe.get_list( + "Salary Slip", filters={"employee": employee, "docstatus": 1}, order_by="start_date desc" ) if not salary_slips: return diff --git a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py index 6c3cdfda512..35ae1f4feaf 100644 --- a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py +++ b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py @@ -1,21 +1,14 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'reference_name', - 'non_standard_fieldnames': { - 'Additional Salary': 'ref_docname', + "fieldname": "reference_name", + "non_standard_fieldnames": { + "Additional Salary": "ref_docname", }, - 'transactions': [ - { - 'label': _('Payment'), - 'items': ['Payment Entry'] - }, - { - 'label': _('Additional Salary'), - 'items': ['Additional Salary'] - } - ] + "transactions": [ + {"label": _("Payment"), "items": ["Payment Entry"]}, + {"label": _("Additional Salary"), "items": ["Additional Salary"]}, + ], } diff --git a/erpnext/payroll/doctype/gratuity/gratuity_list.js b/erpnext/payroll/doctype/gratuity/gratuity_list.js new file mode 100644 index 00000000000..20e3d5b4e52 --- /dev/null +++ b/erpnext/payroll/doctype/gratuity/gratuity_list.js @@ -0,0 +1,12 @@ +frappe.listview_settings["Gratuity"] = { + get_indicator: function(doc) { + let status_color = { + "Draft": "red", + "Submitted": "blue", + "Cancelled": "red", + "Paid": "green", + "Unpaid": "orange", + }; + return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; + } +}; \ No newline at end of file diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py index 098d71c8f80..67313feb5a2 100644 --- a/erpnext/payroll/doctype/gratuity/test_gratuity.py +++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py @@ -4,125 +4,155 @@ import unittest import frappe -from frappe.utils import add_days, flt, get_datetime, getdate +from frappe.tests.utils import FrappeTestCase +from frappe.utils import add_days, add_months, floor, flt, get_datetime, get_first_day, getdate from erpnext.hr.doctype.employee.test_employee import make_employee from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account +from erpnext.hr.doctype.holiday_list.test_holiday_list import set_holiday_list from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip from erpnext.payroll.doctype.salary_slip.test_salary_slip import ( make_deduction_salary_component, make_earning_salary_component, make_employee_salary_slip, + make_holiday_list, ) +from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule test_dependencies = ["Salary Component", "Salary Slip", "Account"] -class TestGratuity(unittest.TestCase): - @classmethod - def setUpClass(cls): - make_earning_salary_component(setup=True, test_tax=True, company_list=['_Test Company']) - make_deduction_salary_component(setup=True, test_tax=True, company_list=['_Test Company']) + +class TestGratuity(FrappeTestCase): def setUp(self): - frappe.db.sql("DELETE FROM `tabGratuity`") - frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") + frappe.db.delete("Gratuity") + frappe.db.delete("Salary Slip") + frappe.db.delete("Additional Salary", {"ref_doctype": "Gratuity"}) + make_earning_salary_component( + setup=True, test_tax=True, company_list=["_Test Company"], include_flexi_benefits=True + ) + make_deduction_salary_component(setup=True, test_tax=True, company_list=["_Test Company"]) + make_holiday_list() + + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") def test_get_last_salary_slip_should_return_none_for_new_employee(self): - new_employee = make_employee("new_employee@salary.com", company='_Test Company') + new_employee = make_employee("new_employee@salary.com", company="_Test Company") salary_slip = get_last_salary_slip(new_employee) - assert salary_slip is None + self.assertIsNone(salary_slip) - def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self): - employee, sal_slip = create_employee_and_get_last_salary_slip() + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_gratuity_based_on_current_slab_via_additional_salary(self): + """ + Range | Fraction + 5-0 | 1 + """ + doj = add_days(getdate(), -(6 * 365)) + relieving_date = getdate() + + employee = make_employee( + "test_employee_gratuity@salary.com", + company="_Test Company", + date_of_joining=doj, + relieving_date=relieving_date, + ) + sal_slip = create_salary_slip("test_employee_gratuity@salary.com") rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)") - gratuity = create_gratuity(pay_via_salary_slip = 1, employee=employee, rule=rule.name) + gratuity = create_gratuity(pay_via_salary_slip=1, employee=employee, rule=rule.name) - #work experience calculation - date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) - employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days - - experience = employee_total_workings_days/rule.total_working_days_per_year - gratuity.reload() - from math import floor - self.assertEqual(floor(experience), gratuity.current_work_experience) - - #amount Calculation - component_amount = frappe.get_all("Salary Detail", - filters={ - "docstatus": 1, - 'parent': sal_slip, - "parentfield": "earnings", - 'salary_component': "Basic Salary" - }, - fields=["amount"]) - - ''' 5 - 0 fraction is 1 ''' + # work experience calculation + employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days + experience = floor(employee_total_workings_days / rule.total_working_days_per_year) + self.assertEqual(gratuity.current_work_experience, experience) + # amount calculation + component_amount = frappe.get_all( + "Salary Detail", + filters={ + "docstatus": 1, + "parent": sal_slip, + "parentfield": "earnings", + "salary_component": "Basic Salary", + }, + fields=["amount"], + limit=1, + ) gratuity_amount = component_amount[0].amount * experience - gratuity.reload() - self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) - #additional salary creation (Pay via salary slip) + # additional salary creation (Pay via salary slip) self.assertTrue(frappe.db.exists("Additional Salary", {"ref_docname": gratuity.name})) - def test_check_gratuity_amount_based_on_all_previous_slabs(self): - employee, sal_slip = create_employee_and_get_last_salary_slip() + # gratuity should be marked "Paid" on the next salary slip submission + salary_slip = make_salary_slip("Test Gratuity", employee=employee) + salary_slip.posting_date = getdate() + salary_slip.insert() + salary_slip.submit() + + gratuity.reload() + self.assertEqual(gratuity.status, "Paid") + + @set_holiday_list("Salary Slip Test Holiday List", "_Test Company") + def test_gratuity_based_on_all_previous_slabs_via_payment_entry(self): + """ + Range | Fraction + 0-1 | 0 + 1-5 | 0.7 + 5-0 | 1 + """ + from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry + + doj = add_days(getdate(), -(6 * 365)) + relieving_date = getdate() + + employee = make_employee( + "test_employee_gratuity@salary.com", + company="_Test Company", + date_of_joining=doj, + relieving_date=relieving_date, + ) + + sal_slip = create_salary_slip("test_employee_gratuity@salary.com") rule = get_gratuity_rule("Rule Under Limited Contract (UAE)") set_mode_of_payment_account() - gratuity = create_gratuity(expense_account = 'Payment Account - _TC', mode_of_payment='Cash', employee=employee) + gratuity = create_gratuity( + expense_account="Payment Account - _TC", mode_of_payment="Cash", employee=employee + ) - #work experience calculation - date_of_joining, relieving_date = frappe.db.get_value('Employee', employee, ['date_of_joining', 'relieving_date']) - employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(date_of_joining)).days + # work experience calculation + employee_total_workings_days = (get_datetime(relieving_date) - get_datetime(doj)).days + experience = floor(employee_total_workings_days / rule.total_working_days_per_year) + self.assertEqual(gratuity.current_work_experience, experience) - experience = employee_total_workings_days/rule.total_working_days_per_year - - gratuity.reload() - - from math import floor - - self.assertEqual(floor(experience), gratuity.current_work_experience) - - #amount Calculation - component_amount = frappe.get_all("Salary Detail", - filters={ - "docstatus": 1, - 'parent': sal_slip, - "parentfield": "earnings", - 'salary_component': "Basic Salary" - }, - fields=["amount"]) - - ''' range | Fraction - 0-1 | 0 - 1-5 | 0.7 - 5-0 | 1 - ''' - - gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount - gratuity.reload() + # amount calculation + component_amount = frappe.get_all( + "Salary Detail", + filters={ + "docstatus": 1, + "parent": sal_slip, + "parentfield": "earnings", + "salary_component": "Basic Salary", + }, + fields=["amount"], + limit=1, + ) + gratuity_amount = ((0 * 1) + (4 * 0.7) + (1 * 1)) * component_amount[0].amount self.assertEqual(flt(gratuity_amount, 2), flt(gratuity.amount, 2)) self.assertEqual(gratuity.status, "Unpaid") - from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry - pay_entry = get_payment_entry("Gratuity", gratuity.name) - pay_entry.reference_no = "123467" - pay_entry.reference_date = getdate() - pay_entry.save() - pay_entry.submit() + pe = get_payment_entry("Gratuity", gratuity.name) + pe.reference_no = "123467" + pe.reference_date = getdate() + pe.submit() + gratuity.reload() - self.assertEqual(gratuity.status, "Paid") - self.assertEqual(flt(gratuity.paid_amount,2), flt(gratuity.amount, 2)) + self.assertEqual(flt(gratuity.paid_amount, 2), flt(gratuity.amount, 2)) - def tearDown(self): - frappe.db.sql("DELETE FROM `tabGratuity`") - frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'") def get_gratuity_rule(name): rule = frappe.db.exists("Gratuity Rule", name) @@ -130,14 +160,12 @@ def get_gratuity_rule(name): create_gratuity_rule() rule = frappe.get_doc("Gratuity Rule", name) rule.applicable_earnings_component = [] - rule.append("applicable_earnings_component", { - "salary_component": "Basic Salary" - }) + rule.append("applicable_earnings_component", {"salary_component": "Basic Salary"}) rule.save() - rule.reload() return rule + def create_gratuity(**args): if args: args = frappe._dict(args) @@ -150,15 +178,16 @@ def create_gratuity(**args): gratuity.payroll_date = getdate() gratuity.salary_component = "Performance Bonus" else: - gratuity.expense_account = args.expense_account or 'Payment Account - _TC' + gratuity.expense_account = args.expense_account or "Payment Account - _TC" gratuity.payable_account = args.payable_account or get_payable_account("_Test Company") - gratuity.mode_of_payment = args.mode_of_payment or 'Cash' + gratuity.mode_of_payment = args.mode_of_payment or "Cash" gratuity.save() gratuity.submit() return gratuity + def set_mode_of_payment_account(): if not frappe.db.exists("Account", "Payment Account - _TC"): mode_of_payment = create_account() @@ -166,14 +195,15 @@ def set_mode_of_payment_account(): mode_of_payment = frappe.get_doc("Mode of Payment", "Cash") mode_of_payment.accounts = [] - mode_of_payment.append("accounts", { - "company": "_Test Company", - "default_account": "_Test Bank - _TC" - }) + mode_of_payment.append( + "accounts", {"company": "_Test Company", "default_account": "_Test Bank - _TC"} + ) mode_of_payment.save() + def create_account(): - return frappe.get_doc({ + return frappe.get_doc( + { "doctype": "Account", "company": "_Test Company", "account_name": "Payment Account", @@ -182,22 +212,21 @@ def create_account(): "currency": "INR", "parent_account": "Bank Accounts - _TC", "account_type": "Bank", - }).insert(ignore_permissions=True) + } + ).insert(ignore_permissions=True) -def create_employee_and_get_last_salary_slip(): - employee = make_employee("test_employee@salary.com", company='_Test Company') - frappe.db.set_value("Employee", employee, "relieving_date", getdate()) - frappe.db.set_value("Employee", employee, "date_of_joining", add_days(getdate(), - (6*365))) - if not frappe.db.exists("Salary Slip", {"employee":employee}): - salary_slip = make_employee_salary_slip("test_employee@salary.com", "Monthly") + +def create_salary_slip(employee): + if not frappe.db.exists("Salary Slip", {"employee": employee}): + posting_date = get_first_day(add_months(getdate(), -1)) + salary_slip = make_employee_salary_slip( + employee, "Monthly", "Test Gratuity", posting_date=posting_date + ) + salary_slip.start_date = posting_date + salary_slip.end_date = None salary_slip.submit() salary_slip = salary_slip.name else: salary_slip = get_last_salary_slip(employee) - if not frappe.db.get_value("Employee", "test_employee@salary.com", "holiday_list"): - from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_holiday_list - make_holiday_list() - frappe.db.set_value("Company", '_Test Company', "default_holiday_list", "Salary Slip Test Holiday List") - - return employee, salary_slip + return salary_slip diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py index d30cfc64848..5cde79a1627 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py @@ -8,25 +8,34 @@ from frappe.model.document import Document class GratuityRule(Document): - def validate(self): for current_slab in self.gratuity_rule_slabs: if (current_slab.from_year > current_slab.to_year) and current_slab.to_year != 0: - frappe.throw(_("Row {0}: From (Year) can not be greater than To (Year)").format(current_slab.idx)) + frappe.throw( + _("Row {0}: From (Year) can not be greater than To (Year)").format(current_slab.idx) + ) + + if ( + current_slab.to_year == 0 and current_slab.from_year == 0 and len(self.gratuity_rule_slabs) > 1 + ): + frappe.throw( + _("You can not define multiple slabs if you have a slab with no lower and upper limits.") + ) - if current_slab.to_year == 0 and current_slab.from_year == 0 and len(self.gratuity_rule_slabs) > 1: - frappe.throw(_("You can not define multiple slabs if you have a slab with no lower and upper limits.")) def get_gratuity_rule(name, slabs, **args): args = frappe._dict(args) rule = frappe.new_doc("Gratuity Rule") rule.name = name - rule.calculate_gratuity_amount_based_on = args.calculate_gratuity_amount_based_on or "Current Slab" - rule.work_experience_calculation_method = args.work_experience_calculation_method or "Take Exact Completed Years" + rule.calculate_gratuity_amount_based_on = ( + args.calculate_gratuity_amount_based_on or "Current Slab" + ) + rule.work_experience_calculation_method = ( + args.work_experience_calculation_method or "Take Exact Completed Years" + ) rule.minimum_year_for_gratuity = 1 - for slab in slabs: slab = frappe._dict(slab) rule.append("gratuity_rule_slabs", slab) diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py index 15e15d13620..fa5a9dedd35 100644 --- a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py +++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py @@ -1,14 +1,8 @@ - from frappe import _ def get_data(): return { - 'fieldname': 'gratuity_rule', - 'transactions': [ - { - 'label': _('Gratuity'), - 'items': ['Gratuity'] - } - ] + "fieldname": "gratuity_rule", + "transactions": [{"label": _("Gratuity"), "items": ["Gratuity"]}], } diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py index 040b2c89353..e62d61f4c2f 100644 --- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py +++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py @@ -4,7 +4,7 @@ from frappe.model.document import Document -#import frappe +# import frappe import erpnext diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py index 84c59a2c2b8..5f2af74dca6 100644 --- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py +++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py @@ -15,6 +15,7 @@ from frappe.utils import ( comma_and, date_diff, flt, + get_link_to_form, getdate, ) @@ -28,11 +29,11 @@ from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee class PayrollEntry(Document): def onload(self): - if not self.docstatus==1 or self.salary_slips_submitted: + if not self.docstatus == 1 or self.salary_slips_submitted: return # check if salary slips were manually submitted - entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name']) + entries = frappe.db.count("Salary Slip", {"payroll_entry": self.name, "docstatus": 1}, ["name"]) if cint(entries) == len(self.employees): self.set_onload("submitted_ss", True) @@ -44,6 +45,7 @@ class PayrollEntry(Document): def before_submit(self): self.validate_employee_details() + self.validate_payroll_payable_account() if self.validate_attendance: if self.validate_employee_attendance(): frappe.throw(_("Cannot Submit, Employees left to mark attendance")) @@ -51,31 +53,59 @@ class PayrollEntry(Document): def validate_employee_details(self): emp_with_sal_slip = [] for employee_details in self.employees: - if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}): + if frappe.db.exists( + "Salary Slip", + { + "employee": employee_details.employee, + "start_date": self.start_date, + "end_date": self.end_date, + "docstatus": 1, + }, + ): emp_with_sal_slip.append(employee_details.employee) if len(emp_with_sal_slip): frappe.throw(_("Salary Slip already exists for {0}").format(comma_and(emp_with_sal_slip))) + def validate_payroll_payable_account(self): + if frappe.db.get_value("Account", self.payroll_payable_account, "account_type"): + frappe.throw( + _( + "Account type cannot be set for payroll payable account {0}, please remove and try again" + ).format(frappe.bold(get_link_to_form("Account", self.payroll_payable_account))) + ) + def on_cancel(self): - frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip` - where payroll_entry=%s """, (self.name))) + frappe.delete_doc( + "Salary Slip", + frappe.db.sql_list( + """select name from `tabSalary Slip` + where payroll_entry=%s """, + (self.name), + ), + ) + self.db_set("salary_slips_created", 0) + self.db_set("salary_slips_submitted", 0) def get_emp_list(self): """ - Returns list of active employees based on selected criteria - and for which salary structure exists + Returns list of active employees based on selected criteria + and for which salary structure exists """ self.check_mandatory() filters = self.make_filters() cond = get_filter_condition(filters) cond += get_joining_relieving_condition(self.start_date, self.end_date) - condition = '' + condition = "" if self.payroll_frequency: - condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": self.payroll_frequency} + condition = """and payroll_frequency = '%(payroll_frequency)s'""" % { + "payroll_frequency": self.payroll_frequency + } - sal_struct = get_sal_struct(self.company, self.currency, self.salary_slip_based_on_timesheet, condition) + sal_struct = get_sal_struct( + self.company, self.currency, self.salary_slip_based_on_timesheet, condition + ) if sal_struct: cond += "and t2.salary_structure IN %(sal_struct)s " cond += "and t2.payroll_payable_account = %(payroll_payable_account)s " @@ -86,20 +116,25 @@ class PayrollEntry(Document): def make_filters(self): filters = frappe._dict() - filters['company'] = self.company - filters['branch'] = self.branch - filters['department'] = self.department - filters['designation'] = self.designation + filters["company"] = self.company + filters["branch"] = self.branch + filters["department"] = self.department + filters["designation"] = self.designation return filters @frappe.whitelist() def fill_employee_details(self): - self.set('employees', []) + self.set("employees", []) employees = self.get_emp_list() if not employees: - error_msg = _("No employees found for the mentioned criteria:Project Name: " + project_name + "
Frequency: " + " " + frequency + "
Update Reminder:" + " " + str(date_start) + "
Expected Date End:" + " " + str(date_end) + "
Percent Progress:" + " " + str(progress) + "
Number of Updates:" + " " + str(len(update)) + "
" + "Number of drafts:" + " " + str(number_of_drafts) + "
" - msg += """| Project ID | Date Updated | Time Updated | Project Status | Notes | """ - for updates in update: - msg += "
|---|---|---|---|---|
| " + str(updates[0]) + " | " + str(updates[1]) + " | " + str(updates[2]) + " | " + str(updates[3]) + " | " + "" + str(updates[4]) + " |
| " + + str(updates[0]) + + " | " + + str(updates[1]) + + " | " + + str(updates[2]) + + " | " + + str(updates[3]) + + " | " + + "" + + str(updates[4]) + + " |
| {0} | {1} | -
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
Drawing No. 07-xxx-PO132
1800 x 1685 x 750
All parts made of Marine Ply
Top w/ Corian dd
CO, CS, VIP Day Cabin
{{ supplier_salutation if supplier_salutation else ''}} {{ supplier_name }},
{{ message }}
-{{_("The Request for Quotation can be accessed by clicking on the following button")}}:
-- -
{{_("Regards")}},
-{{ user_fullname }}
{{_("Please click on the following button to set your new password")}}:
-- -
- + + {{_("Set Password") }} + +
+ {{_("Regards")}},
+ {{ user_fullname }}
+