diff --git a/.flake8 b/.flake8
index 399b176e1d..56c9b9a369 100644
--- a/.flake8
+++ b/.flake8
@@ -29,4 +29,5 @@ ignore =
B950,
W191,
-max-line-length = 200
\ No newline at end of file
+max-line-length = 200
+exclude=.github/helper/semgrep_rules
diff --git a/.github/helper/semgrep_rules/frappe_correctness.py b/.github/helper/semgrep_rules/frappe_correctness.py
index 4798b927f8..745e6463b8 100644
--- a/.github/helper/semgrep_rules/frappe_correctness.py
+++ b/.github/helper/semgrep_rules/frappe_correctness.py
@@ -4,25 +4,61 @@ from frappe import _, flt
from frappe.model.document import Document
+# ruleid: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
- # ruleid: frappe-modifying-after-submit
self.status = 'Submitted'
+
+# ok: frappe-modifying-but-not-comitting
def on_submit(self):
- if flt(self.per_billed) < 100:
- self.update_billing_status()
- else:
- # todook: frappe-modifying-after-submit
- self.status = "Completed"
- self.db_set("status", "Completed")
+ if self.value_of_goods == 0:
+ frappe.throw(_('Value of goods cannot be 0'))
+ self.status = 'Submitted'
+ self.db_set('status', 'Submitted')
-class TestDoc(Document):
- pass
+# ok: frappe-modifying-but-not-comitting
+def on_submit(self):
+ if self.value_of_goods == 0:
+ frappe.throw(_('Value of goods cannot be 0'))
+ x = "y"
+ self.status = x
+ self.db_set('status', x)
- def validate(self):
- #ruleid: frappe-modifying-child-tables-while-iterating
- for item in self.child_table:
- if item.value < 0:
- self.remove(item)
+
+# ok: frappe-modifying-but-not-comitting
+def on_submit(self):
+ x = "y"
+ self.status = x
+ self.save()
+
+# ruleid: frappe-modifying-but-not-comitting-other-method
+class DoctypeClass(Document):
+ def on_submit(self):
+ self.good_method()
+ self.tainted_method()
+
+ def tainted_method(self):
+ self.status = "uptate"
+
+
+# ok: frappe-modifying-but-not-comitting-other-method
+class DoctypeClass(Document):
+ def on_submit(self):
+ self.good_method()
+ self.tainted_method()
+
+ def tainted_method(self):
+ self.status = "update"
+ self.db_set("status", "update")
+
+# ok: frappe-modifying-but-not-comitting-other-method
+class DoctypeClass(Document):
+ def on_submit(self):
+ self.good_method()
+ self.tainted_method()
+ self.save()
+
+ def tainted_method(self):
+ self.status = "uptate"
diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml
index 54df062480..faab3344a6 100644
--- a/.github/helper/semgrep_rules/frappe_correctness.yml
+++ b/.github/helper/semgrep_rules/frappe_correctness.yml
@@ -1,32 +1,93 @@
# This file specifies rules for correctness according to how frappe doctype data model works.
rules:
-- id: frappe-modifying-after-submit
+- id: frappe-modifying-but-not-comitting
patterns:
- - pattern: self.$ATTR = ...
- - pattern-inside: |
- def on_submit(self, ...):
+ - pattern: |
+ def $METHOD(self, ...):
...
+ self.$ATTR = ...
+ - pattern-not: |
+ def $METHOD(self, ...):
+ ...
+ self.$ATTR = ...
+ ...
+ self.db_set(..., self.$ATTR, ...)
+ - pattern-not: |
+ def $METHOD(self, ...):
+ ...
+ self.$ATTR = $SOME_VAR
+ ...
+ self.db_set(..., $SOME_VAR, ...)
+ - pattern-not: |
+ def $METHOD(self, ...):
+ ...
+ self.$ATTR = $SOME_VAR
+ ...
+ self.save()
- metavariable-regex:
metavariable: '$ATTR'
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
- regex: '^(?!status_updater)(.*)$'
+ regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
+ - metavariable-regex:
+ metavariable: "$METHOD"
+ regex: "(on_submit|on_cancel)"
message: |
- Doctype modified after submission. Please check if modification of self.$ATTR is commited to database.
+ DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
languages: [python]
severity: ERROR
-- id: frappe-modifying-after-cancel
+- id: frappe-modifying-but-not-comitting-other-method
patterns:
- - pattern: self.$ATTR = ...
- - pattern-inside: |
- def on_cancel(self, ...):
+ - pattern: |
+ class $DOCTYPE(...):
+ def $METHOD(self, ...):
...
- - metavariable-regex:
- metavariable: '$ATTR'
- regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
+ self.$ANOTHER_METHOD()
+ ...
+
+ def $ANOTHER_METHOD(self, ...):
+ ...
+ self.$ATTR = ...
+ - pattern-not: |
+ class $DOCTYPE(...):
+ def $METHOD(self, ...):
+ ...
+ self.$ANOTHER_METHOD()
+ ...
+
+ def $ANOTHER_METHOD(self, ...):
+ ...
+ self.$ATTR = ...
+ ...
+ self.db_set(..., self.$ATTR, ...)
+ - pattern-not: |
+ class $DOCTYPE(...):
+ def $METHOD(self, ...):
+ ...
+ self.$ANOTHER_METHOD()
+ ...
+
+ def $ANOTHER_METHOD(self, ...):
+ ...
+ self.$ATTR = $SOME_VAR
+ ...
+ self.db_set(..., $SOME_VAR, ...)
+ - pattern-not: |
+ class $DOCTYPE(...):
+ def $METHOD(self, ...):
+ ...
+ self.$ANOTHER_METHOD()
+ ...
+ self.save()
+ def $ANOTHER_METHOD(self, ...):
+ ...
+ self.$ATTR = ...
+ - metavariable-regex:
+ metavariable: "$METHOD"
+ regex: "(on_submit|on_cancel)"
message: |
- Doctype modified after cancellation. Please check if modification of self.$ATTR is commited to database.
+ self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
languages: [python]
severity: ERROR
diff --git a/.github/helper/semgrep_rules/translate.js b/.github/helper/semgrep_rules/translate.js
index 7b92fe2dff..9cdfb75d0b 100644
--- a/.github/helper/semgrep_rules/translate.js
+++ b/.github/helper/semgrep_rules/translate.js
@@ -35,3 +35,10 @@ __('You have' + 'subscribers in your mailing list.')
// ruleid: frappe-translation-js-splitting
__('You have {0} subscribers' +
'in your mailing list', [subscribers.length])
+
+// ok: frappe-translation-js-splitting
+__("Ctrl+Enter to add comment")
+
+// ruleid: frappe-translation-js-splitting
+__('You have {0} subscribers \
+ in your mailing list', [subscribers.length])
diff --git a/.github/helper/semgrep_rules/translate.py b/.github/helper/semgrep_rules/translate.py
index bd6cd9126c..9de6aa94f0 100644
--- a/.github/helper/semgrep_rules/translate.py
+++ b/.github/helper/semgrep_rules/translate.py
@@ -51,3 +51,11 @@ _(f"what" + f"this is also not cool")
_("")
# ruleid: frappe-translation-empty-string
_('')
+
+
+class Test:
+ # ok: frappe-translation-python-splitting
+ def __init__(
+ args
+ ):
+ pass
diff --git a/.github/helper/semgrep_rules/translate.yml b/.github/helper/semgrep_rules/translate.yml
index 3737da5a7e..5f03fb9fd0 100644
--- a/.github/helper/semgrep_rules/translate.yml
+++ b/.github/helper/semgrep_rules/translate.yml
@@ -42,9 +42,10 @@ rules:
- id: frappe-translation-python-splitting
pattern-either:
- - pattern: _(...) + ... + _(...)
+ - pattern: _(...) + _(...)
- pattern: _("..." + "...")
- - pattern-regex: '_\([^\)]*\\\s*'
+ - pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
+ - pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
Please refer: https://frappeframework.com/docs/user/en/translations
@@ -53,8 +54,8 @@ rules:
- id: frappe-translation-js-splitting
pattern-either:
- - pattern-regex: '__\([^\)]*[\+\\]\s*'
- - pattern: __('...' + '...')
+ - pattern-regex: '__\([^\)]*[\\]\s+'
+ - pattern: __('...' + '...', ...)
- pattern: __('...') + __('...')
message: |
Do not split strings inside translate function. Do not concatenate using translate functions.
diff --git a/.github/helper/semgrep_rules/ux.js b/.github/helper/semgrep_rules/ux.js
new file mode 100644
index 0000000000..ae73f9cc60
--- /dev/null
+++ b/.github/helper/semgrep_rules/ux.js
@@ -0,0 +1,9 @@
+
+// ok: frappe-missing-translate-function-js
+frappe.msgprint('{{ _("Both login and password required") }}');
+
+// ruleid: frappe-missing-translate-function-js
+frappe.msgprint('What');
+
+// ok: frappe-missing-translate-function-js
+frappe.throw(' {{ _("Both login and password required") }}. ');
diff --git a/.github/helper/semgrep_rules/ux.py b/.github/helper/semgrep_rules/ux.py
index 4a74457435..a00d3cd8ae 100644
--- a/.github/helper/semgrep_rules/ux.py
+++ b/.github/helper/semgrep_rules/ux.py
@@ -2,30 +2,30 @@ import frappe
from frappe import msgprint, throw, _
-# ruleid: frappe-missing-translate-function
+# ruleid: frappe-missing-translate-function-python
throw("Error Occured")
-# ruleid: frappe-missing-translate-function
+# ruleid: frappe-missing-translate-function-python
frappe.throw("Error Occured")
-# ruleid: frappe-missing-translate-function
+# ruleid: frappe-missing-translate-function-python
frappe.msgprint("Useful message")
-# ruleid: frappe-missing-translate-function
+# ruleid: frappe-missing-translate-function-python
msgprint("Useful message")
-# ok: frappe-missing-translate-function
+# ok: frappe-missing-translate-function-python
translatedmessage = _("Hello")
-# ok: frappe-missing-translate-function
+# ok: frappe-missing-translate-function-python
throw(translatedmessage)
-# ok: frappe-missing-translate-function
+# ok: frappe-missing-translate-function-python
msgprint(translatedmessage)
-# ok: frappe-missing-translate-function
+# ok: frappe-missing-translate-function-python
msgprint(_("Helpful message"))
-# ok: frappe-missing-translate-function
+# ok: frappe-missing-translate-function-python
frappe.throw(_("Error occured"))
diff --git a/.github/helper/semgrep_rules/ux.yml b/.github/helper/semgrep_rules/ux.yml
index ed06a6a80c..dd667f36c0 100644
--- a/.github/helper/semgrep_rules/ux.yml
+++ b/.github/helper/semgrep_rules/ux.yml
@@ -1,15 +1,30 @@
rules:
-- id: frappe-missing-translate-function
+- id: frappe-missing-translate-function-python
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(_("..."), ...)
- - pattern-not: frappe.msgprint(__("..."), ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(_("..."), ...)
- - pattern-not: frappe.throw(__("..."), ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
- languages: [python, javascript, json]
+ languages: [python]
+ severity: ERROR
+
+- id: frappe-missing-translate-function-js
+ pattern-either:
+ - patterns:
+ - pattern: frappe.msgprint("...", ...)
+ - pattern-not: frappe.msgprint(__("..."), ...)
+ # ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
+ - pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
+ - patterns:
+ - pattern: frappe.throw("...", ...)
+ - pattern-not: frappe.throw(__("..."), ...)
+ # ignore microtemplating
+ - pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
+ message: |
+ All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
+ languages: [javascript]
severity: ERROR
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index df08263236..389524e968 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -4,6 +4,8 @@ on:
pull_request:
branches:
- develop
+ - version-13-hotfix
+ - version-13-pre-release
jobs:
semgrep:
name: Frappe Linter
@@ -14,11 +16,19 @@ jobs:
uses: actions/setup-python@v2
with:
python-version: 3.8
- - name: Run semgrep
+
+ - name: Setup semgrep
run: |
python -m pip install -q semgrep
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
+
+ - name: Semgrep errors
+ run: |
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
semgrep --config="r/python.lang.correctness" --quiet --error $files
+
+ - name: Semgrep warnings
+ run: |
+ files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
diff --git a/.pylintrc b/.pylintrc
deleted file mode 100644
index 4b2ea0a564..0000000000
--- a/.pylintrc
+++ /dev/null
@@ -1 +0,0 @@
-disable=access-member-before-definition
\ No newline at end of file
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 8d1767497f..20097ef066 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides
from frappe.utils import getdate
-__version__ = '13.3.1'
+__version__ = '13.4.0'
def get_default_company(user=None):
'''Get default company for user'''
diff --git a/erpnext/accounts/deferred_revenue.py b/erpnext/accounts/deferred_revenue.py
index d5ab1c1704..dd346bc240 100644
--- a/erpnext/accounts/deferred_revenue.py
+++ b/erpnext/accounts/deferred_revenue.py
@@ -41,7 +41,7 @@ def build_conditions(process_type, account, company):
if account:
conditions += "AND %s='%s'"%(deferred_account, account)
elif company:
- conditions += "AND p.company='%s'"%(company)
+ conditions += f"AND p.company = {frappe.db.escape(company)}"
return conditions
@@ -360,12 +360,10 @@ def make_gl_entries(doc, credit_account, debit_account, against,
frappe.flags.deferred_accounting_error = True
def send_mail(deferred_process):
- title = _("Error while processing deferred accounting for {0}".format(deferred_process))
- content = _("""
- Deferred accounting failed for some invoices:
- Please check Process Deferred Accounting {0}
- and submit manually after resolving errors
- """).format(get_link_to_form('Process Deferred Accounting', deferred_process))
+ title = _("Error while processing deferred accounting for {0}").format(deferred_process)
+ link = get_link_to_form('Process Deferred Accounting', deferred_process)
+ content = _("Deferred accounting failed for some invoices:") + "\n"
+ content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link)
sendmail_to_system_managers(title, content)
def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index 78febf9c2e..948c51364e 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -75,8 +75,13 @@ class GLEntry(Document):
def pl_must_have_cost_center(self):
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))
+ msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
+ self.voucher_type, self.voucher_no, self.account)
+ msg += " "
+ msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
+ self.voucher_type)
+
+ frappe.throw(msg, title=_("Missing Cost Center"))
def validate_dimensions_for_pl_and_bs(self):
account_type = frappe.db.get_value("Account", self.account, "report_type")
diff --git a/erpnext/accounts/doctype/gst_account/gst_account.json b/erpnext/accounts/doctype/gst_account/gst_account.json
index 70673387fe..b6ec8844e1 100644
--- a/erpnext/accounts/doctype/gst_account/gst_account.json
+++ b/erpnext/accounts/doctype/gst_account/gst_account.json
@@ -1,196 +1,82 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-01-02 15:48:58.768352",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2018-01-02 15:48:58.768352",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "cgst_account",
+ "sgst_account",
+ "igst_account",
+ "cess_account",
+ "is_reverse_charge_account"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "columns": 1,
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "cgst_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": "CGST Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fieldname": "cgst_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "CGST Account",
+ "options": "Account",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "sgst_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": "SGST Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fieldname": "sgst_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "SGST Account",
+ "options": "Account",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "igst_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": "IGST Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "columns": 2,
+ "fieldname": "igst_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "IGST Account",
+ "options": "Account",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "cess_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": "CESS 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
+ "columns": 2,
+ "fieldname": "cess_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "CESS Account",
+ "options": "Account"
+ },
+ {
+ "columns": 1,
+ "default": "0",
+ "fieldname": "is_reverse_charge_account",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Reverse Charge Account"
}
- ],
- "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-02 15:52:22.335988",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "GST Account",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-04-09 12:30:25.889993",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "GST Account",
+ "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/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index fefab82efc..ed1bd28223 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -39,7 +39,11 @@ class JournalEntry(AccountsController):
self.validate_multi_currency()
self.set_amounts_in_company_currency()
self.validate_debit_credit_amount()
- self.validate_total_debit_and_credit()
+
+ # Do not validate while importing via data import
+ if not frappe.flags.in_import:
+ self.validate_total_debit_and_credit()
+
self.validate_against_jv()
self.validate_reference_doc()
self.set_against_account()
diff --git a/erpnext/accounts/doctype/journal_entry/regional/india.js b/erpnext/accounts/doctype/journal_entry/regional/india.js
new file mode 100644
index 0000000000..75a69ac0cf
--- /dev/null
+++ b/erpnext/accounts/doctype/journal_entry/regional/india.js
@@ -0,0 +1,17 @@
+frappe.ui.form.on("Journal Entry", {
+ refresh: function(frm) {
+ frm.set_query('company_address', function(doc) {
+ if(!doc.company) {
+ frappe.throw(__('Please set Company'));
+ }
+
+ return {
+ query: 'frappe.contacts.doctype.address.address.address_query',
+ filters: {
+ link_doctype: 'Company',
+ link_name: doc.company
+ }
+ };
+ });
+ }
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json b/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json
index 6e7768dc54..bbf1ba0020 100644
--- a/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json
+++ b/erpnext/accounts/doctype/pos_closing_entry_detail/pos_closing_entry_detail.json
@@ -46,6 +46,7 @@
"reqd": 1
},
{
+ "default": "0",
"fieldname": "closing_amount",
"fieldtype": "Currency",
"in_list_view": 1,
@@ -57,7 +58,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-23 16:45:43.662034",
+ "modified": "2021-05-19 20:08:44.523861",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry Detail",
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index 473db565fa..f55fdab21c 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -455,32 +455,26 @@ class POSInvoice(SalesInvoice):
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
- latest_sle = frappe.db.sql("""select qty_after_transaction
- from `tabStock Ledger Entry`
+ bin_qty = frappe.db.sql("""select actual_qty from `tabBin`
where item_code = %s and warehouse = %s
- order by posting_date desc, posting_time desc
limit 1""", (item_code, warehouse), as_dict=1)
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
- sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
+ bin_qty = bin_qty[0].actual_qty or 0 if bin_qty else 0
- if sle_qty and pos_sales_qty:
- return sle_qty - pos_sales_qty
- else:
- return sle_qty
+ return bin_qty - pos_sales_qty
def get_pos_reserved_qty(item_code, warehouse):
reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
where p.name = p_item.parent
- and p.consolidated_invoice is NULL
- and p.docstatus = 1
+ and ifnull(p.consolidated_invoice, '') = ''
and p_item.docstatus = 1
and p_item.item_code = %s
and p_item.warehouse = %s
""", (item_code, warehouse), as_dict=1)
-
+
return reserved_qty[0].qty or 0 if reserved_qty else 0
@frappe.whitelist()
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 bc7874305c..08e072e204 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
@@ -42,8 +42,9 @@ class POSInvoiceMergeLog(Document):
if return_against_status != "Consolidated":
# if return entry is not getting merged in the current pos closing and if it is not consolidated
bold_unconsolidated = frappe.bold("not Consolidated")
- msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}. ")
+ msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}.")
.format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
+ msg += " "
msg += _("Original invoice should be consolidated before or along with the return invoice.")
msg += "
"
msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
@@ -56,12 +57,12 @@ class POSInvoiceMergeLog(Document):
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
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)
+ if sales:
+ sales_invoice = self.process_merging_into_sales_invoice(sales)
+
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
@@ -274,9 +275,9 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
closing_entry.db_set('error_message', '')
closing_entry.update_opening_entry()
- except Exception:
+ except Exception as e:
frappe.db.rollback()
- message_log = frappe.message_log.pop()
+ message_log = frappe.message_log.pop() if frappe.message_log else str(e)
error_message = safe_load_json(message_log)
if closing_entry:
@@ -300,9 +301,9 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
closing_entry.db_set('error_message', '')
closing_entry.update_opening_entry(for_cancel=True)
- except Exception:
+ except Exception as e:
frappe.db.rollback()
- message_log = frappe.message_log.pop()
+ message_log = frappe.message_log.pop() if frappe.message_log else str(e)
error_message = safe_load_json(message_log)
if closing_entry:
@@ -348,11 +349,9 @@ def job_already_enqueued(job_name):
return True
def safe_load_json(message):
- JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
-
try:
json_message = json.loads(message).get('message')
- except JSONDecodeError:
+ except Exception:
json_message = message
return json_message
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
index f61aacbce2..7328f168e3 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.html
@@ -1,24 +1,42 @@
-
{{ filters.party[0] }}
-{{ _("Statement of Accounts") }}
+
+
+
+
{{ _("STATEMENTS OF ACCOUNTS") }}
+
+
{{ _("Customer: ") }} {{filters.party[0] }}
+
+ {{ _("Date: ") }}
+ {{ frappe.format(filters.from_date, 'Date')}}
+ {{ _("to") }}
+ {{ frappe.format(filters.to_date, 'Date')}}
+
+
+
-
- {{ frappe.format(filters.from_date, 'Date')}}
- {{ _("to") }}
- {{ frappe.format(filters.to_date, 'Date')}}
-
-
-
-
-
- {{ _("Date") }} |
- {{ _("Ref") }} |
- {{ _("Party") }} |
- {{ _("Debit") }} |
- {{ _("Credit") }} |
- {{ _("Balance (Dr - Cr)") }} |
-
-
-
+
+
+
+ {{ _("Date") }} |
+ {{ _("Reference") }} |
+ {{ _("Remarks") }} |
+ {{ _("Debit") }} |
+ {{ _("Credit") }} |
+ {{ _("Balance (Dr - Cr)") }} |
+
+
+
{% for row in data %}
{% if(row.posting_date) %}
@@ -58,32 +76,34 @@
{% endfor %}
-
-
-{% if ageing %}
-{{ _("Ageing Report Based On ") }} {{ ageing.ageing_based_on }}
-
- {{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
-
-
-
-
-
-
- 30 Days |
- 60 Days |
- 90 Days |
- 120 Days |
-
-
-
-
- {{ frappe.utils.fmt_money(ageing.range1, currency=filters.presentation_currency) }} |
- {{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }} |
- {{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }} |
- {{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }} |
-
-
-
-{% endif %}
-Printed On {{ frappe.format(frappe.utils.get_datetime(), 'Datetime') }}
\ No newline at end of file
+
+
+ {% if ageing %}
+
{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
+ {{ _("up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
+
+
+
+
+ 30 Days |
+ 60 Days |
+ 90 Days |
+ 120 Days |
+
+
+
+
+ {{ frappe.utils.fmt_money(ageing.range1, currency=filters.presentation_currency) }} |
+ {{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }} |
+ {{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }} |
+ {{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }} |
+
+
+
+ {% endif %}
+ {% if terms_and_conditions %}
+
+ {{ terms_and_conditions }}
+
+ {% endif %}
+
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
index 4be0e2ec06..27a5f50ce2 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
@@ -1,6 +1,5 @@
{
"actions": [],
- "allow_workflow": 1,
"autoname": "Prompt",
"creation": "2020-05-22 16:46:18.712954",
"doctype": "DocType",
@@ -28,9 +27,11 @@
"customers",
"preferences",
"orientation",
- "section_break_14",
"include_ageing",
"ageing_based_on",
+ "section_break_14",
+ "letter_head",
+ "terms_and_conditions",
"section_break_1",
"enable_auto_email",
"section_break_18",
@@ -270,10 +271,22 @@
"fieldname": "body",
"fieldtype": "Text Editor",
"label": "Body"
+ },
+ {
+ "fieldname": "letter_head",
+ "fieldtype": "Link",
+ "label": "Letter Head",
+ "options": "Letter Head"
+ },
+ {
+ "fieldname": "terms_and_conditions",
+ "fieldtype": "Link",
+ "label": "Terms and Conditions",
+ "options": "Terms and Conditions"
}
],
"links": [],
- "modified": "2020-08-08 08:47:09.185728",
+ "modified": "2021-05-21 10:14:22.426672",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index a0dbff3db4..0b0ee904ff 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -64,6 +64,9 @@ def get_report_pdf(doc, consolidated=True):
tax_id = frappe.get_doc('Customer', entry.customer).tax_id
presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \
or doc.currency or get_company_currency(doc.company)
+ if doc.letter_head:
+ from frappe.www.printview import get_letter_head
+ letter_head = get_letter_head(doc, 0)
filters= frappe._dict({
'from_date': doc.from_date,
@@ -91,7 +94,10 @@ def get_report_pdf(doc, consolidated=True):
continue
html = frappe.render_template(template_path, \
- {"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None})
+ {"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None,
+ "letter_head": letter_head if doc.letter_head else None,
+ "terms_and_conditions": frappe.db.get_value('Terms and Conditions', doc.terms_and_conditions, 'terms')
+ if doc.terms_and_conditions else None})
html = frappe.render_template(base_template_path, {"body": html, \
"css": get_print_style(), "title": "Statement For " + entry.customer})
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 7c73ad6c90..1808005f62 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
var me = this;
this._super();
- this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice'];
+ this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet'];
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
// show debit_to in print format
this.frm.set_df_property("debit_to", "print_hide", 0);
@@ -582,6 +582,16 @@ frappe.ui.form.on('Sales Invoice', {
};
});
+ frm.set_query("adjustment_against", function() {
+ return {
+ filters: {
+ company: frm.doc.company,
+ customer: frm.doc.customer,
+ docstatus: 1
+ }
+ };
+ });
+
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
'Sales Invoice': 'Return / Credit Note',
@@ -685,14 +695,16 @@ frappe.ui.form.on('Sales Invoice', {
},
project: function(frm){
- frm.call({
- method: "add_timesheet_data",
- doc: frm.doc,
- callback: function(r, rt) {
- refresh_field(['timesheets'])
- }
- })
- frm.refresh();
+ if (!frm.doc.is_return) {
+ frm.call({
+ method: "add_timesheet_data",
+ doc: frm.doc,
+ callback: function(r, rt) {
+ refresh_field(['timesheets'])
+ }
+ })
+ frm.refresh();
+ }
},
onload: function(frm) {
@@ -807,14 +819,27 @@ frappe.ui.form.on('Sales Invoice', {
}
},
+ add_timesheet_row: function(frm, row, exchange_rate) {
+ frm.add_child('timesheets', {
+ 'activity_type': row.activity_type,
+ 'description': row.description,
+ 'time_sheet': row.parent,
+ 'billing_hours': row.billing_hours,
+ 'billing_amount': flt(row.billing_amount) * flt(exchange_rate),
+ 'timesheet_detail': row.name
+ });
+ frm.refresh_field('timesheets');
+ calculate_total_billing_amount(frm);
+ },
+
refresh: function(frm) {
- if (frm.doc.project) {
+ if (frm.doc.docstatus===0 && !frm.doc.is_return) {
frm.add_custom_button(__('Fetch Timesheet'), function() {
let d = new frappe.ui.Dialog({
title: __('Fetch Timesheet'),
fields: [
{
- "label" : "From",
+ "label" : __("From"),
"fieldname": "from_time",
"fieldtype": "Date",
"reqd": 1,
@@ -824,11 +849,18 @@ frappe.ui.form.on('Sales Invoice', {
fieldname: 'col_break_1',
},
{
- "label" : "To",
+ "label" : __("To"),
"fieldname": "to_time",
"fieldtype": "Date",
"reqd": 1,
- }
+ },
+ {
+ "label" : __("Project"),
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "options": "Project",
+ "default": frm.doc.project
+ },
],
primary_action: function() {
let data = d.get_values();
@@ -837,27 +869,35 @@ frappe.ui.form.on('Sales Invoice', {
args: {
from_time: data.from_time,
to_time: data.to_time,
- project: frm.doc.project
+ project: data.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
+ if (!r.exc && r.message.length > 0) {
+ frm.clear_table('timesheets')
+ r.message.forEach((d) => {
+ let exchange_rate = 1.0;
+ if (frm.doc.currency != d.currency) {
+ frappe.call({
+ method: 'erpnext.setup.utils.get_exchange_rate',
+ args: {
+ from_currency: d.currency,
+ to_currency: frm.doc.currency
+ },
+ callback: function(r) {
+ if (r.message) {
+ exchange_rate = r.message;
+ frm.events.add_timesheet_row(frm, d, exchange_rate);
+ }
+ }
});
- });
- frm.refresh_field('timesheets')
- }
- else {
- frappe.msgprint(__('No Timesheet Found.'))
- }
- d.hide();
+ } else {
+ frm.events.add_timesheet_row(frm, d, exchange_rate);
+ }
+ });
+ } else {
+ frappe.msgprint(__('No Timesheets found with the selected filters.'))
}
+ d.hide();
}
});
},
@@ -867,6 +907,10 @@ frappe.ui.form.on('Sales Invoice', {
})
}
+ if (frm.doc.is_debit_note) {
+ frm.set_df_property('return_against', 'label', 'Adjustment Against');
+ }
+
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 c6c67b4ddc..e7dd6b8a60 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -16,6 +16,7 @@
"is_pos",
"is_consolidated",
"is_return",
+ "is_debit_note",
"update_billed_amount_in_sales_order",
"column_break1",
"company",
@@ -392,7 +393,7 @@
"read_only": 1
},
{
- "depends_on": "return_against",
+ "depends_on": "eval:doc.return_against || doc.is_debit_note",
"fieldname": "return_against",
"fieldtype": "Link",
"hide_days": 1,
@@ -401,7 +402,7 @@
"no_copy": 1,
"options": "Sales Invoice",
"print_hide": 1,
- "read_only": 1,
+ "read_only_depends_on": "eval:doc.is_return",
"search_index": 1
},
{
@@ -748,6 +749,7 @@
{
"collapsible": 1,
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
+ "depends_on": "eval: !doc.is_return",
"fieldname": "time_sheet_list",
"fieldtype": "Section Break",
"hide_days": 1,
@@ -770,6 +772,7 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Total Billing Amount",
+ "options": "currency",
"print_hide": 1,
"read_only": 1
},
@@ -1951,6 +1954,12 @@
"label": "Set Target Warehouse",
"options": "Warehouse"
},
+ {
+ "default": "0",
+ "fieldname": "is_debit_note",
+ "fieldtype": "Check",
+ "label": "Is Debit Note"
+ },
{
"default": "0",
"depends_on": "grand_total",
@@ -1969,7 +1978,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-04-15 23:57:58.766651",
+ "modified": "2021-05-20 22:48:33.988881",
"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 bb74a02606..023f4b049c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -125,6 +125,8 @@ class SalesInvoice(SellingController):
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
if not self.is_return:
self.validate_serial_numbers()
+ else:
+ self.timesheets = []
self.update_packing_list()
self.set_billing_hours_and_amount()
self.update_timesheet_billing_for_project()
@@ -337,7 +339,7 @@ class SalesInvoice(SellingController):
if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_cancel")
-
+ self.unlink_sales_invoice_from_timesheets()
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_status_updater_args(self):
@@ -393,6 +395,18 @@ class SalesInvoice(SellingController):
if validate_against_credit_limit:
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
+ def unlink_sales_invoice_from_timesheets(self):
+ for row in self.timesheets:
+ timesheet = frappe.get_doc('Timesheet', row.time_sheet)
+ for time_log in timesheet.time_logs:
+ if time_log.sales_invoice == self.name:
+ time_log.sales_invoice = None
+ timesheet.calculate_total_amounts()
+ timesheet.calculate_percentage_billed()
+ timesheet.flags.ignore_validate_update_after_submit = True
+ timesheet.set_status()
+ timesheet.db_update_all()
+
@frappe.whitelist()
def set_missing_values(self, for_validate=False):
pos = self.set_pos_fields(for_validate)
@@ -427,7 +441,7 @@ class SalesInvoice(SellingController):
timesheet.calculate_percentage_billed()
timesheet.flags.ignore_validate_update_after_submit = True
timesheet.set_status()
- timesheet.save()
+ timesheet.db_update_all()
def update_time_sheet_detail(self, timesheet, args, sales_invoice):
for data in timesheet.time_logs:
@@ -741,8 +755,10 @@ class SalesInvoice(SellingController):
self.append('timesheets', {
'time_sheet': data.parent,
'billing_hours': data.billing_hours,
- 'billing_amount': data.billing_amt,
- 'timesheet_detail': data.name
+ 'billing_amount': data.billing_amount,
+ 'timesheet_detail': data.name,
+ 'activity_type': data.activity_type,
+ 'description': data.description
})
self.calculate_billing_amount_for_timesheet()
@@ -1121,7 +1137,6 @@ class SalesInvoice(SellingController):
"""
self.set_serial_no_against_delivery_note()
self.validate_serial_against_delivery_note()
- self.validate_serial_against_sales_invoice()
def set_serial_no_against_delivery_note(self):
for item in self.items:
@@ -1152,26 +1167,6 @@ class SalesInvoice(SellingController):
frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
item.idx, item.qty, item.item_code, len(si_serial_nos)))
- def validate_serial_against_sales_invoice(self):
- """ check if serial number is already used in other sales invoice """
- for item in self.items:
- if not item.serial_no:
- continue
-
- for serial_no in item.serial_no.split("\n"):
- serial_no_details = frappe.db.get_value("Serial No", serial_no,
- ["sales_invoice", "item_code"], as_dict=1)
-
- if not serial_no_details:
- continue
-
- if serial_no_details.sales_invoice and serial_no_details.item_code == item.item_code \
- and self.name != serial_no_details.sales_invoice:
- sales_invoice_company = frappe.db.get_value("Sales Invoice", serial_no_details.sales_invoice, "company")
- if sales_invoice_company == self.company:
- frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}")
- .format(serial_no, serial_no_details.sales_invoice))
-
def update_project(self):
if self.project:
project = frappe.get_doc("Project", self.project)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 9059d0b040..df6d483904 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -933,12 +933,6 @@ class TestSalesInvoice(unittest.TestCase):
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"))
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0],
"delivery_document_no"), si.name)
- self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice"),
- si.name)
-
- # check if the serial number is already linked with any other Sales Invoice
- _si = frappe.copy_doc(si.as_dict())
- self.assertRaises(frappe.ValidationError, _si.insert)
return si
diff --git a/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json b/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json
index f7b9aef96c..f069e8dd0b 100644
--- a/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json
+++ b/erpnext/accounts/doctype/sales_invoice_timesheet/sales_invoice_timesheet.json
@@ -1,172 +1,78 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-06-14 19:21:34.321662",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2016-06-14 19:21:34.321662",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "activity_type",
+ "description",
+ "billing_hours",
+ "billing_amount",
+ "time_sheet",
+ "timesheet_detail"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "time_sheet",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Time Sheet",
- "length": 0,
- "no_copy": 0,
- "options": "Timesheet",
- "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": "time_sheet",
+ "fieldtype": "Link",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Time Sheet",
+ "options": "Timesheet",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "billing_hours",
- "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": "Billing Hours",
- "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": "billing_hours",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Billing Hours",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "billing_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": "Billing 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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "billing_amount",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Billing Amount",
+ "options": "currency",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "timesheet_detail",
- "fieldtype": "Data",
- "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": "Timesheet Detail",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "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
+ "allow_on_submit": 1,
+ "fieldname": "timesheet_detail",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Timesheet Detail",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "activity_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Activity Type",
+ "options": "Activity Type",
+ "read_only": 1
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Small Text",
+ "in_list_view": 1,
+ "label": "Description",
+ "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": "2019-02-18 18:50:44.770361",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Sales Invoice Timesheet",
- "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
+ ],
+ "istable": 1,
+ "links": [],
+ "modified": "2021-05-20 22:33:57.234846",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Sales Invoice Timesheet",
+ "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/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py
index fe2bc725e0..ff87276a87 100644
--- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py
@@ -165,7 +165,7 @@ def add_data_for_operating_activities(
if profit_data:
profit_data.update({
"indent": 1,
- "parent_account": get_mapper_for(light_mappers, position=0)['section_header']
+ "parent_account": get_mapper_for(light_mappers, position=1)['section_header']
})
data.append(profit_data)
section_data.append(profit_data)
@@ -312,10 +312,10 @@ def add_data_for_other_activities(
def compute_data(filters, company_currency, profit_data, period_list, light_mappers, full_mapper):
data = []
- operating_activities_mapper = get_mapper_for(light_mappers, position=0)
+ operating_activities_mapper = get_mapper_for(light_mappers, position=1)
other_mappers = [
- get_mapper_for(light_mappers, position=1),
- get_mapper_for(light_mappers, position=2)
+ get_mapper_for(light_mappers, position=2),
+ get_mapper_for(light_mappers, position=3)
]
if operating_activities_mapper:
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/__init__.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
new file mode 100644
index 0000000000..6a0394861b
--- /dev/null
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
@@ -0,0 +1,81 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.require("assets/erpnext/js/financial_statements.js", function() {
+ frappe.query_reports["Dimension-wise Accounts Balance Report"] = {
+ "filters": [
+ {
+ "fieldname": "company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
+ },
+ {
+ "fieldname": "fiscal_year",
+ "label": __("Fiscal Year"),
+ "fieldtype": "Link",
+ "options": "Fiscal Year",
+ "default": frappe.defaults.get_user_default("fiscal_year"),
+ "reqd": 1,
+ "on_change": function(query_report) {
+ var fiscal_year = query_report.get_values().fiscal_year;
+ if (!fiscal_year) {
+ return;
+ }
+ frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
+ var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
+ frappe.query_report.set_filter_value({
+ from_date: fy.year_start_date,
+ to_date: fy.year_end_date
+ });
+ });
+ }
+ },
+ {
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "fieldtype": "Date",
+ "default": frappe.defaults.get_user_default("year_start_date"),
+ },
+ {
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "default": frappe.defaults.get_user_default("year_end_date"),
+ },
+ {
+ "fieldname": "finance_book",
+ "label": __("Finance Book"),
+ "fieldtype": "Link",
+ "options": "Finance Book",
+ },
+ {
+ "fieldname": "dimension",
+ "label": __("Select Dimension"),
+ "fieldtype": "Select",
+ "options": get_accounting_dimension_options(),
+ "reqd": 1,
+ },
+ ],
+ "formatter": erpnext.financial_statements.formatter,
+ "tree": true,
+ "name_field": "account",
+ "parent_field": "parent_account",
+ "initial_depth": 3
+ }
+
+});
+
+function get_accounting_dimension_options() {
+ let options =["", "Cost Center", "Project"];
+ frappe.db.get_list('Accounting Dimension',
+ {fields:['document_type']}).then((res) => {
+ res.forEach((dimension) => {
+ options.push(dimension.document_type);
+ });
+ });
+ return options
+}
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.json b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.json
new file mode 100644
index 0000000000..6141944f9d
--- /dev/null
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.json
@@ -0,0 +1,22 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-04-09 16:48:59.548018",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-04-09 16:48:59.548018",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Dimension-wise Accounts Balance Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "GL Entry",
+ "report_name": "Dimension-wise Accounts Balance Report",
+ "report_type": "Script Report",
+ "roles": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
new file mode 100644
index 0000000000..de7ed4926e
--- /dev/null
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
@@ -0,0 +1,213 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe, erpnext
+from frappe import _
+from frappe.utils import (flt, cstr)
+
+from erpnext.accounts.report.financial_statements import filter_accounts, filter_out_zero_value_rows
+from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
+
+from six import itervalues
+
+def execute(filters=None):
+ validate_filters(filters)
+ dimension_items_list = get_dimension_items_list(filters.dimension, filters.company)
+
+ if not dimension_items_list:
+ return [], []
+
+ dimension_items_list = [''.join(d) for d in dimension_items_list]
+ columns = get_columns(dimension_items_list)
+ data = get_data(filters, dimension_items_list)
+
+ return columns, data
+
+def get_data(filters, dimension_items_list):
+ company_currency = erpnext.get_company_currency(filters.company)
+ acc = frappe.db.sql("""
+ select
+ name, account_number, parent_account, lft, rgt, root_type,
+ report_type, account_name, include_in_gross, account_type, is_group
+ from
+ `tabAccount`
+ where
+ company=%s
+ order by lft""", (filters.company), as_dict=True)
+
+ if not acc:
+ return None
+
+ accounts, accounts_by_name, parent_children_map = filter_accounts(acc)
+
+ min_lft, max_rgt = frappe.db.sql("""select min(lft), max(rgt) from `tabAccount`
+ where company=%s""", (filters.company))[0]
+
+ account = frappe.db.sql_list("""select name from `tabAccount`
+ where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company))
+
+ gl_entries_by_account = {}
+ set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account)
+ format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list)
+ accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list)
+ out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list)
+ out = filter_out_zero_value_rows(out, parent_children_map)
+
+ return out
+
+def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account):
+ for item in dimension_items_list:
+ condition = get_condition(filters.from_date, item, filters.dimension)
+ if account:
+ condition += " and account in ({})"\
+ .format(", ".join([frappe.db.escape(d) for d in account]))
+
+ gl_filters = {
+ "company": filters.get("company"),
+ "from_date": filters.get("from_date"),
+ "to_date": filters.get("to_date"),
+ "finance_book": cstr(filters.get("finance_book"))
+ }
+
+ gl_filters['item'] = ''.join(item)
+
+ if filters.get("include_default_book_entries"):
+ gl_filters["company_fb"] = frappe.db.get_value("Company",
+ filters.company, 'default_finance_book')
+
+ for key, value in filters.items():
+ if value:
+ gl_filters.update({
+ key: value
+ })
+
+ gl_entries = frappe.db.sql("""
+ select
+ posting_date, account, debit, credit, is_opening, fiscal_year,
+ debit_in_account_currency, credit_in_account_currency, account_currency
+ from
+ `tabGL Entry`
+ where
+ company=%(company)s
+ {condition}
+ and posting_date <= %(to_date)s
+ and is_cancelled = 0
+ order by account, posting_date""".format(
+ condition=condition),
+ gl_filters, as_dict=True) #nosec
+
+ for entry in gl_entries:
+ entry['dimension_item'] = ''.join(item)
+ gl_entries_by_account.setdefault(entry.account, []).append(entry)
+
+def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list):
+
+ for entries in itervalues(gl_entries_by_account):
+ for entry in entries:
+ d = accounts_by_name.get(entry.account)
+ if not d:
+ frappe.msgprint(
+ _("Could not retrieve information for {0}.").format(entry.account), title="Error",
+ raise_exception=1
+ )
+ for item in dimension_items_list:
+ if item == entry.dimension_item:
+ d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit)
+
+def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list):
+ data = []
+
+ for d in accounts:
+ has_value = False
+ total = 0
+ row = {
+ "account": d.name,
+ "parent_account": d.parent_account,
+ "indent": d.indent,
+ "from_date": filters.from_date,
+ "to_date": filters.to_date,
+ "currency": company_currency,
+ "account_name": ('{} - {}'.format(d.account_number, d.account_name)
+ if d.account_number else d.account_name)
+ }
+
+ for item in dimension_items_list:
+ row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3)
+
+ if abs(row[frappe.scrub(item)]) >= 0.005:
+ # ignore zero values
+ has_value = True
+ total += flt(d.get(frappe.scrub(item), 0.0), 3)
+
+ row["has_value"] = has_value
+ row["total"] = total
+ data.append(row)
+
+ return data
+
+def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list):
+ """accumulate children's values in parent accounts"""
+ for d in reversed(accounts):
+ if d.parent_account:
+ for item in dimension_items_list:
+ accounts_by_name[d.parent_account][frappe.scrub(item)] = \
+ accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0)
+
+def get_condition(from_date, item, dimension):
+ conditions = []
+
+ if from_date:
+ conditions.append("posting_date >= %(from_date)s")
+ if dimension:
+ if dimension not in ['Cost Center', 'Project']:
+ if dimension in ['Customer', 'Supplier']:
+ dimension = 'Party'
+ else:
+ dimension = 'Voucher No'
+ txt = "{0} = %(item)s".format(frappe.scrub(dimension))
+ conditions.append(txt)
+
+ return " and {}".format(" and ".join(conditions)) if conditions else ""
+
+def get_dimension_items_list(dimension, company):
+ meta = frappe.get_meta(dimension, cached=False)
+ fieldnames = [d.fieldname for d in meta.get("fields")]
+ filters = {}
+ if 'company' in fieldnames:
+ filters['company'] = company
+ return frappe.get_all(dimension, filters, as_list=True)
+
+def get_columns(dimension_items_list, accumulated_values=1, company=None):
+ columns = [{
+ "fieldname": "account",
+ "label": _("Account"),
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 300
+ }]
+ if company:
+ columns.append({
+ "fieldname": "currency",
+ "label": _("Currency"),
+ "fieldtype": "Link",
+ "options": "Currency",
+ "hidden": 1
+ })
+ for item in dimension_items_list:
+ columns.append({
+ "fieldname": frappe.scrub(item),
+ "label": item,
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 150
+ })
+ columns.append({
+ "fieldname": "total",
+ "label": "Total",
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 150
+ })
+
+ return columns
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index fb0d359926..84f786814d 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -166,6 +166,11 @@ frappe.query_reports["General Ledger"] = {
"fieldname": "show_cancelled_entries",
"label": __("Show Cancelled Entries"),
"fieldtype": "Check"
+ },
+ {
+ "fieldname": "show_net_values_in_party_account",
+ "label": __("Show Net Values in Party Account"),
+ "fieldtype": "Check"
}
]
}
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index b5d7992604..562df4f6f7 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -344,6 +344,9 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
consolidated_gle = OrderedDict()
group_by = group_by_field(filters.get('group_by'))
+ if filters.get('show_net_values_in_party_account'):
+ account_type_map = get_account_type_map(filters.get('company'))
+
def update_value_in_dict(data, key, gle):
data[key].debit += flt(gle.debit)
data[key].credit += flt(gle.credit)
@@ -351,6 +354,24 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
data[key].debit_in_account_currency += flt(gle.debit_in_account_currency)
data[key].credit_in_account_currency += flt(gle.credit_in_account_currency)
+ if filters.get('show_net_values_in_party_account') and \
+ account_type_map.get(data[key].account) in ('Receivable', 'Payable'):
+ net_value = flt(data[key].debit) - flt(data[key].credit)
+ net_value_in_account_currency = flt(data[key].debit_in_account_currency) \
+ - flt(data[key].credit_in_account_currency)
+
+ if net_value < 0:
+ dr_or_cr = 'credit'
+ rev_dr_or_cr = 'debit'
+ else:
+ dr_or_cr = 'debit'
+ rev_dr_or_cr = 'credit'
+
+ data[key][dr_or_cr] = abs(net_value)
+ data[key][dr_or_cr+'_in_account_currency'] = abs(net_value_in_account_currency)
+ data[key][rev_dr_or_cr] = 0
+ data[key][rev_dr_or_cr+'_in_account_currency'] = 0
+
if data[key].against_voucher and gle.against_voucher:
data[key].against_voucher += ', ' + gle.against_voucher
@@ -388,6 +409,12 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
return totals, entries
+def get_account_type_map(company):
+ account_type_map = frappe._dict(frappe.get_all('Account', fields=['name', 'account_type'],
+ filters={'company': company}, as_list=1))
+
+ return account_type_map
+
def get_result_as_list(data, filters):
balance, balance_in_account_currency = 0, 0
inv_details = get_supplier_invoice_details()
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index 9de8d19f2a..b020d0a506 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -81,8 +81,7 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
presentation_currency = currency_info['presentation_currency']
company_currency = currency_info['company_currency']
- pl_accounts = [d.name for d in frappe.get_list('Account',
- filters={'report_type': 'Profit and Loss', 'company': company})]
+ account_currencies = list(set(entry['account_currency'] for entry in gl_entries))
for entry in gl_entries:
account = entry['account']
@@ -92,10 +91,15 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
credit_in_account_currency = flt(entry['credit_in_account_currency'])
account_currency = entry['account_currency']
- if account_currency != presentation_currency:
- value = debit or credit
+ if len(account_currencies) == 1 and account_currency == presentation_currency:
+ if entry.get('debit'):
+ entry['debit'] = debit_in_account_currency
- date = entry['posting_date'] if account in pl_accounts else currency_info['report_date']
+ if entry.get('credit'):
+ entry['credit'] = credit_in_account_currency
+ else:
+ value = debit or credit
+ date = currency_info['report_date']
converted_value = convert(value, presentation_currency, company_currency, date)
if entry.get('debit'):
@@ -104,13 +108,6 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
if entry.get('credit'):
entry['credit'] = converted_value
- elif account_currency == presentation_currency:
- if entry.get('debit'):
- entry['debit'] = debit_in_account_currency
-
- if entry.get('credit'):
- entry['credit'] = credit_in_account_currency
-
converted_gl_list.append(entry)
return converted_gl_list
diff --git a/erpnext/assets/doctype/asset_category/asset_category.js b/erpnext/assets/doctype/asset_category/asset_category.js
index 74963c2aa9..51ce157a81 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.js
+++ b/erpnext/assets/doctype/asset_category/asset_category.js
@@ -4,7 +4,7 @@
frappe.ui.form.on('Asset Category', {
onload: function(frm) {
frm.add_fetch('company_name', 'accumulated_depreciation_account', 'accumulated_depreciation_account');
- frm.add_fetch('company_name', 'depreciation_expense_account', 'accumulated_depreciation_account');
+ frm.add_fetch('company_name', 'depreciation_expense_account', 'depreciation_expense_account');
frm.set_query('fixed_asset_account', 'accounts', function(doc, cdt, cdn) {
var d = locals[cdt][cdn];
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 b530d1ab24..180ba93666 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -62,6 +62,7 @@ class RequestforQuotation(BuyingController):
for supplier in self.suppliers:
supplier.email_sent = 0
supplier.quote_status = 'Pending'
+ self.send_to_supplier()
def on_cancel(self):
frappe.db.set(self, 'status', 'Cancelled')
@@ -81,7 +82,7 @@ class RequestforQuotation(BuyingController):
def send_to_supplier(self):
"""Sends RFQ mail to involved suppliers."""
for rfq_supplier in self.suppliers:
- if rfq_supplier.send_email:
+ if rfq_supplier.email_id is not None and rfq_supplier.send_email:
self.validate_email_id(rfq_supplier)
# make new user if required
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 4cc5753cbd..38b8dfdf48 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -383,8 +383,14 @@
"icon": "fa fa-user",
"idx": 370,
"image_field": "image",
- "links": [],
- "modified": "2021-01-06 19:51:40.939087",
+ "links": [
+ {
+ "group": "Item Group",
+ "link_doctype": "Supplier Item Group",
+ "link_fieldname": "supplier"
+ }
+ ],
+ "modified": "2021-05-18 15:10:11.087191",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
diff --git a/erpnext/buying/doctype/supplier_item_group/__init__.py b/erpnext/buying/doctype/supplier_item_group/__init__.py
new file mode 100644
index 0000000000..e69de29bb2
diff --git a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.js b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.js
new file mode 100644
index 0000000000..f7da90d98d
--- /dev/null
+++ b/erpnext/buying/doctype/supplier_item_group/supplier_item_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('Supplier Item Group', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.json b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.json
new file mode 100644
index 0000000000..1971458f61
--- /dev/null
+++ b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.json
@@ -0,0 +1,77 @@
+{
+ "actions": [],
+ "creation": "2021-05-07 18:16:40.621421",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "supplier",
+ "item_group"
+ ],
+ "fields": [
+ {
+ "fieldname": "supplier",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Supplier",
+ "options": "Supplier",
+ "reqd": 1
+ },
+ {
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-05-19 13:48:16.742303",
+ "modified_by": "Administrator",
+ "module": "Buying",
+ "name": "Supplier Item 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": "Purchase User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Purchase Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py
new file mode 100644
index 0000000000..3a2e5d6dce
--- /dev/null
+++ b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py
@@ -0,0 +1,18 @@
+# -*- 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
+
+class SupplierItemGroup(Document):
+ def validate(self):
+ exists = frappe.db.exists({
+ 'doctype': 'Supplier Item Group',
+ 'supplier': self.supplier,
+ 'item_group': self.item_group
+ })
+ if exists:
+ frappe.throw(_("Item Group has already been linked to this supplier."))
\ No newline at end of file
diff --git a/erpnext/buying/doctype/supplier_item_group/test_supplier_item_group.py b/erpnext/buying/doctype/supplier_item_group/test_supplier_item_group.py
new file mode 100644
index 0000000000..c75044d44e
--- /dev/null
+++ b/erpnext/buying/doctype/supplier_item_group/test_supplier_item_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 TestSupplierItemGroup(unittest.TestCase):
+ pass
diff --git a/erpnext/change_log/v13/v13_4_0.md b/erpnext/change_log/v13/v13_4_0.md
new file mode 100644
index 0000000000..eaf4f762d4
--- /dev/null
+++ b/erpnext/change_log/v13/v13_4_0.md
@@ -0,0 +1,54 @@
+# Version 13.4.0 Release Notes
+
+### Features & Enhancements
+
+- Multiple GST enhancement and fixes ([#25249](https://github.com/frappe/erpnext/pull/25249))
+- Linking supplier with an item group for filtering items ([#25683](https://github.com/frappe/erpnext/pull/25683))
+- Leave Policy Assignment Refactor ([#24327](https://github.com/frappe/erpnext/pull/24327))
+- Dimension-wise Accounts Balance Report ([#25260](https://github.com/frappe/erpnext/pull/25260))
+- Show net values in Party Accounts ([#25714](https://github.com/frappe/erpnext/pull/25714))
+- Add pending qty section to batch/serial selector dialog ([#25519](https://github.com/frappe/erpnext/pull/25519))
+- enhancements in Training Event ([#25782](https://github.com/frappe/erpnext/pull/25782))
+- Refactored timesheet ([#25701](https://github.com/frappe/erpnext/pull/25701))
+
+### Fixes
+
+- Process Statement of Accounts formatting ([#25777](https://github.com/frappe/erpnext/pull/25777))
+- Removed serial no validation for sales invoice ([#25817](https://github.com/frappe/erpnext/pull/25817))
+- Fetch email id from dialog box in pos past order summary ([#25808](https://github.com/frappe/erpnext/pull/25808))
+- Don't map set warehouse from delivery note to purchase receipt ([#25672](https://github.com/frappe/erpnext/pull/25672))
+- Apply permission while selecting projects ([#25765](https://github.com/frappe/erpnext/pull/25765))
+- Error on adding bank account to plaid ([#25658](https://github.com/frappe/erpnext/pull/25658))
+- Set disable rounded total if it is globally enabled ([#25789](https://github.com/frappe/erpnext/pull/25789))
+- Wrong amount on CR side in general ledger report for customer when different account currencies are involved ([#25654](https://github.com/frappe/erpnext/pull/25654))
+- Stock move dialog duplicate submit actions (V13) ([#25486](https://github.com/frappe/erpnext/pull/25486))
+- Cashflow mapper not showing data ([#25815](https://github.com/frappe/erpnext/pull/25815))
+- Ignore rounding diff while importing JV using data import ([#25816](https://github.com/frappe/erpnext/pull/25816))
+- Woocommerce order sync issue ([#25688](https://github.com/frappe/erpnext/pull/25688))
+- Expected amount in pos closing payments table ([#25737](https://github.com/frappe/erpnext/pull/25737))
+- Show only company addresses for ITC reversal entry ([#25867](https://github.com/frappe/erpnext/pull/25867))
+- Timeout error while loading warehouse tree ([#25694](https://github.com/frappe/erpnext/pull/25694))
+- Plaid Withdrawals and Deposits are recorded incorrectly ([#25784](https://github.com/frappe/erpnext/pull/25784))
+- Return case for item with available qty equal to one ([#25760](https://github.com/frappe/erpnext/pull/25760))
+- The status of repost item valuation showing In Progress since long time ([#25754](https://github.com/frappe/erpnext/pull/25754))
+- Updated applicable charges form in landed cost voucher ([#25732](https://github.com/frappe/erpnext/pull/25732))
+- Rearrange buttons for Company DocType ([#25617](https://github.com/frappe/erpnext/pull/25617))
+- Show uom for item in selector dialog ([#25697](https://github.com/frappe/erpnext/pull/25697))
+- Warehouse not found in stock entry ([#25776](https://github.com/frappe/erpnext/pull/25776))
+- Use dictionary filter instead of list (bp #25874 pre-release) ([#25875](https://github.com/frappe/erpnext/pull/25875))
+- Send emails on rfq submit ([#25695](https://github.com/frappe/erpnext/pull/25695))
+- Cannot bypass e-invoicing for non gst item invoices ([#25759](https://github.com/frappe/erpnext/pull/25759))
+- Validation message of quality inspection in purchase receipt ([#25666](https://github.com/frappe/erpnext/pull/25666))
+- Dialog variable assignment after definition in POS ([#25681](https://github.com/frappe/erpnext/pull/25681))
+- Wrong quantity after transaction for parallel stock transactions ([#25779](https://github.com/frappe/erpnext/pull/25779))
+- Item Variant Details Report ([#25797](https://github.com/frappe/erpnext/pull/25797))
+- Duplicate stock entry on multiple click ([#25742](https://github.com/frappe/erpnext/pull/25742))
+- Bank statement import via google sheet ([#25676](https://github.com/frappe/erpnext/pull/25676))
+- Change today to now to get data for reposting ([#25702](https://github.com/frappe/erpnext/pull/25702))
+- Parameter for get_filtered_list_for_consolidated_report in consolidated balance sheet ([#25698](https://github.com/frappe/erpnext/pull/25698))
+- Ageing error in PSOA ([#25857](https://github.com/frappe/erpnext/pull/25857))
+- Breaking cost center validation ([#25660](https://github.com/frappe/erpnext/pull/25660))
+- Project filter for Kanban Board ([#25744](https://github.com/frappe/erpnext/pull/25744))
+- Show allow zero valuation only when auto checked ([#25778](https://github.com/frappe/erpnext/pull/25778))
+- Missing cost center message on creating gl entries ([#25755](https://github.com/frappe/erpnext/pull/25755))
+- Address template with upper filter throws jinja error ([#25756](https://github.com/frappe/erpnext/pull/25756))
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index c409850734..544e624725 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -368,6 +368,11 @@ class AccountsController(TransactionBase):
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
+ # Double check for cost center
+ # Items add via promotional scheme may not have cost center set
+ if hasattr(item, 'cost_center') and not item.get('cost_center'):
+ item.set('cost_center', self.get('cost_center') or erpnext.get_default_cost_center(self.company))
+
if ret.get("pricing_rules"):
self.apply_pricing_rule_on_items(item, ret)
self.set_pricing_rule_details(item, ret)
@@ -1006,7 +1011,7 @@ class AccountsController(TransactionBase):
else:
grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
- print(grand_total, base_grand_total)
+
if total != flt(grand_total, self.precision("grand_total")) or \
base_total != flt(base_grand_total, self.precision("base_grand_total")):
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index b31724fa48..46301b7587 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -204,7 +204,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if "description" in searchfields:
searchfields.remove("description")
-
+
columns = ''
extra_searchfields = [field for field in searchfields
if not field in ["name", "item_group", "description"]]
@@ -216,11 +216,22 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
if not field in searchfields]
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
+ if filters.get('supplier'):
+ item_group_list = frappe.get_all('Supplier Item Group', filters = {'supplier': filters.get('supplier')}, fields = ['item_group'])
+
+ item_groups = []
+ for i in item_group_list:
+ item_groups.append(i.item_group)
+
+ del filters['supplier']
+
+ if item_groups:
+ filters['item_group'] = ['in', item_groups]
+
description_cond = ''
if frappe.db.count('Item', cache=True) < 50000:
# scan description only if items are less than 50000
description_cond = 'or tabItem.description LIKE %(txt)s'
-
return frappe.db.sql("""select tabItem.name,
if(length(tabItem.item_name) > 40,
concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name,
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index ed3aee5c1a..83d4c33140 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -76,12 +76,12 @@ status_map = {
["Stopped", "eval:self.status == 'Stopped'"],
["Cancelled", "eval:self.docstatus == 2"],
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
- ["Partially Ordered", "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1"],
["Ordered", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
["Transferred", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'"],
["Issued", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Issue'"],
["Received", "eval:self.status != 'Stopped' and self.per_received == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
["Partially Received", "eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
+ ["Partially Ordered", "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1"],
["Manufactured", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'"]
],
"Bank Transaction": [
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py
index f713684d37..7fd3b34fd5 100755
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py
+++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py
@@ -7,6 +7,7 @@
from __future__ import unicode_literals
import urllib
+from urllib.parse import quote
import hashlib
import hmac
import base64
@@ -68,8 +69,9 @@ def calc_md5(string):
"""
md = hashlib.md5()
md.update(string)
- return base64.encodestring(md.digest()).strip('\n') if six.PY2 \
- else base64.encodebytes(md.digest()).decode().strip()
+ return base64.encodebytes(md.digest()).decode().strip()
+
+
def remove_empty(d):
"""
@@ -177,7 +179,6 @@ class MWS(object):
'SignatureMethod': 'HmacSHA256',
}
params.update(extra_data)
- quote = urllib.quote if six.PY2 else urllib.parse.quote
request_description = '&'.join(['%s=%s' % (k, quote(params[k], safe='-_.~')) for k in sorted(params)])
signature = self.calc_signature(method, request_description)
url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, quote(signature))
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 16c65733f0..ce15e47c5e 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -90,9 +90,9 @@ def add_bank_accounts(response, bank, company):
"bank": bank["bank_name"],
"account": default_gl_account.account,
"account_name": account["name"],
- "account_type": account["type"] or "",
- "account_subtype": account["subtype"] or "",
- "mask": account["mask"] or "",
+ "account_type": account.get("type", ""),
+ "account_subtype": account.get("subtype", ""),
+ "mask": account.get("mask", ""),
"integration_id": account["id"],
"is_company_account": 1,
"company": company
@@ -183,11 +183,11 @@ def new_bank_transaction(transaction):
bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"]))
if float(transaction["amount"]) >= 0:
- debit = float(transaction["amount"])
- credit = 0
- else:
debit = 0
- credit = abs(float(transaction["amount"]))
+ credit = float(transaction["amount"])
+ else:
+ debit = abs(float(transaction["amount"]))
+ credit = 0
status = "Pending" if transaction["pending"] == "True" else "Settled"
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 9d1ce9bbbf..55169dffba 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -268,10 +268,12 @@ doc_events = {
},
"Purchase Invoice": {
"validate": [
- "erpnext.regional.india.utils.update_grand_total_for_rcm",
+ "erpnext.regional.india.utils.validate_reverse_charge_transaction",
+ "erpnext.regional.india.utils.update_itc_availed_fields",
"erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm",
- "erpnext.regional.united_arab_emirates.utils.validate_returns"
- ]
+ "erpnext.regional.united_arab_emirates.utils.validate_returns",
+ "erpnext.regional.india.utils.update_taxable_values"
+ ]
},
"Payment Entry": {
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
@@ -365,10 +367,8 @@ scheduler_events = {
"erpnext.setup.doctype.email_digest.email_digest.send",
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
- "erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy",
"erpnext.hr.utils.generate_leave_encashment",
"erpnext.hr.utils.allocate_earned_leaves",
- "erpnext.hr.utils.grant_leaves_automatically",
"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"
@@ -425,7 +425,6 @@ regional_overrides = {
'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',
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount'
},
diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py
index 0203332164..285374d9f6 100644
--- a/erpnext/hr/doctype/employee/employee_dashboard.py
+++ b/erpnext/hr/doctype/employee/employee_dashboard.py
@@ -11,8 +11,12 @@ def get_data():
},
'transactions': [
{
- 'label': _('Leave and Attendance'),
- 'items': ['Attendance', 'Attendance Request', 'Leave Application', 'Leave Allocation', 'Employee Checkin']
+ 'label': _('Attendance'),
+ 'items': ['Attendance', 'Attendance Request', 'Employee Checkin']
+ },
+ {
+ 'label': _('Leave'),
+ 'items': ['Leave Application', 'Leave Allocation', 'Leave Policy Assignment']
},
{
'label': _('Lifecycle'),
@@ -30,10 +34,6 @@ def get_data():
'label': _('Benefit'),
'items': ['Employee Benefit Application', 'Employee Benefit Claim']
},
- {
- 'label': _('Evaluation'),
- 'items': ['Appraisal']
- },
{
'label': _('Payroll'),
'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus', 'Bank Account']
@@ -42,5 +42,9 @@ def get_data():
'label': _('Training'),
'items': ['Training Event', 'Training Result', 'Training Feedback', 'Employee Skill Map']
},
+ {
+ 'label': _('Evaluation'),
+ 'items': ['Appraisal']
+ },
]
}
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 3db6c239ef..2396a8eee9 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -23,7 +23,6 @@
"show_leaves_of_all_department_members_in_calendar",
"auto_leave_encashment",
"restrict_backdated_leave_application",
- "automatically_allocate_leaves_based_on_leave_policy",
"hiring_settings",
"check_vacancies"
],
@@ -133,12 +132,6 @@
"label": "Role Allowed to Create Backdated Leave Application",
"options": "Role"
},
- {
- "default": "0",
- "fieldname": "automatically_allocate_leaves_based_on_leave_policy",
- "fieldtype": "Check",
- "label": "Automatically Allocate Leaves Based On Leave Policy"
- },
{
"default": "1",
"fieldname": "send_leave_notification",
@@ -155,7 +148,7 @@
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2021-04-26 10:52:56.192773",
+ "modified": "2021-05-11 10:52:56.192773",
"modified_by": "Administrator",
"module": "HR",
"name": "HR Settings",
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index a4a96b813e..2832e2fad3 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -446,8 +446,6 @@ class TestLeaveApplication(unittest.TestCase):
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
- frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
-
from erpnext.hr.utils import allocate_earned_leaves
i = 0
while(i<14):
diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
index e0ffa5dd41..c1da8b47ff 100644
--- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
@@ -44,10 +44,6 @@ class TestLeaveEncashment(unittest.TestCase):
salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee,
other_details={"leave_encashment_amount_per_day": 50})
- #grant Leaves
- frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
-
-
def tearDown(self):
for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]:
frappe.db.sql("delete from `tab%s`" % dt)
diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
index e0ec4be2dc..ff7f0422e0 100644
--- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
+++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
@@ -7,7 +7,7 @@ def get_data():
'transactions': [
{
'label': _('Leaves'),
- 'items': ['Leave Allocation']
+ 'items': ['Leave Policy Assignment', 'Leave Allocation']
},
]
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
index 7c32a0dde0..0aaf4cf616 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
@@ -4,35 +4,22 @@
frappe.ui.form.on('Leave Policy Assignment', {
onload: function(frm) {
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
- },
- refresh: function(frm) {
- if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) {
- frm.add_custom_button(__("Grant Leave"), function() {
-
- frappe.call({
- doc: frm.doc,
- method: "grant_leave_alloc_for_employee",
- callback: function(r) {
- let leave_allocations = r.message;
- let msg = frm.events.get_success_message(leave_allocations);
- frappe.msgprint(msg);
- cur_frm.refresh();
- }
- });
- });
- }
- },
-
- get_success_message: function(leave_allocations) {
- let msg = __("Leaves has been granted successfully");
- msg += "
";
- msg += ""+__('Leave Type')+" | "+__("Leave Allocation")+" | "+__("Leaves Granted")+" |
---|
";
- for (let key in leave_allocations) {
- msg += "
"+key+" | "+leave_allocations[key]["name"]+" | "+leave_allocations[key]["leaves"]+" |
";
- }
- msg += "
";
- return msg;
+ frm.set_query('leave_policy', function() {
+ return {
+ filters: {
+ "docstatus": 1
+ }
+ };
+ });
+ frm.set_query('leave_period', function() {
+ return {
+ filters: {
+ "is_active": 1,
+ "company": frm.doc.company
+ }
+ };
+ });
},
assignment_based_on: function(frm) {
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 462b81df1d..d7cb1c88c9 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -17,6 +17,9 @@ class LeavePolicyAssignment(Document):
self.validate_policy_assignment_overlap()
self.set_dates()
+ def on_submit(self):
+ self.grant_leave_alloc_for_employee()
+
def set_dates(self):
if self.assignment_based_on == "Leave Period":
self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"])
@@ -75,7 +78,7 @@ class LeavePolicyAssignment(Document):
from_date=self.effective_from,
to_date=self.effective_to,
new_leaves_allocated=new_leaves_allocated,
- leave_period=self.leave_period or None,
+ leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else '',
leave_policy_assignment = self.name,
leave_policy = self.leave_policy,
carry_forward=carry_forward
@@ -131,22 +134,6 @@ class LeavePolicyAssignment(Document):
return new_leaves_allocated
-@frappe.whitelist()
-def grant_leave_for_multiple_employees(leave_policy_assignments):
- leave_policy_assignments = json.loads(leave_policy_assignments)
- not_granted = []
- for assignment in leave_policy_assignments:
- try:
- frappe.get_doc("Leave Policy Assignment", assignment).grant_leave_alloc_for_employee()
- except Exception:
- not_granted.append(assignment)
-
- if len(not_granted):
- msg = _("Leave not Granted for Assignments:")+ bold(comma_and(not_granted)) + _(". Please Check documents")
- else:
- msg = _("Leave granted Successfully")
- frappe.msgprint(msg)
-
@frappe.whitelist()
def create_assignment_for_multiple_employees(employees, data):
@@ -166,29 +153,18 @@ def create_assignment_for_multiple_employees(employees, data):
assignment.effective_to = getdate(data.effective_to) or None
assignment.leave_period = data.leave_period or None
assignment.carry_forward = data.carry_forward
-
assignment.save()
- assignment.submit()
+ try:
+ assignment.submit()
+ except frappe.exceptions.ValidationError:
+ continue
+
+ frappe.db.commit()
+
docs_name.append(assignment.name)
+
return docs_name
-
-def automatically_allocate_leaves_based_on_leave_policy():
- today = getdate()
- automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_single_value(
- 'HR Settings', 'automatically_allocate_leaves_based_on_leave_policy'
- )
-
- pending_assignments = frappe.get_list(
- "Leave Policy Assignment",
- filters = {"docstatus": 1, "leaves_allocated": 0, "effective_from": today}
- )
-
- if len(pending_assignments) and automatically_allocate_leaves_based_on_leave_policy:
- for assignment in pending_assignments:
- frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
-
-
def get_leave_type_details():
leave_type_details = frappe._dict()
leave_types = frappe.get_all("Leave Type",
@@ -197,4 +173,3 @@ def get_leave_type_details():
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/leave_policy_assignment_dashboard.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py
new file mode 100644
index 0000000000..4bb0535cf8
--- /dev/null
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+from frappe import _
+
+def get_data():
+ return {
+ 'fieldname': 'leave_policy_assignment',
+ 'transactions': [
+ {
+ 'label': _('Leaves'),
+ 'items': ['Leave Allocation']
+ },
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
index 468f243885..8fe4b8f8ef 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
@@ -6,6 +6,7 @@ frappe.listview_settings['Leave Policy Assignment'] = {
doctype: "Employee",
target: cur_list,
setters: {
+ employee_name: '',
company: '',
department: '',
},
@@ -92,37 +93,6 @@ frappe.listview_settings['Leave Policy Assignment'] = {
}
});
});
-
- list_view.page.add_inner_button(__("Grant Leaves"), function () {
- me.dialog = new frappe.ui.form.MultiSelectDialog({
- doctype: "Leave Policy Assignment",
- target: cur_list,
- setters: {
- company: '',
- employee: '',
- },
- get_query() {
- return {
- filters: {
- docstatus: ['=', 1],
- leaves_allocated: ['=', 0]
- }
- };
- },
- add_filters_group: 1,
- primary_action_label: "Grant Leaves",
- action(leave_policy_assignments) {
- frappe.call({
- method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.grant_leave_for_multiple_employees',
- async: false,
- args: {
- leave_policy_assignments: leave_policy_assignments
- }
- });
- me.dialog.hide();
- }
- });
- });
},
set_effective_date: function () {
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 838e794795..9a14e3588d 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
@@ -35,7 +35,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
- leave_policy_assignment_doc.grant_leave_alloc_for_employee()
leave_policy_assignment_doc.reload()
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
@@ -73,7 +72,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
- leave_policy_assignment_doc.grant_leave_alloc_for_employee()
leave_policy_assignment_doc.reload()
diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js
index 12bc920b18..b7d34b178a 100644
--- a/erpnext/hr/doctype/training_event/training_event.js
+++ b/erpnext/hr/doctype/training_event/training_event.js
@@ -2,23 +2,41 @@
// For license information, please see license.txt
frappe.ui.form.on('Training Event', {
- onload_post_render: function(frm) {
+ onload_post_render: function (frm) {
frm.get_field("employees").grid.set_multiple_add("employee");
},
- refresh: function(frm) {
- if(!frm.doc.__islocal) {
- frm.add_custom_button(__("Training Result"), function() {
+ refresh: function (frm) {
+ if (!frm.doc.__islocal) {
+ frm.add_custom_button(__("Training Result"), function () {
frappe.route_options = {
training_event: frm.doc.name
- }
+ };
frappe.set_route("List", "Training Result");
});
- frm.add_custom_button(__("Training Feedback"), function() {
+ frm.add_custom_button(__("Training Feedback"), function () {
frappe.route_options = {
training_event: frm.doc.name
- }
+ };
frappe.set_route("List", "Training Feedback");
});
}
}
});
+
+frappe.ui.form.on("Training Event Employee", {
+ employee: function (frm) {
+ let emp = [];
+ for (let d in frm.doc.employees) {
+ if (frm.doc.employees[d].employee) {
+ emp.push(frm.doc.employees[d].employee);
+ }
+ }
+ frm.set_query("employee", "employees", function () {
+ return {
+ filters: {
+ name: ["NOT IN", emp]
+ }
+ };
+ });
+ }
+});
diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
index e3a40649b4..2d313e9fac 100644
--- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json
+++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
@@ -1,241 +1,80 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2016-08-08 05:33:39.965305",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
+ "actions": [],
+ "creation": "2016-08-08 05:33:39.965305",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "employee",
+ "employee_name",
+ "department",
+ "column_break_3",
+ "status",
+ "attendance",
+ "is_mandatory"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "employee",
- "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": "Employee",
- "length": 0,
- "no_copy": 0,
- "options": "Employee",
- "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": "employee",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Employee",
+ "options": "Employee"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.employee_name",
- "fieldname": "employee_name",
- "fieldtype": "Read Only",
- "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": "Employee Name",
- "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
- },
+ "fetch_from": "employee.employee_name",
+ "fieldname": "employee_name",
+ "fieldtype": "Read Only",
+ "label": "Employee Name"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "employee.department",
- "fieldname": "department",
- "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": "Department",
- "length": 0,
- "no_copy": 0,
- "options": "Department",
- "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
- },
+ "fetch_from": "employee.department",
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department",
+ "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_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": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Open",
- "fieldname": "status",
- "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": "Status",
- "length": 0,
- "no_copy": 1,
- "options": "Open\nInvited\nCompleted\nFeedback Submitted",
- "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_on_submit": 1,
+ "default": "Open",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Status",
+ "no_copy": 1,
+ "options": "Open\nInvited\nCompleted\nFeedback Submitted"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "attendance",
- "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": "Attendance",
- "length": 0,
- "no_copy": 0,
- "options": "Mandatory\nOptional",
- "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": "attendance",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Attendance",
+ "options": "Present\nAbsent"
+ },
+ {
+ "columns": 2,
+ "default": "1",
+ "fieldname": "is_mandatory",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Mandatory"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2019-01-30 11:28:16.170333",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Training Event Employee",
- "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": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-05-21 12:41:59.336237",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Training Event Employee",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
}
\ No newline at end of file
diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.json b/erpnext/hr/notification/training_scheduled/training_scheduled.json
index 966b887572..e49541e321 100644
--- a/erpnext/hr/notification/training_scheduled/training_scheduled.json
+++ b/erpnext/hr/notification/training_scheduled/training_scheduled.json
@@ -11,16 +11,18 @@
"event": "Submit",
"idx": 0,
"is_standard": 1,
- "message": "\n\n\n
\n \n | \n \n \n \n - {{ doc.introduction }}
\n - {{_(\"Event Location\")}}: {{ doc.location }}
\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n - {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
\n - \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
\n {% else %}\n - {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
\n - {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
\n {% endif %}\n \n {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}\n \n | \n | \n
\n
\n
",
- "modified": "2019-11-29 15:38:31.805409",
+ "message": "\n\n\n
\n \n | \n \n \n {{ doc.introduction }}\n \n - {{_(\"Event Location\")}}: {{ doc.location }}
\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n - {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
\n - \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
\n {% else %}\n - {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
\n - {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
\n {% endif %}\n - {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
\n {% if doc.is_mandatory %}\n - Note: This Training Event is mandatory
\n {% endif %}\n \n \n | \n | \n
\n
\n
",
+ "modified": "2021-05-24 16:29:13.165930",
"modified_by": "Administrator",
"module": "HR",
"name": "Training Scheduled",
"owner": "Administrator",
"recipients": [
{
- "email_by_document_field": "employee_emails"
+ "receiver_by_document_field": "employee_emails"
}
],
+ "send_system_notification": 0,
+ "send_to_all_assignees": 0,
"subject": "Training Scheduled: {{ doc.name }}"
}
\ No newline at end of file
diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.md b/erpnext/hr/notification/training_scheduled/training_scheduled.md
index 374038ac20..418fd4990e 100644
--- a/erpnext/hr/notification/training_scheduled/training_scheduled.md
+++ b/erpnext/hr/notification/training_scheduled/training_scheduled.md
@@ -35,6 +35,9 @@
{% endif %}
{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
+ {% if doc.is_mandatory %}
+ Note: This Training Event is mandatory
+ {% endif %}
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 2540b3db63..80189e87b7 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -500,13 +500,6 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co
total_claimed_amount = sum_of_claimed_amount[0].total_amount
return total_claimed_amount
-def grant_leaves_automatically():
- automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_singles_value("HR Settings", "automatically_allocate_leaves_based_on_leave_policy")
- if automatically_allocate_leaves_based_on_leave_policy:
- lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0})
- for assignment in lpa:
- frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
-
def share_doc_with_approver(doc, user):
# if approver does not have permissions, share
if not frappe.has_permission(doc=doc, ptype="submit", user=user):
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index a6086fb88d..3e5a72db9a 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -76,9 +76,9 @@ frappe.ui.form.on("Work Order", {
frm.set_query("production_item", function() {
return {
query: "erpnext.controllers.queries.item_query",
- filters:[
- ['is_stock_item', '=',1]
- ]
+ filters: {
+ "is_stock_item": 1,
+ }
};
});
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 82d223cada..1e8ce3c658 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -769,6 +769,7 @@ erpnext.patches.v13_0.rename_discharge_date_in_ip_record
erpnext.patches.v12_0.create_taxable_value_field
erpnext.patches.v12_0.add_gst_category_in_delivery_note
erpnext.patches.v12_0.purchase_receipt_status
+erpnext.patches.v12_0.create_itc_reversal_custom_fields
erpnext.patches.v13_0.fix_non_unique_represents_company
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021
@@ -778,3 +779,5 @@ erpnext.patches.v12_0.add_ewaybill_validity_field
erpnext.patches.v13_0.germany_make_custom_fields
erpnext.patches.v13_0.germany_fill_debtor_creditor_number
erpnext.patches.v13_0.set_pos_closing_as_failed
+erpnext.patches.v13_0.update_timesheet_changes
+erpnext.patches.v13_0.set_training_event_attendance
diff --git a/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py
new file mode 100644
index 0000000000..0078a53cd6
--- /dev/null
+++ b/erpnext/patches/v12_0/create_itc_reversal_custom_fields.py
@@ -0,0 +1,115 @@
+from __future__ import unicode_literals
+import frappe
+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 erpnext.regional.india.utils import get_gst_accounts
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name'])
+ if not company:
+ return
+
+ frappe.reload_doc("regional", "doctype", "gst_settings")
+ frappe.reload_doc("accounts", "doctype", "gst_account")
+
+ journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
+ make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
+
+ custom_fields = {
+ 'Journal Entry': [
+ dict(fieldname='reversal_type', label='Reversal Type',
+ fieldtype='Select', insert_after='voucher_type', print_hide=1,
+ options="As per rules 42 & 43 of CGST Rules\nOthers",
+ depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
+ mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
+ dict(fieldname='company_address', label='Company Address',
+ fieldtype='Link', options='Address', insert_after='reversal_type',
+ print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
+ mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
+ dict(fieldname='company_gstin', label='Company GSTIN',
+ fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1,
+ fetch_from='company_address.gstin',
+ depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
+ mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'")
+ ],
+ 'Purchase Invoice': [
+ dict(fieldname='eligibility_for_itc', label='Eligibility For ITC',
+ fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1,
+ options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC',
+ default="All Other ITC")
+ ],
+ 'Purchase Invoice Item': [
+ dict(fieldname='taxable_value', label='Taxable Value',
+ fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
+ print_hide=1)
+ ]
+ }
+
+ create_custom_fields(custom_fields, update=True)
+
+ # Patch ITC Availed fields from Data to Currency
+ # Patch Availed ITC for current fiscal_year
+
+ gst_accounts = get_gst_accounts(only_non_reverse_charge=1)
+
+ frappe.db.sql("""
+ UPDATE `tabCustom Field` SET fieldtype='Currency', options='Company:company:default_currency'
+ WHERE dt = 'Purchase Invoice' and fieldname in ('itc_integrated_tax', 'itc_state_tax', 'itc_central_tax',
+ 'itc_cess_amount')
+ """)
+
+ frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_integrated_tax = '0'
+ WHERE trim(coalesce(itc_integrated_tax, '')) = '' """)
+
+ frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_state_tax = '0'
+ WHERE trim(coalesce(itc_state_tax, '')) = '' """)
+
+ frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_central_tax = '0'
+ WHERE trim(coalesce(itc_central_tax, '')) = '' """)
+
+ frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_cess_amount = '0'
+ WHERE trim(coalesce(itc_cess_amount, '')) = '' """)
+
+ # Get purchase invoices
+ invoices = frappe.get_all('Purchase Invoice',
+ {'posting_date': ('>=', '2021-04-01'), 'eligibility_for_itc': ('!=', 'Ineligible')},
+ ['name'])
+
+ amount_map = {}
+
+ if invoices:
+ invoice_list = set([d.name for d in invoices])
+
+ # Get GST applied
+ amounts = frappe.db.sql("""
+ SELECT parent, account_head, sum(base_tax_amount_after_discount_amount) as amount
+ FROM `tabPurchase Taxes and Charges`
+ where parent in %s
+ GROUP BY parent, account_head
+ """, (invoice_list), as_dict=1)
+
+ for d in amounts:
+ amount_map.setdefault(d.parent,
+ {
+ 'itc_integrated_tax': 0,
+ 'itc_state_tax': 0,
+ 'itc_central_tax': 0,
+ 'itc_cess_amount': 0
+ })
+
+ if d.account_head in gst_accounts.get('igst_account'):
+ amount_map[d.parent]['itc_integrated_tax'] += d.amount
+ if d.account_head in gst_accounts.get('cgst_account'):
+ amount_map[d.parent]['itc_central_tax'] += d.amount
+ if d.account_head in gst_accounts.get('sgst_account'):
+ amount_map[d.parent]['itc_state_tax'] += d.amount
+ if d.account_head in gst_accounts.get('cess_account'):
+ amount_map[d.parent]['itc_cess_amount'] += d.amount
+
+ for invoice, values in amount_map.items():
+ frappe.db.set_value('Purchase Invoice', invoice, {
+ 'itc_integrated_tax': values.get('itc_integrated_tax'),
+ 'itc_central_tax': values.get('itc_central_tax'),
+ 'itc_state_tax': values['itc_state_tax'],
+ 'itc_cess_amount': values['itc_cess_amount'],
+ })
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/set_training_event_attendance.py b/erpnext/patches/v13_0/set_training_event_attendance.py
new file mode 100644
index 0000000000..18cad8d86c
--- /dev/null
+++ b/erpnext/patches/v13_0/set_training_event_attendance.py
@@ -0,0 +1,9 @@
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc('hr', 'doctype', 'training_event')
+ frappe.reload_doc('hr', 'doctype', 'training_event_employee')
+
+ frappe.db.sql("update `tabTraining Event Employee` set `attendance` = 'Present'")
+ frappe.db.sql("update `tabTraining Event Employee` set `is_mandatory` = 1 where `attendance` = 'Mandatory'")
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_timesheet_changes.py b/erpnext/patches/v13_0/update_timesheet_changes.py
new file mode 100644
index 0000000000..93b7f8e59a
--- /dev/null
+++ b/erpnext/patches/v13_0/update_timesheet_changes.py
@@ -0,0 +1,25 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ frappe.reload_doc("projects", "doctype", "timesheet")
+ frappe.reload_doc("projects", "doctype", "timesheet_detail")
+
+ if frappe.db.has_column("Timesheet Detail", "billable"):
+ rename_field("Timesheet Detail", "billable", "is_billable")
+
+ base_currency = frappe.defaults.get_global_default('currency')
+
+ frappe.db.sql("""UPDATE `tabTimesheet Detail`
+ SET base_billing_rate = billing_rate,
+ base_billing_amount = billing_amount,
+ base_costing_rate = costing_rate,
+ base_costing_amount = costing_amount""")
+
+ frappe.db.sql("""UPDATE `tabTimesheet`
+ SET currency = '{0}',
+ exchange_rate = 1.0,
+ base_total_billable_amount = total_billable_amount,
+ base_total_billed_amount = total_billed_amount,
+ base_total_costing_amount = total_costing_amount""".format(base_currency))
\ No newline at end of file
diff --git a/erpnext/patches/v7_0/convert_timelog_to_timesheet.py b/erpnext/patches/v7_0/convert_timelog_to_timesheet.py
index 3af6622d96..8c60b5b71e 100644
--- a/erpnext/patches/v7_0/convert_timelog_to_timesheet.py
+++ b/erpnext/patches/v7_0/convert_timelog_to_timesheet.py
@@ -51,7 +51,7 @@ def execute():
def get_timelog_data(data):
return {
- 'billable': data.billable,
+ 'is_billable': data.billable,
'from_time': data.from_time,
'hours': data.hours,
'to_time': data.to_time,
diff --git a/erpnext/projects/doctype/activity_type/activity_type.js b/erpnext/projects/doctype/activity_type/activity_type.js
index 7eb3571af1..f1ba882812 100644
--- a/erpnext/projects/doctype/activity_type/activity_type.js
+++ b/erpnext/projects/doctype/activity_type/activity_type.js
@@ -1,4 +1,8 @@
frappe.ui.form.on("Activity Type", {
+ onload: function(frm) {
+ frm.set_currency_labels(["billing_rate", "costing_rate"], frappe.defaults.get_global_default('currency'));
+ },
+
refresh: function(frm) {
frm.add_custom_button(__("Activity Cost per Employee"), function() {
frappe.route_options = {"activity_type": frm.doc.name};
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index c5265e23c0..31460f66ea 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -87,7 +87,7 @@ frappe.ui.form.on("Project", {
frm.add_custom_button(__("Kanban Board"), () => {
frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
- project: frm.doc.project_name
+ project: frm.doc.name
}).then(() => {
frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
});
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 55c5149a9c..c8fbe0bf7b 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -523,8 +523,9 @@ def update_project_sales_billing():
def create_kanban_board_if_not_exists(project):
from frappe.desk.doctype.kanban_board.kanban_board import quick_kanban_board
- if not frappe.db.exists('Kanban Board', project):
- quick_kanban_board('Task', project, 'status', project)
+ project = frappe.get_doc('Project', project)
+ if not frappe.db.exists('Kanban Board', project.project_name):
+ quick_kanban_board('Task', project.project_name, 'status', project.name)
return True
diff --git a/erpnext/projects/doctype/task/task.js b/erpnext/projects/doctype/task/task.js
index 6a9d2d1424..3cd92ee719 100644
--- a/erpnext/projects/doctype/task/task.js
+++ b/erpnext/projects/doctype/task/task.js
@@ -5,12 +5,6 @@ frappe.provide("erpnext.projects");
frappe.ui.form.on("Task", {
setup: function (frm) {
- frm.set_query("project", function () {
- return {
- query: "erpnext.projects.doctype.task.task.get_project"
- }
- });
-
frm.make_methods = {
'Timesheet': () => frappe.model.open_mapped_doc({
method: 'erpnext.projects.doctype.task.task.make_timesheet',
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index d21ac0f2f0..2b0c3abdd7 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -37,7 +37,7 @@ class TestTimesheet(unittest.TestCase):
emp = make_employee("test_employee_6@salary.com")
make_salary_structure_for_timesheet(emp)
- timesheet = make_timesheet(emp, simulate=True, billable=1)
+ timesheet = make_timesheet(emp, simulate=True, is_billable=1)
self.assertEqual(timesheet.total_hours, 2)
self.assertEqual(timesheet.total_billable_hours, 2)
@@ -49,7 +49,7 @@ class TestTimesheet(unittest.TestCase):
emp = make_employee("test_employee_6@salary.com")
make_salary_structure_for_timesheet(emp)
- timesheet = make_timesheet(emp, simulate=True, billable=0)
+ timesheet = make_timesheet(emp, simulate=True, is_billable=0)
self.assertEqual(timesheet.total_hours, 2)
self.assertEqual(timesheet.total_billable_hours, 0)
@@ -61,7 +61,7 @@ class TestTimesheet(unittest.TestCase):
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)
+ timesheet = make_timesheet(emp, simulate = True, is_billable=1)
salary_slip = make_salary_slip(timesheet.name)
salary_slip.submit()
@@ -82,7 +82,7 @@ class TestTimesheet(unittest.TestCase):
def test_sales_invoice_from_timesheet(self):
emp = make_employee("test_employee_6@salary.com")
- timesheet = make_timesheet(emp, simulate=True, billable=1)
+ timesheet = make_timesheet(emp, simulate=True, is_billable=1)
sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer')
sales_invoice.due_date = nowdate()
sales_invoice.submit()
@@ -100,7 +100,7 @@ class TestTimesheet(unittest.TestCase):
emp = make_employee("test_employee_6@salary.com")
project = frappe.get_value("Project", {"project_name": "_Test Project"})
- timesheet = make_timesheet(emp, simulate=True, billable=1, project=project, company='_Test Company')
+ timesheet = make_timesheet(emp, simulate=True, is_billable=1, project=project, company='_Test Company')
sales_invoice = create_sales_invoice(do_not_save=True)
sales_invoice.project = project
sales_invoice.submit()
@@ -171,13 +171,13 @@ def make_salary_structure_for_timesheet(employee, company=None):
return salary_structure
-def make_timesheet(employee, simulate=False, billable = 0, activity_type="_Test Activity Type", project=None, task=None, company=None):
+def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Test Activity Type", project=None, task=None, company=None):
update_activity_type(activity_type)
timesheet = frappe.new_doc("Timesheet")
timesheet.employee = employee
timesheet.company = company or '_Test Company'
timesheet_detail = timesheet.append('time_logs', {})
- timesheet_detail.billable = billable
+ timesheet_detail.is_billable = is_billable
timesheet_detail.activity_type = activity_type
timesheet_detail.from_time = now_datetime()
timesheet_detail.hours = 2
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index b123af5d18..84c7b8118b 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -90,17 +90,99 @@ frappe.ui.form.on("Timesheet", {
}
if(frm.doc.per_billed > 0) {
frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false);
- frm.fields_dict["time_logs"].grid.toggle_enable("billable", false);
+ frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false);
}
+ frm.trigger('setup_filters');
+ frm.trigger('set_dynamic_field_label');
+ },
+
+ customer: function(frm) {
+ frm.set_query('parent_project', function(doc) {
+ return {
+ filters: {
+ "customer": doc.customer
+ }
+ };
+ });
+ frm.set_query('project', 'time_logs', function(doc) {
+ return {
+ filters: {
+ "customer": doc.customer
+ }
+ };
+ });
+ frm.refresh();
+ },
+
+ currency: function(frm) {
+ let base_currency = frappe.defaults.get_global_default('currency');
+ if (base_currency != frm.doc.currency) {
+ frappe.call({
+ method: "erpnext.setup.utils.get_exchange_rate",
+ args: {
+ from_currency: frm.doc.currency,
+ to_currency: base_currency
+ },
+ callback: function(r) {
+ if (r.message) {
+ frm.set_value('exchange_rate', flt(r.message));
+ frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + " = [?] " + base_currency);
+ }
+ }
+ });
+ }
+ frm.trigger('set_dynamic_field_label');
+ },
+
+ exchange_rate: function(frm) {
+ $.each(frm.doc.time_logs, function(i, d) {
+ calculate_billing_costing_amount(frm, d.doctype, d.name);
+ });
+ calculate_time_and_amount(frm);
+ },
+
+ set_dynamic_field_label: function(frm) {
+ let base_currency = frappe.defaults.get_global_default('currency');
+ frm.set_currency_labels(["base_total_costing_amount", "base_total_billable_amount", "base_total_billed_amount"], base_currency);
+ frm.set_currency_labels(["total_costing_amount", "total_billable_amount", "total_billed_amount"], frm.doc.currency);
+
+ frm.toggle_display(["base_total_costing_amount", "base_total_billable_amount", "base_total_billed_amount"],
+ frm.doc.currency != base_currency);
+
+ if (frm.doc.time_logs.length > 0) {
+ frm.set_currency_labels(["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], base_currency, "time_logs");
+ frm.set_currency_labels(["billing_rate", "billing_amount", "costing_rate", "costing_amount"], frm.doc.currency, "time_logs");
+
+ let time_logs_grid = frm.fields_dict.time_logs.grid;
+ $.each(["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], function(i, d) {
+ if (frappe.meta.get_docfield(time_logs_grid.doctype, d))
+ time_logs_grid.set_column_disp(d, frm.doc.currency != base_currency);
+ });
+ }
+ frm.refresh_fields();
},
make_invoice: function(frm) {
+ let fields = [{
+ "fieldtype": "Link",
+ "label": __("Item Code"),
+ "fieldname": "item_code",
+ "options": "Item"
+ }];
+
+ if (!frm.doc.customer) {
+ fields.push({
+ "fieldtype": "Link",
+ "label": __("Customer"),
+ "fieldname": "customer",
+ "options": "Customer",
+ "default": frm.doc.customer
+ });
+ }
+
let dialog = new frappe.ui.Dialog({
- title: __("Select Item (optional)"),
- fields: [
- {"fieldtype": "Link", "label": __("Item Code"), "fieldname": "item_code", "options":"Item"},
- {"fieldtype": "Link", "label": __("Customer"), "fieldname": "customer", "options":"Customer"}
- ]
+ title: __("Create Sales Invoice"),
+ fields: fields
});
dialog.set_primary_action(__('Create Sales Invoice'), () => {
@@ -113,7 +195,8 @@ frappe.ui.form.on("Timesheet", {
args: {
"source_name": frm.doc.name,
"item_code": args.item_code,
- "customer": args.customer
+ "customer": frm.doc.customer || args.customer,
+ "currency": frm.doc.currency
},
freeze: true,
callback: function(r) {
@@ -136,8 +219,7 @@ frappe.ui.form.on("Timesheet", {
parent_project: function(frm) {
set_project_in_timelog(frm);
- },
-
+ }
});
frappe.ui.form.on("Timesheet Detail", {
@@ -171,35 +253,34 @@ frappe.ui.form.on("Timesheet Detail", {
if(frm.doc.parent_project) {
frappe.model.set_value(cdt, cdn, 'project', frm.doc.parent_project);
}
-
- var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row');
- $trigger_again.on('click', () => {
- $('.form-grid')
- .find('[data-fieldname="timer"]')
- .append(frappe.render_template("timesheet"));
- frm.trigger("control_timer");
- });
},
+
hours: function(frm, cdt, cdn) {
calculate_end_time(frm, cdt, cdn);
+ calculate_billing_costing_amount(frm, cdt, cdn);
+ calculate_time_and_amount(frm);
},
billing_hours: function(frm, cdt, cdn) {
calculate_billing_costing_amount(frm, cdt, cdn);
+ calculate_time_and_amount(frm);
},
billing_rate: function(frm, cdt, cdn) {
calculate_billing_costing_amount(frm, cdt, cdn);
+ calculate_time_and_amount(frm);
},
costing_rate: function(frm, cdt, cdn) {
calculate_billing_costing_amount(frm, cdt, cdn);
+ calculate_time_and_amount(frm);
},
- billable: function(frm, cdt, cdn) {
+ is_billable: function(frm, cdt, cdn) {
update_billing_hours(frm, cdt, cdn);
update_time_rates(frm, cdt, cdn);
calculate_billing_costing_amount(frm, cdt, cdn);
+ calculate_time_and_amount(frm);
},
activity_type: function(frm, cdt, cdn) {
@@ -207,7 +288,8 @@ frappe.ui.form.on("Timesheet Detail", {
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
args: {
employee: frm.doc.employee,
- activity_type: frm.selected_doc.activity_type
+ activity_type: frm.selected_doc.activity_type,
+ currency: frm.doc.currency
},
callback: function(r){
if(r.message){
@@ -239,9 +321,9 @@ var calculate_end_time = function(frm, cdt, cdn) {
}
};
-var update_billing_hours = function(frm, cdt, cdn){
- var child = locals[cdt][cdn];
- if(!child.billable) {
+var update_billing_hours = function(frm, cdt, cdn) {
+ let child = frappe.get_doc(cdt, cdn);
+ if (!child.is_billable) {
frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0);
} else {
// bill all hours by default
@@ -249,40 +331,44 @@ var update_billing_hours = function(frm, cdt, cdn){
}
};
-var update_time_rates = function(frm, cdt, cdn){
- var child = locals[cdt][cdn];
- if(!child.billable){
+var update_time_rates = function(frm, cdt, cdn) {
+ let child = frappe.get_doc(cdt, cdn);
+ if (!child.is_billable) {
frappe.model.set_value(cdt, cdn, 'billing_rate', 0.0);
}
};
-var calculate_billing_costing_amount = function(frm, cdt, cdn){
- var child = locals[cdt][cdn];
- var billing_amount = 0.0;
- var costing_amount = 0.0;
-
- if(child.billing_hours && child.billable){
- billing_amount = (child.billing_hours * child.billing_rate);
+var calculate_billing_costing_amount = function(frm, cdt, cdn) {
+ let row = frappe.get_doc(cdt, cdn);
+ let billing_amount = 0.0;
+ let base_billing_amount = 0.0;
+ let exchange_rate = flt(frm.doc.exchange_rate);
+ frappe.model.set_value(cdt, cdn, 'base_billing_rate', flt(row.billing_rate) * exchange_rate);
+ frappe.model.set_value(cdt, cdn, 'base_costing_rate', flt(row.costing_rate) * exchange_rate);
+ if (row.billing_hours && row.is_billable) {
+ base_billing_amount = flt(row.billing_hours) * flt(row.base_billing_rate);
+ billing_amount = flt(row.billing_hours) * flt(row.billing_rate);
}
- costing_amount = flt(child.costing_rate * child.hours);
+
+ frappe.model.set_value(cdt, cdn, 'base_billing_amount', base_billing_amount);
+ frappe.model.set_value(cdt, cdn, 'base_costing_amount', flt(row.base_costing_rate) * flt(row.hours));
frappe.model.set_value(cdt, cdn, 'billing_amount', billing_amount);
- frappe.model.set_value(cdt, cdn, 'costing_amount', costing_amount);
- calculate_time_and_amount(frm);
+ frappe.model.set_value(cdt, cdn, 'costing_amount', flt(row.costing_rate) * flt(row.hours));
};
var calculate_time_and_amount = function(frm) {
- var tl = frm.doc.time_logs || [];
- var total_working_hr = 0;
- var total_billing_hr = 0;
- var total_billable_amount = 0;
- var total_costing_amount = 0;
+ let tl = frm.doc.time_logs || [];
+ let total_working_hr = 0;
+ let total_billing_hr = 0;
+ let total_billable_amount = 0;
+ let total_costing_amount = 0;
for(var i=0; i ( item.name !== name ) && ( item.item_code === item_code ) )
+ .map( item => flt(item.qty) )
+ .reduce( (i, j) => i + j, 0);
+ return totalSelectedQty;
+}
+
+function check_can_calculate_pending_qty(me) {
+ const { frm: { doc }, item } = me;
+ const docChecks = doc.bom_no
+ && doc.fg_completed_qty
+ && erpnext.stock.bom
+ && erpnext.stock.bom.name === doc.bom_no;
+ const itemChecks = !!item;
+ return docChecks && itemChecks;
+}
diff --git a/erpnext/regional/address_template/templates/united_states.html b/erpnext/regional/address_template/templates/united_states.html
index 089315e4e4..77fce46b9d 100644
--- a/erpnext/regional/address_template/templates/united_states.html
+++ b/erpnext/regional/address_template/templates/united_states.html
@@ -1,4 +1,4 @@
{{ address_line1 }}
{% if address_line2 %}{{ address_line2 }}
{% endif -%}
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}
{% endif -%}
-{% if country != "United States" %}{{ country|upper }}{% endif -%}
+{% if country != "United States" %}{{ country }}{% endif -%}
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 369a4001ef..3b6a45a3b4 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.html
@@ -172,7 +172,7 @@
- (A) {{__("ITC Available (whether in full op part)")}} |
+ (A) {{__("ITC Available (whether in full or part)")}} |
|
|
|
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 a5dd5a2e09..641520437f 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -3,148 +3,21 @@
# For license information, please see license.txt
from __future__ import unicode_literals
+import os
+import json
import frappe
+from six import iteritems
from frappe import _
from frappe.model.document import Document
-import json
-from six import iteritems
-from frappe.utils import flt, getdate
+from frappe.utils import flt, cstr
from erpnext.regional.india import state_numbers
class GSTR3BReport(Document):
- def before_save(self):
-
+ def validate(self):
self.get_data()
def get_data(self):
-
- self.report_dict = {
- "gstin": "",
- "ret_period": "",
- "inward_sup": {
- "isup_details": [
- {
- "ty": "GST",
- "intra": 0,
- "inter": 0
- },
- {
- "ty": "NONGST",
- "inter": 0,
- "intra": 0
- }
- ]
- },
- "sup_details": {
- "osup_zero": {
- "csamt": 0,
- "txval": 0,
- "iamt": 0
- },
- "osup_nil_exmp": {
- "txval": 0
- },
- "osup_det": {
- "samt": 0,
- "csamt": 0,
- "txval": 0,
- "camt": 0,
- "iamt": 0
- },
- "isup_rev": {
- "samt": 0,
- "csamt": 0,
- "txval": 0,
- "camt": 0,
- "iamt": 0
- },
- "osup_nongst": {
- "txval": 0,
- }
- },
- "inter_sup": {
- "unreg_details": [],
- "comp_details": [],
- "uin_details": []
- },
- "itc_elg": {
- "itc_avl": [
- {
- "csamt": 0,
- "samt": 0,
- "ty": "IMPG",
- "camt": 0,
- "iamt": 0
- },
- {
- "csamt": 0,
- "samt": 0,
- "ty": "IMPS",
- "camt": 0,
- "iamt": 0
- },
- {
- "samt": 0,
- "csamt": 0,
- "ty": "ISRC",
- "camt": 0,
- "iamt": 0
- },
- {
- "ty": "ISD",
- "iamt": 0,
- "camt": 0,
- "samt": 0,
- "csamt": 0
- },
- {
- "samt": 0,
- "csamt": 0,
- "ty": "OTH",
- "camt": 0,
- "iamt": 0
- }
- ],
- "itc_rev": [
- {
- "ty": "RUL",
- "iamt": 0,
- "camt": 0,
- "samt": 0,
- "csamt": 0
- },
- {
- "ty": "OTH",
- "iamt": 0,
- "camt": 0,
- "samt": 0,
- "csamt": 0
- }
- ],
- "itc_net": {
- "samt": 0,
- "csamt": 0,
- "camt": 0,
- "iamt": 0
- },
- "itc_inelg": [
- {
- "ty": "RUL",
- "iamt": 0,
- "camt": 0,
- "samt": 0,
- "csamt": 0
- },
- {
- "ty": "OTH",
- "iamt": 0,
- "camt": 0,
- "samt": 0,
- "csamt": 0
- }
- ]
- }
- }
+ self.report_dict = json.loads(get_json('gstr_3b_report_template'))
self.gst_details = self.get_company_gst_details()
self.report_dict["gstin"] = self.gst_details.get("gstin")
@@ -152,23 +25,19 @@ class GSTR3BReport(Document):
self.month_no = get_period(self.month)
self.account_heads = self.get_account_heads()
- outward_supply_tax_amounts = self.get_tax_amounts("Sales Invoice")
- inward_supply_tax_amounts = self.get_tax_amounts("Purchase Invoice", reverse_charge="Y")
+ self.get_outward_supply_details("Sales Invoice")
+ self.set_outward_taxable_supplies()
+
+ self.get_outward_supply_details("Purchase Invoice", reverse_charge=True)
+ self.set_supplies_liable_to_reverse_charge()
+
itc_details = self.get_itc_details()
-
- self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"])
- self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"])
- self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas"], reverse_charge="Y")
- self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2)
self.set_itc_details(itc_details)
-
- inter_state_supplies = self.get_inter_state_supplies(self.gst_details.get("gst_state_number"))
+ self.get_itc_reversal_entries()
inward_nil_exempt = self.get_inward_nil_exempt(self.gst_details.get("gst_state"))
- self.set_inter_state_supply(inter_state_supplies)
self.set_inward_nil_exempt(inward_nil_exempt)
self.missing_field_invoices = self.get_missing_field_invoices()
-
self.json_output = frappe.as_json(self.report_dict)
def set_inward_nil_exempt(self, inward_nil_exempt):
@@ -178,189 +47,95 @@ class GSTR3BReport(Document):
self.report_dict["inward_sup"]["isup_details"][1]["intra"] = flt(inward_nil_exempt.get("non_gst").get("intra"), 2)
def set_itc_details(self, itc_details):
-
- itc_type_map = {
+ itc_eligible_type_map = {
'IMPG': 'Import Of Capital Goods',
'IMPS': 'Import Of Service',
+ 'ISRC': 'ITC on Reverse Charge',
'ISD': 'Input Service Distributor',
'OTH': 'All Other ITC'
}
+ itc_ineligible_map = {
+ 'RUL': 'Ineligible As Per Section 17(5)',
+ 'OTH': 'Ineligible Others'
+ }
+
net_itc = self.report_dict["itc_elg"]["itc_net"]
for d in self.report_dict["itc_elg"]["itc_avl"]:
-
- itc_type = itc_type_map.get(d["ty"])
-
- if d["ty"] == 'ISRC':
- reverse_charge = ["Y"]
- itc_type = 'All Other ITC'
- gst_category = ['Unregistered', 'Overseas']
- else:
- gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
- reverse_charge = ["N", "Y"]
-
- for account_head in self.account_heads:
- for category in gst_category:
- for charge_type in reverse_charge:
- for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
- d[key[0]] += flt(itc_details.get((category, itc_type, charge_type, account_head.get(key[1])), {}).get("amount"), 2)
-
+ itc_type = itc_eligible_type_map.get(d["ty"])
for key in ['iamt', 'camt', 'samt', 'csamt']:
+ d[key] = flt(itc_details.get(itc_type, {}).get(key))
net_itc[key] += flt(d[key], 2)
- for account_head in self.account_heads:
- itc_inelg = self.report_dict["itc_elg"]["itc_inelg"][1]
- for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
- itc_inelg[key[0]] = flt(itc_details.get(("Ineligible", "N", account_head.get(key[1])), {}).get("amount"), 2)
+ for d in self.report_dict["itc_elg"]["itc_inelg"]:
+ itc_type = itc_ineligible_map.get(d["ty"])
+ for key in ['iamt', 'camt', 'samt', 'csamt']:
+ d[key] = flt(itc_details.get(itc_type, {}).get(key))
- def prepare_data(self, doctype, tax_details, supply_type, supply_category, gst_category_list, reverse_charge="N"):
+ def get_itc_reversal_entries(self):
+ reversal_entries = frappe.db.sql("""
+ SELECT ja.account, j.reversal_type, sum(credit_in_account_currency) as amount
+ FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
+ where j.docstatus = 1
+ and j.is_opening = 'No'
+ and ja.parent = j.name
+ and j.voucher_type = 'Reversal Of ITC'
+ and month(j.posting_date) = %s and year(j.posting_date) = %s
+ and j.company = %s and j.company_gstin = %s
+ GROUP BY ja.account, j.reversal_type""", (self.month_no, self.year, self.company,
+ self.gst_details.get("gstin")), as_dict=1)
- account_map = {
- 'sgst_account': 'samt',
- 'cess_account': 'csamt',
- 'cgst_account': 'camt',
- 'igst_account': 'iamt'
- }
+ net_itc = self.report_dict["itc_elg"]["itc_net"]
- txval = 0
- total_taxable_value = self.get_total_taxable_value(doctype, reverse_charge)
+ for entry in reversal_entries:
+ if entry.reversal_type == 'As per rules 42 & 43 of CGST Rules':
+ index = 0
+ else:
+ index = 1
- for gst_category in gst_category_list:
- txval += total_taxable_value.get(gst_category,0)
- for account_head in self.account_heads:
- for account_type, account_name in iteritems(account_head):
- if account_map.get(account_type) in self.report_dict.get(supply_type).get(supply_category):
- self.report_dict[supply_type][supply_category][account_map.get(account_type)] += \
- flt(tax_details.get((account_name, gst_category), {}).get("amount"), 2)
-
- 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):
- if key[0] == "Unregistered":
- self.report_dict["inter_sup"]["unreg_details"].append(value)
-
- if key[0] == "Registered Composition":
- self.report_dict["inter_sup"]["comp_details"].append(value)
-
- if key[0] == "UIN Holders":
- self.report_dict["inter_sup"]["uin_details"].append(value)
-
- def get_total_taxable_value(self, doctype, reverse_charge):
-
- return frappe._dict(frappe.db.sql("""
- select gst_category, sum(net_total) as total
- from `tab{doctype}`
- where docstatus = 1 and month(posting_date) = %s
- and year(posting_date) = %s and reverse_charge = %s
- and company = %s and company_gstin = %s
- group by gst_category
- """ #nosec
- .format(doctype = doctype), (self.month_no, self.year, reverse_charge, self.company, self.gst_details.get("gstin"))))
+ for key in ['camt', 'samt', 'iamt', 'csamt']:
+ if entry.account in self.account_heads.get(key):
+ self.report_dict["itc_elg"]["itc_rev"][index][key] += flt(entry.amount)
+ net_itc[key] -= flt(entry.amount)
def get_itc_details(self):
- itc_amount = frappe.db.sql("""
- select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount,
- t.account_head, s.eligibility_for_itc, s.reverse_charge
- from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t
- where s.docstatus = 1 and t.parent = s.name
- and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
- and s.company_gstin = %s
- group by t.account_head, s.gst_category, s.eligibility_for_itc
- """,
- (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
+ itc_amounts = frappe.db.sql("""
+ SELECT eligibility_for_itc, sum(itc_integrated_tax) as itc_integrated_tax,
+ sum(itc_central_tax) as itc_central_tax,
+ sum(itc_state_tax) as itc_state_tax,
+ sum(itc_cess_amount) as itc_cess_amount
+ FROM `tabPurchase Invoice`
+ WHERE docstatus = 1
+ and is_opening = 'No'
+ and month(posting_date) = %s and year(posting_date) = %s and company = %s
+ and company_gstin = %s
+ GROUP BY eligibility_for_itc
+ """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
itc_details = {}
-
- for d in itc_amount:
- itc_details.setdefault((d.gst_category, d.eligibility_for_itc, d.reverse_charge, d.account_head),{
- "amount": d.tax_amount
+ for d in itc_amounts:
+ itc_details.setdefault(d.eligibility_for_itc, {
+ 'iamt': d.itc_integrated_tax,
+ 'camt': d.itc_central_tax,
+ 'samt': d.itc_state_tax,
+ 'csamt': d.itc_cess_amount
})
return itc_details
- def get_nil_rated_supply_value(self):
-
- return frappe.db.sql("""
- select sum(i.base_amount) as total from
- `tabSales Invoice Item` i, `tabSales Invoice` s
- where s.docstatus = 1 and i.parent = s.name and i.is_nil_exempt = 1
- and month(s.posting_date) = %s and year(s.posting_date) = %s
- and s.company = %s and s.company_gstin = %s""",
- (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)[0].total
-
- def get_inter_state_supplies(self, state_number):
- inter_state_supply_tax = frappe.db.sql(""" select t.account_head, t.tax_amount_after_discount_amount as tax_amount,
- s.name, s.net_total, s.place_of_supply, s.gst_category from `tabSales Invoice` s, `tabSales Taxes and Charges` t
- where t.parent = s.name and s.docstatus = 1 and month(s.posting_date) = %s and year(s.posting_date) = %s
- and s.company = %s and s.company_gstin = %s and s.gst_category in ('Unregistered', 'Registered Composition', 'UIN Holders')
- """, (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
-
- inter_state_supply_tax_mapping = {}
- inter_state_supply_details = {}
-
- for d in inter_state_supply_tax:
- inter_state_supply_tax_mapping.setdefault(d.name, {
- 'place_of_supply': d.place_of_supply,
- 'taxable_value': d.net_total,
- 'gst_category': d.gst_category,
- 'camt': 0.0,
- 'samt': 0.0,
- 'iamt': 0.0,
- 'csamt': 0.0
- })
-
- if d.account_head in [a.cgst_account for a in self.account_heads]:
- inter_state_supply_tax_mapping[d.name]['camt'] += d.tax_amount
-
- if d.account_head in [a.sgst_account for a in self.account_heads]:
- inter_state_supply_tax_mapping[d.name]['samt'] += d.tax_amount
-
- if d.account_head in [a.igst_account for a in self.account_heads]:
- inter_state_supply_tax_mapping[d.name]['iamt'] += d.tax_amount
-
- if d.account_head in [a.cess_account for a in self.account_heads]:
- inter_state_supply_tax_mapping[d.name]['csamt'] += d.tax_amount
-
- for key, value in iteritems(inter_state_supply_tax_mapping):
- if value.get('place_of_supply'):
- osup_det = self.report_dict["sup_details"]["osup_det"]
- osup_det["txval"] = flt(osup_det["txval"] + value['taxable_value'], 2)
- osup_det["iamt"] = flt(osup_det["iamt"] + value['iamt'], 2)
- osup_det["camt"] = flt(osup_det["camt"] + value['camt'], 2)
- osup_det["samt"] = flt(osup_det["samt"] + value['samt'], 2)
- osup_det["csamt"] = flt(osup_det["csamt"] + value['csamt'], 2)
-
- if state_number != value.get('place_of_supply').split("-")[0]:
- inter_state_supply_details.setdefault((value.get('gst_category'), value.get('place_of_supply')), {
- "txval": 0.0,
- "pos": value.get('place_of_supply').split("-")[0],
- "iamt": 0.0
- })
-
- inter_state_supply_details[(value.get('gst_category'), value.get('place_of_supply'))]['txval'] += value['taxable_value']
- inter_state_supply_details[(value.get('gst_category'), value.get('place_of_supply'))]['iamt'] += value['iamt']
-
- 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
+ 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 p.is_opening = 'No'
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, 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)
+ and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') 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, 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_details = {
"gst": {
@@ -388,37 +163,193 @@ class GSTR3BReport(Document):
return inward_nil_exempt_details
- def get_tax_amounts(self, doctype, reverse_charge="N"):
+ def get_outward_supply_details(self, doctype, reverse_charge=None):
+ self.get_outward_tax_invoices(doctype, reverse_charge=reverse_charge)
+ self.get_outward_items(doctype)
+ self.get_outward_tax_details(doctype)
+ def get_outward_tax_invoices(self, doctype, reverse_charge=None):
+ self.invoices = []
+ self.invoice_detail_map = {}
+ condition = ''
+
+ if reverse_charge:
+ condition += "AND reverse_charge = 'Y'"
+
+ invoice_details = frappe.db.sql("""
+ SELECT
+ name, gst_category, export_type, place_of_supply
+ FROM
+ `tab{doctype}`
+ WHERE
+ docstatus = 1
+ AND month(posting_date) = %s
+ AND year(posting_date) = %s
+ AND company = %s
+ AND company_gstin = %s
+ AND is_opening = 'No'
+ {reverse_charge}
+ ORDER BY name
+ """.format(doctype=doctype, reverse_charge=condition), (self.month_no, self.year,
+ self.company, self.gst_details.get("gstin")), as_dict=1)
+
+ for d in invoice_details:
+ self.invoice_detail_map.setdefault(d.name, d)
+ self.invoices.append(d.name)
+
+ def get_outward_items(self, doctype):
+ self.invoice_items = frappe._dict()
+ self.is_nil_exempt = []
+ self.is_non_gst = []
+
+ if self.get('invoices'):
+ item_details = frappe.db.sql("""
+ SELECT
+ item_code, parent, taxable_value, base_net_amount, item_tax_rate,
+ is_nil_exempt, is_non_gst
+ FROM
+ `tab%s Item`
+ WHERE parent in (%s)
+ """ % (doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
+
+ for d in item_details:
+ if d.item_code not in self.invoice_items.get(d.parent, {}):
+ self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
+ sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in item_details
+ if i.item_code == d.item_code and i.parent == d.parent))
+
+ if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
+ self.is_nil_exempt.append(d.item_code)
+
+ if d.is_non_gst and d.item_code not in self.is_non_gst:
+ self.is_non_gst.append(d.item_code)
+
+ def get_outward_tax_details(self, doctype):
if doctype == "Sales Invoice":
tax_template = 'Sales Taxes and Charges'
elif doctype == "Purchase Invoice":
tax_template = 'Purchase Taxes and Charges'
- tax_amounts = frappe.db.sql("""
- select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head
- from `tab{doctype}` s , `tab{template}` t
- where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s
- and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
- and s.company_gstin = %s
- group by t.account_head, s.gst_category
- """ #nosec
- .format(doctype=doctype, template=tax_template),
- (reverse_charge, self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
+ self.items_based_on_tax_rate = {}
+ self.invoice_cess = frappe._dict()
+ self.cgst_sgst_invoices = []
- tax_details = {}
+ if self.get('invoices'):
+ tax_details = frappe.db.sql("""
+ SELECT
+ parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount
+ FROM `tab%s`
+ WHERE
+ parenttype = %s and docstatus = 1
+ and parent in (%s)
+ ORDER BY account_head
+ """ % (tax_template, '%s', ', '.join(['%s']*len(self.invoices))),
+ tuple([doctype] + list(self.invoices)))
- for d in tax_amounts:
- tax_details.setdefault(
- (d.account_head,d.gst_category),{
- "amount": d.get("tax_amount"),
- }
- )
+ for parent, account, item_wise_tax_detail, tax_amount in tax_details:
+ if account in self.account_heads.get('csamt'):
+ self.invoice_cess.setdefault(parent, tax_amount)
+ else:
+ if item_wise_tax_detail:
+ try:
+ item_wise_tax_detail = json.loads(item_wise_tax_detail)
+ cgst_or_sgst = False
+ if account in self.account_heads.get('camt') \
+ or account in self.account_heads.get('samt'):
+ cgst_or_sgst = True
- return tax_details
+ for item_code, tax_amounts in item_wise_tax_detail.items():
+ if not (cgst_or_sgst or account in self.account_heads.get('iamt') or
+ (item_code in self.is_non_gst + self.is_nil_exempt)):
+ continue
+
+ tax_rate = tax_amounts[0]
+ if tax_rate:
+ if cgst_or_sgst:
+ tax_rate *= 2
+ if parent not in self.cgst_sgst_invoices:
+ self.cgst_sgst_invoices.append(parent)
+
+ rate_based_dict = self.items_based_on_tax_rate\
+ .setdefault(parent, {}).setdefault(tax_rate, [])
+ if item_code not in rate_based_dict:
+ rate_based_dict.append(item_code)
+ except ValueError:
+ continue
+
+
+ if self.get('invoice_items'):
+ # Build itemised tax for export invoices, nil and exempted where tax table is blank
+ for invoice, items in iteritems(self.invoice_items):
+ if invoice not in self.items_based_on_tax_rate and (self.invoice_detail_map.get(invoice, {}).get('export_type')
+ == "Without Payment of Tax"):
+ self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
+
+ def set_outward_taxable_supplies(self):
+ inter_state_supply_details = {}
+
+ for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
+ for rate, items in items_based_on_rate.items():
+ for item_code, taxable_value in self.invoice_items.get(inv).items():
+ if item_code in items:
+ if item_code in self.is_nil_exempt:
+ self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value
+ elif item_code in self.is_non_gst:
+ self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value
+ elif rate == 0:
+ self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value
+ #self.report_dict['sup_details']['osup_zero'][key] += tax_amount
+ else:
+ if inv in self.cgst_sgst_invoices:
+ tax_rate = rate/2
+ self.report_dict['sup_details']['osup_det']['camt'] += (taxable_value * tax_rate /100)
+ self.report_dict['sup_details']['osup_det']['samt'] += (taxable_value * tax_rate /100)
+ self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
+ else:
+ self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100)
+ self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
+
+ gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
+ place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory'
+
+ if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \
+ self.gst_details.get("gst_state") != place_of_supply.split("-")[1]:
+ inter_state_supply_details.setdefault((gst_category, place_of_supply), {
+ "txval": 0.0,
+ "pos": place_of_supply.split("-")[0],
+ "iamt": 0.0
+ })
+ inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value
+ inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100)
+
+ self.set_inter_state_supply(inter_state_supply_details)
+
+ def set_supplies_liable_to_reverse_charge(self):
+ for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
+ for rate, items in items_based_on_rate.items():
+ for item_code, taxable_value in self.invoice_items.get(inv).items():
+ if item_code in items:
+ if inv in self.cgst_sgst_invoices:
+ tax_rate = rate/2
+ self.report_dict['sup_details']['isup_rev']['camt'] += (taxable_value * tax_rate /100)
+ self.report_dict['sup_details']['isup_rev']['samt'] += (taxable_value * tax_rate /100)
+ self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value
+ else:
+ self.report_dict['sup_details']['isup_rev']['iamt'] += (taxable_value * rate /100)
+ self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value
+
+ def set_inter_state_supply(self, inter_state_supply):
+ for key, value in iteritems(inter_state_supply):
+ if key[0] == "Unregistered":
+ self.report_dict["inter_sup"]["unreg_details"].append(value)
+
+ if key[0] == "Registered Composition":
+ self.report_dict["inter_sup"]["comp_details"].append(value)
+
+ if key[0] == "UIN Holders":
+ self.report_dict["inter_sup"]["uin_details"].append(value)
def get_company_gst_details(self):
-
gst_details = frappe.get_all("Address",
fields=["gstin", "gst_state", "gst_state_number"],
filters={
@@ -431,20 +362,28 @@ class GSTR3BReport(Document):
frappe.throw(_("Please enter GSTIN and state for the Company Address {0}").format(self.company_address))
def get_account_heads(self):
+ account_map = {
+ 'sgst_account': 'samt',
+ 'cess_account': 'csamt',
+ 'cgst_account': 'camt',
+ 'igst_account': 'iamt'
+ }
- account_heads = frappe.get_all("GST Account",
- fields=["cgst_account", "sgst_account", "igst_account", "cess_account"],
- filters={
- "company":self.company
- })
+ account_heads = {}
+ gst_settings_accounts = frappe.get_all("GST Account",
+ filters={'company': self.company, 'is_reverse_charge_account': 0},
+ fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
- if account_heads:
- return account_heads
- else:
- frappe.throw(_("Please set account heads in GST Settings for Compnay {0}").format(self.company))
+ if not gst_settings_accounts:
+ frappe.throw(_("Please set GST Accounts in GST Settings"))
+
+ for d in gst_settings_accounts:
+ for acc, val in d.items():
+ account_heads.setdefault(account_map.get(acc), []).append(val)
+
+ return account_heads
def get_missing_field_invoices(self):
-
missing_field_invoices = []
for doctype in ["Sales Invoice", "Purchase Invoice"]:
@@ -456,26 +395,32 @@ class GSTR3BReport(Document):
party_type = 'Supplier'
party = 'supplier'
- docnames = frappe.db.sql("""
- select t1.name from `tab{doctype}` t1, `tab{party_type}` t2
- where t1.docstatus = 1 and month(t1.posting_date) = %s and year(t1.posting_date) = %s
+ docnames = frappe.db.sql(
+ """
+ SELECT t1.name FROM `tab{doctype}` t1, `tab{party_type}` t2
+ WHERE t1.docstatus = 1 and t1.is_opening = 'No'
+ and month(t1.posting_date) = %s and year(t1.posting_date) = %s
and t1.company = %s and t1.place_of_supply IS NULL and t1.{party} = t2.name and
t2.gst_category != 'Overseas'
- """.format(doctype = doctype, party_type = party_type, party=party), (self.month_no, self.year, self.company), as_dict=1) #nosec
+ """.format(doctype = doctype, party_type = party_type,
+ party=party) ,(self.month_no, self.year, self.company), as_dict=1) #nosec
for d in docnames:
missing_field_invoices.append(d.name)
return ",".join(missing_field_invoices)
-def get_state_code(state):
+def get_json(template):
+ file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template))
+ with open(file_path, 'r') as f:
+ return cstr(f.read())
+def get_state_code(state):
state_code = state_numbers.get(state)
return state_code
def get_period(month, year=None):
-
month_no = {
"January": 1,
"February": 2,
@@ -499,13 +444,11 @@ def get_period(month, year=None):
@frappe.whitelist()
def view_report(name):
-
json_data = frappe.get_value("GSTR 3B Report", name, 'json_output')
return json.loads(json_data)
@frappe.whitelist()
def make_json(name):
-
json_data = frappe.get_value("GSTR 3B Report", name, 'json_output')
file_name = "GST3B.json"
frappe.local.response.filename = file_name
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report_template.json b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report_template.json
new file mode 100644
index 0000000000..a68bd6a6e5
--- /dev/null
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report_template.json
@@ -0,0 +1,127 @@
+{
+ "gstin": "",
+ "ret_period": "",
+ "inward_sup": {
+ "isup_details": [
+ {
+ "ty": "GST",
+ "intra": 0,
+ "inter": 0
+ },
+ {
+ "ty": "NONGST",
+ "inter": 0,
+ "intra": 0
+ }
+ ]
+ },
+ "sup_details": {
+ "osup_zero": {
+ "csamt": 0,
+ "txval": 0,
+ "iamt": 0
+ },
+ "osup_nil_exmp": {
+ "txval": 0
+ },
+ "osup_det": {
+ "samt": 0,
+ "csamt": 0,
+ "txval": 0,
+ "camt": 0,
+ "iamt": 0
+ },
+ "isup_rev": {
+ "samt": 0,
+ "csamt": 0,
+ "txval": 0,
+ "camt": 0,
+ "iamt": 0
+ },
+ "osup_nongst": {
+ "txval": 0
+ }
+ },
+ "inter_sup": {
+ "unreg_details": [],
+ "comp_details": [],
+ "uin_details": []
+ },
+ "itc_elg": {
+ "itc_avl": [
+ {
+ "csamt": 0,
+ "samt": 0,
+ "ty": "IMPG",
+ "camt": 0,
+ "iamt": 0
+ },
+ {
+ "csamt": 0,
+ "samt": 0,
+ "ty": "IMPS",
+ "camt": 0,
+ "iamt": 0
+ },
+ {
+ "samt": 0,
+ "csamt": 0,
+ "ty": "ISRC",
+ "camt": 0,
+ "iamt": 0
+ },
+ {
+ "ty": "ISD",
+ "iamt": 0,
+ "camt": 0,
+ "samt": 0,
+ "csamt": 0
+ },
+ {
+ "samt": 0,
+ "csamt": 0,
+ "ty": "OTH",
+ "camt": 0,
+ "iamt": 0
+ }
+ ],
+ "itc_rev": [
+ {
+ "ty": "RUL",
+ "iamt": 0,
+ "camt": 0,
+ "samt": 0,
+ "csamt": 0
+ },
+ {
+ "ty": "OTH",
+ "iamt": 0,
+ "camt": 0,
+ "samt": 0,
+ "csamt": 0
+ }
+ ],
+ "itc_net": {
+ "samt": 0,
+ "csamt": 0,
+ "camt": 0,
+ "iamt": 0
+ },
+ "itc_inelg": [
+ {
+ "ty": "RUL",
+ "iamt": 0,
+ "camt": 0,
+ "samt": 0,
+ "csamt": 0
+ },
+ {
+ "ty": "OTH",
+ "iamt": 0,
+ "camt": 0,
+ "samt": 0,
+ "csamt": 0
+ }
+ ]
+ }
+}
\ No newline at end of file
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 ef8af24c42..3857ce1cdb 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
@@ -60,8 +60,7 @@ class TestGSTR3BReport(unittest.TestCase):
output = json.loads(report.json_output)
- self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 36),
- self.assertEqual(output["sup_details"]["osup_zero"]["iamt"], 18),
+ self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 54)
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]["intra"], 250)
diff --git a/erpnext/regional/germany/utils/datev/datev_csv.py b/erpnext/regional/germany/utils/datev/datev_csv.py
index 826d51f712..122c15fd81 100644
--- a/erpnext/regional/germany/utils/datev/datev_csv.py
+++ b/erpnext/regional/germany/utils/datev/datev_csv.py
@@ -55,8 +55,7 @@ def get_datev_csv(data, filters, csv_class):
quoting=QUOTE_NONNUMERIC
)
- if not six.PY2:
- data = data.encode('latin_1', errors='replace')
+ data = data.encode('latin_1', errors='replace')
header = get_header(filters, csv_class)
header = ';'.join(header).encode('latin_1', errors='replace')
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
index b4e7a8889e..843fb012b9 100644
--- a/erpnext/regional/india/e_invoice/utils.py
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -43,8 +43,9 @@ def validate_eligibility(doc):
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
no_taxes_applied = not doc.get('taxes')
+ has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst'))
- if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied:
+ if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item:
return False
return True
@@ -533,11 +534,9 @@ def santize_einvoice_fields(einvoice):
return einvoice
def safe_json_load(json_string):
- JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
-
try:
return json.loads(json_string)
- except JSONDecodeError as e:
+ except json.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)
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index b12e152b14..229e0c031e 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -114,9 +114,12 @@ def add_print_formats():
def make_property_setters(patch=False):
# GST rules do not allow for an invoice no. bigger than 16 characters
+ journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
+
if not patch:
make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
+ make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@@ -198,15 +201,20 @@ def make_custom_fields(update=True):
purchase_invoice_itc_fields = [
dict(fieldname='eligibility_for_itc', label='Eligibility For ITC',
fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1,
- options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nIneligible\nAll Other ITC', default="All Other ITC"),
+ options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC',
+ default="All Other ITC"),
dict(fieldname='itc_integrated_tax', label='Availed ITC Integrated Tax',
- fieldtype='Data', insert_after='eligibility_for_itc', print_hide=1),
+ fieldtype='Currency', insert_after='eligibility_for_itc',
+ options='Company:company:default_currency', print_hide=1),
dict(fieldname='itc_central_tax', label='Availed ITC Central Tax',
- fieldtype='Data', insert_after='itc_integrated_tax', print_hide=1),
+ fieldtype='Currency', insert_after='itc_integrated_tax',
+ options='Company:company:default_currency', print_hide=1),
dict(fieldname='itc_state_tax', label='Availed ITC State/UT Tax',
- fieldtype='Data', insert_after='itc_central_tax', print_hide=1),
+ fieldtype='Currency', insert_after='itc_central_tax',
+ options='Company:company:default_currency', print_hide=1),
dict(fieldname='itc_cess_amount', label='Availed ITC Cess',
- fieldtype='Data', insert_after='itc_state_tax', print_hide=1),
+ fieldtype='Currency', insert_after='itc_state_tax',
+ options='Company:company:default_currency', print_hide=1),
]
sales_invoice_gst_fields = [
@@ -236,6 +244,23 @@ def make_custom_fields(update=True):
depends_on="eval:doc.gst_category=='Overseas' "),
]
+ journal_entry_fields = [
+ dict(fieldname='reversal_type', label='Reversal Type',
+ fieldtype='Select', insert_after='voucher_type', print_hide=1,
+ options="As per rules 42 & 43 of CGST Rules\nOthers",
+ depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
+ mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
+ dict(fieldname='company_address', label='Company Address',
+ fieldtype='Link', options='Address', insert_after='reversal_type',
+ print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
+ mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
+ dict(fieldname='company_gstin', label='Company GSTIN',
+ fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1,
+ fetch_from='company_address.gstin',
+ depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
+ mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'")
+ ]
+
inter_state_gst_field = [
dict(fieldname='is_inter_state', label='Is Inter State',
fieldtype='Check', insert_after='disabled', print_hide=1),
@@ -430,13 +455,13 @@ def make_custom_fields(update=True):
dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
print_hide=1, hidden=1),
-
+
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
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_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
+ dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
no_copy=1, print_hide=1),
dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
@@ -469,6 +494,7 @@ def make_custom_fields(update=True):
'Purchase Receipt': purchase_invoice_gst_fields,
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category,
+ 'Journal Entry': journal_entry_fields,
'Sales Order': sales_invoice_gst_fields,
'Tax Category': inter_state_gst_field,
'Item': [
@@ -486,7 +512,7 @@ def make_custom_fields(update=True):
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
- 'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
+ 'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
'Material Request Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
'Salary Component': [
dict(fieldname= 'component_type',
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 052d7bdedf..fc227defbf 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -204,8 +204,6 @@ def get_regional_address_details(party_details, doctype, company):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
master_doctype = "Sales Taxes and Charges Template"
-
- get_tax_template_for_sez(party_details, master_doctype, company, 'Customer')
get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges'):
@@ -216,7 +214,6 @@ def get_regional_address_details(party_details, doctype, company):
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
master_doctype = "Purchase Taxes and Charges Template"
- get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier')
get_tax_template_based_on_category(master_doctype, company, party_details)
if party_details.get('taxes_and_charges'):
@@ -283,20 +280,6 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code):
{'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name')
return default_tax
-def get_tax_template_for_sez(party_details, master_doctype, company, party_type):
-
- gst_details = frappe.db.get_value(party_type, {'name': party_details.get(frappe.scrub(party_type))},
- ['gst_category', 'export_type'], as_dict=1)
-
- if gst_details:
- if gst_details.gst_category == 'SEZ' and gst_details.export_type == 'With Payment of Tax':
- default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0,
- "gst_state": number_state_mapping[party_details.company_gstin[:2]]})
-
- party_details["taxes_and_charges"] = default_tax
- party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
-
-
def calculate_annual_eligible_hra_exemption(doc):
basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"])
if not (basic_component and hra_component):
@@ -697,13 +680,22 @@ def validate_state_code(state_code, address):
return int(state_code)
@frappe.whitelist()
-def get_gst_accounts(company, account_wise=False):
+def get_gst_accounts(company=None, account_wise=False, only_reverse_charge=0, only_non_reverse_charge=0):
+ filters={"parent": "GST Settings"}
+
+ if company:
+ filters.update({'company': company})
+ if only_reverse_charge:
+ filters.update({'is_reverse_charge_account': 1})
+ elif only_non_reverse_charge:
+ filters.update({'is_reverse_charge_account': 0})
+
gst_accounts = frappe._dict()
gst_settings_accounts = frappe.get_all("GST Account",
- filters={"parent": "GST Settings", "company": company},
+ filters=filters,
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
- if not gst_settings_accounts and not frappe.flags.in_test:
+ if not gst_settings_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate:
frappe.throw(_("Please set GST Accounts in GST Settings"))
for d in gst_settings_accounts:
@@ -715,101 +707,63 @@ def get_gst_accounts(company, account_wise=False):
return gst_accounts
-def update_grand_total_for_rcm(doc, method):
+def validate_reverse_charge_transaction(doc, method):
country = frappe.get_cached_value('Company', doc.company, 'country')
if country != 'India':
return
- gst_tax, base_gst_tax = get_gst_tax_amount(doc)
-
- if not base_gst_tax:
- return
+ base_gst_tax = 0
+ base_reverse_charge_booked = 0
if doc.reverse_charge == 'Y':
- doc.taxes_and_charges_added -= gst_tax
- doc.total_taxes_and_charges -= gst_tax
- doc.base_taxes_and_charges_added -= base_gst_tax
- doc.base_total_taxes_and_charges -= base_gst_tax
+ gst_accounts = get_gst_accounts(doc.company, only_reverse_charge=1)
+ reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ + gst_accounts.get('igst_account')
- update_totals(gst_tax, base_gst_tax, doc)
-
-def update_totals(gst_tax, base_gst_tax, doc):
- doc.base_grand_total -= base_gst_tax
- doc.grand_total -= gst_tax
-
- if doc.meta.get_field("rounded_total"):
- if doc.is_rounded_total_disabled():
- doc.outstanding_amount = doc.grand_total
- else:
- doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total,
- doc.currency, doc.precision("rounded_total"))
-
- doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total,
- doc.precision("rounding_adjustment"))
-
- doc.outstanding_amount = doc.rounded_total or doc.grand_total
-
- doc.in_words = money_in_words(doc.grand_total, doc.currency)
- doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company))
- doc.set_payment_schedule()
-
-def make_regional_gl_entries(gl_entries, doc):
- country = frappe.get_cached_value('Company', doc.company, 'country')
-
- 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') \
+ gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1)
+ non_reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ gst_accounts.get('igst_account')
for tax in doc.get('taxes'):
- if tax.category not in ("Total", "Valuation and Total"):
- continue
+ if tax.account_head in non_reverse_charge_accounts:
+ if tax.add_deduct_tax == 'Add':
+ base_gst_tax += tax.base_tax_amount_after_discount_amount
+ else:
+ base_gst_tax += tax.base_tax_amount_after_discount_amount
+ elif tax.account_head in reverse_charge_accounts:
+ if tax.add_deduct_tax == 'Add':
+ base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount
+ else:
+ base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount
- dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
- if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
- account_currency = get_account_currency(tax.account_head)
+ if base_gst_tax != base_reverse_charge_booked:
+ msg = _("Booked reverse charge is not equal to applied tax amount")
+ msg += "
"
+ msg += _("Please refer {gst_document_link} to learn more about how to setup and create reverse charge invoice").format(
+ gst_document_link='GST Documentation')
- gl_entries.append(doc.get_gl_dict(
- {
- "account": tax.account_head,
- "cost_center": tax.cost_center,
- "posting_date": doc.posting_date,
- "against": doc.supplier,
- dr_or_cr: tax.base_tax_amount_after_discount_amount,
- dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \
- if account_currency==doc.company_currency \
- else tax.tax_amount_after_discount_amount
- }, account_currency, item=tax)
- )
+ frappe.throw(msg)
- return gl_entries
+def update_itc_availed_fields(doc, method):
+ country = frappe.get_cached_value('Company', doc.company, 'country')
-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', [])
+ if country != 'India':
+ return
- base_gst_tax = 0
- gst_tax = 0
+ # Initialize values
+ doc.itc_integrated_tax = doc.itc_state_tax = doc.itc_central_tax = doc.itc_cess_amount = 0
+ gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1)
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
+ if tax.account_head in gst_accounts.get('igst_account', []):
+ doc.itc_integrated_tax += flt(tax.base_tax_amount_after_discount_amount)
+ if tax.account_head in gst_accounts.get('sgst_account', []):
+ doc.itc_state_tax += flt(tax.base_tax_amount_after_discount_amount)
+ if tax.account_head in gst_accounts.get('cgst_account', []):
+ doc.itc_central_tax += flt(tax.base_tax_amount_after_discount_amount)
+ if tax.account_head in gst_accounts.get('cess_account', []):
+ doc.itc_cess_amount += flt(tax.base_tax_amount_after_discount_amount)
@frappe.whitelist()
def get_regional_round_off_accounts(company, account_list):
diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js
index 1a7ff2bf5a..444f5dbb8c 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.js
+++ b/erpnext/regional/report/gstr_1/gstr_1.js
@@ -46,7 +46,13 @@ frappe.query_reports["GSTR-1"] = {
"label": __("Type of Business"),
"fieldtype": "Select",
"reqd": 1,
- "options": ["B2B", "B2C Large", "B2C Small", "CDNR", "EXPORT"],
+ "options": [
+ { "value": "B2B", "label": __("B2B Invoices - 4A, 4B, 4C, 6B, 6C") },
+ { "value": "B2C Large", "label": __("B2C(Large) Invoices - 5A, 5B") },
+ { "value": "B2C Small", "label": __("B2C(Small) Invoices - 7") },
+ { "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
+ { "value": "EXPORT", "label": __("Export Invoice - 6A") }
+ ],
"default": "B2B"
}
],
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 808fd3a2cc..1e28a40f81 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -32,6 +32,7 @@ class Gstr1Report(object):
reverse_charge,
return_against,
is_return,
+ is_debit_note,
gst_category,
export_type,
port_code,
@@ -42,7 +43,7 @@ class Gstr1Report(object):
def run(self):
self.get_columns()
- self.gst_accounts = get_gst_accounts(self.filters.company)
+ self.gst_accounts = get_gst_accounts(self.filters.company, only_non_reverse_charge=1)
self.get_invoice_data()
if self.invoices:
@@ -62,9 +63,9 @@ class Gstr1Report(object):
for rate, items in items_based_on_rate.items():
row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items)
- if self.filters.get("type_of_business") == "CDNR":
+ if self.filters.get("type_of_business") == "CDNR-REG":
row.append("Y" if invoice_details.posting_date <= date(2017, 7, 1) else "N")
- row.append("C" if invoice_details.return_against else "R")
+ row.append("C" if invoice_details.is_return else "D")
if taxable_value:
self.data.append(row)
@@ -105,7 +106,7 @@ class Gstr1Report(object):
def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items):
row = []
for fieldname in self.invoice_fields:
- if self.filters.get("type_of_business") == "CDNR" and fieldname == "invoice_value":
+ if self.filters.get("type_of_business") == "CDNR-REG" and fieldname == "invoice_value":
row.append(abs(invoice_details.base_rounded_total) or abs(invoice_details.base_grand_total))
elif fieldname == "invoice_value":
row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total)
@@ -171,7 +172,7 @@ class Gstr1Report(object):
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') AND is_return != 1"
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
@@ -179,19 +180,19 @@ class Gstr1Report(object):
frappe.throw(_("Please set B2C Limit in GST Settings."))
if self.filters.get("type_of_business") == "B2C Large":
- conditions += """ and ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
- and grand_total > {0} and is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
+ conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
+ AND grand_total > {0} AND is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
elif self.filters.get("type_of_business") == "B2C Small":
- conditions += """ and (
+ conditions += """ AND (
SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2)
- or grand_total <= {0}) and is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
+ OR grand_total <= {0}) and is_return != 1 AND gst_category ='Unregistered' """.format(flt(b2c_limit))
- elif self.filters.get("type_of_business") == "CDNR":
- conditions += """ and is_return = 1 """
+ elif self.filters.get("type_of_business") == "CDNR-REG":
+ conditions += """ AND (is_return = 1 OR is_debit_note = 1) AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ')"""
elif self.filters.get("type_of_business") == "EXPORT":
- conditions += """ and is_return !=1 and gst_category = 'Overseas' """
+ conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
return conditions
def get_invoice_items(self):
@@ -403,7 +404,7 @@ class Gstr1Report(object):
"width": 100
}
]
- elif self.filters.get("type_of_business") == "CDNR":
+ elif self.filters.get("type_of_business") == "CDNR-REG":
self.invoice_columns = [
{
"fieldname": "customer_gstin",
@@ -437,6 +438,17 @@ class Gstr1Report(object):
"options": "Sales Invoice",
"width":120
},
+ {
+ "fieldname": "reverse_charge",
+ "label": "Reverse Charge",
+ "fieldtype": "Data"
+ },
+ {
+ "fieldname": "export_type",
+ "label": "Export Type",
+ "fieldtype": "Data",
+ "hidden": 1
+ },
{
"fieldname": "reason_for_issuing_document",
"label": "Reason For Issuing document",
@@ -449,6 +461,11 @@ class Gstr1Report(object):
"fieldtype": "Data",
"width": 120
},
+ {
+ "fieldname": "gst_category",
+ "label": "GST Category",
+ "fieldtype": "Data"
+ },
{
"fieldname": "invoice_value",
"label": "Invoice Value",
@@ -458,10 +475,10 @@ class Gstr1Report(object):
]
self.other_columns = [
{
- "fieldname": "cess_amount",
- "label": "Cess Amount",
- "fieldtype": "Currency",
- "width": 100
+ "fieldname": "cess_amount",
+ "label": "Cess Amount",
+ "fieldtype": "Currency",
+ "width": 100
},
{
"fieldname": "pre_gst",
@@ -589,6 +606,12 @@ def get_json(filters, report_name, data):
out = get_export_json(res)
gst_json["exp"] = out
+ elif filters["type_of_business"] == 'CDNR-REG':
+ for item in report_data[:-1]:
+ res.setdefault(item["customer_gstin"], {}).setdefault(item["invoice_number"],[]).append(item)
+
+ out = get_cdnr_reg_json(res, gstin)
+ gst_json["cdnr"] = out
return {
'report_name': report_name,
@@ -628,7 +651,6 @@ def get_b2b_json(res, gstin):
return out
def get_b2cs_json(data, gstin):
-
company_state_number = gstin[0:2]
out = []
@@ -713,6 +735,54 @@ def get_export_json(res):
return out
+def get_cdnr_reg_json(res, gstin):
+ out = []
+
+ for gst_in in res:
+ cdnr_item, inv = {"ctin": gst_in, "nt": []}, []
+ if not gst_in: continue
+
+ for number, invoice in iteritems(res[gst_in]):
+ if not invoice[0]["place_of_supply"]:
+ frappe.throw(_("""{0} not entered in Invoice {1}.
+ Please update and try again""").format(frappe.bold("Place Of Supply"),
+ frappe.bold(invoice[0]['invoice_number'])))
+
+ inv_item = {
+ "nt_num": invoice[0]["invoice_number"],
+ "nt_dt": getdate(invoice[0]["posting_date"]).strftime('%d-%m-%Y'),
+ "val": abs(flt(invoice[0]["invoice_value"])),
+ "ntty": invoice[0]["document_type"],
+ "pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]),
+ "rchrg": invoice[0]["reverse_charge"],
+ "inv_type": get_invoice_type_for_cdnr(invoice[0])
+ }
+
+ inv_item["itms"] = []
+ for item in invoice:
+ inv_item["itms"].append(get_rate_and_tax_details(item, gstin))
+
+ inv.append(inv_item)
+
+ if not inv: continue
+ cdnr_item["nt"] = inv
+ out.append(cdnr_item)
+
+ return out
+
+def get_invoice_type_for_cdnr(row):
+ if row.get('gst_category') == 'SEZ':
+ if row.get('export_type') == 'WPAY':
+ invoice_type = 'SEWP'
+ else:
+ invoice_type = 'SEWOP'
+ elif row.get('gst_category') == 'Deemed Export':
+ row.invoice_type = 'DE'
+ elif row.get('gst_category') == 'Registered Regular':
+ invoice_type = 'R'
+
+ return invoice_type
+
def get_basic_invoice_detail(row):
return {
"inum": row["invoice_number"],
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 acf4eb371f..cec831d616 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
@@ -241,7 +241,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
send_email() {
const frm = this.events.get_frm();
- const recipients = this.email_dialog.get_values().recipients;
+ const recipients = this.email_dialog.get_values().email_id;
const doc = this.doc || frm.doc;
const print_format = frm.pos_print_format;
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 9957aad019..b24048d1ce 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -90,12 +90,6 @@ frappe.ui.form.on("Company", {
frm.toggle_enable("default_currency", (frm.doc.__onload &&
!frm.doc.__onload.transactions_exist));
- if (frm.has_perm('write')) {
- frm.add_custom_button(__('Create Tax Template'), function() {
- frm.trigger("make_default_tax_template");
- });
- }
-
if (frappe.perm.has_perm("Cost Center", 0, 'read')) {
frm.add_custom_button(__('Cost Centers'), function() {
frappe.set_route('Tree', 'Cost Center', {'company': frm.doc.name});
@@ -121,17 +115,21 @@ frappe.ui.form.on("Company", {
}
if (frm.has_perm('write')) {
- frm.add_custom_button(__('Default Tax Template'), function() {
+ frm.add_custom_button(__('Create Tax Template'), function() {
frm.trigger("make_default_tax_template");
- }, __('Create'));
+ }, __('Manage'));
+ }
+
+ if (frappe.user.has_role('System Manager')) {
+ if (frm.has_perm('write')) {
+ frm.add_custom_button(__('Delete Transactions'), function() {
+ frm.trigger("delete_company_transactions");
+ }, __('Manage'));
+ }
}
}
erpnext.company.set_chart_of_accounts_options(frm.doc);
-
- if (!frappe.user.has_role('System Manager')) {
- frm.get_field("delete_company_transactions").hide();
- }
},
make_default_tax_template: function(frm) {
@@ -145,11 +143,6 @@ frappe.ui.form.on("Company", {
})
},
- onload_post_render: function(frm) {
- if(frm.get_field("delete_company_transactions").$input)
- frm.get_field("delete_company_transactions").$input.addClass("btn-danger");
- },
-
country: function(frm) {
erpnext.company.set_chart_of_accounts_options(frm.doc);
},
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 83cbf475ab..061986d92d 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -99,7 +99,6 @@
"company_description",
"registration_info",
"registration_details",
- "delete_company_transactions",
"lft",
"rgt",
"old_parent"
@@ -666,11 +665,6 @@
"oldfieldname": "registration_details",
"oldfieldtype": "Code"
},
- {
- "fieldname": "delete_company_transactions",
- "fieldtype": "Button",
- "label": "Delete Company Transactions"
- },
{
"fieldname": "lft",
"fieldtype": "Int",
@@ -747,7 +741,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2021-02-16 15:53:37.167589",
+ "modified": "2021-05-07 03:11:28.189740",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
diff --git a/erpnext/setup/doctype/global_defaults/global_defaults.py b/erpnext/setup/doctype/global_defaults/global_defaults.py
index e587217181..a0ba1efb5b 100644
--- a/erpnext/setup/doctype/global_defaults/global_defaults.py
+++ b/erpnext/setup/doctype/global_defaults/global_defaults.py
@@ -59,13 +59,15 @@ class GlobalDefaults(Document):
# Make property setters to hide rounded total fields
for doctype in ("Quotation", "Sales Order", "Sales Invoice", "Delivery Note",
- "Supplier Quotation", "Purchase Order", "Purchase Invoice"):
+ "Supplier Quotation", "Purchase Order", "Purchase Invoice", "Purchase Receipt"):
make_property_setter(doctype, "base_rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False)
make_property_setter(doctype, "base_rounded_total", "print_hide", 1, "Check", validate_fields_for_doctype=False)
make_property_setter(doctype, "rounded_total", "hidden", self.disable_rounded_total, "Check", validate_fields_for_doctype=False)
make_property_setter(doctype, "rounded_total", "print_hide", self.disable_rounded_total, "Check", validate_fields_for_doctype=False)
+ make_property_setter(doctype, "disable_rounded_total", "default", cint(self.disable_rounded_total), "Text", validate_fields_for_doctype=False)
+
def toggle_in_words(self):
self.disable_in_words = cint(self.disable_in_words)
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 933ca8ab3d..a657ecf105 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -268,7 +268,9 @@ erpnext.stock.move_item = function (item, source, target, actual_qty, rate, call
frappe.call({
method: 'erpnext.stock.doctype.stock_entry.stock_entry_utils.make_stock_entry',
args: values,
+ btn: dialog.get_primary_btn(),
freeze: true,
+ freeze_message: __('Creating Stock Entry'),
callback: function (r) {
frappe.show_alert(__('Stock Entry {0} created',
['' + r.message.name + '']));
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index d326a04173..cce51cb9b1 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -732,7 +732,8 @@ def make_inter_company_transaction(doctype, source_name, target_doc=None):
"doctype": target_doctype,
"postprocess": update_details,
"field_no_map": [
- "taxes_and_charges"
+ "taxes_and_charges",
+ "set_warehouse"
]
},
doctype +" Item": {
diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
index 4fcdb4c10c..9c59c13ac0 100644
--- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
+++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
@@ -10,8 +10,8 @@
"exchange_rate",
"description",
"col_break3",
- "base_amount",
- "amount"
+ "amount",
+ "base_amount"
],
"fields": [
{
@@ -59,7 +59,7 @@
{
"fieldname": "base_amount",
"fieldtype": "Currency",
- "label": "Base Amount",
+ "label": "Amount (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
}
@@ -67,7 +67,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-12-26 01:07:23.233604",
+ "modified": "2021-05-17 13:57:10.807980",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Taxes and Charges",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 61e60f3922..f1292d8cbd 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -243,16 +243,23 @@ class PurchaseReceipt(BuyingController):
def get_gl_entries(self, warehouse_account=None):
from erpnext.accounts.general_ledger import process_gl_map
+ gl_entries = []
+ self.make_item_gl_entries(gl_entries, warehouse_account=warehouse_account)
+ self.make_tax_gl_entries(gl_entries)
+ self.get_asset_gl_entry(gl_entries)
+
+ return process_gl_map(gl_entries)
+
+ def make_item_gl_entries(self, gl_entries, warehouse_account=None):
stock_rbnb = self.get_company_default("stock_received_but_not_billed")
landed_cost_entries = get_item_account_wise_additional_cost(self.name)
expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
auto_accounting_for_non_stock_items = cint(frappe.db.get_value('Company', self.company, 'enable_perpetual_inventory_for_non_stock_items'))
- gl_entries = []
warehouse_with_no_account = []
- negative_expense_to_be_booked = 0.0
stock_items = self.get_stock_items()
+
for d in self.get("items"):
if d.item_code in stock_items and flt(d.valuation_rate) and flt(d.qty):
if warehouse_account.get(d.warehouse):
@@ -263,21 +270,22 @@ class PurchaseReceipt(BuyingController):
if not stock_value_diff:
continue
+ warehouse_account_name = warehouse_account[d.warehouse]["account"]
+ warehouse_account_currency = warehouse_account[d.warehouse]["account_currency"]
+ supplier_warehouse_account = warehouse_account.get(self.supplier_warehouse, {}).get("account")
+ supplier_warehouse_account_currency = warehouse_account.get(self.supplier_warehouse, {}).get("account_currency")
+ remarks = self.get("remarks") or _("Accounting Entry for Stock")
+
# If PR is sub-contracted and fg item rate is zero
- # in that case if account for shource and target warehouse are same,
+ # in that case if account for source and target warehouse are same,
# then GL entries should not be posted
if flt(stock_value_diff) == flt(d.rm_supp_cost) \
and warehouse_account.get(self.supplier_warehouse) \
- and warehouse_account[d.warehouse]["account"] == warehouse_account[self.supplier_warehouse]["account"]:
+ and warehouse_account_name == supplier_warehouse_account:
continue
- gl_entries.append(self.get_gl_dict({
- "account": warehouse_account[d.warehouse]["account"],
- "against": stock_rbnb,
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": stock_value_diff
- }, warehouse_account[d.warehouse]["account_currency"], item=d))
+ self.add_gl_entry(gl_entries, warehouse_account_name, d.cost_center, stock_value_diff, 0.0, remarks,
+ stock_rbnb, account_currency=warehouse_account_currency, item=d)
# GL Entry for from warehouse or Stock Received but not billed
# Intentionally passed negative debit amount to avoid incorrect GL Entry validation
@@ -287,43 +295,28 @@ class PurchaseReceipt(BuyingController):
credit_amount = flt(d.base_net_amount, d.precision("base_net_amount")) \
if credit_currency == self.company_currency else flt(d.net_amount, d.precision("net_amount"))
if credit_amount:
- gl_entries.append(self.get_gl_dict({
- "account": warehouse_account[d.from_warehouse]['account'] \
- if d.from_warehouse else stock_rbnb,
- "against": warehouse_account[d.warehouse]["account"],
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": -1 * flt(d.base_net_amount, d.precision("base_net_amount")),
- "debit_in_account_currency": -1 * credit_amount
- }, credit_currency, item=d))
+ account = warehouse_account[d.from_warehouse]['account'] \
+ if d.from_warehouse else stock_rbnb
- negative_expense_to_be_booked += flt(d.item_tax_amount)
+ self.add_gl_entry(gl_entries, account, d.cost_center,
+ -1 * flt(d.base_net_amount, d.precision("base_net_amount")), 0.0, remarks, warehouse_account_name,
+ debit_in_account_currency=-1 * credit_amount, account_currency=credit_currency, item=d)
- # Amount added through landed-cost-voucher
+ # Amount added through landed-cos-voucher
if d.landed_cost_voucher_amount and landed_cost_entries:
for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]):
account_currency = get_account_currency(account)
- gl_entries.append(self.get_gl_dict({
- "account": account,
- "account_currency": account_currency,
- "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"]) 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))
+ credit_amount = (flt(amount["base_amount"]) if (amount["base_amount"] or
+ account_currency!=self.company_currency) else flt(amount["amount"]))
+
+ self.add_gl_entry(gl_entries, account, d.cost_center, 0.0, credit_amount, remarks,
+ warehouse_account_name, credit_in_account_currency=flt(amount["amount"]),
+ account_currency=account_currency, project=d.project, item=d)
# sub-contracting warehouse
if flt(d.rm_supp_cost) and warehouse_account.get(self.supplier_warehouse):
- gl_entries.append(self.get_gl_dict({
- "account": warehouse_account[self.supplier_warehouse]["account"],
- "against": warehouse_account[d.warehouse]["account"],
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(d.rm_supp_cost)
- }, warehouse_account[self.supplier_warehouse]["account_currency"], item=d))
+ self.add_gl_entry(gl_entries, supplier_warehouse_account, d.cost_center, 0.0, flt(d.rm_supp_cost),
+ remarks, warehouse_account_name, account_currency=supplier_warehouse_account_currency, item=d)
# divisional loss adjustment
valuation_amount_as_per_doc = flt(d.base_net_amount, d.precision("base_net_amount")) + \
@@ -340,46 +333,32 @@ class PurchaseReceipt(BuyingController):
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": cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": divisional_loss,
- "project": d.project
- }, credit_currency, item=d))
+ self.add_gl_entry(gl_entries, loss_account, cost_center, divisional_loss, 0.0, remarks,
+ warehouse_account_name, account_currency=credit_currency, project=d.project, item=d)
elif d.warehouse not in warehouse_with_no_account or \
d.rejected_warehouse not in warehouse_with_no_account:
warehouse_with_no_account.append(d.warehouse)
elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items:
-
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
credit_currency = get_account_currency(service_received_but_not_billed_account)
-
- gl_entries.append(self.get_gl_dict({
- "account": service_received_but_not_billed_account,
- "against": d.expense_account,
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Service"),
- "project": d.project,
- "credit": d.amount,
- "voucher_detail_no": d.name
- }, credit_currency, item=d))
-
debit_currency = get_account_currency(d.expense_account)
+ remarks = self.get("remarks") or _("Accounting Entry for Service")
- gl_entries.append(self.get_gl_dict({
- "account": d.expense_account,
- "against": service_received_but_not_billed_account,
- "cost_center": d.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Service"),
- "project": d.project,
- "debit": d.amount,
- "voucher_detail_no": d.name
- }, debit_currency, item=d))
+ self.add_gl_entry(gl_entries, service_received_but_not_billed_account, d.cost_center, 0.0, d.amount,
+ remarks, d.expense_account, account_currency=credit_currency, project=d.project,
+ voucher_detail_no=d.name, item=d)
- self.get_asset_gl_entry(gl_entries)
+ self.add_gl_entry(gl_entries, d.expense_account, d.cost_center, d.amount, 0.0, remarks, service_received_but_not_billed_account,
+ account_currency = debit_currency, project=d.project, voucher_detail_no=d.name, item=d)
+
+ if warehouse_with_no_account:
+ frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" +
+ "\n".join(warehouse_with_no_account))
+
+ def make_tax_gl_entries(self, gl_entries):
+ expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
+ negative_expense_to_be_booked = sum([flt(d.item_tax_amount) for d in self.get('items')])
# Cost center-wise amount breakup for other charges included for valuation
valuation_tax = {}
for tax in self.get("taxes"):
@@ -420,23 +399,33 @@ class PurchaseReceipt(BuyingController):
applicable_amount = negative_expense_to_be_booked * (valuation_tax[tax.name] / total_valuation_amount)
amount_including_divisional_loss -= applicable_amount
- gl_entries.append(
- self.get_gl_dict({
- "account": account,
- "cost_center": tax.cost_center,
- "credit": applicable_amount,
- "remarks": self.remarks or _("Accounting Entry for Stock"),
- "against": against_account
- }, item=tax)
- )
+ self.add_gl_entry(gl_entries, account, tax.cost_center, 0.0, applicable_amount, self.remarks or _("Accounting Entry for Stock"),
+ against_account, item=tax)
i += 1
- if warehouse_with_no_account:
- frappe.msgprint(_("No accounting entries for the following warehouses") + ": \n" +
- "\n".join(warehouse_with_no_account))
+ def add_gl_entry(self, gl_entries, account, cost_center, debit, credit, remarks, against_account,
+ debit_in_account_currency=None, credit_in_account_currency=None, account_currency=None,
+ project=None, voucher_detail_no=None, item=None):
+ gl_entry = {
+ "account": account,
+ "cost_center": cost_center,
+ "debit": debit,
+ "credit": credit,
+ "against_account": against_account,
+ "remarks": remarks,
+ }
- return process_gl_map(gl_entries)
+ if voucher_detail_no:
+ gl_entry.update({"voucher_detail_no": voucher_detail_no})
+
+ if debit_in_account_currency:
+ gl_entry.update({"debit_in_account_currency": debit_in_account_currency})
+
+ if credit_in_account_currency:
+ gl_entry.update({"credit_in_account_currency": credit_in_account_currency})
+
+ gl_entries.append(self.get_gl_dict(gl_entry, item=item))
def get_asset_gl_entry(self, gl_entries):
for item in self.get("items"):
@@ -458,30 +447,21 @@ class PurchaseReceipt(BuyingController):
asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate)
base_asset_amount = flt(item.base_net_amount + item.item_tax_amount)
+ remarks = self.get("remarks") or _("Accounting Entry for Asset")
cwip_account_currency = get_account_currency(cwip_account)
# debit cwip account
- gl_entries.append(self.get_gl_dict({
- "account": cwip_account,
- "against": arbnb_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
- "debit": base_asset_amount,
- "debit_in_account_currency": (base_asset_amount
- if cwip_account_currency == self.company_currency else asset_amount)
- }, item=item))
+ debit_in_account_currency = (base_asset_amount
+ if cwip_account_currency == self.company_currency else asset_amount)
+ self.add_gl_entry(gl_entries, cwip_account, item.cost_center, base_asset_amount, 0.0, remarks,
+ arbnb_account, debit_in_account_currency=debit_in_account_currency, item=item)
asset_rbnb_currency = get_account_currency(arbnb_account)
# credit arbnb account
- gl_entries.append(self.get_gl_dict({
- "account": arbnb_account,
- "against": cwip_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Asset"),
- "credit": base_asset_amount,
- "credit_in_account_currency": (base_asset_amount
- if asset_rbnb_currency == self.company_currency else asset_amount)
- }, item=item))
+ credit_in_account_currency = (base_asset_amount
+ if asset_rbnb_currency == self.company_currency else asset_amount)
+ self.add_gl_entry(gl_entries, arbnb_account, item.cost_center, 0.0, base_asset_amount, remarks,
+ cwip_account, credit_in_account_currency=credit_in_account_currency, item=item)
def add_lcv_gl_entries(self, item, gl_entries):
expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation")
@@ -492,23 +472,13 @@ class PurchaseReceipt(BuyingController):
# This returns company's default cwip account
asset_account = get_asset_account("capital_work_in_progress_account", company=self.company)
- gl_entries.append(self.get_gl_dict({
- "account": expenses_included_in_asset_valuation,
- "against": asset_account,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(item.landed_cost_voucher_amount),
- "project": item.project
- }, item=item))
+ remarks = self.get("remarks") or _("Accounting Entry for Stock")
- gl_entries.append(self.get_gl_dict({
- "account": asset_account,
- "against": expenses_included_in_asset_valuation,
- "cost_center": item.cost_center,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "debit": flt(item.landed_cost_voucher_amount),
- "project": item.project
- }, item=item))
+ self.add_gl_entry(gl_entries, expenses_included_in_asset_valuation, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount),
+ remarks, asset_account, project=item.project, item=item)
+
+ self.add_gl_entry(gl_entries, asset_account, item.cost_center, 0.0, flt(item.landed_cost_voucher_amount),
+ remarks, expenses_included_in_asset_valuation, project=item.project, item=item)
def update_assets(self, item, valuation_rate):
assets = frappe.db.get_all('Asset',
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 63c71891e4..5b626ea345 100644
--- a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -4,8 +4,9 @@
from __future__ import unicode_literals
import frappe, erpnext
+from rq.timeouts import JobTimeoutException
from frappe.model.document import Document
-from frappe.utils import cint, get_link_to_form, add_to_date, now, today
+from frappe.utils import cint, get_link_to_form, add_to_date, now, today, time_diff_in_hours
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
@@ -57,7 +58,8 @@ def repost(doc):
repost_gl_entries(doc)
doc.set_status('Completed')
- except Exception:
+
+ except (Exception, JobTimeoutException):
frappe.db.rollback()
traceback = frappe.get_traceback()
frappe.log_error(traceback)
@@ -113,6 +115,12 @@ def notify_error_to_stock_managers(doc, traceback):
frappe.sendmail(recipients=recipients, subject=subject, message=message)
def repost_entries():
+ job_log = frappe.get_all('Scheduled Job Log', fields = ['status', 'creation'],
+ filters = {'scheduled_job_type': 'repost_item_valuation.repost_entries'}, order_by='creation desc', limit=1)
+
+ if job_log and job_log[0]['status'] == 'Start' and time_diff_in_hours(now(), job_log[0]['creation']) < 2:
+ return
+
riv_entries = get_repost_item_valuation_entries()
for row in riv_entries:
@@ -132,4 +140,4 @@ def get_repost_item_valuation_entries():
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
+ """, date, as_dict=1)
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 772c8df96e..de23e769f8 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -107,6 +107,7 @@ frappe.ui.form.on('Stock Entry', {
frappe.flags.hide_serial_batch_dialog = true;
}
});
+ attach_bom_items(frm.doc.bom_no);
},
setup_quality_inspection: function(frm) {
@@ -311,6 +312,7 @@ frappe.ui.form.on('Stock Entry', {
}
frm.trigger("setup_quality_inspection");
+ attach_bom_items(frm.doc.bom_no)
},
stock_entry_type: function(frm){
@@ -598,7 +600,6 @@ frappe.ui.form.on('Stock Entry', {
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 {
filters:{
@@ -608,12 +609,13 @@ frappe.ui.form.on('Stock Entry', {
}
};
};
- frm.trigger('set_tansit_warehouse');
+ frm.trigger('set_transit_warehouse');
}
},
- set_tansit_warehouse: function(frm) {
- if(frm.doc.add_to_transit && frm.doc.purpose == 'Material Transfer' && !frm.doc.to_warehouse) {
+ set_transit_warehouse: function(frm) {
+ if(frm.doc.add_to_transit && frm.doc.purpose == 'Material Transfer' && !frm.doc.to_warehouse
+ && frm.doc.from_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) => {
@@ -919,6 +921,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
method: "get_items",
callback: function(r) {
if(!r.exc) refresh_field("items");
+ if(me.frm.doc.bom_no) attach_bom_items(me.frm.doc.bom_no)
}
});
}
@@ -982,7 +985,7 @@ erpnext.stock.StockEntry = erpnext.stock.StockController.extend({
},
from_warehouse: function(doc) {
- this.frm.trigger('set_tansit_warehouse');
+ this.frm.trigger('set_transit_warehouse');
this.set_warehouse_in_children(doc.items, "s_warehouse", doc.from_warehouse);
},
@@ -1064,4 +1067,22 @@ erpnext.stock.select_batch_and_serial_no = (frm, item) => {
}
+function attach_bom_items(bom_no) {
+ if (check_should_not_attach_bom_items(bom_no)) return
+ frappe.db.get_doc("BOM",bom_no).then(bom => {
+ const {name, items} = bom
+ erpnext.stock.bom = {name, items:{}}
+ items.forEach(item => {
+ erpnext.stock.bom.items[item.item_code] = item;
+ });
+ });
+}
+
+function check_should_not_attach_bom_items(bom_no) {
+ return (
+ bom_no === undefined ||
+ (erpnext.stock.bom && erpnext.stock.bom.name === bom_no)
+ );
+}
+
$.extend(cur_frm.cscript, new erpnext.stock.StockEntry({frm: cur_frm}));
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index 98c047a09e..7f94591005 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -637,6 +637,8 @@
{
"default": "0",
"depends_on": "eval: doc.purpose=='Material Transfer' && !doc.outgoing_stock_entry",
+ "fetch_from": "stock_entry_type.add_to_transit",
+ "fetch_if_empty": 1,
"fieldname": "add_to_transit",
"fieldtype": "Check",
"label": "Add to Transit",
@@ -655,7 +657,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-09 14:58:13.267321",
+ "modified": "2021-05-21 11:29:11.917161",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json
index 0f2b55ec34..eee38be027 100644
--- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json
+++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.json
@@ -6,7 +6,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "purpose"
+ "purpose",
+ "add_to_transit"
],
"fields": [
{
@@ -18,10 +19,17 @@
"options": "\nMaterial Issue\nMaterial Receipt\nMaterial Transfer\nMaterial Transfer for Manufacture\nMaterial Consumption for Manufacture\nManufacture\nRepack\nSend to Subcontractor",
"reqd": 1,
"set_only_once": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval: doc.purpose == 'Material Transfer'",
+ "fieldname": "add_to_transit",
+ "fieldtype": "Check",
+ "label": "Add to Transit"
}
],
"links": [],
- "modified": "2020-08-10 23:24:37.160817",
+ "modified": "2021-05-21 11:27:01.144110",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Type",
diff --git a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py
index a4116aba2c..1069ec8713 100644
--- a/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py
+++ b/erpnext/stock/doctype/stock_entry_type/stock_entry_type.py
@@ -7,4 +7,6 @@ from __future__ import unicode_literals
from frappe.model.document import Document
class StockEntryType(Document):
- pass
+ def validate(self):
+ if self.add_to_transit and self.purpose != 'Material Transfer':
+ self.add_to_transit = 0
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 85c7ebe263..6bbba051f9 100644
--- a/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
+++ b/erpnext/stock/doctype/stock_reconciliation_item/stock_reconciliation_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2015-02-17 01:06:05.072764",
"doctype": "DocType",
"document_type": "Other",
@@ -170,6 +171,7 @@
},
{
"default": "0",
+ "depends_on": "allow_zero_valuation_rate",
"fieldname": "allow_zero_valuation_rate",
"fieldtype": "Check",
"label": "Allow Zero Valuation Rate",
@@ -179,7 +181,7 @@
],
"istable": 1,
"links": [],
- "modified": "2021-03-23 11:09:44.407157",
+ "modified": "2021-05-21 12:13:33.041266",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Reconciliation Item",
@@ -189,4 +191,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index 6c84f168fd..2062bddc7c 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -3,8 +3,9 @@
from __future__ import unicode_literals
import frappe, erpnext
-from frappe.utils import cint, nowdate
+from frappe.utils import cint, flt
from frappe import throw, _
+from collections import defaultdict
from frappe.utils.nestedset import NestedSet
from erpnext.stock import get_warehouse_account
from frappe.contacts.address_and_contact import load_address_and_contact
@@ -139,8 +140,6 @@ class Warehouse(NestedSet):
@frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False):
- from erpnext.stock.utils import get_stock_value_from_bin
-
if is_root:
parent = ""
@@ -153,13 +152,48 @@ def get_children(doctype, parent=None, company=None, is_root=False):
warehouses = frappe.get_list(doctype, fields=fields, filters=filters, order_by='name')
+ company_currency = ''
+ if company:
+ company_currency = frappe.get_cached_value('Company', company, 'default_currency')
+
+ warehouse_wise_value = get_warehouse_wise_stock_value(company)
+
# return warehouses
for wh in warehouses:
- wh["balance"] = get_stock_value_from_bin(warehouse=wh.value)
- if company:
- wh["company_currency"] = frappe.db.get_value('Company', company, 'default_currency')
+ wh["balance"] = warehouse_wise_value.get(wh.value)
+ if company_currency:
+ wh["company_currency"] = company_currency
return warehouses
+def get_warehouse_wise_stock_value(company):
+ warehouses = frappe.get_all('Warehouse',
+ fields = ['name', 'parent_warehouse'], filters = {'company': company})
+ parent_warehouse = {d.name : d.parent_warehouse for d in warehouses}
+
+ filters = {'warehouse': ('in', [data.name for data in warehouses])}
+ bin_data = frappe.get_all('Bin', fields = ['sum(stock_value) as stock_value', 'warehouse'],
+ filters = filters, group_by = 'warehouse')
+
+ warehouse_wise_stock_value = defaultdict(float)
+ for row in bin_data:
+ if not row.stock_value:
+ continue
+
+ warehouse_wise_stock_value[row.warehouse] = row.stock_value
+ update_value_in_parent_warehouse(warehouse_wise_stock_value,
+ parent_warehouse, row.warehouse, row.stock_value)
+
+ return warehouse_wise_stock_value
+
+def update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict, warehouse, stock_value):
+ parent_warehouse = parent_warehouse_dict.get(warehouse)
+ if not parent_warehouse:
+ return
+
+ warehouse_wise_stock_value[parent_warehouse] += flt(stock_value)
+ update_value_in_parent_warehouse(warehouse_wise_stock_value, parent_warehouse_dict,
+ parent_warehouse, stock_value)
+
@frappe.whitelist()
def add_node():
from frappe.desk.treeview import make_tree_args
diff --git a/erpnext/stock/doctype/warehouse/warehouse_tree.js b/erpnext/stock/doctype/warehouse/warehouse_tree.js
index 3665c0530f..407d7d1ccd 100644
--- a/erpnext/stock/doctype/warehouse/warehouse_tree.js
+++ b/erpnext/stock/doctype/warehouse/warehouse_tree.js
@@ -20,7 +20,7 @@ frappe.treeview_settings['Warehouse'] = {
onrender: function(node) {
if (node.data && node.data.balance!==undefined) {
$(''
- + format_currency(Math.abs(node.data.balance), node.data.company_currency)
+ + format_currency((node.data.balance), node.data.company_currency)
+ '').insertBefore(node.$ul);
}
}
diff --git a/erpnext/stock/report/item_variant_details/item_variant_details.py b/erpnext/stock/report/item_variant_details/item_variant_details.py
index e8449cc33e..d8563d7927 100644
--- a/erpnext/stock/report/item_variant_details/item_variant_details.py
+++ b/erpnext/stock/report/item_variant_details/item_variant_details.py
@@ -14,47 +14,58 @@ def get_data(item):
if not item:
return []
item_dicts = []
- variants = None
- variant_results = frappe.db.sql("""select name from `tabItem`
- where variant_of = %s""", item, as_dict=1)
+ variant_results = frappe.db.get_all(
+ "Item",
+ fields=["name"],
+ filters={
+ "variant_of": ["=", item],
+ "disabled": 0
+ }
+ )
+
if not variant_results:
- frappe.msgprint(_("There isn't any item variant for the selected item"))
+ frappe.msgprint(_("There aren't any item variants for the selected item"))
return []
else:
- variants = ", ".join([frappe.db.escape(variant['name']) for variant in variant_results])
+ variant_list = [variant['name'] for variant in variant_results]
- order_count_map = get_open_sales_orders_map(variants)
- stock_details_map = get_stock_details_map(variants)
- buying_price_map = get_buying_price_map(variants)
- selling_price_map = get_selling_price_map(variants)
- attr_val_map = get_attribute_values_map(variants)
+ order_count_map = get_open_sales_orders_count(variant_list)
+ stock_details_map = get_stock_details_map(variant_list)
+ buying_price_map = get_buying_price_map(variant_list)
+ selling_price_map = get_selling_price_map(variant_list)
+ attr_val_map = get_attribute_values_map(variant_list)
- attribute_list = [d[0] for d in frappe.db.sql("""select attribute
- from `tabItem Variant Attribute`
- where parent in ({variants}) group by attribute""".format(variants=variants))]
+ attributes = frappe.db.get_all(
+ "Item Variant Attribute",
+ fields=["attribute"],
+ filters={
+ "parent": ["in", variant_list]
+ },
+ group_by="attribute"
+ )
+ attribute_list = [row.get("attribute") for row in attributes]
# Prepare dicts
variant_dicts = [{"variant_name": d['name']} for d in variant_results]
for item_dict in variant_dicts:
- name = item_dict["variant_name"]
+ name = item_dict.get("variant_name")
- for d in attribute_list:
- attr_dict = attr_val_map[name]
- if attr_dict and attr_dict.get(d):
- item_dict[d] = attr_val_map[name][d]
+ for attribute in attribute_list:
+ attr_dict = attr_val_map.get(name)
+ if attr_dict and attr_dict.get(attribute):
+ item_dict[frappe.scrub(attribute)] = attr_val_map.get(name).get(attribute)
- item_dict["Open Orders"] = order_count_map.get(name) or 0
+ item_dict["open_orders"] = order_count_map.get(name) or 0
if stock_details_map.get(name):
- item_dict["Inventory"] = stock_details_map.get(name)["Inventory"] or 0
- item_dict["In Production"] = stock_details_map.get(name)["In Production"] or 0
- item_dict["Available Selling"] = stock_details_map.get(name)["Available Selling"] or 0
+ item_dict["current_stock"] = stock_details_map.get(name)["Inventory"] or 0
+ item_dict["in_production"] = stock_details_map.get(name)["In Production"] or 0
else:
- item_dict["Inventory"] = item_dict["In Production"] = item_dict["Available Selling"] = 0
+ item_dict["current_stock"] = item_dict["in_production"] = 0
- item_dict["Avg. Buying Price List Rate"] = buying_price_map.get(name) or 0
- item_dict["Avg. Selling Price List Rate"] = selling_price_map.get(name) or 0
+ item_dict["avg_buying_price_list_rate"] = buying_price_map.get(name) or 0
+ item_dict["avg_selling_price_list_rate"] = selling_price_map.get(name) or 0
item_dicts.append(item_dict)
@@ -71,117 +82,158 @@ def get_columns(item):
item_doc = frappe.get_doc("Item", item)
- for d in item_doc.attributes:
- columns.append(d.attribute + ":Data:100")
+ for entry in item_doc.attributes:
+ columns.append({
+ "fieldname": frappe.scrub(entry.attribute),
+ "label": entry.attribute,
+ "fieldtype": "Data",
+ "width": 100
+ })
- columns += [_("Avg. Buying Price List Rate") + ":Currency:110", _("Avg. Selling Price List Rate") + ":Currency:110",
- _("Inventory") + ":Float:100", _("In Production") + ":Float:100",
- _("Open Orders") + ":Float:100", _("Available Selling") + ":Float:100"
+ additional_columns = [
+ {
+ "fieldname": "avg_buying_price_list_rate",
+ "label": _("Avg. Buying Price List Rate"),
+ "fieldtype": "Currency",
+ "width": 150
+ },
+ {
+ "fieldname": "avg_selling_price_list_rate",
+ "label": _("Avg. Selling Price List Rate"),
+ "fieldtype": "Currency",
+ "width": 150
+ },
+ {
+ "fieldname": "current_stock",
+ "label": _("Current Stock"),
+ "fieldtype": "Float",
+ "width": 120
+ },
+ {
+ "fieldname": "in_production",
+ "label": _("In Production"),
+ "fieldtype": "Float",
+ "width": 150
+ },
+ {
+ "fieldname": "open_orders",
+ "label": _("Open Sales Orders"),
+ "fieldtype": "Float",
+ "width": 150
+ }
]
+ columns.extend(additional_columns)
return columns
-def get_open_sales_orders_map(variants):
- open_sales_orders = frappe.db.sql("""
- select
- count(*) as count,
- item_code
- from
- `tabSales Order Item`
- where
- docstatus = 1 and
- qty > ifnull(delivered_qty, 0) and
- item_code in ({variants})
- group by
- item_code
- """.format(variants=variants), as_dict=1)
+def get_open_sales_orders_count(variants_list):
+ open_sales_orders = frappe.db.get_list(
+ "Sales Order",
+ fields=[
+ "name",
+ "`tabSales Order Item`.item_code"
+ ],
+ filters=[
+ ["Sales Order", "docstatus", "=", 1],
+ ["Sales Order Item", "item_code", "in", variants_list]
+ ],
+ distinct=1
+ )
order_count_map = {}
- for d in open_sales_orders:
- order_count_map[d["item_code"]] = d["count"]
+ for row in open_sales_orders:
+ item_code = row.get("item_code")
+ if order_count_map.get(item_code) is None:
+ order_count_map[item_code] = 1
+ else:
+ order_count_map[item_code] += 1
return order_count_map
-def get_stock_details_map(variants):
- stock_details = frappe.db.sql("""
- select
- sum(planned_qty) as planned_qty,
- sum(actual_qty) as actual_qty,
- sum(projected_qty) as projected_qty,
- item_code
- from
- `tabBin`
- where
- item_code in ({variants})
- group by
- item_code
- """.format(variants=variants), as_dict=1)
+def get_stock_details_map(variant_list):
+ stock_details = frappe.db.get_all(
+ "Bin",
+ fields=[
+ "sum(planned_qty) as planned_qty",
+ "sum(actual_qty) as actual_qty",
+ "sum(projected_qty) as projected_qty",
+ "item_code",
+ ],
+ filters={
+ "item_code": ["in", variant_list]
+ },
+ group_by="item_code"
+ )
stock_details_map = {}
- for d in stock_details:
- name = d["item_code"]
+ for row in stock_details:
+ name = row.get("item_code")
stock_details_map[name] = {
- "Inventory" :d["actual_qty"],
- "In Production" :d["planned_qty"],
- "Available Selling" :d["projected_qty"]
+ "Inventory": row.get("actual_qty"),
+ "In Production": row.get("planned_qty")
}
return stock_details_map
-def get_buying_price_map(variants):
- buying = frappe.db.sql("""
- select
- avg(price_list_rate) as avg_rate,
- item_code
- from
- `tabItem Price`
- where
- item_code in ({variants}) and buying=1
- group by
- item_code
- """.format(variants=variants), as_dict=1)
+def get_buying_price_map(variant_list):
+ buying = frappe.db.get_all(
+ "Item Price",
+ fields=[
+ "avg(price_list_rate) as avg_rate",
+ "item_code",
+ ],
+ filters={
+ "item_code": ["in", variant_list],
+ "buying": 1
+ },
+ group_by="item_code"
+ )
buying_price_map = {}
- for d in buying:
- buying_price_map[d["item_code"]] = d["avg_rate"]
+ for row in buying:
+ buying_price_map[row.get("item_code")] = row.get("avg_rate")
return buying_price_map
-def get_selling_price_map(variants):
- selling = frappe.db.sql("""
- select
- avg(price_list_rate) as avg_rate,
- item_code
- from
- `tabItem Price`
- where
- item_code in ({variants}) and selling=1
- group by
- item_code
- """.format(variants=variants), as_dict=1)
+def get_selling_price_map(variant_list):
+ selling = frappe.db.get_all(
+ "Item Price",
+ fields=[
+ "avg(price_list_rate) as avg_rate",
+ "item_code",
+ ],
+ filters={
+ "item_code": ["in", variant_list],
+ "selling": 1
+ },
+ group_by="item_code"
+ )
selling_price_map = {}
- for d in selling:
- selling_price_map[d["item_code"]] = d["avg_rate"]
+ for row in selling:
+ selling_price_map[row.get("item_code")] = row.get("avg_rate")
return selling_price_map
-def get_attribute_values_map(variants):
- list_attr = frappe.db.sql("""
- select
- attribute, attribute_value, parent
- from
- `tabItem Variant Attribute`
- where
- parent in ({variants})
- """.format(variants=variants), as_dict=1)
+def get_attribute_values_map(variant_list):
+ attribute_list = frappe.db.get_all(
+ "Item Variant Attribute",
+ fields=[
+ "attribute",
+ "attribute_value",
+ "parent"
+ ],
+ filters={
+ "parent": ["in", variant_list]
+ }
+ )
attr_val_map = {}
- for d in list_attr:
- name = d["parent"]
+ for row in attribute_list:
+ name = row.get("parent")
if not attr_val_map.get(name):
attr_val_map[name] = {}
- attr_val_map[name][d["attribute"]] = d["attribute_value"]
+ attr_val_map[name][row.get("attribute")] = row.get("attribute_value")
return attr_val_map
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 9729987d2d..b2825fc26f 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -232,7 +232,8 @@ class update_entries_after(object):
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)
+ limit 1
+ for update""", args, as_dict=1)
return sle[0] if sle else frappe._dict()
@@ -623,7 +624,7 @@ class update_entries_after(object):
break
# If no entry found with outgoing rate, collapse stack
- if index == None:
+ if index is None: # nosemgrep
new_stock_value = sum((d[0]*d[1] for d in self.wh_data.stock_queue)) - qty_to_pop*outgoing_rate
new_stock_qty = sum((d[0] for d in self.wh_data.stock_queue)) - qty_to_pop
self.wh_data.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
diff --git a/requirements.txt b/requirements.txt
index f1ffeb8f48..32da48e9d5 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -10,4 +10,3 @@ python-youtube~=0.8.0
taxjar~=1.9.2
tweepy~=3.10.0
Unidecode~=1.2.0
-WooCommerce~=3.0.0