fix: resolved conflicts
diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..399b176
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,32 @@
+[flake8]
+ignore =
+ E121,
+ E126,
+ E127,
+ E128,
+ E203,
+ E225,
+ E226,
+ E231,
+ E241,
+ E251,
+ E261,
+ E265,
+ E302,
+ E303,
+ E305,
+ E402,
+ E501,
+ E741,
+ W291,
+ W292,
+ W293,
+ W391,
+ W503,
+ W504,
+ F403,
+ B007,
+ B950,
+ W191,
+
+max-line-length = 200
\ No newline at end of file
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
new file mode 100644
index 0000000..7b0f944
--- /dev/null
+++ b/.github/helper/install.sh
@@ -0,0 +1,46 @@
+#!/bin/bash
+
+set -e
+
+cd ~ || exit
+
+sudo apt-get install redis-server
+
+sudo apt install nodejs
+
+sudo apt install npm
+
+pip install frappe-bench
+
+git clone https://github.com/frappe/frappe --branch "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}" --depth 1
+bench init --skip-assets --frappe-path ~/frappe --python "$(which python)" frappe-bench
+
+mkdir ~/frappe-bench/sites/test_site
+cp -r "${GITHUB_WORKSPACE}/.github/helper/site_config.json" ~/frappe-bench/sites/test_site/
+
+mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
+mysql --host 127.0.0.1 --port 3306 -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
+
+mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
+mysql --host 127.0.0.1 --port 3306 -u root -e "CREATE DATABASE test_frappe"
+mysql --host 127.0.0.1 --port 3306 -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
+
+mysql --host 127.0.0.1 --port 3306 -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
+mysql --host 127.0.0.1 --port 3306 -u root -e "FLUSH PRIVILEGES"
+
+wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
+tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
+sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
+sudo chmod o+x /usr/local/bin/wkhtmltopdf
+sudo apt-get install libcups2-dev
+
+cd ~/frappe-bench || exit
+
+sed -i 's/watch:/# watch:/g' Procfile
+sed -i 's/schedule:/# schedule:/g' Procfile
+sed -i 's/socketio:/# socketio:/g' Procfile
+sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
+
+bench get-app erpnext "${GITHUB_WORKSPACE}"
+bench start &
+bench --site test_site reinstall --yes
diff --git a/.travis/site_config.json b/.github/helper/site_config.json
similarity index 89%
rename from .travis/site_config.json
rename to .github/helper/site_config.json
index 572bbd0..60ef80c 100644
--- a/.travis/site_config.json
+++ b/.github/helper/site_config.json
@@ -1,4 +1,6 @@
{
+ "db_host": "127.0.0.1",
+ "db_port": 3306,
"db_name": "test_frappe",
"db_password": "test_frappe",
"auto_email_id": "test@example.com",
diff --git a/.github/workflows/ci-tests.yml b/.github/workflows/ci-tests.yml
new file mode 100644
index 0000000..78c2f5a
--- /dev/null
+++ b/.github/workflows/ci-tests.yml
@@ -0,0 +1,94 @@
+name: CI
+
+on: [pull_request, workflow_dispatch, push]
+
+jobs:
+ test:
+ runs-on: ubuntu-18.04
+
+ strategy:
+ fail-fast: false
+
+ matrix:
+ include:
+ - TYPE: "server"
+ JOB_NAME: "Server"
+ RUN_COMMAND: cd ~/frappe-bench/ && bench --site test_site run-tests --app erpnext --coverage
+ - TYPE: "patch"
+ JOB_NAME: "Patch"
+ RUN_COMMAND: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate
+
+ name: ${{ matrix.JOB_NAME }}
+
+ 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.6
+
+ - 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: Install
+ run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+
+ - name: Run Tests
+ run: ${{ matrix.RUN_COMMAND }}
+ env:
+ TYPE: ${{ matrix.TYPE }}
+
+ - name: Coverage
+ if: matrix.TYPE == 'server'
+ run: |
+ cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
+ cd ${GITHUB_WORKSPACE}
+ pip install coveralls==2.2.0
+ pip install coverage==4.5.4
+ coveralls
+ env:
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
+
diff --git a/.travis.yml b/.travis.yml
deleted file mode 100644
index 77d427e..0000000
--- a/.travis.yml
+++ /dev/null
@@ -1,69 +0,0 @@
-language: python
-dist: trusty
-
-git:
- depth: 1
-
-cache:
- - pip
-
-addons:
- hosts: test_site
- mariadb: 10.3
-
-jobs:
- include:
- - name: "Python 3.6 Server Side Test"
- python: 3.6
- script: bench --site test_site run-tests --app erpnext --coverage
-
- - name: "Python 3.6 Patch Test"
- python: 3.6
- before_script:
- - wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz
- - bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz
- script: bench --site test_site migrate
-
-install:
- - cd ~
- - nvm install 10
-
- - pip install frappe-bench
-
- - git clone https://github.com/frappe/frappe --branch $TRAVIS_BRANCH --depth 1
- - bench init --skip-assets --frappe-path ~/frappe --python $(which python) frappe-bench
-
- - mkdir ~/frappe-bench/sites/test_site
- - cp -r $TRAVIS_BUILD_DIR/.travis/site_config.json ~/frappe-bench/sites/test_site/
-
- - mysql -u root -e "SET GLOBAL character_set_server = 'utf8mb4'"
- - mysql -u root -e "SET GLOBAL collation_server = 'utf8mb4_unicode_ci'"
-
- - mysql -u root -e "CREATE DATABASE test_frappe"
- - mysql -u root -e "CREATE USER 'test_frappe'@'localhost' IDENTIFIED BY 'test_frappe'"
- - mysql -u root -e "GRANT ALL PRIVILEGES ON \`test_frappe\`.* TO 'test_frappe'@'localhost'"
-
- - mysql -u root -e "UPDATE mysql.user SET Password=PASSWORD('travis') WHERE User='root'"
- - mysql -u root -e "FLUSH PRIVILEGES"
-
- - wget -O /tmp/wkhtmltox.tar.xz https://github.com/frappe/wkhtmltopdf/raw/master/wkhtmltox-0.12.3_linux-generic-amd64.tar.xz
- - tar -xf /tmp/wkhtmltox.tar.xz -C /tmp
- - sudo mv /tmp/wkhtmltox/bin/wkhtmltopdf /usr/local/bin/wkhtmltopdf
- - sudo chmod o+x /usr/local/bin/wkhtmltopdf
- - sudo apt-get install libcups2-dev
-
- - cd ~/frappe-bench
-
- - sed -i 's/watch:/# watch:/g' Procfile
- - sed -i 's/schedule:/# schedule:/g' Procfile
- - sed -i 's/socketio:/# socketio:/g' Procfile
- - sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
-
- - bench get-app erpnext $TRAVIS_BUILD_DIR
- - bench start &
- - bench --site test_site reinstall --yes
-
-after_script:
- - pip install coverage==4.5.4
- - pip install python-coveralls
- - coveralls -b apps/erpnext -d ../../sites/.coverage
diff --git a/README.md b/README.md
index 15782a2..bb592ae 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
<p>ERP made simple</p>
</p>
-[](https://travis-ci.com/frappe/erpnext)
+[](https://github.com/frappe/erpnext/actions/workflows/ci-tests.yml)
[](https://www.codetriage.com/frappe/erpnext)
[](https://coveralls.io/github/frappe/erpnext?branch=develop)
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 5a5c448..199a183 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -109,7 +109,7 @@
'''
if company or frappe.flags.company:
return frappe.get_cached_value('Company',
- company or frappe.flags.company, 'country')
+ company or frappe.flags.company, 'country')
elif frappe.flags.country:
return frappe.flags.country
else:
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index c801cfc..0606823 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -214,6 +214,7 @@
if parent_value_changed:
doc.save()
+ @frappe.whitelist()
def convert_group_to_ledger(self):
if self.check_if_child_exists():
throw(_("Account with child nodes cannot be converted to ledger"))
@@ -224,6 +225,7 @@
self.save()
return 1
+ @frappe.whitelist()
def convert_ledger_to_group(self):
if self.check_gle_exists():
throw(_("Account with existing transaction can not be converted to group."))
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json
index 89465ee..ee501f6 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR03_gnucash.json
@@ -63,17 +63,21 @@
"Gewinnermittlung \u00a74/3 nicht Ergebniswirksam": {
"account_number": "1371"
},
- "Abziehbare VSt. 7%": {
- "account_number": "1571"
- },
- "Abziehbare VSt. 19%": {
- "account_number": "1576"
- },
- "Abziehbare VStr. nach \u00a713b UStG 19%": {
- "account_number": "1577"
- },
- "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
- "account_number": "3120"
+ "Abziehbare Vorsteuer": {
+ "account_type": "Tax",
+ "is_group": 1,
+ "Abziehbare Vorsteuer 7%": {
+ "account_number": "1571"
+ },
+ "Abziehbare Vorsteuer 19%": {
+ "account_number": "1576"
+ },
+ "Abziehbare Vorsteuer nach \u00a713b UStG 19%": {
+ "account_number": "1577"
+ },
+ "Leistungen \u00a713b UStG 19% Vorsteuer, 19% Umsatzsteuer": {
+ "account_number": "3120"
+ }
}
},
"III. Wertpapiere": {
@@ -196,6 +200,7 @@
},
"Umsatzsteuer": {
"is_group": 1,
+ "account_type": "Tax",
"Umsatzsteuer 7%": {
"account_number": "1771"
},
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json
index 7fa6708..57e8bdd 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04.json
@@ -292,18 +292,21 @@
"Umsatzsteuerforderungen fr\u00fchere Jahre": {}
},
"Sonstige Verm\u00f6gensgegenst\u00e4nde oder sonstige Verbindlichkeiten": {
- "Abziehbare Vorsteuer": {},
- "Abziehbare Vorsteuer 16%": {},
- "Abziehbare Vorsteuer 19%": {},
- "Abziehbare Vorsteuer 7%": {},
- "Abziehbare Vorsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Unsatzsteuerlager": {},
- "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb": {},
- "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 16%": {},
- "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19%": {},
- "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb von Neufahrzeugen von Lieferanten ohne Ust-Identifikationsnummer": {},
- "Abziehbare Vorsteuer nach \u00a7 13b UStG ": {},
- "Abziehbare Vorsteuer nach \u00a7 13b UStG 16%": {},
- "Abziehbare Vorsteuer nach \u00a7 13b UStG 19%": {},
+ "Abziehbare Vorsteuer": {
+ "account_type": "Tax",
+ "is_group": 1,
+ "Abziehbare Vorsteuer 16%": {},
+ "Abziehbare Vorsteuer 19%": {},
+ "Abziehbare Vorsteuer 7%": {},
+ "Abziehbare Vorsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Unsatzsteuerlager": {},
+ "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb": {},
+ "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 16%": {},
+ "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb 19%": {},
+ "Abziehbare Vorsteuer aus innergemeinschaftlichem Erwerb von Neufahrzeugen von Lieferanten ohne Ust-Identifikationsnummer": {},
+ "Abziehbare Vorsteuer nach \u00a7 13b UStG ": {},
+ "Abziehbare Vorsteuer nach \u00a7 13b UStG 16%": {},
+ "Abziehbare Vorsteuer nach \u00a7 13b UStG 19%": {}
+ },
"Aufl\u00f6sung Vorsteuer aus Vorjahr \u00a7 4/3 EStG": {},
"Aufzuteilende Vorsteuer": {},
"Aufzuteilende Vorsteuer 16%": {},
@@ -673,23 +676,26 @@
"Sonstige Verrechnungskonten (Interimskonto)": {
"account_type": "Stock Received But Not Billed"
},
- "Umsatzsteuer": {},
- "Umsatzsteuer 16%": {},
- "Umsatzsteuer 19%": {},
- "Umsatzsteuer 7%": {},
- "Umsatzsteuer Vorjahr": {},
- "Umsatzsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Umsatzsteuerlager": {},
- "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen": {},
- "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen 19%": {},
- "Umsatzsteuer aus innergemeinschaftlichem Erwerb ": {},
- "Umsatzsteuer aus innergemeinschaftlichem Erwerb 16%": {},
- "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19%": {},
- "Umsatzsteuer aus innergemeinschaftlichem Erwerb ohne Vorsteuerabzug": {},
- "Umsatzsteuer fr\u00fchere Jahre": {},
- "Umsatzsteuer laufendes Jahr": {},
- "Umsatzsteuer nach \u00a713b UStG": {},
- "Umsatzsteuer nach \u00a713b UStG 16%": {},
- "Umsatzsteuer nach \u00a713b UStG 19%": {},
+ "Umsatzsteuer": {
+ "account_type": "Tax",
+ "is_group": 1,
+ "Umsatzsteuer 16%": {},
+ "Umsatzsteuer 19%": {},
+ "Umsatzsteuer 7%": {},
+ "Umsatzsteuer Vorjahr": {},
+ "Umsatzsteuer aus der Auslagerung von Gegenst\u00e4nden aus einem Umsatzsteuerlager": {},
+ "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen": {},
+ "Umsatzsteuer aus im Inland steuerpflichtigen EG-Lieferungen 19%": {},
+ "Umsatzsteuer aus innergemeinschaftlichem Erwerb ": {},
+ "Umsatzsteuer aus innergemeinschaftlichem Erwerb 16%": {},
+ "Umsatzsteuer aus innergemeinschaftlichem Erwerb 19%": {},
+ "Umsatzsteuer aus innergemeinschaftlichem Erwerb ohne Vorsteuerabzug": {},
+ "Umsatzsteuer fr\u00fchere Jahre": {},
+ "Umsatzsteuer laufendes Jahr": {},
+ "Umsatzsteuer nach \u00a713b UStG": {},
+ "Umsatzsteuer nach \u00a713b UStG 16%": {},
+ "Umsatzsteuer nach \u00a713b UStG 19%": {}
+ },
"Umsatzsteuer- Vorauszahlungen": {},
"Umsatzsteuer- Vorauszahlungen 1/11": {},
"Verbindlichkeiten aus Lohn- und Kirchensteuer": {}
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json
index 849df18..2bf55cf 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json
@@ -659,6 +659,7 @@
},
"Abziehbare Vorsteuer (Gruppe)": {
"is_group": 1,
+ "account_type": "Tax",
"Abziehbare Vorsteuer": {
"account_number": "1400"
},
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index dd26c4c..0ebf0eb 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -43,11 +43,11 @@
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:
- frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self)
+ 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)
+ delete_accounting_dimension(doc=self, queue='long')
else:
frappe.enqueue(delete_accounting_dimension, doc=self)
@@ -58,8 +58,13 @@
if not self.fieldname:
self.fieldname = scrub(self.label)
-def make_dimension_in_accounting_doctypes(doc):
- doclist = get_doctypes_with_dimensions()
+ 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()
+
doc_count = len(get_accounting_dimensions())
count = 0
@@ -79,13 +84,13 @@
"owner": "Administrator"
}
- if doctype == "Budget":
- add_dimension_to_budget_doctype(df, doc)
- else:
- meta = frappe.get_meta(doctype, cached=False)
- fieldnames = [d.fieldname for d in meta.get("fields")]
+ 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)
count += 1
@@ -175,23 +180,17 @@
frappe.clear_cache(doctype=doctype)
def get_doctypes_with_dimensions():
- doclist = ["GL Entry", "Sales Invoice", "POS Invoice", "Purchase Invoice", "Payment Entry", "Asset",
- "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note",
- "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item",
- "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
- "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
- "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
- "Subscription Plan"]
-
- return doclist
+ return frappe.get_hooks("accounting_dimension_doctypes")
def get_accounting_dimensions(as_list=True):
- accounting_dimensions = frappe.get_all("Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"])
+ if frappe.flags.accounting_dimensions is None:
+ frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension",
+ fields=["label", "fieldname", "disabled", "document_type"])
if as_list:
- return [d.fieldname for d in accounting_dimensions]
+ return [d.fieldname for d in frappe.flags.accounting_dimensions]
else:
- return accounting_dimensions
+ 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
diff --git a/erpnext/accounts/doctype/accounting_period/accounting_period.py b/erpnext/accounts/doctype/accounting_period/accounting_period.py
index df6cedd..63b5dbb 100644
--- a/erpnext/accounts/doctype/accounting_period/accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/accounting_period.py
@@ -39,6 +39,7 @@
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", \
diff --git a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
index 022d7a7..10cd939 100644
--- a/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
+++ b/erpnext/accounts/doctype/accounting_period/test_accounting_period.py
@@ -11,36 +11,36 @@
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
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.save()
+ def test_overlap(self):
+ 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")
- self.assertRaises(OverlapError, ap2.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")
+ self.assertRaises(OverlapError, ap2.save)
- def test_accounting_period(self):
- ap1 = create_accounting_period(period_name = "Test Accounting Period 2")
- ap1.save()
+ def test_accounting_period(self):
+ 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")
- self.assertRaises(ClosedAccountingPeriod, doc.submit)
+ 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 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)
+ args = frappe._dict(args)
- accounting_period = frappe.new_doc("Accounting Period")
- accounting_period.start_date = args.start_date or nowdate()
- 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 = frappe.new_doc("Accounting Period")
+ accounting_period.start_date = args.start_date or nowdate()
+ 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
+ })
- return accounting_period
\ No newline at end of file
+ return accounting_period
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json
index b42f1f9..de67ab1 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.json
+++ b/erpnext/accounts/doctype/bank_account/bank_account.json
@@ -86,6 +86,7 @@
},
{
"default": "0",
+ "description": "Setting the account as a Company Account is necessary for Bank Reconciliation",
"fieldname": "is_company_account",
"fieldtype": "Check",
"label": "Is Company Account"
@@ -207,7 +208,7 @@
}
],
"links": [],
- "modified": "2020-07-17 13:59:50.795412",
+ "modified": "2020-10-23 16:48:06.303658",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
diff --git a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
index 76d82e7..79f5596 100644
--- a/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
+++ b/erpnext/accounts/doctype/bank_clearance/bank_clearance.py
@@ -12,6 +12,7 @@
}
class BankClearance(Document):
+ @frappe.whitelist()
def get_payment_entries(self):
if not (self.from_date and self.to_date):
frappe.throw(_("From Date and To Date are Mandatory"))
@@ -108,6 +109,7 @@
row.update(d)
self.total_amount += flt(amount)
+ @frappe.whitelist()
def update_clearance_date(self):
clearance_date_updated = False
for d in self.get('payment_entries'):
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/accounts/doctype/bank_reconciliation_tool/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/accounts/doctype/bank_reconciliation_tool/__init__.py
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
new file mode 100644
index 0000000..10f660a
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -0,0 +1,163 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+frappe.ui.form.on("Bank Reconciliation Tool", {
+ setup: function (frm) {
+ frm.set_query("bank_account", function () {
+ return {
+ filters: {
+ company: ["in", frm.doc.company],
+ 'is_company_account': 1
+ },
+ };
+ });
+ },
+
+ refresh: function (frm) {
+ frappe.require("assets/js/bank-reconciliation-tool.min.js", () =>
+ frm.trigger("make_reconciliation_tool")
+ );
+ frm.upload_statement_button = frm.page.set_secondary_action(
+ __("Upload Bank Statement"),
+ () =>
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement",
+ args: {
+ dt: frm.doc.doctype,
+ dn: frm.doc.name,
+ company: frm.doc.company,
+ bank_account: frm.doc.bank_account,
+ },
+ callback: function (r) {
+ if (!r.exc) {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route(
+ "Form",
+ doc[0].doctype,
+ doc[0].name
+ );
+ }
+ },
+ })
+ );
+ },
+
+ after_save: function (frm) {
+ frm.trigger("make_reconciliation_tool");
+ },
+
+ bank_account: function (frm) {
+ frappe.db.get_value(
+ "Bank Account",
+ frm.bank_account,
+ "account",
+ (r) => {
+ frappe.db.get_value(
+ "Account",
+ r.account,
+ "account_currency",
+ (r) => {
+ frm.currency = r.account_currency;
+ }
+ );
+ }
+ );
+ frm.trigger("get_account_opening_balance");
+ },
+
+ bank_statement_from_date: function (frm) {
+ frm.trigger("get_account_opening_balance");
+ },
+
+ make_reconciliation_tool(frm) {
+ frm.get_field("reconciliation_tool_cards").$wrapper.empty();
+ if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
+ frm.trigger("get_cleared_balance").then(() => {
+ if (
+ frm.doc.bank_account &&
+ frm.doc.bank_statement_from_date &&
+ frm.doc.bank_statement_to_date &&
+ frm.doc.bank_statement_closing_balance
+ ) {
+ frm.trigger("render_chart");
+ frm.trigger("render");
+ frappe.utils.scroll_to(
+ frm.get_field("reconciliation_tool_cards").$wrapper,
+ true,
+ 30
+ );
+ }
+ });
+ }
+ },
+
+ get_account_opening_balance(frm) {
+ if (frm.doc.bank_account && frm.doc.bank_statement_from_date) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
+ args: {
+ bank_account: frm.doc.bank_account,
+ till_date: frm.doc.bank_statement_from_date,
+ },
+ callback: (response) => {
+ frm.set_value("account_opening_balance", response.message);
+ },
+ });
+ }
+ },
+
+ get_cleared_balance(frm) {
+ if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
+ return frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
+ args: {
+ bank_account: frm.doc.bank_account,
+ till_date: frm.doc.bank_statement_to_date,
+ },
+ callback: (response) => {
+ frm.cleared_balance = response.message;
+ },
+ });
+ }
+ },
+
+ render_chart(frm) {
+ frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
+ {
+ $reconciliation_tool_cards: frm.get_field(
+ "reconciliation_tool_cards"
+ ).$wrapper,
+ bank_statement_closing_balance:
+ frm.doc.bank_statement_closing_balance,
+ cleared_balance: frm.cleared_balance,
+ currency: frm.currency,
+ }
+ );
+ },
+
+ render(frm) {
+ if (frm.doc.bank_account) {
+ frm.bank_reconciliation_data_table_manager = new erpnext.accounts.bank_reconciliation.DataTableManager(
+ {
+ company: frm.doc.company,
+ bank_account: frm.doc.bank_account,
+ $reconciliation_tool_dt: frm.get_field(
+ "reconciliation_tool_dt"
+ ).$wrapper,
+ $no_bank_transactions: frm.get_field(
+ "no_bank_transactions"
+ ).$wrapper,
+ bank_statement_from_date: frm.doc.bank_statement_from_date,
+ bank_statement_to_date: frm.doc.bank_statement_to_date,
+ bank_statement_closing_balance:
+ frm.doc.bank_statement_closing_balance,
+ cards_manager: frm.cards_manager,
+ }
+ );
+ }
+ },
+});
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json
new file mode 100644
index 0000000..4837db3
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json
@@ -0,0 +1,113 @@
+{
+ "actions": [],
+ "creation": "2020-12-02 10:13:02.148040",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "bank_account",
+ "column_break_1",
+ "bank_statement_from_date",
+ "bank_statement_to_date",
+ "column_break_2",
+ "account_opening_balance",
+ "bank_statement_closing_balance",
+ "section_break_1",
+ "reconciliation_tool_cards",
+ "reconciliation_tool_dt",
+ "no_bank_transactions"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "bank_account",
+ "fieldtype": "Link",
+ "label": "Bank Account",
+ "options": "Bank Account"
+ },
+ {
+ "fieldname": "column_break_1",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: doc.bank_account",
+ "fieldname": "bank_statement_from_date",
+ "fieldtype": "Date",
+ "label": "Bank Statement From Date"
+ },
+ {
+ "depends_on": "eval: doc.bank_statement_from_date",
+ "fieldname": "bank_statement_to_date",
+ "fieldtype": "Date",
+ "label": "Bank Statement To Date"
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: doc.bank_statement_from_date",
+ "fieldname": "account_opening_balance",
+ "fieldtype": "Currency",
+ "label": "Account Opening Balance",
+ "options": "Currency",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.bank_statement_to_date",
+ "fieldname": "bank_statement_closing_balance",
+ "fieldtype": "Currency",
+ "label": "Bank Statement Closing Balance",
+ "options": "Currency"
+ },
+ {
+ "depends_on": "eval: doc.bank_statement_closing_balance",
+ "fieldname": "section_break_1",
+ "fieldtype": "Section Break",
+ "label": "Reconcile"
+ },
+ {
+ "fieldname": "reconciliation_tool_cards",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "reconciliation_tool_dt",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "no_bank_transactions",
+ "fieldtype": "HTML",
+ "options": "<div class=\"text-muted text-center\">No Matching Bank Transactions Found</div>"
+ }
+ ],
+ "hide_toolbar": 1,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-02-02 01:35:53.043578",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Bank Reconciliation Tool",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
new file mode 100644
index 0000000..8a17233
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -0,0 +1,452 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import json
+
+import frappe
+from frappe.model.document import Document
+from frappe import _
+from frappe.utils import flt
+
+from erpnext import get_company_currency
+from erpnext.accounts.utils import get_balance_on
+from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import get_entries, get_amounts_not_reflected_in_system
+from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
+
+
+class BankReconciliationTool(Document):
+ pass
+
+@frappe.whitelist()
+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])
+ if to_date:
+ filters.append(['date', '<=', to_date])
+ if 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
+ )
+ 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
+ })
+ data = get_entries(filters)
+
+ balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
+
+ 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) \
+ + amounts_not_reflected_in_system
+
+ return bank_bal
+
+
+@frappe.whitelist()
+def update_bank_transaction(bank_transaction_name, reference_number, party_type=None, party=None):
+ # updates bank transaction based on the new parameters provided by the user from Vouchers
+ bank_transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
+ bank_transaction.reference_number = reference_number
+ 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'],
+ )[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):
+ # 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
+ )[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))
+ accounts = []
+ # Multi Currency?
+ 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,
+ })
+
+ 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,
+ })
+
+ 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
+ }
+ journal_entry = frappe.new_doc('Journal Entry')
+ journal_entry.update(journal_entry_dict)
+ journal_entry.set("accounts", accounts)
+
+
+ if allow_edit:
+ return journal_entry
+
+ journal_entry.insert()
+ journal_entry.submit()
+
+ if bank_transaction.deposit > 0:
+ paid_amount = bank_transaction.deposit
+ else:
+ paid_amount = bank_transaction.withdrawal
+
+ vouchers = json.dumps([{
+ "payment_doctype":"Journal Entry",
+ "payment_name":journal_entry.name,
+ "amount":paid_amount}])
+
+ 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):
+ # 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
+ )[0]
+ paid_amount = bank_transaction.unallocated_amount
+ payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
+
+ 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,
+ "paid_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
+ if project:
+ payment_entry.project = project
+ if cost_center:
+ payment_entry.cost_center = cost_center
+ if payment_type == "Receive":
+ payment_entry.paid_to = company_account
+ else:
+ payment_entry.paid_from = company_account
+
+ payment_entry.validate()
+
+ if allow_edit:
+ return payment_entry
+
+ payment_entry.insert()
+
+ payment_entry.submit()
+ 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)
+ 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)
+
+ if total_amount > transaction.unallocated_amount:
+ frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
+ account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
+
+ for voucher in vouchers:
+ gl_entry = frappe.db.get_value("GL Entry", dict(account=account, voucher_type=voucher['payment_doctype'], voucher_no=voucher['payment_name']), ['credit', 'debit'], as_dict=1)
+ gl_amount, transaction_amount = (gl_entry.credit, transaction.deposit) if gl_entry.credit > 0 else (gl_entry.debit, transaction.withdrawal)
+ allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
+
+ transaction.append("payment_entries", {
+ "payment_document": voucher['payment_entry'].doctype,
+ "payment_entry": voucher['payment_entry'].name,
+ "allocated_amount": allocated_amount
+ })
+
+ transaction.save()
+ transaction.update_allocations()
+ return frappe.get_doc("Bank Transaction", bank_transaction_name)
+
+@frappe.whitelist()
+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]
+ (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
+ 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
+ }
+
+ matching_vouchers = []
+ for query in subquery:
+ matching_vouchers.extend(
+ frappe.db.sql(query, filters,)
+ )
+
+ return sorted(matching_vouchers, key = lambda x: x[0], reverse=True) if matching_vouchers else []
+
+def get_queries(bank_account, company, transaction, document_types):
+ # get queries to get matching vouchers
+ amount_condition = "=" if "exact_match" in document_types else "<="
+ account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
+ queries = []
+
+ if "payment_entry" in document_types:
+ pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction)
+ queries.extend([pe_amount_matching])
+
+ if "journal_entry" in document_types:
+ je_amount_matching = get_je_matching_query(amount_condition, transaction)
+ queries.extend([je_amount_matching])
+
+ if transaction.deposit > 0 and "sales_invoice" in document_types:
+ si_amount_matching = get_si_matching_query(amount_condition)
+ queries.extend([si_amount_matching])
+
+ if transaction.withdrawal > 0:
+ if "purchase_invoice" in document_types:
+ pi_amount_matching = get_pi_matching_query(amount_condition)
+ queries.extend([pi_amount_matching])
+
+ if "expense_claim" in document_types:
+ ec_amount_matching = get_ec_matching_query(bank_account, company, amount_condition)
+ queries.extend([ec_amount_matching])
+
+ return queries
+
+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"""
+ SELECT
+ (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
+ + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Payment Entry' as doctype,
+ name,
+ paid_amount,
+ reference_no,
+ reference_date,
+ party,
+ party_type,
+ posting_date,
+ {currency_field}
+ FROM
+ `tabPayment Entry`
+ WHERE
+ paid_amount {amount_condition} %(amount)s
+ AND docstatus = 1
+ AND payment_type IN (%(payment_type)s, 'Internal Transfer')
+ AND ifnull(clearance_date, '') = ""
+ AND {account_from_to} = %(bank_account)s
+ """
+
+
+def get_je_matching_query(amount_condition, transaction):
+ # get matching journal entry query
+ cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
+ return f"""
+
+ SELECT
+ (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
+ + 1) AS rank ,
+ 'Journal Entry' as doctype,
+ je.name,
+ jea.{cr_or_dr}_in_account_currency as paid_amount,
+ je.cheque_no as reference_no,
+ je.cheque_date as reference_date,
+ je.pay_to_recd_from as party,
+ jea.party_type,
+ je.posting_date,
+ jea.account_currency as currency
+ FROM
+ `tabJournal Entry Account` as jea
+ JOIN
+ `tabJournal Entry` as je
+ ON
+ jea.parent = je.name
+ WHERE
+ (je.clearance_date is null or je.clearance_date='0000-00-00')
+ AND jea.account = %(bank_account)s
+ AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
+ AND je.docstatus = 1
+ """
+
+
+def get_si_matching_query(amount_condition):
+ # get matchin sales invoice query
+ return f"""
+ SELECT
+ ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Sales Invoice' as doctype,
+ si.name,
+ sip.amount as paid_amount,
+ '' as reference_no,
+ '' as reference_date,
+ si.customer as party,
+ 'Customer' as party_type,
+ si.posting_date,
+ si.currency
+
+ FROM
+ `tabSales Invoice Payment` as sip
+ JOIN
+ `tabSales Invoice` as si
+ ON
+ sip.parent = si.name
+ WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
+ AND sip.account = %(bank_account)s
+ AND sip.amount {amount_condition} %(amount)s
+ AND si.docstatus = 1
+ """
+
+def get_pi_matching_query(amount_condition):
+ # get matching purchase invoice query
+ return f"""
+ SELECT
+ ( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Purchase Invoice' as doctype,
+ name,
+ paid_amount,
+ '' as reference_no,
+ '' as reference_date,
+ supplier as party,
+ 'Supplier' as party_type,
+ posting_date,
+ currency
+ FROM
+ `tabPurchase Invoice`
+ WHERE
+ paid_amount {amount_condition} %(amount)s
+ AND docstatus = 1
+ AND is_paid = 1
+ AND ifnull(clearance_date, '') = ""
+ 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_list("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
+ ( CASE WHEN employee = %(party)s THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Expense Claim' as doctype,
+ name,
+ total_sanctioned_amount as paid_amount,
+ '' as reference_no,
+ '' as reference_date,
+ employee as party,
+ 'Employee' as party_type,
+ posting_date,
+ '{company_currency}' as currency
+ FROM
+ `tabExpense Claim`
+ WHERE
+ total_sanctioned_amount {amount_condition} %(amount)s
+ AND docstatus = 1
+ AND is_paid = 1
+ AND ifnull(clearance_date, '') = ""
+ AND mode_of_payment in {mode_of_payments}
+ """
diff --git a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
similarity index 77%
copy from erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
copy to erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
index 2ad7984..d96950a 100644
--- a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
@@ -6,5 +6,5 @@
# import frappe
import unittest
-class TestMembershipSettings(unittest.TestCase):
+class TestBankReconciliationTool(unittest.TestCase):
pass
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/accounts/doctype/bank_statement_import/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/bank_statement_settings/__init__.py
rename to erpnext/accounts/doctype/bank_statement_import/__init__.py
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.css b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.css
new file mode 100644
index 0000000..5206540
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.css
@@ -0,0 +1,3 @@
+.warnings .warning {
+ margin-bottom: 40px;
+}
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
new file mode 100644
index 0000000..3dbd605
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
@@ -0,0 +1,535 @@
+// Copyright (c) 2019, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Bank Statement Import", {
+ setup(frm) {
+ frappe.realtime.on("data_import_refresh", ({ data_import }) => {
+ frm.import_in_progress = false;
+ if (data_import !== frm.doc.name) return;
+ frappe.model.clear_doc("Bank Statement Import", frm.doc.name);
+ frappe.model
+ .with_doc("Bank Statement Import", frm.doc.name)
+ .then(() => {
+ frm.refresh();
+ });
+ });
+ frappe.realtime.on("data_import_progress", (data) => {
+ frm.import_in_progress = true;
+ if (data.data_import !== frm.doc.name) {
+ return;
+ }
+ let percent = Math.floor((data.current * 100) / data.total);
+ let seconds = Math.floor(data.eta);
+ let minutes = Math.floor(data.eta / 60);
+ let eta_message =
+ // prettier-ignore
+ seconds < 60
+ ? __('About {0} seconds remaining', [seconds])
+ : minutes === 1
+ ? __('About {0} minute remaining', [minutes])
+ : __('About {0} minutes remaining', [minutes]);
+
+ let message;
+ if (data.success) {
+ let message_args = [data.current, data.total, eta_message];
+ message =
+ frm.doc.import_type === "Insert New Records"
+ ? __("Importing {0} of {1}, {2}", message_args)
+ : __("Updating {0} of {1}, {2}", message_args);
+ }
+ if (data.skipping) {
+ message = __(
+ "Skipping {0} of {1}, {2}",
+ [
+ data.current,
+ data.total,
+ eta_message,
+ ]
+ );
+ }
+ frm.dashboard.show_progress(
+ __("Import Progress"),
+ percent,
+ message
+ );
+ frm.page.set_indicator(__("In Progress"), "orange");
+
+ // hide progress when complete
+ if (data.current === data.total) {
+ setTimeout(() => {
+ frm.dashboard.hide();
+ frm.refresh();
+ }, 2000);
+ }
+ });
+
+ frm.set_query("reference_doctype", () => {
+ return {
+ filters: {
+ name: ["in", frappe.boot.user.can_import],
+ },
+ };
+ });
+
+ frm.get_field("import_file").df.options = {
+ restrictions: {
+ allowed_file_types: [".csv", ".xls", ".xlsx"],
+ },
+ };
+
+ frm.has_import_file = () => {
+ return frm.doc.import_file || frm.doc.google_sheets_url;
+ };
+ },
+
+ refresh(frm) {
+ frm.page.hide_icon_group();
+ frm.trigger("update_indicators");
+ frm.trigger("import_file");
+ frm.trigger("show_import_log");
+ frm.trigger("show_import_warnings");
+ frm.trigger("toggle_submit_after_import");
+ frm.trigger("show_import_status");
+ frm.trigger("show_report_error_button");
+
+ if (frm.doc.status === "Partial Success") {
+ frm.add_custom_button(__("Export Errored Rows"), () =>
+ frm.trigger("export_errored_rows")
+ );
+ }
+
+ if (frm.doc.status.includes("Success")) {
+ frm.add_custom_button(
+ __("Go to {0} List", [frm.doc.reference_doctype]),
+ () => frappe.set_route("List", frm.doc.reference_doctype)
+ );
+ }
+ },
+
+ onload_post_render(frm) {
+ frm.trigger("update_primary_action");
+ },
+
+ update_primary_action(frm) {
+ if (frm.is_dirty()) {
+ frm.enable_save();
+ return;
+ }
+ frm.disable_save();
+ if (frm.doc.status !== "Success") {
+ if (!frm.is_new() && frm.has_import_file()) {
+ let label =
+ frm.doc.status === "Pending"
+ ? __("Start Import")
+ : __("Retry");
+ frm.page.set_primary_action(label, () =>
+ frm.events.start_import(frm)
+ );
+ } else {
+ frm.page.set_primary_action(__("Save"), () => frm.save());
+ }
+ }
+ },
+
+ update_indicators(frm) {
+ const indicator = frappe.get_indicator(frm.doc);
+ if (indicator) {
+ frm.page.set_indicator(indicator[0], indicator[1]);
+ } else {
+ frm.page.clear_indicator();
+ }
+ },
+
+ show_import_status(frm) {
+ let import_log = JSON.parse(frm.doc.import_log || "[]");
+ let successful_records = import_log.filter((log) => log.success);
+ let failed_records = import_log.filter((log) => !log.success);
+ if (successful_records.length === 0) return;
+
+ let message;
+ if (failed_records.length === 0) {
+ let message_args = [successful_records.length];
+ if (frm.doc.import_type === "Insert New Records") {
+ message =
+ successful_records.length > 1
+ ? __("Successfully imported {0} records.", message_args)
+ : __("Successfully imported {0} record.", message_args);
+ } else {
+ message =
+ successful_records.length > 1
+ ? __("Successfully updated {0} records.", message_args)
+ : __("Successfully updated {0} record.", message_args);
+ }
+ } else {
+ let message_args = [successful_records.length, import_log.length];
+ if (frm.doc.import_type === "Insert New Records") {
+ message =
+ successful_records.length > 1
+ ? __(
+ "Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
+ message_args
+ )
+ : __(
+ "Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",
+ message_args
+ );
+ } else {
+ message =
+ successful_records.length > 1
+ ? __(
+ "Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
+ message_args
+ )
+ : __(
+ "Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",
+ message_args
+ );
+ }
+ }
+ frm.dashboard.set_headline(message);
+ },
+
+ show_report_error_button(frm) {
+ if (frm.doc.status === "Error") {
+ frappe.db
+ .get_list("Error Log", {
+ filters: { method: frm.doc.name },
+ fields: ["method", "error"],
+ order_by: "creation desc",
+ limit: 1,
+ })
+ .then((result) => {
+ if (result.length > 0) {
+ frm.add_custom_button("Report Error", () => {
+ let fake_xhr = {
+ responseText: JSON.stringify({
+ exc: result[0].error,
+ }),
+ };
+ frappe.request.report_error(fake_xhr, {});
+ });
+ }
+ });
+ }
+ },
+
+ start_import(frm) {
+ frm.call({
+ method: "form_start_import",
+ args: { data_import: frm.doc.name },
+ btn: frm.page.btn_primary,
+ }).then((r) => {
+ if (r.message === true) {
+ frm.disable_save();
+ }
+ });
+ },
+
+ download_template() {
+ let method =
+ "/api/method/frappe.core.doctype.data_import.data_import.download_template";
+
+ open_url_post(method, {
+ doctype: "Bank Transaction",
+ export_records: "5_records",
+ export_fields: {
+ "Bank Transaction": [
+ "date",
+ "deposit",
+ "withdrawal",
+ "description",
+ "reference_number",
+ ],
+ },
+ });
+ },
+
+ reference_doctype(frm) {
+ frm.trigger("toggle_submit_after_import");
+ },
+
+ toggle_submit_after_import(frm) {
+ frm.toggle_display("submit_after_import", false);
+ let doctype = frm.doc.reference_doctype;
+ if (doctype) {
+ frappe.model.with_doctype(doctype, () => {
+ let meta = frappe.get_meta(doctype);
+ frm.toggle_display("submit_after_import", meta.is_submittable);
+ });
+ }
+ },
+
+ google_sheets_url(frm) {
+ if (!frm.is_dirty()) {
+ frm.trigger("import_file");
+ } else {
+ frm.trigger("update_primary_action");
+ }
+ },
+
+ refresh_google_sheet(frm) {
+ frm.trigger("import_file");
+ },
+
+ import_file(frm) {
+ frm.toggle_display("section_import_preview", frm.has_import_file());
+ if (!frm.has_import_file()) {
+ frm.get_field("import_preview").$wrapper.empty();
+ return;
+ } else {
+ frm.trigger("update_primary_action");
+ }
+
+ // load import preview
+ frm.get_field("import_preview").$wrapper.empty();
+ $('<span class="text-muted">')
+ .html(__("Loading import file..."))
+ .appendTo(frm.get_field("import_preview").$wrapper);
+
+ frm.call({
+ method: "get_preview_from_template",
+ args: {
+ data_import: frm.doc.name,
+ import_file: frm.doc.import_file,
+ google_sheets_url: frm.doc.google_sheets_url,
+ },
+ error_handlers: {
+ TimestampMismatchError() {
+ // ignore this error
+ },
+ },
+ }).then((r) => {
+ let preview_data = r.message;
+ frm.events.show_import_preview(frm, preview_data);
+ frm.events.show_import_warnings(frm, preview_data);
+ });
+ },
+ // method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
+
+ show_import_preview(frm, preview_data) {
+ let import_log = JSON.parse(frm.doc.import_log || "[]");
+
+ if (
+ frm.import_preview &&
+ frm.import_preview.doctype === frm.doc.reference_doctype
+ ) {
+ frm.import_preview.preview_data = preview_data;
+ frm.import_preview.import_log = import_log;
+ frm.import_preview.refresh();
+ return;
+ }
+
+ frappe.require("/assets/js/data_import_tools.min.js", () => {
+ frm.import_preview = new frappe.data_import.ImportPreview({
+ wrapper: frm.get_field("import_preview").$wrapper,
+ doctype: frm.doc.reference_doctype,
+ preview_data,
+ import_log,
+ frm,
+ events: {
+ remap_column(changed_map) {
+ let template_options = JSON.parse(
+ frm.doc.template_options || "{}"
+ );
+ template_options.column_to_field_map =
+ template_options.column_to_field_map || {};
+ Object.assign(
+ template_options.column_to_field_map,
+ changed_map
+ );
+ frm.set_value(
+ "template_options",
+ JSON.stringify(template_options)
+ );
+ frm.save().then(() => frm.trigger("import_file"));
+ },
+ },
+ });
+ });
+ },
+
+ export_errored_rows(frm) {
+ open_url_post(
+ "/api/method/frappe.core.doctype.data_import.data_import.download_errored_template",
+ {
+ data_import_name: frm.doc.name,
+ }
+ );
+ },
+
+ show_import_warnings(frm, preview_data) {
+ let columns = preview_data.columns;
+ let warnings = JSON.parse(frm.doc.template_warnings || "[]");
+ warnings = warnings.concat(preview_data.warnings || []);
+
+ frm.toggle_display("import_warnings_section", warnings.length > 0);
+ if (warnings.length === 0) {
+ frm.get_field("import_warnings").$wrapper.html("");
+ return;
+ }
+
+ // group warnings by row
+ let warnings_by_row = {};
+ let other_warnings = [];
+ for (let warning of warnings) {
+ if (warning.row) {
+ warnings_by_row[warning.row] =
+ warnings_by_row[warning.row] || [];
+ warnings_by_row[warning.row].push(warning);
+ } else {
+ other_warnings.push(warning);
+ }
+ }
+
+ let html = "";
+ html += Object.keys(warnings_by_row)
+ .map((row_number) => {
+ let message = warnings_by_row[row_number]
+ .map((w) => {
+ if (w.field) {
+ let label =
+ w.field.label +
+ (w.field.parent !== frm.doc.reference_doctype
+ ? ` (${w.field.parent})`
+ : "");
+ return `<li>${label}: ${w.message}</li>`;
+ }
+ return `<li>${w.message}</li>`;
+ })
+ .join("");
+ return `
+ <div class="warning" data-row="${row_number}">
+ <h5 class="text-uppercase">${__("Row {0}", [row_number])}</h5>
+ <div class="body"><ul>${message}</ul></div>
+ </div>
+ `;
+ })
+ .join("");
+
+ html += other_warnings
+ .map((warning) => {
+ let header = "";
+ if (warning.col) {
+ let column_number = `<span class="text-uppercase">${__(
+ "Column {0}",
+ [warning.col]
+ )}</span>`;
+ let column_header = columns[warning.col].header_title;
+ header = `${column_number} (${column_header})`;
+ }
+ return `
+ <div class="warning" data-col="${warning.col}">
+ <h5>${header}</h5>
+ <div class="body">${warning.message}</div>
+ </div>
+ `;
+ })
+ .join("");
+ frm.get_field("import_warnings").$wrapper.html(`
+ <div class="row">
+ <div class="col-sm-10 warnings">${html}</div>
+ </div>
+ `);
+ },
+
+ show_failed_logs(frm) {
+ frm.trigger("show_import_log");
+ },
+
+ show_import_log(frm) {
+ let import_log = JSON.parse(frm.doc.import_log || "[]");
+ let logs = import_log;
+ frm.toggle_display("import_log", false);
+ frm.toggle_display("import_log_section", logs.length > 0);
+
+ if (logs.length === 0) {
+ frm.get_field("import_log_preview").$wrapper.empty();
+ return;
+ }
+
+ let rows = logs
+ .map((log) => {
+ let html = "";
+ if (log.success) {
+ if (frm.doc.import_type === "Insert New Records") {
+ html = __(
+ "Successfully imported {0}", [
+ `<span class="underline">${frappe.utils.get_form_link(
+ frm.doc.reference_doctype,
+ log.docname,
+ true
+ )}<span>`,
+ ]
+ );
+ } else {
+ html = __(
+ "Successfully updated {0}", [
+ `<span class="underline">${frappe.utils.get_form_link(
+ frm.doc.reference_doctype,
+ log.docname,
+ true
+ )}<span>`,
+ ]
+ );
+ }
+ } else {
+ let messages = log.messages
+ .map(JSON.parse)
+ .map((m) => {
+ let title = m.title
+ ? `<strong>${m.title}</strong>`
+ : "";
+ let message = m.message
+ ? `<div>${m.message}</div>`
+ : "";
+ return title + message;
+ })
+ .join("");
+ let id = frappe.dom.get_unique_id();
+ html = `${messages}
+ <button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
+ ${__("Show Traceback")}
+ </button>
+ <div class="collapse" id="${id}" style="margin-top: 15px;">
+ <div class="well">
+ <pre>${log.exception}</pre>
+ </div>
+ </div>`;
+ }
+ let indicator_color = log.success ? "green" : "red";
+ let title = log.success ? __("Success") : __("Failure");
+
+ if (frm.doc.show_failed_logs && log.success) {
+ return "";
+ }
+
+ return `<tr>
+ <td>${log.row_indexes.join(", ")}</td>
+ <td>
+ <div class="indicator ${indicator_color}">${title}</div>
+ </td>
+ <td>
+ ${html}
+ </td>
+ </tr>`;
+ })
+ .join("");
+
+ if (!rows && frm.doc.show_failed_logs) {
+ rows = `<tr><td class="text-center text-muted" colspan=3>
+ ${__("No failed logs")}
+ </td></tr>`;
+ }
+
+ frm.get_field("import_log_preview").$wrapper.html(`
+ <table class="table table-bordered">
+ <tr class="text-muted">
+ <th width="10%">${__("Row Number")}</th>
+ <th width="10%">${__("Status")}</th>
+ <th width="80%">${__("Message")}</th>
+ </tr>
+ ${rows}
+ </table>
+ `);
+ },
+});
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json
new file mode 100644
index 0000000..5e913cc
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json
@@ -0,0 +1,227 @@
+{
+ "actions": [],
+ "autoname": "format:Bank Statement Import on {creation}",
+ "beta": 1,
+ "creation": "2019-08-04 14:16:08.318714",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "bank_account",
+ "bank",
+ "column_break_4",
+ "google_sheets_url",
+ "refresh_google_sheet",
+ "html_5",
+ "import_file",
+ "download_template",
+ "status",
+ "template_options",
+ "import_warnings_section",
+ "template_warnings",
+ "import_warnings",
+ "section_import_preview",
+ "import_preview",
+ "import_log_section",
+ "import_log",
+ "show_failed_logs",
+ "import_log_preview",
+ "reference_doctype",
+ "import_type",
+ "submit_after_import",
+ "mute_emails"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "bank_account",
+ "fieldtype": "Link",
+ "label": "Bank Account",
+ "options": "Bank Account",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "depends_on": "eval:doc.bank_account",
+ "fetch_from": "bank_account.bank",
+ "fieldname": "bank",
+ "fieldtype": "Link",
+ "label": "Bank",
+ "options": "Bank",
+ "read_only": 1,
+ "set_only_once": 1
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "download_template",
+ "fieldtype": "Button",
+ "label": "Download Template"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "import_file",
+ "fieldtype": "Attach",
+ "in_list_view": 1,
+ "label": "Import File"
+ },
+ {
+ "fieldname": "import_preview",
+ "fieldtype": "HTML",
+ "label": "Import Preview"
+ },
+ {
+ "fieldname": "section_import_preview",
+ "fieldtype": "Section Break",
+ "label": "Preview"
+ },
+ {
+ "fieldname": "template_options",
+ "fieldtype": "Code",
+ "hidden": 1,
+ "label": "Template Options",
+ "options": "JSON",
+ "read_only": 1
+ },
+ {
+ "fieldname": "import_log",
+ "fieldtype": "Code",
+ "label": "Import Log",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "import_log_section",
+ "fieldtype": "Section Break",
+ "label": "Import Log"
+ },
+ {
+ "fieldname": "import_log_preview",
+ "fieldtype": "HTML",
+ "label": "Import Log Preview"
+ },
+ {
+ "default": "Pending",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "label": "Status",
+ "options": "Pending\nSuccess\nPartial Success\nError",
+ "read_only": 1
+ },
+ {
+ "fieldname": "template_warnings",
+ "fieldtype": "Code",
+ "hidden": 1,
+ "label": "Template Warnings",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "import_warnings_section",
+ "fieldtype": "Section Break",
+ "label": "Import File Errors and Warnings"
+ },
+ {
+ "fieldname": "import_warnings",
+ "fieldtype": "HTML",
+ "label": "Import Warnings"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_failed_logs",
+ "fieldtype": "Check",
+ "label": "Show Failed Logs"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal && !doc.import_file",
+ "fieldname": "html_5",
+ "fieldtype": "HTML",
+ "options": "<h5 class=\"text-muted uppercase\">Or</h5>"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal && !doc.import_file\n",
+ "description": "Must be a publicly accessible Google Sheets URL",
+ "fieldname": "google_sheets_url",
+ "fieldtype": "Data",
+ "label": "Import from Google Sheets"
+ },
+ {
+ "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved",
+ "fieldname": "refresh_google_sheet",
+ "fieldtype": "Button",
+ "label": "Refresh Google Sheet"
+ },
+ {
+ "default": "Bank Transaction",
+ "fieldname": "reference_doctype",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "Document Type",
+ "options": "DocType",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "default": "Insert New Records",
+ "fieldname": "import_type",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "Import Type",
+ "options": "\nInsert New Records\nUpdate Existing Records",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "submit_after_import",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Submit After Import",
+ "set_only_once": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "mute_emails",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Don't Send Emails",
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "hide_toolbar": 1,
+ "links": [],
+ "modified": "2021-02-10 19:29:59.027325",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Bank Statement Import",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
new file mode 100644
index 0000000..9f41b13
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -0,0 +1,205 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import csv
+import json
+import re
+
+import openpyxl
+from openpyxl.styles import Font
+from openpyxl.utils import get_column_letter
+from six import string_types
+
+import frappe
+from frappe.core.doctype.data_import.importer import Importer, ImportFile
+from frappe.utils.background_jobs import enqueue
+from frappe.utils.xlsxutils import handle_html, ILLEGAL_CHARACTERS_RE
+from frappe import _
+
+from frappe.core.doctype.data_import.data_import import DataImport
+
+class BankStatementImport(DataImport):
+ def __init__(self, *args, **kwargs):
+ super(BankStatementImport, self).__init__(*args, **kwargs)
+
+ def validate(self):
+ doc_before_save = self.get_doc_before_save()
+ if (
+ not (self.import_file or self.google_sheets_url)
+ or (doc_before_save and doc_before_save.import_file != self.import_file)
+ or (doc_before_save and doc_before_save.google_sheets_url != self.google_sheets_url)
+ ):
+
+ template_options_dict = {}
+ column_to_field_map = {}
+ bank = frappe.get_doc("Bank", self.bank)
+ for i in bank.bank_transaction_mapping:
+ column_to_field_map[i.file_field] = i.bank_transaction_field
+ template_options_dict["column_to_field_map"] = column_to_field_map
+ self.template_options = json.dumps(template_options_dict)
+
+ self.template_warnings = ""
+
+ self.validate_import_file()
+ self.validate_google_sheets_url()
+
+ def start_import(self):
+
+ from frappe.core.page.background_jobs.background_jobs import get_info
+ from frappe.utils.scheduler import is_scheduler_inactive
+
+ if is_scheduler_inactive() and not frappe.flags.in_test:
+ frappe.throw(
+ _("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")
+ )
+
+ enqueued_jobs = [d.get("job_name") for d in get_info()]
+
+ if self.name not in enqueued_jobs:
+ enqueue(
+ start_import,
+ queue="default",
+ timeout=6000,
+ event="data_import",
+ job_name=self.name,
+ data_import=self.name,
+ bank_account=self.bank_account,
+ import_file_path=self.import_file,
+ bank=self.bank,
+ template_options=self.template_options,
+ now=frappe.conf.developer_mode or frappe.flags.in_test,
+ )
+ return True
+
+ 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, bank, template_options):
+ """This method runs in background job"""
+
+ update_mapping_db(bank, template_options)
+
+ data_import = frappe.get_doc("Bank Statement Import", data_import)
+
+ import_file = ImportFile("Bank Transaction", file = import_file_path, import_type="Insert New Records")
+ data = import_file.raw_data
+
+ add_bank_account(data, bank_account)
+ write_files(import_file, data)
+
+ try:
+ i = Importer(data_import.reference_doctype, data_import=data_import)
+ i.import_data()
+ except Exception:
+ frappe.db.rollback()
+ data_import.db_set("status", "Error")
+ frappe.log_error(title=data_import.name)
+ finally:
+ frappe.flags.in_import = False
+
+ 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.save()
+
+def add_bank_account(data, bank_account):
+ bank_account_loc = None
+ if "Bank Account" not in data[0]:
+ data[0].append("Bank Account")
+ else:
+ for loc, header in enumerate(data[0]):
+ if header == "Bank Account":
+ bank_account_loc = loc
+
+ for row in data[1:]:
+ if bank_account_loc:
+ row[bank_account_loc] = 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()
+ extension = parts[1]
+ extension = extension.lstrip(".")
+
+ if extension == "csv":
+ 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)
+
+def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
+ # from xlsx utils with changes
+ column_widths = column_widths or []
+ if wb is None:
+ wb = openpyxl.Workbook(write_only=True)
+
+ ws = wb.create_sheet(sheet_name, 0)
+
+ for i, column_width in enumerate(column_widths):
+ if column_width:
+ ws.column_dimensions[get_column_letter(i + 1)].width = column_width
+
+ row1 = ws.row_dimensions[1]
+ 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']):
+ 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)
+
+ clean_row.append(value)
+
+ ws.append(clean_row)
+
+ 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,
+ })
+
+ if args.bank_account:
+ bsi.update({
+ "bank_account": args.bank_account
+ })
+
+ return bsi
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js
new file mode 100644
index 0000000..6c75402
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js
@@ -0,0 +1,36 @@
+let imports_in_progress = [];
+
+frappe.listview_settings['Bank Statement Import'] = {
+ onload(listview) {
+ frappe.realtime.on('data_import_progress', data => {
+ if (!imports_in_progress.includes(data.data_import)) {
+ imports_in_progress.push(data.data_import);
+ }
+ });
+ frappe.realtime.on('data_import_refresh', data => {
+ imports_in_progress = imports_in_progress.filter(
+ d => d !== data.data_import
+ );
+ listview.refresh();
+ });
+ },
+ get_indicator: function(doc) {
+ var colors = {
+ 'Pending': 'orange',
+ 'Not Started': 'orange',
+ 'Partial Success': 'orange',
+ 'Success': 'green',
+ 'In Progress': 'orange',
+ 'Error': 'red'
+ };
+ let status = doc.status;
+ if (imports_in_progress.includes(doc.name)) {
+ status = 'In Progress';
+ }
+ if (status == 'Pending') {
+ status = 'Not Started';
+ }
+ return [__(status), colors[status], 'status,=,' + doc.status];
+ },
+ hide_name_column: true
+};
diff --git a/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py
new file mode 100644
index 0000000..cd58314
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestBankStatementImport(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.js b/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.js
deleted file mode 100644
index 46aa4f2..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, sathishpy@gmail.com and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Bank Statement Settings', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.json b/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.json
deleted file mode 100644
index 53fbf7d..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.json
+++ /dev/null
@@ -1,272 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "beta": 0,
- "creation": "2017-11-13 13:38:10.863592",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Bank Account",
- "length": 0,
- "no_copy": 0,
- "options": "Bank",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "'%d/%m/%Y'",
- "fieldname": "date_format",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Date Format",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "statement_header_mapping",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Statement Header Mapping",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "header_items",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Statement Headers",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Settings Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "transaction_data_mapping",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Transaction Data Mapping",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mapped_items",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Mapped Items",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Transaction Settings Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-04-07 18:57:04.048423",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.py b/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.py
deleted file mode 100644
index 6c4dd1b..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementSettings(Document):
- def autoname(self):
- self.name = self.bank + "-Statement-Settings"
diff --git a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.js b/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.js
deleted file mode 100644
index f2381c0..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Bank Statement Settings", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially([
- // insert a new Bank Statement Settings
- () => frappe.tests.make('Bank Statement Settings', [
- // values to be set
- {key: 'value'}
- ]),
- () => {
- assert.equal(cur_frm.doc.key, 'value');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.py b/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.py
deleted file mode 100644
index aa7fe83..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import frappe
-import unittest
-
-class TestBankStatementSettings(unittest.TestCase):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.json b/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.json
deleted file mode 100644
index 7c93f26..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.json
+++ /dev/null
@@ -1,101 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-01-08 00:16:42.762980",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mapped_header",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Mapped Header",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "stmt_header",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Bank Header",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-01-08 00:19:14.841134",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Settings Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py b/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py
deleted file mode 100644
index 9438e9a..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementSettingsItem(Document):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py
+++ /dev/null
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.js b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.js
deleted file mode 100644
index 736ed35..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.js
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (c) 2017, sathishpy@gmail.com and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Bank Statement Transaction Entry', {
- setup: function(frm) {
- frm.events.account_filters(frm)
- frm.events.invoice_filter(frm)
- },
- refresh: function(frm) {
- frm.set_df_property("bank_account", "read_only", frm.doc.__islocal ? 0 : 1);
- frm.set_df_property("from_date", "read_only", frm.doc.__islocal ? 0 : 1);
- frm.set_df_property("to_date", "read_only", frm.doc.__islocal ? 0 : 1);
- },
- invoke_doc_function(frm, method) {
- frappe.call({
- doc: frm.doc,
- method: method,
- callback: function(r) {
- if(!r.exe) {
- frm.refresh_fields();
- }
- }
- });
- },
- account_filters: function(frm) {
- frm.fields_dict['bank_account'].get_query = function(doc, dt, dn) {
- return {
- filters:[
- ["Account", "account_type", "in", ["Bank"]]
- ]
- }
- };
- frm.fields_dict['receivable_account'].get_query = function(doc, dt, dn) {
- return {
- filters: {"account_type": "Receivable"}
- }
- };
- frm.fields_dict['payable_account'].get_query = function(doc, dt, dn) {
- return {
- filters: {"account_type": "Payable"}
- }
- };
- },
-
- invoice_filter: function(frm) {
- frm.set_query("invoice", "payment_invoice_items", function(doc, cdt, cdn) {
- let row = locals[cdt][cdn]
- if (row.party_type == "Customer") {
- return {
- filters:[[row.invoice_type, "customer", "in", [row.party]],
- [row.invoice_type, "status", "!=", "Cancelled" ],
- [row.invoice_type, "posting_date", "<", row.transaction_date ],
- [row.invoice_type, "outstanding_amount", ">", 0 ]]
- }
- } else if (row.party_type == "Supplier") {
- return {
- filters:[[row.invoice_type, "supplier", "in", [row.party]],
- [row.invoice_type, "status", "!=", "Cancelled" ],
- [row.invoice_type, "posting_date", "<", row.transaction_date ],
- [row.invoice_type, "outstanding_amount", ">", 0 ]]
- }
- }
- });
- },
-
- match_invoices: function(frm) {
- frm.events.invoke_doc_function(frm, "populate_matching_invoices");
- },
- create_payments: function(frm) {
- frm.events.invoke_doc_function(frm, "create_payment_entries");
- },
- submit_payments: function(frm) {
- frm.events.invoke_doc_function(frm, "submit_payment_entries");
- },
-});
-
-
-frappe.ui.form.on('Bank Statement Transaction Invoice Item', {
- party_type: function(frm, cdt, cdn) {
- let row = locals[cdt][cdn];
- if (row.party_type == "Customer") {
- row.invoice_type = "Sales Invoice";
- } else if (row.party_type == "Supplier") {
- row.invoice_type = "Purchase Invoice";
- } else if (row.party_type == "Account") {
- row.invoice_type = "Journal Entry";
- }
- refresh_field("invoice_type", row.name, "payment_invoice_items");
-
- },
- invoice_type: function(frm, cdt, cdn) {
- let row = locals[cdt][cdn];
- if (row.invoice_type == "Purchase Invoice") {
- row.party_type = "Supplier";
- } else if (row.invoice_type == "Sales Invoice") {
- row.party_type = "Customer";
- }
- refresh_field("party_type", row.name, "payment_invoice_items");
- }
-});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.json b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.json
deleted file mode 100644
index fb80169..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.json
+++ /dev/null
@@ -1,792 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "beta": 0,
- "creation": "2017-11-07 13:48:13.123185",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank_account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Bank Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "from_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "From Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "To Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank_settings",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Bank Statement Settings",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Settings",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Bank",
- "length": 0,
- "no_copy": 0,
- "options": "Bank",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "receivable_account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Receivable Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payable_account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payable Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank_statement",
- "fieldtype": "Attach",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Bank Statement",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Bank Transaction Entries",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "new_transaction_items",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "New Transactions",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Transaction Payment Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.new_transaction_items && doc.new_transaction_items.length",
- "fieldname": "section_break_9",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "match_invoices",
- "fieldtype": "Button",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Match Transaction to Invoices",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_14",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "create_payments",
- "fieldtype": "Button",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Create New Payment/Journal Entry",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_16",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "submit_payments",
- "fieldtype": "Button",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Submit/Reconcile Payments",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.new_transaction_items && doc.new_transaction_items.length",
- "fieldname": "section_break_18",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Matching Invoices",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_invoice_items",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payment Invoice Items",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Transaction Invoice Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reconciled_transactions",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Reconciled Transactions",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reconciled_transaction_items",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Reconciled Transactions",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Transaction Payment Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amended_from",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Amended From",
- "length": 0,
- "no_copy": 1,
- "options": "Bank Statement Transaction Entry",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-09-14 18:04:44.170455",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Transaction Entry",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py
deleted file mode 100644
index 27dd8e4..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py
+++ /dev/null
@@ -1,443 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-from frappe.model.document import Document
-from erpnext.accounts.utils import get_outstanding_invoices
-from frappe.utils import nowdate
-from datetime import datetime
-import csv, os, re, io
-import difflib
-import copy
-
-class BankStatementTransactionEntry(Document):
- def autoname(self):
- self.name = self.bank_account + "-" + self.from_date + "-" + self.to_date
- if self.bank:
- mapper_name = self.bank + "-Statement-Settings"
- if not frappe.db.exists("Bank Statement Settings", mapper_name):
- self.create_settings(self.bank)
- self.bank_settings = mapper_name
-
- def create_settings(self, bank):
- mapper = frappe.new_doc("Bank Statement Settings")
- mapper.bank = bank
- mapper.date_format = "%Y-%m-%d"
- mapper.bank_account = self.bank_account
- for header in ["Date", "Particulars", "Withdrawals", "Deposits", "Balance"]:
- header_item = mapper.append("header_items", {})
- header_item.mapped_header = header_item.stmt_header = header
- mapper.save()
-
- def on_update(self):
- if (not self.bank_statement):
- self.reconciled_transaction_items = self.new_transaction_items = []
- return
-
- if len(self.new_transaction_items + self.reconciled_transaction_items) == 0:
- self.populate_payment_entries()
- else:
- self.match_invoice_to_payment()
-
- def validate(self):
- if not self.new_transaction_items:
- self.populate_payment_entries()
-
- def get_statement_headers(self):
- if not self.bank_settings:
- frappe.throw(_("Bank Data mapper doesn't exist"))
- mapper_doc = frappe.get_doc("Bank Statement Settings", self.bank_settings)
- headers = {entry.mapped_header:entry.stmt_header for entry in mapper_doc.header_items}
- return headers
-
- def populate_payment_entries(self):
- if self.bank_statement is None: return
- file_url = self.bank_statement
- if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0):
- frappe.throw(_("Transactions already retreived from the statement"))
-
- date_format = frappe.get_value("Bank Statement Settings", self.bank_settings, "date_format")
- if (date_format is None):
- date_format = '%Y-%m-%d'
- if self.bank_settings:
- mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items
- statement_headers = self.get_statement_headers()
- transactions = get_transaction_entries(file_url, statement_headers)
- for entry in transactions:
- date = entry[statement_headers["Date"]].strip()
- #print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"]))
- if (not date): continue
- transaction_date = datetime.strptime(date, date_format).date()
- if (self.from_date and transaction_date < datetime.strptime(self.from_date, '%Y-%m-%d').date()): continue
- if (self.to_date and transaction_date > datetime.strptime(self.to_date, '%Y-%m-%d').date()): continue
- bank_entry = self.append('new_transaction_items', {})
- bank_entry.transaction_date = transaction_date
- bank_entry.description = entry[statement_headers["Particulars"]]
-
- mapped_item = next((entry for entry in mapped_items if entry.mapping_type == "Transaction" and frappe.safe_decode(entry.bank_data.lower()) in frappe.safe_decode(bank_entry.description.lower())), None)
- if (mapped_item is not None):
- bank_entry.party_type = mapped_item.mapped_data_type
- bank_entry.party = mapped_item.mapped_data
- else:
- bank_entry.party_type = "Supplier" if not entry[statement_headers["Deposits"]].strip() else "Customer"
- party_list = frappe.get_all(bank_entry.party_type, fields=["name"])
- parties = [party.name for party in party_list]
- matches = difflib.get_close_matches(frappe.safe_decode(bank_entry.description.lower()), parties, 1, 0.4)
- if len(matches) > 0: bank_entry.party = matches[0]
- bank_entry.amount = -float(entry[statement_headers["Withdrawals"]]) if not entry[statement_headers["Deposits"]].strip() else float(entry[statement_headers["Deposits"]])
- self.map_unknown_transactions()
- self.map_transactions_on_journal_entry()
-
- def map_transactions_on_journal_entry(self):
- for entry in self.new_transaction_items:
- vouchers = frappe.db.sql("""select name, posting_date from `tabJournal Entry`
- where posting_date='{0}' and total_credit={1} and cheque_no='{2}' and docstatus != 2
- """.format(entry.transaction_date, abs(entry.amount), frappe.safe_decode(entry.description)), as_dict=True)
- if (len(vouchers) == 1):
- entry.reference_name = vouchers[0].name
-
- def populate_matching_invoices(self):
- self.payment_invoice_items = []
- self.map_unknown_transactions()
- added_invoices = []
- for entry in self.new_transaction_items:
- if (not entry.party or entry.party_type == "Account"): continue
- account = self.receivable_account if entry.party_type == "Customer" else self.payable_account
- invoices = get_outstanding_invoices(entry.party_type, entry.party, account)
- transaction_date = datetime.strptime(entry.transaction_date, "%Y-%m-%d").date()
- outstanding_invoices = [invoice for invoice in invoices if invoice.posting_date <= transaction_date]
- amount = abs(entry.amount)
- matching_invoices = [invoice for invoice in outstanding_invoices if invoice.outstanding_amount == amount]
- sorted(outstanding_invoices, key=lambda k: k['posting_date'])
- for e in (matching_invoices + outstanding_invoices):
- added = next((inv for inv in added_invoices if inv == e.get('voucher_no')), None)
- if (added is not None): continue
- ent = self.append('payment_invoice_items', {})
- ent.transaction_date = entry.transaction_date
- ent.payment_description = frappe.safe_decode(entry.description)
- ent.party_type = entry.party_type
- ent.party = entry.party
- ent.invoice = e.get('voucher_no')
- added_invoices += [ent.invoice]
- ent.invoice_type = "Sales Invoice" if entry.party_type == "Customer" else "Purchase Invoice"
- ent.invoice_date = e.get('posting_date')
- ent.outstanding_amount = e.get('outstanding_amount')
- ent.allocated_amount = min(float(e.get('outstanding_amount')), amount)
- amount -= float(e.get('outstanding_amount'))
- if (amount <= 5): break
- self.match_invoice_to_payment()
- self.populate_matching_vouchers()
- self.map_transactions_on_journal_entry()
-
- def match_invoice_to_payment(self):
- added_payments = []
- for entry in self.new_transaction_items:
- if (not entry.party or entry.party_type == "Account"): continue
- entry.account = self.receivable_account if entry.party_type == "Customer" else self.payable_account
- amount = abs(entry.amount)
- payment, matching_invoices = None, []
- for inv_entry in self.payment_invoice_items:
- if (inv_entry.payment_description != frappe.safe_decode(entry.description) or inv_entry.transaction_date != entry.transaction_date): continue
- if (inv_entry.party != entry.party): continue
- matching_invoices += [inv_entry.invoice_type + "|" + inv_entry.invoice]
- payment = get_payments_matching_invoice(inv_entry.invoice, entry.amount, entry.transaction_date)
- doc = frappe.get_doc(inv_entry.invoice_type, inv_entry.invoice)
- inv_entry.invoice_date = doc.posting_date
- inv_entry.outstanding_amount = doc.outstanding_amount
- inv_entry.allocated_amount = min(float(doc.outstanding_amount), amount)
- amount -= inv_entry.allocated_amount
- if (amount < 0): break
-
- amount = abs(entry.amount)
- if (payment is None):
- order_doctype = "Sales Order" if entry.party_type=="Customer" else "Purchase Order"
- from erpnext.controllers.accounts_controller import get_advance_payment_entries
- payment_entries = get_advance_payment_entries(entry.party_type, entry.party, entry.account, order_doctype, against_all_orders=True)
- payment_entries += self.get_matching_payments(entry.party, amount, entry.transaction_date)
- payment = next((payment for payment in payment_entries if payment.amount == amount and payment not in added_payments), None)
- if (payment is None):
- print("Failed to find payments for {0}:{1}".format(entry.party, amount))
- continue
- added_payments += [payment]
- entry.reference_type = payment.reference_type
- entry.reference_name = payment.reference_name
- entry.mode_of_payment = "Wire Transfer"
- entry.outstanding_amount = min(amount, 0)
- if (entry.payment_reference is None):
- entry.payment_reference = frappe.safe_decode(entry.description)
- entry.invoices = ",".join(matching_invoices)
- #print("Matching payment is {0}:{1}".format(entry.reference_type, entry.reference_name))
-
- def get_matching_payments(self, party, amount, pay_date):
- query = """select 'Payment Entry' as reference_type, name as reference_name, paid_amount as amount
- from `tabPayment Entry` where party='{0}' and paid_amount={1} and posting_date='{2}' and docstatus != 2
- """.format(party, amount, pay_date)
- matching_payments = frappe.db.sql(query, as_dict=True)
- return matching_payments
-
- def map_unknown_transactions(self):
- for entry in self.new_transaction_items:
- if (entry.party): continue
- inv_type = "Sales Invoice" if (entry.amount > 0) else "Purchase Invoice"
- party_type = "customer" if (entry.amount > 0) else "supplier"
-
- query = """select posting_date, name, {0}, outstanding_amount
- from `tab{1}` where ROUND(outstanding_amount)={2} and posting_date < '{3}'
- """.format(party_type, inv_type, round(abs(entry.amount)), entry.transaction_date)
- invoices = frappe.db.sql(query, as_dict = True)
- if(len(invoices) > 0):
- entry.party = invoices[0].get(party_type)
-
- def populate_matching_vouchers(self):
- for entry in self.new_transaction_items:
- if (not entry.party or entry.reference_name): continue
- print("Finding matching voucher for {0}".format(frappe.safe_decode(entry.description)))
- amount = abs(entry.amount)
- invoices = []
- vouchers = get_matching_journal_entries(self.from_date, self.to_date, entry.party, self.bank_account, amount)
- if len(vouchers) == 0: continue
- for voucher in vouchers:
- added = next((entry.invoice for entry in self.payment_invoice_items if entry.invoice == voucher.voucher_no), None)
- if (added):
- print("Found voucher {0}".format(added))
- continue
- print("Adding voucher {0} {1} {2}".format(voucher.voucher_no, voucher.posting_date, voucher.debit))
- ent = self.append('payment_invoice_items', {})
- ent.invoice_date = voucher.posting_date
- ent.invoice_type = "Journal Entry"
- ent.invoice = voucher.voucher_no
- ent.payment_description = frappe.safe_decode(entry.description)
- ent.allocated_amount = max(voucher.debit, voucher.credit)
-
- invoices += [ent.invoice_type + "|" + ent.invoice]
- entry.reference_type = "Journal Entry"
- entry.mode_of_payment = "Wire Transfer"
- entry.reference_name = ent.invoice
- #entry.account = entry.party
- entry.invoices = ",".join(invoices)
- break
-
-
- def create_payment_entries(self):
- for payment_entry in self.new_transaction_items:
- if (not payment_entry.party): continue
- if (payment_entry.reference_name): continue
- print("Creating payment entry for {0}".format(frappe.safe_decode(payment_entry.description)))
- if (payment_entry.party_type == "Account"):
- payment = self.create_journal_entry(payment_entry)
- invoices = [payment.doctype + "|" + payment.name]
- payment_entry.invoices = ",".join(invoices)
- else:
- payment = self.create_payment_entry(payment_entry)
- invoices = [entry.reference_doctype + "|" + entry.reference_name for entry in payment.references if entry is not None]
- payment_entry.invoices = ",".join(invoices)
- payment_entry.mode_of_payment = payment.mode_of_payment
- payment_entry.account = self.receivable_account if payment_entry.party_type == "Customer" else self.payable_account
- payment_entry.reference_name = payment.name
- payment_entry.reference_type = payment.doctype
- frappe.msgprint(_("Successfully created payment entries"))
-
- def create_payment_entry(self, pe):
- payment = frappe.new_doc("Payment Entry")
- payment.posting_date = pe.transaction_date
- payment.payment_type = "Receive" if pe.party_type == "Customer" else "Pay"
- payment.mode_of_payment = "Wire Transfer"
- payment.party_type = pe.party_type
- payment.party = pe.party
- payment.paid_to = self.bank_account if pe.party_type == "Customer" else self.payable_account
- payment.paid_from = self.receivable_account if pe.party_type == "Customer" else self.bank_account
- payment.paid_amount = payment.received_amount = abs(pe.amount)
- payment.reference_no = pe.description
- payment.reference_date = pe.transaction_date
- payment.save()
- for inv_entry in self.payment_invoice_items:
- if (pe.description != inv_entry.payment_description or pe.transaction_date != inv_entry.transaction_date): continue
- if (pe.party != inv_entry.party): continue
- reference = payment.append("references", {})
- reference.reference_doctype = inv_entry.invoice_type
- reference.reference_name = inv_entry.invoice
- reference.allocated_amount = inv_entry.allocated_amount
- print ("Adding invoice {0} {1}".format(reference.reference_name, reference.allocated_amount))
- payment.setup_party_account_field()
- payment.set_missing_values()
- #payment.set_exchange_rate()
- #payment.set_amounts()
- #print("Created payment entry {0}".format(payment.as_dict()))
- payment.save()
- return payment
-
- def create_journal_entry(self, pe):
- je = frappe.new_doc("Journal Entry")
- je.is_opening = "No"
- je.voucher_type = "Bank Entry"
- je.cheque_no = pe.description
- je.cheque_date = pe.transaction_date
- je.remark = pe.description
- je.posting_date = pe.transaction_date
- if (pe.amount < 0):
- je.append("accounts", {"account": pe.party, "debit_in_account_currency": abs(pe.amount)})
- je.append("accounts", {"account": self.bank_account, "credit_in_account_currency": abs(pe.amount)})
- else:
- je.append("accounts", {"account": pe.party, "credit_in_account_currency": pe.amount})
- je.append("accounts", {"account": self.bank_account, "debit_in_account_currency": pe.amount})
- je.save()
- return je
-
- def update_payment_entry(self, payment):
- lst = []
- invoices = payment.invoices.strip().split(',')
- if (len(invoices) == 0): return
- amount = float(abs(payment.amount))
- for invoice_entry in invoices:
- if (not invoice_entry.strip()): continue
- invs = invoice_entry.split('|')
- invoice_type, invoice = invs[0], invs[1]
- outstanding_amount = frappe.get_value(invoice_type, invoice, 'outstanding_amount')
-
- lst.append(frappe._dict({
- 'voucher_type': payment.reference_type,
- 'voucher_no' : payment.reference_name,
- 'against_voucher_type' : invoice_type,
- 'against_voucher' : invoice,
- 'account' : payment.account,
- 'party_type': payment.party_type,
- 'party': frappe.get_value("Payment Entry", payment.reference_name, "party"),
- 'unadjusted_amount' : float(amount),
- 'allocated_amount' : min(outstanding_amount, amount)
- }))
- amount -= outstanding_amount
- if lst:
- from erpnext.accounts.utils import reconcile_against_document
- try:
- reconcile_against_document(lst)
- except:
- frappe.throw(_("Exception occurred while reconciling {0}").format(payment.reference_name))
-
- def submit_payment_entries(self):
- for payment in self.new_transaction_items:
- if payment.reference_name is None: continue
- doc = frappe.get_doc(payment.reference_type, payment.reference_name)
- if doc.docstatus == 1:
- if (payment.reference_type == "Journal Entry"): continue
- if doc.unallocated_amount == 0: continue
- print("Reconciling payment {0}".format(payment.reference_name))
- self.update_payment_entry(payment)
- else:
- print("Submitting payment {0}".format(payment.reference_name))
- if (payment.reference_type == "Payment Entry"):
- if (payment.payment_reference):
- doc.reference_no = payment.payment_reference
- doc.mode_of_payment = payment.mode_of_payment
- doc.save()
- doc.submit()
- self.move_reconciled_entries()
- self.populate_matching_invoices()
-
- def move_reconciled_entries(self):
- idx = 0
- while idx < len(self.new_transaction_items):
- entry = self.new_transaction_items[idx]
- try:
- print("Checking transaction {0}: {2} in {1} entries".format(idx, len(self.new_transaction_items), frappe.safe_decode(entry.description)))
- except UnicodeEncodeError:
- pass
- idx += 1
- if entry.reference_name is None: continue
- doc = frappe.get_doc(entry.reference_type, entry.reference_name)
- if doc.docstatus == 1 and (entry.reference_type == "Journal Entry" or doc.unallocated_amount == 0):
- self.remove(entry)
- rc_entry = self.append('reconciled_transaction_items', {})
- dentry = entry.as_dict()
- dentry.pop('idx', None)
- rc_entry.update(dentry)
- idx -= 1
-
-
-def get_matching_journal_entries(from_date, to_date, account, against, amount):
- query = """select voucher_no, posting_date, account, against, debit_in_account_currency as debit, credit_in_account_currency as credit
- from `tabGL Entry`
- where posting_date between '{0}' and '{1}' and account = '{2}' and against = '{3}' and debit = '{4}'
- """.format(from_date, to_date, account, against, amount)
- jv_entries = frappe.db.sql(query, as_dict=True)
- #print("voucher query:{0}\n Returned {1} entries".format(query, len(jv_entries)))
- return jv_entries
-
-def get_payments_matching_invoice(invoice, amount, pay_date):
- query = """select pe.name as reference_name, per.reference_doctype as reference_type, per.outstanding_amount, per.allocated_amount
- from `tabPayment Entry Reference` as per JOIN `tabPayment Entry` as pe on pe.name = per.parent
- where per.reference_name='{0}' and (posting_date='{1}' or reference_date='{1}') and pe.docstatus != 2
- """.format(invoice, pay_date)
- payments = frappe.db.sql(query, as_dict=True)
- if (len(payments) == 0): return
- payment = next((payment for payment in payments if payment.allocated_amount == amount), payments[0])
- #Hack: Update the reference type which is set to invoice type
- payment.reference_type = "Payment Entry"
- return payment
-
-def is_headers_present(headers, row):
- for header in headers:
- if header not in row:
- return False
- return True
-
-def get_header_index(headers, row):
- header_index = {}
- for header in headers:
- if header in row:
- header_index[header] = row.index(header)
- return header_index
-
-def get_transaction_info(headers, header_index, row):
- transaction = {}
- for header in headers:
- transaction[header] = row[header_index[header]]
- if (transaction[header] == None):
- transaction[header] = ""
- return transaction
-
-def get_transaction_entries(file_url, headers):
- header_index = {}
- rows, transactions = [], []
-
- if (file_url.lower().endswith("xlsx")):
- from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
- rows = read_xlsx_file_from_attached_file(file_url=file_url)
- elif (file_url.lower().endswith("csv")):
- from frappe.utils.csvutils import read_csv_content
- _file = frappe.get_doc("File", {"file_url": file_url})
- filepath = _file.get_full_path()
- with open(filepath,'rb') as csvfile:
- rows = read_csv_content(csvfile.read())
- elif (file_url.lower().endswith("xls")):
- filename = file_url.split("/")[-1]
- rows = get_rows_from_xls_file(filename)
- else:
- frappe.throw(_("Only .csv and .xlsx files are supported currently"))
-
- stmt_headers = headers.values()
- for row in rows:
- if len(row) == 0 or row[0] == None or not row[0]: continue
- #print("Processing row {0}".format(row))
- if header_index:
- transaction = get_transaction_info(stmt_headers, header_index, row)
- transactions.append(transaction)
- elif is_headers_present(stmt_headers, row):
- header_index = get_header_index(stmt_headers, row)
- return transactions
-
-def get_rows_from_xls_file(filename):
- _file = frappe.get_doc("File", {"file_name": filename})
- filepath = _file.get_full_path()
- import xlrd
- book = xlrd.open_workbook(filepath)
- sheets = book.sheets()
- rows = []
- for row in range(1, sheets[0].nrows):
- row_values = []
- for col in range(1, sheets[0].ncols):
- row_values.append(sheets[0].cell_value(row, col))
- rows.append(row_values)
- return rows
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.js b/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.js
deleted file mode 100644
index 46d570f..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Bank Statement Transaction Entry", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially([
- // insert a new Bank Statement Transaction Entry
- () => frappe.tests.make('Bank Statement Transaction Entry', [
- // values to be set
- {key: 'value'}
- ]),
- () => {
- assert.equal(cur_frm.doc.key, 'value');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.py
deleted file mode 100644
index 4589483..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import frappe
-import unittest
-
-class TestBankStatementTransactionEntry(unittest.TestCase):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py
+++ /dev/null
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.json b/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.json
deleted file mode 100644
index d96c94d..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.json
+++ /dev/null
@@ -1,365 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-07 13:58:53.827058",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "transaction_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Transaction Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 4,
- "fieldname": "payment_description",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Payment Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "party_type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Party Type",
- "length": 0,
- "no_copy": 0,
- "options": "Customer\nSupplier\nAccount",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "party",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Party",
- "length": 0,
- "no_copy": 0,
- "options": "party_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "invoice_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Invoice Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "invoice_type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Invoice Type",
- "length": 0,
- "no_copy": 0,
- "options": "Sales Invoice\nPurchase Invoice\nJournal Entry",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "invoice",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "invoice",
- "length": 0,
- "no_copy": 0,
- "options": "invoice_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "outstanding_amount",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Outstanding Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "allocated_amount",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Allocated Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-09-14 19:03:30.949831",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Transaction Invoice Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py b/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py
deleted file mode 100644
index cb1b158..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementTransactionInvoiceItem(Document):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py
+++ /dev/null
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.json b/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.json
deleted file mode 100644
index 177dccd..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.json
+++ /dev/null
@@ -1,494 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-07 14:03:05.651413",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "transaction_date",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Transaction Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 4,
- "fieldname": "description",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "party_type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Party Type",
- "length": 0,
- "no_copy": 0,
- "options": "Customer\nSupplier\nAccount",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "party",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Party",
- "length": 0,
- "no_copy": 0,
- "options": "party_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Reference Type",
- "length": 0,
- "no_copy": 0,
- "options": "Payment Entry\nJournal Entry",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mode_of_payment",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Mode of Payment",
- "length": 0,
- "no_copy": 0,
- "options": "Mode of Payment",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "outstanding_amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "outstanding_amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_10",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "reference_name",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Reference Name",
- "length": 0,
- "no_copy": 0,
- "options": "reference_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_reference",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payment Reference",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "invoices",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Invoices",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2017-11-15 19:18:52.876221",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Transaction Payment Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py b/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py
deleted file mode 100644
index 9840c0d..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementTransactionPaymentItem(Document):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py
+++ /dev/null
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.js b/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.js
deleted file mode 100644
index 46aa4f2..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, sathishpy@gmail.com and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Bank Statement Settings', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.json b/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.json
deleted file mode 100644
index 474bb90..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.json
+++ /dev/null
@@ -1,266 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "beta": 0,
- "creation": "2017-11-13 13:38:10.863592",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank_account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Bank Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "'%d/%m/%Y'",
- "fieldname": "date_format",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Date Format",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "statement_header_mapping",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Statement Header Mapping",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "header_items",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Statement Headers",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Settings Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "transaction_data_mapping",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Transaction Data Mapping",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mapped_items",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Mapped Items",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Transaction Settings Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-01-12 10:34:32.840487",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.py b/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.py
deleted file mode 100644
index de9a85f..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementSettings(Document):
- def autoname(self):
- self.name = self.bank_account + "-Mappings"
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.js b/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.js
deleted file mode 100644
index f2381c0..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Bank Statement Settings", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially([
- // insert a new Bank Statement Settings
- () => frappe.tests.make('Bank Statement Settings', [
- // values to be set
- {key: 'value'}
- ]),
- () => {
- assert.equal(cur_frm.doc.key, 'value');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.py b/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.py
deleted file mode 100644
index aa7fe83..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import frappe
-import unittest
-
-class TestBankStatementSettings(unittest.TestCase):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py
+++ /dev/null
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.json b/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.json
deleted file mode 100644
index 47c3209..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.json
+++ /dev/null
@@ -1,166 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-13 13:42:00.335432",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Transaction",
- "fieldname": "mapping_type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Mapping Type",
- "length": 0,
- "no_copy": 0,
- "options": "Transaction",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank_data",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Bank Data",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Account",
- "fieldname": "mapped_data_type",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Mapped Data Type",
- "length": 0,
- "no_copy": 0,
- "options": "Account\nCustomer\nSupplier\nAccount",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mapped_data",
- "fieldtype": "Dynamic Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Mapped Data",
- "length": 0,
- "no_copy": 0,
- "options": "mapped_data_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-01-08 00:13:49.973501",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Transaction Settings Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py b/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py
deleted file mode 100644
index bf0a590..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementTransactionSettingsItem(Document):
- pass
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
index 8b1bab1..3758b52 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
@@ -1,32 +1,70 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Bank Transaction', {
+frappe.ui.form.on("Bank Transaction", {
onload(frm) {
- frm.set_query('payment_document', 'payment_entries', function() {
+ frm.set_query("payment_document", "payment_entries", function () {
return {
- "filters": {
- "name": ["in", ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice", "Expense Claim"]]
- }
+ filters: {
+ name: [
+ "in",
+ [
+ "Payment Entry",
+ "Journal Entry",
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Expense Claim",
+ ],
+ ],
+ },
};
});
- }
+ },
+ bank_account: function (frm) {
+ set_bank_statement_filter(frm);
+ },
+
+ setup: function (frm) {
+ frm.set_query("party_type", function () {
+ return {
+ filters: {
+ name: ["in", Object.keys(frappe.boot.party_account_types)],
+ },
+ };
+ });
+ },
});
-frappe.ui.form.on('Bank Transaction Payments', {
- payment_entries_remove: function(frm, cdt, cdn) {
+frappe.ui.form.on("Bank Transaction Payments", {
+ payment_entries_remove: function (frm, cdt, cdn) {
update_clearance_date(frm, cdt, cdn);
- }
+ },
});
const update_clearance_date = (frm, cdt, cdn) => {
if (frm.doc.docstatus === 1) {
- frappe.xcall('erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment',
- {doctype: cdt, docname: cdn})
- .then(e => {
+ frappe
+ .xcall(
+ "erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
+ { doctype: cdt, docname: cdn }
+ )
+ .then((e) => {
if (e == "success") {
- frappe.show_alert({message:__("Document {0} successfully uncleared", [e]), indicator:'green'});
+ frappe.show_alert({
+ message: __("Document {0} successfully uncleared", [e]),
+ indicator: "green",
+ });
}
});
}
-};
\ No newline at end of file
+};
+
+function set_bank_statement_filter(frm) {
+ frm.set_query("bank_statement", function () {
+ return {
+ filters: {
+ bank_account: frm.doc.bank_account,
+ },
+ };
+ });
+}
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
index 39937bb..69ee497 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -1,833 +1,245 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
"allow_import": 1,
- "allow_rename": 0,
"autoname": "naming_series:",
- "beta": 0,
"creation": "2018-10-22 18:19:02.784533",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "date",
+ "column_break_2",
+ "status",
+ "bank_account",
+ "company",
+ "section_break_4",
+ "deposit",
+ "withdrawal",
+ "column_break_7",
+ "currency",
+ "section_break_10",
+ "description",
+ "section_break_14",
+ "reference_number",
+ "transaction_id",
+ "payment_entries",
+ "section_break_18",
+ "allocated_amount",
+ "amended_from",
+ "column_break_17",
+ "unallocated_amount",
+ "party_section",
+ "party_type",
+ "party"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "ACC-BTN-.YYYY.-",
- "fetch_if_empty": 0,
"fieldname": "naming_series",
"fieldtype": "Select",
"hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Series",
- "length": 0,
"no_copy": 1,
"options": "ACC-BTN-.YYYY.-",
- "permlevel": 0,
- "precision": "",
"print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
+ "set_only_once": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "date",
"fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Date"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Pending",
- "fetch_if_empty": 0,
"fieldname": "status",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Status",
- "length": 0,
- "no_copy": 0,
- "options": "\nPending\nSettled\nUnreconciled\nReconciled",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "\nPending\nSettled\nUnreconciled\nReconciled"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "bank_account",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Bank Account",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Bank Account"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
"fetch_from": "bank_account.company",
- "fetch_if_empty": 0,
"fieldname": "company",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Company",
- "length": 0,
- "no_copy": 0,
"options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "debit",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Debit",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "credit",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Credit",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_7",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "currency",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Currency",
- "length": 0,
- "no_copy": 0,
- "options": "Currency",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Currency"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_10",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "description",
"fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Description"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_14",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "allow_on_submit": 1,
"fieldname": "reference_number",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Reference Number",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Reference Number"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "transaction_id",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Transaction ID",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
"unique": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
"allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "payment_entries",
"fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Payment Entries",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Transaction Payments",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Bank Transaction Payments"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_18",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "allocated_amount",
"fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Allocated Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Allocated Amount"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "amended_from",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Amended From",
- "length": 0,
"no_copy": 1,
"options": "Bank Transaction",
- "permlevel": 0,
"print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_17",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_if_empty": 0,
"fieldname": "unallocated_amount",
"fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Unallocated Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Unallocated Amount"
+ },
+ {
+ "fieldname": "party_section",
+ "fieldtype": "Section Break",
+ "label": "Payment From / To"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "party_type",
+ "fieldtype": "Link",
+ "label": "Party Type",
+ "options": "DocType"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "party",
+ "fieldtype": "Dynamic Link",
+ "label": "Party",
+ "options": "party_type"
+ },
+ {
+ "fieldname": "deposit",
+ "oldfieldname": "debit",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Deposit"
+ },
+ {
+ "fieldname": "withdrawal",
+ "oldfieldname": "credit",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Withdrawal"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
"is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-05-11 05:27:55.244721",
+ "links": [],
+ "modified": "2020-12-30 19:40:54.221070",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
},
{
- "amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "date",
"sort_order": "DESC",
"title_field": "bank_account",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 0e45db3..5246baa 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -11,7 +11,7 @@
class BankTransaction(StatusUpdater):
def after_insert(self):
- self.unallocated_amount = abs(flt(self.credit) - flt(self.debit))
+ self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit))
def on_submit(self):
self.clear_linked_payment_entries()
@@ -30,13 +30,13 @@
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.credit) - flt(self.debit)) - 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.credit) - flt(self.debit)))
+ frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)))
- amount = self.debit or self.credit
+ amount = self.deposit or self.withdrawal
if amount == self.allocated_amount:
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
@@ -44,18 +44,11 @@
def clear_linked_payment_entries(self):
for payment_entry in self.payment_entries:
- allocated_amount = get_total_allocated_amount(payment_entry)
- paid_amount = get_paid_amount(payment_entry, self.currency)
+ if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
+ self.clear_simple_entry(payment_entry)
- if paid_amount and allocated_amount:
- if flt(allocated_amount[0]["allocated_amount"]) > flt(paid_amount):
- frappe.throw(_("The total allocated amount ({0}) is greated than the paid amount ({1}).").format(flt(allocated_amount[0]["allocated_amount"]), flt(paid_amount)))
- else:
- if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
- self.clear_simple_entry(payment_entry)
-
- elif payment_entry.payment_document == "Sales Invoice":
- self.clear_sales_invoice(payment_entry)
+ elif payment_entry.payment_document == "Sales Invoice":
+ self.clear_sales_invoice(payment_entry)
def clear_simple_entry(self, payment_entry):
frappe.db.set_value(payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", self.date)
@@ -112,3 +105,4 @@
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
return doc.payment_entry
+
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index e9fc5f0..ce149f9 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -5,21 +5,24 @@
import frappe
import unittest
+import json
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
-from erpnext.accounts.page.bank_reconciliation.bank_reconciliation import reconcile, get_linked_payments
+from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import reconcile_vouchers, get_linked_payments
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase):
- def setUp(self):
+ @classmethod
+ def setUpClass(cls):
make_pos_profile()
add_transactions()
- add_payments()
+ add_vouchers()
- def tearDown(self):
+ @classmethod
+ def tearDownClass(cls):
for bt in frappe.get_all("Bank Transaction"):
doc = frappe.get_doc("Bank Transaction", bt.name)
doc.cancel()
@@ -32,20 +35,21 @@
# Delete POS Profile
frappe.db.sql("delete from `tabPOS Profile`")
- frappe.flags.test_bank_transactions_created = False
- frappe.flags.test_payments_created = False
-
# 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)
- self.assertTrue(linked_payments[0].party == "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/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
- payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
- reconcile(bank_transaction.name, "Payment Entry", payment.name)
+ 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}])
+ reconcile_vouchers(bank_transaction.name, vouchers)
unallocated_amount = frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount")
self.assertTrue(unallocated_amount == 0)
@@ -53,45 +57,39 @@
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
self.assertTrue(clearance_date is not None)
- # Check if ERPNext can correctly fetch a linked payment based on the party
- def test_linked_payments_based_on_party(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
- linked_payments = get_linked_payments(bank_transaction.name)
- self.assertTrue(len(linked_payments)==1)
-
# 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)
- self.assertTrue(linked_payments[0].payment_type == "Pay")
+ 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"))
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
- reconcile(bank_transaction.name, "Payment Entry", payment.name)
+ 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"))
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
- self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name)
-
- # Raise an error if creditor transaction vs creditor payment
- def test_invalid_creditor_reconcilation(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"))
- payment = frappe.get_doc("Payment Entry", dict(party="Conrad Electronic", paid_amount=690))
- self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name)
-
- # Raise an error if debitor transaction vs debitor payment
- def test_invalid_debitor_reconcilation(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
- payment = frappe.get_doc("Payment Entry", dict(party="Fayva", paid_amount=109080))
- self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name)
+ 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"))
payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"]))
- reconcile(bank_transaction.name, "Sales Invoice", payment.name)
+ 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)
@@ -116,17 +114,13 @@
pass
def add_transactions():
- if frappe.flags.test_bank_transactions_created:
- return
-
- frappe.set_user("Administrator")
create_bank_account()
doc = frappe.get_doc({
"doctype": "Bank Transaction",
"description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
"date": "2018-10-23",
- "debit": 1200,
+ "deposit": 1200,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
@@ -136,7 +130,7 @@
"doctype": "Bank Transaction",
"description":"1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
"date": "2018-10-23",
- "debit": 1700,
+ "deposit": 1700,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
@@ -146,7 +140,7 @@
"doctype": "Bank Transaction",
"description":"Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
"date": "2018-10-26",
- "debit": 690,
+ "withdrawal": 690,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
@@ -156,7 +150,7 @@
"doctype": "Bank Transaction",
"description":"Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
"date": "2018-10-27",
- "debit": 3900,
+ "deposit": 3900,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
@@ -166,20 +160,14 @@
"doctype": "Bank Transaction",
"description":"I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
"date": "2018-10-27",
- "credit": 109080,
+ "withdrawal": 109080,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
doc.submit()
- frappe.flags.test_bank_transactions_created = True
-def add_payments():
- if frappe.flags.test_payments_created:
- return
-
- frappe.set_user("Administrator")
-
+def add_vouchers():
try:
frappe.get_doc({
"doctype": "Supplier",
@@ -192,6 +180,7 @@
pass
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
+
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
@@ -242,10 +231,15 @@
except frappe.DuplicateEntryError:
pass
- pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900)
+ 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()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
+ pe.paid_amount = 690
+ pe.received_amount = 690
pe.insert()
pe.submit()
@@ -266,13 +260,6 @@
except frappe.DuplicateEntryError:
pass
- si = create_sales_invoice(customer="Fayva", qty=1, rate=109080)
- pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
- pe.reference_no = "Fayva Oct 18"
- pe.reference_date = "2018-10-29"
- pe.insert()
- pe.submit()
-
mode_of_payment = frappe.get_doc({
"doctype": "Mode of Payment",
"name": "Cash"
@@ -285,14 +272,12 @@
})
mode_of_payment.save()
- si = create_sales_invoice(customer="Fayva", qty=1, rate=109080, do_not_submit=1)
+ 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.save()
+ si.insert()
si.submit()
-
- frappe.flags.test_payments_created = True
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/c_form/c_form.py b/erpnext/accounts/doctype/c_form/c_form.py
index 9b64f81..fd86ed4 100644
--- a/erpnext/accounts/doctype/c_form/c_form.py
+++ b/erpnext/accounts/doctype/c_form/c_form.py
@@ -57,6 +57,7 @@
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 """
if invoice_no:
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 342f21b..03c3eb0 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
@@ -22,9 +22,10 @@
'allow_account_creation_against_child_company'])
if parent_company and (not allow_account_creation_against_child_company):
- frappe.throw(_("""{0} is a child company. Please import accounts against parent company
- or enable {1} in company master""").format(frappe.bold(company),
- frappe.bold('Allow Account Creation Against Child Company')), title='Wrong 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'))
if frappe.db.get_all('GL Entry', {"company": company}, "name", limit=1):
return False
@@ -74,7 +75,9 @@
if as_dict:
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
- if not row[1]: row[1] = row[0]
+ if not row[1]:
+ row[1] = row[0]
+ row[3] = row[2]
data.append(row)
# convert csv data
@@ -96,7 +99,9 @@
if as_dict:
data.append({frappe.scrub(header): row[index] for index, header in enumerate(headers)})
else:
- if not row[1]: row[1] = row[0]
+ if not row[1]:
+ row[1] = row[0]
+ row[3] = row[2]
data.append(row)
return data
@@ -147,7 +152,13 @@
from frappe import _
for row in data:
- account_name, parent_account = row[0:2]
+ account_name, parent_account, account_number, parent_account_number = row[0:4]
+ if account_number:
+ account_name = "{} - {}".format(account_number, account_name)
+ if parent_account_number:
+ parent_account_number = cstr(parent_account_number).strip()
+ parent_account = "{} - {}".format(parent_account_number, parent_account)
+
if parent_account == account_name == child:
return [parent_account]
elif account_name == child:
@@ -159,20 +170,23 @@
charts_map, paths = {}, []
- line_no = 3
+ line_no = 2
error_messages = []
for i in data:
- account_name, dummy, 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))
+ if account_number:
+ account_number = cstr(account_number).strip()
+ account_name = "{} - {}".format(account_number, account_name)
+
charts_map[account_name] = {}
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
- if account_number: charts_map[account_name]["account_number"] = account_number
path = return_parent(data, account_name)[::-1]
paths.append(path) # List of path is created
line_no += 1
@@ -221,7 +235,7 @@
def get_template(template_type):
- fields = ["Account Name", "Parent Account", "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)
@@ -241,23 +255,23 @@
def get_sample_template(writer):
template = [
- ["Application Of Funds(Assets)", "", "", 1, "", "Asset"],
- ["Sources Of Funds(Liabilities)", "", "", 1, "", "Liability"],
- ["Equity", "", "", 1, "", "Equity"],
- ["Expenses", "", "", 1, "", "Expense"],
- ["Income", "", "", 1, "", "Income"],
- ["Bank Accounts", "Application Of Funds(Assets)", "", 1, "Bank", "Asset"],
- ["Cash In Hand", "Application Of Funds(Assets)", "", 1, "Cash", "Asset"],
- ["Stock Assets", "Application Of Funds(Assets)", "", 1, "Stock", "Asset"],
- ["Cost Of Goods Sold", "Expenses", "", 0, "Cost of Goods Sold", "Expense"],
- ["Asset Depreciation", "Expenses", "", 0, "Depreciation", "Expense"],
- ["Fixed Assets", "Application Of Funds(Assets)", "", 0, "Fixed Asset", "Asset"],
- ["Accounts Payable", "Sources Of Funds(Liabilities)", "", 0, "Payable", "Liability"],
- ["Accounts Receivable", "Application Of Funds(Assets)", "", 1, "Receivable", "Asset"],
- ["Stock Expenses", "Expenses", "", 0, "Stock Adjustment", "Expense"],
- ["Sample Bank", "Bank Accounts", "", 0, "Bank", "Asset"],
- ["Cash", "Cash In Hand", "", 0, "Cash", "Asset"],
- ["Stores", "Stock Assets", "", 0, "Stock", "Asset"],
+ ["Application Of Funds(Assets)", "", "", "", 1, "", "Asset"],
+ ["Sources Of Funds(Liabilities)", "", "", "", 1, "", "Liability"],
+ ["Equity", "", "", "", 1, "", "Equity"],
+ ["Expenses", "", "", "", 1, "", "Expense"],
+ ["Income", "", "", "", 1, "", "Income"],
+ ["Bank Accounts", "Application Of Funds(Assets)", "", "", 1, "Bank", "Asset"],
+ ["Cash In Hand", "Application Of Funds(Assets)", "", "", 1, "Cash", "Asset"],
+ ["Stock Assets", "Application Of Funds(Assets)", "", "", 1, "Stock", "Asset"],
+ ["Cost Of Goods Sold", "Expenses", "", "", 0, "Cost of Goods Sold", "Expense"],
+ ["Asset Depreciation", "Expenses", "", "", 0, "Depreciation", "Expense"],
+ ["Fixed Assets", "Application Of Funds(Assets)", "", "", 0, "Fixed Asset", "Asset"],
+ ["Accounts Payable", "Sources Of Funds(Liabilities)", "", "", 0, "Payable", "Liability"],
+ ["Accounts Receivable", "Application Of Funds(Assets)", "", "", 1, "Receivable", "Asset"],
+ ["Stock Expenses", "Expenses", "", "", 0, "Stock Adjustment", "Expense"],
+ ["Sample Bank", "Bank Accounts", "", "", 0, "Bank", "Asset"],
+ ["Cash", "Cash In Hand", "", "", 0, "Cash", "Asset"],
+ ["Stores", "Stock Assets", "", "", 0, "Stock", "Asset"],
]
for row in template:
diff --git a/erpnext/accounts/doctype/cost_center/cost_center.py b/erpnext/accounts/doctype/cost_center/cost_center.py
index 12094d4..8a5473f 100644
--- a/erpnext/accounts/doctype/cost_center/cost_center.py
+++ b/erpnext/accounts/doctype/cost_center/cost_center.py
@@ -50,6 +50,7 @@
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):
if self.check_if_child_exists():
frappe.throw(_("Cannot convert Cost Center to ledger as it has child nodes"))
@@ -60,6 +61,7 @@
self.save()
return 1
+ @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"))
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 9594706..c1b8ba7 100644
--- a/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
+++ b/erpnext/accounts/doctype/exchange_rate_revaluation/exchange_rate_revaluation.py
@@ -27,6 +27,7 @@
if not (self.company and self.posting_date):
frappe.throw(_("Please select Company and Posting Date to getting entries"))
+ @frappe.whitelist()
def get_accounts_data(self, account=None):
accounts = []
self.validate_mandatory()
@@ -95,6 +96,7 @@
message = _("No outstanding invoices found")
frappe.msgprint(message)
+ @frappe.whitelist()
def make_jv_entry(self):
if self.total_gain_loss == 0:
return
diff --git a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
index da6a3fd..4255626 100644
--- a/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
+++ b/erpnext/accounts/doctype/fiscal_year/fiscal_year.py
@@ -12,6 +12,7 @@
class FiscalYearIncorrectDate(frappe.ValidationError): pass
class FiscalYear(Document):
+ @frappe.whitelist()
def set_as_default(self):
frappe.db.set_value("Global Defaults", None, "current_fiscal_year", self.name)
global_defaults = frappe.get_doc("Global Defaults")
@@ -54,7 +55,7 @@
def on_update(self):
check_duplicate_fiscal_year(self)
frappe.cache().delete_value("fiscal_years")
-
+
def on_trash(self):
global_defaults = frappe.get_doc("Global Defaults")
if global_defaults.current_fiscal_year == self.name:
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index b0a864f..78febf9 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -27,30 +27,30 @@
def validate(self):
self.flags.ignore_submit_comment = True
- self.check_mandatory()
self.validate_and_set_fiscal_year()
self.pl_must_have_cost_center()
- self.validate_cost_center()
if not self.flags.from_repost:
+ self.check_mandatory()
+ self.validate_cost_center()
self.check_pl_account()
self.validate_party()
self.validate_currency()
- def on_update_with_args(self, adv_adj, update_outstanding = 'Yes', from_repost=False):
- if not from_repost:
+ def on_update(self):
+ adv_adj = self.flags.adv_adj
+ if not self.flags.from_repost:
self.validate_account_details(adv_adj)
self.validate_dimensions_for_pl_and_bs()
self.validate_allowed_dimensions()
+ validate_balance_type(self.account, adv_adj)
+ validate_frozen_account(self.account, adv_adj)
- validate_frozen_account(self.account, adv_adj)
- validate_balance_type(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 update_outstanding == 'Yes' and not from_repost:
- update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
- self.against_voucher)
+ # 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'):
+ 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']
@@ -58,7 +58,7 @@
if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
- account_type = frappe.db.get_value("Account", self.account, "account_type")
+ account_type = frappe.get_cached_value("Account", self.account, "account_type")
if not (self.party_type and self.party):
if account_type == "Receivable":
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
@@ -73,7 +73,7 @@
.format(self.voucher_type, self.voucher_no, self.account))
def pl_must_have_cost_center(self):
- if frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss":
+ if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.")
.format(self.voucher_type, self.voucher_no, self.account))
@@ -140,25 +140,16 @@
.format(self.voucher_type, self.voucher_no, self.account, self.company))
def validate_cost_center(self):
- if not hasattr(self, "cost_center_company"):
- self.cost_center_company = {}
+ if not self.cost_center: return
- def _get_cost_center_company():
- if not self.cost_center_company.get(self.cost_center):
- self.cost_center_company[self.cost_center] = frappe.db.get_value(
- "Cost Center", self.cost_center, "company")
+ is_group, company = frappe.get_cached_value('Cost Center',
+ self.cost_center, ['is_group', 'company'])
- return self.cost_center_company[self.cost_center]
-
- def _check_is_group():
- return cint(frappe.get_cached_value('Cost Center', self.cost_center, 'is_group'))
-
- if self.cost_center and _get_cost_center_company() != self.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))
- if not self.flags.from_repost and not self.voucher_type == 'Period Closing Voucher' \
- and self.cost_center and _check_is_group():
+ 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)))
@@ -184,7 +175,6 @@
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")
@@ -250,7 +240,7 @@
def validate_frozen_account(account, adv_adj=None):
- frozen_account = frappe.db.get_value("Account", account, "freeze_account")
+ 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')
@@ -300,4 +290,8 @@
oldname = doc.name
set_name_from_naming_options(frappe.get_meta(doctype).autoname, doc)
newname = doc.name
- frappe.db.sql("""UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s""".format(doctype), (newname, oldname))
+ frappe.db.sql(
+ "UPDATE `tab{}` SET name = %s, to_rename = 0 where name = %s".format(doctype),
+ (newname, oldname),
+ auto_commit=True
+ )
diff --git a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
index af8940c..7b62b61 100644
--- a/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
+++ b/erpnext/accounts/doctype/invoice_discounting/invoice_discounting.py
@@ -125,6 +125,7 @@
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'
@@ -174,6 +175,7 @@
return je
+ @frappe.whitelist()
def close_loan(self):
je = frappe.new_doc("Journal Entry")
je.voucher_type = 'Journal Entry'
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 8915f79..77c9e95 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.json
@@ -1,7 +1,7 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
- "autoname": "field:title",
"creation": "2018-11-22 22:45:00.370913",
"doctype": "DocType",
"document_type": "Setup",
@@ -20,8 +20,7 @@
"in_list_view": 1,
"label": "Title",
"no_copy": 1,
- "reqd": 1,
- "unique": 1
+ "reqd": 1
},
{
"fieldname": "taxes",
@@ -33,12 +32,14 @@
{
"fieldname": "company",
"fieldtype": "Link",
+ "in_list_view": 1,
"label": "Company",
"options": "Company",
"reqd": 1
}
],
- "modified": "2020-09-18 17:26:09.703215",
+ "links": [],
+ "modified": "2021-03-08 19:50:21.416513",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Item Tax Template",
@@ -81,5 +82,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
+ "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 e77481d..d9155cb 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template.py
@@ -11,6 +11,11 @@
def validate(self):
self.validate_tax_accounts()
+ 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)
+
def validate_tax_accounts(self):
"""Check whether Tax Rate is not entered twice for same Tax Type"""
check_list = []
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index cb90f80..ff2c8c2 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -102,7 +102,7 @@
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"))
-
+
def validate_stock_accounts(self):
stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
for account in stock_accounts:
@@ -229,11 +229,11 @@
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(_("For {0}, only credit accounts can be linked against another debit entry")
- .format(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(_("For {0}, only debit accounts can be linked against another credit entry")
- .format(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"))
@@ -564,6 +564,7 @@
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'):
msgprint(_("'Entries' cannot be empty"), raise_exception=True)
@@ -1077,4 +1078,4 @@
},
}, target_doc)
- return doclist
\ No newline at end of file
+ return doclist
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 d54a47e..3247369 100644
--- a/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
+++ b/erpnext/accounts/doctype/mode_of_payment/mode_of_payment.py
@@ -12,7 +12,7 @@
self.validate_accounts()
self.validate_repeating_companies()
self.validate_pos_mode_of_payment()
-
+
def validate_repeating_companies(self):
"""Error when Same Company is entered multiple times in accounts"""
accounts_list = []
@@ -31,10 +31,10 @@
def validate_pos_mode_of_payment(self):
if not self.enabled:
- pos_profiles = frappe.db.sql("""SELECT sip.parent FROM `tabSales Invoice Payment` sip
+ 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."
diff --git a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
index 18f853c..88667d7 100644
--- a/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
+++ b/erpnext/accounts/doctype/monthly_distribution/monthly_distribution.py
@@ -8,6 +8,7 @@
from frappe.model.document import Document
class MonthlyDistribution(Document):
+ @frappe.whitelist()
def get_months(self):
month_list = ['January','February','March','April','May','June','July','August','September',
'October','November','December']
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 76027a3..29dc96e 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
@@ -167,6 +167,7 @@
return invoice
+ @frappe.whitelist()
def make_invoices(self):
self.validate_company()
invoices = self.get_invoices()
@@ -198,6 +199,7 @@
try:
publish(idx, len(invoices), d.doctype)
doc = frappe.get_doc(d)
+ doc.flags.ignore_mandatory = True
doc.insert()
doc.submit()
frappe.db.commit()
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 bdfe532..8d6de2d 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
@@ -6,10 +6,12 @@
import frappe
import unittest
-test_dependencies = ["Customer", "Supplier"]
+from frappe.cache_manager import clear_doctype_cache
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account
+test_dependencies = ["Customer", "Supplier"]
+
class TestOpeningInvoiceCreationTool(unittest.TestCase):
def setUp(self):
if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
@@ -24,22 +26,25 @@
def test_opening_sales_invoice_creation(self):
property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
- invoices = self.make_invoices(company="_Test Opening Invoice Company")
+ try:
+ 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)
+ # Check if update stock is not enabled
+ self.assertEqual(si.update_stock, 0)
- property_setter.delete()
+ finally:
+ property_setter.delete()
+ clear_doctype_cache("Sales Invoice")
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
@@ -143,4 +148,4 @@
customer.insert(ignore_permissions=True)
return customer.name
else:
- return frappe.db.exists("Customer", customer_name)
\ No newline at end of file
+ return frappe.db.exists("Customer", customer_name)
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index f5c488d..c2e804e 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -92,14 +92,16 @@
});
frm.set_query("reference_doctype", "references", function() {
- if (frm.doc.party_type=="Customer") {
+ if (frm.doc.party_type == "Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
- } else if (frm.doc.party_type=="Supplier") {
+ } else if (frm.doc.party_type == "Supplier") {
var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"];
- } else if (frm.doc.party_type=="Employee") {
+ } else if (frm.doc.party_type == "Employee") {
var doctypes = ["Expense Claim", "Journal Entry"];
- } else if (frm.doc.party_type=="Student") {
+ } else if (frm.doc.party_type == "Student") {
var doctypes = ["Fees"];
+ } else if (frm.doc.party_type == "Donor") {
+ var doctypes = ["Donation"];
} else {
var doctypes = ["Journal Entry"];
}
@@ -128,7 +130,7 @@
const child = locals[cdt][cdn];
const filters = {"docstatus": 1, "company": doc.company};
const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice',
- 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning'];
+ 'Purchase Order', 'Expense Claim', 'Fees', 'Dunning', 'Donation'];
if (in_list(party_type_doctypes, child.reference_doctype)) {
filters[doc.party_type.toLowerCase()] = doc.party;
@@ -281,7 +283,7 @@
let party_types = Object.keys(frappe.boot.party_account_types);
if(frm.doc.party_type && !party_types.includes(frm.doc.party_type)){
frm.set_value("party_type", "");
- frappe.throw(__("Party can only be one of "+ party_types.join(", ")));
+ frappe.throw(__("Party can only be one of {0}", [party_types.join(", ")]));
}
frm.set_query("party", function() {
@@ -603,12 +605,22 @@
{fieldtype:"Column Break"},
{fieldtype:"Float", label: __("Less Than Amount"), fieldname:"outstanding_amt_less_than"},
{fieldtype:"Section Break"},
+ {fieldtype:"Link", label:__("Cost Center"), fieldname:"cost_center", options:"Cost Center",
+ "get_query": function() {
+ return {
+ "filters": {"company": frm.doc.company}
+ }
+ }
+ },
+ {fieldtype:"Column Break"},
+ {fieldtype:"Section Break"},
{fieldtype:"Check", label: __("Allocate Payment Amount"), fieldname:"allocate_payment_amount", default:1},
];
frappe.prompt(fields, function(filters){
frappe.flags.allocate_payment_amount = true;
frm.events.validate_filters_data(frm, filters);
+ frm.doc.cost_center = filters.cost_center;
frm.events.get_outstanding_documents(frm, filters);
}, __("Filters"), __("Get Outstanding Documents"));
},
@@ -625,13 +637,13 @@
let to_field = fields[key][1];
if (filters[from_field] && !filters[to_field]) {
- frappe.throw(__("Error: {0} is mandatory field",
- [to_field.replace(/_/g, " ")]
- ));
+ frappe.throw(
+ __("Error: {0} is mandatory field", [to_field.replace(/_/g, " ")])
+ );
} else if (filters[from_field] && filters[from_field] > filters[to_field]) {
- frappe.throw(__("{0}: {1} must be less than {2}",
- [key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")]
- ));
+ frappe.throw(
+ __("{0}: {1} must be less than {2}", [key, from_field.replace(/_/g, " "), to_field.replace(/_/g, " ")])
+ );
}
}
},
@@ -680,6 +692,8 @@
c.total_amount = d.invoice_amount;
c.outstanding_amount = d.outstanding_amount;
c.bill_no = d.bill_no;
+ c.payment_term = d.payment_term;
+ c.allocated_amount = d.allocated_amount;
if(!in_list(["Sales Order", "Purchase Order", "Expense Claim", "Fees"], d.voucher_type)) {
if(flt(d.outstanding_amount) > 0)
@@ -705,7 +719,8 @@
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
- (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student")
+ (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
+ (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
) {
if(total_positive_outstanding > total_negative_outstanding)
if (!frm.doc.paid_amount)
@@ -748,7 +763,8 @@
(frm.doc.payment_type=="Receive" && frm.doc.party_type=="Customer") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Supplier") ||
(frm.doc.payment_type=="Pay" && frm.doc.party_type=="Employee") ||
- (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student")
+ (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Student") ||
+ (frm.doc.payment_type=="Receive" && frm.doc.party_type=="Donor")
) {
if(total_positive_outstanding_including_order > paid_amount) {
var remaining_outstanding = total_positive_outstanding_including_order - paid_amount;
@@ -760,12 +776,15 @@
} else if (in_list(["Customer", "Supplier"], frm.doc.party_type)) {
if(paid_amount > total_negative_outstanding) {
if(total_negative_outstanding == 0) {
- frappe.msgprint(__("Cannot {0} {1} {2} without any negative outstanding invoice",
- [frm.doc.payment_type,
- (frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type]));
+ frappe.msgprint(
+ __("Cannot {0} {1} {2} without any negative outstanding invoice", [frm.doc.payment_type,
+ (frm.doc.party_type=="Customer" ? "to" : "from"), frm.doc.party_type])
+ );
return false
} else {
- frappe.msgprint(__("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding]));
+ frappe.msgprint(
+ __("Paid Amount cannot be greater than total negative outstanding amount {0}", [total_negative_outstanding])
+ );
return false;
}
} else {
@@ -777,10 +796,13 @@
}
$.each(frm.doc.references || [], function(i, row) {
- row.allocated_amount = 0 //If allocate payment amount checkbox is unchecked, set zero to allocate amount
- if(frappe.flags.allocate_payment_amount != 0){
- if(row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
- if(row.outstanding_amount >= allocated_positive_outstanding) {
+ if (frappe.flags.allocate_payment_amount == 0) {
+ //If allocate payment amount checkbox is unchecked, set zero to allocate amount
+ row.allocated_amount = 0;
+
+ } else if (frappe.flags.allocate_payment_amount != 0 && !row.allocated_amount) {
+ if (row.outstanding_amount > 0 && allocated_positive_outstanding > 0) {
+ if (row.outstanding_amount >= allocated_positive_outstanding) {
row.allocated_amount = allocated_positive_outstanding;
} else {
row.allocated_amount = row.outstanding_amount;
@@ -788,9 +810,11 @@
allocated_positive_outstanding -= flt(row.allocated_amount);
} else if (row.outstanding_amount < 0 && allocated_negative_outstanding) {
- if(Math.abs(row.outstanding_amount) >= allocated_negative_outstanding)
+ if (Math.abs(row.outstanding_amount) >= allocated_negative_outstanding) {
row.allocated_amount = -1*allocated_negative_outstanding;
- else row.allocated_amount = row.outstanding_amount;
+ } else {
+ row.allocated_amount = row.outstanding_amount;
+ };
allocated_negative_outstanding -= Math.abs(flt(row.allocated_amount));
}
@@ -905,6 +929,12 @@
frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Expense Claim or Journal Entry", [row.idx]));
return false;
}
+
+ if (frm.doc.party_type == "Donor" && row.reference_doctype != "Donation") {
+ frappe.model.set_value(row.doctype, row.name, "reference_doctype", null);
+ frappe.msgprint(__("Row #{0}: Reference Document Type must be Donation", [row.idx]));
+ return false;
+ }
}
if (row) {
@@ -1056,11 +1086,6 @@
frm.set_value("paid_from_account_balance", r.message.paid_from_account_balance);
frm.set_value("paid_to_account_balance", r.message.paid_to_account_balance);
frm.set_value("party_balance", r.message.party_balance);
- },
- () => {
- if(frm.doc.payment_type != "Internal") {
- frm.clear_table("references");
- }
}
]);
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 2e1f201..328584a 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -536,7 +536,8 @@
"fieldtype": "Data",
"hidden": 1,
"label": "Title",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1
},
{
"depends_on": "party",
@@ -588,7 +589,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 13:56:20.007336",
+ "modified": "2021-03-08 13:05:16.958866",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
@@ -632,4 +633,4 @@
"sort_order": "DESC",
"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 31a4c8a..62ab76c 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -72,6 +72,7 @@
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_expense_claim()
+ self.update_donation()
self.update_payment_schedule()
self.set_status()
@@ -82,6 +83,7 @@
self.update_outstanding_amounts()
self.update_advance_paid()
self.update_expense_claim()
+ self.update_donation(cancel=1)
self.delink_advance_entry_references()
self.update_payment_schedule(cancel=1)
self.set_payment_req_status()
@@ -242,9 +244,11 @@
elif self.party_type == "Supplier":
valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry")
elif self.party_type == "Employee":
- valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance")
+ valid_reference_doctypes = ("Expense Claim", "Journal Entry", "Employee Advance", "Gratuity")
elif self.party_type == "Shareholder":
valid_reference_doctypes = ("Journal Entry")
+ elif self.party_type == "Donor":
+ valid_reference_doctypes = ("Donation")
for d in self.get("references"):
if not d.allocated_amount:
@@ -329,33 +333,50 @@
invoice_payment_amount_map = {}
invoice_paid_amount_map = {}
- for reference in self.get('references'):
- if reference.payment_term and reference.reference_name:
- key = (reference.payment_term, reference.reference_name)
+ 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)
- invoice_payment_amount_map[key] += reference.allocated_amount
+ invoice_payment_amount_map[key] += ref.allocated_amount
if not invoice_paid_amount_map.get(key):
- payment_schedule = frappe.get_all('Payment Schedule', filters={'parent': reference.reference_name},
- fields=['paid_amount', 'payment_amount', 'payment_term'])
+ payment_schedule = frappe.get_all(
+ '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, reference.reference_name)
+ invoice_key = (term.payment_term, ref.reference_name)
invoice_paid_amount_map.setdefault(invoice_key, {})
- invoice_paid_amount_map[invoice_key]['outstanding'] = term.payment_amount - term.paid_amount
+ invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
+ invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
- for key, amount in iteritems(invoice_payment_amount_map):
+ for key, allocated_amount in iteritems(invoice_payment_amount_map):
+ 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(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` - %s
- WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
+ 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]))
else:
- outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
-
- if amount > outstanding:
+ if allocated_amount > outstanding:
frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0]))
- if amount and outstanding:
- frappe.db.sql(""" UPDATE `tabPayment Schedule` SET paid_amount = `paid_amount` + %s
- WHERE parent = %s and payment_term = %s""", (amount, key[1], key[0]))
+ if allocated_amount and outstanding:
+ 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]))
def set_status(self):
if self.docstatus == 2:
@@ -455,6 +476,10 @@
.format(total_negative_outstanding), InvalidPaymentEntry)
def set_title(self):
+ if frappe.flags.in_import and self.title:
+ # do not set title dynamically if title exists during data import.
+ return
+
if self.payment_type in ("Receive", "Pay"):
self.title = self.party
else:
@@ -604,7 +629,7 @@
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"):
+ 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):
@@ -614,6 +639,13 @@
doc = frappe.get_doc("Expense Claim", d.reference_name)
update_reimbursed_amount(doc, self.name)
+ 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:
+ is_paid = 0 if cancel else 1
+ frappe.db.set_value("Donation", d.reference_name, "paid", is_paid)
+
def on_recurring(self, reference_doc, auto_repeat_doc):
self.reference_no = reference_doc.name
self.reference_date = nowdate()
@@ -693,6 +725,8 @@
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)
+
for d in outstanding_invoices:
d["exchange_rate"] = 1
if party_account_currency != company_currency:
@@ -720,6 +754,46 @@
return data
+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 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')
+ if allocate_payment_based_on_payment_terms:
+ 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,
+ 'allocated_amount': payment_term.outstanding
+ }))
+
+ if invoice_ref_based_on_payment_terms:
+ for idx, ref in invoice_ref_based_on_payment_terms.items():
+ voucher_no = outstanding_invoices[idx]['voucher_no']
+ voucher_type = outstanding_invoices[idx]['voucher_type']
+
+ frappe.msgprint(_("Spliting {} {} into {} rows as per payment terms").format(
+ voucher_type, voucher_no, len(ref)), alert=True)
+
+ outstanding_invoices.pop(idx - 1)
+ outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
+
+ return outstanding_invoices
+
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":
@@ -913,6 +987,9 @@
total_amount = ref_doc.get("grand_total")
exchange_rate = 1
outstanding_amount = ref_doc.get("outstanding_amount")
+ elif reference_doctype == "Donation":
+ total_amount = ref_doc.get("amount")
+ exchange_rate = 1
elif reference_doctype == "Dunning":
total_amount = ref_doc.get("dunning_amount")
exchange_rate = 1
@@ -932,6 +1009,8 @@
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
if not total_amount:
if party_account_currency == company_currency:
total_amount = ref_doc.base_grand_total
@@ -955,6 +1034,8 @@
outstanding_amount = flt(outstanding_amount) * flt(exchange_rate)
if party_account_currency == company_currency:
exchange_rate = 1
+ elif reference_doctype == "Gratuity":
+ outstanding_amount = ref_doc.amount - flt(ref_doc.paid_amount)
else:
outstanding_amount = flt(total_amount) - flt(ref_doc.advance_paid)
else:
@@ -996,7 +1077,7 @@
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)
-
+
if not total_amount:
total_amount, exchange_rate = get_total_amount_exchange_rate_base_on_currency(
party_account_currency, company_currency, ref_doc)
@@ -1069,6 +1150,8 @@
paid_amount, received_amount = set_paid_amount_and_received_amount(
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)
+
pe = frappe.new_doc("Payment Entry")
pe.payment_type = payment_type
pe.company = doc.company
@@ -1138,11 +1221,20 @@
pe.setup_party_account_field()
pe.set_missing_values()
+
if party_account and bank:
if dt == "Employee Advance":
reference_doc = doc
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_difference_amount()
+
return pe
def get_bank_cash_account(doc, bank_account):
@@ -1160,10 +1252,12 @@
party_type = "Customer"
elif dt in ("Purchase Invoice", "Purchase Order"):
party_type = "Supplier"
- elif dt in ("Expense Claim", "Employee Advance"):
+ elif dt in ("Expense Claim", "Employee Advance", "Gratuity"):
party_type = "Employee"
- elif dt in ("Fees"):
+ elif dt == "Fees":
party_type = "Student"
+ elif dt == "Donation":
+ party_type = "Donor"
return party_type
def set_party_account(dt, dn, doc, party_type):
@@ -1177,6 +1271,8 @@
party_account = doc.advance_account
elif dt == "Expense Claim":
party_account = doc.payable_account
+ elif dt == "Gratuity":
+ party_account = doc.payable_account
else:
party_account = get_party_account(party_type, doc.get(party_type.lower()), doc.company)
return party_account
@@ -1189,7 +1285,7 @@
return party_account_currency
def set_payment_type(dt, doc):
- if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \
+ 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:
@@ -1222,6 +1318,12 @@
elif dt == "Dunning":
grand_total = doc.grand_total
outstanding_amount = doc.grand_total
+ elif dt == "Donation":
+ grand_total = doc.amount
+ outstanding_amount = doc.amount
+ elif dt == "Gratuity":
+ grand_total = doc.amount
+ outstanding_amount = flt(doc.amount) - flt(doc.paid_amount)
else:
if party_account_currency == doc.company_currency:
grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total)
@@ -1253,6 +1355,33 @@
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:
+ 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)
+ else:
+ discount_amount = term.discount
+
+ discount_amount_in_foreign_currency = discount_amount * doc.get('conversion_rate', 1)
+
+ if doc.doctype == 'Sales Invoice':
+ paid_amount -= discount_amount
+ received_amount -= discount_amount_in_foreign_currency
+ else:
+ received_amount -= discount_amount
+ paid_amount -= discount_amount_in_foreign_currency
+
+ total_discount += discount_amount
+
+ if total_discount:
+ 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):
references = []
for payment_term in payment_schedule:
@@ -1326,4 +1455,4 @@
}, target_doc, set_missing_values)
- return doclist
\ No newline at end of file
+ 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 772fc1a..4641d6b 100644
--- a/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/test_payment_entry.py
@@ -193,6 +193,34 @@
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'
+
+ 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.save()
+
+ si.submit()
+
+ pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Cash - _TC")
+ pe.submit()
+ si.load_from_db()
+
+ 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",
@@ -591,6 +619,26 @@
}]
}).insert()
+def create_payment_terms_template_with_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()
def create_payment_term(name):
if not frappe.db.exists('Payment Term', name):
diff --git a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
index 8f5e9fb..912ad09 100644
--- a/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
+++ b/erpnext/accounts/doctype/payment_entry_reference/payment_entry_reference.json
@@ -58,7 +58,7 @@
"fieldname": "total_amount",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Total Amount",
+ "label": "Grand Total",
"print_hide": 1,
"read_only": 1
},
@@ -92,9 +92,10 @@
"options": "Payment Term"
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-03-13 12:07:19.362539",
+ "modified": "2021-02-10 11:25:47.144392",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry Reference",
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index f7a15c0..cf6ec18 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -11,6 +11,7 @@
from erpnext.controllers.accounts_controller import get_advance_payment_entries
class PaymentReconciliation(Document):
+ @frappe.whitelist()
def get_unreconciled_entries(self):
self.get_nonreconciled_payment_entries()
self.get_invoice_entries()
@@ -147,6 +148,7 @@
ent.currency = e.get('currency')
ent.outstanding_amount = e.get('outstanding_amount')
+ @frappe.whitelist()
def reconcile(self, args):
for e in self.get('payments'):
e.invoice_type = None
@@ -197,6 +199,7 @@
'difference_account': row.difference_account
})
+ @frappe.whitelist()
def get_difference_amount(self, child_row):
if child_row.get("reference_type") != 'Payment Entry': return
diff --git a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
index d363cf1..e362566 100644
--- a/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
+++ b/erpnext/accounts/doctype/payment_schedule/payment_schedule.json
@@ -6,11 +6,23 @@
"engine": "InnoDB",
"field_order": [
"payment_term",
+ "section_break_15",
"description",
+ "section_break_4",
"due_date",
- "invoice_portion",
- "payment_amount",
"mode_of_payment",
+ "column_break_5",
+ "invoice_portion",
+ "section_break_6",
+ "discount_type",
+ "discount_date",
+ "column_break_9",
+ "discount",
+ "section_break_9",
+ "payment_amount",
+ "discounted_amount",
+ "column_break_3",
+ "outstanding",
"paid_amount"
],
"fields": [
@@ -25,6 +37,7 @@
},
{
"columns": 2,
+ "fetch_from": "payment_term.description",
"fieldname": "description",
"fieldtype": "Small Text",
"in_list_view": 1,
@@ -62,14 +75,82 @@
"options": "Mode of Payment"
},
{
+ "depends_on": "paid_amount",
"fieldname": "paid_amount",
"fieldtype": "Currency",
"label": "Paid Amount"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "depends_on": "discounted_amount",
+ "fieldname": "discounted_amount",
+ "fieldtype": "Currency",
+ "label": "Discounted Amount",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "payment_amount",
+ "fieldname": "outstanding",
+ "fieldtype": "Currency",
+ "label": "Outstanding",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "discount",
+ "fieldname": "discount_date",
+ "fieldtype": "Date",
+ "label": "Discount Date",
+ "mandatory_depends_on": "discount"
+ },
+ {
+ "default": "Percentage",
+ "fetch_from": "payment_term.discount_type",
+ "fieldname": "discount_type",
+ "fieldtype": "Select",
+ "label": "Discount Type",
+ "options": "Percentage\nAmount"
+ },
+ {
+ "fetch_from": "payment_term.discount",
+ "fieldname": "discount",
+ "fieldtype": "Float",
+ "label": "Discount"
+ },
+ {
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_15",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-03-13 17:58:24.729526",
+ "modified": "2021-02-15 21:03:12.540546",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Schedule",
diff --git a/erpnext/accounts/doctype/payment_term/payment_term.js b/erpnext/accounts/doctype/payment_term/payment_term.js
index 054c2d1..acd0144 100644
--- a/erpnext/accounts/doctype/payment_term/payment_term.js
+++ b/erpnext/accounts/doctype/payment_term/payment_term.js
@@ -1,2 +1,22 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.ui.form.on('Payment Term', {
+ onload(frm) {
+ frm.trigger('set_dynamic_description');
+ },
+ discount(frm) {
+ frm.trigger('set_dynamic_description');
+ },
+ discount_type(frm) {
+ frm.trigger('set_dynamic_description');
+ },
+ set_dynamic_description(frm) {
+ if (frm.doc.discount) {
+ let description = __("{0}% of total invoice value will be given as discount.", [frm.doc.discount]);
+ if (frm.doc.discount_type == 'Amount') {
+ description = __("{0} will be given as discount.", [fmt_money(frm.doc.discount)]);
+ }
+ frm.set_df_property("discount", "description", description);
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_term/payment_term.json b/erpnext/accounts/doctype/payment_term/payment_term.json
index e77c244..aec4965 100644
--- a/erpnext/accounts/doctype/payment_term/payment_term.json
+++ b/erpnext/accounts/doctype/payment_term/payment_term.json
@@ -1,386 +1,166 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 1,
- "allow_rename": 1,
- "autoname": "field:payment_term_name",
- "beta": 0,
- "creation": "2017-08-10 15:24:54.876365",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "allow_import": 1,
+ "allow_rename": 1,
+ "autoname": "field:payment_term_name",
+ "creation": "2017-08-10 15:24:54.876365",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "payment_term_name",
+ "invoice_portion",
+ "mode_of_payment",
+ "column_break_3",
+ "due_date_based_on",
+ "credit_days",
+ "credit_months",
+ "section_break_8",
+ "discount_type",
+ "discount",
+ "column_break_11",
+ "discount_validity_based_on",
+ "discount_validity",
+ "section_break_6",
+ "description"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_term_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payment Term Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "bold": 1,
+ "fieldname": "payment_term_name",
+ "fieldtype": "Data",
+ "label": "Payment Term Name",
+ "unique": 1
+ },
{
- "description": "Provide the invoice portion in percent",
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "invoice_portion",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Invoice Portion",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "bold": 1,
+ "fieldname": "invoice_portion",
+ "fieldtype": "Float",
+ "label": "Invoice Portion (%)"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mode_of_payment",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Mode of Payment",
- "length": 0,
- "no_copy": 0,
- "options": "Mode of Payment",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "due_date_based_on",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Due Date Based On",
- "length": 0,
- "no_copy": 0,
- "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "bold": 1,
+ "fieldname": "due_date_based_on",
+ "fieldtype": "Select",
+ "label": "Due Date Based On",
+ "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
+ },
{
- "description": "Give number of days according to prior selection",
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
- "fieldname": "credit_days",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Credit Days",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "bold": 1,
+ "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
+ "fieldname": "credit_days",
+ "fieldtype": "Int",
+ "label": "Credit Days"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
- "fieldname": "credit_months",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Credit Months",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
+ "fieldname": "credit_months",
+ "fieldtype": "Int",
+ "label": "Credit Months"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "bold": 1,
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "label": "Description"
+ },
+ {
+ "fieldname": "section_break_8",
+ "fieldtype": "Section Break",
+ "label": "Discount Settings"
+ },
+ {
+ "default": "Percentage",
+ "fieldname": "discount_type",
+ "fieldtype": "Select",
+ "label": "Discount Type",
+ "options": "Percentage\nAmount"
+ },
+ {
+ "fieldname": "discount",
+ "fieldtype": "Float",
+ "label": "Discount"
+ },
+ {
+ "default": "Day(s) after invoice date",
+ "depends_on": "discount",
+ "fieldname": "discount_validity_based_on",
+ "fieldtype": "Select",
+ "label": "Discount Validity Based On",
+ "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
+ },
+ {
+ "depends_on": "discount",
+ "fieldname": "discount_validity",
+ "fieldtype": "Int",
+ "label": "Discount Validity",
+ "mandatory_depends_on": "discount"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2020-10-14 10:47:32.830478",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Payment Term",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "links": [],
+ "modified": "2021-02-15 20:30:56.256403",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Term",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
index f5c5bca..84c8d09 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.js
@@ -3,11 +3,6 @@
frappe.ui.form.on('Payment Terms Template', {
setup: function(frm) {
- frm.add_fetch("payment_term", "description", "description");
- frm.add_fetch("payment_term", "invoice_portion", "invoice_portion");
- frm.add_fetch("payment_term", "due_date_based_on", "due_date_based_on");
- frm.add_fetch("payment_term", "credit_days", "credit_days");
- frm.add_fetch("payment_term", "credit_months", "credit_months");
- frm.add_fetch("payment_term", "mode_of_payment", "mode_of_payment");
+
}
});
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 2b2b6af..80e3348 100644
--- a/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
+++ b/erpnext/accounts/doctype/payment_terms_template/payment_terms_template.py
@@ -13,7 +13,6 @@
class PaymentTermsTemplate(Document):
def validate(self):
self.validate_invoice_portion()
- self.validate_credit_days()
self.check_duplicate_terms()
def validate_invoice_portion(self):
@@ -24,11 +23,6 @@
if flt(total_portion, 2) != 100.00:
frappe.msgprint(_('Combined invoice portion must equal 100%'), raise_exception=1, indicator='red')
- def validate_credit_days(self):
- for term in self.terms:
- if cint(term.credit_days) < 0:
- frappe.msgprint(_('Credit Days cannot be a negative number'), raise_exception=1, indicator='red')
-
def check_duplicate_terms(self):
terms = []
for term in self.terms:
diff --git a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json
index eee3223..20b3dca 100644
--- a/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json
+++ b/erpnext/accounts/doctype/payment_terms_template_detail/payment_terms_template_detail.json
@@ -1,278 +1,164 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "",
- "beta": 0,
- "creation": "2017-08-10 15:34:09.409562",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-08-10 15:34:09.409562",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "payment_term",
+ "section_break_13",
+ "description",
+ "section_break_4",
+ "invoice_portion",
+ "mode_of_payment",
+ "column_break_3",
+ "due_date_based_on",
+ "credit_days",
+ "credit_months",
+ "section_break_8",
+ "discount_type",
+ "discount",
+ "column_break_11",
+ "discount_validity_based_on",
+ "discount_validity"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "payment_term",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Payment Term",
- "length": 0,
- "no_copy": 0,
- "options": "Payment Term",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fieldname": "payment_term",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Payment Term",
+ "options": "Payment Term"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "description",
- "fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fetch_from": "payment_term.description",
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "default": "0",
- "fieldname": "invoice_portion",
- "fieldtype": "Percent",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Invoice Portion",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fetch_from": "payment_term.invoice_portion",
+ "fetch_if_empty": 1,
+ "fieldname": "invoice_portion",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Invoice Portion (%)",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "due_date_based_on",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Due Date Based On",
- "length": 0,
- "no_copy": 0,
- "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fetch_from": "payment_term.due_date_based_on",
+ "fetch_if_empty": 1,
+ "fieldname": "due_date_based_on",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Due Date Based On",
+ "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "default": "0",
- "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
- "fieldname": "credit_days",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Credit Days",
- "length": 0,
- "no_copy": 0,
- "options": "",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 2,
+ "default": "0",
+ "depends_on": "eval:in_list(['Day(s) after invoice date', 'Day(s) after the end of the invoice month'], doc.due_date_based_on)",
+ "fetch_from": "payment_term.credit_days",
+ "fetch_if_empty": 1,
+ "fieldname": "credit_days",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Credit Days",
+ "non_negative": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "0",
- "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
- "fieldname": "credit_months",
- "fieldtype": "Int",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Credit Months",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "0",
+ "depends_on": "eval:doc.due_date_based_on=='Month(s) after the end of the invoice month'",
+ "fetch_from": "payment_term.credit_months",
+ "fetch_if_empty": 1,
+ "fieldname": "credit_months",
+ "fieldtype": "Int",
+ "label": "Credit Months",
+ "non_negative": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mode_of_payment",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Mode of Payment",
- "length": 0,
- "no_copy": 0,
- "options": "Mode of Payment",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fetch_from": "payment_term.mode_of_payment",
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_8",
+ "fieldtype": "Section Break",
+ "label": "Discount Settings"
+ },
+ {
+ "default": "Percentage",
+ "fetch_from": "payment_term.discount_type",
+ "fetch_if_empty": 1,
+ "fieldname": "discount_type",
+ "fieldtype": "Select",
+ "label": "Discount Type",
+ "options": "Percentage\nAmount"
+ },
+ {
+ "fetch_from": "payment_term.discount",
+ "fetch_if_empty": 1,
+ "fieldname": "discount",
+ "fieldtype": "Float",
+ "label": "Discount"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "Day(s) after invoice date",
+ "depends_on": "discount",
+ "fetch_from": "payment_term.discount_validity_based_on",
+ "fetch_if_empty": 1,
+ "fieldname": "discount_validity_based_on",
+ "fieldtype": "Select",
+ "label": "Discount Validity Based On",
+ "options": "Day(s) after invoice date\nDay(s) after the end of the invoice month\nMonth(s) after the end of the invoice month"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_13",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
+ {
+ "depends_on": "discount",
+ "fetch_from": "payment_term.discount_validity",
+ "fetch_if_empty": 1,
+ "fieldname": "discount_validity",
+ "fieldtype": "Int",
+ "label": "Discount Validity",
+ "mandatory_depends_on": "discount"
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-08-21 16:15:55.143025",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Payment Terms Template Detail",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-24 11:56:12.410807",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Payment Terms Template Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "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 f5224a2..a05e598 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -18,7 +18,7 @@
self.validate_pos_closing()
self.validate_pos_invoices()
-
+
def validate_pos_closing(self):
user = frappe.db.sql("""
SELECT name FROM `tabPOS Closing Entry`
@@ -37,12 +37,12 @@
bold_user = frappe.bold(self.user)
frappe.throw(_("POS Closing Entry {} against {} between selected period")
.format(bold_already_exists, bold_user), title=_("Invalid Period"))
-
+
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,
+ 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")))
@@ -68,14 +68,15 @@
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})
-
+
def on_submit(self):
consolidate_pos_invoices(closing_entry=self)
-
+
def on_cancel(self):
unconsolidate_pos_invoices(closing_entry=self)
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 40db09e..b596c0c 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
@@ -5,12 +5,21 @@
import frappe
import unittest
from frappe.utils import nowdate
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import make_closing_entry_from_opening
from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPOSClosingEntry(unittest.TestCase):
+ def setUp(self):
+ # Make stock available for POS Sales
+ make_stock_entry(target="_Test Warehouse - _TC", qty=2, basic_rate=100)
+
+ def tearDown(self):
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+
def test_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
@@ -41,9 +50,6 @@
self.assertEqual(pcv_doc.total_quantity, 2)
self.assertEqual(pcv_doc.net_total, 6700)
- frappe.set_user("Administrator")
- frappe.db.sql("delete from `tabPOS Profile`")
-
def test_cancelling_of_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile()
opening_entry = create_opening_entry(pos_profile, test_user.name)
@@ -84,8 +90,6 @@
self.assertEqual(si_doc.docstatus, 2)
self.assertEqual(pos_inv1.status, 'Paid')
- frappe.set_user("Administrator")
- frappe.db.sql("delete from `tabPOS Profile`")
def init_user_and_profile(**args):
user = 'test@example.com'
@@ -103,4 +107,4 @@
pos_profile.save()
- return test_user, pos_profile
\ No newline at end of file
+ return test_user, pos_profile
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 94573f9..832fb80 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -57,7 +57,7 @@
self.apply_loyalty_points()
self.check_phone_payments()
self.set_status(update=True)
-
+
def before_cancel(self):
if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1:
pos_closing_entry = frappe.get_all(
@@ -179,10 +179,18 @@
if d.get("serial_no"):
serial_nos = get_serial_nos(d.serial_no)
for sr in serial_nos:
- serial_no_exists = frappe.db.exists("POS Invoice Item", {
- "parent": self.return_against,
- "serial_no": ["like", d.get("serial_no")]
- })
+ serial_no_exists = frappe.db.sql("""
+ SELECT name
+ FROM `tabPOS Invoice Item`
+ WHERE
+ parent = %s
+ and (serial_no = %s
+ or serial_no like %s
+ or serial_no like %s
+ or serial_no like %s
+ )
+ """, (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)
@@ -190,7 +198,7 @@
_("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")
@@ -213,7 +221,7 @@
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)
@@ -292,7 +300,7 @@
if not self.get('payments') and not for_validate:
update_multi_mode_option(self, profile)
-
+
if self.is_return and not for_validate:
add_return_modes(self, profile)
@@ -347,6 +355,7 @@
return profile
+ @frappe.whitelist()
def set_missing_values(self, for_validate=False):
profile = self.set_pos_fields(for_validate)
@@ -369,12 +378,20 @@
"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)
+ 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")
+ @frappe.whitelist()
def create_payment_request(self):
for pay in self.payments:
if pay.type == "Phone":
@@ -392,7 +409,7 @@
pay_req.request_phone_payment()
return pay_req
-
+
def get_new_payment_request(self, mop):
payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
"payment_account": mop.account,
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index 57a23af..6d388c4 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -9,8 +9,20 @@
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+from erpnext.stock.doctype.item.test_item import make_item
class TestPOSInvoice(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ frappe.db.sql("delete from `tabTax Rule`")
+
+ def tearDown(self):
+ if frappe.session.user != "Administrator":
+ frappe.set_user("Administrator")
+
+ if frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
+ frappe.db.set_value("Selling Settings", None, "validate_selling_price", 0)
+
def test_timestamp_change(self):
w = create_pos_invoice(do_not_save=1)
w.docstatus = 0
@@ -99,10 +111,10 @@
item_row = inv.get("items")[0]
add_items = [
- (54, '_Test Account Excise Duty @ 12'),
- (288, '_Test Account Excise Duty @ 15'),
- (144, '_Test Account Excise Duty @ 20'),
- (430, '_Test Item Tax Template 1')
+ (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)
@@ -198,6 +210,65 @@
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.stock_entry.test_stock_entry import make_serialized_item
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ 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.get("items")[0].serial_no = serial_nos[0]
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+
+ pos.insert()
+ pos.submit()
+
+ pos_return = make_sales_return(pos.name)
+
+ pos_return.insert()
+ pos_return.submit()
+ self.assertEqual(pos_return.get('items')[0].serial_no, serial_nos[0])
+
+ def test_partial_pos_returns(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ 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.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.insert()
+ pos.submit()
+
+ 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.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])
+
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,
@@ -311,7 +382,6 @@
pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 3470)
- frappe.set_user("Administrator")
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
@@ -353,7 +423,6 @@
pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 840)
- frappe.set_user("Administrator")
def test_merging_with_validate_selling_price(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
@@ -362,10 +431,12 @@
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
- make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=1, rate=300)
+ item = "Test Selling Price Validation"
+ make_item(item, {"is_stock_item": 1})
+ make_purchase_receipt(item_code=item, warehouse="_Test Warehouse - _TC", qty=1, rate=300)
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 = create_pos_invoice(item=item, rate=300, do_not_submit=1)
pos_inv.append('payments', {
'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300
})
@@ -379,7 +450,7 @@
})
self.assertRaises(frappe.ValidationError, pos_inv.submit)
- pos_inv2 = create_pos_invoice(rate=400, do_not_submit=1)
+ 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
})
@@ -398,8 +469,6 @@
pos_inv2.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
self.assertEqual(rounded_total, 400)
- frappe.set_user("Administrator")
- frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 0)
def create_pos_invoice(**args):
args = frappe._dict(args)
@@ -449,4 +518,4 @@
else:
pos_inv.payment_schedule = []
- return pos_inv
\ No newline at end of file
+ return pos_inv
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 2b6e7de..8b71eb0 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -87,6 +87,7 @@
"edit_references",
"sales_order",
"so_detail",
+ "pos_invoice_item",
"column_break_74",
"delivery_note",
"dn_detail",
@@ -790,11 +791,20 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
+ },
+ {
+ "fieldname": "pos_invoice_item",
+ "fieldtype": "Data",
+ "ignore_user_permissions": 1,
+ "label": "POS Invoice Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-07-22 13:40:34.418346",
+ "modified": "2021-01-04 17:34:49.924531",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
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 c88d679..40f77b4 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
@@ -29,7 +29,7 @@
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'])
-
+
bold_pos_invoice = frappe.bold(d.pos_invoice)
bold_status = frappe.bold(status)
if docstatus != 1:
@@ -58,7 +58,7 @@
sales_invoice, credit_note = "", ""
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
-
+
if returns:
credit_note = self.process_merging_into_credit_note(returns)
@@ -74,7 +74,7 @@
def process_merging_into_sales_invoice(self, data):
sales_invoice = self.get_new_sales_invoice()
-
+
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1
@@ -98,19 +98,19 @@
self.consolidated_credit_note = credit_note.name
return credit_note.name
-
+
def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], []
loyalty_amount_sum, loyalty_points_sum = 0, 0
for doc in data:
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
-
+
if doc.redeem_loyalty_points:
invoice.loyalty_redemption_account = doc.loyalty_redemption_account
invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center
loyalty_points_sum += doc.loyalty_points
loyalty_amount_sum += doc.loyalty_amount
-
+
for item in doc.get('items'):
found = False
for i in items:
@@ -118,12 +118,13 @@
i.uom == item.uom and i.net_rate == item.net_rate):
found = True
i.qty = i.qty + item.qty
+
if not found:
item.rate = item.net_rate
item.price_list_rate = 0
si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
items.append(si_item)
-
+
for tax in doc.get('taxes'):
found = False
for t in taxes:
@@ -162,7 +163,7 @@
invoice.ignore_pricing_rule = 1
return invoice
-
+
def get_new_sales_invoice(self):
sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer
@@ -194,7 +195,7 @@
}
pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
-
+
return pos_invoices
def get_invoice_customer_map(pos_invoices):
@@ -204,7 +205,7 @@
customer = invoice.get('customer')
pos_invoice_customer_map.setdefault(customer, [])
pos_invoice_customer_map[customer].append(invoice)
-
+
return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
index db046c9..d880caa 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -14,85 +14,89 @@
def test_consolidated_invoice_creation(self):
frappe.db.sql("delete from `tabPOS Invoice`")
- test_user, pos_profile = init_user_and_profile()
+ try:
+ 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.submit()
+ 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.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.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.submit()
- pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
- pos_inv3.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
- })
- pos_inv3.submit()
+ pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
+ pos_inv3.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
+ })
+ pos_inv3.submit()
- consolidate_pos_invoices()
+ consolidate_pos_invoices()
- pos_inv.load_from_db()
- self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
+ pos_inv.load_from_db()
+ self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
- pos_inv3.load_from_db()
- self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
+ pos_inv3.load_from_db()
+ self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
- self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
+ self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice)
- frappe.set_user("Administrator")
- frappe.db.sql("delete from `tabPOS Profile`")
- frappe.db.sql("delete from `tabPOS Invoice`")
-
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
+
def test_consolidated_credit_note_creation(self):
frappe.db.sql("delete from `tabPOS Invoice`")
- test_user, pos_profile = init_user_and_profile()
+ try:
+ 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.submit()
+ 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.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.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.submit()
- pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
- pos_inv3.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
- })
- pos_inv3.submit()
+ pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1)
+ pos_inv3.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300
+ })
+ pos_inv3.submit()
- pos_inv_cn = make_sales_return(pos_inv.name)
- pos_inv_cn.set("payments", [])
- pos_inv_cn.append('payments', {
- 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
- })
- pos_inv_cn.paid_amount = -300
- pos_inv_cn.submit()
+ pos_inv_cn = make_sales_return(pos_inv.name)
+ pos_inv_cn.set("payments", [])
+ pos_inv_cn.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300
+ })
+ pos_inv_cn.paid_amount = -300
+ pos_inv_cn.submit()
- consolidate_pos_invoices()
+ consolidate_pos_invoices()
- pos_inv.load_from_db()
- self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
+ pos_inv.load_from_db()
+ self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
- pos_inv3.load_from_db()
- self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
+ pos_inv3.load_from_db()
+ self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice))
- pos_inv_cn.load_from_db()
- self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
- self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
+ pos_inv_cn.load_from_db()
+ self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice))
+ self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return"))
- frappe.set_user("Administrator")
- frappe.db.sql("delete from `tabPOS Profile`")
- frappe.db.sql("delete from `tabPOS Invoice`")
+ finally:
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+ frappe.db.sql("delete from `tabPOS Invoice`")
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
index cb5b3a5..0023a84 100644
--- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
@@ -20,15 +20,16 @@
if not cint(frappe.db.get_value("User", self.user, "enabled")):
frappe.throw(_("User {} is disabled. Please select valid user/cashier").format(self.user))
-
+
def validate_payment_method_account(self):
invalid_modes = []
for d in self.balance_details:
- account = frappe.db.get_value("Mode of Payment Account",
- {"parent": d.mode_of_payment, "company": self.company}, "default_account")
- if not account:
- invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
-
+ if d.mode_of_payment:
+ account = frappe.db.get_value("Mode of Payment Account",
+ {"parent": d.mode_of_payment, "company": self.company}, "default_account")
+ if not account:
+ invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment))
+
if invalid_modes:
if invalid_modes == 1:
msg = _("Please set default Cash or Bank account in Mode of Payment {}")
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
index d08a854..428989a 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
@@ -44,6 +44,14 @@
"column_break_21",
"min_amt",
"max_amt",
+ "product_discount_scheme_section",
+ "same_item",
+ "free_item",
+ "free_qty",
+ "free_item_rate",
+ "column_break_42",
+ "free_item_uom",
+ "is_recursive",
"section_break_23",
"valid_from",
"valid_upto",
@@ -62,13 +70,6 @@
"discount_amount",
"discount_percentage",
"for_price_list",
- "product_discount_scheme_section",
- "same_item",
- "free_item",
- "free_qty",
- "column_break_51",
- "free_item_uom",
- "free_item_rate",
"section_break_13",
"threshold_percentage",
"priority",
@@ -357,7 +358,6 @@
"reqd": 1
},
{
- "depends_on": "eval: doc.selling == 1",
"fieldname": "margin",
"fieldtype": "Section Break",
"label": "Margin"
@@ -460,10 +460,6 @@
"label": "Qty"
},
{
- "fieldname": "column_break_51",
- "fieldtype": "Column Break"
- },
- {
"fieldname": "free_item_uom",
"fieldtype": "Link",
"label": "UOM",
@@ -553,19 +549,33 @@
"fieldname": "promotional_scheme",
"fieldtype": "Link",
"label": "Promotional Scheme",
- "options": "Promotional Scheme"
+ "no_copy": 1,
+ "options": "Promotional Scheme",
+ "print_hide": 1,
+ "read_only": 1
},
{
"description": "Simple Python Expression, Example: territory != 'All Territories'",
"fieldname": "condition",
"fieldtype": "Code",
"label": "Condition"
+ },
+ {
+ "fieldname": "column_break_42",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "description": "Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on",
+ "fieldname": "is_recursive",
+ "fieldtype": "Check",
+ "label": "Is Recursive"
}
],
"icon": "fa fa-gift",
"idx": 1,
"links": [],
- "modified": "2020-12-04 00:36:24.698219",
+ "modified": "2021-03-06 22:01:24.840422",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index 0565264..aedf1c6 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -136,7 +136,7 @@
for d in self.items:
max_discount = frappe.get_cached_value("Item", d.item_code, "max_discount")
if max_discount and flt(self.discount_percentage) > flt(max_discount):
- throw(_("Max discount allowed for item: {0} is {1}%").format(self.item_code, max_discount))
+ throw(_("Max discount allowed for item: {0} is {1}%").format(d.item_code, max_discount))
def validate_price_list_with_currency(self):
if self.currency and self.for_price_list:
@@ -237,6 +237,7 @@
"doctype": args.doctype,
"has_margin": False,
"name": args.name,
+ "free_item_data": [],
"parent": args.parent,
"parenttype": args.parenttype,
"child_docname": args.get('child_docname')
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index d163335..c676abd 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -367,7 +367,7 @@
if items and doc.get("items"):
for row in doc.get('items'):
- if row.get(apply_on) not in items: continue
+ if (row.get(apply_on) or args.get(apply_on)) not in items: continue
if pr_doc.mixed_conditions:
amt = args.get('qty') * args.get("price_list_rate")
@@ -479,7 +479,7 @@
doc.calculate_taxes_and_totals()
elif d.price_or_product_discount == 'Product':
- item_details = frappe._dict({'parenttype': doc.doctype})
+ item_details = frappe._dict({'parenttype': doc.doctype, 'free_item_data': []})
get_product_discount_rule(d, item_details, doc=doc)
apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values()
@@ -508,9 +508,16 @@
frappe.throw(_("Free item not set in the pricing rule {0}")
.format(get_link_to_form("Pricing Rule", pricing_rule.name)))
- item_details.free_item_data = {
+ qty = pricing_rule.free_qty or 1
+ if pricing_rule.is_recursive:
+ transaction_qty = args.get('qty') if args else doc.total_qty
+ if transaction_qty:
+ qty = flt(transaction_qty) * qty
+
+ free_item_data_args = {
'item_code': free_item,
- 'qty': pricing_rule.free_qty or 1,
+ 'qty': qty,
+ 'pricing_rules': pricing_rule.name,
'rate': pricing_rule.free_item_rate or 0,
'price_list_rate': pricing_rule.free_item_rate or 0,
'is_free_item': 1
@@ -519,24 +526,26 @@
item_data = frappe.get_cached_value('Item', free_item, ['item_name',
'description', 'stock_uom'], as_dict=1)
- item_details.free_item_data.update(item_data)
- item_details.free_item_data['uom'] = pricing_rule.free_item_uom or item_data.stock_uom
- item_details.free_item_data['conversion_factor'] = get_conversion_factor(free_item,
- item_details.free_item_data['uom']).get("conversion_factor", 1)
+ free_item_data_args.update(item_data)
+ free_item_data_args['uom'] = pricing_rule.free_item_uom or item_data.stock_uom
+ free_item_data_args['conversion_factor'] = get_conversion_factor(free_item,
+ free_item_data_args['uom']).get("conversion_factor", 1)
if item_details.get("parenttype") == 'Purchase Order':
- item_details.free_item_data['schedule_date'] = doc.schedule_date if doc else today()
+ free_item_data_args['schedule_date'] = doc.schedule_date if doc else today()
if item_details.get("parenttype") == 'Sales Order':
- item_details.free_item_data['delivery_date'] = doc.delivery_date if doc else today()
+ free_item_data_args['delivery_date'] = doc.delivery_date if doc else today()
+
+ item_details.free_item_data.append(free_item_data_args)
def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False):
- if pricing_rule_args.get('item_code'):
- items = [d.item_code for d in doc.items
- if d.item_code == (pricing_rule_args.get("item_code")) and d.is_free_item]
+ if pricing_rule_args:
+ items = tuple([(d.item_code, d.pricing_rules) for d in doc.items if d.is_free_item])
- if not items:
- doc.append('items', pricing_rule_args)
+ for args in pricing_rule_args:
+ if not items or (args.get('item_code'), args.get('pricing_rules')) not in items:
+ doc.append('items', args)
def get_pricing_rule_items(pr_doc):
apply_on_data = []
diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
index 89f7238..523e9ee 100644
--- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
+++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
@@ -12,16 +12,16 @@
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']
+ 'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules']
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']
+ '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']
+ 'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules']
class PromotionalScheme(Document):
def validate(self):
diff --git a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json
index 224b8de..795fb1c 100644
--- a/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json
+++ b/erpnext/accounts/doctype/promotional_scheme_price_discount/promotional_scheme_price_discount.json
@@ -1,792 +1,181 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
+ "actions": [],
"creation": "2019-03-24 14:48:59.649168",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "disable",
+ "apply_multiple_pricing_rules",
+ "column_break_2",
+ "rule_description",
+ "section_break_2",
+ "min_qty",
+ "max_qty",
+ "column_break_3",
+ "min_amount",
+ "max_amount",
+ "section_break_6",
+ "rate_or_discount",
+ "column_break_10",
+ "rate",
+ "discount_amount",
+ "discount_percentage",
+ "section_break_11",
+ "warehouse",
+ "threshold_percentage",
+ "validate_applied_rule",
+ "column_break_14",
+ "priority",
+ "apply_discount_on_rate"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"fieldname": "disable",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Disable",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Disable"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "rule_description",
"fieldtype": "Small Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Rule Description",
- "length": 0,
"no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "reqd": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "section_break_2",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
"columns": 1,
"default": "0",
"fieldname": "min_qty",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Min Qty",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Min Qty"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
"columns": 1,
"default": "0",
"fieldname": "max_qty",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Max Qty",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Max Qty"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0",
"fieldname": "min_amount",
"fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Min Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Min Amount"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0",
- "depends_on": "",
"fieldname": "max_amount",
"fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Max Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Max Amount"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
"fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Discount Percentage",
- "depends_on": "",
"fieldname": "rate_or_discount",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Discount Type",
- "length": 0,
- "no_copy": 0,
- "options": "\nRate\nDiscount Percentage\nDiscount Amount",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "\nRate\nDiscount Percentage\nDiscount Amount"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
"fieldname": "column_break_10",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
"columns": 2,
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate",
"fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Rate",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Rate"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.rate_or_discount==\"Discount Amount\"",
"fieldname": "discount_amount",
"fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Discount Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Discount Amount"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.rate_or_discount==\"Discount Percentage\"",
"fieldname": "discount_percentage",
"fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Discount Percentage",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Discount Percentage"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "section_break_11",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "warehouse",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Warehouse",
- "length": 0,
- "no_copy": 0,
- "options": "Warehouse",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Warehouse"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "threshold_percentage",
"fieldtype": "Percent",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Threshold for Suggestion",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Threshold for Suggestion"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "1",
"fieldname": "validate_applied_rule",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Validate Applied Rule",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Validate Applied Rule"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "column_break_14",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "priority",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Priority",
- "length": 0,
- "no_copy": 0,
- "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "\n1\n2\n3\n4\n5\n6\n7\n8\n9\n10\n11\n12\n13\n14\n15\n16\n17\n18\n19\n20"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"depends_on": "priority",
"fieldname": "apply_multiple_pricing_rules",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Apply Multiple Pricing Rules",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Apply Multiple Pricing Rules"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "0",
"depends_on": "eval:in_list(['Discount Percentage', 'Discount Amount'], doc.rate_or_discount) && doc.apply_multiple_pricing_rules",
"fieldname": "apply_discount_on_rate",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Apply Discount on Rate",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Apply Discount on Rate"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
+ "index_web_pages_for_search": 1,
"istable": 1,
- "max_attachments": 0,
- "modified": "2019-03-24 14:48:59.649168",
+ "links": [],
+ "modified": "2021-03-07 11:56:23.424137",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Promotional Scheme Price Discount",
- "name_case": "",
"owner": "Administrator",
"permissions": [],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json b/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json
index 72d53bf..3eab515 100644
--- a/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json
+++ b/erpnext/accounts/doctype/promotional_scheme_product_discount/promotional_scheme_product_discount.json
@@ -1,10 +1,12 @@
{
+ "actions": [],
"creation": "2019-03-24 14:48:59.649168",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"disable",
+ "apply_multiple_pricing_rules",
"column_break_2",
"rule_description",
"section_break_1",
@@ -25,7 +27,7 @@
"threshold_percentage",
"column_break_15",
"priority",
- "apply_multiple_pricing_rules"
+ "is_recursive"
],
"fields": [
{
@@ -152,10 +154,19 @@
"fieldname": "apply_multiple_pricing_rules",
"fieldtype": "Check",
"label": "Apply Multiple Pricing Rules"
+ },
+ {
+ "default": "0",
+ "description": "Discounts to be applied in sequential ranges like buy 1 get 1, buy 2 get 2, buy 3 get 3 and so on",
+ "fieldname": "is_recursive",
+ "fieldtype": "Check",
+ "label": "Is Recursive"
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-07-21 00:00:56.674284",
+ "links": [],
+ "modified": "2021-03-06 21:58:18.162346",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Promotional Scheme Product Discount",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 06aa20b..66a8e20 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -524,7 +524,7 @@
},
onload: function(frm) {
- if(frm.doc.__onload) {
+ if(frm.doc.__onload && frm.is_new()) {
if(frm.doc.supplier) {
frm.doc.apply_tds = frm.doc.__onload.supplier_tds ? 1 : 0;
}
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 451c936..18b6637 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -58,6 +58,7 @@
"rejected_warehouse",
"col_break_warehouse",
"set_from_warehouse",
+ "supplier_warehouse",
"is_subcontracted",
"items_section",
"update_stock",
@@ -1350,7 +1351,7 @@
"options": "Company"
},
{
- "depends_on": "eval:doc.update_stock && (doc.is_subcontracted==\"Yes\" || doc.is_internal_supplier)",
+ "depends_on": "eval:doc.update_stock && doc.is_internal_supplier",
"description": "Sets 'From Warehouse' in each row of the items table.",
"fieldname": "set_from_warehouse",
"fieldtype": "Link",
@@ -1360,13 +1361,24 @@
"print_hide": 1,
"print_width": "50px",
"width": "50px"
+ },
+ {
+ "depends_on": "eval:doc.update_stock && doc.is_subcontracted==\"Yes\"",
+ "fieldname": "supplier_warehouse",
+ "fieldtype": "Link",
+ "label": "Supplier Warehouse",
+ "no_copy": 1,
+ "options": "Warehouse",
+ "print_hide": 1,
+ "print_width": "50px",
+ "width": "50px"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-26 20:49:03.305063",
+ "modified": "2021-03-09 21:12:30.422084",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index dacd50a..5c4e32e 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -968,7 +968,7 @@
# 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 self.rounding_adjustment and self.base_rounding_adjustment:
+ 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)
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 2c088ce..50492f5 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -456,7 +456,7 @@
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,
+ 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")
@@ -898,7 +898,7 @@
acc_settings.submit_journal_entries = 1
acc_settings.save()
- item = create_item("_Test Item for Deferred Accounting")
+ item = create_item("_Test Item for Deferred Accounting", is_purchase_item=True)
item.enable_deferred_expense = 1
item.deferred_expense_account = deferred_account
item.save()
@@ -1031,7 +1031,7 @@
pi.is_return = args.is_return
pi.credit_to = args.return_against or "Creditors - _TC"
pi.is_subcontracted = args.is_subcontracted or "No"
- if args.supplier_warehouse:
+ if args.supplier_warehouse:
pi.supplier_warehouse = "_Test Warehouse 1 - _TC"
pi.append("items", {
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_records.json b/erpnext/accounts/doctype/purchase_invoice/test_records.json
index 7030faf..9f9e90d 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_records.json
+++ b/erpnext/accounts/doctype/purchase_invoice/test_records.json
@@ -18,7 +18,7 @@
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"item_code": "_Test Item Home Desktop 100",
"item_name": "_Test Item Home Desktop 100",
- "item_tax_template": "_Test Account Excise Duty @ 10",
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"parentfield": "items",
"qty": 10,
"rate": 50,
@@ -43,7 +43,7 @@
}
],
"grand_total": 0,
- "naming_series": "_T-BILL",
+ "naming_series": "T-PINV-",
"taxes": [
{
"account_head": "_Test Account Shipping Charges - _TC",
@@ -167,7 +167,7 @@
}
],
"grand_total": 0,
- "naming_series": "_T-Purchase Invoice-",
+ "naming_series": "T-PINV-",
"taxes": [
{
"account_head": "_Test Account Shipping Charges - _TC",
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 07e75ac..96ad0fd 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -28,10 +28,16 @@
"stock_qty",
"sec_break1",
"price_list_rate",
- "discount_percentage",
- "discount_amount",
"col_break3",
"base_price_list_rate",
+ "section_break_26",
+ "margin_type",
+ "margin_rate_or_amount",
+ "rate_with_margin",
+ "column_break_30",
+ "discount_percentage",
+ "discount_amount",
+ "base_rate_with_margin",
"sec_break2",
"rate",
"amount",
@@ -789,6 +795,7 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
},
@@ -799,12 +806,54 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_26",
+ "fieldtype": "Section Break",
+ "label": "Discount and Margin"
+ },
+ {
+ "depends_on": "price_list_rate",
+ "fieldname": "margin_type",
+ "fieldtype": "Select",
+ "label": "Margin Type",
+ "options": "\nPercentage\nAmount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate",
+ "fieldname": "margin_rate_or_amount",
+ "fieldtype": "Float",
+ "label": "Margin Rate or Amount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_30",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "base_rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:43:21.488258",
+ "modified": "2021-02-23 00:59:52.614805",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index d3e8a44..b361c0c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -695,6 +695,7 @@
refresh_field(['timesheets'])
}
})
+ frm.refresh();
},
onload: function(frm) {
@@ -810,6 +811,65 @@
},
refresh: function(frm) {
+ if (frm.doc.project) {
+ frm.add_custom_button(__('Fetch Timesheet'), function() {
+ let d = new frappe.ui.Dialog({
+ title: __('Fetch Timesheet'),
+ fields: [
+ {
+ "label" : "From",
+ "fieldname": "from_time",
+ "fieldtype": "Date",
+ "reqd": 1,
+ },
+ {
+ fieldtype: 'Column Break',
+ fieldname: 'col_break_1',
+ },
+ {
+ "label" : "To",
+ "fieldname": "to_time",
+ "fieldtype": "Date",
+ "reqd": 1,
+ }
+ ],
+ primary_action: function() {
+ let data = d.get_values();
+ frappe.call({
+ method: "erpnext.projects.doctype.timesheet.timesheet.get_projectwise_timesheet_data",
+ args: {
+ from_time: data.from_time,
+ to_time: data.to_time,
+ project: frm.doc.project
+ },
+ callback: function(r) {
+ if(!r.exc) {
+ if(r.message.length > 0) {
+ frm.clear_table('timesheets')
+ r.message.forEach((d) => {
+ frm.add_child('timesheets',{
+ 'time_sheet': d.parent,
+ 'billing_hours': d.billing_hours,
+ 'billing_amount': d.billing_amt,
+ 'timesheet_detail': d.name
+ });
+ });
+ frm.refresh_field('timesheets')
+ }
+ else {
+ frappe.msgprint(__('No Timesheet Found.'))
+ }
+ d.hide();
+ }
+ }
+ });
+ },
+ primary_action_label: __('Get Timesheets')
+ });
+ d.show();
+ })
+ }
+
if (frappe.boot.active_domains.includes("Healthcare")) {
frm.set_df_property("patient", "hidden", 0);
frm.set_df_property("patient_name", "hidden", 0);
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 720a917..d382386 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1952,13 +1952,12 @@
"is_submittable": 1,
"links": [
{
- "custom": 1,
"group": "Reference",
"link_doctype": "POS Invoice",
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-02-01 15:42:26.261540",
+ "modified": "2021-03-31 15:42:26.261540",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 9599d4e..21d550a 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -21,6 +21,7 @@
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points
from erpnext.accounts.deferred_revenue import validate_service_stop_date
+from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
from frappe.model.utils import get_fetch_values
from frappe.contacts.doctype.address.address import get_address_display
@@ -76,6 +77,8 @@
if not self.is_pos:
self.so_dn_required()
+ self.set_tax_withholding()
+
self.validate_proj_cust()
self.validate_pos_return()
self.validate_with_previous_doc()
@@ -153,6 +156,32 @@
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)))
+ def set_tax_withholding(self):
+ tax_withholding_details = get_party_tax_withholding_details(self)
+
+ if not tax_withholding_details:
+ return
+
+ accounts = []
+ tax_withholding_account = tax_withholding_details.get("account_head")
+
+ for d in self.taxes:
+ if d.account_head == tax_withholding_account:
+ d.update(tax_withholding_details)
+ accounts.append(d.account_head)
+
+ 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]
+
+ for d in to_remove:
+ self.remove(d)
+
+ # calculate totals again after applying TDS
+ self.calculate_taxes_and_totals()
+
def before_save(self):
set_account_for_mode_of_payment(self)
@@ -361,6 +390,7 @@
if validate_against_credit_limit:
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
+ @frappe.whitelist()
def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate)
@@ -700,6 +730,7 @@
else:
self.calculate_billing_amount_for_timesheet()
+ @frappe.whitelist()
def add_timesheet_data(self):
self.set('timesheets', [])
if self.project:
@@ -1030,7 +1061,8 @@
)
def make_gle_for_rounding_adjustment(self, gl_entries):
- if flt(self.rounding_adjustment, self.precision("rounding_adjustment")) and self.base_rounding_adjustment:
+ 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)
@@ -1256,6 +1288,7 @@
break
# Healthcare
+ @frappe.whitelist()
def set_healthcare_services(self, checked_values):
self.set("items", [])
from erpnext.stock.get_item_details import get_item_details
diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json
index ee6419d..3781f8c 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_records.json
+++ b/erpnext/accounts/doctype/sales_invoice/test_records.json
@@ -31,7 +31,7 @@
"base_grand_total": 561.8,
"grand_total": 561.8,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"base_net_total": 500.0,
"taxes": [
{
@@ -104,7 +104,7 @@
"base_grand_total": 630.0,
"grand_total": 630.0,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"base_net_total": 500.0,
"taxes": [
{
@@ -148,7 +148,7 @@
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"item_code": "_Test Item Home Desktop 100",
"item_name": "_Test Item Home Desktop 100",
- "item_tax_template": "_Test Account Excise Duty @ 10",
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"parentfield": "items",
"price_list_rate": 50,
"qty": 10,
@@ -175,7 +175,7 @@
],
"grand_total": 0,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"taxes": [
{
"account_head": "_Test Account Shipping Charges - _TC",
@@ -276,7 +276,7 @@
"expense_account": "_Test Account Cost for Goods Sold - _TC",
"item_code": "_Test Item Home Desktop 100",
"item_name": "_Test Item Home Desktop 100",
- "item_tax_template": "_Test Account Excise Duty @ 10",
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"parentfield": "items",
"price_list_rate": 62.5,
"qty": 10,
@@ -301,7 +301,7 @@
],
"grand_total": 0,
"is_pos": 0,
- "naming_series": "_T-Sales Invoice-",
+ "naming_series": "T-SINV-",
"taxes": [
{
"account_head": "_Test Account Excise Duty - _TC",
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 7cd1828..f09cc5a 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -405,10 +405,10 @@
item_row = si.get("items")[0]
add_items = [
- (54, '_Test Account Excise Duty @ 12'),
- (288, '_Test Account Excise Duty @ 15'),
- (144, '_Test Account Excise Duty @ 20'),
- (430, '_Test Item Tax Template 1')
+ (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)
@@ -1800,6 +1800,15 @@
si.selling_price_list = "_Test Price List Rest of the World"
si.update_stock = 1
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
+ )
+
add_taxes(si)
si.save()
@@ -2077,14 +2086,14 @@
item.save()
item.append("taxes", {
- "item_tax_template": "_Test Item Tax Template 1",
+ "item_tax_template": "_Test Item Tax Template 1 - _TC",
"valid_from": add_days(nowdate(), 1)
})
item.save()
sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1)
- sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1"
+ sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1 - _TC"
self.assertRaises(frappe.ValidationError, sales_invoice.save)
item.taxes = []
@@ -2106,6 +2115,7 @@
si.return_against = args.return_against
si.currency=args.currency or "INR"
si.conversion_rate = args.conversion_rate or 1
+ si.naming_series = args.naming_series or "T-SINV-"
si.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
@@ -2269,4 +2279,4 @@
"cost_center": "Main - TCP1",
"description": "Excise Duty",
"rate": 12
- })
\ No newline at end of file
+ })
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 b403c7b..8e6952a 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -818,6 +818,7 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
}
@@ -825,7 +826,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:42:37.796771",
+ "modified": "2021-02-23 01:05:22.123527",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
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 429a9f3..52d19d5 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
@@ -46,5 +46,5 @@
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}):
+ 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/subscription/subscription.js b/erpnext/accounts/doctype/subscription/subscription.js
index ba98eb9..1a90664 100644
--- a/erpnext/accounts/doctype/subscription/subscription.js
+++ b/erpnext/accounts/doctype/subscription/subscription.js
@@ -10,6 +10,14 @@
}
}
});
+
+ frm.set_query('cost_center', function() {
+ return {
+ filters: {
+ company: frm.doc.company
+ }
+ };
+ });
},
refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/subscription/subscription.json b/erpnext/accounts/doctype/subscription/subscription.json
index afb94fe..e80df2a 100644
--- a/erpnext/accounts/doctype/subscription/subscription.json
+++ b/erpnext/accounts/doctype/subscription/subscription.json
@@ -7,9 +7,10 @@
"engine": "InnoDB",
"field_order": [
"party_type",
- "status",
- "cb_1",
"party",
+ "cb_1",
+ "company",
+ "status",
"subscription_period",
"start_date",
"end_date",
@@ -44,80 +45,107 @@
{
"allow_on_submit": 1,
"fieldname": "cb_1",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
+ "no_copy": 1,
"options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "subscription_period",
"fieldtype": "Section Break",
- "label": "Subscription Period"
+ "label": "Subscription Period",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "cancelation_date",
"fieldtype": "Date",
"label": "Cancelation Date",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "trial_period_start",
"fieldtype": "Date",
"label": "Trial Period Start Date",
- "set_only_once": 1
+ "set_only_once": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.trial_period_start",
"fieldname": "trial_period_end",
"fieldtype": "Date",
"label": "Trial Period End Date",
- "set_only_once": 1
+ "set_only_once": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_11",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "current_invoice_start",
"fieldtype": "Date",
"label": "Current Invoice Start Date",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "current_invoice_end",
"fieldtype": "Date",
"label": "Current Invoice End Date",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"description": "Number of days that the subscriber has to pay invoices generated by this subscription",
"fieldname": "days_until_due",
"fieldtype": "Int",
- "label": "Days Until Due"
+ "label": "Days Until Due",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"fieldname": "cancel_at_period_end",
"fieldtype": "Check",
- "label": "Cancel At End Of Period"
+ "label": "Cancel At End Of Period",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"fieldname": "generate_invoice_at_period_start",
"fieldtype": "Check",
- "label": "Generate Invoice At Beginning Of Period"
+ "label": "Generate Invoice At Beginning Of Period",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "sb_4",
"fieldtype": "Section Break",
- "label": "Plans"
+ "label": "Plans",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -125,62 +153,84 @@
"fieldtype": "Table",
"label": "Plans",
"options": "Subscription Plan Detail",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)",
"fieldname": "sb_1",
"fieldtype": "Section Break",
- "label": "Taxes"
+ "label": "Taxes",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "sb_2",
"fieldtype": "Section Break",
- "label": "Discounts"
+ "label": "Discounts",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "apply_additional_discount",
"fieldtype": "Select",
"label": "Apply Additional Discount On",
- "options": "\nGrand Total\nNet Total"
+ "options": "\nGrand Total\nNet Total",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "cb_2",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "additional_discount_percentage",
"fieldtype": "Percent",
- "label": "Additional DIscount Percentage"
+ "label": "Additional DIscount Percentage",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "additional_discount_amount",
"fieldtype": "Currency",
- "label": "Additional DIscount Amount"
+ "label": "Additional DIscount Amount",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.invoices",
"fieldname": "sb_3",
"fieldtype": "Section Break",
- "label": "Invoices"
+ "label": "Invoices",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "invoices",
"fieldtype": "Table",
"label": "Invoices",
- "options": "Subscription Invoice"
+ "options": "Subscription Invoice",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
- "label": "Accounting Dimensions"
+ "label": "Accounting Dimensions",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "dimension_col_break",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "party_type",
@@ -188,7 +238,9 @@
"label": "Party Type",
"options": "DocType",
"reqd": 1,
- "set_only_once": 1
+ "set_only_once": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "party",
@@ -197,21 +249,27 @@
"label": "Party",
"options": "party_type",
"reqd": 1,
- "set_only_once": 1
+ "set_only_once": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.party_type === 'Customer'",
"fieldname": "sales_tax_template",
"fieldtype": "Link",
"label": "Sales Taxes and Charges Template",
- "options": "Sales Taxes and Charges Template"
+ "options": "Sales Taxes and Charges Template",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.party_type === 'Supplier'",
"fieldname": "purchase_tax_template",
"fieldtype": "Link",
"label": "Purchase Taxes and Charges Template",
- "options": "Purchase Taxes and Charges Template"
+ "options": "Purchase Taxes and Charges Template",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
@@ -219,36 +277,55 @@
"fieldname": "follow_calendar_months",
"fieldtype": "Check",
"label": "Follow Calendar Months",
- "set_only_once": 1
+ "set_only_once": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date",
"fieldname": "generate_new_invoices_past_due_date",
"fieldtype": "Check",
- "label": "Generate New Invoices Past Due Date"
+ "label": "Generate New Invoices Past Due Date",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "Subscription End Date",
- "set_only_once": 1
+ "set_only_once": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Subscription Start Date",
- "set_only_once": 1
+ "set_only_once": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
- "options": "Cost Center"
+ "options": "Cost Center",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "show_days": 1,
+ "show_seconds": 1
}
],
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-06-25 10:52:52.265105",
+ "modified": "2021-02-09 15:44:20.024789",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription",
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index e023b47..826044a 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -1,3 +1,4 @@
+
# -*- coding: utf-8 -*-
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
@@ -5,12 +6,13 @@
from __future__ import unicode_literals
import frappe
+import erpnext
from frappe import _
from frappe.model.document import Document
from frappe.utils.data import nowdate, getdate, cstr, cint, add_days, date_diff, get_last_day, add_to_date, flt
from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
-
+from erpnext import get_default_company
class Subscription(Document):
def before_insert(self):
@@ -243,6 +245,7 @@
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'))
def validate_trial_period(self):
"""
@@ -304,6 +307,14 @@
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()
+ if not company:
+ 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 \
else self.current_invoice_end
@@ -330,6 +341,7 @@
# 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)
# Taxes
@@ -380,7 +392,8 @@
Returns the `Item`s linked to `Subscription Plan`
"""
if prorate:
- prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start)
+ prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start,
+ self.generate_invoice_at_period_start)
items = []
party = self.party
@@ -583,10 +596,13 @@
return calendar_months
-def get_prorata_factor(period_end, period_start):
- diff = flt(date_diff(nowdate(), period_start) + 1)
- plan_days = flt(date_diff(period_end, period_start) + 1)
- prorate_factor = diff / plan_days
+def get_prorata_factor(period_end, period_start, is_prepaid):
+ if is_prepaid:
+ prorate_factor = 1
+ else:
+ diff = flt(date_diff(nowdate(), period_start) + 1)
+ plan_days = flt(date_diff(period_end, period_start) + 1)
+ prorate_factor = diff / plan_days
return prorate_factor
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index c17fccd..7c58e98 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -321,7 +321,8 @@
self.assertEqual(
flt(
- get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start),
+ get_prorata_factor(subscription.current_invoice_end, subscription.current_invoice_start,
+ subscription.generate_invoice_at_period_start),
2),
flt(prorate_factor, 2)
)
@@ -561,9 +562,7 @@
current_inv = subscription.get_current_invoice()
self.assertEqual(current_inv.status, "Unpaid")
- 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)
+ prorate_factor = 1
self.assertEqual(flt(current_inv.grand_total, 2), flt(prorate_factor * 900, 2))
diff --git a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json
index f54e887..8a0d1de 100644
--- a/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json
+++ b/erpnext/accounts/doctype/subscription_invoice/subscription_invoice.json
@@ -13,21 +13,28 @@
"fieldname": "document_type",
"fieldtype": "Link",
"label": "Document Type ",
+ "no_copy": 1,
"options": "DocType",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "invoice",
"fieldtype": "Dynamic Link",
"in_list_view": 1,
"label": "Invoice",
+ "no_copy": 1,
"options": "document_type",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-06-01 22:23:54.462718",
+ "modified": "2021-02-09 15:43:32.026233",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Subscription Invoice",
diff --git a/erpnext/accounts/doctype/tax_category/tax_category.json b/erpnext/accounts/doctype/tax_category/tax_category.json
index 6f682a0..f7145af 100644
--- a/erpnext/accounts/doctype/tax_category/tax_category.json
+++ b/erpnext/accounts/doctype/tax_category/tax_category.json
@@ -11,15 +11,18 @@
],
"fields": [
{
+ "allow_in_quick_entry": 1,
"fieldname": "title",
"fieldtype": "Data",
+ "in_list_view": 1,
"label": "Title",
+ "reqd": 1,
"unique": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-08-30 19:41:25.783852",
+ "modified": "2021-03-03 11:50:38.748872",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Tax Category",
diff --git a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
index 632e30d..ac1ffd9 100644
--- a/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
+++ b/erpnext/accounts/doctype/tax_rule/test_tax_rule.py
@@ -14,10 +14,15 @@
from six import iteritems
class TestTaxRule(unittest.TestCase):
- def setUp(self):
+ @classmethod
+ def setUpClass(cls):
+ frappe.db.set_value("Shopping Cart Settings", None, "enabled", 0)
+
+ @classmethod
+ def tearDownClass(cls):
frappe.db.sql("delete from `tabTax Rule`")
- def tearDown(self):
+ def setUp(self):
frappe.db.sql("delete from `tabTax Rule`")
def test_conflict(self):
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 32ad4cb..961bdb1 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -12,37 +12,62 @@
class TaxWithholdingCategory(Document):
pass
-def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None):
+def get_party_details(inv):
+ party_type, party = '', ''
+ if inv.doctype == 'Sales Invoice':
+ party_type = 'Customer'
+ party = inv.customer
+ else:
+ party_type = 'Supplier'
+ party = inv.supplier
+
+ return party_type, party
+
+def get_party_tax_withholding_details(inv, tax_withholding_category=None):
pan_no = ''
- suppliers = []
+ parties = []
+ party_type, party = get_party_details(inv)
if not tax_withholding_category:
- tax_withholding_category, pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, ['tax_withholding_category', 'pan'])
+ tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan'])
if not tax_withholding_category:
return
+ # if tax_withholding_category passed as an argument but not pan_no
if not pan_no:
- pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, 'pan')
+ pan_no = frappe.db.get_value(party_type, party, 'pan')
# Get others suppliers with the same PAN No
if pan_no:
- suppliers = [d.name for d in frappe.get_all('Supplier', fields=['name'], filters={'pan': pan_no})]
+ parties = frappe.get_all(party_type, filters={ 'pan': pan_no }, pluck='name')
- if not suppliers:
- suppliers.append(ref_doc.supplier)
+ if not parties:
+ parties.append(party)
- fy = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company)
- tax_details = get_tax_withholding_details(tax_withholding_category, fy[0], ref_doc.company)
+ fiscal_year = get_fiscal_year(inv.posting_date, company=inv.company)
+ tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
+
if not tax_details:
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
- .format(tax_withholding_category, ref_doc.company))
+ .format(tax_withholding_category, inv.company))
- tds_amount = get_tds_amount(suppliers, ref_doc.net_total, ref_doc.company,
- tax_details, fy, ref_doc.posting_date, pan_no)
+ 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))
- tax_row = get_tax_row(tax_details, tds_amount)
+ tax_amount, tax_deducted = get_tax_amount(
+ party_type, parties,
+ inv, tax_details,
+ fiscal_year, pan_no
+ )
+
+ 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)
return tax_row
@@ -69,147 +94,254 @@
frappe.throw(_("No Tax Withholding data found for the current Fiscal Year."))
-def get_tax_row(tax_details, tds_amount):
-
- return {
+def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
+ row = {
"category": "Total",
- "add_deduct_tax": "Deduct",
"charge_type": "Actual",
- "account_head": tax_details.account_head,
+ "tax_amount": tax_amount,
"description": tax_details.description,
- "tax_amount": tds_amount
+ "account_head": tax_details.account_head
}
-def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_details, posting_date, pan_no=None):
- fiscal_year, year_start_date, year_end_date = fiscal_year_details
- tds_amount = 0
- tds_deducted = 0
+ if tax_deducted:
+ # TCS already deducted on previous invoices
+ # So, TCS will be calculated by 'Previous Row Total'
- def _get_tds(amount, rate):
- if amount <= 0:
- return 0
-
- return amount * rate / 100
-
- ldc_name = frappe.db.get_value('Lower Deduction Certificate',
- {
- 'pan_no': pan_no,
- 'fiscal_year': fiscal_year
- }, 'name')
- ldc = ''
-
- if ldc_name:
- ldc = frappe.get_doc('Lower Deduction Certificate', ldc_name)
-
- entries = frappe.db.sql("""
- select voucher_no, credit
- from `tabGL Entry`
- where company = %s and
- party in %s and fiscal_year=%s and credit > 0
- and is_opening = 'No'
- """, (company, tuple(suppliers), fiscal_year), as_dict=1)
-
- vouchers = [d.voucher_no for d in entries]
- advance_vouchers = get_advance_vouchers(suppliers, fiscal_year=fiscal_year, company=company)
-
- tds_vouchers = vouchers + advance_vouchers
-
- if tds_vouchers:
- tds_deducted = frappe.db.sql("""
- SELECT sum(credit) FROM `tabGL Entry`
- WHERE
- account=%s and fiscal_year=%s and credit > 0
- and voucher_no in ({0})""". format(','.join(['%s'] * len(tds_vouchers))),
- ((tax_details.account_head, fiscal_year) + tuple(tds_vouchers)))
-
- tds_deducted = tds_deducted[0][0] if tds_deducted and tds_deducted[0][0] else 0
-
- if tds_deducted:
- if ldc:
- limit_consumed = frappe.db.get_value('Purchase Invoice',
- {
- 'supplier': ('in', suppliers),
- 'apply_tds': 1,
- 'docstatus': 1
- }, 'sum(net_total)')
-
- if ldc and is_valid_certificate(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)
+ 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
+ })
else:
- tds_amount = _get_tds(net_total, tax_details.rate)
- else:
- supplier_credit_amount = frappe.get_all('Purchase Invoice',
- fields = ['sum(net_total)'],
- filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1)
+ # if only TCS is to be charged, then net total is chargeable amount
+ row.update({
+ "charge_type": "On Net Total",
+ "rate": tax_details.rate
+ })
- supplier_credit_amount = (supplier_credit_amount[0][0]
- if supplier_credit_amount and supplier_credit_amount[0][0] else 0)
+ return row
- jv_supplier_credit_amt = frappe.get_all('Journal Entry Account',
- fields = ['sum(credit_in_account_currency)'],
- filters = {
- 'parent': ('in', vouchers), 'docstatus': 1,
- 'party': ('in', suppliers),
- 'reference_type': ('not in', ['Purchase Invoice'])
- }, as_list=1)
+def get_tax_row_for_tds(tax_details, tax_amount):
+ return {
+ "category": "Total",
+ "charge_type": "Actual",
+ "tax_amount": tax_amount,
+ "add_deduct_tax": "Deduct",
+ "description": tax_details.description,
+ "account_head": tax_details.account_head
+ }
- supplier_credit_amount += (jv_supplier_credit_amt[0][0]
- if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0)
+def get_lower_deduction_certificate(fiscal_year, pan_no):
+ ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name')
+ if ldc_name:
+ return frappe.get_doc('Lower Deduction Certificate', ldc_name)
- supplier_credit_amount += net_total
+def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
+ fiscal_year = fiscal_year_details[0]
- debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date)
- supplier_credit_amount -= debit_note_amount
+ vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
+ advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
+ taxable_vouchers = vouchers + advance_vouchers
- if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold)
- or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)):
+ tax_deducted = 0
+ if taxable_vouchers:
+ tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
- if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total,
- ldc.certificate_limit):
- tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate,
- tax_details)
+ tax_amount = 0
+ posting_date = inv.posting_date
+ if party_type == 'Supplier':
+ ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
+ if tax_deducted:
+ net_total = inv.net_total
+ if ldc:
+ tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total)
else:
- tds_amount = _get_tds(supplier_credit_amount, tax_details.rate)
+ 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,
+ fiscal_year_details, tax_deducted, vouchers
+ )
+
+ 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
+ else:
+ # if no TCS has been charged in FY,
+ # then chargeable value is "prev invoices + advances" value which cross the threshold
+ tax_amount = get_tcs_amount(
+ parties, inv, tax_details,
+ fiscal_year_details, vouchers, advance_vouchers
+ )
+
+ return tax_amount, tax_deducted
+
+def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
+ dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
+
+ filters = {
+ dr_or_cr: ['>', 0],
+ 'company': company,
+ 'party_type': party_type,
+ 'party': ['in', parties],
+ 'fiscal_year': fiscal_year,
+ 'is_opening': 'No',
+ 'is_cancelled': 0
+ }
+
+ return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""]
+
+def get_advance_vouchers(parties, fiscal_year=None, 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'
+
+ filters = {
+ dr_or_cr: ['>', 0],
+ 'is_opening': 'No',
+ 'is_cancelled': 0,
+ 'party_type': party_type,
+ 'party': ['in', parties],
+ 'against_voucher': ['is', 'not set']
+ }
+
+ if fiscal_year:
+ filters['fiscal_year'] = fiscal_year
+ 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 [""]
+
+def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
+ # check if TDS / TCS account is already charged on taxable vouchers
+ filters = {
+ 'is_cancelled': 0,
+ 'credit': ['>', 0],
+ 'fiscal_year': fiscal_year,
+ 'account': tax_details.account_head,
+ 'voucher_no': ['in', taxable_vouchers],
+ }
+ field = "sum(credit)"
+
+ return frappe.db.get_value('GL Entry', filters, field) or 0.0
+
+def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
+ tds_amount = 0
+
+ supp_credit_amt = frappe.db.get_value('Purchase Invoice', {
+ 'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1
+ }, 'sum(net_total)') 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, fiscal_year_details, inv.company)
+ supp_credit_amt -= debit_note_amount
+
+ threshold = tax_details.get('threshold', 0)
+ cumulative_threshold = tax_details.get('cumulative_threshold', 0)
+
+ if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
+ if ldc and is_valid_certificate(
+ ldc.valid_from, ldc.valid_upto,
+ inv.posting_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:
+ tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
return tds_amount
-def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None):
- condition = "fiscal_year=%s" % fiscal_year
+def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers):
+ tcs_amount = 0
+ fiscal_year, _, _ = fiscal_year_details
+
+ # 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
+
+ # 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
+
+ # sum of credit entries made from sales invoice
+ credit_note_amt = frappe.db.get_value('GL Entry', {
+ 'is_cancelled': 0,
+ 'credit': ['>', 0],
+ 'party': ['in', parties],
+ 'fiscal_year': fiscal_year,
+ 'company': inv.company,
+ 'voucher_type': 'Sales Invoice',
+ }, 'sum(credit)') or 0.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)):
+ 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, fiscal_year, 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)')
+
+ if is_valid_certificate(
+ 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)
+
+ return tds_amount
+
+def get_debit_note_amount(suppliers, fiscal_year_details, company=None):
+ _, year_start_date, year_end_date = fiscal_year_details
+
+ filters = {
+ 'supplier': ['in', suppliers],
+ 'is_return': 1,
+ 'docstatus': 1,
+ 'posting_date': ['between', (year_start_date, year_end_date)]
+ }
+ fields = ['abs(sum(net_total)) as net_total']
if company:
- condition += "and company =%s" % (company)
- if from_date and to_date:
- condition += "and posting_date between %s and %s" % (from_date, to_date)
+ filters['company'] = company
- ## Appending the same supplier again if length of suppliers list is 1
- ## since tuple of single element list contains None, For example ('Test Supplier 1', )
- ## and the below query fails
- if len(suppliers) == 1:
- suppliers.append(suppliers[0])
-
- return frappe.db.sql_list("""
- select distinct voucher_no
- from `tabGL Entry`
- where party in %s and %s and debit > 0
- and is_opening = 'No'
- """, (tuple(suppliers), condition)) or []
-
-def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None):
- condition = "and 1=1"
- if company:
- condition = " and company=%s " % company
-
- if len(suppliers) == 1:
- suppliers.append(suppliers[0])
-
- return flt(frappe.db.sql("""
- select abs(sum(net_total))
- from `tabPurchase Invoice`
- where supplier in %s and is_return=1 and docstatus=1
- and posting_date between %s and %s %s
- """, (tuple(suppliers), year_start_date, year_end_date, condition)))
+ 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):
@@ -227,4 +359,4 @@
certificate_limit > deducted_amount):
valid = True
- return valid
\ No newline at end of file
+ return valid
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 ef77674..dd3b49a 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
@@ -9,7 +9,7 @@
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
-test_dependencies = ["Supplier Group"]
+test_dependencies = ["Supplier Group", "Customer Group"]
class TestTaxWithholdingCategory(unittest.TestCase):
@classmethod
@@ -18,6 +18,9 @@
create_records()
create_tax_with_holding_category()
+ def tearDown(self):
+ cancel_invoices()
+
def test_cumulative_threshold_tds(self):
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS")
invoices = []
@@ -128,9 +131,59 @@
for d in invoices:
d.cancel()
+ def test_cumulative_threshold_tcs(self):
+ 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.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.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'])
+ 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.submit()
+
+ 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:
+ 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()
+
+ for d in sales_invoices:
+ frappe.get_doc('Sales Invoice', d).cancel()
+
def create_purchase_invoice(**args):
# return sales invoice doc object
- item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
+ item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name")
args = frappe._dict(args)
pi = frappe.get_doc({
@@ -145,7 +198,7 @@
"taxes": [],
"items": [{
'doctype': 'Purchase Invoice Item',
- 'item_code': item.name,
+ 'item_code': item,
'qty': args.qty or 1,
'rate': args.rate or 10000,
'cost_center': 'Main - _TC',
@@ -156,6 +209,34 @@
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")
+
+ 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.save()
+ return si
+
def create_records():
# create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']:
@@ -168,7 +249,17 @@
"doctype": "Supplier",
}).insert()
- # create an item
+ 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()
+
+ # create item
if not frappe.db.exists('Item', "TDS Item"):
frappe.get_doc({
"doctype": "Item",
@@ -178,7 +269,16 @@
"is_stock_item": 0,
}).insert()
- # create an account
+ 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',
@@ -189,6 +289,17 @@
'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()
+
def create_tax_with_holding_category():
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
@@ -210,6 +321,23 @@
}]
}).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": [{
+ 'fiscal_year': fiscal_year,
+ '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({
@@ -226,4 +354,4 @@
'company': '_Test Company',
'account': 'TDS - _TC'
}]
- }).insert()
\ No newline at end of file
+ }).insert()
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 287c79f..dac0c21 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -44,9 +44,9 @@
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):
+def process_gl_map(gl_map, merge_entries=True, precision=None):
if merge_entries:
- gl_map = merge_similar_entries(gl_map)
+ gl_map = merge_similar_entries(gl_map, precision)
for entry in gl_map:
# toggle debit, credit if negative entry
if flt(entry.debit) < 0:
@@ -69,7 +69,7 @@
return gl_map
-def merge_similar_entries(gl_map):
+def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()
for entry in gl_map:
@@ -88,7 +88,9 @@
company = gl_map[0].company if gl_map else erpnext.get_default_company()
company_currency = erpnext.get_company_currency(company)
- precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
+
+ if not precision:
+ 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)
@@ -132,8 +134,8 @@
gle.update(args)
gle.flags.ignore_permissions = 1
gle.flags.from_repost = from_repost
- gle.insert()
- gle.run_method("on_update_with_args", adv_adj, update_outstanding, from_repost)
+ gle.flags.adv_adj = adv_adj
+ gle.flags.update_outstanding = update_outstanding or 'Yes'
gle.submit()
if not from_repost:
@@ -194,7 +196,7 @@
if not round_off_gle:
for k in ["voucher_type", "voucher_no", "company",
- "posting_date", "remarks", "is_opening"]:
+ "posting_date", "remarks"]:
round_off_gle[k] = gl_map[0][k]
round_off_gle.update({
@@ -206,6 +208,7 @@
"cost_center": round_off_cost_center,
"party_type": None,
"party": None,
+ "is_opening": "No",
"against_voucher_type": None,
"against_voucher": None
})
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
deleted file mode 100644
index 6ae81d7..0000000
--- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
+++ /dev/null
@@ -1,583 +0,0 @@
-frappe.provide("erpnext.accounts");
-
-frappe.pages['bank-reconciliation'].on_page_load = function(wrapper) {
- new erpnext.accounts.bankReconciliation(wrapper);
-}
-
-erpnext.accounts.bankReconciliation = class BankReconciliation {
- constructor(wrapper) {
- this.page = frappe.ui.make_app_page({
- parent: wrapper,
- title: __("Bank Reconciliation"),
- single_column: true
- });
- this.parent = wrapper;
- this.page = this.parent.page;
-
- this.check_plaid_status();
- this.make();
- }
-
- make() {
- const me = this;
-
- me.$main_section = $(`<div class="reconciliation page-main-content"></div>`).appendTo(me.page.main);
- const empty_state = __("Upload a bank statement, link or reconcile a bank account")
- me.$main_section.append(`<div class="flex justify-center align-center text-muted"
- style="height: 50vh; display: flex;"><h5 class="text-muted">${empty_state}</h5></div>`)
-
- me.page.add_field({
- fieldtype: 'Link',
- label: __('Company'),
- fieldname: 'company',
- options: "Company",
- onchange: function() {
- if (this.value) {
- me.company = this.value;
- } else {
- me.company = null;
- me.bank_account = null;
- }
- }
- })
- me.page.add_field({
- fieldtype: 'Link',
- label: __('Bank Account'),
- fieldname: 'bank_account',
- options: "Bank Account",
- get_query: function() {
- if(!me.company) {
- frappe.throw(__("Please select company first"));
- return
- }
-
- return {
- filters: {
- "company": me.company
- }
- }
- },
- onchange: function() {
- if (this.value) {
- me.bank_account = this.value;
- me.add_actions();
- } else {
- me.bank_account = null;
- me.page.hide_actions_menu();
- }
- }
- })
- }
-
- check_plaid_status() {
- const me = this;
- frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
- if (r && r.enabled === "1") {
- me.plaid_status = "active"
- } else {
- me.plaid_status = "inactive"
- }
- })
- }
-
- add_actions() {
- const me = this;
-
- me.page.show_menu()
-
- me.page.add_menu_item(__("Upload a statement"), function() {
- me.clear_page_content();
- new erpnext.accounts.bankTransactionUpload(me);
- }, true)
-
- if (me.plaid_status==="active") {
- me.page.add_menu_item(__("Synchronize this account"), function() {
- me.clear_page_content();
- new erpnext.accounts.bankTransactionSync(me);
- }, true)
- }
-
- me.page.add_menu_item(__("Reconcile this account"), function() {
- me.clear_page_content();
- me.make_reconciliation_tool();
- }, true)
- }
-
- clear_page_content() {
- const me = this;
- $(me.page.body).find('.frappe-list').remove();
- me.$main_section.empty();
- }
-
- make_reconciliation_tool() {
- const me = this;
- frappe.model.with_doctype("Bank Transaction", () => {
- erpnext.accounts.ReconciliationList = new erpnext.accounts.ReconciliationTool({
- parent: me.parent,
- doctype: "Bank Transaction"
- });
- })
- }
-}
-
-
-erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
- constructor(parent) {
- this.parent = parent;
- this.data = [];
-
- const assets = [
- "/assets/frappe/css/frappe-datatable.css",
- "/assets/frappe/js/lib/clusterize.min.js",
- "/assets/frappe/js/lib/Sortable.min.js",
- "/assets/frappe/js/lib/frappe-datatable.js"
- ];
-
- frappe.require(assets, () => {
- this.make();
- });
- }
-
- make() {
- const me = this;
- new frappe.ui.FileUploader({
- method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement',
- allow_multiple: 0,
- on_success: function(attachment, r) {
- if (!r.exc && r.message) {
- me.data = r.message;
- me.setup_transactions_dom();
- me.create_datatable();
- me.add_primary_action();
- }
- }
- })
- }
-
- setup_transactions_dom() {
- const me = this;
- me.parent.$main_section.append('<div class="transactions-table"></div>');
- }
-
- create_datatable() {
- try {
- this.datatable = new DataTable('.transactions-table', {
- columns: this.data.columns,
- data: this.data.data
- })
- }
- catch(err) {
- let msg = __("Your file could not be processed. It should be a standard CSV or XLSX file with headers in the first row.");
- frappe.throw(msg)
- }
-
- }
-
- add_primary_action() {
- const me = this;
- me.parent.page.set_primary_action(__("Submit"), function() {
- me.add_bank_entries()
- }, null, __("Creating bank entries..."))
- }
-
- add_bank_entries() {
- const me = this;
- frappe.xcall('erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.create_bank_entries',
- {columns: this.datatable.datamanager.columns, data: this.datatable.datamanager.data, bank_account: me.parent.bank_account}
- ).then((result) => {
- let result_title = result.errors == 0 ? __("{0} bank transaction(s) created", [result.success]) : __("{0} bank transaction(s) created and {1} errors", [result.success, result.errors])
- let result_msg = `
- <div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
- <h5 class="text-muted">${result_title}</h5>
- </div>`
- me.parent.page.clear_primary_action();
- me.parent.$main_section.empty();
- me.parent.$main_section.append(result_msg);
- if (result.errors == 0) {
- frappe.show_alert({message:__("All bank transactions have been created"), indicator:'green'});
- } else {
- frappe.show_alert({message:__("Please check the error log for details about the import errors"), indicator:'red'});
- }
- })
- }
-}
-
-erpnext.accounts.bankTransactionSync = class bankTransactionSync {
- constructor(parent) {
- this.parent = parent;
- this.data = [];
-
- this.init_config()
- }
-
- init_config() {
- const me = this;
- frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration')
- .then(result => {
- me.plaid_env = result.plaid_env;
- me.client_name = result.client_name;
- me.link_token = result.link_token;
- me.sync_transactions();
- })
- }
-
- sync_transactions() {
- const me = this;
- frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => {
- frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', {
- bank: r.bank,
- bank_account: me.parent.bank_account,
- freeze: true
- })
- .then((result) => {
- let result_title = (result && result.length > 0)
- ? __("{0} bank transaction(s) created", [result.length])
- : __("This bank account is already synchronized");
-
- let result_msg = `
- <div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
- <h5 class="text-muted">${result_title}</h5>
- </div>`
-
- this.parent.$main_section.append(result_msg)
- frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' });
- })
- })
- }
-}
-
-
-erpnext.accounts.ReconciliationTool = class ReconciliationTool extends frappe.views.BaseList {
- constructor(opts) {
- super(opts);
- this.show();
- }
-
- setup_defaults() {
- super.setup_defaults();
-
- this.page_title = __("Bank Reconciliation");
- this.doctype = 'Bank Transaction';
- this.fields = ['date', 'description', 'debit', 'credit', 'currency']
-
- }
-
- setup_view() {
- this.render_header();
- }
-
- setup_side_bar() {
- //
- }
-
- make_standard_filters() {
- //
- }
-
- freeze() {
- this.$result.find('.list-count').html(`<span>${__('Refreshing')}...</span>`);
- }
-
- get_args() {
- const args = super.get_args();
-
- return Object.assign({}, args, {
- ...args.filters.push(["Bank Transaction", "docstatus", "=", 1],
- ["Bank Transaction", "unallocated_amount", ">", 0])
- });
-
- }
-
- update_data(r) {
- let data = r.message || [];
-
- if (this.start === 0) {
- this.data = data;
- } else {
- this.data = this.data.concat(data);
- }
- }
-
- render() {
- const me = this;
- this.$result.find('.list-row-container').remove();
- $('[data-fieldname="name"]').remove();
- me.data.map((value) => {
- const row = $('<div class="list-row-container">').data("data", value).appendTo(me.$result).get(0);
- new erpnext.accounts.ReconciliationRow(row, value);
- })
- }
-
- render_header() {
- const me = this;
- if ($(this.wrapper).find('.transaction-header').length === 0) {
- me.$result.append(frappe.render_template("bank_transaction_header"));
- }
- }
-}
-
-erpnext.accounts.ReconciliationRow = class ReconciliationRow {
- constructor(row, data) {
- this.data = data;
- this.row = row;
- this.make();
- this.bind_events();
- }
-
- make() {
- $(this.row).append(frappe.render_template("bank_transaction_row", this.data))
- }
-
- bind_events() {
- const me = this;
- $(me.row).on('click', '.clickable-section', function() {
- me.bank_entry = $(this).attr("data-name");
- me.show_dialog($(this).attr("data-name"));
- })
-
- $(me.row).on('click', '.new-reconciliation', function() {
- me.bank_entry = $(this).attr("data-name");
- me.show_dialog($(this).attr("data-name"));
- })
-
- $(me.row).on('click', '.new-payment', function() {
- me.bank_entry = $(this).attr("data-name");
- me.new_payment();
- })
-
- $(me.row).on('click', '.new-invoice', function() {
- me.bank_entry = $(this).attr("data-name");
- me.new_invoice();
- })
-
- $(me.row).on('click', '.new-expense', function() {
- me.bank_entry = $(this).attr("data-name");
- me.new_expense();
- })
- }
-
- new_payment() {
- const me = this;
- const paid_amount = me.data.credit > 0 ? me.data.credit : me.data.debit;
- const payment_type = me.data.credit > 0 ? "Receive": "Pay";
- const party_type = me.data.credit > 0 ? "Customer": "Supplier";
-
- frappe.new_doc("Payment Entry", {"payment_type": payment_type, "paid_amount": paid_amount,
- "party_type": party_type, "paid_from": me.data.bank_account})
- }
-
- new_invoice() {
- const me = this;
- const invoice_type = me.data.credit > 0 ? "Sales Invoice" : "Purchase Invoice";
-
- frappe.new_doc(invoice_type)
- }
-
- new_expense() {
- frappe.new_doc("Expense Claim")
- }
-
-
- show_dialog(data) {
- const me = this;
-
- frappe.db.get_value("Bank Account", me.data.bank_account, "account", (r) => {
- me.gl_account = r.account;
- })
-
- frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments',
- { bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") }
- ).then((result) => {
- me.make_dialog(result)
- })
- }
-
- make_dialog(data) {
- const me = this;
- me.selected_payment = null;
-
- const fields = [
- {
- fieldtype: 'Section Break',
- fieldname: 'section_break_1',
- label: __('Automatic Reconciliation')
- },
- {
- fieldtype: 'HTML',
- fieldname: 'payment_proposals'
- },
- {
- fieldtype: 'Section Break',
- fieldname: 'section_break_2',
- label: __('Search for a payment')
- },
- {
- fieldtype: 'Link',
- fieldname: 'payment_doctype',
- options: 'DocType',
- label: 'Payment DocType',
- get_query: () => {
- return {
- filters : {
- "name": ["in", ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice", "Expense Claim"]]
- }
- }
- },
- },
- {
- fieldtype: 'Column Break',
- fieldname: 'column_break_1',
- },
- {
- fieldtype: 'Dynamic Link',
- fieldname: 'payment_entry',
- options: 'payment_doctype',
- label: 'Payment Document',
- get_query: () => {
- let dt = this.dialog.fields_dict.payment_doctype.value;
- if (dt === "Payment Entry") {
- return {
- query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.payment_entry_query",
- filters : {
- "bank_account": this.data.bank_account,
- "company": this.data.company
- }
- }
- } else if (dt === "Journal Entry") {
- return {
- query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.journal_entry_query",
- filters : {
- "bank_account": this.data.bank_account,
- "company": this.data.company
- }
- }
- } else if (dt === "Sales Invoice") {
- return {
- query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.sales_invoices_query"
- }
- } else if (dt === "Purchase Invoice") {
- return {
- filters : [
- ["Purchase Invoice", "ifnull(clearance_date, '')", "=", ""],
- ["Purchase Invoice", "docstatus", "=", 1],
- ["Purchase Invoice", "company", "=", this.data.company]
- ]
- }
- } else if (dt === "Expense Claim") {
- return {
- filters : [
- ["Expense Claim", "ifnull(clearance_date, '')", "=", ""],
- ["Expense Claim", "docstatus", "=", 1],
- ["Expense Claim", "company", "=", this.data.company]
- ]
- }
- }
- },
- onchange: function() {
- if (me.selected_payment !== this.value) {
- me.selected_payment = this.value;
- me.display_payment_details(this);
- }
- }
- },
- {
- fieldtype: 'Section Break',
- fieldname: 'section_break_3'
- },
- {
- fieldtype: 'HTML',
- fieldname: 'payment_details'
- },
- ];
-
- me.dialog = new frappe.ui.Dialog({
- title: __("Choose a corresponding payment"),
- fields: fields,
- size: "large"
- });
-
- const proposals_wrapper = me.dialog.fields_dict.payment_proposals.$wrapper;
- if (data && data.length > 0) {
- proposals_wrapper.append(frappe.render_template("linked_payment_header"));
- data.map(value => {
- proposals_wrapper.append(frappe.render_template("linked_payment_row", value))
- })
- } else {
- const empty_data_msg = __("ERPNext could not find any matching payment entry")
- proposals_wrapper.append(`<div class="text-center"><h5 class="text-muted">${empty_data_msg}</h5></div>`)
- }
-
- $(me.dialog.body).on('click', '.reconciliation-btn', (e) => {
- const payment_entry = $(e.target).attr('data-name');
- const payment_doctype = $(e.target).attr('data-doctype');
- frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.reconcile',
- {bank_transaction: me.bank_entry, payment_doctype: payment_doctype, payment_name: payment_entry})
- .then((result) => {
- setTimeout(function(){
- erpnext.accounts.ReconciliationList.refresh();
- }, 2000);
- me.dialog.hide();
- })
- })
-
- me.dialog.show();
- }
-
- display_payment_details(event) {
- const me = this;
- if (event.value) {
- let dt = me.dialog.fields_dict.payment_doctype.value;
- me.dialog.fields_dict['payment_details'].$wrapper.empty();
- frappe.db.get_doc(dt, event.value)
- .then(doc => {
- let displayed_docs = []
- let payment = []
- if (dt === "Payment Entry") {
- payment.currency = doc.payment_type == "Receive" ? doc.paid_to_account_currency : doc.paid_from_account_currency;
- payment.doctype = dt
- payment.posting_date = doc.posting_date;
- payment.party = doc.party;
- payment.reference_no = doc.reference_no;
- payment.reference_date = doc.reference_date;
- payment.paid_amount = doc.paid_amount;
- payment.name = doc.name;
- displayed_docs.push(payment);
- } else if (dt === "Journal Entry") {
- doc.accounts.forEach(payment => {
- if (payment.account === me.gl_account) {
- payment.doctype = dt;
- payment.posting_date = doc.posting_date;
- payment.party = doc.pay_to_recd_from;
- payment.reference_no = doc.cheque_no;
- payment.reference_date = doc.cheque_date;
- payment.currency = payment.account_currency;
- payment.paid_amount = payment.credit > 0 ? payment.credit : payment.debit;
- payment.name = doc.name;
- displayed_docs.push(payment);
- }
- })
- } else if (dt === "Sales Invoice") {
- doc.payments.forEach(payment => {
- if (payment.clearance_date === null || payment.clearance_date === "") {
- payment.doctype = dt;
- payment.posting_date = doc.posting_date;
- payment.party = doc.customer;
- payment.reference_no = doc.remarks;
- payment.currency = doc.currency;
- payment.paid_amount = payment.amount;
- payment.name = doc.name;
- displayed_docs.push(payment);
- }
- })
- }
-
- const details_wrapper = me.dialog.fields_dict.payment_details.$wrapper;
- details_wrapper.append(frappe.render_template("linked_payment_header"));
- displayed_docs.forEach(payment => {
- details_wrapper.append(frappe.render_template("linked_payment_row", payment));
- })
- })
- }
-
- }
-}
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.json b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.json
deleted file mode 100644
index feea368..0000000
--- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "content": null,
- "creation": "2018-11-24 12:03:14.646669",
- "docstatus": 0,
- "doctype": "Page",
- "idx": 0,
- "modified": "2018-11-24 12:03:14.646669",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "bank-reconciliation",
- "owner": "Administrator",
- "page_name": "bank-reconciliation",
- "roles": [
- {
- "role": "System Manager"
- },
- {
- "role": "Accounts Manager"
- },
- {
- "role": "Accounts User"
- }
- ],
- "script": null,
- "standard": "Yes",
- "style": null,
- "system_page": 0,
- "title": "Bank Reconciliation"
-}
\ No newline at end of file
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py
deleted file mode 100644
index 8abe20c..0000000
--- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py
+++ /dev/null
@@ -1,369 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-import difflib
-from frappe.utils import flt
-from six import iteritems
-from erpnext import get_company_currency
-
-@frappe.whitelist()
-def reconcile(bank_transaction, payment_doctype, payment_name):
- transaction = frappe.get_doc("Bank Transaction", bank_transaction)
- payment_entry = frappe.get_doc(payment_doctype, payment_name)
-
- account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
- gl_entry = frappe.get_doc("GL Entry", dict(account=account, voucher_type=payment_doctype, voucher_no=payment_name))
-
- if payment_doctype == "Payment Entry" and payment_entry.unallocated_amount > transaction.unallocated_amount:
- frappe.throw(_("The unallocated amount of Payment Entry {0} is greater than the Bank Transaction's unallocated amount").format(payment_name))
-
- if transaction.unallocated_amount == 0:
- frappe.throw(_("This bank transaction is already fully reconciled"))
-
- if transaction.credit > 0 and gl_entry.credit > 0:
- frappe.throw(_("The selected payment entry should be linked with a debtor bank transaction"))
-
- if transaction.debit > 0 and gl_entry.debit > 0:
- frappe.throw(_("The selected payment entry should be linked with a creditor bank transaction"))
-
- add_payment_to_transaction(transaction, payment_entry, gl_entry)
-
- return 'reconciled'
-
-def add_payment_to_transaction(transaction, payment_entry, gl_entry):
- gl_amount, transaction_amount = (gl_entry.credit, transaction.debit) if gl_entry.credit > 0 else (gl_entry.debit, transaction.credit)
- allocated_amount = gl_amount if gl_amount <= transaction_amount else transaction_amount
- transaction.append("payment_entries", {
- "payment_document": payment_entry.doctype,
- "payment_entry": payment_entry.name,
- "allocated_amount": allocated_amount
- })
-
- transaction.save()
- transaction.update_allocations()
-
-@frappe.whitelist()
-def get_linked_payments(bank_transaction):
- transaction = frappe.get_doc("Bank Transaction", bank_transaction)
- bank_account = frappe.db.get_values("Bank Account", transaction.bank_account, ["account", "company"], as_dict=True)
-
- # Get all payment entries with a matching amount
- amount_matching = check_matching_amount(bank_account[0].account, bank_account[0].company, transaction)
-
- # Get some data from payment entries linked to a corresponding bank transaction
- description_matching = get_matching_descriptions_data(bank_account[0].company, transaction)
-
- if amount_matching:
- return check_amount_vs_description(amount_matching, description_matching)
-
- elif description_matching:
- description_matching = filter(lambda x: not x.get('clearance_date'), description_matching)
- if not description_matching:
- return []
-
- return sorted(list(description_matching), key = lambda x: x["posting_date"], reverse=True)
-
- else:
- return []
-
-def check_matching_amount(bank_account, company, transaction):
- payments = []
- amount = transaction.credit if transaction.credit > 0 else transaction.debit
-
- payment_type = "Receive" if transaction.credit > 0 else "Pay"
- account_from_to = "paid_to" if transaction.credit > 0 else "paid_from"
- currency_field = "paid_to_account_currency as currency" if transaction.credit > 0 else "paid_from_account_currency as currency"
-
- payment_entries = frappe.get_all("Payment Entry", fields=["'Payment Entry' as doctype", "name", "paid_amount", "payment_type", "reference_no", "reference_date",
- "party", "party_type", "posting_date", "{0}".format(currency_field)], filters=[["paid_amount", "like", "{0}%".format(amount)],
- ["docstatus", "=", "1"], ["payment_type", "=", [payment_type, "Internal Transfer"]], ["ifnull(clearance_date, '')", "=", ""], ["{0}".format(account_from_to), "=", "{0}".format(bank_account)]])
-
- jea_side = "debit" if transaction.credit > 0 else "credit"
- journal_entries = frappe.db.sql(f"""
- SELECT
- 'Journal Entry' as doctype, je.name, je.posting_date, je.cheque_no as reference_no,
- jea.account_currency as currency, je.pay_to_recd_from as party, je.cheque_date as reference_date,
- jea.{jea_side}_in_account_currency as paid_amount
- FROM
- `tabJournal Entry Account` as jea
- JOIN
- `tabJournal Entry` as je
- ON
- jea.parent = je.name
- WHERE
- (je.clearance_date is null or je.clearance_date='0000-00-00')
- AND
- jea.account = %(bank_account)s
- AND
- jea.{jea_side}_in_account_currency like %(txt)s
- AND
- je.docstatus = 1
- """, {
- 'bank_account': bank_account,
- 'txt': '%%%s%%' % amount
- }, as_dict=True)
-
- if transaction.credit > 0:
- sales_invoices = frappe.db.sql("""
- SELECT
- 'Sales Invoice' as doctype, si.name, si.customer as party,
- si.posting_date, sip.amount as paid_amount
- FROM
- `tabSales Invoice Payment` as sip
- JOIN
- `tabSales Invoice` as si
- ON
- sip.parent = si.name
- WHERE
- (sip.clearance_date is null or sip.clearance_date='0000-00-00')
- AND
- sip.account = %s
- AND
- sip.amount like %s
- AND
- si.docstatus = 1
- """, (bank_account, amount), as_dict=True)
- else:
- sales_invoices = []
-
- if transaction.debit > 0:
- purchase_invoices = frappe.get_all("Purchase Invoice",
- fields = ["'Purchase Invoice' as doctype", "name", "paid_amount", "supplier as party", "posting_date", "currency"],
- filters=[
- ["paid_amount", "like", "{0}%".format(amount)],
- ["docstatus", "=", "1"],
- ["is_paid", "=", "1"],
- ["ifnull(clearance_date, '')", "=", ""],
- ["cash_bank_account", "=", "{0}".format(bank_account)]
- ]
- )
-
- mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account",
- filters={"default_account": bank_account}, fields=["parent"])]
-
- company_currency = get_company_currency(company)
-
- expense_claims = frappe.get_all("Expense Claim",
- fields=["'Expense Claim' as doctype", "name", "total_sanctioned_amount as paid_amount",
- "employee as party", "posting_date", "'{0}' as currency".format(company_currency)],
- filters=[
- ["total_sanctioned_amount", "like", "{0}%".format(amount)],
- ["docstatus", "=", "1"],
- ["is_paid", "=", "1"],
- ["ifnull(clearance_date, '')", "=", ""],
- ["mode_of_payment", "in", "{0}".format(tuple(mode_of_payments))]
- ]
- )
- else:
- purchase_invoices = expense_claims = []
-
- for data in [payment_entries, journal_entries, sales_invoices, purchase_invoices, expense_claims]:
- if data:
- payments.extend(data)
-
- return payments
-
-def get_matching_descriptions_data(company, transaction):
- if not transaction.description :
- return []
-
- bank_transactions = frappe.db.sql("""
- SELECT
- bt.name, bt.description, bt.date, btp.payment_document, btp.payment_entry
- FROM
- `tabBank Transaction` as bt
- LEFT JOIN
- `tabBank Transaction Payments` as btp
- ON
- bt.name = btp.parent
- WHERE
- bt.allocated_amount > 0
- AND
- bt.docstatus = 1
- """, as_dict=True)
-
- selection = []
- for bank_transaction in bank_transactions:
- if bank_transaction.description:
- seq=difflib.SequenceMatcher(lambda x: x == " ", transaction.description, bank_transaction.description)
-
- if seq.ratio() > 0.6:
- bank_transaction["ratio"] = seq.ratio()
- selection.append(bank_transaction)
-
- document_types = set([x["payment_document"] for x in selection])
-
- links = {}
- for document_type in document_types:
- links[document_type] = [x["payment_entry"] for x in selection if x["payment_document"]==document_type]
-
-
- data = []
- company_currency = get_company_currency(company)
- for key, value in iteritems(links):
- if key == "Payment Entry":
- data.extend(frappe.get_all("Payment Entry", filters=[["name", "in", value]],
- fields=["'Payment Entry' as doctype", "posting_date", "party", "reference_no",
- "reference_date", "paid_amount", "paid_to_account_currency as currency", "clearance_date"]))
- if key == "Journal Entry":
- journal_entries = frappe.get_all("Journal Entry", filters=[["name", "in", value]],
- fields=["name", "'Journal Entry' as doctype", "posting_date",
- "pay_to_recd_from as party", "cheque_no as reference_no", "cheque_date as reference_date",
- "total_credit as paid_amount", "clearance_date"])
- for journal_entry in journal_entries:
- journal_entry_accounts = frappe.get_all("Journal Entry Account", filters={"parenttype": journal_entry["doctype"], "parent": journal_entry["name"]}, fields=["account_currency"])
- journal_entry["currency"] = journal_entry_accounts[0]["account_currency"] if journal_entry_accounts else company_currency
- data.extend(journal_entries)
- if key == "Sales Invoice":
- data.extend(frappe.get_all("Sales Invoice", filters=[["name", "in", value]], fields=["'Sales Invoice' as doctype", "posting_date", "customer_name as party", "paid_amount", "currency"]))
- if key == "Purchase Invoice":
- data.extend(frappe.get_all("Purchase Invoice", filters=[["name", "in", value]], fields=["'Purchase Invoice' as doctype", "posting_date", "supplier_name as party", "paid_amount", "currency"]))
- if key == "Expense Claim":
- expense_claims = frappe.get_all("Expense Claim", filters=[["name", "in", value]], fields=["'Expense Claim' as doctype", "posting_date", "employee_name as party", "total_amount_reimbursed as paid_amount"])
- data.extend([dict(x,**{"currency": company_currency}) for x in expense_claims])
-
- return data
-
-def check_amount_vs_description(amount_matching, description_matching):
- result = []
-
- if description_matching:
- for am_match in amount_matching:
- for des_match in description_matching:
- if des_match.get("clearance_date"):
- continue
-
- if am_match["party"] == des_match["party"]:
- if am_match not in result:
- result.append(am_match)
- continue
-
- if "reference_no" in am_match and "reference_no" in des_match:
- # Sequence Matcher does not handle None as input
- am_reference = am_match["reference_no"] or ""
- des_reference = des_match["reference_no"] or ""
-
- if difflib.SequenceMatcher(lambda x: x == " ", am_reference, des_reference).ratio() > 70:
- if am_match not in result:
- result.append(am_match)
- if result:
- return sorted(result, key = lambda x: x["posting_date"], reverse=True)
- else:
- return sorted(amount_matching, key = lambda x: x["posting_date"], reverse=True)
-
- else:
- return sorted(amount_matching, key = lambda x: x["posting_date"], reverse=True)
-
-def get_matching_transactions_payments(description_matching):
- payments = [x["payment_entry"] for x in description_matching]
-
- payment_by_ratio = {x["payment_entry"]: x["ratio"] for x in description_matching}
-
- if payments:
- reference_payment_list = frappe.get_all("Payment Entry", fields=["name", "paid_amount", "payment_type", "reference_no", "reference_date",
- "party", "party_type", "posting_date", "paid_to_account_currency"], filters=[["name", "in", payments]])
-
- return sorted(reference_payment_list, key=lambda x: payment_by_ratio[x["name"]])
-
- else:
- return []
-
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def payment_entry_query(doctype, txt, searchfield, start, page_len, filters):
- account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account")
- if not account:
- return
-
- return frappe.db.sql("""
- SELECT
- name, party, paid_amount, received_amount, reference_no
- FROM
- `tabPayment Entry`
- WHERE
- (clearance_date is null or clearance_date='0000-00-00')
- AND (paid_from = %(account)s or paid_to = %(account)s)
- AND (name like %(txt)s or party like %(txt)s)
- AND docstatus = 1
- ORDER BY
- if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), name
- LIMIT
- %(start)s, %(page_len)s""",
- {
- 'txt': "%%%s%%" % txt,
- '_txt': txt.replace("%", ""),
- 'start': start,
- 'page_len': page_len,
- 'account': account
- }
- )
-
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def journal_entry_query(doctype, txt, searchfield, start, page_len, filters):
- account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account")
-
- return frappe.db.sql("""
- SELECT
- jea.parent, je.pay_to_recd_from,
- if(jea.debit_in_account_currency > 0, jea.debit_in_account_currency, jea.credit_in_account_currency)
- FROM
- `tabJournal Entry Account` as jea
- LEFT JOIN
- `tabJournal Entry` as je
- ON
- jea.parent = je.name
- WHERE
- (je.clearance_date is null or je.clearance_date='0000-00-00')
- AND
- jea.account = %(account)s
- AND
- (jea.parent like %(txt)s or je.pay_to_recd_from like %(txt)s)
- AND
- je.docstatus = 1
- ORDER BY
- if(locate(%(_txt)s, jea.parent), locate(%(_txt)s, jea.parent), 99999),
- jea.parent
- LIMIT
- %(start)s, %(page_len)s""",
- {
- 'txt': "%%%s%%" % txt,
- '_txt': txt.replace("%", ""),
- 'start': start,
- 'page_len': page_len,
- 'account': account
- }
- )
-
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def sales_invoices_query(doctype, txt, searchfield, start, page_len, filters):
- return frappe.db.sql("""
- SELECT
- sip.parent, si.customer, sip.amount, sip.mode_of_payment
- FROM
- `tabSales Invoice Payment` as sip
- LEFT JOIN
- `tabSales Invoice` as si
- ON
- sip.parent = si.name
- WHERE
- (sip.clearance_date is null or sip.clearance_date='0000-00-00')
- AND
- (sip.parent like %(txt)s or si.customer like %(txt)s)
- ORDER BY
- if(locate(%(_txt)s, sip.parent), locate(%(_txt)s, sip.parent), 99999),
- sip.parent
- LIMIT
- %(start)s, %(page_len)s""",
- {
- 'txt': "%%%s%%" % txt,
- '_txt': txt.replace("%", ""),
- 'start': start,
- 'page_len': page_len
- }
- )
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_transaction_header.html b/erpnext/accounts/page/bank_reconciliation/bank_transaction_header.html
deleted file mode 100644
index 94f183b..0000000
--- a/erpnext/accounts/page/bank_reconciliation/bank_transaction_header.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<div class="transaction-header">
- <div class="level list-row list-row-head text-muted small">
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ __("Date") }}
- </div>
- <div class="col-xs-11 col-sm-4 ellipsis list-subject">
- {{ __("Description") }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ __("Debit") }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ __("Credit") }}
- </div>
- <div class="col-sm-1 ellipsis hidden-xs">
- {{ __("Currency") }}
- </div>
- <div class="col-sm-1 ellipsis">
- </div>
- </div>
-</div>
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html b/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html
deleted file mode 100644
index 742b84c..0000000
--- a/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<div class="list-row transaction-item">
- <div>
- <div class="clickable-section" data-name={{ name }}>
- <div class="col-sm-2 ellipsis hidden-xs">
- {%= frappe.datetime.str_to_user(date) %}
- </div>
- <div class="col-xs-8 col-sm-4 ellipsis list-subject">
- {{ description }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {%= format_currency(debit, currency) %}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {%= format_currency(credit, currency) %}
- </div>
- <div class="col-sm-1 ellipsis hidden-xs">
- {{ currency }}
- </div>
- </div>
- <div class="col-xs-3 col-sm-1">
- <div class="btn-group">
- <a class="dropdown-toggle btn btn-default btn-xs" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- <span>Actions </span>
- <span class="caret"></span>
- </a>
- <ul class="dropdown-menu reports-dropdown" style="max-height: 300px; overflow-y: auto; right: 0px; left: auto;">
- <li><a class="new-reconciliation" data-name={{ name }}>{{ __("Reconcile") }}</a></li>
- <li class="divider"></li>
- <li><a class="new-payment" data-name={{ name }}>{{ __("New Payment") }}</a></li>
- <li><a class="new-invoice" data-name={{ name }}>{{ __("New Invoice") }}</a></li>
- <li><a class="new-expense" data-name={{ name }}>{{ __("New Expense") }}</a></li>
- </ul>
- </div>
- </div>
- </div>
-</div>
diff --git a/erpnext/accounts/page/bank_reconciliation/linked_payment_header.html b/erpnext/accounts/page/bank_reconciliation/linked_payment_header.html
deleted file mode 100644
index 4542c36..0000000
--- a/erpnext/accounts/page/bank_reconciliation/linked_payment_header.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<div class="transaction-header">
- <div class="level list-row list-row-head text-muted small">
- <div class="col-xs-3 col-sm-2 ellipsis">
- {{ __("Payment Name") }}
- </div>
- <div class="col-xs-3 col-sm-2 ellipsis">
- {{ __("Reference Date") }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ __("Amount") }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ __("Party") }}
- </div>
- <div class="col-xs-3 col-sm-2 ellipsis">
- {{ __("Reference Number") }}
- </div>
- <div class="col-xs-2 col-sm-2">
- </div>
- </div>
-</div>
diff --git a/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html b/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html
deleted file mode 100644
index bdbc9fc..0000000
--- a/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<div class="list-row">
- <div>
- <div class="col-xs-3 col-sm-2 ellipsis">
- {{ name }}
- </div>
- <div class="col-xs-3 col-sm-2 ellipsis">
- {% if (typeof reference_date !== "undefined") %}
- {%= frappe.datetime.str_to_user(reference_date) %}
- {% else %}
- {% if (typeof posting_date !== "undefined") %}
- {%= frappe.datetime.str_to_user(posting_date) %}
- {% endif %}
- {% endif %}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ format_currency(paid_amount, currency) }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {% if (typeof party !== "undefined") %}
- {{ party }}
- {% endif %}
- </div>
- <div class="col-xs-3 col-sm-2 ellipsis">
- {% if (typeof reference_no !== "undefined") %}
- {{ reference_no }}
- {% else %}
- {{ "" }}
- {% endif %}
- </div>
- <div class="col-xs-2 col-sm-2">
- <div class="text-right margin-bottom">
- <button class="btn btn-primary btn-xs reconciliation-btn" data-doctype="{{ doctype }}" data-name="{{ name }}">{{ __("Reconcile") }}</button>
- </div>
- </div>
- </div>
-</div>
\ No newline at end of file
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 38b2284..e01cb6e 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -617,6 +617,7 @@
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)
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 8eef2ad..71c26e8 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
@@ -22,8 +22,8 @@
</p>
</div>
{% endif %}
+ <h5 class="font-bold" style="margin-top: 0px;">1. Transaction Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
- <h5 class="font-bold" style="margin-left: 15px; margin-top: 0px;">1. Transaction Details</h5>
<div class="col-xs-8 column-break">
<div class="row data-field">
<div class="col-xs-4"><label>IRN</label></div>
@@ -54,8 +54,8 @@
<img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
</div>
</div>
+ <h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">2. Party Details</h5>
<div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
- <h5 class="font-bold" style="margin-left: 15px; margin-bottom: 0px;">2. Party Details</h5>
{%- set seller = einvoice.SellerDtls -%}
<div class="col-xs-6 column-break">
<h5 style="margin-bottom: 5px;">Seller</h5>
@@ -89,7 +89,7 @@
</div>
</div>
<div style="overflow-x: auto;">
- <h5 class="font-bold" style="margin-bottom: 0px;">3. Item Details</h5>
+ <h5 class="font-bold" style="margin-top: 15px; margin-bottom: 10px;">3. Item Details</h5>
<table class="table table-bordered">
<thead>
<tr>
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
index 79a6aab..f4fd06b 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
@@ -258,7 +258,7 @@
{% } %}
{% } else { %}
{% if(data[i]["party"]|| " ") { %}
- {% if((data[i]["party"]) != __("'Total'")) { %}
+ {% if(!data[i]["is_total_row"]) { %}
<td>
{% if(!(filters.customer || filters.supplier)) { %}
{%= data[i]["party"] %}
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index 51fc7ec..444b40e 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -364,7 +364,7 @@
payment_terms_details = frappe.db.sql("""
select
si.name, si.party_account_currency, si.currency, si.conversion_rate,
- ps.due_date, ps.payment_amount, ps.description, ps.paid_amount
+ ps.due_date, ps.payment_amount, ps.description, ps.paid_amount, ps.discounted_amount
from `tab{0}` si, `tabPayment Schedule` ps
where
si.name = ps.parent and
@@ -395,13 +395,13 @@
"invoiced": invoiced,
"invoice_grand_total": row.invoiced,
"payment_term": d.description,
- "paid": d.paid_amount,
+ "paid": d.paid_amount + d.discounted_amount,
"credit_note": 0.0,
- "outstanding": invoiced - d.paid_amount
+ "outstanding": invoiced - d.paid_amount - d.discounted_amount
}))
if d.paid_amount:
- row['paid'] -= d.paid_amount
+ row['paid'] -= d.paid_amount + d.discounted_amount
def allocate_closing_to_term(self, row, term, key):
if row[key]:
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 d011689..0c4a422 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -222,7 +222,7 @@
set_gl_entries_by_account(start_date,
end_date, root.lft, root.rgt, filters,
- gl_entries_by_account, accounts_by_name, ignore_closing_entries=False)
+ gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
accumulate_values_into_parents(accounts, accounts_by_name, companies)
@@ -240,8 +240,7 @@
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
for entries in gl_entries_by_account.values():
for entry in entries:
- key = entry.account_number or entry.account_name
- d = accounts_by_name.get(key)
+ d = accounts_by_name.get(entry.account_name)
if d:
for company in companies:
# check if posting date is within the period
@@ -256,7 +255,8 @@
"""accumulate children's values in parent accounts"""
for d in reversed(accounts):
if d.parent_account:
- account = d.parent_account.split(' - ')[0].strip()
+ account = d.parent_account_name
+
if not accounts_by_name.get(account):
continue
@@ -267,16 +267,34 @@
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)
if not accounts:
return None, None
+ accounts = update_parent_account_names(accounts)
+
accounts, accounts_by_name, parent_children_map = filter_accounts(accounts)
return accounts, accounts_by_name
+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.
+ """
+ name_to_account_map = { d.name : d.account_name for d in accounts }
+
+ for account in accounts:
+ if account.parent_account:
+ account["parent_account_name"] = name_to_account_map[account.parent_account]
+
+ return accounts
+
def get_companies(filters):
companies = {}
all_companies = get_subsidiary_companies(filters.get('company'))
@@ -339,7 +357,7 @@
return data
def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account,
- accounts_by_name, ignore_closing_entries=False):
+ accounts_by_name, accounts, ignore_closing_entries=False):
"""Returns a dict like { "account": [gl entries], ... }"""
company_lft, company_rgt = frappe.get_cached_value('Company',
@@ -381,16 +399,32 @@
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
for entry in gl_entries:
- key = entry.account_number or entry.account_name
- validate_entries(key, entry, accounts_by_name)
- gl_entries_by_account.setdefault(key, []).append(entry)
+ 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 validate_entries(key, entry, accounts_by_name):
+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)
+
+def validate_entries(key, entry, accounts_by_name, accounts):
if key not in accounts_by_name:
- field = "Account number" if entry.account_number else "Account name"
- frappe.throw(_("{0} {1} is not present in the parent company").format(field, key))
+ args = get_account_details(entry.account)
+
+ if args.parent_account:
+ parent_args = get_account_details(args.parent_account)
+
+ args.update({
+ 'lft': parent_args.lft + 1,
+ 'rgt': parent_args.rgt - 1,
+ 'root_type': parent_args.root_type,
+ 'report_type': parent_args.report_type
+ })
+
+ accounts_by_name.setdefault(key, args)
+ accounts.append(args)
def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = []
@@ -436,8 +470,7 @@
parent_children_map = {}
accounts_by_name = {}
for d in accounts:
- key = d.account_number or d.account_name
- accounts_by_name[key] = d
+ accounts_by_name[d.account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d)
filtered_accounts = []
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index 7dfce85..14efa1f 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -51,7 +51,11 @@
"from_date": start_date
})
- to_date = add_months(start_date, months_to_add)
+ 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)
+
start_date = to_date
# Subtract one day from to_date, as it may be first day in next fiscal year or month
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index f735d87..b5d7992 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -129,6 +129,9 @@
order_by_statement = "order by posting_date, account, creation"
+ if filters.get("include_dimensions"):
+ order_by_statement = "order by posting_date, creation"
+
if filters.get("group_by") == _("Group by Voucher"):
order_by_statement = "order by posting_date, voucher_type, voucher_no"
@@ -142,7 +145,9 @@
distributed_cost_center_query = ""
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,
+ 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,
credit_in_account_currency*(DCC_allocation.percentage_allocation/100) as credit_in_account_currency """
distributed_cost_center_query = """
@@ -200,7 +205,7 @@
def get_conditions(filters):
conditions = []
- if filters.get("account"):
+ if filters.get("account") and not filters.get("include_dimensions"):
lft, rgt = frappe.db.get_value("Account", filters["account"], ["lft", "rgt"])
conditions.append("""account in (select name from tabAccount
where lft>=%s and rgt<=%s and docstatus<2)""" % (lft, rgt))
@@ -245,17 +250,19 @@
if match_conditions:
conditions.append(match_conditions)
- accounting_dimensions = get_accounting_dimensions(as_list=False)
+ if filters.get("include_dimensions"):
+ accounting_dimensions = get_accounting_dimensions(as_list=False)
- 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))
- conditions.append("{0} in %({0})s".format(dimension.fieldname))
- else:
- conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
+ if accounting_dimensions:
+ 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))
+ conditions.append("{0} in %({0})s".format(dimension.fieldname))
+ else:
+ conditions.append("{0} in (%({0})s)".format(dimension.fieldname))
return "and {}".format(" and ".join(conditions)) if conditions else ""
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index c7cfee7..a8280c1 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -55,7 +55,7 @@
except IndexError:
account = []
total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account,
- filters.company, filters.from_date, filters.to_date)
+ filters.company, filters.from_date, filters.to_date, filters.fiscal_year)
if total_invoiced_amount or tds_deducted:
row = [supplier.pan, supplier.name]
@@ -68,7 +68,7 @@
return out
-def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date):
+def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, fiscal_year):
''' calculate total invoice amount and total tds deducted for given supplier '''
entries = frappe.db.sql("""
@@ -94,7 +94,9 @@
""".format(', '.join(["'%s'" % d for d in vouchers])),
(account, from_date, to_date, company))[0][0])
- debit_note_amount = get_debit_note_amount([supplier], from_date, to_date, company=company)
+ date_range_filter = [fiscal_year, from_date, to_date]
+
+ debit_note_amount = get_debit_note_amount([supplier], date_range_filter, company=company)
total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 60d1e20..89a05b1 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -897,18 +897,18 @@
frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
-
if not warehouse_account:
warehouse_account = get_warehouse_account_map(company)
- gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
+ precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
+ gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
for voucher_type, voucher_no in stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), [])
- voucher_obj = frappe.get_doc(voucher_type, voucher_no)
+ voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle:
- if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
+ if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
_delete_gl_entries(voucher_type, voucher_no)
voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
@@ -954,16 +954,17 @@
return gl_entries
-def compare_existing_and_expected_gle(existing_gle, expected_gle):
+def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
matched = True
for entry in expected_gle:
account_existed = False
for e in existing_gle:
if entry.account == e.account:
account_existed = True
- if entry.account == e.account and entry.against_account == e.against_account \
- and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
- and (entry.debit != e.debit or entry.credit != e.credit):
+ if (entry.account == e.account and entry.against_account == e.against_account
+ and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
+ and ( flt(entry.debit, precision) != flt(e.debit, precision) or
+ flt(entry.credit, precision) != flt(e.credit, precision))):
matched = False
break
if not account_existed:
diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
index 8d24ca8..fadb665 100644
--- a/erpnext/accounts/workspace/accounting/accounting.json
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -1061,7 +1061,7 @@
"type": "Link"
}
],
- "modified": "2020-12-01 13:38:35.349024",
+ "modified": "2021-03-04 00:38:35.349024",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@@ -1071,7 +1071,7 @@
"pin_to_top": 0,
"shortcuts": [
{
- "label": "Chart Of Accounts",
+ "label": "Chart of Accounts",
"link_to": "Account",
"type": "DocType"
},
@@ -1116,4 +1116,4 @@
"type": "Dashboard"
}
]
-}
\ No newline at end of file
+}
diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
index afbd9b4..9000dea 100644
--- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
+++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
@@ -71,6 +71,7 @@
"exp_end_date": add_days(start_date, crop_task.get("end_day") - 1)
}).insert()
+ @frappe.whitelist()
def reload_linked_analysis(self):
linked_doctypes = ['Soil Texture', 'Soil Analysis', 'Plant Analysis']
required_fields = ['location', 'name', 'collection_datetime']
@@ -87,6 +88,7 @@
frappe.publish_realtime("List of Linked Docs",
output, user=frappe.session.user)
+ @frappe.whitelist()
def append_to_child(self, obj_to_append):
for doctype in obj_to_append:
for doc_name in set(obj_to_append[doctype]):
diff --git a/erpnext/agriculture/doctype/fertilizer/fertilizer.py b/erpnext/agriculture/doctype/fertilizer/fertilizer.py
index dc2781c..9cb492a 100644
--- a/erpnext/agriculture/doctype/fertilizer/fertilizer.py
+++ b/erpnext/agriculture/doctype/fertilizer/fertilizer.py
@@ -7,6 +7,7 @@
from frappe.model.document import Document
class Fertilizer(Document):
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Fertilizer'})
for doc in docs:
diff --git a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py
index 304727e..2806cc6 100644
--- a/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py
+++ b/erpnext/agriculture/doctype/plant_analysis/plant_analysis.py
@@ -8,6 +8,7 @@
from frappe.model.document import Document
class PlantAnalysis(Document):
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Plant Analysis'})
for doc in docs:
diff --git a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py
index 17b96a0..37835f8 100644
--- a/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py
+++ b/erpnext/agriculture/doctype/soil_analysis/soil_analysis.py
@@ -7,6 +7,7 @@
from frappe.model.document import Document
class SoilAnalysis(Document):
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Analysis'})
for doc in docs:
diff --git a/erpnext/agriculture/doctype/soil_texture/soil_texture.py b/erpnext/agriculture/doctype/soil_texture/soil_texture.py
index 8c1d7ed..209b2c8 100644
--- a/erpnext/agriculture/doctype/soil_texture/soil_texture.py
+++ b/erpnext/agriculture/doctype/soil_texture/soil_texture.py
@@ -13,6 +13,7 @@
soil_edit_order = [2, 1, 0]
soil_types = ['clay_composition', 'sand_composition', 'silt_composition']
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Soil Texture'})
for doc in docs:
@@ -26,6 +27,7 @@
if sum(self.get(soil_type) for soil_type in self.soil_types) != 100:
frappe.throw(_('Soil compositions do not add up to 100'))
+ @frappe.whitelist()
def update_soil_edit(self, soil_type):
self.soil_edit_order[self.soil_types.index(soil_type)] = max(self.soil_edit_order)+1
self.soil_type = self.get_soil_type()
@@ -35,8 +37,8 @@
if sum(self.soil_edit_order) < 5: return
last_edit_index = self.soil_edit_order.index(min(self.soil_edit_order))
- # set composition of the last edited soil
- self.set( self.soil_types[last_edit_index],
+ # set composition of the last edited soil
+ self.set(self.soil_types[last_edit_index],
100 - sum(cint(self.get(soil_type)) for soil_type in self.soil_types) + cint(self.get(self.soil_types[last_edit_index])))
# calculate soil type
@@ -67,4 +69,4 @@
elif (c >= 40 and sa <= 45 and si < 40):
return 'Clay'
else:
- return 'Select'
\ No newline at end of file
+ return 'Select'
diff --git a/erpnext/agriculture/doctype/water_analysis/water_analysis.py b/erpnext/agriculture/doctype/water_analysis/water_analysis.py
index 88f1fbd..d9f007c 100644
--- a/erpnext/agriculture/doctype/water_analysis/water_analysis.py
+++ b/erpnext/agriculture/doctype/water_analysis/water_analysis.py
@@ -9,11 +9,13 @@
from frappe import _
class WaterAnalysis(Document):
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Water Analysis'})
for doc in docs:
self.append('water_analysis_criteria', {'title': str(doc.name)})
+ @frappe.whitelist()
def update_lab_result_date(self):
if not self.result_datetime:
self.result_datetime = self.laboratory_testing_datetime
diff --git a/erpnext/agriculture/doctype/weather/weather.py b/erpnext/agriculture/doctype/weather/weather.py
index 938daa2..235e684 100644
--- a/erpnext/agriculture/doctype/weather/weather.py
+++ b/erpnext/agriculture/doctype/weather/weather.py
@@ -7,6 +7,7 @@
from frappe.model.document import Document
class Weather(Document):
+ @frappe.whitelist()
def load_contents(self):
docs = frappe.get_all("Agriculture Analysis Criteria", filters={'linked_doctype':'Weather'})
for doc in docs:
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index e8e8ec6..9aff144 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -553,6 +553,7 @@
make_gl_entries(gl_entries)
self.db_set('booked_fixed_asset', 1)
+ @frappe.whitelist()
def get_depreciation_rate(self, args, on_validate=False):
if isinstance(args, string_types):
args = json.loads(args)
diff --git a/erpnext/assets/doctype/asset_category/asset_category.json b/erpnext/assets/doctype/asset_category/asset_category.json
index b7d1226..a25f546 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.json
+++ b/erpnext/assets/doctype/asset_category/asset_category.json
@@ -19,7 +19,6 @@
],
"fields": [
{
- "depends_on": "eval:!doc.asset_category_name",
"fieldname": "asset_category_name",
"fieldtype": "Data",
"in_list_view": 1,
@@ -67,7 +66,7 @@
}
],
"links": [],
- "modified": "2021-01-22 12:31:14.425319",
+ "modified": "2021-02-24 15:05:38.621803",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Category",
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.json b/erpnext/buying/doctype/buying_settings/buying_settings.json
index 618212d..248cb9a 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.json
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.json
@@ -96,7 +96,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-13 12:00:23.276329",
+ "modified": "2021-03-02 17:34:04.190677",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying Settings",
@@ -113,5 +113,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
-}
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index d32e98e..ef9372e 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -133,6 +133,7 @@
d.material_request_item, "schedule_date")
+ @frappe.whitelist()
def get_last_purchase_rate(self):
"""get last purchase rates for all items"""
@@ -252,6 +253,7 @@
self.update_prevdoc_status()
# Must be called after updating ordered qty in Material Request
+ # bin uses Material Request Items to recalculate & update
self.update_requested_qty()
self.update_ordered_qty()
@@ -366,7 +368,6 @@
"Purchase Order": {
"doctype": "Purchase Receipt",
"field_map": {
- "per_billed": "per_billed",
"supplier_warehouse":"supplier_warehouse"
},
"validation": {
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index d568ef1..3c4f908 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -90,6 +90,50 @@
frappe.db.set_value('Item', '_Test Item', 'over_billing_allowance', 0)
frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0)
+ def test_update_remove_child_linked_to_mr(self):
+ """Test impact on linked PO and MR on deleting/updating row."""
+ mr = make_material_request(qty=10)
+ po = make_purchase_order(mr.name)
+ po.supplier = "_Test Supplier"
+ po.save()
+ po.submit()
+
+ first_item_of_po = po.get("items")[0]
+ existing_ordered_qty = get_ordered_qty() # 10
+ existing_requested_qty = get_requested_qty() # 0
+
+ # decrease ordered qty by 3 (10 -> 7) and add item
+ trans_item = json.dumps([
+ {
+ 'item_code': first_item_of_po.item_code,
+ 'rate': first_item_of_po.rate,
+ 'qty': 7,
+ 'docname': first_item_of_po.name
+ },
+ {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 2}
+ ])
+ update_child_qty_rate('Purchase Order', trans_item, po.name)
+ mr.reload()
+
+ # requested qty increases as ordered qty decreases
+ self.assertEqual(get_requested_qty(), existing_requested_qty + 3) # 3
+ self.assertEqual(mr.items[0].ordered_qty, 7)
+
+ self.assertEqual(get_ordered_qty(), existing_ordered_qty - 3) # 7
+
+ # delete first item linked to Material Request
+ trans_item = json.dumps([
+ {'item_code' : '_Test Item 2', 'rate' : 200, 'qty' : 2}
+ ])
+ update_child_qty_rate('Purchase Order', trans_item, po.name)
+ mr.reload()
+
+ # requested qty increases as ordered qty is 0 (deleted row)
+ self.assertEqual(get_requested_qty(), existing_requested_qty + 10) # 10
+ self.assertEqual(mr.items[0].ordered_qty, 0)
+
+ # ordered qty decreases as ordered qty is 0 (deleted row)
+ self.assertEqual(get_ordered_qty(), existing_ordered_qty - 10) # 0
def test_update_child(self):
mr = make_material_request(qty=10)
@@ -120,7 +164,6 @@
self.assertEqual(po.get("items")[0].amount, 1400)
self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3)
-
def test_update_child_adding_new_item(self):
po = create_purchase_order(do_not_save=1)
po.items[0].qty = 4
@@ -129,6 +172,7 @@
pr = make_pr_against_po(po.name, 2)
po.load_from_db()
+ existing_ordered_qty = get_ordered_qty()
first_item_of_po = po.get("items")[0]
trans_item = json.dumps([
@@ -145,7 +189,8 @@
po.reload()
self.assertEquals(len(po.get('items')), 2)
self.assertEqual(po.status, 'To Receive and Bill')
-
+ # ordered qty should increase on row addition
+ self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7)
def test_update_child_removing_item(self):
po = create_purchase_order(do_not_save=1)
@@ -156,6 +201,7 @@
po.reload()
first_item_of_po = po.get("items")[0]
+ existing_ordered_qty = get_ordered_qty()
# add an item
trans_item = json.dumps([
{
@@ -168,6 +214,10 @@
update_child_qty_rate('Purchase Order', trans_item, po.name)
po.reload()
+
+ # ordered qty should increase on row addition
+ self.assertEqual(get_ordered_qty(), existing_ordered_qty + 7)
+
# check if can remove received item
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': po.get("items")[1].name}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate, 'Purchase Order', trans_item, po.name)
@@ -187,6 +237,9 @@
self.assertEquals(len(po.get('items')), 1)
self.assertEqual(po.status, 'To Receive and Bill')
+ # ordered qty should decrease (back to initial) on row deletion
+ self.assertEqual(get_ordered_qty(), existing_ordered_qty)
+
def test_update_child_perm(self):
po = create_purchase_order(item_code= "_Test Item", qty=4)
@@ -230,13 +283,15 @@
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
- new_item_with_tax.append("taxes", {
- "item_tax_template": "Test Update Items Template",
- "valid_from": nowdate()
- })
- new_item_with_tax.save()
+ if not frappe.db.exists("Item Tax",
+ {"item_tax_template": "Test Update Items Template - _TC", "parent": "Test Item with Tax"}):
+ new_item_with_tax.append("taxes", {
+ "item_tax_template": "Test Update Items Template - _TC",
+ "valid_from": nowdate()
+ })
+ new_item_with_tax.save()
- tax_template = "_Test Account Excise Duty @ 10"
+ tax_template = "_Test Account Excise Duty @ 10 - _TC"
item = "_Test Item Home Desktop 100"
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
item_doc = frappe.get_doc("Item", item)
@@ -287,7 +342,7 @@
po.cancel()
po.delete()
new_item_with_tax.delete()
- frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
+ frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete()
def test_update_child_uom_conv_factor_change(self):
po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes")
@@ -723,7 +778,7 @@
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
make_stock_entry(target="_Test Warehouse - _TC",
- item_code="_Test Item Home Desktop 100", qty=10, basic_rate=100)
+ item_code="_Test Item Home Desktop 100", qty=20, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC",
item_code = "Test Extra Item 1", qty=100, basic_rate=100)
make_stock_entry(target="_Test Warehouse - _TC",
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 75b2954..5baf693 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -27,11 +27,17 @@
"stock_qty",
"sec_break1",
"price_list_rate",
+ "last_purchase_rate",
+ "col_break3",
+ "base_price_list_rate",
+ "discount_and_margin_section",
+ "margin_type",
+ "margin_rate_or_amount",
+ "rate_with_margin",
+ "column_break_28",
"discount_percentage",
"discount_amount",
- "col_break3",
- "last_purchase_rate",
- "base_price_list_rate",
+ "base_rate_with_margin",
"sec_break2",
"rate",
"amount",
@@ -733,15 +739,59 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "discount_and_margin_section",
+ "fieldtype": "Section Break",
+ "label": "Discount and Margin"
+ },
+ {
+ "depends_on": "price_list_rate",
+ "fieldname": "margin_type",
+ "fieldtype": "Select",
+ "label": "Margin Type",
+ "options": "\nPercentage\nAmount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate",
+ "fieldname": "margin_rate_or_amount",
+ "fieldtype": "Float",
+ "label": "Margin Rate or Amount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_28",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "base_rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:44:41.816974",
+ "modified": "2021-02-23 01:00:27.132705",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index a51498e..b530d1a 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -66,6 +66,7 @@
def on_cancel(self):
frappe.db.set(self, 'status', 'Cancelled')
+ @frappe.whitelist()
def get_supplier_email_preview(self, supplier):
"""Returns formatted email preview as string."""
rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers))
@@ -127,6 +128,10 @@
'link_doctype': 'Supplier',
'link_name': rfq_supplier.supplier
})
+ contact.append('email_ids', {
+ 'email_id': user.name,
+ 'is_primary': 1
+ })
if not contact.email_id and not contact.user:
contact.email_id = user.name
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 40362b1..4cc5753 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -26,7 +26,6 @@
"supplier_group",
"supplier_type",
"pan",
- "language",
"allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt",
"disabled",
@@ -57,6 +56,7 @@
"website",
"supplier_details",
"column_break_30",
+ "language",
"is_frozen"
],
"fields": [
@@ -384,7 +384,7 @@
"idx": 370,
"image_field": "image",
"links": [],
- "modified": "2020-06-17 23:18:20",
+ "modified": "2021-01-06 19:51:40.939087",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
diff --git a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
index 6e6eaed..2528240 100644
--- a/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
+++ b/erpnext/buying/doctype/supplier_scorecard/test_supplier_scorecard.py
@@ -9,9 +9,7 @@
class TestSupplierScorecard(unittest.TestCase):
def test_create_scorecard(self):
- delete_test_scorecards()
- my_doc = make_supplier_scorecard()
- doc = my_doc.insert()
+ doc = make_supplier_scorecard().insert()
self.assertEqual(doc.name, valid_scorecard[0].get("supplier"))
def test_criteria_weight(self):
@@ -121,7 +119,8 @@
{
"weight":100.0,
"doctype":"Supplier Scorecard Scoring Criteria",
- "criteria_name":"Delivery"
+ "criteria_name":"Delivery",
+ "formula": "100"
}
],
"supplier":"_Test Supplier",
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 12a81c7..36d399c 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -26,7 +26,8 @@
class AccountMissingError(frappe.ValidationError): pass
-force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
+force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate",
+ "pricing_rules", "weight_per_unit", "weight_uom", "total_weight")
class AccountsController(TransactionBase):
def __init__(self, *args, **kwargs):
@@ -516,6 +517,7 @@
frappe.db.sql("""delete from `tab%s` where parentfield=%s and parent = %s
and allocated_amount = 0""" % (childtype, '%s', '%s'), (parentfield, self.name))
+ @frappe.whitelist()
def apply_shipping_rule(self):
if self.shipping_rule:
shipping_rule = frappe.get_doc("Shipping Rule", self.shipping_rule)
@@ -536,6 +538,7 @@
return {}
+ @frappe.whitelist()
def set_advances(self):
"""Returns list of advances against Account, Party, Reference"""
@@ -920,7 +923,8 @@
else:
for d in self.get("payment_schedule"):
if d.invoice_portion:
- d.payment_amount = flt(grand_total * flt(d.invoice_portion) / 100, d.precision('payment_amount'))
+ d.payment_amount = flt(grand_total * flt(d.invoice_portion / 100), d.precision('payment_amount'))
+ d.outstanding = d.payment_amount
def set_due_date(self):
due_dates = [d.due_date for d in self.get("payment_schedule") if d.due_date]
@@ -1235,18 +1239,24 @@
term_details.description = term.description
term_details.invoice_portion = term.invoice_portion
term_details.payment_amount = flt(term.invoice_portion) * flt(grand_total) / 100
+ term_details.discount_type = term.discount_type
+ term_details.discount = term.discount
+ # term_details.discounted_amount = flt(grand_total) * (term.discount / 100) if term.discount_type == 'Percentage' else discount
+ term_details.outstanding = term_details.payment_amount
+ term_details.mode_of_payment = term.mode_of_payment
+
if bill_date:
term_details.due_date = get_due_date(term, bill_date)
+ term_details.discount_date = get_discount_date(term, bill_date)
elif posting_date:
term_details.due_date = get_due_date(term, posting_date)
+ term_details.discount_date = get_discount_date(term, posting_date)
if getdate(term_details.due_date) < getdate(posting_date):
term_details.due_date = posting_date
- term_details.mode_of_payment = term.mode_of_payment
return term_details
-
def get_due_date(term, posting_date=None, bill_date=None):
due_date = None
date = bill_date or posting_date
@@ -1258,6 +1268,16 @@
due_date = add_months(get_last_day(date), term.credit_months)
return due_date
+def get_discount_date(term, posting_date=None, bill_date=None):
+ discount_validity = None
+ date = bill_date or posting_date
+ if term.discount_validity_based_on == "Day(s) after invoice date":
+ discount_validity = add_days(date, term.discount_validity)
+ elif term.discount_validity_based_on == "Day(s) after the end of the invoice month":
+ discount_validity = add_days(get_last_day(date), term.discount_validity)
+ elif term.discount_validity_based_on == "Month(s) after the end of the invoice month":
+ discount_validity = add_months(get_last_day(date), term.discount_validity)
+ return discount_validity
def get_supplier_block_status(party_name):
"""
@@ -1316,25 +1336,63 @@
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
child_item = frappe.new_doc(child_doctype, p_doc, child_docname)
item = frappe.get_doc("Item", trans_item.get('item_code'))
+
for field in ("item_code", "item_name", "description", "item_group"):
- child_item.update({field: item.get(field)})
+ child_item.update({field: item.get(field)})
+
date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date"
child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
+ child_item.stock_uom = item.stock_uom
child_item.uom = trans_item.get("uom") or item.stock_uom
+ child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
+
if child_doctype == "Purchase Order Item":
- child_item.base_rate = 1 # Initiallize value will update in parent validation
- child_item.base_amount = 1 # Initiallize value will update in parent validation
+ # Initialized value will update in parent validation
+ child_item.base_rate = 1
+ child_item.base_amount = 1
if child_doctype == "Sales Order Item":
child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
if not child_item.warehouse:
frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
.format(frappe.bold("default warehouse"), frappe.bold(item.item_code)))
+
set_child_tax_template_and_map(item, child_item, p_doc)
add_taxes_from_tax_template(child_item, p_doc)
return child_item
+def validate_child_on_delete(row, parent):
+ """Check if partially transacted item (row) is being deleted."""
+ if parent.doctype == "Sales Order":
+ if flt(row.delivered_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(row.idx, row.item_code))
+ if flt(row.work_order_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(row.idx, row.item_code))
+ if flt(row.ordered_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(row.idx, row.item_code))
+
+ if parent.doctype == "Purchase Order" and flt(row.received_qty):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(row.idx, row.item_code))
+
+ if flt(row.billed_amt):
+ frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(row.idx, row.item_code))
+
+def update_bin_on_delete(row, doctype):
+ """Update bin for deleted item (row)."""
+ from erpnext.stock.stock_balance import update_bin_qty, get_reserved_qty, get_ordered_qty, get_indented_qty
+ qty_dict = {}
+
+ if doctype == "Sales Order":
+ qty_dict["reserved_qty"] = get_reserved_qty(row.item_code, row.warehouse)
+ else:
+ if row.material_request_item:
+ qty_dict["indented_qty"] = get_indented_qty(row.item_code, row.warehouse)
+
+ qty_dict["ordered_qty"] = get_ordered_qty(row.item_code, row.warehouse)
+
+ update_bin_qty(row.item_code, row.warehouse, qty_dict)
+
def validate_and_delete_children(parent, data):
deleted_children = []
updated_item_names = [d.get("docname") for d in data]
@@ -1343,23 +1401,17 @@
deleted_children.append(item)
for d in deleted_children:
- if parent.doctype == "Sales Order":
- if flt(d.delivered_qty):
- frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been delivered").format(d.idx, d.item_code))
- if flt(d.work_order_qty):
- frappe.throw(_("Row #{0}: Cannot delete item {1} which has work order assigned to it.").format(d.idx, d.item_code))
- if flt(d.ordered_qty):
- frappe.throw(_("Row #{0}: Cannot delete item {1} which is assigned to customer's purchase order.").format(d.idx, d.item_code))
-
- if parent.doctype == "Purchase Order" and flt(d.received_qty):
- frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been received").format(d.idx, d.item_code))
-
- if flt(d.billed_amt):
- frappe.throw(_("Row #{0}: Cannot delete item {1} which has already been billed.").format(d.idx, d.item_code))
-
+ validate_child_on_delete(d, parent)
d.cancel()
d.delete()
+ # need to update ordered qty in Material Request first
+ # bin uses Material Request Items to recalculate & update
+ parent.update_prevdoc_status()
+
+ for d in deleted_children:
+ update_bin_on_delete(d, parent.doctype)
+
@frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def check_doc_permissions(doc, perm_type='create'):
@@ -1394,7 +1446,7 @@
)
def get_new_child_item(item_row):
- child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
+ child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
def validate_quantity(child_item, d):
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index e469838..219d529 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -278,7 +278,7 @@
if self.is_subcontracted == "Yes":
if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and not self.supplier_warehouse:
- frappe.throw(_("Supplier Warehouse mandatory for sub-contracted Purchase Receipt"))
+ frappe.throw(_("Supplier Warehouse mandatory for sub-contracted {0}").format(self.doctype))
for item in self.get("items"):
if item in self.sub_contracted_items and not item.bom:
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index 0e1829a..de61b35 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -204,8 +204,6 @@
return items
def get_returned_qty_map_for_row(row_name, doctype):
- if doctype == "POS Invoice": return {}
-
child_doctype = doctype + " Item"
reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
@@ -354,7 +352,12 @@
target_doc.so_detail = source_doc.so_detail
target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account
- target_doc.sales_invoice_item = source_doc.name
+
+ if doctype == "Sales Invoice":
+ target_doc.sales_invoice_item = source_doc.name
+ else:
+ target_doc.pos_invoice_item = source_doc.name
+
target_doc.price_list_rate = 0
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index c61b67b..edc40c4 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -142,6 +142,11 @@
self.base_net_total * sales_person.allocated_percentage / 100.0,
self.precision("allocated_amount", sales_person))
+ if sales_person.commission_rate:
+ sales_person.incentives = flt(
+ sales_person.allocated_amount * flt(sales_person.commission_rate) / 100.0,
+ self.precision("incentives", sales_person))
+
total += sales_person.allocated_percentage
if sales_team and total != 100.0:
@@ -497,4 +502,4 @@
for d in obj.get("items"):
if d.item_code:
if getattr(d, "income_account", None):
- set_item_default(d.item_code, obj.company, 'income_account', d.income_account)
\ No newline at end of file
+ set_item_default(d.item_code, obj.company, 'income_account', d.income_account)
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index ea9659c..f352bae 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -74,7 +74,7 @@
gl_list = []
warehouse_with_no_account = []
- precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
+ precision = self.get_debit_field_precision()
for item_row in voucher_details:
sle_list = sle_map.get(item_row.name)
@@ -131,7 +131,13 @@
if frappe.db.get_value("Warehouse", wh, "company"):
frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
- return process_gl_map(gl_list)
+ return process_gl_map(gl_list, precision=precision)
+
+ def get_debit_field_precision(self):
+ if not frappe.flags.debit_field_precision:
+ frappe.flags.debit_field_precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
+
+ return frappe.flags.debit_field_precision
def update_stock_ledger_entries(self, sle):
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
@@ -244,7 +250,7 @@
.format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
else:
- is_expense_account = frappe.db.get_value("Account",
+ is_expense_account = frappe.get_cached_value("Account",
item.get("expense_account"), "report_type")=="Profit and Loss"
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") and not is_expense_account:
frappe.throw(_("Expense / Difference account ({0}) must be a 'Profit or Loss' account")
@@ -400,7 +406,8 @@
def set_rate_of_stock_uom(self):
if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
for d in self.get("items"):
- d.stock_uom_rate = d.rate / d.conversion_factor
+ if d.conversion_factor:
+ d.stock_uom_rate = d.rate / d.conversion_factor
def validate_internal_transfer(self):
if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \
@@ -488,48 +495,53 @@
"voucher_no": self.name,
"company": self.company
})
- if check_if_future_sle_exists(args):
+ if future_sle_exists(args):
create_repost_item_valuation_entry(args)
elif not is_reposting_pending():
check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name)
-
+
def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
-def check_if_future_sle_exists(args):
- sl_entries = frappe.db.get_all("Stock Ledger Entry",
+def future_sle_exists(args):
+ sl_entries = frappe.get_all("Stock Ledger Entry",
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
fields=["item_code", "warehouse"],
order_by="creation asc")
- distinct_item_warehouses = list(set([(d.item_code, d.warehouse) for d in sl_entries]))
+ if not sl_entries:
+ return
- sle_exists = False
- for item_code, warehouse in distinct_item_warehouses:
- args.update({
- "item_code": item_code,
- "warehouse": warehouse
- })
- if get_sle(args):
- sle_exists = True
- break
- return sle_exists
+ warehouse_items_map = {}
+ for entry in sl_entries:
+ if entry.warehouse not in warehouse_items_map:
+ warehouse_items_map[entry.warehouse] = set()
-def get_sle(args):
+ warehouse_items_map[entry.warehouse].add(entry.item_code)
+
+ or_conditions = []
+ for warehouse, items in warehouse_items_map.items():
+ or_conditions.append(
+ "warehouse = '{}' and item_code in ({})".format(
+ warehouse,
+ ", ".join(frappe.db.escape(item) for item in items)
+ )
+ )
+
return frappe.db.sql("""
select name
from `tabStock Ledger Entry`
where
- item_code=%(item_code)s
- and warehouse=%(warehouse)s
- and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
+ ({})
+ and timestamp(posting_date, posting_time)
+ >= timestamp(%(posting_date)s, %(posting_time)s)
and voucher_no != %(voucher_no)s
and is_cancelled = 0
limit 1
- """, args)
+ """.format(" or ".join(or_conditions)), args)
def create_repost_item_valuation_entry(args):
args = frappe._dict(args)
@@ -547,4 +559,4 @@
repost_entry.allow_zero_rate = args.allow_zero_rate
repost_entry.flags.ignore_links = True
repost_entry.save()
- repost_entry.submit()
\ No newline at end of file
+ repost_entry.submit()
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index cfa4991..5f73c55 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -15,6 +15,8 @@
class calculate_taxes_and_totals(object):
def __init__(self, doc):
self.doc = doc
+ frappe.flags.round_off_applicable_accounts = []
+ get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate()
def calculate(self):
@@ -107,11 +109,14 @@
elif item.discount_amount and item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
- if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item']:
+ if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item', 'Purchase Invoice Item', 'Purchase Order Item', 'Purchase Receipt Item']:
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
- item.discount_amount = item.rate_with_margin - item.rate
+ if item.discount_amount and not item.discount_percentage:
+ item.rate -= item.discount_amount
+ else:
+ item.discount_amount = item.rate_with_margin - item.rate
elif flt(item.price_list_rate) > 0:
item.discount_amount = item.price_list_rate - item.rate
elif flt(item.price_list_rate) > 0 and not item.discount_amount:
@@ -332,10 +337,18 @@
elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty
+ current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount)
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount
+ def get_final_current_tax_amount(self, tax, current_tax_amount):
+ # Some countries need individual tax components to be rounded
+ # Handeled via regional doctypess
+ if tax.account_head in frappe.flags.round_off_applicable_accounts:
+ current_tax_amount = round(current_tax_amount, 0)
+ return current_tax_amount
+
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount):
# store tax breakup for each item
key = item.item_code or item.item_name
@@ -693,6 +706,15 @@
)
)
+@frappe.whitelist()
+def get_round_off_applicable_accounts(company, account_list):
+ account_list = get_regional_round_off_accounts(company, account_list)
+
+ return account_list
+
+@erpnext.allow_regional
+def get_regional_round_off_accounts(company, account_list):
+ pass
@erpnext.allow_regional
def update_itemised_tax_data(doc):
@@ -776,7 +798,7 @@
for d in self.doc.get(self.tax_field):
if d.account_currency == company_currency:
d.exchange_rate = 1
- elif not d.exchange_rate or d.exchange_rate == 1 or self.doc.posting_date:
+ elif not d.exchange_rate:
d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account,
account_currency=d.account_currency, company=self.doc.company)
@@ -786,4 +808,4 @@
def set_amounts_in_company_currency(self):
for d in self.doc.get(self.tax_field):
d.amount = flt(d.amount, d.precision("amount"))
- d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount"))
\ No newline at end of file
+ d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount"))
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 2df1793..1b33fd7 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -49,6 +49,7 @@
"phone",
"mobile_no",
"fax",
+ "website",
"more_info",
"type",
"market_segment",
@@ -56,8 +57,8 @@
"request_type",
"column_break3",
"company",
- "website",
"territory",
+ "language",
"unsubscribed",
"blog_subscriber",
"title"
@@ -447,13 +448,19 @@
"fieldtype": "Select",
"label": "Address Type",
"options": "Billing\nShipping\nOffice\nPersonal\nPlant\nPostal\nShop\nSubsidiary\nWarehouse\nCurrent\nPermanent\nOther"
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
}
],
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
"links": [],
- "modified": "2020-10-13 15:24:00.094811",
+ "modified": "2021-01-06 19:39:58.748978",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
diff --git a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
index 377e061..d8c6fb4 100644
--- a/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
+++ b/erpnext/crm/doctype/linkedin_settings/linkedin_settings.py
@@ -11,7 +11,8 @@
from six.moves.urllib.parse import urlencode
class LinkedInSettings(Document):
- def get_authorization_url(self):
+ @frappe.whitelist()
+ def get_authorization_url(self):
params = urlencode({
"response_type":"code",
"client_id": self.consumer_key,
@@ -35,7 +36,7 @@
headers = {
"Content-Type": "application/x-www-form-urlencoded"
}
-
+
response = self.http_post(url=url, data=body, headers=headers)
response = frappe.parse_json(response.content.decode())
self.db_set("access_token", response["access_token"])
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 08958b7..ac374a9 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -24,6 +24,12 @@
frm.trigger('set_contact_link');
}
},
+ contact_date: function(frm) {
+ if(frm.doc.contact_date < frappe.datetime.now_datetime()){
+ frm.set_value("contact_date", "");
+ frappe.throw(__("Next follow up date should be greater than now."))
+ }
+ },
onload_post_render: function(frm) {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index eee13f7..2e09a76 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -54,6 +54,7 @@
"campaign",
"column_break1",
"transaction_date",
+ "language",
"amended_from",
"lost_reasons"
],
@@ -419,12 +420,18 @@
"fieldtype": "Duration",
"label": "First Response Time",
"read_only": 1
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
}
],
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
- "modified": "2020-08-12 17:34:35.066961",
+ "modified": "2021-01-06 19:42:46.190051",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index 47b05f3..23ad98a 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -85,6 +85,7 @@
self.opportunity_from = "Lead"
self.party_name = lead_name
+ @frappe.whitelist()
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_active_quotation():
frappe.db.set(self, 'status', 'Lost')
@@ -248,7 +249,6 @@
"doctype": "Quotation",
"field_map": {
"opportunity_from": "quotation_to",
- "opportunity_type": "order_type",
"name": "enq_no",
}
},
diff --git a/erpnext/crm/doctype/twitter_settings/twitter_settings.py b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
index 976a23d..1e1beab 100644
--- a/erpnext/crm/doctype/twitter_settings/twitter_settings.py
+++ b/erpnext/crm/doctype/twitter_settings/twitter_settings.py
@@ -11,6 +11,7 @@
from tweepy.error import TweepError
class TwitterSettings(Document):
+ @frappe.whitelist()
def get_authorize_url(self):
callback_url = "{0}/api/method/erpnext.crm.doctype.twitter_settings.twitter_settings.callback?".format(frappe.utils.get_url())
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"), callback_url)
@@ -21,12 +22,12 @@
frappe.msgprint(_("Error! Failed to get request token."))
frappe.throw(_('Invalid {0} or {1}').format(frappe.bold("Consumer Key"), frappe.bold("Consumer Secret Key")))
-
+
def get_access_token(self, oauth_token, oauth_verifier):
auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
- auth.request_token = {
+ auth.request_token = {
'oauth_token' : oauth_token,
- 'oauth_token_secret' : oauth_verifier
+ 'oauth_token_secret' : oauth_verifier
}
try:
@@ -50,10 +51,10 @@
frappe.throw(_('Invalid Consumer Key or Consumer Secret Key'))
def get_api(self, access_token, access_token_secret):
- # authentication of consumer key and secret
- auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
- # authentication of access token and secret
- auth.set_access_token(access_token, access_token_secret)
+ # authentication of consumer key and secret
+ auth = tweepy.OAuthHandler(self.consumer_key, self.get_password(fieldname="consumer_secret"))
+ # authentication of access token and secret
+ auth.set_access_token(access_token, access_token_secret)
return tweepy.API(auth)
@@ -64,7 +65,7 @@
if media:
media_id = self.upload_image(media)
return self.send_tweet(text, media_id)
-
+
def upload_image(self, media):
media = get_file_path(media)
api = self.get_api(self.access_token, self.access_token_secret)
diff --git a/erpnext/education/api.py b/erpnext/education/api.py
index 948e7cc..afa0be9 100644
--- a/erpnext/education/api.py
+++ b/erpnext/education/api.py
@@ -36,6 +36,7 @@
student.save()
program_enrollment = frappe.new_doc("Program Enrollment")
program_enrollment.student = student.name
+ program_enrollment.student_category = student.student_category
program_enrollment.student_name = student.title
program_enrollment.program = frappe.db.get_value("Student Applicant", source_name, "program")
frappe.publish_realtime('enroll_student_progress', {"progress": [2, 4]}, user=frappe.session.user)
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
index 97c29ab..6a0dcf4 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
@@ -13,6 +13,7 @@
class CourseSchedulingTool(Document):
+ @frappe.whitelist()
def schedule_course(self):
"""Creates course schedules as per specified parameters"""
diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.py b/erpnext/education/doctype/fee_schedule/fee_schedule.py
index 1543acd..0b025c7 100644
--- a/erpnext/education/doctype/fee_schedule/fee_schedule.py
+++ b/erpnext/education/doctype/fee_schedule/fee_schedule.py
@@ -52,6 +52,7 @@
self.grand_total = no_of_students*self.total_amount
self.grand_total_in_words = money_in_words(self.grand_total)
+ @frappe.whitelist()
def create_fees(self):
self.db_set("fee_creation_status", "In Process")
frappe.publish_realtime("fee_schedule_progress",
diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py
index d18c0f9..b282bab 100644
--- a/erpnext/education/doctype/program_enrollment/program_enrollment.py
+++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py
@@ -91,6 +91,8 @@
(fee, fee) for fee in fee_list]
msgprint(_("Fee Records Created - {0}").format(comma_and(fee_list)))
+
+ @frappe.whitelist()
def get_courses(self):
return frappe.db.sql('''select course from `tabProgram Course` where parent = %s and required = 1''', (self.program), as_dict=1)
diff --git a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
index 9f8f9f4..5833b67 100644
--- a/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
+++ b/erpnext/education/doctype/program_enrollment_tool/program_enrollment_tool.py
@@ -14,6 +14,7 @@
academic_term_reqd = cint(frappe.db.get_single_value('Education Settings', 'academic_term_reqd'))
self.set_onload("academic_term_reqd", academic_term_reqd)
+ @frappe.whitelist()
def get_students(self):
students = []
if not self.get_students_from:
@@ -30,7 +31,7 @@
.format(condition), self.as_dict(), as_dict=1)
elif self.get_students_from == "Program Enrollment":
condition2 = 'and student_batch_name=%(student_batch)s' if self.student_batch else " "
- students = frappe.db.sql('''select student, student_name, student_batch_name from `tabProgram Enrollment`
+ students = frappe.db.sql('''select student, student_name, student_batch_name, student_category from `tabProgram Enrollment`
where program=%(program)s and academic_year=%(academic_year)s {0} {1} and docstatus != 2'''
.format(condition, condition2), self.as_dict(), as_dict=1)
@@ -49,6 +50,7 @@
else:
frappe.throw(_("No students Found"))
+ @frappe.whitelist()
def enroll_students(self):
total = len(self.students)
for i, stud in enumerate(self.students):
@@ -57,6 +59,7 @@
prog_enrollment = frappe.new_doc("Program Enrollment")
prog_enrollment.student = stud.student
prog_enrollment.student_name = stud.student_name
+ prog_enrollment.student_category = stud.student_category
prog_enrollment.program = self.new_program
prog_enrollment.academic_year = self.new_academic_year
prog_enrollment.academic_term = self.new_academic_term
diff --git a/erpnext/education/doctype/student_applicant/student_applicant.json b/erpnext/education/doctype/student_applicant/student_applicant.json
index 6df9b9a..95f9224 100644
--- a/erpnext/education/doctype/student_applicant/student_applicant.json
+++ b/erpnext/education/doctype/student_applicant/student_applicant.json
@@ -11,6 +11,7 @@
"middle_name",
"last_name",
"program",
+ "student_category",
"lms_only",
"paid",
"column_break_8",
@@ -257,12 +258,18 @@
"options": "Student Applicant",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "student_category",
+ "fieldtype": "Link",
+ "label": "Student Category",
+ "options": "Student Category"
}
],
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2020-10-05 13:59:45.631647",
+ "modified": "2021-03-01 23:00:25.119241",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Applicant",
diff --git a/erpnext/education/doctype/student_attendance/student_attendance.json b/erpnext/education/doctype/student_attendance/student_attendance.json
index 55384b9..e6e46d1 100644
--- a/erpnext/education/doctype/student_attendance/student_attendance.json
+++ b/erpnext/education/doctype/student_attendance/student_attendance.json
@@ -10,6 +10,7 @@
"naming_series",
"student",
"student_name",
+ "student_mobile_number",
"course_schedule",
"student_group",
"column_break_3",
@@ -93,11 +94,19 @@
"options": "Student Attendance",
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fetch_from": "student.student_mobile_number",
+ "fieldname": "student_mobile_number",
+ "fieldtype": "Read Only",
+ "label": "Student Mobile Number",
+ "options": "Phone"
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-07-08 13:55:42.580181",
+ "modified": "2021-03-24 00:02:11.005895",
"modified_by": "Administrator",
"module": "Education",
"name": "Student Attendance",
diff --git a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py
index d7645e3..dc8667e 100644
--- a/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py
+++ b/erpnext/education/doctype/student_group_creation_tool/student_group_creation_tool.py
@@ -9,6 +9,7 @@
from erpnext.education.doctype.student_group.student_group import get_students
class StudentGroupCreationTool(Document):
+ @frappe.whitelist()
def get_courses(self):
group_list = []
@@ -42,6 +43,7 @@
return group_list
+ @frappe.whitelist()
def create_student_groups(self):
if not self.courses:
frappe.throw(_("""No Student Groups created."""))
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
index cc75a0a..148c1a6 100644
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
+++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_methods.py
@@ -117,7 +117,7 @@
return response
except Exception as e:
delay = math.pow(4, x) * 125
- frappe.log_error(message=e, title=str(mws_method))
+ frappe.log_error(message=e, title=f'Method "{mws_method.__name__}" failed')
time.sleep(delay)
continue
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
index 407f826..8f3b427 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
@@ -103,7 +103,7 @@
}
],
"links": [],
- "modified": "2021-01-29 12:02:16.106942",
+ "modified": "2021-03-02 17:35:14.084342",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Mpesa Settings",
@@ -147,5 +147,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
index b571802..fdfaa1b 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
@@ -59,9 +59,10 @@
request_amounts.append(amount)
else:
request_amounts = [request_amount]
-
+
return request_amounts
+ @frappe.whitelist()
def get_account_balance_info(self):
payload = dict(
reference_doctype="Mpesa Settings",
@@ -198,7 +199,7 @@
completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
completed_payments.append(completed_amount)
mpesa_receipts.append(completed_mpesa_receipt)
-
+
return mpesa_receipts, completed_payments
def get_account_balance(request_payload):
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
index 122aa41..e7176ea 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.json
@@ -70,7 +70,7 @@
],
"issingle": 1,
"links": [],
- "modified": "2020-10-29 20:24:56.916104",
+ "modified": "2021-03-02 17:35:27.544259",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Plaid Settings",
@@ -88,5 +88,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 70c7f3f..16c6573 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -15,6 +15,7 @@
class PlaidSettings(Document):
@staticmethod
+ @frappe.whitelist()
def get_link_token():
plaid = PlaidConnector()
return plaid.get_link_token()
@@ -204,8 +205,8 @@
"date": getdate(transaction["date"]),
"status": status,
"bank_account": bank_account,
- "debit": debit,
- "credit": credit,
+ "deposit": debit,
+ "withdrawal": credit,
"currency": transaction["iso_currency_code"],
"transaction_id": transaction["transaction_id"],
"reference_number": transaction["payment_meta"]["reference_number"],
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
index 3c90637..e2243ea 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/test_plaid_settings.py
@@ -23,14 +23,9 @@
doc.cancel()
doc.delete()
- for ba in frappe.get_all("Bank Account"):
- frappe.get_doc("Bank Account", ba.name).delete()
-
- for at in frappe.get_all("Bank Account Type"):
- frappe.get_doc("Bank Account Type", at.name).delete()
-
- for ast in frappe.get_all("Bank Account Subtype"):
- frappe.get_doc("Bank Account Subtype", ast.name).delete()
+ for doctype in ("Bank Account", "Bank Account Type", "Bank Account Subtype"):
+ for d in frappe.get_all(doctype):
+ frappe.delete_doc(doctype, d.name, force=True)
def test_plaid_disabled(self):
frappe.db.set_value("Plaid Settings", None, "enabled", 0)
diff --git a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
index 96a533e..866ea66 100644
--- a/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
+++ b/erpnext/erpnext_integrations/doctype/quickbooks_migrator/quickbooks_migrator.py
@@ -54,6 +54,7 @@
self.authorization_url = self.oauth.authorization_url(self.authorization_endpoint)[0]
+ @frappe.whitelist()
def migrate(self):
frappe.enqueue_doc("QuickBooks Migrator", "QuickBooks Migrator", "_migrate", queue="long")
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json
index 20ec063..308e7d1 100644
--- a/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/shopify_settings.json
@@ -330,7 +330,7 @@
],
"issingle": 1,
"links": [],
- "modified": "2020-11-05 20:44:03.664891",
+ "modified": "2021-03-02 17:35:41.953317",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Shopify Settings",
@@ -348,5 +348,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
index 30fa23c..5f471ab 100644
--- a/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
+++ b/erpnext/erpnext_integrations/doctype/shopify_settings/test_shopify_settings.py
@@ -5,7 +5,7 @@
import frappe
import unittest, os, json
-from frappe.utils import cstr
+from frappe.utils import cstr, cint
from erpnext.erpnext_integrations.connectors.shopify_connection import create_order
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_product import make_item
from erpnext.erpnext_integrations.doctype.shopify_settings.sync_customer import create_customer
@@ -13,21 +13,31 @@
class ShopifySettings(unittest.TestCase):
- def setUp(self):
+ @classmethod
+ def setUpClass(cls):
frappe.set_user("Administrator")
+ cls.allow_negative_stock = cint(frappe.db.get_value('Stock Settings', None, 'allow_negative_stock'))
+ if not cls.allow_negative_stock:
+ frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1)
+
# use the fixture data
- import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"),
- ignore_links=True, overwrite=True)
+ import_doc(frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json"))
frappe.reload_doctype("Customer")
frappe.reload_doctype("Sales Order")
frappe.reload_doctype("Delivery Note")
frappe.reload_doctype("Sales Invoice")
- self.setup_shopify()
+ cls.setup_shopify()
- def setup_shopify(self):
+ @classmethod
+ def tearDownClass(cls):
+ if not cls.allow_negative_stock:
+ frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0)
+
+ @classmethod
+ def setup_shopify(cls):
shopify_settings = frappe.get_doc("Shopify Settings")
shopify_settings.taxes = []
@@ -57,21 +67,20 @@
"delivery_note_series": "DN-"
}).save(ignore_permissions=True)
- self.shopify_settings = shopify_settings
+ cls.shopify_settings = shopify_settings
def test_order(self):
- ### Create Customer ###
+ # Create Customer
with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_customer.json")) as shopify_customer:
shopify_customer = json.load(shopify_customer)
create_customer(shopify_customer.get("customer"), self.shopify_settings)
- ### Create Item ###
+ # Create Item
with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_item.json")) as shopify_item:
shopify_item = json.load(shopify_item)
make_item("_Test Warehouse - _TC", shopify_item.get("product"))
-
- ### Create Order ###
+ # Create Order
with open (os.path.join(os.path.dirname(__file__), "test_data", "shopify_order.json")) as shopify_order:
shopify_order = json.load(shopify_order)
@@ -81,17 +90,17 @@
self.assertEqual(cstr(shopify_order.get("order").get("id")), sales_order.shopify_order_id)
- #check for customer
+ # Check for customer
shopify_order_customer_id = cstr(shopify_order.get("order").get("customer").get("id"))
sales_order_customer_id = frappe.get_value("Customer", sales_order.customer, "shopify_customer_id")
self.assertEqual(shopify_order_customer_id, sales_order_customer_id)
- #check sales invoice
+ # Check sales invoice
sales_invoice = frappe.get_doc("Sales Invoice", {"shopify_order_id": sales_order.shopify_order_id})
self.assertEqual(sales_invoice.rounded_total, sales_order.rounded_total)
- #check delivery note
+ # Check delivery note
delivery_note_count = frappe.db.sql("""select count(*) from `tabDelivery Note`
where shopify_order_id = %s""", sales_order.shopify_order_id)[0][0]
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
index 462685f..907a223 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.py
@@ -594,18 +594,22 @@
frappe.db.set_value("Price List", "Tally Price List", "enabled", 0)
frappe.flags.in_migrate = False
+ @frappe.whitelist()
def process_master_data(self):
self.set_status("Processing Master Data")
frappe.enqueue_doc(self.doctype, self.name, "_process_master_data", queue="long", timeout=3600)
+ @frappe.whitelist()
def import_master_data(self):
self.set_status("Importing Master Data")
frappe.enqueue_doc(self.doctype, self.name, "_import_master_data", queue="long", timeout=3600)
+ @frappe.whitelist()
def process_day_book_data(self):
self.set_status("Processing Day Book Data")
frappe.enqueue_doc(self.doctype, self.name, "_process_day_book_data", queue="long", timeout=3600)
+ @frappe.whitelist()
def import_day_book_data(self):
self.set_status("Importing Day Book Data")
frappe.enqueue_doc(self.doctype, self.name, "_import_day_book_data", queue="long", timeout=3600)
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.js b/erpnext/healthcare/doctype/appointment_type/appointment_type.js
index 15916a5..861675a 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.js
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.js
@@ -2,4 +2,82 @@
// For license information, please see license.txt
frappe.ui.form.on('Appointment Type', {
+ refresh: function(frm) {
+ frm.set_query('price_list', function() {
+ return {
+ filters: {'selling': 1}
+ };
+ });
+
+ frm.set_query('medical_department', 'items', function(doc) {
+ let item_list = doc.items.map(({medical_department}) => medical_department);
+ return {
+ filters: [
+ ['Medical Department', 'name', 'not in', item_list]
+ ]
+ };
+ });
+
+ frm.set_query('op_consulting_charge_item', 'items', function() {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
+
+ frm.set_query('inpatient_visit_charge_item', 'items', function() {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
+ }
});
+
+frappe.ui.form.on('Appointment Type Service Item', {
+ op_consulting_charge_item: function(frm, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ if (frm.doc.price_list && d.op_consulting_charge_item) {
+ frappe.call({
+ 'method': 'frappe.client.get_value',
+ args: {
+ 'doctype': 'Item Price',
+ 'filters': {
+ 'item_code': d.op_consulting_charge_item,
+ 'price_list': frm.doc.price_list
+ },
+ 'fieldname': ['price_list_rate']
+ },
+ callback: function(data) {
+ if (data.message.price_list_rate) {
+ frappe.model.set_value(cdt, cdn, 'op_consulting_charge', data.message.price_list_rate);
+ }
+ }
+ });
+ }
+ },
+
+ inpatient_visit_charge_item: function(frm, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ if (frm.doc.price_list && d.inpatient_visit_charge_item) {
+ frappe.call({
+ 'method': 'frappe.client.get_value',
+ args: {
+ 'doctype': 'Item Price',
+ 'filters': {
+ 'item_code': d.inpatient_visit_charge_item,
+ 'price_list': frm.doc.price_list
+ },
+ 'fieldname': ['price_list_rate']
+ },
+ callback: function (data) {
+ if (data.message.price_list_rate) {
+ frappe.model.set_value(cdt, cdn, 'inpatient_visit_charge', data.message.price_list_rate);
+ }
+ }
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.json b/erpnext/healthcare/doctype/appointment_type/appointment_type.json
index 58753bb..3872318 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.json
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.json
@@ -12,7 +12,10 @@
"appointment_type",
"ip",
"default_duration",
- "color"
+ "color",
+ "billing_section",
+ "price_list",
+ "items"
],
"fields": [
{
@@ -52,10 +55,27 @@
"label": "Color",
"no_copy": 1,
"report_hide": 1
+ },
+ {
+ "fieldname": "billing_section",
+ "fieldtype": "Section Break",
+ "label": "Billing"
+ },
+ {
+ "fieldname": "price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "options": "Price List"
+ },
+ {
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Appointment Type Service Items",
+ "options": "Appointment Type Service Item"
}
],
"links": [],
- "modified": "2020-02-03 21:06:05.833050",
+ "modified": "2021-01-22 09:41:05.010524",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Appointment Type",
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.py b/erpnext/healthcare/doctype/appointment_type/appointment_type.py
index 1dacffa..67a24f3 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.py
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.py
@@ -4,6 +4,53 @@
from __future__ import unicode_literals
from frappe.model.document import Document
+import frappe
class AppointmentType(Document):
- pass
+ def validate(self):
+ if self.items and self.price_list:
+ for item in self.items:
+ existing_op_item_price = frappe.db.exists('Item Price', {
+ 'item_code': item.op_consulting_charge_item,
+ 'price_list': self.price_list
+ })
+
+ if not existing_op_item_price and item.op_consulting_charge_item and item.op_consulting_charge:
+ make_item_price(self.price_list, item.op_consulting_charge_item, item.op_consulting_charge)
+
+ existing_ip_item_price = frappe.db.exists('Item Price', {
+ 'item_code': item.inpatient_visit_charge_item,
+ 'price_list': self.price_list
+ })
+
+ if not existing_ip_item_price and item.inpatient_visit_charge_item and item.inpatient_visit_charge:
+ make_item_price(self.price_list, item.inpatient_visit_charge_item, item.inpatient_visit_charge)
+
+@frappe.whitelist()
+def get_service_item_based_on_department(appointment_type, department):
+ item_list = frappe.db.get_value('Appointment Type Service Item',
+ filters = {'medical_department': department, 'parent': appointment_type},
+ fieldname = ['op_consulting_charge_item',
+ 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
+ as_dict = 1
+ )
+
+ # if department wise items are not set up
+ # use the generic items
+ if not item_list:
+ item_list = frappe.db.get_value('Appointment Type Service Item',
+ filters = {'parent': appointment_type},
+ fieldname = ['op_consulting_charge_item',
+ 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
+ as_dict = 1
+ )
+
+ return item_list
+
+def make_item_price(price_list, item, item_price):
+ frappe.get_doc({
+ 'doctype': 'Item Price',
+ 'price_list': price_list,
+ 'item_code': item,
+ 'price_list_rate': item_price
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
diff --git a/erpnext/accounts/doctype/bank_statement_settings_item/__init__.py b/erpnext/healthcare/doctype/appointment_type_service_item/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/bank_statement_settings_item/__init__.py
rename to erpnext/healthcare/doctype/appointment_type_service_item/__init__.py
diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json
new file mode 100644
index 0000000..5ff68cd
--- /dev/null
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json
@@ -0,0 +1,67 @@
+{
+ "actions": [],
+ "creation": "2021-01-22 09:34:53.373105",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "medical_department",
+ "op_consulting_charge_item",
+ "op_consulting_charge",
+ "column_break_4",
+ "inpatient_visit_charge_item",
+ "inpatient_visit_charge"
+ ],
+ "fields": [
+ {
+ "fieldname": "medical_department",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Medical Department",
+ "options": "Medical Department"
+ },
+ {
+ "fieldname": "op_consulting_charge_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Out Patient Consulting Charge Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "op_consulting_charge",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Out Patient Consulting Charge"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "inpatient_visit_charge_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Inpatient Visit Charge Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "inpatient_visit_charge",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Inpatient Visit Charge Item"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-01-22 09:35:26.503443",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Appointment Type Service Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
new file mode 100644
index 0000000..b2e0e82
--- /dev/null
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class AppointmentTypeServiceItem(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
index ff51646..b55d5d6 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
@@ -364,7 +364,7 @@
let age = new Date();
age.setTime(ageMS);
let years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
// List Stock items
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
index 325c209..cbf89ee 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
@@ -54,6 +54,7 @@
def set_title(self):
self.title = _('{0} - {1}').format(self.patient_name or self.patient, self.procedure_template)[:100]
+ @frappe.whitelist()
def complete_procedure(self):
if self.consume_stock and self.items:
stock_entry = make_stock_entry(self)
@@ -96,6 +97,7 @@
if self.consume_stock and self.items:
return stock_entry
+ @frappe.whitelist()
def start_procedure(self):
allow_start = self.set_actual_qty()
if allow_start:
@@ -116,6 +118,7 @@
return allow_start
+ @frappe.whitelist()
def make_material_receipt(self, submit=False):
stock_entry = frappe.new_doc('Stock Entry')
diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
index cb747f9..8162f03 100644
--- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
+++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
@@ -159,6 +159,7 @@
"fieldname": "op_consulting_charge",
"fieldtype": "Currency",
"label": "Out Patient Consulting Charge",
+ "mandatory_depends_on": "op_consulting_charge_item",
"options": "Currency"
},
{
@@ -174,7 +175,8 @@
{
"fieldname": "inpatient_visit_charge",
"fieldtype": "Currency",
- "label": "Inpatient Visit Charge"
+ "label": "Inpatient Visit Charge",
+ "mandatory_depends_on": "inpatient_visit_charge_item"
},
{
"depends_on": "eval: !doc.__islocal",
@@ -280,7 +282,7 @@
],
"image_field": "image",
"links": [],
- "modified": "2020-04-06 13:44:24.759623",
+ "modified": "2021-01-22 10:14:43.187675",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Practitioner",
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
index e731908..3a299ed 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
@@ -14,6 +14,7 @@
def validate(self):
self.validate_medication_orders()
+ @frappe.whitelist()
def get_medication_orders(self):
# pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self)
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py
index 33cbbec..b379e98 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py
+++ b/erpnext/healthcare/doctype/inpatient_medication_order/inpatient_medication_order.py
@@ -57,6 +57,7 @@
self.db_set('status', status)
+ @frappe.whitelist()
def add_order_entries(self, order):
if order.get('drug_code'):
dosage = frappe.get_doc('Prescription Dosage', order.get('dosage'))
diff --git a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py
index a21caca..21776d2 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py
+++ b/erpnext/healthcare/doctype/inpatient_medication_order/test_inpatient_medication_order.py
@@ -81,15 +81,8 @@
self.ip_record.reload()
discharge_patient(self.ip_record)
- for entry in frappe.get_all('Inpatient Medication Entry'):
- doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
- doc.cancel()
- doc.delete()
-
- for entry in frappe.get_all('Inpatient Medication Order'):
- doc = frappe.get_doc('Inpatient Medication Order', entry.name)
- doc.cancel()
- doc.delete()
+ for doctype in ["Inpatient Medication Entry", "Inpatient Medication Order"]:
+ frappe.db.sql("delete from `tab{doctype}`".format(doctype=doctype))
def create_dosage_form():
if not frappe.db.exists('Dosage Form', 'Tablet'):
diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json
index 5ced845..aaf0e85 100644
--- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json
+++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.json
@@ -53,7 +53,7 @@
"discharge_ordered_date",
"discharge_practitioner",
"discharge_encounter",
- "discharge_date",
+ "discharge_datetime",
"cb_discharge",
"discharge_instructions",
"followup_date",
@@ -404,14 +404,15 @@
"permlevel": 1
},
{
- "fieldname": "discharge_date",
- "fieldtype": "Date",
+ "fieldname": "discharge_datetime",
+ "fieldtype": "Datetime",
"label": "Discharge Date",
"read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-05-21 02:26:22.144575",
+ "modified": "2021-03-18 14:44:11.689956",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Record",
diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
index 88d7f0b..f4d1eaf 100644
--- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
+++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
@@ -53,12 +53,15 @@
+ """ <b><a href="/app/Form/Inpatient Record/{0}">{0}</a></b>""".format(ip_record[0].name))
frappe.throw(msg)
+ @frappe.whitelist()
def admit(self, service_unit, check_in, expected_discharge=None):
admit_patient(self, service_unit, check_in, expected_discharge)
+ @frappe.whitelist()
def discharge(self):
discharge_patient(self)
+ @frappe.whitelist()
def transfer(self, service_unit, check_in, leave_from):
if leave_from:
patient_leave_service_unit(self, check_in, leave_from)
@@ -151,7 +154,7 @@
def discharge_patient(inpatient_record):
validate_inpatient_invoicing(inpatient_record)
- inpatient_record.discharge_date = today()
+ inpatient_record.discharge_datetime = now_datetime()
inpatient_record.status = "Discharged"
inpatient_record.save(ignore_permissions = True)
diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.js b/erpnext/healthcare/doctype/lab_test/lab_test.js
index f1634c1..bb7976c 100644
--- a/erpnext/healthcare/doctype/lab_test/lab_test.js
+++ b/erpnext/healthcare/doctype/lab_test/lab_test.js
@@ -258,5 +258,5 @@
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/patient/patient.js b/erpnext/healthcare/doctype/patient/patient.js
index 490f247..bce42e5 100644
--- a/erpnext/healthcare/doctype/patient/patient.js
+++ b/erpnext/healthcare/doctype/patient/patient.js
@@ -46,11 +46,11 @@
}
},
onload: function (frm) {
- if(!frm.doc.dob){
+ if (!frm.doc.dob) {
$(frm.fields_dict['age_html'].wrapper).html('');
}
- if(frm.doc.dob){
- $(frm.fields_dict['age_html'].wrapper).html('AGE : ' + get_age(frm.doc.dob));
+ if (frm.doc.dob) {
+ $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${get_age(frm.doc.dob)}`);
}
}
});
@@ -65,7 +65,7 @@
}
else {
let age_str = get_age(frm.doc.dob);
- $(frm.fields_dict['age_html'].wrapper).html('AGE : ' + age_str);
+ $(frm.fields_dict['age_html'].wrapper).html(`${__('AGE')} : ${age_str}`);
}
}
else {
diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py
index 63dd8d4..789d452 100644
--- a/erpnext/healthcare/doctype/patient/patient.py
+++ b/erpnext/healthcare/doctype/patient/patient.py
@@ -108,9 +108,10 @@
if self.dob:
dob = getdate(self.dob)
age = dateutil.relativedelta.relativedelta(getdate(), dob)
- age_str = str(age.years) + ' year(s) ' + str(age.months) + ' month(s) ' + str(age.days) + ' day(s)'
+ age_str = str(age.years) + ' ' + _("Years(s)") + ' ' + str(age.months) + ' ' + _("Month(s)") + ' ' + str(age.days) + ' ' + _("Day(s)")
return age_str
+ @frappe.whitelist()
def invoice_patient_registration(self):
if frappe.db.get_single_value('Healthcare Settings', 'registration_fee'):
company = frappe.defaults.get_user_default('company')
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
index 3d5073b..2976ef1 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
@@ -24,11 +24,13 @@
});
frm.set_query('practitioner', function() {
- return {
- filters: {
- 'department': frm.doc.department
- }
- };
+ if (frm.doc.department) {
+ return {
+ filters: {
+ 'department': frm.doc.department
+ }
+ };
+ }
});
frm.set_query('service_unit', function() {
@@ -140,6 +142,20 @@
patient: function(frm) {
if (frm.doc.patient) {
frm.trigger('toggle_payment_fields');
+ frappe.call({
+ method: 'frappe.client.get',
+ args: {
+ doctype: 'Patient',
+ name: frm.doc.patient
+ },
+ callback: function (data) {
+ let age = null;
+ if (data.message.dob) {
+ age = calculate_age(data.message.dob);
+ }
+ frappe.model.set_value(frm.doctype, frm.docname, 'patient_age', age);
+ }
+ });
} else {
frm.set_value('patient_name', '');
frm.set_value('patient_sex', '');
@@ -148,6 +164,37 @@
}
},
+ practitioner: function(frm) {
+ if (frm.doc.practitioner ) {
+ frm.events.set_payment_details(frm);
+ }
+ },
+
+ appointment_type: function(frm) {
+ if (frm.doc.appointment_type) {
+ frm.events.set_payment_details(frm);
+ }
+ },
+
+ set_payment_details: function(frm) {
+ frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing').then(val => {
+ if (val) {
+ frappe.call({
+ method: 'erpnext.healthcare.utils.get_service_item_and_practitioner_charge',
+ args: {
+ doc: frm.doc
+ },
+ callback: function(data) {
+ if (data.message) {
+ frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.practitioner_charge);
+ frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.service_item);
+ }
+ }
+ });
+ }
+ });
+ },
+
therapy_plan: function(frm) {
frm.trigger('set_therapy_type_filter');
},
@@ -190,14 +237,18 @@
// show payment fields as non-mandatory
frm.toggle_display('mode_of_payment', 0);
frm.toggle_display('paid_amount', 0);
+ frm.toggle_display('billing_item', 0);
frm.toggle_reqd('mode_of_payment', 0);
frm.toggle_reqd('paid_amount', 0);
+ frm.toggle_reqd('billing_item', 0);
} else {
// if automated appointment invoicing is disabled, hide fields
frm.toggle_display('mode_of_payment', data.message ? 1 : 0);
frm.toggle_display('paid_amount', data.message ? 1 : 0);
+ frm.toggle_display('billing_item', data.message ? 1 : 0);
frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0);
frm.toggle_reqd('paid_amount', data.message ? 1 :0);
+ frm.toggle_reqd('billing_item', data.message ? 1 : 0);
}
}
});
@@ -540,61 +591,10 @@
);
};
-frappe.ui.form.on('Patient Appointment', 'practitioner', function(frm) {
- if (frm.doc.practitioner) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Healthcare Practitioner',
- name: frm.doc.practitioner
- },
- callback: function (data) {
- frappe.model.set_value(frm.doctype, frm.docname, 'department', data.message.department);
- frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.op_consulting_charge);
- frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.op_consulting_charge_item);
- }
- });
- }
-});
-
-frappe.ui.form.on('Patient Appointment', 'patient', function(frm) {
- if (frm.doc.patient) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Patient',
- name: frm.doc.patient
- },
- callback: function (data) {
- let age = null;
- if (data.message.dob) {
- age = calculate_age(data.message.dob);
- }
- frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age);
- }
- });
- }
-});
-
-frappe.ui.form.on('Patient Appointment', 'appointment_type', function(frm) {
- if (frm.doc.appointment_type) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Appointment Type',
- name: frm.doc.appointment_type
- },
- callback: function(data) {
- frappe.model.set_value(frm.doctype,frm.docname, 'duration',data.message.default_duration);
- }
- });
- }
-});
-
let calculate_age = function(birth) {
let ageMS = Date.parse(Date()) - Date.parse(birth);
let age = new Date();
age.setTime(ageMS);
let years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
index 35600e4..83c92af 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
@@ -19,19 +19,19 @@
"inpatient_record",
"column_break_1",
"company",
+ "practitioner",
+ "practitioner_name",
+ "department",
"service_unit",
+ "section_break_12",
+ "appointment_type",
+ "duration",
"procedure_template",
"get_procedure_from_encounter",
"procedure_prescription",
"therapy_plan",
"therapy_type",
"get_prescribed_therapies",
- "practitioner",
- "practitioner_name",
- "department",
- "section_break_12",
- "appointment_type",
- "duration",
"column_break_17",
"appointment_date",
"appointment_time",
@@ -79,6 +79,7 @@
"set_only_once": 1
},
{
+ "fetch_from": "appointment_type.default_duration",
"fieldname": "duration",
"fieldtype": "Int",
"in_filter": 1,
@@ -144,7 +145,6 @@
"in_standard_filter": 1,
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
- "read_only": 1,
"reqd": 1,
"search_index": 1,
"set_only_once": 1
@@ -158,7 +158,6 @@
"in_standard_filter": 1,
"label": "Department",
"options": "Medical Department",
- "read_only": 1,
"search_index": 1,
"set_only_once": 1
},
@@ -227,12 +226,14 @@
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
- "options": "Mode of Payment"
+ "options": "Mode of Payment",
+ "read_only_depends_on": "invoiced"
},
{
"fieldname": "paid_amount",
"fieldtype": "Currency",
- "label": "Paid Amount"
+ "label": "Paid Amount",
+ "read_only_depends_on": "invoiced"
},
{
"fieldname": "column_break_2",
@@ -302,7 +303,8 @@
"fieldname": "therapy_plan",
"fieldtype": "Link",
"label": "Therapy Plan",
- "options": "Therapy Plan"
+ "options": "Therapy Plan",
+ "set_only_once": 1
},
{
"fieldname": "ref_sales_invoice",
@@ -347,7 +349,7 @@
}
],
"links": [],
- "modified": "2020-12-16 13:16:58.578503",
+ "modified": "2021-02-08 13:13:15.116833",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Appointment",
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index f2b94b8..cdd4ad3 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -26,6 +26,7 @@
def after_insert(self):
self.update_prescription_details()
+ self.set_payment_details()
invoice_appointment(self)
self.update_fee_validity()
send_confirmation_msg(self)
@@ -85,6 +86,13 @@
def set_appointment_datetime(self):
self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00")
+ def set_payment_details(self):
+ if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
+ details = get_service_item_and_practitioner_charge(self)
+ self.db_set('billing_item', details.get('service_item'))
+ if not self.paid_amount:
+ self.db_set('paid_amount', details.get('practitioner_charge'))
+
def validate_customer_created(self):
if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
if not frappe.db.get_value('Patient', self.patient, 'customer'):
@@ -105,6 +113,7 @@
if fee_validity:
frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till))
+ @frappe.whitelist()
def get_therapy_types(self):
if not self.therapy_plan:
return
@@ -148,31 +157,37 @@
fee_validity = None
if automate_invoicing and not appointment_invoiced and not fee_validity:
- sales_invoice = frappe.new_doc('Sales Invoice')
- sales_invoice.patient = appointment_doc.patient
- sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
- sales_invoice.appointment = appointment_doc.name
- sales_invoice.due_date = getdate()
- sales_invoice.company = appointment_doc.company
- sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
+ create_sales_invoice(appointment_doc)
- item = sales_invoice.append('items', {})
- item = get_appointment_item(appointment_doc, item)
- # Add payments if payment details are supplied else proceed to create invoice as Unpaid
- if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
- sales_invoice.is_pos = 1
- payment = sales_invoice.append('payments', {})
- payment.mode_of_payment = appointment_doc.mode_of_payment
- payment.amount = appointment_doc.paid_amount
+def create_sales_invoice(appointment_doc):
+ sales_invoice = frappe.new_doc('Sales Invoice')
+ sales_invoice.patient = appointment_doc.patient
+ sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
+ sales_invoice.appointment = appointment_doc.name
+ sales_invoice.due_date = getdate()
+ sales_invoice.company = appointment_doc.company
+ sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
- sales_invoice.set_missing_values(for_validate=True)
- sales_invoice.flags.ignore_mandatory = True
- sales_invoice.save(ignore_permissions=True)
- sales_invoice.submit()
- frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
- frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1)
- frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)
+ item = sales_invoice.append('items', {})
+ item = get_appointment_item(appointment_doc, item)
+
+ # Add payments if payment details are supplied else proceed to create invoice as Unpaid
+ if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
+ sales_invoice.is_pos = 1
+ payment = sales_invoice.append('payments', {})
+ payment.mode_of_payment = appointment_doc.mode_of_payment
+ payment.amount = appointment_doc.paid_amount
+
+ sales_invoice.set_missing_values(for_validate=True)
+ sales_invoice.flags.ignore_mandatory = True
+ sales_invoice.save(ignore_permissions=True)
+ sales_invoice.submit()
+ frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
+ frappe.db.set_value('Patient Appointment', appointment_doc.name, {
+ 'invoiced': 1,
+ 'ref_sales_invoice': sales_invoice.name
+ })
def check_is_new_patient(patient, name=None):
@@ -187,13 +202,14 @@
def get_appointment_item(appointment_doc, item):
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment_doc)
- item.item_code = service_item
+ details = get_service_item_and_practitioner_charge(appointment_doc)
+ charge = appointment_doc.paid_amount or details.get('practitioner_charge')
+ item.item_code = details.get('service_item')
item.description = _('Consulting Charges: {0}').format(appointment_doc.practitioner)
item.income_account = get_income_account(appointment_doc.practitioner, appointment_doc.company)
item.cost_center = frappe.get_cached_value('Company', appointment_doc.company, 'cost_center')
- item.rate = practitioner_charge
- item.amount = practitioner_charge
+ item.rate = charge
+ item.amount = charge
item.qty = 1
item.reference_dt = 'Patient Appointment'
item.reference_dn = appointment_doc.name
diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
index f7ec6f5..2bb8a53 100644
--- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
@@ -32,7 +32,8 @@
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1)
- self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'), 1)
+ appointment.reload()
+ self.assertEqual(appointment.invoiced, 1)
encounter = make_encounter(appointment.name)
self.assertTrue(encounter)
self.assertEqual(encounter.company, appointment.company)
@@ -41,7 +42,7 @@
# invoiced flag mapped from appointment
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'))
- def test_invoicing(self):
+ def test_auto_invoicing(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
@@ -57,6 +58,50 @@
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'patient'), appointment.patient)
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
+ def test_auto_invoicing_based_on_department(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ appointment_type = create_appointment_type()
+
+ appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
+ invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department')
+ appointment.reload()
+
+ self.assertEqual(appointment.invoiced, 1)
+ self.assertEqual(appointment.billing_item, 'HLC-SI-001')
+ self.assertEqual(appointment.paid_amount, 200)
+
+ sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
+ self.assertTrue(sales_invoice_name)
+ self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
+
+ def test_auto_invoicing_according_to_appointment_type_charge(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+
+ item = create_healthcare_service_items()
+ items = [{
+ 'op_consulting_charge_item': item,
+ 'op_consulting_charge': 300
+ }]
+ appointment_type = create_appointment_type(args={
+ 'name': 'Generic Appointment Type charge',
+ 'items': items
+ })
+
+ appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
+ invoice=1, appointment_type=appointment_type.name)
+ appointment.reload()
+
+ self.assertEqual(appointment.invoiced, 1)
+ self.assertEqual(appointment.billing_item, item)
+ self.assertEqual(appointment.paid_amount, 300)
+
+ sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
+ self.assertTrue(sales_invoice_name)
+
def test_appointment_cancel(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
@@ -178,14 +223,15 @@
encounter.submit()
return encounter
-def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0, service_unit=None, save=1):
+def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
+ service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items()
frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item)
frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item)
appointment = frappe.new_doc('Patient Appointment')
appointment.patient = patient
appointment.practitioner = practitioner
- appointment.department = '_Test Medical Department'
+ appointment.department = department or '_Test Medical Department'
appointment.appointment_date = appointment_date
appointment.company = '_Test Company'
appointment.duration = 15
@@ -193,7 +239,8 @@
appointment.service_unit = service_unit
if invoice:
appointment.mode_of_payment = 'Cash'
- appointment.paid_amount = 500
+ if appointment_type:
+ appointment.appointment_type = appointment_type
if procedure_template:
appointment.procedure_template = create_clinical_procedure_template().get('name')
if save:
@@ -223,4 +270,29 @@
template.description = 'Knee Surgery and Rehab'
template.rate = 50000
template.save()
- return template
\ No newline at end of file
+ return template
+
+def create_appointment_type(args=None):
+ if not args:
+ args = frappe.local.form_dict
+
+ name = args.get('name') or 'Test Appointment Type wise Charge'
+
+ if frappe.db.exists('Appointment Type', name):
+ return frappe.get_doc('Appointment Type', name)
+
+ else:
+ item = create_healthcare_service_items()
+ items = [{
+ 'medical_department': '_Test Medical Department',
+ 'op_consulting_charge_item': item,
+ 'op_consulting_charge': 200
+ }]
+ return frappe.get_doc({
+ 'doctype': 'Appointment Type',
+ 'appointment_type': args.get('name') or 'Test Appointment Type wise Charge',
+ 'default_duration': args.get('default_duration') or 20,
+ 'color': args.get('color') or '#7575ff',
+ 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}),
+ 'items': args.get('items') or items
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
index e960f0a..aaeaa69 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.js
@@ -358,5 +358,5 @@
let age = new Date();
age.setTime(ageMS);
let years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py
index 2e8c994..887d58a 100644
--- a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py
+++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py
@@ -34,6 +34,7 @@
frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format(
entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
+ @frappe.whitelist()
def get_doctype_fields(self, document_type, fields):
multicheck_fields = []
doc_fields = frappe.get_meta(document_type).fields
@@ -49,6 +50,7 @@
return multicheck_fields
+ @frappe.whitelist()
def get_date_field_for_dt(self, document_type):
meta = frappe.get_meta(document_type)
date_fields = meta.get('fields', {
diff --git a/erpnext/healthcare/doctype/sample_collection/sample_collection.js b/erpnext/healthcare/doctype/sample_collection/sample_collection.js
index 0390391..ddf8285 100644
--- a/erpnext/healthcare/doctype/sample_collection/sample_collection.js
+++ b/erpnext/healthcare/doctype/sample_collection/sample_collection.js
@@ -36,5 +36,5 @@
var age = new Date();
age.setTime(ageMS);
var years = age.getFullYear() - 1970;
- return years + ' Year(s) ' + age.getMonth() + ' Month(s) ' + age.getDate() + ' Day(s)';
+ return `${years} ${__('Years(s)')} ${age.getMonth()} ${__('Month(s)')} ${age.getDate()} ${__('Day(s)')}`;
};
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
index ac01c60..e209660 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -33,6 +33,7 @@
self.db_set('total_sessions', total_sessions)
self.db_set('total_sessions_completed', total_sessions_completed)
+ @frappe.whitelist()
def set_therapy_details_from_template(self):
# Add therapy types in the child table
self.set('therapy_plan_details', [])
diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py
index d4027df..d3d22c8 100644
--- a/erpnext/healthcare/utils.py
+++ b/erpnext/healthcare/utils.py
@@ -5,9 +5,11 @@
from __future__ import unicode_literals
import math
import frappe
+import json
from frappe import _
from frappe.utils.formatters import format_value
from frappe.utils import time_diff_in_hours, rounded
+from six import string_types
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account
from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity
from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple
@@ -64,7 +66,9 @@
income_account = None
service_item = None
if appointment.practitioner:
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment)
+ details = get_service_item_and_practitioner_charge(appointment)
+ service_item = details.get('service_item')
+ practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(appointment.practitioner, appointment.company)
appointments_to_invoice.append({
'reference_type': 'Patient Appointment',
@@ -97,7 +101,9 @@
frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'):
continue
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter)
+ details = get_service_item_and_practitioner_charge(encounter)
+ service_item = details.get('service_item')
+ practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(encounter.practitioner, encounter.company)
encounters_to_invoice.append({
@@ -173,7 +179,7 @@
if procedure.invoice_separately_as_consumables and procedure.consume_stock \
and procedure.status == 'Completed' and not procedure.consumption_invoiced:
- service_item = get_healthcare_service_item('clinical_procedure_consumable_item')
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
if not service_item:
msg = _('Please Configure Clinical Procedure Consumable Item in ')
msg += '''<b><a href='/app/Form/Healthcare Settings'>Healthcare Settings</a></b>'''
@@ -304,24 +310,50 @@
return therapy_sessions_to_invoice
-
+@frappe.whitelist()
def get_service_item_and_practitioner_charge(doc):
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ service_item = None
+ practitioner_charge = None
+ department = doc.medical_department if doc.doctype == 'Patient Encounter' else doc.department
+
is_inpatient = doc.inpatient_record
- if is_inpatient:
- service_item = get_practitioner_service_item(doc.practitioner, 'inpatient_visit_charge_item')
+
+ if doc.get('appointment_type'):
+ service_item, practitioner_charge = get_appointment_type_service_item(doc.appointment_type, department, is_inpatient)
+
+ if not service_item and not practitioner_charge:
+ service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient)
if not service_item:
- service_item = get_healthcare_service_item('inpatient_visit_charge_item')
- else:
- service_item = get_practitioner_service_item(doc.practitioner, 'op_consulting_charge_item')
- if not service_item:
- service_item = get_healthcare_service_item('op_consulting_charge_item')
+ service_item = get_healthcare_service_item(is_inpatient)
+
if not service_item:
throw_config_service_item(is_inpatient)
- practitioner_charge = get_practitioner_charge(doc.practitioner, is_inpatient)
if not practitioner_charge:
throw_config_practitioner_charge(is_inpatient, doc.practitioner)
+ return {'service_item': service_item, 'practitioner_charge': practitioner_charge}
+
+
+def get_appointment_type_service_item(appointment_type, department, is_inpatient):
+ from erpnext.healthcare.doctype.appointment_type.appointment_type import get_service_item_based_on_department
+
+ item_list = get_service_item_based_on_department(appointment_type, department)
+ service_item = None
+ practitioner_charge = None
+
+ if item_list:
+ if is_inpatient:
+ service_item = item_list.get('inpatient_visit_charge_item')
+ practitioner_charge = item_list.get('inpatient_visit_charge')
+ else:
+ service_item = item_list.get('op_consulting_charge_item')
+ practitioner_charge = item_list.get('op_consulting_charge')
+
return service_item, practitioner_charge
@@ -345,12 +377,27 @@
frappe.throw(msg, title=_('Missing Configuration'))
-def get_practitioner_service_item(practitioner, service_item_field):
- return frappe.db.get_value('Healthcare Practitioner', practitioner, service_item_field)
+def get_practitioner_service_item(practitioner, is_inpatient):
+ service_item = None
+ practitioner_charge = None
+
+ if is_inpatient:
+ service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['inpatient_visit_charge_item', 'inpatient_visit_charge'])
+ else:
+ service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['op_consulting_charge_item', 'op_consulting_charge'])
+
+ return service_item, practitioner_charge
-def get_healthcare_service_item(service_item_field):
- return frappe.db.get_single_value('Healthcare Settings', service_item_field)
+def get_healthcare_service_item(is_inpatient):
+ service_item = None
+
+ if is_inpatient:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'inpatient_visit_charge_item')
+ else:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'op_consulting_charge_item')
+
+ return service_item
def get_practitioner_charge(practitioner, is_inpatient):
@@ -381,7 +428,8 @@
invoiced = True
if item.reference_dt == 'Clinical Procedure':
- if get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
+ if service_item == item.item_code:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced)
else:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced)
@@ -403,7 +451,8 @@
def validate_invoiced_on_submit(item):
- if item.reference_dt == 'Clinical Procedure' and get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code:
+ if item.reference_dt == 'Clinical Procedure' and \
+ frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') == item.item_code:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced')
else:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced')
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 109d921..c2798a3 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -195,6 +195,10 @@
{"name": "call-disconnect", "src": "/assets/erpnext/sounds/call-disconnect.mp3", "volume": 0.2},
]
+has_upload_permission = {
+ "Employee": "erpnext.hr.doctype.employee.employee.has_upload_permission"
+}
+
has_website_permission = {
"Sales Order": "erpnext.controllers.website_list_for_contact.has_website_permission",
"Quotation": "erpnext.controllers.website_list_for_contact.has_website_permission",
@@ -272,9 +276,15 @@
'Address': {
'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category']
},
+ 'Supplier': {
+ 'validate': 'erpnext.regional.india.utils.validate_pan_for_india'
+ },
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
},
+ ('Sales Invoice', 'Purchase Invoice'): {
+ 'validate': ['erpnext.regional.india.utils.validate_document_name']
+ },
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
"after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
@@ -318,6 +328,7 @@
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
"erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
+ "erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
],
"daily": [
"erpnext.stock.reorder_item.reorder_item",
@@ -355,13 +366,13 @@
"erpnext.hr.utils.generate_leave_encashment",
"erpnext.hr.utils.allocate_earned_leaves",
"erpnext.hr.utils.grant_leaves_automatically",
- "erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall.create_process_loan_security_shortfall",
- "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
+ "erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
+ "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
"erpnext.crm.doctype.lead.lead.daily_open_lead"
],
"monthly_long": [
"erpnext.accounts.deferred_revenue.process_deferred_accounting",
- "erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
+ "erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_demand_loans"
]
}
@@ -390,6 +401,15 @@
communication_doctypes = ["Customer", "Supplier"]
+accounting_dimension_doctypes = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset",
+ "Expense Claim", "Expense Claim Detail", "Expense Taxes and Charges", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note",
+ "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account", "Material Request Item", "Delivery Note Item",
+ "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction", "Sales Taxes and Charges", "Purchase Taxes and Charges", "Shipping Rule",
+ "Landed Cost Item", "Asset Value Adjustment", "Loyalty Program", "Fee Schedule", "Fee Structure", "Stock Reconciliation",
+ "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool", "Opening Invoice Creation Tool Item", "Subscription",
+ "Subscription Plan"
+]
+
regional_overrides = {
'France': {
'erpnext.tests.test_regional.test_method': 'erpnext.regional.france.utils.test_method'
@@ -399,6 +419,7 @@
'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header': 'erpnext.regional.india.utils.get_itemised_tax_breakup_header',
'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data': 'erpnext.regional.india.utils.get_itemised_tax_breakup_data',
'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
+ 'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries',
diff --git a/erpnext/hr/doctype/attendance/attendance.py b/erpnext/hr/doctype/attendance/attendance.py
index 373b940..18a4fe5 100644
--- a/erpnext/hr/doctype/attendance/attendance.py
+++ b/erpnext/hr/doctype/attendance/attendance.py
@@ -131,6 +131,10 @@
data = json.loads(data)
data = frappe._dict(data)
company = frappe.get_value('Employee', data.employee, 'company')
+ if not data.unmarked_days:
+ frappe.throw(_("Please select a date."))
+ return
+
for date in data.unmarked_days:
doc_dict = {
'doctype': 'Attendance',
diff --git a/erpnext/hr/doctype/attendance/attendance_list.js b/erpnext/hr/doctype/attendance/attendance_list.js
index 6df3dbd..0c7eafe 100644
--- a/erpnext/hr/doctype/attendance/attendance_list.js
+++ b/erpnext/hr/doctype/attendance/attendance_list.js
@@ -12,7 +12,7 @@
onload: function(list_view) {
let me = this;
const months = moment.months()
- list_view.page.add_inner_button( __("Mark Attendance"), function(){
+ list_view.page.add_inner_button( __("Mark Attendance"), function() {
let dialog = new frappe.ui.Dialog({
title: __("Mark Attendance"),
fields: [
@@ -22,11 +22,12 @@
fieldtype: 'Link',
options: 'Employee',
reqd: 1,
- onchange: function(){
+ onchange: function() {
dialog.set_df_property("unmarked_days", "hidden", 1);
dialog.set_df_property("status", "hidden", 1);
dialog.set_df_property("month", "value", '');
dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
}
},
{
@@ -35,13 +36,18 @@
fieldname: "month",
options: months,
reqd: 1,
- onchange: function(){
+ onchange: function() {
if(dialog.fields_dict.employee.value && dialog.fields_dict.month.value) {
dialog.set_df_property("status", "hidden", 0);
dialog.set_df_property("unmarked_days", "options", []);
+ dialog.no_unmarked_days_left = false;
me.get_multi_select_options(dialog.fields_dict.employee.value, dialog.fields_dict.month.value).then(options =>{
- dialog.set_df_property("unmarked_days", "hidden", 0);
- dialog.set_df_property("unmarked_days", "options", options);
+ if (options.length > 0) {
+ dialog.set_df_property("unmarked_days", "hidden", 0);
+ dialog.set_df_property("unmarked_days", "options", options);
+ } else {
+ dialog.no_unmarked_days_left = true;
+ }
});
}
}
@@ -64,21 +70,25 @@
hidden: 1
},
],
- primary_action(data){
- frappe.confirm(__('Mark attendance as <b>' + data.status + '</b> for <b>' + data.month +'</b>' + ' on selected dates?'), () => {
- frappe.call({
- method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
- args: {
- data : data
- },
- callback: function(r) {
- if(r.message === 1) {
- frappe.show_alert({message:__("Attendance Marked"), indicator:'blue'});
- cur_dialog.hide();
+ primary_action(data) {
+ if (cur_dialog.no_unmarked_days_left) {
+ frappe.msgprint(__("Attendance for the month of {0} , has already been marked for the Employee {1}",[dialog.fields_dict.month.value, dialog.fields_dict.employee.value]));
+ } else {
+ frappe.confirm(__('Mark attendance as {0} for {1} on selected dates?', [data.status,data.month]), () => {
+ frappe.call({
+ method: "erpnext.hr.doctype.attendance.attendance.mark_bulk_attendance",
+ args: {
+ data: data
+ },
+ callback: function(r) {
+ if (r.message === 1) {
+ frappe.show_alert({message: __("Attendance Marked"), indicator: 'blue'});
+ cur_dialog.hide();
+ }
}
- }
+ });
});
- });
+ }
dialog.hide();
list_view.refresh();
},
diff --git a/erpnext/hr/doctype/attendance_request/test_attendance_request.py b/erpnext/hr/doctype/attendance_request/test_attendance_request.py
index 92b1eae..3c42bd9 100644
--- a/erpnext/hr/doctype/attendance_request/test_attendance_request.py
+++ b/erpnext/hr/doctype/attendance_request/test_attendance_request.py
@@ -8,6 +8,8 @@
from frappe.utils import nowdate
from datetime import date
+test_dependencies = ["Employee"]
+
class TestAttendanceRequest(unittest.TestCase):
def setUp(self):
for doctype in ["Attendance Request", "Attendance"]:
@@ -56,4 +58,4 @@
self.assertEqual(attendance.docstatus, 2)
def get_employee():
- return frappe.get_doc("Employee", "_T-Employee-00001")
\ No newline at end of file
+ return frappe.get_doc("Employee", "_T-Employee-00001")
diff --git a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py
index 1615ab3..74ce301 100644
--- a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py
+++ b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py
@@ -10,6 +10,8 @@
from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period
from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on
+test_dependencies = ["Employee"]
+
class TestCompensatoryLeaveRequest(unittest.TestCase):
def setUp(self):
frappe.db.sql(''' delete from `tabCompensatory Leave Request`''')
@@ -129,4 +131,4 @@
],
"holiday_list_name": "_Test Compensatory Leave"
})
- holiday_list.save()
\ No newline at end of file
+ holiday_list.save()
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index dc2aaa4..5123d6a 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2021-01-01 16:54:33.477439",
+ "modified": "2021-01-02 16:54:33.477439",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index d0e7d05..629bc57 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -8,7 +8,7 @@
from frappe.model.naming import set_name_by_naming_series
from frappe import throw, _, scrub
from frappe.permissions import add_user_permission, remove_user_permission, \
- set_user_permission_if_allowed, has_permission
+ set_user_permission_if_allowed, has_permission, get_doc_permissions
from frappe.model.document import Document
from erpnext.utilities.transaction_base import delete_events
from frappe.utils.nestedset import NestedSet
@@ -66,7 +66,7 @@
def validate_user_details(self):
data = frappe.db.get_value('User',
self.user_id, ['enabled', 'user_image'], as_dict=1)
- if data.get("user_image"):
+ if data.get("user_image") and self.image == '':
self.image = data.get("user_image")
self.validate_for_enabled_user_id(data.get("enabled", 0))
self.validate_duplicate_user_id()
@@ -501,3 +501,10 @@
'allow': 'Employee',
'for_value': employee_name
})
+
+def has_upload_permission(doc, ptype='read', user=None):
+ if not user:
+ user = frappe.session.user
+ if get_doc_permissions(doc, user=user, ptype=ptype).get(ptype):
+ return True
+ return doc.user_id == user
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index c0e614a..7d652a7 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -48,6 +48,7 @@
self.assertRaises(EmployeeLeftValidationError, employee1_doc.save)
def make_employee(user, company=None, **kwargs):
+ ""
if not frappe.db.get_value("User", user):
frappe.get_doc({
"doctype": "User",
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.json b/erpnext/hr/doctype/employee_advance/employee_advance.json
index cf6b540..04f98d1 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.json
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.json
@@ -181,7 +181,6 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -201,7 +200,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-25 12:01:55.980721",
+ "modified": "2021-03-31 14:42:47.321368",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee Advance",
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py
index bf893d5..e7bb6dc 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.py
@@ -211,6 +211,7 @@
self.total_claimed_amount += flt(d.amount)
self.total_sanctioned_amount += flt(d.sanctioned_amount)
+ @frappe.whitelist()
def calculate_taxes(self):
self.total_taxes_and_charges = 0
for tax in self.taxes:
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index f999635..09666c5 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -13,6 +13,7 @@
"stop_birthday_reminders",
"expense_approver_mandatory_in_expense_claim",
"leave_settings",
+ "send_leave_notification",
"leave_approval_notification_template",
"leave_status_notification_template",
"role_allowed_to_create_backdated_leave_application",
@@ -69,15 +70,19 @@
"label": "Leave Settings"
},
{
+ "depends_on": "eval: doc.send_leave_notification == 1",
"fieldname": "leave_approval_notification_template",
"fieldtype": "Link",
"label": "Leave Approval Notification Template",
+ "mandatory_depends_on": "eval: doc.send_leave_notification == 1",
"options": "Email Template"
},
{
+ "depends_on": "eval: doc.send_leave_notification == 1",
"fieldname": "leave_status_notification_template",
"fieldtype": "Link",
"label": "Leave Status Notification Template",
+ "mandatory_depends_on": "eval: doc.send_leave_notification == 1",
"options": "Email Template"
},
{
@@ -132,13 +137,19 @@
"fieldname": "automatically_allocate_leaves_based_on_leave_policy",
"fieldtype": "Check",
"label": "Automatically Allocate Leaves Based On Leave Policy"
+ },
+ {
+ "default": "1",
+ "fieldname": "send_leave_notification",
+ "fieldtype": "Check",
+ "label": "Send Leave Notification"
}
],
"icon": "fa fa-cog",
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2020-08-27 14:30:28.995324",
+ "modified": "2021-03-14 02:04:22.907159",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
@@ -155,5 +166,6 @@
}
],
"sort_field": "modified",
- "sort_order": "ASC"
-}
+ "sort_order": "ASC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/job_applicant/test_job_applicant.py b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
index 6d275c8..8728342 100644
--- a/erpnext/hr/doctype/job_applicant/test_job_applicant.py
+++ b/erpnext/hr/doctype/job_applicant/test_job_applicant.py
@@ -13,11 +13,21 @@
def create_job_applicant(**args):
args = frappe._dict(args)
- job_applicant = frappe.get_doc({
- "doctype": "Job Applicant",
+
+ filters = {
"applicant_name": args.applicant_name or "_Test Applicant",
"email_id": args.email_id or "test_applicant@example.com",
+ }
+
+ if frappe.db.exists("Job Applicant", filters):
+ return frappe.get_doc("Job Applicant", filters)
+
+ job_applicant = frappe.get_doc({
+ "doctype": "Job Applicant",
"status": args.status or "Open"
})
+
+ job_applicant.update(filters)
job_applicant.save()
- return job_applicant
\ No newline at end of file
+
+ return job_applicant
diff --git a/erpnext/hr/doctype/job_offer/test_job_offer.py b/erpnext/hr/doctype/job_offer/test_job_offer.py
index 8886596..690a692 100644
--- a/erpnext/hr/doctype/job_offer/test_job_offer.py
+++ b/erpnext/hr/doctype/job_offer/test_job_offer.py
@@ -13,14 +13,15 @@
class TestJobOffer(unittest.TestCase):
def test_job_offer_creation_against_vacancies(self):
- create_staffing_plan(staffing_details=[{
- "designation": "Designer",
+ frappe.db.set_value("HR Settings", None, "check_vacancies", 1)
+ job_applicant = create_job_applicant(email_id="test_job_offer@example.com")
+ job_offer = create_job_offer(job_applicant=job_applicant.name, designation="UX Designer")
+
+ create_staffing_plan(name='Test No Vacancies', staffing_details=[{
+ "designation": "UX Designer",
"vacancies": 0,
"estimated_cost_per_position": 5000
}])
- frappe.db.set_value("HR Settings", None, "check_vacancies", 1)
- job_applicant = create_job_applicant(email_id="test_job_offer@example.com")
- job_offer = create_job_offer(job_applicant=job_applicant.name, designation="Researcher")
self.assertRaises(frappe.ValidationError, job_offer.submit)
# test creation of job offer when vacancies are not present
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index 5e3822e..11302ca 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -18,7 +18,6 @@
class LeaveAllocation(Document):
def validate(self):
self.validate_period()
- self.validate_new_leaves_allocated_value()
self.validate_allocation_overlap()
self.validate_back_dated_allocation()
self.set_total_leaves_allocated()
@@ -72,11 +71,6 @@
if frappe.db.get_value("Leave Type", self.leave_type, "is_lwp"):
frappe.throw(_("Leave Type {0} cannot be allocated since it is leave without pay").format(self.leave_type))
- def validate_new_leaves_allocated_value(self):
- """validate that leave allocation is in multiples of 0.5"""
- if flt(self.new_leaves_allocated) % 0.5:
- frappe.throw(_("Leaves must be allocated in multiples of 0.5"), ValueMultiplierError)
-
def validate_allocation_overlap(self):
leave_allocation = frappe.db.sql("""
SELECT
@@ -105,6 +99,7 @@
.format(formatdate(future_allocation[0].from_date), future_allocation[0].name),
BackDatedAllocationError)
+ @frappe.whitelist()
def set_total_leaves_allocated(self):
self.unused_leaves = get_carry_forwarded_leaves(self.employee,
self.leave_type, self.from_date, self.carry_forward)
diff --git a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
index 26f077a..0b71036 100644
--- a/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/test_leave_allocation.py
@@ -6,6 +6,10 @@
from erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry import process_expired_allocation, expire_allocation
class TestLeaveAllocation(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ frappe.db.sql("delete from `tabLeave Period`")
+
def test_overlapping_allocation(self):
frappe.db.sql("delete from `tabLeave Allocation`")
@@ -177,4 +181,4 @@
})
return leave_allocation
-test_dependencies = ["Employee", "Leave Type"]
\ No newline at end of file
+test_dependencies = ["Employee", "Leave Type"]
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 132c3bd..350cead 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -40,7 +40,8 @@
def on_update(self):
if self.status == "Open" and self.docstatus < 1:
# notify leave approver about creation
- self.notify_leave_approver()
+ if frappe.db.get_single_value("HR Settings", "send_leave_notification"):
+ self.notify_leave_approver()
def on_submit(self):
if self.status == "Open":
@@ -50,7 +51,8 @@
self.update_attendance()
# notify leave applier about approval
- self.notify_employee()
+ if frappe.db.get_single_value("HR Settings", "send_leave_notification"):
+ self.notify_employee()
self.create_leave_ledger_entry()
self.reload()
@@ -60,7 +62,8 @@
def on_cancel(self):
self.create_leave_ledger_entry(submit=False)
# notify leave applier about cancellation
- self.notify_employee()
+ if frappe.db.get_single_value("HR Settings", "send_leave_notification"):
+ self.notify_employee()
self.cancel_attendance()
def validate_applicable_after(self):
diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
index 6324b04..9f667a6 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
+++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
@@ -4,11 +4,11 @@
<thead>
<tr>
<th style="width: 16%">{{ __("Leave Type") }}</th>
- <th style="width: 16%" class="text-right">{{ __("Total Allocated Leaves") }}</th>
- <th style="width: 16%" class="text-right">{{ __("Expired Leaves") }}</th>
- <th style="width: 16%" class="text-right">{{ __("Used Leaves") }}</th>
- <th style="width: 16%" class="text-right">{{ __("Pending Leaves") }}</th>
- <th style="width: 16%" class="text-right">{{ __("Available Leaves") }}</th>
+ <th style="width: 16%" class="text-right">{{ __("Total Allocated Leave") }}</th>
+ <th style="width: 16%" class="text-right">{{ __("Expired Leave") }}</th>
+ <th style="width: 16%" class="text-right">{{ __("Used Leave") }}</th>
+ <th style="width: 16%" class="text-right">{{ __("Pending Leave") }}</th>
+ <th style="width: 16%" class="text-right">{{ __("Available Leave") }}</th>
</tr>
</thead>
<tbody>
@@ -25,5 +25,5 @@
</tbody>
</table>
{% else %}
-<p style="margin-top: 30px;"> No Leaves have been allocated. </p>
-{% endif %}
\ No newline at end of file
+<p style="margin-top: 30px;"> No Leave has been allocated. </p>
+{% endif %}
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index 53b7a39..48bfa0c 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -12,7 +12,7 @@
from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
-test_dependencies = ["Leave Allocation", "Leave Block List"]
+test_dependencies = ["Leave Allocation", "Leave Block List", "Employee"]
_test_records = [
{
@@ -56,6 +56,7 @@
@classmethod
def setUpClass(cls):
set_leave_approver()
+ frappe.db.sql("delete from tabAttendance where employee='_T-Employee-00001'")
def tearDown(self):
frappe.set_user("Administrator")
@@ -230,8 +231,9 @@
def test_optional_leave(self):
leave_period = get_leave_period()
today = nowdate()
- from datetime import date
holiday_list = 'Test Holiday List for Optional Holiday'
+ optional_leave_date = add_days(today, 7)
+
if not frappe.db.exists('Holiday List', holiday_list):
frappe.get_doc(dict(
doctype = 'Holiday List',
@@ -239,7 +241,7 @@
from_date = add_months(today, -6),
to_date = add_months(today, 6),
holidays = [
- dict(holiday_date = today, description = 'Test')
+ dict(holiday_date = optional_leave_date, description = 'Test')
]
)).insert()
employee = get_employee()
@@ -255,7 +257,7 @@
allocate_leaves(employee, leave_period, leave_type, 10)
- date = add_days(today, - 1)
+ date = add_days(today, 6)
leave_application = frappe.get_doc(dict(
doctype = 'Leave Application',
@@ -270,14 +272,14 @@
# can only apply on optional holidays
self.assertRaises(NotAnOptionalHoliday, leave_application.insert)
- leave_application.from_date = today
- leave_application.to_date = today
+ leave_application.from_date = optional_leave_date
+ leave_application.to_date = optional_leave_date
leave_application.status = "Approved"
leave_application.insert()
leave_application.submit()
# check leave balance is reduced
- self.assertEqual(get_leave_balance_on(employee.name, leave_type, today), 9)
+ self.assertEqual(get_leave_balance_on(employee.name, leave_type, optional_leave_date), 9)
def test_leaves_allowed(self):
employee = get_employee()
@@ -341,7 +343,7 @@
to_date = add_days(date, 4),
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
self.assertRaises(frappe.ValidationError, leave_application.insert)
@@ -363,7 +365,7 @@
to_date = add_days(date, 4),
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
self.assertTrue(leave_application.insert())
@@ -393,7 +395,7 @@
to_date = add_days(date, 4),
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
self.assertRaises(frappe.ValidationError, leave_application.insert)
@@ -508,7 +510,7 @@
description = "_Test Reason",
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
leave_application.submit()
leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=dict(transaction_name=leave_application.name))
@@ -540,7 +542,7 @@
description = "_Test Reason",
company = "_Test Company",
docstatus = 1,
- status = "Approved"
+ status = "Approved"
))
leave_application.submit()
@@ -639,4 +641,4 @@
"docstatus": 1
}).insert()
- allocate_leave.submit()
\ No newline at end of file
+ allocate_leave.submit()
diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.json b/erpnext/hr/doctype/leave_encashment/leave_encashment.json
index 83eeae3..dcb5874 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.json
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.json
@@ -130,7 +130,6 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -155,7 +154,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-25 11:56:06.777241",
+ "modified": "2021-03-31 14:45:27.948207",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Encashment",
diff --git a/erpnext/hr/doctype/leave_encashment/leave_encashment.py b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
index 4c1a465..e041b7f 100644
--- a/erpnext/hr/doctype/leave_encashment/leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/leave_encashment.py
@@ -63,6 +63,7 @@
frappe.db.get_value('Leave Allocation', self.leave_allocation, 'total_leaves_encashed') - self.encashable_days)
self.create_leave_ledger_entry(submit=False)
+ @frappe.whitelist()
def get_leave_details_for_encashment(self):
salary_structure = get_assigned_salary_structure(self.employee, self.encashment_date or getdate(nowdate()))
if not salary_structure:
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
index 63559c4..66dced4 100644
--- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.py
@@ -52,7 +52,9 @@
ledger.update(args)
if submit:
- frappe.get_doc(ledger).submit()
+ doc = frappe.get_doc(ledger)
+ doc.flags.ignore_permissions = 1
+ doc.submit()
else:
delete_ledger_entry(ledger)
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
index a0327bd..3373350 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
@@ -106,12 +106,14 @@
"fieldname": "leaves_allocated",
"fieldtype": "Check",
"hidden": 1,
- "label": "Leaves Allocated"
+ "label": "Leaves Allocated",
+ "no_copy": 1,
+ "print_hide": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-12-31 16:43:30.695206",
+ "modified": "2021-03-01 17:54:01.014509",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
index a5068bc..462b81d 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -6,7 +6,7 @@
import frappe
from frappe.model.document import Document
from frappe import _, bold
-from frappe.utils import getdate, date_diff, comma_and, formatdate
+from frappe.utils import getdate, date_diff, comma_and, formatdate, get_datetime, flt
from math import ceil
import json
from six import string_types
@@ -36,6 +36,7 @@
frappe.throw(_("Leave Policy: {0} already assigned for Employee {1} for period {2} to {3}")
.format(bold(self.leave_policy), bold(self.employee), bold(formatdate(self.effective_from)), bold(formatdate(self.effective_to))))
+ @frappe.whitelist()
def grant_leave_alloc_for_employee(self):
if self.leaves_allocated:
frappe.throw(_("Leave already have been assigned for this Leave Policy Assignment"))
@@ -84,17 +85,52 @@
return allocation.name, new_leaves_allocated
def get_new_leaves(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
+ from frappe.model.meta import get_field_precision
+ precision = get_field_precision(frappe.get_meta("Leave Allocation").get_field("new_leaves_allocated"))
+
+ # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
+ if leave_type_details.get(leave_type).is_compensatory == 1:
+ new_leaves_allocated = 0
+
+ elif leave_type_details.get(leave_type).is_earned_leave == 1:
+ if self.assignment_based_on == "Leave Period":
+ new_leaves_allocated = self.get_leaves_for_passed_months(leave_type, new_leaves_allocated, leave_type_details, date_of_joining)
+ else:
+ new_leaves_allocated = 0
# Calculate leaves at pro-rata basis for employees joining after the beginning of the given leave period
- if getdate(date_of_joining) > getdate(self.effective_from):
+ elif getdate(date_of_joining) > getdate(self.effective_from):
remaining_period = ((date_diff(self.effective_to, date_of_joining) + 1) / (date_diff(self.effective_to, self.effective_from) + 1))
new_leaves_allocated = ceil(new_leaves_allocated * remaining_period)
- # Earned Leaves and Compensatory Leaves are allocated by scheduler, initially allocate 0
- if leave_type_details.get(leave_type).is_earned_leave == 1 or leave_type_details.get(leave_type).is_compensatory == 1:
- new_leaves_allocated = 0
+ return flt(new_leaves_allocated, precision)
+
+ def get_leaves_for_passed_months(self, leave_type, new_leaves_allocated, leave_type_details, date_of_joining):
+ from erpnext.hr.utils import get_monthly_earned_leave
+
+ current_month = get_datetime().month
+ current_year = get_datetime().year
+
+ from_date = frappe.db.get_value("Leave Period", self.leave_period, "from_date")
+ if getdate(date_of_joining) > getdate(from_date):
+ from_date = date_of_joining
+
+ from_date_month = get_datetime(from_date).month
+ from_date_year = get_datetime(from_date).year
+
+ months_passed = 0
+ if current_year == from_date_year and current_month > from_date_month:
+ months_passed = current_month - from_date_month
+ elif current_year > from_date_year:
+ months_passed = (12 - from_date_month) + current_month
+
+ if months_passed > 0:
+ monthly_earned_leave = get_monthly_earned_leave(new_leaves_allocated,
+ leave_type_details.get(leave_type).earned_leave_frequency, leave_type_details.get(leave_type).rounding)
+ new_leaves_allocated = monthly_earned_leave * months_passed
return new_leaves_allocated
+
@frappe.whitelist()
def grant_leave_for_multiple_employees(leave_policy_assignments):
leave_policy_assignments = json.loads(leave_policy_assignments)
@@ -156,7 +192,8 @@
def get_leave_type_details():
leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type",
- fields=["name", "is_lwp", "is_earned_leave", "is_compensatory", "is_carry_forward", "expire_carry_forwarded_leaves_after_days"])
+ fields=["name", "is_lwp", "is_earned_leave", "is_compensatory",
+ "is_carry_forward", "expire_carry_forwarded_leaves_after_days", "earned_leave_frequency", "rounding"])
for d in leave_types:
leave_type_details.setdefault(d.name, d)
return leave_type_details
diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
index c7bc6fb..838e794 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
@@ -9,6 +9,8 @@
from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import create_assignment_for_multiple_employees
from erpnext.hr.doctype.leave_policy.test_leave_policy import create_leave_policy
+test_dependencies = ["Employee"]
+
class TestLeavePolicyAssignment(unittest.TestCase):
def setUp(self):
diff --git a/erpnext/hr/doctype/leave_type/leave_type.json b/erpnext/hr/doctype/leave_type/leave_type.json
index a209291..fc577ef 100644
--- a/erpnext/hr/doctype/leave_type/leave_type.json
+++ b/erpnext/hr/doctype/leave_type/leave_type.json
@@ -172,7 +172,7 @@
"fieldname": "rounding",
"fieldtype": "Select",
"label": "Rounding",
- "options": "0.5\n1.0"
+ "options": "\n0.25\n0.5\n1.0"
},
{
"depends_on": "is_carry_forward",
@@ -197,6 +197,7 @@
"label": "Based On Date Of Joining"
},
{
+ "default": "0",
"depends_on": "eval:doc.is_lwp == 0",
"fieldname": "is_ppl",
"fieldtype": "Check",
@@ -213,7 +214,7 @@
"icon": "fa fa-flag",
"idx": 1,
"links": [],
- "modified": "2020-10-15 15:49:47.555105",
+ "modified": "2021-03-02 11:22:33.776320",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Type",
diff --git a/erpnext/hr/doctype/shift_request/test_shift_request.py b/erpnext/hr/doctype/shift_request/test_shift_request.py
index 3dcfcbf..230bb2b 100644
--- a/erpnext/hr/doctype/shift_request/test_shift_request.py
+++ b/erpnext/hr/doctype/shift_request/test_shift_request.py
@@ -7,6 +7,8 @@
import unittest
from frappe.utils import nowdate, add_days
+test_dependencies = ["Shift Type"]
+
class TestShiftRequest(unittest.TestCase):
def setUp(self):
for doctype in ["Shift Request", "Shift Assignment"]:
@@ -46,4 +48,4 @@
department_doc = frappe.get_doc("Department", department)
department_doc.append('shift_request_approver',{'approver': "test1@example.com"})
department_doc.save()
- department_doc.reload()
\ No newline at end of file
+ department_doc.reload()
diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py
index 054e7e3..d5fdda8 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.py
+++ b/erpnext/hr/doctype/shift_type/shift_type.py
@@ -15,6 +15,7 @@
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
class ShiftType(Document):
+ @frappe.whitelist()
def process_auto_attendance(self):
if not cint(self.enable_auto_attendance) or not self.process_attendance_after or not self.last_sync_of_checkin:
return
diff --git a/erpnext/hr/doctype/shift_type/test_records.json b/erpnext/hr/doctype/shift_type/test_records.json
new file mode 100644
index 0000000..9040b91
--- /dev/null
+++ b/erpnext/hr/doctype/shift_type/test_records.json
@@ -0,0 +1,8 @@
+[
+ {
+ "doctype": "Shift Type",
+ "name": "Day Shift",
+ "start_time": "9:00:00",
+ "end_time": "18:00:00"
+ }
+]
diff --git a/erpnext/hr/doctype/shift_type/test_shift_type.py b/erpnext/hr/doctype/shift_type/test_shift_type.py
index 535072a..bc4f0ea 100644
--- a/erpnext/hr/doctype/shift_type/test_shift_type.py
+++ b/erpnext/hr/doctype/shift_type/test_shift_type.py
@@ -7,14 +7,4 @@
import unittest
class TestShiftType(unittest.TestCase):
- def test_make_shift_type(self):
- if frappe.db.exists("Shift Type", "Day Shift"):
- return
- shift_type = frappe.get_doc({
- "doctype": "Shift Type",
- "name": "Day Shift",
- "start_time": "9:00:00",
- "end_time": "18:00:00"
- })
- shift_type.insert()
-
\ No newline at end of file
+ pass
diff --git a/erpnext/hr/doctype/skill/skill.json b/erpnext/hr/doctype/skill/skill.json
index 5182973..4c8a8c9 100644
--- a/erpnext/hr/doctype/skill/skill.json
+++ b/erpnext/hr/doctype/skill/skill.json
@@ -3,7 +3,7 @@
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
- "allow_rename": 0,
+ "allow_rename": 1,
"autoname": "field:skill_name",
"beta": 0,
"creation": "2019-04-16 09:54:39.486915",
@@ -16,7 +16,7 @@
"fields": [
{
"allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
+ "allow_in_quick_entry": 1,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
@@ -46,6 +46,12 @@
"set_only_once": 0,
"translatable": 0,
"unique": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description"
}
],
"has_web_view": 0,
@@ -56,7 +62,7 @@
"issingle": 0,
"istable": 0,
"max_attachments": 0,
- "modified": "2019-04-16 09:55:00.536328",
+ "modified": "2021-02-26 10:55:00.536328",
"modified_by": "Administrator",
"module": "HR",
"name": "Skill",
@@ -110,4 +116,4 @@
"track_changes": 1,
"track_seen": 0,
"track_views": 0
-}
\ No newline at end of file
+}
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
index 5b84d00..533149a 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
@@ -39,6 +39,7 @@
detail.current_count = designation_counts['employee_count']
detail.current_openings = designation_counts['job_openings']
+ detail.total_estimated_cost = 0
if detail.number_of_positions > 0:
if detail.vacancies > 0 and detail.estimated_cost_per_position:
detail.total_estimated_cost = cint(detail.vacancies) * flt(detail.estimated_cost_per_position)
diff --git a/erpnext/hr/page/team_updates/team_updates.js b/erpnext/hr/page/team_updates/team_updates.js
index 13d0074..3583297 100644
--- a/erpnext/hr/page/team_updates/team_updates.js
+++ b/erpnext/hr/page/team_updates/team_updates.js
@@ -36,7 +36,7 @@
start: me.start
},
callback: function(r) {
- if(r.message) {
+ if (r.message && r.message.length > 0) {
r.message.forEach(function(d) {
me.add_row(d);
});
@@ -75,6 +75,6 @@
}
me.last_feed_date = date;
- $(frappe.render_template('team_update_row', data)).appendTo(me.body)
+ $(frappe.render_template('team_update_row', data)).appendTo(me.body);
}
-}
\ No newline at end of file
+}
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 1b92358..06f9160 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -40,17 +40,17 @@
'fieldname': 'opening_balance',
'width': 130,
}, {
- 'label': _('Leaves Allocated'),
+ 'label': _('Leave Allocated'),
'fieldtype': 'float',
'fieldname': 'leaves_allocated',
'width': 130,
}, {
- 'label': _('Leaves Taken'),
+ 'label': _('Leave Taken'),
'fieldtype': 'float',
'fieldname': 'leaves_taken',
'width': 130,
}, {
- 'label': _('Leaves Expired'),
+ 'label': _('Leave Expired'),
'fieldtype': 'float',
'fieldname': 'leaves_expired',
'width': 130,
diff --git a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py
index e961114..f5fece8 100644
--- a/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py
+++ b/erpnext/hr/report/recruitment_analytics/recruitment_analytics.py
@@ -31,7 +31,7 @@
"fieldtype": "Link",
"fieldname": "job_opening",
"options": "Job Opening",
- "width": 100
+ "width": 105
},
{
"label": _("Job Applicant"),
@@ -44,13 +44,13 @@
"label": _("Applicant name"),
"fieldtype": "data",
"fieldname": "applicant_name",
- "width": 120
+ "width": 130
},
{
"label": _("Application Status"),
"fieldtype": "Data",
"fieldname": "application_status",
- "width": 100
+ "width": 150
},
{
"label": _("Job Offer"),
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index e2aa7a4..0c4c1ca 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -1,16 +1,19 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-import frappe, erpnext
-from frappe import _
-from frappe.utils import formatdate, format_datetime, getdate, get_datetime, nowdate, flt, cstr, add_days, today
-from frappe.model.document import Document
-from frappe.desk.form import assign_to
+import erpnext
+import frappe
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
+from frappe import _
+from frappe.desk.form import assign_to
+from frappe.model.document import Document
+from frappe.utils import (add_days, cstr, flt, format_datetime, formatdate,
+ get_datetime, getdate, nowdate, today, unique)
+
class DuplicateDeclarationError(frappe.ValidationError): pass
+
class EmployeeBoardingController(Document):
'''
Create the project and the task for the boarding process
@@ -48,27 +51,38 @@
continue
task = frappe.get_doc({
- "doctype": "Task",
- "project": self.project,
- "subject": activity.activity_name + " : " + self.employee_name,
- "description": activity.description,
- "department": self.department,
- "company": self.company,
- "task_weight": activity.task_weight
- }).insert(ignore_permissions=True)
+ "doctype": "Task",
+ "project": self.project,
+ "subject": activity.activity_name + " : " + self.employee_name,
+ "description": activity.description,
+ "department": self.department,
+ "company": self.company,
+ "task_weight": activity.task_weight
+ }).insert(ignore_permissions=True)
activity.db_set("task", task.name)
+
users = [activity.user] if activity.user else []
if activity.role:
- user_list = frappe.db.sql_list('''select distinct(parent) from `tabHas Role`
- where parenttype='User' and role=%s''', activity.role)
- users = users + user_list
+ user_list = frappe.db.sql_list('''
+ SELECT
+ DISTINCT(has_role.parent)
+ FROM
+ `tabHas Role` has_role
+ LEFT JOIN `tabUser` user
+ ON has_role.parent = user.name
+ WHERE
+ has_role.parenttype = 'User'
+ AND user.enabled = 1
+ AND has_role.role = %s
+ ''', activity.role)
+ users = unique(users + user_list)
if "Administrator" in users:
users.remove("Administrator")
# assign the task the users
if users:
- self.assign_task_to_users(task, set(users))
+ self.assign_task_to_users(task, users)
def assign_task_to_users(self, task, users):
for user in users:
@@ -316,13 +330,7 @@
update_previous_leave_allocation(allocation, annual_allocation, e_leave_type)
def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type):
- divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
- if annual_allocation:
- earned_leaves = flt(annual_allocation) / divide_by_frequency[e_leave_type.earned_leave_frequency]
- if e_leave_type.rounding == "0.5":
- earned_leaves = round(earned_leaves * 2) / 2
- else:
- earned_leaves = round(earned_leaves)
+ earned_leaves = get_monthly_earned_leave(annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding)
allocation = frappe.get_doc('Leave Allocation', allocation.name)
new_allocation = flt(allocation.total_leaves_allocated) + flt(earned_leaves)
@@ -335,6 +343,21 @@
today_date = today()
create_additional_leave_ledger_entry(allocation, earned_leaves, today_date)
+def get_monthly_earned_leave(annual_leaves, frequency, rounding):
+ earned_leaves = 0.0
+ divide_by_frequency = {"Yearly": 1, "Half-Yearly": 6, "Quarterly": 4, "Monthly": 12}
+ if annual_leaves:
+ earned_leaves = flt(annual_leaves) / divide_by_frequency[frequency]
+ if rounding:
+ if rounding == "0.25":
+ earned_leaves = round(earned_leaves * 4) / 4
+ elif rounding == "0.5":
+ earned_leaves = round(earned_leaves * 2) / 2
+ else:
+ earned_leaves = round(earned_leaves)
+
+ return earned_leaves
+
def get_leave_allocations(date, leave_type):
return frappe.db.sql("""select name, employee, from_date, to_date, leave_policy_assignment, leave_policy
diff --git a/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json b/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json
new file mode 100644
index 0000000..b8abf21
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json
@@ -0,0 +1,29 @@
+{
+ "based_on": "disbursement_date",
+ "chart_name": "Loan Disbursements",
+ "chart_type": "Sum",
+ "creation": "2021-02-06 18:40:36.148470",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Loan Disbursement",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Disbursement\",\"docstatus\",\"=\",\"1\",false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "modified": "2021-02-06 18:40:49.308663",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Disbursements",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "",
+ "time_interval": "Daily",
+ "timeseries": 1,
+ "timespan": "Last Month",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "disbursed_amount",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json
new file mode 100644
index 0000000..aa0f78a
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json
@@ -0,0 +1,31 @@
+{
+ "based_on": "posting_date",
+ "chart_name": "Loan Interest Accrual",
+ "chart_type": "Sum",
+ "color": "#39E4A5",
+ "creation": "2021-02-18 20:07:04.843876",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Loan Interest Accrual",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Interest Accrual\",\"docstatus\",\"=\",\"1\",false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2021-02-21 21:01:26.022634",
+ "modified": "2021-02-21 21:01:44.930712",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Interest Accrual",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "",
+ "time_interval": "Monthly",
+ "timeseries": 1,
+ "timespan": "Last Year",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "interest_amount",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json
new file mode 100644
index 0000000..35bd43b
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json
@@ -0,0 +1,31 @@
+{
+ "based_on": "creation",
+ "chart_name": "New Loans",
+ "chart_type": "Count",
+ "color": "#449CF0",
+ "creation": "2021-02-06 16:59:27.509170",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2021-02-21 20:55:33.515025",
+ "modified": "2021-02-21 21:00:33.900821",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "New Loans",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "",
+ "time_interval": "Daily",
+ "timeseries": 1,
+ "timespan": "Last Month",
+ "type": "Bar",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json b/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
new file mode 100644
index 0000000..76c27b0
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
@@ -0,0 +1,31 @@
+{
+ "based_on": "",
+ "chart_name": "Top 10 Pledged Loan Securities",
+ "chart_type": "Custom",
+ "color": "#EC864B",
+ "creation": "2021-02-06 22:02:46.284479",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2021-02-21 21:00:57.043034",
+ "modified": "2021-02-21 21:01:10.048623",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Top 10 Pledged Loan Securities",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "Top 10 Pledged Loan Securities",
+ "time_interval": "Yearly",
+ "timeseries": 0,
+ "timespan": "Last Year",
+ "type": "Bar",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/loan_management/dashboard_chart_source/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/loan_management/dashboard_chart_source/__init__.py
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py
diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js
new file mode 100644
index 0000000..cf75cc8
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js
@@ -0,0 +1,14 @@
+frappe.provide('frappe.dashboards.chart_sources');
+
+frappe.dashboards.chart_sources["Top 10 Pledged Loan Securities"] = {
+ method: "erpnext.loan_management.dashboard_chart_source.top_10_pledged_loan_securities.top_10_pledged_loan_securities.get_data",
+ filters: [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company")
+ }
+ ]
+};
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
new file mode 100644
index 0000000..42c9b1c
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
@@ -0,0 +1,13 @@
+{
+ "creation": "2021-02-06 22:01:01.332628",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart Source",
+ "idx": 0,
+ "modified": "2021-02-06 22:01:01.332628",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Top 10 Pledged Loan Securities",
+ "owner": "Administrator",
+ "source_name": "Top 10 Pledged Loan Securities ",
+ "timeseries": 0
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py
new file mode 100644
index 0000000..6bb0440
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py
@@ -0,0 +1,76 @@
+# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.utils.dashboard import cache_source
+from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \
+ import get_loan_security_details
+from six import iteritems
+
+@frappe.whitelist()
+@cache_source
+def get_data(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)
+ else:
+ chart = frappe._dict(frappe.parse_json(chart))
+
+ filters = {}
+ current_pledges = {}
+
+ if filters:
+ filters = frappe.parse_json(filters)[0]
+
+ conditions = ""
+ labels = []
+ values = []
+
+ if filters.get('company'):
+ conditions = "AND company = %(company)s"
+
+ loan_security_details = get_loan_security_details()
+
+ unpledges = frappe._dict(frappe.db.sql("""
+ SELECT u.loan_security, sum(u.qty) as qty
+ FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
+ WHERE u.parent = up.name
+ AND up.status = 'Approved'
+ {conditions}
+ GROUP BY u.loan_security
+ """.format(conditions=conditions), filters, as_list=1))
+
+ pledges = frappe._dict(frappe.db.sql("""
+ SELECT p.loan_security, sum(p.qty) as qty
+ FROM `tabLoan Security Pledge` lp, `tabPledge`p
+ WHERE p.parent = lp.name
+ AND lp.status = 'Pledged'
+ {conditions}
+ GROUP BY p.loan_security
+ """.format(conditions=conditions), filters, as_list=1))
+
+ for security, qty in iteritems(pledges):
+ current_pledges.setdefault(security, qty)
+ current_pledges[security] -= unpledges.get(security, 0.0)
+
+ sorted_pledges = dict(sorted(current_pledges.items(), key=lambda item: item[1], reverse=True))
+
+ count = 0
+ for security, qty in iteritems(sorted_pledges):
+ values.append(qty * loan_security_details.get(security, {}).get('latest_price', 0))
+ labels.append(security)
+ count +=1
+
+ ## Just need top 10 securities
+ if count == 10:
+ break
+
+ return {
+ 'labels': labels,
+ 'datasets': [{
+ 'name': 'Top 10 Securities',
+ 'chartType': 'bar',
+ 'values': values
+ }]
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index e607d4f..83a813f 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -201,7 +201,9 @@
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
# checking greater than 0 as there may be some minor precision error
- if pending_amount < write_off_limit:
+ if not pending_amount:
+ frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
+ elif pending_amount < write_off_limit:
# Auto create loan write off and update status as loan closure requested
write_off = make_loan_write_off(loan)
write_off.submit()
@@ -348,3 +350,13 @@
if employee_currency != company_currency:
frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
.format(applicant, employee_currency))
+
+@frappe.whitelist()
+def get_shortfall_applicants():
+ loans = frappe.get_all('Loan Security Shortfall', {'status': 'Pending'}, pluck='loan')
+ applicants = set(frappe.get_all('Loan', {'name': ('in', loans)}, pluck='name'))
+
+ return {
+ "value": len(applicants),
+ "fieldtype": "Int"
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index f3c9db6..4b9a894 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -275,6 +275,11 @@
frappe.db.sql(""" UPDATE `tabLoan Security Price` SET loan_security_price = 250
where loan_security='Test Security 2'""")
+ create_process_loan_security_shortfall()
+ loan_security_shortfall = frappe.get_doc("Loan Security Shortfall", {"loan": loan.name})
+ self.assertEquals(loan_security_shortfall.status, "Completed")
+ self.assertEquals(loan_security_shortfall.shortfall_amount, 0)
+
def test_loan_security_unpledge(self):
pledge = [{
"loan_security": "Test Security 1",
@@ -547,7 +552,7 @@
# 30 days - grace period
penalty_days = 30 - 4
- penalty_applicable_amount = flt(amounts['interest_amount']/2, 2)
+ penalty_applicable_amount = flt(amounts['interest_amount']/2)
penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2)
process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30')
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
index e59db4c..9c0147e 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -197,7 +197,7 @@
security.qty = cint(security.amount/security.loan_security_price)
security.amount = security.qty * security.loan_security_price
- security.post_haircut_amount = security.amount - (security.amount * security.haircut/100)
+ security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut/100))
maximum_loan_amount += security.post_haircut_amount
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index 7d7992d..7978350 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -246,7 +246,5 @@
if not posting_date:
posting_date = getdate()
- precision = cint(frappe.db.get_default("currency_precision")) or 2
-
- return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), precision)
+ return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100))
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index ac30c91..bac06c4 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -81,8 +81,8 @@
last_accrual_date = get_last_accrual_date(self.against_loan)
# get posting date upto which interest has to be accrued
- per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
- self.rate_of_interest, self.posting_date), 2)
+ per_day_interest = get_per_day_interest(self.pending_principal_amount,
+ self.rate_of_interest, self.posting_date)
no_of_days = flt(flt(self.total_interest_paid - self.interest_payable,
precision)/per_day_interest, 0) - 1
@@ -105,8 +105,6 @@
})
def update_paid_amount(self):
- precision = cint(frappe.db.get_default("currency_precision")) or 2
-
loan = frappe.get_doc("Loan", self.against_loan)
for payment in self.repayment_details:
@@ -114,7 +112,7 @@
SET paid_principal_amount = `paid_principal_amount` + %s,
paid_interest_amount = `paid_interest_amount` + %s
WHERE name = %s""",
- (flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual))
+ (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual))
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
WHERE name = %s """, (loan.total_amount_paid + self.amount_paid,
@@ -148,8 +146,6 @@
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
def allocate_amounts(self, repayment_details):
- precision = cint(frappe.db.get_default("currency_precision")) or 2
-
self.set('repayment_details', [])
self.principal_amount_paid = 0
total_interest_paid = 0
@@ -185,21 +181,18 @@
# no of days for which to accrue interest
# Interest can only be accrued for an entire day and not partial
if interest_paid > repayment_details['unaccrued_interest']:
- per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
- self.rate_of_interest, self.posting_date), precision)
interest_paid -= repayment_details['unaccrued_interest']
total_interest_paid += repayment_details['unaccrued_interest']
else:
# get no of days for which interest can be paid
- per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
- self.rate_of_interest, self.posting_date), precision)
+ per_day_interest = get_per_day_interest(self.pending_principal_amount,
+ self.rate_of_interest, self.posting_date)
no_of_days = cint(interest_paid/per_day_interest)
total_interest_paid += no_of_days * per_day_interest
interest_paid -= no_of_days * per_day_interest
self.total_interest_paid = total_interest_paid
-
if interest_paid:
self.principal_amount_paid += interest_paid
@@ -369,7 +362,7 @@
if pending_days > 0:
principal_amount = flt(pending_principal_amount, precision)
per_day_interest = get_per_day_interest(principal_amount, loan_type_details.rate_of_interest, posting_date)
- unaccrued_interest += (pending_days * flt(per_day_interest, precision))
+ unaccrued_interest += (pending_days * per_day_interest)
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json
index 102bc0d..99b5c72 100644
--- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "LM-LSS-.#####",
"creation": "2019-09-06 11:33:34.709540",
"doctype": "DocType",
@@ -14,6 +15,7 @@
"shortfall_amount",
"column_break_8",
"security_value",
+ "shortfall_percentage",
"section_break_8",
"process_loan_security_shortfall"
],
@@ -85,10 +87,18 @@
{
"fieldname": "column_break_8",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "shortfall_percentage",
+ "fieldtype": "Percent",
+ "label": "Shortfall Percentage",
+ "read_only": 1
}
],
"in_create": 1,
- "modified": "2019-10-24 06:24:26.128997",
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-04-01 08:13:43.263772",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Shortfall",
diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
index 6469806..6539436 100644
--- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
@@ -22,7 +22,9 @@
if security_value >= loan_security_shortfall.shortfall_amount:
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name, {
"status": "Completed",
- "shortfall_amount": loan_security_shortfall.shortfall_amount})
+ "shortfall_amount": loan_security_shortfall.shortfall_amount,
+ "shortfall_percentage": 0
+ })
else:
frappe.db.set_value("Loan Security Shortfall", loan_security_shortfall.name,
"shortfall_amount", loan_security_shortfall.shortfall_amount - security_value)
@@ -55,6 +57,9 @@
'total_interest_payable', 'disbursed_amount', 'status'],
filters={'status': ('in',['Disbursed','Partially Disbursed']), 'is_secured_loan': 1})
+ loan_shortfall_map = frappe._dict(frappe.get_all("Loan Security Shortfall",
+ fields=["loan", "name"], filters={"status": "Pending"}, as_list=1))
+
loan_security_map = {}
for loan in loans:
@@ -62,7 +67,8 @@
outstanding_amount = flt(loan.total_payment) - flt(loan.total_interest_payable) \
- flt(loan.total_principal_paid)
else:
- outstanding_amount = loan.disbursed_amount
+ outstanding_amount = flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
+ - flt(loan.total_principal_paid)
pledged_securities = get_pledged_security_qty(loan.name)
ltv_ratio = ''
@@ -71,16 +77,22 @@
for security, qty in pledged_securities.items():
if not ltv_ratio:
ltv_ratio = get_ltv_ratio(security)
- security_value += loan_security_price_map.get(security) * qty
+ security_value += flt(loan_security_price_map.get(security)) * flt(qty)
- current_ratio = (outstanding_amount/security_value) * 100
+ current_ratio = (outstanding_amount/security_value) * 100 if security_value else 0
if current_ratio > ltv_ratio:
shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100)
create_loan_security_shortfall(loan.name, outstanding_amount, security_value, shortfall_amount,
- process_loan_security_shortfall)
+ current_ratio, process_loan_security_shortfall)
+ elif loan_shortfall_map.get(loan.name):
+ shortfall_amount = outstanding_amount - ((security_value * ltv_ratio) / 100)
+ if shortfall_amount <= 0:
+ shortfall = loan_shortfall_map.get(loan.name)
+ update_pending_shortfall(shortfall)
-def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall):
+def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, shortfall_ratio,
+ process_loan_security_shortfall):
existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name")
if existing_shortfall:
@@ -93,6 +105,7 @@
ltv_shortfall.loan_amount = loan_amount
ltv_shortfall.security_value = security_value
ltv_shortfall.shortfall_amount = shortfall_amount
+ ltv_shortfall.shortfall_percentage = shortfall_ratio
ltv_shortfall.process_loan_security_shortfall = process_loan_security_shortfall
ltv_shortfall.save()
@@ -101,3 +114,12 @@
ltv_ratio = frappe.db.get_value('Loan Security Type', loan_security_type, 'loan_to_value_ratio')
return ltv_ratio
+def update_pending_shortfall(shortfall):
+ # Get all pending loan security shortfall
+ frappe.db.set_value("Loan Security Shortfall", shortfall,
+ {
+ "status": "Completed",
+ "shortfall_amount": 0,
+ "shortfall_percentage": 0
+ })
+
diff --git a/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json
new file mode 100644
index 0000000..e060253
--- /dev/null
+++ b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json
@@ -0,0 +1,70 @@
+{
+ "cards": [
+ {
+ "card": "New Loans"
+ },
+ {
+ "card": "Active Loans"
+ },
+ {
+ "card": "Closed Loans"
+ },
+ {
+ "card": "Total Disbursed"
+ },
+ {
+ "card": "Open Loan Applications"
+ },
+ {
+ "card": "New Loan Applications"
+ },
+ {
+ "card": "Total Sanctioned Amount"
+ },
+ {
+ "card": "Active Securities"
+ },
+ {
+ "card": "Applicants With Unpaid Shortfall"
+ },
+ {
+ "card": "Total Shortfall Amount"
+ },
+ {
+ "card": "Total Repayment"
+ },
+ {
+ "card": "Total Write Off"
+ }
+ ],
+ "charts": [
+ {
+ "chart": "New Loans",
+ "width": "Half"
+ },
+ {
+ "chart": "Loan Disbursements",
+ "width": "Half"
+ },
+ {
+ "chart": "Top 10 Pledged Loan Securities",
+ "width": "Half"
+ },
+ {
+ "chart": "Loan Interest Accrual",
+ "width": "Half"
+ }
+ ],
+ "creation": "2021-02-06 16:52:43.484752",
+ "dashboard_name": "Loan Dashboard",
+ "docstatus": 0,
+ "doctype": "Dashboard",
+ "idx": 0,
+ "is_default": 0,
+ "is_standard": 1,
+ "modified": "2021-02-21 20:53:47.531699",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Dashboard",
+ "owner": "Administrator"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/active_loans/active_loans.json b/erpnext/loan_management/number_card/active_loans/active_loans.json
new file mode 100644
index 0000000..7e0db47
--- /dev/null
+++ b/erpnext/loan_management/number_card/active_loans/active_loans.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:10:26.132493",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"in\",[\"Disbursed\",\"Partially Disbursed\",null],false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Active Loans",
+ "modified": "2021-02-06 17:29:20.304087",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Active Loans",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/active_securities/active_securities.json b/erpnext/loan_management/number_card/active_securities/active_securities.json
new file mode 100644
index 0000000..298e410
--- /dev/null
+++ b/erpnext/loan_management/number_card/active_securities/active_securities.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 19:07:21.344199",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Security",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Security\",\"disabled\",\"=\",0,false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Active Securities",
+ "modified": "2021-02-06 19:07:26.671516",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Active Securities",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json b/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json
new file mode 100644
index 0000000..3b9eba1
--- /dev/null
+++ b/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json
@@ -0,0 +1,21 @@
+{
+ "creation": "2021-02-07 18:55:12.632616",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "filters_json": "null",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Applicants With Unpaid Shortfall",
+ "method": "erpnext.loan_management.doctype.loan.loan.get_shortfall_applicants",
+ "modified": "2021-02-07 21:46:27.369795",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Applicants With Unpaid Shortfall",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Custom"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/closed_loans/closed_loans.json b/erpnext/loan_management/number_card/closed_loans/closed_loans.json
new file mode 100644
index 0000000..c2f2244
--- /dev/null
+++ b/erpnext/loan_management/number_card/closed_loans/closed_loans.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-21 19:51:49.261813",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"=\",\"Closed\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Closed Loans",
+ "modified": "2021-02-21 19:51:54.087903",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Closed Loans",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json b/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json
new file mode 100644
index 0000000..65c8ce6
--- /dev/null
+++ b/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json
@@ -0,0 +1,21 @@
+{
+ "creation": "2021-02-07 21:57:14.758007",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "filters_json": "null",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Last Interest Accrual",
+ "method": "erpnext.loan_management.doctype.loan.loan.get_last_accrual_date",
+ "modified": "2021-02-07 21:59:47.525197",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Last Interest Accrual",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Custom"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json b/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json
new file mode 100644
index 0000000..7e655ff
--- /dev/null
+++ b/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:59:10.051269",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Application",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Application\",\"docstatus\",\"=\",\"1\",false],[\"Loan Application\",\"creation\",\"Timespan\",\"today\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "New Loan Applications",
+ "modified": "2021-02-06 17:59:21.880979",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "New Loan Applications",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/new_loans/new_loans.json b/erpnext/loan_management/number_card/new_loans/new_loans.json
new file mode 100644
index 0000000..424f0f1
--- /dev/null
+++ b/erpnext/loan_management/number_card/new_loans/new_loans.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:56:34.624031",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"creation\",\"Timespan\",\"today\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "New Loans",
+ "modified": "2021-02-06 17:58:20.209166",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "New Loans",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json b/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json
new file mode 100644
index 0000000..1d5e84e
--- /dev/null
+++ b/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:23:32.509899",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Application",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Application\",\"docstatus\",\"=\",\"1\",false],[\"Loan Application\",\"status\",\"=\",\"Open\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Open Loan Applications",
+ "modified": "2021-02-06 17:29:09.761011",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Open Loan Applications",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json b/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json
new file mode 100644
index 0000000..4a3f869
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "disbursed_amount",
+ "creation": "2021-02-06 16:52:19.505462",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Disbursement",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Disbursement\",\"docstatus\",\"=\",\"1\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Disbursed Amount",
+ "modified": "2021-02-06 17:29:38.453870",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Disbursed",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_repayment/total_repayment.json b/erpnext/loan_management/number_card/total_repayment/total_repayment.json
new file mode 100644
index 0000000..38de42b
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_repayment/total_repayment.json
@@ -0,0 +1,24 @@
+{
+ "aggregate_function_based_on": "amount_paid",
+ "color": "#29CD42",
+ "creation": "2021-02-21 19:27:45.989222",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Repayment",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Repayment\",\"docstatus\",\"=\",\"1\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Repayment",
+ "modified": "2021-02-21 19:34:59.656546",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Repayment",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json b/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json
new file mode 100644
index 0000000..dfb9d24
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "loan_amount",
+ "creation": "2021-02-06 17:05:04.704162",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"=\",\"Sanctioned\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Sanctioned Amount",
+ "modified": "2021-02-06 17:29:29.930557",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Sanctioned Amount",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json b/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json
new file mode 100644
index 0000000..aa6b093
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "shortfall_amount",
+ "creation": "2021-02-09 08:07:20.096995",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Security Shortfall",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Unpaid Shortfall Amount",
+ "modified": "2021-02-09 08:09:00.355547",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Shortfall Amount",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_write_off/total_write_off.json b/erpnext/loan_management/number_card/total_write_off/total_write_off.json
new file mode 100644
index 0000000..c85169a
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_write_off/total_write_off.json
@@ -0,0 +1,24 @@
+{
+ "aggregate_function_based_on": "write_off_amount",
+ "color": "#CB2929",
+ "creation": "2021-02-21 19:48:29.004429",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Write Off",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Write Off\",\"docstatus\",\"=\",\"1\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Write Off",
+ "modified": "2021-02-21 19:48:58.604159",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Write Off",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
index ab586bc..0ccd149 100644
--- a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
@@ -36,7 +36,7 @@
def get_data(filters):
data = []
- loan_security_details = get_loan_security_details(filters)
+ loan_security_details = get_loan_security_details()
pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters,
loan_security_details)
@@ -64,7 +64,7 @@
return data
-def get_loan_security_details(filters):
+def get_loan_security_details():
security_detail_map = {}
loan_security_price_map = {}
lsp_validity_map = {}
diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
index a3e69bb..2a74a1e 100644
--- a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
@@ -63,9 +63,11 @@
currency = erpnext.get_company_currency(filters.get('company'))
for loan in loan_details:
+ total_payment = loan.total_payment if loan.status == 'Disbursed' else loan.disbursed_amount
+
loan.update({
"sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)),
- "principal_outstanding": flt(loan.total_payment) - flt(loan.total_principal_paid) \
+ "principal_outstanding": flt(total_payment) - flt(loan.total_principal_paid) \
- flt(loan.total_interest_payable) - flt(loan.written_off_amount),
"total_repayment": flt(payments.get(loan.loan)),
"accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")),
@@ -171,7 +173,7 @@
return current_pledges
def get_loan_wise_security_value(filters, current_pledges):
- loan_security_details = get_loan_security_details(filters)
+ loan_security_details = get_loan_security_details()
loan_wise_security_value = {}
for key in current_pledges:
diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
index adc8013..887a86a 100644
--- a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
+++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
@@ -35,7 +35,7 @@
def get_data(filters):
data = []
- loan_security_details = get_loan_security_details(filters)
+ loan_security_details = get_loan_security_details()
current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details)
currency = erpnext.get_company_currency(filters.get('company'))
@@ -76,7 +76,7 @@
if qty:
security_wise_map[key[1]]['applicant_count'] += 1
- total_portfolio_value += flt(qty * loan_security_details.get(key[1])['latest_price'])
+ total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
return security_wise_map, total_portfolio_value
diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json
index 2e8b5bf..18559dc 100644
--- a/erpnext/loan_management/workspace/loan_management/loan_management.json
+++ b/erpnext/loan_management/workspace/loan_management/loan_management.json
@@ -10,6 +10,7 @@
"hide_custom": 0,
"icon": "loan",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Loan Management",
"links": [
@@ -219,7 +220,7 @@
"type": "Link"
}
],
- "modified": "2021-01-12 11:27:56.079724",
+ "modified": "2021-02-18 17:31:53.586508",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Management",
@@ -239,6 +240,12 @@
"label": "Loan",
"link_to": "Loan",
"type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "label": "Dashboard",
+ "link_to": "Loan Dashboard",
+ "type": "Dashboard"
}
]
}
\ No newline at end of file
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index cba6a2d..0aefe19 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -12,6 +12,7 @@
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
class MaintenanceSchedule(TransactionBase):
+ @frappe.whitelist()
def generate_schedule(self):
self.set('schedules', [])
frappe.db.sql("""delete from `tabMaintenance Schedule Detail`
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 03beedb..979f7ca 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -113,6 +113,7 @@
return item
+ @frappe.whitelist()
def get_routing(self):
if self.routing:
self.set("operations", [])
@@ -145,6 +146,7 @@
if not item.get(r):
item.set(r, ret[r])
+ @frappe.whitelist()
def get_bom_material_detail(self, args=None):
""" Get raw material details like uom, desc and rate"""
if not args:
@@ -210,6 +212,7 @@
.format(self.rm_cost_as_per, arg["item_code"]), alert=True)
return flt(rate) * flt(self.plc_conversion_rate or 1) / (self.conversion_rate or 1)
+ @frappe.whitelist()
def update_cost(self, update_parent=True, from_child_bom=False, save=True):
if self.docstatus == 2:
return
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 3239478..4050a7d 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -134,7 +134,13 @@
bom.items[0].conversion_factor = 6
bom.insert()
- reset_item_valuation_rate(item_code='_Test Item', qty=200, rate=200)
+ reset_item_valuation_rate(
+ item_code='_Test Item',
+ warehouse_list=frappe.get_all("Warehouse",
+ {"is_group":0, "company": bom.company}, pluck="name"),
+ qty=200,
+ rate=200
+ )
bom.update_cost()
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index ec28eb7..8aa0ffd 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -164,6 +164,7 @@
"time_in_mins": time_diff_in_minutes(row.planned_end_time, row.planned_start_time),
})
+ @frappe.whitelist()
def get_required_items(self):
if not self.get('work_order'):
return
@@ -255,6 +256,9 @@
data.actual_operation_time = time_in_mins
data.actual_start_time = time_data[0].start_time if time_data else None
data.actual_end_time = time_data[0].end_time if time_data else None
+ if data.get("workstation") != self.workstation:
+ # workstations can change in a job card
+ data.workstation = self.workstation
wo.flags.ignore_validate_update_after_submit = True
wo.update_operation_status()
@@ -267,6 +271,17 @@
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
+ def set_transferred_qty_in_job_card(self, ste_doc):
+ for row in ste_doc.items:
+ if not row.job_card_item: continue
+
+ qty = frappe.db.sql(""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
+ WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
+ se.purpose = 'Material Transfer for Manufacture'
+ """, (row.job_card_item))[0][0]
+
+ frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
+
def set_transferred_qty(self, update_status=False):
if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
@@ -279,7 +294,8 @@
self.transferred_qty = frappe.db.get_value('Stock Entry', {
'job_card': self.name,
'work_order': self.work_order,
- 'docstatus': 1
+ 'docstatus': 1,
+ 'purpose': 'Material Transfer for Manufacture'
}, 'sum(fg_completed_qty)') or 0
self.db_set("transferred_qty", self.transferred_qty)
@@ -420,6 +436,7 @@
target.purpose = "Material Transfer for Manufacture"
target.from_bom = 1
target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
+ target.set_transfer_qty()
target.calculate_rate_and_amount()
target.set_missing_values()
target.set_stock_entry_type()
@@ -437,9 +454,10 @@
"field_map": {
"source_warehouse": "s_warehouse",
"required_qty": "qty",
- "uom": "stock_uom"
+ "name": "job_card_item"
},
"postprocess": update_item,
+ "condition": lambda doc: doc.required_qty > 0
}
}, target_doc, set_missing_values)
diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
index bc9fe10..100ef4c 100644
--- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
+++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
@@ -1,363 +1,120 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-07-09 17:20:44.737289",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2018-07-09 17:20:44.737289",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "source_warehouse",
+ "uom",
+ "item_group",
+ "column_break_3",
+ "stock_uom",
+ "item_name",
+ "description",
+ "qty_section",
+ "required_qty",
+ "column_break_9",
+ "transferred_qty",
+ "allow_alternative_item"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item_code",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Item Code",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "source_warehouse",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 1,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Source Warehouse",
- "length": 0,
- "no_copy": 0,
- "options": "Warehouse",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "source_warehouse",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "in_list_view": 1,
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "uom",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_3",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Item Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "qty_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Qty",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "qty_section",
+ "fieldtype": "Section Break",
+ "label": "Qty"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "required_qty",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Required Qty",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Required Qty",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_9",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "allow_alternative_item",
- "fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Allow Alternative Item",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "default": "0",
+ "fieldname": "allow_alternative_item",
+ "fieldtype": "Check",
+ "label": "Allow Alternative Item"
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "transferred_qty",
+ "fieldtype": "Float",
+ "label": "Transferred Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-08-28 15:23:48.099459",
- "modified_by": "Administrator",
- "module": "Manufacturing",
- "name": "Job Card Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-11 13:50:13.804108",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Job Card Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
index f93b244..6c60bbd 100644
--- a/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
+++ b/erpnext/manufacturing/doctype/material_request_plan_item/material_request_plan_item.json
@@ -11,10 +11,14 @@
"from_warehouse",
"warehouse",
"column_break_4",
+ "required_bom_qty",
"quantity",
"uom",
"projected_qty",
"actual_qty",
+ "ordered_qty",
+ "reserved_qty_for_production",
+ "safety_stock",
"item_details",
"description",
"min_order_qty",
@@ -129,11 +133,40 @@
"fieldtype": "Link",
"label": "From Warehouse",
"options": "Warehouse"
+ },
+ {
+ "fetch_from": "item_code.safety_stock",
+ "fieldname": "safety_stock",
+ "fieldtype": "Float",
+ "label": "Safety Stock",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "ordered_qty",
+ "fieldtype": "Float",
+ "label": "Ordered Qty",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "reserved_qty_for_production",
+ "fieldtype": "Float",
+ "label": "Reserved Qty for Production",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "required_bom_qty",
+ "fieldtype": "Float",
+ "label": "Required Qty as per BOM",
+ "no_copy": 1,
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-02-03 12:22:29.913302",
+ "modified": "2021-03-26 12:41:13.013149",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Material Request Plan Item",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.js b/erpnext/manufacturing/doctype/production_plan/production_plan.js
index b723387..288c1d0 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.js
@@ -25,6 +25,16 @@
}
});
+ frm.set_query('material_request', 'material_requests', function() {
+ return {
+ filters: {
+ material_request_type: "Manufacture",
+ docstatus: 1,
+ status: ["!=", "Stopped"],
+ }
+ };
+ });
+
frm.fields_dict['po_items'].grid.get_field('item_code').get_query = function(doc) {
return {
query: "erpnext.controllers.queries.item_query",
@@ -251,7 +261,8 @@
get_items_for_material_requests: function(frm, warehouses) {
const set_fields = ['actual_qty', 'item_code','item_name', 'description', 'uom', 'from_warehouse',
- 'min_order_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'material_request_type'];
+ 'min_order_qty', 'required_bom_qty', 'quantity', 'sales_order', 'warehouse', 'projected_qty', 'ordered_qty',
+ 'reserved_qty_for_production', 'material_request_type'];
frappe.call({
method: "erpnext.manufacturing.doctype.production_plan.production_plan.get_items_for_material_requests",
@@ -369,4 +380,4 @@
['Sales Order','docstatus', '=' ,1]
]
}
-};
\ No newline at end of file
+};
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.json b/erpnext/manufacturing/doctype/production_plan/production_plan.json
index 7daf706..f114700 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.json
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.json
@@ -32,6 +32,7 @@
"material_request_planning",
"include_non_stock_items",
"include_subcontracted_items",
+ "include_safety_stock",
"ignore_existing_ordered_qty",
"column_break_25",
"for_warehouse",
@@ -309,13 +310,19 @@
"fieldtype": "Select",
"label": "Sales Order Status",
"options": "\nTo Deliver and Bill\nTo Bill\nTo Deliver"
+ },
+ {
+ "default": "0",
+ "fieldname": "include_safety_stock",
+ "fieldtype": "Check",
+ "label": "Include Safety Stock in Required Qty Calculation"
}
],
"icon": "fa fa-calendar",
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-10 18:01:54.991970",
+ "modified": "2021-03-08 11:17:25.470147",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Production Plan",
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 8f9dd05..cef2d8b 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -29,6 +29,7 @@
if not flt(d.planned_qty):
frappe.throw(_("Please enter Planned Qty for Item {0} at row {1}").format(d.item_code, d.idx))
+ @frappe.whitelist()
def get_open_sales_orders(self):
""" Pull sales orders which are pending to deliver based on criteria selected"""
open_so = get_sales_orders(self)
@@ -50,6 +51,7 @@
'grand_total': data.base_grand_total
})
+ @frappe.whitelist()
def get_pending_material_requests(self):
""" Pull Material Requests that are pending based on criteria selected"""
mr_filter = item_filter = ""
@@ -68,7 +70,7 @@
from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item
where mr_item.parent = mr.name
and mr.material_request_type = "Manufacture"
- and mr.docstatus = 1 and mr.company = %(company)s
+ and mr.docstatus = 1 and mr.status != "Stopped" and mr.company = %(company)s
and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1}
and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code
and bom.is_active = 1))
@@ -92,6 +94,7 @@
'material_request_date': data.transaction_date
})
+ @frappe.whitelist()
def get_items(self):
if self.get_items_from == "Sales Order":
self.get_so_items()
@@ -219,6 +222,7 @@
filters = {'docstatus': 0, 'production_plan': ("=", self.name)}):
frappe.delete_doc('Work Order', d.name)
+ @frappe.whitelist()
def set_status(self, close=None):
self.status = {
0: 'Draft',
@@ -302,6 +306,7 @@
return item_dict
+ @frappe.whitelist()
def make_work_order(self):
wo_list = []
self.validate_data()
@@ -367,6 +372,7 @@
except OverProductionError:
pass
+ @frappe.whitelist()
def make_material_request(self):
'''Create Material Requests grouped by Sales Order and Material Request Type'''
material_request_list = []
@@ -434,12 +440,14 @@
if isinstance(doc, string_types):
doc = frappe._dict(json.loads(doc))
- item_list = [['Item Code', 'Description', 'Stock UOM', 'Required Qty', 'Warehouse',
- 'projected Qty', 'Actual Qty']]
+ item_list = [['Item Code', 'Description', 'Stock UOM', 'Warehouse', 'Required Qty as per BOM',
+ 'Projected Qty', 'Actual Qty', 'Ordered Qty', 'Reserved Qty for Production',
+ 'Safety Stock', 'Required Qty']]
for d in get_items_for_material_requests(doc):
- item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('quantity'),
- d.get('warehouse'), d.get('projected_qty'), d.get('actual_qty')])
+ item_list.append([d.get('item_code'), d.get('description'), d.get('stock_uom'), d.get('warehouse'),
+ d.get('required_bom_qty'), d.get('projected_qty'), d.get('actual_qty'), d.get('ordered_qty'),
+ d.get('reserved_qty_for_production'), d.get('safety_stock'), d.get('quantity')])
if not doc.get('for_warehouse'):
row = {'item_code': d.get('item_code')}
@@ -447,8 +455,9 @@
if d.get("warehouse") == bin_dict.get('warehouse'):
continue
- item_list.append(['', '', '', '', bin_dict.get('warehouse'),
- bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0)])
+ item_list.append(['', '', '', bin_dict.get('warehouse'), '',
+ bin_dict.get('projected_qty', 0), bin_dict.get('actual_qty', 0),
+ bin_dict.get('ordered_qty', 0), bin_dict.get('reserved_qty_for_production', 0)])
build_csv_response(item_list, doc.name)
@@ -482,7 +491,7 @@
ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty,
item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse,
item.default_bom as default_bom, bom_item.description as description,
- bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty,
+ bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, item.safety_stock as safety_stock,
item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor
FROM
`tabBOM Item` bom_item
@@ -518,8 +527,8 @@
include_non_stock_items, include_subcontracted_items, d.qty)
return item_details
-def get_material_request_items(row, sales_order,
- company, ignore_existing_ordered_qty, warehouse, bin_dict):
+def get_material_request_items(row, sales_order, company,
+ ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict):
total_qty = row['qty']
required_qty = 0
@@ -543,17 +552,24 @@
if frappe.db.get_value("UOM", row['purchase_uom'], "must_be_whole_number"):
required_qty = ceil(required_qty)
+ if include_safety_stock:
+ required_qty += flt(row['safety_stock'])
+
if required_qty > 0:
return {
'item_code': row.item_code,
'item_name': row.item_name,
'quantity': required_qty,
+ 'required_bom_qty': total_qty,
'description': row.description,
'stock_uom': row.get("stock_uom"),
'warehouse': warehouse or row.get('source_warehouse') \
or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"),
+ 'safety_stock': row.safety_stock,
'actual_qty': bin_dict.get("actual_qty", 0),
'projected_qty': bin_dict.get("projected_qty", 0),
+ 'ordered_qty': bin_dict.get("ordered_qty", 0),
+ 'reserved_qty_for_production': bin_dict.get("reserved_qty_for_production", 0),
'min_order_qty': row['min_order_qty'],
'material_request_type': row.get("default_material_request_type"),
'sales_order': sales_order,
@@ -620,7 +636,8 @@
""".format(lft, rgt, company)
return frappe.db.sql(""" select ifnull(sum(projected_qty),0) as projected_qty,
- ifnull(sum(actual_qty),0) as actual_qty, warehouse from `tabBin`
+ ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty,
+ ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse from `tabBin`
where item_code = %(item_code)s {conditions}
group by item_code, warehouse
""".format(conditions=conditions), { "item_code": row['item_code'] }, as_dict=1)
@@ -660,6 +677,7 @@
company = doc.get('company')
ignore_existing_ordered_qty = doc.get('ignore_existing_ordered_qty')
+ include_safety_stock = doc.get('include_safety_stock')
so_item_details = frappe._dict()
for data in po_items:
@@ -711,6 +729,7 @@
'description' : item_master.description,
'stock_uom' : item_master.stock_uom,
'conversion_factor' : conversion_factor,
+ 'safety_stock': item_master.safety_stock
}
)
@@ -732,7 +751,7 @@
if details.qty > 0:
items = get_material_request_items(details, sales_order, company,
- ignore_existing_ordered_qty, warehouse, bin_dict)
+ ignore_existing_ordered_qty, include_safety_stock, warehouse, bin_dict)
if items:
mr_items.append(items)
diff --git a/erpnext/manufacturing/doctype/routing/test_routing.py b/erpnext/manufacturing/doctype/routing/test_routing.py
index 73d05a6..6a38dcf 100644
--- a/erpnext/manufacturing/doctype/routing/test_routing.py
+++ b/erpnext/manufacturing/doctype/routing/test_routing.py
@@ -13,8 +13,15 @@
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
class TestRouting(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ cls.item_code = "Test Routing Item - A"
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.sql('delete from tabBOM where item=%s', cls.item_code)
+
def test_sequence_id(self):
- item_code = "Test Routing Item - A"
operations = [{"operation": "Test Operation A", "workstation": "Test Workstation A", "time_in_mins": 30},
{"operation": "Test Operation B", "workstation": "Test Workstation A", "time_in_mins": 20}]
@@ -22,8 +29,8 @@
setup_operations(operations)
routing_doc = create_routing(routing_name="Testing Route", operations=operations)
- bom_doc = setup_bom(item_code=item_code, routing=routing_doc.name)
- wo_doc = make_wo_order_test_record(production_item = item_code, bom_no=bom_doc.name)
+ bom_doc = setup_bom(item_code=self.item_code, routing=routing_doc.name)
+ wo_doc = make_wo_order_test_record(production_item = self.item_code, bom_no=bom_doc.name)
for row in routing_doc.operations:
self.assertEqual(row.sequence_id, row.idx)
@@ -74,7 +81,7 @@
})
if not args.raw_materials:
- if not frappe.db.exists('Item', "Test Extra Item 1"):
+ if not frappe.db.exists('Item', "Test Extra Item N-1"):
make_item("Test Extra Item N-1", {
'is_stock_item': 1,
})
@@ -88,4 +95,4 @@
else:
bom_doc = frappe.get_doc("BOM", name)
- return bom_doc
\ No newline at end of file
+ return bom_doc
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index 06a8e19..6b1fafe 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -82,7 +82,7 @@
wo_order.set_work_order_operations()
self.assertEqual(wo_order.planned_operating_cost, cost*2)
- def test_resered_qty_for_partial_completion(self):
+ def test_reserved_qty_for_partial_completion(self):
item = "_Test Item"
warehouse = create_warehouse("Test Warehouse for reserved_qty - _TC")
@@ -94,11 +94,11 @@
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2,
source_warehouse=warehouse, skip_transfer=1)
- bin1_on_submit = get_bin(item, warehouse)
+ reserved_qty_on_submission = cint(get_bin(item, warehouse).reserved_qty_for_production)
# reserved qty for production is updated
- self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2,
- cint(bin1_on_submit.reserved_qty_for_production))
+ self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission)
+
test_stock_entry.make_stock_entry(item_code="_Test Item",
target=warehouse, qty=100, basic_rate=100)
@@ -111,7 +111,7 @@
bin1_at_completion = get_bin(item, warehouse)
self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production),
- cint(bin1_on_submit.reserved_qty_for_production) - 1)
+ reserved_qty_on_submission - 1)
def test_production_item(self):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True)
@@ -371,14 +371,14 @@
def test_job_card(self):
stock_entries = []
- data = frappe.get_cached_value('BOM',
- {'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
+ bom = frappe.get_doc('BOM', {
+ 'docstatus': 1,
+ 'with_operations': 1,
+ 'company': '_Test Company'
+ })
- bom, bom_item = data
-
- bom_doc = frappe.get_doc('BOM', bom)
- work_order = make_wo_order_test_record(item=bom_item, qty=1,
- bom_no=bom, source_warehouse="_Test Warehouse - _TC")
+ work_order = make_wo_order_test_record(item=bom.item, qty=1,
+ bom_no=bom.name, source_warehouse="_Test Warehouse - _TC")
for row in work_order.required_items:
stock_entry_doc = test_stock_entry.make_stock_entry(item_code=row.item_code,
@@ -390,14 +390,14 @@
stock_entries.append(ste)
job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
- self.assertEqual(len(job_cards), len(bom_doc.operations))
+ self.assertEqual(len(job_cards), len(bom.operations))
for i, job_card in enumerate(job_cards):
doc = frappe.get_doc("Job Card", job_card)
doc.append("time_logs", {
- "from_time": now(),
- "hours": i,
- "to_time": add_to_date(now(), i),
+ "from_time": add_to_date(None, i),
+ "hours": 1,
+ "to_time": add_to_date(None, i + 1),
"completed_qty": doc.for_quantity
})
doc.submit()
@@ -592,6 +592,55 @@
frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM")
+ def test_make_stock_entry_for_customer_provided_item(self):
+ finished_item = 'Test Item for Make Stock Entry 1'
+ make_item(finished_item, {
+ "include_item_in_manufacturing": 1,
+ "is_stock_item": 1
+ })
+
+ customer_provided_item = 'CUST-0987'
+ make_item(customer_provided_item, {
+ 'is_purchase_item': 0,
+ 'is_customer_provided_item': 1,
+ "is_stock_item": 1,
+ "include_item_in_manufacturing": 1,
+ 'customer': '_Test Customer'
+ })
+
+ if not frappe.db.exists('BOM', {'item': finished_item}):
+ make_bom(item=finished_item, raw_materials=[customer_provided_item], rm_qty=1)
+
+ company = "_Test Company with perpetual inventory"
+ customer_warehouse = create_warehouse("Test Customer Provided Warehouse", company=company)
+ wo = make_wo_order_test_record(item=finished_item, qty=1, source_warehouse=customer_warehouse,
+ company=company)
+
+ ste = frappe.get_doc(make_stock_entry(wo.name, purpose='Material Transfer for Manufacture'))
+ ste.insert()
+
+ self.assertEqual(len(ste.items), 1)
+ for item in ste.items:
+ self.assertEqual(item.allow_zero_valuation_rate, 1)
+ self.assertEqual(item.valuation_rate, 0)
+
+ def test_valuation_rate_missing_on_make_stock_entry(self):
+ item_name = 'Test Valuation Rate Missing'
+ make_item(item_name, {
+ "is_stock_item": 1,
+ "include_item_in_manufacturing": 1,
+ })
+
+ if not frappe.db.get_value('BOM', {'item': item_name}):
+ make_bom(item=item_name, raw_materials=[item_name], rm_qty=1)
+
+ company = "_Test Company with perpetual inventory"
+ source_warehouse = create_warehouse("Test Valuation Rate Missing Warehouse", company=company)
+ wo = make_wo_order_test_record(item=item_name, qty=1, source_warehouse=source_warehouse,
+ company=company)
+
+ self.assertRaises(frappe.ValidationError, make_stock_entry, wo.name, 'Material Transfer for Manufacture')
+
def get_scrap_item_details(bom_no):
scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
@@ -609,6 +658,15 @@
def make_wo_order_test_record(**args):
args = frappe._dict(args)
+ if args.company and args.company != "_Test Company":
+ warehouse_map = {
+ "fg_warehouse": "_Test FG Warehouse",
+ "wip_warehouse": "_Test WIP Warehouse"
+ }
+
+ for attr, wh_name in warehouse_map.items():
+ if not args.get(attr):
+ args[attr] = create_warehouse(wh_name, company=args.company)
wo_order = frappe.new_doc("Work Order")
wo_order.production_item = args.production_item or args.item or args.item_code or "_Test FG Item"
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.json b/erpnext/manufacturing/doctype/work_order/work_order.json
index 585a09d..cd9edee 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.json
+++ b/erpnext/manufacturing/doctype/work_order/work_order.json
@@ -333,8 +333,7 @@
"fieldname": "operations",
"fieldtype": "Table",
"label": "Operations",
- "options": "Work Order Operation",
- "read_only": 1
+ "options": "Work Order Operation"
},
{
"depends_on": "operations",
@@ -496,7 +495,7 @@
"image_field": "image",
"is_submittable": 1,
"links": [],
- "modified": "2020-05-05 19:32:43.323054",
+ "modified": "2021-03-16 13:27:51.116484",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Work Order",
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index ca530bb..8507f5e 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -509,6 +509,7 @@
stock_bin = get_bin(d.item_code, d.source_warehouse)
stock_bin.update_reserved_qty_for_production()
+ @frappe.whitelist()
def get_items_and_operations_from_bom(self):
self.set_required_items()
self.set_work_order_operations()
@@ -528,6 +529,10 @@
if not reset_only_qty:
self.required_items = []
+ operation = None
+ if self.get('operations') and len(self.operations) == 1:
+ operation = self.operations[0].operation
+
if self.bom_no and self.qty:
item_dict = get_bom_items_as_dict(self.bom_no, self.company, qty=self.qty,
fetch_exploded = self.use_multi_level_bom)
@@ -536,6 +541,9 @@
for d in self.get("required_items"):
if item_dict.get(d.item_code):
d.required_qty = item_dict.get(d.item_code).get("qty")
+
+ if not d.operation:
+ d.operation = operation
else:
# Attribute a big number (999) to idx for sorting putpose in case idx is NULL
# For instance in BOM Explosion Item child table, the items coming from sub assembly items
@@ -543,7 +551,7 @@
self.append('required_items', {
'rate': item.rate,
'amount': item.amount,
- 'operation': item.operation,
+ 'operation': item.operation or operation,
'item_code': item.item_code,
'item_name': item.item_name,
'description': item.description,
@@ -606,6 +614,7 @@
item.db_set('consumed_qty', flt(consumed_qty), update_modified=False)
+ @frappe.whitelist()
def make_bom(self):
data = frappe.db.sql(""" select sed.item_code, sed.qty, sed.s_warehouse
from `tabStock Entry Detail` sed, `tabStock Entry` se
@@ -879,7 +888,7 @@
doc.schedule_time_logs(row)
doc.insert()
- frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)))
+ frappe.msgprint(_("Job card {0} created").format(get_link_to_form("Job Card", doc.name)), alert=True)
return doc
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
index f7b407b..ffd9242 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
@@ -88,11 +88,11 @@
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
def get_manufacturer_records():
- details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no, parent"])
+ details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
manufacture_details = frappe._dict()
for detail in details:
dic = manufacture_details.setdefault(detail.get('parent'), {})
dic.setdefault('manufacturer', []).append(detail.get('manufacturer'))
dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no'))
- return manufacture_details
\ No newline at end of file
+ return manufacture_details
diff --git a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
index 2ca9f16..fc27d35 100644
--- a/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
+++ b/erpnext/manufacturing/report/exponential_smoothing_forecasting/exponential_smoothing_forecasting.py
@@ -61,7 +61,7 @@
from_date = add_years(self.filters.from_date, cint(self.filters.no_of_years) * -1)
self.period_list = get_period_list(from_date, self.filters.to_date,
- from_date, self.filters.to_date, None, self.filters.periodicity, ignore_fiscal_year=True)
+ from_date, self.filters.to_date, "Date Range", self.filters.periodicity, ignore_fiscal_year=True)
order_data = self.get_data_for_forecast() or []
diff --git a/erpnext/non_profit/doctype/membership_settings/__init__.py b/erpnext/non_profit/doctype/donation/__init__.py
similarity index 100%
copy from erpnext/non_profit/doctype/membership_settings/__init__.py
copy to erpnext/non_profit/doctype/donation/__init__.py
diff --git a/erpnext/non_profit/doctype/donation/donation.js b/erpnext/non_profit/doctype/donation/donation.js
new file mode 100644
index 0000000..10e8220
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation.js
@@ -0,0 +1,26 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Donation', {
+ refresh: function(frm) {
+ if (frm.doc.docstatus === 1 && !frm.doc.paid) {
+ frm.add_custom_button(__('Create Payment Entry'), function() {
+ frm.events.make_payment_entry(frm);
+ });
+ }
+ },
+
+ make_payment_entry: function(frm) {
+ return frappe.call({
+ method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
+ args: {
+ 'dt': frm.doc.doctype,
+ 'dn': frm.doc.name
+ },
+ callback: function(r) {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route('Form', doc[0].doctype, doc[0].name);
+ }
+ });
+ },
+});
diff --git a/erpnext/non_profit/doctype/donation/donation.json b/erpnext/non_profit/doctype/donation/donation.json
new file mode 100644
index 0000000..6759569
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation.json
@@ -0,0 +1,156 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2021-02-17 10:28:52.645731",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "donor",
+ "donor_name",
+ "email",
+ "column_break_4",
+ "company",
+ "date",
+ "payment_details_section",
+ "paid",
+ "amount",
+ "mode_of_payment",
+ "razorpay_payment_id",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "donor",
+ "fieldtype": "Link",
+ "label": "Donor",
+ "options": "Donor",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "donor.donor_name",
+ "fieldname": "donor_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Donor Name",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donor.email",
+ "fieldname": "email",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Email",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "payment_details_section",
+ "fieldtype": "Section Break",
+ "label": "Payment Details"
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "reqd": 1
+ },
+ {
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment"
+ },
+ {
+ "fieldname": "razorpay_payment_id",
+ "fieldtype": "Data",
+ "label": "Razorpay Payment ID",
+ "read_only": 1
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "NPO-DTN-.YYYY.-"
+ },
+ {
+ "default": "0",
+ "fieldname": "paid",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Paid"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Donation",
+ "print_hide": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-03-11 10:53:11.269005",
+ "modified_by": "Administrator",
+ "module": "Non Profit",
+ "name": "Donation",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Non Profit Manager",
+ "select": 1,
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "donor_name, email",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "donor_name",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donation/donation.py b/erpnext/non_profit/doctype/donation/donation.py
new file mode 100644
index 0000000..4fd1a30
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation.py
@@ -0,0 +1,220 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import six
+import json
+from frappe.model.document import Document
+from frappe import _
+from frappe.utils import getdate, flt, get_link_to_form
+from frappe.email import sendmail_to_system_managers
+from erpnext.non_profit.doctype.membership.membership import verify_signature
+
+class Donation(Document):
+ def validate(self):
+ if not self.donor or not frappe.db.exists('Donor', self.donor):
+ # for web forms
+ user_type = frappe.db.get_value('User', frappe.session.user, 'user_type')
+ if user_type == 'Website User':
+ self.create_donor_for_website_user()
+ else:
+ frappe.throw(_('Please select a Member'))
+
+ def create_donor_for_website_user(self):
+ donor_name = frappe.get_value('Donor', dict(email=frappe.session.user))
+
+ if not donor_name:
+ user = frappe.get_doc('User', frappe.session.user)
+ donor = frappe.get_doc(dict(
+ doctype='Donor',
+ donor_type=self.get('donor_type'),
+ email=frappe.session.user,
+ member_name=user.get_fullname()
+ )).insert(ignore_permissions=True)
+ donor_name = donor.name
+
+ if self.get('__islocal'):
+ self.donor = donor_name
+
+ def on_payment_authorized(self, *args, **kwargs):
+ self.load_from_db()
+ self.create_payment_entry()
+
+ def create_payment_entry(self, date=None):
+ settings = frappe.get_doc('Non Profit Settings')
+ if not settings.automate_donation_payment_entries:
+ return
+
+ if not settings.donation_payment_account:
+ frappe.throw(_('You need to set <b>Payment Account</b> for Donation in {0}').format(
+ get_link_to_form('Non Profit Settings', 'Non Profit Settings')))
+
+ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+
+ frappe.flags.ignore_account_permission = True
+ pe = get_payment_entry(dt=self.doctype, dn=self.name)
+ frappe.flags.ignore_account_permission = False
+ pe.paid_from = settings.donation_debit_account
+ pe.paid_to = settings.donation_payment_account
+ pe.posting_date = date or getdate()
+ pe.reference_no = self.name
+ pe.reference_date = date or getdate()
+ pe.flags.ignore_mandatory = True
+ pe.insert()
+ pe.submit()
+
+
+@frappe.whitelist(allow_guest=True)
+def capture_razorpay_donations(*args, **kwargs):
+ """
+ Creates Donation from Razorpay Webhook Request Data on payment.captured event
+ Creates Donor from email if not found
+ """
+ data = frappe.request.get_data(as_text=True)
+
+ try:
+ verify_signature(data, endpoint='Donation')
+ except Exception as e:
+ log = frappe.log_error(e, 'Donation Webhook Verification Error')
+ notify_failure(log)
+ return { 'status': 'Failed', 'reason': e }
+
+ if isinstance(data, six.string_types):
+ data = json.loads(data)
+ data = frappe._dict(data)
+
+ payment = data.payload.get('payment', {}).get('entity', {})
+ payment = frappe._dict(payment)
+
+ try:
+ if not data.event == 'payment.captured':
+ return
+
+ # to avoid capturing subscription payments as donations
+ if payment.description and 'subscription' in str(payment.description).lower():
+ return
+
+ donor = get_donor(payment.email)
+ if not donor:
+ donor = create_donor(payment)
+
+ donation = create_donation(donor, payment)
+ donation.run_method('create_payment_entry')
+
+ except Exception as e:
+ message = '{0}\n\n{1}\n\n{2}: {3}'.format(e, frappe.get_traceback(), _('Payment ID'), payment.id)
+ log = frappe.log_error(message, _('Error creating donation entry for {0}').format(donor.name))
+ notify_failure(log)
+ return { 'status': 'Failed', 'reason': e }
+
+ return { 'status': 'Success' }
+
+
+def create_donation(donor, payment):
+ if not frappe.db.exists('Mode of Payment', payment.method):
+ create_mode_of_payment(payment.method)
+
+ company = get_company_for_donations()
+ donation = frappe.get_doc({
+ 'doctype': 'Donation',
+ 'company': company,
+ 'donor': donor.name,
+ 'donor_name': donor.donor_name,
+ 'email': donor.email,
+ 'date': getdate(),
+ 'amount': flt(payment.amount) / 100, # Convert to rupees from paise
+ 'mode_of_payment': payment.method,
+ 'razorpay_payment_id': payment.id
+ }).insert(ignore_mandatory=True)
+
+ donation.submit()
+ return donation
+
+
+def get_donor(email):
+ donors = frappe.get_all('Donor',
+ filters={'email': email},
+ order_by='creation desc')
+
+ try:
+ return frappe.get_doc('Donor', donors[0]['name'])
+ except Exception:
+ return None
+
+
+@frappe.whitelist()
+def create_donor(payment):
+ donor_details = frappe._dict(payment)
+ donor_type = frappe.db.get_single_value('Non Profit Settings', 'default_donor_type')
+
+ donor = frappe.new_doc('Donor')
+ donor.update({
+ 'donor_name': donor_details.email,
+ 'donor_type': donor_type,
+ 'email': donor_details.email,
+ 'contact': donor_details.contact
+ })
+
+ if donor_details.get('notes'):
+ donor = get_additional_notes(donor, donor_details)
+
+ donor.insert(ignore_mandatory=True)
+ return donor
+
+
+def get_company_for_donations():
+ company = frappe.db.get_single_value('Non Profit Settings', 'donation_company')
+ if not company:
+ from erpnext.healthcare.setup import get_company
+ company = get_company()
+ return company
+
+
+def get_additional_notes(donor, donor_details):
+ if type(donor_details.notes) == dict:
+ for k, v in donor_details.notes.items():
+ notes = '\n'.join('{}: {}'.format(k, v))
+
+ # extract donor name from notes
+ if 'name' in k.lower():
+ donor.update({
+ 'donor_name': donor_details.notes.get(k)
+ })
+
+ # extract pan from notes
+ if 'pan' in k.lower():
+ donor.update({
+ 'pan_number': donor_details.notes.get(k)
+ })
+
+ donor.add_comment('Comment', notes)
+
+ elif type(donor_details.notes) == str:
+ donor.add_comment('Comment', donor_details.notes)
+
+ return donor
+
+
+def create_mode_of_payment(method):
+ frappe.get_doc({
+ 'doctype': 'Mode of Payment',
+ 'mode_of_payment': method
+ }).insert(ignore_mandatory=True)
+
+
+def notify_failure(log):
+ try:
+ content = '''
+ Dear System Manager,
+ Razorpay webhook for creating donation failed due to some reason.
+ Please check the error log linked below
+ Error Log: {0}
+ Regards, Administrator
+ '''.format(get_link_to_form('Error Log', log.name))
+
+ sendmail_to_system_managers(_('[Important] [ERPNext] Razorpay donation webhook failed, please check.'), content)
+ except Exception:
+ pass
+
diff --git a/erpnext/non_profit/doctype/donation/donation_dashboard.py b/erpnext/non_profit/doctype/donation/donation_dashboard.py
new file mode 100644
index 0000000..7e25c8d
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/donation_dashboard.py
@@ -0,0 +1,16 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'donation',
+ 'non_standard_fieldnames': {
+ 'Payment Entry': 'reference_name'
+ },
+ 'transactions': [
+ {
+ 'label': _('Payment'),
+ 'items': ['Payment Entry']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donation/test_donation.py b/erpnext/non_profit/doctype/donation/test_donation.py
new file mode 100644
index 0000000..c6a534d
--- /dev/null
+++ b/erpnext/non_profit/doctype/donation/test_donation.py
@@ -0,0 +1,76 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from erpnext.non_profit.doctype.donation.donation import create_donation
+
+class TestDonation(unittest.TestCase):
+ def setUp(self):
+ create_donor_type()
+ settings = frappe.get_doc('Non Profit Settings')
+ settings.company = '_Test Company'
+ settings.donation_company = '_Test Company'
+ settings.default_donor_type = '_Test Donor'
+ settings.automate_donation_payment_entries = 1
+ settings.donation_debit_account = 'Debtors - _TC'
+ settings.donation_payment_account = 'Cash - _TC'
+ settings.creation_user = 'Administrator'
+ settings.flags.ignore_permissions = True
+ settings.save()
+
+ def test_payment_entry_for_donations(self):
+ donor = create_donor()
+ create_mode_of_payment()
+ payment = frappe._dict({
+ 'amount': 100,
+ 'method': 'Debit Card',
+ 'id': 'pay_MeXAmsgeKOhq7O'
+ })
+ donation = create_donation(donor, payment)
+
+ self.assertTrue(donation.name)
+
+ # Naive test to check if at all payment entry is generated
+ # This method is actually triggered from Payment Gateway
+ # In any case if details were missing, this would throw an error
+ donation.on_payment_authorized()
+ donation.reload()
+
+ self.assertEquals(donation.paid, 1)
+ self.assertTrue(frappe.db.exists('Payment Entry', {'reference_no': donation.name}))
+
+
+def create_donor_type():
+ if not frappe.db.exists('Donor Type', '_Test Donor'):
+ frappe.get_doc({
+ 'doctype': 'Donor Type',
+ 'donor_type': '_Test Donor'
+ }).insert()
+
+
+def create_donor():
+ donor = frappe.db.exists('Donor', 'donor@test.com')
+ if donor:
+ return frappe.get_doc('Donor', 'donor@test.com')
+ else:
+ return frappe.get_doc({
+ 'doctype': 'Donor',
+ 'donor_name': '_Test Donor',
+ 'donor_type': '_Test Donor',
+ 'email': 'donor@test.com'
+ }).insert()
+
+
+def create_mode_of_payment():
+ if not frappe.db.exists('Mode of Payment', 'Debit Card'):
+ frappe.get_doc({
+ 'doctype': 'Mode of Payment',
+ 'mode_of_payment': 'Debit Card',
+ 'accounts': [{
+ 'company': '_Test Company',
+ 'default_account': 'Cash - _TC'
+ }]
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/donor/donor.json b/erpnext/non_profit/doctype/donor/donor.json
index 9639265..72f24ef 100644
--- a/erpnext/non_profit/doctype/donor/donor.json
+++ b/erpnext/non_profit/doctype/donor/donor.json
@@ -76,8 +76,13 @@
}
],
"image_field": "image",
- "links": [],
- "modified": "2020-09-16 23:46:04.083274",
+ "links": [
+ {
+ "link_doctype": "Donation",
+ "link_fieldname": "donor"
+ }
+ ],
+ "modified": "2021-02-17 16:36:33.470731",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Donor",
diff --git a/erpnext/non_profit/doctype/donor/donor.py b/erpnext/non_profit/doctype/donor/donor.py
index 9121d0c..fb70e59 100644
--- a/erpnext/non_profit/doctype/donor/donor.py
+++ b/erpnext/non_profit/doctype/donor/donor.py
@@ -11,3 +11,8 @@
"""Load address and contacts in `__onload`"""
load_address_and_contact(self)
+ def validate(self):
+ from frappe.utils import validate_email_address
+ if self.email:
+ validate_email_address(self.email.strip(), True)
+
diff --git a/erpnext/non_profit/doctype/member/member.js b/erpnext/non_profit/doctype/member/member.js
index 199dcfc..6b8f1b1 100644
--- a/erpnext/non_profit/doctype/member/member.js
+++ b/erpnext/non_profit/doctype/member/member.js
@@ -3,7 +3,7 @@
frappe.ui.form.on('Member', {
setup: function(frm) {
- frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
if (val && (frm.doc.subscription_id || frm.doc.customer_id)) {
frm.set_df_property('razorpay_details_section', 'hidden', false);
}
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 04b99f9..efc072e 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -7,7 +7,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.contacts.address_and_contact import load_address_and_contact
-from frappe.utils import cint
+from frappe.utils import cint, get_link_to_form
from frappe.integrations.utils import get_payment_gateway_controller
from erpnext.non_profit.doctype.membership_type.membership_type import get_membership_type
@@ -26,9 +26,10 @@
validate_email_address(email.strip(), True)
def setup_subscription(self):
- membership_settings = frappe.get_doc("Membership Settings")
- if not membership_settings.enable_razorpay:
- frappe.throw("Please enable Razorpay to setup subscription")
+ non_profit_settings = frappe.get_doc('Non Profit Settings')
+ if not non_profit_settings.enable_razorpay_for_memberships:
+ frappe.throw('Please check Enable Razorpay for Memberships in {0} to setup subscription').format(
+ get_link_to_form('Non Profit Settings', 'Non Profit Settings'))
controller = get_payment_gateway_controller("Razorpay")
settings = controller.get_settings({})
@@ -40,7 +41,7 @@
subscription_details = {
"plan_id": plan_id,
- "billing_frequency": cint(membership_settings.billing_frequency),
+ "billing_frequency": cint(non_profit_settings.billing_frequency),
"customer_notify": 1
}
@@ -52,6 +53,7 @@
return subscription
+ @frappe.whitelist()
def make_customer_and_link(self):
if self.customer:
frappe.msgprint(_("A customer is already linked to this Member"))
diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js
index 573ac33..3187204 100644
--- a/erpnext/non_profit/doctype/membership/membership.js
+++ b/erpnext/non_profit/doctype/membership/membership.js
@@ -3,7 +3,7 @@
frappe.ui.form.on('Membership', {
setup: function(frm) {
- frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ frappe.db.get_single_value("Non Profit Settings", "enable_razorpay_for_memberships").then(val => {
if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
})
},
@@ -26,7 +26,7 @@
});
});
- frappe.db.get_single_value("Membership Settings", "send_email").then(val => {
+ frappe.db.get_single_value("Non Profit Settings", "send_email").then(val => {
if (val) frm.add_custom_button("Send Acknowledgement", () => {
frm.call("send_acknowlement").then(() => {
frm.reload_doc();
diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json
index 6da053f..11d32f9 100644
--- a/erpnext/non_profit/doctype/membership/membership.json
+++ b/erpnext/non_profit/doctype/membership/membership.json
@@ -10,6 +10,7 @@
"member_name",
"membership_type",
"column_break_3",
+ "company",
"membership_status",
"membership_validity_section",
"from_date",
@@ -132,11 +133,18 @@
"fieldtype": "Data",
"label": "Member Name",
"read_only": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2021-01-21 16:31:20.032656",
+ "modified": "2021-02-19 14:33:44.925122",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership",
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index c113b80..e8ae618 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -6,6 +6,7 @@
import json
import frappe
import six
+import os
from datetime import datetime
from frappe.model.document import Document
from frappe.email import sendmail_to_system_managers
@@ -47,7 +48,7 @@
last_membership = erpnext.get_last_membership(self.member)
# if person applied for offline membership
- if last_membership and not frappe.session.user == "Administrator":
+ if last_membership and last_membership.name != self.name and not frappe.session.user == "Administrator":
# if last membership does not expire in 30 days, then do not allow to renew
if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) :
frappe.throw(_("You can only renew if your membership expires within 30 days"))
@@ -58,7 +59,7 @@
else:
self.from_date = nowdate()
- if frappe.db.get_single_value("Membership Settings", "billing_cycle") == "Yearly":
+ if frappe.db.get_single_value("Non Profit Settings", "billing_cycle") == "Yearly":
self.to_date = add_years(self.from_date, 1)
else:
self.to_date = add_months(self.from_date, 1)
@@ -68,11 +69,12 @@
return
self.load_from_db()
self.db_set("paid", 1)
- settings = frappe.get_doc("Membership Settings")
- if settings.enable_invoicing and settings.create_for_web_forms:
- self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True)
+ settings = frappe.get_doc("Non Profit Settings")
+ if settings.allow_invoicing and settings.automate_membership_invoicing:
+ self.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
+ @frappe.whitelist()
def generate_invoice(self, save=True, with_payment_entry=False):
if not (self.paid or self.currency or self.amount):
frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
@@ -85,10 +87,11 @@
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
plan = frappe.get_doc("Membership Type", self.membership_type)
- settings = frappe.get_doc("Membership Settings")
+ settings = frappe.get_doc("Non Profit Settings")
self.validate_membership_type_and_settings(plan, settings)
invoice = make_invoice(self, member, plan, settings)
+ self.reload()
self.invoice = invoice.name
if with_payment_entry:
@@ -102,7 +105,7 @@
def validate_membership_type_and_settings(self, plan, settings):
settings_link = get_link_to_form("Membership Type", self.membership_type)
- if not settings.debit_account:
+ if not settings.membership_debit_account:
frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))
if not settings.company:
@@ -113,25 +116,27 @@
get_link_to_form("Membership Type", self.membership_type)))
def make_payment_entry(self, settings, invoice):
- if not settings.payment_account:
- frappe.throw(_("You need to set <b>Payment Account</b> in {0}").format(
- get_link_to_form("Membership Type", self.membership_type)))
+ if not settings.membership_payment_account:
+ frappe.throw(_("You need to set <b>Payment Account</b> for Membership in {0}").format(
+ get_link_to_form("Non Profit Settings", "Non Profit Settings")))
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
frappe.flags.ignore_account_permission = True
pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
frappe.flags.ignore_account_permission=False
- pe.paid_to = settings.payment_account
+ pe.paid_to = settings.membership_payment_account
pe.reference_no = self.name
pe.reference_date = getdate()
- pe.save(ignore_permissions=True)
+ pe.flags.ignore_mandatory = True
+ pe.save()
pe.submit()
+ @frappe.whitelist()
def send_acknowlement(self):
- settings = frappe.get_doc("Membership Settings")
+ settings = frappe.get_doc("Non Profit Settings")
if not settings.send_email:
frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in {0}").format(
- get_link_to_form("Membership Settings", "Membership Settings")))
+ get_link_to_form("Non Profit Settings", "Non Profit Settings")))
member = frappe.get_doc("Member", self.member)
if not member.email_id:
@@ -170,7 +175,7 @@
invoice = frappe.get_doc({
"doctype": "Sales Invoice",
"customer": member.customer,
- "debit_to": settings.debit_account,
+ "debit_to": settings.membership_debit_account,
"currency": membership.currency,
"company": settings.company,
"is_pos": 0,
@@ -183,7 +188,7 @@
]
})
invoice.set_missing_values()
- invoice.insert(ignore_permissions=True)
+ invoice.insert()
invoice.submit()
frappe.msgprint(_("Sales Invoice created successfully"))
@@ -203,17 +208,18 @@
return None
-def verify_signature(data):
- if frappe.flags.in_test:
+def verify_signature(data, endpoint="Membership"):
+ if frappe.flags.in_test or os.environ.get("CI"):
return True
signature = frappe.request.headers.get("X-Razorpay-Signature")
- settings = frappe.get_doc("Membership Settings")
- key = settings.get_webhook_secret()
+ settings = frappe.get_doc("Non Profit Settings")
+ key = settings.get_webhook_secret(endpoint)
controller = frappe.get_doc("Razorpay Settings")
controller.verify_signature(data, signature, key)
+ frappe.set_user(settings.creation_user)
@frappe.whitelist(allow_guest=True)
@@ -222,7 +228,7 @@
try:
verify_signature(data)
except Exception as e:
- log = frappe.log_error(e, "Webhook Verification Error")
+ log = frappe.log_error(e, "Membership Webhook Verification Error")
notify_failure(log)
return { "status": "Failed", "reason": e}
@@ -250,16 +256,15 @@
member.subscription_id = subscription.id
member.customer_id = payment.customer_id
- if subscription.notes and type(subscription.notes) == dict:
- notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items())
- member.add_comment("Comment", notes)
- elif subscription.notes and type(subscription.notes) == str:
- member.add_comment("Comment", subscription.notes)
+ if subscription.get("notes"):
+ member = get_additional_notes(member, subscription)
+ company = get_company_for_memberships()
# Update Membership
membership = frappe.new_doc("Membership")
membership.update({
+ "company": company,
"member": member.name,
"membership_status": "Current",
"membership_type": member.membership_type,
@@ -270,15 +275,23 @@
"to_date": datetime.fromtimestamp(subscription.current_end),
"amount": payment.amount / 100 # Convert to rupees from paise
})
- membership.insert(ignore_permissions=True)
+ membership.flags.ignore_mandatory = True
+ membership.insert()
# Update membership values
member.subscription_start = datetime.fromtimestamp(subscription.start_at)
member.subscription_end = datetime.fromtimestamp(subscription.end_at)
member.subscription_activated = 1
- member.save(ignore_permissions=True)
+ member.flags.ignore_mandatory = True
+ member.save()
+
+ settings = frappe.get_doc("Non Profit Settings")
+ if settings.allow_invoicing and settings.automate_membership_invoicing:
+ membership.reload()
+ membership.generate_invoice(with_payment_entry=settings.automate_membership_payment_entries, save=True)
+
except Exception as e:
- message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id)
+ message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), _("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log)
return { "status": "Failed", "reason": e}
@@ -286,6 +299,39 @@
return { "status": "Success" }
+def get_company_for_memberships():
+ company = frappe.db.get_single_value("Non Profit Settings", "company")
+ if not company:
+ from erpnext.healthcare.setup import get_company
+ company = get_company()
+ return company
+
+
+def get_additional_notes(member, subscription):
+ if type(subscription.notes) == dict:
+ for k, v in subscription.notes.items():
+ notes = "\n".join("{}: {}".format(k, v))
+
+ # extract member name from notes
+ if "name" in k.lower():
+ member.update({
+ "member_name": subscription.notes.get(k)
+ })
+
+ # extract pan number from notes
+ if "pan" in k.lower():
+ member.update({
+ "pan_number": subscription.notes.get(k)
+ })
+
+ member.add_comment("Comment", notes)
+
+ elif type(subscription.notes) == str:
+ member.add_comment("Comment", subscription.notes)
+
+ return member
+
+
def notify_failure(log):
try:
content = """
diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py
index ff7e6c4..31da792 100644
--- a/erpnext/non_profit/doctype/membership/test_membership.py
+++ b/erpnext/non_profit/doctype/membership/test_membership.py
@@ -10,33 +10,7 @@
class TestMembership(unittest.TestCase):
def setUp(self):
- # Get default company
- company = frappe.get_doc("Company", erpnext.get_default_company())
-
- # update membership settings
- settings = frappe.get_doc("Membership Settings")
- # Enable razorpay
- settings.enable_razorpay = 1
- settings.billing_cycle = "Monthly"
- settings.billing_frequency = 24
- # Enable invoicing
- settings.enable_invoicing = 1
- settings.make_payment_entry = 1
- settings.company = company.name
- settings.payment_account = company.default_cash_account
- settings.debit_account = company.default_receivable_account
- settings.save()
-
- # make test plan
- if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
- plan = frappe.new_doc("Membership Type")
- plan.membership_type = "_rzpy_test_milythm"
- plan.amount = 100
- plan.razorpay_plan_id = "_rzpy_test_milythm"
- plan.linked_item = create_item("_Test Item for Non Profit Membership").name
- plan.insert()
- else:
- plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
+ plan = setup_membership()
# make test member
self.member_doc = create_member(frappe._dict({
@@ -78,7 +52,7 @@
})
def set_config(key, value):
- frappe.db.set_value("Membership Settings", None, key, value)
+ frappe.db.set_value("Non Profit Settings", None, key, value)
def make_membership(member, payload={}):
data = {
@@ -109,3 +83,36 @@
else:
item = frappe.get_doc("Item", item_code)
return item
+
+def setup_membership():
+ # Get default company
+ company = frappe.get_doc("Company", erpnext.get_default_company())
+
+ # update non profit settings
+ settings = frappe.get_doc("Non Profit Settings")
+ # Enable razorpay
+ settings.enable_razorpay_for_memberships = 1
+ settings.billing_cycle = "Monthly"
+ settings.billing_frequency = 24
+ # Enable invoicing
+ settings.allow_invoicing = 1
+ settings.automate_membership_payment_entries = 1
+ settings.company = company.name
+ settings.donation_company = company.name
+ settings.membership_payment_account = company.default_cash_account
+ settings.membership_debit_account = company.default_receivable_account
+ settings.flags.ignore_mandatory = True
+ settings.save()
+
+ # make test plan
+ if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
+ plan = frappe.new_doc("Membership Type")
+ plan.membership_type = "_rzpy_test_milythm"
+ plan.amount = 100
+ plan.razorpay_plan_id = "_rzpy_test_milythm"
+ plan.linked_item = create_item("_Test Item for Non Profit Membership").name
+ plan.insert()
+ else:
+ plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
+
+ return plan
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js
deleted file mode 100644
index c95aab2..0000000
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js
+++ /dev/null
@@ -1,81 +0,0 @@
-// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on("Membership Settings", {
- refresh: function(frm) {
- if (frm.doc.webhook_secret) {
- frm.add_custom_button(__("Revoke <Key></Key>"), () => {
- frm.call("revoke_key").then(() => {
- frm.refresh();
- })
- });
- }
-
- frm.set_query("inv_print_format", function() {
- return {
- filters: {
- "doc_type": "Sales Invoice"
- }
- };
- });
-
- frm.set_query("membership_print_format", function() {
- return {
- filters: {
- "doc_type": "Membership"
- }
- };
- });
-
- frm.set_query("debit_account", function() {
- return {
- filters: {
- "account_type": "Receivable",
- "is_group": 0,
- "company": frm.doc.company
- }
- };
- });
-
- frm.set_query("payment_account", function () {
- var account_types = ["Bank", "Cash"];
- return {
- filters: {
- "account_type": ["in", account_types],
- "is_group": 0,
- "company": frm.doc.company
- }
- };
- });
-
- let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership";
-
- frm.set_intro(__("You can learn more about memberships in the manual. ") + `<a href='${docs_url}'>${__('ERPNext Docs')}</a>`, true);
-
- frm.trigger("add_generate_button");
- frm.trigger("add_copy_buttonn");
- },
-
- add_generate_button: function(frm) {
- let label;
-
- if (frm.doc.webhook_secret) {
- label = __("Regenerate Webhook Secret");
- } else {
- label = __("Generate Webhook Secret");
- }
- frm.add_custom_button(label, () => {
- frm.call("generate_webhook_key").then(() => {
- frm.refresh();
- });
- });
- },
-
- add_copy_buttonn: function(frm) {
- if (frm.doc.webhook_secret) {
- frm.add_custom_button(__("Copy Webhook URL"), () => {
- frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`);
- });
- }
- }
-});
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
deleted file mode 100644
index 3887b0a..0000000
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json
+++ /dev/null
@@ -1,192 +0,0 @@
-{
- "actions": [],
- "creation": "2020-03-29 12:57:03.005120",
- "doctype": "DocType",
- "editable_grid": 1,
- "engine": "InnoDB",
- "field_order": [
- "enable_razorpay",
- "razorpay_settings_section",
- "billing_cycle",
- "billing_frequency",
- "webhook_secret",
- "column_break_6",
- "enable_invoicing",
- "create_for_web_forms",
- "make_payment_entry",
- "company",
- "debit_account",
- "payment_account",
- "column_break_9",
- "send_email",
- "send_invoice",
- "membership_print_format",
- "inv_print_format",
- "email_template"
- ],
- "fields": [
- {
- "fieldname": "billing_cycle",
- "fieldtype": "Select",
- "label": "Billing Cycle",
- "options": "Monthly\nYearly"
- },
- {
- "default": "0",
- "fieldname": "enable_razorpay",
- "fieldtype": "Check",
- "label": "Enable RazorPay For Memberships"
- },
- {
- "depends_on": "eval:doc.enable_razorpay",
- "fieldname": "razorpay_settings_section",
- "fieldtype": "Section Break",
- "label": "RazorPay Settings"
- },
- {
- "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
- "fieldname": "billing_frequency",
- "fieldtype": "Int",
- "label": "Billing Frequency"
- },
- {
- "fieldname": "webhook_secret",
- "fieldtype": "Password",
- "label": "Webhook Secret",
- "read_only": 1
- },
- {
- "fieldname": "column_break_6",
- "fieldtype": "Section Break",
- "label": "Invoicing"
- },
- {
- "depends_on": "eval:doc.enable_invoicing",
- "fieldname": "debit_account",
- "fieldtype": "Link",
- "label": "Debit Account",
- "mandatory_depends_on": "eval:doc.enable_auto_invoicing",
- "options": "Account"
- },
- {
- "fieldname": "column_break_9",
- "fieldtype": "Column Break"
- },
- {
- "depends_on": "eval:doc.enable_invoicing",
- "fieldname": "company",
- "fieldtype": "Link",
- "label": "Company",
- "mandatory_depends_on": "eval:doc.enable_auto_invoicing",
- "options": "Company"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.enable_invoicing && doc.send_email",
- "fieldname": "send_invoice",
- "fieldtype": "Check",
- "label": "Send Invoice with Email"
- },
- {
- "default": "0",
- "fieldname": "send_email",
- "fieldtype": "Check",
- "label": "Send Membership Acknowledgement"
- },
- {
- "depends_on": "eval: doc.send_invoice",
- "fieldname": "inv_print_format",
- "fieldtype": "Link",
- "label": "Invoice Print Format",
- "mandatory_depends_on": "eval: doc.send_invoice",
- "options": "Print Format"
- },
- {
- "depends_on": "eval:doc.send_email",
- "fieldname": "membership_print_format",
- "fieldtype": "Link",
- "label": "Membership Print Format",
- "options": "Print Format"
- },
- {
- "depends_on": "eval:doc.send_email",
- "fieldname": "email_template",
- "fieldtype": "Link",
- "label": "Email Template",
- "mandatory_depends_on": "eval:doc.send_email",
- "options": "Email Template"
- },
- {
- "default": "0",
- "fieldname": "enable_invoicing",
- "fieldtype": "Check",
- "label": "Enable Invoicing",
- "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.enable_invoicing",
- "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
- "fieldname": "make_payment_entry",
- "fieldtype": "Check",
- "label": "Make Payment Entry"
- },
- {
- "depends_on": "eval:doc.make_payment_entry",
- "fieldname": "payment_account",
- "fieldtype": "Link",
- "label": "Payment To",
- "mandatory_depends_on": "eval:doc.make_payment_entry",
- "options": "Account"
- },
- {
- "default": "0",
- "depends_on": "eval:doc.enable_invoicing",
- "description": "Automatically create an invoice when payment is authorized from a web form entry",
- "fieldname": "create_for_web_forms",
- "fieldtype": "Check",
- "label": "Auto Create Invoice for Web Forms"
- }
- ],
- "index_web_pages_for_search": 1,
- "issingle": 1,
- "links": [],
- "modified": "2021-01-21 19:57:53.213286",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Membership Settings",
- "owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- },
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Non Profit Manager",
- "share": 1,
- "write": 1
- },
- {
- "email": 1,
- "print": 1,
- "read": 1,
- "role": "Non Profit Member",
- "share": 1
- }
- ],
- "quick_entry": 1,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.py b/erpnext/non_profit/doctype/membership_settings/membership_settings.py
deleted file mode 100644
index f3b2eee..0000000
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.py
+++ /dev/null
@@ -1,33 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-from frappe.integrations.utils import get_payment_gateway_controller
-from frappe.model.document import Document
-
-class MembershipSettings(Document):
- def generate_webhook_key(self):
- key = frappe.generate_hash(length=20)
- self.webhook_secret = key
- self.save()
-
- frappe.msgprint(
- _("Here is your webhook secret, this will be shown to you only once.") + "<br><br>" + key,
- _("Webhook Secret")
- );
-
- def revoke_key(self):
- self.webhook_secret = None;
- self.save()
-
- def get_webhook_secret(self):
- return self.get_password(fieldname="webhook_secret", raise_exception=False)
-
-@frappe.whitelist()
-def get_plans_for_membership(*args, **kwargs):
- controller = get_payment_gateway_controller("Razorpay")
- plans = controller.get_plans()
- return [plan.get("item") for plan in plans.get("items")]
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js
index 91a5cb7..2f24276 100644
--- a/erpnext/non_profit/doctype/membership_type/membership_type.js
+++ b/erpnext/non_profit/doctype/membership_type/membership_type.js
@@ -3,11 +3,11 @@
frappe.ui.form.on('Membership Type', {
refresh: function (frm) {
- frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => {
+ frappe.db.get_single_value('Non Profit Settings', 'enable_razorpay_for_memberships').then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
});
- frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => {
+ frappe.db.get_single_value('Non Profit Settings', 'allow_invoicing').then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false);
});
diff --git a/erpnext/non_profit/doctype/membership_settings/__init__.py b/erpnext/non_profit/doctype/non_profit_settings/__init__.py
similarity index 100%
rename from erpnext/non_profit/doctype/membership_settings/__init__.py
rename to erpnext/non_profit/doctype/non_profit_settings/__init__.py
diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
new file mode 100644
index 0000000..4c4ca98
--- /dev/null
+++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.js
@@ -0,0 +1,133 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Non Profit Settings", {
+ refresh: function(frm) {
+ frm.set_query("inv_print_format", function() {
+ return {
+ filters: {
+ "doc_type": "Sales Invoice"
+ }
+ };
+ });
+
+ frm.set_query("membership_print_format", function() {
+ return {
+ filters: {
+ "doc_type": "Membership"
+ }
+ };
+ });
+
+ frm.set_query("membership_debit_account", function() {
+ return {
+ filters: {
+ "account_type": "Receivable",
+ "is_group": 0,
+ "company": frm.doc.company
+ }
+ };
+ });
+
+ frm.set_query("donation_debit_account", function() {
+ return {
+ filters: {
+ "account_type": "Receivable",
+ "is_group": 0,
+ "company": frm.doc.donation_company
+ }
+ };
+ });
+
+ frm.set_query("membership_payment_account", function () {
+ var account_types = ["Bank", "Cash"];
+ return {
+ filters: {
+ "account_type": ["in", account_types],
+ "is_group": 0,
+ "company": frm.doc.company
+ }
+ };
+ });
+
+ frm.set_query("donation_payment_account", function () {
+ var account_types = ["Bank", "Cash"];
+ return {
+ filters: {
+ "account_type": ["in", account_types],
+ "is_group": 0,
+ "company": frm.doc.donation_company
+ }
+ };
+ });
+
+ let docs_url = "https://docs.erpnext.com/docs/user/manual/en/non_profit/membership";
+
+ frm.set_intro(__("You can learn more about memberships in the manual. ") + `<a href='${docs_url}'>${__('ERPNext Docs')}</a>`, true);
+ frm.trigger("setup_buttons_for_membership");
+ frm.trigger("setup_buttons_for_donation");
+ },
+
+ setup_buttons_for_membership: function(frm) {
+ let label;
+
+ if (frm.doc.membership_webhook_secret) {
+
+ frm.add_custom_button(__("Copy Webhook URL"), () => {
+ frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.membership.membership.trigger_razorpay_subscription`);
+ }, __("Memberships"));
+
+ frm.add_custom_button(__("Revoke Key"), () => {
+ frm.call("revoke_key", {
+ key: "membership_webhook_secret"
+ }).then(() => {
+ frm.refresh();
+ });
+ }, __("Memberships"));
+
+ label = __("Regenerate Webhook Secret");
+
+ } else {
+ label = __("Generate Webhook Secret");
+ }
+
+ frm.add_custom_button(label, () => {
+ frm.call("generate_webhook_secret", {
+ field: "membership_webhook_secret"
+ }).then(() => {
+ frm.refresh();
+ });
+ }, __("Memberships"));
+ },
+
+ setup_buttons_for_donation: function(frm) {
+ let label;
+
+ if (frm.doc.donation_webhook_secret) {
+ label = __("Regenerate Webhook Secret");
+
+ frm.add_custom_button(__("Copy Webhook URL"), () => {
+ frappe.utils.copy_to_clipboard(`https://${frappe.boot.sitename}/api/method/erpnext.non_profit.doctype.donation.donation.capture_razorpay_donations`);
+ }, __("Donations"));
+
+ frm.add_custom_button(__("Revoke Key"), () => {
+ frm.call("revoke_key", {
+ key: "donation_webhook_secret"
+ }).then(() => {
+ frm.refresh();
+ });
+ }, __("Donations"));
+
+ } else {
+ label = __("Generate Webhook Secret");
+ }
+
+ frm.add_custom_button(label, () => {
+ frm.call("generate_webhook_secret", {
+ field: "donation_webhook_secret"
+ }).then(() => {
+ frm.refresh();
+ });
+ }, __("Donations"));
+ }
+});
diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json
new file mode 100644
index 0000000..25ff0c1
--- /dev/null
+++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.json
@@ -0,0 +1,273 @@
+{
+ "actions": [],
+ "creation": "2020-03-29 12:57:03.005120",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "enable_razorpay_for_memberships",
+ "razorpay_settings_section",
+ "billing_cycle",
+ "billing_frequency",
+ "membership_webhook_secret",
+ "column_break_6",
+ "allow_invoicing",
+ "automate_membership_invoicing",
+ "automate_membership_payment_entries",
+ "company",
+ "membership_debit_account",
+ "membership_payment_account",
+ "column_break_9",
+ "send_email",
+ "send_invoice",
+ "membership_print_format",
+ "inv_print_format",
+ "email_template",
+ "donation_settings_section",
+ "donation_company",
+ "default_donor_type",
+ "donation_webhook_secret",
+ "column_break_22",
+ "automate_donation_payment_entries",
+ "donation_debit_account",
+ "donation_payment_account",
+ "section_break_27",
+ "creation_user"
+ ],
+ "fields": [
+ {
+ "fieldname": "billing_cycle",
+ "fieldtype": "Select",
+ "label": "Billing Cycle",
+ "options": "Monthly\nYearly"
+ },
+ {
+ "depends_on": "eval:doc.enable_razorpay_for_memberships",
+ "fieldname": "razorpay_settings_section",
+ "fieldtype": "Section Break",
+ "label": "RazorPay Settings for Memberships"
+ },
+ {
+ "description": "The number of billing cycles for which the customer should be charged. For example, if a customer is buying a 1-year membership that should be billed on a monthly basis, this value should be 12.",
+ "fieldname": "billing_frequency",
+ "fieldtype": "Int",
+ "label": "Billing Frequency"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Section Break",
+ "label": "Membership Invoicing"
+ },
+ {
+ "fieldname": "column_break_9",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "This company will be set for the Memberships created via webhook.",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.allow_invoicing && doc.send_email",
+ "fieldname": "send_invoice",
+ "fieldtype": "Check",
+ "label": "Send Invoice with Email"
+ },
+ {
+ "default": "0",
+ "fieldname": "send_email",
+ "fieldtype": "Check",
+ "label": "Send Membership Acknowledgement"
+ },
+ {
+ "depends_on": "eval: doc.send_invoice",
+ "fieldname": "inv_print_format",
+ "fieldtype": "Link",
+ "label": "Invoice Print Format",
+ "mandatory_depends_on": "eval: doc.send_invoice",
+ "options": "Print Format"
+ },
+ {
+ "depends_on": "eval:doc.send_email",
+ "fieldname": "membership_print_format",
+ "fieldtype": "Link",
+ "label": "Membership Print Format",
+ "options": "Print Format"
+ },
+ {
+ "depends_on": "eval:doc.send_email",
+ "fieldname": "email_template",
+ "fieldtype": "Link",
+ "label": "Email Template",
+ "mandatory_depends_on": "eval:doc.send_email",
+ "options": "Email Template"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_invoicing",
+ "fieldtype": "Check",
+ "label": "Allow Invoicing for Memberships",
+ "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.allow_invoicing",
+ "description": "Automatically create an invoice when payment is authorized from a web form entry",
+ "fieldname": "automate_membership_invoicing",
+ "fieldtype": "Check",
+ "label": "Automate Invoicing for Web Forms"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.allow_invoicing",
+ "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
+ "fieldname": "automate_membership_payment_entries",
+ "fieldtype": "Check",
+ "label": "Automate Payment Entry Creation"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_razorpay_for_memberships",
+ "fieldtype": "Check",
+ "label": "Enable RazorPay For Memberships"
+ },
+ {
+ "depends_on": "eval:doc.automate_membership_payment_entries",
+ "description": "Account for accepting membership payments",
+ "fieldname": "membership_payment_account",
+ "fieldtype": "Link",
+ "label": "Membership Payment To",
+ "mandatory_depends_on": "eval:doc.automate_membership_payment_entries",
+ "options": "Account"
+ },
+ {
+ "fieldname": "membership_webhook_secret",
+ "fieldtype": "Password",
+ "label": "Membership Webhook Secret",
+ "read_only": 1
+ },
+ {
+ "fieldname": "donation_webhook_secret",
+ "fieldtype": "Password",
+ "label": "Donation Webhook Secret",
+ "read_only": 1
+ },
+ {
+ "depends_on": "automate_donation_payment_entries",
+ "description": "Account for accepting donation payments",
+ "fieldname": "donation_payment_account",
+ "fieldtype": "Link",
+ "label": "Donation Payment To",
+ "mandatory_depends_on": "automate_donation_payment_entries",
+ "options": "Account"
+ },
+ {
+ "default": "0",
+ "description": "Auto creates Payment Entry for Donations created from web forms.",
+ "fieldname": "automate_donation_payment_entries",
+ "fieldtype": "Check",
+ "label": "Automate Donation Payment Entries"
+ },
+ {
+ "depends_on": "eval:doc.allow_invoicing",
+ "fieldname": "membership_debit_account",
+ "fieldtype": "Link",
+ "label": "Debit Account",
+ "mandatory_depends_on": "eval:doc.allow_invoicing",
+ "options": "Account"
+ },
+ {
+ "depends_on": "automate_donation_payment_entries",
+ "fieldname": "donation_debit_account",
+ "fieldtype": "Link",
+ "label": "Debit Account",
+ "mandatory_depends_on": "automate_donation_payment_entries",
+ "options": "Account"
+ },
+ {
+ "description": "This company will be set for the Donations created via webhook.",
+ "fieldname": "donation_company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "donation_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Donation Settings"
+ },
+ {
+ "fieldname": "column_break_22",
+ "fieldtype": "Column Break"
+ },
+ {
+ "description": "This Donor Type will be set for the Donor created via Donation web form entry.",
+ "fieldname": "default_donor_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Default Donor Type",
+ "options": "Donor Type",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_27",
+ "fieldtype": "Section Break"
+ },
+ {
+ "description": "The user that will be used to create Donations, Memberships, Invoices, and Payment Entries. This user should have the relevant permissions.",
+ "fieldname": "creation_user",
+ "fieldtype": "Link",
+ "label": "Creation User",
+ "options": "User",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-03-11 10:43:38.124240",
+ "modified_by": "Administrator",
+ "module": "Non Profit",
+ "name": "Non Profit Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Non Profit Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "Non Profit Member",
+ "share": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py
new file mode 100644
index 0000000..a84cc2c
--- /dev/null
+++ b/erpnext/non_profit/doctype/non_profit_settings/non_profit_settings.py
@@ -0,0 +1,38 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.integrations.utils import get_payment_gateway_controller
+from frappe.model.document import Document
+
+class NonProfitSettings(Document):
+ @frappe.whitelist()
+ def generate_webhook_secret(self, field="membership_webhook_secret"):
+ key = frappe.generate_hash(length=20)
+ self.set(field, key)
+ self.save()
+
+ secret_for = "Membership" if field == "membership_webhook_secret" else "Donation"
+
+ frappe.msgprint(
+ _("Here is your webhook secret for {0} API, this will be shown to you only once.").format(secret_for) + "<br><br>" + key,
+ _("Webhook Secret")
+ )
+
+ @frappe.whitelist()
+ def revoke_key(self, key):
+ self.set(key, None)
+ self.save()
+
+ def get_webhook_secret(self, endpoint="Membership"):
+ fieldname = "membership_webhook_secret" if endpoint == "Membership" else "donation_webhook_secret"
+ return self.get_password(fieldname=fieldname, raise_exception=False)
+
+@frappe.whitelist()
+def get_plans_for_membership(*args, **kwargs):
+ controller = get_payment_gateway_controller("Razorpay")
+ plans = controller.get_plans()
+ return [plan.get("item") for plan in plans.get("items")]
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py b/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
similarity index 79%
rename from erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
rename to erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
index 2ad7984..3f0ede3 100644
--- a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
+++ b/erpnext/non_profit/doctype/non_profit_settings/test_non_profit_settings.py
@@ -6,5 +6,5 @@
# import frappe
import unittest
-class TestMembershipSettings(unittest.TestCase):
+class TestNonProfitSettings(unittest.TestCase):
pass
diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json
index da2a514..2557d77 100644
--- a/erpnext/non_profit/workspace/non_profit/non_profit.json
+++ b/erpnext/non_profit/workspace/non_profit/non_profit.json
@@ -10,6 +10,7 @@
"hide_custom": 0,
"icon": "non-profit",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Non Profit",
"links": [
@@ -109,7 +110,7 @@
"hidden": 0,
"is_query_report": 0,
"label": "Membership Settings",
- "link_to": "Membership Settings",
+ "link_to": "Non Profit Settings",
"link_type": "DocType",
"onboard": 0,
"type": "Link"
@@ -161,7 +162,7 @@
{
"hidden": 0,
"is_query_report": 0,
- "label": "Donor",
+ "label": "Donation",
"onboard": 0,
"type": "Card Break"
},
@@ -184,9 +185,35 @@
"link_type": "DocType",
"onboard": 0,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Donation",
+ "link_to": "Donation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tax Exemption Certification (India)",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tax Exemption 80G Certificate",
+ "link_to": "Tax Exemption 80G Certificate",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
}
],
- "modified": "2020-12-01 13:38:38.351409",
+ "modified": "2021-03-11 11:38:09.140655",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Non Profit",
@@ -201,8 +228,8 @@
"type": "DocType"
},
{
- "label": "Membership Settings",
- "link_to": "Membership Settings",
+ "label": "Non Profit Settings",
+ "link_to": "Non Profit Settings",
"type": "DocType"
},
{
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index dcccbc5..487400e 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -99,7 +99,7 @@
execute:frappe.delete_doc("DocType", "Purchase Request Item")
erpnext.patches.v4_2.recalculate_bom_cost
erpnext.patches.v4_2.fix_gl_entries_for_stock_transactions
-erpnext.patches.v4_2.update_requested_and_ordered_qty
+erpnext.patches.v4_2.update_requested_and_ordered_qty #2021-03-31
execute:frappe.rename_doc("DocType", "Support Ticket", "Issue", force=True)
erpnext.patches.v4_4.make_email_accounts
execute:frappe.delete_doc("DocType", "Contact Control")
@@ -208,7 +208,7 @@
erpnext.patches.v5_7.item_template_attributes
execute:frappe.delete_doc_if_exists("DocType", "Manage Variants")
execute:frappe.delete_doc_if_exists("DocType", "Manage Variants Item")
-erpnext.patches.v4_2.repost_reserved_qty #2016-04-15
+erpnext.patches.v4_2.repost_reserved_qty #2021-03-31
erpnext.patches.v5_4.update_purchase_cost_against_project
erpnext.patches.v5_8.update_order_reference_in_return_entries
erpnext.patches.v5_8.add_credit_note_print_heading
@@ -720,7 +720,7 @@
erpnext.patches.v12_0.update_item_tax_template_company
erpnext.patches.v13_0.move_branch_code_to_bank_account
erpnext.patches.v13_0.healthcare_lab_module_rename_doctypes
-erpnext.patches.v13_0.add_standard_navbar_items #4
+erpnext.patches.v13_0.add_standard_navbar_items #2021-03-24
erpnext.patches.v13_0.stock_entry_enhancements
erpnext.patches.v12_0.update_state_code_for_daman_and_diu
erpnext.patches.v12_0.rename_lost_reason_detail
@@ -752,6 +752,16 @@
erpnext.patches.v13_0.convert_qi_parameter_to_link_field
erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes
erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021
+erpnext.patches.v13_0.update_payment_terms_outstanding
erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
+erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
+erpnext.patches.v13_0.update_vehicle_no_reqd_condition
+erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
+erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
+erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
+erpnext.patches.v13_0.setup_uae_vat_fields
+execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext')
+erpnext.patches.v13_0.rename_discharge_date_in_ip_record
+erpnext.patches.v12_0.purchase_receipt_status
erpnext.patches.v13_0.update_recipient_email_digest
diff --git a/erpnext/patches/v11_0/refactor_autoname_naming.py b/erpnext/patches/v11_0/refactor_autoname_naming.py
index 5dc5d3b..b997ba2 100644
--- a/erpnext/patches/v11_0/refactor_autoname_naming.py
+++ b/erpnext/patches/v11_0/refactor_autoname_naming.py
@@ -20,7 +20,7 @@
'Certified Consultant': 'NPO-CONS-.YYYY.-.#####',
'Chat Room': 'CHAT-ROOM-.#####',
'Compensatory Leave Request': 'HR-CMP-.YY.-.MM.-.#####',
- 'Custom Script': 'SYS-SCR-.#####',
+ 'Client Script': 'SYS-SCR-.#####',
'Employee Benefit Application': 'HR-BEN-APP-.YY.-.MM.-.#####',
'Employee Benefit Application Detail': '',
'Employee Benefit Claim': 'HR-BEN-CLM-.YY.-.MM.-.#####',
diff --git a/erpnext/patches/v11_1/update_bank_transaction_status.py b/erpnext/patches/v11_1/update_bank_transaction_status.py
index 1acdfcc..544bc5e 100644
--- a/erpnext/patches/v11_1/update_bank_transaction_status.py
+++ b/erpnext/patches/v11_1/update_bank_transaction_status.py
@@ -7,9 +7,20 @@
def execute():
frappe.reload_doc("accounts", "doctype", "bank_transaction")
- frappe.db.sql(""" UPDATE `tabBank Transaction`
- SET status = 'Reconciled'
- WHERE
- status = 'Settled' and (debit = allocated_amount or credit = allocated_amount)
- and ifnull(allocated_amount, 0) > 0
- """)
\ No newline at end of file
+ bank_transaction_fields = frappe.get_meta("Bank Transaction").get_valid_columns()
+
+ if 'debit' in bank_transaction_fields:
+ frappe.db.sql(""" UPDATE `tabBank Transaction`
+ SET status = 'Reconciled'
+ WHERE
+ status = 'Settled' and (debit = allocated_amount or credit = allocated_amount)
+ and ifnull(allocated_amount, 0) > 0
+ """)
+
+ elif 'deposit' in bank_transaction_fields:
+ frappe.db.sql(""" UPDATE `tabBank Transaction`
+ SET status = 'Reconciled'
+ WHERE
+ status = 'Settled' and (deposit = allocated_amount or withdrawal = allocated_amount)
+ and ifnull(allocated_amount, 0) > 0
+ """)
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/add_state_code_for_ladakh.py b/erpnext/patches/v12_0/add_state_code_for_ladakh.py
index d41101c..29a7b4b 100644
--- a/erpnext/patches/v12_0/add_state_code_for_ladakh.py
+++ b/erpnext/patches/v12_0/add_state_code_for_ladakh.py
@@ -11,6 +11,7 @@
# Update options in gst_state custom fields
for field in custom_fields:
- gst_state_field = frappe.get_doc('Custom Field', field)
- gst_state_field.options = '\n'.join(states)
- gst_state_field.save()
+ if frappe.db.exists('Custom Field', field):
+ gst_state_field = frappe.get_doc('Custom Field', field)
+ gst_state_field.options = '\n'.join(states)
+ gst_state_field.save()
diff --git a/erpnext/patches/v12_0/purchase_receipt_status.py b/erpnext/patches/v12_0/purchase_receipt_status.py
new file mode 100644
index 0000000..1a99b31
--- /dev/null
+++ b/erpnext/patches/v12_0/purchase_receipt_status.py
@@ -0,0 +1,30 @@
+""" This patch fixes old purchase receipts (PR) where even after submitting
+ the PR, the `status` remains "Draft". `per_billed` field was copied over from previous
+ doc (PO), hence it is recalculated for setting new correct status of PR.
+"""
+
+import frappe
+
+logger = frappe.logger("patch", allow_site=True, file_count=50)
+
+def execute():
+ affected_purchase_receipts = frappe.db.sql(
+ """select name from `tabPurchase Receipt`
+ where status = 'Draft' and per_billed = 100 and docstatus = 1"""
+ )
+
+ if not affected_purchase_receipts:
+ return
+
+ logger.info("purchase_receipt_status: begin patch, PR count: {}"
+ .format(len(affected_purchase_receipts)))
+
+
+ for pr in affected_purchase_receipts:
+ pr_name = pr[0]
+ logger.info("purchase_receipt_status: patching PR - {}".format(pr_name))
+
+ pr_doc = frappe.get_doc("Purchase Receipt", pr_name)
+
+ pr_doc.update_billing_status(update_modified=False)
+ pr_doc.set_status(update=True, update_modified=False)
diff --git a/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py
new file mode 100644
index 0000000..af1f6e7
--- /dev/null
+++ b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ doctypes = [
+ "Bank Statement Settings",
+ "Bank Statement Settings Item",
+ "Bank Statement Transaction Entry",
+ "Bank Statement Transaction Invoice Item",
+ "Bank Statement Transaction Payment Item",
+ "Bank Statement Transaction Settings Item",
+ "Bank Statement Transaction Settings",
+ ]
+
+ for doctype in doctypes:
+ frappe.delete_doc("DocType", doctype, force=1)
+
+ frappe.delete_doc("Page", "bank-reconciliation", force=1)
+
+ rename_field("Bank Transaction", "debit", "deposit")
+ rename_field("Bank Transaction", "credit", "withdrawal")
diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
index f60e0d3..d968e1f 100644
--- a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
+++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
@@ -1,14 +1,41 @@
import frappe
from frappe import _
+from frappe.utils import getdate, get_time, today
from erpnext.stock.stock_ledger import update_entries_after
from erpnext.accounts.utils import update_gl_entries_after
def execute():
- data = frappe.db.sql(''' SELECT name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time
- from `tabStock Ledger Entry` where creation > '2020-12-26 12:58:55.903836' and is_cancelled = 0
- order by timestamp(posting_date, posting_time) asc, creation asc''', as_dict=1)
+ for doctype in ('repost_item_valuation', 'stock_entry_detail', 'purchase_receipt_item',
+ 'purchase_invoice_item', 'delivery_note_item', 'sales_invoice_item', 'packed_item'):
+ frappe.reload_doc('stock', 'doctype', doctype)
+ frappe.reload_doc('buying', 'doctype', 'purchase_receipt_item_supplied')
- for index, d in enumerate(data):
+ reposting_project_deployed_on = get_creation_time()
+ posting_date = getdate(reposting_project_deployed_on)
+ posting_time = get_time(reposting_project_deployed_on)
+
+ if posting_date == today():
+ return
+
+ frappe.clear_cache()
+ frappe.flags.warehouse_account_map = {}
+
+ data = frappe.db.sql('''
+ SELECT
+ name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time
+ FROM
+ `tabStock Ledger Entry`
+ WHERE
+ creation > %s
+ and is_cancelled = 0
+ ORDER BY timestamp(posting_date, posting_time) asc, creation asc
+ ''', reposting_project_deployed_on, as_dict=1)
+
+ frappe.db.auto_commit_on_many_writes = 1
+ print("Reposting Stock Ledger Entries...")
+ total_sle = len(data)
+ i = 0
+ for d in data:
update_entries_after({
"item_code": d.item_code,
"warehouse": d.warehouse,
@@ -19,9 +46,18 @@
"sle_id": d.name
}, allow_negative_stock=True)
- frappe.db.auto_commit_on_many_writes = 1
+ i += 1
+ if i%100 == 0:
+ print(i, "/", total_sle)
+
+
+ print("Reposting General Ledger Entries...")
for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
- update_gl_entries_after('2020-12-25', '01:58:55', company=row.name)
+ update_gl_entries_after(posting_date, posting_time, company=row.name)
- frappe.db.auto_commit_on_many_writes = 0
\ No newline at end of file
+ frappe.db.auto_commit_on_many_writes = 0
+
+def get_creation_time():
+ return frappe.db.sql(''' SELECT create_time FROM
+ INFORMATION_SCHEMA.TABLES where TABLE_NAME = "tabRepost Item Valuation" ''', as_list=1)[0][0]
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py
new file mode 100644
index 0000000..491dc82
--- /dev/null
+++ b/erpnext/patches/v13_0/rename_discharge_date_in_ip_record.py
@@ -0,0 +1,8 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ frappe.reload_doc("Healthcare", "doctype", "Inpatient Record")
+ if frappe.db.has_column("Inpatient Record", "discharge_date"):
+ rename_field("Inpatient Record", "discharge_date", "discharge_datetime")
diff --git a/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py
new file mode 100644
index 0000000..3fa09a7
--- /dev/null
+++ b/erpnext/patches/v13_0/rename_membership_settings_to_non_profit_settings.py
@@ -0,0 +1,22 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ if frappe.db.table_exists("Membership Settings"):
+ frappe.rename_doc("DocType", "Membership Settings", "Non Profit Settings")
+ frappe.reload_doctype("Non Profit Settings", force=True)
+
+ if frappe.db.table_exists("Non Profit Settings"):
+ rename_fields_map = {
+ "enable_invoicing": "allow_invoicing",
+ "create_for_web_forms": "automate_membership_invoicing",
+ "make_payment_entry": "automate_membership_payment_entries",
+ "enable_razorpay": "enable_razorpay_for_memberships",
+ "debit_account": "membership_debit_account",
+ "payment_account": "membership_payment_account",
+ "webhook_secret": "membership_webhook_secret"
+ }
+
+ for old_name, new_name in rename_fields_map.items():
+ rename_field("Non Profit Settings", old_name, new_name)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
new file mode 100644
index 0000000..833c355
--- /dev/null
+++ b/erpnext/patches/v13_0/setup_fields_for_80g_certificate_and_donation.py
@@ -0,0 +1,13 @@
+import frappe
+from erpnext.regional.india.setup import make_custom_fields
+
+def execute():
+ if frappe.get_all('Company', filters = {'country': 'India'}):
+ make_custom_fields()
+
+ if not frappe.db.exists('Party Type', 'Donor'):
+ frappe.get_doc({
+ 'doctype': 'Party Type',
+ 'party_type': 'Donor',
+ 'account_type': 'Receivable'
+ }).insert(ignore_permissions=True)
diff --git a/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py
new file mode 100644
index 0000000..01fd6a1
--- /dev/null
+++ b/erpnext/patches/v13_0/setup_gratuity_rule_for_india_and_uae.py
@@ -0,0 +1,16 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc('payroll', 'doctype', 'gratuity_rule')
+ frappe.reload_doc('payroll', 'doctype', 'gratuity_rule_slab')
+ frappe.reload_doc('payroll', 'doctype', 'gratuity_applicable_component')
+ if frappe.db.exists("Company", {"country": "India"}):
+ from erpnext.regional.india.setup import create_gratuity_rule
+ create_gratuity_rule()
+ if frappe.db.exists("Company", {"country": "United Arab Emirates"}):
+ from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
+ create_gratuity_rule()
diff --git a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
index de08aa2..2d3b096 100644
--- a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
+++ b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
@@ -6,6 +6,8 @@
if "Healthcare" not in frappe.get_active_domains():
return
+ frappe.reload_doc("healthcare", "doctype", "Therapy Session")
+ frappe.reload_doc("healthcare", "doctype", "Inpatient Medication Order")
frappe.reload_doc("healthcare", "doctype", "Patient History Settings")
frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type")
frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type")
diff --git a/erpnext/patches/v13_0/setup_uae_vat_fields.py b/erpnext/patches/v13_0/setup_uae_vat_fields.py
new file mode 100644
index 0000000..d7a5c68
--- /dev/null
+++ b/erpnext/patches/v13_0/setup_uae_vat_fields.py
@@ -0,0 +1,12 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from erpnext.regional.united_arab_emirates.setup import setup
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'United Arab Emirates'})
+ if not company:
+ return
+
+ setup()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_payment_terms_outstanding.py b/erpnext/patches/v13_0/update_payment_terms_outstanding.py
new file mode 100644
index 0000000..4816b40
--- /dev/null
+++ b/erpnext/patches/v13_0/update_payment_terms_outstanding.py
@@ -0,0 +1,15 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc("accounts", "doctype", "Payment Schedule")
+ if frappe.db.count('Payment Schedule'):
+ frappe.db.sql('''
+ UPDATE
+ `tabPayment Schedule` ps
+ SET
+ ps.outstanding = (ps.payment_amount - ps.paid_amount)
+ ''')
diff --git a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
new file mode 100644
index 0000000..c26cddb
--- /dev/null
+++ b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
@@ -0,0 +1,9 @@
+import frappe
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ if frappe.db.exists('Custom Field', { 'fieldname': 'vehicle_no' }):
+ frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', '')
diff --git a/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py b/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py
index ef3f1d6..c564f8b 100644
--- a/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py
+++ b/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py
@@ -9,7 +9,7 @@
# NOTE: sequence is important
renamed_fields = get_all_renamed_fields()
- for dt, script_field, ref_dt_field in (("Custom Script", "script", "dt"), ("Print Format", "html", "doc_type")):
+ for dt, script_field, ref_dt_field in (("Client Script", "script", "dt"), ("Print Format", "html", "doc_type")):
cond1 = " or ".join("""{0} like "%%{1}%%" """.format(script_field, d[0].replace("_", "\\_")) for d in renamed_fields)
cond2 = " and standard = 'No'" if dt == "Print Format" else ""
diff --git a/erpnext/patches/v7_0/remove_doctypes_and_reports.py b/erpnext/patches/v7_0/remove_doctypes_and_reports.py
index 746cae0..2356e2f 100644
--- a/erpnext/patches/v7_0/remove_doctypes_and_reports.py
+++ b/erpnext/patches/v7_0/remove_doctypes_and_reports.py
@@ -7,7 +7,7 @@
where name in('Time Log Batch', 'Time Log Batch Detail', 'Time Log')""")
frappe.db.sql("""delete from `tabDocField` where parent in ('Time Log', 'Time Log Batch')""")
- frappe.db.sql("""update `tabCustom Script` set dt = 'Timesheet' where dt = 'Time Log'""")
+ frappe.db.sql("""update `tabClient Script` set dt = 'Timesheet' where dt = 'Time Log'""")
for data in frappe.db.sql(""" select label, fieldname from `tabCustom Field` where dt = 'Time Log'""", as_dict=1):
custom_field = frappe.get_doc({
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.json b/erpnext/payroll/doctype/additional_salary/additional_salary.json
index 2b29f66..61ae7e4 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.json
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.json
@@ -163,7 +163,6 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -176,7 +175,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-20 17:51:13.419716",
+ "modified": "2021-03-31 14:45:48.566756",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Additional Salary",
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py
index f5af677..13b6c05 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.py
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py
@@ -9,17 +9,10 @@
from frappe.utils import getdate, date_diff, comma_and, formatdate
class AdditionalSalary(Document):
-
def on_submit(self):
if self.ref_doctype == "Employee Advance" and self.ref_docname:
frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", self.amount)
- def before_insert(self):
- if frappe.db.exists("Additional Salary", {"employee": self.employee, "salary_component": self.salary_component,
- "amount": self.amount, "payroll_date": self.payroll_date, "company": self.company, "docstatus": 1}):
-
- frappe.throw(_("Additional Salary Component Exists."))
-
def validate(self):
self.validate_dates()
self.validate_salary_structure()
@@ -89,10 +82,11 @@
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days
-@frappe.whitelist()
-def get_additional_salary_component(employee, start_date, end_date, component_type):
- additional_salaries = frappe.db.sql("""
- select name, salary_component, type, amount, overwrite_salary_structure_amount, deduct_full_tax_on_selected_payroll_date
+def get_additional_salaries(employee, start_date, end_date, component_type):
+ additional_salary_list = frappe.db.sql("""
+ select name, salary_component as component, type, amount,
+ overwrite_salary_structure_amount as overwrite,
+ deduct_full_tax_on_selected_payroll_date
from `tabAdditional Salary`
where employee=%(employee)s
and docstatus = 1
@@ -102,7 +96,7 @@
from_date <= %(to_date)s and to_date >= %(to_date)s
)
and type = %(component_type)s
- order by salary_component, overwrite_salary_structure_amount DESC
+ order by salary_component, overwrite ASC
""", {
'employee': employee,
'from_date': start_date,
@@ -110,38 +104,18 @@
'component_type': "Earning" if component_type == "earnings" else "Deduction"
}, as_dict=1)
- existing_salary_components= []
- salary_components_details = {}
- additional_salary_details = []
+ additional_salaries = []
+ components_to_overwrite = []
- overwrites_components = [ele.salary_component for ele in additional_salaries if ele.overwrite_salary_structure_amount == 1]
+ 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"))
- component_fields = ["depends_on_payment_days", "salary_component_abbr", "is_tax_applicable", "variable_based_on_taxable_salary", 'type']
- for d in additional_salaries:
+ components_to_overwrite.append(d.component)
- if d.salary_component not in existing_salary_components:
- component = frappe.get_all("Salary Component", filters={'name': d.salary_component}, fields=component_fields)
- struct_row = frappe._dict({'salary_component': d.salary_component})
- if component:
- struct_row.update(component[0])
+ additional_salaries.append(d)
- struct_row['deduct_full_tax_on_selected_payroll_date'] = d.deduct_full_tax_on_selected_payroll_date
- struct_row['is_additional_component'] = 1
-
- salary_components_details[d.salary_component] = struct_row
-
-
- if overwrites_components.count(d.salary_component) > 1:
- frappe.throw(_("Multiple Additional Salaries with overwrite property exist for Salary Component: {0} between {1} and {2}.".format(d.salary_component, start_date, end_date)), title=_("Error"))
- else:
- additional_salary_details.append({
- 'name': d.name,
- 'component': d.salary_component,
- 'amount': d.amount,
- 'type': d.type,
- 'overwrite': d.overwrite_salary_structure_amount,
- })
-
- existing_salary_components.append(d.salary_component)
-
- return salary_components_details, additional_salary_details
+ return additional_salaries
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
index 4c45580..c6f764c 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
@@ -124,7 +124,6 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -148,7 +147,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-12-14 15:52:08.566418",
+ "modified": "2021-03-31 14:46:22.465521",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Application",
diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js
index ea9ccd5..e1f8431 100644
--- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js
+++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.js
@@ -21,7 +21,6 @@
callback: function(r) {
if (r.message) {
frm.set_value('currency', r.message);
- frm.set_df_property('currency', 'hidden', 0);
}
}
});
diff --git a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json
index da24aac..e331b7a 100644
--- a/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json
+++ b/erpnext/payroll/doctype/employee_benefit_claim/employee_benefit_claim.json
@@ -125,10 +125,9 @@
"label": "Attachments"
},
{
- "default": "Company:company:default_currency",
+ "depends_on": "eval: doc.employee",
"fieldname": "currency",
"fieldtype": "Link",
- "hidden": 1,
"label": "Currency",
"options": "Currency",
"read_only": 1,
@@ -145,7 +144,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-25 11:49:56.097352",
+ "modified": "2021-03-31 15:51:51.489269",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Claim",
diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json
index e5b1052..51346c6 100644
--- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.json
+++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.json
@@ -75,7 +75,6 @@
"reqd": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -95,7 +94,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-20 17:22:16.468042",
+ "modified": "2021-03-31 14:48:00.919839",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Incentive",
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js
index 0e0c9b5..fb11875 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js
+++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.js
@@ -47,5 +47,26 @@
});
}).addClass("btn-primary");
}
+ },
+
+ employee: function(frm) {
+ if (frm.doc.employee) {
+ frm.trigger('get_employee_currency');
+ }
+ },
+
+ get_employee_currency: function(frm) {
+ frappe.call({
+ method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
+ args: {
+ employee: frm.doc.employee,
+ },
+ callback: function(r) {
+ if (r.message) {
+ frm.set_value('currency', r.message);
+ frm.refresh_fields();
+ }
+ }
+ });
}
});
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
index 83d4ae5..873bf88 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
+++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/employee_tax_exemption_declaration.json
@@ -108,7 +108,7 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
+ "depends_on": "eval: doc.employee",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
@@ -119,7 +119,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-20 16:42:24.493761",
+ "modified": "2021-03-31 20:41:57.387749",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Tax Exemption Declaration",
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js
index 497f35c..4fb0a37 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js
+++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.js
@@ -58,5 +58,26 @@
currency: function(frm) {
frm.refresh_fields();
- }
+ },
+
+ employee: function(frm) {
+ if (frm.doc.employee) {
+ frm.trigger('get_employee_currency');
+ }
+ },
+
+ get_employee_currency: function(frm) {
+ frappe.call({
+ method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
+ args: {
+ employee: frm.doc.employee,
+ },
+ callback: function(r) {
+ if (r.message) {
+ frm.set_value('currency', r.message);
+ frm.refresh_fields();
+ }
+ }
+ });
+ },
});
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
index 53f18cb..f32202a 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
+++ b/erpnext/payroll/doctype/employee_tax_exemption_proof_submission/employee_tax_exemption_proof_submission.json
@@ -131,7 +131,7 @@
"read_only": 1
},
{
- "default": "Company:company:default_currency",
+ "depends_on": "eval: doc.employee",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
@@ -142,7 +142,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-20 16:47:03.410020",
+ "modified": "2021-03-31 20:48:32.639885",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Tax Exemption Proof Submission",
diff --git a/erpnext/accounts/page/bank_reconciliation/__init__.py b/erpnext/payroll/doctype/gratuity/__init__.py
similarity index 100%
rename from erpnext/accounts/page/bank_reconciliation/__init__.py
rename to erpnext/payroll/doctype/gratuity/__init__.py
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.js b/erpnext/payroll/doctype/gratuity/gratuity.js
new file mode 100644
index 0000000..565d2c4
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity.js
@@ -0,0 +1,72 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Gratuity', {
+ setup: function (frm) {
+ frm.set_query('salary_component', function () {
+ return {
+ filters: {
+ type: "Earning"
+ }
+ };
+ });
+ frm.set_query("expense_account", function () {
+ return {
+ filters: {
+ "root_type": "Expense",
+ "is_group": 0,
+ "company": frm.doc.company
+ }
+ };
+ });
+
+ frm.set_query("payable_account", function () {
+ return {
+ filters: {
+ "root_type": "Liability",
+ "is_group": 0,
+ "company": frm.doc.company
+ }
+ };
+ });
+ },
+ refresh: function (frm) {
+ if (frm.doc.docstatus === 1 && frm.doc.pay_via_salary_slip === 0 && frm.doc.status === "Unpaid") {
+ frm.add_custom_button(__("Create Payment Entry"), function () {
+ return frappe.call({
+ method: 'erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry',
+ args: {
+ "dt": frm.doc.doctype,
+ "dn": frm.doc.name
+ },
+ callback: function (r) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route("Form", doclist[0].doctype, doclist[0].name);
+ }
+ });
+ });
+ }
+ },
+ employee: function (frm) {
+ frm.events.calculate_work_experience_and_amount(frm);
+ },
+ gratuity_rule: function (frm) {
+ frm.events.calculate_work_experience_and_amount(frm);
+ },
+ calculate_work_experience_and_amount: function (frm) {
+
+ if (frm.doc.employee && frm.doc.gratuity_rule) {
+ frappe.call({
+ method: "erpnext.payroll.doctype.gratuity.gratuity.calculate_work_experience_and_amount",
+ args: {
+ employee: frm.doc.employee,
+ gratuity_rule: frm.doc.gratuity_rule
+ }
+ }).then((r) => {
+ frm.set_value("current_work_experience", r.message['current_work_experience']);
+ frm.set_value("amount", r.message['amount']);
+ });
+ }
+ }
+
+});
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.json b/erpnext/payroll/doctype/gratuity/gratuity.json
new file mode 100644
index 0000000..5cffd7e
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity.json
@@ -0,0 +1,232 @@
+{
+ "actions": [],
+ "autoname": "HR-GRA-PAY-.#####",
+ "creation": "2020-08-05 20:52:13.024683",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "employee",
+ "employee_name",
+ "department",
+ "designation",
+ "column_break_3",
+ "posting_date",
+ "status",
+ "company",
+ "gratuity_rule",
+ "section_break_5",
+ "pay_via_salary_slip",
+ "payroll_date",
+ "salary_component",
+ "payable_account",
+ "expense_account",
+ "mode_of_payment",
+ "cost_center",
+ "column_break_15",
+ "current_work_experience",
+ "amount",
+ "paid_amount",
+ "amended_from"
+ ],
+ "fields": [
+ {
+ "fieldname": "employee",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Employee",
+ "options": "Employee",
+ "reqd": 1,
+ "search_index": 1
+ },
+ {
+ "fetch_from": "employee.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "pay_via_salary_slip",
+ "fieldtype": "Check",
+ "label": "Pay via Salary Slip"
+ },
+ {
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting date",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 1",
+ "fieldname": "salary_component",
+ "fieldtype": "Link",
+ "label": "Salary Component",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 1",
+ "options": "Salary Component"
+ },
+ {
+ "default": "0",
+ "fieldname": "current_work_experience",
+ "fieldtype": "Int",
+ "label": "Current Work Experience",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Total Amount",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "options": "Draft\nUnpaid\nPaid",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "fieldname": "expense_account",
+ "fieldtype": "Link",
+ "label": "Expense Account",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "options": "Account"
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "options": "Mode of Payment"
+ },
+ {
+ "fieldname": "gratuity_rule",
+ "fieldtype": "Link",
+ "label": "Gratuity Rule",
+ "options": "Gratuity Rule",
+ "reqd": 1
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "label": "Payment Configuration"
+ },
+ {
+ "fetch_from": "employee.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Data",
+ "label": "Employee Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "employee.department",
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "employee.designation",
+ "fieldname": "designation",
+ "fieldtype": "Data",
+ "label": "Designation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Gratuity",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_15",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 1",
+ "fieldname": "payroll_date",
+ "fieldtype": "Date",
+ "label": "Payroll Date",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 1"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.pay_via_salary_slip == 0",
+ "fieldname": "paid_amount",
+ "fieldtype": "Currency",
+ "label": "Paid Amount",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "fieldname": "payable_account",
+ "fieldtype": "Link",
+ "label": "Payable Account",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "options": "Account"
+ },
+ {
+ "depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "fieldname": "cost_center",
+ "fieldtype": "Link",
+ "label": "Cost Center",
+ "mandatory_depends_on": "eval: doc.pay_via_salary_slip == 0",
+ "options": "Cost Center"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-11-02 18:21:11.971488",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Gratuity",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/gratuity.py b/erpnext/payroll/doctype/gratuity/gratuity.py
new file mode 100644
index 0000000..1acd6e3
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity.py
@@ -0,0 +1,249 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _, bold
+from frappe.utils import flt, get_datetime, get_link_to_form
+from erpnext.accounts.general_ledger import make_gl_entries
+from erpnext.controllers.accounts_controller import AccountsController
+from math import floor
+
+class Gratuity(AccountsController):
+ def validate(self):
+ data = calculate_work_experience_and_amount(self.employee, self.gratuity_rule)
+ self.current_work_experience = data["current_work_experience"]
+ self.amount = data["amount"]
+ if self.docstatus == 1:
+ self.status = "Unpaid"
+
+ def on_submit(self):
+ if self.pay_via_salary_slip:
+ self.create_additional_salary()
+ else:
+ self.create_gl_entries()
+
+ def on_cancel(self):
+ self.ignore_linked_doctypes = ['GL Entry']
+ self.create_gl_entries(cancel=True)
+
+ def create_gl_entries(self, cancel=False):
+ gl_entries = self.get_gl_entries()
+ make_gl_entries(gl_entries, cancel)
+
+ def get_gl_entries(self):
+ gl_entry = []
+ # 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)
+ )
+
+ # 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)
+ )
+ else:
+ frappe.throw(_("Total Amount can not be zero"))
+
+ return gl_entry
+
+ def create_additional_salary(self):
+ if self.pay_via_salary_slip:
+ 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
+ additional_salary.amount = self.amount
+ additional_salary.payroll_date = self.payroll_date
+ additional_salary.company = self.company
+ additional_salary.ref_doctype = self.doctype
+ additional_salary.ref_docname = self.name
+ additional_salary.submit()
+
+ def set_total_advance_paid(self):
+ 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
+
+ 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")
+
+
+@frappe.whitelist()
+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}
+
+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"])
+
+ 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))))
+
+ 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)
+ return current_work_experience
+
+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":
+ 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):
+ 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))
+ 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))
+ }
+
+ if status == "On Leave":
+ 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)
+
+
+ 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)
+
+ 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
+ year_left = 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)
+ if slab_found:
+ 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
+ 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)
+ 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
+ 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)))
+ 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"])
+ 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]
+
+ return applicable_earnings_component
+
+def get_total_applicable_component_amount(employee, applicable_earnings_component, gratuity_rule):
+ 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_list("Salary Detail",
+ filters={
+ "docstatus": 1,
+ 'parent': sal_slip,
+ "parentfield": "earnings",
+ 'salary_component': ('in', applicable_earnings_component)
+ },
+ 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"))
+ for data in component_and_amounts:
+ 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
+ if experience >= from_year and (to_year == 0 or experience < to_year):
+ 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")
+
+def get_salary_structure(employee):
+ 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
+
+def get_last_salary_slip(employee):
+ return frappe.get_list("Salary Slip", filters = {
+ "employee": employee, 'docstatus': 1
+ },
+ order_by = "start_date desc")[0].name
+
diff --git a/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py
new file mode 100644
index 0000000..5b2489f
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/gratuity_dashboard.py
@@ -0,0 +1,20 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'reference_name',
+ 'non_standard_fieldnames': {
+ 'Additional Salary': 'ref_docname',
+ },
+ 'transactions': [
+ {
+ 'label': _('Payment'),
+ 'items': ['Payment Entry']
+ },
+ {
+ 'label': _('Additional Salary'),
+ 'items': ['Additional Salary']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity/test_gratuity.py b/erpnext/payroll/doctype/gratuity/test_gratuity.py
new file mode 100644
index 0000000..7daea2d
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity/test_gratuity.py
@@ -0,0 +1,195 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import make_employee_salary_slip, make_earning_salary_component, \
+ make_deduction_salary_component
+from erpnext.payroll.doctype.gratuity.gratuity import get_last_salary_slip
+from erpnext.regional.united_arab_emirates.setup import create_gratuity_rule
+from erpnext.hr.doctype.expense_claim.test_expense_claim import get_payable_account
+from frappe.utils import getdate, add_days, get_datetime, flt
+
+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'])
+
+ def setUp(self):
+ frappe.db.sql("DELETE FROM `tabGratuity`")
+ frappe.db.sql("DELETE FROM `tabAdditional Salary` WHERE ref_doctype = 'Gratuity'")
+
+ def test_check_gratuity_amount_based_on_current_slab_and_additional_salary_creation(self):
+ employee, sal_slip = create_employee_and_get_last_salary_slip()
+
+ rule = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)")
+
+ 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_list("Salary Detail",
+ filters={
+ "docstatus": 1,
+ 'parent': sal_slip,
+ "parentfield": "earnings",
+ 'salary_component': "Basic Salary"
+ },
+ fields=["amount"])
+
+ ''' 5 - 0 fraction is 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)
+ 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()
+ 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)
+
+ #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_list("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()
+
+ 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()
+ gratuity.reload()
+
+ self.assertEqual(gratuity.status, "Paid")
+ 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)
+ if not rule:
+ create_gratuity_rule()
+ rule = frappe.get_doc("Gratuity Rule", name)
+ rule.applicable_earnings_component = []
+ 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)
+ gratuity = frappe.new_doc("Gratuity")
+ gratuity.employee = args.employee
+ gratuity.posting_date = getdate()
+ gratuity.gratuity_rule = args.rule or "Rule Under Limited Contract (UAE)"
+ gratuity.pay_via_salary_slip = args.pay_via_salary_slip or 0
+ if gratuity.pay_via_salary_slip:
+ gratuity.payroll_date = getdate()
+ gratuity.salary_component = "Performance Bonus"
+ else:
+ 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.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()
+
+ 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.save()
+
+def create_account():
+ return frappe.get_doc({
+ "doctype": "Account",
+ "company": "_Test Company",
+ "account_name": "Payment Account",
+ "root_type": "Asset",
+ "report_type": "Balance Sheet",
+ "currency": "INR",
+ "parent_account": "Bank Accounts - _TC",
+ "account_type": "Bank",
+ }).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")
+ 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
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/payroll/doctype/gratuity_applicable_component/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/payroll/doctype/gratuity_applicable_component/__init__.py
diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json
new file mode 100644
index 0000000..eea0e85
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.json
@@ -0,0 +1,32 @@
+{
+ "actions": [],
+ "creation": "2020-08-05 19:00:28.097265",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "salary_component"
+ ],
+ "fields": [
+ {
+ "fieldname": "salary_component",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Salary Component ",
+ "options": "Salary Component",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-08-05 20:17:13.855035",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Gratuity Applicable Component",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py
new file mode 100644
index 0000000..23e4340
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_applicable_component/gratuity_applicable_component.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class GratuityApplicableComponent(Document):
+ pass
diff --git a/erpnext/accounts/page/bank_reconciliation/__init__.py b/erpnext/payroll/doctype/gratuity_rule/__init__.py
similarity index 100%
copy from erpnext/accounts/page/bank_reconciliation/__init__.py
copy to erpnext/payroll/doctype/gratuity_rule/__init__.py
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js
new file mode 100644
index 0000000..ee6c5df
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.js
@@ -0,0 +1,40 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Gratuity Rule', {
+ // refresh: function(frm) {
+
+ // }
+});
+
+frappe.ui.form.on('Gratuity Rule Slab', {
+
+ /*
+ Slabs should be in order like
+
+ from | to | fraction
+ 0 | 4 | 0.5
+ 4 | 6 | 0.7
+
+ So, on row addition setting current_row.from = previous row.to.
+ On to_year insert we have to check that it is not less than from_year
+
+ Wrong order may lead to Wrong Calculation
+ */
+
+ gratuity_rule_slabs_add(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ let array_idx = row.idx - 1;
+ if (array_idx > 0) {
+ row.from_year = cur_frm.doc.gratuity_rule_slabs[array_idx - 1].to_year;
+ frm.refresh();
+ }
+ },
+
+ to_year(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ if (row.to_year <= row.from_year && row.to_year === 0) {
+ frappe.throw(__("To(Year) year can not be less than From(year) "));
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json
new file mode 100644
index 0000000..84cdcf5
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.json
@@ -0,0 +1,114 @@
+{
+ "actions": [],
+ "autoname": "Prompt",
+ "creation": "2020-08-05 19:00:36.103500",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "applicable_earnings_component",
+ "work_experience_calculation_function",
+ "total_working_days_per_year",
+ "column_break_3",
+ "disable",
+ "calculate_gratuity_amount_based_on",
+ "minimum_year_for_gratuity",
+ "gratuity_rules_section",
+ "gratuity_rule_slabs"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "fieldname": "disable",
+ "fieldtype": "Check",
+ "label": "Disable"
+ },
+ {
+ "fieldname": "calculate_gratuity_amount_based_on",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Calculate Gratuity Amount Based On",
+ "options": "Current Slab\nSum of all previous slabs",
+ "reqd": 1
+ },
+ {
+ "description": "Salary components should be part of the Salary Structure.",
+ "fieldname": "applicable_earnings_component",
+ "fieldtype": "Table MultiSelect",
+ "label": "Applicable Earnings Component",
+ "options": "Gratuity Applicable Component",
+ "reqd": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "gratuity_rules_section",
+ "fieldtype": "Section Break",
+ "label": "Gratuity Rules"
+ },
+ {
+ "description": "Leave <b>From</b> and <b>To</b> 0 for no upper and lower limit.",
+ "fieldname": "gratuity_rule_slabs",
+ "fieldtype": "Table",
+ "label": "Current Work Experience",
+ "options": "Gratuity Rule Slab",
+ "reqd": 1
+ },
+ {
+ "default": "Round off Work Experience",
+ "fieldname": "work_experience_calculation_function",
+ "fieldtype": "Select",
+ "label": "Work Experience Calculation method",
+ "options": "Round off Work Experience\nTake Exact Completed Years"
+ },
+ {
+ "default": "365",
+ "fieldname": "total_working_days_per_year",
+ "fieldtype": "Int",
+ "label": "Total working Days Per Year"
+ },
+ {
+ "fieldname": "minimum_year_for_gratuity",
+ "fieldtype": "Int",
+ "label": "Minimum Year for Gratuity"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-12-03 17:08:27.891535",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Gratuity Rule",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py
new file mode 100644
index 0000000..29a6ebe
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule.py
@@ -0,0 +1,33 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.document import Document
+from frappe import _
+
+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(_("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."))
+
+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.minimum_year_for_gratuity = 1
+
+
+ for slab in slabs:
+ slab = frappe._dict(slab)
+ rule.append("gratuity_rule_slabs", slab)
+ return rule
diff --git a/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py
new file mode 100644
index 0000000..0d70163
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule/gratuity_rule_dashboard.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'gratuity_rule',
+ 'transactions': [
+ {
+ 'label': _('Gratuity'),
+ 'items': ['Gratuity']
+ }
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py b/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py
similarity index 78%
copy from erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
copy to erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py
index 2ad7984..1f5dc4e 100644
--- a/erpnext/non_profit/doctype/membership_settings/test_membership_settings.py
+++ b/erpnext/payroll/doctype/gratuity_rule/test_gratuity_rule.py
@@ -6,5 +6,5 @@
# import frappe
import unittest
-class TestMembershipSettings(unittest.TestCase):
+class TestGratuityRule(unittest.TestCase):
pass
diff --git a/erpnext/accounts/page/bank_reconciliation/__init__.py b/erpnext/payroll/doctype/gratuity_rule_slab/__init__.py
similarity index 100%
copy from erpnext/accounts/page/bank_reconciliation/__init__.py
copy to erpnext/payroll/doctype/gratuity_rule_slab/__init__.py
diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json
new file mode 100644
index 0000000..bc37b0f
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.json
@@ -0,0 +1,50 @@
+{
+ "actions": [],
+ "creation": "2020-08-05 19:12:49.423500",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "from_year",
+ "to_year",
+ "fraction_of_applicable_earnings"
+ ],
+ "fields": [
+ {
+ "fieldname": "fraction_of_applicable_earnings",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Fraction of Applicable Earnings ",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "from_year",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "From(Year)",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "to_year",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "To(Year)",
+ "reqd": 1
+ }
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2020-08-17 14:09:56.781712",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Gratuity Rule Slab",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py
new file mode 100644
index 0000000..fa468e7
--- /dev/null
+++ b/erpnext/payroll/doctype/gratuity_rule_slab/gratuity_rule_slab.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class GratuityRuleSlab(Document):
+ pass
diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json
index 9fa261d..c343a44 100644
--- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json
+++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.json
@@ -93,7 +93,7 @@
"options": "Income Tax Slab Other Charges"
},
{
- "default": "Company:company:default_currency",
+ "fetch_from": "company.default_currency",
"fieldname": "currency",
"fieldtype": "Link",
"label": "Currency",
@@ -104,7 +104,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-19 13:54:24.728075",
+ "modified": "2021-03-31 20:53:33.323712",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Income Tax Slab",
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 6bcd4e0..7890471 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -95,6 +95,7 @@
return emp_list
+ @frappe.whitelist()
def fill_employee_details(self):
self.set('employees', [])
employees = self.get_emp_list()
@@ -142,6 +143,7 @@
if not self.get(fieldname):
frappe.throw(_("Please set {0}").format(self.meta.get_label(fieldname)))
+ @frappe.whitelist()
def create_salary_slips(self):
"""
Creates salary slip for selected employees if already not created
@@ -329,6 +331,7 @@
amount = flt(amount) * flt(conversion_rate)
return exchange_rate, amount
+ @frappe.whitelist()
def make_payment_entry(self):
self.check_permission('write')
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index e098ec7..84c3814 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -12,7 +12,7 @@
from erpnext.payroll.doctype.salary_slip.test_salary_slip import get_salary_component_account, \
make_earning_salary_component, make_deduction_salary_component, create_account, make_employee_salary_slip
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure, create_salary_structure_assignment
-from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry
+from erpnext.loan_management.doctype.loan.test_loan import create_loan, make_loan_disbursement_entry, create_loan_type, create_loan_accounts
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
class TestPayrollEntry(unittest.TestCase):
@@ -168,15 +168,23 @@
salary_structure = "Test Salary Structure for Loan"
make_salary_structure(salary_structure, "Monthly", employee=employee_doc.name, company="_Test Company", currency=company_doc.default_currency)
+ if not frappe.db.exists("Loan Type", "Car Loan"):
+ create_loan_accounts()
+ create_loan_type("Car Loan", 500000, 8.4,
+ is_term_loan=1,
+ mode_of_payment='Cash',
+ payment_account='Payment Account - _TC',
+ loan_account='Loan Account - _TC',
+ interest_income_account='Interest Income Account - _TC',
+ penalty_income_account='Penalty Income Account - _TC')
+
loan = create_loan(applicant, "Car Loan", 280000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
loan.submit()
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=add_months(nowdate(), -1))
-
process_loan_interest_accrual_for_term_loans(posting_date=nowdate())
-
dates = get_start_end_dates('Monthly', nowdate())
make_payroll_entry(company="_Test Company", start_date=dates.start_date, payable_account=company_doc.default_payroll_payable_account,
currency=company_doc.default_currency, end_date=dates.end_date, branch=branch, cost_center="Main - _TC", payment_account="Cash - _TC")
@@ -267,4 +275,4 @@
salary_slip.calculate_net_pay()
salary_slip.db_update()
- return salary_slip
\ No newline at end of file
+ return salary_slip
diff --git a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json
index c47caa1..54377e9 100644
--- a/erpnext/payroll/doctype/payroll_settings/payroll_settings.json
+++ b/erpnext/payroll/doctype/payroll_settings/payroll_settings.json
@@ -15,6 +15,7 @@
"daily_wages_fraction_for_half_day",
"email_salary_slip_to_employee",
"encrypt_salary_slips_in_emails",
+ "show_leave_balances_in_salary_slip",
"password_policy"
],
"fields": [
@@ -23,58 +24,44 @@
"fieldname": "payroll_based_on",
"fieldtype": "Select",
"label": "Calculate Payroll Working Days Based On",
- "options": "Leave\nAttendance",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Leave\nAttendance"
},
{
"fieldname": "max_working_hours_against_timesheet",
"fieldtype": "Float",
- "label": "Max working hours against Timesheet",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Max working hours against Timesheet"
},
{
"default": "0",
"description": "If checked, Total no. of Working Days will include holidays, and this will reduce the value of Salary Per Day",
"fieldname": "include_holidays_in_total_working_days",
"fieldtype": "Check",
- "label": "Include holidays in Total no. of Working Days",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Include holidays in Total no. of Working Days"
},
{
"default": "0",
"description": "If checked, hides and disables Rounded Total field in Salary Slips",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
- "label": "Disable Rounded Total",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Disable Rounded Total"
},
{
"fieldname": "column_break_11",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Column Break"
},
{
"default": "0.5",
"description": "The fraction of daily wages to be paid for half-day attendance",
"fieldname": "daily_wages_fraction_for_half_day",
"fieldtype": "Float",
- "label": "Fraction of Daily Salary for Half Day",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Fraction of Daily Salary for Half Day"
},
{
"default": "1",
"description": "Emails salary slip to employee based on preferred email selected in Employee",
"fieldname": "email_salary_slip_to_employee",
"fieldtype": "Check",
- "label": "Email Salary Slip to Employee",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Email Salary Slip to Employee"
},
{
"default": "0",
@@ -82,9 +69,7 @@
"description": "The salary slip emailed to the employee will be password protected, the password will be generated based on the password policy.",
"fieldname": "encrypt_salary_slips_in_emails",
"fieldtype": "Check",
- "label": "Encrypt Salary Slips in Emails",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Encrypt Salary Slips in Emails"
},
{
"depends_on": "eval: doc.encrypt_salary_slips_in_emails == 1",
@@ -92,24 +77,27 @@
"fieldname": "password_policy",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Password Policy",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Password Policy"
},
{
"depends_on": "eval:doc.payroll_based_on == 'Attendance'",
"fieldname": "consider_unmarked_attendance_as",
"fieldtype": "Select",
"label": "Consider Unmarked Attendance As",
- "options": "Present\nAbsent",
- "show_days": 1,
- "show_seconds": 1
+ "options": "Present\nAbsent"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_leave_balances_in_salary_slip",
+ "fieldtype": "Check",
+ "label": "Show Leave Balances in Salary Slip"
}
],
"icon": "fa fa-cog",
+ "index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-06-22 17:00:58.408030",
+ "modified": "2021-03-03 17:49:59.579723",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Settings",
@@ -126,5 +114,6 @@
}
],
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json
index 6647230..cd563bc 100644
--- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.json
+++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.json
@@ -93,7 +93,6 @@
"reqd": 1
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.employee)",
"fieldname": "currency",
"fieldtype": "Link",
@@ -106,7 +105,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-20 17:27:47.003134",
+ "modified": "2021-03-31 14:50:29.401020",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Retention Bonus",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js
index 7460c75..3e8a213 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.js
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js
@@ -74,43 +74,46 @@
if (!frm.doc.letter_head && company.default_letter_head) {
frm.set_value('letter_head', company.default_letter_head);
}
+ },
+
+ currency: function(frm) {
frm.trigger("set_dynamic_labels");
},
set_dynamic_labels: function(frm) {
var company_currency = frm.doc.company? erpnext.get_currency(frm.doc.company): frappe.defaults.get_default("currency");
- frappe.run_serially([
- () => frm.events.set_exchange_rate(frm, company_currency),
- () => frm.events.change_form_labels(frm, company_currency),
- () => frm.events.change_grid_labels(frm),
- () => frm.refresh_fields()
- ]);
+ if (frm.doc.employee && frm.doc.currency) {
+ frappe.run_serially([
+ () => frm.events.set_exchange_rate(frm, company_currency),
+ () => frm.events.change_form_labels(frm, company_currency),
+ () => frm.events.change_grid_labels(frm),
+ () => frm.refresh_fields()
+ ]);
+ }
},
set_exchange_rate: function(frm, company_currency) {
- if (frm.doc.docstatus === 0) {
- if (frm.doc.currency) {
- var from_currency = frm.doc.currency;
- if (from_currency != company_currency) {
- frm.events.hide_loan_section(frm);
- frappe.call({
- method: "erpnext.setup.utils.get_exchange_rate",
- args: {
- from_currency: from_currency,
- to_currency: company_currency,
- },
- callback: function(r) {
- frm.set_value("exchange_rate", flt(r.message));
- frm.set_df_property('exchange_rate', 'hidden', 0);
- frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
- + " = [?] " + company_currency);
- }
- });
- } else {
- frm.set_value("exchange_rate", 1.0);
- frm.set_df_property('exchange_rate', 'hidden', 1);
- frm.set_df_property("exchange_rate", "description", "" );
- }
+ if (frm.doc.currency) {
+ var from_currency = frm.doc.currency;
+ if (from_currency != company_currency) {
+ frm.events.hide_loan_section(frm);
+ frappe.call({
+ method: "erpnext.setup.utils.get_exchange_rate",
+ args: {
+ from_currency: from_currency,
+ to_currency: company_currency,
+ },
+ callback: function(r) {
+ frm.set_value("exchange_rate", flt(r.message));
+ frm.set_df_property("exchange_rate", "hidden", 0);
+ frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
+ + " = [?] " + company_currency);
+ }
+ });
+ } else {
+ frm.set_value("exchange_rate", 1.0);
+ frm.set_df_property("exchange_rate", "hidden", 1);
+ frm.set_df_property("exchange_rate", "description", "");
}
}
},
@@ -213,7 +216,7 @@
});
var set_totals = function(frm) {
- if (frm.doc.docstatus === 0) {
+ if (frm.doc.docstatus === 0 && frm.doc.doctype === "Salary Slip") {
if (frm.doc.earnings || frm.doc.deductions) {
frappe.call({
method: "set_totals",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index 9f9691b..ec56076 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -80,6 +80,8 @@
"total_in_words",
"column_break_69",
"base_total_in_words",
+ "leave_details_section",
+ "leave_details",
"section_break_75",
"amended_from"
],
@@ -498,7 +500,6 @@
"fieldtype": "Column Break"
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)",
"fetch_from": "salary_structure.currency",
"fieldname": "currency",
@@ -612,13 +613,25 @@
"label": "Month To Date(Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fieldname": "leave_details_section",
+ "fieldtype": "Section Break",
+ "label": "Leave Details"
+ },
+ {
+ "fieldname": "leave_details",
+ "fieldtype": "Table",
+ "label": "Leave Details",
+ "options": "Salary Slip Leave",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2021-01-14 13:37:38.180920",
+ "modified": "2021-03-31 15:39:28.817166",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 2d3bc57..aa9acd8 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -13,12 +13,13 @@
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
from erpnext.utilities.transaction_base import TransactionBase
from frappe.utils.background_jobs import enqueue
-from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salary_component
+from erpnext.payroll.doctype.additional_salary.additional_salary import get_additional_salaries
from erpnext.payroll.doctype.payroll_period.payroll_period import get_period_factor, get_payroll_period
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
from erpnext.accounts.utils import get_fiscal_year
+from six import iteritems
class SalarySlip(TransactionBase):
def __init__(self, *args, **kwargs):
@@ -53,6 +54,7 @@
self.compute_year_to_date()
self.compute_month_to_date()
self.compute_component_wise_year_to_date()
+ self.add_leave_balances()
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
@@ -78,9 +80,26 @@
if (frappe.db.get_single_value("Payroll Settings", "email_salary_slip_to_employee")) and not frappe.flags.via_payroll_entry:
self.email_salary_slip()
+ self.update_payment_status_for_gratuity()
+
+ def update_payment_status_for_gratuity(self):
+ add_salary = frappe.db.get_all("Additional Salary",
+ filters = {
+ "payroll_date": ("BETWEEN", [self.start_date, self.end_date]),
+ "employee": self.employee,
+ "ref_doctype": "Gratuity",
+ "docstatus": 1,
+ }, fields = ["ref_docname", "name"], limit=1)
+
+ if len(add_salary):
+ status = "Paid" if self.docstatus == 1 else "Unpaid"
+ if add_salary[0].name in [data.additional_salary for data in self.earnings]:
+ frappe.db.set_value("Gratuity", add_salary.ref_docname, "status", status)
+
def on_cancel(self):
self.set_status()
self.update_status()
+ self.update_payment_status_for_gratuity()
self.cancel_loan_repayment_entry()
def on_trash(self):
@@ -123,6 +142,7 @@
self.start_date = date_details.start_date
self.end_date = date_details.end_date
+ @frappe.whitelist()
def get_emp_and_working_day_details(self):
'''First time, load all the components from salary structure'''
if self.employee:
@@ -504,7 +524,8 @@
return amount
except NameError as err:
- frappe.throw(_("Name error: {0}").format(err))
+ frappe.throw(_("{0} <br> This error can be due to missing or deleted field.").format(err),
+ title=_("Name error"))
except SyntaxError as err:
frappe.throw(_("Syntax error in formula or condition: {0}").format(err))
except Exception as e:
@@ -538,15 +559,16 @@
self.update_component_row(frappe._dict(last_benefit.struct_row), amount, "earnings")
def add_additional_salary_components(self, component_type):
- salary_components_details, additional_salary_details = get_additional_salary_component(self.employee,
+ additional_salaries = get_additional_salaries(self.employee,
self.start_date, self.end_date, component_type)
- if salary_components_details and additional_salary_details:
- for additional_salary in additional_salary_details:
- additional_salary =frappe._dict(additional_salary)
- amount = additional_salary.amount
- overwrite = additional_salary.overwrite
- self.update_component_row(frappe._dict(salary_components_details[additional_salary.component]), amount,
- component_type, overwrite=overwrite, additional_salary=additional_salary.name)
+
+ for additional_salary in additional_salaries:
+ self.update_component_row(
+ get_salary_component_data(additional_salary.component),
+ additional_salary.amount,
+ component_type,
+ additional_salary
+ )
def add_tax_components(self, payroll_period):
# Calculate variable_based_on_taxable_salary after all components updated in salary slip
@@ -563,46 +585,62 @@
for d in tax_components:
tax_amount = self.calculate_variable_based_on_taxable_salary(d, payroll_period)
- tax_row = self.get_salary_slip_row(d)
+ tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions")
- def update_component_row(self, struct_row, amount, key, overwrite=1, additional_salary = ''):
+ def update_component_row(self, component_data, amount, component_type, additional_salary=None):
component_row = None
- for d in self.get(key):
- if d.salary_component == struct_row.salary_component:
+ for d in self.get(component_type):
+ if d.salary_component != component_data.salary_component:
+ continue
+
+ if (
+ not d.additional_salary
+ and (not additional_salary or additional_salary.overwrite)
+ or additional_salary
+ and additional_salary.name == d.additional_salary
+ ):
component_row = d
- if not component_row or (struct_row.get("is_additional_component") and not overwrite):
- if amount:
- self.append(key, {
- 'amount': amount,
- 'default_amount': amount if not struct_row.get("is_additional_component") else 0,
- 'depends_on_payment_days' : struct_row.depends_on_payment_days,
- 'salary_component' : struct_row.salary_component,
- 'abbr' : struct_row.abbr or struct_row.get("salary_component_abbr"),
- 'additional_salary': additional_salary,
- 'do_not_include_in_total' : struct_row.do_not_include_in_total,
- 'is_tax_applicable': struct_row.is_tax_applicable,
- 'is_flexible_benefit': struct_row.is_flexible_benefit,
- 'variable_based_on_taxable_salary': struct_row.variable_based_on_taxable_salary,
- 'deduct_full_tax_on_selected_payroll_date': struct_row.deduct_full_tax_on_selected_payroll_date,
- 'additional_amount': amount if struct_row.get("is_additional_component") else 0,
- 'exempted_from_income_tax': struct_row.exempted_from_income_tax
- })
+ break
+
+ if additional_salary and additional_salary.overwrite:
+ # Additional Salary with overwrite checked, remove default rows of same component
+ self.set(component_type, [
+ d for d in self.get(component_type)
+ if d.salary_component != component_data.salary_component
+ or d.additional_salary and additional_salary.name != d.additional_salary
+ or d == component_row
+ ])
+
+ if not component_row:
+ if not amount:
+ return
+
+ component_row = self.append(component_type)
+ for attr in (
+ 'depends_on_payment_days', 'salary_component',
+ 'do_not_include_in_total', 'is_tax_applicable',
+ 'is_flexible_benefit', 'variable_based_on_taxable_salary',
+ 'exempted_from_income_tax'
+ ):
+ component_row.set(attr, component_data.get(attr))
+
+ abbr = component_data.get('abbr') or component_data.get('salary_component_abbr')
+ component_row.set('abbr', abbr)
+
+ if additional_salary:
+ component_row.default_amount = 0
+ component_row.additional_amount = amount
+ component_row.additional_salary = additional_salary.name
+ component_row.deduct_full_tax_on_selected_payroll_date = \
+ additional_salary.deduct_full_tax_on_selected_payroll_date
else:
- if struct_row.get("is_additional_component"):
- if overwrite:
- component_row.additional_amount = amount - component_row.get("default_amount", 0)
- component_row.additional_salary = additional_salary
- else:
- component_row.additional_amount = amount
+ component_row.default_amount = amount
+ component_row.additional_amount = 0
+ component_row.deduct_full_tax_on_selected_payroll_date = \
+ component_data.deduct_full_tax_on_selected_payroll_date
- if not overwrite and component_row.default_amount:
- amount += component_row.default_amount
- else:
- component_row.default_amount = amount
-
- component_row.amount = amount
- component_row.deduct_full_tax_on_selected_payroll_date = struct_row.deduct_full_tax_on_selected_payroll_date
+ component_row.amount = amount
def calculate_variable_based_on_taxable_salary(self, tax_component, payroll_period):
if not payroll_period:
@@ -928,26 +966,14 @@
if condition:
return frappe.safe_eval(condition, self.whitelisted_globals, data)
except NameError as err:
- frappe.throw(_("Name error: {0}").format(err))
+ frappe.throw(_("{0} <br> This error can be due to missing or deleted field.").format(err),
+ title=_("Name error"))
except SyntaxError as err:
frappe.throw(_("Syntax error in condition: {0}").format(err))
except Exception as e:
frappe.throw(_("Error in formula or condition: {0}").format(e))
raise
- def get_salary_slip_row(self, salary_component):
- component = frappe.get_doc("Salary Component", salary_component)
- # Data for update_component_row
- struct_row = frappe._dict()
- struct_row['depends_on_payment_days'] = component.depends_on_payment_days
- struct_row['salary_component'] = component.name
- struct_row['abbr'] = component.salary_component_abbr
- struct_row['do_not_include_in_total'] = component.do_not_include_in_total
- struct_row['is_tax_applicable'] = component.is_tax_applicable
- struct_row['is_flexible_benefit'] = component.is_flexible_benefit
- struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary
- return struct_row
-
def get_component_totals(self, component_type, depends_on_payment_days=0):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
@@ -1010,7 +1036,6 @@
self.total_loan_repayment += payment.total_payment
def get_loan_details(self):
-
return frappe.get_all("Loan",
fields=["name", "interest_income_account", "loan_account", "loan_type"],
filters = {
@@ -1093,20 +1118,22 @@
self.bank_name = emp.bank_name
self.bank_account_no = emp.bank_ac_no
+ @frappe.whitelist()
def process_salary_based_on_working_days(self):
self.get_working_days_details(lwp=self.leave_without_pay)
self.calculate_net_pay()
+ @frappe.whitelist()
def set_totals(self):
self.gross_pay = 0.0
if self.salary_slip_based_on_timesheet == 1:
self.calculate_total_for_salary_slip_based_on_timesheet()
else:
self.total_deduction = 0.0
- if self.earnings:
+ if hasattr(self, "earnings"):
for earning in self.earnings:
self.gross_pay += flt(earning.amount, earning.precision("amount"))
- if self.deductions:
+ if hasattr(self, "deductions"):
for deduction in self.deductions:
self.total_deduction += flt(deduction.amount, deduction.precision("amount"))
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)
@@ -1123,6 +1150,7 @@
#calculate total working hours, earnings based on hourly wages and totals
def calculate_total_for_salary_slip_based_on_timesheet(self):
if self.timesheets:
+ self.total_working_hours = 0
for timesheet in self.timesheets:
if timesheet.working_hours:
self.total_working_hours += timesheet.working_hours
@@ -1212,6 +1240,22 @@
return period_start_date, period_end_date
+ def add_leave_balances(self):
+ self.set('leave_details', [])
+
+ if frappe.db.get_single_value('Payroll Settings', 'show_leave_balances_in_salary_slip'):
+ from erpnext.hr.doctype.leave_application.leave_application import get_leave_details
+ leave_details = get_leave_details(self.employee, self.end_date)
+
+ for leave_type, leave_values in iteritems(leave_details['leave_allocation']):
+ self.append('leave_details', {
+ 'leave_type': leave_type,
+ 'total_allocated_leaves': flt(leave_values.get('total_leaves')),
+ 'expired_leaves': flt(leave_values.get('expired_leaves')),
+ 'used_leaves': flt(leave_values.get('leaves_taken')),
+ 'pending_leaves': flt(leave_values.get('pending_leaves')),
+ 'available_leaves': flt(leave_values.get('remaining_leaves'))
+ })
def unlink_ref_doc_from_salary_slip(ref_no):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
@@ -1223,4 +1267,20 @@
def generate_password_for_pdf(policy_template, employee):
employee = frappe.get_doc("Employee", employee)
- return policy_template.format(**employee.as_dict())
\ No newline at end of file
+ return policy_template.format(**employee.as_dict())
+
+def get_salary_component_data(component):
+ return frappe.get_value(
+ "Salary Component",
+ component,
+ [
+ "name as salary_component",
+ "depends_on_payment_days",
+ "salary_component_abbr as abbr",
+ "do_not_include_in_total",
+ "is_tax_applicable",
+ "is_flexible_benefit",
+ "variable_based_on_taxable_salary",
+ ],
+ as_dict=1,
+ )
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index f58a8e5..7672695 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -21,6 +21,7 @@
class TestSalarySlip(unittest.TestCase):
def setUp(self):
setup_test()
+
def tearDown(self):
frappe.db.set_value("Payroll Settings", None, "include_holidays_in_total_working_days", 0)
frappe.set_user("Administrator")
@@ -245,7 +246,7 @@
make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
payroll_period=payroll_period)
- frappe.db.sql("""delete from `tabLoan""")
+ frappe.db.sql("delete from tabLoan")
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
loan.submit()
@@ -360,7 +361,6 @@
# as per assigned salary structure 40500 in monthly salary so 236000*5/100/12
frappe.db.sql("""delete from `tabPayroll Period`""")
frappe.db.sql("""delete from `tabSalary Component`""")
- frappe.db.sql("""delete from `tabAdditional Salary`""")
payroll_period = create_payroll_period()
diff --git a/erpnext/accounts/page/bank_reconciliation/__init__.py b/erpnext/payroll/doctype/salary_slip_leave/__init__.py
similarity index 100%
copy from erpnext/accounts/page/bank_reconciliation/__init__.py
copy to erpnext/payroll/doctype/salary_slip_leave/__init__.py
diff --git a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json
new file mode 100644
index 0000000..7ac453b
--- /dev/null
+++ b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.json
@@ -0,0 +1,78 @@
+{
+ "actions": [],
+ "creation": "2021-02-19 11:45:18.173417",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "leave_type",
+ "total_allocated_leaves",
+ "expired_leaves",
+ "used_leaves",
+ "pending_leaves",
+ "available_leaves"
+ ],
+ "fields": [
+ {
+ "fieldname": "leave_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Leave Type",
+ "no_copy": 1,
+ "options": "Leave Type",
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_allocated_leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Total Allocated Leave",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "expired_leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Expired Leave",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "used_leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Used Leave",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "pending_leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Pending Leave",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "available_leaves",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Available Leave",
+ "no_copy": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-19 10:47:48.546724",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Salary Slip Leave",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.py b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.py
new file mode 100644
index 0000000..7a92bf1
--- /dev/null
+++ b/erpnext/payroll/doctype/salary_slip_leave/salary_slip_leave.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class SalarySlipLeave(Document):
+ pass
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js
index 1378bf0..6aa1387 100755
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.js
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js
@@ -142,6 +142,8 @@
],
primary_action: function() {
var data = d.get_values();
+ delete data.company
+ delete data.currency
frappe.call({
doc: frm.doc,
method: "assign_salary_structure",
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.json b/erpnext/payroll/doctype/salary_structure/salary_structure.json
index de56fc8..5dd1d70 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.json
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.json
@@ -232,7 +232,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-09-30 11:30:32.190798",
+ "modified": "2021-03-31 15:41:12.342380",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure",
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py
index 1712081..352c180 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -100,7 +100,7 @@
from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab)
else:
assign_salary_structure_for_employees(employees, self,
- payroll_payable_account=payroll_payable_account,
+ payroll_payable_account=payroll_payable_account,
from_date=from_date, base=base, variable=variable, income_tax_slab=income_tax_slab)
else:
frappe.msgprint(_("No Employee Found"))
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
index 92bb347..50fabed 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.json
@@ -125,7 +125,6 @@
"options": "Income Tax Slab"
},
{
- "default": "Company:company:default_currency",
"depends_on": "eval:(doc.docstatus==1 || doc.salary_structure)",
"fetch_from": "salary_structure.currency",
"fieldname": "currency",
@@ -146,7 +145,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-30 18:07:48.251311",
+ "modified": "2021-03-31 15:49:36.361253",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Structure Assignment",
diff --git a/erpnext/portal/product_configurator/test_product_configurator.py b/erpnext/portal/product_configurator/test_product_configurator.py
index 97042db..3521e7e 100644
--- a/erpnext/portal/product_configurator/test_product_configurator.py
+++ b/erpnext/portal/product_configurator/test_product_configurator.py
@@ -10,8 +10,38 @@
test_dependencies = ["Item"]
class TestProductConfigurator(unittest.TestCase):
- def setUp(self):
- self.create_variant_item()
+ @classmethod
+ def setUpClass(cls):
+ cls.create_variant_item()
+
+ @classmethod
+ def create_variant_item(cls):
+ if not frappe.db.exists('Item', '_Test Variant Item - 2XL'):
+ frappe.get_doc({
+ "description": "_Test Variant Item - 2XL",
+ "item_code": "_Test Variant Item - 2XL",
+ "item_name": "_Test Variant Item - 2XL",
+ "doctype": "Item",
+ "is_stock_item": 1,
+ "variant_of": "_Test Variant Item",
+ "item_group": "_Test Item Group",
+ "stock_uom": "_Test UOM",
+ "item_defaults": [{
+ "company": "_Test Company",
+ "default_warehouse": "_Test Warehouse - _TC",
+ "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "buying_cost_center": "_Test Cost Center - _TC",
+ "selling_cost_center": "_Test Cost Center - _TC",
+ "income_account": "Sales - _TC"
+ }],
+ "attributes": [
+ {
+ "attribute": "Test Size",
+ "attribute_value": "2XL"
+ }
+ ],
+ "show_variant_in_website": 1
+ }).insert()
def test_product_list(self):
template_items = frappe.get_all('Item', {'show_in_website': 1})
@@ -46,39 +76,6 @@
def test_get_products_for_website(self):
items = get_products_for_website(attribute_filters={
- 'Test Size': ['Medium']
+ 'Test Size': ['2XL']
})
self.assertEqual(len(items), 1)
-
-
- def create_variant_item(self):
- if not frappe.db.exists('Item', '_Test Variant Item 1'):
- frappe.get_doc({
- "description": "_Test Variant Item 12",
- "doctype": "Item",
- "is_stock_item": 1,
- "variant_of": "_Test Variant Item",
- "item_code": "_Test Variant Item 1",
- "item_group": "_Test Item Group",
- "item_name": "_Test Variant Item 1",
- "stock_uom": "_Test UOM",
- "item_defaults": [{
- "company": "_Test Company",
- "default_warehouse": "_Test Warehouse - _TC",
- "expense_account": "_Test Account Cost for Goods Sold - _TC",
- "buying_cost_center": "_Test Cost Center - _TC",
- "selling_cost_center": "_Test Cost Center - _TC",
- "income_account": "Sales - _TC"
- }],
- "attributes": [
- {
- "attribute": "Test Size",
- "attribute_value": "Medium"
- }
- ],
- "show_variant_in_website": 1
- }).insert()
-
-
- def tearDown(self):
- frappe.db.rollback()
\ No newline at end of file
diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py
index 21fd7c2..d77eb2c 100644
--- a/erpnext/portal/product_configurator/utils.py
+++ b/erpnext/portal/product_configurator/utils.py
@@ -298,7 +298,7 @@
def get_items(filters=None, search=None):
- start = frappe.form_dict.start or 0
+ start = frappe.form_dict.get('start', 0)
products_settings = get_product_settings()
page_length = products_settings.products_per_page
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index 3570a0f..c5265e2 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -18,8 +18,8 @@
};
},
onload: function (frm) {
- var so = frappe.meta.get_docfield("Project", "sales_order");
- so.get_route_options_for_new_doc = function (field) {
+ const so = frm.get_docfield("sales_order");
+ so.get_route_options_for_new_doc = () => {
if (frm.is_new()) return;
return {
"customer": frm.doc.customer,
@@ -75,24 +75,27 @@
frm.add_custom_button(__('Cancelled'), () => {
frm.events.set_status(frm, 'Cancelled');
}, __('Set Status'));
- }
- if (frappe.model.can_read("Task")) {
- frm.add_custom_button(__("Gantt Chart"), function () {
- frappe.route_options = {
- "project": frm.doc.name
- };
- frappe.set_route("List", "Task", "Gantt");
- });
- frm.add_custom_button(__("Kanban Board"), () => {
- frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
- project: frm.doc.project_name
- }).then(() => {
- frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
+ if (frappe.model.can_read("Task")) {
+ frm.add_custom_button(__("Gantt Chart"), function () {
+ frappe.route_options = {
+ "project": frm.doc.name
+ };
+ frappe.set_route("List", "Task", "Gantt");
});
- });
+
+ frm.add_custom_button(__("Kanban Board"), () => {
+ frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
+ project: frm.doc.project_name
+ }).then(() => {
+ frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
+ });
+ });
+ }
}
+
+
},
create_duplicate: function(frm) {
@@ -135,4 +138,4 @@
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
-}
\ No newline at end of file
+}
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 8ba0b6c..f9e1359 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -81,12 +81,18 @@
def calculate_start_date(self, task_details):
self.start_date = add_days(self.expected_start_date, task_details.start)
- self.start_date = update_if_holiday(self.holiday_list, self.start_date)
+ self.start_date = self.update_if_holiday(self.start_date)
return self.start_date
def calculate_end_date(self, task_details):
self.end_date = add_days(self.start_date, task_details.duration)
- return update_if_holiday(self.holiday_list, self.end_date)
+ return self.update_if_holiday(self.end_date)
+
+ def update_if_holiday(self, date):
+ holiday_list = self.holiday_list or get_holiday_list(self.company)
+ while is_holiday(holiday_list, date):
+ date = add_days(date, 1)
+ return date
def dependency_mapping(self, template_tasks, project_tasks):
for template_task in template_tasks:
@@ -541,9 +547,3 @@
project.status = status
project.save()
-
-def update_if_holiday(holiday_list, date):
- holiday_list = holiday_list or get_holiday_list()
- while is_holiday(holiday_list, date):
- date = add_days(date, 1)
- return date
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index d85c826..a92bad1 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -4,13 +4,14 @@
import frappe, unittest
+from frappe.utils import getdate, nowdate, add_days
+
+from erpnext.projects.doctype.project_template.test_project_template import make_project_template
+from erpnext.projects.doctype.task.test_task import create_task
+
test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"]
-from erpnext.projects.doctype.project_template.test_project_template import make_project_template
-from erpnext.projects.doctype.project.project import update_if_holiday
-from erpnext.projects.doctype.task.test_task import create_task
-from frappe.utils import getdate, nowdate, add_days
class TestProject(unittest.TestCase):
def test_project_with_template_having_no_parent_and_depend_tasks(self):
@@ -37,7 +38,7 @@
task1 = task_exists("Test Template Task Parent")
if not task1:
- task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=1)
+ task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=4)
task2 = task_exists("Test Template Task Child 1")
if not task2:
@@ -52,7 +53,7 @@
tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
- self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 1))
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 4))
self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))
@@ -97,7 +98,8 @@
project_name = name,
status = 'Open',
project_template = template.name,
- expected_start_date = nowdate()
+ expected_start_date = nowdate(),
+ company="_Test Company"
)).insert()
return project
@@ -112,7 +114,8 @@
doctype = 'Project',
project_name = args.project_name,
status = 'Open',
- expected_start_date = args.start_date
+ expected_start_date = args.start_date,
+ company= args.company or '_Test Company'
))
if args.project_template_name:
@@ -131,7 +134,7 @@
def calculate_end_date(project, start, duration):
start = add_days(project.expected_start_date, start)
- start = update_if_holiday(project.holiday_list, start)
+ start = project.update_if_holiday(start)
end = add_days(start, duration)
- end = update_if_holiday(project.holiday_list, end)
- return getdate(end)
\ No newline at end of file
+ end = project.update_if_holiday(end)
+ return getdate(end)
diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json
index 69530b1..16caaa2 100644
--- a/erpnext/projects/doctype/project_template_task/project_template_task.json
+++ b/erpnext/projects/doctype/project_template_task/project_template_task.json
@@ -20,6 +20,7 @@
},
{
"columns": 6,
+ "fetch_from": "task.subject",
"fieldname": "subject",
"fieldtype": "Read Only",
"in_list_view": 1,
@@ -28,7 +29,7 @@
],
"istable": 1,
"links": [],
- "modified": "2021-01-07 15:13:40.995071",
+ "modified": "2021-02-24 15:18:49.095071",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Template Task",
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index a2095c9..855ff5f 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -17,312 +17,326 @@
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
class Task(NestedSet):
- nsm_parent_field = 'parent_task'
+ nsm_parent_field = 'parent_task'
- def get_feed(self):
- return '{0}: {1}'.format(_(self.status), self.subject)
+ def get_feed(self):
+ return '{0}: {1}'.format(_(self.status), self.subject)
- def get_customer_details(self):
- cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
- if cust:
- ret = {'customer_name': cust and cust[0][0] or ''}
- return ret
+ def get_customer_details(self):
+ cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
+ if cust:
+ ret = {'customer_name': cust and cust[0][0] or ''}
+ return ret
- def validate(self):
- self.validate_dates()
- self.validate_parent_project_dates()
- self.validate_progress()
- self.validate_status()
- self.update_depends_on()
- self.validate_dependencies_for_template_task()
+ def validate(self):
+ self.validate_dates()
+ self.validate_parent_expected_end_date()
+ self.validate_parent_project_dates()
+ self.validate_progress()
+ self.validate_status()
+ self.update_depends_on()
+ self.validate_dependencies_for_template_task()
- def validate_dates(self):
- if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
- frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
- frappe.bold("Expected End Date")))
+ def validate_dates(self):
+ if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
+ frappe.bold("Expected End Date")))
- if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
- frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
- frappe.bold("Actual End Date")))
+ if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
+ frappe.bold("Actual End Date")))
- def validate_parent_project_dates(self):
- if not self.project or frappe.flags.in_test:
- return
+ def validate_parent_expected_end_date(self):
+ if self.parent_task:
+ parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date")
+ if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date):
+ frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date)))
- expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
+ def validate_parent_project_dates(self):
+ if not self.project or frappe.flags.in_test:
+ return
- if expected_end_date:
- validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
- validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
+ expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
- def validate_status(self):
- if self.is_template and self.status != "Template":
- self.status = "Template"
- if self.status!=self.get_db_value("status") and self.status == "Completed":
- for d in self.depends_on:
- if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
- frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
+ if expected_end_date:
+ validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
+ validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
- close_all_assignments(self.doctype, self.name)
+ def validate_status(self):
+ if self.is_template and self.status != "Template":
+ self.status = "Template"
+ if self.status!=self.get_db_value("status") and self.status == "Completed":
+ for d in self.depends_on:
+ if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
+ frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
- def validate_progress(self):
- if flt(self.progress or 0) > 100:
- frappe.throw(_("Progress % for a task cannot be more than 100."))
+ close_all_assignments(self.doctype, self.name)
- if flt(self.progress) == 100:
- self.status = 'Completed'
+ def validate_progress(self):
+ if flt(self.progress or 0) > 100:
+ frappe.throw(_("Progress % for a task cannot be more than 100."))
- if self.status == 'Completed':
- self.progress = 100
+ if flt(self.progress) == 100:
+ self.status = 'Completed'
- def validate_dependencies_for_template_task(self):
- if self.is_template:
- self.validate_parent_template_task()
- self.validate_depends_on_tasks()
-
- def validate_parent_template_task(self):
- if self.parent_task:
- if not frappe.db.get_value("Task", self.parent_task, "is_template"):
- parent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(self.parent_task)
- frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
-
- def validate_depends_on_tasks(self):
- if self.depends_on:
- for task in self.depends_on:
- if not frappe.db.get_value("Task", task.task, "is_template"):
- dependent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(task.task)
- frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
+ if self.status == 'Completed':
+ self.progress = 100
- def update_depends_on(self):
- depends_on_tasks = self.depends_on_tasks or ""
- for d in self.depends_on:
- if d.task and d.task not in depends_on_tasks:
- depends_on_tasks += d.task + ","
- self.depends_on_tasks = depends_on_tasks
+ def validate_dependencies_for_template_task(self):
+ if self.is_template:
+ self.validate_parent_template_task()
+ self.validate_depends_on_tasks()
- def update_nsm_model(self):
- frappe.utils.nestedset.update_nsm(self)
+ def validate_parent_template_task(self):
+ if self.parent_task:
+ if not frappe.db.get_value("Task", self.parent_task, "is_template"):
+ parent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(self.parent_task)
+ frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
- def on_update(self):
- self.update_nsm_model()
- self.check_recursion()
- self.reschedule_dependent_tasks()
- self.update_project()
- self.unassign_todo()
- self.populate_depends_on()
+ def validate_depends_on_tasks(self):
+ if self.depends_on:
+ for task in self.depends_on:
+ if not frappe.db.get_value("Task", task.task, "is_template"):
+ dependent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(task.task)
+ frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
- def unassign_todo(self):
- if self.status == "Completed":
- close_all_assignments(self.doctype, self.name)
- if self.status == "Cancelled":
- clear(self.doctype, self.name)
+ def update_depends_on(self):
+ depends_on_tasks = self.depends_on_tasks or ""
+ for d in self.depends_on:
+ if d.task and d.task not in depends_on_tasks:
+ depends_on_tasks += d.task + ","
+ self.depends_on_tasks = depends_on_tasks
- def update_total_expense_claim(self):
- self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
- where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
+ def update_nsm_model(self):
+ frappe.utils.nestedset.update_nsm(self)
- def update_time_and_costing(self):
- tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
- sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
- sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
- ,self.name, as_dict=1)[0]
- if self.status == "Open":
- self.status = "Working"
- self.total_costing_amount= tl.total_costing_amount
- self.total_billing_amount= tl.total_billing_amount
- self.actual_time= tl.time
- self.act_start_date= tl.start_date
- self.act_end_date= tl.end_date
+ def on_update(self):
+ self.update_nsm_model()
+ self.check_recursion()
+ self.reschedule_dependent_tasks()
+ self.update_project()
+ self.unassign_todo()
+ self.populate_depends_on()
- def update_project(self):
- if self.project and not self.flags.from_project:
- frappe.get_cached_doc("Project", self.project).update_project()
+ def unassign_todo(self):
+ if self.status == "Completed":
+ close_all_assignments(self.doctype, self.name)
+ if self.status == "Cancelled":
+ clear(self.doctype, self.name)
- def check_recursion(self):
- if self.flags.ignore_recursion_check: return
- check_list = [['task', 'parent'], ['parent', 'task']]
- for d in check_list:
- task_list, count = [self.name], 0
- while (len(task_list) > count ):
- tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
- (d[0], d[1], '%s'), cstr(task_list[count]))
- count = count + 1
- for b in tasks:
- if b[0] == self.name:
- frappe.throw(_("Circular Reference Error"), CircularReferenceError)
- if b[0]:
- task_list.append(b[0])
+ def update_total_expense_claim(self):
+ self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
+ where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
- if count == 15:
- break
+ def update_time_and_costing(self):
+ tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
+ sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
+ sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
+ ,self.name, as_dict=1)[0]
+ if self.status == "Open":
+ self.status = "Working"
+ self.total_costing_amount= tl.total_costing_amount
+ self.total_billing_amount= tl.total_billing_amount
+ self.actual_time= tl.time
+ self.act_start_date= tl.start_date
+ self.act_end_date= tl.end_date
- def reschedule_dependent_tasks(self):
- end_date = self.exp_end_date or self.act_end_date
- if end_date:
- for task_name in frappe.db.sql("""
- select name from `tabTask` as parent
- where parent.project = %(project)s
- and parent.name in (
- select parent from `tabTask Depends On` as child
- where child.task = %(task)s and child.project = %(project)s)
- """, {'project': self.project, 'task':self.name }, as_dict=1):
- task = frappe.get_doc("Task", task_name.name)
- if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
- task_duration = date_diff(task.exp_end_date, task.exp_start_date)
- task.exp_start_date = add_days(end_date, 1)
- task.exp_end_date = add_days(task.exp_start_date, task_duration)
- task.flags.ignore_recursion_check = True
- task.save()
+ def update_project(self):
+ if self.project and not self.flags.from_project:
+ frappe.get_cached_doc("Project", self.project).update_project()
- def has_webform_permission(self):
- project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
- if project_user:
- return True
+ def check_recursion(self):
+ if self.flags.ignore_recursion_check: return
+ check_list = [['task', 'parent'], ['parent', 'task']]
+ for d in check_list:
+ task_list, count = [self.name], 0
+ while (len(task_list) > count ):
+ tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
+ (d[0], d[1], '%s'), cstr(task_list[count]))
+ count = count + 1
+ for b in tasks:
+ if b[0] == self.name:
+ frappe.throw(_("Circular Reference Error"), CircularReferenceError)
+ if b[0]:
+ task_list.append(b[0])
- def populate_depends_on(self):
- if self.parent_task:
- parent = frappe.get_doc('Task', self.parent_task)
- if self.name not in [row.task for row in parent.depends_on]:
- parent.append("depends_on", {
- "doctype": "Task Depends On",
- "task": self.name,
- "subject": self.subject
- })
- parent.save()
+ if count == 15:
+ break
- def on_trash(self):
- if check_if_child_exists(self.name):
- throw(_("Child Task exists for this Task. You can not delete this Task."))
+ def reschedule_dependent_tasks(self):
+ end_date = self.exp_end_date or self.act_end_date
+ if end_date:
+ for task_name in frappe.db.sql("""
+ select name from `tabTask` as parent
+ where parent.project = %(project)s
+ and parent.name in (
+ select parent from `tabTask Depends On` as child
+ where child.task = %(task)s and child.project = %(project)s)
+ """, {'project': self.project, 'task':self.name }, as_dict=1):
+ task = frappe.get_doc("Task", task_name.name)
+ if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
+ task_duration = date_diff(task.exp_end_date, task.exp_start_date)
+ task.exp_start_date = add_days(end_date, 1)
+ task.exp_end_date = add_days(task.exp_start_date, task_duration)
+ task.flags.ignore_recursion_check = True
+ task.save()
- self.update_nsm_model()
+ def has_webform_permission(self):
+ project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
+ if project_user:
+ return True
- def after_delete(self):
- self.update_project()
+ def populate_depends_on(self):
+ if self.parent_task:
+ parent = frappe.get_doc('Task', self.parent_task)
+ if self.name not in [row.task for row in parent.depends_on]:
+ parent.append("depends_on", {
+ "doctype": "Task Depends On",
+ "task": self.name,
+ "subject": self.subject
+ })
+ parent.save()
- def update_status(self):
- if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
- from datetime import datetime
- if self.exp_end_date < datetime.now().date():
- self.db_set('status', 'Overdue', update_modified=False)
- self.update_project()
+ def on_trash(self):
+ if check_if_child_exists(self.name):
+ throw(_("Child Task exists for this Task. You can not delete this Task."))
+
+ self.update_nsm_model()
+
+ def after_delete(self):
+ self.update_project()
+
+ def update_status(self):
+ if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
+ from datetime import datetime
+ if self.exp_end_date < datetime.now().date():
+ self.db_set('status', 'Overdue', update_modified=False)
+ self.update_project()
@frappe.whitelist()
def check_if_child_exists(name):
- child_tasks = frappe.get_all("Task", filters={"parent_task": name})
- child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
- return child_tasks
+ child_tasks = frappe.get_all("Task", filters={"parent_task": name})
+ child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
+ return child_tasks
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_project(doctype, txt, searchfield, start, page_len, filters):
- from erpnext.controllers.queries import get_match_cond
- return frappe.db.sql(""" select name from `tabProject`
- where %(key)s like %(txt)s
- %(mcond)s
- order by name
- limit %(start)s, %(page_len)s""" % {
- 'key': searchfield,
- 'txt': frappe.db.escape('%' + txt + '%'),
- 'mcond':get_match_cond(doctype),
- 'start': start,
- 'page_len': page_len
- })
+ from erpnext.controllers.queries import get_match_cond
+ meta = frappe.get_meta(doctype)
+ searchfields = meta.get_search_fields()
+ search_columns = ", " + ", ".join(searchfields) if searchfields else ''
+ search_cond = " or " + " or ".join([field + " like %(txt)s" for field in searchfields])
+
+ return frappe.db.sql(""" select name {search_columns} from `tabProject`
+ where %(key)s like %(txt)s
+ %(mcond)s
+ {search_condition}
+ order by name
+ limit %(start)s, %(page_len)s""".format(search_columns = search_columns,
+ search_condition=search_cond), {
+ 'key': searchfield,
+ 'txt': '%' + txt + '%',
+ 'mcond':get_match_cond(doctype),
+ 'start': start,
+ 'page_len': page_len
+ })
@frappe.whitelist()
def set_multiple_status(names, status):
- names = json.loads(names)
- for name in names:
- task = frappe.get_doc("Task", name)
- task.status = status
- task.save()
+ names = json.loads(names)
+ for name in names:
+ task = frappe.get_doc("Task", name)
+ task.status = status
+ task.save()
def set_tasks_as_overdue():
- tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
- for task in tasks:
- if task.status == "Pending Review":
- if getdate(task.review_date) > getdate(today()):
- continue
- frappe.get_doc("Task", task.name).update_status()
+ tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
+ for task in tasks:
+ if task.status == "Pending Review":
+ if getdate(task.review_date) > getdate(today()):
+ continue
+ frappe.get_doc("Task", task.name).update_status()
@frappe.whitelist()
def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
- def set_missing_values(source, target):
- target.append("time_logs", {
- "hours": source.actual_time,
- "completed": source.status == "Completed",
- "project": source.project,
- "task": source.name
- })
+ def set_missing_values(source, target):
+ target.append("time_logs", {
+ "hours": source.actual_time,
+ "completed": source.status == "Completed",
+ "project": source.project,
+ "task": source.name
+ })
- doclist = get_mapped_doc("Task", source_name, {
- "Task": {
- "doctype": "Timesheet"
- }
- }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
+ doclist = get_mapped_doc("Task", source_name, {
+ "Task": {
+ "doctype": "Timesheet"
+ }
+ }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
- return doclist
+ return doclist
@frappe.whitelist()
def get_children(doctype, parent, task=None, project=None, is_root=False):
- filters = [['docstatus', '<', '2']]
+ filters = [['docstatus', '<', '2']]
- if task:
- filters.append(['parent_task', '=', task])
- elif parent and not is_root:
- # via expand child
- filters.append(['parent_task', '=', parent])
- else:
- filters.append(['ifnull(`parent_task`, "")', '=', ''])
+ if task:
+ filters.append(['parent_task', '=', task])
+ elif parent and not is_root:
+ # via expand child
+ filters.append(['parent_task', '=', parent])
+ else:
+ filters.append(['ifnull(`parent_task`, "")', '=', ''])
- if project:
- filters.append(['project', '=', project])
+ if project:
+ filters.append(['project', '=', project])
- tasks = frappe.get_list(doctype, fields=[
- 'name as value',
- 'subject as title',
- 'is_group as expandable'
- ], filters=filters, order_by='name')
+ tasks = frappe.get_list(doctype, fields=[
+ 'name as value',
+ 'subject as title',
+ 'is_group as expandable'
+ ], filters=filters, order_by='name')
- # return tasks
- return tasks
+ # return tasks
+ return tasks
@frappe.whitelist()
def add_node():
- from frappe.desk.treeview import make_tree_args
- args = frappe.form_dict
- args.update({
- "name_field": "subject"
- })
- args = make_tree_args(**args)
+ from frappe.desk.treeview import make_tree_args
+ args = frappe.form_dict
+ args.update({
+ "name_field": "subject"
+ })
+ args = make_tree_args(**args)
- if args.parent_task == 'All Tasks' or args.parent_task == args.project:
- args.parent_task = None
+ if args.parent_task == 'All Tasks' or args.parent_task == args.project:
+ args.parent_task = None
- frappe.get_doc(args).insert()
+ frappe.get_doc(args).insert()
@frappe.whitelist()
def add_multiple_tasks(data, parent):
- data = json.loads(data)
- new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
- new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
+ data = json.loads(data)
+ new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
+ new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
- for d in data:
- if not d.get("subject"): continue
- new_doc['subject'] = d.get("subject")
- new_task = frappe.get_doc(new_doc)
- new_task.insert()
+ for d in data:
+ if not d.get("subject"): continue
+ new_doc['subject'] = d.get("subject")
+ new_task = frappe.get_doc(new_doc)
+ new_task.insert()
def on_doctype_update():
- frappe.db.add_index("Task", ["lft", "rgt"])
+ frappe.db.add_index("Task", ["lft", "rgt"])
def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
- if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
- frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
+ if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
+ frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
- if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
- frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
+ if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
+ frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index 4cb3804..f7c764e 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -13,9 +13,18 @@
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.payroll.doctype.salary_structure.test_salary_structure \
import make_salary_structure, create_salary_structure_assignment
+from erpnext.payroll.doctype.salary_slip.test_salary_slip import (
+ make_earning_salary_component,
+ make_deduction_salary_component
+)
from erpnext.hr.doctype.employee.test_employee import make_employee
class TestTimesheet(unittest.TestCase):
+ @classmethod
+ def setUpClass(cls):
+ make_earning_salary_component(setup=True, company_list=['_Test Company'])
+ make_deduction_salary_component(setup=True, company_list=['_Test Company'])
+
def setUp(self):
for dt in ["Salary Slip", "Salary Structure", "Salary Structure Assignment", "Timesheet"]:
frappe.db.sql("delete from `tab%s`" % dt)
@@ -49,7 +58,7 @@
self.assertEqual(timesheet.total_billable_amount, 0)
def test_salary_slip_from_timesheet(self):
- emp = make_employee("test_employee_6@salary.com")
+ emp = make_employee("test_employee_6@salary.com", company="_Test Company")
salary_structure = make_salary_structure_for_timesheet(emp)
timesheet = make_timesheet(emp, simulate = True, billable=1)
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index ea81b3e..ed02f79 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -204,14 +204,16 @@
ts_detail.billing_rate = 0.0
@frappe.whitelist()
-def get_projectwise_timesheet_data(project, parent=None):
- cond = ''
+def get_projectwise_timesheet_data(project, parent=None, from_time=None, to_time=None):
+ condition = ''
if parent:
- cond = "and parent = %(parent)s"
+ condition = "AND parent = %(parent)s"
+ if from_time and to_time:
+ condition += "AND from_time BETWEEN %(from_time)s AND %(to_time)s"
return frappe.db.sql("""select name, parent, billing_hours, billing_amount as billing_amt
from `tabTimesheet Detail` where parenttype = 'Timesheet' and docstatus=1 and project = %(project)s {0} and billable = 1
- and sales_invoice is null""".format(cond), {'project': project, 'parent': parent}, as_dict=1)
+ and sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 7326238..7a3cb83 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -61,5 +61,10 @@
"selling/page/point_of_sale/pos_past_order_list.js",
"selling/page/point_of_sale/pos_past_order_summary.js",
"selling/page/point_of_sale/pos_controller.js"
+ ],
+ "js/bank-reconciliation-tool.min.js": [
+ "public/js/bank_reconciliation_tool/data_table_manager.js",
+ "public/js/bank_reconciliation_tool/number_card.js",
+ "public/js/bank_reconciliation_tool/dialog_manager.js"
]
}
diff --git a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
new file mode 100644
index 0000000..5bb58fa
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
@@ -0,0 +1,220 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.dialog_manager = new erpnext.accounts.bank_reconciliation.DialogManager(
+ this.company,
+ this.bank_account
+ );
+ this.make_dt();
+ }
+
+ make_dt() {
+ var me = this;
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions",
+ args: {
+ bank_account: this.bank_account,
+ },
+ callback: function (response) {
+ me.format_data(response.message);
+ me.get_dt_columns();
+ me.get_datatable();
+ me.set_listeners();
+ },
+ });
+ }
+
+ get_dt_columns() {
+ this.columns = [
+ {
+ name: "Date",
+ editable: false,
+ width: 100,
+ },
+
+ {
+ name: "Party Type",
+ editable: false,
+ width: 95,
+ },
+ {
+ name: "Party",
+ editable: false,
+ width: 100,
+ },
+ {
+ name: "Description",
+ editable: false,
+ width: 350,
+ },
+ {
+ name: "Deposit",
+ editable: false,
+ width: 100,
+ format: (value) =>
+ "<span style='color:green;'>" +
+ format_currency(value, this.currency) +
+ "</span>",
+ },
+ {
+ name: "Withdrawal",
+ editable: false,
+ width: 100,
+ format: (value) =>
+ "<span style='color:red;'>" +
+ format_currency(value, this.currency) +
+ "</span>",
+ },
+ {
+ name: "Unallocated Amount",
+ editable: false,
+ width: 100,
+ format: (value) =>
+ "<span style='color:blue;'>" +
+ format_currency(value, this.currency) +
+ "</span>",
+ },
+ {
+ name: "Reference Number",
+ editable: false,
+ width: 140,
+ },
+ {
+ name: "Actions",
+ editable: false,
+ sortable: false,
+ focusable: false,
+ dropdown: false,
+ width: 80,
+ },
+ ];
+ }
+
+ format_data(transactions) {
+ this.transactions = [];
+ if (transactions[0]) {
+ this.currency = transactions[0]["currency"];
+ }
+ this.transaction_dt_map = {};
+ let length;
+ transactions.forEach((row) => {
+ length = this.transactions.push(this.format_row(row));
+ this.transaction_dt_map[row["name"]] = length - 1;
+ });
+ }
+
+ format_row(row) {
+ return [
+ row["date"],
+ row["party_type"],
+ row["party"],
+ row["description"],
+ row["deposit"],
+ row["withdrawal"],
+ row["unallocated_amount"],
+ row["reference_number"],
+ `
+ <Button class="btn btn-primary btn-xs center" data-name = ${row["name"]} >
+ Actions
+ </a>
+ `,
+ ];
+ }
+
+ get_datatable() {
+ const datatable_options = {
+ columns: this.columns,
+ data: this.transactions,
+ dynamicRowHeight: true,
+ checkboxColumn: false,
+ inlineFilters: true,
+ };
+ this.datatable = new frappe.DataTable(
+ this.$reconciliation_tool_dt.get(0),
+ datatable_options
+ );
+ $(`.${this.datatable.style.scopeClass} .dt-scrollable`).css(
+ "max-height",
+ "calc(100vh - 400px)"
+ );
+
+ if (this.transactions.length > 0) {
+ this.$reconciliation_tool_dt.show();
+ this.$no_bank_transactions.hide();
+ } else {
+ this.$reconciliation_tool_dt.hide();
+ this.$no_bank_transactions.show();
+ }
+ }
+
+ set_listeners() {
+ var me = this;
+ $(`.${this.datatable.style.scopeClass} .dt-scrollable`).on(
+ "click",
+ `.btn`,
+ function () {
+ me.dialog_manager.show_dialog(
+ $(this).attr("data-name"),
+ (bank_transaction) => me.update_dt_cards(bank_transaction)
+ );
+ return true;
+ }
+ );
+ }
+
+ update_dt_cards(bank_transaction) {
+ const transaction_index = this.transaction_dt_map[
+ bank_transaction.name
+ ];
+ if (bank_transaction.unallocated_amount > 0) {
+ this.transactions[transaction_index] = this.format_row(
+ bank_transaction
+ );
+ } else {
+ this.transactions.splice(transaction_index, 1);
+ }
+ this.datatable.refresh(this.transactions, this.columns);
+
+ if (this.transactions.length == 0) {
+ this.$reconciliation_tool_dt.hide();
+ this.$no_bank_transactions.show();
+ }
+
+ // this.make_dt();
+ this.get_cleared_balance().then(() => {
+ this.cards_manager.$cards[1].set_value(
+ format_currency(this.cleared_balance),
+ this.currency
+ );
+ this.cards_manager.$cards[2].set_value(
+ format_currency(
+ this.bank_statement_closing_balance - this.cleared_balance
+ ),
+ this.currency
+ );
+ this.cards_manager.$cards[2].set_value_color(
+ this.bank_statement_closing_balance - this.cleared_balance == 0
+ ? "text-success"
+ : "text-danger"
+ );
+ });
+ }
+
+ get_cleared_balance() {
+ if (this.bank_account && this.bank_statement_to_date) {
+ return frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
+ args: {
+ bank_account: this.bank_account,
+ till_date: this.bank_statement_to_date,
+ },
+ callback: (response) =>
+ (this.cleared_balance = response.message),
+ });
+ }
+ }
+};
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
new file mode 100644
index 0000000..142fe79
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -0,0 +1,594 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
+ constructor(company, bank_account) {
+ this.bank_account = bank_account;
+ this.company = company;
+ this.make_dialog();
+ }
+
+ show_dialog(bank_transaction_name, update_dt_cards) {
+ this.bank_transaction_name = bank_transaction_name;
+ this.update_dt_cards = update_dt_cards;
+ frappe.call({
+ method: "frappe.client.get_value",
+ args: {
+ doctype: "Bank Transaction",
+ filters: { name: this.bank_transaction_name },
+ fieldname: [
+ "date",
+ "deposit",
+ "withdrawal",
+ "currency",
+ "description",
+ "name",
+ "bank_account",
+ "company",
+ "reference_number",
+ "party_type",
+ "party",
+ "unallocated_amount",
+ "allocated_amount",
+ ],
+ },
+ callback: (r) => {
+ if (r.message) {
+ this.bank_transaction = r.message;
+ r.message.payment_entry = 1;
+ this.dialog.set_values(r.message);
+ this.dialog.show();
+ }
+ },
+ });
+ }
+
+ get_linked_vouchers(document_types) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_linked_payments",
+ args: {
+ bank_transaction_name: this.bank_transaction_name,
+ document_types: document_types,
+ },
+
+ callback: (result) => {
+ const data = result.message;
+
+
+ if (data && data.length > 0) {
+ const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper;
+ proposals_wrapper.show();
+ this.dialog.fields_dict.no_matching_vouchers.$wrapper.hide();
+ this.data = [];
+ data.forEach((row) => {
+ const reference_date = row[5] ? row[5] : row[8];
+ this.data.push([
+ row[1],
+ row[2],
+ reference_date,
+ format_currency(row[3], row[9]),
+ row[6],
+ row[4],
+ ]);
+ });
+ this.get_dt_columns();
+ this.get_datatable(proposals_wrapper);
+ } else {
+ const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper;
+ proposals_wrapper.hide();
+ this.dialog.fields_dict.no_matching_vouchers.$wrapper.show();
+
+ }
+ this.dialog.show();
+ },
+ });
+ }
+
+ get_dt_columns() {
+ this.columns = [
+ {
+ name: "Document Type",
+ editable: false,
+ width: 125,
+ },
+ {
+ name: "Document Name",
+ editable: false,
+ width: 150,
+ },
+ {
+ name: "Reference Date",
+ editable: false,
+ width: 120,
+ },
+ {
+ name: "Amount",
+ editable: false,
+ width: 100,
+ },
+ {
+ name: "Party",
+ editable: false,
+ width: 120,
+ },
+
+ {
+ name: "Reference Number",
+ editable: false,
+ width: 140,
+ },
+ ];
+ }
+
+ get_datatable(proposals_wrapper) {
+ if (!this.datatable) {
+ const datatable_options = {
+ columns: this.columns,
+ data: this.data,
+ dynamicRowHeight: true,
+ checkboxColumn: true,
+ inlineFilters: true,
+ };
+ this.datatable = new frappe.DataTable(
+ proposals_wrapper.get(0),
+ datatable_options
+ );
+ } else {
+ this.datatable.refresh(this.data, this.columns);
+ this.datatable.rowmanager.checkMap = [];
+ }
+ }
+
+ make_dialog() {
+ const me = this;
+ me.selected_payment = null;
+
+ const fields = [
+ {
+ label: __("Action"),
+ fieldname: "action",
+ fieldtype: "Select",
+ options: `Match Against Voucher\nCreate Voucher\nUpdate Bank Transaction`,
+ default: "Match Against Voucher",
+ },
+ {
+ fieldname: "column_break_4",
+ fieldtype: "Column Break",
+ },
+ {
+ label: __("Document Type"),
+ fieldname: "document_type",
+ fieldtype: "Select",
+ options: `Payment Entry\nJournal Entry`,
+ default: "Payment Entry",
+ depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "section_break_1",
+ label: __("Filters"),
+ depends_on: "eval:doc.action=='Match Against Voucher'",
+ },
+ {
+ fieldtype: "Check",
+ label: "Payment Entry",
+ fieldname: "payment_entry",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldtype: "Check",
+ label: "Journal Entry",
+ fieldname: "journal_entry",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldname: "column_break_5",
+ fieldtype: "Column Break",
+ },
+ {
+ fieldtype: "Check",
+ label: "Sales Invoice",
+ fieldname: "sales_invoice",
+ onchange: () => this.update_options(),
+ },
+
+ {
+ fieldtype: "Check",
+ label: "Purchase Invoice",
+ fieldname: "purchase_invoice",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldname: "column_break_5",
+ fieldtype: "Column Break",
+ },
+ {
+ fieldtype: "Check",
+ label: "Expense Claim",
+ fieldname: "expense_claim",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldtype: "Check",
+ label: "Show Only Exact Amount",
+ fieldname: "exact_match",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "section_break_1",
+ label: __("Select Vouchers to Match"),
+ depends_on: "eval:doc.action=='Match Against Voucher'",
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: "payment_proposals",
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: "no_matching_vouchers",
+ options: "<div class='text-muted text-center'>No Matching Vouchers Found</div>"
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "details",
+ label: "Details",
+ depends_on: "eval:doc.action!='Match Against Voucher'",
+ },
+ {
+ fieldname: "reference_number",
+ fieldtype: "Data",
+ label: "Reference Number",
+ mandatory_depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ default: "Today",
+ fieldname: "posting_date",
+ fieldtype: "Date",
+ label: "Posting Date",
+ reqd: 1,
+ depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldname: "reference_date",
+ fieldtype: "Date",
+ label: "Cheque/Reference Date",
+ mandatory_depends_on: "eval:doc.action=='Create Voucher'",
+ depends_on: "eval:doc.action=='Create Voucher'",
+ reqd: 1,
+ },
+ {
+ fieldname: "mode_of_payment",
+ fieldtype: "Link",
+ label: "Mode of Payment",
+ options: "Mode of Payment",
+ depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldname: "edit_in_full_page",
+ fieldtype: "Button",
+ label: "Edit in Full Page",
+ click: () => {
+ this.edit_in_full_page();
+ },
+ depends_on:
+ "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldname: "column_break_7",
+ fieldtype: "Column Break",
+ },
+ {
+ default: "Journal Entry Type",
+ fieldname: "journal_entry_type",
+ fieldtype: "Select",
+ label: "Journal Entry Type",
+ options:
+ "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ },
+ {
+ fieldname: "second_account",
+ fieldtype: "Link",
+ label: "Account",
+ options: "Account",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ get_query: () => {
+ return {
+ filters: {
+ is_group: 0,
+ company: this.company,
+ },
+ };
+ },
+ },
+ {
+ fieldname: "party_type",
+ fieldtype: "Link",
+ label: "Party Type",
+ options: "DocType",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ get_query: function () {
+ return {
+ filters: {
+ name: [
+ "in",
+ Object.keys(frappe.boot.party_account_types),
+ ],
+ },
+ };
+ },
+ },
+ {
+ fieldname: "party",
+ fieldtype: "Dynamic Link",
+ label: "Party",
+ options: "party_type",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ },
+ {
+ fieldname: "project",
+ fieldtype: "Link",
+ label: "Project",
+ options: "Project",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ },
+ {
+ fieldname: "cost_center",
+ fieldtype: "Link",
+ label: "Cost Center",
+ options: "Cost Center",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "details_section",
+ label: "Transaction Details",
+ collapsible: 1,
+ },
+ {
+ fieldname: "deposit",
+ fieldtype: "Currency",
+ label: "Deposit",
+ read_only: 1,
+ },
+ {
+ fieldname: "withdrawal",
+ fieldtype: "Currency",
+ label: "Withdrawal",
+ read_only: 1,
+ },
+ {
+ fieldname: "description",
+ fieldtype: "Small Text",
+ label: "Description",
+ read_only: 1,
+ },
+ {
+ fieldname: "column_break_17",
+ fieldtype: "Column Break",
+ read_only: 1,
+ },
+ {
+ fieldname: "allocated_amount",
+ fieldtype: "Currency",
+ label: "Allocated Amount",
+ read_only: 1,
+ },
+
+ {
+ fieldname: "unallocated_amount",
+ fieldtype: "Currency",
+ label: "Unallocated Amount",
+ read_only: 1,
+ },
+ ];
+
+ me.dialog = new frappe.ui.Dialog({
+ title: __("Reconcile the Bank Transaction"),
+ fields: fields,
+ size: "large",
+ primary_action: (values) =>
+ this.reconciliation_dialog_primary_action(values),
+ });
+ }
+
+ get_selected_attributes() {
+ let selected_attributes = [];
+ this.dialog.$wrapper.find(".checkbox input").each((i, col) => {
+ if ($(col).is(":checked")) {
+ selected_attributes.push($(col).attr("data-fieldname"));
+ }
+ });
+
+ return selected_attributes;
+ }
+
+ update_options() {
+ let selected_attributes = this.get_selected_attributes();
+ this.get_linked_vouchers(selected_attributes);
+ }
+
+ reconciliation_dialog_primary_action(values) {
+ if (values.action == "Match Against Voucher") this.match(values);
+ if (
+ values.action == "Create Voucher" &&
+ values.document_type == "Payment Entry"
+ )
+ this.add_payment_entry(values);
+ if (
+ values.action == "Create Voucher" &&
+ values.document_type == "Journal Entry"
+ )
+ this.add_journal_entry(values);
+ else if (values.action == "Update Bank Transaction")
+ this.update_transaction(values);
+ }
+
+ match() {
+ var selected_map = this.datatable.rowmanager.checkMap;
+ let rows = [];
+ selected_map.forEach((val, index) => {
+ if (val == 1) rows.push(this.datatable.datamanager.rows[index]);
+ });
+ let vouchers = [];
+ rows.forEach((x) => {
+ vouchers.push({
+ payment_doctype: x[2].content,
+ payment_name: x[3].content,
+ amount: x[5].content,
+ });
+ });
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.reconcile_vouchers",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ vouchers: vouchers,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " Matched";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ add_payment_entry(values) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ project: values.project,
+ cost_center: values.cost_center,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " added as Payment Entry";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ add_journal_entry(values) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ entry_type: values.journal_entry_type,
+ second_account: values.second_account,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " added as Journal Entry";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ update_transaction(values) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.update_bank_transaction",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ party_type: values.party_type,
+ party: values.party,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " updated";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ edit_in_full_page() {
+ const values = this.dialog.get_values(true);
+ if (values.document_type == "Payment Entry") {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ project: values.project,
+ cost_center: values.cost_center,
+ allow_edit: true
+ },
+ callback: (r) => {
+ const doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ },
+ });
+ } else {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ entry_type: values.journal_entry_type,
+ second_account: values.second_account,
+ allow_edit: true
+ },
+ callback: (r) => {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ },
+ });
+ }
+ }
+
+};
diff --git a/erpnext/public/js/bank_reconciliation_tool/number_card.js b/erpnext/public/js/bank_reconciliation_tool/number_card.js
new file mode 100644
index 0000000..e10d109
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool/number_card.js
@@ -0,0 +1,75 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.NumberCardManager = class NumberCardManager {
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.make_cards();
+ }
+
+ make_cards() {
+ this.$reconciliation_tool_cards.empty();
+ this.$cards = [];
+ this.$summary = $(`<div class="report-summary"></div>`)
+ .hide()
+ .appendTo(this.$reconciliation_tool_cards);
+ var chart_data = [
+ {
+ value: this.bank_statement_closing_balance,
+ label: "Closing Balance as per Bank Statement",
+ datatype: "Currency",
+ currency: this.currency,
+ },
+ {
+ value: this.cleared_balance,
+ label: "Closing Balance as per ERP",
+ datatype: "Currency",
+ currency: this.currency,
+ },
+ {
+ value:
+ this.bank_statement_closing_balance - this.cleared_balance,
+ label: "Difference",
+ datatype: "Currency",
+ currency: this.currency,
+ },
+ ];
+
+ chart_data.forEach((summary) => {
+ let number_card = new erpnext.accounts.NumberCard(summary);
+ this.$cards.push(number_card);
+
+ number_card.$card.appendTo(this.$summary);
+ });
+ this.$cards[2].set_value_color(
+ this.bank_statement_closing_balance - this.cleared_balance == 0
+ ? "text-success"
+ : "text-danger"
+ );
+ this.$summary.css({"border-bottom": "0px", "margin-left": "0px", "margin-right": "0px"});
+ this.$summary.show();
+ }
+};
+
+erpnext.accounts.NumberCard = class NumberCard {
+ constructor(options) {
+ this.$card = frappe.utils.build_summary_item(options);
+ }
+
+ set_value(value) {
+ this.$card.find("div").text(value);
+ }
+
+ set_value_color(color) {
+ this.$card
+ .find("div")
+ .removeClass("text-danger text-success")
+ .addClass(`${color}`);
+ }
+
+ set_indicator(color) {
+ this.$card
+ .find("span")
+ .removeClass("indicator red green")
+ .addClass(`indicator ${color}`);
+ }
+};
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index c963866..67b12fb 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -141,29 +141,6 @@
this.apply_price_list();
},
- price_list_rate: function(doc, cdt, cdn) {
- var item = frappe.get_doc(cdt, cdn);
-
- frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
-
- let item_rate = item.price_list_rate;
- if (doc.doctype == "Purchase Order" && item.blanket_order_rate) {
- item_rate = item.blanket_order_rate;
- }
-
- if (item.discount_percentage) {
- item.discount_amount = flt(item_rate) * flt(item.discount_percentage) / 100;
- }
-
- if (item.discount_amount) {
- item.rate = flt((item.price_list_rate) - (item.discount_amount), precision('rate', item));
- } else {
- item.rate = item_rate;
- }
-
- this.calculate_taxes_and_totals();
- },
-
discount_percentage: function(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
item.discount_amount = 0.0;
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 416495c..3a3ee38 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -2,7 +2,9 @@
// License: GNU General Public License v3. See license.txt
erpnext.taxes_and_totals = erpnext.payments.extend({
- setup: function() {},
+ setup: function() {
+ this.fetch_round_off_accounts();
+ },
apply_pricing_rule_on_item: function(item) {
let effective_item_rate = item.price_list_rate;
@@ -152,6 +154,24 @@
});
},
+ fetch_round_off_accounts: function() {
+ let me = this;
+ frappe.flags.round_off_applicable_accounts = [];
+
+ if (me.frm.doc.company) {
+ return frappe.call({
+ "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
+ "args": {
+ "company": me.frm.doc.company,
+ "account_list": frappe.flags.round_off_applicable_accounts
+ },
+ callback: function(r) {
+ frappe.flags.round_off_applicable_accounts.push(...r.message);
+ }
+ });
+ }
+ },
+
determine_exclusive_rate: function() {
var me = this;
@@ -372,11 +392,21 @@
} else if (tax.charge_type == "On Item Quantity") {
current_tax_amount = tax_rate * item.qty;
}
+
+ current_tax_amount = this.get_final_tax_amount(tax, current_tax_amount);
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
return current_tax_amount;
},
+ get_final_tax_amount: function(tax, current_tax_amount) {
+ if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) {
+ current_tax_amount = Math.round(current_tax_amount);
+ }
+
+ return current_tax_amount;
+ },
+
set_item_wise_tax: function(item, tax, tax_rate, current_tax_amount) {
// store tax breakup for each item
let tax_detail = tax.item_wise_tax_detail;
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index e5f9049..21a20a7 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -577,7 +577,7 @@
var d = locals[cdt][cdn];
me.add_taxes_from_item_tax_template(d.item_tax_rate);
if (d.free_item_data) {
- me.apply_product_discount(d.free_item_data);
+ me.apply_product_discount(d);
}
},
() => {
@@ -649,6 +649,40 @@
}
},
+ price_list_rate: function(doc, cdt, cdn) {
+ var item = frappe.get_doc(cdt, cdn);
+ frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
+
+ // check if child doctype is Sales Order Item/Qutation Item and calculate the rate
+ if (in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Purchase Receipt Item"]), cdt)
+ this.apply_pricing_rule_on_item(item);
+ else
+ item.rate = flt(item.price_list_rate * (1 - item.discount_percentage / 100.0),
+ precision("rate", item));
+
+ this.calculate_taxes_and_totals();
+ },
+
+ margin_rate_or_amount: function(doc, cdt, cdn) {
+ // calculated the revised total margin and rate on margin rate changes
+ let item = frappe.get_doc(cdt, cdn);
+ this.apply_pricing_rule_on_item(item);
+ this.calculate_taxes_and_totals();
+ cur_frm.refresh_fields();
+ },
+
+ margin_type: function(doc, cdt, cdn) {
+ // calculate the revised total margin and rate on margin type changes
+ let item = frappe.get_doc(cdt, cdn);
+ if (!item.margin_type) {
+ frappe.model.set_value(cdt, cdn, "margin_rate_or_amount", 0);
+ } else {
+ this.apply_pricing_rule_on_item(item, doc, cdt, cdn);
+ this.calculate_taxes_and_totals();
+ cur_frm.refresh_fields();
+ }
+ },
+
get_incoming_rate: function(item, posting_date, posting_time, voucher_type, company) {
let item_args = {
@@ -704,21 +738,15 @@
}
else {
var valid_serial_nos = [];
-
+ var serialnos = [];
// Replacing all occurences of comma with carriage return
- var serial_nos = item.serial_no.trim().replace(/,/g, '\n');
-
- serial_nos = serial_nos.trim().split('\n');
-
- // Trim each string and push unique string to new list
- for (var x=0; x<=serial_nos.length - 1; x++) {
- if (serial_nos[x].trim() != "" && valid_serial_nos.indexOf(serial_nos[x].trim()) == -1) {
- valid_serial_nos.push(serial_nos[x].trim());
+ item.serial_no = item.serial_no.replace(/,/g, '\n');
+ serialnos = item.serial_no.split("\n");
+ for (var i = 0; i < serialnos.length; i++) {
+ if (serialnos[i] != "") {
+ valid_serial_nos.push(serialnos[i]);
}
}
-
- // Add the new list to the serial no. field in grid with each in new line
- item.serial_no = valid_serial_nos.join('\n');
item.conversion_factor = item.conversion_factor || 1;
refresh_field("serial_no", item.name, item.parentfield);
@@ -1030,7 +1058,7 @@
},
set_margin_amount_based_on_currency: function(exchange_rate) {
- if (in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]), this.frm.doc.doctype) {
+ if (in_list(["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "Purchase Invoice", "Purchase Order", "Purchase Receipt"]), this.frm.doc.doctype) {
var me = this;
$.each(this.frm.doc.items || [], function(i, d) {
if(d.margin_type == "Amount") {
@@ -1139,6 +1167,11 @@
this.calculate_net_weight();
}
+ // for handling customization not to fetch price list rate
+ if(frappe.flags.dont_fetch_price_list_rate) {
+ return
+ }
+
if (!dont_fetch_price_list_rate &&
frappe.meta.has_field(doc.doctype, "price_list_currency")) {
this.apply_price_list(item, true);
@@ -1170,7 +1203,7 @@
calculate_stock_uom_rate: function(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
- item.stock_uom_rate = flt(item.rate)/flt(item.conversion_factor);
+ item.stock_uom_rate = flt(item.rate)/flt(item.conversion_factor);
refresh_field("stock_uom_rate", item.name, item.parentfield);
},
service_stop_date: function(frm, cdt, cdn) {
@@ -1280,10 +1313,10 @@
change_grid_labels: function(company_currency) {
var me = this;
- this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount"],
+ this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount", "base_rate_with_margin"],
company_currency, "items");
- this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate"],
+ this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate", "rate_with_margin"],
this.frm.doc.currency, "items");
if(this.frm.fields_dict["operations"]) {
@@ -1321,7 +1354,7 @@
// toggle columns
var item_grid = this.frm.fields_dict["items"].grid;
- $.each(["base_rate", "base_price_list_rate", "base_amount"], function(i, fname) {
+ $.each(["base_rate", "base_price_list_rate", "base_amount", "base_rate_with_margin"], function(i, fname) {
if(frappe.meta.get_docfield(item_grid.doctype, fname))
item_grid.set_column_disp(fname, me.frm.doc.currency != company_currency);
});
@@ -1468,7 +1501,7 @@
});
// if doctype is Quotation Item / Sales Order Iten then add Margin Type and rate in item_list
- if (in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item"]), d.doctype){
+ if (in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Purchase Receipt Item"]), d.doctype) {
item_list[0]["margin_type"] = d.margin_type;
item_list[0]["margin_rate_or_amount"] = d.margin_rate_or_amount;
}
@@ -1499,7 +1532,10 @@
if(k=="price_list_rate") {
if(flt(v) != flt(d.price_list_rate)) price_list_rate_changed = true;
}
- frappe.model.set_value(d.doctype, d.name, k, v);
+
+ if (k !== 'free_item_data') {
+ frappe.model.set_value(d.doctype, d.name, k, v);
+ }
}
}
@@ -1511,7 +1547,7 @@
}
if (d.free_item_data) {
- me.apply_product_discount(d.free_item_data);
+ me.apply_product_discount(d);
}
if (d.apply_rule_on_other_items) {
@@ -1545,20 +1581,31 @@
}
},
- apply_product_discount: function(free_item_data) {
- const items = this.frm.doc.items.filter(d => (d.item_code == free_item_data.item_code
- && d.is_free_item)) || [];
+ apply_product_discount: function(args) {
+ const items = this.frm.doc.items.filter(d => (d.is_free_item)) || [];
- if (!items.length) {
- let row_to_modify = frappe.model.add_child(this.frm.doc,
- this.frm.doc.doctype + ' Item', 'items');
+ const exist_items = items.map(row => (row.item_code, row.pricing_rules));
- for (let key in free_item_data) {
- row_to_modify[key] = free_item_data[key];
+ args.free_item_data.forEach(pr_row => {
+ let row_to_modify = {};
+ if (!items || !in_list(exist_items, (pr_row.item_code, pr_row.pricing_rules))) {
+
+ row_to_modify = frappe.model.add_child(this.frm.doc,
+ this.frm.doc.doctype + ' Item', 'items');
+
+ } else if(items) {
+ row_to_modify = items.filter(d => (d.item_code === pr_row.item_code
+ && d.pricing_rules === pr_row.pricing_rules))[0];
}
- } if (items && items.length && free_item_data) {
- items[0].qty = free_item_data.qty
- }
+
+ for (let key in pr_row) {
+ row_to_modify[key] = pr_row[key];
+ }
+ });
+
+ // free_item_data is a temporary variable
+ args.free_item_data = '';
+ refresh_field('items');
},
apply_price_list: function(item, reset_plc_conversion) {
@@ -1885,7 +1932,6 @@
frappe.throw(__("Please enter Item Code to get batch no"));
} else if (doc.doctype == "Purchase Receipt" ||
(doc.doctype == "Purchase Invoice" && doc.update_stock)) {
-
return {
filters: {'item': item.item_code}
}
@@ -1911,9 +1957,8 @@
set_query_for_item_tax_template: function(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
if(!item.item_code) {
- frappe.throw(__("Please enter Item Code to get item taxes"));
+ return doc.company ? {filters: {company: doc.company}} : {};
} else {
-
let filters = {
'item_code': item.item_code,
'valid_from': ["<=", doc.transaction_date || doc.bill_date || doc.posting_date],
@@ -2124,4 +2169,4 @@
}
}
});
-};
\ No newline at end of file
+};
diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js
index 472c537..e789923 100644
--- a/erpnext/public/js/help_links.js
+++ b/erpnext/public/js/help_links.js
@@ -1,466 +1,1051 @@
-frappe.provide('frappe.help.help_links');
+frappe.provide("frappe.help.help_links");
-const docsUrl = 'https://erpnext.com/docs/';
+const docsUrl = "https://erpnext.com/docs/";
-frappe.help.help_links['rename tool'] = [
- { label: 'Bulk Rename', url: docsUrl + 'user/manual/en/setting-up/data/bulk-rename' },
-]
+frappe.help.help_links["Form/Rename Tool"] = [
+ {
+ label: "Bulk Rename",
+ url: docsUrl + "user/manual/en/setting-up/data/bulk-rename",
+ },
+];
//Setup
-frappe.help.help_links['user'] = [
- { label: 'New User', url: docsUrl + 'user/manual/en/setting-up/users-and-permissions/adding-users' },
- { label: 'Rename User', url: docsUrl + 'user/manual/en/setting-up/articles/rename-user' },
-]
+frappe.help.help_links["List/User"] = [
+ {
+ label: "New User",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/users-and-permissions/adding-users",
+ },
+ {
+ label: "Rename User",
+ url: docsUrl + "user/manual/en/setting-up/articles/rename-user",
+ },
+];
-frappe.help.help_links['permission-manager'] = [
- { label: 'Role Permissions Manager', url: docsUrl + 'user/manual/en/setting-up/users-and-permissions/role-based-permissions' },
- { label: 'Managing Perm Level in Permissions Manager', url: docsUrl + 'user/manual/en/setting-up/articles/managing-perm-level' },
- { label: 'User Permissions', url: docsUrl + 'user/manual/en/setting-up/users-and-permissions/user-permissions' },
- { label: 'Sharing', url: docsUrl + 'user/manual/en/setting-up/users-and-permissions/sharing' },
- { label: 'Password', url: docsUrl + 'user/manual/en/setting-up/articles/change-password' },
-]
+frappe.help.help_links["permission-manager"] = [
+ {
+ label: "Role Permissions Manager",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/users-and-permissions/role-based-permissions",
+ },
+ {
+ label: "Managing Perm Level in Permissions Manager",
+ url: docsUrl + "user/manual/en/setting-up/articles/managing-perm-level",
+ },
+ {
+ label: "User Permissions",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/users-and-permissions/user-permissions",
+ },
+ {
+ label: "Sharing",
+ url:
+ docsUrl + "user/manual/en/setting-up/users-and-permissions/sharing",
+ },
+ {
+ label: "Password",
+ url: docsUrl + "user/manual/en/setting-up/articles/change-password",
+ },
+];
-frappe.help.help_links['system-settings'] = [
- { label: 'Naming Series', url: docsUrl + 'user/manual/en/setting-up/settings/system-settings' },
-]
+frappe.help.help_links["Form/System Settings"] = [
+ {
+ label: "Naming Series",
+ url: docsUrl + "user/manual/en/setting-up/settings/system-settings",
+ },
+];
-frappe.help.help_links['data-import-tool'] = [
- { label: 'Importing and Exporting Data', url: docsUrl + 'user/manual/en/setting-up/data/data-import-tool' },
- { label: 'Overwriting Data from Data Import Tool', url: docsUrl + 'user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool' },
-]
+frappe.help.help_links["data-import-tool"] = [
+ {
+ label: "Importing and Exporting Data",
+ url: docsUrl + "user/manual/en/setting-up/data/data-import-tool",
+ },
+ {
+ label: "Overwriting Data from Data Import Tool",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool",
+ },
+];
-frappe.help.help_links['naming-series'] = [
- { label: 'Naming Series', url: docsUrl + 'user/manual/en/setting-up/settings/naming-series' },
- { label: 'Setting the Current Value for Naming Series', url: docsUrl + 'user/manual/en/setting-up/articles/naming-series-current-value' },
-]
+frappe.help.help_links["module_setup"] = [
+ {
+ label: "Role Permissions Manager",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/users-and-permissions/role-based-permissions",
+ },
+];
-frappe.help.help_links['global-defaults'] = [
- { label: 'Global Settings', url: docsUrl + 'user/manual/en/setting-up/settings/global-defaults' },
-]
+frappe.help.help_links["Form/Naming Series"] = [
+ {
+ label: "Naming Series",
+ url: docsUrl + "user/manual/en/setting-up/settings/naming-series",
+ },
+ {
+ label: "Setting the Current Value for Naming Series",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/articles/naming-series-current-value",
+ },
+];
-frappe.help.help_links['email-digest'] = [
- { label: 'Email Digest', url: docsUrl + 'user/manual/en/setting-up/email/email-digest' },
-]
+frappe.help.help_links["Form/Global Defaults"] = [
+ {
+ label: "Global Settings",
+ url: docsUrl + "user/manual/en/setting-up/settings/global-defaults",
+ },
+];
-frappe.help.help_links['print-heading'] = [
- { label: 'Print Heading', url: docsUrl + 'user/manual/en/setting-up/print/print-headings' },
-]
+frappe.help.help_links["Form/Email Digest"] = [
+ {
+ label: "Email Digest",
+ url: docsUrl + "user/manual/en/setting-up/email/email-digest",
+ },
+];
-frappe.help.help_links['letter-head'] = [
- { label: 'Letter Head', url: docsUrl + 'user/manual/en/setting-up/print/letter-head' },
-]
+frappe.help.help_links["List/Print Heading"] = [
+ {
+ label: "Print Heading",
+ url: docsUrl + "user/manual/en/setting-up/print/print-headings",
+ },
+];
-frappe.help.help_links['address-template'] = [
- { label: 'Address Template', url: docsUrl + 'user/manual/en/setting-up/print/address-template' },
-]
+frappe.help.help_links["List/Letter Head"] = [
+ {
+ label: "Letter Head",
+ url: docsUrl + "user/manual/en/setting-up/print/letter-head",
+ },
+];
-frappe.help.help_links['terms-and-conditions'] = [
- { label: 'Terms and Conditions', url: docsUrl + 'user/manual/en/setting-up/print/terms-and-conditions' },
-]
+frappe.help.help_links["List/Address Template"] = [
+ {
+ label: "Address Template",
+ url: docsUrl + "user/manual/en/setting-up/print/address-template",
+ },
+];
-frappe.help.help_links['cheque-print-template'] = [
- { label: 'Cheque Print Template', url: docsUrl + 'user/manual/en/setting-up/print/cheque-print-template' },
-]
+frappe.help.help_links["List/Terms and Conditions"] = [
+ {
+ label: "Terms and Conditions",
+ url: docsUrl + "user/manual/en/setting-up/print/terms-and-conditions",
+ },
+];
-frappe.help.help_links['email-account'] = [
- { label: 'Email Account', url: docsUrl + 'user/manual/en/setting-up/email/email-account' },
-]
+frappe.help.help_links["List/Cheque Print Template"] = [
+ {
+ label: "Cheque Print Template",
+ url: docsUrl + "user/manual/en/setting-up/print/cheque-print-template",
+ },
+];
-frappe.help.help_links['notification'] = [
- { label: 'Notification', url: docsUrl + 'user/manual/en/setting-up/email/notifications' },
-]
+frappe.help.help_links["List/Email Account"] = [
+ {
+ label: "Email Account",
+ url: docsUrl + "user/manual/en/setting-up/email/email-account",
+ },
+];
-frappe.help.help_links['notification'] = [
- { label: 'Notification', url: docsUrl + 'user/manual/en/setting-up/email/notifications' },
-]
+frappe.help.help_links["List/Notification"] = [
+ {
+ label: "Notification",
+ url: docsUrl + "user/manual/en/setting-up/email/notifications",
+ },
+];
-frappe.help.help_links['email-digest'] = [
- { label: 'Email Digest', url: docsUrl + 'user/manual/en/setting-up/email/email-digest' },
-]
+frappe.help.help_links["Form/Notification"] = [
+ {
+ label: "Notification",
+ url: docsUrl + "user/manual/en/setting-up/email/notifications",
+ },
+];
-frappe.help.help_links['auto-email-report'] = [
- { label: 'Auto Email Reports', url: docsUrl + 'user/manual/en/setting-up/email/email-reports' },
-]
+frappe.help.help_links["List/Email Digest"] = [
+ {
+ label: "Email Digest",
+ url: docsUrl + "user/manual/en/setting-up/email/email-digest",
+ },
+];
-frappe.help.help_links['print-settings'] = [
- { label: 'Print Settings', url: docsUrl + 'user/manual/en/setting-up/print/print-settings' },
-]
+frappe.help.help_links["List/Auto Email Report"] = [
+ {
+ label: "Auto Email Reports",
+ url: docsUrl + "user/manual/en/setting-up/email/email-reports",
+ },
+];
-frappe.help.help_links['print-format-builder'] = [
- { label: 'Print Format Builder', url: docsUrl + 'user/manual/en/setting-up/print/print-settings' },
-]
+frappe.help.help_links["Form/Print Settings"] = [
+ {
+ label: "Print Settings",
+ url: docsUrl + "user/manual/en/setting-up/print/print-settings",
+ },
+];
-frappe.help.help_links['print-heading'] = [
- { label: 'Print Heading', url: docsUrl + 'user/manual/en/setting-up/print/print-headings' },
-]
+frappe.help.help_links["print-format-builder"] = [
+ {
+ label: "Print Format Builder",
+ url: docsUrl + "user/manual/en/setting-up/print/print-settings",
+ },
+];
+
+frappe.help.help_links["List/Print Heading"] = [
+ {
+ label: "Print Heading",
+ url: docsUrl + "user/manual/en/setting-up/print/print-headings",
+ },
+];
//setup-integrations
-frappe.help.help_links['paypal-settings'] = [
- { label: 'PayPal Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/paypal-integration' },
-]
+frappe.help.help_links["Form/PayPal Settings"] = [
+ {
+ label: "PayPal Settings",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/integrations/paypal-integration",
+ },
+];
-frappe.help.help_links['razorpay-settings'] = [
- { label: 'Razorpay Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/razorpay-integration' },
-]
+frappe.help.help_links["Form/Razorpay Settings"] = [
+ {
+ label: "Razorpay Settings",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/integrations/razorpay-integration",
+ },
+];
-frappe.help.help_links['dropbox-settings'] = [
- { label: 'Dropbox Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/dropbox-backup' },
-]
+frappe.help.help_links["Form/Dropbox Settings"] = [
+ {
+ label: "Dropbox Settings",
+ url: docsUrl + "user/manual/en/setting-up/integrations/dropbox-backup",
+ },
+];
-frappe.help.help_links['ldap-settings'] = [
- { label: 'LDAP Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/ldap-integration' },
-]
+frappe.help.help_links["Form/LDAP Settings"] = [
+ {
+ label: "LDAP Settings",
+ url:
+ docsUrl + "user/manual/en/setting-up/integrations/ldap-integration",
+ },
+];
-frappe.help.help_links['stripe-settings'] = [
- { label: 'Stripe Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/stripe-integration' },
-]
+frappe.help.help_links["Form/Stripe Settings"] = [
+ {
+ label: "Stripe Settings",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/integrations/stripe-integration",
+ },
+];
//Sales
-frappe.help.help_links['quotation'] = [
- { label: 'Quotation', url: docsUrl + 'user/manual/en/selling/quotation' },
- { label: 'Applying Discount', url: docsUrl + 'user/manual/en/selling/articles/applying-discount' },
- { label: 'Sales Person', url: docsUrl + 'user/manual/en/selling/articles/sales-persons-in-the-sales-transactions' },
- { label: 'Applying Margin', url: docsUrl + 'user/manual/en/selling/articles/adding-margin' },
-]
+frappe.help.help_links["Form/Quotation"] = [
+ { label: "Quotation", url: docsUrl + "user/manual/en/selling/quotation" },
+ {
+ label: "Applying Discount",
+ url: docsUrl + "user/manual/en/selling/articles/applying-discount",
+ },
+ {
+ label: "Sales Person",
+ url:
+ docsUrl +
+ "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions",
+ },
+ {
+ label: "Applying Margin",
+ url: docsUrl + "user/manual/en/selling/articles/adding-margin",
+ },
+];
-frappe.help.help_links['customer'] = [
- { label: 'Customer', url: docsUrl + 'user/manual/en/CRM/customer' },
- { label: 'Credit Limit', url: docsUrl + 'user/manual/en/accounts/credit-limit' },
-]
+frappe.help.help_links["List/Customer"] = [
+ { label: "Customer", url: docsUrl + "user/manual/en/CRM/customer" },
+ {
+ label: "Credit Limit",
+ url: docsUrl + "user/manual/en/accounts/credit-limit",
+ },
+];
-frappe.help.help_links['customer'] = [
- { label: 'Customer', url: docsUrl + 'user/manual/en/CRM/customer' },
- { label: 'Credit Limit', url: docsUrl + 'user/manual/en/accounts/credit-limit' },
-]
+frappe.help.help_links["Form/Customer"] = [
+ { label: "Customer", url: docsUrl + "user/manual/en/CRM/customer" },
+ {
+ label: "Credit Limit",
+ url: docsUrl + "user/manual/en/accounts/credit-limit",
+ },
+];
-frappe.help.help_links['sales-taxes-and-charges-template'] = [
- { label: 'Setting Up Taxes', url: docsUrl + 'user/manual/en/setting-up/setting-up-taxes' },
-]
+frappe.help.help_links["List/Sales Taxes and Charges Template"] = [
+ {
+ label: "Setting Up Taxes",
+ url: docsUrl + "user/manual/en/setting-up/setting-up-taxes",
+ },
+];
-frappe.help.help_links['sales-taxes-and-charges-template'] = [
- { label: 'Setting Up Taxes', url: docsUrl + 'user/manual/en/setting-up/setting-up-taxes' },
-]
+frappe.help.help_links["Form/Sales Taxes and Charges Template"] = [
+ {
+ label: "Setting Up Taxes",
+ url: docsUrl + "user/manual/en/setting-up/setting-up-taxes",
+ },
+];
-frappe.help.help_links['sales-order'] = [
- { label: 'Sales Order', url: docsUrl + 'user/manual/en/selling/sales-order' },
- { label: 'Recurring Sales Order', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' },
- { label: 'Applying Discount', url: docsUrl + 'user/manual/en/selling/articles/applying-discount' },
- { label: 'Drop Shipping', url: docsUrl + 'user/manual/en/selling/articles/drop-shipping' },
- { label: 'Sales Person', url: docsUrl + 'user/manual/en/selling/articles/sales-persons-in-the-sales-transactions' },
- { label: 'Close Sales Order', url: docsUrl + 'user/manual/en/selling/articles/close-sales-order' },
- { label: 'Applying Margin', url: docsUrl + 'user/manual/en/selling/articles/adding-margin' },
-]
+frappe.help.help_links["List/Sales Order"] = [
+ {
+ label: "Sales Order",
+ url: docsUrl + "user/manual/en/selling/sales-order",
+ },
+ {
+ label: "Recurring Sales Order",
+ url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ },
+ {
+ label: "Applying Discount",
+ url: docsUrl + "user/manual/en/selling/articles/applying-discount",
+ },
+];
-frappe.help.help_links['product-bundle'] = [
- { label: 'Product Bundle', url: docsUrl + 'user/manual/en/selling/setup/product-bundle' },
-]
+frappe.help.help_links["Form/Sales Order"] = [
+ {
+ label: "Sales Order",
+ url: docsUrl + "user/manual/en/selling/sales-order",
+ },
+ {
+ label: "Recurring Sales Order",
+ url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ },
+ {
+ label: "Applying Discount",
+ url: docsUrl + "user/manual/en/selling/articles/applying-discount",
+ },
+ {
+ label: "Drop Shipping",
+ url: docsUrl + "user/manual/en/selling/articles/drop-shipping",
+ },
+ {
+ label: "Sales Person",
+ url:
+ docsUrl +
+ "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions",
+ },
+ {
+ label: "Close Sales Order",
+ url: docsUrl + "user/manual/en/selling/articles/close-sales-order",
+ },
+ {
+ label: "Applying Margin",
+ url: docsUrl + "user/manual/en/selling/articles/adding-margin",
+ },
+];
-frappe.help.help_links['selling-settings'] = [
- { label: 'Selling Settings', url: docsUrl + 'user/manual/en/selling/setup/selling-settings' },
-]
+frappe.help.help_links["Form/Product Bundle"] = [
+ {
+ label: "Product Bundle",
+ url: docsUrl + "user/manual/en/selling/setup/product-bundle",
+ },
+];
+
+frappe.help.help_links["Form/Selling Settings"] = [
+ {
+ label: "Selling Settings",
+ url: docsUrl + "user/manual/en/selling/setup/selling-settings",
+ },
+];
//Buying
-frappe.help.help_links['supplier'] = [
- { label: 'Supplier', url: docsUrl + 'user/manual/en/buying/supplier' },
-]
+frappe.help.help_links["List/Supplier"] = [
+ { label: "Supplier", url: docsUrl + "user/manual/en/buying/supplier" },
+];
-frappe.help.help_links['request-for-quotation'] = [
- { label: 'Request for Quotation', url: docsUrl + 'user/manual/en/buying/request-for-quotation' },
- { label: 'RFQ Video', url: docsUrl + 'user/videos/learn/request-for-quotation.html' },
-]
+frappe.help.help_links["Form/Supplier"] = [
+ { label: "Supplier", url: docsUrl + "user/manual/en/buying/supplier" },
+];
-frappe.help.help_links['supplier-quotation'] = [
- { label: 'Supplier Quotation', url: docsUrl + 'user/manual/en/buying/supplier-quotation' },
-]
+frappe.help.help_links["Form/Request for Quotation"] = [
+ {
+ label: "Request for Quotation",
+ url: docsUrl + "user/manual/en/buying/request-for-quotation",
+ },
+ {
+ label: "RFQ Video",
+ url: docsUrl + "user/videos/learn/request-for-quotation.html",
+ },
+];
-frappe.help.help_links['buying-settings'] = [
- { label: 'Buying Settings', url: docsUrl + 'user/manual/en/buying/setup/buying-settings' },
-]
+frappe.help.help_links["Form/Supplier Quotation"] = [
+ {
+ label: "Supplier Quotation",
+ url: docsUrl + "user/manual/en/buying/supplier-quotation",
+ },
+];
-frappe.help.help_links['purchase-order'] = [
- { label: 'Purchase Order', url: docsUrl + 'user/manual/en/buying/purchase-order' },
- { label: 'Item UoM', url: docsUrl + 'user/manual/en/buying/articles/purchasing-in-different-unit' },
- { label: 'Supplier Item Code', url: docsUrl + 'user/manual/en/buying/articles/maintaining-suppliers-part-no-in-item' },
- { label: 'Recurring Purchase Order', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' },
- { label: 'Subcontracting', url: docsUrl + 'user/manual/en/manufacturing/subcontracting' },
-]
+frappe.help.help_links["Form/Buying Settings"] = [
+ {
+ label: "Buying Settings",
+ url: docsUrl + "user/manual/en/buying/setup/buying-settings",
+ },
+];
-frappe.help.help_links['purchase-taxes-and-charges-template'] = [
- { label: 'Setting Up Taxes', url: docsUrl + 'user/manual/en/setting-up/setting-up-taxes' },
-]
+frappe.help.help_links["List/Purchase Order"] = [
+ {
+ label: "Purchase Order",
+ url: docsUrl + "user/manual/en/buying/purchase-order",
+ },
+ {
+ label: "Recurring Purchase Order",
+ url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ },
+];
-frappe.help.help_links['pos-profile'] = [
- { label: 'POS Profile', url: docsUrl + 'user/manual/en/setting-up/pos-setting' },
-]
+frappe.help.help_links["Form/Purchase Order"] = [
+ {
+ label: "Purchase Order",
+ url: docsUrl + "user/manual/en/buying/purchase-order",
+ },
+ {
+ label: "Item UoM",
+ url:
+ docsUrl +
+ "user/manual/en/buying/articles/purchasing-in-different-unit",
+ },
+ {
+ label: "Supplier Item Code",
+ url:
+ docsUrl +
+ "user/manual/en/buying/articles/maintaining-suppliers-part-no-in-item",
+ },
+ {
+ label: "Recurring Purchase Order",
+ url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ },
+ {
+ label: "Subcontracting",
+ url: docsUrl + "user/manual/en/manufacturing/subcontracting",
+ },
+];
-frappe.help.help_links['price-list'] = [
- { label: 'Price List', url: docsUrl + 'user/manual/en/setting-up/price-lists' },
-]
+frappe.help.help_links["List/Purchase Taxes and Charges Template"] = [
+ {
+ label: "Setting Up Taxes",
+ url: docsUrl + "user/manual/en/setting-up/setting-up-taxes",
+ },
+];
-frappe.help.help_links['authorization-rule'] = [
- { label: 'Authorization Rule', url: docsUrl + 'user/manual/en/setting-up/authorization-rule' },
-]
+frappe.help.help_links["List/POS Profile"] = [
+ {
+ label: "POS Profile",
+ url: docsUrl + "user/manual/en/setting-up/pos-setting",
+ },
+];
-frappe.help.help_links['sms-settings'] = [
- { label: 'SMS Settings', url: docsUrl + 'user/manual/en/setting-up/sms-setting' },
-]
+frappe.help.help_links["List/Price List"] = [
+ {
+ label: "Price List",
+ url: docsUrl + "user/manual/en/setting-up/price-lists",
+ },
+];
-frappe.help.help_links['stock-reconciliation'] = [
- { label: 'Stock Reconciliation', url: docsUrl + 'user/manual/en/setting-up/stock-reconciliation-for-non-serialized-item' },
-]
+frappe.help.help_links["List/Authorization Rule"] = [
+ {
+ label: "Authorization Rule",
+ url: docsUrl + "user/manual/en/setting-up/authorization-rule",
+ },
+];
-frappe.help.help_links['territory/view/tree'] = [
- { label: 'Territory', url: docsUrl + 'user/manual/en/setting-up/territory' },
-]
+frappe.help.help_links["Form/SMS Settings"] = [
+ {
+ label: "SMS Settings",
+ url: docsUrl + "user/manual/en/setting-up/sms-setting",
+ },
+];
-frappe.help.help_links['dropbox-backup'] = [
- { label: 'Dropbox Backup', url: docsUrl + 'user/manual/en/setting-up/third-party-backups' },
- { label: 'Setting Up Dropbox Backup', url: docsUrl + 'user/manual/en/setting-up/articles/setting-up-dropbox-backups' },
-]
+frappe.help.help_links["List/Stock Reconciliation"] = [
+ {
+ label: "Stock Reconciliation",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/stock-reconciliation-for-non-serialized-item",
+ },
+];
-frappe.help.help_links['workflow'] = [
- { label: 'Workflow', url: docsUrl + 'user/manual/en/setting-up/workflows' },
-]
+frappe.help.help_links["Tree/Territory"] = [
+ {
+ label: "Territory",
+ url: docsUrl + "user/manual/en/setting-up/territory",
+ },
+];
-frappe.help.help_links['company'] = [
- { label: 'Company', url: docsUrl + 'user/manual/en/setting-up/company-setup' },
- { label: 'Managing Multiple Companies', url: docsUrl + 'user/manual/en/setting-up/articles/managing-multiple-companies' },
- { label: 'Delete All Related Transactions for a Company', url: docsUrl + 'user/manual/en/setting-up/articles/delete-a-company-and-all-related-transactions' },
-]
+frappe.help.help_links["Form/Dropbox Backup"] = [
+ {
+ label: "Dropbox Backup",
+ url: docsUrl + "user/manual/en/setting-up/third-party-backups",
+ },
+ {
+ label: "Setting Up Dropbox Backup",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/articles/setting-up-dropbox-backups",
+ },
+];
+
+frappe.help.help_links["List/Workflow"] = [
+ { label: "Workflow", url: docsUrl + "user/manual/en/setting-up/workflows" },
+];
+
+frappe.help.help_links["List/Company"] = [
+ {
+ label: "Company",
+ url: docsUrl + "user/manual/en/setting-up/company-setup",
+ },
+ {
+ label: "Managing Multiple Companies",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/articles/managing-multiple-companies",
+ },
+ {
+ label: "Delete All Related Transactions for a Company",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/articles/delete-a-company-and-all-related-transactions",
+ },
+];
//Accounts
-frappe.help.help_links['accounts'] = [
- { label: 'Introduction to Accounts', url: docsUrl + 'user/manual/en/accounts/' },
- { label: 'Chart of Accounts', url: docsUrl + 'user/manual/en/accounts/chart-of-accounts.html' },
- { label: 'Multi Currency Accounting', url: docsUrl + 'user/manual/en/accounts/multi-currency-accounting' },
-]
+frappe.help.help_links["modules/Accounts"] = [
+ {
+ label: "Introduction to Accounts",
+ url: docsUrl + "user/manual/en/accounts/",
+ },
+ {
+ label: "Chart of Accounts",
+ url: docsUrl + "user/manual/en/accounts/chart-of-accounts.html",
+ },
+ {
+ label: "Multi Currency Accounting",
+ url: docsUrl + "user/manual/en/accounts/multi-currency-accounting",
+ },
+];
-frappe.help.help_links['account/view/tree'] = [
- { label: 'Chart of Accounts', url: docsUrl + 'user/manual/en/accounts/chart-of-accounts' },
- { label: 'Managing Tree Mastes', url: docsUrl + 'user/manual/en/setting-up/articles/managing-tree-structure-masters' },
-]
+frappe.help.help_links["Tree/Account"] = [
+ {
+ label: "Chart of Accounts",
+ url: docsUrl + "user/manual/en/accounts/chart-of-accounts",
+ },
+ {
+ label: "Managing Tree Mastes",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/articles/managing-tree-structure-masters",
+ },
+];
-frappe.help.help_links['sales-invoice'] = [
- { label: 'Sales Invoice', url: docsUrl + 'user/manual/en/accounts/sales-invoice' },
- { label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' },
- { label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' },
- { label: 'Recurring Sales Invoice', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' },
-]
+frappe.help.help_links["Form/Sales Invoice"] = [
+ {
+ label: "Sales Invoice",
+ url: docsUrl + "user/manual/en/accounts/sales-invoice",
+ },
+ {
+ label: "Accounts Opening Balance",
+ url: docsUrl + "user/manual/en/accounts/opening-accounts",
+ },
+ {
+ label: "Sales Return",
+ url: docsUrl + "user/manual/en/stock/sales-return",
+ },
+ {
+ label: "Recurring Sales Invoice",
+ url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ },
+];
-frappe.help.help_links['sales-invoice'] = [
- { label: 'Sales Invoice', url: docsUrl + 'user/manual/en/accounts/sales-invoice' },
- { label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' },
- { label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' },
- { label: 'Recurring Sales Invoice', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' },
-]
+frappe.help.help_links["List/Sales Invoice"] = [
+ {
+ label: "Sales Invoice",
+ url: docsUrl + "user/manual/en/accounts/sales-invoice",
+ },
+ {
+ label: "Accounts Opening Balance",
+ url: docsUrl + "user/manual/en/accounts/opening-accounts",
+ },
+ {
+ label: "Sales Return",
+ url: docsUrl + "user/manual/en/stock/sales-return",
+ },
+ {
+ label: "Recurring Sales Invoice",
+ url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ },
+];
-frappe.help.help_links['pos'] = [
- { label: 'Point of Sale Invoice', url: docsUrl + 'user/manual/en/accounts/point-of-sale-pos-invoice' },
-]
+frappe.help.help_links["pos"] = [
+ {
+ label: "Point of Sale Invoice",
+ url: docsUrl + "user/manual/en/accounts/point-of-sale-pos-invoice",
+ },
+];
-frappe.help.help_links['pos-profile'] = [
- { label: 'Point of Sale Profile', url: docsUrl + 'user/manual/en/setting-up/pos-setting' },
-]
+frappe.help.help_links["List/POS Profile"] = [
+ {
+ label: "Point of Sale Profile",
+ url: docsUrl + "user/manual/en/setting-up/pos-setting",
+ },
+];
-frappe.help.help_links['purchase-invoice'] = [
- { label: 'Purchase Invoice', url: docsUrl + 'user/manual/en/accounts/purchase-invoice' },
- { label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' },
- { label: 'Recurring Purchase Invoice', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' },
-]
+frappe.help.help_links["List/Purchase Invoice"] = [
+ {
+ label: "Purchase Invoice",
+ url: docsUrl + "user/manual/en/accounts/purchase-invoice",
+ },
+ {
+ label: "Accounts Opening Balance",
+ url: docsUrl + "user/manual/en/accounts/opening-accounts",
+ },
+ {
+ label: "Recurring Purchase Invoice",
+ url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ },
+];
-frappe.help.help_links['journal-entry'] = [
- { label: 'Journal Entry', url: docsUrl + 'user/manual/en/accounts/journal-entry' },
- { label: 'Advance Payment Entry', url: docsUrl + 'user/manual/en/accounts/advance-payment-entry' },
- { label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' },
-]
+frappe.help.help_links["List/Journal Entry"] = [
+ {
+ label: "Journal Entry",
+ url: docsUrl + "user/manual/en/accounts/journal-entry",
+ },
+ {
+ label: "Advance Payment Entry",
+ url: docsUrl + "user/manual/en/accounts/advance-payment-entry",
+ },
+ {
+ label: "Accounts Opening Balance",
+ url: docsUrl + "user/manual/en/accounts/opening-accounts",
+ },
+];
-frappe.help.help_links['payment-entry'] = [
- { label: 'Payment Entry', url: docsUrl + 'user/manual/en/accounts/payment-entry' },
-]
+frappe.help.help_links["List/Payment Entry"] = [
+ {
+ label: "Payment Entry",
+ url: docsUrl + "user/manual/en/accounts/payment-entry",
+ },
+];
-frappe.help.help_links['payment-request'] = [
- { label: 'Payment Request', url: docsUrl + 'user/manual/en/accounts/payment-request' },
-]
+frappe.help.help_links["List/Payment Request"] = [
+ {
+ label: "Payment Request",
+ url: docsUrl + "user/manual/en/accounts/payment-request",
+ },
+];
-frappe.help.help_links['asset'] = [
- { label: 'Managing Fixed Assets', url: docsUrl + 'user/manual/en/accounts/managing-fixed-assets' },
-]
+frappe.help.help_links["List/Asset"] = [
+ {
+ label: "Managing Fixed Assets",
+ url: docsUrl + "user/manual/en/accounts/managing-fixed-assets",
+ },
+];
-frappe.help.help_links['asset-category'] = [
- { label: 'Asset Category', url: docsUrl + 'user/manual/en/accounts/managing-fixed-assets' },
-]
+frappe.help.help_links["List/Asset Category"] = [
+ {
+ label: "Asset Category",
+ url: docsUrl + "user/manual/en/accounts/managing-fixed-assets",
+ },
+];
-frappe.help.help_links['cost-center/view/tree'] = [
- { label: 'Budgeting', url: docsUrl + 'user/manual/en/accounts/budgeting' },
-]
+frappe.help.help_links["Tree/Cost Center"] = [
+ { label: "Budgeting", url: docsUrl + "user/manual/en/accounts/budgeting" },
+];
-frappe.help.help_links['item'] = [
- { label: 'Item', url: docsUrl + 'user/manual/en/stock/item' },
- { label: 'Item Price', url: docsUrl + 'user/manual/en/stock/item/item-price' },
- { label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' },
- { label: 'Item Wise Taxation', url: docsUrl + 'user/manual/en/accounts/item-wise-taxation' },
- { label: 'Managing Fixed Assets', url: docsUrl + 'user/manual/en/accounts/managing-fixed-assets' },
- { label: 'Item Codification', url: docsUrl + 'user/manual/en/stock/item/item-codification' },
- { label: 'Item Variants', url: docsUrl + 'user/manual/en/stock/item/item-variants' },
- { label: 'Item Valuation', url: docsUrl + 'user/manual/en/stock/item/item-valuation-fifo-and-moving-average' },
-]
+frappe.help.help_links["List/Item"] = [
+ { label: "Item", url: docsUrl + "user/manual/en/stock/item" },
+ {
+ label: "Item Price",
+ url: docsUrl + "user/manual/en/stock/item/item-price",
+ },
+ {
+ label: "Barcode",
+ url:
+ docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
+ },
+ {
+ label: "Item Wise Taxation",
+ url: docsUrl + "user/manual/en/accounts/item-wise-taxation",
+ },
+ {
+ label: "Managing Fixed Assets",
+ url: docsUrl + "user/manual/en/accounts/managing-fixed-assets",
+ },
+ {
+ label: "Item Codification",
+ url: docsUrl + "user/manual/en/stock/item/item-codification",
+ },
+ {
+ label: "Item Variants",
+ url: docsUrl + "user/manual/en/stock/item/item-variants",
+ },
+ {
+ label: "Item Valuation",
+ url:
+ docsUrl +
+ "user/manual/en/stock/item/item-valuation-fifo-and-moving-average",
+ },
+];
-frappe.help.help_links['purchase-receipt'] = [
- { label: 'Purchase Receipt', url: docsUrl + 'user/manual/en/stock/purchase-receipt' },
- { label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' },
-]
+frappe.help.help_links["Form/Item"] = [
+ { label: "Item", url: docsUrl + "user/manual/en/stock/item" },
+ {
+ label: "Item Price",
+ url: docsUrl + "user/manual/en/stock/item/item-price",
+ },
+ {
+ label: "Barcode",
+ url:
+ docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
+ },
+ {
+ label: "Item Wise Taxation",
+ url: docsUrl + "user/manual/en/accounts/item-wise-taxation",
+ },
+ {
+ label: "Managing Fixed Assets",
+ url: docsUrl + "user/manual/en/accounts/managing-fixed-assets",
+ },
+ {
+ label: "Item Codification",
+ url: docsUrl + "user/manual/en/stock/item/item-codification",
+ },
+ {
+ label: "Item Variants",
+ url: docsUrl + "user/manual/en/stock/item/item-variants",
+ },
+ {
+ label: "Item Valuation",
+ url:
+ docsUrl +
+ "user/manual/en/stock/item/item-valuation-fifo-and-moving-average",
+ },
+];
-frappe.help.help_links['delivery-note'] = [
- { label: 'Delivery Note', url: docsUrl + 'user/manual/en/stock/delivery-note' },
- { label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' },
- { label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' },
-]
+frappe.help.help_links["List/Purchase Receipt"] = [
+ {
+ label: "Purchase Receipt",
+ url: docsUrl + "user/manual/en/stock/purchase-receipt",
+ },
+ {
+ label: "Barcode",
+ url:
+ docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
+ },
+];
-frappe.help.help_links['delivery-note'] = [
- { label: 'Delivery Note', url: docsUrl + 'user/manual/en/stock/delivery-note' },
- { label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' },
- { label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' },
- { label: 'Subcontracting', url: docsUrl + 'user/manual/en/manufacturing/subcontracting' },
-]
+frappe.help.help_links["List/Delivery Note"] = [
+ {
+ label: "Delivery Note",
+ url: docsUrl + "user/manual/en/stock/delivery-note",
+ },
+ {
+ label: "Barcode",
+ url:
+ docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
+ },
+ {
+ label: "Sales Return",
+ url: docsUrl + "user/manual/en/stock/sales-return",
+ },
+];
-frappe.help.help_links['installation-note'] = [
- { label: 'Installation Note', url: docsUrl + 'user/manual/en/stock/installation-note' },
-]
+frappe.help.help_links["Form/Delivery Note"] = [
+ {
+ label: "Delivery Note",
+ url: docsUrl + "user/manual/en/stock/delivery-note",
+ },
+ {
+ label: "Sales Return",
+ url: docsUrl + "user/manual/en/stock/sales-return",
+ },
+ {
+ label: "Barcode",
+ url:
+ docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
+ },
+ {
+ label: "Subcontracting",
+ url: docsUrl + "user/manual/en/manufacturing/subcontracting",
+ },
+];
+frappe.help.help_links["List/Installation Note"] = [
+ {
+ label: "Installation Note",
+ url: docsUrl + "user/manual/en/stock/installation-note",
+ },
+];
+frappe.help.help_links["Tree"] = [
+ {
+ label: "Managing Tree Structure Masters",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/articles/managing-tree-structure-masters",
+ },
+];
-frappe.help.help_links['budget'] = [
- { label: 'Budgeting', url: docsUrl + 'user/manual/en/accounts/budgeting' },
-]
+frappe.help.help_links["List/Budget"] = [
+ { label: "Budgeting", url: docsUrl + "user/manual/en/accounts/budgeting" },
+];
//Stock
-frappe.help.help_links['material-request'] = [
- { label: 'Material Request', url: docsUrl + 'user/manual/en/stock/material-request' },
- { label: 'Auto-creation of Material Request', url: docsUrl + 'user/manual/en/stock/articles/auto-creation-of-material-request' },
-]
+frappe.help.help_links["List/Material Request"] = [
+ {
+ label: "Material Request",
+ url: docsUrl + "user/manual/en/stock/material-request",
+ },
+ {
+ label: "Auto-creation of Material Request",
+ url:
+ docsUrl +
+ "user/manual/en/stock/articles/auto-creation-of-material-request",
+ },
+];
-frappe.help.help_links['stock-entry'] = [
- { label: 'Stock Entry', url: docsUrl + 'user/manual/en/stock/stock-entry' },
- { label: 'Stock Entry Types', url: docsUrl + 'user/manual/en/stock/articles/stock-entry-purpose' },
- { label: 'Repack Entry', url: docsUrl + 'user/manual/en/stock/articles/repack-entry' },
- { label: 'Opening Stock', url: docsUrl + 'user/manual/en/stock/opening-stock' },
- { label: 'Subcontracting', url: docsUrl + 'user/manual/en/manufacturing/subcontracting' },
-]
+frappe.help.help_links["Form/Material Request"] = [
+ {
+ label: "Material Request",
+ url: docsUrl + "user/manual/en/stock/material-request",
+ },
+ {
+ label: "Auto-creation of Material Request",
+ url:
+ docsUrl +
+ "user/manual/en/stock/articles/auto-creation-of-material-request",
+ },
+];
-frappe.help.help_links['warehouse/view/tree'] = [
- { label: 'Warehouse', url: docsUrl + 'user/manual/en/stock/warehouse' },
-]
+frappe.help.help_links["Form/Stock Entry"] = [
+ { label: "Stock Entry", url: docsUrl + "user/manual/en/stock/stock-entry" },
+ {
+ label: "Stock Entry Types",
+ url: docsUrl + "user/manual/en/stock/articles/stock-entry-purpose",
+ },
+ {
+ label: "Repack Entry",
+ url: docsUrl + "user/manual/en/stock/articles/repack-entry",
+ },
+ {
+ label: "Opening Stock",
+ url: docsUrl + "user/manual/en/stock/opening-stock",
+ },
+ {
+ label: "Subcontracting",
+ url: docsUrl + "user/manual/en/manufacturing/subcontracting",
+ },
+];
-frappe.help.help_links['serial-no'] = [
- { label: 'Serial No', url: docsUrl + 'user/manual/en/stock/serial-no' },
-]
+frappe.help.help_links["List/Stock Entry"] = [
+ { label: "Stock Entry", url: docsUrl + "user/manual/en/stock/stock-entry" },
+];
-frappe.help.help_links['batch'] = [
- { label: 'Batch', url: docsUrl + 'user/manual/en/stock/batch' },
-]
+frappe.help.help_links["Tree/Warehouse"] = [
+ { label: "Warehouse", url: docsUrl + "user/manual/en/stock/warehouse" },
+];
-frappe.help.help_links['packing-slip'] = [
- { label: 'Packing Slip', url: docsUrl + 'user/manual/en/stock/tools/packing-slip' },
-]
+frappe.help.help_links["List/Serial No"] = [
+ { label: "Serial No", url: docsUrl + "user/manual/en/stock/serial-no" },
+];
-frappe.help.help_links['quality-inspection'] = [
- { label: 'Quality Inspection', url: docsUrl + 'user/manual/en/stock/tools/quality-inspection' },
-]
+frappe.help.help_links["Form/Serial No"] = [
+ { label: "Serial No", url: docsUrl + "user/manual/en/stock/serial-no" },
+];
-frappe.help.help_links['landed-cost-voucher'] = [
- { label: 'Landed Cost Voucher', url: docsUrl + 'user/manual/en/stock/tools/landed-cost-voucher' },
-]
+frappe.help.help_links["Form/Batch"] = [
+ { label: "Batch", url: docsUrl + "user/manual/en/stock/batch" },
+];
-frappe.help.help_links['item-group/view/tree'] = [
- { label: 'Item Group', url: docsUrl + 'user/manual/en/stock/setup/item-group' },
-]
+frappe.help.help_links["Form/Packing Slip"] = [
+ {
+ label: "Packing Slip",
+ url: docsUrl + "user/manual/en/stock/tools/packing-slip",
+ },
+];
-frappe.help.help_links['item-attribute'] = [
- { label: 'Item Attribute', url: docsUrl + 'user/manual/en/stock/setup/item-attribute' },
-]
+frappe.help.help_links["Form/Quality Inspection"] = [
+ {
+ label: "Quality Inspection",
+ url: docsUrl + "user/manual/en/stock/tools/quality-inspection",
+ },
+];
-frappe.help.help_links['uom'] = [
- { label: 'Fractions in UOM', url: docsUrl + 'user/manual/en/stock/articles/managing-fractions-in-uom' },
-]
+frappe.help.help_links["Form/Landed Cost Voucher"] = [
+ {
+ label: "Landed Cost Voucher",
+ url: docsUrl + "user/manual/en/stock/tools/landed-cost-voucher",
+ },
+];
-frappe.help.help_links['stock-reconciliation'] = [
- { label: 'Opening Stock Entry', url: docsUrl + 'user/manual/en/stock/opening-stock' },
-]
+frappe.help.help_links["Tree/Item Group"] = [
+ {
+ label: "Item Group",
+ url: docsUrl + "user/manual/en/stock/setup/item-group",
+ },
+];
+
+frappe.help.help_links["Form/Item Attribute"] = [
+ {
+ label: "Item Attribute",
+ url: docsUrl + "user/manual/en/stock/setup/item-attribute",
+ },
+];
+
+frappe.help.help_links["Form/UOM"] = [
+ {
+ label: "Fractions in UOM",
+ url:
+ docsUrl + "user/manual/en/stock/articles/managing-fractions-in-uom",
+ },
+];
+
+frappe.help.help_links["Form/Stock Reconciliation"] = [
+ {
+ label: "Opening Stock Entry",
+ url: docsUrl + "user/manual/en/stock/opening-stock",
+ },
+];
//CRM
-frappe.help.help_links['lead'] = [
- { label: 'Lead', url: docsUrl + 'user/manual/en/CRM/lead' },
-]
+frappe.help.help_links["Form/Lead"] = [
+ { label: "Lead", url: docsUrl + "user/manual/en/CRM/lead" },
+];
-frappe.help.help_links['opportunity'] = [
- { label: 'Opportunity', url: docsUrl + 'user/manual/en/CRM/opportunity' },
-]
+frappe.help.help_links["Form/Opportunity"] = [
+ { label: "Opportunity", url: docsUrl + "user/manual/en/CRM/opportunity" },
+];
-frappe.help.help_links['address'] = [
- { label: 'Address', url: docsUrl + 'user/manual/en/CRM/address' },
-]
+frappe.help.help_links["Form/Address"] = [
+ { label: "Address", url: docsUrl + "user/manual/en/CRM/address" },
+];
-frappe.help.help_links['contact'] = [
- { label: 'Contact', url: docsUrl + 'user/manual/en/CRM/contact' },
-]
+frappe.help.help_links["Form/Contact"] = [
+ { label: "Contact", url: docsUrl + "user/manual/en/CRM/contact" },
+];
-frappe.help.help_links['newsletter'] = [
- { label: 'Newsletter', url: docsUrl + 'user/manual/en/CRM/newsletter' },
-]
+frappe.help.help_links["Form/Newsletter"] = [
+ { label: "Newsletter", url: docsUrl + "user/manual/en/CRM/newsletter" },
+];
-frappe.help.help_links['campaign'] = [
- { label: 'Campaign', url: docsUrl + 'user/manual/en/CRM/setup/campaign' },
-]
+frappe.help.help_links["Form/Campaign"] = [
+ { label: "Campaign", url: docsUrl + "user/manual/en/CRM/setup/campaign" },
+];
-frappe.help.help_links['sales-person/view/tree'] = [
- { label: 'Sales Person', url: docsUrl + 'user/manual/en/CRM/setup/sales-person' },
-]
+frappe.help.help_links["Tree/Sales Person"] = [
+ {
+ label: "Sales Person",
+ url: docsUrl + "user/manual/en/CRM/setup/sales-person",
+ },
+];
-frappe.help.help_links['sales-person'] = [
- { label: 'Sales Person Target', url: docsUrl + 'user/manual/en/selling/setup/sales-person-target-allocation' },
-]
+frappe.help.help_links["Form/Sales Person"] = [
+ {
+ label: "Sales Person Target",
+ url:
+ docsUrl +
+ "user/manual/en/selling/setup/sales-person-target-allocation",
+ },
+];
+
+//Support
+
+frappe.help.help_links["List/Feedback Trigger"] = [
+ {
+ label: "Feedback Trigger",
+ url: docsUrl + "user/manual/en/setting-up/feedback/setting-up-feedback",
+ },
+];
+
+frappe.help.help_links["List/Feedback Request"] = [
+ {
+ label: "Feedback Request",
+ url: docsUrl + "user/manual/en/setting-up/feedback/submit-feedback",
+ },
+];
+
+frappe.help.help_links["List/Feedback Request"] = [
+ {
+ label: "Feedback Request",
+ url: docsUrl + "user/manual/en/setting-up/feedback/submit-feedback",
+ },
+];
//Manufacturing
-frappe.help.help_links['bom'] = [
- { label: 'Bill of Material', url: docsUrl + 'user/manual/en/manufacturing/bill-of-materials' },
- { label: 'Nested BOM Structure', url: docsUrl + 'user/manual/en/manufacturing/articles/nested-bom-structure' },
-]
+frappe.help.help_links["Form/BOM"] = [
+ {
+ label: "Bill of Material",
+ url: docsUrl + "user/manual/en/manufacturing/bill-of-materials",
+ },
+ {
+ label: "Nested BOM Structure",
+ url:
+ docsUrl +
+ "user/manual/en/manufacturing/articles/nested-bom-structure",
+ },
+];
-frappe.help.help_links['work-order'] = [
- { label: 'Work Order', url: docsUrl + 'user/manual/en/manufacturing/work-order' },
-]
+frappe.help.help_links["Form/Work Order"] = [
+ {
+ label: "Work Order",
+ url: docsUrl + "user/manual/en/manufacturing/work-order",
+ },
+];
-frappe.help.help_links['workstation'] = [
- { label: 'Workstation', url: docsUrl + 'user/manual/en/manufacturing/workstation' },
-]
+frappe.help.help_links["Form/Workstation"] = [
+ {
+ label: "Workstation",
+ url: docsUrl + "user/manual/en/manufacturing/workstation",
+ },
+];
-frappe.help.help_links['operation'] = [
- { label: 'Operation', url: docsUrl + 'user/manual/en/manufacturing/operation' },
-]
+frappe.help.help_links["Form/Operation"] = [
+ {
+ label: "Operation",
+ url: docsUrl + "user/manual/en/manufacturing/operation",
+ },
+];
-frappe.help.help_links['bom-update-tool'] = [
- { label: 'BOM Update Tool', url: docsUrl + 'user/manual/en/manufacturing/tools/bom-update-tool' },
-]
+frappe.help.help_links["Form/BOM Update Tool"] = [
+ {
+ label: "BOM Update Tool",
+ url: docsUrl + "user/manual/en/manufacturing/tools/bom-update-tool",
+ },
+];
//Customize
-frappe.help.help_links['customize-form'] = [
- { label: 'Custom Field', url: docsUrl + 'user/manual/en/customize-erpnext/custom-field' },
- { label: 'Customize Field', url: docsUrl + 'user/manual/en/customize-erpnext/customize-form' },
-]
+frappe.help.help_links["Form/Customize Form"] = [
+ {
+ label: "Custom Field",
+ url: docsUrl + "user/manual/en/customize-erpnext/custom-field",
+ },
+ {
+ label: "Customize Field",
+ url: docsUrl + "user/manual/en/customize-erpnext/customize-form",
+ },
+];
-frappe.help.help_links['custom-field'] = [
- { label: 'Custom Field', url: docsUrl + 'user/manual/en/customize-erpnext/custom-field' },
-]
+frappe.help.help_links["Form/Custom Field"] = [
+ {
+ label: "Custom Field",
+ url: docsUrl + "user/manual/en/customize-erpnext/custom-field",
+ },
+];
-frappe.help.help_links['custom-field'] = [
- { label: 'Custom Field', url: docsUrl + 'user/manual/en/customize-erpnext/custom-field' },
-]
+frappe.help.help_links["Form/Custom Field"] = [
+ {
+ label: "Custom Field",
+ url: docsUrl + "user/manual/en/customize-erpnext/custom-field",
+ },
+];
diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js
index 6cb1207..9548d6c 100644
--- a/erpnext/public/js/telephony.js
+++ b/erpnext/public/js/telephony.js
@@ -4,10 +4,20 @@
if (this.df.options == 'Phone') {
this.setup_phone();
}
+ if (this.frm && this.frm.fields_dict) {
+ Object.values(this.frm.fields_dict).forEach(function(field) {
+ if (field.df.read_only === 1 && field.df.options === 'Phone'
+ && field.disp_area.style[0] != 'display' && !field.has_icon) {
+ field.setup_phone();
+ field.has_icon = true;
+ }
+ });
+ }
},
setup_phone() {
if (frappe.phone_call.handler) {
- this.$wrapper.find('.control-input')
+ let control = this.df.read_only ? '.control-value' : '.control-input';
+ this.$wrapper.find(control)
.append(`
<span class="phone-btn">
<a class="btn-open no-decoration" title="${__('Make a call')}">
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index c39609b..e5b50d8 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -513,6 +513,7 @@
}, {
fieldtype:'Currency',
fieldname:"rate",
+ options: "currency",
default: 0,
read_only: 0,
in_list_view: 1,
@@ -594,21 +595,7 @@
}
erpnext.utils.map_current_doc = function(opts) {
- let query_args = {};
- if (opts.get_query_filters) {
- query_args.filters = opts.get_query_filters;
- }
-
- if (opts.get_query_method) {
- query_args.query = opts.get_query_method;
- }
-
- if (query_args.filters || query_args.query) {
- opts.get_query = () => {
- return query_args;
- }
- }
- var _map = function() {
+ function _map() {
if($.isArray(cur_frm.doc.items) && cur_frm.doc.items.length > 0) {
// remove first item row if empty
if(!cur_frm.doc.items[0].item_code) {
@@ -682,8 +669,22 @@
}
});
}
- if(opts.source_doctype) {
- var d = new frappe.ui.form.MultiSelectDialog({
+
+ let query_args = {};
+ if (opts.get_query_filters) {
+ query_args.filters = opts.get_query_filters;
+ }
+
+ if (opts.get_query_method) {
+ query_args.query = opts.get_query_method;
+ }
+
+ if (query_args.filters || query_args.query) {
+ opts.get_query = () => query_args;
+ }
+
+ if (opts.source_doctype) {
+ const d = new frappe.ui.form.MultiSelectDialog({
doctype: opts.source_doctype,
target: opts.target,
date_field: opts.date_field || undefined,
@@ -702,7 +703,11 @@
_map();
},
});
- } else if(opts.source_name) {
+
+ return d;
+ }
+
+ if (opts.source_name) {
opts.source_name = [opts.source_name];
_map();
}
diff --git a/erpnext/quality_management/doctype/non_conformance/non_conformance.json b/erpnext/quality_management/doctype/non_conformance/non_conformance.json
index bfeb96b..8dfe2d6 100644
--- a/erpnext/quality_management/doctype/non_conformance/non_conformance.json
+++ b/erpnext/quality_management/doctype/non_conformance/non_conformance.json
@@ -70,18 +70,18 @@
},
{
"fieldname": "corrective_action",
- "fieldtype": "Text",
+ "fieldtype": "Text Editor",
"label": "Corrective Action"
},
{
"fieldname": "preventive_action",
- "fieldtype": "Text",
+ "fieldtype": "Text Editor",
"label": "Preventive Action"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-26 15:27:47.247814",
+ "modified": "2021-02-26 15:27:47.247814",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Non Conformance",
@@ -115,4 +115,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py
index bf82cc0..5a8ec73 100644
--- a/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py
+++ b/erpnext/quality_management/doctype/quality_feedback/quality_feedback.py
@@ -7,6 +7,7 @@
from frappe.model.document import Document
class QualityFeedback(Document):
+ @frappe.whitelist()
def set_parameters(self):
if self.template and not getattr(self, 'parameters', []):
for d in frappe.get_doc('Quality Feedback Template', self.template).parameters:
diff --git a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
index ead403d..e2125c3 100644
--- a/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
+++ b/erpnext/quality_management/doctype/quality_meeting/quality_meeting.json
@@ -33,8 +33,7 @@
},
{
"fieldname": "sb_00",
- "fieldtype": "Section Break",
- "label": "Agenda"
+ "fieldtype": "Section Break"
},
{
"fieldname": "agenda",
@@ -44,13 +43,12 @@
},
{
"fieldname": "sb_01",
- "fieldtype": "Section Break",
- "label": "Minutes"
+ "fieldtype": "Section Break"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-10-27 16:36:45.657883",
+ "modified": "2021-02-27 16:36:45.657883",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality Meeting",
@@ -85,4 +83,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.json b/erpnext/regional/doctype/gst_settings/gst_settings.json
index 98c33ad..95b930c 100644
--- a/erpnext/regional/doctype/gst_settings/gst_settings.json
+++ b/erpnext/regional/doctype/gst_settings/gst_settings.json
@@ -1,222 +1,86 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-06-27 15:09:01.318003",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-06-27 15:09:01.318003",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "gst_summary",
+ "column_break_2",
+ "round_off_gst_values",
+ "gstin_email_sent_on",
+ "section_break_4",
+ "gst_accounts",
+ "b2c_limit"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gst_summary",
- "fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "GST Summary",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "gst_summary",
+ "fieldtype": "HTML",
+ "label": "GST Summary",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gstin_email_sent_on",
- "fieldtype": "Date",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "GSTIN Email Sent On",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "gstin_email_sent_on",
+ "fieldtype": "Date",
+ "label": "GSTIN Email Sent On",
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gst_accounts",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "GST Accounts",
- "length": 0,
- "no_copy": 0,
- "options": "GST Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "gst_accounts",
+ "fieldtype": "Table",
+ "label": "GST Accounts",
+ "options": "GST Account",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "250000",
- "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.",
- "fieldname": "b2c_limit",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "B2C Limit",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "default": "250000",
+ "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.",
+ "fieldname": "b2c_limit",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "B2C Limit",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "description": "Enabling this option will round off individual GST components in all the Invoices",
+ "fieldname": "round_off_gst_values",
+ "fieldtype": "Check",
+ "label": "Round Off GST Values",
+ "show_days": 1,
+ "show_seconds": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-02-14 08:14:15.375181",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "GST Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-01-28 17:19:47.969260",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "GST Settings",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+ }
\ No newline at end of file
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html
index 888b2da..369a400 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html
@@ -109,7 +109,7 @@
</td>
</tr>
<tr>
- <td>{{__("Suppliies made to Composition Taxable Persons")}}</td>
+ <td>{{__("Supplies made to Composition Taxable Persons")}}</td>
<td class="right">
{% for row in data.inter_sup.comp_details %}
{% if row %}
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index 68c8a0d..a5dd5a2 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -172,7 +172,6 @@
self.json_output = frappe.as_json(self.report_dict)
def set_inward_nil_exempt(self, inward_nil_exempt):
-
self.report_dict["inward_sup"]["isup_details"][0]["inter"] = flt(inward_nil_exempt.get("gst").get("inter"), 2)
self.report_dict["inward_sup"]["isup_details"][0]["intra"] = flt(inward_nil_exempt.get("gst").get("intra"), 2)
self.report_dict["inward_sup"]["isup_details"][1]["inter"] = flt(inward_nil_exempt.get("non_gst").get("inter"), 2)
@@ -238,7 +237,6 @@
self.report_dict[supply_type][supply_category]["txval"] += flt(txval, 2)
def set_inter_state_supply(self, inter_state_supply):
-
osup_det = self.report_dict["sup_details"]["osup_det"]
for key, value in iteritems(inter_state_supply):
@@ -349,13 +347,20 @@
return inter_state_supply_details
def get_inward_nil_exempt(self, state):
-
inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount,
i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
where p.docstatus = 1 and p.name = i.parent
- and i.is_nil_exempt = 1 or i.is_non_gst = 1 and
+ and p.gst_category != 'Registered Composition'
+ and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and
month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s
- group by p.place_of_supply """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
+ group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
+
+ inward_nil_exempt += frappe.db.sql("""SELECT sum(base_net_total) as base_amount, gst_category, place_of_supply
+ FROM `tabPurchase Invoice`
+ WHERE docstatus = 1 and gst_category = 'Registered Composition'
+ and month(posting_date) = %s and year(posting_date) = %s
+ and company = %s and company_gstin = %s
+ group by place_of_supply""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
inward_nil_exempt_details = {
"gst": {
@@ -370,9 +375,11 @@
for d in inward_nil_exempt:
if d.place_of_supply:
- if d.is_nil_exempt == 1 and state == d.place_of_supply.split("-")[1]:
+ if (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
+ and state == d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["gst"]["intra"] += d.base_amount
- elif d.is_nil_exempt == 1 and state != d.place_of_supply.split("-")[1]:
+ elif (d.is_nil_exempt == 1 or d.get('gst_category') == 'Registered Composition') \
+ and state != d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["gst"]["inter"] += d.base_amount
elif d.is_non_gst == 1 and state == d.place_of_supply.split("-")[1]:
inward_nil_exempt_details["non_gst"]["intra"] += d.base_amount
diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
index 8174da2..ef8af24 100644
--- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
@@ -14,8 +14,20 @@
test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"]
class TestGSTR3BReport(unittest.TestCase):
- def test_gstr_3b_report(self):
+ def setUp(self):
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'")
+ frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'")
+ frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'")
+
+ make_company()
+ make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000})
+ set_account_heads()
+ make_customers()
+ make_suppliers()
+
+ def test_gstr_3b_report(self):
month_number_mapping = {
1: "January",
2: "February",
@@ -31,17 +43,6 @@
12: "December"
}
- frappe.set_user("Administrator")
-
- frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'")
- frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'")
- frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'")
-
- make_company()
- make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000})
- set_account_heads()
- make_customers()
- make_suppliers()
make_sales_invoice()
create_purchase_invoices()
@@ -63,10 +64,46 @@
self.assertEqual(output["sup_details"]["osup_zero"]["iamt"], 18),
self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18),
self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100),
- self.assertEqual(output["inward_sup"]["isup_details"][0]["inter"], 250)
+ self.assertEqual(output["inward_sup"]["isup_details"][0]["intra"], 250)
self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50)
self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50)
+ def test_gst_rounding(self):
+ gst_settings = frappe.get_doc('GST Settings')
+ gst_settings.round_off_gst_values = 1
+ gst_settings.save()
+
+ current_country = frappe.flags.country
+ frappe.flags.country = 'India'
+
+ si = create_sales_invoice(company="_Test Company GST",
+ customer = '_Test GST Customer',
+ currency = 'INR',
+ warehouse = 'Finished Goods - _GST',
+ debit_to = 'Debtors - _GST',
+ income_account = 'Sales - _GST',
+ expense_account = 'Cost of Goods Sold - _GST',
+ cost_center = 'Main - _GST',
+ rate=216,
+ do_not_save=1
+ )
+
+ si.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "IGST - _GST",
+ "cost_center": "Main - _GST",
+ "description": "IGST @ 18.0",
+ "rate": 18
+ })
+
+ si.save()
+ # Check for 39 instead of 38.88
+ self.assertEqual(si.taxes[0].base_tax_amount_after_discount_amount, 39)
+
+ frappe.flags.country = current_country
+ gst_settings.round_off_gst_values = 1
+ gst_settings.save()
+
def make_sales_invoice():
si = create_sales_invoice(company="_Test Company GST",
customer = '_Test GST Customer',
@@ -145,7 +182,6 @@
si3.submit()
def create_purchase_invoices():
-
pi = make_purchase_invoice(
company="_Test Company GST",
supplier = '_Test Registered Supplier',
@@ -192,8 +228,20 @@
pi1.submit()
-def make_suppliers():
+ pi2 = make_purchase_invoice(company="_Test Company GST",
+ customer = '_Test Registered Supplier',
+ currency = 'INR',
+ item = 'Milk',
+ warehouse = 'Finished Goods - _GST',
+ expense_account = 'Cost of Goods Sold - _GST',
+ cost_center = 'Main - _GST',
+ rate=250,
+ qty=1,
+ do_not_save=1
+ )
+ pi2.submit()
+def make_suppliers():
if not frappe.db.exists("Supplier", "_Test Registered Supplier"):
frappe.get_doc({
"supplier_group": "_Test Supplier Group",
@@ -257,7 +305,6 @@
address.save()
def make_customers():
-
if not frappe.db.exists("Customer", "_Test GST Customer"):
frappe.get_doc({
"customer_group": "_Test Customer Group",
@@ -354,9 +401,9 @@
address.save()
def make_company():
-
if frappe.db.exists("Company", "_Test Company GST"):
return
+
company = frappe.new_doc("Company")
company.company_name = "_Test Company GST"
company.abbr = "_GST"
@@ -388,7 +435,6 @@
address.save()
def set_account_heads():
-
gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all(
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/regional/doctype/tax_exemption_80g_certificate/__init__.py
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js
new file mode 100644
index 0000000..54cde9c
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.js
@@ -0,0 +1,67 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Tax Exemption 80G Certificate', {
+ refresh: function(frm) {
+ if (frm.doc.donor) {
+ frm.set_query('donation', function() {
+ return {
+ filters: {
+ docstatus: 1,
+ donor: frm.doc.donor
+ }
+ };
+ });
+ }
+ },
+
+ recipient: function(frm) {
+ if (frm.doc.recipient === 'Donor') {
+ frm.set_value({
+ 'member': '',
+ 'member_name': '',
+ 'member_email': '',
+ 'member_pan_number': '',
+ 'fiscal_year': '',
+ 'total': 0,
+ 'payments': []
+ });
+ } else {
+ frm.set_value({
+ 'donor': '',
+ 'donor_name': '',
+ 'donor_email': '',
+ 'donor_pan_number': '',
+ 'donation': '',
+ 'date_of_donation': '',
+ 'amount': 0,
+ 'mode_of_payment': '',
+ 'razorpay_payment_id': ''
+ });
+ }
+ },
+
+ get_payments: function(frm) {
+ frm.call({
+ doc: frm.doc,
+ method: 'get_payments',
+ freeze: true
+ });
+ },
+
+ company: function(frm) {
+ if ((frm.doc.member || frm.doc.donor) && frm.doc.company) {
+ frm.call({
+ doc: frm.doc,
+ method: 'set_company_address',
+ freeze: true
+ });
+ }
+ },
+
+ donation: function(frm) {
+ if (frm.doc.recipient === 'Donor' && !frm.doc.donor) {
+ frappe.msgprint(__('Please select donor first'));
+ }
+ }
+});
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json
new file mode 100644
index 0000000..9eee722
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.json
@@ -0,0 +1,297 @@
+{
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2021-02-15 12:37:21.577042",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "recipient",
+ "member",
+ "member_name",
+ "member_email",
+ "member_pan_number",
+ "donor",
+ "donor_name",
+ "donor_email",
+ "donor_pan_number",
+ "column_break_4",
+ "date",
+ "fiscal_year",
+ "section_break_11",
+ "company",
+ "company_address",
+ "company_address_display",
+ "column_break_14",
+ "company_pan_number",
+ "company_80g_number",
+ "company_80g_wef",
+ "title",
+ "section_break_6",
+ "get_payments",
+ "payments",
+ "total",
+ "donation_details_section",
+ "donation",
+ "date_of_donation",
+ "amount",
+ "column_break_27",
+ "mode_of_payment",
+ "razorpay_payment_id"
+ ],
+ "fields": [
+ {
+ "fieldname": "recipient",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Certificate Recipient",
+ "options": "Member\nDonor",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fieldname": "member",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Member",
+ "mandatory_depends_on": "eval:doc.recipient === \"Member\";",
+ "options": "Member"
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fetch_from": "member.member_name",
+ "fieldname": "member_name",
+ "fieldtype": "Data",
+ "label": "Member Name",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fieldname": "donor",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Donor",
+ "mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
+ "options": "Donor"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "payments",
+ "fieldtype": "Table",
+ "label": "Payments",
+ "options": "Tax Exemption 80G Certificate Detail"
+ },
+ {
+ "fieldname": "total",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Total",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fieldname": "fiscal_year",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Fiscal Year",
+ "options": "Fiscal Year"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "fieldname": "get_payments",
+ "fieldtype": "Button",
+ "label": "Get Memberships"
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Naming Series",
+ "options": "NPO-80G-.YYYY.-"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break",
+ "label": "Company Details"
+ },
+ {
+ "fieldname": "company_address",
+ "fieldtype": "Link",
+ "label": "Company Address",
+ "options": "Address"
+ },
+ {
+ "fieldname": "column_break_14",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "company.pan_details",
+ "fieldname": "company_pan_number",
+ "fieldtype": "Data",
+ "label": "PAN Number",
+ "read_only": 1
+ },
+ {
+ "fieldname": "company_address_display",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "label": "Company Address Display",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fetch_from": "company.company_80g_number",
+ "fieldname": "company_80g_number",
+ "fieldtype": "Data",
+ "label": "80G Number",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "company.with_effect_from",
+ "fieldname": "company_80g_wef",
+ "fieldtype": "Date",
+ "label": "80G With Effect From",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fieldname": "donation_details_section",
+ "fieldtype": "Section Break",
+ "label": "Donation Details"
+ },
+ {
+ "fieldname": "donation",
+ "fieldtype": "Link",
+ "label": "Donation",
+ "mandatory_depends_on": "eval:doc.recipient === \"Donor\";",
+ "options": "Donation"
+ },
+ {
+ "fetch_from": "donation.amount",
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "label": "Amount",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donation.mode_of_payment",
+ "fieldname": "mode_of_payment",
+ "fieldtype": "Link",
+ "label": "Mode of Payment",
+ "options": "Mode of Payment",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donation.razorpay_payment_id",
+ "fieldname": "razorpay_payment_id",
+ "fieldtype": "Data",
+ "label": "RazorPay Payment ID",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "donation.date",
+ "fieldname": "date_of_donation",
+ "fieldtype": "Date",
+ "label": "Date of Donation",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_27",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fetch_from": "donor.donor_name",
+ "fieldname": "donor_name",
+ "fieldtype": "Data",
+ "label": "Donor Name",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fetch_from": "donor.email",
+ "fieldname": "donor_email",
+ "fieldtype": "Data",
+ "label": "Email",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fetch_from": "member.email_id",
+ "fieldname": "member_email",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Email",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Member\";",
+ "fetch_from": "member.pan_number",
+ "fieldname": "member_pan_number",
+ "fieldtype": "Data",
+ "label": "PAN Details",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.recipient === \"Donor\";",
+ "fetch_from": "donor.pan_number",
+ "fieldname": "donor_pan_number",
+ "fieldtype": "Data",
+ "label": "PAN Details",
+ "read_only": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Title",
+ "print_hide": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-02-22 00:03:34.215633",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "Tax Exemption 80G Certificate",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "search_fields": "member, member_name",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "title",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
new file mode 100644
index 0000000..41a0f11
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/tax_exemption_80g_certificate.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+from frappe.model.document import Document
+from frappe.utils import getdate, flt, get_link_to_form
+from erpnext.accounts.utils import get_fiscal_year
+from frappe.contacts.doctype.address.address import get_company_address
+
+class TaxExemption80GCertificate(Document):
+ def validate(self):
+ self.validate_date()
+ self.validate_duplicates()
+ self.validate_company_details()
+ self.set_company_address()
+ self.calculate_total()
+ self.set_title()
+
+ def validate_date(self):
+ if self.recipient == 'Member':
+ if getdate(self.date):
+ fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
+
+ if not (fiscal_year.year_start_date <= getdate(self.date) \
+ <= fiscal_year.year_end_date):
+ frappe.throw(_('The Certificate Date is not in the Fiscal Year {0}').format(frappe.bold(self.fiscal_year)))
+
+ def validate_duplicates(self):
+ if self.recipient == 'Donor':
+ certificate = frappe.db.exists(self.doctype, {
+ 'donation': self.donation,
+ 'name': ('!=', self.name)
+ })
+ if certificate:
+ frappe.throw(_('An 80G Certificate {0} already exists for the donation {1}').format(
+ get_link_to_form(self.doctype, certificate), frappe.bold(self.donation)
+ ), title=_('Duplicate Certificate'))
+
+ def validate_company_details(self):
+ fields = ['company_80g_number', 'with_effect_from', 'pan_details']
+ company_details = frappe.db.get_value('Company', self.company, fields, as_dict=True)
+ if not company_details.company_80g_number:
+ frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('80G Number'),
+ get_link_to_form('Company', self.company)))
+
+ if not company_details.pan_details:
+ frappe.throw(_('Please set the {0} for company {1}').format(frappe.bold('PAN Number'),
+ get_link_to_form('Company', self.company)))
+
+ @frappe.whitelist()
+ def set_company_address(self):
+ address = get_company_address(self.company)
+ self.company_address = address.company_address
+ self.company_address_display = address.company_address_display
+
+ def calculate_total(self):
+ if self.recipient == 'Donor':
+ return
+
+ total = 0
+ for entry in self.payments:
+ total += flt(entry.amount)
+ self.total = total
+
+ def set_title(self):
+ if self.recipient == 'Member':
+ self.title = self.member_name
+ else:
+ self.title = self.donor_name
+
+ @frappe.whitelist()
+ def get_payments(self):
+ if not self.member:
+ frappe.throw(_('Please select a Member first.'))
+
+ fiscal_year = get_fiscal_year(fiscal_year=self.fiscal_year, as_dict=True)
+
+ memberships = frappe.db.get_all('Membership', {
+ 'member': self.member,
+ 'from_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
+ 'to_date': ['between', (fiscal_year.year_start_date, fiscal_year.year_end_date)],
+ 'membership_status': ('!=', 'Cancelled')
+ }, ['from_date', 'amount', 'name', 'invoice', 'payment_id'], order_by='from_date')
+
+ if not memberships:
+ frappe.msgprint(_('No Membership Payments found against the Member {0}').format(self.member))
+
+ total = 0
+ self.payments = []
+
+ for doc in memberships:
+ self.append('payments', {
+ 'date': doc.from_date,
+ 'amount': doc.amount,
+ 'invoice_id': doc.invoice,
+ 'razorpay_payment_id': doc.payment_id,
+ 'membership': doc.name
+ })
+ total += flt(doc.amount)
+
+ self.total = total
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py
new file mode 100644
index 0000000..346ebbf
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate/test_tax_exemption_80g_certificate.py
@@ -0,0 +1,101 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from frappe.utils import getdate
+from erpnext.accounts.utils import get_fiscal_year
+from erpnext.non_profit.doctype.donation.test_donation import create_donor, create_mode_of_payment, create_donor_type
+from erpnext.non_profit.doctype.donation.donation import create_donation
+from erpnext.non_profit.doctype.membership.test_membership import setup_membership, make_membership
+from erpnext.non_profit.doctype.member.member import create_member
+
+class TestTaxExemption80GCertificate(unittest.TestCase):
+ def setUp(self):
+ frappe.db.sql('delete from `tabTax Exemption 80G Certificate`')
+ frappe.db.sql('delete from `tabMembership`')
+ create_donor_type()
+ settings = frappe.get_doc('Non Profit Settings')
+ settings.company = '_Test Company'
+ settings.donation_company = '_Test Company'
+ settings.default_donor_type = '_Test Donor'
+ settings.creation_user = 'Administrator'
+ settings.save()
+
+ company = frappe.get_doc('Company', '_Test Company')
+ company.pan_details = 'BBBTI3374C'
+ company.company_80g_number = 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087'
+ company.with_effect_from = getdate()
+ company.save()
+
+ def test_duplicate_donation_certificate(self):
+ donor = create_donor()
+ create_mode_of_payment()
+ payment = frappe._dict({
+ 'amount': 100,
+ 'method': 'Debit Card',
+ 'id': 'pay_MeXAmsgeKOhq7O'
+ })
+ donation = create_donation(donor, payment)
+
+ args = frappe._dict({
+ 'recipient': 'Donor',
+ 'donor': donor.name,
+ 'donation': donation.name
+ })
+ certificate = create_80g_certificate(args)
+ certificate.insert()
+
+ # check company details
+ self.assertEquals(certificate.company_pan_number, 'BBBTI3374C')
+ self.assertEquals(certificate.company_80g_number, 'NQ.CIT(E)I2018-19/DEL-IE28615-27062018/10087')
+
+ # check donation details
+ self.assertEquals(certificate.amount, donation.amount)
+
+ duplicate_certificate = create_80g_certificate(args)
+ # duplicate validation
+ self.assertRaises(frappe.ValidationError, duplicate_certificate.insert)
+
+ def test_membership_80g_certificate(self):
+ plan = setup_membership()
+
+ # make test member
+ member_doc = create_member(frappe._dict({
+ 'fullname': "_Test_Member",
+ 'email': "_test_member_erpnext@example.com",
+ 'plan_id': plan.name
+ }))
+ member_doc.make_customer_and_link()
+ member = member_doc.name
+
+ membership = make_membership(member, { "from_date": getdate() })
+ invoice = membership.generate_invoice(save=True)
+
+ args = frappe._dict({
+ 'recipient': 'Member',
+ 'member': member,
+ 'fiscal_year': get_fiscal_year(getdate(), as_dict=True).get('name')
+ })
+ certificate = create_80g_certificate(args)
+ certificate.get_payments()
+ certificate.insert()
+
+ self.assertEquals(len(certificate.payments), 1)
+ self.assertEquals(certificate.payments[0].amount, membership.amount)
+ self.assertEquals(certificate.payments[0].invoice_id, invoice.name)
+
+
+def create_80g_certificate(args):
+ certificate = frappe.get_doc({
+ 'doctype': 'Tax Exemption 80G Certificate',
+ 'recipient': args.recipient,
+ 'date': getdate(),
+ 'company': '_Test Company'
+ })
+
+ certificate.update(args)
+
+ return certificate
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/regional/doctype/tax_exemption_80g_certificate_detail/__init__.py
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json
new file mode 100644
index 0000000..dfa817d
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.json
@@ -0,0 +1,66 @@
+{
+ "actions": [],
+ "creation": "2021-02-15 12:43:52.754124",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "date",
+ "amount",
+ "invoice_id",
+ "column_break_4",
+ "razorpay_payment_id",
+ "membership"
+ ],
+ "fields": [
+ {
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "in_list_view": 1,
+ "label": "Date",
+ "reqd": 1
+ },
+ {
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Amount",
+ "reqd": 1
+ },
+ {
+ "fieldname": "invoice_id",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Invoice ID",
+ "options": "Sales Invoice",
+ "reqd": 1
+ },
+ {
+ "fieldname": "razorpay_payment_id",
+ "fieldtype": "Data",
+ "label": "Razorpay Payment ID"
+ },
+ {
+ "fieldname": "membership",
+ "fieldtype": "Link",
+ "label": "Membership",
+ "options": "Membership"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-15 16:35:10.777587",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "Tax Exemption 80G Certificate Detail",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py
new file mode 100644
index 0000000..bdad798
--- /dev/null
+++ b/erpnext/regional/doctype/tax_exemption_80g_certificate_detail/tax_exemption_80g_certificate_detail.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class TaxExemption80GCertificateDetail(Document):
+ pass
diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json
index 86290cf..f4a3542 100644
--- a/erpnext/regional/india/e_invoice/einv_validation.json
+++ b/erpnext/regional/india/e_invoice/einv_validation.json
@@ -919,7 +919,8 @@
"minLength": 1,
"maxLength": 15,
"pattern": "^([0-9A-Z/-]){1,15}$",
- "description": "Tranport Document Number"
+ "description": "Tranport Document Number",
+ "validationMsg": "Transport Receipt No is invalid"
},
"TransDocDt": {
"type": "string",
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
index 9fa94c4..7cd64f2 100644
--- a/erpnext/regional/india/e_invoice/einvoice.js
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -1,12 +1,12 @@
erpnext.setup_einvoice_actions = (doctype) => {
frappe.ui.form.on(doctype, {
- refresh(frm) {
- const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
+ async refresh(frm) {
+ const einvoicing_enabled = await frappe.db.get_single_value("E Invoice Settings", "enable");
const supply_type = frm.doc.gst_category;
const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
- if (!einvoicing_enabled || !valid_supply_type || company_transaction) return;
+ if (cint(einvoicing_enabled) == 0 || !valid_supply_type || company_transaction) return;
const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
@@ -83,7 +83,7 @@
const action = () => {
const d = new frappe.ui.Dialog({
title: __('Generate E-Way Bill'),
- wide: 1,
+ size: "large",
fields: get_ewaybill_fields(frm),
primary_action: function() {
const data = d.get_values();
@@ -188,7 +188,6 @@
'fieldname': 'vehicle_no',
'label': 'Vehicle No',
'fieldtype': 'Data',
- 'depends_on': 'eval:(doc.mode_of_transport === "Road")',
'default': frm.doc.vehicle_no
},
{
@@ -253,7 +252,7 @@
const get_preview_dialog = (frm, action) => {
const dialog = new frappe.ui.Dialog({
title: __("Preview"),
- wide: 1,
+ size: "large",
fields: [
{
"label": "Preview",
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index 2043f49..8eccc3f 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -10,6 +10,7 @@
import json
import base64
import frappe
+import six
import traceback
import io
from frappe import _, bold
@@ -86,10 +87,10 @@
invoice_date=invoice_date
))
-def get_party_details(address_name):
+def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None):
d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
- if (not d.gstin
+ if ((not d.gstin and not shipping_address)
or not d.city
or not d.pincode
or not d.address_title
@@ -107,13 +108,17 @@
# according to einvoice standard
pincode = 999999
- return frappe._dict(dict(
- gstin=d.gstin, legal_name=d.address_title,
- location=d.city, pincode=d.pincode,
+ party_address_details = frappe._dict(dict(
+ legal_name=sanitize_for_json(d.address_title),
+ location=sanitize_for_json(d.city),
+ pincode=d.pincode,
state_code=d.gst_state_number,
- address_line1=d.address_line1,
- address_line2=d.address_line2
+ address_line1=sanitize_for_json(d.address_line1),
+ address_line2=sanitize_for_json(d.address_line2)
))
+ if d.gstin:
+ party_address_details.gstin = d.gstin
+ return party_address_details
def get_gstin_details(gstin):
if not hasattr(frappe.local, 'gstin_cache'):
@@ -146,8 +151,11 @@
)
return frappe._dict(dict(
- gstin='URP', legal_name=address_title, location=city,
- address_line1=address_line1, address_line2=address_line2,
+ gstin='URP',
+ legal_name=sanitize_for_json(address_title),
+ location=city,
+ address_line1=sanitize_for_json(address_line1),
+ address_line2=sanitize_for_json(address_line2),
pincode=999999, state_code=96, place_of_supply=96
))
@@ -160,7 +168,7 @@
item.update(d.as_dict())
item.sr_no = d.idx
- item.description = d.item_name.replace('"', '\\"')
+ item.description = sanitize_for_json(d.item_name)
item.qty = abs(item.qty)
item.discount_amount = 0
@@ -196,9 +204,11 @@
item[attr] = 0
for t in invoice.taxes:
- # this contains item wise tax rate & tax amount (incl. discount)
- item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
- if t.account_head in gst_accounts_list:
+ is_applicable = t.tax_amount and t.account_head in gst_accounts_list
+ if is_applicable:
+ # this contains item wise tax rate & tax amount (incl. discount)
+ item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
+
item_tax_rate = item_tax_detail[0]
# item tax amount excluding discount amount
item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
@@ -223,7 +233,7 @@
if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
invoice_value_details.base_total = abs(invoice.base_total)
- invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
+ invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount)
else:
invoice_value_details.base_total = abs(invoice.base_net_total)
# since tax already considers discount amount
@@ -320,14 +330,17 @@
item_list = get_item_list(invoice)
doc_details = get_doc_details(invoice)
invoice_value_details = get_invoice_value_details(invoice)
- seller_details = get_party_details(invoice.company_address)
+ seller_details = get_party_details(invoice.company_address, company_address=1)
if invoice.gst_category == 'Overseas':
buyer_details = get_overseas_address_details(invoice.customer_address)
else:
- buyer_details = get_party_details(invoice.customer_address)
- place_of_supply = get_place_of_supply(invoice, invoice.doctype) or invoice.billing_address_gstin
- place_of_supply = place_of_supply[:2]
+ buyer_details = get_party_details(invoice.customer_address, billing_address=1)
+ place_of_supply = get_place_of_supply(invoice, invoice.doctype)
+ if place_of_supply:
+ place_of_supply = place_of_supply.split('-')[0]
+ else:
+ place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2]
buyer_details.update(dict(place_of_supply=place_of_supply))
shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
@@ -335,7 +348,7 @@
if invoice.gst_category == 'Overseas':
shipping_details = get_overseas_address_details(invoice.shipping_address_name)
else:
- shipping_details = get_party_details(invoice.shipping_address_name)
+ shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=1)
if invoice.is_pos and invoice.base_paid_amount:
payment_details = get_payment_details(invoice)
@@ -356,7 +369,7 @@
period_details=period_details, prev_doc_details=prev_doc_details,
export_details=export_details, eway_bill_details=eway_bill_details
)
- einvoice = json.loads(einvoice)
+ einvoice = safe_json_load(einvoice)
validations = json.loads(read_json('einv_validation'))
errors = validate_einvoice(validations, einvoice)
@@ -371,7 +384,21 @@
return einvoice
-def validate_einvoice(validations, einvoice, errors=[]):
+def safe_json_load(json_string):
+ JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
+
+ try:
+ return json.loads(json_string)
+ except JSONDecodeError as e:
+ # print a snippet of 40 characters around the location where error occured
+ pos = e.pos
+ start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
+ snippet = json_string[start:end]
+ frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet))
+
+def validate_einvoice(validations, einvoice, errors=None):
+ if errors is None:
+ errors = []
for fieldname, field_validation in validations.items():
value = einvoice.get(fieldname, None)
if not value or value == "None":
@@ -798,6 +825,13 @@
self.invoice.flags.ignore_validate = True
self.invoice.save()
+
+def sanitize_for_json(string):
+ """Escape JSON specific characters from a string."""
+
+ # json.dumps adds double-quotes to the string. Indexing to remove them.
+ return json.dumps(string)[1:-1]
+
@frappe.whitelist()
def get_einvoice(doctype, docname):
invoice = frappe.get_doc(doctype, docname)
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index 5261984..f7689cf 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -5,6 +5,7 @@
import frappe, os, json
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.india import states
from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
@@ -18,9 +19,11 @@
# TODO: for all countries
def setup_company_independent_fixtures():
make_custom_fields()
+ make_property_setters()
add_permissions()
add_custom_roles_for_reports()
frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test)
+ create_gratuity_rule()
add_print_formats()
def add_hsn_sac_codes():
@@ -105,8 +108,14 @@
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
- frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
- name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
+ frappe.db.set_value("Print Format", "GST POS Invoice", "disabled", 0)
+ frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0)
+ frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0)
+
+def make_property_setters():
+ # GST rules do not allow for an invoice no. bigger than 16 characters
+ make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
+ make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@@ -398,9 +407,9 @@
si_einvoice_fields = [
dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
-
+
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
-
+
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
@@ -498,6 +507,14 @@
fieldtype='Link', options='Salary Component', insert_after='basic_component'),
dict(fieldname='arrear_component', label='Arrear Component',
fieldtype='Link', options='Salary Component', insert_after='hra_component'),
+ dict(fieldname='non_profit_section', label='Non Profit Settings',
+ fieldtype='Section Break', insert_after='asset_received_but_not_billed', collapsible=1),
+ dict(fieldname='company_80g_number', label='80G Number',
+ fieldtype='Data', insert_after='non_profit_section'),
+ dict(fieldname='with_effect_from', label='80G With Effect From',
+ fieldtype='Date', insert_after='company_80g_number'),
+ dict(fieldname='pan_details', label='PAN Number',
+ fieldtype='Data', insert_after='with_effect_from')
],
'Employee Tax Exemption Declaration':[
dict(fieldname='hra_section', label='HRA Exemption',
@@ -580,7 +597,15 @@
'options': '\nWith Payment of Tax\nWithout Payment of Tax'
}
],
- "Member": [
+ 'Member': [
+ {
+ 'fieldname': 'pan_number',
+ 'label': 'PAN Details',
+ 'fieldtype': 'Data',
+ 'insert_after': 'email_id'
+ }
+ ],
+ 'Donor': [
{
'fieldname': 'pan_number',
'label': 'PAN Details',
@@ -642,7 +667,7 @@
pass
docs = get_tds_details(accounts, fiscal_year)
-
+
for d in docs:
try:
doc = frappe.get_doc(d)
@@ -660,7 +685,7 @@
fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
if not fy_exist:
doc.append("rates", d.get('rates')[0])
-
+
doc.flags.ignore_permissions = True
doc.flags.ignore_mandatory = True
doc.save()
@@ -822,4 +847,24 @@
doctype="Tax Withholding Category", accounts=accounts,
rates=[{"fiscal_year": fiscal_year, "tax_withholding_rate": 20,
"single_threshold": 2500, "cumulative_threshold": 0}])
- ]
\ No newline at end of file
+ ]
+
+def create_gratuity_rule():
+
+ # Standard Indain Gratuity Rule
+ if not frappe.db.exists("Gratuity Rule", "Indian Standard Gratuity Rule"):
+ rule = frappe.new_doc("Gratuity Rule")
+ rule.name = "Indian Standard Gratuity Rule"
+ rule.calculate_gratuity_amount_based_on = "Current Slab"
+ rule.work_experience_calculation_method = "Round Off Work Experience"
+ rule.minimum_year_for_gratuity = 5
+
+ fraction = 15/26
+ rule.append("gratuity_rule_slabs", {
+ "from_year": 0,
+ "to_year":0,
+ "fraction_of_applicable_earnings": fraction
+ })
+
+ rule.flags.ignore_mandatory = True
+ rule.save()
diff --git a/erpnext/regional/india/test_utils.py b/erpnext/regional/india/test_utils.py
new file mode 100644
index 0000000..7ce27f6
--- /dev/null
+++ b/erpnext/regional/india/test_utils.py
@@ -0,0 +1,38 @@
+from __future__ import unicode_literals
+
+import unittest
+import frappe
+from unittest.mock import patch
+from erpnext.regional.india.utils import validate_document_name
+
+
+class TestIndiaUtils(unittest.TestCase):
+ @patch("frappe.get_cached_value")
+ def test_validate_document_name(self, mock_get_cached):
+ mock_get_cached.return_value = "India" # mock country
+ posting_date = "2021-05-01"
+
+ invalid_names = [ "SI$1231", "012345678901234567", "SI 2020 05",
+ "SI.2020.0001", "PI2021 - 001" ]
+ for name in invalid_names:
+ doc = frappe._dict(name=name, posting_date=posting_date)
+ self.assertRaises(frappe.ValidationError, validate_document_name, doc)
+
+ valid_names = [ "012345678901236", "SI/2020/0001", "SI/2020-0001",
+ "2020-PI-0001", "PI2020-0001" ]
+ for name in valid_names:
+ doc = frappe._dict(name=name, posting_date=posting_date)
+ try:
+ validate_document_name(doc)
+ except frappe.ValidationError:
+ self.fail("Valid name {} throwing error".format(name))
+
+ @patch("frappe.get_cached_value")
+ def test_validate_document_name_not_india(self, mock_get_cached):
+ mock_get_cached.return_value = "Not India"
+ doc = frappe._dict(name="SI$123", posting_date="2021-05-01")
+
+ try:
+ validate_document_name(doc)
+ except frappe.ValidationError:
+ self.fail("Regional validation related to India are being applied to other countries")
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index e89885f..3637de4 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -2,7 +2,7 @@
import frappe, re, json
from frappe import _
import erpnext
-from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words
+from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate
from erpnext.regional.india import states, state_numbers
from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount
from erpnext.controllers.accounts_controller import get_taxes_and_charges
@@ -14,6 +14,13 @@
from erpnext.accounts.utils import get_account_currency
from frappe.model.utils import get_fetch_values
+
+GST_INVOICE_NUMBER_FORMAT = re.compile(r"^[a-zA-Z0-9\-/]+$") #alphanumeric and - /
+GSTIN_FORMAT = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$")
+GSTIN_UIN_FORMAT = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}")
+PAN_NUMBER_FORMAT = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
+
+
def validate_gstin_for_india(doc, method):
if hasattr(doc, 'gst_state') and doc.gst_state:
doc.gst_state_number = state_numbers[doc.gst_state]
@@ -37,12 +44,10 @@
frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters."))
if gst_category and gst_category == 'UIN Holders':
- p = re.compile("^[0-9]{4}[A-Z]{3}[0-9]{5}[0-9A-Z]{3}")
- if not p.match(doc.gstin):
+ if not GSTIN_UIN_FORMAT.match(doc.gstin):
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"))
else:
- p = re.compile("^[0-9]{2}[A-Z]{4}[0-9A-Z]{1}[0-9]{4}[A-Z]{1}[1-9A-Z]{1}[1-9A-Z]{1}[0-9A-Z]{1}$")
- if not p.match(doc.gstin):
+ if not GSTIN_FORMAT.match(doc.gstin):
frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN."))
validate_gstin_check_digit(doc.gstin)
@@ -55,6 +60,13 @@
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number))
+def validate_pan_for_india(doc, method):
+ if doc.get('country') != 'India' or not doc.pan:
+ return
+
+ if not PAN_NUMBER_FORMAT.match(doc.pan):
+ frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN."))
+
def validate_tax_category(doc, method):
if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
if doc.is_inter_state:
@@ -140,6 +152,20 @@
def set_place_of_supply(doc, method=None):
doc.place_of_supply = get_place_of_supply(doc, doc.doctype)
+def validate_document_name(doc, method=None):
+ """Validate GST invoice number requirements."""
+ country = frappe.get_cached_value("Company", doc.company, "country")
+
+ # Date was chosen as start of next FY to avoid irritating current users.
+ if country != "India" or getdate(doc.posting_date) < getdate("2021-04-01"):
+ return
+
+ if len(doc.name) > 16:
+ frappe.throw(_("Maximum length of document number should be 16 characters as per GST rules. Please change the naming series."))
+
+ if not GST_INVOICE_NUMBER_FORMAT.match(doc.name):
+ frappe.throw(_("Document name should only contain alphanumeric values, dash(-) and slash(/) characters as per GST rules. Please change the naming series."))
+
# don't remove this function it is used in tests
def test_method():
'''test function'''
@@ -693,25 +719,12 @@
if country != 'India':
return
- if not doc.total_taxes_and_charges:
+ gst_tax, base_gst_tax = get_gst_tax_amount(doc)
+
+ if not base_gst_tax:
return
if doc.reverse_charge == 'Y':
- gst_accounts = get_gst_accounts(doc.company)
- gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
- + gst_accounts.get('igst_account')
-
- base_gst_tax = 0
- gst_tax = 0
-
- for tax in doc.get('taxes'):
- if tax.category not in ("Total", "Valuation and Total"):
- continue
-
- if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
- base_gst_tax += tax.base_tax_amount_after_discount_amount
- gst_tax += tax.tax_amount_after_discount_amount
-
doc.taxes_and_charges_added -= gst_tax
doc.total_taxes_and_charges -= gst_tax
doc.base_taxes_and_charges_added -= base_gst_tax
@@ -745,6 +758,11 @@
if country != 'India':
return gl_entries
+ gst_tax, base_gst_tax = get_gst_tax_amount(doc)
+
+ if not base_gst_tax:
+ return gl_entries
+
if doc.reverse_charge == 'Y':
gst_accounts = get_gst_accounts(doc.company)
gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
@@ -772,3 +790,45 @@
)
return gl_entries
+
+def get_gst_tax_amount(doc):
+ gst_accounts = get_gst_accounts(doc.company)
+ gst_account_list = gst_accounts.get('cgst_account', []) + gst_accounts.get('sgst_account', []) \
+ + gst_accounts.get('igst_account', [])
+
+ base_gst_tax = 0
+ gst_tax = 0
+
+ for tax in doc.get('taxes'):
+ if tax.category not in ("Total", "Valuation and Total"):
+ continue
+
+ if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
+ base_gst_tax += tax.base_tax_amount_after_discount_amount
+ gst_tax += tax.tax_amount_after_discount_amount
+
+ return gst_tax, base_gst_tax
+
+@frappe.whitelist()
+def get_regional_round_off_accounts(company, account_list):
+ country = frappe.get_cached_value('Company', company, 'country')
+
+ if country != 'India':
+ return
+
+ if isinstance(account_list, string_types):
+ account_list = json.loads(account_list)
+
+ if not frappe.db.get_single_value('GST Settings', 'round_off_gst_values'):
+ return
+
+ gst_accounts = get_gst_accounts(company)
+
+ gst_account_list = []
+ for account in ['cgst_account', 'sgst_account', 'igst_account']:
+ if account in gst_accounts:
+ gst_account_list += gst_accounts.get(account)
+
+ account_list.extend(gst_account_list)
+
+ return account_list
diff --git a/erpnext/regional/italy/sales_invoice.js b/erpnext/regional/italy/sales_invoice.js
index 586a529..b54ac53 100644
--- a/erpnext/regional/italy/sales_invoice.js
+++ b/erpnext/regional/italy/sales_invoice.js
@@ -11,15 +11,10 @@
callback: function(r) {
frm.reload_doc();
if(r.message) {
- var w = window.open(
- frappe.urllib.get_full_url(
- "/api/method/erpnext.regional.italy.utils.download_e_invoice_file?"
- + "file_name=" + r.message
- )
- )
- if (!w) {
- frappe.msgprint(__("Please enable pop-ups")); return;
- }
+ open_url_post(frappe.request.url, {
+ cmd: 'frappe.core.doctype.file.file.download_file',
+ file_url: r.message
+ });
}
}
});
diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py
index 217d623..a1f5bb9 100644
--- a/erpnext/regional/italy/setup.py
+++ b/erpnext/regional/italy/setup.py
@@ -128,11 +128,8 @@
fetch_from="company.vat_collectability"),
dict(fieldname='sb_e_invoicing_reference', label='E-Invoicing',
fieldtype='Section Break', insert_after='against_income_account', print_hide=1),
- dict(fieldname='company_tax_id', label='Company Tax ID',
- fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1,
- fetch_from="company.tax_id"),
dict(fieldname='company_fiscal_code', label='Company Fiscal Code',
- fieldtype='Data', insert_after='company_tax_id', print_hide=1, read_only=1,
+ fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1,
fetch_from="company.fiscal_code"),
dict(fieldname='company_fiscal_regime', label='Company Fiscal Regime',
fieldtype='Data', insert_after='company_fiscal_code', print_hide=1, read_only=1,
@@ -189,9 +186,7 @@
def setup_report():
report_name = 'Electronic Invoice Register'
-
- frappe.db.sql(""" update `tabReport` set disabled = 0 where
- name = %s """, report_name)
+ frappe.db.set_value("Report", report_name, "disabled", 0)
if not frappe.db.get_value('Custom Role', dict(report=report_name)):
frappe.get_doc(dict(
@@ -219,4 +214,4 @@
update_permission_property(doctype, 'Accounts Manager', 0, 'delete', 1)
add_permission(doctype, 'Accounts Manager', 1)
update_permission_property(doctype, 'Accounts Manager', 1, 'write', 1)
- update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1)
\ No newline at end of file
+ update_permission_property(doctype, 'Accounts Manager', 1, 'create', 1)
diff --git a/erpnext/regional/italy/utils.py b/erpnext/regional/italy/utils.py
index 6842fb2..08573cd 100644
--- a/erpnext/regional/italy/utils.py
+++ b/erpnext/regional/italy/utils.py
@@ -1,6 +1,8 @@
from __future__ import unicode_literals
-import frappe, json, os
+import io
+import json
+import frappe
from frappe.utils import flt, cstr
from erpnext.controllers.taxes_and_totals import get_itemised_tax
from frappe import _
@@ -28,20 +30,22 @@
@frappe.whitelist()
def export_invoices(filters=None):
- saved_xmls = []
+ frappe.has_permission('Sales Invoice', throw=True)
- invoices = frappe.get_all("Sales Invoice", filters=get_conditions(filters), fields=["*"])
+ invoices = frappe.get_all(
+ "Sales Invoice",
+ filters=get_conditions(filters),
+ fields=["name", "company_tax_id"]
+ )
- for invoice in invoices:
- attachments = get_e_invoice_attachments(invoice)
- saved_xmls += [attachment.file_name for attachment in attachments]
+ attachments = get_e_invoice_attachments(invoices)
- zip_filename = "{0}-einvoices.zip".format(frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S"))
+ zip_filename = "{0}-einvoices.zip".format(
+ frappe.utils.get_datetime().strftime("%Y%m%d_%H%M%S"))
- download_zip(saved_xmls, zip_filename)
+ download_zip(attachments, zip_filename)
-@frappe.whitelist()
def prepare_invoice(invoice, progressive_number):
#set company information
company = frappe.get_doc("Company", invoice.company)
@@ -98,7 +102,7 @@
def get_conditions(filters):
filters = json.loads(filters)
- conditions = {"docstatus": 1}
+ conditions = {"docstatus": 1, "company_tax_id": ("!=", "")}
if filters.get("company"): conditions["company"] = filters["company"]
if filters.get("customer"): conditions["customer"] = filters["customer"]
@@ -111,23 +115,22 @@
return conditions
-#TODO: Use function from frappe once PR #6853 is merged.
+
def download_zip(files, output_filename):
- from zipfile import ZipFile
+ import zipfile
- input_files = [frappe.get_site_path('private', 'files', filename) for filename in files]
- output_path = frappe.get_site_path('private', 'files', output_filename)
+ zip_stream = io.BytesIO()
+ with zipfile.ZipFile(zip_stream, 'w', zipfile.ZIP_DEFLATED) as zip_file:
+ for file in files:
+ file_path = frappe.utils.get_files_path(
+ file.file_name, is_private=file.is_private)
- with ZipFile(output_path, 'w') as output_zip:
- for input_file in input_files:
- output_zip.write(input_file, arcname=os.path.basename(input_file))
-
- with open(output_path, 'rb') as fileobj:
- filedata = fileobj.read()
+ zip_file.write(file_path, arcname=file.file_name)
frappe.local.response.filename = output_filename
- frappe.local.response.filecontent = filedata
+ frappe.local.response.filecontent = zip_stream.getvalue()
frappe.local.response.type = "download"
+ zip_stream.close()
def get_invoice_summary(items, taxes):
summary_data = frappe._dict()
@@ -307,23 +310,12 @@
@frappe.whitelist()
def generate_single_invoice(docname):
doc = frappe.get_doc("Sales Invoice", docname)
-
+ frappe.has_permission("Sales Invoice", doc=doc, throw=True)
e_invoice = prepare_and_attach_invoice(doc, True)
+ return e_invoice.file_url
- return e_invoice.file_name
-
-@frappe.whitelist()
-def download_e_invoice_file(file_name):
- content = None
- with open(frappe.get_site_path('private', 'files', file_name), "r") as f:
- content = f.read()
-
- frappe.local.response.filename = file_name
- frappe.local.response.filecontent = content
- frappe.local.response.type = "download"
-
-#Delete e-invoice attachment on cancel.
+# Delete e-invoice attachment on cancel.
def sales_invoice_on_cancel(doc, method):
if get_company_country(doc.company) not in ['Italy',
'Italia', 'Italian Republic', 'Repubblica Italiana']:
@@ -335,16 +327,38 @@
def get_company_country(company):
return frappe.get_cached_value('Company', company, 'country')
-def get_e_invoice_attachments(invoice):
- if not invoice.company_tax_id:
- return []
+def get_e_invoice_attachments(invoices):
+ if not isinstance(invoices, list):
+ if not invoices.company_tax_id:
+ return
+
+ invoices = [invoices]
+
+ tax_id_map = {
+ invoice.name: (
+ invoice.company_tax_id
+ if invoice.company_tax_id.startswith("IT")
+ else "IT" + invoice.company_tax_id
+ ) for invoice in invoices
+ }
+
+ attachments = frappe.get_all(
+ "File",
+ fields=("name", "file_name", "attached_to_name", "is_private"),
+ filters= {
+ "attached_to_name": ('in', tax_id_map),
+ "attached_to_doctype": 'Sales Invoice'
+ }
+ )
out = []
- attachments = get_attachments(invoice.doctype, invoice.name)
- company_tax_id = invoice.company_tax_id if invoice.company_tax_id.startswith("IT") else "IT" + invoice.company_tax_id
-
for attachment in attachments:
- if attachment.file_name and attachment.file_name.startswith(company_tax_id) and attachment.file_name.endswith(".xml"):
+ if (
+ attachment.file_name
+ and attachment.file_name.endswith(".xml")
+ and attachment.file_name.startswith(
+ tax_id_map.get(attachment.attached_to_name))
+ ):
out.append(attachment)
return out
diff --git a/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json
new file mode 100644
index 0000000..a8da0bd
--- /dev/null
+++ b/erpnext/regional/print_format/80g_certificate_for_donation/80g_certificate_for_donation.json
@@ -0,0 +1,26 @@
+{
+ "absolute_value": 0,
+ "align_labels_right": 0,
+ "creation": "2021-02-22 00:17:33.878581",
+ "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}",
+ "custom_format": 1,
+ "default_print_language": "en",
+ "disabled": 0,
+ "doc_type": "Tax Exemption 80G Certificate",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "html": "{% if letter_head and not no_letterhead -%}\n <div class=\"letter-head\">{{ letter_head }}</div>\n{%- endif %}\n\n<div>\n <h3 class=\"text-center\">{{ doc.company }} 80G Donor Certificate</h3>\n</div>\n<br><br>\n\n<div class=\"details\">\n <p> <b>{{ _(\"Certificate No. : \") }}</b> {{ doc.name }} </p>\n <p>\n \t<b>{{ _(\"Date\") }} :</b> {{ doc.get_formatted(\"date\") }}<br>\n </p>\n <br><br>\n \n <div>\n\n This is to confirm that the {{ doc.company }} received an amount of <b>{{doc.get_formatted(\"amount\")}}</b>\n from <b>{{ doc.donor_name }}</b>\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n\n via the Mode of Payment {{doc.mode_of_payment}}\n\n {% if doc.razorpay_payment_id -%}\n bearing RazorPay Payment ID {{ doc.razorpay_payment_id }}\n {%- endif %}\n\n on {{ doc.get_formatted(\"date_of_donation\") }}\n <br><br>\n \n <p>\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n </p>\n\n </div>\n</div>\n\n<br><br>\n<p class=\"company-address text-left\"> {{doc.company_address_display }}</p>\n\n<div class=\"certificate-footer text-center\">\n <p><i>Computer generated receipt - Does not require signature</i></p><br>\n \n {% if doc.company_pan_number %}\n <p>\n <b>{{ doc.company }}'s PAN Account No :</b> {{ doc.company_pan_number }}\n <p><br>\n {% endif %}\n \n <p>\n <b>80G Number : </b> {{ doc.company_80g_number }}\n {% if doc.company_80g_wef %}\n ( w.e.f. {{ doc.get_formatted('company_80g_wef') }} )\n {% endif %}\n </p><br>\n</div>",
+ "idx": 0,
+ "line_breaks": 0,
+ "modified": "2021-02-22 00:20:08.516600",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "80G Certificate for Donation",
+ "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/page/bank_reconciliation/__init__.py b/erpnext/regional/print_format/80g_certificate_for_donation/__init__.py
similarity index 100%
copy from erpnext/accounts/page/bank_reconciliation/__init__.py
copy to erpnext/regional/print_format/80g_certificate_for_donation/__init__.py
diff --git a/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json
new file mode 100644
index 0000000..f1b15aa
--- /dev/null
+++ b/erpnext/regional/print_format/80g_certificate_for_membership/80g_certificate_for_membership.json
@@ -0,0 +1,26 @@
+{
+ "absolute_value": 0,
+ "align_labels_right": 0,
+ "creation": "2021-02-15 16:53:55.026611",
+ "css": ".details {\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n line-height: 150%;\n}\n\n.certificate-footer {\n font-size: 15px;\n font-family: Tahoma, sans-serif;\n line-height: 140%;\n margin-top: 120px;\n}\n\n.company-address {\n color: #666666;\n font-size: 15px;\n font-family: Tahoma, sans-serif;;\n}",
+ "custom_format": 1,
+ "default_print_language": "en",
+ "disabled": 0,
+ "doc_type": "Tax Exemption 80G Certificate",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "html": "{% if letter_head and not no_letterhead -%}\n <div class=\"letter-head\">{{ letter_head }}</div>\n{%- endif %}\n\n<div>\n <h3 class=\"text-center\">{{ doc.company }} Members 80G Donor Certificate</h3>\n <h3 class=\"text-center\">Financial Cycle {{ doc.fiscal_year }}</h3>\n</div>\n<br><br>\n\n<div class=\"details\">\n <p> <b>{{ _(\"Certificate No. : \") }}</b> {{ doc.name }} </p>\n <p>\n \t<b>{{ _(\"Date\") }} :</b> {{ doc.get_formatted(\"date\") }}<br>\n </p>\n <br><br>\n \n <div>\n This is to confirm that the {{ doc.company }} received a total amount of <b>{{doc.get_formatted(\"total\")}}</b>\n from <b>{{ doc.member_name }}</b>\n {% if doc.pan_number -%}\n bearing PAN Number {{ doc.member_pan_number }}\n {%- endif %}\n as per the payment details given below:\n \n <br><br>\n <table class=\"table table-bordered table-condensed\">\n \t<thead>\n \t\t<tr>\n \t\t\t<th >{{ _(\"Date\") }}</th>\n \t\t\t<th class=\"text-right\">{{ _(\"Amount\") }}</th>\n \t\t\t<th class=\"text-right\">{{ _(\"Invoice ID\") }}</th>\n \t\t</tr>\n \t</thead>\n \t<tbody>\n \t\t{%- for payment in doc.payments -%}\n \t\t<tr>\n \t\t\t<td> {{ payment.date }} </td>\n \t\t\t<td class=\"text-right\">{{ payment.get_formatted(\"amount\") }}</td>\n \t\t\t<td class=\"text-right\">{{ payment.invoice_id }}</td>\n \t\t</tr>\n \t\t{%- endfor -%}\n \t</tbody>\n </table>\n \n <br>\n \n <p>\n We thank you for your contribution towards the corpus of the {{ doc.company }} and helping support our work.\n </p>\n\n </div>\n</div>\n\n<br><br>\n<p class=\"company-address text-left\"> {{doc.company_address_display }}</p>\n\n<div class=\"certificate-footer text-center\">\n <p><i>Computer generated receipt - Does not require signature</i></p><br>\n \n {% if doc.company_pan_number %}\n <p>\n <b>{{ doc.company }}'s PAN Account No :</b> {{ doc.company_pan_number }}\n <p><br>\n {% endif %}\n \n <p>\n <b>80G Number : </b> {{ doc.company_80g_number }}\n {% if doc.company_80g_wef %}\n ( w.e.f. {{ doc.get_formatted('company_80g_wef') }} )\n {% endif %}\n </p><br>\n</div>",
+ "idx": 0,
+ "line_breaks": 0,
+ "modified": "2021-02-21 23:29:00.778973",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "80G Certificate for Membership",
+ "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/doctype/bank_statement_settings/__init__.py b/erpnext/regional/print_format/80g_certificate_for_membership/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/regional/print_format/80g_certificate_for_membership/__init__.py
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 96dc3f7..62faa30 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -78,7 +78,7 @@
place_of_supply = invoice_details.get("place_of_supply")
ecommerce_gstin = invoice_details.get("ecommerce_gstin")
- b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin, inv),{
+ b2cs_output.setdefault((rate, place_of_supply, ecommerce_gstin),{
"place_of_supply": "",
"ecommerce_gstin": "",
"rate": "",
@@ -90,7 +90,7 @@
"invoice_value": invoice_details.get("base_grand_total"),
})
- row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin, inv))
+ row = b2cs_output.get((rate, place_of_supply, ecommerce_gstin))
row["place_of_supply"] = place_of_supply
row["ecommerce_gstin"] = ecommerce_gstin
row["rate"] = rate
@@ -236,6 +236,7 @@
self.cgst_sgst_invoices = []
unidentified_gst_accounts = []
+ unidentified_gst_accounts_invoice = []
for parent, account, item_wise_tax_detail, tax_amount in self.tax_details:
if account in self.gst_accounts.cess_account:
self.invoice_cess.setdefault(parent, tax_amount)
@@ -251,6 +252,7 @@
if not (cgst_or_sgst or account in self.gst_accounts.igst_account):
if "gst" in account.lower() and account not in unidentified_gst_accounts:
unidentified_gst_accounts.append(account)
+ unidentified_gst_accounts_invoice.append(parent)
continue
for item_code, tax_amounts in item_wise_tax_detail.items():
@@ -273,7 +275,7 @@
# Build itemised tax for export invoices where tax table is blank
for invoice, items in iteritems(self.invoice_items):
- if invoice not in self.items_based_on_tax_rate \
+ if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \
and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py
index f899349..616c2b8 100644
--- a/erpnext/regional/report/gstr_2/gstr_2.py
+++ b/erpnext/regional/report/gstr_2/gstr_2.py
@@ -44,7 +44,7 @@
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
invoice_details = self.invoices.get(inv)
for rate, items in items_based_on_rate.items():
- if rate:
+ if rate or invoice_details.get('gst_category') == 'Registered Composition':
if inv not in self.igst_invoices:
rate = rate / 2
row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items)
@@ -86,7 +86,7 @@
conditions += opts[1]
if self.filters.get("type_of_business") == "B2B":
- conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') and is_return != 1 "
+ conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ', 'Registered Composition') and is_return != 1 "
elif self.filters.get("type_of_business") == "CDNR":
conditions += """ and is_return = 1 """
diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py
index 776a82c..68208ab 100644
--- a/erpnext/regional/united_arab_emirates/setup.py
+++ b/erpnext/regional/united_arab_emirates/setup.py
@@ -7,12 +7,15 @@
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.permissions import add_permission, update_permission_property
from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax
+from erpnext.payroll.doctype.gratuity_rule.gratuity_rule import get_gratuity_rule
def setup(company=None, patch=True):
make_custom_fields()
add_print_formats()
add_custom_roles_for_reports()
add_permissions()
+ create_gratuity_rule()
+
if company:
create_sales_tax(company)
@@ -155,3 +158,93 @@
add_permission(doctype, role, 0)
update_permission_property(doctype, role, 0, 'write', 1)
update_permission_property(doctype, role, 0, 'create', 1)
+
+def create_gratuity_rule():
+ rule_1 = rule_2 = rule_3 = None
+
+ # Rule Under Limited Contract
+ slabs = get_slab_for_limited_contract()
+ if not frappe.db.exists("Gratuity Rule", "Rule Under Limited Contract (UAE)"):
+ rule_1 = get_gratuity_rule("Rule Under Limited Contract (UAE)", slabs, calculate_gratuity_amount_based_on="Sum of all previous slabs")
+
+ # Rule Under Unlimited Contract on termination
+ slabs = get_slab_for_unlimited_contract_on_termination()
+ if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on termination (UAE)"):
+ rule_2 = get_gratuity_rule("Rule Under Unlimited Contract on termination (UAE)", slabs)
+
+ # Rule Under Unlimited Contract on resignation
+ slabs = get_slab_for_unlimited_contract_on_resignation()
+ if not frappe.db.exists("Gratuity Rule", "Rule Under Unlimited Contract on resignation (UAE)"):
+ rule_3 = get_gratuity_rule("Rule Under Unlimited Contract on resignation (UAE)", slabs)
+
+ #for applicable salary component user need to set this by its own
+ if rule_1:
+ rule_1.flags.ignore_mandatory = True
+ rule_1.save()
+ if rule_2:
+ rule_2.flags.ignore_mandatory = True
+ rule_2.save()
+ if rule_3:
+ rule_3.flags.ignore_mandatory = True
+ rule_3.save()
+
+
+def get_slab_for_limited_contract():
+ return [{
+ "from_year": 0,
+ "to_year":1,
+ "fraction_of_applicable_earnings": 0
+ },
+ {
+ "from_year": 1,
+ "to_year":5,
+ "fraction_of_applicable_earnings": 21/30
+ },
+ {
+ "from_year": 5,
+ "to_year":0,
+ "fraction_of_applicable_earnings": 1
+ }]
+
+def get_slab_for_unlimited_contract_on_termination():
+ return [{
+ "from_year": 0,
+ "to_year":1,
+ "fraction_of_applicable_earnings": 0
+ },
+ {
+ "from_year": 1,
+ "to_year":5,
+ "fraction_of_applicable_earnings": 21/30
+ },
+ {
+ "from_year": 5,
+ "to_year":0,
+ "fraction_of_applicable_earnings": 1
+ }]
+
+def get_slab_for_unlimited_contract_on_resignation():
+ fraction_1 = 1/3 * 21/30
+ fraction_2 = 2/3 * 21/30
+ fraction_3 = 21/30
+
+ return [{
+ "from_year": 0,
+ "to_year":1,
+ "fraction_of_applicable_earnings": 0
+ },
+ {
+ "from_year": 1,
+ "to_year":3,
+ "fraction_of_applicable_earnings": fraction_1
+ },
+ {
+ "from_year": 3,
+ "to_year":5,
+ "fraction_of_applicable_earnings": fraction_2
+ },
+ {
+ "from_year": 5,
+ "to_year":0,
+ "fraction_of_applicable_earnings": fraction_3
+ }]
diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py
index 2b0ecaf..24ab1cf 100644
--- a/erpnext/regional/united_states/setup.py
+++ b/erpnext/regional/united_states/setup.py
@@ -36,5 +36,4 @@
def add_print_formats():
frappe.reload_doc("regional", "print_format", "irs_1099_form")
- frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
- name in('IRS 1099 Form') """)
+ frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0)
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 557c715..7d5e84d 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -16,6 +16,8 @@
"customer_name",
"gender",
"customer_type",
+ "pan",
+ "tax_withholding_category",
"default_bank_account",
"lead_name",
"image",
@@ -34,9 +36,8 @@
"companies",
"currency_and_price_list",
"default_currency",
- "default_price_list",
"column_break_14",
- "language",
+ "default_price_list",
"address_contacts",
"address_html",
"website",
@@ -59,6 +60,7 @@
"column_break_45",
"market_segment",
"industry",
+ "language",
"is_frozen",
"column_break_38",
"loyalty_program",
@@ -479,13 +481,25 @@
"fieldname": "dn_required",
"fieldtype": "Check",
"label": "Allow Sales Invoice Creation Without Delivery Note"
+ },
+ {
+ "fieldname": "pan",
+ "fieldtype": "Data",
+ "label": "PAN"
+ },
+ {
+ "fieldname": "tax_withholding_category",
+ "fieldtype": "Link",
+ "label": "Tax Withholding Category",
+ "options": "Tax Withholding Category"
}
],
"icon": "fa fa-user",
"idx": 363,
"image_field": "image",
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-03-17 11:03:42.706907",
+ "modified": "2021-01-28 12:54:57.258959",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index c452594..96b3fa4 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -230,13 +230,20 @@
frappe.db.set(self, "customer_name", newdn)
def set_loyalty_program(self):
- if self.loyalty_program: return
+ if self.loyalty_program:
+ return
+
loyalty_program = get_loyalty_programs(self)
- if not loyalty_program: return
+ if not loyalty_program:
+ return
+
if len(loyalty_program) == 1:
self.loyalty_program = loyalty_program[0]
else:
- frappe.msgprint(_("Multiple Loyalty Program found for the Customer. Please select manually."))
+ frappe.msgprint(
+ _("Multiple Loyalty Programs found for Customer {}. Please select manually.")
+ .format(frappe.bold(self.customer_name))
+ )
def create_onboarding_docs(self, args):
defaults = frappe.defaults.get_defaults()
@@ -340,7 +347,6 @@
@frappe.whitelist()
def get_loyalty_programs(doc):
''' returns applicable loyalty programs for a customer '''
- from frappe.desk.treeview import get_children
lp_details = []
loyalty_programs = frappe.get_all("Loyalty Program",
@@ -349,15 +355,33 @@
"ifnull(to_date, '2500-01-01')": [">=", today()]})
for loyalty_program in loyalty_programs:
- customer_groups = [d.value for d in get_children("Customer Group", loyalty_program.customer_group)] + [loyalty_program.customer_group]
- customer_territories = [d.value for d in get_children("Territory", loyalty_program.customer_territory)] + [loyalty_program.customer_territory]
-
- if (not loyalty_program.customer_group or doc.customer_group in customer_groups)\
- and (not loyalty_program.customer_territory or doc.territory in customer_territories):
+ if (
+ (not loyalty_program.customer_group
+ or doc.customer_group in get_nested_links(
+ "Customer Group",
+ loyalty_program.customer_group,
+ doc.flags.ignore_permissions
+ ))
+ and (not loyalty_program.customer_territory
+ or doc.territory in get_nested_links(
+ "Territory",
+ loyalty_program.customer_territory,
+ doc.flags.ignore_permissions
+ ))
+ ):
lp_details.append(loyalty_program.name)
return lp_details
+def get_nested_links(link_doctype, link_name, ignore_permissions=False):
+ from frappe.desk.treeview import _get_children
+
+ links = [link_name]
+ for d in _get_children(link_doctype, link_name, ignore_permissions):
+ links.append(d.value)
+
+ return links
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_customer_list(doctype, txt, searchfield, start, page_len, filters=None):
@@ -572,4 +596,4 @@
""", {
'customer': customer,
'txt': '%%%s%%' % txt
- })
\ No newline at end of file
+ })
diff --git a/erpnext/selling/doctype/customer/test_customer.py b/erpnext/selling/doctype/customer/test_customer.py
index 87fdaa3..7761aa7 100644
--- a/erpnext/selling/doctype/customer/test_customer.py
+++ b/erpnext/selling/doctype/customer/test_customer.py
@@ -54,7 +54,11 @@
details = get_party_details("_Test Customer")
for key, value in iteritems(to_check):
- self.assertEqual(value, details.get(key))
+ val = details.get(key)
+ if not val and not isinstance(val, list):
+ val = None
+
+ self.assertEqual(value, val)
def test_party_details_tax_category(self):
from erpnext.accounts.party import get_party_details
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 5da248c..246f923 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -64,6 +64,7 @@
opp = frappe.get_doc("Opportunity", opportunity)
opp.set_status(status=status, update=True)
+ @frappe.whitelist()
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_sales_order():
get_lost_reasons = frappe.get_list('Quotation Lost Reason',
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index a6785f7..8b53902 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -641,6 +641,7 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
}
@@ -648,7 +649,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:39:40.174551",
+ "modified": "2021-02-23 01:13:54.670763",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 1516dd6..d9e52e1 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -150,7 +150,7 @@
if enq:
frappe.db.sql("update `tabOpportunity` set status = %s where name=%s",(flag,enq[0][0]))
- def update_prevdoc_status(self, flag):
+ def update_prevdoc_status(self, flag=None):
for quotation in list(set([d.prevdoc_docname for d in self.get("items")])):
if quotation:
doc = frappe.get_doc("Quotation", quotation)
@@ -180,6 +180,7 @@
update_coupon_code_count(self.coupon_code,'used')
def on_cancel(self):
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
super(SalesOrder, self).on_cancel()
# Cannot cancel closed SO
@@ -371,6 +372,7 @@
self.indicator_color = "green"
self.indicator_title = _("Paid")
+ @frappe.whitelist()
def get_work_order_items(self, for_raw_material_request=0):
'''Returns items with BOM that already do not have a linked work order'''
items = []
@@ -777,6 +779,7 @@
@frappe.whitelist()
def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None):
+ """Creates Purchase Order for each Supplier. Returns a list of doc objects."""
if not selected_items: return
if isinstance(selected_items, string_types):
@@ -819,15 +822,16 @@
target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty))
target.project = source_parent.project
- suppliers = [item.get('supplier') for item in selected_items if item.get('supplier') and item.get('supplier')]
- suppliers = list(set(suppliers))
+ suppliers = [item.get('supplier') for item in selected_items if item.get('supplier')]
+ suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order
- items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code') and item.get('item_code')]
+ items_to_map = [item.get('item_code') for item in selected_items if item.get('item_code')]
items_to_map = list(set(items_to_map))
if not suppliers:
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
+ purchase_orders = []
for supplier in suppliers:
doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": {
@@ -871,7 +875,9 @@
doc.insert()
frappe.db.commit()
- return doc
+ purchase_orders.append(doc)
+
+ return purchase_orders
@frappe.whitelist()
def make_purchase_order(source_name, selected_items=None, target_doc=None):
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index cbfab82..3137621 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1,11 +1,12 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
-import frappe
import json
-from frappe.utils import flt, add_days, nowdate
-import frappe.permissions
import unittest
+import frappe
+import frappe.permissions
+from frappe.utils import flt, add_days, nowdate
+from frappe.core.doctype.user_permission.test_user_permission import create_user
from erpnext.selling.doctype.sales_order.sales_order \
import make_material_request, make_delivery_note, make_sales_invoice, WarehouseRequired
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -17,6 +18,18 @@
from erpnext.stock.doctype.item.test_item import make_item
class TestSalesOrder(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order"))
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ # reset config to previous state
+ frappe.db.set_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
+
def tearDown(self):
frappe.set_user("Administrator")
@@ -328,6 +341,9 @@
prev_total = so.get("base_total")
prev_total_in_words = so.get("base_in_words")
+ # get reserved qty before update items
+ reserved_qty_for_second_item = get_reserved_qty("_Test Item 2")
+
first_item_of_so = so.get("items")[0]
trans_item = json.dumps([
{'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \
@@ -341,6 +357,10 @@
self.assertEqual(so.get("items")[-1].rate, 200)
self.assertEqual(so.get("items")[-1].qty, 7)
self.assertEqual(so.get("items")[-1].amount, 1400)
+
+ # reserved qty should increase after adding row
+ self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 7)
+
self.assertEqual(so.status, 'To Deliver and Bill')
updated_total = so.get("base_total")
@@ -360,6 +380,9 @@
create_dn_against_so(so.name, 2)
make_sales_invoice(so.name)
+ # get reserved qty before update items
+ reserved_qty_for_second_item = get_reserved_qty("_Test Item 2")
+
# add an item so as to try removing items
trans_item = json.dumps([
{"item_code": '_Test Item', "qty": 5, "rate":1000, "docname": so.get("items")[0].name},
@@ -369,6 +392,9 @@
so.reload()
self.assertEqual(len(so.get("items")), 2)
+ # reserved qty should increase after adding row
+ self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item + 2)
+
# check if delivered items can be removed
trans_item = json.dumps([{
"item_code": '_Test Item 2',
@@ -389,6 +415,10 @@
so.reload()
self.assertEqual(len(so.get("items")), 1)
+
+ # reserved qty should decrease (back to initial) after deleting row
+ self.assertEqual(get_reserved_qty('_Test Item 2'), reserved_qty_for_second_item)
+
self.assertEqual(so.status, 'To Deliver and Bill')
@@ -432,10 +462,8 @@
def test_update_child_perm(self):
so = make_sales_order(item_code= "_Test Item", qty=4)
- user = 'test@example.com'
- test_user = frappe.get_doc('User', user)
- test_user.add_roles("Accounts User")
- frappe.set_user(user)
+ test_user = create_user("test_so_child_perms@example.com", "Accounts User")
+ frappe.set_user(test_user.name)
# update qty
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 200, 'qty' : 7, 'docname': so.items[0].name}])
@@ -444,18 +472,14 @@
# add new item
trans_item = json.dumps([{'item_code' : '_Test Item', 'rate' : 100, 'qty' : 2}])
self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Sales Order', trans_item, so.name)
- test_user.remove_roles("Accounts User")
- frappe.set_user("Administrator")
def test_update_child_qty_rate_with_workflow(self):
from frappe.model.workflow import apply_workflow
- frappe.set_user("Administrator")
workflow = make_sales_order_workflow()
so = make_sales_order(item_code= "_Test Item", qty=1, rate=150, do_not_submit=1)
apply_workflow(so, 'Approve')
- frappe.set_user("Administrator")
user = 'test@example.com'
test_user = frappe.get_doc('User', user)
test_user.add_roles("Sales User", "Test Junior Approver")
@@ -496,12 +520,18 @@
so = make_sales_order(item_code = "_Test Item", warehouse=None)
+ # get reserved qty of packed item
+ existing_reserved_qty = get_reserved_qty("_Packed Item")
+
added_item = json.dumps([{"item_code" : "_Product Bundle Item", "rate" : 200, 'qty' : 2}])
update_child_qty_rate('Sales Order', added_item, so.name)
so.reload()
self.assertEqual(so.packed_items[0].qty, 4)
+ # reserved qty in packed item should increase after adding bundle item
+ self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 4)
+
# test uom and conversion factor change
update_uom_conv_factor = json.dumps([{
'item_code': so.get("items")[0].item_code,
@@ -516,6 +546,9 @@
so.reload()
self.assertEqual(so.packed_items[0].qty, 8)
+ # reserved qty in packed item should increase after changing bundle item uom
+ self.assertEqual(get_reserved_qty("_Packed Item"), existing_reserved_qty + 8)
+
def test_update_child_with_tax_template(self):
"""
Test Action: Create a SO with one item having its tax account head already in the SO.
@@ -543,12 +576,12 @@
new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax")
new_item_with_tax.append("taxes", {
- "item_tax_template": "Test Update Items Template",
+ "item_tax_template": "Test Update Items Template - _TC",
"valid_from": nowdate()
})
new_item_with_tax.save()
- tax_template = "_Test Account Excise Duty @ 10"
+ tax_template = "_Test Account Excise Duty @ 10 - _TC"
item = "_Test Item Home Desktop 100"
if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}):
item_doc = frappe.get_doc("Item", item)
@@ -602,37 +635,35 @@
so.cancel()
so.delete()
new_item_with_tax.delete()
- frappe.get_doc("Item Tax Template", "Test Update Items Template").delete()
+ frappe.get_doc("Item Tax Template", "Test Update Items Template - _TC").delete()
frappe.db.set_value("Stock Settings", None, "default_warehouse", old_stock_settings_value)
def test_warehouse_user(self):
- frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
- frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
- frappe.permissions.add_user_permission("Company", "_Test Company 1", "test2@example.com")
-
- test_user = frappe.get_doc("User", "test@example.com")
- test_user.add_roles("Sales User", "Stock User")
- test_user.remove_roles("Sales Manager")
+ test_user = create_user("test_so_warehouse_user@example.com", "Sales User", "Stock User")
test_user_2 = frappe.get_doc("User", "test2@example.com")
test_user_2.add_roles("Sales User", "Stock User")
test_user_2.remove_roles("Sales Manager")
- frappe.set_user("test@example.com")
+ frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 1 - _TC", test_user.name)
+ frappe.permissions.add_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name)
+ frappe.permissions.add_user_permission("Company", "_Test Company 1", test_user_2.name)
- so = make_sales_order(company="_Test Company 1",
+ frappe.set_user(test_user.name)
+
+ so = make_sales_order(company="_Test Company 1", customer="_Test Customer 1",
warehouse="_Test Warehouse 2 - _TC1", do_not_save=True)
so.conversion_rate = 0.02
so.plc_conversion_rate = 0.02
self.assertRaises(frappe.PermissionError, so.insert)
- frappe.set_user("test2@example.com")
+ frappe.set_user(test_user_2.name)
so.insert()
frappe.set_user("Administrator")
- frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", "test@example.com")
- frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", "test2@example.com")
- frappe.permissions.remove_user_permission("Company", "_Test Company 1", "test2@example.com")
+ frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 1 - _TC", test_user.name)
+ frappe.permissions.remove_user_permission("Warehouse", "_Test Warehouse 2 - _TC1", test_user_2.name)
+ frappe.permissions.remove_user_permission("Company", "_Test Company 1", test_user_2.name)
def test_block_delivery_note_against_cancelled_sales_order(self):
so = make_sales_order()
@@ -731,7 +762,7 @@
so = make_sales_order(item_list=so_items, do_not_submit=True)
so.submit()
- po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
+ po = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])[0]
po.submit()
dn = create_dn_against_so(so.name, delivered_qty=2)
@@ -813,7 +844,7 @@
so.submit()
# create po for only one item
- po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
+ po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])[0]
po1.submit()
self.assertEqual(so.customer, po1.customer)
@@ -823,7 +854,7 @@
self.assertEqual(len(po1.items), 1)
# create po for remaining item
- po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])
+ po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])[0]
po2.submit()
# teardown
@@ -834,6 +865,45 @@
so.load_from_db()
so.cancel()
+ def test_drop_shipping_full_for_default_suppliers(self):
+ """Test if multiple POs are generated in one go against different default suppliers."""
+ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier
+
+ if not frappe.db.exists("Item", "_Test Item for Drop Shipping 1"):
+ make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1})
+
+ if not frappe.db.exists("Item", "_Test Item for Drop Shipping 2"):
+ make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1})
+
+ so_items = [
+ {
+ "item_code": "_Test Item for Drop Shipping 1",
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": "_Test Item for Drop Shipping 2",
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier 1'
+ }
+ ]
+
+ # create so and po
+ so = make_sales_order(item_list=so_items, do_not_submit=True)
+ so.submit()
+
+ purchase_orders = make_purchase_order_for_default_supplier(so.name, selected_items=so_items)
+
+ self.assertEqual(len(purchase_orders), 2)
+ self.assertEqual(purchase_orders[0].supplier, '_Test Supplier')
+ self.assertEqual(purchase_orders[1].supplier, '_Test Supplier 1')
+
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])
@@ -1049,6 +1119,38 @@
self.assertRaises(frappe.LinkExistsError, so_doc.cancel)
+ def test_cancel_sales_order_after_cancel_payment_entry(self):
+ from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
+ # make a sales order
+ so = make_sales_order()
+
+ # disable unlinking of payment entry
+ frappe.db.set_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order", 0)
+
+ # create a payment entry against sales order
+ pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC")
+ pe.reference_no = "1"
+ pe.reference_date = nowdate()
+ pe.paid_from_account_currency = so.currency
+ pe.paid_to_account_currency = so.currency
+ pe.source_exchange_rate = 1
+ pe.target_exchange_rate = 1
+ pe.paid_amount = so.grand_total
+ pe.save(ignore_permissions=True)
+ pe.submit()
+
+ # Cancel payment entry
+ po_doc = frappe.get_doc("Payment Entry", pe.name)
+ po_doc.cancel()
+
+ # Cancel sales order
+ try:
+ so_doc = frappe.get_doc('Sales Order', so.name)
+ so_doc.cancel()
+ except Exception:
+ self.fail("Can not cancel sales order with linked cancelled payment entry")
+
def test_request_for_raw_materials(self):
item = make_item("_Test Finished Item", {"is_stock_item": 1,
"maintain_stock": 1,
@@ -1207,4 +1309,4 @@
))
workflow.insert(ignore_permissions=True)
- return workflow
\ No newline at end of file
+ return workflow
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index 37e47a9..1e5590e 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -786,6 +786,7 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
}
@@ -793,7 +794,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:35:07.617320",
+ "modified": "2021-02-23 01:15:05.803091",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
diff --git a/erpnext/selling/doctype/selling_settings/selling_settings.json b/erpnext/selling/doctype/selling_settings/selling_settings.json
index 4044f09..2104c01 100644
--- a/erpnext/selling/doctype/selling_settings/selling_settings.json
+++ b/erpnext/selling/doctype/selling_settings/selling_settings.json
@@ -140,7 +140,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-13 12:12:56.784014",
+ "modified": "2021-03-02 17:35:53.603607",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling Settings",
@@ -157,5 +157,6 @@
}
],
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index 338a3cc..9e3c9a5 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -93,6 +93,10 @@
})
return frappe.utils.play_sound("error");
}
+
+ // filter balance details for empty rows
+ balance_details = balance_details.filter(d => d.mode_of_payment);
+
const method = "erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher";
const res = await frappe.call({ method, args: { pos_profile, company, balance_details }, freeze:true });
!res.exc && me.prepare_app_defaults(res.message);
@@ -393,6 +397,7 @@
this.recent_order_list.toggle_component(false);
frappe.run_serially([
() => this.frm.refresh(name),
+ () => this.frm.call('reset_mode_of_payments'),
() => this.cart.load_invoice(),
() => this.item_selector.toggle_component(true)
]);
@@ -498,10 +503,11 @@
async on_cart_update(args) {
frappe.dom.freeze();
+ let item_row = undefined;
try {
let { field, value, item } = args;
const { item_code, batch_no, serial_no, uom } = item;
- let item_row = this.get_item_from_frm(item_code, batch_no, uom);
+ item_row = this.get_item_from_frm(item_code, batch_no, uom);
const item_selected_from_selector = field === 'qty' && value === "+1"
@@ -553,10 +559,12 @@
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
this.update_cart_html(item_row);
}
+
} catch (error) {
console.log(error);
} finally {
frappe.dom.unfreeze();
+ return item_row;
}
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 044e803..9ab9eef 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -472,7 +472,8 @@
if (!frm) frm = this.events.get_frm();
this.render_net_total(frm.doc.net_total);
- this.render_grand_total(frm.doc.grand_total);
+ const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? frm.doc.grand_total : frm.doc.rounded_total;
+ this.render_grand_total(grand_total);
const taxes = frm.doc.taxes.map(t => {
return {
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index 7c116e9..e0d5b73 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -152,6 +152,10 @@
this.item_group_field.toggle_label(false);
}
+ set_search_value(value) {
+ $(this.search_field.$input[0]).val(value).trigger("input");
+ }
+
bind_events() {
const me = this;
window.onScan = onScan;
@@ -159,7 +163,7 @@
onScan: (sScancode) => {
if (this.search_field && this.$component.is(':visible')) {
this.search_field.set_focus();
- $(this.search_field.$input[0]).val(sScancode).trigger("input");
+ this.set_search_value(sScancode);
this.barcode_scanned = true;
}
}
@@ -178,6 +182,7 @@
uom = uom === "undefined" ? undefined : uom;
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }});
+ me.set_search_value('');
});
this.search_field.$input.on('input', (e) => {
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index be2b769..b10a9e3 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -64,10 +64,7 @@
{fieldname: 'print', fieldtype: 'Data', label: 'Print Preview'}
],
primary_action: () => {
- const frm = this.events.get_frm();
- frm.doc = this.doc;
- frm.print_preview.lang_code = frm.doc.language;
- frm.print_preview.printit(true);
+ this.print_receipt();
},
primary_action_label: __('Print'),
});
@@ -192,13 +189,21 @@
});
this.$summary_container.on('click', '.print-btn', () => {
- const frm = this.events.get_frm();
- frm.doc = this.doc;
- frm.print_preview.lang_code = frm.doc.language;
- frm.print_preview.printit(true);
+ this.print_receipt();
});
}
+ print_receipt() {
+ const frm = this.events.get_frm();
+ frappe.utils.print(
+ frm.doctype,
+ frm.docname,
+ frm.pos_print_format,
+ frm.doc.letter_head,
+ frm.doc.language || frappe.boot.lang
+ );
+ }
+
attach_shortcuts() {
const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
this.$summary_container.find('.print-btn').attr("title", `${ctrl_label}+P`);
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index bcbac3b..22a279d 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -223,7 +223,8 @@
if (success) {
title = __("Payment Received");
- if (amount >= doc.grand_total) {
+ const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
+ if (amount >= grand_total) {
frappe.dom.unfreeze();
message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]);
this.events.submit_invoice();
@@ -243,7 +244,8 @@
auto_set_remaining_amount() {
const doc = this.events.get_frm().doc;
- const remaining_amount = doc.grand_total - doc.paid_amount;
+ const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
+ const remaining_amount = grand_total - doc.paid_amount;
const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined;
if (!current_value && remaining_amount > 0 && this.selected_mode) {
this.selected_mode.set_value(remaining_amount);
@@ -389,7 +391,7 @@
}
attach_cash_shortcuts(doc) {
- const grand_total = doc.grand_total;
+ const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
const currency = doc.currency;
const shortcuts = this.get_cash_shortcuts(flt(grand_total));
@@ -499,7 +501,8 @@
update_totals_section(doc) {
if (!doc) doc = this.events.get_frm().doc;
const paid_amount = doc.paid_amount;
- const remaining = doc.grand_total - doc.paid_amount;
+ const grand_total = cint(frappe.sys_defaults.disable_rounded_total) ? doc.grand_total : doc.rounded_total;
+ const remaining = grand_total - doc.paid_amount;
const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined;
const currency = doc.currency;
const label = change ? __('Change') : __('To Be Paid');
@@ -507,7 +510,7 @@
this.$totals.html(
`<div class="col">
<div class="total-label">Grand Total</div>
- <div class="value">${format_currency(doc.grand_total, currency)}</div>
+ <div class="value">${format_currency(grand_total, currency)}</div>
</div>
<div class="seperator-y"></div>
<div class="col">
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index ce08464..0428573 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -127,20 +127,6 @@
this.set_dynamic_labels();
},
- price_list_rate: function(doc, cdt, cdn) {
- var item = frappe.get_doc(cdt, cdn);
- frappe.model.round_floats_in(item, ["price_list_rate", "discount_percentage"]);
-
- // check if child doctype is Sales Order Item/Qutation Item and calculate the rate
- if(in_list(["Quotation Item", "Sales Order Item", "Delivery Note Item", "Sales Invoice Item", "POS Invoice Item"]), cdt)
- this.apply_pricing_rule_on_item(item);
- else
- item.rate = flt(item.price_list_rate * (1 - item.discount_percentage / 100.0),
- precision("rate", item));
-
- this.calculate_taxes_and_totals();
- },
-
discount_percentage: function(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
item.discount_amount = 0.0;
@@ -353,26 +339,6 @@
refresh_field('product_bundle_help');
},
- margin_rate_or_amount: function(doc, cdt, cdn) {
- // calculated the revised total margin and rate on margin rate changes
- var item = locals[cdt][cdn];
- this.apply_pricing_rule_on_item(item)
- this.calculate_taxes_and_totals();
- cur_frm.refresh_fields();
- },
-
- margin_type: function(doc, cdt, cdn){
- // calculate the revised total margin and rate on margin type changes
- var item = locals[cdt][cdn];
- if(!item.margin_type) {
- frappe.model.set_value(cdt, cdn, "margin_rate_or_amount", 0);
- } else {
- this.apply_pricing_rule_on_item(item, doc,cdt, cdn)
- this.calculate_taxes_and_totals();
- cur_frm.refresh_fields();
- }
- },
-
company_address: function() {
var me = this;
if(this.frm.doc.company_address) {
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 36033d9..c2b5e4f 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -140,7 +140,7 @@
doc: frm.doc,
freeze: true,
callback: function() {
- frappe.msgprint(__("Default tax templates for sales and purchase are created."));
+ frappe.msgprint(__("Default tax templates for sales, purchase and items are created."));
}
})
},
@@ -259,6 +259,7 @@
["default_payroll_payable_account", {"root_type": "Liability"}],
["round_off_account", {"root_type": "Expense"}],
["write_off_account", {"root_type": "Expense"}],
+ ["default_discount_account", {}],
["discount_allowed_account", {"root_type": "Expense"}],
["discount_received_account", {"root_type": "Income"}],
["exchange_gain_loss_account", {"root_type": "Expense"}],
@@ -275,7 +276,7 @@
["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
- ["unrealized_profit_loss_account", {"root_type": "Liability"}]
+ ["unrealized_profit_loss_account", {"root_type": "Liability"},]
], function(i, v) {
erpnext.company.set_custom_query(frm, v);
});
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index d49ae7c..83cbf47 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -59,6 +59,7 @@
"default_deferred_expense_account",
"default_payroll_payable_account",
"default_expense_claim_payable_account",
+ "default_discount_account",
"section_break_22",
"cost_center",
"column_break_26",
@@ -725,7 +726,7 @@
{
"fieldname": "default_in_transit_warehouse",
"fieldtype": "Link",
- "label": "Default In Transit Warehouse",
+ "label": "Default In-Transit Warehouse",
"options": "Warehouse"
},
{
@@ -733,6 +734,12 @@
"fieldtype": "Link",
"label": "Unrealized Profit / Loss Account",
"options": "Account"
+ },
+ {
+ "fieldname": "default_discount_account",
+ "fieldtype": "Link",
+ "label": "Default Payment Discount Account",
+ "options": "Account"
}
],
"icon": "fa fa-building",
@@ -740,7 +747,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2020-12-03 12:27:27.085094",
+ "modified": "2021-02-16 15:53:37.167589",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 819ba78..0922171 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -66,6 +66,7 @@
if frappe.db.sql("select abbr from tabCompany where name!=%s and abbr=%s", (self.name, self.abbr)):
frappe.throw(_("Abbreviation already used for another company"))
+ @frappe.whitelist()
def create_default_tax_template(self):
from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax
create_sales_tax({
@@ -390,8 +391,10 @@
frappe.db.sql("delete from tabDepartment where company=%s", self.name)
frappe.db.sql("delete from `tabTax Withholding Account` where company=%s", self.name)
+ # delete tax templates
frappe.db.sql("delete from `tabSales Taxes and Charges Template` where company=%s", self.name)
frappe.db.sql("delete from `tabPurchase Taxes and Charges Template` where company=%s", self.name)
+ frappe.db.sql("delete from `tabItem Tax Template` where company=%s", self.name)
@frappe.whitelist()
def enqueue_replace_abbr(company, old, new):
diff --git a/erpnext/setup/doctype/company/company_list.js b/erpnext/setup/doctype/company/company_list.js
index 0172865..1d1184f 100644
--- a/erpnext/setup/doctype/company/company_list.js
+++ b/erpnext/setup/doctype/company/company_list.js
@@ -1,10 +1,5 @@
frappe.listview_settings['Company'] = {
- onload: () => {
- frappe.breadcrumbs.add({
- type: 'Custom',
- module: __('Accounts'),
- label: __('Accounts'),
- route: '#modules/Accounts'
- });
- }
-}
\ No newline at end of file
+ onload() {
+ frappe.breadcrumbs.add('Accounts');
+ },
+};
diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py
index 7a72fe3..0df4c87 100644
--- a/erpnext/setup/doctype/company/delete_company_transactions.py
+++ b/erpnext/setup/doctype/company/delete_company_transactions.py
@@ -28,7 +28,7 @@
"Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", "BOM",
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
- "Item Default", "Customer", "Supplier"):
+ "Item Default", "Customer", "Supplier", "GST Account"):
delete_for_doctype(doctype, company_name)
# reset company values
diff --git a/erpnext/setup/doctype/email_digest/email_digest.py b/erpnext/setup/doctype/email_digest/email_digest.py
index 41890ae..02ea578 100644
--- a/erpnext/setup/doctype/email_digest/email_digest.py
+++ b/erpnext/setup/doctype/email_digest/email_digest.py
@@ -24,6 +24,25 @@
self._accounts = {}
self.currency = frappe.db.get_value('Company', self.company, "default_currency")
+ @frappe.whitelist()
+ def get_users(self):
+ """get list of users"""
+ user_list = frappe.db.sql("""
+ select name, enabled from tabUser
+ where name not in ({})
+ and user_type != "Website User"
+ order by enabled desc, name asc""".format(", ".join(["%s"]*len(STANDARD_USERS))), STANDARD_USERS, as_dict=1)
+
+ if self.recipient_list:
+ recipient_list = self.recipient_list.split("\n")
+ else:
+ recipient_list = []
+ for p in user_list:
+ p["checked"] = p["name"] in recipient_list and 1 or 0
+
+ frappe.response['user_list'] = user_list
+
+ @frappe.whitelist()
def send(self):
# send email only to enabled users
valid_users = [p[0] for p in frappe.db.sql("""select name from `tabUser`
diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py
index fa7bc50..76a8450 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.py
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.py
@@ -50,6 +50,7 @@
# clear cache
frappe.clear_cache()
+ @frappe.whitelist()
def get_defaults(self):
return frappe.defaults.get_defaults()
diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json
index e835214..3e0680f 100644
--- a/erpnext/setup/doctype/item_group/item_group.json
+++ b/erpnext/setup/doctype/item_group/item_group.json
@@ -214,7 +214,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
- "modified": "2021-02-08 17:02:44.951572",
+ "modified": "2021-02-18 13:40:30.049650",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
@@ -277,7 +277,7 @@
"export": 1,
"print": 1,
"report": 1,
- "role": "Customer",
+ "role": "All",
"select": 1,
"share": 1
}
diff --git a/erpnext/setup/doctype/item_group/test_records.json b/erpnext/setup/doctype/item_group/test_records.json
index 7115964..146da87 100644
--- a/erpnext/setup/doctype/item_group/test_records.json
+++ b/erpnext/setup/doctype/item_group/test_records.json
@@ -79,13 +79,13 @@
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 10",
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
"tax_category": ""
},
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 12",
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"tax_category": "_Test Tax Category 1"
}
]
@@ -99,7 +99,7 @@
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 15",
+ "item_tax_template": "_Test Account Excise Duty @ 15 - _TC",
"tax_category": ""
}
]
diff --git a/erpnext/setup/doctype/naming_series/naming_series.py b/erpnext/setup/doctype/naming_series/naming_series.py
index abff973..c4f1de1 100644
--- a/erpnext/setup/doctype/naming_series/naming_series.py
+++ b/erpnext/setup/doctype/naming_series/naming_series.py
@@ -10,10 +10,12 @@
from frappe.model.document import Document
from frappe.model.naming import parse_naming_series
from frappe.permissions import get_doctypes_with_read
+from frappe.core.doctype.doctype.doctype import validate_series
class NamingSeriesNotSetError(frappe.ValidationError): pass
class NamingSeries(Document):
+ @frappe.whitelist()
def get_transactions(self, arg=None):
doctypes = list(set(frappe.db.sql_list("""select parent
from `tabDocField` df where fieldname='naming_series'""")
@@ -52,6 +54,7 @@
options = list(filter(lambda x: x, [cstr(n).strip() for n in ol]))
return options
+ @frappe.whitelist()
def update_series(self, arg=None):
"""update series list"""
self.validate_series_set()
@@ -126,7 +129,7 @@
dt = frappe.get_doc("DocType", self.select_doc_for_series)
options = self.scrub_options_list(self.set_options.split("\n"))
for series in options:
- dt.validate_series(series)
+ validate_series(dt, series)
for i in sr:
if i[0]:
existing_series = [d.split('.')[0] for d in i[0].split("\n")]
@@ -138,10 +141,12 @@
if not re.match("^[\w\- /.#{}]*$", n, re.UNICODE):
throw(_('Special Characters except "-", "#", ".", "/", "{" and "}" not allowed in naming series'))
+ @frappe.whitelist()
def get_options(self, arg=None):
if frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series"):
return frappe.get_meta(arg or self.select_doc_for_series).get_field("naming_series").options
+ @frappe.whitelist()
def get_current(self, arg=None):
"""get series current"""
if self.prefix:
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 0bb480b..82f191d 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -142,13 +142,15 @@
}
]
- current_nabvar_items = navbar_settings.help_dropdown
+ current_navbar_items = navbar_settings.help_dropdown
navbar_settings.set('help_dropdown', [])
for item in erpnext_navbar_items:
- navbar_settings.append('help_dropdown', item)
+ current_labels = [item.get('item_label') for item in current_navbar_items]
+ if not item.get('item_label') in current_labels:
+ navbar_settings.append('help_dropdown', item)
- for item in current_nabvar_items:
+ for item in current_navbar_items:
navbar_settings.append('help_dropdown', {
'item_label': item.item_label,
'item_type': item.item_type,
@@ -161,5 +163,4 @@
navbar_settings.save()
def add_app_name():
- settings = frappe.get_doc("System Settings")
- settings.app_name = _("ERPNext")
\ No newline at end of file
+ frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext')
diff --git a/erpnext/setup/setup_wizard/operations/install_fixtures.py b/erpnext/setup/setup_wizard/operations/install_fixtures.py
index 72ed002..5053c6a 100644
--- a/erpnext/setup/setup_wizard/operations/install_fixtures.py
+++ b/erpnext/setup/setup_wizard/operations/install_fixtures.py
@@ -195,6 +195,7 @@
{'doctype': "Party Type", "party_type": "Member", "account_type": "Receivable"},
{'doctype': "Party Type", "party_type": "Shareholder", "account_type": "Payable"},
{'doctype': "Party Type", "party_type": "Student", "account_type": "Receivable"},
+ {'doctype': "Party Type", "party_type": "Donor", "account_type": "Receivable"},
{'doctype': "Opportunity Type", "name": "Hub"},
{'doctype': "Opportunity Type", "name": _("Sales")},
diff --git a/erpnext/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py
index e66fa76..c3c1593 100644
--- a/erpnext/setup/setup_wizard/operations/taxes_setup.py
+++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py
@@ -29,6 +29,7 @@
try:
if accounts:
make_sales_and_purchase_tax_templates(accounts, template_name)
+ make_item_tax_templates(accounts, template_name)
except frappe.NameError:
if frappe.message_log: frappe.message_log.pop()
except RootNotEditable:
@@ -84,6 +85,27 @@
doc = frappe.get_doc(purchase_tax_template)
doc.insert(ignore_permissions=True)
+def make_item_tax_templates(accounts, template_name=None):
+ if not template_name:
+ template_name = accounts[0].name
+
+ item_tax_template = {
+ "doctype": "Item Tax Template",
+ "title": template_name,
+ "company": accounts[0].company,
+ 'taxes': []
+ }
+
+
+ for account in accounts:
+ item_tax_template['taxes'].append({
+ "tax_type": account.name,
+ "tax_rate": account.tax_rate
+ })
+
+ # Items
+ frappe.get_doc(copy.deepcopy(item_tax_template)).insert(ignore_permissions=True)
+
def get_tax_account_group(company):
tax_group = frappe.db.get_value("Account",
{"account_name": "Duties and Taxes", "is_group": 1, "company": company})
diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json
index 69ca7cf..305456b 100644
--- a/erpnext/setup/workspace/home/home.json
+++ b/erpnext/setup/workspace/home/home.json
@@ -10,13 +10,14 @@
"hide_custom": 0,
"icon": "getting-started",
"idx": 0,
+ "is_default": 0,
"is_standard": 1,
"label": "Home",
"links": [
{
"hidden": 0,
"is_query_report": 0,
- "label": "Healthcare",
+ "label": "Accounting",
"onboard": 0,
"type": "Card Break"
},
@@ -24,8 +25,8 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Patient",
- "link_to": "Patient",
+ "label": "Chart of Accounts",
+ "link_to": "Account",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
@@ -34,25 +35,8 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Diagnosis",
- "link_to": "Diagnosis",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Agriculture",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Crop",
- "link_to": "Crop",
+ "label": "Company",
+ "link_to": "Company",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
@@ -61,8 +45,8 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Crop Cycle",
- "link_to": "Crop Cycle",
+ "label": "Customer",
+ "link_to": "Customer",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
@@ -71,112 +55,8 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
- "label": "Location",
- "link_to": "Location",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Fertilizer",
- "link_to": "Fertilizer",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Education",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Student",
- "link_to": "Student",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Course",
- "link_to": "Course",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Instructor",
- "link_to": "Instructor",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Room",
- "link_to": "Room",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
- "label": "Non Profit",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Member",
- "link_to": "Member",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Volunteer",
- "link_to": "Volunteer",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Chapter",
- "link_to": "Chapter",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Donor",
- "link_to": "Donor",
+ "label": "Supplier",
+ "link_to": "Supplier",
"link_type": "DocType",
"onboard": 1,
"type": "Link"
@@ -192,6 +72,16 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
+ "label": "Item",
+ "link_to": "Item",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
"label": "Warehouse",
"link_to": "Warehouse",
"link_type": "DocType",
@@ -305,73 +195,6 @@
{
"hidden": 0,
"is_query_report": 0,
- "label": "Accounting",
- "onboard": 0,
- "type": "Card Break"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Item",
- "link_to": "Item",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Customer",
- "link_to": "Customer",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Supplier",
- "link_to": "Supplier",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Company",
- "link_to": "Company",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Chart of Accounts",
- "link_to": "Account",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "dependencies": "",
- "hidden": 0,
- "is_query_report": 0,
- "label": "Opening Invoice Creation Tool",
- "link_to": "Opening Invoice Creation Tool",
- "link_type": "DocType",
- "onboard": 1,
- "type": "Link"
- },
- {
- "hidden": 0,
- "is_query_report": 0,
"label": "Data Import and Settings",
"onboard": 0,
"type": "Card Break"
@@ -390,6 +213,16 @@
"dependencies": "",
"hidden": 0,
"is_query_report": 0,
+ "label": "Opening Invoice Creation Tool",
+ "link_to": "Opening Invoice Creation Tool",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
"label": "Chart of Accounts Importer",
"link_to": "Chart of Accounts Importer",
"link_type": "DocType",
@@ -415,9 +248,177 @@
"link_type": "DocType",
"onboard": 1,
"type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Healthcare",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient",
+ "link_to": "Patient",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Diagnosis",
+ "link_to": "Diagnosis",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Education",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student",
+ "link_to": "Student",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Instructor",
+ "link_to": "Instructor",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Course",
+ "link_to": "Course",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Room",
+ "link_to": "Room",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Non Profit",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Donor",
+ "link_to": "Donor",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Member",
+ "link_to": "Member",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Volunteer",
+ "link_to": "Volunteer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Chapter",
+ "link_to": "Chapter",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Agriculture",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Location",
+ "link_to": "Location",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Crop",
+ "link_to": "Crop",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Crop Cycle",
+ "link_to": "Crop Cycle",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fertilizer",
+ "link_to": "Fertilizer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
}
],
- "modified": "2021-01-01 12:13:16.055668",
+ "modified": "2021-03-16 15:59:58.416154",
"modified_by": "Administrator",
"module": "Setup",
"name": "Home",
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
index 3691721..7a4bb20 100644
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
+++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
@@ -190,7 +190,7 @@
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2021-02-11 18:48:30.433058",
+ "modified": "2021-03-02 17:34:57.642565",
"modified_by": "Administrator",
"module": "Shopping Cart",
"name": "Shopping Cart Settings",
@@ -207,5 +207,6 @@
}
],
"sort_field": "modified",
- "sort_order": "ASC"
+ "sort_order": "ASC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/shopping_cart/test_shopping_cart.py b/erpnext/shopping_cart/test_shopping_cart.py
index cf59a52..d857bf5 100644
--- a/erpnext/shopping_cart/test_shopping_cart.py
+++ b/erpnext/shopping_cart/test_shopping_cart.py
@@ -16,6 +16,11 @@
Note:
Shopping Cart == Quotation
"""
+
+ @classmethod
+ def tearDownClass(cls):
+ frappe.db.sql("delete from `tabTax Rule`")
+
def setUp(self):
frappe.set_user("Administrator")
create_test_contact_and_address()
@@ -51,8 +56,8 @@
def test_add_to_cart(self):
self.login_as_customer()
- # remove from cart
- self.remove_all_items_from_cart()
+ # clear existing quotations
+ self.clear_existing_quotations()
# add first item
update_cart("_Test Item", 1)
@@ -100,6 +105,7 @@
self.assertEqual(len(quotation.get("items")), 1)
def test_tax_rule(self):
+ self.create_tax_rule()
self.login_as_customer()
quotation = self.create_quotation()
@@ -115,6 +121,13 @@
self.remove_test_quotation(quotation)
+ def create_tax_rule(self):
+ tax_rule = frappe.get_test_records("Tax Rule")[0]
+ try:
+ frappe.get_doc(tax_rule).insert()
+ except frappe.DuplicateEntryError:
+ pass
+
def create_quotation(self):
quotation = frappe.new_doc("Quotation")
@@ -195,10 +208,15 @@
"_Test Contact For _Test Customer")
frappe.set_user("test_contact_customer@example.com")
- def remove_all_items_from_cart(self):
- quotation = _get_cart_quotation()
- quotation.flags.ignore_permissions=True
- quotation.delete()
+ def clear_existing_quotations(self):
+ quotations = frappe.get_all("Quotation", filters={
+ "party_name": get_party().name,
+ "order_type": "Shopping Cart",
+ "docstatus": 0
+ }, order_by="modified desc", pluck="name")
+
+ for quotation in quotations:
+ frappe.delete_doc("Quotation", quotation, ignore_permissions=True, force=True)
def create_user_if_not_exists(self, email, first_name = None):
if frappe.db.exists("User", email):
diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py
index b3ae804..283f7d5 100644
--- a/erpnext/stock/__init__.py
+++ b/erpnext/stock/__init__.py
@@ -38,7 +38,7 @@
frappe.flags.warehouse_account_map[company] = warehouse_account
else:
frappe.flags.warehouse_account_map = warehouse_account
-
+
return frappe.flags.warehouse_account_map.get(company) or frappe.flags.warehouse_account_map
def get_warehouse_account(warehouse, warehouse_account=None):
@@ -64,10 +64,14 @@
if not account and warehouse.company:
account = get_company_default_inventory_account(warehouse.company)
+ if not account and warehouse.company:
+ account = frappe.db.get_value('Account',
+ {'account_type': 'Stock', 'is_group': 0, 'company': warehouse.company}, 'name')
+
if not account and warehouse.company and not warehouse.is_group:
frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}")
.format(warehouse.name, warehouse.company))
return account
def get_company_default_inventory_account(company):
- return frappe.get_cached_value('Company', company, 'default_inventory_account')
+ return frappe.get_cached_value('Company', company, 'default_inventory_account')
diff --git a/erpnext/stock/doctype/batch/batch.py b/erpnext/stock/doctype/batch/batch.py
index c8424f1..8fdda56 100644
--- a/erpnext/stock/doctype/batch/batch.py
+++ b/erpnext/stock/doctype/batch/batch.py
@@ -93,7 +93,7 @@
if create_new_batch:
if batch_number_series:
- self.batch_id = make_autoname(batch_number_series)
+ self.batch_id = make_autoname(batch_number_series, doc=self)
elif batch_uses_naming_series():
self.batch_id = self.get_name_from_naming_series()
else:
diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json
index 04d624e..8e79f0e 100644
--- a/erpnext/stock/doctype/bin/bin.json
+++ b/erpnext/stock/doctype/bin/bin.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "MAT-BIN-.YYYY.-.#####",
"creation": "2013-01-10 16:34:25",
"doctype": "DocType",
@@ -112,7 +113,8 @@
{
"fieldname": "reserved_qty_for_sub_contract",
"fieldtype": "Float",
- "label": "Reserved Qty for sub contract"
+ "label": "Reserved Qty for sub contract",
+ "read_only": 1
},
{
"fieldname": "ma_rate",
@@ -166,7 +168,8 @@
"hide_toolbar": 1,
"idx": 1,
"in_create": 1,
- "modified": "2019-11-18 18:34:59.456882",
+ "links": [],
+ "modified": "2021-03-30 23:09:39.572776",
"modified_by": "Administrator",
"module": "Stock",
"name": "Bin",
@@ -196,5 +199,6 @@
],
"quick_entry": 1,
"search_fields": "item_code,warehouse",
+ "sort_field": "modified",
"sort_order": "ASC"
}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 1088b41..0514bd2 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -16,8 +16,9 @@
def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
'''Called from erpnext.stock.utils.update_bin'''
self.update_qty(args)
+
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
- from erpnext.stock.stock_ledger import update_entries_after, validate_negative_qty_in_future_sle
+ from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
@@ -34,11 +35,13 @@
"posting_time": args.get("posting_time"),
"voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
- "sle_id": args.name
+ "sle_id": args.name,
+ "creation": args.creation
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
- # Validate negative qty in future transactions
- validate_negative_qty_in_future_sle(args)
+ # update qty in future ale and Validate negative qty
+ update_qty_in_future_sle(args, allow_negative_stock)
+
def update_qty(self, args):
# update the stock values (for current quantities)
@@ -51,7 +54,7 @@
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
-
+
self.set_projected_qty()
self.db_update()
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 3544390..d326a04 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -101,7 +101,7 @@
for f in fieldname:
toggle_print_hide(self.meta if key == "parent" else item_meta, f)
- super(DeliveryNote, self).before_print()
+ super(DeliveryNote, self).before_print(settings)
def set_actual_qty(self):
for d in self.get('items'):
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 559f8be..d39b229 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -489,7 +489,10 @@
def test_closed_delivery_note(self):
from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status
- dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
+ make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
+
+ dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
+ cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
dn.submit()
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 1799624..b05090a 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -750,6 +750,7 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
}
@@ -758,7 +759,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:42:03.767968",
+ "modified": "2021-02-23 01:04:08.588104",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/delivery_trip/delivery_trip.py b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
index 28e9533..de85bc3 100644
--- a/erpnext/stock/doctype/delivery_trip/delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/delivery_trip.py
@@ -90,6 +90,7 @@
delivery_notes = [get_link_to_form("Delivery Note", note) for note in delivery_notes]
frappe.msgprint(_("Delivery Notes {0} updated").format(", ".join(delivery_notes)))
+ @frappe.whitelist()
def process_route(self, optimize):
"""
Estimate the arrival times for each stop in the Delivery Trip.
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index f851aaf..2079cf8 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -85,7 +85,7 @@
}
if (frm.doc.variant_of) {
frm.set_intro(__('This Item is a Variant of {0} (Template).',
- [`<a href="/app/item/${frm.doc.variant_of}">${frm.doc.variant_of}</a>`]), true);
+ [`<a href="/app/item/${frm.doc.variant_of}" onclick="location.reload()">${frm.doc.variant_of}</a>`]), true);
}
if (frappe.defaults.get_default("item_naming_by")!="Naming Series" || frm.doc.variant_of) {
@@ -717,6 +717,18 @@
.on('focus', function(e) {
$(e.target).val('').trigger('input');
})
+ .on("awesomplete-open", () => {
+ let modal = field.$input.parents('.modal-dialog')[0];
+ if (modal) {
+ $(modal).removeClass("modal-dialog-scrollable");
+ }
+ })
+ .on("awesomplete-close", () => {
+ let modal = field.$input.parents('.modal-dialog')[0];
+ if (modal) {
+ $(modal).addClass("modal-dialog-scrollable");
+ }
+ });
});
},
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index fcf7c26..6fed9ef 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -521,8 +521,7 @@
"fieldname": "has_variants",
"fieldtype": "Check",
"in_standard_filter": 1,
- "label": "Has Variants",
- "no_copy": 1
+ "label": "Has Variants"
},
{
"default": "Item Attribute",
@@ -538,7 +537,6 @@
"fieldtype": "Table",
"hidden": 1,
"label": "Attributes",
- "no_copy": 1,
"options": "Item Variant Attribute"
},
{
@@ -1056,6 +1054,7 @@
"read_only": 1
},
{
+ "depends_on": "eval: doc.show_in_website || doc.show_variant_in_website",
"fieldname": "website_image_alt",
"fieldtype": "Data",
"label": "Image Description"
@@ -1068,7 +1067,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 1,
- "modified": "2021-01-25 20:49:50.222976",
+ "modified": "2021-03-18 14:04:38.575519",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
@@ -1120,6 +1119,15 @@
{
"read": 1,
"role": "Manufacturing User"
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "report": 1,
+ "role": "All",
+ "select": 1,
+ "share": 1
}
],
"quick_entry": 1,
@@ -1130,4 +1138,4 @@
"sort_order": "DESC",
"title_field": "item_name",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index b661570..7cb84a6 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -50,6 +50,7 @@
self.set_onload('stock_exists', self.stock_ledger_created())
self.set_asset_naming_series()
+ @frappe.whitelist()
def set_asset_naming_series(self):
if not hasattr(self, '_asset_naming_series'):
from erpnext.assets.doctype.asset.asset import get_asset_naming_series
@@ -177,7 +178,7 @@
if not self.valuation_rate and self.standard_rate:
self.valuation_rate = self.standard_rate
- if not self.valuation_rate:
+ if not self.valuation_rate and not self.is_customer_provided_item:
frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered"))
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -706,6 +707,7 @@
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", existing_allow_negative_stock)
frappe.db.auto_commit_on_many_writes = 0
+ @frappe.whitelist()
def copy_specification_from_item_group(self):
self.set("website_specifications", [])
if self.item_group:
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index 109731a..36d0de1 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -104,41 +104,41 @@
def test_item_tax_template(self):
expected_item_tax_template = [
{"item_code": "_Test Item With Item Tax Template", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 10"},
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"},
{"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Account Excise Duty @ 12"},
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"},
{"item_code": "_Test Item With Item Tax Template", "tax_category": "_Test Tax Category 2",
"item_tax_template": None},
{"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 10"},
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"},
{"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Account Excise Duty @ 12"},
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"},
{"item_code": "_Test Item Inherit Group Item Tax Template 1", "tax_category": "_Test Tax Category 2",
"item_tax_template": None},
{"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 15"},
+ "item_tax_template": "_Test Account Excise Duty @ 15 - _TC"},
{"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Account Excise Duty @ 12"},
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC"},
{"item_code": "_Test Item Inherit Group Item Tax Template 2", "tax_category": "_Test Tax Category 2",
"item_tax_template": None},
{"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "",
- "item_tax_template": "_Test Account Excise Duty @ 20"},
+ "item_tax_template": "_Test Account Excise Duty @ 20 - _TC"},
{"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Item Tax Template 1"},
+ "item_tax_template": "_Test Item Tax Template 1 - _TC"},
{"item_code": "_Test Item Override Group Item Tax Template", "tax_category": "_Test Tax Category 2",
"item_tax_template": None},
]
expected_item_tax_map = {
None: {},
- "_Test Account Excise Duty @ 10": {"_Test Account Excise Duty - _TC": 10},
- "_Test Account Excise Duty @ 12": {"_Test Account Excise Duty - _TC": 12},
- "_Test Account Excise Duty @ 15": {"_Test Account Excise Duty - _TC": 15},
- "_Test Account Excise Duty @ 20": {"_Test Account Excise Duty - _TC": 20},
- "_Test Item Tax Template 1": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10,
+ "_Test Account Excise Duty @ 10 - _TC": {"_Test Account Excise Duty - _TC": 10},
+ "_Test Account Excise Duty @ 12 - _TC": {"_Test Account Excise Duty - _TC": 12},
+ "_Test Account Excise Duty @ 15 - _TC": {"_Test Account Excise Duty - _TC": 15},
+ "_Test Account Excise Duty @ 20 - _TC": {"_Test Account Excise Duty - _TC": 20},
+ "_Test Item Tax Template 1 - _TC": {"_Test Account Excise Duty - _TC": 5, "_Test Account Education Cess - _TC": 10,
"_Test Account S&H Education Cess - _TC": 15}
}
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index 8f437b1..c1f20a4 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -12,6 +12,7 @@
"item_name": "_Test Item",
"apply_warehouse_wise_reorder_level": 1,
"gst_hsn_code": "999800",
+ "opening_stock": 10,
"valuation_rate": 100,
"item_defaults": [{
"company": "_Test Company",
@@ -92,7 +93,7 @@
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 10"
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"
}
],
"stock_uom": "_Test UOM 1"
@@ -370,12 +371,12 @@
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 10"
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC"
},
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 12",
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
"tax_category": "_Test Tax Category 1"
}
]
@@ -449,13 +450,13 @@
{
"doctype": "Item Tax",
"parentfield": "taxes",
- "item_tax_template": "_Test Account Excise Duty @ 20"
+ "item_tax_template": "_Test Account Excise Duty @ 20 - _TC"
},
{
"doctype": "Item Tax",
"parentfield": "taxes",
"tax_category": "_Test Tax Category 1",
- "item_tax_template": "_Test Item Tax Template 1"
+ "item_tax_template": "_Test Item Tax Template 1 - _TC"
}
]
},
diff --git a/erpnext/stock/doctype/item_attribute/test_records.json b/erpnext/stock/doctype/item_attribute/test_records.json
index d346979..6aa6ffd 100644
--- a/erpnext/stock/doctype/item_attribute/test_records.json
+++ b/erpnext/stock/doctype/item_attribute/test_records.json
@@ -4,10 +4,12 @@
"attribute_name": "Test Size",
"priority": 1,
"item_attribute_values": [
+ {"attribute_value": "Extra Small", "abbr": "XSL"},
{"attribute_value": "Small", "abbr": "S"},
{"attribute_value": "Medium", "abbr": "M"},
{"attribute_value": "Large", "abbr": "L"},
- {"attribute_value": "Extra Small", "abbr": "XSL"}
+ {"attribute_value": "Extra Large", "abbr": "XL"},
+ {"attribute_value": "2XL", "abbr": "2XL"}
]
},
{
diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
index 471e685..9b1a47e 100644
--- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
+++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"specification",
+ "parameter_group",
"value",
"numeric",
"column_break_3",
@@ -75,12 +76,20 @@
"in_list_view": 1,
"label": "Numeric",
"width": "80px"
+ },
+ {
+ "fetch_from": "specification.parameter_group",
+ "fieldname": "parameter_group",
+ "fieldtype": "Link",
+ "label": "Parameter Group",
+ "options": "Quality Inspection Parameter Group",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-01 19:18:46.924399",
+ "modified": "2021-02-04 18:50:02.056173",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Quality Inspection Parameter",
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index 69a8bf1..8310946 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -12,6 +12,7 @@
from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
class LandedCostVoucher(Document):
+ @frappe.whitelist()
def get_items_from_purchase_receipts(self):
self.set("items", [])
for pr in self.get("purchase_receipts"):
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 527b0d3..7dfc5da 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -354,6 +354,10 @@
},
material_request_type: function(frm) {
frm.toggle_reqd('customer', frm.doc.material_request_type=="Customer Provided");
+
+ if (frm.doc.material_request_type !== 'Material Transfer' && frm.doc.set_from_warehouse) {
+ frm.set_value('set_from_warehouse', '');
+ }
},
});
diff --git a/erpnext/stock/doctype/material_request/material_request.json b/erpnext/stock/doctype/material_request/material_request.json
index d73349d..8d7b238 100644
--- a/erpnext/stock/doctype/material_request/material_request.json
+++ b/erpnext/stock/doctype/material_request/material_request.json
@@ -20,9 +20,9 @@
"company",
"amended_from",
"warehouse_section",
- "set_warehouse",
- "column_break5",
"set_from_warehouse",
+ "column_break5",
+ "set_warehouse",
"items_section",
"scan_barcode",
"items",
@@ -314,7 +314,7 @@
"idx": 70,
"is_submittable": 1,
"links": [],
- "modified": "2020-09-19 01:04:09.285862",
+ "modified": "2021-03-31 23:52:55.392512",
"modified_by": "Administrator",
"module": "Stock",
"name": "Material Request",
diff --git a/erpnext/stock/doctype/packing_slip/packing_slip.py b/erpnext/stock/doctype/packing_slip/packing_slip.py
index a7a29cc..2008bff 100644
--- a/erpnext/stock/doctype/packing_slip/packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/packing_slip.py
@@ -152,6 +152,7 @@
return cint(recommended_case_no[0][0]) + 1
+ @frappe.whitelist()
def get_items(self):
self.set("items", [])
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index 0da57b7..61b7209 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -25,14 +25,15 @@
if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'):
continue
if not item.serial_no:
- frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}".format(
- frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse))),
+ frappe.throw(_("Row #{0}: {1} does not have any available serial numbers in {2}").format(
+ frappe.bold(item.idx), frappe.bold(item.item_code), frappe.bold(item.warehouse)),
title=_("Serial Nos Required"))
if len(item.serial_no.split('\n')) == item.picked_qty:
continue
frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
.format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch"))
+ @frappe.whitelist()
def set_item_locations(self, save=False):
items = self.aggregate_item_qty()
self.item_location_map = frappe._dict()
@@ -380,7 +381,7 @@
stock_entry.set_incoming_rate()
stock_entry.set_actual_qty()
- stock_entry.calculate_rate_and_amount(update_finished_item_rate=False)
+ stock_entry.calculate_rate_and_amount()
return stock_entry.as_dict()
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index 8ea7f89d..01f2b0b 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -22,7 +22,7 @@
'purpose': 'Opening Stock',
'expense_account': 'Temporary Opening - _TC',
'items': [{
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'warehouse': '_Test Warehouse - _TC',
'valuation_rate': 100,
'qty': 5
@@ -37,7 +37,7 @@
'customer': '_Test Customer',
'items_based_on': 'Sales Order',
'locations': [{
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'qty': 5,
'stock_qty': 5,
'conversion_factor': 1,
@@ -47,7 +47,7 @@
})
pick_list.set_item_locations()
- self.assertEqual(pick_list.locations[0].item_code, '_Test Item Home Desktop 100')
+ self.assertEqual(pick_list.locations[0].item_code, '_Test Item')
self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC')
self.assertEqual(pick_list.locations[0].qty, 5)
@@ -237,7 +237,7 @@
'purpose': 'Opening Stock',
'expense_account': 'Temporary Opening - _TC',
'items': [{
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'warehouse': '_Test Warehouse - _TC',
'valuation_rate': 100,
'qty': 10
@@ -251,7 +251,7 @@
'customer': '_Test Customer',
'company': '_Test Company',
'items': [{
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'qty': 10,
'delivery_date': frappe.utils.today()
}],
@@ -264,14 +264,14 @@
'customer': '_Test Customer',
'items_based_on': 'Sales Order',
'locations': [{
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'qty': 5,
'stock_qty': 5,
'conversion_factor': 1,
'sales_order': '_T-Sales Order-1',
'sales_order_item': '_T-Sales Order-1_item',
}, {
- 'item_code': '_Test Item Home Desktop 100',
+ 'item_code': '_Test Item',
'qty': 5,
'stock_qty': 5,
'conversion_factor': 1,
@@ -281,12 +281,12 @@
})
pick_list.set_item_locations()
- self.assertEqual(pick_list.locations[0].item_code, '_Test Item Home Desktop 100')
+ self.assertEqual(pick_list.locations[0].item_code, '_Test Item')
self.assertEqual(pick_list.locations[0].warehouse, '_Test Warehouse - _TC')
self.assertEqual(pick_list.locations[0].qty, 5)
self.assertEqual(pick_list.locations[0].sales_order_item, '_T-Sales Order-1_item')
- self.assertEqual(pick_list.locations[1].item_code, '_Test Item Home Desktop 100')
+ self.assertEqual(pick_list.locations[1].item_code, '_Test Item')
self.assertEqual(pick_list.locations[1].warehouse, '_Test Warehouse - _TC')
self.assertEqual(pick_list.locations[1].qty, 5)
self.assertEqual(pick_list.locations[1].sales_order_item, sales_order.items[0].name)
@@ -302,4 +302,4 @@
# pass
# def test_pick_list_from_material_request(self):
- # pass
\ No newline at end of file
+ # pass
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 550c849..5d7597b 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -176,7 +176,7 @@
if flt(self.per_billed) < 100:
self.update_billing_status()
else:
- self.status = "Completed"
+ self.db_set("status", "Completed")
# Updating stock ledger should always be called after updating prevdoc status,
@@ -295,7 +295,8 @@
"against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(amount["base_amount"]),
+ "credit": (flt(amount["base_amount"]) if (amount["base_amount"] or
+ account_currency!=self.company_currency) else flt(amount["amount"])),
"credit_in_account_currency": flt(amount["amount"]),
"project": d.project
}, item=d))
@@ -323,10 +324,12 @@
else:
loss_account = self.get_company_default("default_expense_account")
+ cost_center = d.cost_center or frappe.get_cached_value("Company", self.company, "cost_center")
+
gl_entries.append(self.get_gl_dict({
"account": loss_account,
"against": warehouse_account[d.warehouse]["account"],
- "cost_center": d.cost_center,
+ "cost_center": cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
"debit": divisional_loss,
"project": d.project
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index ca58ab2..7f0c3fa 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -94,10 +94,15 @@
frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
def test_purchase_receipt_no_gl_entry(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
- existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"}, "stock_value")
+ existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"])
+
+ if existing_bin_qty < 0:
+ make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty))
pr = make_purchase_receipt()
@@ -186,7 +191,7 @@
rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")])
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
-
+
pr.cancel()
def test_subcontracting_gle_fg_item_rate_zero(self):
@@ -907,6 +912,57 @@
ste1.cancel()
po.cancel()
+
+ def test_po_to_pi_and_po_to_pr_worflow_full(self):
+ """Test following behaviour:
+ - Create PO
+ - Create PI from PO and submit
+ - Create PR from PO and submit
+ """
+ from erpnext.buying.doctype.purchase_order import test_purchase_order
+ from erpnext.buying.doctype.purchase_order import purchase_order
+
+ po = test_purchase_order.create_purchase_order()
+
+ pi = purchase_order.make_purchase_invoice(po.name)
+ pi.submit()
+
+ pr = purchase_order.make_purchase_receipt(po.name)
+ pr.submit()
+
+ pr.load_from_db()
+
+ self.assertEqual(pr.status, "Completed")
+ self.assertEqual(pr.per_billed, 100)
+
+ def test_po_to_pi_and_po_to_pr_worflow_partial(self):
+ """Test following behaviour:
+ - Create PO
+ - Create partial PI from PO and submit
+ - Create PR from PO and submit
+ """
+ from erpnext.buying.doctype.purchase_order import test_purchase_order
+ from erpnext.buying.doctype.purchase_order import purchase_order
+
+ po = test_purchase_order.create_purchase_order()
+
+ pi = purchase_order.make_purchase_invoice(po.name)
+ pi.items[0].qty /= 2 # roughly 50%, ^ this function only creates PI with 1 item.
+ pi.submit()
+
+ pr = purchase_order.make_purchase_receipt(po.name)
+ pr.save()
+ # per_billed is only updated after submission.
+ self.assertEqual(flt(pr.per_billed), 0)
+
+ pr.submit()
+
+ pi.load_from_db()
+ pr.load_from_db()
+
+ self.assertEqual(pr.status, "To Bill")
+ self.assertAlmostEqual(pr.per_billed, 50.0, places=2)
+
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index 8974ad9..efe3642 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -37,10 +37,16 @@
"returned_qty",
"rate_and_amount",
"price_list_rate",
- "discount_percentage",
- "discount_amount",
"col_break3",
"base_price_list_rate",
+ "discount_and_margin_section",
+ "margin_type",
+ "margin_rate_or_amount",
+ "rate_with_margin",
+ "column_break_37",
+ "discount_percentage",
+ "discount_amount",
+ "base_rate_with_margin",
"sec_break1",
"rate",
"amount",
@@ -880,6 +886,7 @@
"fieldname": "stock_uom_rate",
"fieldtype": "Currency",
"label": "Rate of Stock UOM",
+ "no_copy": 1,
"options": "currency",
"read_only": 1
},
@@ -890,12 +897,55 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "discount_and_margin_section",
+ "fieldtype": "Section Break",
+ "label": "Discount and Margin"
+ },
+ {
+ "depends_on": "price_list_rate",
+ "fieldname": "margin_type",
+ "fieldtype": "Select",
+ "label": "Margin Type",
+ "options": "\nPercentage\nAmount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate",
+ "fieldname": "margin_rate_or_amount",
+ "fieldtype": "Float",
+ "label": "Margin Rate or Amount",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin",
+ "options": "currency",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_37",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount",
+ "fieldname": "base_rate_with_margin",
+ "fieldtype": "Currency",
+ "label": "Rate With Margin (Company Currency)",
+ "options": "Company:company:default_currency",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-01-30 21:44:06.918515",
+ "modified": "2021-02-23 00:59:14.360847",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index 58b1eca..469511a 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -18,6 +18,7 @@
if self.readings:
self.inspect_and_set_status()
+ @frappe.whitelist()
def get_item_specification_details(self):
if not self.quality_inspection_template:
self.quality_inspection_template = frappe.db.get_value('Item',
@@ -32,6 +33,7 @@
child.update(d)
child.status = "Accepted"
+ @frappe.whitelist()
def get_quality_inspection_template(self):
template = ''
if self.bom_no:
@@ -62,17 +64,21 @@
(quality_inspection, self.modified, self.reference_name, self.item_code))
else:
+ args = [quality_inspection, self.modified, self.reference_name, self.item_code]
doctype = self.reference_type + ' Item'
+
if self.reference_type == 'Stock Entry':
doctype = 'Stock Entry Detail'
if self.reference_type and self.reference_name:
conditions = ""
if self.batch_no and self.docstatus == 1:
- conditions += " and t1.batch_no = '%s'"%(self.batch_no)
+ conditions += " and t1.batch_no = %s"
+ args.append(self.batch_no)
if self.docstatus == 2: # if cancel, then remove qi link wherever same name
- conditions += " and t1.quality_inspection = '%s'"%(self.name)
+ conditions += " and t1.quality_inspection = %s"
+ args.append(self.name)
frappe.db.sql("""
UPDATE
@@ -85,7 +91,7 @@
and t1.parent = t2.name
{conditions}
""".format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions),
- (quality_inspection, self.modified, self.reference_name, self.item_code))
+ args)
def inspect_and_set_status(self):
for reading in self.readings:
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
index 0b5a9b5..418b482 100644
--- a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
@@ -7,24 +7,34 @@
"engine": "InnoDB",
"field_order": [
"parameter",
+ "parameter_group",
"description"
],
"fields": [
{
"fieldname": "parameter",
"fieldtype": "Data",
+ "in_list_view": 1,
"label": "Parameter",
+ "reqd": 1,
"unique": 1
},
{
"fieldname": "description",
"fieldtype": "Text Editor",
"label": "Description"
+ },
+ {
+ "fieldname": "parameter_group",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Parameter Group",
+ "options": "Quality Inspection Parameter Group"
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-12-28 18:06:54.897317",
+ "modified": "2021-02-19 20:33:30.657406",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Parameter",
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/stock/doctype/quality_inspection_parameter_group/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/stock/doctype/quality_inspection_parameter_group/__init__.py
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js
new file mode 100644
index 0000000..8716a29
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Quality Inspection Parameter Group', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json
new file mode 100644
index 0000000..5726474
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json
@@ -0,0 +1,82 @@
+{
+ "actions": [],
+ "autoname": "field:group_name",
+ "creation": "2021-02-04 18:44:12.223295",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "group_name"
+ ],
+ "fields": [
+ {
+ "fieldname": "group_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Parameter Group Name",
+ "reqd": 1,
+ "unique": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-02-04 18:44:12.223295",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Quality Inspection Parameter Group",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Quality Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py
new file mode 100644
index 0000000..1a3b1a0
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class QualityInspectionParameterGroup(Document):
+ pass
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py b/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py
new file mode 100644
index 0000000..212d4b8
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestQualityInspectionParameterGroup(unittest.TestCase):
+ pass
diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
index 35d58ef..0eff5a8 100644
--- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
+++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"specification",
+ "parameter_group",
"status",
"value",
"numeric",
@@ -210,12 +211,20 @@
"fieldtype": "Check",
"in_list_view": 1,
"label": "Numeric"
+ },
+ {
+ "fetch_from": "specification.parameter_group",
+ "fieldname": "parameter_group",
+ "fieldtype": "Link",
+ "label": "Parameter Group",
+ "options": "Quality Inspection Parameter Group",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-01 19:46:22.138018",
+ "modified": "2021-02-04 19:15:37.991221",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Reading",
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
index f22c601..f8cfdf8 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe, erpnext
from frappe.model.document import Document
-from frappe.utils import cint, get_link_to_form
+from frappe.utils import cint, get_link_to_form, add_to_date, today
from erpnext.stock.stock_ledger import repost_future_sle
from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced
from frappe.utils.user import get_users_with_role
@@ -29,7 +29,7 @@
self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company")
elif self.warehouse:
self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company")
-
+
def set_status(self, status=None):
if not status:
status = 'Queued'
@@ -39,6 +39,7 @@
frappe.enqueue(repost, timeout=1800, queue='long',
job_name='repost_sle', now=frappe.flags.in_test, doc=self)
+ @frappe.whitelist()
def restart_reposting(self):
self.set_status('Queued')
frappe.enqueue(repost, timeout=1800, queue='long',
@@ -46,12 +47,14 @@
def repost(doc):
try:
+ if not frappe.db.exists("Repost Item Valuation", doc.name):
+ return
+
doc.set_status('In Progress')
frappe.db.commit()
repost_sl_entries(doc)
repost_gl_entries(doc)
- check_if_stock_and_account_balance_synced(doc.posting_date, doc.company)
doc.set_status('Completed')
except Exception:
@@ -100,7 +103,7 @@
recipients = get_users_with_role("Stock Manager")
if not recipients:
get_users_with_role("System Manager")
-
+
subject = _("Error while reposting item valuation")
message = (_("Hi,") + "<br>"
+ _("An error has been appeared while reposting item valuation via {0}")
@@ -109,4 +112,24 @@
)
frappe.sendmail(recipients=recipients, subject=subject, message=message)
+def repost_entries():
+ riv_entries = get_repost_item_valuation_entries()
+ for row in riv_entries:
+ doc = frappe.get_cached_doc('Repost Item Valuation', row.name)
+ repost(doc)
+
+ riv_entries = get_repost_item_valuation_entries()
+ if riv_entries:
+ return
+
+ for d in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
+ check_if_stock_and_account_balance_synced(today(), d.company)
+
+def get_repost_item_valuation_entries():
+ date = add_to_date(today(), hours=-12)
+
+ return frappe.db.sql(""" SELECT name from `tabRepost Item Valuation`
+ WHERE status != 'Completed' and creation <= %s and docstatus = 1
+ ORDER BY timestamp(posting_date, posting_time) asc, creation asc
+ """, date, as_dict=1)
\ No newline at end of file
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 6bacf1f..c8d8ca9 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -554,7 +554,7 @@
if batch_nos:
try:
- filters["batch_no"] = json.loads(batch_nos)
+ filters["batch_no"] = json.loads(batch_nos) if (type(json.loads(batch_nos)) == list) else [json.loads(batch_nos)]
except:
filters["batch_no"] = [batch_nos]
@@ -626,4 +626,4 @@
batch_no_condition=batch_no_condition
), filters, as_dict=1)
- return serial_numbers
\ No newline at end of file
+ return serial_numbers
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 726118d..98246fb 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -100,6 +100,13 @@
frm.add_fetch("bom_no", "inspection_required", "inspection_required");
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+
+ frappe.db.get_single_value('Stock Settings', 'disable_serial_no_and_batch_selector')
+ .then((value) => {
+ if (value) {
+ frappe.flags.hide_serial_batch_dialog = true;
+ }
+ });
},
setup_quality_inspection: function(frm) {
@@ -231,15 +238,37 @@
}, __("Get Items From"));
frm.add_custom_button(__('Material Request'), function() {
- erpnext.utils.map_current_doc({
+ const allowed_request_types = ["Material Transfer", "Material Issue", "Customer Provided"];
+ const depends_on_condition = "eval:doc.material_request_type==='Customer Provided'";
+ const d = erpnext.utils.map_current_doc({
method: "erpnext.stock.doctype.material_request.material_request.make_stock_entry",
source_doctype: "Material Request",
target: frm,
date_field: "schedule_date",
- setters: {},
+ setters: [{
+ fieldtype: 'Select',
+ label: __('Purpose'),
+ options: allowed_request_types.join("\n"),
+ fieldname: 'material_request_type',
+ default: "Material Transfer",
+ mandatory: 1,
+ change() {
+ if (this.value === 'Customer Provided') {
+ d.dialog.get_field("customer").set_focus();
+ }
+ },
+ },
+ {
+ fieldtype: 'Link',
+ label: __('Customer'),
+ options: 'Customer',
+ fieldname: 'customer',
+ depends_on: depends_on_condition,
+ mandatory_depends_on: depends_on_condition,
+ }],
get_query_filters: {
docstatus: 1,
- material_request_type: ["in", ["Material Transfer", "Material Issue"]],
+ material_request_type: ["in", allowed_request_types],
status: ["not in", ["Transferred", "Issued"]]
}
})
@@ -569,6 +598,7 @@
add_to_transit: function(frm) {
if(frm.doc.add_to_transit && frm.doc.purpose=='Material Transfer') {
+ frm.set_value('to_warehouse', '');
frm.set_value('stock_entry_type', 'Material Transfer');
frm.fields_dict.to_warehouse.get_query = function() {
return {
@@ -579,7 +609,15 @@
}
};
};
- frappe.db.get_value('Company', frm.doc.company, 'default_in_transit_warehouse', (r) => {
+ frm.trigger('set_tansit_warehouse');
+ }
+ },
+
+ set_tansit_warehouse: function(frm) {
+ if(frm.doc.add_to_transit && frm.doc.purpose == 'Material Transfer' && !frm.doc.to_warehouse) {
+ let dt = frm.doc.from_warehouse ? 'Warehouse' : 'Company';
+ let dn = frm.doc.from_warehouse ? frm.doc.from_warehouse : frm.doc.company;
+ frappe.db.get_value(dt, dn, 'default_in_transit_warehouse', (r) => {
if (r.default_in_transit_warehouse) {
frm.set_value('to_warehouse', r.default_in_transit_warehouse);
}
@@ -690,7 +728,7 @@
no_batch_serial_number_value = !d.batch_no;
}
- if (no_batch_serial_number_value) {
+ if (no_batch_serial_number_value && !frappe.flags.hide_serial_batch_dialog) {
erpnext.stock.select_batch_and_serial_no(frm, d);
}
}
@@ -946,6 +984,7 @@
},
from_warehouse: function(doc) {
+ this.frm.trigger('set_tansit_warehouse');
this.set_warehouse_in_children(doc.items, "s_warehouse", doc.from_warehouse);
},
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index d77b70f..f8ac400 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -163,7 +163,7 @@
if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
- if self.job_card and self.purpose != 'Material Transfer for Manufacture':
+ if self.job_card and self.purpose not in ['Material Transfer for Manufacture', 'Repack']:
frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry")
.format(self.job_card))
@@ -276,9 +276,10 @@
item_wise_qty.setdefault(d.item_code, []).append(d.qty)
for item_code, qty_list in iteritems(item_wise_qty):
- if self.fg_completed_qty != sum(qty_list):
+ total = flt(sum(qty_list), frappe.get_precision("Stock Entry Detail", "qty"))
+ if self.fg_completed_qty != total:
frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different")
- .format(frappe.bold(item_code), frappe.bold(sum(qty_list)), frappe.bold(self.fg_completed_qty)))
+ .format(frappe.bold(item_code), frappe.bold(total), frappe.bold(self.fg_completed_qty)))
def validate_difference_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
@@ -457,7 +458,7 @@
Set rate for outgoing, scrapped and finished items
"""
# Set rate for outgoing items
- outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate)
+ outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate, raise_error_if_no_rate)
finished_item_qty = sum([d.transfer_qty for d in self.items if d.is_finished_item])
# Set basic rate for incoming items
@@ -481,13 +482,13 @@
d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
- def set_rate_for_outgoing_items(self, reset_outgoing_rate=True):
+ def set_rate_for_outgoing_items(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
outgoing_items_cost = 0.0
for d in self.get('items'):
if d.s_warehouse:
if reset_outgoing_rate:
args = self.get_args_for_incoming_rate(d)
- rate = get_incoming_rate(args)
+ rate = get_incoming_rate(args, raise_error_if_no_rate)
if rate > 0:
d.basic_rate = rate
@@ -823,6 +824,7 @@
if self.job_card:
job_doc = frappe.get_doc('Job Card', self.job_card)
job_doc.set_transferred_qty(update_status=True)
+ job_doc.set_transferred_qty_in_job_card(self)
if self.work_order:
pro_doc = frappe.get_doc("Work Order", self.work_order)
@@ -837,6 +839,7 @@
if not pro_doc.operations:
pro_doc.set_actual_dates()
+ @frappe.whitelist()
def get_item_details(self, args=None, for_update=False):
item = frappe.db.sql("""select i.name, i.stock_uom, i.description, i.image, i.item_name, i.item_group,
i.has_batch_no, i.sample_quantity, i.has_serial_no, i.allow_alternative_item,
@@ -911,6 +914,7 @@
return ret
+ @frappe.whitelist()
def set_items_for_stock_in(self):
self.items = []
@@ -935,6 +939,7 @@
'batch_no': d.batch_no
})
+ @frappe.whitelist()
def get_items(self):
self.set('items', [])
self.validate_work_order()
@@ -1008,7 +1013,8 @@
self.set_scrap_items()
self.set_actual_qty()
- self.calculate_rate_and_amount(raise_error_if_no_rate=False)
+ self.validate_customer_provided_item()
+ self.calculate_rate_and_amount()
def set_scrap_items(self):
if self.purpose != "Send to Subcontractor" and self.purpose in ["Manufacture", "Repack"]:
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 988ae92..864ff48 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -69,7 +69,8 @@
"putaway_rule",
"column_break_51",
"reference_purchase_receipt",
- "quality_inspection"
+ "quality_inspection",
+ "job_card_item"
],
"fields": [
{
@@ -532,13 +533,22 @@
"fieldname": "is_finished_item",
"fieldtype": "Check",
"label": "Is Finished Item"
+ },
+ {
+ "fieldname": "job_card_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Job Card Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-30 15:00:44.489442",
+ "modified": "2021-02-11 13:47:50.158754",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index 36d09ef..b0e7440 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -38,6 +38,7 @@
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
+
def on_submit(self):
self.check_stock_frozen_date()
self.actual_amt_check()
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 59f1f39..349d8ae 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -125,7 +125,7 @@
pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10',
warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100)
- return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15',
+ return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15',
warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2)
# check sle
@@ -278,7 +278,7 @@
frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR")
-
+
# Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100
pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100)
@@ -292,7 +292,7 @@
# Update raw material's valuation via LCV, Additional cost = 50
lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
-
+
pr1.reload()
self.assertEqual(pr1.items[0].valuation_rate, 125)
@@ -310,31 +310,36 @@
# Back dated stock transactions are only allowed to stock managers
frappe.db.set_value("Stock Settings", None,
"role_allowed_to_create_edit_back_dated_transactions", "Stock Manager")
-
+
# Set User with Stock User role but not Stock Manager
- frappe.set_user("test@example.com")
- user = frappe.get_doc("User", "test@example.com")
- user.add_roles("Stock User")
- user.remove_roles("Stock Manager")
+ try:
+ user = frappe.get_doc("User", "test@example.com")
+ frappe.set_user(user.name)
+ user.add_roles("Stock User")
+ user.remove_roles("Stock Manager")
- stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
- back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
- posting_date=add_days(today(), -1), do_not_submit=True)
+ stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
+ back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
+ posting_date=add_days(today(), -1), do_not_submit=True)
- # Block back-dated entry
- self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit)
+ # Block back-dated entry
+ self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit)
- user.add_roles("Stock Manager")
+ frappe.set_user("Administrator")
+ user.add_roles("Stock Manager")
+ frappe.set_user(user.name)
- # Back dated entry allowed to Stock Manager
- back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
- posting_date=add_days(today(), -1))
+ # Back dated entry allowed to Stock Manager
+ back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
+ posting_date=add_days(today(), -1))
- back_dated_se_2.cancel()
- stock_entry_on_today.cancel()
+ back_dated_se_2.cancel()
+ stock_entry_on_today.cancel()
- frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None)
- frappe.set_user("Administrator")
+ finally:
+ frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None)
+ frappe.set_user("Administrator")
+ user.remove_roles("Stock Manager")
def create_repack_entry(**args):
@@ -398,4 +403,4 @@
make_item(d, properties=properties)
- return items
\ No newline at end of file
+ return items
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index f0a90f9..b452e96 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -29,6 +29,8 @@
self.remove_items_with_no_change()
self.validate_data()
self.validate_expense_account()
+ self.validate_customer_provided_item()
+ self.set_zero_value_for_customer_provided_items()
self.set_total_qty_and_amount()
self.validate_putaway_capacity()
@@ -217,7 +219,7 @@
if row.valuation_rate in ("", None):
row.valuation_rate = previous_sle.get("valuation_rate", 0)
- if row.qty and not row.valuation_rate:
+ if row.qty and not row.valuation_rate and not row.allow_zero_valuation_rate:
frappe.throw(_("Valuation Rate required for Item {0} at row {1}").format(row.item_code, row.idx))
if ((previous_sle and row.qty == previous_sle.get("qty_after_transaction")
@@ -436,6 +438,20 @@
if frappe.db.get_value("Account", self.expense_account, "report_type") == "Profit and Loss":
frappe.throw(_("Difference Account must be a Asset/Liability type account, since this Stock Reconciliation is an Opening Entry"), OpeningEntryAccountError)
+ def set_zero_value_for_customer_provided_items(self):
+ changed_any_values = False
+
+ for d in self.get('items'):
+ is_customer_item = frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item')
+ if is_customer_item and d.valuation_rate:
+ d.valuation_rate = 0.0
+ changed_any_values = True
+
+ if changed_any_values:
+ msgprint(_("Valuation rate for customer provided items has been set to zero."),
+ title=_("Note"), indicator="blue")
+
+
def set_total_qty_and_amount(self):
for d in self.get("items"):
d.amount = flt(d.qty, d.precision("qty")) * flt(d.valuation_rate, d.precision("valuation_rate"))
@@ -531,4 +547,4 @@
account = frappe.db.get_value('Account', {'is_group': 0,
'company': company, 'account_type': 'Temporary'}, 'name')
- return account
\ No newline at end of file
+ return account
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 088456f..6690c6a 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -193,6 +193,16 @@
stock_doc = frappe.get_doc("Stock Reconciliation", d)
stock_doc.cancel()
+ def test_customer_provided_items(self):
+ item_code = 'Stock-Reco-customer-Item-100'
+ create_item(item_code, is_customer_provided_item = 1,
+ customer = '_Test Customer', is_purchase_item = 0)
+
+ sr = create_stock_reconciliation(item_code = item_code, qty = 10, rate = 420)
+
+ self.assertEqual(sr.get("items")[0].allow_zero_valuation_rate, 1)
+ self.assertEqual(sr.get("items")[0].valuation_rate, 0)
+ self.assertEqual(sr.get("items")[0].amount, 0)
def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
diff --git a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
index e53db07..85c7ebe 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -13,6 +13,7 @@
"qty",
"valuation_rate",
"amount",
+ "allow_zero_valuation_rate",
"serial_no_and_batch_section",
"serial_no",
"column_break_11",
@@ -166,10 +167,19 @@
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_zero_valuation_rate",
+ "fieldtype": "Check",
+ "label": "Allow Zero Valuation Rate",
+ "print_hide": 1,
+ "read_only": 1
}
],
"istable": 1,
- "modified": "2019-06-14 17:10:53.188305",
+ "links": [],
+ "modified": "2021-03-23 11:09:44.407157",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
@@ -179,4 +189,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/warehouse/warehouse.js b/erpnext/stock/doctype/warehouse/warehouse.js
index 1bea00e..1f17250 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.js
+++ b/erpnext/stock/doctype/warehouse/warehouse.js
@@ -3,6 +3,18 @@
frappe.ui.form.on("Warehouse", {
+ onload: function(frm) {
+ frm.set_query("default_in_transit_warehouse", function() {
+ return {
+ filters:{
+ 'warehouse_type' : 'Transit',
+ 'is_group': 0,
+ 'company': frm.doc.company
+ }
+ };
+ });
+ },
+
refresh: function(frm) {
frm.toggle_display('warehouse_name', frm.doc.__islocal);
frm.toggle_display(['address_html','contact_html'], !frm.doc.__islocal);
diff --git a/erpnext/stock/doctype/warehouse/warehouse.json b/erpnext/stock/doctype/warehouse/warehouse.json
index 1cc600b..bddb114 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.json
+++ b/erpnext/stock/doctype/warehouse/warehouse.json
@@ -13,6 +13,7 @@
"column_break_3",
"warehouse_type",
"parent_warehouse",
+ "default_in_transit_warehouse",
"is_group",
"column_break_4",
"account",
@@ -230,13 +231,20 @@
{
"fieldname": "column_break_3",
"fieldtype": "Section Break"
+ },
+ {
+ "depends_on": "eval: doc.warehouse_type !== 'Transit';",
+ "fieldname": "default_in_transit_warehouse",
+ "fieldtype": "Link",
+ "label": "Default In-Transit Warehouse",
+ "options": "Warehouse"
}
],
"icon": "fa fa-building",
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2020-08-03 18:41:52.442502",
+ "modified": "2021-02-16 17:21:52.380098",
"modified_by": "Administrator",
"module": "Stock",
"name": "Warehouse",
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 873cfec..e23f7d4 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -110,7 +110,7 @@
get_gross_profit(out)
if args.doctype == 'Material Request':
out.rate = args.rate or out.price_list_rate
- out.amount = flt(args.qty * out.rate)
+ out.amount = flt(args.qty) * flt(out.rate)
return out
@@ -314,7 +314,9 @@
"last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0,
"transaction_date": args.get("transaction_date"),
"against_blanket_order": args.get("against_blanket_order"),
- "bom_no": item.get("default_bom")
+ "bom_no": item.get("default_bom"),
+ "weight_per_unit": args.get("weight_per_unit") or item.get("weight_per_unit"),
+ "weight_uom": args.get("weight_uom") or item.get("weight_uom")
})
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
@@ -369,6 +371,9 @@
if meta.get_field("barcode"):
update_barcode_value(out)
+ if out.get("weight_per_unit"):
+ out['total_weight'] = out.weight_per_unit * out.stock_qty
+
return out
def get_item_warehouse(item, args, overwrite_warehouse, defaults={}):
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index e5d4d62..6dfede4 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -198,7 +198,7 @@
else:
qty_diff = flt(d.actual_qty)
- value_diff = flt(d.stock_value) - flt(qty_dict.bal_val)
+ value_diff = flt(d.stock_value_difference)
if d.posting_date < from_date:
qty_dict.opening_qty += qty_diff
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.js b/erpnext/stock/report/stock_ledger/stock_ledger.js
index 6f12c27..fe2417b 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.js
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.js
@@ -82,11 +82,6 @@
"label": __("Include UOM"),
"fieldtype": "Link",
"options": "UOM"
- },
- {
- "fieldname": "show_cancelled_entries",
- "label": __("Show Cancelled Entries"),
- "fieldtype": "Check"
}
],
"formatter": function (value, row, column, data, default_formatter) {
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 7b5701a..36996e9 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -138,7 +138,7 @@
`tabStock Ledger Entry` sle
WHERE
company = %(company)s
- AND posting_date BETWEEN %(from_date)s AND %(to_date)s
+ AND is_cancelled = 0 AND posting_date BETWEEN %(from_date)s AND %(to_date)s
{sle_conditions}
{item_conditions_sql}
ORDER BY
@@ -209,9 +209,6 @@
if filters.get("project"):
conditions.append("project=%(project)s")
- if not filters.get("show_cancelled_entries"):
- conditions.append("is_cancelled = 0")
-
return "and {}".format(" and ".join(conditions)) if conditions else ""
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 21860b6..121c51c 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -23,6 +23,7 @@
cancel = sl_entries[0].get("is_cancelled")
if cancel:
+ validate_cancellation(sl_entries)
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
for sle in sl_entries:
@@ -45,6 +46,21 @@
args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
+def validate_cancellation(args):
+ if args[0].get("is_cancelled"):
+ repost_entry = frappe.db.get_value("Repost Item Valuation", {
+ 'voucher_type': args[0].voucher_type,
+ 'voucher_no': args[0].voucher_no,
+ 'docstatus': 1
+ }, ['name', 'status'], as_dict=1)
+
+ if repost_entry:
+ if repost_entry.status == 'In Progress':
+ frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
+ if repost_entry.status == 'Queued':
+ doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
+ doc.cancel()
+ doc.delete()
def set_as_cancel(voucher_type, voucher_no):
frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1,
@@ -74,7 +90,8 @@
"item_code": args[i].item_code,
"warehouse": args[i].warehouse,
"posting_date": args[i].posting_date,
- "posting_time": args[i].posting_time
+ "posting_time": args[i].posting_time,
+ "creation": args[i].get("creation")
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
for item_wh, new_sle in iteritems(obj.new_items):
@@ -86,7 +103,7 @@
def get_args_for_voucher(voucher_type, voucher_no):
return frappe.db.get_all("Stock Ledger Entry",
filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
- fields=["item_code", "warehouse", "posting_date", "posting_time"],
+ fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
order_by="creation asc",
group_by="item_code, warehouse"
)
@@ -155,7 +172,7 @@
"""
self.data.setdefault(args.warehouse, frappe._dict())
warehouse_dict = self.data[args.warehouse]
- previous_sle = self.get_sle_before_datetime(args)
+ previous_sle = self.get_previous_sle_of_current_voucher(args)
warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
@@ -167,9 +184,35 @@
"stock_value_difference": 0.0
})
+ def get_previous_sle_of_current_voucher(self, args):
+ """get stock ledger entries filtered by specific posting datetime conditions"""
+
+ args['time_format'] = '%H:%i:%s'
+ if not args.get("posting_date"):
+ args["posting_date"] = "1900-01-01"
+ if not args.get("posting_time"):
+ args["posting_time"] = "00:00"
+
+ sle = frappe.db.sql("""
+ select *, timestamp(posting_date, posting_time) as "timestamp"
+ from `tabStock Ledger Entry`
+ where item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and is_cancelled = 0
+ and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+ order by timestamp(posting_date, posting_time) desc, creation desc
+ limit 1""", args, as_dict=1)
+
+ return sle[0] if sle else frappe._dict()
+
+
def build(self):
+ from erpnext.controllers.stock_controller import future_sle_exists
+
if self.args.get("sle_id"):
- self.process_sle_against_current_voucher()
+ self.process_sle_against_current_timestamp()
+ if not future_sle_exists(self.args):
+ self.update_bin()
else:
entries_to_fix = self.get_future_entries_to_fix()
@@ -183,12 +226,12 @@
if sle.dependant_sle_voucher_detail_no:
entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
+ self.update_bin()
+
if self.exceptions:
self.raise_exceptions()
- self.update_bin()
-
- def process_sle_against_current_voucher(self):
+ def process_sle_against_current_timestamp(self):
sl_entries = self.get_sle_against_current_voucher()
for sle in sl_entries:
self.process_sle(sle)
@@ -204,8 +247,8 @@
where
item_code = %(item_code)s
and warehouse = %(warehouse)s
- and voucher_type = %(voucher_type)s
- and voucher_no = %(voucher_no)s
+ and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+
order by
creation ASC
for update
@@ -232,7 +275,6 @@
return entries_to_fix
elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
return entries_to_fix
-
self.initialize_previous_data(dependant_sle)
args = self.data[dependant_sle.warehouse].previous_sle \
@@ -398,7 +440,7 @@
# Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
- doc = frappe.get_cached_doc(sle.voucher_type, sle.voucher_no)
+ doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
doc.update_valuation_rate(reset_outgoing_rate=False)
for d in (doc.items + doc.supplied_items):
d.db_update()
@@ -639,7 +681,6 @@
# update bin for each warehouse
for warehouse, data in iteritems(self.data):
bin_doc = get_bin(self.item_code, warehouse)
-
bin_doc.update({
"valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction,
@@ -765,6 +806,25 @@
return valuation_rate
+def update_qty_in_future_sle(args, allow_negative_stock=None):
+ frappe.db.sql("""
+ update `tabStock Ledger Entry`
+ set qty_after_transaction = qty_after_transaction + {qty}
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
+ or (
+ timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
+ and creation > %(creation)s
+ )
+ )
+ """.format(qty=args.actual_qty), args)
+
+ validate_negative_qty_in_future_sle(args, allow_negative_stock)
+
def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
allow_negative_stock = allow_negative_stock \
or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
@@ -793,7 +853,7 @@
and voucher_no != %(voucher_no)s
and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
and is_cancelled = 0
- and qty_after_transaction + {0} < 0
+ and qty_after_transaction < 0
order by timestamp(posting_date, posting_time) asc
limit 1
- """.format(args.actual_qty), args, as_dict=1)
+ """, args, as_dict=1)
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index 167e80f..9fe12f9 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -49,8 +49,8 @@
},
refresh: function (frm) {
- if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") {
- if (frm.doc.service_level_agreement) {
+ if (frm.doc.status !== "Closed") {
+ if (frm.doc.service_level_agreement && frm.doc.agreement_status === "Ongoing") {
frappe.call({
"method": "frappe.client.get",
args: {
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index bbbbc4a..767a8a6 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -165,6 +165,7 @@
communication.ignore_mandatory = True
communication.save()
+ @frappe.whitelist()
def split_issue(self, subject, communication_id):
# Bug: Pressing enter doesn't send subject
from copy import deepcopy
@@ -259,6 +260,7 @@
self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
frappe.msgprint(_("Service Level Agreement has been changed to {0}.").format(self.service_level_agreement))
+ @frappe.whitelist()
def reset_service_level_agreement(self, reason, user):
if not frappe.db.get_single_value("Support Settings", "allow_resetting_service_level_agreement"):
frappe.throw(_("Allow Resetting Service Level Agreement from Support Settings."))
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index 483bb15..46d02d8 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -12,7 +12,6 @@
class TestIssue(unittest.TestCase):
def setUp(self):
frappe.db.sql("delete from `tabService Level Agreement`")
- frappe.db.sql("delete from `tabEmployee`")
frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
create_service_level_agreements_for_issues()
diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py
index 3d73531..7861e30 100644
--- a/erpnext/support/report/issue_summary/issue_summary.py
+++ b/erpnext/support/report/issue_summary/issue_summary.py
@@ -260,8 +260,7 @@
self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0
def get_chart_data(self):
- if not self.data:
- return None
+ self.chart = []
labels = []
open_issues = []
@@ -310,8 +309,7 @@
}
def get_report_summary(self):
- if not self.data:
- return None
+ self.report_summary = []
open_issues = 0
replied = 0
diff --git a/erpnext/templates/includes/issue_row.html b/erpnext/templates/includes/issue_row.html
index d909c5f..a04f558 100644
--- a/erpnext/templates/includes/issue_row.html
+++ b/erpnext/templates/includes/issue_row.html
@@ -1,6 +1,6 @@
<div class="web-list-item transaction-list-item">
<a href="/issues?name={{ doc.name }}" class="no-underline">
- <div class="row py-4 border-bottom">
+ <div class="row py-4">
<div class="col-3 d-flex align-items-center">
{% set indicator = 'red' if doc.status == 'Open' else 'gray' %}
{% set indicator = 'green' if doc.status == 'Closed' else indicator %}
diff --git a/erpnext/www/lms/index.py b/erpnext/www/lms/index.py
index 00f66e7..26f59a2 100644
--- a/erpnext/www/lms/index.py
+++ b/erpnext/www/lms/index.py
@@ -13,4 +13,4 @@
def get_featured_programs():
- return utils.get_portal_programs()
\ No newline at end of file
+ return utils.get_portal_programs() or []
\ No newline at end of file
diff --git a/erpnext/www/lms/program.py b/erpnext/www/lms/program.py
index d3b04c2..104d3fa 100644
--- a/erpnext/www/lms/program.py
+++ b/erpnext/www/lms/program.py
@@ -26,4 +26,4 @@
def get_course_progress(courses, program):
progress = {course.name: utils.get_course_progress(course, program) for course in courses}
- return progress
\ No newline at end of file
+ return progress or {}
\ No newline at end of file
diff --git a/sider.yml b/sider.yml
new file mode 100644
index 0000000..2ca6e8d
--- /dev/null
+++ b/sider.yml
@@ -0,0 +1,3 @@
+linter:
+ flake8:
+ config: .flake8
\ No newline at end of file