mirror of
https://github.com/frappe/erpnext.git
synced 2026-07-30 15:54:35 +00:00
Compare commits
386 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab904c22cc | ||
|
|
6b77bac7da | ||
|
|
b9da89a90b | ||
|
|
f3cfb93a5f | ||
|
|
fa2f1348ec | ||
|
|
346c76cf38 | ||
|
|
81e7ebc112 | ||
|
|
bb9713a7b2 | ||
|
|
ac54e80856 | ||
|
|
3c78ac21d9 | ||
|
|
515b8dd8ca | ||
|
|
dac0d87e16 | ||
|
|
365572f5ea | ||
|
|
029e66709d | ||
|
|
8851b7adcf | ||
|
|
116210dcea | ||
|
|
dce943b41f | ||
|
|
cb209d9b57 | ||
|
|
7c7493c91d | ||
|
|
f3da88e301 | ||
|
|
8ddeb83191 | ||
|
|
47f8f3d9dd | ||
|
|
44c53deb90 | ||
|
|
d43aecbe13 | ||
|
|
e8ef54f4d7 | ||
|
|
eacf26d618 | ||
|
|
40b5bb1d15 | ||
|
|
5d46f765a6 | ||
|
|
2654098ce2 | ||
|
|
a403200516 | ||
|
|
1dc7cb8ad1 | ||
|
|
abe6ef4cd4 | ||
|
|
3d1b833f35 | ||
|
|
2dc12505b2 | ||
|
|
f5251b5a86 | ||
|
|
aa6caa00bf | ||
|
|
35bd31e939 | ||
|
|
0ba59049f9 | ||
|
|
9f6b0dbfdf | ||
|
|
79be341ea4 | ||
|
|
b3dc199118 | ||
|
|
9a3c6694b9 | ||
|
|
2c9266807a | ||
|
|
afb236a06b | ||
|
|
94905df774 | ||
|
|
fa5cee479f | ||
|
|
885f1e0fd8 | ||
|
|
e358d34c4f | ||
|
|
093d9f4a70 | ||
|
|
50fbc167f3 | ||
|
|
0731454f50 | ||
|
|
b81dfdcd26 | ||
|
|
68809159ba | ||
|
|
1736029733 | ||
|
|
982960fc0a | ||
|
|
bb7d661a04 | ||
|
|
c6557a136c | ||
|
|
271dad941b | ||
|
|
e9d1cbb339 | ||
|
|
18dddfdea9 | ||
|
|
978d1cde2d | ||
|
|
feaae330d5 | ||
|
|
060127663a | ||
|
|
88bf748275 | ||
|
|
aa464cc615 | ||
|
|
8581b5cdea | ||
|
|
9abb467127 | ||
|
|
f16e0e17cc | ||
|
|
fe59c313d6 | ||
|
|
9e9f730dd1 | ||
|
|
264ffe05b4 | ||
|
|
42686f92e9 | ||
|
|
7931cfd77b | ||
|
|
835d96c727 | ||
|
|
f7cf3fed8e | ||
|
|
0664e64003 | ||
|
|
4f7046e6c2 | ||
|
|
43ca66f461 | ||
|
|
ff7423f15f | ||
|
|
7cbf577f1f | ||
|
|
1454f8bc96 | ||
|
|
a33ebbdc54 | ||
|
|
ee6da35c0b | ||
|
|
1c0ae92912 | ||
|
|
3fc2442622 | ||
|
|
a60ecb95b0 | ||
|
|
ed58f206fa | ||
|
|
305b9508e6 | ||
|
|
d47007adb2 | ||
|
|
7c42b8b702 | ||
|
|
2fb18afea0 | ||
|
|
c822f286c7 | ||
|
|
6260f0caa1 | ||
|
|
4019148a42 | ||
|
|
7a25d5f2de | ||
|
|
1ba0111e16 | ||
|
|
e9c3d3ff0d | ||
|
|
6d39b90b8e | ||
|
|
b69381117f | ||
|
|
301b94e06a | ||
|
|
30f3b4804d | ||
|
|
0313b44f1f | ||
|
|
f9770b69b2 | ||
|
|
5f7c5bcac8 | ||
|
|
873ed1ae4f | ||
|
|
3fca08e3e8 | ||
|
|
220ab16698 | ||
|
|
a8ba52e8a5 | ||
|
|
60fd96085c | ||
|
|
e03965afa5 | ||
|
|
d116aa6f23 | ||
|
|
9c7cdc1079 | ||
|
|
a19f0f484c | ||
|
|
5388ecf85e | ||
|
|
b34e737930 | ||
|
|
56c98aa59e | ||
|
|
aa210ba6c8 | ||
|
|
4bfacabf85 | ||
|
|
99a1eea982 | ||
|
|
48acb56cb0 | ||
|
|
5867d97e7c | ||
|
|
e757970db3 | ||
|
|
2a60a65ccd | ||
|
|
db5593287e | ||
|
|
e8b46bf3a3 | ||
|
|
67e5e95466 | ||
|
|
f1b7bb8878 | ||
|
|
fb742476f8 | ||
|
|
d4b4bd9d97 | ||
|
|
accb98ccf4 | ||
|
|
6acab32934 | ||
|
|
5d4d6bb52b | ||
|
|
a2a462f2f5 | ||
|
|
b1efb7f80b | ||
|
|
97fc449222 | ||
|
|
cb0168ae65 | ||
|
|
de38c70707 | ||
|
|
cb97e950e6 | ||
|
|
a046da1e61 | ||
|
|
28026f7f1c | ||
|
|
93a0ea7118 | ||
|
|
a30cfbce42 | ||
|
|
019c5a338d | ||
|
|
8cad23b8fb | ||
|
|
dd2dba6f01 | ||
|
|
f63a71e2cc | ||
|
|
f81c5cd478 | ||
|
|
a23ef95cd8 | ||
|
|
943def5faa | ||
|
|
7a08f8aeec | ||
|
|
a082d70553 | ||
|
|
60620fe335 | ||
|
|
d7e55387fc | ||
|
|
46567f0fe9 | ||
|
|
5ed32b5995 | ||
|
|
152f9b0a43 | ||
|
|
71e517f043 | ||
|
|
e0f2130731 | ||
|
|
b8d75ff241 | ||
|
|
a3b7682935 | ||
|
|
200f6da8b2 | ||
|
|
7c9018f401 | ||
|
|
4f5d67883c | ||
|
|
5ba315eb96 | ||
|
|
408c4a6f54 | ||
|
|
e357541509 | ||
|
|
1fae8f1fcb | ||
|
|
8fffd1597d | ||
|
|
34e92ab229 | ||
|
|
ffadd671b7 | ||
|
|
47befa697d | ||
|
|
9ce7ea692d | ||
|
|
a739bf90c4 | ||
|
|
676c5280cc | ||
|
|
e0decb0ae2 | ||
|
|
b906cc20ae | ||
|
|
c98421c69a | ||
|
|
d7afb9ef65 | ||
|
|
a915b9cf72 | ||
|
|
a022e01d3f | ||
|
|
077026a7ae | ||
|
|
348a961b53 | ||
|
|
3cf5861a8b | ||
|
|
8e9db11c69 | ||
|
|
867cfa04b2 | ||
|
|
50af32f35c | ||
|
|
6e63dc1360 | ||
|
|
944e3d467c | ||
|
|
b7befe49dc | ||
|
|
8c57394e78 | ||
|
|
f12deae24b | ||
|
|
8e2a84e435 | ||
|
|
698214bd59 | ||
|
|
873d166a4e | ||
|
|
ae948e31ea | ||
|
|
9166d58717 | ||
|
|
2fff5e5cd1 | ||
|
|
ac8014e24c | ||
|
|
46a5a83789 | ||
|
|
11c498d9e5 | ||
|
|
7895d2a048 | ||
|
|
94177c0764 | ||
|
|
0a4abff354 | ||
|
|
5a06ee9230 | ||
|
|
ebe68c1a7a | ||
|
|
35448886bc | ||
|
|
b6609d1649 | ||
|
|
3af060c787 | ||
|
|
9431bb9466 | ||
|
|
efc292a5dd | ||
|
|
37088391bb | ||
|
|
fdd9cc76be | ||
|
|
f328b226fb | ||
|
|
f8348ab681 | ||
|
|
9f71863a6a | ||
|
|
eecfb25c90 | ||
|
|
c99fe8b57d | ||
|
|
41a0e12954 | ||
|
|
a883f16f31 | ||
|
|
07a9f16eb5 | ||
|
|
f0383289d8 | ||
|
|
46209023ce | ||
|
|
e69bd39cdd | ||
|
|
0fcb3cd918 | ||
|
|
44ab131792 | ||
|
|
b648d77316 | ||
|
|
acc5cff7ff | ||
|
|
0012f0b2da | ||
|
|
f2340b360f | ||
|
|
1796f09c0f | ||
|
|
64b58b148f | ||
|
|
9b71e02003 | ||
|
|
50e718f508 | ||
|
|
4415bf9968 | ||
|
|
289650748c | ||
|
|
268893d5d0 | ||
|
|
7b4a5e7e64 | ||
|
|
0d1b04326a | ||
|
|
db9f56bee8 | ||
|
|
2e9d08f8b1 | ||
|
|
02ddd5f2b0 | ||
|
|
1b7d94d70d | ||
|
|
952c60b3f5 | ||
|
|
8c33103838 | ||
|
|
a8c966eb25 | ||
|
|
88b1c1c87e | ||
|
|
771213c415 | ||
|
|
d86f5ec1ba | ||
|
|
a568fc7924 | ||
|
|
c040256793 | ||
|
|
29996ee726 | ||
|
|
1120506e11 | ||
|
|
e64751e3a2 | ||
|
|
bf47c6836e | ||
|
|
e80192e2da | ||
|
|
b2f1b02e34 | ||
|
|
ed090f2e3e | ||
|
|
d796172249 | ||
|
|
ff9f6366ad | ||
|
|
b8683d5532 | ||
|
|
4a156cdc2e | ||
|
|
771b076448 | ||
|
|
e6346ac982 | ||
|
|
666e8cd076 | ||
|
|
547e173fe0 | ||
|
|
37bd0ecf87 | ||
|
|
8244980d79 | ||
|
|
f3650b4f9d | ||
|
|
6b8dad9454 | ||
|
|
1590ac3ec7 | ||
|
|
10b239ec50 | ||
|
|
c9c4a9995b | ||
|
|
b6dc71679e | ||
|
|
5bdb6041b9 | ||
|
|
89828defc5 | ||
|
|
816236b587 | ||
|
|
69f17721ef | ||
|
|
125bb1f99a | ||
|
|
6f786b42a9 | ||
|
|
fed80177de | ||
|
|
2ce36d1edc | ||
|
|
7e44c30404 | ||
|
|
47ced6810f | ||
|
|
6c3f5687f2 | ||
|
|
1b632b683f | ||
|
|
c5660e8511 | ||
|
|
c4338d184e | ||
|
|
d598a61556 | ||
|
|
d262d0ac27 | ||
|
|
7dc2f95932 | ||
|
|
cf87c9138e | ||
|
|
1a42f82d14 | ||
|
|
2c5a0bff47 | ||
|
|
a831e6b552 | ||
|
|
f844c36ab2 | ||
|
|
091c2f3023 | ||
|
|
119c2f01e1 | ||
|
|
40aac908d1 | ||
|
|
4e6d588ae1 | ||
|
|
504f2f06d3 | ||
|
|
d96fd60878 | ||
|
|
d9a219850a | ||
|
|
cb6d884058 | ||
|
|
4102f799dc | ||
|
|
3c53c5b660 | ||
|
|
0a4b3d8129 | ||
|
|
bad489426a | ||
|
|
729e29d268 | ||
|
|
8108d4761b | ||
|
|
00cb04df84 | ||
|
|
dd0cefbeb9 | ||
|
|
d11c215f85 | ||
|
|
5ea5bfad3d | ||
|
|
4da5cb36e7 | ||
|
|
56b58cbeea | ||
|
|
cd87931cee | ||
|
|
355ae1fca6 | ||
|
|
91baa22d59 | ||
|
|
af57e1e299 | ||
|
|
1d3ba46107 | ||
|
|
65a590ced1 | ||
|
|
1a68e10742 | ||
|
|
71f676eedd | ||
|
|
4fe827e86b | ||
|
|
f573840c9f | ||
|
|
e57037b4cf | ||
|
|
51b2fcc8ef | ||
|
|
ff062d501f | ||
|
|
2cbd5a9fcf | ||
|
|
47a7eeca54 | ||
|
|
62bbf0fe45 | ||
|
|
ae8c1ae311 | ||
|
|
8f5ab94b70 | ||
|
|
9507b2d752 | ||
|
|
7e018f94ce | ||
|
|
2cfafede44 | ||
|
|
df1f8fddf6 | ||
|
|
a17fed9cd9 | ||
|
|
d8479a41e5 | ||
|
|
b1244df045 | ||
|
|
d2f5d31f98 | ||
|
|
44ee44dec5 | ||
|
|
bebd77c27d | ||
|
|
0c55a98190 | ||
|
|
32d72fdecb | ||
|
|
046ec928e0 | ||
|
|
0660d6ed01 | ||
|
|
877820b902 | ||
|
|
ab0e381cfc | ||
|
|
5e34cdf00f | ||
|
|
4ecb798585 | ||
|
|
91d269fe1a | ||
|
|
4535a9415f | ||
|
|
e17713c9d6 | ||
|
|
21a5498d5d | ||
|
|
c0b17edbbf | ||
|
|
e9ed379b57 | ||
|
|
4d0d642db7 | ||
|
|
f02438eb54 | ||
|
|
62fa1f0305 | ||
|
|
77d4849ce8 | ||
|
|
fe4df3a14a | ||
|
|
ff570f48a0 | ||
|
|
e4b89d2fcd | ||
|
|
3529622a0d | ||
|
|
8f98238114 | ||
|
|
86e3adf344 | ||
|
|
d6152df3b4 | ||
|
|
4837238f3d | ||
|
|
e4f12f0458 | ||
|
|
23431cf261 | ||
|
|
57e66f958c | ||
|
|
05374cb8b2 | ||
|
|
36b519c962 | ||
|
|
15c9c08261 | ||
|
|
5d1de91b68 | ||
|
|
cf6e10ac7b | ||
|
|
6d99bb5ce6 | ||
|
|
6b38778dcb | ||
|
|
7c47f36a4c | ||
|
|
6ce2111b6d | ||
|
|
f0af24fc6d | ||
|
|
6eb9a114be | ||
|
|
8c01ae952b | ||
|
|
e8cf32e1c8 | ||
|
|
8dfdab9dc1 |
2
.github/helper/.flake8_strict
vendored
2
.github/helper/.flake8_strict
vendored
@@ -1,6 +1,8 @@
|
||||
[flake8]
|
||||
ignore =
|
||||
B007,
|
||||
B009,
|
||||
B010,
|
||||
B950,
|
||||
E101,
|
||||
E111,
|
||||
|
||||
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
|
||||
@@ -131,3 +131,21 @@ rules:
|
||||
key `$X` is uselessly assigned twice. This could be a potential bug.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
|
||||
- id: frappe-manual-commit
|
||||
patterns:
|
||||
- pattern: frappe.db.commit()
|
||||
- pattern-not-inside: |
|
||||
try:
|
||||
...
|
||||
except ...:
|
||||
...
|
||||
message: |
|
||||
Manually commiting a transaction is highly discouraged. Read about the transaction model implemented by Frappe Framework before adding manual commits: https://frappeframework.com/docs/user/en/api/database#database-transaction-model If you think manual commit is required then add a comment explaining why and `// nosemgrep` on the same line.
|
||||
paths:
|
||||
exclude:
|
||||
- "**/patches/**"
|
||||
- "**/demo/**"
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
34
.github/helper/semgrep_rules/report.yml
vendored
34
.github/helper/semgrep_rules/report.yml
vendored
@@ -1,34 +0,0 @@
|
||||
rules:
|
||||
- id: frappe-missing-translate-function-in-report-python
|
||||
paths:
|
||||
include:
|
||||
- "**/report"
|
||||
exclude:
|
||||
- "**/regional"
|
||||
pattern-either:
|
||||
- patterns:
|
||||
- pattern: |
|
||||
{..., "label": "...", ...}
|
||||
- pattern-not: |
|
||||
{..., "label": _("..."), ...}
|
||||
- patterns:
|
||||
- pattern: dict(..., label="...", ...)
|
||||
- pattern-not: dict(..., label=_("..."), ...)
|
||||
message: |
|
||||
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translated-values-in-business-logic
|
||||
paths:
|
||||
include:
|
||||
- "**/report"
|
||||
patterns:
|
||||
- pattern-inside: |
|
||||
{..., filters: [...], ...}
|
||||
- pattern: |
|
||||
{..., options: [..., __("..."), ...], ...}
|
||||
message: |
|
||||
Using translated values in options field will require you to translate the values while comparing in business logic. Instead of passing translated labels provide objects that contain both label and value. e.g. { label: __("Option value"), value: "Option value"}
|
||||
languages: [javascript]
|
||||
severity: ERROR
|
||||
6
.github/helper/semgrep_rules/security.py
vendored
6
.github/helper/semgrep_rules/security.py
vendored
@@ -1,6 +0,0 @@
|
||||
def function_name(input):
|
||||
# ruleid: frappe-codeinjection-eval
|
||||
eval(input)
|
||||
|
||||
# ok: frappe-codeinjection-eval
|
||||
eval("1 + 1")
|
||||
10
.github/helper/semgrep_rules/security.yml
vendored
10
.github/helper/semgrep_rules/security.yml
vendored
@@ -1,10 +0,0 @@
|
||||
rules:
|
||||
- id: frappe-codeinjection-eval
|
||||
patterns:
|
||||
- pattern-not: eval("...")
|
||||
- pattern: eval(...)
|
||||
message: |
|
||||
Detected the use of eval(). eval() can be dangerous if used to evaluate
|
||||
dynamic content. Avoid it or use safe_eval().
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
44
.github/helper/semgrep_rules/translate.js
vendored
44
.github/helper/semgrep_rules/translate.js
vendored
@@ -1,44 +0,0 @@
|
||||
// ruleid: frappe-translation-empty-string
|
||||
__("")
|
||||
// ruleid: frappe-translation-empty-string
|
||||
__('')
|
||||
|
||||
// ok: frappe-translation-js-formatting
|
||||
__('Welcome {0}, get started with ERPNext in just a few clicks.', [full_name]);
|
||||
|
||||
// ruleid: frappe-translation-js-formatting
|
||||
__(`Welcome ${full_name}, get started with ERPNext in just a few clicks.`);
|
||||
|
||||
// ok: frappe-translation-js-formatting
|
||||
__('This is fine');
|
||||
|
||||
|
||||
// ok: frappe-translation-trailing-spaces
|
||||
__('This is fine');
|
||||
|
||||
// ruleid: frappe-translation-trailing-spaces
|
||||
__(' this is not ok ');
|
||||
// ruleid: frappe-translation-trailing-spaces
|
||||
__('this is not ok ');
|
||||
// ruleid: frappe-translation-trailing-spaces
|
||||
__(' this is not ok');
|
||||
|
||||
// ok: frappe-translation-js-splitting
|
||||
__('You have {0} subscribers in your mailing list.', [subscribers.length])
|
||||
|
||||
// todoruleid: frappe-translation-js-splitting
|
||||
__('You have') + subscribers.length + __('subscribers in your mailing list.')
|
||||
|
||||
// ruleid: frappe-translation-js-splitting
|
||||
__('You have' + 'subscribers in your mailing list.')
|
||||
|
||||
// ruleid: frappe-translation-js-splitting
|
||||
__('You have {0} subscribers' +
|
||||
'in your mailing list', [subscribers.length])
|
||||
|
||||
// ok: frappe-translation-js-splitting
|
||||
__("Ctrl+Enter to add comment")
|
||||
|
||||
// ruleid: frappe-translation-js-splitting
|
||||
__('You have {0} subscribers \
|
||||
in your mailing list', [subscribers.length])
|
||||
61
.github/helper/semgrep_rules/translate.py
vendored
61
.github/helper/semgrep_rules/translate.py
vendored
@@ -1,61 +0,0 @@
|
||||
# Examples taken from https://frappeframework.com/docs/user/en/translations
|
||||
# This file is used for testing the tests.
|
||||
|
||||
from frappe import _
|
||||
|
||||
full_name = "Jon Doe"
|
||||
# ok: frappe-translation-python-formatting
|
||||
_('Welcome {0}, get started with ERPNext in just a few clicks.').format(full_name)
|
||||
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_('Welcome %s, get started with ERPNext in just a few clicks.' % full_name)
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_('Welcome %(name)s, get started with ERPNext in just a few clicks.' % {'name': full_name})
|
||||
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_('Welcome {0}, get started with ERPNext in just a few clicks.'.format(full_name))
|
||||
|
||||
|
||||
subscribers = ["Jon", "Doe"]
|
||||
# ok: frappe-translation-python-formatting
|
||||
_('You have {0} subscribers in your mailing list.').format(len(subscribers))
|
||||
|
||||
# ruleid: frappe-translation-python-splitting
|
||||
_('You have') + len(subscribers) + _('subscribers in your mailing list.')
|
||||
|
||||
# ruleid: frappe-translation-python-splitting
|
||||
_('You have {0} subscribers \
|
||||
in your mailing list').format(len(subscribers))
|
||||
|
||||
# ok: frappe-translation-python-splitting
|
||||
_('You have {0} subscribers') \
|
||||
+ 'in your mailing list'
|
||||
|
||||
# ruleid: frappe-translation-trailing-spaces
|
||||
msg = _(" You have {0} pending invoice ")
|
||||
# ruleid: frappe-translation-trailing-spaces
|
||||
msg = _("You have {0} pending invoice ")
|
||||
# ruleid: frappe-translation-trailing-spaces
|
||||
msg = _(" You have {0} pending invoice")
|
||||
|
||||
# ok: frappe-translation-trailing-spaces
|
||||
msg = ' ' + _("You have {0} pending invoices") + ' '
|
||||
|
||||
# ruleid: frappe-translation-python-formatting
|
||||
_(f"can not format like this - {subscribers}")
|
||||
# ruleid: frappe-translation-python-splitting
|
||||
_(f"what" + f"this is also not cool")
|
||||
|
||||
|
||||
# ruleid: frappe-translation-empty-string
|
||||
_("")
|
||||
# ruleid: frappe-translation-empty-string
|
||||
_('')
|
||||
|
||||
|
||||
class Test:
|
||||
# ok: frappe-translation-python-splitting
|
||||
def __init__(
|
||||
args
|
||||
):
|
||||
pass
|
||||
64
.github/helper/semgrep_rules/translate.yml
vendored
64
.github/helper/semgrep_rules/translate.yml
vendored
@@ -1,64 +0,0 @@
|
||||
rules:
|
||||
- id: frappe-translation-empty-string
|
||||
pattern-either:
|
||||
- pattern: _("")
|
||||
- pattern: __("")
|
||||
message: |
|
||||
Empty string is useless for translation.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python, javascript, json]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-trailing-spaces
|
||||
pattern-either:
|
||||
- pattern: _("=~/(^[ \t]+|[ \t]+$)/")
|
||||
- pattern: __("=~/(^[ \t]+|[ \t]+$)/")
|
||||
message: |
|
||||
Trailing or leading whitespace not allowed in translate strings.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python, javascript, json]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-python-formatting
|
||||
pattern-either:
|
||||
- pattern: _("..." % ...)
|
||||
- pattern: _("...".format(...))
|
||||
- pattern: _(f"...")
|
||||
message: |
|
||||
Only positional formatters are allowed and formatting should not be done before translating.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-js-formatting
|
||||
patterns:
|
||||
- pattern: __(`...`)
|
||||
- pattern-not: __("...")
|
||||
message: |
|
||||
Template strings are not allowed for text formatting.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [javascript, json]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-python-splitting
|
||||
pattern-either:
|
||||
- pattern: _(...) + _(...)
|
||||
- pattern: _("..." + "...")
|
||||
- pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
|
||||
- pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
|
||||
message: |
|
||||
Do not split strings inside translate function. Do not concatenate using translate functions.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-translation-js-splitting
|
||||
pattern-either:
|
||||
- pattern-regex: '__\([^\)]*[\\]\s+'
|
||||
- pattern: __('...' + '...', ...)
|
||||
- pattern: __('...') + __('...')
|
||||
message: |
|
||||
Do not split strings inside translate function. Do not concatenate using translate functions.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
languages: [javascript, json]
|
||||
severity: ERROR
|
||||
9
.github/helper/semgrep_rules/ux.js
vendored
9
.github/helper/semgrep_rules/ux.js
vendored
@@ -1,9 +0,0 @@
|
||||
|
||||
// ok: frappe-missing-translate-function-js
|
||||
frappe.msgprint('{{ _("Both login and password required") }}');
|
||||
|
||||
// ruleid: frappe-missing-translate-function-js
|
||||
frappe.msgprint('What');
|
||||
|
||||
// ok: frappe-missing-translate-function-js
|
||||
frappe.throw(' {{ _("Both login and password required") }}. ');
|
||||
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
|
||||
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"}'
|
||||
|
||||
20
.github/workflows/linters.yml
vendored
20
.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
|
||||
@@ -24,4 +17,15 @@ jobs:
|
||||
python-version: 3.8
|
||||
|
||||
- name: Install and Run Pre-commit
|
||||
uses: pre-commit/action@v2.0.0
|
||||
uses: pre-commit/action@v2.0.3
|
||||
|
||||
- name: Download Semgrep rules
|
||||
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
|
||||
|
||||
- uses: returntocorp/semgrep-action@v1
|
||||
env:
|
||||
SEMGREP_TIMEOUT: 120
|
||||
with:
|
||||
config: >-
|
||||
r/python.lang.correctness
|
||||
./frappe-semgrep-rules/rules
|
||||
|
||||
2
.github/workflows/ui-tests.yml
vendored
2
.github/workflows/ui-tests.yml
vendored
@@ -99,6 +99,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$"
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ context('Organizational Chart', () => {
|
||||
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
|
||||
cy.get('@input')
|
||||
.clear({ force: true })
|
||||
.type('Test Org Chart{enter}', { force: true })
|
||||
.type('Test Org Chart{downarrow}{enter}', { force: true })
|
||||
.blur({ force: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,7 +25,7 @@ context('Organizational Chart Mobile', () => {
|
||||
cy.get('.frappe-control[data-fieldname=company] input').focus().as('input');
|
||||
cy.get('@input')
|
||||
.clear({ force: true })
|
||||
.type('Test Org Chart{enter}', { force: true })
|
||||
.type('Test Org Chart{downarrow}{enter}', { force: true })
|
||||
.blur({ force: true });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -7,7 +7,7 @@ import frappe
|
||||
|
||||
from erpnext.hooks import regional_overrides
|
||||
|
||||
__version__ = '13.12.0'
|
||||
__version__ = '13.14.0'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
||||
@@ -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) \
|
||||
|
||||
@@ -175,7 +175,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 ",
|
||||
@@ -283,7 +283,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-19 11:17:38.788054",
|
||||
"modified": "2021-10-11 17:42:36.427699",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
||||
@@ -69,7 +69,7 @@ def import_coa(file_name, company):
|
||||
|
||||
frappe.local.flags.ignore_root_company_validation = True
|
||||
forest = build_forest(data)
|
||||
create_charts(company, custom_chart=forest)
|
||||
create_charts(company, custom_chart=forest, from_coa_importer=True)
|
||||
|
||||
# trigger on_update for company to reset default accounts
|
||||
set_default_accounts(company)
|
||||
@@ -148,7 +148,7 @@ def get_coa(doctype, parent, is_root=False, file_name=None, for_validate=0):
|
||||
|
||||
if not for_validate:
|
||||
forest = build_forest(data)
|
||||
accounts = build_tree_from_json("", chart_data=forest) # returns a list of dict in a tree render-able form
|
||||
accounts = build_tree_from_json("", chart_data=forest, from_coa_importer=True) # returns a list of dict in a tree render-able form
|
||||
|
||||
# filter out to show data for the selected node only
|
||||
accounts = [d for d in accounts if d['parent_account']==parent]
|
||||
@@ -212,11 +212,14 @@ def build_forest(data):
|
||||
if not account_name:
|
||||
error_messages.append("Row {0}: Please enter Account Name".format(line_no))
|
||||
|
||||
name = account_name
|
||||
if account_number:
|
||||
account_number = cstr(account_number).strip()
|
||||
account_name = "{} - {}".format(account_number, account_name)
|
||||
|
||||
charts_map[account_name] = {}
|
||||
charts_map[account_name]['account_name'] = name
|
||||
if account_number: charts_map[account_name]["account_number"] = account_number
|
||||
if cint(is_group) == 1: charts_map[account_name]["is_group"] = is_group
|
||||
if account_type: charts_map[account_name]["account_type"] = account_type
|
||||
if root_type: charts_map[account_name]["root_type"] = root_type
|
||||
|
||||
@@ -16,7 +16,7 @@ class LoyaltyPointEntry(Document):
|
||||
|
||||
def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=None):
|
||||
if not expiry_date:
|
||||
date = today()
|
||||
expiry_date = today()
|
||||
|
||||
return frappe.db.sql('''
|
||||
select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice
|
||||
|
||||
@@ -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:
|
||||
frappe.throw(_("Received Amount should be same as Paid 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()
|
||||
|
||||
@@ -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 = frappe.ui.form.Controller.extend({
|
||||
onload: function() {
|
||||
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 = frappe.ui.form.Controller.ext
|
||||
}
|
||||
});
|
||||
|
||||
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: function() {
|
||||
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: function() {
|
||||
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: function() {
|
||||
this.frm.set_value('party', '');
|
||||
},
|
||||
|
||||
party: function() {
|
||||
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: function() {
|
||||
this.frm.trigger("clear_child_tables");
|
||||
this.frm.refresh();
|
||||
},
|
||||
|
||||
clear_child_tables: function() {
|
||||
this.frm.clear_table("invoices");
|
||||
this.frm.clear_table("payments");
|
||||
this.frm.clear_table("allocation");
|
||||
this.frm.refresh_fields();
|
||||
},
|
||||
|
||||
get_unreconciled_entries: function() {
|
||||
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: function() {
|
||||
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: function() {
|
||||
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 = frappe.ui.form.Controller.ext
|
||||
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 = frappe.ui.form.Controller.ext
|
||||
}]
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
primary_action: () => {
|
||||
const args = dialog.get_values()["allocation"];
|
||||
|
||||
args.forEach(d => {
|
||||
@@ -197,7 +218,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext
|
||||
"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 = frappe.ui.form.Controller.ext
|
||||
},
|
||||
|
||||
reconcile_payment_entries: function() {
|
||||
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",
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
"section_break_6",
|
||||
"allocated_amount",
|
||||
"unreconciled_amount",
|
||||
"amount",
|
||||
"column_break_8",
|
||||
"amount",
|
||||
"is_advance",
|
||||
"section_break_5",
|
||||
"difference_amount",
|
||||
@@ -127,12 +127,13 @@
|
||||
"fieldname": "reference_row",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Reference Row"
|
||||
"label": "Reference Row",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-20 17:23:09.455803",
|
||||
"modified": "2021-10-06 11:48:59.616562",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Payment Reconciliation Allocation",
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -114,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 })
|
||||
|
||||
@@ -162,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
|
||||
@@ -171,6 +178,10 @@ 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
|
||||
@@ -246,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'):
|
||||
|
||||
@@ -19,6 +19,7 @@ from erpnext.stock.get_item_details import get_item_details
|
||||
class TestPricingRule(unittest.TestCase):
|
||||
def setUp(self):
|
||||
delete_existing_pricing_rules()
|
||||
setup_pricing_rule_data()
|
||||
|
||||
def tearDown(self):
|
||||
delete_existing_pricing_rules()
|
||||
@@ -561,6 +562,8 @@ class TestPricingRule(unittest.TestCase):
|
||||
for doc in [si, si1]:
|
||||
doc.delete()
|
||||
|
||||
test_dependencies = ["Campaign"]
|
||||
|
||||
def make_pricing_rule(**args):
|
||||
args = frappe._dict(args)
|
||||
|
||||
@@ -607,6 +610,13 @@ def make_pricing_rule(**args):
|
||||
if args.get(applicable_for):
|
||||
doc.db_set(applicable_for, args.get(applicable_for))
|
||||
|
||||
def setup_pricing_rule_data():
|
||||
if not frappe.db.exists('Campaign', '_Test Campaign'):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Campaign',
|
||||
'campaign_name': '_Test Campaign',
|
||||
'name': '_Test Campaign'
|
||||
}).insert()
|
||||
|
||||
def delete_existing_pricing_rules():
|
||||
for doctype in ["Pricing Rule", "Pricing Rule Item Code",
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
}
|
||||
},
|
||||
})
|
||||
|
||||
@@ -149,16 +149,18 @@
|
||||
"cb_17",
|
||||
"hold_comment",
|
||||
"more_info",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"represents_company",
|
||||
"column_break_147",
|
||||
"is_internal_supplier",
|
||||
"accounting_details_section",
|
||||
"credit_to",
|
||||
"party_account_currency",
|
||||
"is_opening",
|
||||
"against_expense_account",
|
||||
"column_break_63",
|
||||
"unrealized_profit_loss_account",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"is_internal_supplier",
|
||||
"represents_company",
|
||||
"remarks",
|
||||
"subscription_section",
|
||||
"from_date",
|
||||
@@ -1171,6 +1173,15 @@
|
||||
"options": "fa fa-file-text",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "supplier.is_internal_supplier",
|
||||
"fieldname": "is_internal_supplier",
|
||||
"fieldtype": "Check",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Is Internal Supplier",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "credit_to",
|
||||
"fieldtype": "Link",
|
||||
@@ -1196,7 +1207,7 @@
|
||||
"default": "No",
|
||||
"fieldname": "is_opening",
|
||||
"fieldtype": "Select",
|
||||
"label": "Is Opening",
|
||||
"label": "Is Opening Entry",
|
||||
"oldfieldname": "is_opening",
|
||||
"oldfieldtype": "Select",
|
||||
"options": "No\nYes",
|
||||
@@ -1298,15 +1309,6 @@
|
||||
"fieldname": "dimension_col_break",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fetch_from": "supplier.is_internal_supplier",
|
||||
"fieldname": "is_internal_supplier",
|
||||
"fieldtype": "Check",
|
||||
"ignore_user_permissions": 1,
|
||||
"label": "Is Internal Supplier",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "tax_withholding_category",
|
||||
"fieldtype": "Link",
|
||||
@@ -1395,13 +1397,24 @@
|
||||
"hidden": 1,
|
||||
"label": "Ignore Default Payment Terms Template",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "accounting_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Accounting Details",
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_147",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 204,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-28 13:10:28.351810",
|
||||
"modified": "2021-10-12 20:55:16.145651",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Purchase Invoice",
|
||||
|
||||
@@ -15,6 +15,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
|
||||
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
|
||||
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
|
||||
check_if_return_invoice_linked_with_payment_entry,
|
||||
get_total_in_party_account_currency,
|
||||
is_overdue,
|
||||
unlink_inter_company_doc,
|
||||
update_linked_doc,
|
||||
@@ -1147,6 +1148,7 @@ class PurchaseInvoice(BuyingController):
|
||||
return
|
||||
|
||||
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
|
||||
total = get_total_in_party_account_currency(self)
|
||||
|
||||
if not status:
|
||||
if self.docstatus == 2:
|
||||
@@ -1154,9 +1156,9 @@ class PurchaseInvoice(BuyingController):
|
||||
elif self.docstatus == 1:
|
||||
if self.is_internal_transfer():
|
||||
self.status = 'Internal Transfer'
|
||||
elif is_overdue(self):
|
||||
elif is_overdue(self, total):
|
||||
self.status = "Overdue"
|
||||
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
|
||||
elif 0 < outstanding_amount < total:
|
||||
self.status = "Partly Paid"
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
|
||||
@@ -10,9 +10,17 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
this.setup_posting_date_time_check();
|
||||
this._super(doc);
|
||||
},
|
||||
|
||||
company: function() {
|
||||
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: function() {
|
||||
var me = this;
|
||||
this._super();
|
||||
@@ -446,12 +454,15 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
},
|
||||
|
||||
currency() {
|
||||
var me = this;
|
||||
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)
|
||||
if (this.frm.doc.timesheets) {
|
||||
this.frm.doc.timesheets.forEach((d) => {
|
||||
let row = frappe.get_doc(d.doctype, d.name)
|
||||
set_timesheet_detail_rate(row.doctype, row.name, me.frm.doc.currency, row.timesheet_detail)
|
||||
});
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -999,7 +1010,7 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
|
||||
|
||||
frappe.ui.form.on("Sales Invoice Timesheet", {
|
||||
timesheets_remove(frm, cdt, cdn) {
|
||||
timesheets_remove(frm) {
|
||||
frm.trigger("calculate_timesheet_totals");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -124,6 +124,13 @@
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"column_break4",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
"write_off_outstanding_amount_automatically",
|
||||
"column_break_74",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
@@ -144,13 +151,6 @@
|
||||
"column_break_90",
|
||||
"change_amount",
|
||||
"account_for_change_amount",
|
||||
"column_break4",
|
||||
"write_off_amount",
|
||||
"base_write_off_amount",
|
||||
"write_off_outstanding_amount_automatically",
|
||||
"column_break_74",
|
||||
"write_off_account",
|
||||
"write_off_cost_center",
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
@@ -161,14 +161,14 @@
|
||||
"column_break_84",
|
||||
"language",
|
||||
"more_information",
|
||||
"status",
|
||||
"inter_company_invoice_reference",
|
||||
"is_internal_customer",
|
||||
"represents_company",
|
||||
"customer_group",
|
||||
"campaign",
|
||||
"is_discounted",
|
||||
"col_break23",
|
||||
"status",
|
||||
"is_internal_customer",
|
||||
"is_discounted",
|
||||
"source",
|
||||
"more_info",
|
||||
"debit_to",
|
||||
@@ -1990,16 +1990,6 @@
|
||||
"label": "Additional Discount Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_default_payment_terms_template",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Ignore Default Payment Terms Template",
|
||||
"read_only": 1,
|
||||
"show_days": 1,
|
||||
"show_seconds": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "dispatch_address_name",
|
||||
@@ -2015,6 +2005,14 @@
|
||||
"label": "Dispatch Address",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "ignore_default_payment_terms_template",
|
||||
"fieldtype": "Check",
|
||||
"hidden": 1,
|
||||
"label": "Ignore Default Payment Terms Template",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "total_billing_hours",
|
||||
"fieldtype": "Float",
|
||||
@@ -2033,7 +2031,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-09-28 13:09:34.391799",
|
||||
"modified": "2021-10-11 20:19:38.667508",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
@@ -2088,4 +2086,4 @@
|
||||
"title_field": "title",
|
||||
"track_changes": 1,
|
||||
"track_seen": 1
|
||||
}
|
||||
}
|
||||
@@ -1296,12 +1296,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):
|
||||
@@ -1470,6 +1478,7 @@ class SalesInvoice(SellingController):
|
||||
return
|
||||
|
||||
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
|
||||
total = get_total_in_party_account_currency(self)
|
||||
|
||||
if not status:
|
||||
if self.docstatus == 2:
|
||||
@@ -1477,9 +1486,9 @@ class SalesInvoice(SellingController):
|
||||
elif self.docstatus == 1:
|
||||
if self.is_internal_transfer():
|
||||
self.status = 'Internal Transfer'
|
||||
elif is_overdue(self):
|
||||
elif is_overdue(self, total):
|
||||
self.status = "Overdue"
|
||||
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
|
||||
elif 0 < outstanding_amount < total:
|
||||
self.status = "Partly Paid"
|
||||
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
|
||||
self.status = "Unpaid"
|
||||
@@ -1506,27 +1515,42 @@ class SalesInvoice(SellingController):
|
||||
if update:
|
||||
self.db_set('status', self.status, update_modified = update_modified)
|
||||
|
||||
def is_overdue(doc):
|
||||
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
|
||||
|
||||
def get_total_in_party_account_currency(doc):
|
||||
total_fieldname = (
|
||||
"grand_total"
|
||||
if doc.disable_rounded_total
|
||||
else "rounded_total"
|
||||
)
|
||||
if doc.party_account_currency != doc.currency:
|
||||
total_fieldname = "base_" + total_fieldname
|
||||
|
||||
return flt(doc.get(total_fieldname), doc.precision(total_fieldname))
|
||||
|
||||
def is_overdue(doc, total):
|
||||
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
|
||||
if outstanding_amount <= 0:
|
||||
return
|
||||
|
||||
grand_total = flt(doc.grand_total, doc.precision("grand_total"))
|
||||
nowdate = getdate()
|
||||
if doc.payment_schedule:
|
||||
# calculate payable amount till date
|
||||
payable_amount = sum(
|
||||
payment.payment_amount
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < nowdate
|
||||
)
|
||||
today = getdate()
|
||||
if doc.get('is_pos') or not doc.get('payment_schedule'):
|
||||
return getdate(doc.due_date) < today
|
||||
|
||||
if (grand_total - outstanding_amount) < payable_amount:
|
||||
return True
|
||||
# calculate payable amount till date
|
||||
payment_amount_field = (
|
||||
"base_payment_amount"
|
||||
if doc.party_account_currency != doc.currency
|
||||
else "payment_amount"
|
||||
)
|
||||
|
||||
payable_amount = sum(
|
||||
payment.get(payment_amount_field)
|
||||
for payment in doc.payment_schedule
|
||||
if getdate(payment.due_date) < today
|
||||
)
|
||||
|
||||
return (total - outstanding_amount) < payable_amount
|
||||
|
||||
elif getdate(doc.due_date) < nowdate:
|
||||
return True
|
||||
|
||||
def get_discounting_status(sales_invoice):
|
||||
status = None
|
||||
@@ -2002,22 +2026,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:
|
||||
@@ -2033,6 +2058,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
|
||||
|
||||
@@ -1085,8 +1085,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
|
||||
@@ -1441,15 +1439,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)
|
||||
@@ -2334,6 +2339,18 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.reload()
|
||||
self.assertEqual(si.status, "Paid")
|
||||
|
||||
def test_sales_invoice_submission_post_account_freezing_date(self):
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
|
||||
si = create_sales_invoice(do_not_save=True)
|
||||
si.posting_date = add_days(getdate(), 1)
|
||||
si.save()
|
||||
|
||||
self.assertRaises(frappe.ValidationError, si.submit)
|
||||
si.posting_date = getdate()
|
||||
si.submit()
|
||||
|
||||
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
|
||||
|
||||
def get_sales_invoice_for_e_invoice():
|
||||
si = make_sales_invoice_for_ewaybill()
|
||||
si.naming_series = 'INV-2020-.#####'
|
||||
|
||||
@@ -16,9 +16,9 @@
|
||||
"column_break_9",
|
||||
"billing_amount",
|
||||
"section_break_11",
|
||||
"timesheet_detail",
|
||||
"column_break_5",
|
||||
"time_sheet",
|
||||
"timesheet_detail",
|
||||
"column_break_13",
|
||||
"project_name"
|
||||
],
|
||||
"fields": [
|
||||
@@ -91,7 +91,6 @@
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
|
||||
"fieldname": "section_break_7",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Totals"
|
||||
@@ -110,11 +109,15 @@
|
||||
"fieldtype": "Data",
|
||||
"label": "Project Name",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_13",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-08-15 18:37:08.084930",
|
||||
"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):
|
||||
@@ -482,10 +498,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
|
||||
@@ -499,13 +518,16 @@ class Subscription(Document):
|
||||
2. Change the `Subscription` status to 'Past Due Date'
|
||||
3. Change the `Subscription` status to 'Cancelled'
|
||||
"""
|
||||
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)
|
||||
|
||||
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 self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end):
|
||||
self.cancel_subscription_at_period_end()
|
||||
|
||||
@@ -538,15 +560,17 @@ class Subscription(Document):
|
||||
else:
|
||||
self.set_status_grace_period()
|
||||
|
||||
if getdate() > getdate(self.current_invoice_end):
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
# Generate invoices periodically even if current invoice are unpaid
|
||||
if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice()
|
||||
or self.is_prepaid_to_invoice()):
|
||||
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)
|
||||
|
||||
if getdate() > getdate(self.current_invoice_end):
|
||||
self.update_subscription_period(add_days(self.current_invoice_end, 1))
|
||||
|
||||
@staticmethod
|
||||
def is_paid(invoice):
|
||||
"""
|
||||
|
||||
@@ -18,6 +18,7 @@ from frappe.utils.data import (
|
||||
|
||||
from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor
|
||||
|
||||
test_dependencies = ("UOM", "Item Group", "Item")
|
||||
|
||||
def create_plan():
|
||||
if not frappe.db.exists('Subscription Plan', '_Test Plan Name'):
|
||||
@@ -68,7 +69,6 @@ def create_plan():
|
||||
supplier.insert()
|
||||
|
||||
class TestSubscription(unittest.TestCase):
|
||||
|
||||
def setUp(self):
|
||||
create_plan()
|
||||
|
||||
|
||||
@@ -49,15 +49,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
|
||||
@@ -165,6 +174,7 @@ 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')
|
||||
@@ -203,6 +213,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
|
||||
# then chargeable value is "prev invoices + advances" value which cross the threshold
|
||||
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
tax_amount = round(tax_amount)
|
||||
|
||||
return tax_amount, tax_deducted
|
||||
|
||||
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
|
||||
@@ -322,9 +335,6 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
|
||||
else:
|
||||
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
|
||||
|
||||
if cint(tax_details.round_off_tax_amount):
|
||||
tds_amount = round(tds_amount)
|
||||
|
||||
return tds_amount
|
||||
|
||||
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):
|
||||
|
||||
@@ -293,7 +293,7 @@ def check_freezing_date(posting_date, adv_adj=False):
|
||||
if acc_frozen_upto:
|
||||
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,'frozen_accounts_modifier')
|
||||
if getdate(posting_date) <= getdate(acc_frozen_upto) \
|
||||
and not frozen_accounts_modifier in frappe.get_roles() or frappe.session.user == 'Administrator':
|
||||
and (frozen_accounts_modifier not in frappe.get_roles() or frappe.session.user == 'Administrator'):
|
||||
frappe.throw(_("You are not authorized to add or update entries before {0}").format(formatdate(acc_frozen_upto)))
|
||||
|
||||
def set_as_cancel(voucher_type, voucher_no):
|
||||
|
||||
@@ -68,7 +68,7 @@
|
||||
|
||||
{%- if einvoice.ShipDtls -%}
|
||||
{%- set shipping = einvoice.ShipDtls -%}
|
||||
<h5 style="margin-bottom: 5px;">Shipping</h5>
|
||||
<h5 style="margin-bottom: 5px;">Shipped From</h5>
|
||||
<p>{{ shipping.Gstin }}</p>
|
||||
<p>{{ shipping.LglNm }}</p>
|
||||
<p>{{ shipping.Addr1 }}</p>
|
||||
@@ -86,6 +86,17 @@
|
||||
{%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
|
||||
<p>{{ buyer.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
|
||||
|
||||
{%- if einvoice.DispDtls -%}
|
||||
{%- set dispatch = einvoice.DispDtls -%}
|
||||
<h5 style="margin-bottom: 5px;">Dispatched From</h5>
|
||||
{%- if dispatch.Gstin -%} <p>{{ dispatch.Gstin }}</p> {% endif %}
|
||||
<p>{{ dispatch.LglNm }}</p>
|
||||
<p>{{ dispatch.Addr1 }}</p>
|
||||
{%- if dispatch.Addr2 -%} <p>{{ dispatch.Addr2 }}</p> {% endif %}
|
||||
<p>{{ dispatch.Loc }}</p>
|
||||
<p>{{ frappe.db.get_value("Address", doc.dispatch_address_name, "gst_state") }} - {{ dispatch.Pin }}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
<div style="overflow-x: auto;">
|
||||
|
||||
@@ -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,7 +304,10 @@ def get_company_currency(filters=None):
|
||||
return (filters.get('presentation_currency')
|
||||
or frappe.get_cached_value('Company', filters.company, "default_currency"))
|
||||
|
||||
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
|
||||
def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year):
|
||||
start_date = (fiscal_year.year_start_date
|
||||
if filters.filter_based_on == 'Fiscal Year' else filters.period_start_date)
|
||||
|
||||
for entries in gl_entries_by_account.values():
|
||||
for entry in entries:
|
||||
if entry.account_number:
|
||||
@@ -266,15 +316,32 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_d
|
||||
account_name = entry.account_name
|
||||
|
||||
d = accounts_by_name.get(account_name)
|
||||
|
||||
if d:
|
||||
debit, credit = 0, 0
|
||||
for company in companies:
|
||||
# check if posting date is within the period
|
||||
if (entry.company == company or (filters.get('accumulated_in_group_company'))
|
||||
and entry.company in companies.get(company)):
|
||||
d[company] = d.get(company, 0.0) + flt(entry.debit) - flt(entry.credit)
|
||||
parent_company_currency = erpnext.get_company_currency(d.company)
|
||||
child_company_currency = erpnext.get_company_currency(entry.company)
|
||||
|
||||
debit, credit = flt(entry.debit), flt(entry.credit)
|
||||
|
||||
if (not filters.get('presentation_currency')
|
||||
and entry.company != company
|
||||
and parent_company_currency != child_company_currency
|
||||
and filters.get('accumulated_in_group_company')):
|
||||
debit = convert(debit, parent_company_currency, child_company_currency, filters.end_date)
|
||||
credit = convert(credit, parent_company_currency, child_company_currency, filters.end_date)
|
||||
|
||||
d[company] = d.get(company, 0.0) + flt(debit) - flt(credit)
|
||||
|
||||
if entry.posting_date < getdate(start_date):
|
||||
d['company_wise_opening_bal'][company] += (flt(debit) - flt(credit))
|
||||
|
||||
if entry.posting_date < getdate(start_date):
|
||||
d["opening_balance"] = d.get("opening_balance", 0.0) + flt(entry.debit) - flt(entry.credit)
|
||||
d["opening_balance"] = d.get("opening_balance", 0.0) + flt(debit) - flt(credit)
|
||||
|
||||
def accumulate_values_into_parents(accounts, accounts_by_name, companies):
|
||||
"""accumulate children's values in parent accounts"""
|
||||
@@ -282,17 +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)
|
||||
|
||||
@@ -353,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:
|
||||
@@ -367,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)
|
||||
@@ -385,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
|
||||
@@ -447,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)
|
||||
|
||||
@@ -456,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 = []
|
||||
@@ -491,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"])
|
||||
@@ -511,6 +594,7 @@ def filter_accounts(accounts, depth=10):
|
||||
account_name = d.account_number + ' - ' + d.account_name
|
||||
else:
|
||||
account_name = d.account_name
|
||||
d['company_wise_opening_bal'] = defaultdict(float)
|
||||
accounts_by_name[account_name] = d
|
||||
|
||||
parent_children_map.setdefault(d.parent_account or None, []).append(d)
|
||||
|
||||
@@ -155,6 +155,8 @@ def get_gl_entries(filters, accounting_dimensions):
|
||||
|
||||
if filters.get("group_by") == "Group by Voucher":
|
||||
order_by_statement = "order by posting_date, voucher_type, voucher_no"
|
||||
if filters.get("group_by") == "Group by Account":
|
||||
order_by_statement = "order by account, posting_date, creation"
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
filters['company_fb'] = frappe.db.get_value("Company",
|
||||
@@ -421,8 +423,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
update_value_in_dict(totals, 'closing', gle)
|
||||
|
||||
elif gle.posting_date <= to_date:
|
||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
|
||||
update_value_in_dict(totals, 'total', gle)
|
||||
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
|
||||
gle_map[gle.get(group_by)].entries.append(gle)
|
||||
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
|
||||
@@ -436,10 +436,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
else:
|
||||
update_value_in_dict(consolidated_gle, key, gle)
|
||||
|
||||
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
|
||||
update_value_in_dict(totals, 'closing', gle)
|
||||
|
||||
for key, value in consolidated_gle.items():
|
||||
update_value_in_dict(gle_map[value.get(group_by)].totals, 'total', value)
|
||||
update_value_in_dict(totals, 'total', value)
|
||||
update_value_in_dict(gle_map[value.get(group_by)].totals, 'closing', value)
|
||||
update_value_in_dict(totals, 'closing', value)
|
||||
entries.append(value)
|
||||
|
||||
return totals, entries
|
||||
|
||||
@@ -44,16 +44,16 @@ def get_result(filters, tds_docs, tds_accounts, tax_category_map):
|
||||
|
||||
if rate and tds_deducted:
|
||||
row = {
|
||||
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier).pan,
|
||||
'supplier': supplier_map.get(supplier).name
|
||||
'pan' if frappe.db.has_column('Supplier', 'pan') else 'tax_id': supplier_map.get(supplier, {}).get('pan'),
|
||||
'supplier': supplier_map.get(supplier, {}).get('name')
|
||||
}
|
||||
|
||||
if filters.naming_series == 'Naming Series':
|
||||
row.update({'supplier_name': supplier_map.get(supplier).supplier_name})
|
||||
row.update({'supplier_name': supplier_map.get(supplier, {}).get('supplier_name')})
|
||||
|
||||
row.update({
|
||||
'section_code': tax_withholding_category,
|
||||
'entity_type': supplier_map.get(supplier).supplier_type,
|
||||
'entity_type': supplier_map.get(supplier, {}).get('supplier_type'),
|
||||
'tds_rate': rate,
|
||||
'total_amount_credited': total_amount_credited,
|
||||
'tds_deducted': tds_deducted,
|
||||
|
||||
@@ -4,11 +4,14 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from json import loads
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
from frappe import _, throw
|
||||
from frappe.model.meta import get_field_precision
|
||||
from frappe.utils import cint, cstr, flt, formatdate, get_number_format_info, getdate, now, nowdate
|
||||
from six import string_types
|
||||
|
||||
import erpnext
|
||||
|
||||
@@ -447,7 +450,8 @@ def update_reference_in_journal_entry(d, journal_entry, do_not_save=False):
|
||||
|
||||
# new row with references
|
||||
new_row = journal_entry.append("accounts")
|
||||
new_row.update(jv_detail.as_dict().copy())
|
||||
|
||||
new_row.update((frappe.copy_doc(jv_detail)).as_dict())
|
||||
|
||||
new_row.set(d["dr_or_cr"], d["allocated_amount"])
|
||||
new_row.set('debit' if d['dr_or_cr'] == 'debit_in_account_currency' else 'credit',
|
||||
@@ -576,10 +580,10 @@ def remove_ref_doc_link_from_pe(ref_type, ref_no):
|
||||
frappe.msgprint(_("Payment Entries {0} are un-linked").format("\n".join(linked_pe)))
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_company_default(company, fieldname):
|
||||
value = frappe.get_cached_value('Company', company, fieldname)
|
||||
def get_company_default(company, fieldname, ignore_validation=False):
|
||||
value = frappe.get_cached_value('Company', company, fieldname)
|
||||
|
||||
if not value:
|
||||
if not ignore_validation and not value:
|
||||
throw(_("Please set default {0} in Company {1}")
|
||||
.format(frappe.get_meta("Company").get_label(fieldname), company))
|
||||
|
||||
@@ -787,16 +791,28 @@ def get_children(doctype, parent, company, is_root=False):
|
||||
|
||||
if doctype == 'Account':
|
||||
sort_accounts(acc, is_root, key="value")
|
||||
company_currency = frappe.get_cached_value('Company', company, "default_currency")
|
||||
for each in acc:
|
||||
each["company_currency"] = company_currency
|
||||
each["balance"] = flt(get_balance_on(each.get("value"), in_account_currency=False, company=company))
|
||||
|
||||
if each.account_currency != company_currency:
|
||||
each["balance_in_account_currency"] = flt(get_balance_on(each.get("value"), company=company))
|
||||
|
||||
return acc
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_account_balances(accounts, company):
|
||||
|
||||
if isinstance(accounts, string_types):
|
||||
accounts = loads(accounts)
|
||||
|
||||
if not accounts:
|
||||
return []
|
||||
|
||||
company_currency = frappe.get_cached_value("Company", company, "default_currency")
|
||||
|
||||
for account in accounts:
|
||||
account["company_currency"] = company_currency
|
||||
account["balance"] = flt(get_balance_on(account["value"], in_account_currency=False, company=company))
|
||||
if account["account_currency"] and account["account_currency"] != company_currency:
|
||||
account["balance_in_account_currency"] = flt(get_balance_on(account["value"], company=company))
|
||||
|
||||
return accounts
|
||||
|
||||
def create_payment_gateway_account(gateway, payment_channel="Email"):
|
||||
from erpnext.setup.setup_wizard.operations.install_fixtures import create_bank_account
|
||||
|
||||
|
||||
@@ -454,6 +454,17 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "GL Entry",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "KSA VAT Report",
|
||||
"link_to": "KSA VAT",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"only_for": "Saudi Arabia",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@@ -1034,6 +1045,16 @@
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
"label": "KSA VAT Setting",
|
||||
"link_to": "KSA VAT Setting",
|
||||
"link_type": "DocType",
|
||||
"onboard": 0,
|
||||
"only_for": "Saudi Arabia",
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"hidden": 0,
|
||||
"is_query_report": 0,
|
||||
@@ -1082,7 +1103,7 @@
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2021-08-23 16:06:34.167267",
|
||||
"modified": "2021-08-26 13:15:52.872470",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounting",
|
||||
|
||||
@@ -194,7 +194,7 @@ class Asset(AccountsController):
|
||||
start = self.clear_depreciation_schedule()
|
||||
|
||||
# value_after_depreciation - current Asset value
|
||||
if d.value_after_depreciation:
|
||||
if self.docstatus == 1 and d.value_after_depreciation:
|
||||
value_after_depreciation = (flt(d.value_after_depreciation) -
|
||||
flt(self.opening_accumulated_depreciation))
|
||||
else:
|
||||
|
||||
@@ -682,6 +682,27 @@ class TestAsset(unittest.TestCase):
|
||||
# reset indian company
|
||||
frappe.flags.company = company_flag
|
||||
|
||||
def test_expected_value_change(self):
|
||||
"""
|
||||
tests if changing `expected_value_after_useful_life`
|
||||
affects `value_after_depreciation`
|
||||
"""
|
||||
|
||||
asset = create_asset(calculate_depreciation=1)
|
||||
asset.opening_accumulated_depreciation = 2000
|
||||
asset.number_of_depreciations_booked = 1
|
||||
|
||||
asset.finance_books[0].expected_value_after_useful_life = 100
|
||||
asset.save()
|
||||
asset.reload()
|
||||
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
|
||||
|
||||
# changing expected_value_after_useful_life shouldn't affect value_after_depreciation
|
||||
asset.finance_books[0].expected_value_after_useful_life = 200
|
||||
asset.save()
|
||||
asset.reload()
|
||||
self.assertEquals(asset.finance_books[0].value_after_depreciation, 98000.0)
|
||||
|
||||
def create_asset_data():
|
||||
if not frappe.db.exists("Asset Category", "Computers"):
|
||||
create_asset_category()
|
||||
|
||||
@@ -22,7 +22,7 @@ class TestAssetRepair(unittest.TestCase):
|
||||
frappe.db.sql("delete from `tabTax Rule`")
|
||||
|
||||
def test_update_status(self):
|
||||
asset = create_asset()
|
||||
asset = create_asset(submit=1)
|
||||
initial_status = asset.status
|
||||
asset_repair = create_asset_repair(asset = asset)
|
||||
|
||||
@@ -76,7 +76,7 @@ class TestAssetRepair(unittest.TestCase):
|
||||
self.assertEqual(stock_entry.items[0].qty, asset_repair.stock_items[0].consumed_quantity)
|
||||
|
||||
def test_increase_in_asset_value_due_to_stock_consumption(self):
|
||||
asset = create_asset(calculate_depreciation = 1)
|
||||
asset = create_asset(calculate_depreciation = 1, submit=1)
|
||||
initial_asset_value = get_asset_value(asset)
|
||||
asset_repair = create_asset_repair(asset= asset, stock_consumption = 1, submit = 1)
|
||||
asset.reload()
|
||||
@@ -85,7 +85,7 @@ class TestAssetRepair(unittest.TestCase):
|
||||
self.assertEqual(asset_repair.stock_items[0].total_value, increase_in_asset_value)
|
||||
|
||||
def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self):
|
||||
asset = create_asset(calculate_depreciation = 1)
|
||||
asset = create_asset(calculate_depreciation = 1, submit=1)
|
||||
initial_asset_value = get_asset_value(asset)
|
||||
asset_repair = create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
|
||||
asset.reload()
|
||||
@@ -103,7 +103,7 @@ class TestAssetRepair(unittest.TestCase):
|
||||
self.assertEqual(asset_repair.name, gl_entry.voucher_no)
|
||||
|
||||
def test_increase_in_asset_life(self):
|
||||
asset = create_asset(calculate_depreciation = 1)
|
||||
asset = create_asset(calculate_depreciation = 1, submit=1)
|
||||
initial_num_of_depreciations = num_of_depreciations(asset)
|
||||
create_asset_repair(asset= asset, capitalize_repair_cost = 1, submit = 1)
|
||||
asset.reload()
|
||||
@@ -126,7 +126,7 @@ def create_asset_repair(**args):
|
||||
if args.asset:
|
||||
asset = args.asset
|
||||
else:
|
||||
asset = create_asset(is_existing_asset = 1)
|
||||
asset = create_asset(is_existing_asset = 1, submit=1)
|
||||
asset_repair = frappe.new_doc("Asset Repair")
|
||||
asset_repair.update({
|
||||
"asset": asset.name,
|
||||
|
||||
@@ -16,9 +16,8 @@ frappe.query_reports["Fixed Asset Register"] = {
|
||||
fieldname:"status",
|
||||
label: __("Status"),
|
||||
fieldtype: "Select",
|
||||
options: "In Location\nDisposed",
|
||||
default: 'In Location',
|
||||
reqd: 1
|
||||
options: "\nIn Location\nDisposed",
|
||||
default: 'In Location'
|
||||
},
|
||||
{
|
||||
"fieldname":"filter_based_on",
|
||||
|
||||
@@ -45,12 +45,13 @@ def get_conditions(filters):
|
||||
if filters.get('cost_center'):
|
||||
conditions["cost_center"] = filters.get('cost_center')
|
||||
|
||||
# In Store assets are those that are not sold or scrapped
|
||||
operand = 'not in'
|
||||
if status not in 'In Location':
|
||||
operand = 'in'
|
||||
if status:
|
||||
# In Store assets are those that are not sold or scrapped
|
||||
operand = 'not in'
|
||||
if status not in 'In Location':
|
||||
operand = 'in'
|
||||
|
||||
conditions['status'] = (operand, ['Sold', 'Scrapped'])
|
||||
conditions['status'] = (operand, ['Sold', 'Scrapped'])
|
||||
|
||||
return conditions
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ frappe.tour['Buying Settings'] = [
|
||||
{
|
||||
fieldname: "supp_master_name",
|
||||
title: "Supplier Naming By",
|
||||
description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a ") + "<a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a>" + __(" choose the 'Naming Series' option."),
|
||||
description: __("By default, the Supplier Name is set as per the Supplier Name entered. If you want Suppliers to be named by a <a href='https://docs.erpnext.com/docs/user/manual/en/setting-up/settings/naming-series' target='_blank'>Naming Series</a> choose the 'Naming Series' option."),
|
||||
},
|
||||
{
|
||||
fieldname: "buying_price_list",
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
{
|
||||
"creation": "2021-07-28 11:51:42.319984",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2021-10-05 13:06:56.414584",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Buying Settings",
|
||||
"save_on_complete": 0,
|
||||
"steps": [
|
||||
{
|
||||
"description": "When a Supplier is saved, system generates a unique identity or name for that Supplier which can be used to refer the Supplier in various Buying transactions.",
|
||||
"field": "",
|
||||
"fieldname": "supp_master_name",
|
||||
"fieldtype": "Select",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Supplier Naming By",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Supplier Naming By"
|
||||
},
|
||||
{
|
||||
"description": "Configure what should be the default value of Supplier Group when creating a new Supplier.",
|
||||
"field": "",
|
||||
"fieldname": "supplier_group",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Default Supplier Group",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Default Supplier Group"
|
||||
},
|
||||
{
|
||||
"description": "Item prices will be fetched from this Price List.",
|
||||
"field": "",
|
||||
"fieldname": "buying_price_list",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Default Buying Price List",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Default Buying Price List"
|
||||
},
|
||||
{
|
||||
"description": "If this option is configured \"Yes\", ERPNext will prevent you from creating a Purchase Invoice or a Purchase Receipt directly without creating a Purchase Order first.",
|
||||
"field": "",
|
||||
"fieldname": "po_required",
|
||||
"fieldtype": "Select",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Is Purchase Order Required for Purchase Invoice & Receipt Creation?",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Purchase Order Required"
|
||||
},
|
||||
{
|
||||
"description": "If this option is configured \"Yes\", ERPNext will prevent you from creating a Purchase Invoice without creating a Purchase Receipt first.",
|
||||
"field": "",
|
||||
"fieldname": "pr_required",
|
||||
"fieldtype": "Select",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Is Purchase Receipt Required for Purchase Invoice Creation?",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Purchase Receipt Required"
|
||||
}
|
||||
],
|
||||
"title": "Buying Settings"
|
||||
}
|
||||
82
erpnext/buying/form_tour/purchase_order/purchase_order.json
Normal file
82
erpnext/buying/form_tour/purchase_order/purchase_order.json
Normal file
@@ -0,0 +1,82 @@
|
||||
{
|
||||
"creation": "2021-07-29 14:11:58.271113",
|
||||
"docstatus": 0,
|
||||
"doctype": "Form Tour",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"modified": "2021-10-05 13:11:31.436135",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order",
|
||||
"owner": "Administrator",
|
||||
"reference_doctype": "Purchase Order",
|
||||
"save_on_complete": 1,
|
||||
"steps": [
|
||||
{
|
||||
"description": "Select a Supplier",
|
||||
"field": "",
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Supplier",
|
||||
"parent_field": "",
|
||||
"position": "Right",
|
||||
"title": "Supplier"
|
||||
},
|
||||
{
|
||||
"description": "Set the \"Required By\" date for the materials. This sets the \"Required By\" date for all the items.",
|
||||
"field": "",
|
||||
"fieldname": "schedule_date",
|
||||
"fieldtype": "Date",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Required By",
|
||||
"parent_field": "",
|
||||
"position": "Left",
|
||||
"title": "Required By"
|
||||
},
|
||||
{
|
||||
"description": "Items to be purchased can be added here.",
|
||||
"field": "",
|
||||
"fieldname": "items",
|
||||
"fieldtype": "Table",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 0,
|
||||
"label": "Items",
|
||||
"parent_field": "",
|
||||
"position": "Bottom",
|
||||
"title": "Items Table"
|
||||
},
|
||||
{
|
||||
"child_doctype": "Purchase Order Item",
|
||||
"description": "Enter the Item Code.",
|
||||
"field": "",
|
||||
"fieldname": "item_code",
|
||||
"fieldtype": "Link",
|
||||
"has_next_condition": 1,
|
||||
"is_table_field": 1,
|
||||
"label": "Item Code",
|
||||
"next_step_condition": "eval: doc.item_code",
|
||||
"parent_field": "",
|
||||
"parent_fieldname": "items",
|
||||
"position": "Right",
|
||||
"title": "Item Code"
|
||||
},
|
||||
{
|
||||
"child_doctype": "Purchase Order Item",
|
||||
"description": "Enter the required quantity for the material.",
|
||||
"field": "",
|
||||
"fieldname": "qty",
|
||||
"fieldtype": "Float",
|
||||
"has_next_condition": 0,
|
||||
"is_table_field": 1,
|
||||
"label": "Quantity",
|
||||
"parent_field": "",
|
||||
"parent_fieldname": "items",
|
||||
"position": "Bottom",
|
||||
"title": "Quantity"
|
||||
}
|
||||
],
|
||||
"title": "Purchase Order"
|
||||
}
|
||||
@@ -19,7 +19,7 @@
|
||||
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/buying",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"modified": "2020-07-08 14:05:28.273641",
|
||||
"modified": "2021-08-24 18:13:42.463776",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying",
|
||||
@@ -28,23 +28,11 @@
|
||||
{
|
||||
"step": "Introduction to Buying"
|
||||
},
|
||||
{
|
||||
"step": "Create a Supplier"
|
||||
},
|
||||
{
|
||||
"step": "Setup your Warehouse"
|
||||
},
|
||||
{
|
||||
"step": "Create a Product"
|
||||
},
|
||||
{
|
||||
"step": "Create a Material Request"
|
||||
},
|
||||
{
|
||||
"step": "Create your first Purchase Order"
|
||||
},
|
||||
{
|
||||
"step": "Buying Settings"
|
||||
}
|
||||
],
|
||||
"subtitle": "Products, Purchases, Analysis, and more.",
|
||||
|
||||
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"action": "Show Form Tour",
|
||||
"action_label": "Let\u2019s create your first Material Request",
|
||||
"creation": "2020-05-15 14:39:09.818764",
|
||||
"description": "# Track Material Request\n\n\nAlso known as Purchase Request or an Indent, is a document identifying a requirement of a set of items (products or services) for various purposes like procurement, transfer, issue, or manufacturing. Once the Material Request is validated, a purchase manager can take the next actions for purchasing items like requesting RFQ from a supplier or directly placing an order with an identified Supplier.\n\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 1,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-05-15 14:39:09.818764",
|
||||
"modified": "2021-08-24 18:08:08.347501",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create a Material Request",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Material Request",
|
||||
"show_form_tour": 1,
|
||||
"show_full_form": 1,
|
||||
"title": "Create a Material Request",
|
||||
"title": "Track Material Request",
|
||||
"validate_action": 1
|
||||
}
|
||||
@@ -1,19 +1,21 @@
|
||||
{
|
||||
"action": "Create Entry",
|
||||
"action": "Show Form Tour",
|
||||
"action_label": "Let\u2019s create your first Purchase Order",
|
||||
"creation": "2020-05-12 18:17:49.976035",
|
||||
"description": "# Create first Purchase Order\n\nPurchase Order is at the heart of your buying transactions. In ERPNext, Purchase Order can can be created against a Purchase Material Request (indent) and Supplier Quotation as well. Purchase Orders is also linked to Purchase Receipt and Purchase Invoices, allowing you to keep a birds-eye view on your purchase deals.\n\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 0,
|
||||
"is_single": 0,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-05-12 18:31:56.856112",
|
||||
"modified": "2021-08-24 18:08:08.936484",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Create your first Purchase Order",
|
||||
"owner": "Administrator",
|
||||
"reference_document": "Purchase Order",
|
||||
"show_form_tour": 0,
|
||||
"show_full_form": 0,
|
||||
"title": "Create your first Purchase Order",
|
||||
"title": "Create first Purchase Order",
|
||||
"validate_action": 1
|
||||
}
|
||||
@@ -1,19 +1,22 @@
|
||||
{
|
||||
"action": "Watch Video",
|
||||
"action": "Show Form Tour",
|
||||
"action_label": "Let\u2019s walk-through few Buying Settings",
|
||||
"creation": "2020-05-06 15:37:09.477765",
|
||||
"description": "# Buying Settings\n\n\nBuying module\u2019s features are highly configurable as per your business needs. Buying Settings is the place where you can set your preferences for:\n\n- Supplier naming and default values\n- Billing and shipping preference in buying transactions\n\n\n",
|
||||
"docstatus": 0,
|
||||
"doctype": "Onboarding Step",
|
||||
"idx": 0,
|
||||
"is_complete": 0,
|
||||
"is_mandatory": 0,
|
||||
"is_single": 0,
|
||||
"is_single": 1,
|
||||
"is_skipped": 0,
|
||||
"modified": "2020-05-12 18:25:08.509900",
|
||||
"modified": "2021-08-24 18:08:08.345735",
|
||||
"modified_by": "Administrator",
|
||||
"name": "Introduction to Buying",
|
||||
"owner": "Administrator",
|
||||
"show_full_form": 0,
|
||||
"title": "Introduction to Buying",
|
||||
"reference_document": "Buying Settings",
|
||||
"show_form_tour": 1,
|
||||
"show_full_form": 1,
|
||||
"title": "Buying Settings",
|
||||
"validate_action": 1,
|
||||
"video_url": "https://youtu.be/efFajTTQBa8"
|
||||
}
|
||||
@@ -45,7 +45,6 @@ class TestProcurementTracker(unittest.TestCase):
|
||||
pr = make_purchase_receipt(po.name)
|
||||
pr.get("items")[0].cost_center = "Main - _TPC"
|
||||
pr.submit()
|
||||
frappe.db.commit()
|
||||
date_obj = datetime.date(datetime.now())
|
||||
|
||||
po.load_from_db()
|
||||
|
||||
29
erpnext/change_log/v13/v13_13_0.md
Normal file
29
erpnext/change_log/v13/v13_13_0.md
Normal file
@@ -0,0 +1,29 @@
|
||||
# Version 13.13.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- HR Module onboarding ([#25741](https://github.com/frappe/erpnext/pull/25741))
|
||||
- Tracking multiple rounds for the interview ([#25482](https://github.com/frappe/erpnext/pull/25482))
|
||||
- HSN based tax breakup table check in GST Settings (India Localization) ([#27907](https://github.com/frappe/erpnext/pull/27907))
|
||||
|
||||
### Fixes
|
||||
|
||||
- To improve stock transactions added indexes in stock queries and speed up bin updation ([#27758](https://github.com/frappe/erpnext/pull/27758))
|
||||
- Interstate internal transfer invoices not visible in GSTR-1 ([#27970](https://github.com/frappe/erpnext/pull/27970))
|
||||
- Account number and name incorrectly imported using COA importer ([#27967](https://github.com/frappe/erpnext/pull/27967))
|
||||
- Multiple fixes to timesheets ([#27775](https://github.com/frappe/erpnext/pull/27742))
|
||||
- Totals row incorrect value in GL Entry ([#27867](https://github.com/frappe/erpnext/pull/27867))
|
||||
- Sales Order delivery Date not getting set via data import ([#27862](https://github.com/frappe/erpnext/pull/27862))
|
||||
- Add cost center in gl entry for advance payment entry ([#27840](https://github.com/frappe/erpnext/pull/27840))
|
||||
- Item Variant selection empty popup on website ([#27924](https://github.com/frappe/erpnext/pull/27924))
|
||||
- Improve performance of fetching account balance in chart of accounts ([#27661](https://github.com/frappe/erpnext/pull/27661))
|
||||
- Chart Of Accounts import button not visible ([#27748](https://github.com/frappe/erpnext/pull/27748))
|
||||
- Website Items with same Item name unhandled, thumbnails missing ([#27720](https://github.com/frappe/erpnext/pull/27720))
|
||||
- Delete linked Transaction Deletion Record docs on deleting company ([#27785](https://github.com/frappe/erpnext/pull/27785))
|
||||
- Display appropriate message for Payment Term discrepancies in Payment Entry ([#27749](https://github.com/frappe/erpnext/pull/27749))
|
||||
- Updated buying onboarding tours. ([#27800](https://github.com/frappe/erpnext/pull/27800))
|
||||
- Fixed variant qty in BOM while making work order ([#27686](https://github.com/frappe/erpnext/pull/27686))
|
||||
- Availability slots display, disabled Practitioner Schedule ([#27812](https://github.com/frappe/erpnext/pull/27812))
|
||||
- Consolidated report not consider company currency ([#27863](https://github.com/frappe/erpnext/pull/27863))
|
||||
- Batch Number not copied from Purchase Receipt to Stock Entry ([#27794](https://github.com/frappe/erpnext/pull/27794))
|
||||
- Employee Leave Balance report should only consider ledgers of transaction type Leave Allocation ([#27728](https://github.com/frappe/erpnext/pull/27728))
|
||||
42
erpnext/change_log/v13/v13_14_0.md
Normal file
42
erpnext/change_log/v13/v13_14_0.md
Normal file
@@ -0,0 +1,42 @@
|
||||
# Version 13.14.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- KSA E-Invoicing and VAT Report ([#27369](https://github.com/frappe/erpnext/pull/27369))
|
||||
- Added KSA VAT settings to setup KSA VAT accounts
|
||||
- New report KSA VAT to check the vat amounts
|
||||
- Print format for KSA VAT Invoice ([#28166](https://github.com/frappe/erpnext/pull/28166))
|
||||
|
||||
- Provision to setup tax for recurring additional salary in Salary Slip ([#27459](https://github.com/frappe/erpnext/pull/27459))
|
||||
- Add dispatch address in E-invoicing for India localization ([#28084](https://github.com/frappe/erpnext/pull/28084))
|
||||
- Employee initial work history updated when transfer is performed ([#27768](https://github.com/frappe/erpnext/pull/27768))
|
||||
- Provision to setup quality inspection teamplte in the operation which will be use in the Job Card([#28219](https://github.com/frappe/erpnext/pull/28219))
|
||||
- Improved sales invoice submission performance ([#27916](https://github.com/frappe/erpnext/pull/27916))
|
||||
|
||||
|
||||
### Fixes
|
||||
|
||||
- Splitting outstanding rows as per payment terms ([#27946](https://github.com/frappe/erpnext/pull/27946))
|
||||
|
||||
- Make status filter in Fixed Asset Register optional ([#28126](https://github.com/frappe/erpnext/pull/28126))
|
||||
- Skip empty rows while updating unsaved BOM cost ([#28136](https://github.com/frappe/erpnext/pull/28136))
|
||||
- TDS round off not working from second transaction ([#27934](https://github.com/frappe/erpnext/pull/27934))
|
||||
- Update receivable/payable account on company change in the Sales / Purchase Invoice ([#28057](https://github.com/frappe/erpnext/pull/28057))
|
||||
- Changes in Maintenance Schedule gets overwritten on save ([#27990](https://github.com/frappe/erpnext/pull/27990))
|
||||
- Fetch thumbnail from Item master instead of regenerating ([#28005](https://github.com/frappe/erpnext/pull/28005))
|
||||
- Serial Nos not set in the row after scanning in popup ([#28202](https://github.com/frappe/erpnext/pull/28202))
|
||||
- Taxjar customer_address fix, currency fix ([#28262](https://github.com/frappe/erpnext/pull/28262))
|
||||
- TaxJar update - added nexus list, making api call only for nexus ([#27497](https://github.com/frappe/erpnext/pull/27497))
|
||||
- Don't reset rates in Timesheet Detail when Activity Type is cleared ([#28056](https://github.com/frappe/erpnext/pull/28056))
|
||||
- Show full item name in search widget ([#28283](https://github.com/frappe/erpnext/pull/28283))
|
||||
- Avoid automatic customer creation on website user login ([#27914](https://github.com/frappe/erpnext/pull/27914))
|
||||
- POS Closing Entry without linked invoices ([#28042](https://github.com/frappe/erpnext/pull/28042))
|
||||
- Added patch to fix production plan status ([#27567](https://github.com/frappe/erpnext/pull/27567))
|
||||
- Interstate internal transfer invoices was not displying in the GSTR-1 report ([#27970](https://github.com/frappe/erpnext/pull/27970))
|
||||
- Shows opening balance from filtered from date in the stock balance and stock ledger report ([#26877](https://github.com/frappe/erpnext/pull/26877))
|
||||
- Employee filter in YTD and MTD in salary slip ([#27997](https://github.com/frappe/erpnext/pull/27997))
|
||||
- Removed warehouse filter on Batch field for Material Receipt ([#28195](https://github.com/frappe/erpnext/pull/28195))
|
||||
- Account number and name incorrectly imported using COA importer ([#27967](https://github.com/frappe/erpnext/pull/27967))
|
||||
- Autoemail report not showing dynamic report filters ([#28114](https://github.com/frappe/erpnext/pull/28114))
|
||||
- Incorrect VAT Amount in UAE VAT 201 report ([#27994](https://github.com/frappe/erpnext/pull/27994))
|
||||
- Employee Leave Balance report should only consider ledgers of transaction type Leave Allocation([#27728](https://github.com/frappe/erpnext/pull/27728))
|
||||
@@ -820,6 +820,38 @@ class AccountsController(TransactionBase):
|
||||
if frappe.db.get_single_value('Accounts Settings', 'unlink_advance_payment_on_cancelation_of_order'):
|
||||
unlink_ref_doc_from_payment_entries(self)
|
||||
|
||||
if self.doctype == "Sales Order":
|
||||
self.unlink_ref_doc_from_po()
|
||||
|
||||
def unlink_ref_doc_from_po(self):
|
||||
so_items = []
|
||||
for item in self.items:
|
||||
so_items.append(item.name)
|
||||
|
||||
linked_po = list(set(frappe.get_all(
|
||||
'Purchase Order Item',
|
||||
filters = {
|
||||
'sales_order': self.name,
|
||||
'sales_order_item': ['in', so_items],
|
||||
'docstatus': ['<', 2]
|
||||
},
|
||||
pluck='parent'
|
||||
)))
|
||||
|
||||
if linked_po:
|
||||
frappe.db.set_value(
|
||||
'Purchase Order Item', {
|
||||
'sales_order': self.name,
|
||||
'sales_order_item': ['in', so_items],
|
||||
'docstatus': ['<', 2]
|
||||
},{
|
||||
'sales_order': None,
|
||||
'sales_order_item': None
|
||||
}
|
||||
)
|
||||
|
||||
frappe.msgprint(_("Purchase Orders {0} are un-linked").format("\n".join(linked_po)))
|
||||
|
||||
def get_tax_map(self):
|
||||
tax_map = {}
|
||||
for tax in self.get('taxes'):
|
||||
@@ -1037,15 +1069,15 @@ class AccountsController(TransactionBase):
|
||||
|
||||
if role_allowed_to_over_bill in user_roles and total_overbilled_amt > 0.1:
|
||||
frappe.msgprint(_("Overbilling of {} ignored because you have {} role.")
|
||||
.format(total_overbilled_amt, role_allowed_to_over_bill), title=_("Warning"), indicator="orange")
|
||||
.format(total_overbilled_amt, role_allowed_to_over_bill), indicator="orange", alert=True)
|
||||
|
||||
def throw_overbill_exception(self, item, max_allowed_amt):
|
||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||
.format(item.item_code, item.idx, max_allowed_amt))
|
||||
|
||||
def get_company_default(self, fieldname):
|
||||
def get_company_default(self, fieldname, ignore_validation=False):
|
||||
from erpnext.accounts.utils import get_company_default
|
||||
return get_company_default(self.company, fieldname)
|
||||
return get_company_default(self.company, fieldname, ignore_validation=ignore_validation)
|
||||
|
||||
def get_stock_items(self):
|
||||
stock_items = []
|
||||
@@ -1359,8 +1391,8 @@ class AccountsController(TransactionBase):
|
||||
total = 0
|
||||
base_total = 0
|
||||
for d in self.get("payment_schedule"):
|
||||
total += flt(d.payment_amount)
|
||||
base_total += flt(d.base_payment_amount)
|
||||
total += flt(d.payment_amount, d.precision("payment_amount"))
|
||||
base_total += flt(d.base_payment_amount, d.precision("base_payment_amount"))
|
||||
|
||||
base_grand_total = self.get("base_rounded_total") or self.base_grand_total
|
||||
grand_total = self.get("rounded_total") or self.grand_total
|
||||
@@ -1376,8 +1408,9 @@ class AccountsController(TransactionBase):
|
||||
else:
|
||||
grand_total -= self.get("total_advance")
|
||||
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
|
||||
if total != flt(grand_total, self.precision("grand_total")) or \
|
||||
base_total != flt(base_grand_total, self.precision("base_grand_total")):
|
||||
|
||||
if flt(total, self.precision("grand_total")) != flt(grand_total, self.precision("grand_total")) or \
|
||||
flt(base_total, self.precision("base_grand_total")) != flt(base_grand_total, self.precision("base_grand_total")):
|
||||
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
|
||||
|
||||
def is_rounded_total_disabled(self):
|
||||
@@ -1691,17 +1724,58 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
|
||||
|
||||
def update_invoice_status():
|
||||
"""Updates status as Overdue for applicable invoices. Runs daily."""
|
||||
today = getdate()
|
||||
|
||||
for doctype in ("Sales Invoice", "Purchase Invoice"):
|
||||
frappe.db.sql("""
|
||||
update `tab{}` as dt set dt.status = 'Overdue'
|
||||
where dt.docstatus = 1
|
||||
and dt.status != 'Overdue'
|
||||
and dt.outstanding_amount > 0
|
||||
and (dt.grand_total - dt.outstanding_amount) <
|
||||
(select sum(payment_amount) from `tabPayment Schedule` as ps
|
||||
where ps.parent = dt.name and ps.due_date < %s)
|
||||
""".format(doctype), getdate())
|
||||
UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
|
||||
WHERE invoice.docstatus = 1
|
||||
AND invoice.status REGEXP '^Unpaid|^Partly Paid'
|
||||
AND invoice.outstanding_amount > 0
|
||||
AND (
|
||||
{or_condition}
|
||||
(
|
||||
(
|
||||
CASE
|
||||
WHEN invoice.party_account_currency = invoice.currency
|
||||
THEN (
|
||||
CASE
|
||||
WHEN invoice.disable_rounded_total
|
||||
THEN invoice.grand_total
|
||||
ELSE invoice.rounded_total
|
||||
END
|
||||
)
|
||||
ELSE (
|
||||
CASE
|
||||
WHEN invoice.disable_rounded_total
|
||||
THEN invoice.base_grand_total
|
||||
ELSE invoice.base_rounded_total
|
||||
END
|
||||
)
|
||||
END
|
||||
) - invoice.outstanding_amount
|
||||
) < (
|
||||
SELECT SUM(
|
||||
CASE
|
||||
WHEN invoice.party_account_currency = invoice.currency
|
||||
THEN ps.payment_amount
|
||||
ELSE ps.base_payment_amount
|
||||
END
|
||||
)
|
||||
FROM `tabPayment Schedule` ps
|
||||
WHERE ps.parent = invoice.name
|
||||
AND ps.due_date < %(today)s
|
||||
)
|
||||
)
|
||||
""".format(
|
||||
doctype=doctype,
|
||||
or_condition=(
|
||||
"invoice.is_pos AND invoice.due_date < %(today)s OR"
|
||||
if doctype == "Sales Invoice"
|
||||
else ""
|
||||
)
|
||||
), {"today": today}
|
||||
)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):
|
||||
|
||||
@@ -132,7 +132,8 @@ def supplier_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
return frappe.db.sql("""select {field} from `tabSupplier`
|
||||
where docstatus < 2
|
||||
and ({key} like %(txt)s
|
||||
or supplier_name like %(txt)s) and disabled=0
|
||||
or supplier_name like %(txt)s) and disabled=0
|
||||
and (on_hold = 0 or (on_hold = 1 and CURDATE() > release_date))
|
||||
{mcond}
|
||||
order by
|
||||
if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999),
|
||||
@@ -210,12 +211,15 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
meta = frappe.get_meta("Item", cached=True)
|
||||
searchfields = meta.get_search_fields()
|
||||
|
||||
if "description" in searchfields:
|
||||
searchfields.remove("description")
|
||||
# these are handled separately
|
||||
ignored_search_fields = ("item_name", "description")
|
||||
for ignored_field in ignored_search_fields:
|
||||
if ignored_field in searchfields:
|
||||
searchfields.remove(ignored_field)
|
||||
|
||||
columns = ''
|
||||
extra_searchfields = [field for field in searchfields
|
||||
if not field in ["name", "item_group", "description"]]
|
||||
if not field in ["name", "item_group", "description", "item_name"]]
|
||||
|
||||
if extra_searchfields:
|
||||
columns = ", " + ", ".join(extra_searchfields)
|
||||
@@ -252,10 +256,8 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
if frappe.db.count('Item', cache=True) < 50000:
|
||||
# scan description only if items are less than 50000
|
||||
description_cond = 'or tabItem.description LIKE %(txt)s'
|
||||
return frappe.db.sql("""select tabItem.name,
|
||||
if(length(tabItem.item_name) > 40,
|
||||
concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name,
|
||||
tabItem.item_group,
|
||||
return frappe.db.sql("""select
|
||||
tabItem.name, tabItem.item_name, tabItem.item_group,
|
||||
if(length(tabItem.description) > 40, \
|
||||
concat(substr(tabItem.description, 1, 40), "..."), description) as description
|
||||
{columns}
|
||||
@@ -565,7 +567,7 @@ def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters)
|
||||
|
||||
query_filters.append(['name', query_selector, dimensions])
|
||||
|
||||
output = frappe.get_all(doctype, filters=query_filters)
|
||||
output = frappe.get_list(doctype, filters=query_filters)
|
||||
result = [d.name for d in output]
|
||||
|
||||
return [(d,) for d in set(result)]
|
||||
|
||||
@@ -216,11 +216,14 @@ class StatusUpdater(Document):
|
||||
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) /
|
||||
item[args['target_ref_field']]) * 100
|
||||
|
||||
if overflow_percent - allowance > 0.01 and role not in frappe.get_roles():
|
||||
if overflow_percent - allowance > 0.01:
|
||||
item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100)
|
||||
item['reduce_by'] = item[args['target_field']] - item['max_allowed']
|
||||
|
||||
self.limits_crossed_error(args, item, qty_or_amount)
|
||||
if role not in frappe.get_roles():
|
||||
self.limits_crossed_error(args, item, qty_or_amount)
|
||||
else:
|
||||
self.warn_about_bypassing_with_role(item, qty_or_amount, role)
|
||||
|
||||
def limits_crossed_error(self, args, item, qty_or_amount):
|
||||
'''Raise exception for limits crossed'''
|
||||
@@ -238,6 +241,19 @@ class StatusUpdater(Document):
|
||||
frappe.bold(item.get('item_code'))
|
||||
) + '<br><br>' + action_msg, OverAllowanceError, title = _('Limit Crossed'))
|
||||
|
||||
def warn_about_bypassing_with_role(self, item, qty_or_amount, role):
|
||||
action = _("Over Receipt/Delivery") if qty_or_amount == "qty" else _("Overbilling")
|
||||
|
||||
msg = (_("{} of {} {} ignored for item {} because you have {} role.")
|
||||
.format(
|
||||
action,
|
||||
_(item["target_ref_field"].title()),
|
||||
frappe.bold(item["reduce_by"]),
|
||||
frappe.bold(item.get('item_code')),
|
||||
role)
|
||||
)
|
||||
frappe.msgprint(msg, indicator="orange", alert=True)
|
||||
|
||||
def update_qty(self, update_modified=True):
|
||||
"""Updates qty or amount at row level
|
||||
|
||||
|
||||
@@ -79,8 +79,15 @@ class StockController(AccountsController):
|
||||
def clean_serial_nos(self):
|
||||
for row in self.get("items"):
|
||||
if hasattr(row, "serial_no") and row.serial_no:
|
||||
# replace commas by linefeed and remove all spaces in string
|
||||
row.serial_no = row.serial_no.replace(",", "\n").replace(" ", "")
|
||||
# replace commas by linefeed
|
||||
row.serial_no = row.serial_no.replace(",", "\n")
|
||||
|
||||
# strip preceeding and succeeding spaces for each SN
|
||||
# (SN could have valid spaces in between e.g. SN - 123 - 2021)
|
||||
serial_no_list = row.serial_no.split("\n")
|
||||
serial_no_list = [sn.strip() for sn in serial_no_list]
|
||||
|
||||
row.serial_no = "\n".join(serial_no_list)
|
||||
|
||||
def get_gl_entries(self, warehouse_account=None, default_expense_account=None,
|
||||
default_cost_center=None):
|
||||
@@ -591,7 +598,7 @@ def future_sle_exists(args, sl_entries=None):
|
||||
|
||||
data = frappe.db.sql("""
|
||||
select item_code, warehouse, count(name) as total_row
|
||||
from `tabStock Ledger Entry`
|
||||
from `tabStock Ledger Entry` force index (item_warehouse)
|
||||
where
|
||||
({})
|
||||
and timestamp(posting_date, posting_time)
|
||||
|
||||
@@ -260,7 +260,9 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"])
|
||||
|
||||
def calculate_taxes(self):
|
||||
self.doc.rounding_adjustment = 0
|
||||
if not self.doc.get('is_consolidated'):
|
||||
self.doc.rounding_adjustment = 0
|
||||
|
||||
# maintain actual tax rate based on idx
|
||||
actual_tax_dict = dict([[tax.idx, flt(tax.tax_amount, tax.precision("tax_amount"))]
|
||||
for tax in self.doc.get("taxes") if tax.charge_type == "Actual"])
|
||||
@@ -312,7 +314,9 @@ class calculate_taxes_and_totals(object):
|
||||
|
||||
# adjust Discount Amount loss in last tax iteration
|
||||
if i == (len(self.doc.get("taxes")) - 1) and self.discount_amount_applied \
|
||||
and self.doc.discount_amount and self.doc.apply_discount_on == "Grand Total":
|
||||
and self.doc.discount_amount \
|
||||
and self.doc.apply_discount_on == "Grand Total" \
|
||||
and not self.doc.get('is_consolidated'):
|
||||
self.doc.rounding_adjustment = flt(self.doc.grand_total
|
||||
- flt(self.doc.discount_amount) - tax.total,
|
||||
self.doc.precision("rounding_adjustment"))
|
||||
@@ -405,11 +409,16 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.rounding_adjustment = diff
|
||||
|
||||
def calculate_totals(self):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment) \
|
||||
if self.doc.get("taxes") else flt(self.doc.net_total)
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.grand_total = flt(self.doc.get("taxes")[-1].total) + flt(self.doc.rounding_adjustment)
|
||||
else:
|
||||
self.doc.grand_total = flt(self.doc.net_total)
|
||||
|
||||
self.doc.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total
|
||||
if self.doc.get("taxes"):
|
||||
self.doc.total_taxes_and_charges = flt(self.doc.grand_total - self.doc.net_total
|
||||
- flt(self.doc.rounding_adjustment), self.doc.precision("total_taxes_and_charges"))
|
||||
else:
|
||||
self.doc.total_taxes_and_charges = 0.0
|
||||
|
||||
self._set_in_company_currency(self.doc, ["total_taxes_and_charges", "rounding_adjustment"])
|
||||
|
||||
@@ -446,19 +455,20 @@ class calculate_taxes_and_totals(object):
|
||||
self.doc.total_net_weight += d.total_weight
|
||||
|
||||
def set_rounded_total(self):
|
||||
if self.doc.meta.get_field("rounded_total"):
|
||||
if self.doc.is_rounded_total_disabled():
|
||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
||||
return
|
||||
if not self.doc.get('is_consolidated'):
|
||||
if self.doc.meta.get_field("rounded_total"):
|
||||
if self.doc.is_rounded_total_disabled():
|
||||
self.doc.rounded_total = self.doc.base_rounded_total = 0
|
||||
return
|
||||
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
|
||||
self.doc.currency, self.doc.precision("rounded_total"))
|
||||
self.doc.rounded_total = round_based_on_smallest_currency_fraction(self.doc.grand_total,
|
||||
self.doc.currency, self.doc.precision("rounded_total"))
|
||||
|
||||
#if print_in_rate is set, we would have already calculated rounding adjustment
|
||||
self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
|
||||
self.doc.precision("rounding_adjustment"))
|
||||
#if print_in_rate is set, we would have already calculated rounding adjustment
|
||||
self.doc.rounding_adjustment += flt(self.doc.rounded_total - self.doc.grand_total,
|
||||
self.doc.precision("rounding_adjustment"))
|
||||
|
||||
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
||||
self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"])
|
||||
|
||||
def _cleanup(self):
|
||||
if not self.doc.get('is_consolidated'):
|
||||
|
||||
@@ -34,6 +34,7 @@ class Opportunity(TransactionBase):
|
||||
self.validate_item_details()
|
||||
self.validate_uom_is_integer("uom", "qty")
|
||||
self.validate_cust_name()
|
||||
self.map_fields()
|
||||
|
||||
if not self.title:
|
||||
self.title = self.customer_name
|
||||
@@ -41,6 +42,15 @@ class Opportunity(TransactionBase):
|
||||
if not self.with_items:
|
||||
self.items = []
|
||||
|
||||
def map_fields(self):
|
||||
for field in self.meta.fields:
|
||||
if not self.get(field.fieldname):
|
||||
try:
|
||||
value = frappe.db.get_value(self.opportunity_from, self.party_name, field.fieldname)
|
||||
frappe.db.set(self, field.fieldname, value)
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
def make_new_lead_if_required(self):
|
||||
"""Set lead against new opportunity"""
|
||||
if (not self.get("party_name")) and self.contact_email:
|
||||
@@ -295,6 +305,8 @@ def make_request_for_quotation(source_name, target_doc=None):
|
||||
@frappe.whitelist()
|
||||
def make_customer(source_name, target_doc=None):
|
||||
def set_missing_values(source, target):
|
||||
target.opportunity_name = source.name
|
||||
|
||||
if source.opportunity_from == "Lead":
|
||||
target.lead_name = source.party_name
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ class TestECommerceSettings(unittest.TestCase):
|
||||
|
||||
def test_tax_rule_validation(self):
|
||||
frappe.db.sql("update `tabTax Rule` set use_for_shopping_cart = 0")
|
||||
frappe.db.commit()
|
||||
|
||||
cart_settings = self.get_cart_settings()
|
||||
cart_settings.enabled = 1
|
||||
|
||||
@@ -147,7 +147,7 @@ class WebsiteItem(WebsiteGenerator):
|
||||
|
||||
def make_thumbnail(self):
|
||||
"""Make a thumbnail of `website_image`"""
|
||||
if frappe.flags.in_import:
|
||||
if frappe.flags.in_import or frappe.flags.in_migrate:
|
||||
return
|
||||
|
||||
import requests.exceptions
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
import frappe
|
||||
from frappe import _dict
|
||||
from frappe.utils import floor
|
||||
|
||||
|
||||
@@ -96,38 +95,32 @@ class ProductFiltersBuilder:
|
||||
return
|
||||
|
||||
attributes = [row.attribute for row in self.doc.filter_attributes]
|
||||
attribute_docs = [
|
||||
frappe.get_doc('Item Attribute', attribute) for attribute in attributes
|
||||
]
|
||||
|
||||
valid_attributes = []
|
||||
if not attributes:
|
||||
return []
|
||||
|
||||
for attr_doc in attribute_docs:
|
||||
selected_attributes = []
|
||||
for attr in attr_doc.item_attribute_values:
|
||||
or_filters = []
|
||||
filters= [
|
||||
["Item Variant Attribute", "attribute", "=", attr.parent],
|
||||
["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
|
||||
]
|
||||
if self.item_group:
|
||||
or_filters.extend([
|
||||
["item_group", "=", self.item_group],
|
||||
["Website Item Group", "item_group", "=", self.item_group]
|
||||
])
|
||||
result = frappe.db.sql(
|
||||
"""
|
||||
select
|
||||
distinct attribute, attribute_value
|
||||
from
|
||||
`tabItem Variant Attribute`
|
||||
where
|
||||
attribute in %(attributes)s
|
||||
and attribute_value is not null
|
||||
""",
|
||||
{"attributes": attributes},
|
||||
as_dict=1,
|
||||
)
|
||||
|
||||
if frappe.db.get_all("Item", filters, or_filters=or_filters, limit=1):
|
||||
selected_attributes.append(attr)
|
||||
attribute_value_map = {}
|
||||
for d in result:
|
||||
attribute_value_map.setdefault(d.attribute, []).append(d.attribute_value)
|
||||
|
||||
if selected_attributes:
|
||||
valid_attributes.append(
|
||||
_dict(
|
||||
item_attribute_values=selected_attributes,
|
||||
name=attr_doc.name
|
||||
)
|
||||
)
|
||||
|
||||
return valid_attributes
|
||||
out = []
|
||||
for name, values in attribute_value_map.items():
|
||||
out.append(frappe._dict(name=name, item_attribute_values=values))
|
||||
return out
|
||||
|
||||
def get_discount_filters(self, discounts):
|
||||
discount_filters = []
|
||||
@@ -147,4 +140,4 @@ class ProductFiltersBuilder:
|
||||
label = f"{discount}% and below"
|
||||
discount_filters.append([discount, label])
|
||||
|
||||
return discount_filters
|
||||
return discount_filters
|
||||
|
||||
@@ -175,9 +175,7 @@ class TestProductDataEngine(unittest.TestCase):
|
||||
|
||||
filter_engine = ProductFiltersBuilder()
|
||||
attribute_filter = filter_engine.get_attribute_filters()[0]
|
||||
attributes = attribute_filter.item_attribute_values
|
||||
|
||||
attribute_values = [d.attribute_value for d in attributes]
|
||||
attribute_values = attribute_filter.item_attribute_values
|
||||
|
||||
self.assertEqual(attribute_filter.name, "Test Size")
|
||||
self.assertGreater(len(attribute_values), 0)
|
||||
@@ -349,4 +347,4 @@ def create_variant_web_item():
|
||||
variant.save()
|
||||
|
||||
if not frappe.db.exists("Website Item", {"variant_of": "Test Web Item"}):
|
||||
make_website_item(variant, save=True)
|
||||
make_website_item(variant, save=True)
|
||||
|
||||
@@ -1,8 +1,5 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import frappe.defaults
|
||||
|
||||
@@ -17,10 +14,19 @@ def show_cart_count():
|
||||
return False
|
||||
|
||||
def set_cart_count(login_manager):
|
||||
role, parties = check_customer_or_supplier()
|
||||
if role == 'Supplier': return
|
||||
# since this is run only on hooks login event
|
||||
# make sure user is already a customer
|
||||
# before trying to set cart count
|
||||
user_is_customer = is_customer()
|
||||
if not user_is_customer:
|
||||
return
|
||||
|
||||
if show_cart_count():
|
||||
from erpnext.e_commerce.shopping_cart.cart import set_cart_count
|
||||
|
||||
# set_cart_count will try to fetch existing cart quotation
|
||||
# or create one if non existent (and create a customer too)
|
||||
# cart count is calculated from this quotation's items
|
||||
set_cart_count()
|
||||
|
||||
def clear_cart_count(login_manager):
|
||||
@@ -31,13 +37,13 @@ def update_website_context(context):
|
||||
cart_enabled = is_cart_enabled()
|
||||
context["shopping_cart_enabled"] = cart_enabled
|
||||
|
||||
def check_customer_or_supplier():
|
||||
if frappe.session.user:
|
||||
def is_customer():
|
||||
if frappe.session.user and frappe.session.user != "Guest":
|
||||
contact_name = frappe.get_value("Contact", {"email_id": frappe.session.user})
|
||||
if contact_name:
|
||||
contact = frappe.get_doc('Contact', contact_name)
|
||||
for link in contact.links:
|
||||
if link.link_doctype in ('Customer', 'Supplier'):
|
||||
return link.link_doctype, link.link_name
|
||||
if link.link_doctype == 'Customer':
|
||||
return True
|
||||
|
||||
return 'Customer', None
|
||||
return False
|
||||
|
||||
@@ -138,7 +138,9 @@ class Student(Document):
|
||||
enrollment.submit()
|
||||
return enrollment
|
||||
|
||||
def enroll_in_course(self, course_name, program_enrollment, enrollment_date=frappe.utils.datetime.datetime.now()):
|
||||
def enroll_in_course(self, course_name, program_enrollment, enrollment_date=None):
|
||||
if enrollment_date is None:
|
||||
enrollment_date = frappe.utils.datetime.datetime.now()
|
||||
try:
|
||||
enrollment = frappe.get_doc({
|
||||
"doctype": "Course Enrollment",
|
||||
|
||||
@@ -68,5 +68,8 @@ def dump_request_data(data, event="create/order"):
|
||||
@frappe.whitelist()
|
||||
def resync(method, name, request_data):
|
||||
frappe.db.set_value("Shopify Log", name, "status", "Queued", update_modified=False)
|
||||
if not method.startswith("erpnext.erpnext_integrations.connectors.shopify_connection"):
|
||||
return
|
||||
|
||||
frappe.enqueue(method=method, queue='short', timeout=300, is_async=True,
|
||||
**{"order": json.loads(request_data), "request_id": name})
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_rename": 1,
|
||||
"creation": "2021-09-11 05:09:53.773838",
|
||||
"doctype": "DocType",
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"region",
|
||||
"region_code",
|
||||
"country",
|
||||
"country_code"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "region",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Region"
|
||||
},
|
||||
{
|
||||
"fieldname": "region_code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Region Code"
|
||||
},
|
||||
{
|
||||
"fieldname": "country",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Country"
|
||||
},
|
||||
{
|
||||
"fieldname": "country_code",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Country Code"
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-09-14 05:33:06.444710",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "TaxJar Nexus",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class TaxJarNexus(Document):
|
||||
pass
|
||||
@@ -5,5 +5,33 @@ frappe.ui.form.on('TaxJar Settings', {
|
||||
is_sandbox: (frm) => {
|
||||
frm.toggle_reqd("api_key", !frm.doc.is_sandbox);
|
||||
frm.toggle_reqd("sandbox_api_key", frm.doc.is_sandbox);
|
||||
}
|
||||
},
|
||||
|
||||
on_load: (frm) => {
|
||||
frm.set_query('shipping_account_head', function() {
|
||||
return {
|
||||
filters: {
|
||||
'company': frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query('tax_account_head', function() {
|
||||
return {
|
||||
filters: {
|
||||
'company': frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
refresh: (frm) => {
|
||||
frm.add_custom_button(__('Update Nexus List'), function() {
|
||||
frm.call({
|
||||
doc: frm.doc,
|
||||
method: 'update_nexus_list'
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
});
|
||||
|
||||
@@ -6,17 +6,22 @@
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"is_sandbox",
|
||||
"taxjar_calculate_tax",
|
||||
"is_sandbox",
|
||||
"taxjar_create_transactions",
|
||||
"credentials",
|
||||
"api_key",
|
||||
"cb_keys",
|
||||
"sandbox_api_key",
|
||||
"configuration",
|
||||
"company",
|
||||
"column_break_10",
|
||||
"tax_account_head",
|
||||
"configuration_cb",
|
||||
"shipping_account_head"
|
||||
"shipping_account_head",
|
||||
"section_break_12",
|
||||
"nexus_address",
|
||||
"nexus"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@@ -54,6 +59,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "taxjar_calculate_tax",
|
||||
"fieldname": "is_sandbox",
|
||||
"fieldtype": "Check",
|
||||
"label": "Sandbox Mode"
|
||||
@@ -63,12 +69,9 @@
|
||||
"fieldtype": "Password",
|
||||
"label": "Sandbox API Key"
|
||||
},
|
||||
{
|
||||
"fieldname": "configuration_cb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "taxjar_calculate_tax",
|
||||
"fieldname": "taxjar_create_transactions",
|
||||
"fieldtype": "Check",
|
||||
"label": "Create TaxJar Transaction"
|
||||
@@ -82,11 +85,42 @@
|
||||
{
|
||||
"fieldname": "cb_keys",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_12",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Nexus List"
|
||||
},
|
||||
{
|
||||
"fieldname": "nexus_address",
|
||||
"fieldtype": "HTML",
|
||||
"label": "Nexus Address"
|
||||
},
|
||||
{
|
||||
"fieldname": "nexus",
|
||||
"fieldtype": "Table",
|
||||
"label": "Nexus",
|
||||
"options": "TaxJar Nexus",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "configuration_cb",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_10",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"label": "Company",
|
||||
"options": "Company"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2020-04-30 04:38:03.311089",
|
||||
"modified": "2021-11-08 18:02:29.232090",
|
||||
"modified_by": "Administrator",
|
||||
"module": "ERPNext Integrations",
|
||||
"name": "TaxJar Settings",
|
||||
|
||||
@@ -4,9 +4,98 @@
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import json
|
||||
import os
|
||||
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.model.document import Document
|
||||
from frappe.permissions import add_permission, update_permission_property
|
||||
|
||||
from erpnext.erpnext_integrations.taxjar_integration import get_client
|
||||
|
||||
|
||||
class TaxJarSettings(Document):
|
||||
pass
|
||||
|
||||
def on_update(self):
|
||||
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
|
||||
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
|
||||
TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
|
||||
|
||||
fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
|
||||
fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
|
||||
|
||||
if (TAXJAR_CREATE_TRANSACTIONS or TAXJAR_CALCULATE_TAX or TAXJAR_SANDBOX_MODE):
|
||||
if not fields_already_exist:
|
||||
add_product_tax_categories()
|
||||
make_custom_fields()
|
||||
add_permissions()
|
||||
frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
|
||||
|
||||
elif fields_already_exist and fields_hidden:
|
||||
toggle_tax_category_fields(hidden='0')
|
||||
|
||||
elif fields_already_exist:
|
||||
toggle_tax_category_fields(hidden='1')
|
||||
|
||||
def validate(self):
|
||||
self.calculate_taxes_validation_for_create_transactions()
|
||||
|
||||
@frappe.whitelist()
|
||||
def update_nexus_list(self):
|
||||
client = get_client()
|
||||
nexus = client.nexus_regions()
|
||||
|
||||
new_nexus_list = [frappe._dict(address) for address in nexus]
|
||||
|
||||
self.set('nexus', [])
|
||||
self.set('nexus', new_nexus_list)
|
||||
self.save()
|
||||
|
||||
def calculate_taxes_validation_for_create_transactions(self):
|
||||
if not self.taxjar_calculate_tax and (self.taxjar_create_transactions or self.is_sandbox):
|
||||
frappe.throw(frappe._('Before enabling <b>Create Transaction</b> or <b>Sandbox Mode</b>, you need to check the <b>Enable Tax Calculation</b> box'))
|
||||
|
||||
|
||||
def toggle_tax_category_fields(hidden):
|
||||
frappe.set_value('Custom Field', {'dt':'Sales Invoice Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
|
||||
frappe.set_value('Custom Field', {'dt':'Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
|
||||
|
||||
|
||||
def add_product_tax_categories():
|
||||
with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
|
||||
tax_categories = json.loads(f.read())
|
||||
create_tax_categories(tax_categories['categories'])
|
||||
|
||||
def create_tax_categories(data):
|
||||
for d in data:
|
||||
if not frappe.db.exists('Product Tax Category',{'product_tax_code':d.get('product_tax_code')}):
|
||||
tax_category = frappe.new_doc('Product Tax Category')
|
||||
tax_category.description = d.get("description")
|
||||
tax_category.product_tax_code = d.get("product_tax_code")
|
||||
tax_category.category_name = d.get("name")
|
||||
tax_category.db_insert()
|
||||
|
||||
def make_custom_fields(update=True):
|
||||
custom_fields = {
|
||||
'Sales Invoice Item': [
|
||||
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
|
||||
label='Product Tax Category', fetch_from='item_code.product_tax_category'),
|
||||
dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
|
||||
label='Tax Collectable', read_only=1, options='currency'),
|
||||
dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
|
||||
label='Taxable Amount', read_only=1, options='currency')
|
||||
],
|
||||
'Item': [
|
||||
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
|
||||
label='Product Tax Category')
|
||||
]
|
||||
}
|
||||
create_custom_fields(custom_fields, update=update)
|
||||
|
||||
def add_permissions():
|
||||
doctype = "Product Tax Category"
|
||||
for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'):
|
||||
add_permission(doctype, role, 0)
|
||||
update_permission_property(doctype, role, 0, 'write', 1)
|
||||
update_permission_property(doctype, role, 0, 'create', 1)
|
||||
|
||||
@@ -4,9 +4,9 @@ import frappe
|
||||
import taxjar
|
||||
from frappe import _
|
||||
from frappe.contacts.doctype.address.address import get_company_address
|
||||
from frappe.utils import cint
|
||||
from frappe.utils import cint, flt
|
||||
|
||||
from erpnext import get_default_company
|
||||
from erpnext import get_default_company, get_region
|
||||
|
||||
TAX_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "tax_account_head")
|
||||
SHIP_ACCOUNT_HEAD = frappe.db.get_single_value("TaxJar Settings", "shipping_account_head")
|
||||
@@ -21,6 +21,7 @@ SUPPORTED_STATE_CODES = ['AL', 'AK', 'AZ', 'AR', 'CA', 'CO', 'CT', 'DE', 'DC', '
|
||||
'TN', 'TX', 'UT', 'VT', 'VA', 'WA', 'WV', 'WI', 'WY']
|
||||
|
||||
|
||||
|
||||
def get_client():
|
||||
taxjar_settings = frappe.get_single("TaxJar Settings")
|
||||
|
||||
@@ -103,7 +104,7 @@ def get_tax_data(doc):
|
||||
|
||||
shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
|
||||
|
||||
line_items = [get_line_item_dict(item) for item in doc.items]
|
||||
line_items = [get_line_item_dict(item, doc.docstatus) for item in doc.items]
|
||||
|
||||
if from_shipping_state not in SUPPORTED_STATE_CODES:
|
||||
from_shipping_state = get_state_code(from_address, 'Company')
|
||||
@@ -139,18 +140,28 @@ def get_state_code(address, location):
|
||||
|
||||
return state_code
|
||||
|
||||
def get_line_item_dict(item):
|
||||
return dict(
|
||||
def get_line_item_dict(item, docstatus):
|
||||
tax_dict = dict(
|
||||
id = item.get('idx'),
|
||||
quantity = item.get('qty'),
|
||||
unit_price = item.get('rate'),
|
||||
product_tax_code = item.get('product_tax_category')
|
||||
)
|
||||
|
||||
if docstatus == 1:
|
||||
tax_dict.update({
|
||||
'sales_tax':item.get('tax_collectable')
|
||||
})
|
||||
|
||||
return tax_dict
|
||||
|
||||
def set_sales_tax(doc, method):
|
||||
if not TAXJAR_CALCULATE_TAX:
|
||||
return
|
||||
|
||||
if get_region(doc.company) != 'United States':
|
||||
return
|
||||
|
||||
if not doc.items:
|
||||
return
|
||||
|
||||
@@ -164,6 +175,9 @@ def set_sales_tax(doc, method):
|
||||
setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
|
||||
return
|
||||
|
||||
# check if delivering within a nexus
|
||||
check_for_nexus(doc, tax_dict)
|
||||
|
||||
tax_data = validate_tax_request(tax_dict)
|
||||
if tax_data is not None:
|
||||
if not tax_data.amount_to_collect:
|
||||
@@ -191,6 +205,17 @@ def set_sales_tax(doc, method):
|
||||
|
||||
doc.run_method("calculate_taxes_and_totals")
|
||||
|
||||
def check_for_nexus(doc, tax_dict):
|
||||
if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
|
||||
for item in doc.get("items"):
|
||||
item.tax_collectable = flt(0)
|
||||
item.taxable_amount = flt(0)
|
||||
|
||||
for tax in doc.taxes:
|
||||
if tax.account_head == TAX_ACCOUNT_HEAD:
|
||||
doc.taxes.remove(tax)
|
||||
return
|
||||
|
||||
def check_sales_tax_exemption(doc):
|
||||
# if the party is exempt from sales tax, then set all tax account heads to zero
|
||||
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \
|
||||
@@ -241,7 +266,7 @@ def get_shipping_address_details(doc):
|
||||
if doc.shipping_address_name:
|
||||
shipping_address = frappe.get_doc("Address", doc.shipping_address_name)
|
||||
elif doc.customer_address:
|
||||
shipping_address = frappe.get_doc("Address", doc.customer_address_name)
|
||||
shipping_address = frappe.get_doc("Address", doc.customer_address)
|
||||
else:
|
||||
shipping_address = get_company_address_details(doc)
|
||||
|
||||
|
||||
@@ -62,7 +62,9 @@ class InpatientRecord(Document):
|
||||
admit_patient(self, service_unit, check_in, expected_discharge)
|
||||
|
||||
@frappe.whitelist()
|
||||
def discharge(self, check_out=now_datetime()):
|
||||
def discharge(self, check_out=None):
|
||||
if not check_out:
|
||||
check_out = now_datetime()
|
||||
if (getdate(check_out) < getdate(self.admitted_datetime)):
|
||||
frappe.throw(_('Discharge date cannot be less than Admission date'))
|
||||
discharge_patient(self, check_out)
|
||||
|
||||
@@ -433,11 +433,12 @@ let check_and_set_availability = function(frm) {
|
||||
slot_html += `<br><span> <b> ${__('Maximum Capacity:')} </b> ${slot_info.service_unit_capacity} </span>`;
|
||||
}
|
||||
|
||||
slot_html += '</div><br><br>';
|
||||
slot_html += '</div><br>';
|
||||
|
||||
slot_html += slot_info.avail_slot.map(slot => {
|
||||
appointment_count = 0;
|
||||
disabled = false;
|
||||
count_class = tool_tip = '';
|
||||
start_str = slot.from_time;
|
||||
slot_start_time = moment(slot.from_time, 'HH:mm:ss');
|
||||
slot_end_time = moment(slot.to_time, 'HH:mm:ss');
|
||||
@@ -486,10 +487,11 @@ let check_and_set_availability = function(frm) {
|
||||
data-duration=${interval}
|
||||
data-service-unit="${slot_info.service_unit || ''}"
|
||||
style="margin: 0 10px 10px 0; width: auto;" ${disabled ? 'disabled="disabled"' : ""}
|
||||
data-toggle="tooltip" title="${tool_tip}">
|
||||
${start_str.substring(0, start_str.length - 3)}<br>
|
||||
<span class='badge ${count_class}'> ${count} </span>
|
||||
data-toggle="tooltip" title="${tool_tip || ''}">
|
||||
${start_str.substring(0, start_str.length - 3)}
|
||||
${slot_info.service_unit_capacity ? `<br><span class='badge ${count_class}'> ${count} </span>` : ''}
|
||||
</button>`;
|
||||
|
||||
}).join("");
|
||||
|
||||
if (slot_info.service_unit_capacity) {
|
||||
|
||||
@@ -354,7 +354,7 @@ def get_available_slots(practitioner_doc, date):
|
||||
validate_practitioner_schedules(schedule_entry, practitioner)
|
||||
practitioner_schedule = frappe.get_doc('Practitioner Schedule', schedule_entry.schedule)
|
||||
|
||||
if practitioner_schedule:
|
||||
if practitioner_schedule and not practitioner_schedule.disabled:
|
||||
available_slots = []
|
||||
for time_slot in practitioner_schedule.time_slots:
|
||||
if weekday == time_slot.day:
|
||||
|
||||
@@ -21,6 +21,7 @@ class TestPatientMedicalRecord(unittest.TestCase):
|
||||
def setUp(self):
|
||||
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
|
||||
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
|
||||
frappe.db.sql('delete from `tabPatient Appointment`')
|
||||
make_pos_profile()
|
||||
|
||||
def test_medical_record(self):
|
||||
|
||||
@@ -6,7 +6,7 @@ from __future__ import unicode_literals
|
||||
import unittest
|
||||
|
||||
import frappe
|
||||
from frappe.utils import flt, getdate, nowdate
|
||||
from frappe.utils import add_days, flt, getdate, nowdate
|
||||
|
||||
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import (
|
||||
create_appointment,
|
||||
@@ -33,10 +33,12 @@ class TestTherapyPlan(unittest.TestCase):
|
||||
self.assertEqual(plan.status, 'Not Started')
|
||||
|
||||
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
|
||||
session.start_date = getdate()
|
||||
frappe.get_doc(session).submit()
|
||||
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'In Progress')
|
||||
|
||||
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company')
|
||||
session.start_date = add_days(getdate(), 1)
|
||||
frappe.get_doc(session).submit()
|
||||
self.assertEqual(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
|
||||
|
||||
@@ -44,6 +46,7 @@ class TestTherapyPlan(unittest.TestCase):
|
||||
appointment = create_appointment(patient, practitioner, nowdate())
|
||||
|
||||
session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
|
||||
session.start_date = add_days(getdate(), 2)
|
||||
session = frappe.get_doc(session)
|
||||
session.submit()
|
||||
self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user