Merge branch 'develop' into fix-depr-after-sale
This commit is contained in:
commit
0515b4b6f6
3
.github/helper/install.sh
vendored
3
.github/helper/install.sh
vendored
@ -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
|
||||
|
38
.github/helper/semgrep_rules/README.md
vendored
38
.github/helper/semgrep_rules/README.md
vendored
@ -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
|
@ -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"
|
151
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
151
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
@ -1,151 +0,0 @@
|
||||
# This file specifies rules for correctness according to how frappe doctype data model works.
|
||||
|
||||
rules:
|
||||
- id: frappe-modifying-but-not-comitting
|
||||
patterns:
|
||||
- pattern: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
- pattern-not: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
...
|
||||
self.db_set(..., self.$ATTR, ...)
|
||||
- pattern-not: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = $SOME_VAR
|
||||
...
|
||||
self.db_set(..., $SOME_VAR, ...)
|
||||
- pattern-not: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = $SOME_VAR
|
||||
...
|
||||
self.save()
|
||||
- metavariable-regex:
|
||||
metavariable: '$ATTR'
|
||||
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
|
||||
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
|
||||
- metavariable-regex:
|
||||
metavariable: "$METHOD"
|
||||
regex: "(on_submit|on_cancel)"
|
||||
message: |
|
||||
DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-modifying-but-not-comitting-other-method
|
||||
patterns:
|
||||
- pattern: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
- pattern-not: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
...
|
||||
self.db_set(..., self.$ATTR, ...)
|
||||
- pattern-not: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = $SOME_VAR
|
||||
...
|
||||
self.db_set(..., $SOME_VAR, ...)
|
||||
- pattern-not: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
self.save()
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
- metavariable-regex:
|
||||
metavariable: "$METHOD"
|
||||
regex: "(on_submit|on_cancel)"
|
||||
message: |
|
||||
self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-print-function-in-doctypes
|
||||
pattern: print(...)
|
||||
message: |
|
||||
Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement.
|
||||
languages: [python]
|
||||
severity: WARNING
|
||||
paths:
|
||||
include:
|
||||
- "*/**/doctype/*"
|
||||
|
||||
- id: frappe-modifying-child-tables-while-iterating
|
||||
pattern-either:
|
||||
- pattern: |
|
||||
for $ROW in self.$TABLE:
|
||||
...
|
||||
self.remove(...)
|
||||
- pattern: |
|
||||
for $ROW in self.$TABLE:
|
||||
...
|
||||
self.append(...)
|
||||
message: |
|
||||
Child table being modified while iterating on it.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
paths:
|
||||
include:
|
||||
- "*/**/doctype/*"
|
||||
|
||||
- id: frappe-same-key-assigned-twice
|
||||
pattern-either:
|
||||
- pattern: |
|
||||
{..., $X: $A, ..., $X: $B, ...}
|
||||
- pattern: |
|
||||
dict(..., ($X, $A), ..., ($X, $B), ...)
|
||||
- pattern: |
|
||||
_dict(..., ($X, $A), ..., ($X, $B), ...)
|
||||
message: |
|
||||
key `$X` is uselessly assigned twice. This could be a potential bug.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
|
||||
- id: frappe-manual-commit
|
||||
patterns:
|
||||
- pattern: frappe.db.commit()
|
||||
- pattern-not-inside: |
|
||||
try:
|
||||
...
|
||||
except ...:
|
||||
...
|
||||
message: |
|
||||
Manually commiting a transaction is highly discouraged. Read about the transaction model implemented by Frappe Framework before adding manual commits: https://frappeframework.com/docs/user/en/api/database#database-transaction-model If you think manual commit is required then add a comment explaining why and `// nosemgrep` on the same line.
|
||||
paths:
|
||||
exclude:
|
||||
- "**/patches/**"
|
||||
- "**/demo/**"
|
||||
languages: [python]
|
||||
severity: ERROR
|
15
.github/helper/semgrep_rules/report.py
vendored
15
.github/helper/semgrep_rules/report.py
vendored
@ -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"))
|
34
.github/helper/semgrep_rules/report.yml
vendored
34
.github/helper/semgrep_rules/report.yml
vendored
@ -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
|
6
.github/helper/semgrep_rules/security.py
vendored
6
.github/helper/semgrep_rules/security.py
vendored
@ -1,6 +0,0 @@
|
||||
def function_name(input):
|
||||
# ruleid: frappe-codeinjection-eval
|
||||
eval(input)
|
||||
|
||||
# ok: frappe-codeinjection-eval
|
||||
eval("1 + 1")
|
10
.github/helper/semgrep_rules/security.yml
vendored
10
.github/helper/semgrep_rules/security.yml
vendored
@ -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
|
44
.github/helper/semgrep_rules/translate.js
vendored
44
.github/helper/semgrep_rules/translate.js
vendored
@ -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])
|
61
.github/helper/semgrep_rules/translate.py
vendored
61
.github/helper/semgrep_rules/translate.py
vendored
@ -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
|
64
.github/helper/semgrep_rules/translate.yml
vendored
64
.github/helper/semgrep_rules/translate.yml
vendored
@ -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
|
9
.github/helper/semgrep_rules/ux.js
vendored
9
.github/helper/semgrep_rules/ux.js
vendored
@ -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") }}. ');
|
31
.github/helper/semgrep_rules/ux.py
vendored
31
.github/helper/semgrep_rules/ux.py
vendored
@ -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"))
|
30
.github/helper/semgrep_rules/ux.yml
vendored
30
.github/helper/semgrep_rules/ux.yml
vendored
@ -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
|
18
.github/workflows/linters.yml
vendored
18
.github/workflows/linters.yml
vendored
@ -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
|
||||
|
2
.github/workflows/server-tests.yml
vendored
2
.github/workflows/server-tests.yml
vendored
@ -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
|
||||
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
@ -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) \
|
||||
|
@ -69,7 +69,7 @@ def import_coa(file_name, company):
|
||||
|
||||
frappe.local.flags.ignore_root_company_validation = True
|
||||
forest = build_forest(data)
|
||||
create_charts(company, custom_chart=forest)
|
||||
create_charts(company, custom_chart=forest, from_coa_importer=True)
|
||||
|
||||
# trigger on_update for company to reset default accounts
|
||||
set_default_accounts(company)
|
||||
@ -148,7 +148,7 @@ def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
|
||||
|
||||
if not for_validate:
|
||||
forest = build_forest(data)
|
||||
accounts = build_tree_from_json("", chart_data=forest) # returns a list of dict in a tree render-able form
|
||||
accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form
|
||||
|
||||
# filter out to show data for the selected node only
|
||||
accounts = [d for d in accounts if d['parent_account']==parent]
|
||||
@ -212,11 +212,14 @@ def build_forest(data):
|
||||
if not account_name:
|
||||
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
|
||||
|
||||
name = account_name
|
||||
if account_number:
|
||||
account_number = cstr(account_number).strip()
|
||||
account_name = "{} - {}".format(account_number, account_name)
|
||||
|
||||
charts_map[account_name] = {}
|
||||
charts_map[account_name]['account_name'] = name
|
||||
if account_number: charts_map[account_name]["account_number"] = account_number
|
||||
if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
|
||||
if account_type: charts_map[account_name]["account_type"] = account_type
|
||||
if root_type: charts_map[account_name]["root_type"] = root_type
|
||||
|
@ -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",
|
||||
|
@ -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:
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -1439,6 +1439,7 @@ class SalesInvoice(SellingController):
|
||||
return
|
||||
|
||||
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
|
||||
total = get_total_in_party_account_currency(self)
|
||||
|
||||
if not status:
|
||||
if self.docstatus == 2:
|
||||
@ -1446,9 +1447,9 @@ class SalesInvoice(SellingController):
|
||||
elif self.docstatus == 1:
|
||||
if self.is_internal_transfer():
|
||||
self.status = 'Internal Transfer'
|
||||
elif is_overdue(self):
|
||||
elif is_overdue(self, total):
|
||||
self.status = "Overdue"
|
||||
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
|
||||
elif 0 < outstanding_amount < total:
|
||||
self.status = "Partly Paid"
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
@ -1475,27 +1476,42 @@ class SalesInvoice(SellingController):
|
||||
if update:
|
||||
self.db_set('status', self.status, update_modified = update_modified)
|
||||
|
||||
def is_overdue(doc):
|
||||
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
|
||||
|
||||
def get_total_in_party_account_currency(doc):
|
||||
total_fieldname = (
|
||||
"grand_total"
|
||||
if doc.disable_rounded_total
|
||||
else "rounded_total"
|
||||
)
|
||||
if doc.party_account_currency != doc.currency:
|
||||
total_fieldname = "base_" + total_fieldname
|
||||
|
||||
return flt(doc.get(total_fieldname), doc.precision(total_fieldname))
|
||||
|
||||
def is_overdue(doc, total):
|
||||
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
|
||||
if outstanding_amount <= 0:
|
||||
return
|
||||
|
||||
grand_total = flt(doc.grand_total, doc.precision("grand_total"))
|
||||
nowdate = getdate()
|
||||
if doc.payment_schedule:
|
||||
# calculate payable amount till date
|
||||
payable_amount = sum(
|
||||
payment.payment_amount
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < nowdate
|
||||
)
|
||||
today = getdate()
|
||||
if doc.get('is_pos') or not doc.get('payment_schedule'):
|
||||
return getdate(doc.due_date) < today
|
||||
|
||||
if (grand_total - outstanding_amount) < payable_amount:
|
||||
return True
|
||||
# calculate payable amount till date
|
||||
payment_amount_field = (
|
||||
"base_payment_amount"
|
||||
if doc.party_account_currency != doc.currency
|
||||
else "payment_amount"
|
||||
)
|
||||
|
||||
payable_amount = sum(
|
||||
payment.get(payment_amount_field)
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < today
|
||||
)
|
||||
|
||||
return (total - outstanding_amount) < payable_amount
|
||||
|
||||
elif getdate(doc.due_date) < nowdate:
|
||||
return True
|
||||
|
||||
def get_discounting_status(sales_invoice):
|
||||
status = None
|
||||
|
@ -203,6 +203,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
||||
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
tax_amount = round(tax_amount)
|
||||
|
||||
return tax_amount, tax_deducted
|
||||
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
|
||||
@ -322,9 +325,6 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
else:
|
||||
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
tds_amount = round(tds_amount)
|
||||
|
||||
return tds_amount
|
||||
|
||||
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
|
@ -115,7 +115,7 @@ def prepare_companywise_opening_balance(asset_data, liability_data, equity_data,
|
||||
# opening_value = Aseet - liability - equity
|
||||
for data in [asset_data, liability_data, equity_data]:
|
||||
account_name = get_root_account_name(data[0].root_type, company)
|
||||
opening_value += get_opening_balance(account_name, data, company)
|
||||
opening_value += (get_opening_balance(account_name, data, company) or 0.0)
|
||||
|
||||
opening_balance[company] = opening_value
|
||||
|
||||
|
@ -421,8 +421,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
update_value_in_dict(totals, 'closing', gle)
|
||||
|
||||
elif gle.posting_date <= to_date:
|
||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
|
||||
update_value_in_dict(totals, 'total', gle)
|
||||
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
|
||||
gle_map[gle.get(group_by)].entries.append(gle)
|
||||
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
|
||||
@ -436,10 +434,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
else:
|
||||
update_value_in_dict(consolidated_gle, key, gle)
|
||||
|
||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
|
||||
update_value_in_dict(totals, 'closing', gle)
|
||||
|
||||
for key, value in consolidated_gle.items():
|
||||
update_value_in_dict(gle_map[value.get(group_by)].totals, 'total', value)
|
||||
update_value_in_dict(totals, 'total', value)
|
||||
update_value_in_dict(gle_map[value.get(group_by)].totals, 'closing', value)
|
||||
update_value_in_dict(totals, 'closing', value)
|
||||
entries.append(value)
|
||||
|
||||
return totals, entries
|
||||
|
@ -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,
|
||||
|
@ -1686,17 +1686,58 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
||||
|
||||
def update_invoice_status():
|
||||
"""Updates status as Overdue for applicable invoices. Runs daily."""
|
||||
today = getdate()
|
||||
|
||||
for doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
frappe.db.sql("""
|
||||
update `tab{}` as dt set dt.status = 'Overdue'
|
||||
where dt.docstatus = 1
|
||||
and dt.status != 'Overdue'
|
||||
and dt.outstanding_amount > 0
|
||||
and (dt.grand_total - dt.outstanding_amount) <
|
||||
(select sum(payment_amount) from `tabPayment Schedule` as ps
|
||||
where ps.parent = dt.name and ps.due_date < %s)
|
||||
""".format(doctype), getdate())
|
||||
UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
|
||||
WHERE invoice.docstatus = 1
|
||||
AND invoice.status REGEXP '^Unpaid|^Partly Paid'
|
||||
AND invoice.outstanding_amount > 0
|
||||
AND (
|
||||
{or_condition}
|
||||
(
|
||||
(
|
||||
CASE
|
||||
WHEN invoice.party_account_currency = invoice.currency
|
||||
THEN (
|
||||
CASE
|
||||
WHEN invoice.disable_rounded_total
|
||||
THEN invoice.grand_total
|
||||
ELSE invoice.rounded_total
|
||||
END
|
||||
)
|
||||
ELSE (
|
||||
CASE
|
||||
WHEN invoice.disable_rounded_total
|
||||
THEN invoice.base_grand_total
|
||||
ELSE invoice.base_rounded_total
|
||||
END
|
||||
)
|
||||
END
|
||||
) - invoice.outstanding_amount
|
||||
) < (
|
||||
SELECT SUM(
|
||||
CASE
|
||||
WHEN invoice.party_account_currency = invoice.currency
|
||||
THEN ps.payment_amount
|
||||
ELSE ps.base_payment_amount
|
||||
END
|
||||
)
|
||||
FROM `tabPayment Schedule` ps
|
||||
WHERE ps.parent = invoice.name
|
||||
AND ps.due_date < %(today)s
|
||||
)
|
||||
)
|
||||
""".format(
|
||||
doctype=doctype,
|
||||
or_condition=(
|
||||
"invoice.is_pos AND invoice.due_date < %(today)s OR"
|
||||
if doctype == "Sales Invoice"
|
||||
else ""
|
||||
)
|
||||
), {"today": today}
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
||||
|
@ -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'):
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
@ -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))
|
||||
|
@ -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 = []
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
}
|
||||
]
|
||||
}
|
@ -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:
|
64
erpnext/manufacturing/report/test_reports.py
Normal file
64
erpnext/manufacturing/report/test_reports.py
Normal file
@ -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,
|
||||
)
|
@ -294,6 +294,7 @@ erpnext.patches.v13_0.set_operation_time_based_on_operating_cost
|
||||
erpnext.patches.v13_0.validate_options_for_data_field
|
||||
erpnext.patches.v13_0.create_gst_payment_entry_fields
|
||||
erpnext.patches.v14_0.delete_shopify_doctypes
|
||||
erpnext.patches.v13_0.fix_invoice_statuses
|
||||
erpnext.patches.v13_0.replace_supplier_item_group_with_party_specific_item
|
||||
erpnext.patches.v13_0.update_dates_in_tax_withholding_category
|
||||
erpnext.patches.v14_0.update_opportunity_currency_fields
|
||||
@ -307,3 +308,5 @@ erpnext.patches.v13_0.set_status_in_maintenance_schedule_table
|
||||
erpnext.patches.v13_0.add_default_interview_notification_templates
|
||||
erpnext.patches.v13_0.enable_scheduler_job_for_item_reposting
|
||||
erpnext.patches.v13_0.requeue_failed_reposts
|
||||
erpnext.patches.v13_0.healthcare_deprecation_warning
|
||||
erpnext.patches.v14_0.delete_healthcare_doctypes
|
||||
|
113
erpnext/patches/v13_0/fix_invoice_statuses.py
Normal file
113
erpnext/patches/v13_0/fix_invoice_statuses.py
Normal file
@ -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
|
49
erpnext/patches/v14_0/delete_healthcare_doctypes.py
Normal file
49
erpnext/patches/v14_0/delete_healthcare_doctypes.py
Normal file
@ -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)
|
@ -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 = []
|
||||
|
@ -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",
|
||||
|
@ -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}
|
||||
)
|
||||
|
||||
|
@ -536,6 +536,61 @@ class TestSalarySlip(unittest.TestCase):
|
||||
# undelete fixture data
|
||||
frappe.db.rollback()
|
||||
|
||||
def test_tax_for_recurring_additional_salary(self):
|
||||
frappe.db.sql("""delete from `tabPayroll Period`""")
|
||||
frappe.db.sql("""delete from `tabSalary Component`""")
|
||||
|
||||
payroll_period = create_payroll_period()
|
||||
|
||||
create_tax_slab(payroll_period, allow_tax_exemption=True)
|
||||
|
||||
employee = make_employee("test_tax@salary.slip")
|
||||
delete_docs = [
|
||||
"Salary Slip",
|
||||
"Additional Salary",
|
||||
"Employee Tax Exemption Declaration",
|
||||
"Employee Tax Exemption Proof Submission",
|
||||
"Employee Benefit Claim",
|
||||
"Salary Structure Assignment"
|
||||
]
|
||||
for doc in delete_docs:
|
||||
frappe.db.sql("delete from `tab%s` where employee='%s'" % (doc, employee))
|
||||
|
||||
from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
|
||||
|
||||
salary_structure = make_salary_structure("Stucture to test tax", "Monthly",
|
||||
other_details={"max_benefits": 100000}, test_tax=True,
|
||||
employee=employee, payroll_period=payroll_period)
|
||||
|
||||
|
||||
create_salary_slips_for_payroll_period(employee, salary_structure.name,
|
||||
payroll_period, deduct_random=False, num=3)
|
||||
|
||||
tax_paid = get_tax_paid_in_period(employee)
|
||||
|
||||
annual_tax = 23196.0
|
||||
self.assertEqual(tax_paid, annual_tax)
|
||||
|
||||
frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))
|
||||
|
||||
#------------------------------------
|
||||
# Recurring additional salary
|
||||
start_date = add_months(payroll_period.start_date, 3)
|
||||
end_date = add_months(payroll_period.start_date, 5)
|
||||
create_recurring_additional_salary(employee, "Performance Bonus", 20000, start_date, end_date)
|
||||
|
||||
frappe.db.sql("""delete from `tabSalary Slip` where employee=%s""", (employee))
|
||||
|
||||
create_salary_slips_for_payroll_period(employee, salary_structure.name,
|
||||
payroll_period, deduct_random=False, num=4)
|
||||
|
||||
tax_paid = get_tax_paid_in_period(employee)
|
||||
|
||||
annual_tax = 32315.0
|
||||
self.assertEqual(tax_paid, annual_tax)
|
||||
|
||||
frappe.db.rollback()
|
||||
|
||||
def make_activity_for_employee(self):
|
||||
activity_type = frappe.get_doc("Activity Type", "_Test Activity Type")
|
||||
activity_type.billing_rate = 50
|
||||
@ -1007,3 +1062,17 @@ def make_salary_slip_for_payment_days_dependency_test(employee, salary_structure
|
||||
salary_slip = frappe.get_doc("Salary Slip", salary_slip_name)
|
||||
|
||||
return salary_slip
|
||||
|
||||
def create_recurring_additional_salary(employee, salary_component, amount, from_date, to_date, company=None):
|
||||
frappe.get_doc({
|
||||
"doctype": "Additional Salary",
|
||||
"employee": employee,
|
||||
"company": company or erpnext.get_default_company(),
|
||||
"salary_component": salary_component,
|
||||
"is_recurring": 1,
|
||||
"from_date": from_date,
|
||||
"to_date": to_date,
|
||||
"amount": amount,
|
||||
"type": "Earning",
|
||||
"currency": erpnext.get_default_currency()
|
||||
}).submit()
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
}
|
@ -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
|
||||
|
||||
|
@ -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),
|
||||
|
Loading…
x
Reference in New Issue
Block a user