Merge branch 'develop' of https://github.com/frappe/erpnext into term_loan_enhancement
This commit is contained in:
commit
b812660e40
7
.github/helper/.flake8_strict
vendored
7
.github/helper/.flake8_strict
vendored
@ -1,6 +1,8 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
B007,
|
||||
B009,
|
||||
B010,
|
||||
B950,
|
||||
E101,
|
||||
E111,
|
||||
@ -65,11 +67,6 @@ ignore =
|
||||
E713,
|
||||
E712,
|
||||
|
||||
enable-extensions =
|
||||
M90
|
||||
|
||||
select =
|
||||
M511
|
||||
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules,test_*.py
|
||||
|
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"
|
133
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
133
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
@ -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
|
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
|
32
.github/try-on-f-cloud-button.svg
vendored
Normal file
32
.github/try-on-f-cloud-button.svg
vendored
Normal file
@ -0,0 +1,32 @@
|
||||
<svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<g filter="url(#filter0_dd)">
|
||||
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
|
||||
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>
|
||||
<path d="M41.6982 35.5H45.0129V28.7109C45.0129 27.2344 46.0866 26.2188 47.5494 26.2188C48.0085 26.2188 48.6388 26.2969 48.95 26.3984V23.4453C48.6543 23.375 48.2419 23.3281 47.9074 23.3281C46.5691 23.3281 45.472 24.1094 45.0362 25.5938H44.9117V23.5H41.6982V35.5Z" fill="white"/>
|
||||
<path d="M52.8331 40C55.2996 40 56.6068 38.7344 57.2837 36.7969L61.9289 23.5156L58.4197 23.5L55.9221 32.3125H55.7976L53.3233 23.5H49.8374L54.1247 35.8437L53.9302 36.3516C53.4944 37.4766 52.6619 37.5312 51.4947 37.1719L50.7478 39.6562C51.2224 39.8594 51.9927 40 52.8331 40Z" fill="white"/>
|
||||
<path d="M73.6142 35.7344C77.2401 35.7344 79.4966 33.2422 79.4966 29.5469C79.4966 25.8281 77.2401 23.3438 73.6142 23.3438C69.9883 23.3438 67.7319 25.8281 67.7319 29.5469C67.7319 33.2422 69.9883 35.7344 73.6142 35.7344ZM73.6298 33.1562C71.9569 33.1562 71.101 31.6171 71.101 29.5233C71.101 27.4296 71.9569 25.8827 73.6298 25.8827C75.2715 25.8827 76.1274 27.4296 76.1274 29.5233C76.1274 31.6171 75.2715 33.1562 73.6298 33.1562Z" fill="white"/>
|
||||
<path d="M84.7253 28.5625C84.7331 27.0156 85.6512 26.1094 86.9895 26.1094C88.3201 26.1094 89.1215 26.9844 89.1137 28.4531V35.5H92.4284V27.8594C92.4284 25.0625 90.7945 23.3438 88.3046 23.3438C86.5306 23.3438 85.2466 24.2187 84.7097 25.6172H84.5697V23.5H81.4106V35.5H84.7253V28.5625Z" fill="white"/>
|
||||
<path fill-rule="evenodd" clip-rule="evenodd" d="M102.429 19.5H113.429V22.3141H102.429V19.5ZM102.429 35.5V26.6794H112.699V29.4982H105.94V35.5H102.429Z" fill="white"/>
|
||||
<path d="M131.584 24.9625C131.09 21.5057 128.345 19.5 124.785 19.5C120.589 19.5 117.429 22.463 117.429 27.4924C117.429 32.5142 120.55 35.4848 124.785 35.4848C128.604 35.4848 131.137 33.0916 131.584 30.1211L128.651 30.1059C128.282 31.9293 126.745 32.9549 124.824 32.9549C122.22 32.9549 120.354 31.0632 120.354 27.4924C120.354 23.9824 122.204 22.0299 124.832 22.0299C126.784 22.0299 128.314 23.1011 128.651 24.9625H131.584Z" fill="white"/>
|
||||
<path d="M136.409 19.7124H133.571V35.2718H136.409V19.7124Z" fill="white"/>
|
||||
<path d="M144.031 35.5001C147.56 35.5001 149.803 33.0917 149.803 29.483C149.803 25.8667 147.56 23.4507 144.031 23.4507C140.502 23.4507 138.259 25.8667 138.259 29.483C138.259 33.0917 140.502 35.5001 144.031 35.5001ZM144.047 33.2969C142.094 33.2969 141.137 31.6103 141.137 29.4754C141.137 27.3406 142.094 25.6312 144.047 25.6312C145.968 25.6312 146.925 27.3406 146.925 29.4754C146.925 31.6103 145.968 33.2969 144.047 33.2969Z" fill="white"/>
|
||||
<path d="M159.338 30.3641C159.338 32.1419 158.028 33.0232 156.773 33.0232C155.409 33.0232 154.499 32.0887 154.499 30.6072V23.6025H151.66V31.0327C151.66 33.8361 153.307 35.4239 155.675 35.4239C157.479 35.4239 158.749 34.5046 159.298 33.1979H159.424V35.272H162.176V23.6025H159.338V30.3641Z" fill="white"/>
|
||||
<path d="M169.014 35.4769C171.084 35.4769 172.017 34.2841 172.464 33.4332H172.637V35.2718H175.429V19.7124H172.582V25.532H172.464C172.033 24.6887 171.147 23.4503 169.022 23.4503C166.238 23.4503 164.05 25.5624 164.05 29.4522C164.05 33.2965 166.175 35.4769 169.014 35.4769ZM169.806 33.2205C167.931 33.2205 166.943 31.6251 166.943 29.437C166.943 27.2642 167.916 25.7067 169.806 25.7067C171.633 25.7067 172.637 27.173 172.637 29.437C172.637 31.701 171.617 33.2205 169.806 33.2205Z" fill="white"/>
|
||||
</g>
|
||||
<defs>
|
||||
<filter id="filter0_dd" x="0" y="0" width="201" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset/>
|
||||
<feGaussianBlur stdDeviation="0.25"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
|
||||
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
|
||||
<feOffset dy="2"/>
|
||||
<feGaussianBlur stdDeviation="2"/>
|
||||
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
|
||||
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
|
||||
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 4.4 KiB |
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
|
||||
|
@ -21,9 +21,9 @@ repos:
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [
|
||||
'flake8-mutable',
|
||||
'flake8-bugbear',
|
||||
]
|
||||
args: ['--select=M511', '--config', '.github/helper/.flake8_strict']
|
||||
args: ['--config', '.github/helper/.flake8_strict']
|
||||
exclude: ".*setup.py$"
|
||||
|
||||
- repo: https://github.com/timothycrosley/isort
|
||||
|
@ -39,6 +39,12 @@ ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
<a href="https://frappecloud.com/deploy?apps=frappe,erpnext&source=erpnext_readme">
|
||||
<img src=".github/try-on-f-cloud-button.svg" height="40">
|
||||
</a>
|
||||
</div>
|
||||
|
||||
### 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.
|
||||
|
@ -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 });
|
||||
});
|
||||
});
|
||||
|
@ -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))
|
||||
|
@ -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) \
|
||||
|
@ -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",
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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),
|
||||
|
@ -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",
|
||||
|
@ -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",
|
||||
|
@ -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"
|
||||
|
@ -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",
|
||||
|
@ -1284,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):
|
||||
@ -1419,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:
|
||||
@ -1426,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"
|
||||
@ -1455,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
|
||||
|
@ -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
|
||||
@ -2357,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-.#####'
|
||||
|
@ -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):
|
||||
"""
|
||||
|
@ -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()
|
||||
|
||||
|
@ -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):
|
||||
|
@ -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):
|
||||
|
@ -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 = $(`<span>${value}</span>`);
|
||||
|
||||
|
@ -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)
|
||||
|
@ -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,
|
||||
|
@ -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": [],
|
||||
|
@ -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": [],
|
||||
|
@ -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:
|
||||
|
@ -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()
|
||||
|
@ -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,
|
||||
|
@ -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": [],
|
||||
|
@ -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 ") + "<a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a>" + __(" 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 <a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a> choose the 'Naming Series' option."),
|
||||
},
|
||||
{
|
||||
fieldname: "buying_price_list",
|
||||
|
@ -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"
|
||||
}
|
82
erpnext/buying/form_tour/purchase_order/purchase_order.json
Normal file
82
erpnext/buying/form_tour/purchase_order/purchase_order.json
Normal file
@ -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"
|
||||
}
|
@ -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.",
|
||||
|
@ -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
|
||||
}
|
@ -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
|
||||
}
|
@ -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"
|
||||
}
|
@ -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()
|
||||
|
@ -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"
|
||||
}
|
@ -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):
|
||||
|
@ -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)
|
||||
|
@ -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'):
|
||||
|
@ -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": [],
|
||||
|
@ -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": [],
|
||||
|
@ -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
|
||||
}
|
@ -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
|
@ -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'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
|
@ -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",
|
||||
|
@ -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 <b>Create Transaction</b> or <b>Sandbox Mode</b>, you need to check the <b>Enable Tax Calculation</b> 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)
|
||||
|
@ -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 \
|
||||
|
@ -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": [],
|
||||
|
@ -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": [],
|
||||
|
@ -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"
|
||||
|
@ -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
|
||||
|
@ -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()
|
@ -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;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -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))
|
||||
|
@ -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 = []
|
||||
|
@ -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": [],
|
||||
|
@ -336,7 +336,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",
|
||||
@ -362,7 +361,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-10-20 08:28:16.796105",
|
||||
"modified": "2021-10-12 18:10:32.360818",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Loan Management",
|
||||
"name": "Loan",
|
||||
|
@ -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})
|
||||
|
@ -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
|
||||
|
@ -197,7 +197,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)
|
||||
@ -208,3 +208,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)'))
|
||||
|
@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import add_days, add_months, cint, date_diff, flt, get_datetime, getdate
|
||||
from frappe.utils import add_days, cint, date_diff, flt, get_datetime, getdate
|
||||
from six import iteritems
|
||||
|
||||
import erpnext
|
||||
|
@ -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": [],
|
||||
|
@ -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')
|
||||
|
@ -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,
|
||||
|
@ -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})
|
||||
|
||||
|
@ -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,
|
||||
)
|
@ -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": [],
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user