diff --git a/.github/helper/.flake8_strict b/.github/helper/.flake8_strict
index 4c7f5f82cfb..a79137d7c32 100644
--- a/.github/helper/.flake8_strict
+++ b/.github/helper/.flake8_strict
@@ -1,6 +1,8 @@
[flake8]
ignore =
B007,
+ B009,
+ B010,
B950,
E101,
E111,
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index ac623e947db..85f146d3519 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 670d8d280f2..00000000000
--- 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 83d4acfe4ab..00000000000
--- 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 d9603e89aa4..00000000000
--- a/.github/helper/semgrep_rules/frappe_correctness.yml
+++ /dev/null
@@ -1,133 +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
diff --git a/.github/helper/semgrep_rules/report.py b/.github/helper/semgrep_rules/report.py
deleted file mode 100644
index ff278408e18..00000000000
--- 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 f2a9b167399..00000000000
--- 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 f477d7c1768..00000000000
--- 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 8b219792080..00000000000
--- 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 9cdfb75d0be..00000000000
--- 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 9de6aa94f01..00000000000
--- 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 5f03fb9fd00..00000000000
--- 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 ae73f9cc603..00000000000
--- 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 a00d3cd8aef..00000000000
--- 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 dd667f36c0f..00000000000
--- 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/try-on-f-cloud-button.svg b/.github/try-on-f-cloud-button.svg
new file mode 100644
index 00000000000..fe0bb2c52df
--- /dev/null
+++ b/.github/try-on-f-cloud-button.svg
@@ -0,0 +1,32 @@
+
diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml
index 16e490a4609..ebb88c9edac 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 4f84b860af8..77c0aee195b 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/.github/workflows/ui-tests.yml b/.github/workflows/ui-tests.yml
index 658892c20ef..d765f0482c8 100644
--- a/.github/workflows/ui-tests.yml
+++ b/.github/workflows/ui-tests.yml
@@ -104,6 +104,8 @@ jobs:
- name: Build Assets
run: cd ~/frappe-bench/ && bench build
+ env:
+ CI: Yes
- name: UI Tests
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 2b3a471f774..b74d9a640da 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -20,6 +20,9 @@ repos:
rev: 3.9.2
hooks:
- id: flake8
+ additional_dependencies: [
+ 'flake8-bugbear',
+ ]
args: ['--config', '.github/helper/.flake8_strict']
exclude: ".*setup.py$"
diff --git a/README.md b/README.md
index 847904d1dd2..1105a970059 100644
--- a/README.md
+++ b/README.md
@@ -39,6 +39,12 @@ ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a
---
+
+
### Containerized Installation
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.
@@ -49,14 +55,6 @@ The Easy Way: our install script for bench will install all dependencies (e.g. M
New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
-### Virtual Image
-
-You can download a virtual image to run ERPNext in a virtual machine on your local system.
-
-- [ERPNext Download](http://erpnext.com/download)
-
-System and user credentials are listed on the download page.
-
---
## License
@@ -77,6 +75,12 @@ The ERPNext code is licensed as GNU General Public License (v3) and the Document
---
+## Learning
+
+1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
+
+---
+
## Logo and Trademark
The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd.
diff --git a/cypress/integration/test_organizational_chart_desktop.js b/cypress/integration/test_organizational_chart_desktop.js
index 79e08b3bbad..464cce48d03 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 161fae098a2..971ac6d3ef3 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/custom/address.py b/erpnext/accounts/custom/address.py
index a6d08d8ff61..551048e50b4 100644
--- a/erpnext/accounts/custom/address.py
+++ b/erpnext/accounts/custom/address.py
@@ -10,6 +10,7 @@ from frappe.contacts.doctype.address.address import (
class ERPNextAddress(Address):
def validate(self):
self.validate_reference()
+ self.update_compnay_address()
super(ERPNextAddress, self).validate()
def link_address(self):
@@ -19,6 +20,11 @@ class ERPNextAddress(Address):
return super(ERPNextAddress, self).link_address()
+ def update_compnay_address(self):
+ for link in self.get('links'):
+ if link.link_doctype == 'Company':
+ self.is_your_company_address = 1
+
def validate_reference(self):
if self.is_your_company_address and not [
row for row in self.links if row.link_doctype == "Company"
diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py
index f6198eb23ba..605262f7b3e 100644
--- a/erpnext/accounts/doctype/account/account.py
+++ b/erpnext/accounts/doctype/account/account.py
@@ -8,6 +8,8 @@ from frappe import _, throw
from frappe.utils import cint, cstr
from frappe.utils.nestedset import NestedSet, get_ancestors_of, get_descendants_of
+import erpnext
+
class RootNotEditable(frappe.ValidationError): pass
class BalanceMismatchError(frappe.ValidationError): pass
@@ -196,7 +198,7 @@ class Account(NestedSet):
"company": company,
# parent account's currency should be passed down to child account's curreny
# if it is None, it picks it up from default company currency, which might be unintended
- "account_currency": self.account_currency,
+ "account_currency": erpnext.get_company_currency(company),
"parent_account": parent_acc_name_map[company]
})
@@ -207,8 +209,7 @@ class Account(NestedSet):
# update the parent company's value in child companies
doc = frappe.get_doc("Account", child_account)
parent_value_changed = False
- for field in ['account_type', 'account_currency',
- 'freeze_account', 'balance_must_be']:
+ for field in ['account_type', 'freeze_account', 'balance_must_be']:
if doc.get(field) != self.get(field):
parent_value_changed = True
doc.set(field, self.get(field))
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 d6ccd169362..05caafe1c47 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/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 7d0ecfbafd9..55ea571ebf8 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -174,7 +174,7 @@
"default": "0",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
- "label": "Automatically Fetch Payment Terms"
+ "label": "Automatically Fetch Payment Terms from Order"
},
{
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@@ -282,7 +282,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-08-19 11:17:38.788054",
+ "modified": "2021-10-11 17:42:36.427699",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
diff --git a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
index 66a269e7a76..d61f8a6c01c 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.js
@@ -10,6 +10,15 @@ frappe.ui.form.on('Chart of Accounts Importer', {
// make company mandatory
frm.set_df_property('company', 'reqd', frm.doc.company ? 0 : 1);
frm.set_df_property('import_file_section', 'hidden', frm.doc.company ? 0 : 1);
+
+ if (frm.doc.import_file) {
+ frappe.run_serially([
+ () => generate_tree_preview(frm),
+ () => create_import_button(frm),
+ () => frm.set_df_property('chart_preview', 'hidden', 0)
+ ]);
+ }
+
frm.set_df_property('chart_preview', 'hidden',
$(frm.fields_dict['chart_tree'].wrapper).html()!="" ? 0 : 1);
},
@@ -72,13 +81,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
if (!frm.doc.import_file) {
frm.page.set_indicator("");
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file
- } else {
- frappe.run_serially([
- () => validate_coa(frm),
- () => generate_tree_preview(frm),
- () => create_import_button(frm),
- () => frm.set_df_property('chart_preview', 'hidden', 0),
- ]);
}
},
@@ -104,26 +106,24 @@ frappe.ui.form.on('Chart of Accounts Importer', {
});
var create_import_button = function(frm) {
- if (frm.page.show_import_button) {
- frm.page.set_primary_action(__("Import"), function () {
- return frappe.call({
- method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
- args: {
- file_name: frm.doc.import_file,
- company: frm.doc.company
- },
- freeze: true,
- freeze_message: __("Creating Accounts..."),
- callback: function(r) {
- if (!r.exc) {
- clearInterval(frm.page["interval"]);
- frm.page.set_indicator(__('Import Successful'), 'blue');
- create_reset_button(frm);
- }
+ frm.page.set_primary_action(__("Import"), function () {
+ return frappe.call({
+ method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
+ args: {
+ file_name: frm.doc.import_file,
+ company: frm.doc.company
+ },
+ freeze: true,
+ freeze_message: __("Creating Accounts..."),
+ callback: function(r) {
+ if (!r.exc) {
+ clearInterval(frm.page["interval"]);
+ frm.page.set_indicator(__('Import Successful'), 'blue');
+ create_reset_button(frm);
}
- });
- }).addClass('btn btn-primary');
- }
+ }
+ });
+ }).addClass('btn btn-primary');
};
var create_reset_button = function(frm) {
@@ -137,7 +137,6 @@ var create_reset_button = function(frm) {
var validate_coa = function(frm) {
if (frm.doc.import_file) {
let parent = __('All Accounts');
-
return frappe.call({
'method': 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
'args': {
@@ -157,25 +156,23 @@ var validate_coa = function(frm) {
};
var generate_tree_preview = function(frm) {
- if (frm.doc.import_file) {
- let parent = __('All Accounts');
- $(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
+ let parent = __('All Accounts');
+ $(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
- // generate tree structure based on the csv data
- return new frappe.ui.Tree({
- parent: $(frm.fields_dict['chart_tree'].wrapper),
- label: parent,
- expandable: true,
- method: 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
- args: {
- file_name: frm.doc.import_file,
- parent: parent,
- doctype: 'Chart of Accounts Importer',
- file_type: frm.doc.file_type
- },
- onclick: function(node) {
- parent = node.value;
- }
- });
- }
+ // generate tree structure based on the csv data
+ return new frappe.ui.Tree({
+ parent: $(frm.fields_dict['chart_tree'].wrapper),
+ label: parent,
+ expandable: true,
+ method: 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
+ args: {
+ file_name: frm.doc.import_file,
+ parent: parent,
+ doctype: 'Chart of Accounts Importer',
+ file_type: frm.doc.file_type
+ },
+ onclick: function(node) {
+ parent = node.value;
+ }
+ });
};
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 bd2a6f1b08a..eabe408d640 100644
--- a/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
+++ b/erpnext/accounts/doctype/chart_of_accounts_importer/chart_of_accounts_importer.py
@@ -25,7 +25,9 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
class ChartofAccountsImporter(Document):
- pass
+ def validate(self):
+ if self.import_file:
+ get_coa('Chart of Accounts Importer', 'All Accounts', file_name=self.import_file, for_validate=1)
def validate_columns(data):
if not data:
@@ -34,7 +36,8 @@ def validate_columns(data):
no_of_columns = max([len(d) for d in data])
if no_of_columns > 7:
- frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'))
+ frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'),
+ title=(_("Wrong Template")))
@frappe.whitelist()
def validate_company(company):
@@ -66,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)
@@ -145,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]
@@ -209,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/loyalty_point_entry/loyalty_point_entry.py b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
index 0813926f5f2..003389e0b51 100644
--- a/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
+++ b/erpnext/accounts/doctype/loyalty_point_entry/loyalty_point_entry.py
@@ -16,7 +16,7 @@ class LoyaltyPointEntry(Document):
def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=None):
if not expiry_date:
- date = today()
+ expiry_date = today()
return frappe.db.sql('''
select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.json b/erpnext/accounts/doctype/payment_entry/payment_entry.json
index 6f362c1fbb9..ee2e319a6fc 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.json
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.json
@@ -27,10 +27,12 @@
"payment_accounts_section",
"party_balance",
"paid_from",
+ "paid_from_account_type",
"paid_from_account_currency",
"paid_from_account_balance",
"column_break_18",
"paid_to",
+ "paid_to_account_type",
"paid_to_account_currency",
"paid_to_account_balance",
"payment_amounts_section",
@@ -440,7 +442,8 @@
"depends_on": "eval:(doc.paid_from && doc.paid_to)",
"fieldname": "reference_no",
"fieldtype": "Data",
- "label": "Cheque/Reference No"
+ "label": "Cheque/Reference No",
+ "mandatory_depends_on": "eval:(doc.paid_from_account_type == 'Bank' || doc.paid_to_account_type == 'Bank')"
},
{
"fieldname": "column_break_23",
@@ -452,6 +455,7 @@
"fieldname": "reference_date",
"fieldtype": "Date",
"label": "Cheque/Reference Date",
+ "mandatory_depends_on": "eval:(doc.paid_from_account_type == 'Bank' || doc.paid_to_account_type == 'Bank')",
"search_index": 1
},
{
@@ -707,15 +711,30 @@
"label": "Received Amount After Tax (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fetch_from": "paid_from.account_type",
+ "fieldname": "paid_from_account_type",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Paid From Account Type"
+ },
+ {
+ "fetch_from": "paid_to.account_type",
+ "fieldname": "paid_to_account_type",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Paid To Account Type"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-07-09 08:58:15.008761",
+ "modified": "2021-10-22 17:50:24.632806",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Entry",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py
index 8037ca16aa0..9b4a91d4e96 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.py
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py
@@ -505,12 +505,13 @@ class PaymentEntry(AccountsController):
def validate_received_amount(self):
if self.paid_from_account_currency == self.paid_to_account_currency:
- if self.paid_amount != self.received_amount:
+ if self.paid_amount < self.received_amount:
frappe.throw(_("Received Amount cannot be greater than Paid Amount"))
def set_received_amount(self):
self.base_received_amount = self.base_paid_amount
- if self.paid_from_account_currency == self.paid_to_account_currency:
+ if self.paid_from_account_currency == self.paid_to_account_currency \
+ and not self.payment_type == 'Internal Transfer':
self.received_amount = self.paid_amount
def set_amounts_after_tax(self):
@@ -712,10 +713,14 @@ class PaymentEntry(AccountsController):
dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit"
for d in self.get("references"):
+ cost_center = self.cost_center
+ if d.reference_doctype == "Sales Invoice" and not cost_center:
+ cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
gle = party_gl_dict.copy()
gle.update({
"against_voucher_type": d.reference_doctype,
- "against_voucher": d.reference_name
+ "against_voucher": d.reference_name,
+ "cost_center": cost_center
})
allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate),
diff --git a/erpnext/accounts/doctype/payment_order/payment_order.js b/erpnext/accounts/doctype/payment_order/payment_order.js
index aa373bc2fcc..9074defa577 100644
--- a/erpnext/accounts/doctype/payment_order/payment_order.js
+++ b/erpnext/accounts/doctype/payment_order/payment_order.js
@@ -10,6 +10,9 @@ frappe.ui.form.on('Payment Order', {
}
}
});
+
+ frm.set_df_property('references', 'cannot_add_rows', true);
+ frm.set_df_property('references', 'cannot_delete_rows', true);
},
refresh: function(frm) {
if (frm.doc.docstatus == 0) {
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
index b1f3e6fd014..412833bd192 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.js
@@ -52,21 +52,35 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
refresh() {
this.frm.disable_save();
+ this.frm.set_df_property('invoices', 'cannot_delete_rows', true);
+ this.frm.set_df_property('payments', 'cannot_delete_rows', true);
+ this.frm.set_df_property('allocation', 'cannot_delete_rows', true);
+
+ this.frm.set_df_property('invoices', 'cannot_add_rows', true);
+ this.frm.set_df_property('payments', 'cannot_add_rows', true);
+ this.frm.set_df_property('allocation', 'cannot_add_rows', true);
+
if (this.frm.doc.receivable_payable_account) {
this.frm.add_custom_button(__('Get Unreconciled Entries'), () =>
this.frm.trigger("get_unreconciled_entries")
);
+ this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
}
if (this.frm.doc.invoices.length && this.frm.doc.payments.length) {
this.frm.add_custom_button(__('Allocate'), () =>
this.frm.trigger("allocate")
);
+ this.frm.change_custom_button_type('Allocate', null, 'primary');
+ this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
}
if (this.frm.doc.allocation.length) {
this.frm.add_custom_button(__('Reconcile'), () =>
this.frm.trigger("reconcile")
);
+ this.frm.change_custom_button_type('Reconcile', null, 'primary');
+ this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
+ this.frm.change_custom_button_type('Allocate', null, 'default');
}
}
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
index 9023b3646f2..eb0c20f92d9 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.json
@@ -12,15 +12,16 @@
"receivable_payable_account",
"col_break1",
"from_invoice_date",
- "to_invoice_date",
- "minimum_invoice_amount",
- "maximum_invoice_amount",
- "invoice_limit",
- "column_break_13",
"from_payment_date",
- "to_payment_date",
+ "minimum_invoice_amount",
"minimum_payment_amount",
+ "column_break_11",
+ "to_invoice_date",
+ "to_payment_date",
+ "maximum_invoice_amount",
"maximum_payment_amount",
+ "column_break_13",
+ "invoice_limit",
"payment_limit",
"bank_cash_account",
"sec_break1",
@@ -79,6 +80,7 @@
},
{
"depends_on": "eval:(doc.payments).length || (doc.invoices).length",
+ "description": "If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order.",
"fieldname": "sec_break1",
"fieldtype": "Section Break",
"label": "Unreconciled Entries"
@@ -163,6 +165,7 @@
"label": "Maximum Payment Amount"
},
{
+ "description": "System will fetch all the entries if limit value is zero.",
"fieldname": "payment_limit",
"fieldtype": "Int",
"label": "Payment Limit"
@@ -171,13 +174,17 @@
"fieldname": "maximum_invoice_amount",
"fieldtype": "Currency",
"label": "Maximum Invoice Amount"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
"icon": "icon-resize-horizontal",
"issingle": 1,
"links": [],
- "modified": "2021-08-30 13:05:51.977861",
+ "modified": "2021-10-04 20:27:11.114194",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation",
diff --git a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
index b8c65eea847..6a21692c6ac 100644
--- a/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
+++ b/erpnext/accounts/doctype/payment_reconciliation_allocation/payment_reconciliation_allocation.json
@@ -14,8 +14,8 @@
"section_break_6",
"allocated_amount",
"unreconciled_amount",
- "amount",
"column_break_8",
+ "amount",
"is_advance",
"section_break_5",
"difference_amount",
@@ -127,12 +127,13 @@
"fieldname": "reference_row",
"fieldtype": "Data",
"hidden": 1,
- "label": "Reference Row"
+ "label": "Reference Row",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2021-09-20 17:23:09.455803",
+ "modified": "2021-10-06 11:48:59.616562",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Payment Reconciliation Allocation",
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 4d6e4a2ba07..d6e35c6a50d 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 9dae3a7b75e..4f26ed43db7 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 8afa0abd36c..9c9f37bba27 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/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
index 83ecfb47bb5..7c53f4a0b07 100644
--- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
@@ -33,7 +33,9 @@ class TestPOSProfile(unittest.TestCase):
frappe.db.sql("delete from `tabPOS Profile`")
-def get_customers_list(pos_profile={}):
+def get_customers_list(pos_profile=None):
+ if pos_profile is None:
+ pos_profile = {}
cond = "1=1"
customer_groups = []
if pos_profile.get('customer_groups'):
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index 12b486e45eb..ef44b414761 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -29,6 +29,9 @@ def get_pricing_rules(args, doc=None):
pricing_rules = []
values = {}
+ if not frappe.db.exists('Pricing Rule', {'disable': 0, args.transaction_type: 1}):
+ return
+
for apply_on in ['Item Code', 'Item Group', 'Brand']:
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
@@ -398,7 +401,9 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules):
pricing_rules[0].apply_rule_on_other_items = items
return pricing_rules
-def get_qty_amount_data_for_cumulative(pr_doc, doc, items=[]):
+def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
+ if items is None:
+ items = []
sum_qty, sum_amt = [0, 0]
doctype = doc.get('parenttype') or doc.doctype
diff --git a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
index d09f7dc2da2..f5391ca4cc9 100644
--- a/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
+++ b/erpnext/accounts/doctype/promotional_scheme/promotional_scheme.py
@@ -69,7 +69,9 @@ class PromotionalScheme(Document):
{'promotional_scheme': self.name}):
frappe.delete_doc('Pricing Rule', rule.name)
-def get_pricing_rules(doc, rules = {}):
+def get_pricing_rules(doc, rules=None):
+ if rules is None:
+ rules = {}
new_doc = []
for child_doc, fields in {'price_discount_slabs': price_discount_fields,
'product_discount_slabs': product_discount_fields}.items():
@@ -78,7 +80,9 @@ def get_pricing_rules(doc, rules = {}):
return new_doc
-def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}):
+def _get_pricing_rules(doc, child_doc, discount_fields, rules=None):
+ if rules is None:
+ rules = {}
new_doc = []
args = get_args_for_pricing_rule(doc)
applicable_for = frappe.scrub(doc.get('applicable_for'))
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 6c74d2b438f..3526e488e11 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -590,5 +590,11 @@ frappe.ui.form.on("Purchase Invoice", {
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+
+ if (frm.doc.company) {
+ frappe.db.get_value('Company', frm.doc.company, 'default_payable_account', (r) => {
+ frm.set_value('credit_to', r.default_payable_account);
+ });
+ }
},
})
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 55e288eeef9..03cbc4acbc4 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -149,16 +149,18 @@
"cb_17",
"hold_comment",
"more_info",
+ "status",
+ "inter_company_invoice_reference",
+ "represents_company",
+ "column_break_147",
+ "is_internal_supplier",
+ "accounting_details_section",
"credit_to",
"party_account_currency",
"is_opening",
"against_expense_account",
"column_break_63",
"unrealized_profit_loss_account",
- "status",
- "inter_company_invoice_reference",
- "is_internal_supplier",
- "represents_company",
"remarks",
"subscription_section",
"from_date",
@@ -1171,6 +1173,15 @@
"options": "fa fa-file-text",
"print_hide": 1
},
+ {
+ "default": "0",
+ "fetch_from": "supplier.is_internal_supplier",
+ "fieldname": "is_internal_supplier",
+ "fieldtype": "Check",
+ "ignore_user_permissions": 1,
+ "label": "Is Internal Supplier",
+ "read_only": 1
+ },
{
"fieldname": "credit_to",
"fieldtype": "Link",
@@ -1196,7 +1207,7 @@
"default": "No",
"fieldname": "is_opening",
"fieldtype": "Select",
- "label": "Is Opening",
+ "label": "Is Opening Entry",
"oldfieldname": "is_opening",
"oldfieldtype": "Select",
"options": "No\nYes",
@@ -1298,15 +1309,6 @@
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
},
- {
- "default": "0",
- "fetch_from": "supplier.is_internal_supplier",
- "fieldname": "is_internal_supplier",
- "fieldtype": "Check",
- "ignore_user_permissions": 1,
- "label": "Is Internal Supplier",
- "read_only": 1
- },
{
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
@@ -1395,13 +1397,24 @@
"hidden": 1,
"label": "Ignore Default Payment Terms Template",
"read_only": 1
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "accounting_details_section",
+ "fieldtype": "Section Break",
+ "label": "Accounting Details",
+ "print_hide": 1
+ },
+ {
+ "fieldname": "column_break_147",
+ "fieldtype": "Column Break"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-28 13:10:28.351810",
+ "modified": "2021-10-12 20:55:16.145651",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 1c9943fd224..508f728b72d 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.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 73e12843047..bee153b7b8f 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -12,6 +12,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
}
company() {
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
+
+ let me = this;
+ if (this.frm.doc.company) {
+ frappe.db.get_value('Company', this.frm.doc.company, 'default_receivable_account', (r) => {
+ me.frm.set_value('debit_to', r.default_receivable_account);
+ });
+ }
}
onload() {
var me = this;
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index f3adb898aa8..93e32f1a18c 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -124,6 +124,13 @@
"total_advance",
"outstanding_amount",
"disable_rounded_total",
+ "column_break4",
+ "write_off_amount",
+ "base_write_off_amount",
+ "write_off_outstanding_amount_automatically",
+ "column_break_74",
+ "write_off_account",
+ "write_off_cost_center",
"advances_section",
"allocate_advances_automatically",
"get_advances",
@@ -144,13 +151,6 @@
"column_break_90",
"change_amount",
"account_for_change_amount",
- "column_break4",
- "write_off_amount",
- "base_write_off_amount",
- "write_off_outstanding_amount_automatically",
- "column_break_74",
- "write_off_account",
- "write_off_cost_center",
"terms_section_break",
"tc_name",
"terms",
@@ -161,14 +161,14 @@
"column_break_84",
"language",
"more_information",
+ "status",
"inter_company_invoice_reference",
- "is_internal_customer",
"represents_company",
"customer_group",
"campaign",
- "is_discounted",
"col_break23",
- "status",
+ "is_internal_customer",
+ "is_discounted",
"source",
"more_info",
"debit_to",
@@ -2031,7 +2031,7 @@
"link_fieldname": "consolidated_invoice"
}
],
- "modified": "2021-10-02 03:36:10.251715",
+ "modified": "2021-10-11 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index eb26aa2afa0..919012448d2 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -229,9 +229,6 @@ class SalesInvoice(SellingController):
# this sequence because outstanding may get -ve
self.make_gl_entries()
- if self.update_stock == 1:
- self.repost_future_sle_and_gle()
-
if self.update_stock == 1:
self.repost_future_sle_and_gle()
@@ -1287,12 +1284,20 @@ class SalesInvoice(SellingController):
serial_nos = item.serial_no or ""
si_serial_nos = set(get_serial_nos(serial_nos))
+ serial_no_diff = si_serial_nos - dn_serial_nos
- if si_serial_nos - dn_serial_nos:
- frappe.throw(_("Serial Numbers in row {0} does not match with Delivery Note").format(item.idx))
+ if serial_no_diff:
+ dn_link = frappe.utils.get_link_to_form("Delivery Note", item.delivery_note)
+ serial_no_msg = ", ".join(frappe.bold(d) for d in serial_no_diff)
+
+ msg = _("Row #{0}: The following Serial Nos are not present in Delivery Note {1}:").format(
+ item.idx, dn_link)
+ msg += " " + serial_no_msg
+
+ frappe.throw(msg=msg, title=_("Serial Nos Mismatch"))
if item.serial_no and cint(item.qty) != len(si_serial_nos):
- frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
+ frappe.throw(_("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
item.idx, item.qty, item.item_code, len(si_serial_nos)))
def update_project(self):
@@ -1422,6 +1427,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:
@@ -1429,9 +1435,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"
@@ -1458,27 +1464,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
@@ -1954,22 +1975,23 @@ def update_multi_mode_option(doc, pos_profile):
def append_payment(payment_mode):
payment = doc.append('payments', {})
payment.default = payment_mode.default
- payment.mode_of_payment = payment_mode.parent
+ payment.mode_of_payment = payment_mode.mop
payment.account = payment_mode.default_account
payment.type = payment_mode.type
doc.set('payments', [])
invalid_modes = []
- for pos_payment_method in pos_profile.get('payments'):
- pos_payment_method = pos_payment_method.as_dict()
+ mode_of_payments = [d.mode_of_payment for d in pos_profile.get('payments')]
+ mode_of_payments_info = get_mode_of_payments_info(mode_of_payments, doc.company)
- payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
+ for row in pos_profile.get('payments'):
+ payment_mode = mode_of_payments_info.get(row.mode_of_payment)
if not payment_mode:
- invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment))
+ invalid_modes.append(get_link_to_form("Mode of Payment", row.mode_of_payment))
continue
- payment_mode[0].default = pos_payment_method.default
- append_payment(payment_mode[0])
+ payment_mode.default = row.default
+ append_payment(payment_mode)
if invalid_modes:
if invalid_modes == 1:
@@ -1985,6 +2007,24 @@ def get_all_mode_of_payments(doc):
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
{'company': doc.company}, as_dict=1)
+def get_mode_of_payments_info(mode_of_payments, company):
+ data = frappe.db.sql(
+ """
+ select
+ mpa.default_account, mpa.parent as mop, mp.type as type
+ from
+ `tabMode of Payment Account` mpa,`tabMode of Payment` mp
+ where
+ mpa.parent = mp.name and
+ mpa.company = %s and
+ mp.enabled = 1 and
+ mp.name in (%s)
+ group by
+ mp.name
+ """, (company, mode_of_payments), as_dict=1)
+
+ return {row.get('mop'): row for row in data}
+
def get_mode_of_payment_info(mode_of_payment, company):
return frappe.db.sql("""
select mpa.default_account, mpa.parent, mp.type as type
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 8a2e9450e97..56de3c62920 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -1087,8 +1087,6 @@ class TestSalesInvoice(unittest.TestCase):
actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
- frappe.db.commit()
-
self.assertEqual(actual_qty_0 - 5, actual_qty_1)
# outgoing_rate
@@ -2023,11 +2021,7 @@ class TestSalesInvoice(unittest.TestCase):
frappe.local.enable_perpetual_inventory['_Test Company 1'] = old_perpetual_inventory
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock)
- def test_sle_if_target_warehouse_exists_accidentally(self):
- """
- Check if inward entry exists if Target Warehouse accidentally exists
- but Customer is not an internal customer.
- """
+ def test_sle_for_target_warehouse(self):
se = make_stock_entry(
item_code="138-CMS Shoe",
target="Finished Goods - _TC",
@@ -2048,9 +2042,9 @@ class TestSalesInvoice(unittest.TestCase):
sles = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": si.name},
fields=["name", "actual_qty"])
- # check if only one SLE for outward entry is created
- self.assertEqual(len(sles), 1)
- self.assertEqual(sles[0].actual_qty, -1)
+ # check if both SLEs are created
+ self.assertEqual(len(sles), 2)
+ self.assertEqual(sum(d.actual_qty for d in sles), 0.0)
# tear down
si.cancel()
@@ -2361,6 +2355,18 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertEqual(si.status, "Paid")
+ def test_sales_invoice_submission_post_account_freezing_date(self):
+ frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
+ si = create_sales_invoice(do_not_save=True)
+ si.posting_date = add_days(getdate(), 1)
+ si.save()
+
+ self.assertRaises(frappe.ValidationError, si.submit)
+ si.posting_date = getdate()
+ si.submit()
+
+ frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 8171b3b019d..de9550233f9 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -33,7 +33,7 @@ class Subscription(Document):
# update start just before the subscription doc is created
self.update_subscription_period(self.start_date)
- def update_subscription_period(self, date=None):
+ def update_subscription_period(self, date=None, return_date=False):
"""
Subscription period is the period to be billed. This method updates the
beginning of the billing period and end of the billing period.
@@ -41,28 +41,41 @@ class Subscription(Document):
The beginning of the billing period is represented in the doctype as
`current_invoice_start` and the end of the billing period is represented
as `current_invoice_end`.
- """
- self.set_current_invoice_start(date)
- self.set_current_invoice_end()
- def set_current_invoice_start(self, date=None):
+ If return_date is True, it wont update the start and end dates.
+ This is implemented to get the dates to check if is_current_invoice_generated
"""
- This sets the date of the beginning of the current billing period.
+ _current_invoice_start = self.get_current_invoice_start(date)
+ _current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
+
+ if return_date:
+ return _current_invoice_start, _current_invoice_end
+
+ self.current_invoice_start = _current_invoice_start
+ self.current_invoice_end = _current_invoice_end
+
+ def get_current_invoice_start(self, date=None):
+ """
+ This returns the date of the beginning of the current billing period.
If the `date` parameter is not given , it will be automatically set as today's
date.
"""
- if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
- self.current_invoice_start = add_days(self.trial_period_end, 1)
- elif self.trial_period_start and self.is_trialling():
- self.current_invoice_start = self.trial_period_start
- elif date:
- self.current_invoice_start = date
- else:
- self.current_invoice_start = nowdate()
+ _current_invoice_start = None
- def set_current_invoice_end(self):
+ if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
+ _current_invoice_start = add_days(self.trial_period_end, 1)
+ elif self.trial_period_start and self.is_trialling():
+ _current_invoice_start = self.trial_period_start
+ elif date:
+ _current_invoice_start = date
+ else:
+ _current_invoice_start = nowdate()
+
+ return _current_invoice_start
+
+ def get_current_invoice_end(self, date=None):
"""
- This sets the date of the end of the current billing period.
+ This returns the date of the end of the current billing period.
If the subscription is in trial period, it will be set as the end of the
trial period.
@@ -71,44 +84,47 @@ class Subscription(Document):
current billing period where `x` is the billing interval from the
`Subscription Plan` in the `Subscription`.
"""
- if self.is_trialling() and getdate(self.current_invoice_start) < getdate(self.trial_period_end):
- self.current_invoice_end = self.trial_period_end
+ _current_invoice_end = None
+
+ if self.is_trialling() and getdate(date) < getdate(self.trial_period_end):
+ _current_invoice_end = self.trial_period_end
else:
billing_cycle_info = self.get_billing_cycle_data()
if billing_cycle_info:
- if self.is_new_subscription() and getdate(self.start_date) < getdate(self.current_invoice_start):
- self.current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
+ if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
+ _current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
# For cases where trial period is for an entire billing interval
- if getdate(self.current_invoice_end) < getdate(self.current_invoice_start):
- self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
+ if getdate(self.current_invoice_end) < getdate(date):
+ _current_invoice_end = add_to_date(date, **billing_cycle_info)
else:
- self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
+ _current_invoice_end = add_to_date(date, **billing_cycle_info)
else:
- self.current_invoice_end = get_last_day(self.current_invoice_start)
+ _current_invoice_end = get_last_day(date)
if self.follow_calendar_months:
billing_info = self.get_billing_cycle_and_interval()
billing_interval_count = billing_info[0]['billing_interval_count']
calendar_months = get_calendar_months(billing_interval_count)
calendar_month = 0
- current_invoice_end_month = getdate(self.current_invoice_end).month
- current_invoice_end_year = getdate(self.current_invoice_end).year
+ current_invoice_end_month = getdate(_current_invoice_end).month
+ current_invoice_end_year = getdate(_current_invoice_end).year
for month in calendar_months:
if month <= current_invoice_end_month:
calendar_month = month
if cint(calendar_month - billing_interval_count) <= 0 and \
- getdate(self.current_invoice_start).month != 1:
+ getdate(date).month != 1:
calendar_month = 12
current_invoice_end_year -= 1
- self.current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' \
- + cstr(calendar_month) + '-01')
+ _current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' + cstr(calendar_month) + '-01')
- if self.end_date and getdate(self.current_invoice_end) > getdate(self.end_date):
- self.current_invoice_end = self.end_date
+ if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date):
+ _current_invoice_end = self.end_date
+
+ return _current_invoice_end
@staticmethod
def validate_plans_billing_cycle(billing_cycle_data):
@@ -488,8 +504,9 @@ class Subscription(Document):
def is_current_invoice_generated(self):
invoice = self.get_current_invoice()
+ _current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True)
- if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end):
+ if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date):
return True
return False
@@ -542,15 +559,15 @@ class Subscription(Document):
else:
self.set_status_grace_period()
- if getdate() > getdate(self.current_invoice_end):
- self.update_subscription_period(add_days(self.current_invoice_end, 1))
-
# Generate invoices periodically even if current invoice are unpaid
if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice()
or self.is_prepaid_to_invoice()):
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
self.generate_invoice(prorate)
+ if getdate() > getdate(self.current_invoice_end):
+ self.update_subscription_period(add_days(self.current_invoice_end, 1))
+
@staticmethod
def is_paid(invoice):
"""
diff --git a/erpnext/accounts/doctype/subscription/test_subscription.py b/erpnext/accounts/doctype/subscription/test_subscription.py
index e2cf4d5a442..0f7a0a86a4d 100644
--- a/erpnext/accounts/doctype/subscription/test_subscription.py
+++ b/erpnext/accounts/doctype/subscription/test_subscription.py
@@ -18,6 +18,7 @@ from frappe.utils.data import (
from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor
+test_dependencies = ("UOM", "Item Group", "Item")
def create_plan():
if not frappe.db.exists('Subscription Plan', '_Test Plan Name'):
@@ -68,7 +69,6 @@ def create_plan():
supplier.insert()
class TestSubscription(unittest.TestCase):
-
def setUp(self):
create_plan()
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.js b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.js
index b8d6c9af3a9..7b479749465 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.js
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.js
@@ -8,7 +8,8 @@ frappe.ui.form.on('Tax Withholding Category', {
if (child.company) {
return {
filters: {
- 'company': child.company
+ 'company': child.company,
+ 'root_type': ['in', ['Asset', 'Liability']]
}
};
}
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 16ef5fc9745..c36f3cb201b 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -13,6 +13,7 @@ from frappe.utils import cint, getdate
class TaxWithholdingCategory(Document):
def validate(self):
self.validate_dates()
+ self.validate_accounts()
self.validate_thresholds()
def validate_dates(self):
@@ -25,6 +26,14 @@ class TaxWithholdingCategory(Document):
if last_date and getdate(d.to_date) < getdate(last_date):
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
+ def validate_accounts(self):
+ existing_accounts = []
+ for d in self.get('accounts'):
+ if d.get('account') in existing_accounts:
+ frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get('account'))))
+
+ existing_accounts.append(d.get('account'))
+
def validate_thresholds(self):
for d in self.get('rates'):
if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold:
@@ -203,6 +212,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 +334,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/general_ledger.py b/erpnext/accounts/general_ledger.py
index 0cee6f5b3aa..0cae16bc51a 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -293,7 +293,7 @@ def check_freezing_date(posting_date, adv_adj=False):
if acc_frozen_upto:
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier')
if getdate(posting_date) <= getdate(acc_frozen_upto) \
- and not frozen_accounts_modifier in frappe.get_roles() or frappe.session.user == 'Administrator':
+ and (frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == 'Administrator'):
frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto)))
def set_as_cancel(voucher_type, voucher_no):
diff --git a/erpnext/accounts/report/cash_flow/cash_flow.py b/erpnext/accounts/report/cash_flow/cash_flow.py
index d5271885b7e..bb8138bfc2e 100644
--- a/erpnext/accounts/report/cash_flow/cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/cash_flow.py
@@ -139,9 +139,9 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
data["total"] = total
return data
-def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters={}):
+def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None):
cond = ""
- filters = frappe._dict(filters)
+ filters = frappe._dict(filters or {})
if filters.include_default_book_entries:
company_fb = frappe.db.get_value("Company", company, 'default_finance_book')
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
index 6a8301a6f91..e24a5f99184 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.js
@@ -103,8 +103,11 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() {
column.is_tree = true;
}
- value = default_formatter(value, row, column, data);
+ if (data && data.account && column.apply_currency_formatter) {
+ data.currency = erpnext.get_currency(column.company_name);
+ }
+ value = default_formatter(value, row, column, data);
if (!data.parent_account) {
value = $(`${value}`);
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 b0cfbac9cb1..0de2a9854d6 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -3,12 +3,14 @@
from __future__ import unicode_literals
+from collections import defaultdict
+
import frappe
from frappe import _
from frappe.utils import cint, flt, getdate
+import erpnext
from erpnext.accounts.report.balance_sheet.balance_sheet import (
- check_opening_balance,
get_chart_data,
get_provisional_profit_loss,
)
@@ -31,7 +33,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_report_summary as get_pl_summary,
)
-from erpnext.accounts.report.utils import convert_to_presentation_currency
+from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
def execute(filters=None):
@@ -42,7 +44,7 @@ def execute(filters=None):
fiscal_year = get_fiscal_year_data(filters.get('from_fiscal_year'), filters.get('to_fiscal_year'))
companies_column, companies = get_companies(filters)
- columns = get_columns(companies_column)
+ columns = get_columns(companies_column, filters)
if filters.get('report') == "Balance Sheet":
data, message, chart, report_summary = get_balance_sheet_data(fiscal_year, companies, columns, filters)
@@ -73,21 +75,24 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
provisional_profit_loss, total_credit = get_provisional_profit_loss(asset, liability, equity,
companies, filters.get('company'), company_currency, True)
- message, opening_balance = check_opening_balance(asset, liability, equity)
+ message, opening_balance = prepare_companywise_opening_balance(asset, liability, equity, companies)
- if opening_balance and round(opening_balance,2) !=0:
- unclosed ={
+ if opening_balance:
+ unclosed = {
"account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
"account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
"warn_if_negative": True,
"currency": company_currency
}
- for company in companies:
- unclosed[company] = opening_balance
- if provisional_profit_loss:
- provisional_profit_loss[company] = provisional_profit_loss[company] - opening_balance
- unclosed["total"]=opening_balance
+ for company in companies:
+ unclosed[company] = opening_balance.get(company)
+ if provisional_profit_loss and provisional_profit_loss.get(company):
+ provisional_profit_loss[company] = (
+ flt(provisional_profit_loss[company]) - flt(opening_balance.get(company))
+ )
+
+ unclosed["total"] = opening_balance.get(company)
data.append(unclosed)
if provisional_profit_loss:
@@ -102,6 +107,37 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
return data, message, chart, report_summary
+def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, companies):
+ opening_balance = {}
+ for company in companies:
+ opening_value = 0
+
+ # 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) or 0.0)
+
+ opening_balance[company] = opening_value
+
+ if opening_balance:
+ return _("Previous Financial Year is not closed"), opening_balance
+
+ return '', {}
+
+def get_opening_balance(account_name, data, company):
+ for row in data:
+ if row.get('account_name') == account_name:
+ return row.get('company_wise_opening_bal', {}).get(company, 0.0)
+
+def get_root_account_name(root_type, company):
+ return frappe.get_all(
+ 'Account',
+ fields=['account_name'],
+ filters = {'root_type': root_type, 'is_group': 1,
+ 'company': company, 'parent_account': ('is', 'not set')},
+ as_list=1
+ )[0][0]
+
def get_profit_loss_data(fiscal_year, companies, columns, filters):
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
company_currency = get_company_currency(filters)
@@ -193,30 +229,37 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data["total"] = total
return data
-def get_columns(companies):
- columns = [{
- "fieldname": "account",
- "label": _("Account"),
- "fieldtype": "Link",
- "options": "Account",
- "width": 300
- }]
-
- columns.append({
- "fieldname": "currency",
- "label": _("Currency"),
- "fieldtype": "Link",
- "options": "Currency",
- "hidden": 1
- })
+def get_columns(companies, filters):
+ columns = [
+ {
+ "fieldname": "account",
+ "label": _("Account"),
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 300
+ }, {
+ "fieldname": "currency",
+ "label": _("Currency"),
+ "fieldtype": "Link",
+ "options": "Currency",
+ "hidden": 1
+ }
+ ]
for company in companies:
+ apply_currency_formatter = 1 if not filters.presentation_currency else 0
+ currency = filters.presentation_currency
+ if not currency:
+ currency = erpnext.get_company_currency(company)
+
columns.append({
"fieldname": company,
- "label": company,
+ "label": f'{company} ({currency})',
"fieldtype": "Currency",
"options": "currency",
- "width": 150
+ "width": 150,
+ "apply_currency_formatter": apply_currency_formatter,
+ "company_name": company
})
return columns
@@ -236,6 +279,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
start_date = filters.period_start_date if filters.report != 'Balance Sheet' else None
end_date = filters.period_end_date
+ filters.end_date = end_date
+
gl_entries_by_account = {}
for root in frappe.db.sql("""select lft, rgt from tabAccount
where root_type=%s and ifnull(parent_account, '') = ''""", root_type, as_dict=1):
@@ -244,9 +289,10 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
end_date, root.lft, root.rgt, filters,
gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
- calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
+ calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year)
accumulate_values_into_parents(accounts, accounts_by_name, companies)
- out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency)
+
+ out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
if out:
add_total_row(out, root_type, balance_must_be, companies, company_currency)
@@ -257,7 +303,10 @@ def get_company_currency(filters=None):
return (filters.get('presentation_currency')
or frappe.get_cached_value('Company', filters.company, "default_currency"))
-def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
+def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year):
+ start_date = (fiscal_year.year_start_date
+ if filters.filter_based_on == 'Fiscal Year' else filters.period_start_date)
+
for entries in gl_entries_by_account.values():
for entry in entries:
if entry.account_number:
@@ -266,15 +315,32 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_d
account_name = entry.account_name
d = accounts_by_name.get(account_name)
+
if d:
+ debit, credit = 0, 0
for company in companies:
# check if posting date is within the period
if (entry.company == company or (filters.get('accumulated_in_group_company'))
and entry.company in companies.get(company)):
- d[company] = d.get(company, 0.0) + flt(entry.debit) - flt(entry.credit)
+ parent_company_currency = erpnext.get_company_currency(d.company)
+ child_company_currency = erpnext.get_company_currency(entry.company)
+
+ debit, credit = flt(entry.debit), flt(entry.credit)
+
+ if (not filters.get('presentation_currency')
+ and entry.company != company
+ and parent_company_currency != child_company_currency
+ and filters.get('accumulated_in_group_company')):
+ debit = convert(debit, parent_company_currency, child_company_currency, filters.end_date)
+ credit = convert(credit, parent_company_currency, child_company_currency, filters.end_date)
+
+ d[company] = d.get(company, 0.0) + flt(debit) - flt(credit)
+
+ if entry.posting_date < getdate(start_date):
+ d['company_wise_opening_bal'][company] += (flt(debit) - flt(credit))
if entry.posting_date < getdate(start_date):
- d["opening_balance"] = d.get("opening_balance", 0.0) + flt(entry.debit) - flt(entry.credit)
+ d["opening_balance"] = d.get("opening_balance", 0.0) + flt(debit) - flt(credit)
def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts"""
@@ -282,17 +348,18 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
if d.parent_account:
account = d.parent_account_name
- if not accounts_by_name.get(account):
- continue
+ # if not accounts_by_name.get(account):
+ # continue
for company in companies:
accounts_by_name[account][company] = \
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
+ accounts_by_name[account]['company_wise_opening_bal'][company] += d.get('company_wise_opening_bal', {}).get(company, 0.0)
+
accounts_by_name[account]["opening_balance"] = \
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
-
def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters)
@@ -353,7 +420,7 @@ def get_accounts(root_type, filters):
`tabAccount` where company = %s and root_type = %s
""" , (filters.get('company'), root_type), as_dict=1)
-def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency):
+def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
data = []
for d in accounts:
@@ -367,10 +434,13 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
"parent_account": _(d.parent_account),
"indent": flt(d.indent),
"year_start_date": start_date,
+ "root_type": d.root_type,
"year_end_date": end_date,
- "currency": company_currency,
+ "currency": filters.presentation_currency,
+ "company_wise_opening_bal": d.company_wise_opening_bal,
"opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1)
})
+
for company in companies:
if d.get(company) and balance_must_be == "Credit":
# change sign based on Debit or Credit, since calculation is done using (debit - credit)
@@ -385,6 +455,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
row["has_value"] = has_value
row["total"] = total
+
data.append(row)
return data
@@ -447,6 +518,7 @@ def get_account_details(account):
'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1)
def validate_entries(key, entry, accounts_by_name, accounts):
+ # If an account present in the child company and not in the parent company
if key not in accounts_by_name:
args = get_account_details(entry.account)
@@ -456,12 +528,23 @@ def validate_entries(key, entry, accounts_by_name, accounts):
args.update({
'lft': parent_args.lft + 1,
'rgt': parent_args.rgt - 1,
+ 'indent': 3,
'root_type': parent_args.root_type,
- 'report_type': parent_args.report_type
+ 'report_type': parent_args.report_type,
+ 'parent_account_name': parent_args.account_name,
+ 'company_wise_opening_bal': defaultdict(float)
})
accounts_by_name.setdefault(key, args)
- accounts.append(args)
+
+ idx = len(accounts)
+ # To identify parent account index
+ for index, row in enumerate(accounts):
+ if row.parent_account_name == args.parent_account_name:
+ idx = index
+ break
+
+ accounts.insert(idx+1, args)
def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = []
@@ -491,7 +574,6 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency):
for company in companies:
total_row.setdefault(company, 0.0)
total_row[company] += row.get(company, 0.0)
- row[company] = 0.0
total_row.setdefault("total", 0.0)
total_row["total"] += flt(row["total"])
@@ -511,6 +593,7 @@ def filter_accounts(accounts, depth=10):
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
+ d['company_wise_opening_bal'] = defaultdict(float)
accounts_by_name[account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d)
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index 5bd6e583dbb..31416da4ac4 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -155,6 +155,8 @@ def get_gl_entries(filters, accounting_dimensions):
if filters.get("group_by") == "Group by Voucher":
order_by_statement = "order by posting_date, voucher_type, voucher_no"
+ if filters.get("group_by") == "Group by Account":
+ order_by_statement = "order by account, posting_date, creation"
if filters.get("include_default_book_entries"):
filters['company_fb'] = frappe.db.get_value("Company",
@@ -421,8 +423,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 +436,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 621b697aca4..6a7f2e5b535 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/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
index 2b26ac50900..33d17488256 100644
--- a/erpnext/accounts/workspace/accounting/accounting.json
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -1,5 +1,4 @@
{
- "category": "",
"charts": [
{
"chart_name": "Profit and Loss",
@@ -8,18 +7,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Accounts\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Profit and Loss\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chart of Accounts\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Invoice\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Invoice\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Journal Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Payment Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Accounts Receivable\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"General Ledger\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Trial Balance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounting Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"General Ledger\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounts Receivable\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounts Payable\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Financial Statements\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Multi Currency\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bank Statement\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Subscription Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Goods and Services Tax (GST India)\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Share Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Cost Center and Budgeting\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Opening and Closing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Taxes\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Profitability\", \"col\": 4}}]",
"creation": "2020-03-02 15:41:59.515192",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "accounting",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Accounting",
"links": [
{
@@ -533,6 +526,17 @@
"only_for": "United Arab Emirates",
"type": "Link"
},
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "KSA VAT Report",
+ "link_to": "KSA VAT",
+ "link_type": "Report",
+ "onboard": 0,
+ "only_for": "Saudi Arabia",
+ "type": "Link"
+ },
{
"hidden": 0,
"is_query_report": 0,
@@ -1153,6 +1157,16 @@
"onboard": 0,
"type": "Link"
},
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "KSA VAT Setting",
+ "link_to": "KSA VAT Setting",
+ "link_type": "DocType",
+ "onboard": 0,
+ "only_for": "Saudi Arabia",
+ "type": "Link"
+ },
{
"hidden": 0,
"is_query_report": 0,
@@ -1206,15 +1220,12 @@
"type": "Link"
}
],
- "modified": "2021-08-27 12:15:52.872470",
+ "modified": "2021-08-27 12:15:52.872471",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
- "onboarding": "Accounts",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/agriculture/workspace/agriculture/agriculture.json b/erpnext/agriculture/workspace/agriculture/agriculture.json
index 633777eeb70..6714de6d382 100644
--- a/erpnext/agriculture/workspace/agriculture/agriculture.json
+++ b/erpnext/agriculture/workspace/agriculture/agriculture.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Crops & Lands\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Analytics\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Diseases & Fertilizers\", \"col\": 4}}]",
"creation": "2020-03-02 17:23:34.339274",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "agriculture",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Agriculture",
"links": [
{
@@ -163,15 +156,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:15:54.595197",
+ "modified": "2021-08-05 12:15:54.595198",
"modified_by": "Administrator",
"module": "Agriculture",
"name": "Agriculture",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "Agriculture",
"roles": [],
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 7e135be30b7..99a6cc35dbb 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -194,7 +194,7 @@ class Asset(AccountsController):
start = self.clear_depreciation_schedule()
# value_after_depreciation - current Asset value
- if d.value_after_depreciation:
+ if self.docstatus == 1 and d.value_after_depreciation:
value_after_depreciation = (flt(d.value_after_depreciation) -
flt(self.opening_accumulated_depreciation))
else:
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 7183ee7e369..cf4581b4a16 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -682,6 +682,27 @@ class TestAsset(unittest.TestCase):
# reset indian company
frappe.flags.company = company_flag
+ def test_expected_value_change(self):
+ """
+ tests if changing `expected_value_after_useful_life`
+ affects `value_after_depreciation`
+ """
+
+ asset = create_asset(calculate_depreciation=1)
+ asset.opening_accumulated_depreciation = 2000
+ asset.number_of_depreciations_booked = 1
+
+ asset.finance_books[0].expected_value_after_useful_life = 100
+ asset.save()
+ asset.reload()
+ self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
+
+ # changing expected_value_after_useful_life shouldn't affect value_after_depreciation
+ asset.finance_books[0].expected_value_after_useful_life = 200
+ asset.save()
+ asset.reload()
+ self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
+
def create_asset_data():
if not frappe.db.exists("Asset Category", "Computers"):
create_asset_category()
diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
index 9945a328cfc..30e3a5296e0 100644
--- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py
+++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py
@@ -22,7 +22,7 @@ class TestAssetRepair(unittest.TestCase):
frappe.db.sql("delete from `tabTax Rule`")
def test_update_status(self):
- asset = create_asset()
+ asset = create_asset(submit=1)
initial_status = asset.status
asset_repair = create_asset_repair(asset = asset)
@@ -76,7 +76,7 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
def test_increase_in_asset_value_due_to_stock_consumption(self):
- asset = create_asset(calculate_depreciation = 1)
+ asset = create_asset(calculate_depreciation = 1, submit=1)
initial_asset_value = get_asset_value(asset)
asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1)
asset.reload()
@@ -85,7 +85,7 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
- asset = create_asset(calculate_depreciation = 1)
+ asset = create_asset(calculate_depreciation = 1, submit=1)
initial_asset_value = get_asset_value(asset)
asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
asset.reload()
@@ -103,7 +103,7 @@ class TestAssetRepair(unittest.TestCase):
self.assertEqual(asset_repair.name, gl_entry.voucher_no)
def test_increase_in_asset_life(self):
- asset = create_asset(calculate_depreciation = 1)
+ asset = create_asset(calculate_depreciation = 1, submit=1)
initial_num_of_depreciations = num_of_depreciations(asset)
create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
asset.reload()
@@ -126,7 +126,7 @@ def create_asset_repair(**args):
if args.asset:
asset = args.asset
else:
- asset = create_asset(is_existing_asset = 1)
+ asset = create_asset(is_existing_asset = 1, submit=1)
asset_repair = frappe.new_doc("Asset Repair")
asset_repair.update({
"asset": asset.name,
diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json
index dfbf1a378e5..495de46e414 100644
--- a/erpnext/assets/workspace/assets/assets.json
+++ b/erpnext/assets/workspace/assets/assets.json
@@ -1,5 +1,4 @@
{
- "category": "",
"charts": [
{
"chart_name": "Asset Value Analytics",
@@ -8,18 +7,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Assets\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Asset Value Analytics\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Asset\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Asset Category\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Fixed Asset Register\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assets\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Maintenance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
"creation": "2020-03-02 15:43:27.634865",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "assets",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Assets",
"links": [
{
@@ -179,15 +172,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:15:54.839452",
+ "modified": "2021-08-05 12:15:54.839453",
"modified_by": "Administrator",
"module": "Assets",
"name": "Assets",
- "onboarding": "Assets",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/buying/doctype/buying_settings/buying_settings.js b/erpnext/buying/doctype/buying_settings/buying_settings.js
index 944bb61cfeb..32431fc3910 100644
--- a/erpnext/buying/doctype/buying_settings/buying_settings.js
+++ b/erpnext/buying/doctype/buying_settings/buying_settings.js
@@ -11,7 +11,7 @@ frappe.tour['Buying Settings'] = [
{
fieldname: "supp_master_name",
title: "Supplier Naming By",
- description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ") + "Naming Series" + __(" choose the 'Naming Series' option."),
+ description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a Naming Series choose the 'Naming Series' option."),
},
{
fieldname: "buying_price_list",
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 12a09cdd0ec..a57d9a92bb3 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -25,7 +25,6 @@
"column_break0",
"supplier_group",
"supplier_type",
- "pan",
"allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt",
"disabled",
@@ -176,11 +175,6 @@
"options": "Company\nIndividual",
"reqd": 1
},
- {
- "fieldname": "pan",
- "fieldtype": "Data",
- "label": "PAN"
- },
{
"fieldname": "language",
"fieldtype": "Link",
@@ -438,11 +432,12 @@
"link_fieldname": "party"
}
],
- "modified": "2021-09-06 17:37:56.522233",
+ "modified": "2021-10-20 22:03:33.147249",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
"name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/buying/form_tour/buying_settings/buying_settings.json b/erpnext/buying/form_tour/buying_settings/buying_settings.json
new file mode 100644
index 00000000000..fa8c80d6cdf
--- /dev/null
+++ b/erpnext/buying/form_tour/buying_settings/buying_settings.json
@@ -0,0 +1,77 @@
+{
+ "creation": "2021-07-28 11:51:42.319984",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-10-05 13:06:56.414584",
+ "modified_by": "Administrator",
+ "module": "Buying",
+ "name": "Buying Settings",
+ "owner": "Administrator",
+ "reference_doctype": "Buying Settings",
+ "save_on_complete": 0,
+ "steps": [
+ {
+ "description": "When a Supplier is saved, system generates a unique identity or name for that Supplier which can be used to refer the Supplier in various Buying transactions.",
+ "field": "",
+ "fieldname": "supp_master_name",
+ "fieldtype": "Select",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Supplier Naming By",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Supplier Naming By"
+ },
+ {
+ "description": "Configure what should be the default value of Supplier Group when creating a new Supplier.",
+ "field": "",
+ "fieldname": "supplier_group",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Default Supplier Group",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Default Supplier Group"
+ },
+ {
+ "description": "Item prices will be fetched from this Price List.",
+ "field": "",
+ "fieldname": "buying_price_list",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Default Buying Price List",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Default Buying Price List"
+ },
+ {
+ "description": "If this option is configured \"Yes\", ERPNext will prevent you from creating a Purchase Invoice or a Purchase Receipt directly without creating a Purchase Order first.",
+ "field": "",
+ "fieldname": "po_required",
+ "fieldtype": "Select",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Purchase Order Required"
+ },
+ {
+ "description": "If this option is configured \"Yes\", ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first.",
+ "field": "",
+ "fieldname": "pr_required",
+ "fieldtype": "Select",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Purchase Receipt Required"
+ }
+ ],
+ "title": "Buying Settings"
+}
\ No newline at end of file
diff --git a/erpnext/buying/form_tour/purchase_order/purchase_order.json b/erpnext/buying/form_tour/purchase_order/purchase_order.json
new file mode 100644
index 00000000000..3cc88fbf4fe
--- /dev/null
+++ b/erpnext/buying/form_tour/purchase_order/purchase_order.json
@@ -0,0 +1,82 @@
+{
+ "creation": "2021-07-29 14:11:58.271113",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-10-05 13:11:31.436135",
+ "modified_by": "Administrator",
+ "module": "Buying",
+ "name": "Purchase Order",
+ "owner": "Administrator",
+ "reference_doctype": "Purchase Order",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "Select a Supplier",
+ "field": "",
+ "fieldname": "supplier",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Supplier",
+ "parent_field": "",
+ "position": "Right",
+ "title": "Supplier"
+ },
+ {
+ "description": "Set the \"Required By\" date for the materials. This sets the \"Required By\" date for all the items.",
+ "field": "",
+ "fieldname": "schedule_date",
+ "fieldtype": "Date",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Required By",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Required By"
+ },
+ {
+ "description": "Items to be purchased can be added here.",
+ "field": "",
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Items",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Items Table"
+ },
+ {
+ "child_doctype": "Purchase Order Item",
+ "description": "Enter the Item Code.",
+ "field": "",
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "has_next_condition": 1,
+ "is_table_field": 1,
+ "label": "Item Code",
+ "next_step_condition": "eval: doc.item_code",
+ "parent_field": "",
+ "parent_fieldname": "items",
+ "position": "Right",
+ "title": "Item Code"
+ },
+ {
+ "child_doctype": "Purchase Order Item",
+ "description": "Enter the required quantity for the material.",
+ "field": "",
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "has_next_condition": 0,
+ "is_table_field": 1,
+ "label": "Quantity",
+ "parent_field": "",
+ "parent_fieldname": "items",
+ "position": "Bottom",
+ "title": "Quantity"
+ }
+ ],
+ "title": "Purchase Order"
+}
\ No newline at end of file
diff --git a/erpnext/buying/module_onboarding/buying/buying.json b/erpnext/buying/module_onboarding/buying/buying.json
index 887f85b82d1..84e97a2d4d2 100644
--- a/erpnext/buying/module_onboarding/buying/buying.json
+++ b/erpnext/buying/module_onboarding/buying/buying.json
@@ -19,7 +19,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/buying",
"idx": 0,
"is_complete": 0,
- "modified": "2020-07-08 14:05:28.273641",
+ "modified": "2021-08-24 18:13:42.463776",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying",
@@ -28,23 +28,11 @@
{
"step": "Introduction to Buying"
},
- {
- "step": "Create a Supplier"
- },
- {
- "step": "Setup your Warehouse"
- },
- {
- "step": "Create a Product"
- },
{
"step": "Create a Material Request"
},
{
"step": "Create your first Purchase Order"
- },
- {
- "step": "Buying Settings"
}
],
"subtitle": "Products, Purchases, Analysis, and more.",
diff --git a/erpnext/buying/onboarding_step/create_a_material_request/create_a_material_request.json b/erpnext/buying/onboarding_step/create_a_material_request/create_a_material_request.json
index 9dc493dd499..28e86ab0641 100644
--- a/erpnext/buying/onboarding_step/create_a_material_request/create_a_material_request.json
+++ b/erpnext/buying/onboarding_step/create_a_material_request/create_a_material_request.json
@@ -1,19 +1,21 @@
{
- "action": "Create Entry",
+ "action": "Show Form Tour",
+ "action_label": "Let\u2019s create your first Material Request",
"creation": "2020-05-15 14:39:09.818764",
+ "description": "# Track Material Request\n\n\nAlso known as Purchase Request or an Indent, is a document identifying a requirement of a set of items (products or services) for various purposes like procurement, transfer, issue, or manufacturing. Once the Material Request is validated, a purchase manager can take the next actions for purchasing items like requesting RFQ from a supplier or directly placing an order with an identified Supplier.\n\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 1,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-15 14:39:09.818764",
+ "modified": "2021-08-24 18:08:08.347501",
"modified_by": "Administrator",
"name": "Create a Material Request",
"owner": "Administrator",
"reference_document": "Material Request",
+ "show_form_tour": 1,
"show_full_form": 1,
- "title": "Create a Material Request",
+ "title": "Track Material Request",
"validate_action": 1
}
\ No newline at end of file
diff --git a/erpnext/buying/onboarding_step/create_your_first_purchase_order/create_your_first_purchase_order.json b/erpnext/buying/onboarding_step/create_your_first_purchase_order/create_your_first_purchase_order.json
index 9dbed239789..18a39315861 100644
--- a/erpnext/buying/onboarding_step/create_your_first_purchase_order/create_your_first_purchase_order.json
+++ b/erpnext/buying/onboarding_step/create_your_first_purchase_order/create_your_first_purchase_order.json
@@ -1,19 +1,21 @@
{
- "action": "Create Entry",
+ "action": "Show Form Tour",
+ "action_label": "Let\u2019s create your first Purchase Order",
"creation": "2020-05-12 18:17:49.976035",
+ "description": "# Create first Purchase Order\n\nPurchase Order is at the heart of your buying transactions. In ERPNext, Purchase Order can can be created against a Purchase Material Request (indent) and Supplier Quotation as well. Purchase Orders is also linked to Purchase Receipt and Purchase Invoices, allowing you to keep a birds-eye view on your purchase deals.\n\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-12 18:31:56.856112",
+ "modified": "2021-08-24 18:08:08.936484",
"modified_by": "Administrator",
"name": "Create your first Purchase Order",
"owner": "Administrator",
"reference_document": "Purchase Order",
+ "show_form_tour": 0,
"show_full_form": 0,
- "title": "Create your first Purchase Order",
+ "title": "Create first Purchase Order",
"validate_action": 1
}
\ No newline at end of file
diff --git a/erpnext/buying/onboarding_step/introduction_to_buying/introduction_to_buying.json b/erpnext/buying/onboarding_step/introduction_to_buying/introduction_to_buying.json
index fd98fddafae..01ac8b81760 100644
--- a/erpnext/buying/onboarding_step/introduction_to_buying/introduction_to_buying.json
+++ b/erpnext/buying/onboarding_step/introduction_to_buying/introduction_to_buying.json
@@ -1,19 +1,22 @@
{
- "action": "Watch Video",
+ "action": "Show Form Tour",
+ "action_label": "Let\u2019s walk-through few Buying Settings",
"creation": "2020-05-06 15:37:09.477765",
+ "description": "# Buying Settings\n\n\nBuying module\u2019s features are highly configurable as per your business needs. Buying Settings is the place where you can set your preferences for:\n\n- Supplier naming and default values\n- Billing and shipping preference in buying transactions\n\n\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
- "is_single": 0,
+ "is_single": 1,
"is_skipped": 0,
- "modified": "2020-05-12 18:25:08.509900",
+ "modified": "2021-08-24 18:08:08.345735",
"modified_by": "Administrator",
"name": "Introduction to Buying",
"owner": "Administrator",
- "show_full_form": 0,
- "title": "Introduction to Buying",
+ "reference_document": "Buying Settings",
+ "show_form_tour": 1,
+ "show_full_form": 1,
+ "title": "Buying Settings",
"validate_action": 1,
"video_url": "https://youtu.be/efFajTTQBa8"
}
\ No newline at end of file
diff --git a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
index a5b09473a05..fd23795287f 100644
--- a/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
+++ b/erpnext/buying/report/procurement_tracker/test_procurement_tracker.py
@@ -45,7 +45,6 @@ class TestProcurementTracker(unittest.TestCase):
pr = make_purchase_receipt(po.name)
pr.get("items")[0].cost_center = "Main - _TPC"
pr.submit()
- frappe.db.commit()
date_obj = datetime.date(datetime.now())
po.load_from_db()
diff --git a/erpnext/buying/workspace/buying/buying.json b/erpnext/buying/workspace/buying/buying.json
index 6c91e816954..380ef3639f6 100644
--- a/erpnext/buying/workspace/buying/buying.json
+++ b/erpnext/buying/workspace/buying/buying.json
@@ -1,27 +1,18 @@
{
- "cards_label": "",
- "category": "",
"charts": [
{
"chart_name": "Purchase Order Trends",
"label": "Purchase Order Trends"
}
],
- "charts_label": "",
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Buying\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Purchase Order Trends\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Material Request\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Analytics\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Order Analysis\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Buying\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Items & Pricing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Supplier\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Supplier Scorecard\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Regional\", \"col\": 4}}]",
"creation": "2020-01-28 11:50:26.195467",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "buying",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Buying",
"links": [
{
@@ -518,15 +509,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:15:56.218427",
+ "modified": "2021-08-05 12:15:56.218428",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying",
- "onboarding": "Buying",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
@@ -572,6 +560,5 @@
"type": "Dashboard"
}
],
- "shortcuts_label": "",
"title": "Buying"
}
\ No newline at end of file
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index e9b531ecb86..2486012fc45 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -1032,7 +1032,7 @@ class AccountsController(TransactionBase):
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
frappe.msgprint(_("Overbilling of {} ignored because you have {} role.")
- .format(total_overbilled_amt, role_allowed_to_over_bill), title=_("Warning"), indicator="orange")
+ .format(total_overbilled_amt, role_allowed_to_over_bill), indicator="orange", alert=True)
def throw_overbill_exception(self, item, max_allowed_amt):
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
@@ -1354,8 +1354,8 @@ class AccountsController(TransactionBase):
total = 0
base_total = 0
for d in self.get("payment_schedule"):
- total += flt(d.payment_amount)
- base_total += flt(d.base_payment_amount)
+ total += flt(d.payment_amount, d.precision("payment_amount"))
+ base_total += flt(d.base_payment_amount, d.precision("base_payment_amount"))
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
grand_total = self.get("rounded_total") or self.grand_total
@@ -1371,8 +1371,9 @@ class AccountsController(TransactionBase):
else:
grand_total -= self.get("total_advance")
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
- if total != flt(grand_total, self.precision("grand_total")) or \
- base_total != flt(base_grand_total, self.precision("base_grand_total")):
+
+ if flt(total, self.precision("grand_total")) != flt(grand_total, self.precision("grand_total")) or \
+ flt(base_total, self.precision("base_grand_total")) != flt(base_grand_total, self.precision("base_grand_total")):
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
def is_rounded_total_disabled(self):
@@ -1686,17 +1687,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/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 0158a1120f9..bb269f3db22 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -424,7 +424,7 @@ class SellingController(StockController):
or (cint(self.is_return) and self.docstatus==2)):
sl_entries.append(self.get_sle_for_source_warehouse(d))
- if d.target_warehouse and self.get("is_internal_customer"):
+ if d.target_warehouse:
sl_entries.append(self.get_sle_for_target_warehouse(d))
if d.warehouse and ((not cint(self.is_return) and self.docstatus==2)
@@ -559,6 +559,12 @@ class SellingController(StockController):
frappe.throw(_("Row {0}: Delivery Warehouse ({1}) and Customer Warehouse ({2}) can not be same")
.format(d.idx, warehouse, warehouse))
+ if not self.get("is_internal_customer") and any(d.get("target_warehouse") for d in items):
+ msg = _("Target Warehouse is set for some items but the customer is not an internal customer.")
+ msg += " " + _("This {} will be treated as material transfer.").format(_(self.doctype))
+ frappe.msgprint(msg, title="Internal Transfer", alert=True)
+
+
def validate_items(self):
# validate items to see if they have is_sales_item enabled
from erpnext.controllers.buying_controller import validate_item_type
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 8738204ce09..49a76da6976 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -216,11 +216,14 @@ class StatusUpdater(Document):
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) /
item[args['target_ref_field']]) * 100
- if overflow_percent - allowance > 0.01 and role not in frappe.get_roles():
+ if overflow_percent - allowance > 0.01:
item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100)
item['reduce_by'] = item[args['target_field']] - item['max_allowed']
- self.limits_crossed_error(args, item, qty_or_amount)
+ if role not in frappe.get_roles():
+ self.limits_crossed_error(args, item, qty_or_amount)
+ else:
+ self.warn_about_bypassing_with_role(item, qty_or_amount, role)
def limits_crossed_error(self, args, item, qty_or_amount):
'''Raise exception for limits crossed'''
@@ -238,6 +241,19 @@ class StatusUpdater(Document):
frappe.bold(item.get('item_code'))
) + '
' + action_msg, OverAllowanceError, title = _('Limit Crossed'))
+ def warn_about_bypassing_with_role(self, item, qty_or_amount, role):
+ action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling")
+
+ msg = (_("{} of {} {} ignored for item {} because you have {} role.")
+ .format(
+ action,
+ _(item["target_ref_field"].title()),
+ frappe.bold(item["reduce_by"]),
+ frappe.bold(item.get('item_code')),
+ role)
+ )
+ frappe.msgprint(msg, indicator="orange", alert=True)
+
def update_qty(self, update_modified=True):
"""Updates qty or amount at row level
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 78a6e52e4d7..08d422d3bcd 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -79,8 +79,15 @@ class StockController(AccountsController):
def clean_serial_nos(self):
for row in self.get("items"):
if hasattr(row, "serial_no") and row.serial_no:
- # replace commas by linefeed and remove all spaces in string
- row.serial_no = row.serial_no.replace(",", "\n").replace(" ", "")
+ # replace commas by linefeed
+ row.serial_no = row.serial_no.replace(",", "\n")
+
+ # strip preceeding and succeeding spaces for each SN
+ # (SN could have valid spaces in between e.g. SN - 123 - 2021)
+ serial_no_list = row.serial_no.split("\n")
+ serial_no_list = [sn.strip() for sn in serial_no_list]
+
+ row.serial_no = "\n".join(serial_no_list)
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
default_cost_center=None):
@@ -591,7 +598,7 @@ def future_sle_exists(args, sl_entries=None):
data = frappe.db.sql("""
select item_code, warehouse, count(name) as total_row
- from `tabStock Ledger Entry`
+ from `tabStock Ledger Entry` force index (item_warehouse)
where
({})
and timestamp(posting_date, posting_time)
diff --git a/erpnext/crm/doctype/lead/lead.js b/erpnext/crm/doctype/lead/lead.js
index 95cf03241bc..999599ce95b 100644
--- a/erpnext/crm/doctype/lead/lead.js
+++ b/erpnext/crm/doctype/lead/lead.js
@@ -51,7 +51,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
}
}
- add_lead_to_prospect (frm) {
+ add_lead_to_prospect () {
frappe.prompt([
{
fieldname: 'prospect',
@@ -65,7 +65,7 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
frappe.call({
method: 'erpnext.crm.doctype.lead.lead.add_lead_to_prospect',
args: {
- 'lead': frm.doc.name,
+ 'lead': cur_frm.doc.name,
'prospect': data.prospect
},
callback: function(r) {
@@ -79,41 +79,41 @@ erpnext.LeadController = class LeadController extends frappe.ui.form.Controller
}, __('Add Lead to Prospect'), __('Add'));
}
- make_customer (frm) {
+ make_customer () {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_customer",
- frm: frm
+ frm: cur_frm
})
}
- make_opportunity (frm) {
+ make_opportunity () {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_opportunity",
- frm: frm
+ frm: cur_frm
})
}
- make_quotation (frm) {
+ make_quotation () {
frappe.model.open_mapped_doc({
method: "erpnext.crm.doctype.lead.lead.make_quotation",
- frm: frm
+ frm: cur_frm
})
}
- make_prospect (frm) {
+ make_prospect () {
frappe.model.with_doctype("Prospect", function() {
let prospect = frappe.model.get_new_doc("Prospect");
- prospect.company_name = frm.doc.company_name;
- prospect.no_of_employees = frm.doc.no_of_employees;
- prospect.industry = frm.doc.industry;
- prospect.market_segment = frm.doc.market_segment;
- prospect.territory = frm.doc.territory;
- prospect.fax = frm.doc.fax;
- prospect.website = frm.doc.website;
- prospect.prospect_owner = frm.doc.lead_owner;
+ prospect.company_name = cur_frm.doc.company_name;
+ prospect.no_of_employees = cur_frm.doc.no_of_employees;
+ prospect.industry = cur_frm.doc.industry;
+ prospect.market_segment = cur_frm.doc.market_segment;
+ prospect.territory = cur_frm.doc.territory;
+ prospect.fax = cur_frm.doc.fax;
+ prospect.website = cur_frm.doc.website;
+ prospect.prospect_owner = cur_frm.doc.lead_owner;
let lead_prospect_row = frappe.model.add_child(prospect, 'prospect_lead');
- lead_prospect_row.lead = frm.doc.name;
+ lead_prospect_row.lead = cur_frm.doc.name;
frappe.set_route("Form", "Prospect", prospect.name);
});
diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py
index be843a3386c..55e0efaab15 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/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json
index a661b623792..5a63dc18d05 100644
--- a/erpnext/crm/workspace/crm/crm.json
+++ b/erpnext/crm/workspace/crm/crm.json
@@ -1,5 +1,4 @@
{
- "category": "",
"charts": [
{
"chart_name": "Territory Wise Sales"
@@ -7,18 +6,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"CRM\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Lead\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Opportunity\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customer\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Analytics\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Sales Pipeline\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Maintenance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Campaign\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
"creation": "2020-01-23 14:48:30.183272",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "crm",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "CRM",
"links": [
{
@@ -421,15 +414,12 @@
"type": "Link"
}
],
- "modified": "2021-08-19 19:08:08.728876",
+ "modified": "2021-08-20 12:15:56.913092",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM",
- "onboarding": "CRM",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py
index ae498ba57db..be4ee560a51 100644
--- a/erpnext/education/doctype/student/student.py
+++ b/erpnext/education/doctype/student/student.py
@@ -138,7 +138,9 @@ class Student(Document):
enrollment.submit()
return enrollment
- def enroll_in_course(self, course_name, program_enrollment, enrollment_date=frappe.utils.datetime.datetime.now()):
+ def enroll_in_course(self, course_name, program_enrollment, enrollment_date=None):
+ if enrollment_date is None:
+ enrollment_date = frappe.utils.datetime.datetime.now()
try:
enrollment = frappe.get_doc({
"doctype": "Course Enrollment",
diff --git a/erpnext/education/workspace/education/education.json b/erpnext/education/workspace/education/education.json
index c58ddd63cfe..14652956583 100644
--- a/erpnext/education/workspace/education/education.json
+++ b/erpnext/education/workspace/education/education.json
@@ -1,5 +1,4 @@
{
- "category": "",
"charts": [
{
"chart_name": "Program Enrollments",
@@ -8,18 +7,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Education\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Program Enrollments\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Instructor\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Program\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Course\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Fees\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Course Scheduling Tool\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student Attendance Tool\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Student and Instructor\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Content Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Admission\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fees\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Schedule\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"LMS Activity\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assessment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assessment Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
"creation": "2020-03-02 17:22:57.066401",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "education",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Education",
"links": [
{
@@ -699,15 +692,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:15:57.929275",
+ "modified": "2021-08-05 12:15:57.929276",
"modified_by": "Administrator",
"module": "Education",
"name": "Education",
- "onboarding": "Education",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "Education",
"roles": [],
diff --git a/erpnext/stock/report/process_loss_report/__init__.py b/erpnext/erpnext_integrations/doctype/taxjar_nexus/__init__.py
similarity index 100%
rename from erpnext/stock/report/process_loss_report/__init__.py
rename to erpnext/erpnext_integrations/doctype/taxjar_nexus/__init__.py
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.json b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.json
new file mode 100644
index 00000000000..d4d4a512b58
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.json
@@ -0,0 +1,51 @@
+{
+ "actions": [],
+ "allow_rename": 1,
+ "creation": "2021-09-11 05:09:53.773838",
+ "doctype": "DocType",
+ "engine": "InnoDB",
+ "field_order": [
+ "region",
+ "region_code",
+ "country",
+ "country_code"
+ ],
+ "fields": [
+ {
+ "fieldname": "region",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Region"
+ },
+ {
+ "fieldname": "region_code",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Region Code"
+ },
+ {
+ "fieldname": "country",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Country"
+ },
+ {
+ "fieldname": "country_code",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Country Code"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-09-14 05:33:06.444710",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "TaxJar Nexus",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py
new file mode 100644
index 00000000000..c24aa8ca7d4
--- /dev/null
+++ b/erpnext/erpnext_integrations/doctype/taxjar_nexus/taxjar_nexus.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class TaxJarNexus(Document):
+ pass
diff --git a/erpnext/regional/united_states/product_tax_category_data.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/product_tax_category_data.json
similarity index 100%
rename from erpnext/regional/united_states/product_tax_category_data.json
rename to erpnext/erpnext_integrations/doctype/taxjar_settings/product_tax_category_data.json
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js
index 62d5709f51f..d49598932fe 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.js
@@ -5,5 +5,16 @@ frappe.ui.form.on('TaxJar Settings', {
is_sandbox: (frm) => {
frm.toggle_reqd("api_key", !frm.doc.is_sandbox);
frm.toggle_reqd("sandbox_api_key", frm.doc.is_sandbox);
- }
+ },
+
+ refresh: (frm) => {
+ frm.add_custom_button(__('Update Nexus List'), function() {
+ frm.call({
+ doc: frm.doc,
+ method: 'update_nexus_list'
+ });
+ });
+ },
+
+
});
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
index c0d60f7a317..2d17f2ed832 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.json
@@ -6,8 +6,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
- "is_sandbox",
"taxjar_calculate_tax",
+ "is_sandbox",
"taxjar_create_transactions",
"credentials",
"api_key",
@@ -16,7 +16,10 @@
"configuration",
"tax_account_head",
"configuration_cb",
- "shipping_account_head"
+ "shipping_account_head",
+ "section_break_12",
+ "nexus_address",
+ "nexus"
],
"fields": [
{
@@ -54,6 +57,7 @@
},
{
"default": "0",
+ "depends_on": "taxjar_calculate_tax",
"fieldname": "is_sandbox",
"fieldtype": "Check",
"label": "Sandbox Mode"
@@ -69,6 +73,7 @@
},
{
"default": "0",
+ "depends_on": "taxjar_calculate_tax",
"fieldname": "taxjar_create_transactions",
"fieldtype": "Check",
"label": "Create TaxJar Transaction"
@@ -82,11 +87,28 @@
{
"fieldname": "cb_keys",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_12",
+ "fieldtype": "Section Break",
+ "label": "Nexus List"
+ },
+ {
+ "fieldname": "nexus_address",
+ "fieldtype": "HTML",
+ "label": "Nexus Address"
+ },
+ {
+ "fieldname": "nexus",
+ "fieldtype": "Table",
+ "label": "Nexus",
+ "options": "TaxJar Nexus",
+ "read_only": 1
}
],
"issingle": 1,
"links": [],
- "modified": "2020-04-30 04:38:03.311089",
+ "modified": "2021-10-06 10:59:13.475442",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "TaxJar Settings",
diff --git a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
index 9dd481747ec..f430a9e9bae 100644
--- a/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
+++ b/erpnext/erpnext_integrations/doctype/taxjar_settings/taxjar_settings.py
@@ -4,9 +4,98 @@
from __future__ import unicode_literals
-# import frappe
+import json
+import os
+
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document
+from frappe.permissions import add_permission, update_permission_property
+
+from erpnext.erpnext_integrations.taxjar_integration import get_client
class TaxJarSettings(Document):
- pass
+
+ def on_update(self):
+ TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
+ TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
+ TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
+
+ fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
+ fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
+
+ if (TAXJAR_CREATE_TRANSACTIONS or TAXJAR_CALCULATE_TAX or TAXJAR_SANDBOX_MODE):
+ if not fields_already_exist:
+ add_product_tax_categories()
+ make_custom_fields()
+ add_permissions()
+ frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
+
+ elif fields_already_exist and fields_hidden:
+ toggle_tax_category_fields(hidden='0')
+
+ elif fields_already_exist:
+ toggle_tax_category_fields(hidden='1')
+
+ def validate(self):
+ self.calculate_taxes_validation_for_create_transactions()
+
+ @frappe.whitelist()
+ def update_nexus_list(self):
+ client = get_client()
+ nexus = client.nexus_regions()
+
+ new_nexus_list = [frappe._dict(address) for address in nexus]
+
+ self.set('nexus', [])
+ self.set('nexus', new_nexus_list)
+ self.save()
+
+ def calculate_taxes_validation_for_create_transactions(self):
+ if not self.taxjar_calculate_tax and (self.taxjar_create_transactions or self.is_sandbox):
+ frappe.throw(frappe._('Before enabling Create Transaction or Sandbox Mode, you need to check the Enable Tax Calculation box'))
+
+
+def toggle_tax_category_fields(hidden):
+ frappe.set_value('Custom Field', {'dt':'Sales Invoice Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
+ frappe.set_value('Custom Field', {'dt':'Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
+
+
+def add_product_tax_categories():
+ with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
+ tax_categories = json.loads(f.read())
+ create_tax_categories(tax_categories['categories'])
+
+def create_tax_categories(data):
+ for d in data:
+ if not frappe.db.exists('Product Tax Category',{'product_tax_code':d.get('product_tax_code')}):
+ tax_category = frappe.new_doc('Product Tax Category')
+ tax_category.description = d.get("description")
+ tax_category.product_tax_code = d.get("product_tax_code")
+ tax_category.category_name = d.get("name")
+ tax_category.db_insert()
+
+def make_custom_fields(update=True):
+ custom_fields = {
+ 'Sales Invoice Item': [
+ dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
+ label='Product Tax Category', fetch_from='item_code.product_tax_category'),
+ dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
+ label='Tax Collectable', read_only=1),
+ dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
+ label='Taxable Amount', read_only=1)
+ ],
+ 'Item': [
+ dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
+ label='Product Tax Category')
+ ]
+ }
+ create_custom_fields(custom_fields, update=update)
+
+def add_permissions():
+ doctype = "Product Tax Category"
+ for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'):
+ add_permission(doctype, role, 0)
+ update_permission_property(doctype, role, 0, 'write', 1)
+ update_permission_property(doctype, role, 0, 'create', 1)
diff --git a/erpnext/erpnext_integrations/taxjar_integration.py b/erpnext/erpnext_integrations/taxjar_integration.py
index 870a4ef54cc..2a7243c2430 100644
--- a/erpnext/erpnext_integrations/taxjar_integration.py
+++ b/erpnext/erpnext_integrations/taxjar_integration.py
@@ -4,7 +4,7 @@ import frappe
import taxjar
from frappe import _
from frappe.contacts.doctype.address.address import get_company_address
-from frappe.utils import cint
+from frappe.utils import cint, flt
from erpnext import get_default_company
@@ -103,7 +103,7 @@ def get_tax_data(doc):
shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
- line_items = [get_line_item_dict(item) for item in doc.items]
+ line_items = [get_line_item_dict(item, doc.docstatus) for item in doc.items]
if from_shipping_state not in SUPPORTED_STATE_CODES:
from_shipping_state = get_state_code(from_address, 'Company')
@@ -139,14 +139,21 @@ def get_state_code(address, location):
return state_code
-def get_line_item_dict(item):
- return dict(
+def get_line_item_dict(item, docstatus):
+ tax_dict = dict(
id = item.get('idx'),
quantity = item.get('qty'),
unit_price = item.get('rate'),
product_tax_code = item.get('product_tax_category')
)
+ if docstatus == 1:
+ tax_dict.update({
+ 'sales_tax':item.get('tax_collectable')
+ })
+
+ return tax_dict
+
def set_sales_tax(doc, method):
if not TAXJAR_CALCULATE_TAX:
return
@@ -164,6 +171,9 @@ def set_sales_tax(doc, method):
setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
return
+ # check if delivering within a nexus
+ check_for_nexus(doc, tax_dict)
+
tax_data = validate_tax_request(tax_dict)
if tax_data is not None:
if not tax_data.amount_to_collect:
@@ -191,6 +201,17 @@ def set_sales_tax(doc, method):
doc.run_method("calculate_taxes_and_totals")
+def check_for_nexus(doc, tax_dict):
+ if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
+ for item in doc.get("items"):
+ item.tax_collectable = flt(0)
+ item.taxable_amount = flt(0)
+
+ for tax in doc.taxes:
+ if tax.account_head == TAX_ACCOUNT_HEAD:
+ doc.taxes.remove(tax)
+ return
+
def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
index 9f9204a78d8..8e4f92791ab 100644
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Marketplace\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payments\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
"creation": "2020-08-20 19:30:48.138801",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "integration",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "ERPNext Integrations",
"links": [
{
@@ -119,15 +112,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:15:58.740246",
+ "modified": "2021-08-05 12:15:58.740247",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json
index fd4afb85fdd..5fe5afa2c4c 100644
--- a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Integrations Settings\", \"col\": 4}}]",
"creation": "2020-07-31 10:38:54.021237",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "setting",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "ERPNext Integrations Settings",
"links": [
{
@@ -81,15 +74,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:15:58.951704",
+ "modified": "2021-08-05 12:15:58.951705",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations Settings",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index d708f9209f3..05f07f515c4 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -250,6 +250,7 @@ doc_events = {
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": {
+ "after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code",
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
@@ -259,7 +260,10 @@ doc_events = {
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
],
- "on_trash": "erpnext.regional.check_deletion_permission",
+ "on_trash": [
+ "erpnext.regional.check_deletion_permission",
+ "erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
+ ],
"validate": [
"erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values"
diff --git a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py
index bed12e31eaa..8a23682ad47 100644
--- a/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py
+++ b/erpnext/hr/doctype/daily_work_summary/test_daily_work_summary.py
@@ -74,7 +74,6 @@ class TestDailyWorkSummary(unittest.TestCase):
from `tabEmail Queue` as q, `tabEmail Queue Recipient` as r \
where q.name = r.parent""", as_dict=1)
- frappe.db.commit()
def setup_groups(self, hour=None):
# setup email to trigger at this hour
diff --git a/erpnext/hr/doctype/employee/employee_reminders.py b/erpnext/hr/doctype/employee/employee_reminders.py
index 216d8f6bb3a..559bd393e62 100644
--- a/erpnext/hr/doctype/employee/employee_reminders.py
+++ b/erpnext/hr/doctype/employee/employee_reminders.py
@@ -156,6 +156,8 @@ def get_employees_having_an_event_today(event_type):
DAY({condition_column}) = DAY(%(today)s)
AND
MONTH({condition_column}) = MONTH(%(today)s)
+ AND
+ YEAR({condition_column}) < YEAR(%(today)s)
AND
`status` = 'Active'
""",
@@ -166,6 +168,8 @@ def get_employees_having_an_event_today(event_type):
DATE_PART('day', {condition_column}) = date_part('day', %(today)s)
AND
DATE_PART('month', {condition_column}) = date_part('month', %(today)s)
+ AND
+ DATE_PART('year', {condition_column}) < date_part('year', %(today)s)
AND
"status" = 'Active'
""",
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index 8d6dfa2c1d2..8a2da0866e9 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 164d48b8952..b05175200e9 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 b1f66098f0d..29d93f348cc 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 ad2f3ade054..c0440d09e74 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/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js
index 3c4c672816c..218e97d7fc2 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.js
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.js
@@ -10,6 +10,26 @@ frappe.ui.form.on('Expense Claim', {
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ var expenses = frm.doc.expenses;
+ for (var i = 0; i < expenses.length; i++) {
+ var expense = expenses[i];
+ if (!expense.expense_type) {
+ continue;
+ }
+ frappe.call({
+ method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center",
+ args: {
+ "expense_claim_type": expense.expense_type,
+ "company": frm.doc.company
+ },
+ callback: function(r) {
+ if (r.message) {
+ expense.default_account = r.message.account;
+ expense.cost_center = r.message.cost_center;
+ }
+ }
+ });
+ }
},
});
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index 9cb65f7e080..941fd58c7b5 100644
--- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
@@ -176,7 +176,7 @@ def generate_taxes():
account = create_account(company=company_name, account_name="Output Tax CGST", account_type="Tax", parent_account=parent_account)
return {'taxes':[{
"account_head": account,
- "rate": 0,
+ "rate": 9,
"description": "CGST",
"tax_amount": 10,
"total": 210
diff --git a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json
index 020457d4ec6..2f7b8fcf679 100644
--- a/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json
+++ b/erpnext/hr/doctype/expense_taxes_and_charges/expense_taxes_and_charges.json
@@ -56,8 +56,6 @@
},
{
"columns": 2,
- "fetch_from": "account_head.tax_rate",
- "fetch_if_empty": 1,
"fieldname": "rate",
"fieldtype": "Float",
"in_list_view": 1,
@@ -102,7 +100,7 @@
],
"istable": 1,
"links": [],
- "modified": "2020-09-23 20:27:36.027728",
+ "modified": "2021-10-26 20:27:36.027728",
"modified_by": "Administrator",
"module": "HR",
"name": "Expense Taxes and Charges",
@@ -111,4 +109,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/hr/doctype/holiday_list/holiday_list.py b/erpnext/hr/doctype/holiday_list/holiday_list.py
index f46f14d8416..7d1b9916421 100644
--- a/erpnext/hr/doctype/holiday_list/holiday_list.py
+++ b/erpnext/hr/doctype/holiday_list/holiday_list.py
@@ -1,4 +1,3 @@
-
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
@@ -94,9 +93,11 @@ def get_events(start, end, filters=None):
update={"allDay": 1})
-def is_holiday(holiday_list, date=today()):
+def is_holiday(holiday_list, date=None):
"""Returns true if the given date is a holiday in the given holiday list
"""
+ if date is None:
+ date = today()
if holiday_list:
return bool(frappe.get_all('Holiday List',
dict(name=holiday_list, holiday_date=date)))
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index 69af5c54c3b..05b74a0dde9 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -139,7 +139,7 @@ def get_shift_type_timing(shift_types):
return shift_timing_map
-def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=False, next_shift_direction=None):
+def get_employee_shift(employee, for_date=None, consider_default_shift=False, next_shift_direction=None):
"""Returns a Shift Type for the given employee on the given date. (excluding the holidays)
:param employee: Employee for which shift is required.
@@ -147,6 +147,8 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals
:param consider_default_shift: If set to true, default shift is taken when no shift assignment is found.
:param next_shift_direction: One of: None, 'forward', 'reverse'. Direction to look for next shift if shift not found on given date.
"""
+ if for_date is None:
+ for_date = nowdate()
default_shift = frappe.db.get_value('Employee', employee, 'default_shift')
shift_type_name = None
shift_assignment_details = frappe.db.get_value('Shift Assignment', {'employee':employee, 'start_date':('<=', for_date), 'docstatus': '1', 'status': "Active"}, ['shift_type', 'end_date'])
@@ -200,9 +202,11 @@ def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=Fals
return get_shift_details(shift_type_name, for_date)
-def get_employee_shift_timings(employee, for_timestamp=now_datetime(), consider_default_shift=False):
+def get_employee_shift_timings(employee, for_timestamp=None, consider_default_shift=False):
"""Returns previous shift, current/upcoming shift, next_shift for the given timestamp and employee
"""
+ if for_timestamp is None:
+ for_timestamp = now_datetime()
# write and verify a test case for midnight shift.
prev_shift = curr_shift = next_shift = None
curr_shift = get_employee_shift(employee, for_timestamp.date(), consider_default_shift, 'forward')
@@ -220,7 +224,7 @@ def get_employee_shift_timings(employee, for_timestamp=now_datetime(), consider_
return prev_shift, curr_shift, next_shift
-def get_shift_details(shift_type_name, for_date=nowdate()):
+def get_shift_details(shift_type_name, for_date=None):
"""Returns Shift Details which contain some additional information as described below.
'shift_details' contains the following keys:
'shift_type' - Object of DocType Shift Type,
@@ -234,6 +238,8 @@ def get_shift_details(shift_type_name, for_date=nowdate()):
"""
if not shift_type_name:
return None
+ if not for_date:
+ for_date = nowdate()
shift_type = frappe.get_doc('Shift Type', shift_type_name)
start_datetime = datetime.combine(for_date, datetime.min.time()) + shift_type.start_time
for_date = for_date + timedelta(days=1) if shift_type.start_time > shift_type.end_time else for_date
diff --git a/erpnext/hr/doctype/shift_type/shift_type.py b/erpnext/hr/doctype/shift_type/shift_type.py
index e53373df279..7a35b28ac43 100644
--- a/erpnext/hr/doctype/shift_type/shift_type.py
+++ b/erpnext/hr/doctype/shift_type/shift_type.py
@@ -97,7 +97,7 @@ class ShiftType(Document):
assigned_employees = [x[0] for x in assigned_employees]
if consider_default_shift:
- filters = {'default_shift': self.name}
+ filters = {'default_shift': self.name, 'status': ['!=', 'Inactive']}
default_shift_employees = frappe.get_all('Employee', 'name', filters, as_list=True)
default_shift_employees = [x[0] for x in default_shift_employees]
return list(set(assigned_employees+default_shift_employees))
diff --git a/erpnext/hr/doctype/staffing_plan/staffing_plan.py b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
index 57a92b05871..93cd4e1f629 100644
--- a/erpnext/hr/doctype/staffing_plan/staffing_plan.py
+++ b/erpnext/hr/doctype/staffing_plan/staffing_plan.py
@@ -155,7 +155,11 @@ def get_designation_counts(designation, company):
return employee_counts
@frappe.whitelist()
-def get_active_staffing_plan_details(company, designation, from_date=getdate(nowdate()), to_date=getdate(nowdate())):
+def get_active_staffing_plan_details(company, designation, from_date=None, to_date=None):
+ if from_date is None:
+ from_date = getdate(nowdate())
+ if to_date is None:
+ to_date = getdate(nowdate())
if not company or not designation:
frappe.throw(_("Please select Company and Designation"))
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 6bca1368d3f..d463b9b62a8 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 b6f4cadcc9c..0febce1610a 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/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json
index 9c5d0c1b0ec..7408d63eee5 100644
--- a/erpnext/hr/workspace/hr/hr.json
+++ b/erpnext/hr/workspace/hr/hr.json
@@ -1,5 +1,4 @@
{
- "category": "",
"charts": [
{
"chart_name": "Outgoing Salary",
@@ -8,18 +7,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Human Resource\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Employee\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Leave Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Job Applicant\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee Lifecycle\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Shift Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Leaves\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Expense Claims\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fleet Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Recruitment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loans\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Training\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Performance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
"creation": "2020-03-02 15:48:58.322521",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "hr",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "HR",
"links": [
{
@@ -942,15 +935,12 @@
"type": "Link"
}
],
- "modified": "2021-08-31 12:18:59.842918",
+ "modified": "2021-08-31 12:18:59.842919",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",
- "onboarding": "Human Resource",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/loan_management/doctype/loan/loan.json b/erpnext/loan_management/doctype/loan/loan.json
index c9f23ca4df3..5979992bbe8 100644
--- a/erpnext/loan_management/doctype/loan/loan.json
+++ b/erpnext/loan_management/doctype/loan/loan.json
@@ -334,7 +334,6 @@
},
{
"depends_on": "eval:doc.is_secured_loan",
- "fetch_from": "loan_application.maximum_loan_amount",
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"label": "Maximum Loan Amount",
@@ -360,7 +359,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-19 18:10:32.360818",
+ "modified": "2021-10-12 18:10:32.360818",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index 7dbd42297e1..0f2c3cfdfc0 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -137,16 +137,23 @@ class Loan(AccountsController):
frappe.throw(_("Loan amount is mandatory"))
def link_loan_security_pledge(self):
- if self.is_secured_loan:
- loan_security_pledge = frappe.db.get_value('Loan Security Pledge', {'loan_application': self.loan_application},
- 'name')
+ if self.is_secured_loan and self.loan_application:
+ maximum_loan_value = frappe.db.get_value('Loan Security Pledge',
+ {
+ 'loan_application': self.loan_application,
+ 'status': 'Requested'
+ },
+ 'sum(maximum_loan_value)'
+ )
- if loan_security_pledge:
- frappe.db.set_value('Loan Security Pledge', loan_security_pledge, {
- 'loan': self.name,
- 'status': 'Pledged',
- 'pledge_time': now_datetime()
- })
+ if maximum_loan_value:
+ frappe.db.sql("""
+ UPDATE `tabLoan Security Pledge`
+ SET loan = %s, pledge_time = %s, status = 'Pledged'
+ WHERE status = 'Requested' and loan_application = %s
+ """, (self.name, now_datetime(), self.loan_application))
+
+ self.db_set('maximum_loan_amount', maximum_loan_value)
def unlink_loan_security_pledge(self):
pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name})
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
index e492920abb3..ede0467b0e7 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -130,10 +130,11 @@ class LoanApplication(Document):
def create_loan(source_name, target_doc=None, submit=0):
def update_accounts(source_doc, target_doc, source_parent):
account_details = frappe.get_all("Loan Type",
- fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"],
- filters = {'name': source_doc.loan_type}
- )[0]
+ fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"],
+ filters = {'name': source_doc.loan_type})[0]
+ if source_doc.is_secured_loan:
+ target_doc.maximum_loan_amount = 0
target_doc.mode_of_payment = account_details.mode_of_payment
target_doc.payment_account = account_details.payment_account
diff --git a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
index 6d9d4f490d3..99f0d259246 100644
--- a/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
+++ b/erpnext/loan_management/doctype/loan_disbursement/loan_disbursement.py
@@ -198,7 +198,7 @@ def get_disbursal_amount(loan, on_current_security_price=0):
security_value = get_total_pledged_security_value(loan)
if loan_details.is_secured_loan and not on_current_security_price:
- security_value = flt(loan_details.maximum_loan_amount)
+ security_value = get_maximum_amount_as_per_pledged_security(loan)
if not security_value and not loan_details.is_secured_loan:
security_value = flt(loan_details.loan_amount)
@@ -209,3 +209,6 @@ def get_disbursal_amount(loan, on_current_security_price=0):
disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount
return disbursal_amount
+
+def get_maximum_amount_as_per_pledged_security(loan):
+ return flt(frappe.db.get_value('Loan Security Pledge', {'loan': loan}, 'sum(maximum_loan_value)'))
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 13b73573274..40bb581165b 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -411,7 +411,7 @@ def get_amounts(amounts, against_loan, posting_date):
if due_date and not final_due_date:
final_due_date = add_days(due_date, loan_type_details.grace_period_in_days)
- if against_loan_doc.status in ('Disbursed', 'Loan Closure Requested', 'Closed'):
+ if against_loan_doc.status in ('Disbursed', 'Closed') or against_loan_doc.disbursed_amount >= against_loan_doc.loan_amount:
pending_principal_amount = against_loan_doc.total_payment - against_loan_doc.total_principal_paid \
- against_loan_doc.total_interest_payable - against_loan_doc.written_off_amount
else:
diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json
index ca528ec6bd9..7deee0d4612 100644
--- a/erpnext/loan_management/workspace/loan_management/loan_management.json
+++ b/erpnext/loan_management/workspace/loan_management/loan_management.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Loan Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Loan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Processes\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Disbursement and Repayment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Security\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
"creation": "2020-03-12 16:35:55.299820",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "loan",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Loans",
"links": [
{
@@ -245,15 +238,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:18:13.350904",
+ "modified": "2021-08-05 12:18:13.350905",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loans",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index 0bf5aeae711..adb57f9f397 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -47,7 +47,7 @@ class MaintenanceSchedule(TransactionBase):
"Yearly": 365
}
for item in self.items:
- if item.periodicity and item.start_date:
+ if item.periodicity and item.periodicity != "Random" and item.start_date:
if not item.end_date:
if item.no_of_visits:
item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
@@ -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/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 7cfec974fc8..232e3a0b0ff 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -1133,8 +1133,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
query_filters["has_variants"] = 0
if filters and filters.get("is_stock_item"):
- or_cond_filters["is_stock_item"] = 1
- or_cond_filters["has_variants"] = 1
+ query_filters["is_stock_item"] = 1
return frappe.get_list("Item",
fields = fields, filters=query_filters,
diff --git a/erpnext/manufacturing/doctype/bom/test_bom.py b/erpnext/manufacturing/doctype/bom/test_bom.py
index 706ea268c6e..4c032307d80 100644
--- a/erpnext/manufacturing/doctype/bom/test_bom.py
+++ b/erpnext/manufacturing/doctype/bom/test_bom.py
@@ -4,13 +4,14 @@
import unittest
from collections import deque
+from functools import partial
import frappe
from frappe.test_runner import make_test_records
from frappe.utils import cstr, flt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
-from erpnext.manufacturing.doctype.bom.bom import make_variant_bom
+from erpnext.manufacturing.doctype.bom.bom import item_query, make_variant_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
@@ -375,6 +376,16 @@ class TestBOM(unittest.TestCase):
# FG Items in Scrap/Loss Table should have Is Process Loss set
self.assertRaises(frappe.ValidationError, bom_doc.submit)
+ def test_bom_item_query(self):
+ query = partial(item_query, doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters={"is_stock_item": 1})
+
+ test_items = query(txt="_Test")
+ filtered = query(txt="_Test Item 2")
+
+ self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results")
+ self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
+
+
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index b9efe9b41ea..2424ef9a71c 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -311,7 +311,7 @@ class ProductionPlan(Document):
if self.total_produced_qty > 0:
self.status = "In Process"
- if self.total_produced_qty >= self.total_planned_qty:
+ if self.check_have_work_orders_completed():
self.status = "Completed"
if self.status != 'Completed':
@@ -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,
@@ -575,6 +575,15 @@ class ProductionPlan(Document):
self.append("sub_assembly_items", data)
+ def check_have_work_orders_completed(self):
+ wo_status = frappe.db.get_list(
+ "Work Order",
+ filters={"production_plan": self.name},
+ fields="status",
+ pluck="status"
+ )
+ return all(s == "Completed" for s in wo_status)
+
@frappe.whitelist()
def download_raw_materials(doc, warehouses=None):
if isinstance(doc, str):
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 a7aec315ff2..74bd685b799 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/manufacturing/report/process_loss_report/__init__.py b/erpnext/manufacturing/report/process_loss_report/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
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 afe4aff7f1c..7d3d13d98cf 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 499c49f893c..9b544dafa5f 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 00000000000..1de472659eb
--- /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/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
index 84eabcd2bdb..cfa80f8e9fc 100644
--- a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -1,5 +1,4 @@
{
- "category": "",
"charts": [
{
"chart_name": "Produced Quantity"
@@ -7,18 +6,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Manufacturing\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Plan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Forecasting\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM Stock Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Planning Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Production\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bill of Materials\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
"creation": "2020-03-02 17:11:37.032604",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "organization",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Manufacturing",
"links": [
{
@@ -304,15 +297,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:00.825741",
+ "modified": "2021-08-05 12:16:00.825742",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
- "onboarding": "Manufacturing",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "Manufacturing",
"roles": [],
diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json
index e6d4445945e..ba2f919d016 100644
--- a/erpnext/non_profit/workspace/non_profit/non_profit.json
+++ b/erpnext/non_profit/workspace/non_profit/non_profit.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Member\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Non Profit Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Membership\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chapter\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chapter Member\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Grant Application\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Membership\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Volunteer\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Chapter\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Donation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tax Exemption Certification (India)\", \"col\": 4}}]",
"creation": "2020-03-02 17:23:47.811421",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "non-profit",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Non Profit",
"links": [
{
@@ -238,15 +231,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.146206",
+ "modified": "2021-08-05 12:16:01.146207",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Non Profit",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "Non Profit",
"roles": [],
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 22a63139942..1dac50c6e19 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -246,7 +246,7 @@ erpnext.patches.v13_0.update_payment_terms_outstanding
erpnext.patches.v12_0.add_state_code_for_ladakh
erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
-erpnext.patches.v12_0.update_vehicle_no_reqd_condition
+erpnext.patches.v13_0.update_vehicle_no_reqd_condition
erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation
erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings
erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae
@@ -286,12 +286,15 @@ erpnext.patches.v13_0.shopify_deprecation_warning
erpnext.patches.v13_0.migrate_stripe_api
erpnext.patches.v13_0.reset_clearance_date_for_intracompany_payment_entries
erpnext.patches.v13_0.einvoicing_deprecation_warning
+execute:frappe.reload_doc("erpnext_integrations", "doctype", "TaxJar Settings")
+execute:frappe.reload_doc("erpnext_integrations", "doctype", "Product Tax Category")
erpnext.patches.v13_0.custom_fields_for_taxjar_integration
erpnext.patches.v14_0.delete_einvoicing_doctypes
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
@@ -299,7 +302,13 @@ erpnext.patches.v13_0.gst_fields_for_pos_invoice
erpnext.patches.v13_0.create_accounting_dimensions_in_pos_doctypes
erpnext.patches.v13_0.trim_sales_invoice_custom_field_length
erpnext.patches.v13_0.create_custom_field_for_finance_book
-erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries
+erpnext.patches.v13_0.modify_invalid_gain_loss_gl_entries #2
erpnext.patches.v13_0.fix_additional_cost_in_mfg_stock_entry
erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
-erpnext.patches.v13_0.add_default_interview_notification_templates
\ No newline at end of file
+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.v12_0.update_production_plan_status
+erpnext.patches.v13_0.healthcare_deprecation_warning
+erpnext.patches.v14_0.delete_healthcare_doctypes
+erpnext.patches.v13_0.create_pan_field_for_india #2
diff --git a/erpnext/patches/v12_0/update_production_plan_status.py b/erpnext/patches/v12_0/update_production_plan_status.py
new file mode 100644
index 00000000000..06fc503a33f
--- /dev/null
+++ b/erpnext/patches/v12_0/update_production_plan_status.py
@@ -0,0 +1,31 @@
+# Copyright (c) 2021, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+
+
+def execute():
+ frappe.reload_doc("manufacturing", "doctype", "production_plan")
+ frappe.db.sql("""
+ UPDATE `tabProduction Plan` ppl
+ SET status = "Completed"
+ WHERE ppl.name IN (
+ SELECT ss.name FROM (
+ SELECT
+ (
+ count(wo.status = "Completed") =
+ count(pp.name)
+ ) =
+ (
+ pp.status != "Completed"
+ AND pp.total_produced_qty >= pp.total_planned_qty
+ ) AS should_set,
+ pp.name AS name
+ FROM
+ `tabWork Order` wo INNER JOIN`tabProduction Plan` pp
+ ON wo.production_plan = pp.name
+ GROUP BY pp.name
+ HAVING should_set = 1
+ ) ss
+ )
+ """)
diff --git a/erpnext/patches/v13_0/create_pan_field_for_india.py b/erpnext/patches/v13_0/create_pan_field_for_india.py
new file mode 100644
index 00000000000..c37651aaa3b
--- /dev/null
+++ b/erpnext/patches/v13_0/create_pan_field_for_india.py
@@ -0,0 +1,28 @@
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+
+
+def execute():
+ frappe.reload_doc('buying', 'doctype', 'supplier', force=True)
+ frappe.reload_doc('selling', 'doctype', 'customer', force=True)
+
+ custom_fields = {
+ 'Supplier': [
+ {
+ 'fieldname': 'pan',
+ 'label': 'PAN',
+ 'fieldtype': 'Data',
+ 'insert_after': 'supplier_type'
+ }
+ ],
+ 'Customer': [
+ {
+ 'fieldname': 'pan',
+ 'label': 'PAN',
+ 'fieldtype': 'Data',
+ 'insert_after': 'customer_type'
+ }
+ ]
+ }
+
+ create_custom_fields(custom_fields, update=True)
diff --git a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py
index eee9f1189e5..e136d64bb56 100644
--- a/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py
+++ b/erpnext/patches/v13_0/custom_fields_for_taxjar_integration.py
@@ -3,7 +3,7 @@ from __future__ import unicode_literals
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
-from erpnext.regional.united_states.setup import add_permissions
+from erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings import add_permissions
def execute():
@@ -11,7 +11,12 @@ def execute():
if not company:
return
- frappe.reload_doc("regional", "doctype", "product_tax_category")
+ TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
+ TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
+ TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
+
+ if (not TAXJAR_CREATE_TRANSACTIONS and not TAXJAR_CALCULATE_TAX and not TAXJAR_SANDBOX_MODE):
+ return
custom_fields = {
'Sales Invoice Item': [
@@ -29,4 +34,4 @@ def execute():
}
create_custom_fields(custom_fields, update=True)
add_permissions()
- frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=True)
+ frappe.enqueue('erpnext.erpnext_integrations.doctype.taxjar_settings.taxjar_settings.add_product_tax_categories', now=True)
diff --git a/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py b/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py
new file mode 100644
index 00000000000..7a51b432117
--- /dev/null
+++ b/erpnext/patches/v13_0/enable_scheduler_job_for_item_reposting.py
@@ -0,0 +1,8 @@
+import frappe
+
+
+def execute():
+ frappe.reload_doc('core', 'doctype', 'scheduled_job_type')
+ if frappe.db.exists('Scheduled Job Type', 'repost_item_valuation.repost_entries'):
+ frappe.db.set_value('Scheduled Job Type',
+ 'repost_item_valuation.repost_entries', 'stopped', 0)
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 00000000000..4395757159f
--- /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/v13_0/modify_invalid_gain_loss_gl_entries.py b/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
index fa8a86437d0..3af7dac3422 100644
--- a/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
+++ b/erpnext/patches/v13_0/modify_invalid_gain_loss_gl_entries.py
@@ -17,7 +17,7 @@ def execute():
where
ref_exchange_rate = 1
and docstatus = 1
- and ifnull(exchange_gain_loss, '') != ''
+ and ifnull(exchange_gain_loss, 0) != 0
group by
parent
""", as_dict=1)
@@ -30,7 +30,7 @@ def execute():
where
ref_exchange_rate = 1
and docstatus = 1
- and ifnull(exchange_gain_loss, '') != ''
+ and ifnull(exchange_gain_loss, 0) != 0
group by
parent
""", as_dict=1)
@@ -38,12 +38,24 @@ def execute():
if purchase_invoices + sales_invoices:
frappe.log_error(json.dumps(purchase_invoices + sales_invoices, indent=2), title="Patch Log")
+ acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto')
+ if acc_frozen_upto:
+ frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
+
for invoice in purchase_invoices + sales_invoices:
- doc = frappe.get_doc(invoice.type, invoice.name)
- doc.docstatus = 2
- doc.make_gl_entries()
- for advance in doc.advances:
- if advance.ref_exchange_rate == 1:
- advance.db_set('exchange_gain_loss', 0, False)
- doc.docstatus = 1
- doc.make_gl_entries()
\ No newline at end of file
+ try:
+ doc = frappe.get_doc(invoice.type, invoice.name)
+ doc.docstatus = 2
+ doc.make_gl_entries()
+ for advance in doc.advances:
+ if advance.ref_exchange_rate == 1:
+ advance.db_set('exchange_gain_loss', 0, False)
+ doc.docstatus = 1
+ doc.make_gl_entries()
+ frappe.db.commit()
+ except Exception:
+ frappe.db.rollback()
+ print(f'Failed to correct gl entries of {invoice.name}')
+
+ if acc_frozen_upto:
+ frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', acc_frozen_upto)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/requeue_failed_reposts.py b/erpnext/patches/v13_0/requeue_failed_reposts.py
new file mode 100644
index 00000000000..213cb9e26e4
--- /dev/null
+++ b/erpnext/patches/v13_0/requeue_failed_reposts.py
@@ -0,0 +1,13 @@
+import frappe
+from frappe.utils import cstr
+
+
+def execute():
+
+ reposts = frappe.get_all("Repost Item Valuation",
+ {"status": "Failed", "modified": [">", "2021-10-05"] },
+ ["name", "modified", "error_log"])
+
+ for repost in reposts:
+ if "check_freezing_date" in cstr(repost.error_log):
+ frappe.db.set_value("Repost Item Valuation", repost.name, "status", "Queued")
diff --git a/erpnext/patches/v12_0/update_vehicle_no_reqd_condition.py b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
similarity index 81%
rename from erpnext/patches/v12_0/update_vehicle_no_reqd_condition.py
rename to erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
index 69bfaaa2cb1..902707b4b66 100644
--- a/erpnext/patches/v12_0/update_vehicle_no_reqd_condition.py
+++ b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
@@ -2,7 +2,7 @@ import frappe
def execute():
- frappe.reload_doc('custom', 'doctype', 'custom_field')
+ frappe.reload_doc('custom', 'doctype', 'custom_field', force=True)
company = frappe.get_all('Company', filters = {'country': 'India'})
if not company:
return
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 00000000000..28fc01beab5
--- /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 7c0a8eac99c..b6377f40066 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 393f647cc88..665f0a8297e 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.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index 19744037a54..7a80e69374f 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -329,7 +329,7 @@
{
"fieldname": "earning_deduction",
"fieldtype": "Section Break",
- "label": "Earning & Deduction",
+ "label": "Earnings & Deductions",
"oldfieldtype": "Section Break"
},
{
@@ -380,7 +380,7 @@
"depends_on": "total_loan_repayment",
"fieldname": "loan_repayment",
"fieldtype": "Section Break",
- "label": "Loan repayment"
+ "label": "Loan Repayment"
},
{
"fieldname": "loans",
@@ -425,7 +425,7 @@
{
"fieldname": "net_pay_info",
"fieldtype": "Section Break",
- "label": "net pay info"
+ "label": "Net Pay Info"
},
{
"fieldname": "net_pay",
@@ -647,7 +647,7 @@
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-01 10:35:52.374549",
+ "modified": "2021-10-08 11:47:47.098248",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index d113e7e5697..bee96b6430c 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 9ed6686d483..c4b6a38c4e3 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -141,7 +141,6 @@ class TestSalarySlip(unittest.TestCase):
create_salary_structure_assignment,
)
- no_of_days = self.get_no_of_days()
# Payroll based on attendance
frappe.db.set_value("Payroll Settings", None, "payroll_based_on", "Attendance")
@@ -168,9 +167,6 @@ class TestSalarySlip(unittest.TestCase):
ss = make_salary_slip_for_payment_days_dependency_test("test_payment_days_based_component@salary.com", salary_structure.name)
self.assertEqual(ss.absent_days, 1)
- days_in_month = no_of_days[0]
- no_of_holidays = no_of_days[1]
-
ss.reload()
payment_days_based_comp_amount = 0
for component in ss.earnings:
@@ -540,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
@@ -992,13 +1043,14 @@ def make_salary_structure_for_payment_days_based_component_dependency():
return salary_structure_doc
def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure):
- employee = frappe.db.get_value("Employee", {
- "user_id": employee
- },
+ employee = frappe.db.get_value(
+ "Employee",
+ {"user_id": employee},
["name", "company", "employee_name"],
- as_dict=True)
+ as_dict=True
+ )
- salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": employee})})
+ salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": employee.name})
if not salary_slip_name:
salary_slip = make_salary_slip(salary_structure, employee=employee.name)
@@ -1009,4 +1061,18 @@ def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure
else:
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
- return salary_slip
\ No newline at end of file
+ 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/payroll/workspace/payroll/payroll.json b/erpnext/payroll/workspace/payroll/payroll.json
index b55bdc77112..7246dae5bc9 100644
--- a/erpnext/payroll/workspace/payroll/payroll.json
+++ b/erpnext/payroll/workspace/payroll/payroll.json
@@ -1,5 +1,4 @@
{
- "category": "",
"charts": [
{
"chart_name": "Outgoing Salary",
@@ -8,18 +7,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Payroll\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Salary Structure\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Payroll Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Salary Slip\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Income Tax Slab\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Salary Register\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payroll\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Taxation\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Compensations\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
"creation": "2020-05-27 19:54:23.405607",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "money-coins-1",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Payroll",
"links": [
{
@@ -319,15 +312,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.335324",
+ "modified": "2021-08-05 12:16:01.335325",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll",
- "onboarding": "Payroll",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index 1655b76b988..f615f051f0c 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/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json
index 065f1eda1f3..1df2b089839 100644
--- a/erpnext/projects/workspace/projects/projects.json
+++ b/erpnext/projects/workspace/projects/projects.json
@@ -1,5 +1,4 @@
{
- "category": "",
"charts": [
{
"chart_name": "Project Summary",
@@ -8,18 +7,12 @@
],
"content": "[{\"type\": \"chart\", \"data\": {\"chart_name\": \"Open Projects\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Task\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Project\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Timesheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Project Billing Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Projects\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Time Tracking\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
"creation": "2020-03-02 15:46:04.874669",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "project",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Projects",
"links": [
{
@@ -201,15 +194,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.540145",
+ "modified": "2021-08-05 12:16:01.540147",
"modified_by": "Administrator",
"module": "Projects",
"name": "Projects",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 702064fe556..b5a6d8fdf65 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/help_links.js b/erpnext/public/js/help_links.js
index d0c935f4887..b643ccae947 100644
--- a/erpnext/public/js/help_links.js
+++ b/erpnext/public/js/help_links.js
@@ -5,7 +5,7 @@ const docsUrl = "https://erpnext.com/docs/";
frappe.help.help_links["Form/Rename Tool"] = [
{
label: "Bulk Rename",
- url: docsUrl + "user/manual/en/setting-up/data/bulk-rename",
+ url: docsUrl + "user/manual/en/using-erpnext/articles/bulk-rename",
},
];
@@ -59,10 +59,23 @@ frappe.help.help_links["Form/System Settings"] = [
},
];
-frappe.help.help_links["data-import-tool"] = [
+frappe.help.help_links["Form/Data Import"] = [
{
label: "Importing and Exporting Data",
- url: docsUrl + "user/manual/en/setting-up/data/data-import-tool",
+ url: docsUrl + "user/manual/en/setting-up/data/data-import",
+ },
+ {
+ label: "Overwriting Data from Data Import Tool",
+ url:
+ docsUrl +
+ "user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool",
+ },
+];
+
+frappe.help.help_links["List/Data Import"] = [
+ {
+ label: "Importing and Exporting Data",
+ url: docsUrl + "user/manual/en/setting-up/data/data-import",
},
{
label: "Overwriting Data from Data Import Tool",
@@ -101,14 +114,14 @@ frappe.help.help_links["Form/Global Defaults"] = [
},
];
-frappe.help.help_links["Form/Email Digest"] = [
+frappe.help.help_links["List/Print Heading"] = [
{
- label: "Email Digest",
- url: docsUrl + "user/manual/en/setting-up/email/email-digest",
+ label: "Print Heading",
+ url: docsUrl + "user/manual/en/setting-up/print/print-headings",
},
];
-frappe.help.help_links["List/Print Heading"] = [
+frappe.help.help_links["Form/Print Heading"] = [
{
label: "Print Heading",
url: docsUrl + "user/manual/en/setting-up/print/print-headings",
@@ -153,18 +166,25 @@ frappe.help.help_links["List/Email Account"] = [
frappe.help.help_links["List/Notification"] = [
{
label: "Notification",
- url: docsUrl + "user/manual/en/setting-up/email/notifications",
+ url: docsUrl + "user/manual/en/setting-up/notifications",
},
];
frappe.help.help_links["Form/Notification"] = [
{
label: "Notification",
- url: docsUrl + "user/manual/en/setting-up/email/notifications",
+ url: docsUrl + "user/manual/en/setting-up/notifications",
},
];
-frappe.help.help_links["List/Email Digest"] = [
+frappe.help.help_links["Form/Email Digest"] = [
+ {
+ label: "Email Digest",
+ url: docsUrl + "user/manual/en/setting-up/email/email-digest",
+ },
+];
+
+frappe.help.help_links["Form/Email Digest"] = [
{
label: "Email Digest",
url: docsUrl + "user/manual/en/setting-up/email/email-digest",
@@ -174,7 +194,7 @@ frappe.help.help_links["List/Email Digest"] = [
frappe.help.help_links["List/Auto Email Report"] = [
{
label: "Auto Email Reports",
- url: docsUrl + "user/manual/en/setting-up/email/email-reports",
+ url: docsUrl + "user/manual/en/setting-up/email/auto-email-reports",
},
];
@@ -188,14 +208,7 @@ frappe.help.help_links["Form/Print Settings"] = [
frappe.help.help_links["print-format-builder"] = [
{
label: "Print Format Builder",
- url: docsUrl + "user/manual/en/setting-up/print/print-settings",
- },
-];
-
-frappe.help.help_links["List/Print Heading"] = [
- {
- label: "Print Heading",
- url: docsUrl + "user/manual/en/setting-up/print/print-headings",
+ url: docsUrl + "user/manual/en/setting-up/print/print-format-builder",
},
];
@@ -300,7 +313,7 @@ frappe.help.help_links["List/Sales Order"] = [
},
{
label: "Recurring Sales Order",
- url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
},
{
label: "Applying Discount",
@@ -315,7 +328,7 @@ frappe.help.help_links["Form/Sales Order"] = [
},
{
label: "Recurring Sales Order",
- url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
},
{
label: "Applying Discount",
@@ -344,14 +357,14 @@ frappe.help.help_links["Form/Sales Order"] = [
frappe.help.help_links["Form/Product Bundle"] = [
{
label: "Product Bundle",
- url: docsUrl + "user/manual/en/selling/setup/product-bundle",
+ url: docsUrl + "user/manual/en/selling/product-bundle",
},
];
frappe.help.help_links["Form/Selling Settings"] = [
{
label: "Selling Settings",
- url: docsUrl + "user/manual/en/selling/setup/selling-settings",
+ url: docsUrl + "user/manual/en/selling/selling-settings",
},
];
@@ -397,7 +410,7 @@ frappe.help.help_links["List/Purchase Order"] = [
},
{
label: "Recurring Purchase Order",
- url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
},
];
@@ -420,7 +433,7 @@ frappe.help.help_links["Form/Purchase Order"] = [
},
{
label: "Recurring Purchase Order",
- url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
},
{
label: "Subcontracting",
@@ -435,24 +448,17 @@ frappe.help.help_links["List/Purchase Taxes and Charges Template"] = [
},
];
-frappe.help.help_links["List/POS Profile"] = [
- {
- label: "POS Profile",
- url: docsUrl + "user/manual/en/setting-up/pos-setting",
- },
-];
-
frappe.help.help_links["List/Price List"] = [
{
label: "Price List",
- url: docsUrl + "user/manual/en/setting-up/price-lists",
+ url: docsUrl + "user/manual/en/stock/price-lists",
},
];
frappe.help.help_links["List/Authorization Rule"] = [
{
label: "Authorization Rule",
- url: docsUrl + "user/manual/en/setting-up/authorization-rule",
+ url: docsUrl + "user/manual/en/customize-erpnext/authorization-rule",
},
];
@@ -468,27 +474,14 @@ frappe.help.help_links["List/Stock Reconciliation"] = [
label: "Stock Reconciliation",
url:
docsUrl +
- "user/manual/en/setting-up/stock-reconciliation-for-non-serialized-item",
+ "user/manual/en/stock/stock-reconciliation",
},
];
frappe.help.help_links["Tree/Territory"] = [
{
label: "Territory",
- url: docsUrl + "user/manual/en/setting-up/territory",
- },
-];
-
-frappe.help.help_links["Form/Dropbox Backup"] = [
- {
- label: "Dropbox Backup",
- url: docsUrl + "user/manual/en/setting-up/third-party-backups",
- },
- {
- label: "Setting Up Dropbox Backup",
- url:
- docsUrl +
- "user/manual/en/setting-up/articles/setting-up-dropbox-backups",
+ url: docsUrl + "user/manual/en/selling/territory",
},
];
@@ -501,12 +494,6 @@ frappe.help.help_links["List/Company"] = [
label: "Company",
url: docsUrl + "user/manual/en/setting-up/company-setup",
},
- {
- label: "Managing Multiple Companies",
- url:
- docsUrl +
- "user/manual/en/setting-up/articles/managing-multiple-companies",
- },
{
label: "Delete All Related Transactions for a Company",
url:
@@ -517,21 +504,6 @@ frappe.help.help_links["List/Company"] = [
//Accounts
-frappe.help.help_links["modules/Accounts"] = [
- {
- label: "Introduction to Accounts",
- url: docsUrl + "user/manual/en/accounts/",
- },
- {
- label: "Chart of Accounts",
- url: docsUrl + "user/manual/en/accounts/chart-of-accounts.html",
- },
- {
- label: "Multi Currency Accounting",
- url: docsUrl + "user/manual/en/accounts/multi-currency-accounting",
- },
-];
-
frappe.help.help_links["Tree/Account"] = [
{
label: "Chart of Accounts",
@@ -552,7 +524,7 @@ frappe.help.help_links["Form/Sales Invoice"] = [
},
{
label: "Accounts Opening Balance",
- url: docsUrl + "user/manual/en/accounts/opening-accounts",
+ url: docsUrl + "user/manual/en/accounts/opening-balance",
},
{
label: "Sales Return",
@@ -560,7 +532,7 @@ frappe.help.help_links["Form/Sales Invoice"] = [
},
{
label: "Recurring Sales Invoice",
- url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
},
];
@@ -571,7 +543,7 @@ frappe.help.help_links["List/Sales Invoice"] = [
},
{
label: "Accounts Opening Balance",
- url: docsUrl + "user/manual/en/accounts/opening-accounts",
+ url: docsUrl + "user/manual/en/accounts/opening-balances",
},
{
label: "Sales Return",
@@ -579,21 +551,28 @@ frappe.help.help_links["List/Sales Invoice"] = [
},
{
label: "Recurring Sales Invoice",
- url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
},
];
-frappe.help.help_links["pos"] = [
+frappe.help.help_links["point-of-sale"] = [
{
label: "Point of Sale Invoice",
- url: docsUrl + "user/manual/en/accounts/point-of-sale-pos-invoice",
+ url: docsUrl + "user/manual/en/accounts/point-of-sales",
},
];
frappe.help.help_links["List/POS Profile"] = [
{
label: "Point of Sale Profile",
- url: docsUrl + "user/manual/en/setting-up/pos-setting",
+ url: docsUrl + "user/manual/en/accounts/pos-profile",
+ },
+];
+
+frappe.help.help_links["Form/POS Profile"] = [
+ {
+ label: "POS Profile",
+ url: docsUrl + "user/manual/en/accounts/pos-profile",
},
];
@@ -604,11 +583,11 @@ frappe.help.help_links["List/Purchase Invoice"] = [
},
{
label: "Accounts Opening Balance",
- url: docsUrl + "user/manual/en/accounts/opening-accounts",
+ url: docsUrl + "user/manual/en/accounts/opening-balance",
},
{
label: "Recurring Purchase Invoice",
- url: docsUrl + "user/manual/en/accounts/recurring-orders-and-invoices",
+ url: docsUrl + "user/manual/en/accounts/articles/recurring-orders-and-invoices",
},
];
@@ -623,7 +602,7 @@ frappe.help.help_links["List/Journal Entry"] = [
},
{
label: "Accounts Opening Balance",
- url: docsUrl + "user/manual/en/accounts/opening-accounts",
+ url: docsUrl + "user/manual/en/accounts/opening-balance",
},
];
@@ -644,7 +623,7 @@ frappe.help.help_links["List/Payment Request"] = [
frappe.help.help_links["List/Asset"] = [
{
label: "Managing Fixed Assets",
- url: docsUrl + "user/manual/en/accounts/opening-balance/fixed_assets",
+ url: docsUrl + "user/manual/en/asset",
},
];
@@ -659,6 +638,8 @@ frappe.help.help_links["Tree/Cost Center"] = [
{ label: "Budgeting", url: docsUrl + "user/manual/en/accounts/budgeting" },
];
+//Stock
+
frappe.help.help_links["List/Item"] = [
{ label: "Item", url: docsUrl + "user/manual/en/stock/item" },
{
@@ -676,7 +657,7 @@ frappe.help.help_links["List/Item"] = [
},
{
label: "Managing Fixed Assets",
- url: docsUrl + "user/manual/en/accounts/opening-balance/fixed_assets",
+ url: docsUrl + "user/manual/en/asset",
},
{
label: "Item Codification",
@@ -711,7 +692,7 @@ frappe.help.help_links["Form/Item"] = [
},
{
label: "Managing Fixed Assets",
- url: docsUrl + "user/manual/en/accounts/opening-balance/fixed_assets",
+ url: docsUrl + "user/manual/en/asset",
},
{
label: "Item Codification",
@@ -771,10 +752,6 @@ frappe.help.help_links["Form/Delivery Note"] = [
url:
docsUrl + "user/manual/en/stock/articles/track-items-using-barcode",
},
- {
- label: "Subcontracting",
- url: docsUrl + "user/manual/en/manufacturing/subcontracting",
- },
];
frappe.help.help_links["List/Installation Note"] = [
@@ -784,21 +761,10 @@ frappe.help.help_links["List/Installation Note"] = [
},
];
-frappe.help.help_links["Tree"] = [
- {
- label: "Managing Tree Structure Masters",
- url:
- docsUrl +
- "user/manual/en/setting-up/articles/managing-tree-structure-masters",
- },
-];
-
frappe.help.help_links["List/Budget"] = [
{ label: "Budgeting", url: docsUrl + "user/manual/en/accounts/budgeting" },
];
-//Stock
-
frappe.help.help_links["List/Material Request"] = [
{
label: "Material Request",
@@ -861,6 +827,10 @@ frappe.help.help_links["Form/Serial No"] = [
{ label: "Serial No", url: docsUrl + "user/manual/en/stock/serial-no" },
];
+frappe.help.help_links["List/Batch"] = [
+ { label: "Batch", url: docsUrl + "user/manual/en/stock/batch" },
+];
+
frappe.help.help_links["Form/Batch"] = [
{ label: "Batch", url: docsUrl + "user/manual/en/stock/batch" },
];
@@ -868,35 +838,35 @@ frappe.help.help_links["Form/Batch"] = [
frappe.help.help_links["Form/Packing Slip"] = [
{
label: "Packing Slip",
- url: docsUrl + "user/manual/en/stock/tools/packing-slip",
+ url: docsUrl + "user/manual/en/stock/packing-slip",
},
];
frappe.help.help_links["Form/Quality Inspection"] = [
{
label: "Quality Inspection",
- url: docsUrl + "user/manual/en/stock/tools/quality-inspection",
+ url: docsUrl + "user/manual/en/stock/quality-inspection",
},
];
frappe.help.help_links["Form/Landed Cost Voucher"] = [
{
label: "Landed Cost Voucher",
- url: docsUrl + "user/manual/en/stock/tools/landed-cost-voucher",
+ url: docsUrl + "user/manual/en/stock/landed-cost-voucher",
},
];
frappe.help.help_links["Tree/Item Group"] = [
{
label: "Item Group",
- url: docsUrl + "user/manual/en/stock/setup/item-group",
+ url: docsUrl + "user/manual/en/stock/item-group",
},
];
frappe.help.help_links["Form/Item Attribute"] = [
{
label: "Item Attribute",
- url: docsUrl + "user/manual/en/stock/setup/item-attribute",
+ url: docsUrl + "user/manual/en/stock/item-attribute",
},
];
@@ -911,7 +881,7 @@ frappe.help.help_links["Form/UOM"] = [
frappe.help.help_links["Form/Stock Reconciliation"] = [
{
label: "Opening Stock Entry",
- url: docsUrl + "user/manual/en/stock/opening-stock",
+ url: docsUrl + "user/manual/en/stock/stock-reconciliation",
},
];
@@ -938,13 +908,13 @@ frappe.help.help_links["Form/Newsletter"] = [
];
frappe.help.help_links["Form/Campaign"] = [
- { label: "Campaign", url: docsUrl + "user/manual/en/CRM/setup/campaign" },
+ { label: "Campaign", url: docsUrl + "user/manual/en/CRM/campaign" },
];
frappe.help.help_links["Tree/Sales Person"] = [
{
label: "Sales Person",
- url: docsUrl + "user/manual/en/CRM/setup/sales-person",
+ url: docsUrl + "user/manual/en/CRM/sales-person",
},
];
@@ -953,30 +923,13 @@ frappe.help.help_links["Form/Sales Person"] = [
label: "Sales Person Target",
url:
docsUrl +
- "user/manual/en/selling/setup/sales-person-target-allocation",
+ "user/manual/en/selling/sales-person-target-allocation",
},
-];
-
-//Support
-
-frappe.help.help_links["List/Feedback Trigger"] = [
{
- label: "Feedback Trigger",
- url: docsUrl + "user/manual/en/setting-up/feedback/setting-up-feedback",
- },
-];
-
-frappe.help.help_links["List/Feedback Request"] = [
- {
- label: "Feedback Request",
- url: docsUrl + "user/manual/en/setting-up/feedback/submit-feedback",
- },
-];
-
-frappe.help.help_links["List/Feedback Request"] = [
- {
- label: "Feedback Request",
- url: docsUrl + "user/manual/en/setting-up/feedback/submit-feedback",
+ label: "Sales Person in Transactions",
+ url:
+ docsUrl +
+ "user/manual/en/selling/articles/sales-persons-in-the-sales-transactions",
},
];
@@ -1019,7 +972,7 @@ frappe.help.help_links["Form/Operation"] = [
frappe.help.help_links["Form/BOM Update Tool"] = [
{
label: "BOM Update Tool",
- url: docsUrl + "user/manual/en/manufacturing/tools/bom-update-tool",
+ url: docsUrl + "user/manual/en/manufacturing/bom-update-tool",
},
];
@@ -1036,7 +989,7 @@ frappe.help.help_links["Form/Customize Form"] = [
},
];
-frappe.help.help_links["Form/Custom Field"] = [
+frappe.help.help_links["List/Custom Field"] = [
{
label: "Custom Field",
url: docsUrl + "user/manual/en/customize-erpnext/custom-field",
diff --git a/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js b/erpnext/public/js/hierarchy_chart/hierarchy_chart_desktop.js
index 7b358195c3e..831626aa915 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/public/js/utils.js b/erpnext/public/js/utils.js
index 7f39b990bf0..0323a426f0e 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -712,6 +712,7 @@ erpnext.utils.map_current_doc = function(opts) {
allow_child_item_selection: opts.allow_child_item_selection,
child_fieldname: opts.child_fielname,
child_columns: opts.child_columns,
+ size: opts.size,
action: function(selections, args) {
let values = selections;
if (values.length === 0) {
diff --git a/erpnext/quality_management/workspace/quality/quality.json b/erpnext/quality_management/workspace/quality/quality.json
index 4dc8129d890..ae284701824 100644
--- a/erpnext/quality_management/workspace/quality/quality.json
+++ b/erpnext/quality_management/workspace/quality/quality.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Goal\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Procedure\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Inspection\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Review\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Quality Action\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Non Conformance\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Goal and Procedure\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Feedback\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Meeting\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Review and Action\", \"col\": 4}}]",
"creation": "2020-03-02 15:49:28.632014",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "quality",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Quality",
"links": [
{
@@ -149,15 +142,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.699912",
+ "modified": "2021-08-05 12:16:01.699913",
"modified_by": "Administrator",
"module": "Quality Management",
"name": "Quality",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/regional/__init__.py b/erpnext/regional/__init__.py
index 45a689efa8b..d7dcbf4fe18 100644
--- a/erpnext/regional/__init__.py
+++ b/erpnext/regional/__init__.py
@@ -31,3 +31,4 @@ def create_transaction_log(doc, method):
"document_name": doc.name,
"data": data
}).insert(ignore_permissions=True)
+
diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.json b/erpnext/regional/doctype/gst_settings/gst_settings.json
index 95b930c4c86..fc579d4b38c 100644
--- a/erpnext/regional/doctype/gst_settings/gst_settings.json
+++ b/erpnext/regional/doctype/gst_settings/gst_settings.json
@@ -6,8 +6,10 @@
"engine": "InnoDB",
"field_order": [
"gst_summary",
- "column_break_2",
+ "gst_tax_settings_section",
"round_off_gst_values",
+ "column_break_4",
+ "hsn_wise_tax_breakup",
"gstin_email_sent_on",
"section_break_4",
"gst_accounts",
@@ -17,37 +19,23 @@
{
"fieldname": "gst_summary",
"fieldtype": "HTML",
- "label": "GST Summary",
- "show_days": 1,
- "show_seconds": 1
- },
- {
- "fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "show_days": 1,
- "show_seconds": 1
+ "label": "GST Summary"
},
{
"fieldname": "gstin_email_sent_on",
"fieldtype": "Date",
"label": "GSTIN Email Sent On",
- "read_only": 1,
- "show_days": 1,
- "show_seconds": 1
+ "read_only": 1
},
{
"fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "show_days": 1,
- "show_seconds": 1
+ "fieldtype": "Section Break"
},
{
"fieldname": "gst_accounts",
"fieldtype": "Table",
"label": "GST Accounts",
- "options": "GST Account",
- "show_days": 1,
- "show_seconds": 1
+ "options": "GST Account"
},
{
"default": "250000",
@@ -56,24 +44,35 @@
"fieldtype": "Data",
"in_list_view": 1,
"label": "B2C Limit",
- "reqd": 1,
- "show_days": 1,
- "show_seconds": 1
+ "reqd": 1
},
{
"default": "0",
"description": "Enabling this option will round off individual GST components in all the Invoices",
"fieldname": "round_off_gst_values",
"fieldtype": "Check",
- "label": "Round Off GST Values",
- "show_days": 1,
- "show_seconds": 1
+ "label": "Round Off GST Values"
+ },
+ {
+ "default": "0",
+ "fieldname": "hsn_wise_tax_breakup",
+ "fieldtype": "Check",
+ "label": "Tax Breakup Table Based On HSN Code"
+ },
+ {
+ "fieldname": "gst_tax_settings_section",
+ "fieldtype": "Section Break",
+ "label": "GST Tax Settings"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
}
],
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2021-01-28 17:19:47.969260",
+ "modified": "2021-10-11 18:10:14.242614",
"modified_by": "Administrator",
"module": "Regional",
"name": "GST Settings",
@@ -83,4 +82,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
- }
\ No newline at end of file
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py b/erpnext/regional/doctype/ksa_vat_purchase_account/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json
new file mode 100644
index 00000000000..89ba3e977af
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.json
@@ -0,0 +1,49 @@
+{
+ "actions": [],
+ "creation": "2021-07-13 09:17:09.862163",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "item_tax_template",
+ "account"
+ ],
+ "fields": [
+ {
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Account",
+ "options": "Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "fieldname": "item_tax_template",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Tax Template",
+ "options": "Item Tax Template",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-08-04 06:42:38.205597",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA VAT Purchase Account",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py
new file mode 100644
index 00000000000..3920bc546c1
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_purchase_account/ksa_vat_purchase_account.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Havenir Solutions and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class KSAVATPurchaseAccount(Document):
+ pass
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/__init__.py b/erpnext/regional/doctype/ksa_vat_sales_account/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js
new file mode 100644
index 00000000000..72613f4064f
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Havenir Solutions and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('KSA VAT Sales Account', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json
new file mode 100644
index 00000000000..df2747891dc
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.json
@@ -0,0 +1,49 @@
+{
+ "actions": [],
+ "creation": "2021-07-13 08:46:33.820968",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "title",
+ "item_tax_template",
+ "account"
+ ],
+ "fields": [
+ {
+ "fieldname": "account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Account",
+ "options": "Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "fieldname": "item_tax_template",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Tax Template",
+ "options": "Item Tax Template",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-08-04 06:42:00.081407",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA VAT Sales Account",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py
new file mode 100644
index 00000000000..7c2689f530e
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_sales_account/ksa_vat_sales_account.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Havenir Solutions and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class KSAVATSalesAccount(Document):
+ pass
diff --git a/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py b/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py
new file mode 100644
index 00000000000..1d6a6a793dc
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_sales_account/test_ksa_vat_sales_account.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Havenir Solutions and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestKSAVATSalesAccount(unittest.TestCase):
+ pass
diff --git a/erpnext/regional/doctype/ksa_vat_setting/__init__.py b/erpnext/regional/doctype/ksa_vat_setting/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js
new file mode 100644
index 00000000000..00b62b9adfb
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2021, Havenir Solutions and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('KSA VAT Setting', {
+ onload: function () {
+ frappe.breadcrumbs.add('Accounts', 'KSA VAT Setting');
+ }
+});
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json
new file mode 100644
index 00000000000..33619467ed0
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.json
@@ -0,0 +1,49 @@
+{
+ "actions": [],
+ "autoname": "field:company",
+ "creation": "2021-07-13 08:49:01.100356",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "ksa_vat_sales_accounts",
+ "ksa_vat_purchase_accounts"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "ksa_vat_sales_accounts",
+ "fieldtype": "Table",
+ "label": "KSA VAT Sales Accounts",
+ "options": "KSA VAT Sales Account",
+ "reqd": 1
+ },
+ {
+ "fieldname": "ksa_vat_purchase_accounts",
+ "fieldtype": "Table",
+ "label": "KSA VAT Purchase Accounts",
+ "options": "KSA VAT Purchase Account",
+ "reqd": 1
+ }
+ ],
+ "links": [],
+ "modified": "2021-08-26 04:29:06.499378",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA VAT Setting",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "company",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py
new file mode 100644
index 00000000000..bdae1161fd7
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Havenir Solutions and contributors
+# For license information, please see license.txt
+
+# import frappe
+from frappe.model.document import Document
+
+
+class KSAVATSetting(Document):
+ pass
diff --git a/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js
new file mode 100644
index 00000000000..269cbec5fb4
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_setting/ksa_vat_setting_list.js
@@ -0,0 +1,5 @@
+frappe.listview_settings['KSA VAT Setting'] = {
+ onload () {
+ frappe.breadcrumbs.add('Accounts');
+ }
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py b/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py
new file mode 100644
index 00000000000..7207901fd43
--- /dev/null
+++ b/erpnext/regional/doctype/ksa_vat_setting/test_ksa_vat_setting.py
@@ -0,0 +1,9 @@
+# Copyright (c) 2021, Havenir Solutions and Contributors
+# See license.txt
+
+# import frappe
+import unittest
+
+
+class TestKSAVATSetting(unittest.TestCase):
+ pass
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index afb1b07ccc4..1cbb154125f 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -614,11 +614,17 @@ def get_custom_fields():
fieldtype='Currency', insert_after='monthly_hra_exemption', read_only=1, depends_on='house_rent_payment_amount')
],
'Supplier': [
+ {
+ 'fieldname': 'pan',
+ 'label': 'PAN',
+ 'fieldtype': 'Data',
+ 'insert_after': 'supplier_type'
+ },
{
'fieldname': 'gst_transporter_id',
'label': 'GST Transporter ID',
'fieldtype': 'Data',
- 'insert_after': 'supplier_type',
+ 'insert_after': 'pan',
'depends_on': 'eval:doc.is_transporter'
},
{
@@ -640,11 +646,17 @@ def get_custom_fields():
}
],
'Customer': [
+ {
+ 'fieldname': 'pan',
+ 'label': 'PAN',
+ 'fieldtype': 'Data',
+ 'insert_after': 'customer_type'
+ },
{
'fieldname': 'gst_category',
'label': 'GST Category',
'fieldtype': 'Select',
- 'insert_after': 'customer_type',
+ 'insert_after': 'pan',
'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders',
'default': 'Unregistered'
},
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 94936143d88..1733220c0ac 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -62,7 +62,7 @@ def validate_gstin_for_india(doc, method):
.format(doc.gst_state_number), title=_("Invalid GSTIN"))
def validate_pan_for_india(doc, method):
- if doc.get('country') != 'India' or not doc.pan:
+ if doc.get('country') != 'India' or not doc.get('pan'):
return
if not PAN_NUMBER_FORMAT.match(doc.pan):
@@ -112,7 +112,11 @@ def validate_gstin_check_digit(gstin, label='GSTIN'):
frappe.throw(_("""Invalid {0}! The check digit validation has failed. Please ensure you've typed the {0} correctly.""").format(label))
def get_itemised_tax_breakup_header(item_doctype, tax_accounts):
- return [_("Item"), _("Taxable Amount")] + tax_accounts
+ hsn_wise_in_gst_settings = frappe.db.get_single_value('GST Settings','hsn_wise_tax_breakup')
+ if frappe.get_meta(item_doctype).has_field('gst_hsn_code') and hsn_wise_in_gst_settings:
+ return [_("HSN/SAC"), _("Taxable Amount")] + tax_accounts
+ else:
+ return [_("Item"), _("Taxable Amount")] + tax_accounts
def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False):
itemised_tax = get_itemised_tax(doc.taxes, with_tax_account=account_wise)
@@ -122,14 +126,17 @@ def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False):
if not frappe.get_meta(doc.doctype + " Item").has_field('gst_hsn_code'):
return itemised_tax, itemised_taxable_amount
- if hsn_wise:
+ hsn_wise_in_gst_settings = frappe.db.get_single_value('GST Settings','hsn_wise_tax_breakup')
+
+ tax_breakup_hsn_wise = hsn_wise or hsn_wise_in_gst_settings
+ if tax_breakup_hsn_wise:
item_hsn_map = frappe._dict()
for d in doc.items:
item_hsn_map.setdefault(d.item_code or d.item_name, d.get("gst_hsn_code"))
hsn_tax = {}
for item, taxes in itemised_tax.items():
- item_or_hsn = item if not hsn_wise else item_hsn_map.get(item)
+ item_or_hsn = item if not tax_breakup_hsn_wise else item_hsn_map.get(item)
hsn_tax.setdefault(item_or_hsn, frappe._dict())
for tax_desc, tax_detail in taxes.items():
key = tax_desc
@@ -142,7 +149,7 @@ def get_itemised_tax_breakup_data(doc, account_wise=False, hsn_wise=False):
# set taxable amount
hsn_taxable_amount = frappe._dict()
for item in itemised_taxable_amount:
- item_or_hsn = item if not hsn_wise else item_hsn_map.get(item)
+ item_or_hsn = item if not tax_breakup_hsn_wise else item_hsn_map.get(item)
hsn_taxable_amount.setdefault(item_or_hsn, 0)
hsn_taxable_amount[item_or_hsn] += itemised_taxable_amount.get(item)
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 23924c5fb66..7d401bab669 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/regional/report/ksa_vat/__init__.py b/erpnext/regional/report/ksa_vat/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.js b/erpnext/regional/report/ksa_vat/ksa_vat.js
new file mode 100644
index 00000000000..d46d260ac1e
--- /dev/null
+++ b/erpnext/regional/report/ksa_vat/ksa_vat.js
@@ -0,0 +1,60 @@
+// Copyright (c) 2016, Havenir Solutions and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["KSA VAT"] = {
+ onload() {
+ frappe.breadcrumbs.add('Accounts');
+ },
+ "filters": [
+ {
+ "fieldname": "company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "reqd": 1,
+ "default": frappe.defaults.get_user_default("Company")
+ },
+ {
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ },
+ {
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "default": frappe.datetime.get_today()
+ }
+ ],
+ "formatter": function(value, row, column, data, default_formatter) {
+ if (data
+ && (data.title=='VAT on Sales' || data.title=='VAT on Purchases')
+ && data.title==value) {
+ value = $(`${value}`);
+ var $value = $(value).css("font-weight", "bold");
+ value = $value.wrap("").parent().html();
+ return value
+ }else if (data.title=='Grand Total'){
+ if (data.title==value) {
+ value = $(`${value}`);
+ var $value = $(value).css("font-weight", "bold");
+ value = $value.wrap("").parent().html();
+ return value
+ }else{
+ value = default_formatter(value, row, column, data);
+ value = $(`${value}`);
+ var $value = $(value).css("font-weight", "bold");
+ value = $value.wrap("").parent().html();
+ console.log($value)
+ return value
+ }
+ }else{
+ value = default_formatter(value, row, column, data);
+ return value;
+ }
+ },
+};
diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.json b/erpnext/regional/report/ksa_vat/ksa_vat.json
new file mode 100644
index 00000000000..036e2603103
--- /dev/null
+++ b/erpnext/regional/report/ksa_vat/ksa_vat.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-07-13 08:54:38.000949",
+ "disable_prepared_report": 1,
+ "disabled": 1,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-08-26 04:14:37.202594",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "KSA VAT",
+ "owner": "Administrator",
+ "prepared_report": 1,
+ "ref_doctype": "GL Entry",
+ "report_name": "KSA VAT",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Accounts Manager"
+ },
+ {
+ "role": "Accounts User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/regional/report/ksa_vat/ksa_vat.py b/erpnext/regional/report/ksa_vat/ksa_vat.py
new file mode 100644
index 00000000000..a42ebc9f7e5
--- /dev/null
+++ b/erpnext/regional/report/ksa_vat/ksa_vat.py
@@ -0,0 +1,176 @@
+# Copyright (c) 2013, Havenir Solutions and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+
+import json
+
+import frappe
+from frappe import _
+from frappe.utils import get_url_to_list
+
+
+def execute(filters=None):
+ columns = columns = get_columns()
+ data = get_data(filters)
+ return columns, data
+
+def get_columns():
+ return [
+ {
+ "fieldname": "title",
+ "label": _("Title"),
+ "fieldtype": "Data",
+ "width": 300
+ },
+ {
+ "fieldname": "amount",
+ "label": _("Amount (SAR)"),
+ "fieldtype": "Currency",
+ "width": 150,
+ },
+ {
+ "fieldname": "adjustment_amount",
+ "label": _("Adjustment (SAR)"),
+ "fieldtype": "Currency",
+ "width": 150,
+ },
+ {
+ "fieldname": "vat_amount",
+ "label": _("VAT Amount (SAR)"),
+ "fieldtype": "Currency",
+ "width": 150,
+ }
+ ]
+
+def get_data(filters):
+ data = []
+
+ # Validate if vat settings exist
+ company = filters.get('company')
+ if frappe.db.exists('KSA VAT Setting', company) is None:
+ url = get_url_to_list('KSA VAT Setting')
+ frappe.msgprint(_('Create KSA VAT Setting for this company').format(url))
+ return data
+
+ ksa_vat_setting = frappe.get_doc('KSA VAT Setting', company)
+
+ # Sales Heading
+ append_data(data, 'VAT on Sales', '', '', '')
+
+ grand_total_taxable_amount = 0
+ grand_total_taxable_adjustment_amount = 0
+ grand_total_tax = 0
+
+ for vat_setting in ksa_vat_setting.ksa_vat_sales_accounts:
+ total_taxable_amount, total_taxable_adjustment_amount, \
+ total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Sales Invoice')
+
+ # Adding results to data
+ append_data(data, vat_setting.title, total_taxable_amount,
+ total_taxable_adjustment_amount, total_tax)
+
+ grand_total_taxable_amount += total_taxable_amount
+ grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
+ grand_total_tax += total_tax
+
+ # Sales Grand Total
+ append_data(data, 'Grand Total', grand_total_taxable_amount,
+ grand_total_taxable_adjustment_amount, grand_total_tax)
+
+ # Blank Line
+ append_data(data, '', '', '', '')
+
+ # Purchase Heading
+ append_data(data, 'VAT on Purchases', '', '', '')
+
+ grand_total_taxable_amount = 0
+ grand_total_taxable_adjustment_amount = 0
+ grand_total_tax = 0
+
+ for vat_setting in ksa_vat_setting.ksa_vat_purchase_accounts:
+ total_taxable_amount, total_taxable_adjustment_amount, \
+ total_tax = get_tax_data_for_each_vat_setting(vat_setting, filters, 'Purchase Invoice')
+
+ # Adding results to data
+ append_data(data, vat_setting.title, total_taxable_amount,
+ total_taxable_adjustment_amount, total_tax)
+
+ grand_total_taxable_amount += total_taxable_amount
+ grand_total_taxable_adjustment_amount += total_taxable_adjustment_amount
+ grand_total_tax += total_tax
+
+ # Purchase Grand Total
+ append_data(data, 'Grand Total', grand_total_taxable_amount,
+ grand_total_taxable_adjustment_amount, grand_total_tax)
+
+ return data
+
+def get_tax_data_for_each_vat_setting(vat_setting, filters, doctype):
+ '''
+ (KSA, {filters}, 'Sales Invoice') => 500, 153, 10 \n
+ calculates and returns \n
+ total_taxable_amount, total_taxable_adjustment_amount, total_tax'''
+ from_date = filters.get('from_date')
+ to_date = filters.get('to_date')
+
+ # Initiate variables
+ total_taxable_amount = 0
+ total_taxable_adjustment_amount = 0
+ total_tax = 0
+ # Fetch All Invoices
+ invoices = frappe.get_list(doctype,
+ filters ={
+ 'docstatus': 1,
+ 'posting_date': ['between', [from_date, to_date]]
+ }, fields =['name', 'is_return'])
+
+ for invoice in invoices:
+ invoice_items = frappe.get_list(f'{doctype} Item',
+ filters ={
+ 'docstatus': 1,
+ 'parent': invoice.name,
+ 'item_tax_template': vat_setting.item_tax_template
+ }, fields =['item_code', 'net_amount'])
+
+ for item in invoice_items:
+ # Summing up total taxable amount
+ if invoice.is_return == 0:
+ total_taxable_amount += item.net_amount
+
+ if invoice.is_return == 1:
+ total_taxable_adjustment_amount += item.net_amount
+
+ # Summing up total tax
+ total_tax += get_tax_amount(item.item_code, vat_setting.account, doctype, invoice.name)
+
+ return total_taxable_amount, total_taxable_adjustment_amount, total_tax
+
+
+
+def append_data(data, title, amount, adjustment_amount, vat_amount):
+ """Returns data with appended value."""
+ data.append({"title": _(title), "amount": amount, "adjustment_amount": adjustment_amount, "vat_amount": vat_amount})
+
+def get_tax_amount(item_code, account_head, doctype, parent):
+ if doctype == 'Sales Invoice':
+ tax_doctype = 'Sales Taxes and Charges'
+
+ elif doctype == 'Purchase Invoice':
+ tax_doctype = 'Purchase Taxes and Charges'
+
+ item_wise_tax_detail = frappe.get_value(tax_doctype, {
+ 'docstatus': 1,
+ 'parent': parent,
+ 'account_head': account_head
+ }, 'item_wise_tax_detail')
+
+ tax_amount = 0
+ if item_wise_tax_detail and len(item_wise_tax_detail) > 0:
+ item_wise_tax_detail = json.loads(item_wise_tax_detail)
+ for key, value in item_wise_tax_detail.items():
+ if key == item_code:
+ tax_amount = value[1]
+ break
+
+ return tax_amount
diff --git a/erpnext/regional/saudi_arabia/setup.py b/erpnext/regional/saudi_arabia/setup.py
index 9b3677d2c64..6113f48d3f1 100644
--- a/erpnext/regional/saudi_arabia/setup.py
+++ b/erpnext/regional/saudi_arabia/setup.py
@@ -2,10 +2,36 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
-
-from erpnext.regional.united_arab_emirates.setup import make_custom_fields, add_print_formats
-
+import frappe
+from frappe.permissions import add_permission, update_permission_property
+from erpnext.regional.united_arab_emirates.setup import make_custom_fields as uae_custom_fields, add_print_formats
+from erpnext.regional.saudi_arabia.wizard.operations.setup_ksa_vat_setting import create_ksa_vat_setting
+from frappe.custom.doctype.custom_field.custom_field import create_custom_field
def setup(company=None, patch=True):
- make_custom_fields()
+ uae_custom_fields()
add_print_formats()
+ add_permissions()
+ create_ksa_vat_setting(company)
+ make_qrcode_field()
+
+def add_permissions():
+ """Add Permissions for KSA VAT Setting."""
+ add_permission('KSA VAT Setting', 'All', 0)
+ for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
+ add_permission('KSA VAT Setting', role, 0)
+ update_permission_property('KSA VAT Setting', role, 0, 'write', 1)
+ update_permission_property('KSA VAT Setting', role, 0, 'create', 1)
+
+ """Enable KSA VAT Report"""
+ frappe.db.set_value('Report', 'KSA VAT', 'disabled', 0)
+
+def make_qrcode_field():
+ """Created QR code Image file"""
+ qr_code_field = dict(
+ fieldname='qr_code',
+ label='QR Code',
+ fieldtype='Attach Image',
+ read_only=1, no_copy=1, hidden=1)
+
+ create_custom_field('Sales Invoice', qr_code_field)
diff --git a/erpnext/regional/saudi_arabia/utils.py b/erpnext/regional/saudi_arabia/utils.py
new file mode 100644
index 00000000000..cc6c0af7a56
--- /dev/null
+++ b/erpnext/regional/saudi_arabia/utils.py
@@ -0,0 +1,77 @@
+import io
+import os
+
+import frappe
+from pyqrcode import create as qr_create
+
+from erpnext import get_region
+
+
+def create_qr_code(doc, method):
+ """Create QR Code after inserting Sales Inv
+ """
+
+ region = get_region(doc.company)
+ if region not in ['Saudi Arabia']:
+ return
+
+ # if QR Code field not present, do nothing
+ if not hasattr(doc, 'qr_code'):
+ return
+
+ # Don't create QR Code if it already exists
+ qr_code = doc.get("qr_code")
+ if qr_code and frappe.db.exists({"doctype": "File", "file_url": qr_code}):
+ return
+
+ meta = frappe.get_meta('Sales Invoice')
+
+ for field in meta.get_image_fields():
+ if field.fieldname == 'qr_code':
+ # Creating public url to print format
+ default_print_format = frappe.db.get_value('Property Setter', dict(property='default_print_format', doc_type=doc.doctype), "value")
+
+ # System Language
+ language = frappe.get_system_settings('language')
+
+ # creating qr code for the url
+ url = f"{ frappe.utils.get_url() }/{ doc.doctype }/{ doc.name }?format={ default_print_format or 'Standard' }&_lang={ language }&key={ doc.get_signature() }"
+ qr_image = io.BytesIO()
+ url = qr_create(url, error='L')
+ url.png(qr_image, scale=2, quiet_zone=1)
+
+ # making file
+ filename = f"QR-CODE-{doc.name}.png".replace(os.path.sep, "__")
+ _file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": filename,
+ "is_private": 0,
+ "content": qr_image.getvalue(),
+ "attached_to_doctype": doc.get("doctype"),
+ "attached_to_name": doc.get("name"),
+ "attached_to_field": "qr_code"
+ })
+
+ _file.save()
+
+ # assigning to document
+ doc.db_set('qr_code', _file.file_url)
+ doc.notify_update()
+
+ break
+
+
+def delete_qr_code_file(doc, method):
+ """Delete QR Code on deleted sales invoice"""
+
+ region = get_region(doc.company)
+ if region not in ['Saudi Arabia']:
+ return
+
+ if hasattr(doc, 'qr_code'):
+ if doc.get('qr_code'):
+ file_doc = frappe.get_list('File', {
+ 'file_url': doc.get('qr_code')
+ })
+ if len(file_doc):
+ frappe.delete_doc('File', file_doc[0].name)
\ No newline at end of file
diff --git a/erpnext/regional/saudi_arabia/wizard/__init__.py b/erpnext/regional/saudi_arabia/wizard/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/saudi_arabia/wizard/data/__init__.py b/erpnext/regional/saudi_arabia/wizard/data/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json b/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json
new file mode 100644
index 00000000000..709d65be041
--- /dev/null
+++ b/erpnext/regional/saudi_arabia/wizard/data/ksa_vat_settings.json
@@ -0,0 +1,47 @@
+[
+ {
+ "type": "Sales Account",
+ "accounts": [
+ {
+ "title": "Standard rated Sales",
+ "item_tax_template": "KSA VAT 5%",
+ "account": "VAT 5%"
+ },
+ {
+ "title": "Zero rated domestic sales",
+ "item_tax_template": "KSA VAT Zero",
+ "account": "VAT Zero"
+ },
+ {
+ "title": "Exempted sales",
+ "item_tax_template": "KSA VAT Exempted",
+ "account": "VAT Zero"
+ }
+ ]
+ },
+ {
+ "type": "Purchase Account",
+ "accounts": [
+ {
+ "title": "Standard rated domestic purchases",
+ "item_tax_template": "KSA VAT 5%",
+ "account": "VAT 5%"
+ },
+ {
+ "title": "Imports subject to VAT paid at customs",
+ "item_tax_template": "KSA Excise 50%",
+ "account": "Excise 50%"
+ },
+ {
+ "title": "Zero rated purchases",
+ "item_tax_template": "KSA VAT Zero",
+ "account": "VAT Zero"
+ },
+ {
+ "title": "Exempted purchases",
+ "item_tax_template": "KSA VAT Exempted",
+ "account": "VAT Zero"
+ }
+ ]
+ }
+]
\ No newline at end of file
diff --git a/erpnext/regional/saudi_arabia/wizard/operations/__init__.py b/erpnext/regional/saudi_arabia/wizard/operations/__init__.py
new file mode 100644
index 00000000000..e69de29bb2d
diff --git a/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py
new file mode 100644
index 00000000000..3c89edd37ed
--- /dev/null
+++ b/erpnext/regional/saudi_arabia/wizard/operations/setup_ksa_vat_setting.py
@@ -0,0 +1,46 @@
+import json
+import os
+
+import frappe
+
+from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges
+
+
+def create_ksa_vat_setting(company):
+ """On creation of first company. Creates KSA VAT Setting"""
+
+ company = frappe.get_doc('Company', company)
+ setup_taxes_and_charges(company.name, company.country)
+
+ file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'ksa_vat_settings.json')
+ with open(file_path, 'r') as json_file:
+ account_data = json.load(json_file)
+
+ # Creating KSA VAT Setting
+ ksa_vat_setting = frappe.get_doc({
+ 'doctype': 'KSA VAT Setting',
+ 'company': company.name
+ })
+
+ for data in account_data:
+ if data['type'] == 'Sales Account':
+ for row in data['accounts']:
+ item_tax_template = row['item_tax_template']
+ account = row['account']
+ ksa_vat_setting.append('ksa_vat_sales_accounts', {
+ 'title': row['title'],
+ 'item_tax_template': f'{item_tax_template} - {company.abbr}',
+ 'account': f'{account} - {company.abbr}'
+ })
+
+ elif data['type'] == 'Purchase Account':
+ for row in data['accounts']:
+ item_tax_template = row['item_tax_template']
+ account = row['account']
+ ksa_vat_setting.append('ksa_vat_purchase_accounts', {
+ 'title': row['title'],
+ 'item_tax_template': f'{item_tax_template} - {company.abbr}',
+ 'account': f'{account} - {company.abbr}'
+ })
+
+ ksa_vat_setting.save()
diff --git a/erpnext/regional/united_states/setup.py b/erpnext/regional/united_states/setup.py
index 9c183af1d13..cf78f927c59 100644
--- a/erpnext/regional/united_states/setup.py
+++ b/erpnext/regional/united_states/setup.py
@@ -14,30 +14,9 @@ def setup(company=None, patch=True):
setup_company_independent_fixtures(patch=patch)
def setup_company_independent_fixtures(company=None, patch=True):
- add_product_tax_categories()
make_custom_fields()
- add_permissions()
- frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
add_print_formats()
-# Product Tax categories imported from taxjar api
-def add_product_tax_categories():
- with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
- tax_categories = json.loads(f.read())
- create_tax_categories(tax_categories['categories'])
-
-def create_tax_categories(data):
- for d in data:
- tax_category = frappe.new_doc('Product Tax Category')
- tax_category.description = d.get("description")
- tax_category.product_tax_code = d.get("product_tax_code")
- tax_category.category_name = d.get("name")
- try:
- tax_category.db_insert()
- except frappe.DuplicateEntryError:
- pass
-
-
def make_custom_fields(update=True):
custom_fields = {
'Supplier': [
@@ -59,29 +38,10 @@ def make_custom_fields(update=True):
'Quotation': [
dict(fieldname='exempt_from_sales_tax', fieldtype='Check', insert_after='taxes_and_charges',
label='Is customer exempted from sales tax?')
- ],
- 'Sales Invoice Item': [
- dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
- label='Product Tax Category', fetch_from='item_code.product_tax_category'),
- dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
- label='Tax Collectable', read_only=1),
- dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
- label='Taxable Amount', read_only=1)
- ],
- 'Item': [
- dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
- label='Product Tax Category')
]
}
create_custom_fields(custom_fields, update=update)
-def add_permissions():
- doctype = "Product Tax Category"
- for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'):
- add_permission(doctype, role, 0)
- update_permission_property(doctype, role, 0, 'write', 1)
- update_permission_property(doctype, role, 0, 'create', 1)
-
def add_print_formats():
frappe.reload_doc("regional", "print_format", "irs_1099_form")
frappe.db.set_value("Print Format", "IRS 1099 Form", "disabled", 0)
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index e811435e669..ae406306170 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -16,7 +16,6 @@
"customer_name",
"gender",
"customer_type",
- "pan",
"tax_withholding_category",
"default_bank_account",
"lead_name",
@@ -486,11 +485,6 @@
"fieldtype": "Check",
"label": "Allow Sales Invoice Creation Without Delivery Note"
},
- {
- "fieldname": "pan",
- "fieldtype": "Data",
- "label": "PAN"
- },
{
"fieldname": "tax_withholding_category",
"fieldtype": "Link",
@@ -517,11 +511,12 @@
"link_fieldname": "party"
}
],
- "modified": "2021-09-06 17:38:54.196663",
+ "modified": "2021-10-20 22:07:52.485809",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
"name_case": "Title Case",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 79e40f65dba..d7f1f034e9e 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -110,7 +110,7 @@ class SalesOrder(SellingController):
if self.order_type == 'Sales' and not self.skip_delivery_note:
delivery_date_list = [d.delivery_date for d in self.get("items") if d.delivery_date]
max_delivery_date = max(delivery_date_list) if delivery_date_list else None
- if not self.delivery_date:
+ if (max_delivery_date and not self.delivery_date) or (max_delivery_date and getdate(self.delivery_date) != getdate(max_delivery_date)):
self.delivery_date = max_delivery_date
if self.delivery_date:
for d in self.get("items"):
@@ -119,8 +119,6 @@ class SalesOrder(SellingController):
if getdate(self.transaction_date) > getdate(d.delivery_date):
frappe.msgprint(_("Expected Delivery Date should be after Sales Order Date"),
indicator='orange', title=_('Warning'))
- if getdate(self.delivery_date) != getdate(max_delivery_date):
- self.delivery_date = max_delivery_date
else:
frappe.throw(_("Please enter Delivery Date"))
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index ca696034d6b..e24166145fd 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -1454,7 +1454,6 @@ def make_sales_order_workflow():
frappe.get_doc(dict(doctype='Role', role_name='Test Junior Approver')).insert(ignore_if_duplicate=True)
frappe.get_doc(dict(doctype='Role', role_name='Test Approver')).insert(ignore_if_duplicate=True)
- frappe.db.commit()
frappe.cache().hdel('roles', frappe.session.user)
workflow = frappe.get_doc({
diff --git a/erpnext/selling/workspace/retail/retail.json b/erpnext/selling/workspace/retail/retail.json
index 9d2e6cabbc3..a851ace738c 100644
--- a/erpnext/selling/workspace/retail/retail.json
+++ b/erpnext/selling/workspace/retail/retail.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Point Of Sale\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings & Configurations\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loyalty Program\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Opening & Closing\", \"col\": 4}}]",
"creation": "2020-03-02 17:18:32.505616",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "retail",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Retail",
"links": [
{
@@ -108,15 +101,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.840988",
+ "modified": "2021-08-05 12:16:01.840989",
"modified_by": "Administrator",
"module": "Selling",
"name": "Retail",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "Retail",
"roles": [],
diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json
index 345187f93c4..db2e6bafd55 100644
--- a/erpnext/selling/workspace/selling/selling.json
+++ b/erpnext/selling/workspace/selling/selling.json
@@ -1,26 +1,18 @@
{
- "category": "",
"charts": [
{
"chart_name": "Sales Order Trends",
"label": "Sales Order Trends"
}
],
- "charts_label": "Selling ",
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Selling\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Sales Order Trends\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Quick Access\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Analytics\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Order Analysis\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Selling\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Items and Pricing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
"creation": "2020-01-28 11:49:12.092882",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "sell",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Selling",
"links": [
{
@@ -570,15 +562,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:01.990702",
+ "modified": "2021-08-05 12:16:01.990703",
"modified_by": "Administrator",
"module": "Selling",
"name": "Selling",
- "onboarding": "Selling",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
@@ -619,6 +608,5 @@
"type": "Dashboard"
}
],
- "shortcuts_label": "Quick Access",
"title": "Selling"
}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index 8403193df53..95ca3867ee7 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -46,43 +46,6 @@ frappe.ui.form.on("Company", {
});
},
- change_abbreviation(frm) {
- var dialog = new frappe.ui.Dialog({
- title: "Replace Abbr",
- fields: [
- {"fieldtype": "Data", "label": "New Abbreviation", "fieldname": "new_abbr",
- "reqd": 1 },
- {"fieldtype": "Button", "label": "Update", "fieldname": "update"},
- ]
- });
-
- dialog.fields_dict.update.$input.click(function() {
- var args = dialog.get_values();
- if (!args) return;
- frappe.show_alert(__("Update in progress. It might take a while."));
- return frappe.call({
- method: "erpnext.setup.doctype.company.company.enqueue_replace_abbr",
- args: {
- "company": frm.doc.name,
- "old": frm.doc.abbr,
- "new": args.new_abbr
- },
- callback: function(r) {
- if (r.exc) {
- frappe.msgprint(__("There were errors."));
- return;
- } else {
- frm.set_value("abbr", args.new_abbr);
- }
- dialog.hide();
- frm.refresh();
- },
- btn: this
- });
- });
- dialog.show();
- },
-
company_name: function(frm) {
if(frm.doc.__islocal) {
// add missing " " arg in split method
@@ -164,10 +127,6 @@ frappe.ui.form.on("Company", {
}, __('Manage'));
}
}
-
- frm.add_custom_button(__('Change Abbreviation'), () => {
- frm.trigger('change_abbreviation');
- }, __('Manage'));
}
erpnext.company.set_chart_of_accounts_options(frm.doc);
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 58cb52c04dd..63d96bf85e7 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -125,7 +125,8 @@
"label": "Abbr",
"oldfieldname": "abbr",
"oldfieldtype": "Data",
- "reqd": 1
+ "reqd": 1,
+ "set_only_once": 1
},
{
"bold": 1,
@@ -747,10 +748,11 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2021-07-12 11:27:06.353860",
+ "modified": "2021-10-04 12:09:25.833133",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
+ "naming_rule": "By fieldname",
"nsm_parent_field": "parent_company",
"owner": "Administrator",
"permissions": [
@@ -808,4 +810,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 87d67a5f9d0..0b1b4a1ec02 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -399,44 +399,6 @@ class Company(NestedSet):
if not frappe.db.get_value('GL Entry', {'company': self.name}):
frappe.db.sql("delete from `tabProcess Deferred Accounting` where company=%s", self.name)
-@frappe.whitelist()
-def enqueue_replace_abbr(company, old, new):
- kwargs = dict(queue="long", company=company, old=old, new=new)
- frappe.enqueue('erpnext.setup.doctype.company.company.replace_abbr', **kwargs)
-
-
-@frappe.whitelist()
-def replace_abbr(company, old, new):
- new = new.strip()
- if not new:
- frappe.throw(_("Abbr can not be blank or space"))
-
- frappe.only_for("System Manager")
-
- def _rename_record(doc):
- parts = doc[0].rsplit(" - ", 1)
- if len(parts) == 1 or parts[1].lower() == old.lower():
- frappe.rename_doc(dt, doc[0], parts[0] + " - " + new, force=True)
-
- def _rename_records(dt):
- # rename is expensive so let's be economical with memory usage
- doc = (d for d in frappe.db.sql("select name from `tab%s` where company=%s" % (dt, '%s'), company))
- for d in doc:
- _rename_record(d)
- try:
- frappe.db.auto_commit_on_many_writes = 1
- for dt in ["Warehouse", "Account", "Cost Center", "Department",
- "Sales Taxes and Charges Template", "Purchase Taxes and Charges Template"]:
- _rename_records(dt)
- frappe.db.commit()
- frappe.db.set_value("Company", company, "abbr", new)
-
- except Exception:
- frappe.log_error(title=_('Abbreviation Rename Error'))
- finally:
- frappe.db.auto_commit_on_many_writes = 0
-
-
def get_name_with_abbr(name, company):
company_abbr = frappe.get_cached_value('Company', company, "abbr")
parts = name.split(" - ")
diff --git a/erpnext/setup/doctype/uom/uom.json b/erpnext/setup/doctype/uom/uom.json
index 3a4e7f6dc4b..844a11f1397 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/setup/setup_wizard/operations/taxes_setup.py b/erpnext/setup/setup_wizard/operations/taxes_setup.py
index faa25dfbaa2..58a14d20f20 100644
--- a/erpnext/setup/setup_wizard/operations/taxes_setup.py
+++ b/erpnext/setup/setup_wizard/operations/taxes_setup.py
@@ -192,7 +192,7 @@ def get_or_create_account(company_name, account):
default_root_type = 'Liability'
root_type = account.get('root_type', default_root_type)
- existing_accounts = frappe.get_list('Account',
+ existing_accounts = frappe.get_all('Account',
filters={
'company': company_name,
'root_type': root_type
@@ -247,7 +247,7 @@ def get_or_create_tax_group(company_name, root_type):
# Create a new group account named 'Duties and Taxes' or 'Tax Assets' just
# below the root account
- root_account = frappe.get_list('Account', {
+ root_account = frappe.get_all('Account', {
'is_group': 1,
'root_type': root_type,
'company': company_name,
diff --git a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index ef4b050ceb2..1412acfcead 100644
--- a/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -1,31 +1,21 @@
{
- "category": "",
"charts": [],
- "content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Projects Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Accounts Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Stock Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"HR Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Selling Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Buying Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Support Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Shopping Cart Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Portal Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Manufacturing Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Education Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Hotel Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Healthcare Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Domain Settings\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Products Settings\", \"col\": 4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\\n\\t\\t\\t\\n\\t\\t\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Projects Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Accounts Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Stock Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"HR Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Selling Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Buying Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Support Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Shopping Cart Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Portal Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Domain Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Products Settings\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Naming Series\",\"col\":4}}]",
"creation": "2020-03-12 14:47:51.166455",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "setting",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "ERPNext Settings",
"links": [],
- "modified": "2021-08-05 12:15:59.052327",
+ "modified": "2021-10-26 21:32:55.323591",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
@@ -37,6 +27,14 @@
"link_to": "Projects Settings",
"type": "DocType"
},
+ {
+ "color": "Grey",
+ "doc_view": "",
+ "icon": "dot-horizontal",
+ "label": "Naming Series",
+ "link_to": "Naming Series",
+ "type": "DocType"
+ },
{
"icon": "accounting",
"label": "Accounts Settings",
diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json
index a4e7ad863b0..4e1ccf9b94f 100644
--- a/erpnext/setup/workspace/home/home.json
+++ b/erpnext/setup/workspace/home/home.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
- "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
+ "content": "[{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"level\":4,\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Item\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Customer\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Supplier\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Sales Invoice\",\"col\":4}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Leaderboard\",\"col\":4}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"level\":4,\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Accounting\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Stock\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Human Resources\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"CRM\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Data Import and Settings\",\"col\":4}}]",
"creation": "2020-01-23 13:46:38.833076",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "getting-started",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Home",
"links": [
{
@@ -278,15 +271,12 @@
"type": "Link"
}
],
- "modified": "2021-08-10 15:33:20.704740",
+ "modified": "2021-08-10 15:33:20.704741",
"modified_by": "Administrator",
"module": "Setup",
"name": "Home",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py
index f8a22b0e020..1164a5d3949 100644
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py
+++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/test_shopping_cart_settings.py
@@ -44,7 +44,6 @@ class TestShoppingCartSettings(unittest.TestCase):
def test_tax_rule_validation(self):
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
- frappe.db.commit()
cart_settings = self.get_cart_settings()
cart_settings.enabled = 1
diff --git a/erpnext/shopping_cart/utils.py b/erpnext/shopping_cart/utils.py
index f412e61f062..5f0c7923814 100644
--- a/erpnext/shopping_cart/utils.py
+++ b/erpnext/shopping_cart/utils.py
@@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-
import frappe
from erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings import (
@@ -18,10 +15,19 @@ def show_cart_count():
return False
def set_cart_count(login_manager):
- role, parties = check_customer_or_supplier()
- if role == 'Supplier': return
+ # since this is run only on hooks login event
+ # make sure user is already a customer
+ # before trying to set cart count
+ user_is_customer = is_customer()
+ if not user_is_customer:
+ return
+
if show_cart_count():
from erpnext.shopping_cart.cart import set_cart_count
+
+ # set_cart_count will try to fetch existing cart quotation
+ # or create one if non existent (and create a customer too)
+ # cart count is calculated from this quotation's items
set_cart_count()
def clear_cart_count(login_manager):
@@ -32,13 +38,13 @@ def update_website_context(context):
cart_enabled = is_cart_enabled()
context["shopping_cart_enabled"] = cart_enabled
-def check_customer_or_supplier():
- if frappe.session.user:
+def is_customer():
+ if frappe.session.user and frappe.session.user != "Guest":
contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user})
if contact_name:
contact = frappe.get_doc('Contact', contact_name)
for link in contact.links:
- if link.link_doctype in ('Customer', 'Supplier'):
- return link.link_doctype, link.link_name
+ if link.link_doctype == 'Customer':
+ return True
- return 'Customer', None
+ return False
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index 79989307efc..0a663c2a188 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -1,8 +1,5 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-
-import unittest
import frappe
from frappe.exceptions import ValidationError
@@ -11,9 +8,10 @@ from frappe.utils import cint, flt
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.stock.doctype.batch.batch import UnableToSelectBatchError, get_batch_no, get_batch_qty
from erpnext.stock.get_item_details import get_item_details
+from erpnext.tests.utils import ERPNextTestCase
-class TestBatch(unittest.TestCase):
+class TestBatch(ERPNextTestCase):
def test_item_has_batch_enabled(self):
self.assertRaises(ValidationError, frappe.get_doc({
"doctype": "Batch",
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 5fbc2d8dee1..4be0415564d 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -14,51 +14,6 @@ class Bin(Document):
self.stock_uom = frappe.get_cached_value('Item', self.item_code, 'stock_uom')
self.set_projected_qty()
- def update_stock(self, args, allow_negative_stock=False, via_landed_cost_voucher=False):
- '''Called from erpnext.stock.utils.update_bin'''
- self.update_qty(args)
-
- if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
- from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
-
- if not args.get("posting_date"):
- args["posting_date"] = nowdate()
-
- if args.get("is_cancelled") and via_landed_cost_voucher:
- return
-
- # Reposts only current voucher SL Entries
- # Updates valuation rate, stock value, stock queue for current transaction
- update_entries_after({
- "item_code": self.item_code,
- "warehouse": self.warehouse,
- "posting_date": args.get("posting_date"),
- "posting_time": args.get("posting_time"),
- "voucher_type": args.get("voucher_type"),
- "voucher_no": args.get("voucher_no"),
- "sle_id": args.name,
- "creation": args.creation
- }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
-
- # update qty in future ale and Validate negative qty
- update_qty_in_future_sle(args, allow_negative_stock)
-
-
- def update_qty(self, args):
- # update the stock values (for current quantities)
- if args.get("voucher_type")=="Stock Reconciliation":
- self.actual_qty = args.get("qty_after_transaction")
- else:
- self.actual_qty = flt(self.actual_qty) + flt(args.get("actual_qty"))
-
- self.ordered_qty = flt(self.ordered_qty) + flt(args.get("ordered_qty"))
- self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
- self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
- self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
-
- self.set_projected_qty()
- self.db_update()
-
def set_projected_qty(self):
self.projected_qty = (flt(self.actual_qty) + flt(self.ordered_qty)
+ flt(self.indented_qty) + flt(self.planned_qty) - flt(self.reserved_qty)
@@ -143,3 +98,67 @@ class Bin(Document):
def on_doctype_update():
frappe.db.add_index("Bin", ["item_code", "warehouse"])
+
+
+def update_stock(bin_name, args, allow_negative_stock=False, via_landed_cost_voucher=False):
+ '''Called from erpnext.stock.utils.update_bin'''
+ update_qty(bin_name, args)
+
+ if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
+ from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
+
+ if not args.get("posting_date"):
+ args["posting_date"] = nowdate()
+
+ if args.get("is_cancelled") and via_landed_cost_voucher:
+ return
+
+ # Reposts only current voucher SL Entries
+ # Updates valuation rate, stock value, stock queue for current transaction
+ update_entries_after({
+ "item_code": args.get('item_code'),
+ "warehouse": args.get('warehouse'),
+ "posting_date": args.get("posting_date"),
+ "posting_time": args.get("posting_time"),
+ "voucher_type": args.get("voucher_type"),
+ "voucher_no": args.get("voucher_no"),
+ "sle_id": args.get('name'),
+ "creation": args.get('creation')
+ }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+
+ # update qty in future sle and Validate negative qty
+ update_qty_in_future_sle(args, allow_negative_stock)
+
+def get_bin_details(bin_name):
+ return frappe.db.get_value('Bin', bin_name, ['actual_qty', 'ordered_qty',
+ 'reserved_qty', 'indented_qty', 'planned_qty', 'reserved_qty_for_production',
+ 'reserved_qty_for_sub_contract'], as_dict=1)
+
+def update_qty(bin_name, args):
+ bin_details = get_bin_details(bin_name)
+
+ # update the stock values (for current quantities)
+ if args.get("voucher_type")=="Stock Reconciliation":
+ actual_qty = args.get('qty_after_transaction')
+ else:
+ actual_qty = bin_details.actual_qty + flt(args.get("actual_qty"))
+
+ ordered_qty = flt(bin_details.ordered_qty) + flt(args.get("ordered_qty"))
+ reserved_qty = flt(bin_details.reserved_qty) + flt(args.get("reserved_qty"))
+ indented_qty = flt(bin_details.indented_qty) + flt(args.get("indented_qty"))
+ planned_qty = flt(bin_details.planned_qty) + flt(args.get("planned_qty"))
+
+
+ # compute projected qty
+ projected_qty = (flt(actual_qty) + flt(ordered_qty)
+ + flt(indented_qty) + flt(planned_qty) - flt(reserved_qty)
+ - flt(bin_details.reserved_qty_for_production) - flt(bin_details.reserved_qty_for_sub_contract))
+
+ frappe.db.set_value('Bin', bin_name, {
+ 'actual_qty': actual_qty,
+ 'ordered_qty': ordered_qty,
+ 'reserved_qty': reserved_qty,
+ 'indented_qty': indented_qty,
+ 'planned_qty': planned_qty,
+ 'projected_qty': projected_qty
+ })
\ No newline at end of file
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 9bf142c4b44..ad1b3b43aee 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -395,8 +395,7 @@
"fieldtype": "Link",
"label": "Billing Address Name",
"options": "Address",
- "print_hide": 1,
- "read_only": 1
+ "print_hide": 1
},
{
"fieldname": "tax_id",
@@ -1309,7 +1308,7 @@
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2021-09-28 13:10:09.761714",
+ "modified": "2021-10-08 14:29:13.428984",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 5542cd00d4c..f75b52cec8e 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -185,7 +185,6 @@ class DeliveryNote(SellingController):
if not d['warehouse'] and frappe.db.get_value("Item", d['item_code'], "is_stock_item") == 1:
frappe.throw(_("Warehouse required for stock Item {0}").format(d["item_code"]))
-
def update_current_stock(self):
if self.get("_action") and self._action != "update_after_submit":
for d in self.get('items'):
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 7fda94b269d..f58b586ab20 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -5,7 +5,6 @@
from __future__ import unicode_literals
import json
-import unittest
import frappe
from frappe.utils import cstr, flt, nowdate, nowtime
@@ -37,9 +36,10 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
)
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.tests.utils import ERPNextTestCase
-class TestDeliveryNote(unittest.TestCase):
+class TestDeliveryNote(ERPNextTestCase):
def test_over_billing_against_dn(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index b05090a237e..a96c29925e5 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -468,7 +468,7 @@
"width": "100px"
},
{
- "depends_on": "eval:parent.is_internal_customer",
+ "depends_on": "eval:parent.is_internal_customer || doc.target_warehouse",
"fieldname": "target_warehouse",
"fieldtype": "Link",
"hidden": 1,
@@ -759,7 +759,7 @@
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2021-02-23 01:04:08.588104",
+ "modified": "2021-10-05 12:12:44.018872",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
@@ -767,4 +767,4 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
index c9081c908f7..c6ff73e633b 100644
--- a/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
+++ b/erpnext/stock/doctype/delivery_trip/test_delivery_trip.py
@@ -14,11 +14,12 @@ from erpnext.stock.doctype.delivery_trip.delivery_trip import (
make_expense_claim,
notify_customers,
)
-from erpnext.tests.utils import create_test_contact_and_address
+from erpnext.tests.utils import ERPNextTestCase, create_test_contact_and_address
-class TestDeliveryTrip(unittest.TestCase):
+class TestDeliveryTrip(ERPNextTestCase):
def setUp(self):
+ super().setUp()
driver = create_driver()
create_vehicle()
create_delivery_notification()
@@ -32,6 +33,7 @@ class TestDeliveryTrip(unittest.TestCase):
frappe.db.sql("delete from `tabVehicle`")
frappe.db.sql("delete from `tabEmail Template`")
frappe.db.sql("delete from `tabDelivery Trip`")
+ return super().tearDown()
def test_expense_claim_fields_are_fetched_properly(self):
expense_claim = make_expense_claim(self.delivery_trip.name)
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index 768e5eae2da..8cc9f74a42a 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -181,6 +181,8 @@ class Item(WebsiteGenerator):
"doctype": "Item Price",
"price_list": price_list,
"item_code": self.name,
+ "uom": self.stock_uom,
+ "brand": self.brand,
"currency": erpnext.get_default_currency(),
"price_list_rate": self.standard_rate
})
@@ -634,9 +636,21 @@ class Item(WebsiteGenerator):
_("An Item Group exists with same name, please change the item name or rename the item group"))
def update_item_price(self):
- frappe.db.sql("""update `tabItem Price` set item_name=%s,
- item_description=%s, brand=%s where item_code=%s""",
- (self.item_name, self.description, self.brand, self.name))
+ frappe.db.sql("""
+ UPDATE `tabItem Price`
+ SET
+ item_name=%(item_name)s,
+ item_description=%(item_description)s,
+ brand=%(brand)s
+ WHERE item_code=%(item_code)s
+ """,
+ dict(
+ item_name=self.item_name,
+ item_description=self.description,
+ brand=self.brand,
+ item_code=self.name
+ )
+ )
def on_trash(self):
super(Item, self).on_trash()
diff --git a/erpnext/stock/doctype/item/test_item.py b/erpnext/stock/doctype/item/test_item.py
index e911d35db38..9198272513b 100644
--- a/erpnext/stock/doctype/item/test_item.py
+++ b/erpnext/stock/doctype/item/test_item.py
@@ -4,7 +4,6 @@
from __future__ import unicode_literals
import json
-import unittest
import frappe
from frappe.test_runner import make_test_objects
@@ -25,7 +24,7 @@ from erpnext.stock.doctype.item.item import (
)
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.get_item_details import get_item_details
-from erpnext.tests.utils import change_settings
+from erpnext.tests.utils import ERPNextTestCase, change_settings
test_ignore = ["BOM"]
test_dependencies = ["Warehouse", "Item Group", "Item Tax Template", "Brand", "Item Attribute"]
@@ -53,8 +52,9 @@ def make_item(item_code, properties=None):
return item
-class TestItem(unittest.TestCase):
+class TestItem(ERPNextTestCase):
def setUp(self):
+ super().setUp()
frappe.flags.attribute_values = None
def get_item(self, idx):
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index 2be8ef740a4..af6cc472e34 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -4,7 +4,6 @@
from __future__ import unicode_literals
import json
-import unittest
import frappe
from frappe.utils import flt
@@ -21,10 +20,12 @@ from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
create_stock_reconciliation,
)
+from erpnext.tests.utils import ERPNextTestCase
-class TestItemAlternative(unittest.TestCase):
+class TestItemAlternative(ERPNextTestCase):
def setUp(self):
+ super().setUp()
make_items()
def test_alternative_item_for_subcontract_rm(self):
diff --git a/erpnext/stock/doctype/item_attribute/test_item_attribute.py b/erpnext/stock/doctype/item_attribute/test_item_attribute.py
index fc809f443e6..2cd711bbb19 100644
--- a/erpnext/stock/doctype/item_attribute/test_item_attribute.py
+++ b/erpnext/stock/doctype/item_attribute/test_item_attribute.py
@@ -3,17 +3,17 @@
from __future__ import unicode_literals
-import unittest
-
import frappe
test_records = frappe.get_test_records('Item Attribute')
from erpnext.stock.doctype.item_attribute.item_attribute import ItemAttributeIncrementError
+from erpnext.tests.utils import ERPNextTestCase
-class TestItemAttribute(unittest.TestCase):
+class TestItemAttribute(ERPNextTestCase):
def setUp(self):
+ super().setUp()
if frappe.db.exists("Item Attribute", "_Test_Length"):
frappe.delete_doc("Item Attribute", "_Test_Length")
diff --git a/erpnext/stock/doctype/item_price/test_item_price.py b/erpnext/stock/doctype/item_price/test_item_price.py
index 5ed80921660..3a51fbbe17e 100644
--- a/erpnext/stock/doctype/item_price/test_item_price.py
+++ b/erpnext/stock/doctype/item_price/test_item_price.py
@@ -3,17 +3,17 @@
from __future__ import unicode_literals
-import unittest
-
import frappe
from frappe.test_runner import make_test_records_for_doctype
from erpnext.stock.doctype.item_price.item_price import ItemPriceDuplicateItem
from erpnext.stock.get_item_details import get_price_list_rate_for, process_args
+from erpnext.tests.utils import ERPNextTestCase
-class TestItemPrice(unittest.TestCase):
+class TestItemPrice(ERPNextTestCase):
def setUp(self):
+ super().setUp()
frappe.db.sql("delete from `tabItem Price`")
make_test_records_for_doctype("Item Price", force=True)
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 58a72f72dd1..339eaaaf7a5 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -4,8 +4,6 @@
from __future__ import unicode_literals
-import unittest
-
import frappe
from frappe.utils import flt
@@ -16,9 +14,10 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import (
get_gl_entries,
make_purchase_receipt,
)
+from erpnext.tests.utils import ERPNextTestCase
-class TestLandedCostVoucher(unittest.TestCase):
+class TestLandedCostVoucher(ERPNextTestCase):
def test_landed_cost_voucher(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
diff --git a/erpnext/stock/doctype/material_request/material_request.py b/erpnext/stock/doctype/material_request/material_request.py
index cf98b19e7a1..17df9777b19 100644
--- a/erpnext/stock/doctype/material_request/material_request.py
+++ b/erpnext/stock/doctype/material_request/material_request.py
@@ -296,7 +296,7 @@ def make_purchase_order(source_name, target_doc=None, args=None):
return d.ordered_qty < d.stock_qty and child_filter
- doclist = get_mapped_doc("Material Request", source_name, {
+ doclist = get_mapped_doc("Material Request", source_name, {
"Material Request": {
"doctype": "Purchase Order",
"validation": {
@@ -323,7 +323,7 @@ def make_purchase_order(source_name, target_doc=None, args=None):
@frappe.whitelist()
def make_request_for_quotation(source_name, target_doc=None):
- doclist = get_mapped_doc("Material Request", source_name, {
+ doclist = get_mapped_doc("Material Request", source_name, {
"Material Request": {
"doctype": "Request for Quotation",
"validation": {
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 5c2ac2584f7..f66a228e35e 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -6,8 +6,6 @@
from __future__ import unicode_literals
-import unittest
-
import frappe
from frappe.utils import flt, today
@@ -18,9 +16,10 @@ from erpnext.stock.doctype.material_request.material_request import (
make_supplier_quotation,
raise_work_orders,
)
+from erpnext.tests.utils import ERPNextTestCase
-class TestMaterialRequest(unittest.TestCase):
+class TestMaterialRequest(ERPNextTestCase):
def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert()
diff --git a/erpnext/stock/doctype/packing_slip/test_packing_slip.py b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
index 193adfcf1cb..c70cba67f2b 100644
--- a/erpnext/stock/doctype/packing_slip/test_packing_slip.py
+++ b/erpnext/stock/doctype/packing_slip/test_packing_slip.py
@@ -6,6 +6,8 @@ from __future__ import unicode_literals
import unittest
# test_records = frappe.get_test_records('Packing Slip')
+from erpnext.tests.utils import ERPNextTestCase
+
class TestPackingSlip(unittest.TestCase):
pass
diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json
index 21467935370..c604c711ef5 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.json
+++ b/erpnext/stock/doctype/pick_list/pick_list.json
@@ -18,7 +18,9 @@
"get_item_locations",
"section_break_6",
"locations",
- "amended_from"
+ "amended_from",
+ "print_settings_section",
+ "group_same_items"
],
"fields": [
{
@@ -110,14 +112,28 @@
"options": "STO-PICK-.YYYY.-",
"reqd": 1,
"set_only_once": 1
+ },
+ {
+ "fieldname": "print_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Print Settings"
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "0",
+ "fieldname": "group_same_items",
+ "fieldtype": "Check",
+ "label": "Group Same Items",
+ "print_hide": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-03-17 11:38:41.932875",
+ "modified": "2021-10-05 15:08:40.369957",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",
+ "naming_rule": "By \"Naming Series\" field",
"owner": "Administrator",
"permissions": [
{
@@ -184,4 +200,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index dffbe80fa39..4c02f3db43b 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -2,10 +2,8 @@
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-
import json
-from collections import OrderedDict
+from collections import OrderedDict, defaultdict
import frappe
from frappe import _
@@ -121,6 +119,34 @@ class PickList(Document):
and (self.for_qty is None or self.for_qty == 0):
frappe.throw(_("Qty of Finished Goods Item should be greater than 0."))
+ def before_print(self, settings=None):
+ if self.get("group_same_items"):
+ self.group_similar_items()
+
+ def group_similar_items(self):
+ group_item_qty = defaultdict(float)
+ group_picked_qty = defaultdict(float)
+
+ for item in self.locations:
+ group_item_qty[(item.item_code, item.warehouse)] += item.qty
+ group_picked_qty[(item.item_code, item.warehouse)] += item.picked_qty
+
+ duplicate_list = []
+ for item in self.locations:
+ if (item.item_code, item.warehouse) in group_item_qty:
+ item.qty = group_item_qty[(item.item_code, item.warehouse)]
+ item.picked_qty = group_picked_qty[(item.item_code, item.warehouse)]
+ item.stock_qty = group_item_qty[(item.item_code, item.warehouse)]
+ del group_item_qty[(item.item_code, item.warehouse)]
+ else:
+ duplicate_list.append(item)
+
+ for item in duplicate_list:
+ self.remove(item)
+
+ for idx, item in enumerate(self.locations, start=1):
+ item.idx = idx
+
def validate_item_locations(pick_list):
if not pick_list.locations:
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index aa710ad0e97..58b46e1eefc 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -3,9 +3,8 @@
# See license.txt
from __future__ import unicode_literals
-import unittest
-
import frappe
+from frappe import _dict
test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
@@ -15,9 +14,10 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
EmptyStockReconciliationItemsError,
)
+from erpnext.tests.utils import ERPNextTestCase
-class TestPickList(unittest.TestCase):
+class TestPickList(ERPNextTestCase):
def test_pick_list_picks_warehouse_for_each_item(self):
try:
@@ -357,6 +357,39 @@ class TestPickList(unittest.TestCase):
sales_order.cancel()
purchase_receipt.cancel()
+ def test_pick_list_grouping_before_print(self):
+ def _compare_dicts(a, b):
+ "compare dicts but ignore missing keys in `a`"
+ for key, value in a.items():
+ self.assertEqual(b.get(key), value, msg=f"{key} doesn't match")
+
+ # nothing should be grouped
+ pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[
+ _dict(item_code="A", warehouse="X", qty=1, picked_qty=2),
+ _dict(item_code="B", warehouse="X", qty=1, picked_qty=2),
+ _dict(item_code="A", warehouse="Y", qty=1, picked_qty=2),
+ _dict(item_code="B", warehouse="Y", qty=1, picked_qty=2),
+ ])
+ pl.before_print()
+ self.assertEqual(len(pl.locations), 4)
+
+ # grouping should halve the number of items
+ pl = frappe.get_doc(doctype="Pick List", group_same_items=True, locations=[
+ _dict(item_code="A", warehouse="X", qty=5, picked_qty=1),
+ _dict(item_code="B", warehouse="Y", qty=4, picked_qty=2),
+ _dict(item_code="A", warehouse="X", qty=3, picked_qty=2),
+ _dict(item_code="B", warehouse="Y", qty=2, picked_qty=2),
+ ])
+ pl.before_print()
+ self.assertEqual(len(pl.locations), 2)
+
+ expected_items = [
+ _dict(item_code="A", warehouse="X", qty=8, picked_qty=3),
+ _dict(item_code="B", warehouse="Y", qty=6, picked_qty=4),
+ ]
+ for expected_item, created_item in zip(expected_items, pl.locations):
+ _compare_dicts(expected_item, created_item)
+
# def test_pick_list_skips_items_in_expired_batch(self):
# pass
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 07a568db869..47c8df9a2c9 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -842,7 +842,8 @@ def make_stock_entry(source_name,target_doc=None):
"doctype": "Stock Entry Detail",
"field_map": {
"warehouse": "s_warehouse",
- "parent": "reference_purchase_receipt"
+ "parent": "reference_purchase_receipt",
+ "batch_no": "batch_no"
},
},
}, target_doc, set_missing_values)
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 044856cca95..de177444285 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -17,9 +17,10 @@ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchas
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError, get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.stock_ledger import SerialNoExistsInFutureTransaction
+from erpnext.tests.utils import ERPNextTestCase
-class TestPurchaseReceipt(unittest.TestCase):
+class TestPurchaseReceipt(ERPNextTestCase):
def setUp(self):
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
index 0aa7610575e..c25bca94dbb 100644
--- a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
+++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
@@ -3,8 +3,6 @@
# See license.txt
from __future__ import unicode_literals
-import unittest
-
import frappe
from erpnext.stock.doctype.batch.test_batch import make_new_batch
@@ -13,9 +11,10 @@ from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_pu
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.get_item_details import get_conversion_factor
+from erpnext.tests.utils import ERPNextTestCase
-class TestPutawayRule(unittest.TestCase):
+class TestPutawayRule(ERPNextTestCase):
def setUp(self):
if not frappe.db.exists("Item", "_Rice"):
make_item("_Rice", {
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index f5d076a077a..308c62875d5 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -1,8 +1,6 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and Contributors
# See license.txt
-import unittest
-
import frappe
from frappe.utils import nowdate
@@ -15,12 +13,14 @@ from erpnext.controllers.stock_controller import (
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestCase
# test_records = frappe.get_test_records('Quality Inspection')
-class TestQualityInspection(unittest.TestCase):
+class TestQualityInspection(ERPNextTestCase):
def setUp(self):
+ super().setUp()
create_item("_Test Item with QA")
frappe.db.set_value(
"Item", "_Test Item with QA", "inspection_required_before_delivery", 1
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 82d8aaed5b3..a9254fb9ecf 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -611,7 +611,9 @@ def get_pos_reserved_serial_nos(filters):
return reserved_sr_nos
-def fetch_serial_numbers(filters, qty, do_not_include=[]):
+def fetch_serial_numbers(filters, qty, do_not_include=None):
+ if do_not_include is None:
+ do_not_include = []
batch_join_selection = ""
batch_no_condition = ""
batch_nos = filters.get("batch_no")
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index 818c163c681..570f22e6af3 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -6,8 +6,6 @@
from __future__ import unicode_literals
-import unittest
-
import frappe
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@@ -20,9 +18,10 @@ test_dependencies = ["Item"]
test_records = frappe.get_test_records('Serial No')
from erpnext.stock.doctype.serial_no.serial_no import *
+from erpnext.tests.utils import ERPNextTestCase
-class TestSerialNo(unittest.TestCase):
+class TestSerialNo(ERPNextTestCase):
def test_cannot_create_direct(self):
frappe.delete_doc_if_exists("Serial No", "_TCSER0001")
@@ -185,14 +184,14 @@ class TestSerialNo(unittest.TestCase):
se = frappe.copy_doc(test_records[0])
se.get("items")[0].item_code = item_code
- se.get("items")[0].qty = 3
- se.get("items")[0].serial_no = " _TS1, _TS2 , _TS3 "
- se.get("items")[0].transfer_qty = 3
+ se.get("items")[0].qty = 4
+ se.get("items")[0].serial_no = " _TS1, _TS2 , _TS3 , _TS4 - 2021"
+ se.get("items")[0].transfer_qty = 4
se.set_stock_entry_type()
se.insert()
se.submit()
- self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3")
+ self.assertEqual(se.get("items")[0].serial_no, "_TS1\n_TS2\n_TS3\n_TS4 - 2021")
frappe.db.rollback()
diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py
index 9914cf80158..dcd0b7c17c7 100644
--- a/erpnext/stock/doctype/shipment/test_shipment.py
+++ b/erpnext/stock/doctype/shipment/test_shipment.py
@@ -3,15 +3,15 @@
# See license.txt
from __future__ import unicode_literals
-import unittest
from datetime import date, timedelta
import frappe
from erpnext.stock.doctype.delivery_note.delivery_note import make_shipment
+from erpnext.tests.utils import ERPNextTestCase
-class TestShipment(unittest.TestCase):
+class TestShipment(ERPNextTestCase):
def test_shipment_from_delivery_note(self):
delivery_note = create_test_delivery_note()
delivery_note.submit()
@@ -47,7 +47,6 @@ def create_test_delivery_note():
}
)
delivery_note.insert()
- frappe.db.commit()
return delivery_note
@@ -91,7 +90,6 @@ def create_test_shipment(delivery_notes = None):
}
)
shipment.insert()
- frappe.db.commit()
return shipment
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
index 2463a21ed61..2651407d16f 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
@@ -142,6 +142,7 @@
"oldfieldtype": "Data",
"print_width": "150px",
"read_only": 1,
+ "search_index": 1,
"width": "150px"
},
{
@@ -316,7 +317,7 @@
"in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-09-07 11:10:35.318872",
+ "modified": "2021-10-08 13:42:51.857631",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",
@@ -338,4 +339,4 @@
],
"sort_field": "modified",
"sort_order": "DESC"
-}
\ No newline at end of file
+}
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index caa1d42b662..2cf71accf83 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -181,4 +181,4 @@ def on_doctype_update():
frappe.db.add_index("Stock Ledger Entry", ["voucher_no", "voucher_type"])
frappe.db.add_index("Stock Ledger Entry", ["batch_no", "item_code", "warehouse"])
- frappe.db.add_index("Stock Ledger Entry", ["voucher_detail_no"])
+ frappe.db.add_index("Stock Ledger Entry", ["warehouse", "item_code"], "item_warehouse")
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 61bae49b0bd..ff33c2789b7 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -3,8 +3,6 @@
# See license.txt
from __future__ import unicode_literals
-import unittest
-
import frappe
from frappe.core.page.permission_manager.permission_manager import reset
from frappe.utils import add_days, today
@@ -21,9 +19,10 @@ from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import
create_stock_reconciliation,
)
from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.tests.utils import ERPNextTestCase
-class TestStockLedgerEntry(unittest.TestCase):
+class TestStockLedgerEntry(ERPNextTestCase):
def setUp(self):
items = create_items()
reset('Stock Entry')
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 8647bee40ec..415ac5eb267 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -6,8 +6,6 @@
from __future__ import unicode_literals
-import unittest
-
import frappe
from frappe.utils import add_days, flt, nowdate, nowtime, random_string
@@ -22,12 +20,13 @@ from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import (
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
-from erpnext.tests.utils import change_settings
+from erpnext.tests.utils import ERPNextTestCase, change_settings
-class TestStockReconciliation(unittest.TestCase):
+class TestStockReconciliation(ERPNextTestCase):
@classmethod
def setUpClass(self):
+ super().setUpClass()
create_batch_or_serial_no_items()
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -372,7 +371,6 @@ class TestStockReconciliation(unittest.TestCase):
"""
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.stock_ledger import NegativeStockError
- frappe.db.commit()
item_code = "Backdated-Reco-Cancellation-Item"
warehouse = "_Test Warehouse - _TC"
@@ -395,10 +393,6 @@ class TestStockReconciliation(unittest.TestCase):
repost_exists = bool(frappe.db.exists("Repost Item Valuation", {"voucher_no": sr.name}))
self.assertFalse(repost_exists, msg="Negative stock validation not working on reco cancellation")
- # teardown
- frappe.db.rollback()
-
-
def test_valid_batch(self):
create_batch_item_with_batch("Testing Batch Item 1", "001")
create_batch_item_with_batch("Testing Batch Item 2", "002")
diff --git a/erpnext/stock/doctype/stock_settings/test_stock_settings.py b/erpnext/stock/doctype/stock_settings/test_stock_settings.py
index 7e8090499fb..bf8ac5dc79a 100644
--- a/erpnext/stock/doctype/stock_settings/test_stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/test_stock_settings.py
@@ -7,9 +7,12 @@ import unittest
import frappe
+from erpnext.tests.utils import ERPNextTestCase
-class TestStockSettings(unittest.TestCase):
+
+class TestStockSettings(ERPNextTestCase):
def setUp(self):
+ super().setUp()
frappe.db.set_value("Stock Settings", None, "clean_description_html", 0)
def test_settings(self):
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index 1ca7181f279..98317ec9c57 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -2,8 +2,6 @@
# License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals
-import unittest
-
import frappe
from frappe.test_runner import make_test_records
from frappe.utils import cint
@@ -12,11 +10,13 @@ import erpnext
from erpnext.accounts.doctype.account.test_account import create_account, get_inventory_account
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.tests.utils import ERPNextTestCase
test_records = frappe.get_test_records('Warehouse')
-class TestWarehouse(unittest.TestCase):
+class TestWarehouse(ERPNextTestCase):
def setUp(self):
+ super().setUp()
if not frappe.get_value('Item', '_Test Item'):
make_test_records('Item')
diff --git a/erpnext/stock/form_tour/material_request/material_request.json b/erpnext/stock/form_tour/material_request/material_request.json
new file mode 100644
index 00000000000..145b4a06c2b
--- /dev/null
+++ b/erpnext/stock/form_tour/material_request/material_request.json
@@ -0,0 +1,97 @@
+{
+ "creation": "2021-07-29 12:32:08.929900",
+ "docstatus": 0,
+ "doctype": "Form Tour",
+ "idx": 0,
+ "is_standard": 1,
+ "modified": "2021-10-05 13:11:13.119453",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Material Request",
+ "owner": "Administrator",
+ "reference_doctype": "Material Request",
+ "save_on_complete": 1,
+ "steps": [
+ {
+ "description": "The purpose of the material request can be selected here. For now select \"Purchase\" as the purpose.",
+ "field": "",
+ "fieldname": "material_request_type",
+ "fieldtype": "Select",
+ "has_next_condition": 1,
+ "is_table_field": 0,
+ "label": "Purpose",
+ "next_step_condition": "eval: doc.material_request_type == \"Purchase\"",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Purpose"
+ },
+ {
+ "description": "Set the \"Required By\" date for the materials. This sets the \"Required By\" date for all the items.",
+ "field": "",
+ "fieldname": "schedule_date",
+ "fieldtype": "Date",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Required By",
+ "next_step_condition": "",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Required By"
+ },
+ {
+ "description": "Setting the target warehouse sets it for all the items.",
+ "field": "",
+ "fieldname": "set_warehouse",
+ "fieldtype": "Link",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Set Target Warehouse",
+ "next_step_condition": "",
+ "parent_field": "",
+ "position": "Left",
+ "title": "Target Warehouse"
+ },
+ {
+ "description": "Items table",
+ "field": "",
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "has_next_condition": 0,
+ "is_table_field": 0,
+ "label": "Items",
+ "parent_field": "",
+ "position": "Bottom",
+ "title": "Items"
+ },
+ {
+ "child_doctype": "Material Request Item",
+ "description": "Select an Item code. Item details will be fetched automatically.",
+ "field": "",
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "has_next_condition": 1,
+ "is_table_field": 1,
+ "label": "Item Code",
+ "next_step_condition": "eval: doc.item_code",
+ "parent_field": "",
+ "parent_fieldname": "items",
+ "position": "Right",
+ "title": "Item Code"
+ },
+ {
+ "child_doctype": "Material Request Item",
+ "description": "Enter the required quantity for the material.",
+ "field": "",
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "has_next_condition": 0,
+ "is_table_field": 1,
+ "label": "Quantity",
+ "parent_field": "",
+ "parent_fieldname": "items",
+ "position": "Bottom",
+ "title": "Quantity"
+ }
+ ],
+ "title": "Material Request"
+}
\ No newline at end of file
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 19597c3d993..e0190b64a75 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -89,7 +89,13 @@ def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=Tru
out.update(get_bin_details(args.item_code, args.get("from_warehouse")))
elif out.get("warehouse"):
- out.update(get_bin_details(args.item_code, out.warehouse, args.company))
+ if doc and doc.get('doctype') == 'Purchase Order':
+ # calculate company_total_stock only for po
+ bin_details = get_bin_details(args.item_code, out.warehouse, args.company)
+ else:
+ bin_details = get_bin_details(args.item_code, out.warehouse)
+
+ out.update(bin_details)
# update args with out, if key or value not exists
for key, value in iteritems(out):
@@ -382,7 +388,7 @@ def get_basic_details(args, item, overwrite_warehouse=True):
return out
-def get_item_warehouse(item, args, overwrite_warehouse, defaults={}):
+def get_item_warehouse(item, args, overwrite_warehouse, defaults=None):
if not defaults:
defaults = frappe._dict({
'item_defaults' : get_item_defaults(item.name, args.company),
@@ -485,8 +491,9 @@ def get_item_tax_template(args, item, out):
"item_tax_template": None
}
"""
- item_tax_template = args.get("item_tax_template")
- item_tax_template = _get_item_tax_template(args, item.taxes, out)
+ item_tax_template = None
+ if item.taxes:
+ item_tax_template = _get_item_tax_template(args, item.taxes, out)
if not item_tax_template:
item_group = item.item_group
@@ -502,17 +509,17 @@ def _get_item_tax_template(args, taxes, out=None, for_validate=False):
taxes_with_no_validity = []
for tax in taxes:
- tax_company = frappe.get_value("Item Tax Template", tax.item_tax_template, 'company')
- if (tax.valid_from or tax.maximum_net_rate) and tax_company == args['company']:
- # In purchase Invoice first preference will be given to supplier invoice date
- # if supplier date is not present then posting date
- validation_date = args.get('transaction_date') or args.get('bill_date') or args.get('posting_date')
+ tax_company = frappe.get_cached_value("Item Tax Template", tax.item_tax_template, 'company')
+ if tax_company == args['company']:
+ if (tax.valid_from or tax.maximum_net_rate):
+ # In purchase Invoice first preference will be given to supplier invoice date
+ # if supplier date is not present then posting date
+ validation_date = args.get('transaction_date') or args.get('bill_date') or args.get('posting_date')
- if getdate(tax.valid_from) <= getdate(validation_date) \
- and is_within_valid_range(args, tax):
- taxes_with_validity.append(tax)
- else:
- if tax_company == args['company']:
+ if getdate(tax.valid_from) <= getdate(validation_date) \
+ and is_within_valid_range(args, tax):
+ taxes_with_validity.append(tax)
+ else:
taxes_with_no_validity.append(tax)
if taxes_with_validity:
@@ -890,8 +897,7 @@ def get_pos_profile_item_details(company, args, pos_profile=None, update_data=Fa
res[fieldname] = pos_profile.get(fieldname)
if res.get("warehouse"):
- res.actual_qty = get_bin_details(args.item_code,
- res.warehouse).get("actual_qty")
+ res.actual_qty = get_bin_details(args.item_code, res.warehouse).get("actual_qty")
return res
diff --git a/erpnext/stock/reorder_item.py b/erpnext/stock/reorder_item.py
index 3cd4cd27617..7c6fbfd9cd1 100644
--- a/erpnext/stock/reorder_item.py
+++ b/erpnext/stock/reorder_item.py
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import json
+from math import ceil
import frappe
from frappe import _
@@ -149,11 +150,16 @@ def create_material_request(material_requests):
conversion_factor = frappe.db.get_value("UOM Conversion Detail",
{'parent': item.name, 'uom': uom}, 'conversion_factor') or 1.0
+ must_be_whole_number = frappe.db.get_value("UOM", uom, "must_be_whole_number", cache=True)
+ qty = d.reorder_qty / conversion_factor
+ if must_be_whole_number:
+ qty = ceil(qty)
+
mr.append("items", {
"doctype": "Material Request Item",
"item_code": d.item_code,
"schedule_date": add_days(nowdate(),cint(item.lead_time_days)),
- "qty": d.reorder_qty / conversion_factor,
+ "qty": qty,
"uom": uom,
"stock_uom": item.stock_uom,
"warehouse": d.warehouse,
diff --git a/erpnext/stock/report/stock_analytics/test_stock_analytics.py b/erpnext/stock/report/stock_analytics/test_stock_analytics.py
index 21e1205bfcc..32df5859375 100644
--- a/erpnext/stock/report/stock_analytics/test_stock_analytics.py
+++ b/erpnext/stock/report/stock_analytics/test_stock_analytics.py
@@ -5,9 +5,10 @@ from frappe import _dict
from erpnext.accounts.utils import get_fiscal_year
from erpnext.stock.report.stock_analytics.stock_analytics import get_period_date_ranges
+from erpnext.tests.utils import ERPNextTestCase
-class TestStockAnalyticsReport(unittest.TestCase):
+class TestStockAnalyticsReport(ERPNextTestCase):
def test_get_period_date_ranges(self):
filters = _dict(range="Monthly", from_date="2020-12-28", to_date="2021-02-06")
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index fc5d5c12da4..bb53c557371 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 1ea58fed191..4e20b472617 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),
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index 1b5b792f946..bdbec52f7e4 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -13,8 +13,8 @@ from six import iteritems
import erpnext
from erpnext.stock.utils import (
- get_bin,
get_incoming_outgoing_rate_for_cancel,
+ get_or_make_bin,
get_valuation_method,
)
@@ -123,12 +123,11 @@ def set_as_cancel(voucher_type, voucher_no):
(now(), frappe.session.user, voucher_type, voucher_no))
def make_entry(args, allow_negative_stock=False, via_landed_cost_voucher=False):
- args.update({"doctype": "Stock Ledger Entry"})
+ args["doctype"] = "Stock Ledger Entry"
sle = frappe.get_doc(args)
sle.flags.ignore_permissions = 1
sle.allow_negative_stock=allow_negative_stock
sle.via_landed_cost_voucher = via_landed_cost_voucher
- sle.insert()
sle.submit()
return sle
@@ -805,14 +804,13 @@ class update_entries_after(object):
def update_bin(self):
# update bin for each warehouse
for warehouse, data in iteritems(self.data):
- bin_doc = get_bin(self.item_code, warehouse)
- bin_doc.update({
+ bin_record = get_or_make_bin(self.item_code, warehouse)
+
+ frappe.db.set_value('Bin', bin_record, {
"valuation_rate": data.valuation_rate,
"actual_qty": data.qty_after_transaction,
"stock_value": data.stock_value
})
- bin_doc.flags.via_stock_ledger_entry = True
- bin_doc.save(ignore_permissions=True)
def get_previous_sle_of_current_voucher(args, exclude_current_voucher=False):
@@ -918,7 +916,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
company = erpnext.get_default_company()
last_valuation_rate = frappe.db.sql("""select valuation_rate
- from `tabStock Ledger Entry`
+ from `tabStock Ledger Entry` force index (item_warehouse)
where
item_code = %s
AND warehouse = %s
@@ -929,7 +927,7 @@ def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
if not last_valuation_rate:
# Get valuation rate from last sle for the item against any warehouse
last_valuation_rate = frappe.db.sql("""select valuation_rate
- from `tabStock Ledger Entry`
+ from `tabStock Ledger Entry` force index (item_code)
where
item_code = %s
AND valuation_rate > 0
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index aeb06e987f8..c4a0497b744 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -180,12 +180,27 @@ def get_bin(item_code, warehouse):
bin_obj.flags.ignore_permissions = True
return bin_obj
+def get_or_make_bin(item_code, warehouse) -> str:
+ bin_record = frappe.db.get_value('Bin', {'item_code': item_code, 'warehouse': warehouse})
+
+ if not bin_record:
+ bin_obj = frappe.get_doc({
+ "doctype": "Bin",
+ "item_code": item_code,
+ "warehouse": warehouse,
+ })
+ bin_obj.flags.ignore_permissions = 1
+ bin_obj.insert()
+ bin_record = bin_obj.name
+
+ return bin_record
+
def update_bin(args, allow_negative_stock=False, via_landed_cost_voucher=False):
+ from erpnext.stock.doctype.bin.bin import update_stock
is_stock_item = frappe.get_cached_value('Item', args.get("item_code"), 'is_stock_item')
if is_stock_item:
- bin = get_bin(args.get("item_code"), args.get("warehouse"))
- bin.update_stock(args, allow_negative_stock, via_landed_cost_voucher)
- return bin
+ bin_record = get_or_make_bin(args.get("item_code"), args.get("warehouse"))
+ update_stock(bin_record, args, allow_negative_stock, via_landed_cost_voucher)
else:
frappe.msgprint(_("Item {0} ignored since it is not a stock item").format(args.get("item_code")))
diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json
index 26d10ce7038..9c805150f19 100644
--- a/erpnext/stock/workspace/stock/stock.json
+++ b/erpnext/stock/workspace/stock/stock.json
@@ -1,6 +1,4 @@
{
- "cards_label": "Masters & Reports",
- "category": "",
"charts": [
{
"chart_name": "Warehouse wise Stock Value"
@@ -8,18 +6,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Stock\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Quick Access\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Material Request\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Stock Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Receipt\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Delivery Note\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Stock Ledger\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Stock Balance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Masters & Reports\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Items and Pricing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Stock Transactions\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Stock Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Serial No and Batch\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Incorrect Data Report\", \"col\": 4}}]",
"creation": "2020-03-02 15:43:10.096528",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "stock",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Stock",
"links": [
{
@@ -764,15 +756,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:02.361509",
+ "modified": "2021-08-05 12:16:02.361519",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
- "onboarding": "Stock",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
@@ -831,6 +820,5 @@
"type": "Dashboard"
}
],
- "shortcuts_label": "Quick Access",
"title": "Stock"
}
\ No newline at end of file
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 7d7399d097c..0fe1068a76c 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -228,7 +228,7 @@ def get_time_in_timedelta(time):
def set_first_response_time(communication, method):
if communication.get('reference_doctype') == "Issue":
issue = get_parent_doc(communication)
- if is_first_response(issue):
+ if is_first_response(issue) and issue.service_level_agreement:
first_response_time = calculate_first_response_time(issue, get_datetime(issue.first_responded_on))
issue.db_set("first_response_time", first_response_time)
diff --git a/erpnext/support/doctype/support_settings/support_settings.json b/erpnext/support/doctype/support_settings/support_settings.json
index 5d3d3ace59d..bf1daa16f86 100644
--- a/erpnext/support/doctype/support_settings/support_settings.json
+++ b/erpnext/support/doctype/support_settings/support_settings.json
@@ -37,7 +37,6 @@
},
{
"default": "7",
- "description": "Auto close Issue after 7 days",
"fieldname": "close_issue_after_days",
"fieldtype": "Int",
"label": "Close Issue After Days"
@@ -164,7 +163,7 @@
],
"issingle": 1,
"links": [],
- "modified": "2020-06-11 13:08:38.473616",
+ "modified": "2021-10-14 13:08:38.473616",
"modified_by": "Administrator",
"module": "Support",
"name": "Support Settings",
@@ -185,4 +184,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
\ No newline at end of file
+}
diff --git a/erpnext/support/workspace/support/support.json b/erpnext/support/workspace/support/support.json
index 4c5829d7a03..d68c7c70cf7 100644
--- a/erpnext/support/workspace/support/support.json
+++ b/erpnext/support/workspace/support/support.json
@@ -1,20 +1,13 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Issue\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Maintenance Visit\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Service Level Agreement\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Issues\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Maintenance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Service Level Agreement\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Warranty\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
"creation": "2020-03-02 15:48:23.224699",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "support",
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Support",
"links": [
{
@@ -176,15 +169,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:02.699923",
+ "modified": "2021-08-05 12:16:02.699924",
"modified_by": "Administrator",
"module": "Support",
"name": "Support",
- "onboarding": "",
"owner": "Administrator",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
diff --git a/erpnext/tests/utils.py b/erpnext/tests/utils.py
index a3cab4b59da..91df5480e35 100644
--- a/erpnext/tests/utils.py
+++ b/erpnext/tests/utils.py
@@ -2,6 +2,7 @@
# License: GNU General Public License v3. See license.txt
import copy
+import unittest
from contextlib import contextmanager
from typing import Any, Dict, NewType, Optional
@@ -12,6 +13,21 @@ ReportFilters = Dict[str, Any]
ReportName = NewType("ReportName", str)
+class ERPNextTestCase(unittest.TestCase):
+ """A sane default test class for ERPNext tests."""
+
+
+ @classmethod
+ def setUpClass(cls) -> None:
+ frappe.db.commit()
+ return super().setUpClass()
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ frappe.db.rollback()
+ return super().tearDownClass()
+
+
def create_test_contact_and_address():
frappe.db.sql('delete from tabContact')
frappe.db.sql('delete from `tabContact Email`')
diff --git a/erpnext/utilities/workspace/utilities/utilities.json b/erpnext/utilities/workspace/utilities/utilities.json
index 4ad4afb8f41..02a8af5d6c9 100644
--- a/erpnext/utilities/workspace/utilities/utilities.json
+++ b/erpnext/utilities/workspace/utilities/utilities.json
@@ -1,19 +1,12 @@
{
- "category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Video\", \"col\": 4}}]",
"creation": "2020-09-10 12:21:22.335307",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
- "extends": "",
- "extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"idx": 0,
- "is_default": 0,
- "is_standard": 0,
"label": "Utilities",
"links": [
{
@@ -47,15 +40,12 @@
"type": "Link"
}
],
- "modified": "2021-08-05 12:16:03.350804",
+ "modified": "2021-08-05 12:16:03.350805",
"modified_by": "Administrator",
"module": "Utilities",
"name": "Utilities",
- "onboarding": "",
"owner": "user@erpnext.com",
"parent_page": "",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],