Merge branch 'develop' of https://github.com/frappe/erpnext into asset-capitalization
# Conflicts: # erpnext/accounts/doctype/sales_invoice/sales_invoice.py # erpnext/assets/doctype/asset/test_asset.py # erpnext/assets/workspace/assets/assets.json
This commit is contained in:
commit
2d9da22721
2
.github/helper/.flake8_strict
vendored
2
.github/helper/.flake8_strict
vendored
@ -1,6 +1,8 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
B007,
|
||||
B009,
|
||||
B010,
|
||||
B950,
|
||||
E101,
|
||||
E111,
|
||||
|
2
.github/helper/documentation.py
vendored
2
.github/helper/documentation.py
vendored
@ -24,6 +24,8 @@ def docs_link_exists(body):
|
||||
parts = parsed_url.path.split('/')
|
||||
if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
|
||||
return True
|
||||
elif parsed_url.netloc == "docs.erpnext.com":
|
||||
return True
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
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"))
|
21
.github/helper/semgrep_rules/report.yml
vendored
21
.github/helper/semgrep_rules/report.yml
vendored
@ -1,21 +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
|
||||
|
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 |
2
.github/workflows/docker-release.yml
vendored
2
.github/workflows/docker-release.yml
vendored
@ -11,4 +11,4 @@ jobs:
|
||||
- name: curl
|
||||
run: |
|
||||
apk add curl bash
|
||||
curl -s -X POST -H "Content-Type: application/json" -H "Accept: application/json" -H "Travis-API-Version: 3" -H "Authorization: token ${{ secrets.TRAVIS_CI_TOKEN }}" -d '{"request":{"branch":"master"}}' https://api.travis-ci.com/repo/frappe%2Ffrappe_docker/requests
|
||||
curl -X POST -H "Accept: application/vnd.github.v3+json" -H "Authorization: Bearer ${{ secrets.CI_PAT }}" https://api.github.com/repos/frappe/frappe_docker/actions/workflows/build_stable.yml/dispatches -d '{"ref":"main"}'
|
||||
|
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
|
||||
|
23
.github/workflows/patch.yml
vendored
23
.github/workflows/patch.yml
vendored
@ -86,4 +86,27 @@ jobs:
|
||||
cd ~/frappe-bench/
|
||||
wget https://erpnext.com/files/v10-erpnext.sql.gz
|
||||
bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz
|
||||
|
||||
git -C "apps/frappe" remote set-url upstream https://github.com/frappe/frappe.git
|
||||
git -C "apps/erpnext" remote set-url upstream https://github.com/frappe/erpnext.git
|
||||
|
||||
for version in $(seq 12 13)
|
||||
do
|
||||
echo "Updating to v$version"
|
||||
branch_name="version-$version"
|
||||
|
||||
git -C "apps/frappe" fetch --depth 1 upstream $branch_name:$branch_name
|
||||
git -C "apps/erpnext" fetch --depth 1 upstream $branch_name:$branch_name
|
||||
|
||||
git -C "apps/frappe" checkout -q -f $branch_name
|
||||
git -C "apps/erpnext" checkout -q -f $branch_name
|
||||
|
||||
bench setup requirements --python
|
||||
bench --site test_site migrate
|
||||
done
|
||||
|
||||
|
||||
echo "Updating to latest version"
|
||||
git -C "apps/frappe" checkout -q -f "${GITHUB_BASE_REF:-${GITHUB_REF##*/}}"
|
||||
git -C "apps/erpnext" checkout -q -f "$GITHUB_SHA"
|
||||
bench --site test_site migrate
|
||||
|
40
.github/workflows/server-tests.yml
vendored
40
.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
|
||||
@ -99,34 +101,10 @@ jobs:
|
||||
CI_BUILD_ID: ${{ github.run_id }}
|
||||
ORCHESTRATOR_URL: http://test-orchestrator.frappe.io
|
||||
|
||||
- name: Upload Coverage Data
|
||||
run: |
|
||||
cp ~/frappe-bench/sites/.coverage ${GITHUB_WORKSPACE}
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip3 install coverage==5.5
|
||||
pip3 install coveralls==3.0.1
|
||||
coveralls
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
|
||||
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
|
||||
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
|
||||
COVERALLS_PARALLEL: true
|
||||
|
||||
coveralls:
|
||||
name: Coverage Wrap Up
|
||||
needs: test
|
||||
container: python:3-slim
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Clone
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Coveralls Finished
|
||||
run: |
|
||||
cd ${GITHUB_WORKSPACE}
|
||||
pip3 install coverage==5.5
|
||||
pip3 install coveralls==3.0.1
|
||||
coveralls --finish
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload coverage data
|
||||
uses: codecov/codecov-action@v2
|
||||
with:
|
||||
name: MariaDB
|
||||
fail_ci_if_error: true
|
||||
files: /home/runner/frappe-bench/sites/coverage.xml
|
||||
verbose: true
|
||||
|
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
@ -104,6 +104,8 @@ jobs:
|
||||
|
||||
- name: Build Assets
|
||||
run: cd ~/frappe-bench/ && bench build
|
||||
env:
|
||||
CI: Yes
|
||||
|
||||
- name: UI Tests
|
||||
run: cd ~/frappe-bench/ && bench --site test_site run-ui-tests erpnext --headless
|
||||
|
58
.mergify.yml
Normal file
58
.mergify.yml
Normal file
@ -0,0 +1,58 @@
|
||||
pull_request_rules:
|
||||
- name: Auto-close PRs on stable branch
|
||||
conditions:
|
||||
- and:
|
||||
- and:
|
||||
- author!=surajshetty3416
|
||||
- author!=gavindsouza
|
||||
- author!=rohitwaghchaure
|
||||
- author!=nabinhait
|
||||
- or:
|
||||
- base=version-13
|
||||
- base=version-12
|
||||
actions:
|
||||
close:
|
||||
comment:
|
||||
message: |
|
||||
@{{author}}, thanks for the contribution, but we do not accept pull requests on a stable branch. Please raise PR on an appropriate hotfix branch.
|
||||
https://github.com/frappe/erpnext/wiki/Pull-Request-Checklist#which-branch
|
||||
|
||||
- name: backport to version-13-hotfix
|
||||
conditions:
|
||||
- label="backport version-13-hotfix"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-13-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-13-pre-release
|
||||
conditions:
|
||||
- label="backport version-13-pre-release"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-13-pre-release
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-12-hotfix
|
||||
conditions:
|
||||
- label="backport version-12-hotfix"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-12-hotfix
|
||||
assignees:
|
||||
- "{{ author }}"
|
||||
|
||||
- name: backport to version-12-pre-release
|
||||
conditions:
|
||||
- label="backport version-12-pre-release"
|
||||
actions:
|
||||
backport:
|
||||
branches:
|
||||
- version-12-pre-release
|
||||
assignees:
|
||||
- "{{ author }}"
|
@ -20,6 +20,9 @@ repos:
|
||||
rev: 3.9.2
|
||||
hooks:
|
||||
- id: flake8
|
||||
additional_dependencies: [
|
||||
'flake8-bugbear',
|
||||
]
|
||||
args: ['--config', '.github/helper/.flake8_strict']
|
||||
exclude: ".*setup.py$"
|
||||
|
||||
|
8
.snyk
8
.snyk
@ -1,8 +0,0 @@
|
||||
# Snyk (https://snyk.io) policy file, patches or ignores known vulnerabilities.
|
||||
version: v1.14.0
|
||||
ignore: {}
|
||||
# patches apply the minimum changes required to fix a vulnerability
|
||||
patch:
|
||||
SNYK-JS-LODASH-450202:
|
||||
- cypress > getos > async > lodash:
|
||||
patched: '2020-01-31T01:35:12.802Z'
|
22
README.md
22
README.md
@ -7,7 +7,7 @@
|
||||
|
||||
[![CI](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml/badge.svg?branch=develop)](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml)
|
||||
[![Open Source Helpers](https://www.codetriage.com/frappe/erpnext/badges/users.svg)](https://www.codetriage.com/frappe/erpnext)
|
||||
[![Coverage Status](https://coveralls.io/repos/github/frappe/erpnext/badge.svg?branch=develop)](https://coveralls.io/github/frappe/erpnext?branch=develop)
|
||||
[![codecov](https://codecov.io/gh/frappe/erpnext/branch/develop/graph/badge.svg?token=0TwvyUg3I5)](https://codecov.io/gh/frappe/erpnext)
|
||||
|
||||
[https://erpnext.com](https://erpnext.com)
|
||||
|
||||
@ -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.
|
||||
@ -49,14 +55,6 @@ The Easy Way: our install script for bench will install all dependencies (e.g. M
|
||||
|
||||
New passwords will be created for the ERPNext "Administrator" user, the MariaDB root user, and the frappe user (the script displays the passwords and saves them to ~/frappe_passwords.txt).
|
||||
|
||||
### Virtual Image
|
||||
|
||||
You can download a virtual image to run ERPNext in a virtual machine on your local system.
|
||||
|
||||
- [ERPNext Download](http://erpnext.com/download)
|
||||
|
||||
System and user credentials are listed on the download page.
|
||||
|
||||
---
|
||||
|
||||
## License
|
||||
@ -77,6 +75,12 @@ The ERPNext code is licensed as GNU General Public License (v3) and the Document
|
||||
|
||||
---
|
||||
|
||||
## Learning
|
||||
|
||||
1. [Frappe School](https://frappe.school) - Learn Frappe Framework and ERPNext from the various courses by the maintainers or from the community.
|
||||
|
||||
---
|
||||
|
||||
## Logo and Trademark
|
||||
|
||||
The brand name ERPNext and the logo are trademarks of Frappe Technologies Pvt. Ltd.
|
||||
|
17
codecov.yml
Normal file
17
codecov.yml
Normal file
@ -0,0 +1,17 @@
|
||||
codecov:
|
||||
require_ci_to_pass: yes
|
||||
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 0.5%
|
||||
|
||||
comment:
|
||||
layout: "diff, files"
|
||||
require_changes: true
|
||||
after_n_builds: 3
|
||||
|
||||
ignore:
|
||||
- "erpnext/demo"
|
@ -6,7 +6,7 @@ context('Organizational Chart', () => {
|
||||
|
||||
it('navigates to org chart', () => {
|
||||
cy.visit('/app');
|
||||
cy.awesomebar('Organizational Chart');
|
||||
cy.visit('/app/organizational-chart');
|
||||
cy.url().should('include', '/organizational-chart');
|
||||
|
||||
cy.window().its('frappe.csrf_token').then(csrf_token => {
|
||||
@ -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 });
|
||||
});
|
||||
});
|
||||
|
@ -7,7 +7,7 @@ context('Organizational Chart Mobile', () => {
|
||||
it('navigates to org chart', () => {
|
||||
cy.viewport(375, 667);
|
||||
cy.visit('/app');
|
||||
cy.awesomebar('Organizational Chart');
|
||||
cy.visit('/app/organizational-chart');
|
||||
cy.url().should('include', '/organizational-chart');
|
||||
|
||||
cy.window().its('frappe.csrf_token').then(csrf_token => {
|
||||
@ -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 });
|
||||
});
|
||||
});
|
||||
|
@ -10,6 +10,7 @@ from frappe.contacts.doctype.address.address import (
|
||||
class ERPNextAddress(Address):
|
||||
def validate(self):
|
||||
self.validate_reference()
|
||||
self.update_compnay_address()
|
||||
super(ERPNextAddress, self).validate()
|
||||
|
||||
def link_address(self):
|
||||
@ -19,6 +20,11 @@ class ERPNextAddress(Address):
|
||||
|
||||
return super(ERPNextAddress, self).link_address()
|
||||
|
||||
def update_compnay_address(self):
|
||||
for link in self.get('links'):
|
||||
if link.link_doctype == 'Company':
|
||||
self.is_your_company_address = 1
|
||||
|
||||
def validate_reference(self):
|
||||
if self.is_your_company_address and not [
|
||||
row for row in self.links if row.link_doctype == "Company"
|
||||
|
@ -374,12 +374,15 @@ def make_gl_entries(doc, credit_account, debit_account, against,
|
||||
try:
|
||||
make_gl_entries(gl_entries, cancel=(doc.docstatus == 2), merge_entries=True)
|
||||
frappe.db.commit()
|
||||
except Exception:
|
||||
frappe.db.rollback()
|
||||
traceback = frappe.get_traceback()
|
||||
frappe.log_error(message=traceback)
|
||||
except Exception as e:
|
||||
if frappe.flags.in_test:
|
||||
raise e
|
||||
else:
|
||||
frappe.db.rollback()
|
||||
traceback = frappe.get_traceback()
|
||||
frappe.log_error(message=traceback)
|
||||
|
||||
frappe.flags.deferred_accounting_error = True
|
||||
frappe.flags.deferred_accounting_error = True
|
||||
|
||||
def send_mail(deferred_process):
|
||||
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
|
||||
|
@ -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))
|
||||
|
@ -45,6 +45,49 @@ frappe.treeview_settings["Account"] = {
|
||||
],
|
||||
root_label: "Accounts",
|
||||
get_tree_nodes: 'erpnext.accounts.utils.get_children',
|
||||
on_get_node: function(nodes, deep=false) {
|
||||
if (frappe.boot.user.can_read.indexOf("GL Entry") == -1) return;
|
||||
|
||||
let accounts = [];
|
||||
if (deep) {
|
||||
// in case of `get_all_nodes`
|
||||
accounts = nodes.reduce((acc, node) => [...acc, ...node.data], []);
|
||||
} else {
|
||||
accounts = nodes;
|
||||
}
|
||||
|
||||
const get_balances = frappe.call({
|
||||
method: 'erpnext.accounts.utils.get_account_balances',
|
||||
args: {
|
||||
accounts: accounts,
|
||||
company: cur_tree.args.company
|
||||
},
|
||||
});
|
||||
|
||||
get_balances.then(r => {
|
||||
if (!r.message || r.message.length == 0) return;
|
||||
|
||||
for (let account of r.message) {
|
||||
|
||||
const node = cur_tree.nodes && cur_tree.nodes[account.value];
|
||||
if (!node || node.is_root) continue;
|
||||
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
const balance = account.balance_in_account_currency || account.balance;
|
||||
const dr_or_cr = balance > 0 ? "Dr": "Cr";
|
||||
const format = (value, currency) => format_currency(Math.abs(value), currency);
|
||||
|
||||
if (account.balance!==undefined) {
|
||||
$('<span class="balance-area pull-right">'
|
||||
+ (account.balance_in_account_currency ?
|
||||
(format(account.balance_in_account_currency, account.account_currency) + " / ") : "")
|
||||
+ format(account.balance, account.company_currency)
|
||||
+ " " + dr_or_cr
|
||||
+ '</span>').insertBefore(node.$ul);
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
add_tree_node: 'erpnext.accounts.utils.add_ac',
|
||||
menu_items:[
|
||||
{
|
||||
@ -122,24 +165,6 @@ frappe.treeview_settings["Account"] = {
|
||||
}
|
||||
}, "add");
|
||||
},
|
||||
onrender: function(node) {
|
||||
if (frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
|
||||
|
||||
// show Dr if positive since balance is calculated as debit - credit else show Cr
|
||||
let balance = node.data.balance_in_account_currency || node.data.balance;
|
||||
let dr_or_cr = balance > 0 ? "Dr": "Cr";
|
||||
|
||||
if (node.data && node.data.balance!==undefined) {
|
||||
$('<span class="balance-area pull-right">'
|
||||
+ (node.data.balance_in_account_currency ?
|
||||
(format_currency(Math.abs(node.data.balance_in_account_currency),
|
||||
node.data.account_currency) + " / ") : "")
|
||||
+ format_currency(Math.abs(node.data.balance), node.data.company_currency)
|
||||
+ " " + dr_or_cr
|
||||
+ '</span>').insertBefore(node.$ul);
|
||||
}
|
||||
}
|
||||
},
|
||||
toolbar: [
|
||||
{
|
||||
label:__("Add Child"),
|
||||
|
@ -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,
|
||||
@ -81,7 +81,7 @@ def add_suffix_if_duplicate(account_name, account_number, accounts):
|
||||
def identify_is_group(child):
|
||||
if child.get("is_group"):
|
||||
is_group = child.get("is_group")
|
||||
elif len(set(child.keys()) - set(["account_type", "root_type", "is_group", "tax_rate", "account_number"])):
|
||||
elif len(set(child.keys()) - set(["account_name", "account_type", "root_type", "is_group", "tax_rate", "account_number"])):
|
||||
is_group = 1
|
||||
else:
|
||||
is_group = 0
|
||||
@ -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",
|
||||
|
@ -10,13 +10,17 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
||||
// make company mandatory
|
||||
frm.set_df_property('company', 'reqd', frm.doc.company ? 0 : 1);
|
||||
frm.set_df_property('import_file_section', 'hidden', frm.doc.company ? 0 : 1);
|
||||
|
||||
if (frm.doc.import_file) {
|
||||
frappe.run_serially([
|
||||
() => generate_tree_preview(frm),
|
||||
() => create_import_button(frm),
|
||||
() => frm.set_df_property('chart_preview', 'hidden', 0)
|
||||
]);
|
||||
}
|
||||
|
||||
frm.set_df_property('chart_preview', 'hidden',
|
||||
$(frm.fields_dict['chart_tree'].wrapper).html()!="" ? 0 : 1);
|
||||
|
||||
// Show import button when file is successfully attached
|
||||
if (frm.page && frm.page.show_import_button) {
|
||||
create_import_button(frm);
|
||||
}
|
||||
},
|
||||
|
||||
download_template: function(frm) {
|
||||
@ -77,9 +81,6 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
||||
if (!frm.doc.import_file) {
|
||||
frm.page.set_indicator("");
|
||||
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper on removing file
|
||||
} else {
|
||||
generate_tree_preview(frm);
|
||||
validate_csv_data(frm);
|
||||
}
|
||||
},
|
||||
|
||||
@ -104,26 +105,9 @@ frappe.ui.form.on('Chart of Accounts Importer', {
|
||||
}
|
||||
});
|
||||
|
||||
var validate_csv_data = function(frm) {
|
||||
frappe.call({
|
||||
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.validate_accounts",
|
||||
args: {file_name: frm.doc.import_file},
|
||||
callback: function(r) {
|
||||
if(r.message && r.message[0]===true) {
|
||||
frm.page["show_import_button"] = true;
|
||||
frm.page["total_accounts"] = r.message[1];
|
||||
frm.trigger("refresh");
|
||||
} else {
|
||||
frm.page.set_indicator(__('Resolve error and upload again.'), 'orange');
|
||||
frappe.throw(__(r.message));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var create_import_button = function(frm) {
|
||||
frm.page.set_primary_action(__("Import"), function () {
|
||||
frappe.call({
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.import_coa",
|
||||
args: {
|
||||
file_name: frm.doc.import_file,
|
||||
@ -132,7 +116,7 @@ var create_import_button = function(frm) {
|
||||
freeze: true,
|
||||
freeze_message: __("Creating Accounts..."),
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if (!r.exc) {
|
||||
clearInterval(frm.page["interval"]);
|
||||
frm.page.set_indicator(__('Import Successful'), 'blue');
|
||||
create_reset_button(frm);
|
||||
@ -150,12 +134,33 @@ var create_reset_button = function(frm) {
|
||||
}).addClass('btn btn-primary');
|
||||
};
|
||||
|
||||
var validate_coa = function(frm) {
|
||||
if (frm.doc.import_file) {
|
||||
let parent = __('All Accounts');
|
||||
return frappe.call({
|
||||
'method': 'erpnext.accounts.doctype.chart_of_accounts_importer.chart_of_accounts_importer.get_coa',
|
||||
'args': {
|
||||
file_name: frm.doc.import_file,
|
||||
parent: parent,
|
||||
doctype: 'Chart of Accounts Importer',
|
||||
file_type: frm.doc.file_type,
|
||||
for_validate: 1
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message['show_import_button']) {
|
||||
frm.page['show_import_button'] = Boolean(r.message['show_import_button']);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
var generate_tree_preview = function(frm) {
|
||||
let parent = __('All Accounts');
|
||||
$(frm.fields_dict['chart_tree'].wrapper).empty(); // empty wrapper to load new data
|
||||
|
||||
// generate tree structure based on the csv data
|
||||
new frappe.ui.Tree({
|
||||
return new frappe.ui.Tree({
|
||||
parent: $(frm.fields_dict['chart_tree'].wrapper),
|
||||
label: parent,
|
||||
expandable: true,
|
||||
|
@ -26,7 +26,18 @@ from erpnext.accounts.doctype.account.chart_of_accounts.chart_of_accounts import
|
||||
|
||||
class ChartofAccountsImporter(Document):
|
||||
def validate(self):
|
||||
validate_accounts(self.import_file)
|
||||
if self.import_file:
|
||||
get_coa('Chart of Accounts Importer', 'All Accounts', file_name=self.import_file, for_validate=1)
|
||||
|
||||
def validate_columns(data):
|
||||
if not data:
|
||||
frappe.throw(_('No data found. Seems like you uploaded a blank file'))
|
||||
|
||||
no_of_columns = max([len(d) for d in data])
|
||||
|
||||
if no_of_columns > 7:
|
||||
frappe.throw(_('More columns found than expected. Please compare the uploaded file with standard template'),
|
||||
title=(_("Wrong Template")))
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_company(company):
|
||||
@ -56,8 +67,9 @@ def import_coa(file_name, company):
|
||||
else:
|
||||
data = generate_data_from_excel(file_doc, extension)
|
||||
|
||||
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)
|
||||
@ -120,7 +132,7 @@ def generate_data_from_excel(file_doc, extension, as_dict=False):
|
||||
return data
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_coa(doctype, parent, is_root=False, file_name=None):
|
||||
def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
|
||||
''' called by tree view (to fetch node's children) '''
|
||||
|
||||
file_doc, extension = get_file(file_name)
|
||||
@ -131,13 +143,21 @@ def get_coa(doctype, parent, is_root=False, file_name=None):
|
||||
else:
|
||||
data = generate_data_from_excel(file_doc, extension)
|
||||
|
||||
forest = build_forest(data)
|
||||
accounts = build_tree_from_json("", chart_data=forest) # returns alist of dict in a tree render-able form
|
||||
validate_columns(data)
|
||||
validate_accounts(file_doc, extension)
|
||||
|
||||
# filter out to show data for the selected node only
|
||||
accounts = [d for d in accounts if d['parent_account']==parent]
|
||||
if not for_validate:
|
||||
forest = build_forest(data)
|
||||
accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form
|
||||
|
||||
return accounts
|
||||
# filter out to show data for the selected node only
|
||||
accounts = [d for d in accounts if d['parent_account']==parent]
|
||||
|
||||
return accounts
|
||||
else:
|
||||
return {
|
||||
'show_import_button': 1
|
||||
}
|
||||
|
||||
def build_forest(data):
|
||||
'''
|
||||
@ -192,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
|
||||
@ -294,10 +317,7 @@ def get_sample_template(writer):
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def validate_accounts(file_name):
|
||||
|
||||
file_doc, extension = get_file(file_name)
|
||||
|
||||
def validate_accounts(file_doc, extension):
|
||||
if extension == 'csv':
|
||||
accounts = generate_data_from_csv(file_doc, as_dict=True)
|
||||
else:
|
||||
@ -316,15 +336,10 @@ def validate_accounts(file_name):
|
||||
|
||||
validate_root(accounts_dict)
|
||||
|
||||
validate_account_types(accounts_dict)
|
||||
|
||||
return [True, len(accounts)]
|
||||
|
||||
def validate_root(accounts):
|
||||
roots = [accounts[d] for d in accounts if not accounts[d].get('parent_account')]
|
||||
if len(roots) < 4:
|
||||
frappe.throw(_("Number of root accounts cannot be less than 4"))
|
||||
|
||||
error_messages = []
|
||||
|
||||
for account in roots:
|
||||
@ -333,9 +348,19 @@ def validate_root(accounts):
|
||||
elif account.get("root_type") not in get_root_types() and account.get("account_name"):
|
||||
error_messages.append(_("Root Type for {0} must be one of the Asset, Liability, Income, Expense and Equity").format(account.get("account_name")))
|
||||
|
||||
validate_missing_roots(roots)
|
||||
|
||||
if error_messages:
|
||||
frappe.throw("<br>".join(error_messages))
|
||||
|
||||
def validate_missing_roots(roots):
|
||||
root_types_added = set(d.get('root_type') for d in roots)
|
||||
|
||||
missing = list(set(get_root_types()) - root_types_added)
|
||||
|
||||
if missing:
|
||||
frappe.throw(_("Please add Root Account for - {0}").format(' , '.join(missing)))
|
||||
|
||||
def get_root_types():
|
||||
return ('Asset', 'Liability', 'Expense', 'Income', 'Equity')
|
||||
|
||||
@ -361,23 +386,6 @@ def get_mandatory_account_types():
|
||||
{'account_type': 'Stock', 'root_type': 'Asset'}
|
||||
]
|
||||
|
||||
|
||||
def validate_account_types(accounts):
|
||||
account_types_for_ledger = ["Cost of Goods Sold", "Depreciation", "Fixed Asset", "Payable", "Receivable", "Stock Adjustment"]
|
||||
account_types = [accounts[d]["account_type"] for d in accounts if not accounts[d]['is_group'] == 1]
|
||||
|
||||
missing = list(set(account_types_for_ledger) - set(account_types))
|
||||
if missing:
|
||||
frappe.throw(_("Please identify/create Account (Ledger) for type - {0}").format(' , '.join(missing)))
|
||||
|
||||
account_types_for_group = ["Bank", "Cash", "Stock"]
|
||||
# fix logic bug
|
||||
account_groups = [accounts[d]["account_type"] for d in accounts if accounts[d]['is_group'] == 1]
|
||||
|
||||
missing = list(set(account_types_for_group) - set(account_groups))
|
||||
if missing:
|
||||
frappe.throw(_("Please identify/create Account (Group) for type - {0}").format(' , '.join(missing)))
|
||||
|
||||
def unset_existing_data(company):
|
||||
linked = frappe.db.sql('''select fieldname from tabDocField
|
||||
where fieldtype="Link" and options="Account" and parent="Company"''', as_dict=True)
|
||||
|
@ -13,7 +13,7 @@ def get_data():
|
||||
},
|
||||
{
|
||||
'label': _('References'),
|
||||
'items': ['Period Closing Voucher', 'Tax Withholding Category']
|
||||
'items': ['Period Closing Voucher']
|
||||
},
|
||||
{
|
||||
'label': _('Target Details'),
|
||||
|
@ -1,63 +1,33 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2014-10-02 13:35:44.155278",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2014-10-02 13:35:44.155278",
|
||||
"doctype": "DocType",
|
||||
"document_type": "Setup",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
}
|
||||
],
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2016-07-11 03:28:00.505946",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year Company",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 0,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_seen": 0
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-28 18:01:53.495929",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Fiscal Year Company",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -58,7 +58,8 @@ class GLEntry(Document):
|
||||
|
||||
# Update outstanding amt on against voucher
|
||||
if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees']
|
||||
and self.against_voucher and self.flags.update_outstanding == 'Yes'):
|
||||
and self.against_voucher and self.flags.update_outstanding == 'Yes'
|
||||
and not frappe.flags.is_reverse_depr_entry):
|
||||
update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
|
||||
self.against_voucher)
|
||||
|
||||
|
@ -13,10 +13,12 @@
|
||||
"voucher_type",
|
||||
"naming_series",
|
||||
"finance_book",
|
||||
"tax_withholding_category",
|
||||
"column_break1",
|
||||
"from_template",
|
||||
"company",
|
||||
"posting_date",
|
||||
"apply_tds",
|
||||
"2_add_edit_gl_entries",
|
||||
"accounts",
|
||||
"section_break99",
|
||||
@ -498,16 +500,32 @@
|
||||
"options": "Journal Entry Template",
|
||||
"print_hide": 1,
|
||||
"report_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.apply_tds",
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
"label": "Tax Withholding Category",
|
||||
"mandatory_depends_on": "eval:doc.apply_tds",
|
||||
"options": "Tax Withholding Category"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "eval:['Credit Note', 'Debit Note'].includes(doc.voucher_type)",
|
||||
"fieldname": "apply_tds",
|
||||
"fieldtype": "Check",
|
||||
"label": "Apply Tax Withholding Amount "
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 176,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-30 13:56:01.121995",
|
||||
"modified": "2021-09-09 15:31:14.484029",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Journal Entry",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -15,6 +15,9 @@ from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
|
||||
from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import (
|
||||
get_party_account_based_on_invoice_discounting,
|
||||
)
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_party_tax_withholding_details,
|
||||
)
|
||||
from erpnext.accounts.party import get_party_account
|
||||
from erpnext.accounts.utils import (
|
||||
check_if_stock_and_account_balance_synced,
|
||||
@ -55,9 +58,13 @@ class JournalEntry(AccountsController):
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
self.validate_against_jv()
|
||||
if not frappe.flags.is_reverse_depr_entry:
|
||||
self.validate_against_jv()
|
||||
self.validate_stock_accounts()
|
||||
|
||||
self.validate_reference_doc()
|
||||
self.set_against_account()
|
||||
if self.docstatus == 0:
|
||||
self.set_against_account()
|
||||
self.create_remarks()
|
||||
self.set_print_format_fields()
|
||||
self.validate_expense_claim()
|
||||
@ -65,7 +72,10 @@ class JournalEntry(AccountsController):
|
||||
self.validate_empty_accounts_table()
|
||||
self.set_account_and_party_balance()
|
||||
self.validate_inter_company_accounts()
|
||||
self.validate_stock_accounts()
|
||||
|
||||
if self.docstatus == 0:
|
||||
self.apply_tax_withholding()
|
||||
|
||||
if not self.title:
|
||||
self.title = self.get_title()
|
||||
|
||||
@ -139,6 +149,72 @@ class JournalEntry(AccountsController):
|
||||
frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
|
||||
.format(account), StockAccountInvalidTransaction)
|
||||
|
||||
def apply_tax_withholding(self):
|
||||
from erpnext.accounts.report.general_ledger.general_ledger import get_account_type_map
|
||||
|
||||
if not self.apply_tds or self.voucher_type not in ('Debit Note', 'Credit Note'):
|
||||
return
|
||||
|
||||
parties = [d.party for d in self.get('accounts') if d.party]
|
||||
parties = list(set(parties))
|
||||
|
||||
if len(parties) > 1:
|
||||
frappe.throw(_("Cannot apply TDS against multiple parties in one entry"))
|
||||
|
||||
account_type_map = get_account_type_map(self.company)
|
||||
party_type = 'supplier' if self.voucher_type == 'Credit Note' else 'customer'
|
||||
doctype = 'Purchase Invoice' if self.voucher_type == 'Credit Note' else 'Sales Invoice'
|
||||
debit_or_credit = 'debit_in_account_currency' if self.voucher_type == 'Credit Note' else 'credit_in_account_currency'
|
||||
rev_debit_or_credit = 'credit_in_account_currency' if debit_or_credit == 'debit_in_account_currency' else 'debit_in_account_currency'
|
||||
|
||||
party_account = get_party_account(party_type.title(), parties[0], self.company)
|
||||
|
||||
net_total = sum(d.get(debit_or_credit) for d in self.get('accounts') if account_type_map.get(d.account)
|
||||
not in ('Tax', 'Chargeable'))
|
||||
|
||||
party_amount = sum(d.get(rev_debit_or_credit) for d in self.get('accounts') if d.account == party_account)
|
||||
|
||||
inv = frappe._dict({
|
||||
party_type: parties[0],
|
||||
'doctype': doctype,
|
||||
'company': self.company,
|
||||
'posting_date': self.posting_date,
|
||||
'net_total': net_total
|
||||
})
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(inv, self.tax_withholding_category)
|
||||
|
||||
if not tax_withholding_details:
|
||||
return
|
||||
|
||||
accounts = []
|
||||
for d in self.get('accounts'):
|
||||
if d.get('account') == tax_withholding_details.get("account_head"):
|
||||
d.update({
|
||||
'account': tax_withholding_details.get("account_head"),
|
||||
debit_or_credit: tax_withholding_details.get('tax_amount')
|
||||
})
|
||||
|
||||
accounts.append(d.get('account'))
|
||||
|
||||
if d.get('account') == party_account:
|
||||
d.update({
|
||||
rev_debit_or_credit: party_amount - tax_withholding_details.get('tax_amount')
|
||||
})
|
||||
|
||||
if not accounts or tax_withholding_details.get("account_head") not in accounts:
|
||||
self.append("accounts", {
|
||||
'account': tax_withholding_details.get("account_head"),
|
||||
rev_debit_or_credit: tax_withholding_details.get('tax_amount'),
|
||||
'against_account': parties[0]
|
||||
})
|
||||
|
||||
to_remove = [d for d in self.get('accounts')
|
||||
if not d.get(rev_debit_or_credit) and d.account == tax_withholding_details.get("account_head")]
|
||||
|
||||
for d in to_remove:
|
||||
self.remove(d)
|
||||
|
||||
def update_inter_company_jv(self):
|
||||
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
|
||||
frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
|
||||
|
@ -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
|
||||
|
@ -27,10 +27,12 @@
|
||||
"payment_accounts_section",
|
||||
"party_balance",
|
||||
"paid_from",
|
||||
"paid_from_account_type",
|
||||
"paid_from_account_currency",
|
||||
"paid_from_account_balance",
|
||||
"column_break_18",
|
||||
"paid_to",
|
||||
"paid_to_account_type",
|
||||
"paid_to_account_currency",
|
||||
"paid_to_account_balance",
|
||||
"payment_amounts_section",
|
||||
@ -440,7 +442,8 @@
|
||||
"depends_on": "eval:(doc.paid_from && doc.paid_to)",
|
||||
"fieldname": "reference_no",
|
||||
"fieldtype": "Data",
|
||||
"label": "Cheque/Reference No"
|
||||
"label": "Cheque/Reference No",
|
||||
"mandatory_depends_on": "eval:(doc.paid_from_account_type == 'Bank' || doc.paid_to_account_type == 'Bank')"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_23",
|
||||
@ -452,6 +455,7 @@
|
||||
"fieldname": "reference_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Cheque/Reference Date",
|
||||
"mandatory_depends_on": "eval:(doc.paid_from_account_type == 'Bank' || doc.paid_to_account_type == 'Bank')",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@ -707,15 +711,30 @@
|
||||
"label": "Received Amount After Tax (Company Currency)",
|
||||
"options": "Company:company:default_currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fetch_from": "paid_from.account_type",
|
||||
"fieldname": "paid_from_account_type",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Paid From Account Type"
|
||||
},
|
||||
{
|
||||
"fetch_from": "paid_to.account_type",
|
||||
"fieldname": "paid_to_account_type",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Paid To Account Type"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-07-09 08:58:15.008761",
|
||||
"modified": "2021-10-22 17:50:24.632806",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -389,7 +389,10 @@ class PaymentEntry(AccountsController):
|
||||
invoice_paid_amount_map[invoice_key]['outstanding'] = term.outstanding
|
||||
invoice_paid_amount_map[invoice_key]['discounted_amt'] = ref.total_amount * (term.discount / 100)
|
||||
|
||||
for key, allocated_amount in iteritems(invoice_payment_amount_map):
|
||||
for idx, (key, allocated_amount) in enumerate(iteritems(invoice_payment_amount_map), 1):
|
||||
if not invoice_paid_amount_map.get(key):
|
||||
frappe.throw(_('Payment term {0} not used in {1}').format(key[0], key[1]))
|
||||
|
||||
outstanding = flt(invoice_paid_amount_map.get(key, {}).get('outstanding'))
|
||||
discounted_amt = flt(invoice_paid_amount_map.get(key, {}).get('discounted_amt'))
|
||||
|
||||
@ -404,7 +407,7 @@ class PaymentEntry(AccountsController):
|
||||
(allocated_amount - discounted_amt, discounted_amt, allocated_amount, key[1], key[0]))
|
||||
else:
|
||||
if allocated_amount > outstanding:
|
||||
frappe.throw(_('Cannot allocate more than {0} against payment term {1}').format(outstanding, key[0]))
|
||||
frappe.throw(_('Row #{0}: Cannot allocate more than {1} against payment term {2}').format(idx, outstanding, key[0]))
|
||||
|
||||
if allocated_amount and outstanding:
|
||||
frappe.db.sql("""
|
||||
@ -502,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):
|
||||
@ -709,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),
|
||||
@ -1045,12 +1053,6 @@ def get_outstanding_reference_documents(args):
|
||||
party_account_currency = get_account_currency(args.get("party_account"))
|
||||
company_currency = frappe.get_cached_value('Company', args.get("company"), "default_currency")
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
negative_outstanding_invoices = []
|
||||
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), args.get("party"),
|
||||
args.get("party_account"), args.get("company"), party_account_currency, company_currency)
|
||||
|
||||
# Get positive outstanding sales /purchase invoices/ Fees
|
||||
condition = ""
|
||||
if args.get("voucher_type") and args.get("voucher_no"):
|
||||
@ -1097,6 +1099,12 @@ def get_outstanding_reference_documents(args):
|
||||
orders_to_be_billed = get_orders_to_be_billed(args.get("posting_date"),args.get("party_type"),
|
||||
args.get("party"), args.get("company"), party_account_currency, company_currency, filters=args)
|
||||
|
||||
# Get negative outstanding sales /purchase invoices
|
||||
negative_outstanding_invoices = []
|
||||
if args.get("party_type") not in ["Student", "Employee"] and not args.get("voucher_no"):
|
||||
negative_outstanding_invoices = get_negative_outstanding_invoices(args.get("party_type"), args.get("party"),
|
||||
args.get("party_account"), party_account_currency, company_currency, condition=condition)
|
||||
|
||||
data = negative_outstanding_invoices + outstanding_invoices + orders_to_be_billed
|
||||
|
||||
if not data:
|
||||
@ -1129,22 +1137,26 @@ def split_invoices_based_on_payment_terms(outstanding_invoices):
|
||||
'invoice_amount': flt(d.invoice_amount),
|
||||
'outstanding_amount': flt(d.outstanding_amount),
|
||||
'payment_amount': payment_term.payment_amount,
|
||||
'payment_term': payment_term.payment_term,
|
||||
'allocated_amount': payment_term.outstanding
|
||||
'payment_term': payment_term.payment_term
|
||||
}))
|
||||
|
||||
outstanding_invoices_after_split = []
|
||||
if invoice_ref_based_on_payment_terms:
|
||||
for idx, ref in invoice_ref_based_on_payment_terms.items():
|
||||
voucher_no = outstanding_invoices[idx]['voucher_no']
|
||||
voucher_type = outstanding_invoices[idx]['voucher_type']
|
||||
voucher_no = ref[0]['voucher_no']
|
||||
voucher_type = ref[0]['voucher_type']
|
||||
|
||||
frappe.msgprint(_("Spliting {} {} into {} rows as per payment terms").format(
|
||||
frappe.msgprint(_("Spliting {} {} into {} row(s) as per Payment Terms").format(
|
||||
voucher_type, voucher_no, len(ref)), alert=True)
|
||||
|
||||
outstanding_invoices.pop(idx - 1)
|
||||
outstanding_invoices += invoice_ref_based_on_payment_terms[idx]
|
||||
outstanding_invoices_after_split += invoice_ref_based_on_payment_terms[idx]
|
||||
|
||||
return outstanding_invoices
|
||||
existing_row = list(filter(lambda x: x.get('voucher_no') == voucher_no, outstanding_invoices))
|
||||
index = outstanding_invoices.index(existing_row[0])
|
||||
outstanding_invoices.pop(index)
|
||||
|
||||
outstanding_invoices_after_split += outstanding_invoices
|
||||
return outstanding_invoices_after_split
|
||||
|
||||
def get_orders_to_be_billed(posting_date, party_type, party,
|
||||
company, party_account_currency, company_currency, cost_center=None, filters=None):
|
||||
@ -1211,7 +1223,7 @@ def get_orders_to_be_billed(posting_date, party_type, party,
|
||||
return order_list
|
||||
|
||||
def get_negative_outstanding_invoices(party_type, party, party_account,
|
||||
company, party_account_currency, company_currency, cost_center=None):
|
||||
party_account_currency, company_currency, cost_center=None, condition=None):
|
||||
voucher_type = "Sales Invoice" if party_type == "Customer" else "Purchase Invoice"
|
||||
supplier_condition = ""
|
||||
if voucher_type == "Purchase Invoice":
|
||||
@ -1233,19 +1245,21 @@ def get_negative_outstanding_invoices(party_type, party, party_account,
|
||||
`tab{voucher_type}`
|
||||
where
|
||||
{party_type} = %s and {party_account} = %s and docstatus = 1 and
|
||||
company = %s and outstanding_amount < 0
|
||||
outstanding_amount < 0
|
||||
{supplier_condition}
|
||||
{condition}
|
||||
order by
|
||||
posting_date, name
|
||||
""".format(**{
|
||||
"supplier_condition": supplier_condition,
|
||||
"condition": condition,
|
||||
"rounded_total_field": rounded_total_field,
|
||||
"grand_total_field": grand_total_field,
|
||||
"voucher_type": voucher_type,
|
||||
"party_type": scrub(party_type),
|
||||
"party_account": "debit_to" if party_type == "Customer" else "credit_to",
|
||||
"cost_center": cost_center
|
||||
}), (party, party_account, company), as_dict=True)
|
||||
}), (party, party_account), as_dict=True)
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -93,6 +93,7 @@
|
||||
"options": "Payment Term"
|
||||
},
|
||||
{
|
||||
"depends_on": "exchange_gain_loss",
|
||||
"fieldname": "exchange_gain_loss",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Exchange Gain/Loss",
|
||||
@ -103,7 +104,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-21 13:30:11.605388",
|
||||
"modified": "2021-09-26 17:06:55.597389",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Entry Reference",
|
||||
|
@ -10,6 +10,9 @@ frappe.ui.form.on('Payment Order', {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frm.set_df_property('references', 'cannot_add_rows', true);
|
||||
frm.set_df_property('references', 'cannot_delete_rows', true);
|
||||
},
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.docstatus == 0) {
|
||||
|
@ -4,9 +4,14 @@
|
||||
frappe.provide("erpnext.accounts");
|
||||
erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationController extends frappe.ui.form.Controller {
|
||||
onload() {
|
||||
var me = this;
|
||||
const default_company = frappe.defaults.get_default('company');
|
||||
this.frm.set_value('company', default_company);
|
||||
|
||||
this.frm.set_query("party_type", function() {
|
||||
this.frm.set_value('party_type', '');
|
||||
this.frm.set_value('party', '');
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
|
||||
this.frm.set_query("party_type", () => {
|
||||
return {
|
||||
"filters": {
|
||||
"name": ["in", Object.keys(frappe.boot.party_account_types)],
|
||||
@ -14,133 +19,149 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}
|
||||
});
|
||||
|
||||
this.frm.set_query('receivable_payable_account', function() {
|
||||
check_mandatory(me.frm);
|
||||
this.frm.set_query('receivable_payable_account', () => {
|
||||
return {
|
||||
filters: {
|
||||
"company": me.frm.doc.company,
|
||||
"company": this.frm.doc.company,
|
||||
"is_group": 0,
|
||||
"account_type": frappe.boot.party_account_types[me.frm.doc.party_type]
|
||||
"account_type": frappe.boot.party_account_types[this.frm.doc.party_type]
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_query('bank_cash_account', function() {
|
||||
check_mandatory(me.frm, true);
|
||||
this.frm.set_query('bank_cash_account', () => {
|
||||
return {
|
||||
filters:[
|
||||
['Account', 'company', '=', me.frm.doc.company],
|
||||
['Account', 'company', '=', this.frm.doc.company],
|
||||
['Account', 'is_group', '=', 0],
|
||||
['Account', 'account_type', 'in', ['Bank', 'Cash']]
|
||||
]
|
||||
};
|
||||
});
|
||||
|
||||
this.frm.set_value('party_type', '');
|
||||
this.frm.set_value('party', '');
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
|
||||
var check_mandatory = (frm, only_company=false) => {
|
||||
var title = __("Mandatory");
|
||||
if (only_company && !frm.doc.company) {
|
||||
frappe.throw({message: __("Please Select a Company First"), title: title});
|
||||
} else if (!frm.doc.company || !frm.doc.party_type) {
|
||||
frappe.throw({message: __("Please Select Both Company and Party Type First"), title: title});
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
refresh() {
|
||||
this.frm.disable_save();
|
||||
|
||||
this.frm.set_df_property('invoices', 'cannot_delete_rows', true);
|
||||
this.frm.set_df_property('payments', 'cannot_delete_rows', true);
|
||||
this.frm.set_df_property('allocation', 'cannot_delete_rows', true);
|
||||
|
||||
this.frm.set_df_property('invoices', 'cannot_add_rows', true);
|
||||
this.frm.set_df_property('payments', 'cannot_add_rows', true);
|
||||
this.frm.set_df_property('allocation', 'cannot_add_rows', true);
|
||||
|
||||
|
||||
if (this.frm.doc.receivable_payable_account) {
|
||||
this.frm.add_custom_button(__('Get Unreconciled Entries'), () =>
|
||||
this.frm.trigger("get_unreconciled_entries")
|
||||
);
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'primary');
|
||||
}
|
||||
if (this.frm.doc.invoices.length && this.frm.doc.payments.length) {
|
||||
this.frm.add_custom_button(__('Allocate'), () =>
|
||||
this.frm.trigger("allocate")
|
||||
);
|
||||
this.frm.change_custom_button_type('Allocate', null, 'primary');
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
|
||||
}
|
||||
if (this.frm.doc.allocation.length) {
|
||||
this.frm.add_custom_button(__('Reconcile'), () =>
|
||||
this.frm.trigger("reconcile")
|
||||
);
|
||||
this.frm.change_custom_button_type('Reconcile', null, 'primary');
|
||||
this.frm.change_custom_button_type('Get Unreconciled Entries', null, 'default');
|
||||
this.frm.change_custom_button_type('Allocate', null, 'default');
|
||||
}
|
||||
}
|
||||
|
||||
company() {
|
||||
var me = this;
|
||||
this.frm.set_value('party', '');
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
me.frm.clear_table("allocation");
|
||||
me.frm.clear_table("invoices");
|
||||
me.frm.clear_table("payments");
|
||||
me.frm.refresh_fields();
|
||||
me.frm.trigger('party');
|
||||
}
|
||||
|
||||
party_type() {
|
||||
this.frm.set_value('party', '');
|
||||
}
|
||||
|
||||
party() {
|
||||
var me = this;
|
||||
if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) {
|
||||
this.frm.set_value('receivable_payable_account', '');
|
||||
this.frm.trigger("clear_child_tables");
|
||||
|
||||
if (!this.frm.doc.receivable_payable_account && this.frm.doc.party_type && this.frm.doc.party) {
|
||||
return frappe.call({
|
||||
method: "erpnext.accounts.party.get_party_account",
|
||||
args: {
|
||||
company: me.frm.doc.company,
|
||||
party_type: me.frm.doc.party_type,
|
||||
party: me.frm.doc.party
|
||||
company: this.frm.doc.company,
|
||||
party_type: this.frm.doc.party_type,
|
||||
party: this.frm.doc.party
|
||||
},
|
||||
callback: function(r) {
|
||||
callback: (r) => {
|
||||
if (!r.exc && r.message) {
|
||||
me.frm.set_value("receivable_payable_account", r.message);
|
||||
this.frm.set_value("receivable_payable_account", r.message);
|
||||
}
|
||||
me.frm.refresh();
|
||||
this.frm.refresh();
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
receivable_payable_account() {
|
||||
this.frm.trigger("clear_child_tables");
|
||||
this.frm.refresh();
|
||||
}
|
||||
|
||||
clear_child_tables() {
|
||||
this.frm.clear_table("invoices");
|
||||
this.frm.clear_table("payments");
|
||||
this.frm.clear_table("allocation");
|
||||
this.frm.refresh_fields();
|
||||
}
|
||||
|
||||
get_unreconciled_entries() {
|
||||
var me = this;
|
||||
this.frm.clear_table("allocation");
|
||||
return this.frm.call({
|
||||
doc: me.frm.doc,
|
||||
doc: this.frm.doc,
|
||||
method: 'get_unreconciled_entries',
|
||||
callback: function(r, rt) {
|
||||
if (!(me.frm.doc.payments.length || me.frm.doc.invoices.length)) {
|
||||
frappe.throw({message: __("No invoice and payment records found for this party")});
|
||||
callback: () => {
|
||||
if (!(this.frm.doc.payments.length || this.frm.doc.invoices.length)) {
|
||||
frappe.throw({message: __("No Unreconciled Invoices and Payments found for this party and account")});
|
||||
} else if (!(this.frm.doc.invoices.length)) {
|
||||
frappe.throw({message: __("No Outstanding Invoices found for this party")});
|
||||
} else if (!(this.frm.doc.payments.length)) {
|
||||
frappe.throw({message: __("No Unreconciled Payments found for this party")});
|
||||
}
|
||||
me.frm.refresh();
|
||||
this.frm.refresh();
|
||||
}
|
||||
});
|
||||
|
||||
}
|
||||
|
||||
allocate() {
|
||||
var me = this;
|
||||
let payments = me.frm.fields_dict.payments.grid.get_selected_children();
|
||||
let payments = this.frm.fields_dict.payments.grid.get_selected_children();
|
||||
if (!(payments.length)) {
|
||||
payments = me.frm.doc.payments;
|
||||
payments = this.frm.doc.payments;
|
||||
}
|
||||
let invoices = me.frm.fields_dict.invoices.grid.get_selected_children();
|
||||
let invoices = this.frm.fields_dict.invoices.grid.get_selected_children();
|
||||
if (!(invoices.length)) {
|
||||
invoices = me.frm.doc.invoices;
|
||||
invoices = this.frm.doc.invoices;
|
||||
}
|
||||
return me.frm.call({
|
||||
doc: me.frm.doc,
|
||||
return this.frm.call({
|
||||
doc: this.frm.doc,
|
||||
method: 'allocate_entries',
|
||||
args: {
|
||||
payments: payments,
|
||||
invoices: invoices
|
||||
},
|
||||
callback: function() {
|
||||
me.frm.refresh();
|
||||
callback: () => {
|
||||
this.frm.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
reconcile() {
|
||||
var me = this;
|
||||
var show_dialog = me.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
|
||||
var show_dialog = this.frm.doc.allocation.filter(d => d.difference_amount && !d.difference_account);
|
||||
|
||||
if (show_dialog && show_dialog.length) {
|
||||
|
||||
@ -172,10 +193,10 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
label: __("Difference Account"),
|
||||
fieldname: 'difference_account',
|
||||
reqd: 1,
|
||||
get_query: function() {
|
||||
get_query: () => {
|
||||
return {
|
||||
filters: {
|
||||
company: me.frm.doc.company,
|
||||
company: this.frm.doc.company,
|
||||
is_group: 0
|
||||
}
|
||||
}
|
||||
@ -189,7 +210,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}]
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
primary_action: () => {
|
||||
const args = dialog.get_values()["allocation"];
|
||||
|
||||
args.forEach(d => {
|
||||
@ -197,7 +218,7 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
"difference_account", d.difference_account);
|
||||
});
|
||||
|
||||
me.reconcile_payment_entries();
|
||||
this.reconcile_payment_entries();
|
||||
dialog.hide();
|
||||
},
|
||||
primary_action_label: __('Reconcile Entries')
|
||||
@ -223,15 +244,12 @@ erpnext.accounts.PaymentReconciliationController = class PaymentReconciliationCo
|
||||
}
|
||||
|
||||
reconcile_payment_entries() {
|
||||
var me = this;
|
||||
|
||||
return this.frm.call({
|
||||
doc: me.frm.doc,
|
||||
doc: this.frm.doc,
|
||||
method: 'reconcile',
|
||||
callback: function(r, rt) {
|
||||
me.frm.clear_table("allocation");
|
||||
me.frm.refresh_fields();
|
||||
me.frm.refresh();
|
||||
callback: () => {
|
||||
this.frm.clear_table("allocation");
|
||||
this.frm.refresh();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -12,15 +12,16 @@
|
||||
"receivable_payable_account",
|
||||
"col_break1",
|
||||
"from_invoice_date",
|
||||
"to_invoice_date",
|
||||
"minimum_invoice_amount",
|
||||
"maximum_invoice_amount",
|
||||
"invoice_limit",
|
||||
"column_break_13",
|
||||
"from_payment_date",
|
||||
"to_payment_date",
|
||||
"minimum_invoice_amount",
|
||||
"minimum_payment_amount",
|
||||
"column_break_11",
|
||||
"to_invoice_date",
|
||||
"to_payment_date",
|
||||
"maximum_invoice_amount",
|
||||
"maximum_payment_amount",
|
||||
"column_break_13",
|
||||
"invoice_limit",
|
||||
"payment_limit",
|
||||
"bank_cash_account",
|
||||
"sec_break1",
|
||||
@ -79,6 +80,7 @@
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:(doc.payments).length || (doc.invoices).length",
|
||||
"description": "If you need to reconcile particular transactions against each other, then please select accordingly. If not, all the transactions will be allocated in FIFO order.",
|
||||
"fieldname": "sec_break1",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Unreconciled Entries"
|
||||
@ -163,6 +165,7 @@
|
||||
"label": "Maximum Payment Amount"
|
||||
},
|
||||
{
|
||||
"description": "System will fetch all the entries if limit value is zero.",
|
||||
"fieldname": "payment_limit",
|
||||
"fieldtype": "Int",
|
||||
"label": "Payment Limit"
|
||||
@ -171,13 +174,17 @@
|
||||
"fieldname": "maximum_invoice_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Maximum Invoice Amount"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_11",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"hide_toolbar": 1,
|
||||
"icon": "icon-resize-horizontal",
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-30 13:05:51.977861",
|
||||
"modified": "2021-10-04 20:27:11.114194",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation",
|
||||
|
@ -7,14 +7,15 @@
|
||||
"field_order": [
|
||||
"reference_type",
|
||||
"reference_name",
|
||||
"reference_row",
|
||||
"column_break_3",
|
||||
"invoice_type",
|
||||
"invoice_number",
|
||||
"section_break_6",
|
||||
"allocated_amount",
|
||||
"unreconciled_amount",
|
||||
"amount",
|
||||
"column_break_8",
|
||||
"amount",
|
||||
"is_advance",
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
@ -121,11 +122,18 @@
|
||||
"label": "Amount",
|
||||
"options": "Currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Row",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-30 10:58:42.665107",
|
||||
"modified": "2021-10-06 11:48:59.616562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
|
@ -180,8 +180,7 @@
|
||||
"fieldname": "pos_transactions",
|
||||
"fieldtype": "Table",
|
||||
"label": "POS Transactions",
|
||||
"options": "POS Invoice Reference",
|
||||
"reqd": 1
|
||||
"options": "POS Invoice Reference"
|
||||
},
|
||||
{
|
||||
"fieldname": "pos_opening_entry",
|
||||
@ -229,7 +228,7 @@
|
||||
"link_fieldname": "pos_closing_entry"
|
||||
}
|
||||
],
|
||||
"modified": "2021-05-05 16:59:49.723261",
|
||||
"modified": "2021-10-20 16:19:25.340565",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry",
|
||||
|
@ -40,6 +40,7 @@ class POSInvoice(SalesInvoice):
|
||||
self.validate_change_amount()
|
||||
self.validate_change_account()
|
||||
self.validate_item_cost_centers()
|
||||
self.validate_warehouse()
|
||||
self.validate_serialised_or_batched_item()
|
||||
self.validate_stock_availablility()
|
||||
self.validate_return_items_qty()
|
||||
|
@ -4,7 +4,7 @@
|
||||
frappe.ui.form.on('POS Invoice Merge Log', {
|
||||
setup: function(frm) {
|
||||
frm.set_query("pos_invoice", "pos_invoices", doc => {
|
||||
return{
|
||||
return {
|
||||
filters: {
|
||||
'docstatus': 1,
|
||||
'customer': doc.customer,
|
||||
@ -12,5 +12,10 @@ frappe.ui.form.on('POS Invoice Merge Log', {
|
||||
}
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
merge_invoices_based_on: function(frm) {
|
||||
frm.set_value('customer', '');
|
||||
frm.set_value('customer_group', '');
|
||||
}
|
||||
});
|
||||
|
@ -6,9 +6,11 @@
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"posting_date",
|
||||
"customer",
|
||||
"merge_invoices_based_on",
|
||||
"column_break_3",
|
||||
"pos_closing_entry",
|
||||
"customer",
|
||||
"customer_group",
|
||||
"section_break_3",
|
||||
"pos_invoices",
|
||||
"references_section",
|
||||
@ -88,12 +90,27 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "POS Closing Entry",
|
||||
"options": "POS Closing Entry"
|
||||
},
|
||||
{
|
||||
"fieldname": "merge_invoices_based_on",
|
||||
"fieldtype": "Select",
|
||||
"label": "Merge Invoices Based On",
|
||||
"options": "Customer\nCustomer Group",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
|
||||
"fieldname": "customer_group",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer Group",
|
||||
"mandatory_depends_on": "eval:doc.merge_invoices_based_on == 'Customer Group'",
|
||||
"options": "Customer Group"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-01 11:53:57.267579",
|
||||
"modified": "2021-09-14 11:17:19.001142",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Invoice Merge Log",
|
||||
|
@ -23,6 +23,9 @@ class POSInvoiceMergeLog(Document):
|
||||
self.validate_pos_invoice_status()
|
||||
|
||||
def validate_customer(self):
|
||||
if self.merge_invoices_based_on == 'Customer Group':
|
||||
return
|
||||
|
||||
for d in self.pos_invoices:
|
||||
if d.customer != self.customer:
|
||||
frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer))
|
||||
@ -111,6 +114,8 @@ class POSInvoiceMergeLog(Document):
|
||||
def merge_pos_invoice_into(self, invoice, data):
|
||||
items, payments, taxes = [], [], []
|
||||
loyalty_amount_sum, loyalty_points_sum = 0, 0
|
||||
rounding_adjustment, base_rounding_adjustment = 0, 0
|
||||
rounded_total, base_rounded_total = 0, 0
|
||||
for doc in data:
|
||||
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
|
||||
|
||||
@ -124,7 +129,7 @@ class POSInvoiceMergeLog(Document):
|
||||
found = False
|
||||
for i in items:
|
||||
if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and
|
||||
i.uom == item.uom and i.net_rate == item.net_rate):
|
||||
i.uom == item.uom and i.net_rate == item.net_rate and i.warehouse == item.warehouse):
|
||||
found = True
|
||||
i.qty = i.qty + item.qty
|
||||
|
||||
@ -159,6 +164,11 @@ class POSInvoiceMergeLog(Document):
|
||||
found = True
|
||||
if not found:
|
||||
payments.append(payment)
|
||||
rounding_adjustment += doc.rounding_adjustment
|
||||
rounded_total += doc.rounded_total
|
||||
base_rounding_adjustment += doc.rounding_adjustment
|
||||
base_rounded_total += doc.rounded_total
|
||||
|
||||
|
||||
if loyalty_points_sum:
|
||||
invoice.redeem_loyalty_points = 1
|
||||
@ -168,10 +178,19 @@ class POSInvoiceMergeLog(Document):
|
||||
invoice.set('items', items)
|
||||
invoice.set('payments', payments)
|
||||
invoice.set('taxes', taxes)
|
||||
invoice.set('rounding_adjustment',rounding_adjustment)
|
||||
invoice.set('rounding_adjustment',base_rounding_adjustment)
|
||||
invoice.set('base_rounded_total',base_rounded_total)
|
||||
invoice.set('rounded_total',rounded_total)
|
||||
invoice.additional_discount_percentage = 0
|
||||
invoice.discount_amount = 0.0
|
||||
invoice.taxes_and_charges = None
|
||||
invoice.ignore_pricing_rule = 1
|
||||
invoice.customer = self.customer
|
||||
|
||||
if self.merge_invoices_based_on == 'Customer Group':
|
||||
invoice.flags.ignore_pos_profile = True
|
||||
invoice.pos_profile = ''
|
||||
|
||||
return invoice
|
||||
|
||||
@ -228,7 +247,7 @@ def get_all_unconsolidated_invoices():
|
||||
return pos_invoices
|
||||
|
||||
def get_invoice_customer_map(pos_invoices):
|
||||
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
|
||||
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Customer 2' : [{}] }
|
||||
pos_invoice_customer_map = {}
|
||||
for invoice in pos_invoices:
|
||||
customer = invoice.get('customer')
|
||||
@ -238,7 +257,10 @@ def get_invoice_customer_map(pos_invoices):
|
||||
return pos_invoice_customer_map
|
||||
|
||||
def consolidate_pos_invoices(pos_invoices=None, closing_entry=None):
|
||||
invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions')) or get_all_unconsolidated_invoices()
|
||||
invoices = pos_invoices or (closing_entry and closing_entry.get('pos_transactions'))
|
||||
if frappe.flags.in_test and not invoices:
|
||||
invoices = get_all_unconsolidated_invoices()
|
||||
|
||||
invoice_by_customer = get_invoice_customer_map(invoices)
|
||||
|
||||
if len(invoices) >= 10 and closing_entry:
|
||||
|
@ -120,6 +120,7 @@
|
||||
{
|
||||
"fieldname": "payments",
|
||||
"fieldtype": "Table",
|
||||
"label": "Payment Methods",
|
||||
"options": "POS Payment Method",
|
||||
"reqd": 1
|
||||
},
|
||||
@ -377,7 +378,7 @@
|
||||
"link_fieldname": "pos_profile"
|
||||
}
|
||||
],
|
||||
"modified": "2021-02-01 13:52:51.081311",
|
||||
"modified": "2021-10-14 14:17:00.469298",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Profile",
|
||||
|
@ -33,7 +33,9 @@ class TestPOSProfile(unittest.TestCase):
|
||||
|
||||
frappe.db.sql("delete from `tabPOS Profile`")
|
||||
|
||||
def get_customers_list(pos_profile={}):
|
||||
def get_customers_list(pos_profile=None):
|
||||
if pos_profile is None:
|
||||
pos_profile = {}
|
||||
cond = "1=1"
|
||||
customer_groups = []
|
||||
if pos_profile.get('customer_groups'):
|
||||
|
@ -2,11 +2,11 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
let search_fields_datatypes = ['Data', 'Link', 'Dynamic Link', 'Long Text', 'Select', 'Small Text', 'Text', 'Text Editor'];
|
||||
let do_not_include_fields = ["naming_series", "item_code", "item_name", "stock_uom", "hub_sync_id", "asset_naming_series",
|
||||
let do_not_include_fields = ["naming_series", "item_code", "item_name", "stock_uom", "asset_naming_series",
|
||||
"default_material_request_type", "valuation_method", "warranty_period", "weight_uom", "batch_number_series",
|
||||
"serial_no_series", "purchase_uom", "customs_tariff_number", "sales_uom", "deferred_revenue_account",
|
||||
"deferred_expense_account", "quality_inspection_template", "route", "slideshow", "website_image_alt", "thumbnail",
|
||||
"web_long_description", "hub_sync_id"]
|
||||
"web_long_description"]
|
||||
|
||||
frappe.ui.form.on('POS Settings', {
|
||||
onload: function(frm) {
|
||||
|
@ -7,10 +7,8 @@ from __future__ import unicode_literals
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe import MandatoryError
|
||||
|
||||
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
|
||||
from erpnext.healthcare.doctype.lab_test_template.lab_test_template import make_item_price
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import make_item
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
@ -623,3 +621,12 @@ def delete_existing_pricing_rules():
|
||||
"Pricing Rule Item Group", "Pricing Rule Brand"]:
|
||||
|
||||
frappe.db.sql("delete from `tab{0}`".format(doctype))
|
||||
|
||||
|
||||
def make_item_price(item, price_list_name, item_price):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Item Price',
|
||||
'price_list': price_list_name,
|
||||
'item_code': item,
|
||||
'price_list_rate': item_price
|
||||
}).insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
|
@ -29,6 +29,9 @@ def get_pricing_rules(args, doc=None):
|
||||
pricing_rules = []
|
||||
values = {}
|
||||
|
||||
if not frappe.db.exists('Pricing Rule', {'disable': 0, args.transaction_type: 1}):
|
||||
return
|
||||
|
||||
for apply_on in ['Item Code', 'Item Group', 'Brand']:
|
||||
pricing_rules.extend(_get_pricing_rules(apply_on, args, values))
|
||||
if pricing_rules and not apply_multiple_pricing_rules(pricing_rules):
|
||||
@ -398,7 +401,9 @@ def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules):
|
||||
pricing_rules[0].apply_rule_on_other_items = items
|
||||
return pricing_rules
|
||||
|
||||
def get_qty_amount_data_for_cumulative(pr_doc, doc, items=[]):
|
||||
def get_qty_amount_data_for_cumulative(pr_doc, doc, items=None):
|
||||
if items is None:
|
||||
items = []
|
||||
sum_qty, sum_amt = [0, 0]
|
||||
doctype = doc.get('parenttype') or doc.doctype
|
||||
|
||||
|
@ -219,6 +219,7 @@
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"description": "A customer must have primary contact email.",
|
||||
"fieldname": "primary_mandatory",
|
||||
"fieldtype": "Check",
|
||||
"label": "Send To Primary Contact"
|
||||
@ -286,10 +287,11 @@
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2021-05-21 11:14:22.426672",
|
||||
"modified": "2021-09-06 21:00:45.732505",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
"naming_rule": "Set by user",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -196,7 +196,10 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
primary_email = customer.get('email_id') or ''
|
||||
billing_email = get_customer_emails(customer.name, 1, billing_and_primary=False)
|
||||
|
||||
if billing_email == '' or (primary_email == '' and int(primary_mandatory)):
|
||||
if int(primary_mandatory):
|
||||
if (primary_email == ''):
|
||||
continue
|
||||
elif (billing_email == '') and (primary_email == ''):
|
||||
continue
|
||||
|
||||
customer_list.append({
|
||||
@ -208,10 +211,29 @@ def fetch_customers(customer_collection, collection_name, primary_mandatory):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_customer_emails(customer_name, primary_mandatory, billing_and_primary=True):
|
||||
""" Returns first email from Contact Email table as a Billing email
|
||||
when Is Billing Contact checked
|
||||
and Primary email- email with Is Primary checked """
|
||||
|
||||
billing_email = frappe.db.sql("""
|
||||
SELECT c.email_id FROM `tabContact` AS c JOIN `tabDynamic Link` AS l ON c.name=l.parent
|
||||
WHERE l.link_doctype='Customer' and l.link_name=%s and c.is_billing_contact=1
|
||||
order by c.creation desc""", customer_name)
|
||||
SELECT
|
||||
email.email_id
|
||||
FROM
|
||||
`tabContact Email` AS email
|
||||
JOIN
|
||||
`tabDynamic Link` AS link
|
||||
ON
|
||||
email.parent=link.parent
|
||||
JOIN
|
||||
`tabContact` AS contact
|
||||
ON
|
||||
contact.name=link.parent
|
||||
WHERE
|
||||
link.link_doctype='Customer'
|
||||
and link.link_name=%s
|
||||
and contact.is_billing_contact=1
|
||||
ORDER BY
|
||||
contact.creation desc""", customer_name)
|
||||
|
||||
if len(billing_email) == 0 or (billing_email[0][0] is None):
|
||||
if billing_and_primary:
|
||||
|
@ -69,7 +69,9 @@ class PromotionalScheme(Document):
|
||||
{'promotional_scheme': self.name}):
|
||||
frappe.delete_doc('Pricing Rule', rule.name)
|
||||
|
||||
def get_pricing_rules(doc, rules = {}):
|
||||
def get_pricing_rules(doc, rules=None):
|
||||
if rules is None:
|
||||
rules = {}
|
||||
new_doc = []
|
||||
for child_doc, fields in {'price_discount_slabs': price_discount_fields,
|
||||
'product_discount_slabs': product_discount_fields}.items():
|
||||
@ -78,7 +80,9 @@ def get_pricing_rules(doc, rules = {}):
|
||||
|
||||
return new_doc
|
||||
|
||||
def _get_pricing_rules(doc, child_doc, discount_fields, rules = {}):
|
||||
def _get_pricing_rules(doc, child_doc, discount_fields, rules=None):
|
||||
if rules is None:
|
||||
rules = {}
|
||||
new_doc = []
|
||||
args = get_args_for_pricing_rule(doc)
|
||||
applicable_for = frappe.scrub(doc.get('applicable_for'))
|
||||
|
@ -590,5 +590,11 @@ frappe.ui.form.on("Purchase Invoice", {
|
||||
|
||||
company: function(frm) {
|
||||
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
|
||||
|
||||
if (frm.doc.company) {
|
||||
frappe.db.get_value('Company', frm.doc.company, 'default_payable_account', (r) => {
|
||||
frm.set_value('credit_to', r.default_payable_account);
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -15,6 +15,8 @@ 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,
|
||||
validate_inter_company_party,
|
||||
@ -1145,6 +1147,12 @@ class PurchaseInvoice(BuyingController):
|
||||
if not self.apply_tds:
|
||||
return
|
||||
|
||||
if self.apply_tds and not self.get('tax_withholding_category'):
|
||||
self.tax_withholding_category = frappe.db.get_value('Supplier', self.supplier, 'tax_withholding_category')
|
||||
|
||||
if not self.tax_withholding_category:
|
||||
return
|
||||
|
||||
tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
|
||||
|
||||
if not tax_withholding_details:
|
||||
@ -1175,10 +1183,8 @@ class PurchaseInvoice(BuyingController):
|
||||
self.status = 'Draft'
|
||||
return
|
||||
|
||||
precision = self.precision("outstanding_amount")
|
||||
outstanding_amount = flt(self.outstanding_amount, precision)
|
||||
due_date = getdate(self.due_date)
|
||||
nowdate = getdate()
|
||||
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:
|
||||
@ -1186,9 +1192,11 @@ class PurchaseInvoice(BuyingController):
|
||||
elif self.docstatus == 1:
|
||||
if self.is_internal_transfer():
|
||||
self.status = 'Internal Transfer'
|
||||
elif outstanding_amount > 0 and due_date < nowdate:
|
||||
elif is_overdue(self, total):
|
||||
self.status = "Overdue"
|
||||
elif outstanding_amount > 0 and due_date >= nowdate:
|
||||
elif 0 < outstanding_amount < total:
|
||||
self.status = "Partly Paid"
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
#Check if outstanding amount is 0 due to debit note issued against invoice
|
||||
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Purchase Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
||||
|
@ -2,28 +2,58 @@
|
||||
// License: GNU General Public License v3. See license.txt
|
||||
|
||||
// render
|
||||
frappe.listview_settings['Purchase Invoice'] = {
|
||||
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company",
|
||||
"currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"],
|
||||
get_indicator: function(doc) {
|
||||
if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
|
||||
return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"];
|
||||
} else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
|
||||
if(cint(doc.on_hold) && !doc.release_date) {
|
||||
return [__("On Hold"), "darkgrey"];
|
||||
} else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
|
||||
return [__("Temporarily on Hold"), "darkgrey"];
|
||||
} else if (frappe.datetime.get_diff(doc.due_date) < 0) {
|
||||
return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"];
|
||||
} else {
|
||||
return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"];
|
||||
}
|
||||
} else if (cint(doc.is_return)) {
|
||||
return [__("Return"), "gray", "is_return,=,Yes"];
|
||||
} else if (doc.company == doc.represents_company && doc.is_internal_supplier) {
|
||||
return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"];
|
||||
} else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
|
||||
return [__("Paid"), "green", "outstanding_amount,=,0"];
|
||||
frappe.listview_settings["Purchase Invoice"] = {
|
||||
add_fields: [
|
||||
"supplier",
|
||||
"supplier_name",
|
||||
"base_grand_total",
|
||||
"outstanding_amount",
|
||||
"due_date",
|
||||
"company",
|
||||
"currency",
|
||||
"is_return",
|
||||
"release_date",
|
||||
"on_hold",
|
||||
"represents_company",
|
||||
"is_internal_supplier",
|
||||
],
|
||||
get_indicator(doc) {
|
||||
if (doc.status == "Debit Note Issued") {
|
||||
return [__(doc.status), "darkgrey", "status,=," + doc.status];
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
flt(doc.outstanding_amount) > 0 &&
|
||||
doc.docstatus == 1 &&
|
||||
cint(doc.on_hold)
|
||||
) {
|
||||
if (!doc.release_date) {
|
||||
return [__("On Hold"), "darkgrey"];
|
||||
} else if (
|
||||
frappe.datetime.get_diff(
|
||||
doc.release_date,
|
||||
frappe.datetime.nowdate()
|
||||
) > 0
|
||||
) {
|
||||
return [__("Temporarily on Hold"), "darkgrey"];
|
||||
}
|
||||
}
|
||||
|
||||
const status_colors = {
|
||||
"Unpaid": "orange",
|
||||
"Paid": "green",
|
||||
"Return": "gray",
|
||||
"Overdue": "red",
|
||||
"Partly Paid": "yellow",
|
||||
"Internal Transfer": "darkgrey",
|
||||
};
|
||||
|
||||
if (status_colors[doc.status]) {
|
||||
return [
|
||||
__(doc.status),
|
||||
status_colors[doc.status],
|
||||
"status,=," + doc.status,
|
||||
];
|
||||
}
|
||||
},
|
||||
};
|
||||
|
@ -1151,10 +1151,11 @@ class TestPurchaseInvoice(unittest.TestCase):
|
||||
tax_withholding_category = 'TDS - 194 - Dividends - Individual')
|
||||
|
||||
# Update tax withholding category with current fiscal year and rate details
|
||||
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
|
||||
update_tax_witholding_category('_Test Company', 'TDS Payable - _TC')
|
||||
|
||||
# Create Purchase Order with TDS applied
|
||||
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item')
|
||||
po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000, item='_Test Non Stock Item',
|
||||
posting_date='2021-09-15')
|
||||
po.apply_tds = 1
|
||||
po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
|
||||
po.save()
|
||||
@ -1226,16 +1227,20 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
|
||||
doc.assertEqual(expected_gle[i][2], gle.credit)
|
||||
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
|
||||
|
||||
def update_tax_witholding_category(company, account, date):
|
||||
def update_tax_witholding_category(company, account):
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
fiscal_year = get_fiscal_year(date=date, company=company)
|
||||
fiscal_year = get_fiscal_year(fiscal_year='2021')
|
||||
|
||||
if not frappe.db.get_value('Tax Withholding Rate',
|
||||
{'parent': 'TDS - 194 - Dividends - Individual', 'fiscal_year': fiscal_year[0]}):
|
||||
{'parent': 'TDS - 194 - Dividends - Individual', 'from_date': ('>=', fiscal_year[1]),
|
||||
'to_date': ('<=', fiscal_year[2])}):
|
||||
tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
|
||||
tds_category.set('rates', [])
|
||||
|
||||
tds_category.append('rates', {
|
||||
'fiscal_year': fiscal_year[0],
|
||||
'from_date': fiscal_year[1],
|
||||
'to_date': fiscal_year[2],
|
||||
'tax_withholding_rate': 10,
|
||||
'single_threshold': 2500,
|
||||
'cumulative_threshold': 0
|
||||
|
@ -97,6 +97,7 @@
|
||||
"width": "100px"
|
||||
},
|
||||
{
|
||||
"depends_on": "exchange_gain_loss",
|
||||
"fieldname": "exchange_gain_loss",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Exchange Gain/Loss",
|
||||
@ -104,6 +105,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "exchange_gain_loss",
|
||||
"fieldname": "ref_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reference Exchange Rate",
|
||||
@ -115,7 +117,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-20 16:26:53.820530",
|
||||
"modified": "2021-09-26 15:47:28.167371",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Advance",
|
||||
|
@ -12,6 +12,13 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
}
|
||||
company() {
|
||||
erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
|
||||
|
||||
let me = this;
|
||||
if (this.frm.doc.company) {
|
||||
frappe.db.get_value('Company', this.frm.doc.company, 'default_receivable_account', (r) => {
|
||||
me.frm.set_value('debit_to', r.default_receivable_account);
|
||||
});
|
||||
}
|
||||
}
|
||||
onload() {
|
||||
var me = this;
|
||||
@ -445,15 +452,6 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
this.frm.refresh_field("base_paid_amount");
|
||||
}
|
||||
|
||||
currency() {
|
||||
this._super();
|
||||
$.each(cur_frm.doc.timesheets, function(i, d) {
|
||||
let row = frappe.get_doc(d.doctype, d.name)
|
||||
set_timesheet_detail_rate(row.doctype, row.name, cur_frm.doc.currency, row.timesheet_detail)
|
||||
});
|
||||
calculate_total_billing_amount(cur_frm)
|
||||
}
|
||||
|
||||
currency() {
|
||||
var me = this;
|
||||
super.currency();
|
||||
@ -462,7 +460,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
|
||||
let row = frappe.get_doc(d.doctype, d.name)
|
||||
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
|
||||
});
|
||||
calculate_total_billing_amount(this.frm);
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -734,19 +732,6 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
}
|
||||
},
|
||||
|
||||
project: function(frm){
|
||||
if (!frm.doc.is_return) {
|
||||
frm.call({
|
||||
method: "add_timesheet_data",
|
||||
doc: frm.doc,
|
||||
callback: function(r, rt) {
|
||||
refresh_field(['timesheets'])
|
||||
}
|
||||
})
|
||||
frm.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(frm) {
|
||||
frm.redemption_conversion_factor = null;
|
||||
},
|
||||
@ -857,25 +842,92 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
}
|
||||
},
|
||||
|
||||
add_timesheet_row: function(frm, row, exchange_rate) {
|
||||
frm.add_child('timesheets', {
|
||||
'activity_type': row.activity_type,
|
||||
'description': row.description,
|
||||
'time_sheet': row.parent,
|
||||
'billing_hours': row.billing_hours,
|
||||
'billing_amount': flt(row.billing_amount) * flt(exchange_rate),
|
||||
'timesheet_detail': row.name,
|
||||
'project_name': row.project_name
|
||||
project: function(frm) {
|
||||
if (frm.doc.project) {
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
project: frm.doc.project
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async add_timesheet_data(frm, kwargs) {
|
||||
if (kwargs === "Sales Invoice") {
|
||||
// called via frm.trigger()
|
||||
kwargs = Object();
|
||||
}
|
||||
|
||||
if (!kwargs.hasOwnProperty("project") && frm.doc.project) {
|
||||
kwargs.project = frm.doc.project;
|
||||
}
|
||||
|
||||
const timesheets = await frm.events.get_timesheet_data(frm, kwargs);
|
||||
return frm.events.set_timesheet_data(frm, timesheets);
|
||||
},
|
||||
|
||||
async get_timesheet_data(frm, kwargs) {
|
||||
return frappe.call({
|
||||
method: "erpnext.projects.doctype.timesheet.timesheet.get_projectwise_timesheet_data",
|
||||
args: kwargs
|
||||
}).then(r => {
|
||||
if (!r.exc && r.message.length > 0) {
|
||||
return r.message
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
});
|
||||
frm.refresh_field('timesheets');
|
||||
calculate_total_billing_amount(frm);
|
||||
},
|
||||
|
||||
set_timesheet_data: function(frm, timesheets) {
|
||||
frm.clear_table("timesheets")
|
||||
timesheets.forEach(timesheet => {
|
||||
if (frm.doc.currency != timesheet.currency) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
from_currency: timesheet.currency,
|
||||
to_currency: frm.doc.currency
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
exchange_rate = r.message;
|
||||
frm.events.append_time_log(frm, timesheet, exchange_rate);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frm.events.append_time_log(frm, timesheet, 1.0);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
append_time_log: function(frm, time_log, exchange_rate) {
|
||||
const row = frm.add_child("timesheets");
|
||||
row.activity_type = time_log.activity_type;
|
||||
row.description = time_log.description;
|
||||
row.time_sheet = time_log.time_sheet;
|
||||
row.from_time = time_log.from_time;
|
||||
row.to_time = time_log.to_time;
|
||||
row.billing_hours = time_log.billing_hours;
|
||||
row.billing_amount = flt(time_log.billing_amount) * flt(exchange_rate);
|
||||
row.timesheet_detail = time_log.name;
|
||||
row.project_name = time_log.project_name;
|
||||
|
||||
frm.refresh_field("timesheets");
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
},
|
||||
|
||||
calculate_timesheet_totals: function(frm) {
|
||||
frm.set_value("total_billing_amount",
|
||||
frm.doc.timesheets.reduce((a, b) => a + (b["billing_amount"] || 0.0), 0.0));
|
||||
frm.set_value("total_billing_hours",
|
||||
frm.doc.timesheets.reduce((a, b) => a + (b["billing_hours"] || 0.0), 0.0));
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.docstatus===0 && !frm.doc.is_return) {
|
||||
frm.add_custom_button(__('Fetch Timesheet'), function() {
|
||||
frm.add_custom_button(__("Fetch Timesheet"), function() {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Fetch Timesheet'),
|
||||
title: __("Fetch Timesheet"),
|
||||
fields: [
|
||||
{
|
||||
"label" : __("From"),
|
||||
@ -884,8 +936,8 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
"reqd": 1,
|
||||
},
|
||||
{
|
||||
fieldtype: 'Column Break',
|
||||
fieldname: 'col_break_1',
|
||||
fieldtype: "Column Break",
|
||||
fieldname: "col_break_1",
|
||||
},
|
||||
{
|
||||
"label" : __("To"),
|
||||
@ -902,48 +954,18 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
let data = d.get_values();
|
||||
frappe.call({
|
||||
method: "erpnext.projects.doctype.timesheet.timesheet.get_projectwise_timesheet_data",
|
||||
args: {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project
|
||||
},
|
||||
callback: function(r) {
|
||||
if (!r.exc && r.message.length > 0) {
|
||||
frm.clear_table('timesheets')
|
||||
r.message.forEach((d) => {
|
||||
let exchange_rate = 1.0;
|
||||
if (frm.doc.currency != d.currency) {
|
||||
frappe.call({
|
||||
method: 'erpnext.setup.utils.get_exchange_rate',
|
||||
args: {
|
||||
from_currency: d.currency,
|
||||
to_currency: frm.doc.currency
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
exchange_rate = r.message;
|
||||
frm.events.add_timesheet_row(frm, d, exchange_rate);
|
||||
}
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frm.events.add_timesheet_row(frm, d, exchange_rate);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frappe.msgprint(__('No Timesheets found with the selected filters.'))
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
const data = d.get_values();
|
||||
frm.events.add_timesheet_data(frm, {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: data.project
|
||||
});
|
||||
d.hide();
|
||||
},
|
||||
primary_action_label: __('Get Timesheets')
|
||||
primary_action_label: __("Get Timesheets")
|
||||
});
|
||||
d.show();
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
if (frm.doc.is_debit_note) {
|
||||
@ -976,26 +998,20 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
frm: frm
|
||||
});
|
||||
},
|
||||
|
||||
create_dunning: function(frm) {
|
||||
frappe.model.open_mapped_doc({
|
||||
method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning",
|
||||
frm: frm
|
||||
});
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
var calculate_total_billing_amount = function(frm) {
|
||||
var doc = frm.doc;
|
||||
|
||||
doc.total_billing_amount = 0.0
|
||||
if (doc.timesheets) {
|
||||
doc.timesheets.forEach((d) => {
|
||||
doc.total_billing_amount += flt(d.billing_amount)
|
||||
});
|
||||
frappe.ui.form.on("Sales Invoice Timesheet", {
|
||||
timesheets_remove(frm) {
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
|
||||
refresh_field('total_billing_amount')
|
||||
}
|
||||
});
|
||||
|
||||
var set_timesheet_detail_rate = function(cdt, cdn, currency, timelog) {
|
||||
frappe.call({
|
||||
@ -1042,276 +1058,3 @@ var select_loyalty_program = function(frm, loyalty_programs) {
|
||||
|
||||
dialog.show();
|
||||
}
|
||||
|
||||
// Healthcare
|
||||
var get_healthcare_services_to_invoice = function(frm) {
|
||||
var me = this;
|
||||
let selected_patient = '';
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("Get Items from Healthcare Services"),
|
||||
fields:[
|
||||
{
|
||||
fieldtype: 'Link',
|
||||
options: 'Patient',
|
||||
label: 'Patient',
|
||||
fieldname: "patient",
|
||||
reqd: true
|
||||
},
|
||||
{ fieldtype: 'Section Break' },
|
||||
{ fieldtype: 'HTML', fieldname: 'results_area' }
|
||||
]
|
||||
});
|
||||
var $wrapper;
|
||||
var $results;
|
||||
var $placeholder;
|
||||
dialog.set_values({
|
||||
'patient': frm.doc.patient
|
||||
});
|
||||
dialog.fields_dict["patient"].df.onchange = () => {
|
||||
var patient = dialog.fields_dict.patient.input.value;
|
||||
if(patient && patient!=selected_patient){
|
||||
selected_patient = patient;
|
||||
var method = "erpnext.healthcare.utils.get_healthcare_services_to_invoice";
|
||||
var args = {patient: patient, company: frm.doc.company};
|
||||
var columns = (["service", "reference_name", "reference_type"]);
|
||||
get_healthcare_items(frm, true, $results, $placeholder, method, args, columns);
|
||||
}
|
||||
else if(!patient){
|
||||
selected_patient = '';
|
||||
$results.empty();
|
||||
$results.append($placeholder);
|
||||
}
|
||||
}
|
||||
$wrapper = dialog.fields_dict.results_area.$wrapper.append(`<div class="results"
|
||||
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`);
|
||||
$results = $wrapper.find('.results');
|
||||
$placeholder = $(`<div class="multiselect-empty-state">
|
||||
<span class="text-center" style="margin-top: -40px;">
|
||||
<i class="fa fa-2x fa-heartbeat text-extra-muted"></i>
|
||||
<p class="text-extra-muted">No billable Healthcare Services found</p>
|
||||
</span>
|
||||
</div>`);
|
||||
$results.on('click', '.list-item--head :checkbox', (e) => {
|
||||
$results.find('.list-item-container .list-row-check')
|
||||
.prop("checked", ($(e.target).is(':checked')));
|
||||
});
|
||||
set_primary_action(frm, dialog, $results, true);
|
||||
dialog.show();
|
||||
};
|
||||
|
||||
var get_healthcare_items = function(frm, invoice_healthcare_services, $results, $placeholder, method, args, columns) {
|
||||
var me = this;
|
||||
$results.empty();
|
||||
frappe.call({
|
||||
method: method,
|
||||
args: args,
|
||||
callback: function(data) {
|
||||
if(data.message){
|
||||
$results.append(make_list_row(columns, invoice_healthcare_services));
|
||||
for(let i=0; i<data.message.length; i++){
|
||||
$results.append(make_list_row(columns, invoice_healthcare_services, data.message[i]));
|
||||
}
|
||||
}else {
|
||||
$results.append($placeholder);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var make_list_row= function(columns, invoice_healthcare_services, result={}) {
|
||||
var me = this;
|
||||
// Make a head row by default (if result not passed)
|
||||
let head = Object.keys(result).length === 0;
|
||||
let contents = ``;
|
||||
columns.forEach(function(column) {
|
||||
contents += `<div class="list-item__content ellipsis">
|
||||
${
|
||||
head ? `<span class="ellipsis">${__(frappe.model.unscrub(column))}</span>`
|
||||
|
||||
:(column !== "name" ? `<span class="ellipsis">${__(result[column])}</span>`
|
||||
: `<a class="list-id ellipsis">
|
||||
${__(result[column])}</a>`)
|
||||
}
|
||||
</div>`;
|
||||
})
|
||||
|
||||
let $row = $(`<div class="list-item">
|
||||
<div class="list-item__content" style="flex: 0 0 10px;">
|
||||
<input type="checkbox" class="list-row-check" ${result.checked ? 'checked' : ''}>
|
||||
</div>
|
||||
${contents}
|
||||
</div>`);
|
||||
|
||||
$row = list_row_data_items(head, $row, result, invoice_healthcare_services);
|
||||
return $row;
|
||||
};
|
||||
|
||||
var set_primary_action= function(frm, dialog, $results, invoice_healthcare_services) {
|
||||
var me = this;
|
||||
dialog.set_primary_action(__('Add'), function() {
|
||||
let checked_values = get_checked_values($results);
|
||||
if(checked_values.length > 0){
|
||||
if(invoice_healthcare_services) {
|
||||
frm.set_value("patient", dialog.fields_dict.patient.input.value);
|
||||
}
|
||||
frm.set_value("items", []);
|
||||
add_to_item_line(frm, checked_values, invoice_healthcare_services);
|
||||
dialog.hide();
|
||||
}
|
||||
else{
|
||||
if(invoice_healthcare_services){
|
||||
frappe.msgprint(__("Please select Healthcare Service"));
|
||||
}
|
||||
else{
|
||||
frappe.msgprint(__("Please select Drug"));
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
var get_checked_values= function($results) {
|
||||
return $results.find('.list-item-container').map(function() {
|
||||
let checked_values = {};
|
||||
if ($(this).find('.list-row-check:checkbox:checked').length > 0 ) {
|
||||
checked_values['dn'] = $(this).attr('data-dn');
|
||||
checked_values['dt'] = $(this).attr('data-dt');
|
||||
checked_values['item'] = $(this).attr('data-item');
|
||||
if($(this).attr('data-rate') != 'undefined'){
|
||||
checked_values['rate'] = $(this).attr('data-rate');
|
||||
}
|
||||
else{
|
||||
checked_values['rate'] = false;
|
||||
}
|
||||
if($(this).attr('data-income-account') != 'undefined'){
|
||||
checked_values['income_account'] = $(this).attr('data-income-account');
|
||||
}
|
||||
else{
|
||||
checked_values['income_account'] = false;
|
||||
}
|
||||
if($(this).attr('data-qty') != 'undefined'){
|
||||
checked_values['qty'] = $(this).attr('data-qty');
|
||||
}
|
||||
else{
|
||||
checked_values['qty'] = false;
|
||||
}
|
||||
if($(this).attr('data-description') != 'undefined'){
|
||||
checked_values['description'] = $(this).attr('data-description');
|
||||
}
|
||||
else{
|
||||
checked_values['description'] = false;
|
||||
}
|
||||
return checked_values;
|
||||
}
|
||||
}).get();
|
||||
};
|
||||
|
||||
var get_drugs_to_invoice = function(frm) {
|
||||
var me = this;
|
||||
let selected_encounter = '';
|
||||
var dialog = new frappe.ui.Dialog({
|
||||
title: __("Get Items from Prescriptions"),
|
||||
fields:[
|
||||
{ fieldtype: 'Link', options: 'Patient', label: 'Patient', fieldname: "patient", reqd: true },
|
||||
{ fieldtype: 'Link', options: 'Patient Encounter', label: 'Patient Encounter', fieldname: "encounter", reqd: true,
|
||||
description:'Quantity will be calculated only for items which has "Nos" as UoM. You may change as required for each invoice item.',
|
||||
get_query: function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
patient: dialog.get_value("patient"),
|
||||
company: frm.doc.company,
|
||||
docstatus: 1
|
||||
}
|
||||
};
|
||||
}
|
||||
},
|
||||
{ fieldtype: 'Section Break' },
|
||||
{ fieldtype: 'HTML', fieldname: 'results_area' }
|
||||
]
|
||||
});
|
||||
var $wrapper;
|
||||
var $results;
|
||||
var $placeholder;
|
||||
dialog.set_values({
|
||||
'patient': frm.doc.patient,
|
||||
'encounter': ""
|
||||
});
|
||||
dialog.fields_dict["encounter"].df.onchange = () => {
|
||||
var encounter = dialog.fields_dict.encounter.input.value;
|
||||
if(encounter && encounter!=selected_encounter){
|
||||
selected_encounter = encounter;
|
||||
var method = "erpnext.healthcare.utils.get_drugs_to_invoice";
|
||||
var args = {encounter: encounter};
|
||||
var columns = (["drug_code", "quantity", "description"]);
|
||||
get_healthcare_items(frm, false, $results, $placeholder, method, args, columns);
|
||||
}
|
||||
else if(!encounter){
|
||||
selected_encounter = '';
|
||||
$results.empty();
|
||||
$results.append($placeholder);
|
||||
}
|
||||
}
|
||||
$wrapper = dialog.fields_dict.results_area.$wrapper.append(`<div class="results"
|
||||
style="border: 1px solid #d1d8dd; border-radius: 3px; height: 300px; overflow: auto;"></div>`);
|
||||
$results = $wrapper.find('.results');
|
||||
$placeholder = $(`<div class="multiselect-empty-state">
|
||||
<span class="text-center" style="margin-top: -40px;">
|
||||
<i class="fa fa-2x fa-heartbeat text-extra-muted"></i>
|
||||
<p class="text-extra-muted">No Drug Prescription found</p>
|
||||
</span>
|
||||
</div>`);
|
||||
$results.on('click', '.list-item--head :checkbox', (e) => {
|
||||
$results.find('.list-item-container .list-row-check')
|
||||
.prop("checked", ($(e.target).is(':checked')));
|
||||
});
|
||||
set_primary_action(frm, dialog, $results, false);
|
||||
dialog.show();
|
||||
};
|
||||
|
||||
var list_row_data_items = function(head, $row, result, invoice_healthcare_services) {
|
||||
if(invoice_healthcare_services){
|
||||
head ? $row.addClass('list-item--head')
|
||||
: $row = $(`<div class="list-item-container"
|
||||
data-dn= "${result.reference_name}" data-dt= "${result.reference_type}" data-item= "${result.service}"
|
||||
data-rate = ${result.rate}
|
||||
data-income-account = "${result.income_account}"
|
||||
data-qty = ${result.qty}
|
||||
data-description = "${result.description}">
|
||||
</div>`).append($row);
|
||||
}
|
||||
else{
|
||||
head ? $row.addClass('list-item--head')
|
||||
: $row = $(`<div class="list-item-container"
|
||||
data-item= "${result.drug_code}"
|
||||
data-qty = ${result.quantity}
|
||||
data-description = "${result.description}">
|
||||
</div>`).append($row);
|
||||
}
|
||||
return $row
|
||||
};
|
||||
|
||||
var add_to_item_line = function(frm, checked_values, invoice_healthcare_services){
|
||||
if(invoice_healthcare_services){
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "set_healthcare_services",
|
||||
args:{
|
||||
checked_values: checked_values
|
||||
},
|
||||
callback: function() {
|
||||
frm.trigger("validate");
|
||||
frm.refresh_fields();
|
||||
}
|
||||
});
|
||||
}
|
||||
else{
|
||||
for(let i=0; i<checked_values.length; i++){
|
||||
var si_item = frappe.model.add_child(frm.doc, 'Sales Invoice Item', 'items');
|
||||
frappe.model.set_value(si_item.doctype, si_item.name, 'item_code', checked_values[i]['item']);
|
||||
frappe.model.set_value(si_item.doctype, si_item.name, 'qty', 1);
|
||||
if(checked_values[i]['qty'] > 1){
|
||||
frappe.model.set_value(si_item.doctype, si_item.name, 'qty', parseFloat(checked_values[i]['qty']));
|
||||
}
|
||||
}
|
||||
frm.refresh_fields();
|
||||
}
|
||||
};
|
||||
|
@ -74,6 +74,7 @@
|
||||
"time_sheet_list",
|
||||
"timesheets",
|
||||
"total_billing_amount",
|
||||
"total_billing_hours",
|
||||
"section_break_30",
|
||||
"total_qty",
|
||||
"base_total",
|
||||
@ -123,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",
|
||||
@ -143,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",
|
||||
@ -160,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",
|
||||
@ -247,7 +248,7 @@
|
||||
"depends_on": "customer",
|
||||
"fetch_from": "customer.customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Small Text",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"in_global_search": 1,
|
||||
@ -1061,6 +1062,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Apply Additional Discount On",
|
||||
"length": 15,
|
||||
"options": "\nGrand Total\nNet Total",
|
||||
"print_hide": 1
|
||||
},
|
||||
@ -1147,7 +1149,7 @@
|
||||
{
|
||||
"description": "In Words will be visible once you save the Sales Invoice.",
|
||||
"fieldname": "base_in_words",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Small Text",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "In Words (Company Currency)",
|
||||
@ -1207,7 +1209,7 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "in_words",
|
||||
"fieldtype": "Data",
|
||||
"fieldtype": "Small Text",
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "In Words",
|
||||
@ -1560,6 +1562,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Print Language",
|
||||
"length": 6,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -1647,8 +1650,9 @@
|
||||
"hide_seconds": 1,
|
||||
"in_standard_filter": 1,
|
||||
"label": "Status",
|
||||
"length": 30,
|
||||
"no_copy": 1,
|
||||
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
|
||||
"options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nPartly Paid\nUnpaid\nUnpaid and Discounted\nPartly Paid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -1706,6 +1710,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Is Opening Entry",
|
||||
"length": 4,
|
||||
"oldfieldname": "is_opening",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "No\nYes",
|
||||
@ -1717,6 +1722,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "C-Form Applicable",
|
||||
"length": 4,
|
||||
"no_copy": 1,
|
||||
"options": "No\nYes",
|
||||
"print_hide": 1
|
||||
@ -1948,6 +1954,7 @@
|
||||
"fetch_from": "customer.represents_company",
|
||||
"fieldname": "represents_company",
|
||||
"fieldtype": "Link",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Represents Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
@ -2005,6 +2012,13 @@
|
||||
"hidden": 1,
|
||||
"label": "Ignore Default Payment Terms Template",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_billing_hours",
|
||||
"fieldtype": "Float",
|
||||
"label": "Total Billing Hours",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@ -2017,11 +2031,12 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-27 20:13:40.456462",
|
||||
"modified": "2021-10-11 20:19:38.667508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
"name_case": "Title Case",
|
||||
"naming_rule": "By \"Naming Series\" field",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
|
@ -27,9 +27,9 @@ from erpnext.assets.doctype.asset.depreciation import (
|
||||
get_disposal_account_and_cost_center,
|
||||
get_gl_entries_on_asset_disposal,
|
||||
get_gl_entries_on_asset_regain,
|
||||
make_depreciation_entry,
|
||||
)
|
||||
from erpnext.controllers.selling_controller import SellingController
|
||||
from erpnext.healthcare.utils import manage_invoice_submit_cancel
|
||||
from erpnext.projects.doctype.timesheet.timesheet import get_projectwise_timesheet_data
|
||||
from erpnext.setup.doctype.company.company import update_company_current_month_sales
|
||||
from erpnext.stock.doctype.batch.batch import set_batch_nos
|
||||
@ -219,9 +219,6 @@ class SalesInvoice(SellingController):
|
||||
# this sequence because outstanding may get -ve
|
||||
self.make_gl_entries()
|
||||
|
||||
if self.update_stock == 1:
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
if self.update_stock == 1:
|
||||
self.repost_future_sle_and_gle()
|
||||
|
||||
@ -252,13 +249,6 @@ class SalesInvoice(SellingController):
|
||||
if self.redeem_loyalty_points and not self.is_consolidated and self.loyalty_points:
|
||||
self.apply_loyalty_points()
|
||||
|
||||
# Healthcare Service Invoice.
|
||||
domain_settings = frappe.get_doc('Domain Settings')
|
||||
active_domains = [d.domain for d in domain_settings.active_domains]
|
||||
|
||||
if "Healthcare" in active_domains:
|
||||
manage_invoice_submit_cancel(self, "on_submit")
|
||||
|
||||
self.process_common_party_accounting()
|
||||
|
||||
def validate_pos_return(self):
|
||||
@ -341,12 +331,6 @@ class SalesInvoice(SellingController):
|
||||
|
||||
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
|
||||
|
||||
# Healthcare Service Invoice.
|
||||
domain_settings = frappe.get_doc('Domain Settings')
|
||||
active_domains = [d.domain for d in domain_settings.active_domains]
|
||||
|
||||
if "Healthcare" in active_domains:
|
||||
manage_invoice_submit_cancel(self, "on_cancel")
|
||||
self.unlink_sales_invoice_from_timesheets()
|
||||
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
|
||||
|
||||
@ -488,7 +472,7 @@ class SalesInvoice(SellingController):
|
||||
self.account_for_change_amount = frappe.get_cached_value('Company', self.company, 'default_cash_account')
|
||||
|
||||
from erpnext.stock.get_item_details import get_pos_profile, get_pos_profile_item_details
|
||||
if not self.pos_profile:
|
||||
if not self.pos_profile and not self.flags.ignore_pos_profile:
|
||||
pos_profile = get_pos_profile(self.company) or {}
|
||||
if not pos_profile:
|
||||
return
|
||||
@ -761,7 +745,7 @@ class SalesInvoice(SellingController):
|
||||
if self.project:
|
||||
for data in get_projectwise_timesheet_data(self.project):
|
||||
self.append('timesheets', {
|
||||
'time_sheet': data.parent,
|
||||
'time_sheet': data.time_sheet,
|
||||
'billing_hours': data.billing_hours,
|
||||
'billing_amount': data.billing_amount,
|
||||
'timesheet_detail': data.name,
|
||||
@ -772,12 +756,11 @@ class SalesInvoice(SellingController):
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
|
||||
def calculate_billing_amount_for_timesheet(self):
|
||||
total_billing_amount = 0.0
|
||||
for data in self.timesheets:
|
||||
if data.billing_amount:
|
||||
total_billing_amount += data.billing_amount
|
||||
def timesheet_sum(field):
|
||||
return sum((ts.get(field) or 0.0) for ts in self.timesheets)
|
||||
|
||||
self.total_billing_amount = total_billing_amount
|
||||
self.total_billing_amount = timesheet_sum("billing_amount")
|
||||
self.total_billing_hours = timesheet_sum("billing_hours")
|
||||
|
||||
def get_warehouse(self):
|
||||
user_pos_profile = frappe.db.sql("""select name, warehouse from `tabPOS Profile`
|
||||
@ -941,6 +924,7 @@ class SalesInvoice(SellingController):
|
||||
asset.db_set("disposal_date", None)
|
||||
|
||||
if asset.calculate_depreciation:
|
||||
self.reverse_depreciation_entry_made_after_sale(asset)
|
||||
self.reset_depreciation_schedule(asset)
|
||||
|
||||
else:
|
||||
@ -996,6 +980,89 @@ class SalesInvoice(SellingController):
|
||||
self.check_finance_books(item, asset)
|
||||
return asset
|
||||
|
||||
def check_finance_books(self, item, asset):
|
||||
if (len(asset.finance_books) > 1 and not item.finance_book
|
||||
and asset.finance_books[0].finance_book):
|
||||
frappe.throw(_("Select finance book for the item {0} at row {1}")
|
||||
.format(item.item_code, item.idx))
|
||||
|
||||
def depreciate_asset(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
asset.prepare_depreciation_data(date_of_sale=self.posting_date)
|
||||
asset.save()
|
||||
|
||||
make_depreciation_entry(asset.name, self.posting_date)
|
||||
|
||||
def reset_depreciation_schedule(self, asset):
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
|
||||
# recreate original depreciation schedule of the asset
|
||||
asset.prepare_depreciation_data(date_of_return=self.posting_date)
|
||||
|
||||
self.modify_depreciation_schedule_for_asset_repairs(asset)
|
||||
asset.save()
|
||||
|
||||
def modify_depreciation_schedule_for_asset_repairs(self, asset):
|
||||
asset_repairs = frappe.get_all(
|
||||
'Asset Repair',
|
||||
filters = {'asset': asset.name},
|
||||
fields = ['name', 'increase_in_asset_life']
|
||||
)
|
||||
|
||||
for repair in asset_repairs:
|
||||
if repair.increase_in_asset_life:
|
||||
asset_repair = frappe.get_doc('Asset Repair', repair.name)
|
||||
asset_repair.modify_depreciation_schedule()
|
||||
asset.prepare_depreciation_data()
|
||||
|
||||
def reverse_depreciation_entry_made_after_sale(self, asset):
|
||||
from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
|
||||
|
||||
posting_date_of_original_invoice = self.get_posting_date_of_sales_invoice()
|
||||
|
||||
row = -1
|
||||
finance_book = asset.get('schedules')[0].get('finance_book')
|
||||
for schedule in asset.get('schedules'):
|
||||
if schedule.finance_book != finance_book:
|
||||
row = 0
|
||||
finance_book = schedule.finance_book
|
||||
else:
|
||||
row += 1
|
||||
|
||||
if schedule.schedule_date == posting_date_of_original_invoice:
|
||||
if not self.sale_was_made_on_original_schedule_date(asset, schedule, row, posting_date_of_original_invoice) \
|
||||
or self.sale_happens_in_the_future(posting_date_of_original_invoice):
|
||||
|
||||
reverse_journal_entry = make_reverse_journal_entry(schedule.journal_entry)
|
||||
reverse_journal_entry.posting_date = nowdate()
|
||||
frappe.flags.is_reverse_depr_entry = True
|
||||
reverse_journal_entry.submit()
|
||||
|
||||
frappe.flags.is_reverse_depr_entry = False
|
||||
asset.flags.ignore_validate_update_after_submit = True
|
||||
schedule.journal_entry = None
|
||||
asset.save()
|
||||
|
||||
def get_posting_date_of_sales_invoice(self):
|
||||
return frappe.db.get_value('Sales Invoice', self.return_against, 'posting_date')
|
||||
|
||||
# if the invoice had been posted on the date the depreciation was initially supposed to happen, the depreciation shouldn't be undone
|
||||
def sale_was_made_on_original_schedule_date(self, asset, schedule, row, posting_date_of_original_invoice):
|
||||
for finance_book in asset.get('finance_books'):
|
||||
if schedule.finance_book == finance_book.finance_book:
|
||||
orginal_schedule_date = add_months(finance_book.depreciation_start_date,
|
||||
row * cint(finance_book.frequency_of_depreciation))
|
||||
|
||||
if orginal_schedule_date == posting_date_of_original_invoice:
|
||||
return True
|
||||
return False
|
||||
|
||||
def sale_happens_in_the_future(self, posting_date_of_original_invoice):
|
||||
if posting_date_of_original_invoice > getdate():
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@property
|
||||
def enable_discount_accounting(self):
|
||||
if not hasattr(self, "_enable_discount_accounting"):
|
||||
@ -1220,12 +1287,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):
|
||||
@ -1348,59 +1423,14 @@ class SalesInvoice(SellingController):
|
||||
if points_to_redeem < 1: # since points_to_redeem is integer
|
||||
break
|
||||
|
||||
# Healthcare
|
||||
@frappe.whitelist()
|
||||
def set_healthcare_services(self, checked_values):
|
||||
self.set("items", [])
|
||||
from erpnext.stock.get_item_details import get_item_details
|
||||
for checked_item in checked_values:
|
||||
item_line = self.append("items", {})
|
||||
price_list, price_list_currency = frappe.db.get_values("Price List", {"selling": 1}, ['name', 'currency'])[0]
|
||||
args = {
|
||||
'doctype': "Sales Invoice",
|
||||
'item_code': checked_item['item'],
|
||||
'company': self.company,
|
||||
'customer': frappe.db.get_value("Patient", self.patient, "customer"),
|
||||
'selling_price_list': price_list,
|
||||
'price_list_currency': price_list_currency,
|
||||
'plc_conversion_rate': 1.0,
|
||||
'conversion_rate': 1.0
|
||||
}
|
||||
item_details = get_item_details(args)
|
||||
item_line.item_code = checked_item['item']
|
||||
item_line.qty = 1
|
||||
if checked_item['qty']:
|
||||
item_line.qty = checked_item['qty']
|
||||
if checked_item['rate']:
|
||||
item_line.rate = checked_item['rate']
|
||||
else:
|
||||
item_line.rate = item_details.price_list_rate
|
||||
item_line.amount = float(item_line.rate) * float(item_line.qty)
|
||||
if checked_item['income_account']:
|
||||
item_line.income_account = checked_item['income_account']
|
||||
if checked_item['dt']:
|
||||
item_line.reference_dt = checked_item['dt']
|
||||
if checked_item['dn']:
|
||||
item_line.reference_dn = checked_item['dn']
|
||||
if checked_item['description']:
|
||||
item_line.description = checked_item['description']
|
||||
|
||||
self.set_missing_values(for_validate = True)
|
||||
|
||||
def set_status(self, update=False, status=None, update_modified=True):
|
||||
if self.is_new():
|
||||
if self.get('amended_from'):
|
||||
self.status = 'Draft'
|
||||
return
|
||||
|
||||
precision = self.precision("outstanding_amount")
|
||||
outstanding_amount = flt(self.outstanding_amount, precision)
|
||||
due_date = getdate(self.due_date)
|
||||
nowdate = getdate()
|
||||
|
||||
discounting_status = None
|
||||
if self.is_discounted:
|
||||
discounting_status = get_discounting_status(self.name)
|
||||
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:
|
||||
@ -1408,15 +1438,13 @@ class SalesInvoice(SellingController):
|
||||
elif self.docstatus == 1:
|
||||
if self.is_internal_transfer():
|
||||
self.status = 'Internal Transfer'
|
||||
elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discounting_status=='Disbursed':
|
||||
self.status = "Overdue and Discounted"
|
||||
elif outstanding_amount > 0 and due_date < nowdate:
|
||||
elif is_overdue(self, total):
|
||||
self.status = "Overdue"
|
||||
elif outstanding_amount > 0 and due_date >= nowdate and self.is_discounted and discounting_status=='Disbursed':
|
||||
self.status = "Unpaid and Discounted"
|
||||
elif outstanding_amount > 0 and due_date >= nowdate:
|
||||
elif 0 < outstanding_amount < total:
|
||||
self.status = "Partly Paid"
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
#Check if outstanding amount is 0 due to credit note issued against invoice
|
||||
# Check if outstanding amount is 0 due to credit note issued against invoice
|
||||
elif outstanding_amount <= 0 and self.is_return == 0 and frappe.db.get_value('Sales Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}):
|
||||
self.status = "Credit Note Issued"
|
||||
elif self.is_return == 1:
|
||||
@ -1425,12 +1453,57 @@ class SalesInvoice(SellingController):
|
||||
self.status = "Paid"
|
||||
else:
|
||||
self.status = "Submitted"
|
||||
|
||||
if (
|
||||
self.status in ("Unpaid", "Partly Paid", "Overdue")
|
||||
and self.is_discounted
|
||||
and get_discounting_status(self.name) == "Disbursed"
|
||||
):
|
||||
self.status += " and Discounted"
|
||||
|
||||
else:
|
||||
self.status = "Draft"
|
||||
|
||||
if update:
|
||||
self.db_set('status', self.status, update_modified = update_modified)
|
||||
|
||||
|
||||
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
|
||||
|
||||
today = getdate()
|
||||
if doc.get('is_pos') or not doc.get('payment_schedule'):
|
||||
return getdate(doc.due_date) < today
|
||||
|
||||
# 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
|
||||
|
||||
|
||||
def get_discounting_status(sales_invoice):
|
||||
status = None
|
||||
|
||||
@ -1905,22 +1978,23 @@ def update_multi_mode_option(doc, pos_profile):
|
||||
def append_payment(payment_mode):
|
||||
payment = doc.append('payments', {})
|
||||
payment.default = payment_mode.default
|
||||
payment.mode_of_payment = payment_mode.parent
|
||||
payment.mode_of_payment = payment_mode.mop
|
||||
payment.account = payment_mode.default_account
|
||||
payment.type = payment_mode.type
|
||||
|
||||
doc.set('payments', [])
|
||||
invalid_modes = []
|
||||
for pos_payment_method in pos_profile.get('payments'):
|
||||
pos_payment_method = pos_payment_method.as_dict()
|
||||
mode_of_payments = [d.mode_of_payment for d in pos_profile.get('payments')]
|
||||
mode_of_payments_info = get_mode_of_payments_info(mode_of_payments, doc.company)
|
||||
|
||||
payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company)
|
||||
for row in pos_profile.get('payments'):
|
||||
payment_mode = mode_of_payments_info.get(row.mode_of_payment)
|
||||
if not payment_mode:
|
||||
invalid_modes.append(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment))
|
||||
invalid_modes.append(get_link_to_form("Mode of Payment", row.mode_of_payment))
|
||||
continue
|
||||
|
||||
payment_mode[0].default = pos_payment_method.default
|
||||
append_payment(payment_mode[0])
|
||||
payment_mode.default = row.default
|
||||
append_payment(payment_mode)
|
||||
|
||||
if invalid_modes:
|
||||
if invalid_modes == 1:
|
||||
@ -1936,6 +2010,24 @@ def get_all_mode_of_payments(doc):
|
||||
where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""",
|
||||
{'company': doc.company}, as_dict=1)
|
||||
|
||||
def get_mode_of_payments_info(mode_of_payments, company):
|
||||
data = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
mpa.default_account, mpa.parent as mop, mp.type as type
|
||||
from
|
||||
`tabMode of Payment Account` mpa,`tabMode of Payment` mp
|
||||
where
|
||||
mpa.parent = mp.name and
|
||||
mpa.company = %s and
|
||||
mp.enabled = 1 and
|
||||
mp.name in (%s)
|
||||
group by
|
||||
mp.name
|
||||
""", (company, mode_of_payments), as_dict=1)
|
||||
|
||||
return {row.get('mop'): row for row in data}
|
||||
|
||||
def get_mode_of_payment_info(mode_of_payment, company):
|
||||
return frappe.db.sql("""
|
||||
select mpa.default_account, mpa.parent, mp.type as type
|
||||
|
@ -6,18 +6,20 @@ frappe.listview_settings['Sales Invoice'] = {
|
||||
add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company",
|
||||
"currency", "is_return"],
|
||||
get_indicator: function(doc) {
|
||||
var status_color = {
|
||||
const status_colors = {
|
||||
"Draft": "grey",
|
||||
"Unpaid": "orange",
|
||||
"Paid": "green",
|
||||
"Return": "gray",
|
||||
"Credit Note Issued": "gray",
|
||||
"Unpaid and Discounted": "orange",
|
||||
"Partly Paid and Discounted": "yellow",
|
||||
"Overdue and Discounted": "red",
|
||||
"Overdue": "red",
|
||||
"Partly Paid": "yellow",
|
||||
"Internal Transfer": "darkgrey"
|
||||
};
|
||||
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
|
||||
return [__(doc.status), status_colors[doc.status], "status,=,"+doc.status];
|
||||
},
|
||||
right_column: "grand_total"
|
||||
};
|
||||
|
@ -133,6 +133,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
def test_payment_entry_unlink_against_invoice(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
si = frappe.copy_doc(test_records[0])
|
||||
si.is_pos = 0
|
||||
si.insert()
|
||||
@ -156,6 +157,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
def test_payment_entry_unlink_against_standalone_credit_note(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
si1 = create_sales_invoice(rate=1000)
|
||||
si2 = create_sales_invoice(rate=300)
|
||||
si3 = create_sales_invoice(qty=-1, rate=300, is_return=1)
|
||||
@ -1085,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
|
||||
@ -1420,15 +1420,22 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
itemised_tax, itemised_taxable_amount = get_itemised_tax_breakup_data(si)
|
||||
|
||||
expected_itemised_tax = {
|
||||
"999800": {
|
||||
"_Test Item": {
|
||||
"Service Tax": {
|
||||
"tax_rate": 10.0,
|
||||
"tax_amount": 1500.0
|
||||
"tax_amount": 1000.0
|
||||
}
|
||||
},
|
||||
"_Test Item 2": {
|
||||
"Service Tax": {
|
||||
"tax_rate": 10.0,
|
||||
"tax_amount": 500.0
|
||||
}
|
||||
}
|
||||
}
|
||||
expected_itemised_taxable_amount = {
|
||||
"999800": 15000.0
|
||||
"_Test Item": 10000.0,
|
||||
"_Test Item 2": 5000.0
|
||||
}
|
||||
|
||||
self.assertEqual(itemised_tax, expected_itemised_tax)
|
||||
@ -1639,6 +1646,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
def test_credit_note(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
si = create_sales_invoice(item_code = "_Test Item", qty = (5 * -1), rate=500, is_return = 1)
|
||||
|
||||
outstanding_amount = get_outstanding_amount(si.doctype,
|
||||
@ -1790,6 +1798,47 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
check_gl_entries(self, si.name, expected_gle, "2019-01-30")
|
||||
|
||||
def test_deferred_revenue_post_account_freeze_upto_by_admin(self):
|
||||
frappe.set_user("Administrator")
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
|
||||
|
||||
deferred_account = create_account(account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
|
||||
item = create_item("_Test Item for Deferred Accounting")
|
||||
item.enable_deferred_revenue = 1
|
||||
item.deferred_revenue_account = deferred_account
|
||||
item.no_of_months = 12
|
||||
item.save()
|
||||
|
||||
si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_save=True)
|
||||
si.items[0].enable_deferred_revenue = 1
|
||||
si.items[0].service_start_date = "2019-01-10"
|
||||
si.items[0].service_end_date = "2019-03-15"
|
||||
si.items[0].deferred_revenue_account = deferred_account
|
||||
si.save()
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', getdate('2019-01-31'))
|
||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', 'System Manager')
|
||||
|
||||
pda1 = frappe.get_doc(dict(
|
||||
doctype='Process Deferred Accounting',
|
||||
posting_date=nowdate(),
|
||||
start_date="2019-01-01",
|
||||
end_date="2019-03-31",
|
||||
type="Income",
|
||||
company="_Test Company"
|
||||
))
|
||||
|
||||
pda1.insert()
|
||||
self.assertRaises(frappe.ValidationError, pda1.submit)
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
frappe.db.set_value('Accounts Settings', None, 'frozen_accounts_modifier', None)
|
||||
|
||||
def test_fixed_deferred_revenue(self):
|
||||
deferred_account = create_account(account_name="Deferred Revenue",
|
||||
parent_account="Current Liabilities - _TC", company="_Test Company")
|
||||
@ -1972,11 +2021,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
frappe.local.enable_perpetual_inventory['_Test Company 1'] = old_perpetual_inventory
|
||||
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", old_negative_stock)
|
||||
|
||||
def test_sle_if_target_warehouse_exists_accidentally(self):
|
||||
"""
|
||||
Check if inward entry exists if Target Warehouse accidentally exists
|
||||
but Customer is not an internal customer.
|
||||
"""
|
||||
def test_sle_for_target_warehouse(self):
|
||||
se = make_stock_entry(
|
||||
item_code="138-CMS Shoe",
|
||||
target="Finished Goods - _TC",
|
||||
@ -1997,9 +2042,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
sles = frappe.get_all("Stock Ledger Entry", filters={"voucher_no": si.name},
|
||||
fields=["name", "actual_qty"])
|
||||
|
||||
# check if only one SLE for outward entry is created
|
||||
self.assertEqual(len(sles), 1)
|
||||
self.assertEqual(sles[0].actual_qty, -1)
|
||||
# check if both SLEs are created
|
||||
self.assertEqual(len(sles), 2)
|
||||
self.assertEqual(sum(d.actual_qty for d in sles), 0.0)
|
||||
|
||||
# tear down
|
||||
si.cancel()
|
||||
@ -2192,9 +2237,9 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
check_gl_entries(self, si.name, expected_gle, add_days(nowdate(), -1))
|
||||
enable_discount_accounting(enable=0)
|
||||
|
||||
def test_asset_depreciation_on_sale(self):
|
||||
def test_asset_depreciation_on_sale_with_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on Sept 30.
|
||||
Tests if an Asset set to depreciate yearly on June 30, that gets sold on Sept 30, creates an additional depreciation entry on its date of sale.
|
||||
"""
|
||||
|
||||
create_asset_data()
|
||||
@ -2207,7 +2252,7 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.48],
|
||||
["2021-06-30", 20000.0, 21311.48],
|
||||
["2021-09-30", 3966.76, 25278.24]
|
||||
["2021-09-30", 5041.1, 26352.58]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
@ -2216,6 +2261,59 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertTrue(schedule.journal_entry)
|
||||
|
||||
def test_asset_depreciation_on_sale_without_pro_rata(self):
|
||||
"""
|
||||
Tests if an Asset set to depreciate yearly on Dec 31, that gets sold on Dec 31 after two years, created an additional depreciation entry on its date of sale.
|
||||
"""
|
||||
|
||||
create_asset_data()
|
||||
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1,
|
||||
available_for_use_date=getdate("2019-12-31"), total_number_of_depreciations=3,
|
||||
expected_value_after_useful_life=10000, depreciation_start_date=getdate("2020-12-31"), submit=1)
|
||||
|
||||
post_depreciation_entries(getdate("2021-09-30"))
|
||||
|
||||
create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-12-31"))
|
||||
asset.load_from_db()
|
||||
|
||||
expected_values = [
|
||||
["2020-12-31", 30000, 30000],
|
||||
["2021-12-31", 30000, 60000]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertTrue(schedule.journal_entry)
|
||||
|
||||
def test_depreciation_on_return_of_sold_asset(self):
|
||||
from erpnext.controllers.sales_and_purchase_return import make_return_doc
|
||||
|
||||
create_asset_data()
|
||||
asset = create_asset(item_code="Macbook Pro", calculate_depreciation=1, submit=1)
|
||||
post_depreciation_entries(getdate("2021-09-30"))
|
||||
|
||||
si = create_sales_invoice(item_code="Macbook Pro", asset=asset.name, qty=1, rate=90000, posting_date=getdate("2021-09-30"))
|
||||
return_si = make_return_doc("Sales Invoice", si.name)
|
||||
return_si.submit()
|
||||
asset.load_from_db()
|
||||
|
||||
expected_values = [
|
||||
["2020-06-30", 1311.48, 1311.48, True],
|
||||
["2021-06-30", 20000.0, 21311.48, True],
|
||||
["2022-06-30", 20000.0, 41311.48, False],
|
||||
["2023-06-30", 20000.0, 61311.48, False],
|
||||
["2024-06-30", 20000.0, 81311.48, False],
|
||||
["2025-06-06", 18688.52, 100000.0, False]
|
||||
]
|
||||
|
||||
for i, schedule in enumerate(asset.schedules):
|
||||
self.assertEqual(getdate(expected_values[i][0]), schedule.schedule_date)
|
||||
self.assertEqual(expected_values[i][1], schedule.depreciation_amount)
|
||||
self.assertEqual(expected_values[i][2], schedule.accumulated_depreciation_amount)
|
||||
self.assertEqual(schedule.journal_entry, schedule.journal_entry)
|
||||
|
||||
def test_sales_invoice_against_supplier(self):
|
||||
from erpnext.accounts.doctype.opening_invoice_creation_tool.test_opening_invoice_creation_tool import (
|
||||
make_customer,
|
||||
@ -2262,6 +2360,66 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
party_link.delete()
|
||||
frappe.db.set_value('Accounts Settings', None, 'enable_common_party_accounting', 0)
|
||||
|
||||
def test_payment_statuses(self):
|
||||
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
|
||||
|
||||
today = nowdate()
|
||||
|
||||
# Test Overdue
|
||||
si = create_sales_invoice(do_not_submit=True)
|
||||
si.payment_schedule = []
|
||||
si.append("payment_schedule", {
|
||||
"due_date": add_days(today, -5),
|
||||
"invoice_portion": 50,
|
||||
"payment_amount": si.grand_total / 2
|
||||
})
|
||||
si.append("payment_schedule", {
|
||||
"due_date": add_days(today, 5),
|
||||
"invoice_portion": 50,
|
||||
"payment_amount": si.grand_total / 2
|
||||
})
|
||||
si.submit()
|
||||
self.assertEqual(si.status, "Overdue")
|
||||
|
||||
# Test payment less than due amount
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_amount = 1
|
||||
pe.references[0].allocated_amount = pe.paid_amount
|
||||
pe.submit()
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Overdue")
|
||||
|
||||
# Test Partly Paid
|
||||
pe = frappe.copy_doc(pe)
|
||||
pe.paid_amount = si.grand_total / 2
|
||||
pe.references[0].allocated_amount = pe.paid_amount
|
||||
pe.submit()
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Partly Paid")
|
||||
|
||||
# Test Paid
|
||||
pe = get_payment_entry("Sales Invoice", si.name, bank_account="_Test Bank - _TC")
|
||||
pe.reference_no = "1"
|
||||
pe.reference_date = nowdate()
|
||||
pe.paid_amount = si.outstanding_amount
|
||||
pe.submit()
|
||||
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-.#####'
|
||||
@ -2294,6 +2452,7 @@ def make_test_address_for_ewaybill():
|
||||
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
|
||||
address = frappe.get_doc({
|
||||
"address_line1": "_Test Address Line 1",
|
||||
"address_line2": "_Test Address Line 2",
|
||||
"address_title": "_Test Address for Eway bill",
|
||||
"address_type": "Billing",
|
||||
"city": "_Test City",
|
||||
@ -2315,11 +2474,12 @@ def make_test_address_for_ewaybill():
|
||||
|
||||
address.save()
|
||||
|
||||
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
|
||||
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Billing'):
|
||||
address = frappe.get_doc({
|
||||
"address_line1": "_Test Address Line 1",
|
||||
"address_line2": "_Test Address Line 2",
|
||||
"address_title": "_Test Customer-Address for Eway bill",
|
||||
"address_type": "Shipping",
|
||||
"address_type": "Billing",
|
||||
"city": "_Test City",
|
||||
"state": "Test State",
|
||||
"country": "India",
|
||||
@ -2339,9 +2499,34 @@ def make_test_address_for_ewaybill():
|
||||
|
||||
address.save()
|
||||
|
||||
if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
|
||||
address = frappe.get_doc({
|
||||
"address_line1": "_Test Address Line 1",
|
||||
"address_line2": "_Test Address Line 2",
|
||||
"address_title": "_Test Customer-Address for Eway bill",
|
||||
"address_type": "Shipping",
|
||||
"city": "_Test City",
|
||||
"state": "Test State",
|
||||
"country": "India",
|
||||
"doctype": "Address",
|
||||
"is_primary_address": 1,
|
||||
"phone": "+910000000000",
|
||||
"gst_state": "Maharashtra",
|
||||
"gst_state_number": "27",
|
||||
"pincode": "410098"
|
||||
}).insert()
|
||||
|
||||
address.append("links", {
|
||||
"link_doctype": "Customer",
|
||||
"link_name": "_Test Customer"
|
||||
})
|
||||
|
||||
address.save()
|
||||
|
||||
if not frappe.db.exists('Address', '_Test Dispatch-Address for Eway bill-Shipping'):
|
||||
address = frappe.get_doc({
|
||||
"address_line1": "_Test Dispatch Address Line 1",
|
||||
"address_line2": "_Test Dispatch Address Line 2",
|
||||
"address_title": "_Test Dispatch-Address for Eway bill",
|
||||
"address_type": "Shipping",
|
||||
"city": "_Test City",
|
||||
@ -2356,11 +2541,6 @@ def make_test_address_for_ewaybill():
|
||||
"pincode": "1100101"
|
||||
}).insert()
|
||||
|
||||
address.append("links", {
|
||||
"link_doctype": "Company",
|
||||
"link_name": "_Test Company"
|
||||
})
|
||||
|
||||
address.save()
|
||||
|
||||
def make_test_transporter_for_ewaybill():
|
||||
@ -2400,7 +2580,8 @@ def make_sales_invoice_for_ewaybill():
|
||||
|
||||
si.distance = 2000
|
||||
si.company_address = "_Test Address for Eway bill-Billing"
|
||||
si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
|
||||
si.customer_address = "_Test Customer-Address for Eway bill-Billing"
|
||||
si.shipping_address_name = "_Test Customer-Address for Eway bill-Shipping"
|
||||
si.dispatch_address_name = "_Test Dispatch-Address for Eway bill-Shipping"
|
||||
si.vehicle_no = "KA12KA1234"
|
||||
si.gst_category = "Registered Regular"
|
||||
|
@ -98,6 +98,7 @@
|
||||
"width": "120px"
|
||||
},
|
||||
{
|
||||
"depends_on": "exchange_gain_loss",
|
||||
"fieldname": "exchange_gain_loss",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Exchange Gain/Loss",
|
||||
@ -105,6 +106,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "exchange_gain_loss",
|
||||
"fieldname": "ref_exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Reference Exchange Rate",
|
||||
@ -116,7 +118,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-04 20:25:49.832052",
|
||||
"modified": "2021-09-26 15:47:46.911595",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Advance",
|
||||
|
@ -7,12 +7,19 @@
|
||||
"field_order": [
|
||||
"activity_type",
|
||||
"description",
|
||||
"billing_hours",
|
||||
"billing_amount",
|
||||
"section_break_3",
|
||||
"from_time",
|
||||
"column_break_5",
|
||||
"to_time",
|
||||
"section_break_7",
|
||||
"billing_hours",
|
||||
"column_break_9",
|
||||
"billing_amount",
|
||||
"section_break_11",
|
||||
"time_sheet",
|
||||
"project_name",
|
||||
"timesheet_detail"
|
||||
"timesheet_detail",
|
||||
"column_break_13",
|
||||
"project_name"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -64,20 +71,53 @@
|
||||
"label": "Description",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "From Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_time",
|
||||
"fieldtype": "Datetime",
|
||||
"label": "To Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_3",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Time"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_5",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_9",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Reference"
|
||||
},
|
||||
{
|
||||
"fieldname": "project_name",
|
||||
"fieldtype": "Data",
|
||||
"label": "Project Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-06-08 14:43:02.748981",
|
||||
"modified": "2021-10-02 03:48:44.979777",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Timesheet",
|
||||
|
@ -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):
|
||||
@ -400,6 +416,7 @@ class Subscription(Document):
|
||||
|
||||
invoice.flags.ignore_mandatory = True
|
||||
|
||||
invoice.set_missing_values()
|
||||
invoice.save()
|
||||
|
||||
if self.submit_invoice:
|
||||
@ -485,10 +502,13 @@ class Subscription(Document):
|
||||
# Check invoice dates and make sure it doesn't have outstanding invoices
|
||||
return getdate() >= getdate(self.current_invoice_start)
|
||||
|
||||
def is_current_invoice_generated(self):
|
||||
def is_current_invoice_generated(self, _current_start_date=None, _current_end_date=None):
|
||||
invoice = self.get_current_invoice()
|
||||
|
||||
if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end):
|
||||
if not (_current_start_date and _current_end_date):
|
||||
_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(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date):
|
||||
return True
|
||||
|
||||
return False
|
||||
@ -505,7 +525,9 @@ class Subscription(Document):
|
||||
if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice():
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
if not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()):
|
||||
if not self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \
|
||||
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)
|
||||
|
||||
@ -545,11 +567,14 @@ class Subscription(Document):
|
||||
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()):
|
||||
if self.generate_new_invoices_past_due_date and not \
|
||||
self.is_current_invoice_generated(self.current_invoice_start, self.current_invoice_end) \
|
||||
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)
|
||||
|
||||
|
||||
@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()
|
||||
|
||||
|
@ -8,7 +8,8 @@ frappe.ui.form.on('Tax Withholding Category', {
|
||||
if (child.company) {
|
||||
return {
|
||||
filters: {
|
||||
'company': child.company
|
||||
'company': child.company,
|
||||
'root_type': ['in', ['Asset', 'Liability']]
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -9,11 +9,35 @@ from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import cint, getdate
|
||||
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
|
||||
class TaxWithholdingCategory(Document):
|
||||
pass
|
||||
def validate(self):
|
||||
self.validate_dates()
|
||||
self.validate_accounts()
|
||||
self.validate_thresholds()
|
||||
|
||||
def validate_dates(self):
|
||||
last_date = None
|
||||
for d in self.get('rates'):
|
||||
if getdate(d.from_date) >= getdate(d.to_date):
|
||||
frappe.throw(_("Row #{0}: From Date cannot be before To Date").format(d.idx))
|
||||
|
||||
# validate overlapping of dates
|
||||
if last_date and getdate(d.to_date) < getdate(last_date):
|
||||
frappe.throw(_("Row #{0}: Dates overlapping with other row").format(d.idx))
|
||||
|
||||
def validate_accounts(self):
|
||||
existing_accounts = []
|
||||
for d in self.get('accounts'):
|
||||
if d.get('account') in existing_accounts:
|
||||
frappe.throw(_("Account {0} added multiple times").format(frappe.bold(d.get('account'))))
|
||||
|
||||
existing_accounts.append(d.get('account'))
|
||||
|
||||
def validate_thresholds(self):
|
||||
for d in self.get('rates'):
|
||||
if d.cumulative_threshold and d.cumulative_threshold < d.single_threshold:
|
||||
frappe.throw(_("Row #{0}: Cumulative threshold cannot be less than Single Transaction threshold").format(d.idx))
|
||||
|
||||
def get_party_details(inv):
|
||||
party_type, party = '', ''
|
||||
@ -34,15 +58,24 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
pan_no = ''
|
||||
parties = []
|
||||
party_type, party = get_party_details(inv)
|
||||
has_pan_field = frappe.get_meta(party_type).has_field("pan")
|
||||
|
||||
if not tax_withholding_category:
|
||||
tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan'])
|
||||
if has_pan_field:
|
||||
fields = ['tax_withholding_category', 'pan']
|
||||
else:
|
||||
fields = ['tax_withholding_category']
|
||||
|
||||
tax_withholding_details = frappe.db.get_value(party_type, party, fields, as_dict=1)
|
||||
|
||||
tax_withholding_category = tax_withholding_details.get('tax_withholding_category')
|
||||
pan_no = tax_withholding_details.get('pan')
|
||||
|
||||
if not tax_withholding_category:
|
||||
return
|
||||
|
||||
# if tax_withholding_category passed as an argument but not pan_no
|
||||
if not pan_no:
|
||||
if not pan_no and has_pan_field:
|
||||
pan_no = frappe.db.get_value(party_type, party, 'pan')
|
||||
|
||||
# Get others suppliers with the same PAN No
|
||||
@ -52,8 +85,8 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
if not parties:
|
||||
parties.append(party)
|
||||
|
||||
fiscal_year = get_fiscal_year(inv.get('posting_date') or inv.get('transaction_date'), company=inv.company)
|
||||
tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
|
||||
posting_date = inv.get('posting_date') or inv.get('transaction_date')
|
||||
tax_details = get_tax_withholding_details(tax_withholding_category, posting_date, inv.company)
|
||||
|
||||
if not tax_details:
|
||||
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
|
||||
@ -67,7 +100,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
tax_amount, tax_deducted = get_tax_amount(
|
||||
party_type, parties,
|
||||
inv, tax_details,
|
||||
fiscal_year, pan_no
|
||||
posting_date, pan_no
|
||||
)
|
||||
|
||||
if party_type == 'Supplier':
|
||||
@ -77,16 +110,19 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
|
||||
|
||||
return tax_row
|
||||
|
||||
def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
|
||||
def get_tax_withholding_details(tax_withholding_category, posting_date, company):
|
||||
tax_withholding = frappe.get_doc("Tax Withholding Category", tax_withholding_category)
|
||||
|
||||
tax_rate_detail = get_tax_withholding_rates(tax_withholding, fiscal_year)
|
||||
tax_rate_detail = get_tax_withholding_rates(tax_withholding, posting_date)
|
||||
|
||||
for account_detail in tax_withholding.accounts:
|
||||
if company == account_detail.company:
|
||||
return frappe._dict({
|
||||
"tax_withholding_category": tax_withholding_category,
|
||||
"account_head": account_detail.account,
|
||||
"rate": tax_rate_detail.tax_withholding_rate,
|
||||
"from_date": tax_rate_detail.from_date,
|
||||
"to_date": tax_rate_detail.to_date,
|
||||
"threshold": tax_rate_detail.single_threshold,
|
||||
"cumulative_threshold": tax_rate_detail.cumulative_threshold,
|
||||
"description": tax_withholding.category_name if tax_withholding.category_name else tax_withholding_category,
|
||||
@ -95,13 +131,13 @@ def get_tax_withholding_details(tax_withholding_category, fiscal_year, company):
|
||||
"round_off_tax_amount": tax_withholding.round_off_tax_amount
|
||||
})
|
||||
|
||||
def get_tax_withholding_rates(tax_withholding, fiscal_year):
|
||||
def get_tax_withholding_rates(tax_withholding, posting_date):
|
||||
# returns the row that matches with the fiscal year from posting date
|
||||
for rate in tax_withholding.rates:
|
||||
if rate.fiscal_year == fiscal_year:
|
||||
if getdate(rate.from_date) <= getdate(posting_date) <= getdate(rate.to_date):
|
||||
return rate
|
||||
|
||||
frappe.throw(_("No Tax Withholding data found for the current Fiscal Year."))
|
||||
frappe.throw(_("No Tax Withholding data found for the current posting date."))
|
||||
|
||||
def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
|
||||
row = {
|
||||
@ -143,38 +179,39 @@ def get_tax_row_for_tds(tax_details, tax_amount):
|
||||
"account_head": tax_details.account_head
|
||||
}
|
||||
|
||||
def get_lower_deduction_certificate(fiscal_year, pan_no):
|
||||
ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name')
|
||||
def get_lower_deduction_certificate(tax_details, pan_no):
|
||||
ldc_name = frappe.db.get_value('Lower Deduction Certificate',
|
||||
{
|
||||
'pan_no': pan_no,
|
||||
'tax_withholding_category': tax_details.tax_withholding_category,
|
||||
'valid_from': ('>=', tax_details.from_date),
|
||||
'valid_upto': ('<=', tax_details.to_date)
|
||||
}, 'name')
|
||||
|
||||
if ldc_name:
|
||||
return frappe.get_doc('Lower Deduction Certificate', ldc_name)
|
||||
|
||||
def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
|
||||
fiscal_year = fiscal_year_details[0]
|
||||
|
||||
|
||||
vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
|
||||
advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
|
||||
def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=None):
|
||||
vouchers = get_invoice_vouchers(parties, tax_details, inv.company, party_type=party_type)
|
||||
advance_vouchers = get_advance_vouchers(parties, company=inv.company, from_date=tax_details.from_date,
|
||||
to_date=tax_details.to_date, party_type=party_type)
|
||||
taxable_vouchers = vouchers + advance_vouchers
|
||||
|
||||
tax_deducted = 0
|
||||
if taxable_vouchers:
|
||||
tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
|
||||
tax_deducted = get_deducted_tax(taxable_vouchers, tax_details)
|
||||
|
||||
tax_amount = 0
|
||||
posting_date = inv.get('posting_date') or inv.get('transaction_date')
|
||||
if party_type == 'Supplier':
|
||||
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
|
||||
ldc = get_lower_deduction_certificate(tax_details, pan_no)
|
||||
if tax_deducted:
|
||||
net_total = inv.net_total
|
||||
if ldc:
|
||||
tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total)
|
||||
tax_amount = get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total)
|
||||
else:
|
||||
tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
|
||||
else:
|
||||
tax_amount = get_tds_amount(
|
||||
ldc, parties, inv, tax_details,
|
||||
fiscal_year_details, tax_deducted, vouchers
|
||||
)
|
||||
tax_amount = get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers)
|
||||
|
||||
elif party_type == 'Customer':
|
||||
if tax_deducted:
|
||||
@ -183,29 +220,50 @@ def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, p
|
||||
else:
|
||||
# if no TCS has been charged in FY,
|
||||
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
||||
tax_amount = get_tcs_amount(
|
||||
parties, inv, tax_details,
|
||||
fiscal_year_details, vouchers, advance_vouchers
|
||||
)
|
||||
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, fiscal_year, company, party_type='Supplier'):
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
|
||||
dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
|
||||
doctype = 'Purchase Invoice' if party_type == 'Supplier' else 'Sales Invoice'
|
||||
|
||||
filters = {
|
||||
dr_or_cr: ['>', 0],
|
||||
'company': company,
|
||||
'party_type': party_type,
|
||||
'party': ['in', parties],
|
||||
'fiscal_year': fiscal_year,
|
||||
frappe.scrub(party_type): ['in', parties],
|
||||
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
|
||||
'is_opening': 'No',
|
||||
'is_cancelled': 0
|
||||
'docstatus': 1
|
||||
}
|
||||
|
||||
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""]
|
||||
if not tax_details.get('consider_party_ledger_amount') and doctype != "Sales Invoice":
|
||||
filters.update({
|
||||
'apply_tds': 1,
|
||||
'tax_withholding_category': tax_details.get('tax_withholding_category')
|
||||
})
|
||||
|
||||
def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'):
|
||||
invoices = frappe.get_all(doctype, filters=filters, pluck="name") or [""]
|
||||
|
||||
journal_entries = frappe.db.sql("""
|
||||
SELECT j.name
|
||||
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
|
||||
WHERE
|
||||
j.docstatus = 1
|
||||
AND j.is_opening = 'No'
|
||||
AND j.posting_date between %s and %s
|
||||
AND ja.{dr_or_cr} > 0
|
||||
AND ja.party in %s
|
||||
""".format(dr_or_cr=dr_or_cr), (tax_details.from_date, tax_details.to_date, tuple(parties)), as_list=1)
|
||||
|
||||
if journal_entries:
|
||||
journal_entries = journal_entries[0]
|
||||
|
||||
return invoices + journal_entries
|
||||
|
||||
def get_advance_vouchers(parties, company=None, from_date=None, to_date=None, party_type='Supplier'):
|
||||
# for advance vouchers, debit and credit is reversed
|
||||
dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit'
|
||||
|
||||
@ -218,8 +276,6 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None
|
||||
'against_voucher': ['is', 'not set']
|
||||
}
|
||||
|
||||
if fiscal_year:
|
||||
filters['fiscal_year'] = fiscal_year
|
||||
if company:
|
||||
filters['company'] = company
|
||||
if from_date and to_date:
|
||||
@ -227,20 +283,21 @@ def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None
|
||||
|
||||
return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
|
||||
|
||||
def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
|
||||
def get_deducted_tax(taxable_vouchers, tax_details):
|
||||
# check if TDS / TCS account is already charged on taxable vouchers
|
||||
filters = {
|
||||
'is_cancelled': 0,
|
||||
'credit': ['>', 0],
|
||||
'fiscal_year': fiscal_year,
|
||||
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
|
||||
'account': tax_details.account_head,
|
||||
'voucher_no': ['in', taxable_vouchers],
|
||||
}
|
||||
field = "sum(credit)"
|
||||
field = "credit"
|
||||
|
||||
return frappe.db.get_value('GL Entry', filters, field) or 0.0
|
||||
entries = frappe.db.get_all('GL Entry', filters, pluck=field)
|
||||
return sum(entries)
|
||||
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
|
||||
def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
tds_amount = 0
|
||||
invoice_filters = {
|
||||
'name': ('in', vouchers),
|
||||
@ -264,7 +321,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
|
||||
supp_credit_amt += supp_jv_credit_amt
|
||||
supp_credit_amt += inv.net_total
|
||||
|
||||
debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, inv.company)
|
||||
debit_note_amount = get_debit_note_amount(parties, tax_details.from_date, tax_details.to_date, inv.company)
|
||||
supp_credit_amt -= debit_note_amount
|
||||
|
||||
threshold = tax_details.get('threshold', 0)
|
||||
@ -287,14 +344,10 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
|
||||
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, fiscal_year_details, vouchers, adv_vouchers):
|
||||
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
tcs_amount = 0
|
||||
fiscal_year, _, _ = fiscal_year_details
|
||||
|
||||
# sum of debit entries made from sales invoices
|
||||
invoiced_amt = frappe.db.get_value('GL Entry', {
|
||||
@ -313,14 +366,14 @@ def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv
|
||||
}, 'sum(credit)') or 0.0
|
||||
|
||||
# sum of credit entries made from sales invoice
|
||||
credit_note_amt = frappe.db.get_value('GL Entry', {
|
||||
credit_note_amt = sum(frappe.db.get_all('GL Entry', {
|
||||
'is_cancelled': 0,
|
||||
'credit': ['>', 0],
|
||||
'party': ['in', parties],
|
||||
'fiscal_year': fiscal_year,
|
||||
'posting_date': ['between', (tax_details.from_date, tax_details.to_date)],
|
||||
'company': inv.company,
|
||||
'voucher_type': 'Sales Invoice',
|
||||
}, 'sum(credit)') or 0.0
|
||||
}, pluck='credit'))
|
||||
|
||||
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
|
||||
|
||||
@ -339,7 +392,7 @@ def get_invoice_total_without_tcs(inv, tax_details):
|
||||
|
||||
return inv.grand_total - tcs_tax_row_amount
|
||||
|
||||
def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total):
|
||||
def get_tds_amount_from_ldc(ldc, parties, pan_no, tax_details, posting_date, net_total):
|
||||
tds_amount = 0
|
||||
limit_consumed = frappe.db.get_value('Purchase Invoice', {
|
||||
'supplier': ('in', parties),
|
||||
@ -356,14 +409,13 @@ def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, post
|
||||
|
||||
return tds_amount
|
||||
|
||||
def get_debit_note_amount(suppliers, fiscal_year_details, company=None):
|
||||
_, year_start_date, year_end_date = fiscal_year_details
|
||||
def get_debit_note_amount(suppliers, from_date, to_date, company=None):
|
||||
|
||||
filters = {
|
||||
'supplier': ['in', suppliers],
|
||||
'is_return': 1,
|
||||
'docstatus': 1,
|
||||
'posting_date': ['between', (year_start_date, year_end_date)]
|
||||
'posting_date': ['between', (from_date, to_date)]
|
||||
}
|
||||
fields = ['abs(sum(net_total)) as net_total']
|
||||
|
||||
|
@ -176,6 +176,29 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
for d in invoices:
|
||||
d.cancel()
|
||||
|
||||
def test_multi_category_single_supplier(self):
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier5", "tax_withholding_category", "Test Service Category")
|
||||
invoices = []
|
||||
|
||||
pi = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 500, do_not_save=True)
|
||||
pi.tax_withholding_category = "Test Service Category"
|
||||
pi.save()
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# Second Invoice will apply TDS checked
|
||||
pi1 = create_purchase_invoice(supplier = "Test TDS Supplier5", rate = 2500, do_not_save=True)
|
||||
pi1.tax_withholding_category = "Test Goods Category"
|
||||
pi1.save()
|
||||
pi1.submit()
|
||||
invoices.append(pi1)
|
||||
|
||||
self.assertEqual(pi1.taxes[0].tax_amount, 250)
|
||||
|
||||
#delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
d.cancel()
|
||||
|
||||
def cancel_invoices():
|
||||
purchase_invoices = frappe.get_all("Purchase Invoice", {
|
||||
'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
|
||||
@ -251,7 +274,8 @@ def create_sales_invoice(**args):
|
||||
|
||||
def create_records():
|
||||
# create a new suppliers
|
||||
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3', 'Test TDS Supplier4']:
|
||||
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2', 'Test TDS Supplier3',
|
||||
'Test TDS Supplier4', 'Test TDS Supplier5']:
|
||||
if frappe.db.exists('Supplier', name):
|
||||
continue
|
||||
|
||||
@ -313,16 +337,16 @@ def create_records():
|
||||
}).insert()
|
||||
|
||||
def create_tax_with_holding_category():
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
|
||||
|
||||
# Cummulative thresold
|
||||
fiscal_year = get_fiscal_year(today(), company="_Test Company")
|
||||
# Cumulative threshold
|
||||
if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TDS"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Cumulative Threshold TDS",
|
||||
"category_name": "10% TDS",
|
||||
"rates": [{
|
||||
'fiscal_year': fiscal_year,
|
||||
'from_date': fiscal_year[1],
|
||||
'to_date': fiscal_year[2],
|
||||
'tax_withholding_rate': 10,
|
||||
'single_threshold': 0,
|
||||
'cumulative_threshold': 30000.00
|
||||
@ -339,7 +363,8 @@ def create_tax_with_holding_category():
|
||||
"name": "Cumulative Threshold TCS",
|
||||
"category_name": "10% TCS",
|
||||
"rates": [{
|
||||
'fiscal_year': fiscal_year,
|
||||
'from_date': fiscal_year[1],
|
||||
'to_date': fiscal_year[2],
|
||||
'tax_withholding_rate': 10,
|
||||
'single_threshold': 0,
|
||||
'cumulative_threshold': 30000.00
|
||||
@ -357,7 +382,8 @@ def create_tax_with_holding_category():
|
||||
"name": "Single Threshold TDS",
|
||||
"category_name": "10% TDS",
|
||||
"rates": [{
|
||||
'fiscal_year': fiscal_year,
|
||||
'from_date': fiscal_year[1],
|
||||
'to_date': fiscal_year[2],
|
||||
'tax_withholding_rate': 10,
|
||||
'single_threshold': 20000.00,
|
||||
'cumulative_threshold': 0
|
||||
@ -377,7 +403,8 @@ def create_tax_with_holding_category():
|
||||
"consider_party_ledger_amount": 1,
|
||||
"tax_on_excess_amount": 1,
|
||||
"rates": [{
|
||||
'fiscal_year': fiscal_year,
|
||||
'from_date': fiscal_year[1],
|
||||
'to_date': fiscal_year[2],
|
||||
'tax_withholding_rate': 10,
|
||||
'single_threshold': 0,
|
||||
'cumulative_threshold': 30000
|
||||
@ -387,3 +414,39 @@ def create_tax_with_holding_category():
|
||||
'account': 'TDS - _TC'
|
||||
}]
|
||||
}).insert()
|
||||
|
||||
if not frappe.db.exists("Tax Withholding Category", "Test Service Category"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Test Service Category",
|
||||
"category_name": "Test Service Category",
|
||||
"rates": [{
|
||||
'from_date': fiscal_year[1],
|
||||
'to_date': fiscal_year[2],
|
||||
'tax_withholding_rate': 10,
|
||||
'single_threshold': 2000,
|
||||
'cumulative_threshold': 2000
|
||||
}],
|
||||
"accounts": [{
|
||||
'company': '_Test Company',
|
||||
'account': 'TDS - _TC'
|
||||
}]
|
||||
}).insert()
|
||||
|
||||
if not frappe.db.exists("Tax Withholding Category", "Test Goods Category"):
|
||||
frappe.get_doc({
|
||||
"doctype": "Tax Withholding Category",
|
||||
"name": "Test Goods Category",
|
||||
"category_name": "Test Goods Category",
|
||||
"rates": [{
|
||||
'from_date': fiscal_year[1],
|
||||
'to_date': fiscal_year[2],
|
||||
'tax_withholding_rate': 10,
|
||||
'single_threshold': 2000,
|
||||
'cumulative_threshold': 2000
|
||||
}],
|
||||
"accounts": [{
|
||||
'company': '_Test Company',
|
||||
'account': 'TDS - _TC'
|
||||
}]
|
||||
}).insert()
|
||||
|
@ -1,202 +1,72 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-07-17 16:53:13.716665",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2018-07-17 16:53:13.716665",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"from_date",
|
||||
"to_date",
|
||||
"tax_withholding_rate",
|
||||
"column_break_3",
|
||||
"single_threshold",
|
||||
"cumulative_threshold"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "fiscal_year",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Fiscal Year",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Fiscal Year",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 1,
|
||||
"fieldname": "tax_withholding_rate",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Tax Withholding Rate",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 2,
|
||||
"fieldname": "tax_withholding_rate",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Tax Withholding Rate",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 2,
|
||||
"fieldname": "single_threshold",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Single Transaction Threshold"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "single_threshold",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Single Transaction Threshold",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 3,
|
||||
"fieldname": "cumulative_threshold",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Cumulative Transaction Threshold"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 3,
|
||||
"fieldname": "cumulative_threshold",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Cumulative Transaction Threshold",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"columns": 2,
|
||||
"fieldname": "from_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "From Date",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"fieldname": "to_date",
|
||||
"fieldtype": "Date",
|
||||
"in_list_view": 1,
|
||||
"label": "To Date",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-07-17 17:13:09.819580",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Rate",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-31 11:42:12.213977",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Tax Withholding Rate",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -284,13 +284,16 @@ def check_freezing_date(posting_date, adv_adj=False):
|
||||
"""
|
||||
Nobody can do GL Entries where posting date is before freezing date
|
||||
except authorized person
|
||||
|
||||
Administrator has all the roles so this check will be bypassed if any role is allowed to post
|
||||
Hence stop admin to bypass if accounts are freezed
|
||||
"""
|
||||
if not adv_adj:
|
||||
acc_frozen_upto = frappe.db.get_value('Accounts Settings', None, 'acc_frozen_upto')
|
||||
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():
|
||||
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):
|
||||
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"creation": "2021-10-19 18:06:53.083133",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format Field Template",
|
||||
"document_type": "Purchase Invoice",
|
||||
"field": "taxes",
|
||||
"idx": 0,
|
||||
"modified": "2021-10-19 18:06:53.083133",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice Taxes",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"template_file": "templates/print_formats/includes/taxes_and_charges.html"
|
||||
}
|
@ -0,0 +1,16 @@
|
||||
{
|
||||
"creation": "2021-10-19 17:50:00.152759",
|
||||
"docstatus": 0,
|
||||
"doctype": "Print Format Field Template",
|
||||
"document_type": "Sales Invoice",
|
||||
"field": "taxes",
|
||||
"idx": 0,
|
||||
"modified": "2021-10-19 18:13:20.894207",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Taxes",
|
||||
"owner": "Administrator",
|
||||
"standard": 1,
|
||||
"template": "",
|
||||
"template_file": "templates/print_formats/includes/taxes_and_charges.html"
|
||||
}
|
@ -4,7 +4,7 @@
|
||||
frappe.query_reports["Accounts Payable"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"fieldname": "company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
@ -12,19 +12,19 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"default": frappe.defaults.get_user_default("Company")
|
||||
},
|
||||
{
|
||||
"fieldname":"report_date",
|
||||
"fieldname": "report_date",
|
||||
"label": __("Posting Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.get_today()
|
||||
},
|
||||
{
|
||||
"fieldname":"finance_book",
|
||||
"fieldname": "finance_book",
|
||||
"label": __("Finance Book"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"fieldname":"cost_center",
|
||||
"fieldname": "cost_center",
|
||||
"label": __("Cost Center"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Cost Center",
|
||||
@ -38,7 +38,7 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"supplier",
|
||||
"fieldname": "supplier",
|
||||
"label": __("Supplier"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier",
|
||||
@ -54,48 +54,48 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"ageing_based_on",
|
||||
"fieldname": "ageing_based_on",
|
||||
"label": __("Ageing Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": 'Posting Date\nDue Date\nSupplier Invoice Date',
|
||||
"default": "Due Date"
|
||||
},
|
||||
{
|
||||
"fieldname":"range1",
|
||||
"fieldname": "range1",
|
||||
"label": __("Ageing Range 1"),
|
||||
"fieldtype": "Int",
|
||||
"default": "30",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"range2",
|
||||
"fieldname": "range2",
|
||||
"label": __("Ageing Range 2"),
|
||||
"fieldtype": "Int",
|
||||
"default": "60",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"range3",
|
||||
"fieldname": "range3",
|
||||
"label": __("Ageing Range 3"),
|
||||
"fieldtype": "Int",
|
||||
"default": "90",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"range4",
|
||||
"fieldname": "range4",
|
||||
"label": __("Ageing Range 4"),
|
||||
"fieldtype": "Int",
|
||||
"default": "120",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"payment_terms_template",
|
||||
"fieldname": "payment_terms_template",
|
||||
"label": __("Payment Terms Template"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"fieldname":"supplier_group",
|
||||
"fieldname": "supplier_group",
|
||||
"label": __("Supplier Group"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier Group"
|
||||
@ -106,12 +106,17 @@ frappe.query_reports["Accounts Payable"] = {
|
||||
"fieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"fieldname":"based_on_payment_terms",
|
||||
"fieldname": "based_on_payment_terms",
|
||||
"label": __("Based On Payment Terms"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname":"tax_id",
|
||||
"fieldname": "show_remarks",
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_id",
|
||||
"label": __("Tax Id"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
|
@ -4,7 +4,7 @@
|
||||
frappe.query_reports["Accounts Receivable"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname":"company",
|
||||
"fieldname": "company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
@ -12,19 +12,19 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"default": frappe.defaults.get_user_default("Company")
|
||||
},
|
||||
{
|
||||
"fieldname":"report_date",
|
||||
"fieldname": "report_date",
|
||||
"label": __("Posting Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.datetime.get_today()
|
||||
},
|
||||
{
|
||||
"fieldname":"finance_book",
|
||||
"fieldname": "finance_book",
|
||||
"label": __("Finance Book"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Finance Book"
|
||||
},
|
||||
{
|
||||
"fieldname":"cost_center",
|
||||
"fieldname": "cost_center",
|
||||
"label": __("Cost Center"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Cost Center",
|
||||
@ -38,7 +38,7 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"customer",
|
||||
"fieldname": "customer",
|
||||
"label": __("Customer"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer",
|
||||
@ -67,66 +67,66 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"ageing_based_on",
|
||||
"fieldname": "ageing_based_on",
|
||||
"label": __("Ageing Based On"),
|
||||
"fieldtype": "Select",
|
||||
"options": 'Posting Date\nDue Date',
|
||||
"default": "Due Date"
|
||||
},
|
||||
{
|
||||
"fieldname":"range1",
|
||||
"fieldname": "range1",
|
||||
"label": __("Ageing Range 1"),
|
||||
"fieldtype": "Int",
|
||||
"default": "30",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"range2",
|
||||
"fieldname": "range2",
|
||||
"label": __("Ageing Range 2"),
|
||||
"fieldtype": "Int",
|
||||
"default": "60",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"range3",
|
||||
"fieldname": "range3",
|
||||
"label": __("Ageing Range 3"),
|
||||
"fieldtype": "Int",
|
||||
"default": "90",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"range4",
|
||||
"fieldname": "range4",
|
||||
"label": __("Ageing Range 4"),
|
||||
"fieldtype": "Int",
|
||||
"default": "120",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"customer_group",
|
||||
"fieldname": "customer_group",
|
||||
"label": __("Customer Group"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Customer Group"
|
||||
},
|
||||
{
|
||||
"fieldname":"payment_terms_template",
|
||||
"fieldname": "payment_terms_template",
|
||||
"label": __("Payment Terms Template"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Payment Terms Template"
|
||||
},
|
||||
{
|
||||
"fieldname":"sales_partner",
|
||||
"fieldname": "sales_partner",
|
||||
"label": __("Sales Partner"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Partner"
|
||||
},
|
||||
{
|
||||
"fieldname":"sales_person",
|
||||
"fieldname": "sales_person",
|
||||
"label": __("Sales Person"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Sales Person"
|
||||
},
|
||||
{
|
||||
"fieldname":"territory",
|
||||
"fieldname": "territory",
|
||||
"label": __("Territory"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Territory"
|
||||
@ -137,45 +137,50 @@ frappe.query_reports["Accounts Receivable"] = {
|
||||
"fieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"fieldname":"based_on_payment_terms",
|
||||
"fieldname": "based_on_payment_terms",
|
||||
"label": __("Based On Payment Terms"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname":"show_future_payments",
|
||||
"fieldname": "show_future_payments",
|
||||
"label": __("Show Future Payments"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname":"show_delivery_notes",
|
||||
"fieldname": "show_delivery_notes",
|
||||
"label": __("Show Linked Delivery Notes"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname":"show_sales_person",
|
||||
"fieldname": "show_sales_person",
|
||||
"label": __("Show Sales Person"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname":"tax_id",
|
||||
"fieldname": "show_remarks",
|
||||
"label": __("Show Remarks"),
|
||||
"fieldtype": "Check",
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_id",
|
||||
"label": __("Tax Id"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"customer_name",
|
||||
"fieldname": "customer_name",
|
||||
"label": __("Customer Name"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"payment_terms",
|
||||
"fieldname": "payment_terms",
|
||||
"label": __("Payment Tems"),
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname":"credit_limit",
|
||||
"fieldname": "credit_limit",
|
||||
"label": __("Credit Limit"),
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 1
|
||||
|
@ -106,6 +106,7 @@ class ReceivablePayableReport(object):
|
||||
party = gle.party,
|
||||
posting_date = gle.posting_date,
|
||||
account_currency = gle.account_currency,
|
||||
remarks = gle.remarks if self.filters.get("show_remarks") else None,
|
||||
invoiced = 0.0,
|
||||
paid = 0.0,
|
||||
credit_note = 0.0,
|
||||
@ -583,10 +584,12 @@ class ReceivablePayableReport(object):
|
||||
else:
|
||||
select_fields = "debit, credit"
|
||||
|
||||
remarks = ", remarks" if self.filters.get("show_remarks") else ""
|
||||
|
||||
self.gl_entries = frappe.db.sql("""
|
||||
select
|
||||
name, posting_date, account, party_type, party, voucher_type, voucher_no, cost_center,
|
||||
against_voucher_type, against_voucher, account_currency, {0}
|
||||
against_voucher_type, against_voucher, account_currency, {0} {remarks}
|
||||
from
|
||||
`tabGL Entry`
|
||||
where
|
||||
@ -595,7 +598,7 @@ class ReceivablePayableReport(object):
|
||||
and party_type=%s
|
||||
and (party is not null and party != '')
|
||||
{1} {2} {3}"""
|
||||
.format(select_fields, date_condition, conditions, order_by), values, as_dict=True)
|
||||
.format(select_fields, date_condition, conditions, order_by, remarks=remarks), values, as_dict=True)
|
||||
|
||||
def get_sales_invoices_or_customers_based_on_sales_person(self):
|
||||
if self.filters.get("sales_person"):
|
||||
@ -754,6 +757,10 @@ class ReceivablePayableReport(object):
|
||||
self.add_column(label=_('Voucher Type'), fieldname='voucher_type', fieldtype='Data')
|
||||
self.add_column(label=_('Voucher No'), fieldname='voucher_no', fieldtype='Dynamic Link',
|
||||
options='voucher_type', width=180)
|
||||
|
||||
if self.filters.show_remarks:
|
||||
self.add_column(label=_('Remarks'), fieldname='remarks', fieldtype='Text', width=200),
|
||||
|
||||
self.add_column(label='Due Date', fieldtype='Date')
|
||||
|
||||
if self.party_type == "Supplier":
|
||||
|
@ -139,9 +139,9 @@ def get_account_type_based_data(company, account_type, period_list, accumulated_
|
||||
data["total"] = total
|
||||
return data
|
||||
|
||||
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters={}):
|
||||
def get_account_type_based_gl_data(company, start_date, end_date, account_type, filters=None):
|
||||
cond = ""
|
||||
filters = frappe._dict(filters)
|
||||
filters = frappe._dict(filters or {})
|
||||
|
||||
if filters.include_default_book_entries:
|
||||
company_fb = frappe.db.get_value("Company", company, 'default_finance_book')
|
||||
|
@ -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,38 @@ 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]:
|
||||
if 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 +230,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 +280,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 +290,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,19 +304,44 @@ 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:
|
||||
d = accounts_by_name.get(entry.account_name)
|
||||
if entry.account_number:
|
||||
account_name = entry.account_number + ' - ' + entry.account_name
|
||||
else:
|
||||
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"""
|
||||
@ -277,17 +349,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)
|
||||
|
||||
@ -307,7 +380,14 @@ def update_parent_account_names(accounts):
|
||||
of account_number and suffix of company abbr. This function adds key called
|
||||
`parent_account_name` which does not have such prefix/suffix.
|
||||
"""
|
||||
name_to_account_map = { d.name : d.account_name for d in accounts }
|
||||
name_to_account_map = {}
|
||||
|
||||
for d in accounts:
|
||||
if d.account_number:
|
||||
account_name = d.account_number + ' - ' + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
name_to_account_map[d.name] = account_name
|
||||
|
||||
for account in accounts:
|
||||
if account.parent_account:
|
||||
@ -341,7 +421,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:
|
||||
@ -355,10 +435,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)
|
||||
@ -373,6 +456,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
|
||||
@ -420,7 +504,11 @@ def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, g
|
||||
convert_to_presentation_currency(gl_entries, currency_info, filters.get('company'))
|
||||
|
||||
for entry in gl_entries:
|
||||
account_name = entry.account_name
|
||||
if entry.account_number:
|
||||
account_name = entry.account_number + ' - ' + entry.account_name
|
||||
else:
|
||||
account_name = entry.account_name
|
||||
|
||||
validate_entries(account_name, entry, accounts_by_name, accounts)
|
||||
gl_entries_by_account.setdefault(account_name, []).append(entry)
|
||||
|
||||
@ -431,6 +519,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)
|
||||
|
||||
@ -440,12 +529,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 = []
|
||||
@ -475,7 +575,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"])
|
||||
@ -491,7 +590,13 @@ def filter_accounts(accounts, depth=10):
|
||||
parent_children_map = {}
|
||||
accounts_by_name = {}
|
||||
for d in accounts:
|
||||
accounts_by_name[d.account_name] = d
|
||||
if d.account_number:
|
||||
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)
|
||||
|
||||
filtered_accounts = []
|
||||
|
@ -110,9 +110,26 @@ frappe.query_reports["General Ledger"] = {
|
||||
"fieldname":"group_by",
|
||||
"label": __("Group by"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["", __("Group by Voucher"), __("Group by Voucher (Consolidated)"),
|
||||
__("Group by Account"), __("Group by Party")],
|
||||
"default": __("Group by Voucher (Consolidated)")
|
||||
"options": [
|
||||
"",
|
||||
{
|
||||
label: __("Group by Voucher"),
|
||||
value: "Group by Voucher",
|
||||
},
|
||||
{
|
||||
label: __("Group by Voucher (Consolidated)"),
|
||||
value: "Group by Voucher (Consolidated)",
|
||||
},
|
||||
{
|
||||
label: __("Group by Account"),
|
||||
value: "Group by Account",
|
||||
},
|
||||
{
|
||||
label: __("Group by Party"),
|
||||
value: "Group by Party",
|
||||
},
|
||||
],
|
||||
"default": "Group by Voucher (Consolidated)"
|
||||
},
|
||||
{
|
||||
"fieldname":"tax_id",
|
||||
|
@ -62,14 +62,14 @@ def validate_filters(filters, account_details):
|
||||
if not account_details.get(account):
|
||||
frappe.throw(_("Account {0} does not exists").format(account))
|
||||
|
||||
if (filters.get("account") and filters.get("group_by") == _('Group by Account')):
|
||||
if (filters.get("account") and filters.get("group_by") == 'Group by Account'):
|
||||
filters.account = frappe.parse_json(filters.get('account'))
|
||||
for account in filters.account:
|
||||
if account_details[account].is_group == 0:
|
||||
frappe.throw(_("Can not filter based on Child Account, if grouped by Account"))
|
||||
|
||||
if (filters.get("voucher_no")
|
||||
and filters.get("group_by") in [_('Group by Voucher')]):
|
||||
and filters.get("group_by") in ['Group by Voucher']):
|
||||
frappe.throw(_("Can not filter based on Voucher No, if grouped by Voucher"))
|
||||
|
||||
if filters.from_date > filters.to_date:
|
||||
@ -153,8 +153,10 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
if filters.get("include_dimensions"):
|
||||
order_by_statement = "order by posting_date, creation"
|
||||
|
||||
if filters.get("group_by") == _("Group by Voucher"):
|
||||
if filters.get("group_by") == "Group by Voucher":
|
||||
order_by_statement = "order by posting_date, voucher_type, voucher_no"
|
||||
if filters.get("group_by") == "Group by Account":
|
||||
order_by_statement = "order by account, posting_date, creation"
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
filters['company_fb'] = frappe.db.get_value("Company",
|
||||
@ -312,13 +314,13 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
# Opening for filtered account
|
||||
data.append(totals.opening)
|
||||
|
||||
if filters.get("group_by") != _('Group by Voucher (Consolidated)'):
|
||||
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
|
||||
for acc, acc_dict in iteritems(gle_map):
|
||||
# acc
|
||||
if acc_dict.entries:
|
||||
# opening
|
||||
data.append({})
|
||||
if filters.get("group_by") != _("Group by Voucher"):
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
data.append(acc_dict.totals.opening)
|
||||
|
||||
data += acc_dict.entries
|
||||
@ -327,7 +329,7 @@ def get_data_with_opening_closing(filters, account_details, accounting_dimension
|
||||
data.append(acc_dict.totals.total)
|
||||
|
||||
# closing
|
||||
if filters.get("group_by") != _("Group by Voucher"):
|
||||
if filters.get("group_by") != "Group by Voucher":
|
||||
data.append(acc_dict.totals.closing)
|
||||
data.append({})
|
||||
else:
|
||||
@ -357,9 +359,9 @@ def get_totals_dict():
|
||||
)
|
||||
|
||||
def group_by_field(group_by):
|
||||
if group_by == _('Group by Party'):
|
||||
if group_by == 'Group by Party':
|
||||
return 'party'
|
||||
elif group_by in [_('Group by Voucher (Consolidated)'), _('Group by Account')]:
|
||||
elif group_by in ['Group by Voucher (Consolidated)', 'Group by Account']:
|
||||
return 'account'
|
||||
else:
|
||||
return 'voucher_no'
|
||||
@ -421,11 +423,9 @@ 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)'):
|
||||
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)'):
|
||||
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
|
||||
keylist = [gle.get("voucher_type"), gle.get("voucher_no"), gle.get("account")]
|
||||
for dim in accounting_dimensions:
|
||||
keylist.append(gle.get(dim))
|
||||
@ -436,10 +436,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
else:
|
||||
update_value_in_dict(consolidated_gle, key, gle)
|
||||
|
||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
|
||||
update_value_in_dict(totals, 'closing', gle)
|
||||
|
||||
for key, value in consolidated_gle.items():
|
||||
update_value_in_dict(gle_map[value.get(group_by)].totals, 'total', value)
|
||||
update_value_in_dict(totals, 'total', value)
|
||||
update_value_in_dict(gle_map[value.get(group_by)].totals, 'closing', value)
|
||||
update_value_in_dict(totals, 'closing', value)
|
||||
entries.append(value)
|
||||
|
||||
return totals, entries
|
||||
|
@ -1,12 +1,15 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2018-08-21 11:25:00.551823",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2018-09-21 11:25:00.551823",
|
||||
"modified": "2021-09-20 17:43:39.518851",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "TDS Computation Summary",
|
||||
|
@ -2,11 +2,10 @@ from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.utils import flt
|
||||
|
||||
from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import (
|
||||
get_advance_vouchers,
|
||||
get_debit_note_amount,
|
||||
from erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly import (
|
||||
get_result,
|
||||
get_tds_docs,
|
||||
)
|
||||
from erpnext.accounts.utils import get_fiscal_year
|
||||
|
||||
@ -17,9 +16,12 @@ def execute(filters=None):
|
||||
filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
|
||||
|
||||
columns = get_columns(filters)
|
||||
res = get_result(filters)
|
||||
tds_docs, tds_accounts, tax_category_map = get_tds_docs(filters)
|
||||
|
||||
return columns, res
|
||||
res = get_result(filters, tds_docs, tds_accounts, tax_category_map)
|
||||
final_result = group_by_supplier_and_category(res)
|
||||
|
||||
return columns, final_result
|
||||
|
||||
def validate_filters(filters):
|
||||
''' Validate if dates are properly set and lie in the same fiscal year'''
|
||||
@ -33,81 +35,39 @@ def validate_filters(filters):
|
||||
|
||||
filters["fiscal_year"] = from_year
|
||||
|
||||
def get_result(filters):
|
||||
# if no supplier selected, fetch data for all tds applicable supplier
|
||||
# else fetch relevant data for selected supplier
|
||||
pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
|
||||
fields = ["name", pan+" as pan", "tax_withholding_category", "supplier_type", "supplier_name"]
|
||||
def group_by_supplier_and_category(data):
|
||||
supplier_category_wise_map = {}
|
||||
|
||||
if filters.supplier:
|
||||
filters.supplier = frappe.db.get_list('Supplier',
|
||||
{"name": filters.supplier}, fields)
|
||||
else:
|
||||
filters.supplier = frappe.db.get_list('Supplier',
|
||||
{"tax_withholding_category": ["!=", ""]}, fields)
|
||||
for row in data:
|
||||
supplier_category_wise_map.setdefault((row.get('supplier'), row.get('section_code')), {
|
||||
'pan': row.get('pan'),
|
||||
'supplier': row.get('supplier'),
|
||||
'supplier_name': row.get('supplier_name'),
|
||||
'section_code': row.get('section_code'),
|
||||
'entity_type': row.get('entity_type'),
|
||||
'tds_rate': row.get('tds_rate'),
|
||||
'total_amount_credited': 0.0,
|
||||
'tds_deducted': 0.0
|
||||
})
|
||||
|
||||
supplier_category_wise_map.get((row.get('supplier'), row.get('section_code')))['total_amount_credited'] += \
|
||||
row.get('total_amount_credited', 0.0)
|
||||
|
||||
supplier_category_wise_map.get((row.get('supplier'), row.get('section_code')))['tds_deducted'] += \
|
||||
row.get('tds_deducted', 0.0)
|
||||
|
||||
final_result = get_final_result(supplier_category_wise_map)
|
||||
|
||||
return final_result
|
||||
|
||||
|
||||
def get_final_result(supplier_category_wise_map):
|
||||
out = []
|
||||
for supplier in filters.supplier:
|
||||
tds = frappe.get_doc("Tax Withholding Category", supplier.tax_withholding_category)
|
||||
rate = [d.tax_withholding_rate for d in tds.rates if d.fiscal_year == filters.fiscal_year]
|
||||
|
||||
if rate:
|
||||
rate = rate[0]
|
||||
|
||||
try:
|
||||
account = [d.account for d in tds.accounts if d.company == filters.company][0]
|
||||
|
||||
except IndexError:
|
||||
account = []
|
||||
total_invoiced_amount, tds_deducted = get_invoice_and_tds_amount(supplier.name, account,
|
||||
filters.company, filters.from_date, filters.to_date, filters.fiscal_year)
|
||||
|
||||
if total_invoiced_amount or tds_deducted:
|
||||
row = [supplier.pan, supplier.name]
|
||||
|
||||
if filters.naming_series == 'Naming Series':
|
||||
row.append(supplier.supplier_name)
|
||||
|
||||
row.extend([tds.name, supplier.supplier_type, rate, total_invoiced_amount, tds_deducted])
|
||||
out.append(row)
|
||||
for key, value in supplier_category_wise_map.items():
|
||||
out.append(value)
|
||||
|
||||
return out
|
||||
|
||||
def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, fiscal_year):
|
||||
''' calculate total invoice amount and total tds deducted for given supplier '''
|
||||
|
||||
entries = frappe.db.sql("""
|
||||
select voucher_no, credit
|
||||
from `tabGL Entry`
|
||||
where party in (%s) and credit > 0
|
||||
and company=%s and is_cancelled = 0
|
||||
and posting_date between %s and %s
|
||||
""", (supplier, company, from_date, to_date), as_dict=1)
|
||||
|
||||
supplier_credit_amount = flt(sum(d.credit for d in entries))
|
||||
|
||||
vouchers = [d.voucher_no for d in entries]
|
||||
vouchers += get_advance_vouchers([supplier], company=company,
|
||||
from_date=from_date, to_date=to_date)
|
||||
|
||||
tds_deducted = 0
|
||||
if vouchers:
|
||||
tds_deducted = flt(frappe.db.sql("""
|
||||
select sum(credit)
|
||||
from `tabGL Entry`
|
||||
where account=%s and posting_date between %s and %s
|
||||
and company=%s and credit > 0 and voucher_no in ({0})
|
||||
""".format(', '.join("'%s'" % d for d in vouchers)),
|
||||
(account, from_date, to_date, company))[0][0])
|
||||
|
||||
date_range_filter = [fiscal_year, from_date, to_date]
|
||||
|
||||
debit_note_amount = get_debit_note_amount([supplier], date_range_filter, company=company)
|
||||
|
||||
total_invoiced_amount = supplier_credit_amount + tds_deducted - debit_note_amount
|
||||
|
||||
return total_invoiced_amount, tds_deducted
|
||||
|
||||
def get_columns(filters):
|
||||
columns = [
|
||||
{
|
||||
@ -149,7 +109,7 @@ def get_columns(filters):
|
||||
{
|
||||
"label": _("TDS Rate %"),
|
||||
"fieldname": "tds_rate",
|
||||
"fieldtype": "Float",
|
||||
"fieldtype": "Percent",
|
||||
"width": 90
|
||||
},
|
||||
{
|
||||
|
@ -16,69 +16,6 @@ frappe.query_reports["TDS Payable Monthly"] = {
|
||||
"label": __("Supplier"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Supplier",
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {
|
||||
"tax_withholding_category": ["!=", ""],
|
||||
}
|
||||
}
|
||||
},
|
||||
on_change: function() {
|
||||
frappe.query_report.set_filter_value("purchase_invoice", "");
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"purchase_invoice",
|
||||
"label": __("Purchase Invoice"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Invoice",
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {
|
||||
"name": ["in", frappe.query_report.invoices]
|
||||
}
|
||||
}
|
||||
},
|
||||
on_change: function() {
|
||||
let supplier = frappe.query_report.get_filter_value('supplier');
|
||||
if(!supplier) return; // return if no supplier selected
|
||||
|
||||
// filter invoices based on selected supplier
|
||||
let invoices = [];
|
||||
frappe.query_report.invoice_data.map(d => {
|
||||
if(d.supplier==supplier)
|
||||
invoices.push(d.name)
|
||||
});
|
||||
frappe.query_report.invoices = invoices;
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"purchase_order",
|
||||
"label": __("Purchase Order"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Purchase Order",
|
||||
"get_query": function() {
|
||||
return {
|
||||
"filters": {
|
||||
"name": ["in", frappe.query_report.invoices]
|
||||
}
|
||||
}
|
||||
},
|
||||
on_change: function() {
|
||||
let supplier = frappe.query_report.get_filter_value('supplier');
|
||||
if(!supplier) return; // return if no supplier selected
|
||||
|
||||
// filter invoices based on selected supplier
|
||||
let invoices = [];
|
||||
frappe.query_report.invoice_data.map(d => {
|
||||
if(d.supplier==supplier)
|
||||
invoices.push(d.name)
|
||||
});
|
||||
frappe.query_report.invoices = invoices;
|
||||
frappe.query_report.refresh();
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname":"from_date",
|
||||
@ -96,23 +33,5 @@ frappe.query_reports["TDS Payable Monthly"] = {
|
||||
"reqd": 1,
|
||||
"width": "60px"
|
||||
}
|
||||
],
|
||||
|
||||
onload: function(report) {
|
||||
// fetch all tds applied invoices
|
||||
frappe.call({
|
||||
"method": "erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly.get_tds_invoices_and_orders",
|
||||
callback: function(r) {
|
||||
let invoices = [];
|
||||
|
||||
r.message.map(d => {
|
||||
invoices.push(d.name);
|
||||
});
|
||||
|
||||
report["invoice_data"] = r.message.invoices;
|
||||
report["invoices"] = invoices;
|
||||
|
||||
}
|
||||
});
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -1,13 +1,15 @@
|
||||
{
|
||||
"add_total_row": 1,
|
||||
"columns": [],
|
||||
"creation": "2018-08-21 11:32:30.874923",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2019-09-24 13:46:16.473711",
|
||||
"modified": "2021-09-20 12:05:50.387572",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "TDS Payable Monthly",
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user