diff --git a/.github/helper/install.sh b/.github/helper/install.sh index ac623e947d..85f146d351 100644 --- a/.github/helper/install.sh +++ b/.github/helper/install.sh @@ -37,6 +37,9 @@ sed -i 's/socketio:/# socketio:/g' Procfile 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 670d8d280f..0000000000 --- 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 83d4acfe4a..0000000000 --- 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 166e98a8a2..0000000000 --- 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 ff278408e1..0000000000 --- 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 f2a9b16739..0000000000 --- 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 f477d7c176..0000000000 --- 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 8b21979208..0000000000 --- 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 9cdfb75d0b..0000000000 --- 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 9de6aa94f0..0000000000 --- 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 5f03fb9fd0..0000000000 --- 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 ae73f9cc60..0000000000 --- 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 a00d3cd8ae..0000000000 --- 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 dd667f36c0..0000000000 --- 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 16e490a460..ebb88c9eda 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -10,13 +10,6 @@ jobs: 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 @@ jobs: - 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 4f84b860af..77c0aee195 100644 --- a/.github/workflows/server-tests.yml +++ b/.github/workflows/server-tests.yml @@ -91,6 +91,8 @@ jobs: - 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 79e08b3bba..464cce48d0 100644 --- a/cypress/integration/test_organizational_chart_desktop.js +++ b/cypress/integration/test_organizational_chart_desktop.js @@ -24,7 +24,7 @@ context('Organizational Chart', () => { 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 161fae098a..971ac6d3ef 100644 --- a/cypress/integration/test_organizational_chart_mobile.js +++ b/cypress/integration/test_organizational_chart_mobile.js @@ -25,7 +25,7 @@ context('Organizational Chart Mobile', () => { 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 d6ccd16936..05caafe1c4 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 six import iteritems 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 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch if root_account: root_type = child.get("root_type") - if account_name not in ["account_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 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch 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 @@ def validate_bank_account(coa, bank_account): 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 @@ def build_tree_from_json(chart_template, chart_data=None): ''' 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 5e596f8677..eabe408d64 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 @@ def import_coa(file_name, company): 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 @@ def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0): if not for_validate: forest = build_forest(data) - accounts = build_tree_from_json("", chart_data=forest) # 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 @@ def build_forest(data): 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 4d6e4a2ba0..d6e35c6a50 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 9dae3a7b75..4f26ed43db 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 @@ def get_invoice_customer_map(pos_invoices): 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 8afa0abd36..9c9f37bba2 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 1c9943fd22..508f728b72 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.deferred_revenue import validate_service_stop_date 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 @@ class PurchaseInvoice(BuyingController): 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 @@ class PurchaseInvoice(BuyingController): 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 23123bd853..379bbbe911 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1439,6 +1439,7 @@ class SalesInvoice(SellingController): 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 @@ class SalesInvoice(SellingController): 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 @@ class SalesInvoice(SellingController): 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" + ) + + 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 - elif getdate(doc.due_date) < nowdate: - return True 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 16ef5fc974..c3cb8396d0 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 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N # 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 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers): 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 a600ead9e5..0de2a9854d 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 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, # 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 5bd6e583db..0094bc2eeb 100644 --- a/erpnext/accounts/report/general_ledger/general_ledger.py +++ b/erpnext/accounts/report/general_ledger/general_ledger.py @@ -421,8 +421,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): 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 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map): 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 621b697aca..6a7f2e5b53 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 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map): 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 e9b531ecb8..88c439b4f6 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -1686,17 +1686,58 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype, 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 be843a3386..55e0efaab1 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -33,6 +33,7 @@ class Opportunity(TransactionBase): 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 @@ class Opportunity(TransactionBase): 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 8d6dfa2c1d..8a2da0866e 100644 --- a/erpnext/hr/doctype/employee/test_employee.py +++ b/erpnext/hr/doctype/employee/test_employee.py @@ -55,6 +55,7 @@ def make_employee(user, company=None, **kwargs): "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 164d48b895..b05175200e 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 import _ 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 @@ class EmployeePromotion(Document): 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 b1f66098f0..29d93f348c 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 import _ 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 @@ class EmployeeTransfer(Document): 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 @@ class EmployeeTransfer(Document): 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 @@ class EmployeeTransfer(Document): 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 ad2f3ade05..c0440d09e7 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 @@ class TestEmployeeTransfer(unittest.TestCase): 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 @@ class TestEmployeeTransfer(unittest.TestCase): 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 6bca1368d3..d463b9b62a 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 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type): 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 b6f4cadcc9..0febce1610 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -29,7 +29,15 @@ def set_employee_name(doc): 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 @@ def update_employee(employee, details, date=None, cancel=False): 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 a1df9cfd0e..adb57f9f39 100644 --- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py +++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py @@ -199,12 +199,16 @@ class MaintenanceSchedule(TransactionBase): 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 b9efe9b41e..7e6fc3c4a6 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -424,7 +424,7 @@ class ProductionPlan(Document): 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 a7aec315ff..74bd685b79 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 @@ def get_data(filters): } 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 @@ def get_data(filters): 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 = [] @@ -172,12 +172,6 @@ def get_columns(filters): "options": "Operation", "width": 110 }, - { - "label": _("Employee Name"), - "fieldname": "employee_name", - "fieldtype": "Data", - "width": 110 - }, { "label": _("Total Completed Qty"), "fieldname": "total_completed_qty", 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 afe4aff7f1..7d3d13d98c 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 98% 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 499c49f893..9b544dafa5 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 @@ def run_query(query_args: QueryArgs) -> Data: {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 0000000000..1de472659e --- /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 bd166928bc..e446d6be42 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -294,6 +294,7 @@ erpnext.patches.v13_0.set_operation_time_based_on_operating_cost 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.set_status_in_maintenance_schedule_table 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 0000000000..4395757159 --- /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 0000000000..28fc01beab --- /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 7c0a8eac99..b6377f4006 100644 --- a/erpnext/payroll/doctype/additional_salary/additional_salary.py +++ b/erpnext/payroll/doctype/additional_salary/additional_salary.py @@ -125,27 +125,28 @@ class AdditionalSalary(Document): no_of_days = date_diff(getdate(end_date), getdate(start_date)) + 1 return amount_per_day * no_of_days +@frappe.whitelist() def get_additional_salaries(employee, start_date, end_date, component_type): - 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 393f647cc8..665f0a8297 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 d113e7e569..bee96b6430 100644 --- a/erpnext/payroll/doctype/salary_slip/salary_slip.py +++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py @@ -172,7 +172,6 @@ class SalarySlip(TransactionBase): 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 @@ class SalarySlip(TransactionBase): 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 @@ class SalarySlip(TransactionBase): 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 @@ class SalarySlip(TransactionBase): 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 @@ class SalarySlip(TransactionBase): 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 @@ class SalarySlip(TransactionBase): "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 @@ class SalarySlip(TransactionBase): 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 @@ class SalarySlip(TransactionBase): 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 @@ class SalarySlip(TransactionBase): 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 178cd5c9d0..c4b6a38c4e 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 @@ class TestSalarySlip(unittest.TestCase): # 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 @@ def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure 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 1655b76b98..f615f051f0 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.js +++ b/erpnext/projects/doctype/timesheet/timesheet.js @@ -32,12 +32,12 @@ frappe.ui.form.on("Timesheet", { }; }, - 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 @@ frappe.ui.form.on("Timesheet Detail", { 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 @@ frappe.ui.form.on("Timesheet Detail", { 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 702064fe55..b5a6d8fdf6 100644 --- a/erpnext/public/js/controllers/taxes_and_totals.js +++ b/erpnext/public/js/controllers/taxes_and_totals.js @@ -137,7 +137,9 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { 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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { 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 @@ erpnext.taxes_and_totals = class TaxesAndTotals extends erpnext.payments { 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 7b358195c3..831626aa91 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 @@ erpnext.HierarchyChart = class { 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 23924c5fb6..7d401bab66 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -172,13 +172,6 @@ class Gstr1Report(object): 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 @@ class Gstr1Report(object): 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 3a4e7f6dc4..844a11f139 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 fc5d5c12da..bb53c55737 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -202,7 +202,9 @@ def get_item_warehouse_map(filters, sle): 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 1ea58fed19..4e20b47261 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -21,7 +21,7 @@ def execute(filters=None): 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 @@ def get_sle_conditions(filters): 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 @@ def get_opening_balance(filters, columns): "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),