Merge branch 'develop' into fix-depr-after-sale
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index ac623e9..85f146d 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -37,6 +37,9 @@
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext "${GITHUB_WORKSPACE}"
+
+if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
+
bench start &> bench_run_logs.txt &
bench --site test_site reinstall --yes
bench build --app frappe
diff --git a/.github/helper/semgrep_rules/README.md b/.github/helper/semgrep_rules/README.md
deleted file mode 100644
index 670d8d2..0000000
--- a/.github/helper/semgrep_rules/README.md
+++ /dev/null
@@ -1,38 +0,0 @@
-# Semgrep linting
-
-## What is semgrep?
-Semgrep or "semantic grep" is language agnostic static analysis tool. In simple terms semgrep is syntax-aware `grep`, so unlike regex it doesn't get confused by different ways of writing same thing or whitespaces or code split in multiple lines etc.
-
-Example:
-
-To check if a translate function is using f-string or not the regex would be `r"_\(\s*f[\"']"` while equivalent rule in semgrep would be `_(f"...")`. As semgrep knows grammer of language it takes care of unnecessary whitespace, type of quotation marks etc.
-
-You can read more such examples in `.github/helper/semgrep_rules` directory.
-
-# Why/when to use this?
-We want to maintain quality of contributions, at the same time remembering all the good practices can be pain to deal with while evaluating contributions. Using semgrep if you can translate "best practice" into a rule then it can automate the task for us.
-
-## Running locally
-
-Install semgrep using homebrew `brew install semgrep` or pip `pip install semgrep`.
-
-To run locally use following command:
-
-`semgrep --config=.github/helper/semgrep_rules [file/folder names]`
-
-## Testing
-semgrep allows testing the tests. Refer to this page: https://semgrep.dev/docs/writing-rules/testing-rules/
-
-When writing new rules you should write few positive and few negative cases as shown in the guide and current tests.
-
-To run current tests: `semgrep --test --test-ignore-todo .github/helper/semgrep_rules`
-
-
-## Reference
-
-If you are new to Semgrep read following pages to get started on writing/modifying rules:
-
-- https://semgrep.dev/docs/getting-started/
-- https://semgrep.dev/docs/writing-rules/rule-syntax
-- https://semgrep.dev/docs/writing-rules/pattern-examples/
-- https://semgrep.dev/docs/writing-rules/rule-ideas/#common-use-cases
diff --git a/.github/helper/semgrep_rules/frappe_correctness.py b/.github/helper/semgrep_rules/frappe_correctness.py
deleted file mode 100644
index 83d4acf..0000000
--- a/.github/helper/semgrep_rules/frappe_correctness.py
+++ /dev/null
@@ -1,64 +0,0 @@
-import frappe
-from frappe import _
-
-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'))
- self.status = 'Submitted'
-
-
-# ok: frappe-modifying-but-not-comitting
-def on_submit(self):
- if self.value_of_goods == 0:
- frappe.throw(_('Value of goods cannot be 0'))
- self.status = 'Submitted'
- self.db_set('status', 'Submitted')
-
-# 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)
-
-
-# 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
deleted file mode 100644
index 166e98a..0000000
--- a/.github/helper/semgrep_rules/frappe_correctness.yml
+++ /dev/null
@@ -1,151 +0,0 @@
-# This file specifies rules for correctness according to how frappe doctype data model works.
-
-rules:
-- id: frappe-modifying-but-not-comitting
- patterns:
- - 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: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
- - metavariable-regex:
- metavariable: "$METHOD"
- regex: "(on_submit|on_cancel)"
- message: |
- DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
- languages: [python]
- severity: ERROR
-
-- id: frappe-modifying-but-not-comitting-other-method
- patterns:
- - pattern: |
- class $DOCTYPE(...):
- def $METHOD(self, ...):
- ...
- 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: |
- self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
- languages: [python]
- severity: ERROR
-
-- id: frappe-print-function-in-doctypes
- pattern: print(...)
- message: |
- Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement.
- languages: [python]
- severity: WARNING
- paths:
- include:
- - "*/**/doctype/*"
-
-- id: frappe-modifying-child-tables-while-iterating
- pattern-either:
- - pattern: |
- for $ROW in self.$TABLE:
- ...
- self.remove(...)
- - pattern: |
- for $ROW in self.$TABLE:
- ...
- self.append(...)
- message: |
- Child table being modified while iterating on it.
- languages: [python]
- severity: ERROR
- paths:
- include:
- - "*/**/doctype/*"
-
-- id: frappe-same-key-assigned-twice
- pattern-either:
- - pattern: |
- {..., $X: $A, ..., $X: $B, ...}
- - pattern: |
- dict(..., ($X, $A), ..., ($X, $B), ...)
- - pattern: |
- _dict(..., ($X, $A), ..., ($X, $B), ...)
- message: |
- key `$X` is uselessly assigned twice. This could be a potential bug.
- languages: [python]
- severity: ERROR
-
-
-- id: frappe-manual-commit
- patterns:
- - pattern: frappe.db.commit()
- - pattern-not-inside: |
- try:
- ...
- except ...:
- ...
- message: |
- Manually commiting a transaction is highly discouraged. Read about the transaction model implemented by Frappe Framework before adding manual commits: https://frappeframework.com/docs/user/en/api/database#database-transaction-model If you think manual commit is required then add a comment explaining why and `// nosemgrep` on the same line.
- paths:
- exclude:
- - "**/patches/**"
- - "**/demo/**"
- languages: [python]
- severity: ERROR
diff --git a/.github/helper/semgrep_rules/report.py b/.github/helper/semgrep_rules/report.py
deleted file mode 100644
index ff27840..0000000
--- a/.github/helper/semgrep_rules/report.py
+++ /dev/null
@@ -1,15 +0,0 @@
-from frappe import _
-
-
-# ruleid: frappe-missing-translate-function-in-report-python
-{"label": "Field Label"}
-
-# ruleid: frappe-missing-translate-function-in-report-python
-dict(label="Field Label")
-
-
-# ok: frappe-missing-translate-function-in-report-python
-{"label": _("Field Label")}
-
-# ok: frappe-missing-translate-function-in-report-python
-dict(label=_("Field Label"))
diff --git a/.github/helper/semgrep_rules/report.yml b/.github/helper/semgrep_rules/report.yml
deleted file mode 100644
index f2a9b16..0000000
--- a/.github/helper/semgrep_rules/report.yml
+++ /dev/null
@@ -1,34 +0,0 @@
-rules:
-- id: frappe-missing-translate-function-in-report-python
- paths:
- include:
- - "**/report"
- exclude:
- - "**/regional"
- pattern-either:
- - patterns:
- - pattern: |
- {..., "label": "...", ...}
- - pattern-not: |
- {..., "label": _("..."), ...}
- - patterns:
- - pattern: dict(..., label="...", ...)
- - pattern-not: dict(..., label=_("..."), ...)
- 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]
- severity: ERROR
-
-- id: frappe-translated-values-in-business-logic
- paths:
- include:
- - "**/report"
- patterns:
- - pattern-inside: |
- {..., filters: [...], ...}
- - pattern: |
- {..., options: [..., __("..."), ...], ...}
- message: |
- Using translated values in options field will require you to translate the values while comparing in business logic. Instead of passing translated labels provide objects that contain both label and value. e.g. { label: __("Option value"), value: "Option value"}
- languages: [javascript]
- severity: ERROR
diff --git a/.github/helper/semgrep_rules/security.py b/.github/helper/semgrep_rules/security.py
deleted file mode 100644
index f477d7c..0000000
--- a/.github/helper/semgrep_rules/security.py
+++ /dev/null
@@ -1,6 +0,0 @@
-def function_name(input):
- # ruleid: frappe-codeinjection-eval
- eval(input)
-
-# ok: frappe-codeinjection-eval
-eval("1 + 1")
diff --git a/.github/helper/semgrep_rules/security.yml b/.github/helper/semgrep_rules/security.yml
deleted file mode 100644
index 8b21979..0000000
--- a/.github/helper/semgrep_rules/security.yml
+++ /dev/null
@@ -1,10 +0,0 @@
-rules:
-- id: frappe-codeinjection-eval
- patterns:
- - pattern-not: eval("...")
- - pattern: eval(...)
- message: |
- Detected the use of eval(). eval() can be dangerous if used to evaluate
- dynamic content. Avoid it or use safe_eval().
- languages: [python]
- severity: ERROR
diff --git a/.github/helper/semgrep_rules/translate.js b/.github/helper/semgrep_rules/translate.js
deleted file mode 100644
index 9cdfb75..0000000
--- a/.github/helper/semgrep_rules/translate.js
+++ /dev/null
@@ -1,44 +0,0 @@
-// ruleid: frappe-translation-empty-string
-__("")
-// ruleid: frappe-translation-empty-string
-__('')
-
-// ok: frappe-translation-js-formatting
-__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name]);
-
-// ruleid: frappe-translation-js-formatting
-__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`);
-
-// ok: frappe-translation-js-formatting
-__('This is fine');
-
-
-// ok: frappe-translation-trailing-spaces
-__('This is fine');
-
-// ruleid: frappe-translation-trailing-spaces
-__(' this is not ok ');
-// ruleid: frappe-translation-trailing-spaces
-__('this is not ok ');
-// ruleid: frappe-translation-trailing-spaces
-__(' this is not ok');
-
-// ok: frappe-translation-js-splitting
-__('You have {0} subscribers in your mailing list.', [subscribers.length])
-
-// todoruleid: frappe-translation-js-splitting
-__('You have') + subscribers.length + __('subscribers in your mailing list.')
-
-// ruleid: frappe-translation-js-splitting
-__('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
deleted file mode 100644
index 9de6aa9..0000000
--- a/.github/helper/semgrep_rules/translate.py
+++ /dev/null
@@ -1,61 +0,0 @@
-# Examples taken from https://frappeframework.com/docs/user/en/translations
-# This file is used for testing the tests.
-
-from frappe import _
-
-full_name = "Jon Doe"
-# ok: frappe-translation-python-formatting
-_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)
-
-# ruleid: frappe-translation-python-formatting
-_('Welcome %s, get started with ERPNext in just a few clicks.' % full_name)
-# ruleid: frappe-translation-python-formatting
-_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})
-
-# ruleid: frappe-translation-python-formatting
-_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))
-
-
-subscribers = ["Jon", "Doe"]
-# ok: frappe-translation-python-formatting
-_('You have {0} subscribers in your mailing list.').format(len(subscribers))
-
-# ruleid: frappe-translation-python-splitting
-_('You have') + len(subscribers) + _('subscribers in your mailing list.')
-
-# ruleid: frappe-translation-python-splitting
-_('You have {0} subscribers \
- in your mailing list').format(len(subscribers))
-
-# ok: frappe-translation-python-splitting
-_('You have {0} subscribers') \
- + 'in your mailing list'
-
-# ruleid: frappe-translation-trailing-spaces
-msg = _(" You have {0} pending invoice ")
-# ruleid: frappe-translation-trailing-spaces
-msg = _("You have {0} pending invoice ")
-# ruleid: frappe-translation-trailing-spaces
-msg = _(" You have {0} pending invoice")
-
-# ok: frappe-translation-trailing-spaces
-msg = ' ' + _("You have {0} pending invoices") + ' '
-
-# ruleid: frappe-translation-python-formatting
-_(f"can not format like this - {subscribers}")
-# ruleid: frappe-translation-python-splitting
-_(f"what" + f"this is also not cool")
-
-
-# ruleid: frappe-translation-empty-string
-_("")
-# 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
deleted file mode 100644
index 5f03fb9..0000000
--- a/.github/helper/semgrep_rules/translate.yml
+++ /dev/null
@@ -1,64 +0,0 @@
-rules:
-- id: frappe-translation-empty-string
- pattern-either:
- - pattern: _("")
- - pattern: __("")
- message: |
- Empty string is useless for translation.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python, javascript, json]
- severity: ERROR
-
-- id: frappe-translation-trailing-spaces
- pattern-either:
- - pattern: _("=~/(^[ \t]+|[ \t]+$)/")
- - pattern: __("=~/(^[ \t]+|[ \t]+$)/")
- message: |
- Trailing or leading whitespace not allowed in translate strings.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python, javascript, json]
- severity: ERROR
-
-- id: frappe-translation-python-formatting
- pattern-either:
- - pattern: _("..." % ...)
- - pattern: _("...".format(...))
- - pattern: _(f"...")
- message: |
- Only positional formatters are allowed and formatting should not be done before translating.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [python]
- severity: ERROR
-
-- id: frappe-translation-js-formatting
- patterns:
- - pattern: __(`...`)
- - pattern-not: __("...")
- message: |
- Template strings are not allowed for text formatting.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [javascript, json]
- severity: ERROR
-
-- id: frappe-translation-python-splitting
- pattern-either:
- - pattern: _(...) + _(...)
- - pattern: _("..." + "...")
- - 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
- languages: [python]
- severity: ERROR
-
-- id: frappe-translation-js-splitting
- pattern-either:
- - pattern-regex: '__\([^\)]*[\\]\s+'
- - pattern: __('...' + '...', ...)
- - pattern: __('...') + __('...')
- message: |
- Do not split strings inside translate function. Do not concatenate using translate functions.
- Please refer: https://frappeframework.com/docs/user/en/translations
- languages: [javascript, json]
- severity: ERROR
diff --git a/.github/helper/semgrep_rules/ux.js b/.github/helper/semgrep_rules/ux.js
deleted file mode 100644
index ae73f9c..0000000
--- a/.github/helper/semgrep_rules/ux.js
+++ /dev/null
@@ -1,9 +0,0 @@
-
-// 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
deleted file mode 100644
index a00d3cd..0000000
--- a/.github/helper/semgrep_rules/ux.py
+++ /dev/null
@@ -1,31 +0,0 @@
-import frappe
-from frappe import msgprint, throw, _
-
-
-# ruleid: frappe-missing-translate-function-python
-throw("Error Occured")
-
-# ruleid: frappe-missing-translate-function-python
-frappe.throw("Error Occured")
-
-# ruleid: frappe-missing-translate-function-python
-frappe.msgprint("Useful message")
-
-# ruleid: frappe-missing-translate-function-python
-msgprint("Useful message")
-
-
-# ok: frappe-missing-translate-function-python
-translatedmessage = _("Hello")
-
-# ok: frappe-missing-translate-function-python
-throw(translatedmessage)
-
-# ok: frappe-missing-translate-function-python
-msgprint(translatedmessage)
-
-# ok: frappe-missing-translate-function-python
-msgprint(_("Helpful message"))
-
-# 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
deleted file mode 100644
index dd667f3..0000000
--- a/.github/helper/semgrep_rules/ux.yml
+++ /dev/null
@@ -1,30 +0,0 @@
-rules:
-- id: frappe-missing-translate-function-python
- pattern-either:
- - patterns:
- - pattern: frappe.msgprint("...", ...)
- - pattern-not: frappe.msgprint(_("..."), ...)
- - patterns:
- - pattern: 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]
- 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/linters.yml b/.github/workflows/linters.yml
index 16e490a..ebb88c9 100644
--- a/.github/workflows/linters.yml
+++ b/.github/workflows/linters.yml
@@ -10,13 +10,6 @@
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- - uses: returntocorp/semgrep-action@v1
- env:
- SEMGREP_TIMEOUT: 120
- with:
- config: >-
- r/python.lang.correctness
- .github/helper/semgrep_rules
- name: Set up Python 3.8
uses: actions/setup-python@v2
@@ -25,3 +18,14 @@
- name: Install and Run Pre-commit
uses: pre-commit/action@v2.0.3
+
+ - name: Download Semgrep rules
+ run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
+
+ - uses: returntocorp/semgrep-action@v1
+ env:
+ SEMGREP_TIMEOUT: 120
+ with:
+ config: >-
+ r/python.lang.correctness
+ ./frappe-semgrep-rules/rules
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml
index 4f84b86..77c0aee 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests.yml
@@ -91,6 +91,8 @@
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
+ env:
+ TYPE: server
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage
diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js
index 79e08b3..464cce4 100644
--- a/cypress/integration/test_organizational_chart_desktop.js
+++ b/cypress/integration/test_organizational_chart_desktop.js
@@ -24,7 +24,7 @@
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
cy.get('@input')
.clear({ force: true })
- .type('Test Org Chart{enter}', { force: true })
+ .type('Test Org Chart{downarrow}{enter}', { force: true })
.blur({ force: true });
});
});
diff --git a/cypress/integration/test_organizational_chart_mobile.js b/cypress/integration/test_organizational_chart_mobile.js
index 161fae0..971ac6d 100644
--- a/cypress/integration/test_organizational_chart_mobile.js
+++ b/cypress/integration/test_organizational_chart_mobile.js
@@ -25,7 +25,7 @@
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
cy.get('@input')
.clear({ force: true })
- .type('Test Org Chart{enter}', { force: true })
+ .type('Test Org Chart{downarrow}{enter}', { force: true })
.blur({ force: true });
});
});
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
index d6ccd16..05caafe 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/chart_of_accounts.py
@@ -12,7 +12,7 @@
from unidecode import unidecode
-def create_charts(company, chart_template=None, existing_company=None, custom_chart=None):
+def create_charts(company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None):
chart = custom_chart or get_chart(chart_template, existing_company)
if chart:
accounts = []
@@ -22,7 +22,7 @@
if root_account:
root_type = child.get("root_type")
- if account_name not in ["account_number", "account_type",
+ if account_name not in ["account_name", "account_number", "account_type",
"root_type", "is_group", "tax_rate"]:
account_number = cstr(child.get("account_number")).strip()
@@ -35,7 +35,7 @@
account = frappe.get_doc({
"doctype": "Account",
- "account_name": account_name,
+ "account_name": child.get('account_name') if from_coa_importer else account_name,
"company": company,
"parent_account": parent,
"is_group": is_group,
@@ -213,7 +213,7 @@
return (bank_account in accounts)
@frappe.whitelist()
-def build_tree_from_json(chart_template, chart_data=None):
+def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False):
''' get chart template from its folder and parse the json to be rendered as tree '''
chart = chart_data or get_chart(chart_template)
@@ -226,9 +226,12 @@
''' recursively called to form a parent-child based list of dict from chart template '''
for account_name, child in iteritems(children):
account = {}
- if account_name in ["account_number", "account_type",\
+ if account_name in ["account_name", "account_number", "account_type",\
"root_type", "is_group", "tax_rate"]: continue
+ if from_coa_importer:
+ account_name = child['account_name']
+
account['parent_account'] = parent
account['expandable'] = True if identify_is_group(child) else False
account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
index 5e596f8..eabe408 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -69,7 +69,7 @@
frappe.local.flags.ignore_root_company_validation = True
forest = build_forest(data)
- create_charts(company, custom_chart=forest)
+ create_charts(company, custom_chart=forest, from_coa_importer=True)
# trigger on_update for company to reset default accounts
set_default_accounts(company)
@@ -148,7 +148,7 @@
if not for_validate:
forest = build_forest(data)
- accounts = build_tree_from_json("", chart_data=forest) # returns a list of dict in a tree render-able form
+ accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form
# filter out to show data for the selected node only
accounts = [d for d in accounts if d['parent_account']==parent]
@@ -212,11 +212,14 @@
if not account_name:
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
+ name = account_name
if account_number:
account_number = cstr(account_number).strip()
account_name = "{} - {}".format(account_number, account_name)
charts_map[account_name] = {}
+ charts_map[account_name]['account_name'] = name
+ if account_number: charts_map[account_name]["account_number"] = account_number
if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
if account_type: charts_map[account_name]["account_type"] = account_type
if root_type: charts_map[account_name]["root_type"] = root_type
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
index 4d6e4a2..d6e35c6 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
@@ -180,8 +180,7 @@
"fieldname": "pos_transactions",
"fieldtype": "Table",
"label": "POS Transactions",
- "options": "POS Invoice Reference",
- "reqd": 1
+ "options": "POS Invoice Reference"
},
{
"fieldname": "pos_opening_entry",
@@ -229,7 +228,7 @@
"link_fieldname": "pos_closing_entry"
}
],
- "modified": "2021-05-05 16:59:49.723261",
+ "modified": "2021-10-20 16:19:25.340565",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry",
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 9dae3a7..4f26ed4 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
@@ -246,7 +246,10 @@
return pos_invoice_customer_map
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
- invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices()
+ invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions'))
+ if frappe.flags.in_test and not invoices:
+ invoices = get_all_unconsolidated_invoices()
+
invoice_by_customer = get_invoice_customer_map(invoices)
if len(invoices) >= 10 and closing_entry:
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json
index 8afa0ab..9c9f37b 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.json
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json
@@ -120,6 +120,7 @@
{
"fieldname": "payments",
"fieldtype": "Table",
+ "label": "Payment Methods",
"options": "POS Payment Method",
"reqd": 1
},
@@ -377,7 +378,7 @@
"link_fieldname": "pos_profile"
}
],
- "modified": "2021-02-01 13:52:51.081311",
+ "modified": "2021-10-14 14:17:00.469298",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 1c9943f..508f728 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -15,6 +15,7 @@
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
check_if_return_invoice_linked_with_payment_entry,
+ get_total_in_party_account_currency,
is_overdue,
unlink_inter_company_doc,
update_linked_doc,
@@ -1183,6 +1184,7 @@
return
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
+ total = get_total_in_party_account_currency(self)
if not status:
if self.docstatus == 2:
@@ -1190,9 +1192,9 @@
elif self.docstatus == 1:
if self.is_internal_transfer():
self.status = 'Internal Transfer'
- elif is_overdue(self):
+ elif is_overdue(self, total):
self.status = "Overdue"
- elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
+ elif 0 < outstanding_amount < total:
self.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index 23123bd..379bbbe 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -1439,6 +1439,7 @@
return
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
+ total = get_total_in_party_account_currency(self)
if not status:
if self.docstatus == 2:
@@ -1446,9 +1447,9 @@
elif self.docstatus == 1:
if self.is_internal_transfer():
self.status = 'Internal Transfer'
- elif is_overdue(self):
+ elif is_overdue(self, total):
self.status = "Overdue"
- elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
+ elif 0 < outstanding_amount < total:
self.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
@@ -1475,27 +1476,42 @@
if update:
self.db_set('status', self.status, update_modified = update_modified)
-def is_overdue(doc):
- outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
+def get_total_in_party_account_currency(doc):
+ total_fieldname = (
+ "grand_total"
+ if doc.disable_rounded_total
+ else "rounded_total"
+ )
+ if doc.party_account_currency != doc.currency:
+ total_fieldname = "base_" + total_fieldname
+
+ return flt(doc.get(total_fieldname), doc.precision(total_fieldname))
+
+def is_overdue(doc, total):
+ outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
if outstanding_amount <= 0:
return
- grand_total = flt(doc.grand_total, doc.precision("grand_total"))
- nowdate = getdate()
- if doc.payment_schedule:
- # calculate payable amount till date
- payable_amount = sum(
- payment.payment_amount
- for payment in doc.payment_schedule
- if getdate(payment.due_date) < nowdate
- )
+ today = getdate()
+ if doc.get('is_pos') or not doc.get('payment_schedule'):
+ return getdate(doc.due_date) < today
- if (grand_total - outstanding_amount) < payable_amount:
- return True
+ # calculate payable amount till date
+ payment_amount_field = (
+ "base_payment_amount"
+ if doc.party_account_currency != doc.currency
+ else "payment_amount"
+ )
- elif getdate(doc.due_date) < nowdate:
- return True
+ payable_amount = sum(
+ payment.get(payment_amount_field)
+ for payment in doc.payment_schedule
+ if getdate(payment.due_date) < today
+ )
+
+ return (total - outstanding_amount) < payable_amount
+
def get_discounting_status(sales_invoice):
status = None
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 16ef5fc..c3cb839 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -203,6 +203,9 @@
# then chargeable value is "prev invoices + advances" value which cross the threshold
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
+ if cint(tax_details.round_off_tax_amount):
+ tax_amount = round(tax_amount)
+
return tax_amount, tax_deducted
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
@@ -322,9 +325,6 @@
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
- if cint(tax_details.round_off_tax_amount):
- tds_amount = round(tds_amount)
-
return tds_amount
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index a600ead..0de2a98 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -115,7 +115,7 @@
# opening_value = Aseet - liability - equity
for data in [asset_data, liability_data, equity_data]:
account_name = get_root_account_name(data[0].root_type, company)
- opening_value += get_opening_balance(account_name, data, company)
+ opening_value += (get_opening_balance(account_name, data, company) or 0.0)
opening_balance[company] = opening_value
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 5bd6e58..0094bc2 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -421,8 +421,6 @@
update_value_in_dict(totals, 'closing', gle)
elif gle.posting_date <= to_date:
- update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
- update_value_in_dict(totals, 'total', gle)
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
gle_map[gle.get(group_by)].entries.append(gle)
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
@@ -436,10 +434,11 @@
else:
update_value_in_dict(consolidated_gle, key, gle)
- update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
- update_value_in_dict(totals, 'closing', gle)
-
for key, value in consolidated_gle.items():
+ update_value_in_dict(gle_map[value.get(group_by)].totals, 'total', value)
+ update_value_in_dict(totals, 'total', value)
+ update_value_in_dict(gle_map[value.get(group_by)].totals, 'closing', value)
+ update_value_in_dict(totals, 'closing', value)
entries.append(value)
return totals, entries
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index 621b697..6a7f2e5 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -44,16 +44,16 @@
if rate and tds_deducted:
row = {
- 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier).pan,
- 'supplier': supplier_map.get(supplier).name
+ 'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
+ 'supplier': supplier_map.get(supplier, {}).get('name')
}
if filters.naming_series == 'Naming Series':
- row.update({'supplier_name': supplier_map.get(supplier).supplier_name})
+ row.update({'supplier_name': supplier_map.get(supplier, {}).get('supplier_name')})
row.update({
'section_code': tax_withholding_category,
- 'entity_type': supplier_map.get(supplier).supplier_type,
+ 'entity_type': supplier_map.get(supplier, {}).get('supplier_type'),
'tds_rate': rate,
'total_amount_credited': total_amount_credited,
'tds_deducted': tds_deducted,
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index e9b531e..88c439b 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1686,17 +1686,58 @@
def update_invoice_status():
"""Updates status as Overdue for applicable invoices. Runs daily."""
+ today = getdate()
for doctype in ("Sales Invoice", "Purchase Invoice"):
frappe.db.sql("""
- update `tab{}` as dt set dt.status = 'Overdue'
- where dt.docstatus = 1
- and dt.status != 'Overdue'
- and dt.outstanding_amount > 0
- and (dt.grand_total - dt.outstanding_amount) <
- (select sum(payment_amount) from `tabPayment Schedule` as ps
- where ps.parent = dt.name and ps.due_date < %s)
- """.format(doctype), getdate())
+ UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
+ WHERE invoice.docstatus = 1
+ AND invoice.status REGEXP '^Unpaid|^Partly Paid'
+ AND invoice.outstanding_amount > 0
+ AND (
+ {or_condition}
+ (
+ (
+ CASE
+ WHEN invoice.party_account_currency = invoice.currency
+ THEN (
+ CASE
+ WHEN invoice.disable_rounded_total
+ THEN invoice.grand_total
+ ELSE invoice.rounded_total
+ END
+ )
+ ELSE (
+ CASE
+ WHEN invoice.disable_rounded_total
+ THEN invoice.base_grand_total
+ ELSE invoice.base_rounded_total
+ END
+ )
+ END
+ ) - invoice.outstanding_amount
+ ) < (
+ SELECT SUM(
+ CASE
+ WHEN invoice.party_account_currency = invoice.currency
+ THEN ps.payment_amount
+ ELSE ps.base_payment_amount
+ END
+ )
+ FROM `tabPayment Schedule` ps
+ WHERE ps.parent = invoice.name
+ AND ps.due_date < %(today)s
+ )
+ )
+ """.format(
+ doctype=doctype,
+ or_condition=(
+ "invoice.is_pos AND invoice.due_date < %(today)s OR"
+ if doctype == "Sales Invoice"
+ else ""
+ )
+ ), {"today": today}
+ )
@frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index be843a3..55e0efa 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.py
+++ b/erpnext/crm/doctype/opportunity/opportunity.py
@@ -33,6 +33,7 @@
self.validate_item_details()
self.validate_uom_is_integer("uom", "qty")
self.validate_cust_name()
+ self.map_fields()
if not self.title:
self.title = self.customer_name
@@ -43,6 +44,15 @@
else:
self.calculate_totals()
+ def map_fields(self):
+ for field in self.meta.fields:
+ if not self.get(field.fieldname):
+ try:
+ value = frappe.db.get_value(self.opportunity_from, self.party_name, field.fieldname)
+ frappe.db.set(self, field.fieldname, value)
+ except Exception:
+ continue
+
def calculate_totals(self):
total = base_total = 0
for item in self.get('items'):
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index 8d6dfa2..8a2da08 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -55,6 +55,7 @@
"email": user,
"first_name": user,
"new_password": "password",
+ "send_welcome_email": 0,
"roles": [{"doctype": "Has Role", "role": "Employee"}]
}).insert()
diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py
index 164d48b..b051752 100644
--- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py
+++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py
@@ -9,7 +9,7 @@
from frappe.model.document import Document
from frappe.utils import getdate
-from erpnext.hr.utils import update_employee, validate_active_employee
+from erpnext.hr.utils import update_employee_work_history, validate_active_employee
class EmployeePromotion(Document):
@@ -23,10 +23,10 @@
def on_submit(self):
employee = frappe.get_doc("Employee", self.employee)
- employee = update_employee(employee, self.promotion_details, date=self.promotion_date)
+ employee = update_employee_work_history(employee, self.promotion_details, date=self.promotion_date)
employee.save()
def on_cancel(self):
employee = frappe.get_doc("Employee", self.employee)
- employee = update_employee(employee, self.promotion_details, cancel=True)
+ employee = update_employee_work_history(employee, self.promotion_details, cancel=True)
employee.save()
diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
index b1f6609..29d93f3 100644
--- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
@@ -9,7 +9,7 @@
from frappe.model.document import Document
from frappe.utils import getdate
-from erpnext.hr.utils import update_employee
+from erpnext.hr.utils import update_employee_work_history
class EmployeeTransfer(Document):
@@ -24,7 +24,7 @@
new_employee = frappe.copy_doc(employee)
new_employee.name = None
new_employee.employee_number = None
- new_employee = update_employee(new_employee, self.transfer_details, date=self.transfer_date)
+ new_employee = update_employee_work_history(new_employee, self.transfer_details, date=self.transfer_date)
if self.new_company and self.company != self.new_company:
new_employee.internal_work_history = []
new_employee.date_of_joining = self.transfer_date
@@ -39,7 +39,7 @@
employee.db_set("relieving_date", self.transfer_date)
employee.db_set("status", "Left")
else:
- employee = update_employee(employee, self.transfer_details, date=self.transfer_date)
+ employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date)
if self.new_company and self.company != self.new_company:
employee.company = self.new_company
employee.date_of_joining = self.transfer_date
@@ -56,7 +56,7 @@
employee.status = "Active"
employee.relieving_date = ''
else:
- employee = update_employee(employee, self.transfer_details, cancel=True)
+ employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date, cancel=True)
if self.new_company != self.company:
employee.company = self.company
employee.save()
diff --git a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
index ad2f3ad..c0440d0 100644
--- a/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/test_employee_transfer.py
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import unittest
+from datetime import date
import frappe
from frappe.utils import add_days, getdate
@@ -15,7 +16,12 @@
def setUp(self):
make_employee("employee2@transfers.com")
make_employee("employee3@transfers.com")
- frappe.db.sql("""delete from `tabEmployee Transfer`""")
+ create_company()
+ create_employee()
+ create_employee_transfer()
+
+ def tearDown(self):
+ frappe.db.rollback()
def test_submit_before_transfer_date(self):
transfer_obj = frappe.get_doc({
@@ -57,3 +63,77 @@
self.assertTrue(transfer.new_employee_id)
self.assertEqual(frappe.get_value("Employee", transfer.new_employee_id, "status"), "Active")
self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left")
+
+ def test_employee_history(self):
+ name = frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name")
+ doc = frappe.get_doc("Employee",name)
+ count = 0
+ department = ["Accounts - TC", "Management - TC"]
+ designation = ["Accountant", "Manager"]
+ dt = [getdate("01-10-2021"), date.today()]
+
+ for data in doc.internal_work_history:
+ self.assertEqual(data.department, department[count])
+ self.assertEqual(data.designation, designation[count])
+ self.assertEqual(data.from_date, dt[count])
+ count = count + 1
+
+ data = frappe.db.get_list("Employee Transfer", filters={"employee":name}, fields=["*"])
+ doc = frappe.get_doc("Employee Transfer", data[0]["name"])
+ doc.cancel()
+ employee_doc = frappe.get_doc("Employee",name)
+
+ for data in employee_doc.internal_work_history:
+ self.assertEqual(data.designation, designation[0])
+ self.assertEqual(data.department, department[0])
+ self.assertEqual(data.from_date, dt[0])
+
+def create_employee():
+ doc = frappe.get_doc({
+ "doctype": "Employee",
+ "first_name": "John",
+ "company": "Test Company",
+ "gender": "Male",
+ "date_of_birth": getdate("30-09-1980"),
+ "date_of_joining": getdate("01-10-2021"),
+ "department": "Accounts - TC",
+ "designation": "Accountant"
+ })
+
+ doc.save()
+
+def create_company():
+ exists = frappe.db.exists("Company", "Test Company")
+ if not exists:
+ doc = frappe.get_doc({
+ "doctype": "Company",
+ "company_name": "Test Company",
+ "default_currency": "INR",
+ "country": "India"
+ })
+
+ doc.save()
+
+def create_employee_transfer():
+ doc = frappe.get_doc({
+ "doctype": "Employee Transfer",
+ "employee": frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name"),
+ "transfer_date": date.today(),
+ "transfer_details": [
+ {
+ "property": "Designation",
+ "current": "Accountant",
+ "new": "Manager",
+ "fieldname": "designation"
+ },
+ {
+ "property": "Department",
+ "current": "Accounts - TC",
+ "new": "Management - TC",
+ "fieldname": "department"
+ }
+ ]
+ })
+
+ doc.save()
+ doc.submit()
\ No newline at end of file
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 6bca136..d463b9b 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -182,10 +182,11 @@
records= frappe.db.sql("""
SELECT
employee, leave_type, from_date, to_date, leaves, transaction_name,
- is_carry_forward, is_expired
+ transaction_type, is_carry_forward, is_expired
FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
AND docstatus=1
+ AND transaction_type = 'Leave Allocation'
AND (from_date between %(from_date)s AND %(to_date)s
OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s))
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index b6f4cad..0febce1 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -29,7 +29,15 @@
if doc.employee and not doc.employee_name:
doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name")
-def update_employee(employee, details, date=None, cancel=False):
+def update_employee_work_history(employee, details, date=None, cancel=False):
+ if not employee.internal_work_history and not cancel:
+ employee.append("internal_work_history", {
+ "branch": employee.branch,
+ "designation": employee.designation,
+ "department": employee.department,
+ "from_date": employee.date_of_joining
+ })
+
internal_work_history = {}
for item in details:
field = frappe.get_meta("Employee").get_field(item.fieldname)
@@ -44,11 +52,35 @@
setattr(employee, item.fieldname, new_data)
if item.fieldname in ["department", "designation", "branch"]:
internal_work_history[item.fieldname] = item.new
+
if internal_work_history and not cancel:
internal_work_history["from_date"] = date
employee.append("internal_work_history", internal_work_history)
+
+ if cancel:
+ delete_employee_work_history(details, employee, date)
+
return employee
+def delete_employee_work_history(details, employee, date):
+ filters = {}
+ for d in details:
+ for history in employee.internal_work_history:
+ if d.property == "Department" and history.department == d.new:
+ department = d.new
+ filters["department"] = department
+ if d.property == "Designation" and history.designation == d.new:
+ designation = d.new
+ filters["designation"] = designation
+ if d.property == "Branch" and history.branch == d.new:
+ branch = d.new
+ filters["branch"] = branch
+ if date and date == history.from_date:
+ filters["from_date"] = date
+ if filters:
+ frappe.db.delete("Employee Internal Work History", filters)
+
+
@frappe.whitelist()
def get_employee_fields_label():
fields = []
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index a1df9cf..adb57f9 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -199,12 +199,16 @@
if chk:
throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order))
+ def validate_no_of_visits(self):
+ return len(self.schedules) != sum(d.no_of_visits for d in self.items)
+
def validate(self):
self.validate_end_date_visits()
self.validate_maintenance_detail()
self.validate_dates_with_periodicity()
self.validate_sales_order()
- self.generate_schedule()
+ if not self.schedules or self.validate_no_of_visits():
+ self.generate_schedule()
def on_update(self):
frappe.db.set(self, 'status', 'Draft')
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index b9efe9b..7e6fc3c 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -424,7 +424,7 @@
po = frappe.new_doc('Purchase Order')
po.supplier = supplier
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
- po.is_subcontracted_item = 'Yes'
+ po.is_subcontracted = 'Yes'
for row in po_list:
args = {
'item_code': row.production_item,
diff --git a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
index a7aec31..74bd685 100644
--- a/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
+++ b/erpnext/manufacturing/report/job_card_summary/job_card_summary.py
@@ -24,7 +24,7 @@
}
fields = ["name", "status", "work_order", "production_item", "item_name", "posting_date",
- "total_completed_qty", "workstation", "operation", "employee_name", "total_time_in_mins"]
+ "total_completed_qty", "workstation", "operation", "total_time_in_mins"]
for field in ["work_order", "workstation", "operation", "company"]:
if filters.get(field):
@@ -45,7 +45,7 @@
job_card_time_details = {}
for job_card_data in frappe.get_all("Job Card Time Log",
fields=["min(from_time) as from_time", "max(to_time) as to_time", "parent"],
- filters=job_card_time_filter, group_by="parent", debug=1):
+ filters=job_card_time_filter, group_by="parent"):
job_card_time_details[job_card_data.parent] = job_card_data
res = []
@@ -173,12 +173,6 @@
"width": 110
},
{
- "label": _("Employee Name"),
- "fieldname": "employee_name",
- "fieldtype": "Data",
- "width": 110
- },
- {
"label": _("Total Completed Qty"),
"fieldname": "total_completed_qty",
"fieldtype": "Float",
diff --git a/erpnext/stock/report/process_loss_report/__init__.py b/erpnext/manufacturing/report/process_loss_report/__init__.py
similarity index 100%
rename from erpnext/stock/report/process_loss_report/__init__.py
rename to erpnext/manufacturing/report/process_loss_report/__init__.py
diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.js b/erpnext/manufacturing/report/process_loss_report/process_loss_report.js
similarity index 100%
rename from erpnext/stock/report/process_loss_report/process_loss_report.js
rename to erpnext/manufacturing/report/process_loss_report/process_loss_report.js
diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.json b/erpnext/manufacturing/report/process_loss_report/process_loss_report.json
similarity index 83%
rename from erpnext/stock/report/process_loss_report/process_loss_report.json
rename to erpnext/manufacturing/report/process_loss_report/process_loss_report.json
index afe4aff..7d3d13d 100644
--- a/erpnext/stock/report/process_loss_report/process_loss_report.json
+++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.json
@@ -9,9 +9,9 @@
"filters": [],
"idx": 0,
"is_standard": "Yes",
- "modified": "2021-08-24 16:38:15.233395",
+ "modified": "2021-10-20 22:03:57.606612",
"modified_by": "Administrator",
- "module": "Stock",
+ "module": "Manufacturing",
"name": "Process Loss Report",
"owner": "Administrator",
"prepared_report": 0,
@@ -21,9 +21,6 @@
"roles": [
{
"role": "Manufacturing User"
- },
- {
- "role": "Stock User"
}
]
}
\ No newline at end of file
diff --git a/erpnext/stock/report/process_loss_report/process_loss_report.py b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py
similarity index 97%
rename from erpnext/stock/report/process_loss_report/process_loss_report.py
rename to erpnext/manufacturing/report/process_loss_report/process_loss_report.py
index 499c49f..9b544da 100644
--- a/erpnext/stock/report/process_loss_report/process_loss_report.py
+++ b/erpnext/manufacturing/report/process_loss_report/process_loss_report.py
@@ -111,7 +111,7 @@
{work_order_filter}
GROUP BY
se.work_order
- """.format(**query_args), query_args, as_dict=1, debug=1)
+ """.format(**query_args), query_args, as_dict=1)
def update_data_with_total_pl_value(data: Data) -> None:
for row in data:
diff --git a/erpnext/manufacturing/report/test_reports.py b/erpnext/manufacturing/report/test_reports.py
new file mode 100644
index 0000000..1de4726
--- /dev/null
+++ b/erpnext/manufacturing/report/test_reports.py
@@ -0,0 +1,64 @@
+import unittest
+from typing import List, Tuple
+
+import frappe
+
+from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
+
+DEFAULT_FILTERS = {
+ "company": "_Test Company",
+ "from_date": "2010-01-01",
+ "to_date": "2030-01-01",
+ "warehouse": "_Test Warehouse - _TC",
+}
+
+
+REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
+ ("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}),
+ ("BOM Operations Time", {}),
+ ("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
+ ("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
+ ("Cost of Poor Quality Report", {}),
+ ("Downtime Analysis", {}),
+ (
+ "Exponential Smoothing Forecasting",
+ {
+ "based_on_document": "Sales Order",
+ "based_on_field": "Qty",
+ "no_of_years": 3,
+ "periodicity": "Yearly",
+ "smoothing_constant": 0.3,
+ },
+ ),
+ ("Job Card Summary", {"fiscal_year": "2021-2022"}),
+ ("Production Analytics", {"range": "Monthly"}),
+ ("Quality Inspection Summary", {}),
+ ("Process Loss Report", {}),
+ ("Work Order Stock Report", {}),
+ ("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
+]
+
+
+if frappe.db.a_row_exists("Production Plan"):
+ REPORT_FILTER_TEST_CASES.append(
+ ("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
+ )
+
+OPTIONAL_FILTERS = {
+ "warehouse": "_Test Warehouse - _TC",
+ "item": "_Test Item",
+ "item_group": "_Test Item Group",
+}
+
+
+class TestManufacturingReports(unittest.TestCase):
+ def test_execute_all_manufacturing_reports(self):
+ """Test that all script report in manufacturing modules are executable with supported filters"""
+ for report, filter in REPORT_FILTER_TEST_CASES:
+ execute_script_report(
+ report_name=report,
+ module="Manufacturing",
+ filters=filter,
+ default_filters=DEFAULT_FILTERS,
+ optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
+ )
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index bd16692..e446d6b 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -294,6 +294,7 @@
erpnext.patches.v13_0.validate_options_for_data_field
erpnext.patches.v13_0.create_gst_payment_entry_fields
erpnext.patches.v14_0.delete_shopify_doctypes
+erpnext.patches.v13_0.fix_invoice_statuses
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
erpnext.patches.v14_0.update_opportunity_currency_fields
@@ -307,3 +308,5 @@
erpnext.patches.v13_0.add_default_interview_notification_templates
erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
erpnext.patches.v13_0.requeue_failed_reposts
+erpnext.patches.v13_0.healthcare_deprecation_warning
+erpnext.patches.v14_0.delete_healthcare_doctypes
diff --git a/erpnext/patches/v13_0/fix_invoice_statuses.py b/erpnext/patches/v13_0/fix_invoice_statuses.py
new file mode 100644
index 0000000..4395757
--- /dev/null
+++ b/erpnext/patches/v13_0/fix_invoice_statuses.py
@@ -0,0 +1,113 @@
+import frappe
+from frappe.utils import flt, getdate
+
+from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
+ get_total_in_party_account_currency,
+ is_overdue,
+)
+
+TODAY = getdate()
+
+def execute():
+ # This fix is not related to Party Specific Item,
+ # but it is needed for code introduced after Party Specific Item was
+ # If your DB doesn't have this doctype yet, you should be fine
+ if not frappe.db.exists("DocType", "Party Specific Item"):
+ return
+
+ for doctype in ("Purchase Invoice", "Sales Invoice"):
+ fields = [
+ "name",
+ "status",
+ "due_date",
+ "outstanding_amount",
+ "grand_total",
+ "base_grand_total",
+ "rounded_total",
+ "base_rounded_total",
+ "disable_rounded_total",
+ ]
+ if doctype == "Sales Invoice":
+ fields.append("is_pos")
+
+ invoices_to_update = frappe.get_all(
+ doctype,
+ fields=fields,
+ filters={
+ "docstatus": 1,
+ "status": ("in", (
+ "Overdue",
+ "Overdue and Discounted",
+ "Partly Paid",
+ "Partly Paid and Discounted"
+ )),
+ "outstanding_amount": (">", 0),
+ "modified": (">", "2021-01-01")
+ # an assumption is being made that only invoices modified
+ # after 2021 got affected as incorrectly overdue.
+ # required for performance reasons.
+ }
+ )
+
+ invoices_to_update = {
+ invoice.name: invoice for invoice in invoices_to_update
+ }
+
+ payment_schedule_items = frappe.get_all(
+ "Payment Schedule",
+ fields=(
+ "due_date",
+ "payment_amount",
+ "base_payment_amount",
+ "parent"
+ ),
+ filters={"parent": ("in", invoices_to_update)}
+ )
+
+ for item in payment_schedule_items:
+ invoices_to_update[item.parent].setdefault(
+ "payment_schedule", []
+ ).append(item)
+
+ status_map = {}
+
+ for invoice in invoices_to_update.values():
+ invoice.doctype = doctype
+ doc = frappe.get_doc(invoice)
+ correct_status = get_correct_status(doc)
+ if not correct_status or doc.status == correct_status:
+ continue
+
+ status_map.setdefault(correct_status, []).append(doc.name)
+
+ for status, docs in status_map.items():
+ frappe.db.set_value(
+ doctype, {"name": ("in", docs)},
+ "status",
+ status,
+ update_modified=False
+ )
+
+
+
+def get_correct_status(doc):
+ outstanding_amount = flt(
+ doc.outstanding_amount, doc.precision("outstanding_amount")
+ )
+ total = get_total_in_party_account_currency(doc)
+
+ status = ""
+ if is_overdue(doc, total):
+ status = "Overdue"
+ elif 0 < outstanding_amount < total:
+ status = "Partly Paid"
+ elif outstanding_amount > 0 and getdate(doc.due_date) >= TODAY:
+ status = "Unpaid"
+
+ if not status:
+ return
+
+ if doc.status.endswith(" and Discounted"):
+ status += " and Discounted"
+
+ return status
diff --git a/erpnext/patches/v14_0/delete_healthcare_doctypes.py b/erpnext/patches/v14_0/delete_healthcare_doctypes.py
new file mode 100644
index 0000000..28fc01b
--- /dev/null
+++ b/erpnext/patches/v14_0/delete_healthcare_doctypes.py
@@ -0,0 +1,49 @@
+import frappe
+
+
+def execute():
+ if "healthcare" in frappe.get_installed_apps():
+ return
+
+ frappe.delete_doc("Workspace", "Healthcare", ignore_missing=True, force=True)
+
+ pages = frappe.get_all("Page", {"module": "healthcare"}, pluck='name')
+ for page in pages:
+ frappe.delete_doc("Page", page, ignore_missing=True, force=True)
+
+ reports = frappe.get_all("Report", {"module": "healthcare", "is_standard": "Yes"}, pluck='name')
+ for report in reports:
+ frappe.delete_doc("Report", report, ignore_missing=True, force=True)
+
+ print_formats = frappe.get_all("Print Format", {"module": "healthcare", "standard": "Yes"}, pluck='name')
+ for print_format in print_formats:
+ frappe.delete_doc("Print Format", print_format, ignore_missing=True, force=True)
+
+ frappe.reload_doc("website", "doctype", "website_settings")
+ forms = frappe.get_all("Web Form", {"module": "healthcare", "is_standard": 1}, pluck='name')
+ for form in forms:
+ frappe.delete_doc("Web Form", form, ignore_missing=True, force=True)
+
+ dashboards = frappe.get_all("Dashboard", {"module": "healthcare", "is_standard": 1}, pluck='name')
+ for dashboard in dashboards:
+ frappe.delete_doc("Dashboard", dashboard, ignore_missing=True, force=True)
+
+ dashboards = frappe.get_all("Dashboard Chart", {"module": "healthcare", "is_standard": 1}, pluck='name')
+ for dashboard in dashboards:
+ frappe.delete_doc("Dashboard Chart", dashboard, ignore_missing=True, force=True)
+
+ frappe.reload_doc("desk", "doctype", "number_card")
+ cards = frappe.get_all("Number Card", {"module": "healthcare", "is_standard": 1}, pluck='name')
+ for card in cards:
+ frappe.delete_doc("Number Card", card, ignore_missing=True, force=True)
+
+ titles = ['Lab Test', 'Prescription', 'Patient Appointment']
+ items = frappe.get_all('Portal Menu Item', filters=[['title', 'in', titles]], pluck='name')
+ for item in items:
+ frappe.delete_doc("Portal Menu Item", item, ignore_missing=True, force=True)
+
+ doctypes = frappe.get_all("DocType", {"module": "healthcare", "custom": 0}, pluck='name')
+ for doctype in doctypes:
+ frappe.delete_doc("DocType", doctype, ignore_missing=True)
+
+ frappe.delete_doc("Module Def", "Healthcare", ignore_missing=True, force=True)
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.py b/erpnext/payroll/doctype/additional_salary/additional_salary.py
index 7c0a8ea..b6377f4 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.py
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py
@@ -125,27 +125,28 @@
no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1
return amount_per_day * no_of_days
+@frappe.whitelist()
def get_additional_salaries(employee, start_date, end_date, component_type):
- additional_salary_list = frappe.db.sql("""
- select name, salary_component as component, type, amount,
- overwrite_salary_structure_amount as overwrite,
- deduct_full_tax_on_selected_payroll_date
- from `tabAdditional Salary`
- where employee=%(employee)s
- and docstatus = 1
- and (
- payroll_date between %(from_date)s and %(to_date)s
- or
- from_date <= %(to_date)s and to_date >= %(to_date)s
- )
- and type = %(component_type)s
- order by salary_component, overwrite ASC
- """, {
- 'employee': employee,
- 'from_date': start_date,
- 'to_date': end_date,
- 'component_type': "Earning" if component_type == "earnings" else "Deduction"
- }, as_dict=1)
+ comp_type = 'Earning' if component_type == 'earnings' else 'Deduction'
+
+ additional_sal = frappe.qb.DocType('Additional Salary')
+ component_field = additional_sal.salary_component.as_('component')
+ overwrite_field = additional_sal.overwrite_salary_structure_amount.as_('overwrite')
+
+ additional_salary_list = frappe.qb.from_(
+ additional_sal
+ ).select(
+ additional_sal.name, component_field, additional_sal.type,
+ additional_sal.amount, additional_sal.is_recurring, overwrite_field,
+ additional_sal.deduct_full_tax_on_selected_payroll_date
+ ).where(
+ (additional_sal.employee == employee)
+ & (additional_sal.docstatus == 1)
+ & (additional_sal.type == comp_type)
+ ).where(
+ additional_sal.payroll_date[start_date: end_date]
+ | ((additional_sal.from_date <= end_date) & (additional_sal.to_date >= end_date))
+ ).run(as_dict=True)
additional_salaries = []
components_to_overwrite = []
diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json
index 393f647..665f0a8 100644
--- a/erpnext/payroll/doctype/salary_detail/salary_detail.json
+++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json
@@ -12,6 +12,7 @@
"year_to_date",
"section_break_5",
"additional_salary",
+ "is_recurring_additional_salary",
"statistical_component",
"depends_on_payment_days",
"exempted_from_income_tax",
@@ -235,11 +236,19 @@
"label": "Year To Date",
"options": "currency",
"read_only": 1
- }
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.parenttype=='Salary Slip' && doc.additional_salary",
+ "fieldname": "is_recurring_additional_salary",
+ "fieldtype": "Check",
+ "label": "Is Recurring Additional Salary",
+ "read_only": 1
+ }
],
"istable": 1,
"links": [],
- "modified": "2021-01-14 13:39:15.847158",
+ "modified": "2021-08-30 13:39:15.847158",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Detail",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index d113e7e..bee96b6 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -172,7 +172,6 @@
and employee = %s and name != %s {0}""".format(cond),
(self.start_date, self.end_date, self.employee, self.name))
if ret_exist:
- self.employee = ''
frappe.throw(_("Salary Slip of employee {0} already created for this period").format(self.employee))
else:
for data in self.timesheets:
@@ -630,7 +629,8 @@
get_salary_component_data(additional_salary.component),
additional_salary.amount,
component_type,
- additional_salary
+ additional_salary,
+ is_recurring = additional_salary.is_recurring
)
def add_tax_components(self, payroll_period):
@@ -651,7 +651,7 @@
tax_row = get_salary_component_data(d)
self.update_component_row(tax_row, tax_amount, "deductions")
- def update_component_row(self, component_data, amount, component_type, additional_salary=None):
+ def update_component_row(self, component_data, amount, component_type, additional_salary=None, is_recurring = 0):
component_row = None
for d in self.get(component_type):
if d.salary_component != component_data.salary_component:
@@ -698,6 +698,8 @@
else:
component_row.default_amount = 0
component_row.additional_amount = amount
+
+ component_row.is_recurring_additional_salary = is_recurring
component_row.additional_salary = additional_salary.name
component_row.deduct_full_tax_on_selected_payroll_date = \
additional_salary.deduct_full_tax_on_selected_payroll_date
@@ -894,25 +896,33 @@
amount, additional_amount = earning.default_amount, earning.additional_amount
if earning.is_tax_applicable:
- if additional_amount:
- taxable_earnings += (amount - additional_amount)
- additional_income += additional_amount
- if earning.deduct_full_tax_on_selected_payroll_date:
- additional_income_with_full_tax += additional_amount
- continue
-
if earning.is_flexible_benefit:
flexi_benefits += amount
else:
- taxable_earnings += amount
+ taxable_earnings += (amount - additional_amount)
+ additional_income += additional_amount
+
+ # Get additional amount based on future recurring additional salary
+ if additional_amount and earning.is_recurring_additional_salary:
+ additional_income += self.get_future_recurring_additional_amount(earning.additional_salary,
+ earning.additional_amount) # Used earning.additional_amount to consider the amount for the full month
+
+ if earning.deduct_full_tax_on_selected_payroll_date:
+ additional_income_with_full_tax += additional_amount
if allow_tax_exemption:
for ded in self.deductions:
if ded.exempted_from_income_tax:
- amount = ded.amount
+ amount, additional_amount = ded.amount, ded.additional_amount
if based_on_payment_days:
- amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)[0]
- taxable_earnings -= flt(amount)
+ amount, additional_amount = self.get_amount_based_on_payment_days(ded, joining_date, relieving_date)
+
+ taxable_earnings -= flt(amount - additional_amount)
+ additional_income -= additional_amount
+
+ if additional_amount and ded.is_recurring_additional_salary:
+ additional_income -= self.get_future_recurring_additional_amount(ded.additional_salary,
+ ded.additional_amount) # Used ded.additional_amount to consider the amount for the full month
return frappe._dict({
"taxable_earnings": taxable_earnings,
@@ -921,11 +931,21 @@
"flexi_benefits": flexi_benefits
})
+ def get_future_recurring_additional_amount(self, additional_salary, monthly_additional_amount):
+ future_recurring_additional_amount = 0
+ to_date = frappe.db.get_value("Additional Salary", additional_salary, 'to_date')
+ # future month count excluding current
+ future_recurring_period = (getdate(to_date).month - getdate(self.start_date).month)
+ if future_recurring_period > 0:
+ future_recurring_additional_amount = monthly_additional_amount * future_recurring_period # Used earning.additional_amount to consider the amount for the full month
+ return future_recurring_additional_amount
+
def get_amount_based_on_payment_days(self, row, joining_date, relieving_date):
amount, additional_amount = row.amount, row.additional_amount
if (self.salary_structure and
- cint(row.depends_on_payment_days) and cint(self.total_working_days) and
- (not self.salary_slip_based_on_timesheet or
+ cint(row.depends_on_payment_days) and cint(self.total_working_days)
+ and not (row.additional_salary and row.default_amount) # to identify overwritten additional salary
+ and (not self.salary_slip_based_on_timesheet or
getdate(self.start_date) < joining_date or
(relieving_date and getdate(self.end_date) > relieving_date)
)):
@@ -1244,7 +1264,7 @@
salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as net_sum', 'sum(gross_pay) as gross_sum'],
- filters = {'employee_name' : self.employee_name,
+ filters = {'employee' : self.employee,
'start_date' : ['>=', period_start_date],
'end_date' : ['<', period_end_date],
'name': ['!=', self.name],
@@ -1264,7 +1284,7 @@
first_day_of_the_month = get_first_day(self.start_date)
salary_slip_sum = frappe.get_list('Salary Slip',
fields = ['sum(net_pay) as sum'],
- filters = {'employee_name' : self.employee_name,
+ filters = {'employee' : self.employee,
'start_date' : ['>=', first_day_of_the_month],
'end_date' : ['<', self.start_date],
'name': ['!=', self.name],
@@ -1288,13 +1308,13 @@
INNER JOIN `tabSalary Slip` as salary_slip
ON detail.parent = salary_slip.name
WHERE
- salary_slip.employee_name = %(employee_name)s
+ salary_slip.employee = %(employee)s
AND detail.salary_component = %(component)s
AND salary_slip.start_date >= %(period_start_date)s
AND salary_slip.end_date < %(period_end_date)s
AND salary_slip.name != %(docname)s
AND salary_slip.docstatus = 1""",
- {'employee_name': self.employee_name, 'component': component.salary_component, 'period_start_date': period_start_date,
+ {'employee': self.employee, 'component': component.salary_component, 'period_start_date': period_start_date,
'period_end_date': period_end_date, 'docname': self.name}
)
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 178cd5c..c4b6a38 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -536,6 +536,61 @@
# undelete fixture data
frappe.db.rollback()
+ def test_tax_for_recurring_additional_salary(self):
+ frappe.db.sql("""delete from `tabPayroll Period`""")
+ frappe.db.sql("""delete from `tabSalary Component`""")
+
+ payroll_period = create_payroll_period()
+
+ create_tax_slab(payroll_period, allow_tax_exemption=True)
+
+ employee = make_employee("test_tax@salary.slip")
+ delete_docs = [
+ "Salary Slip",
+ "Additional Salary",
+ "Employee Tax Exemption Declaration",
+ "Employee Tax Exemption Proof Submission",
+ "Employee Benefit Claim",
+ "Salary Structure Assignment"
+ ]
+ for doc in delete_docs:
+ frappe.db.sql("delete from `tab%s` where employee='%s'" % (doc, employee))
+
+ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+
+ salary_structure = make_salary_structure("Stucture to test tax", "Monthly",
+ other_details={"max_benefits": 100000}, test_tax=True,
+ employee=employee, payroll_period=payroll_period)
+
+
+ create_salary_slips_for_payroll_period(employee, salary_structure.name,
+ payroll_period, deduct_random=False, num=3)
+
+ tax_paid = get_tax_paid_in_period(employee)
+
+ annual_tax = 23196.0
+ self.assertEqual(tax_paid, annual_tax)
+
+ frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))
+
+ #------------------------------------
+ # Recurring additional salary
+ start_date = add_months(payroll_period.start_date, 3)
+ end_date = add_months(payroll_period.start_date, 5)
+ create_recurring_additional_salary(employee, "Performance Bonus", 20000, start_date, end_date)
+
+ frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))
+
+ create_salary_slips_for_payroll_period(employee, salary_structure.name,
+ payroll_period, deduct_random=False, num=4)
+
+ tax_paid = get_tax_paid_in_period(employee)
+
+ annual_tax = 32315.0
+ self.assertEqual(tax_paid, annual_tax)
+
+ frappe.db.rollback()
+
def make_activity_for_employee(self):
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
activity_type.billing_rate = 50
@@ -1007,3 +1062,17 @@
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
return salary_slip
+
+def create_recurring_additional_salary(employee, salary_component, amount, from_date, to_date, company=None):
+ frappe.get_doc({
+ "doctype": "Additional Salary",
+ "employee": employee,
+ "company": company or erpnext.get_default_company(),
+ "salary_component": salary_component,
+ "is_recurring": 1,
+ "from_date": from_date,
+ "to_date": to_date,
+ "amount": amount,
+ "type": "Earning",
+ "currency": erpnext.get_default_currency()
+ }).submit()
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index 1655b76..f615f05 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -32,12 +32,12 @@
};
},
- onload: function(frm){
+ onload: function(frm) {
if (frm.doc.__islocal && frm.doc.time_logs) {
calculate_time_and_amount(frm);
}
- if (frm.is_new()) {
+ if (frm.is_new() && !frm.doc.employee) {
set_employee_and_company(frm);
}
},
@@ -283,7 +283,9 @@
calculate_time_and_amount(frm);
},
- activity_type: function(frm, cdt, cdn) {
+ activity_type: function (frm, cdt, cdn) {
+ if (!frappe.get_doc(cdt, cdn).activity_type) return;
+
frappe.call({
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
args: {
@@ -291,10 +293,10 @@
activity_type: frm.selected_doc.activity_type,
currency: frm.doc.currency
},
- callback: function(r){
- if(r.message){
- frappe.model.set_value(cdt, cdn, 'billing_rate', r.message['billing_rate']);
- frappe.model.set_value(cdt, cdn, 'costing_rate', r.message['costing_rate']);
+ callback: function (r) {
+ if (r.message) {
+ frappe.model.set_value(cdt, cdn, "billing_rate", r.message["billing_rate"]);
+ frappe.model.set_value(cdt, cdn, "costing_rate", r.message["costing_rate"]);
calculate_billing_costing_amount(frm, cdt, cdn);
}
}
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 702064f..b5a6d8f 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -137,7 +137,9 @@
var me = this;
$.each(this.frm.doc["taxes"] || [], function(i, tax) {
- tax.item_wise_tax_detail = {};
+ if (!tax.dont_recompute_tax) {
+ tax.item_wise_tax_detail = {};
+ }
var tax_fields = ["total", "tax_amount_after_discount_amount",
"tax_amount_for_current_item", "grand_total_for_current_item",
"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"];
@@ -421,7 +423,9 @@
current_tax_amount = tax_rate * item.qty;
}
- this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
+ if (!tax.dont_recompute_tax) {
+ this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
+ }
return current_tax_amount;
}
@@ -589,7 +593,9 @@
delete tax[fieldname];
});
- tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail);
+ if (!tax.dont_recompute_tax) {
+ tax.item_wise_tax_detail = JSON.stringify(tax.item_wise_tax_detail);
+ }
});
}
}
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
index 7b35819..831626a 100644
--- a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
+++ b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
@@ -334,10 +334,12 @@
if (child_nodes) {
$.each(child_nodes, (_i, data) => {
- this.add_node(node, data);
- setTimeout(() => {
- this.add_connector(node.id, data.id);
- }, 250);
+ if (!$(`[id="${data.id}"]`).length) {
+ this.add_node(node, data);
+ setTimeout(() => {
+ this.add_connector(node.id, data.id);
+ }, 250);
+ }
});
}
}
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 23924c5..7d401ba 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -172,13 +172,6 @@
self.invoices = frappe._dict()
conditions = self.get_conditions()
- company_gstins = get_company_gstin_number(self.filters.get('company'), all_gstins=True)
-
- if company_gstins:
- self.filters.update({
- 'company_gstins': company_gstins
- })
-
invoice_data = frappe.db.sql("""
select
{select_columns}
@@ -242,7 +235,7 @@
elif self.filters.get("type_of_business") == "EXPORT":
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
- conditions += " AND IFNULL(billing_address_gstin, '') NOT IN %(company_gstins)s"
+ conditions += " AND IFNULL(billing_address_gstin, '') != company_gstin"
return conditions
diff --git a/erpnext/setup/doctype/uom/uom.json b/erpnext/setup/doctype/uom/uom.json
index 3a4e7f6..844a11f 100644
--- a/erpnext/setup/doctype/uom/uom.json
+++ b/erpnext/setup/doctype/uom/uom.json
@@ -1,164 +1,82 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:uom_name",
- "beta": 0,
"creation": "2013-01-10 16:34:24",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Setup",
- "editable_grid": 0,
+ "engine": "InnoDB",
+ "field_order": [
+ "enabled",
+ "uom_name",
+ "must_be_whole_number"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fieldname": "uom_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "UOM Name",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "uom_name",
"oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
"unique": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "default": "0",
"description": "Check this to disallow fractions. (for Nos)",
"fieldname": "must_be_whole_number",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Must be Whole Number",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Must be Whole Number"
+ },
+ {
+ "default": "1",
+ "fieldname": "enabled",
+ "fieldtype": "Check",
+ "label": "Enabled"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
"icon": "fa fa-compass",
"idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-29 06:35:56.143361",
+ "links": [],
+ "modified": "2021-10-18 14:07:43.722144",
"modified_by": "Administrator",
"module": "Setup",
"name": "UOM",
+ "naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
"import": 1,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Item Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
- "role": "Stock Manager",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Stock Manager"
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
- "role": "Stock User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Stock User"
}
],
"quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
"show_name_in_global_search": 1,
- "sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "sort_field": "modified",
+ "sort_order": "ASC"
}
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index fc5d5c1..bb53c55 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -202,7 +202,9 @@
value_diff = flt(d.stock_value_difference)
- if d.posting_date < from_date:
+ if d.posting_date < from_date or (d.posting_date == from_date
+ and d.voucher_type == "Stock Reconciliation" and
+ frappe.db.get_value("Stock Reconciliation", d.voucher_no, "purpose") == "Opening Stock"):
qty_dict.opening_qty += qty_diff
qty_dict.opening_val += value_diff
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 1ea58fe..4e20b47 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -21,7 +21,7 @@
items = get_items(filters)
sl_entries = get_stock_ledger_entries(filters, items)
item_details = get_item_details(items, sl_entries, include_uom)
- opening_row = get_opening_balance(filters, columns)
+ opening_row = get_opening_balance(filters, columns, sl_entries)
precision = cint(frappe.db.get_single_value("System Settings", "float_precision"))
data = []
@@ -218,7 +218,7 @@
return "and {}".format(" and ".join(conditions)) if conditions else ""
-def get_opening_balance(filters, columns):
+def get_opening_balance(filters, columns, sl_entries):
if not (filters.item_code and filters.warehouse and filters.from_date):
return
@@ -230,6 +230,15 @@
"posting_time": "00:00:00"
})
+ # check if any SLEs are actually Opening Stock Reconciliation
+ for sle in sl_entries:
+ if (sle.get("voucher_type") == "Stock Reconciliation"
+ and sle.get("date").split()[0] == filters.from_date
+ and frappe.db.get_value("Stock Reconciliation", sle.voucher_no, "purpose") == "Opening Stock"
+ ):
+ last_entry = sle
+ sl_entries.remove(sle)
+
row = {
"item_code": _("'Opening'"),
"qty_after_transaction": last_entry.get("qty_after_transaction", 0),