Merge branch 'develop' of https://github.com/frappe/erpnext into term_loan_enhancement

This commit is contained in:
Deepesh Garg 2021-10-24 16:54:58 +05:30
commit b812660e40
191 changed files with 2605 additions and 1465 deletions

View File

@ -1,6 +1,8 @@
[flake8]
ignore =
B007,
B009,
B010,
B950,
E101,
E111,
@ -65,11 +67,6 @@ ignore =
E713,
E712,
enable-extensions =
M90
select =
M511
max-line-length = 200
exclude=.github/helper/semgrep_rules,test_*.py

View File

@ -37,6 +37,9 @@ sed -i 's/socketio:/# socketio:/g' Procfile
sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext "${GITHUB_WORKSPACE}"
if [ "$TYPE" == "server" ]; then bench setup requirements --dev; fi
bench start &> bench_run_logs.txt &
bench --site test_site reinstall --yes
bench build --app frappe

View File

@ -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

View File

@ -1,64 +0,0 @@
import frappe
from frappe import _
from frappe.model.document import Document
# ruleid: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
self.status = 'Submitted'
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
self.status = 'Submitted'
self.db_set('status', 'Submitted')
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0'))
x = "y"
self.status = x
self.db_set('status', x)
# ok: frappe-modifying-but-not-comitting
def on_submit(self):
x = "y"
self.status = x
self.save()
# ruleid: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
def tainted_method(self):
self.status = "uptate"
# ok: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
def tainted_method(self):
self.status = "update"
self.db_set("status", "update")
# ok: frappe-modifying-but-not-comitting-other-method
class DoctypeClass(Document):
def on_submit(self):
self.good_method()
self.tainted_method()
self.save()
def tainted_method(self):
self.status = "uptate"

View File

@ -1,133 +0,0 @@
# This file specifies rules for correctness according to how frappe doctype data model works.
rules:
- id: frappe-modifying-but-not-comitting
patterns:
- pattern: |
def $METHOD(self, ...):
...
self.$ATTR = ...
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = ...
...
self.db_set(..., self.$ATTR, ...)
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.db_set(..., $SOME_VAR, ...)
- pattern-not: |
def $METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.save()
- metavariable-regex:
metavariable: '$ATTR'
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
- metavariable-regex:
metavariable: "$METHOD"
regex: "(on_submit|on_cancel)"
message: |
DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
languages: [python]
severity: ERROR
- id: frappe-modifying-but-not-comitting-other-method
patterns:
- pattern: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
...
self.db_set(..., self.$ATTR, ...)
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = $SOME_VAR
...
self.db_set(..., $SOME_VAR, ...)
- pattern-not: |
class $DOCTYPE(...):
def $METHOD(self, ...):
...
self.$ANOTHER_METHOD()
...
self.save()
def $ANOTHER_METHOD(self, ...):
...
self.$ATTR = ...
- metavariable-regex:
metavariable: "$METHOD"
regex: "(on_submit|on_cancel)"
message: |
self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
languages: [python]
severity: ERROR
- id: frappe-print-function-in-doctypes
pattern: print(...)
message: |
Did you mean to leave this print statement in? Consider using msgprint or logger instead of print statement.
languages: [python]
severity: WARNING
paths:
include:
- "*/**/doctype/*"
- id: frappe-modifying-child-tables-while-iterating
pattern-either:
- pattern: |
for $ROW in self.$TABLE:
...
self.remove(...)
- pattern: |
for $ROW in self.$TABLE:
...
self.append(...)
message: |
Child table being modified while iterating on it.
languages: [python]
severity: ERROR
paths:
include:
- "*/**/doctype/*"
- id: frappe-same-key-assigned-twice
pattern-either:
- pattern: |
{..., $X: $A, ..., $X: $B, ...}
- pattern: |
dict(..., ($X, $A), ..., ($X, $B), ...)
- pattern: |
_dict(..., ($X, $A), ..., ($X, $B), ...)
message: |
key `$X` is uselessly assigned twice. This could be a potential bug.
languages: [python]
severity: ERROR

View File

@ -1,15 +0,0 @@
from frappe import _
# ruleid: frappe-missing-translate-function-in-report-python
{"label": "Field Label"}
# ruleid: frappe-missing-translate-function-in-report-python
dict(label="Field Label")
# ok: frappe-missing-translate-function-in-report-python
{"label": _("Field Label")}
# ok: frappe-missing-translate-function-in-report-python
dict(label=_("Field Label"))

View File

@ -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

View File

@ -1,6 +0,0 @@
def function_name(input):
# ruleid: frappe-codeinjection-eval
eval(input)
# ok: frappe-codeinjection-eval
eval("1 + 1")

View File

@ -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

View File

@ -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])

View File

@ -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

View File

@ -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

View File

@ -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") }}. ');

View File

@ -1,31 +0,0 @@
import frappe
from frappe import msgprint, throw, _
# ruleid: frappe-missing-translate-function-python
throw("Error Occured")
# ruleid: frappe-missing-translate-function-python
frappe.throw("Error Occured")
# ruleid: frappe-missing-translate-function-python
frappe.msgprint("Useful message")
# ruleid: frappe-missing-translate-function-python
msgprint("Useful message")
# ok: frappe-missing-translate-function-python
translatedmessage = _("Hello")
# ok: frappe-missing-translate-function-python
throw(translatedmessage)
# ok: frappe-missing-translate-function-python
msgprint(translatedmessage)
# ok: frappe-missing-translate-function-python
msgprint(_("Helpful message"))
# ok: frappe-missing-translate-function-python
frappe.throw(_("Error occured"))

View File

@ -1,30 +0,0 @@
rules:
- id: frappe-missing-translate-function-python
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(_("..."), ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(_("..."), ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [python]
severity: ERROR
- id: frappe-missing-translate-function-js
pattern-either:
- patterns:
- pattern: frappe.msgprint("...", ...)
- pattern-not: frappe.msgprint(__("..."), ...)
# ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
- pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
- patterns:
- pattern: frappe.throw("...", ...)
- pattern-not: frappe.throw(__("..."), ...)
# ignore microtemplating
- pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
message: |
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
languages: [javascript]
severity: ERROR

32
.github/try-on-f-cloud-button.svg vendored Normal file
View File

@ -0,0 +1,32 @@
<svg width="201" height="60" viewBox="0 0 201 60" fill="none" xmlns="http://www.w3.org/2000/svg">
<g filter="url(#filter0_dd)">
<rect x="4" y="2" width="193" height="52" rx="6" fill="#2490EF"/>
<path d="M28 22.2891H32.8786V35.5H36.2088V22.2891H41.0874V19.5H28V22.2891Z" fill="white"/>
<path d="M41.6982 35.5H45.0129V28.7109C45.0129 27.2344 46.0866 26.2188 47.5494 26.2188C48.0085 26.2188 48.6388 26.2969 48.95 26.3984V23.4453C48.6543 23.375 48.2419 23.3281 47.9074 23.3281C46.5691 23.3281 45.472 24.1094 45.0362 25.5938H44.9117V23.5H41.6982V35.5Z" fill="white"/>
<path d="M52.8331 40C55.2996 40 56.6068 38.7344 57.2837 36.7969L61.9289 23.5156L58.4197 23.5L55.9221 32.3125H55.7976L53.3233 23.5H49.8374L54.1247 35.8437L53.9302 36.3516C53.4944 37.4766 52.6619 37.5312 51.4947 37.1719L50.7478 39.6562C51.2224 39.8594 51.9927 40 52.8331 40Z" fill="white"/>
<path d="M73.6142 35.7344C77.2401 35.7344 79.4966 33.2422 79.4966 29.5469C79.4966 25.8281 77.2401 23.3438 73.6142 23.3438C69.9883 23.3438 67.7319 25.8281 67.7319 29.5469C67.7319 33.2422 69.9883 35.7344 73.6142 35.7344ZM73.6298 33.1562C71.9569 33.1562 71.101 31.6171 71.101 29.5233C71.101 27.4296 71.9569 25.8827 73.6298 25.8827C75.2715 25.8827 76.1274 27.4296 76.1274 29.5233C76.1274 31.6171 75.2715 33.1562 73.6298 33.1562Z" fill="white"/>
<path d="M84.7253 28.5625C84.7331 27.0156 85.6512 26.1094 86.9895 26.1094C88.3201 26.1094 89.1215 26.9844 89.1137 28.4531V35.5H92.4284V27.8594C92.4284 25.0625 90.7945 23.3438 88.3046 23.3438C86.5306 23.3438 85.2466 24.2187 84.7097 25.6172H84.5697V23.5H81.4106V35.5H84.7253V28.5625Z" fill="white"/>
<path fill-rule="evenodd" clip-rule="evenodd" d="M102.429 19.5H113.429V22.3141H102.429V19.5ZM102.429 35.5V26.6794H112.699V29.4982H105.94V35.5H102.429Z" fill="white"/>
<path d="M131.584 24.9625C131.09 21.5057 128.345 19.5 124.785 19.5C120.589 19.5 117.429 22.463 117.429 27.4924C117.429 32.5142 120.55 35.4848 124.785 35.4848C128.604 35.4848 131.137 33.0916 131.584 30.1211L128.651 30.1059C128.282 31.9293 126.745 32.9549 124.824 32.9549C122.22 32.9549 120.354 31.0632 120.354 27.4924C120.354 23.9824 122.204 22.0299 124.832 22.0299C126.784 22.0299 128.314 23.1011 128.651 24.9625H131.584Z" fill="white"/>
<path d="M136.409 19.7124H133.571V35.2718H136.409V19.7124Z" fill="white"/>
<path d="M144.031 35.5001C147.56 35.5001 149.803 33.0917 149.803 29.483C149.803 25.8667 147.56 23.4507 144.031 23.4507C140.502 23.4507 138.259 25.8667 138.259 29.483C138.259 33.0917 140.502 35.5001 144.031 35.5001ZM144.047 33.2969C142.094 33.2969 141.137 31.6103 141.137 29.4754C141.137 27.3406 142.094 25.6312 144.047 25.6312C145.968 25.6312 146.925 27.3406 146.925 29.4754C146.925 31.6103 145.968 33.2969 144.047 33.2969Z" fill="white"/>
<path d="M159.338 30.3641C159.338 32.1419 158.028 33.0232 156.773 33.0232C155.409 33.0232 154.499 32.0887 154.499 30.6072V23.6025H151.66V31.0327C151.66 33.8361 153.307 35.4239 155.675 35.4239C157.479 35.4239 158.749 34.5046 159.298 33.1979H159.424V35.272H162.176V23.6025H159.338V30.3641Z" fill="white"/>
<path d="M169.014 35.4769C171.084 35.4769 172.017 34.2841 172.464 33.4332H172.637V35.2718H175.429V19.7124H172.582V25.532H172.464C172.033 24.6887 171.147 23.4503 169.022 23.4503C166.238 23.4503 164.05 25.5624 164.05 29.4522C164.05 33.2965 166.175 35.4769 169.014 35.4769ZM169.806 33.2205C167.931 33.2205 166.943 31.6251 166.943 29.437C166.943 27.2642 167.916 25.7067 169.806 25.7067C171.633 25.7067 172.637 27.173 172.637 29.437C172.637 31.701 171.617 33.2205 169.806 33.2205Z" fill="white"/>
</g>
<defs>
<filter id="filter0_dd" x="0" y="0" width="201" height="60" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset/>
<feGaussianBlur stdDeviation="0.25"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.5 0"/>
<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow"/>
<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
<feOffset dy="2"/>
<feGaussianBlur stdDeviation="2"/>
<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.13 0"/>
<feBlend mode="normal" in2="effect1_dropShadow" result="effect2_dropShadow"/>
<feBlend mode="normal" in="SourceGraphic" in2="effect2_dropShadow" result="shape"/>
</filter>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 4.4 KiB

View File

@ -10,13 +10,6 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: returntocorp/semgrep-action@v1
env:
SEMGREP_TIMEOUT: 120
with:
config: >-
r/python.lang.correctness
.github/helper/semgrep_rules
- name: Set up Python 3.8
uses: actions/setup-python@v2
@ -25,3 +18,14 @@ jobs:
- name: Install and Run Pre-commit
uses: pre-commit/action@v2.0.3
- name: Download Semgrep rules
run: git clone --depth 1 https://github.com/frappe/semgrep-rules.git frappe-semgrep-rules
- uses: returntocorp/semgrep-action@v1
env:
SEMGREP_TIMEOUT: 120
with:
config: >-
r/python.lang.correctness
./frappe-semgrep-rules/rules

View File

@ -91,6 +91,8 @@ jobs:
- name: Install
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
env:
TYPE: server
- name: Run Tests
run: cd ~/frappe-bench/ && bench --site test_site run-parallel-tests --app erpnext --use-orchestrator --with-coverage

View File

@ -21,9 +21,9 @@ repos:
hooks:
- id: flake8
additional_dependencies: [
'flake8-mutable',
'flake8-bugbear',
]
args: ['--select=M511', '--config', '.github/helper/.flake8_strict']
args: ['--config', '.github/helper/.flake8_strict']
exclude: ".*setup.py$"
- repo: https://github.com/timothycrosley/isort

View File

@ -39,6 +39,12 @@ ERPNext is built on the [Frappe Framework](https://github.com/frappe/frappe), a
---
<div align="center">
<a href="https://frappecloud.com/deploy?apps=frappe,erpnext&source=erpnext_readme">
<img src=".github/try-on-f-cloud-button.svg" height="40">
</a>
</div>
### Containerized Installation
Use docker to deploy ERPNext in production or for development of [Frappe](https://github.com/frappe/frappe) apps. See https://github.com/frappe/frappe_docker for more details.

View File

@ -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 });
});
});

View File

@ -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 });
});
});

View File

@ -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))

View File

@ -12,7 +12,7 @@ from six import iteritems
from unidecode import unidecode
def create_charts(company, chart_template=None, existing_company=None, custom_chart=None):
def create_charts(company, chart_template=None, existing_company=None, custom_chart=None, from_coa_importer=None):
chart = custom_chart or get_chart(chart_template, existing_company)
if chart:
accounts = []
@ -22,7 +22,7 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
if root_account:
root_type = child.get("root_type")
if account_name not in ["account_number", "account_type",
if account_name not in ["account_name", "account_number", "account_type",
"root_type", "is_group", "tax_rate"]:
account_number = cstr(child.get("account_number")).strip()
@ -35,7 +35,7 @@ def create_charts(company, chart_template=None, existing_company=None, custom_ch
account = frappe.get_doc({
"doctype": "Account",
"account_name": account_name,
"account_name": child.get('account_name') if from_coa_importer else account_name,
"company": company,
"parent_account": parent,
"is_group": is_group,
@ -213,7 +213,7 @@ def validate_bank_account(coa, bank_account):
return (bank_account in accounts)
@frappe.whitelist()
def build_tree_from_json(chart_template, chart_data=None):
def build_tree_from_json(chart_template, chart_data=None, from_coa_importer=False):
''' get chart template from its folder and parse the json to be rendered as tree '''
chart = chart_data or get_chart(chart_template)
@ -226,9 +226,12 @@ def build_tree_from_json(chart_template, chart_data=None):
''' recursively called to form a parent-child based list of dict from chart template '''
for account_name, child in iteritems(children):
account = {}
if account_name in ["account_number", "account_type",\
if account_name in ["account_name", "account_number", "account_type",\
"root_type", "is_group", "tax_rate"]: continue
if from_coa_importer:
account_name = child['account_name']
account['parent_account'] = parent
account['expandable'] = True if identify_is_group(child) else False
account['value'] = (cstr(child.get('account_number')).strip() + ' - ' + account_name) \

View File

@ -174,7 +174,7 @@
"default": "0",
"fieldname": "automatically_fetch_payment_terms",
"fieldtype": "Check",
"label": "Automatically Fetch Payment Terms"
"label": "Automatically Fetch Payment Terms from Order"
},
{
"description": "The percentage you are allowed to bill more against the amount ordered. For example, if the order value is $100 for an item and tolerance is set as 10%, then you are allowed to bill up to $110 ",
@ -282,7 +282,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2021-08-19 11:17:38.788054",
"modified": "2021-10-11 17:42:36.427699",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",

View File

@ -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

View File

@ -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

View File

@ -505,12 +505,13 @@ class PaymentEntry(AccountsController):
def validate_received_amount(self):
if self.paid_from_account_currency == self.paid_to_account_currency:
if self.paid_amount != self.received_amount:
if self.paid_amount < self.received_amount:
frappe.throw(_("Received Amount cannot be greater than Paid Amount"))
def set_received_amount(self):
self.base_received_amount = self.base_paid_amount
if self.paid_from_account_currency == self.paid_to_account_currency:
if self.paid_from_account_currency == self.paid_to_account_currency \
and not self.payment_type == 'Internal Transfer':
self.received_amount = self.paid_amount
def set_amounts_after_tax(self):
@ -712,10 +713,14 @@ class PaymentEntry(AccountsController):
dr_or_cr = "credit" if erpnext.get_party_account_type(self.party_type) == 'Receivable' else "debit"
for d in self.get("references"):
cost_center = self.cost_center
if d.reference_doctype == "Sales Invoice" and not cost_center:
cost_center = frappe.db.get_value(d.reference_doctype, d.reference_name, "cost_center")
gle = party_gl_dict.copy()
gle.update({
"against_voucher_type": d.reference_doctype,
"against_voucher": d.reference_name
"against_voucher": d.reference_name,
"cost_center": cost_center
})
allocated_amount_in_company_currency = flt(flt(d.allocated_amount) * flt(d.exchange_rate),

View File

@ -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",

View File

@ -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",

View File

@ -15,6 +15,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date
from erpnext.accounts.doctype.gl_entry.gl_entry import update_outstanding_amt
from erpnext.accounts.doctype.sales_invoice.sales_invoice import (
check_if_return_invoice_linked_with_payment_entry,
get_total_in_party_account_currency,
is_overdue,
unlink_inter_company_doc,
update_linked_doc,
@ -1183,6 +1184,7 @@ class PurchaseInvoice(BuyingController):
return
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
total = get_total_in_party_account_currency(self)
if not status:
if self.docstatus == 2:
@ -1190,9 +1192,9 @@ class PurchaseInvoice(BuyingController):
elif self.docstatus == 1:
if self.is_internal_transfer():
self.status = 'Internal Transfer'
elif is_overdue(self):
elif is_overdue(self, total):
self.status = "Overdue"
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
elif 0 < outstanding_amount < total:
self.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"

View File

@ -124,6 +124,13 @@
"total_advance",
"outstanding_amount",
"disable_rounded_total",
"column_break4",
"write_off_amount",
"base_write_off_amount",
"write_off_outstanding_amount_automatically",
"column_break_74",
"write_off_account",
"write_off_cost_center",
"advances_section",
"allocate_advances_automatically",
"get_advances",
@ -144,13 +151,6 @@
"column_break_90",
"change_amount",
"account_for_change_amount",
"column_break4",
"write_off_amount",
"base_write_off_amount",
"write_off_outstanding_amount_automatically",
"column_break_74",
"write_off_account",
"write_off_cost_center",
"terms_section_break",
"tc_name",
"terms",
@ -161,14 +161,14 @@
"column_break_84",
"language",
"more_information",
"status",
"inter_company_invoice_reference",
"is_internal_customer",
"represents_company",
"customer_group",
"campaign",
"is_discounted",
"col_break23",
"status",
"is_internal_customer",
"is_discounted",
"source",
"more_info",
"debit_to",
@ -2031,7 +2031,7 @@
"link_fieldname": "consolidated_invoice"
}
],
"modified": "2021-10-02 03:36:10.251715",
"modified": "2021-10-11 20:19:38.667508",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",

View File

@ -1284,12 +1284,20 @@ class SalesInvoice(SellingController):
serial_nos = item.serial_no or ""
si_serial_nos = set(get_serial_nos(serial_nos))
serial_no_diff = si_serial_nos - dn_serial_nos
if si_serial_nos - dn_serial_nos:
frappe.throw(_("Serial Numbers in row {0} does not match with Delivery Note").format(item.idx))
if serial_no_diff:
dn_link = frappe.utils.get_link_to_form("Delivery Note", item.delivery_note)
serial_no_msg = ", ".join(frappe.bold(d) for d in serial_no_diff)
msg = _("Row #{0}: The following Serial Nos are not present in Delivery Note {1}:").format(
item.idx, dn_link)
msg += " " + serial_no_msg
frappe.throw(msg=msg, title=_("Serial Nos Mismatch"))
if item.serial_no and cint(item.qty) != len(si_serial_nos):
frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
frappe.throw(_("Row #{0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
item.idx, item.qty, item.item_code, len(si_serial_nos)))
def update_project(self):
@ -1419,6 +1427,7 @@ class SalesInvoice(SellingController):
return
outstanding_amount = flt(self.outstanding_amount, self.precision("outstanding_amount"))
total = get_total_in_party_account_currency(self)
if not status:
if self.docstatus == 2:
@ -1426,9 +1435,9 @@ class SalesInvoice(SellingController):
elif self.docstatus == 1:
if self.is_internal_transfer():
self.status = 'Internal Transfer'
elif is_overdue(self):
elif is_overdue(self, total):
self.status = "Overdue"
elif 0 < outstanding_amount < flt(self.grand_total, self.precision("grand_total")):
elif 0 < outstanding_amount < total:
self.status = "Partly Paid"
elif outstanding_amount > 0 and getdate(self.due_date) >= getdate():
self.status = "Unpaid"
@ -1455,27 +1464,42 @@ class SalesInvoice(SellingController):
if update:
self.db_set('status', self.status, update_modified = update_modified)
def is_overdue(doc):
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
def get_total_in_party_account_currency(doc):
total_fieldname = (
"grand_total"
if doc.disable_rounded_total
else "rounded_total"
)
if doc.party_account_currency != doc.currency:
total_fieldname = "base_" + total_fieldname
return flt(doc.get(total_fieldname), doc.precision(total_fieldname))
def is_overdue(doc, total):
outstanding_amount = flt(doc.outstanding_amount, doc.precision("outstanding_amount"))
if outstanding_amount <= 0:
return
grand_total = flt(doc.grand_total, doc.precision("grand_total"))
nowdate = getdate()
if doc.payment_schedule:
today = getdate()
if doc.get('is_pos') or not doc.get('payment_schedule'):
return getdate(doc.due_date) < today
# calculate payable amount till date
payable_amount = sum(
payment.payment_amount
for payment in doc.payment_schedule
if getdate(payment.due_date) < nowdate
payment_amount_field = (
"base_payment_amount"
if doc.party_account_currency != doc.currency
else "payment_amount"
)
if (grand_total - outstanding_amount) < payable_amount:
return True
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

View File

@ -1087,8 +1087,6 @@ class TestSalesInvoice(unittest.TestCase):
actual_qty_1 = get_qty_after_transaction(item_code = "_Test Item", warehouse = "Stores - TCP1")
frappe.db.commit()
self.assertEqual(actual_qty_0 - 5, actual_qty_1)
# outgoing_rate
@ -2357,6 +2355,18 @@ class TestSalesInvoice(unittest.TestCase):
si.reload()
self.assertEqual(si.status, "Paid")
def test_sales_invoice_submission_post_account_freezing_date(self):
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', add_days(getdate(), 1))
si = create_sales_invoice(do_not_save=True)
si.posting_date = add_days(getdate(), 1)
si.save()
self.assertRaises(frappe.ValidationError, si.submit)
si.posting_date = getdate()
si.submit()
frappe.db.set_value('Accounts Settings', None, 'acc_frozen_upto', None)
def get_sales_invoice_for_e_invoice():
si = make_sales_invoice_for_ewaybill()
si.naming_series = 'INV-2020-.#####'

View File

@ -33,7 +33,7 @@ class Subscription(Document):
# update start just before the subscription doc is created
self.update_subscription_period(self.start_date)
def update_subscription_period(self, date=None):
def update_subscription_period(self, date=None, return_date=False):
"""
Subscription period is the period to be billed. This method updates the
beginning of the billing period and end of the billing period.
@ -41,28 +41,41 @@ class Subscription(Document):
The beginning of the billing period is represented in the doctype as
`current_invoice_start` and the end of the billing period is represented
as `current_invoice_end`.
"""
self.set_current_invoice_start(date)
self.set_current_invoice_end()
def set_current_invoice_start(self, date=None):
If return_date is True, it wont update the start and end dates.
This is implemented to get the dates to check if is_current_invoice_generated
"""
This sets the date of the beginning of the current billing period.
_current_invoice_start = self.get_current_invoice_start(date)
_current_invoice_end = self.get_current_invoice_end(_current_invoice_start)
if return_date:
return _current_invoice_start, _current_invoice_end
self.current_invoice_start = _current_invoice_start
self.current_invoice_end = _current_invoice_end
def get_current_invoice_start(self, date=None):
"""
This returns the date of the beginning of the current billing period.
If the `date` parameter is not given , it will be automatically set as today's
date.
"""
if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
self.current_invoice_start = add_days(self.trial_period_end, 1)
elif self.trial_period_start and self.is_trialling():
self.current_invoice_start = self.trial_period_start
elif date:
self.current_invoice_start = date
else:
self.current_invoice_start = nowdate()
_current_invoice_start = None
def set_current_invoice_end(self):
if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date):
_current_invoice_start = add_days(self.trial_period_end, 1)
elif self.trial_period_start and self.is_trialling():
_current_invoice_start = self.trial_period_start
elif date:
_current_invoice_start = date
else:
_current_invoice_start = nowdate()
return _current_invoice_start
def get_current_invoice_end(self, date=None):
"""
This sets the date of the end of the current billing period.
This returns the date of the end of the current billing period.
If the subscription is in trial period, it will be set as the end of the
trial period.
@ -71,44 +84,47 @@ class Subscription(Document):
current billing period where `x` is the billing interval from the
`Subscription Plan` in the `Subscription`.
"""
if self.is_trialling() and getdate(self.current_invoice_start) < getdate(self.trial_period_end):
self.current_invoice_end = self.trial_period_end
_current_invoice_end = None
if self.is_trialling() and getdate(date) < getdate(self.trial_period_end):
_current_invoice_end = self.trial_period_end
else:
billing_cycle_info = self.get_billing_cycle_data()
if billing_cycle_info:
if self.is_new_subscription() and getdate(self.start_date) < getdate(self.current_invoice_start):
self.current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
if self.is_new_subscription() and getdate(self.start_date) < getdate(date):
_current_invoice_end = add_to_date(self.start_date, **billing_cycle_info)
# For cases where trial period is for an entire billing interval
if getdate(self.current_invoice_end) < getdate(self.current_invoice_start):
self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
if getdate(self.current_invoice_end) < getdate(date):
_current_invoice_end = add_to_date(date, **billing_cycle_info)
else:
self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info)
_current_invoice_end = add_to_date(date, **billing_cycle_info)
else:
self.current_invoice_end = get_last_day(self.current_invoice_start)
_current_invoice_end = get_last_day(date)
if self.follow_calendar_months:
billing_info = self.get_billing_cycle_and_interval()
billing_interval_count = billing_info[0]['billing_interval_count']
calendar_months = get_calendar_months(billing_interval_count)
calendar_month = 0
current_invoice_end_month = getdate(self.current_invoice_end).month
current_invoice_end_year = getdate(self.current_invoice_end).year
current_invoice_end_month = getdate(_current_invoice_end).month
current_invoice_end_year = getdate(_current_invoice_end).year
for month in calendar_months:
if month <= current_invoice_end_month:
calendar_month = month
if cint(calendar_month - billing_interval_count) <= 0 and \
getdate(self.current_invoice_start).month != 1:
getdate(date).month != 1:
calendar_month = 12
current_invoice_end_year -= 1
self.current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' \
+ cstr(calendar_month) + '-01')
_current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' + cstr(calendar_month) + '-01')
if self.end_date and getdate(self.current_invoice_end) > getdate(self.end_date):
self.current_invoice_end = self.end_date
if self.end_date and getdate(_current_invoice_end) > getdate(self.end_date):
_current_invoice_end = self.end_date
return _current_invoice_end
@staticmethod
def validate_plans_billing_cycle(billing_cycle_data):
@ -488,8 +504,9 @@ class Subscription(Document):
def is_current_invoice_generated(self):
invoice = self.get_current_invoice()
_current_start_date, _current_end_date = self.update_subscription_period(date=add_days(self.current_invoice_end, 1), return_date=True)
if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end):
if invoice and getdate(_current_start_date) <= getdate(invoice.posting_date) <= getdate(_current_end_date):
return True
return False
@ -542,15 +559,15 @@ class Subscription(Document):
else:
self.set_status_grace_period()
if getdate() > getdate(self.current_invoice_end):
self.update_subscription_period(add_days(self.current_invoice_end, 1))
# Generate invoices periodically even if current invoice are unpaid
if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice()
or self.is_prepaid_to_invoice()):
prorate = frappe.db.get_single_value('Subscription Settings', 'prorate')
self.generate_invoice(prorate)
if getdate() > getdate(self.current_invoice_end):
self.update_subscription_period(add_days(self.current_invoice_end, 1))
@staticmethod
def is_paid(invoice):
"""

View File

@ -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()

View File

@ -203,6 +203,9 @@ def get_tax_amount(party_type, parties, inv, tax_details, posting_date, pan_no=N
# then chargeable value is "prev invoices + advances" value which cross the threshold
tax_amount = get_tcs_amount(parties, inv, tax_details, vouchers, advance_vouchers)
if cint(tax_details.round_off_tax_amount):
tax_amount = round(tax_amount)
return tax_amount, tax_deducted
def get_invoice_vouchers(parties, tax_details, company, party_type='Supplier'):
@ -322,9 +325,6 @@ def get_tds_amount(ldc, parties, inv, tax_details, tax_deducted, vouchers):
else:
tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
if cint(tax_details.round_off_tax_amount):
tds_amount = round(tds_amount)
return tds_amount
def get_tcs_amount(parties, inv, tax_details, vouchers, adv_vouchers):

View File

@ -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):

View File

@ -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>`);

View File

@ -3,12 +3,14 @@
from __future__ import unicode_literals
from collections import defaultdict
import frappe
from frappe import _
from frappe.utils import cint, flt, getdate
import erpnext
from erpnext.accounts.report.balance_sheet.balance_sheet import (
check_opening_balance,
get_chart_data,
get_provisional_profit_loss,
)
@ -31,7 +33,7 @@ from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement
from erpnext.accounts.report.profit_and_loss_statement.profit_and_loss_statement import (
get_report_summary as get_pl_summary,
)
from erpnext.accounts.report.utils import convert_to_presentation_currency
from erpnext.accounts.report.utils import convert, convert_to_presentation_currency
def execute(filters=None):
@ -42,7 +44,7 @@ def execute(filters=None):
fiscal_year = get_fiscal_year_data(filters.get('from_fiscal_year'), filters.get('to_fiscal_year'))
companies_column, companies = get_companies(filters)
columns = get_columns(companies_column)
columns = get_columns(companies_column, filters)
if filters.get('report') == "Balance Sheet":
data, message, chart, report_summary = get_balance_sheet_data(fiscal_year, companies, columns, filters)
@ -73,21 +75,24 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
provisional_profit_loss, total_credit = get_provisional_profit_loss(asset, liability, equity,
companies, filters.get('company'), company_currency, True)
message, opening_balance = check_opening_balance(asset, liability, equity)
message, opening_balance = prepare_companywise_opening_balance(asset, liability, equity, companies)
if opening_balance and round(opening_balance,2) !=0:
unclosed ={
if opening_balance:
unclosed = {
"account_name": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
"account": "'" + _("Unclosed Fiscal Years Profit / Loss (Credit)") + "'",
"warn_if_negative": True,
"currency": company_currency
}
for company in companies:
unclosed[company] = opening_balance
if provisional_profit_loss:
provisional_profit_loss[company] = provisional_profit_loss[company] - opening_balance
unclosed["total"]=opening_balance
for company in companies:
unclosed[company] = opening_balance.get(company)
if provisional_profit_loss and provisional_profit_loss.get(company):
provisional_profit_loss[company] = (
flt(provisional_profit_loss[company]) - flt(opening_balance.get(company))
)
unclosed["total"] = opening_balance.get(company)
data.append(unclosed)
if provisional_profit_loss:
@ -102,6 +107,37 @@ def get_balance_sheet_data(fiscal_year, companies, columns, filters):
return data, message, chart, report_summary
def prepare_companywise_opening_balance(asset_data, liability_data, equity_data, companies):
opening_balance = {}
for company in companies:
opening_value = 0
# opening_value = Aseet - liability - equity
for data in [asset_data, liability_data, equity_data]:
account_name = get_root_account_name(data[0].root_type, company)
opening_value += (get_opening_balance(account_name, data, company) or 0.0)
opening_balance[company] = opening_value
if opening_balance:
return _("Previous Financial Year is not closed"), opening_balance
return '', {}
def get_opening_balance(account_name, data, company):
for row in data:
if row.get('account_name') == account_name:
return row.get('company_wise_opening_bal', {}).get(company, 0.0)
def get_root_account_name(root_type, company):
return frappe.get_all(
'Account',
fields=['account_name'],
filters = {'root_type': root_type, 'is_group': 1,
'company': company, 'parent_account': ('is', 'not set')},
as_list=1
)[0][0]
def get_profit_loss_data(fiscal_year, companies, columns, filters):
income, expense, net_profit_loss = get_income_expense_data(companies, fiscal_year, filters)
company_currency = get_company_currency(filters)
@ -193,30 +229,37 @@ def get_account_type_based_data(account_type, companies, fiscal_year, filters):
data["total"] = total
return data
def get_columns(companies):
columns = [{
def get_columns(companies, filters):
columns = [
{
"fieldname": "account",
"label": _("Account"),
"fieldtype": "Link",
"options": "Account",
"width": 300
}]
columns.append({
}, {
"fieldname": "currency",
"label": _("Currency"),
"fieldtype": "Link",
"options": "Currency",
"hidden": 1
})
}
]
for company in companies:
apply_currency_formatter = 1 if not filters.presentation_currency else 0
currency = filters.presentation_currency
if not currency:
currency = erpnext.get_company_currency(company)
columns.append({
"fieldname": company,
"label": company,
"label": f'{company} ({currency})',
"fieldtype": "Currency",
"options": "currency",
"width": 150
"width": 150,
"apply_currency_formatter": apply_currency_formatter,
"company_name": company
})
return columns
@ -236,6 +279,8 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
start_date = filters.period_start_date if filters.report != 'Balance Sheet' else None
end_date = filters.period_end_date
filters.end_date = end_date
gl_entries_by_account = {}
for root in frappe.db.sql("""select lft, rgt from tabAccount
where root_type=%s and ifnull(parent_account, '') = ''""", root_type, as_dict=1):
@ -244,9 +289,10 @@ def get_data(companies, root_type, balance_must_be, fiscal_year, filters=None, i
end_date, root.lft, root.rgt, filters,
gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year)
accumulate_values_into_parents(accounts, accounts_by_name, companies)
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency)
out = prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters)
if out:
add_total_row(out, root_type, balance_must_be, companies, company_currency)
@ -257,7 +303,10 @@ def get_company_currency(filters=None):
return (filters.get('presentation_currency')
or frappe.get_cached_value('Company', filters.company, "default_currency"))
def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters):
def calculate_values(accounts_by_name, gl_entries_by_account, companies, filters, fiscal_year):
start_date = (fiscal_year.year_start_date
if filters.filter_based_on == 'Fiscal Year' else filters.period_start_date)
for entries in gl_entries_by_account.values():
for entry in entries:
if entry.account_number:
@ -266,15 +315,32 @@ def calculate_values(accounts_by_name, gl_entries_by_account, companies, start_d
account_name = entry.account_name
d = accounts_by_name.get(account_name)
if d:
debit, credit = 0, 0
for company in companies:
# check if posting date is within the period
if (entry.company == company or (filters.get('accumulated_in_group_company'))
and entry.company in companies.get(company)):
d[company] = d.get(company, 0.0) + flt(entry.debit) - flt(entry.credit)
parent_company_currency = erpnext.get_company_currency(d.company)
child_company_currency = erpnext.get_company_currency(entry.company)
debit, credit = flt(entry.debit), flt(entry.credit)
if (not filters.get('presentation_currency')
and entry.company != company
and parent_company_currency != child_company_currency
and filters.get('accumulated_in_group_company')):
debit = convert(debit, parent_company_currency, child_company_currency, filters.end_date)
credit = convert(credit, parent_company_currency, child_company_currency, filters.end_date)
d[company] = d.get(company, 0.0) + flt(debit) - flt(credit)
if entry.posting_date < getdate(start_date):
d["opening_balance"] = d.get("opening_balance", 0.0) + flt(entry.debit) - flt(entry.credit)
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(debit) - flt(credit)
def accumulate_values_into_parents(accounts, accounts_by_name, companies):
"""accumulate children's values in parent accounts"""
@ -282,17 +348,18 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies):
if d.parent_account:
account = d.parent_account_name
if not accounts_by_name.get(account):
continue
# if not accounts_by_name.get(account):
# continue
for company in companies:
accounts_by_name[account][company] = \
accounts_by_name[account].get(company, 0.0) + d.get(company, 0.0)
accounts_by_name[account]['company_wise_opening_bal'][company] += d.get('company_wise_opening_bal', {}).get(company, 0.0)
accounts_by_name[account]["opening_balance"] = \
accounts_by_name[account].get("opening_balance", 0.0) + d.get("opening_balance", 0.0)
def get_account_heads(root_type, companies, filters):
accounts = get_accounts(root_type, filters)
@ -353,7 +420,7 @@ def get_accounts(root_type, filters):
`tabAccount` where company = %s and root_type = %s
""" , (filters.get('company'), root_type), as_dict=1)
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency):
def prepare_data(accounts, start_date, end_date, balance_must_be, companies, company_currency, filters):
data = []
for d in accounts:
@ -367,10 +434,13 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
"parent_account": _(d.parent_account),
"indent": flt(d.indent),
"year_start_date": start_date,
"root_type": d.root_type,
"year_end_date": end_date,
"currency": company_currency,
"currency": filters.presentation_currency,
"company_wise_opening_bal": d.company_wise_opening_bal,
"opening_balance": d.get("opening_balance", 0.0) * (1 if balance_must_be == "Debit" else -1)
})
for company in companies:
if d.get(company) and balance_must_be == "Credit":
# change sign based on Debit or Credit, since calculation is done using (debit - credit)
@ -385,6 +455,7 @@ def prepare_data(accounts, start_date, end_date, balance_must_be, companies, com
row["has_value"] = has_value
row["total"] = total
data.append(row)
return data
@ -447,6 +518,7 @@ def get_account_details(account):
'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1)
def validate_entries(key, entry, accounts_by_name, accounts):
# If an account present in the child company and not in the parent company
if key not in accounts_by_name:
args = get_account_details(entry.account)
@ -456,12 +528,23 @@ def validate_entries(key, entry, accounts_by_name, accounts):
args.update({
'lft': parent_args.lft + 1,
'rgt': parent_args.rgt - 1,
'indent': 3,
'root_type': parent_args.root_type,
'report_type': parent_args.report_type
'report_type': parent_args.report_type,
'parent_account_name': parent_args.account_name,
'company_wise_opening_bal': defaultdict(float)
})
accounts_by_name.setdefault(key, args)
accounts.append(args)
idx = len(accounts)
# To identify parent account index
for index, row in enumerate(accounts):
if row.parent_account_name == args.parent_account_name:
idx = index
break
accounts.insert(idx+1, args)
def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = []
@ -491,7 +574,6 @@ def add_total_row(out, root_type, balance_must_be, companies, company_currency):
for company in companies:
total_row.setdefault(company, 0.0)
total_row[company] += row.get(company, 0.0)
row[company] = 0.0
total_row.setdefault("total", 0.0)
total_row["total"] += flt(row["total"])
@ -511,6 +593,7 @@ def filter_accounts(accounts, depth=10):
account_name = d.account_number + ' - ' + d.account_name
else:
account_name = d.account_name
d['company_wise_opening_bal'] = defaultdict(float)
accounts_by_name[account_name] = d
parent_children_map.setdefault(d.parent_account or None, []).append(d)

View File

@ -421,8 +421,6 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
update_value_in_dict(totals, 'closing', gle)
elif gle.posting_date <= to_date:
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'total', gle)
update_value_in_dict(totals, 'total', gle)
if filters.get("group_by") != 'Group by Voucher (Consolidated)':
gle_map[gle.get(group_by)].entries.append(gle)
elif filters.get("group_by") == 'Group by Voucher (Consolidated)':
@ -436,10 +434,11 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
else:
update_value_in_dict(consolidated_gle, key, gle)
update_value_in_dict(gle_map[gle.get(group_by)].totals, 'closing', gle)
update_value_in_dict(totals, 'closing', gle)
for key, value in consolidated_gle.items():
update_value_in_dict(gle_map[value.get(group_by)].totals, 'total', value)
update_value_in_dict(totals, 'total', value)
update_value_in_dict(gle_map[value.get(group_by)].totals, 'closing', value)
update_value_in_dict(totals, 'closing', value)
entries.append(value)
return totals, entries

View File

@ -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,

View File

@ -1,5 +1,4 @@
{
"category": "",
"charts": [
{
"chart_name": "Profit and Loss",
@ -8,18 +7,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Accounts\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Profit and Loss\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Chart of Accounts\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Invoice\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Invoice\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Journal Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Payment Entry\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Accounts Receivable\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"General Ledger\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Trial Balance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounting Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"General Ledger\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounts Receivable\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Accounts Payable\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Financial Statements\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Multi Currency\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bank Statement\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Subscription Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Goods and Services Tax (GST India)\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Share Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Cost Center and Budgeting\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Opening and Closing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Taxes\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Profitability\", \"col\": 4}}]",
"creation": "2020-03-02 15:41:59.515192",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "accounting",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "Accounting",
"links": [
{
@ -533,6 +526,17 @@
"only_for": "United Arab Emirates",
"type": "Link"
},
{
"dependencies": "GL Entry",
"hidden": 0,
"is_query_report": 1,
"label": "KSA VAT Report",
"link_to": "KSA VAT",
"link_type": "Report",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -1153,6 +1157,16 @@
"onboard": 0,
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
"label": "KSA VAT Setting",
"link_to": "KSA VAT Setting",
"link_type": "DocType",
"onboard": 0,
"only_for": "Saudi Arabia",
"type": "Link"
},
{
"hidden": 0,
"is_query_report": 0,
@ -1206,15 +1220,12 @@
"type": "Link"
}
],
"modified": "2021-08-27 12:15:52.872470",
"modified": "2021-08-27 12:15:52.872471",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
"onboarding": "Accounts",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],

View File

@ -1,20 +1,13 @@
{
"category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Crops & Lands\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Analytics\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Diseases & Fertilizers\", \"col\": 4}}]",
"creation": "2020-03-02 17:23:34.339274",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "agriculture",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "Agriculture",
"links": [
{
@ -163,15 +156,12 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:15:54.595197",
"modified": "2021-08-05 12:15:54.595198",
"modified_by": "Administrator",
"module": "Agriculture",
"name": "Agriculture",
"onboarding": "",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "Agriculture",
"roles": [],

View File

@ -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:

View File

@ -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()

View File

@ -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,

View File

@ -1,5 +1,4 @@
{
"category": "",
"charts": [
{
"chart_name": "Asset Value Analytics",
@ -8,18 +7,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Assets\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Asset Value Analytics\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Asset\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Asset Category\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Fixed Asset Register\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assets\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Maintenance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
"creation": "2020-03-02 15:43:27.634865",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "assets",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "Assets",
"links": [
{
@ -179,15 +172,12 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:15:54.839452",
"modified": "2021-08-05 12:15:54.839453",
"modified_by": "Administrator",
"module": "Assets",
"name": "Assets",
"onboarding": "Assets",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],

View File

@ -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",

View File

@ -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"
}

View 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"
}

View File

@ -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.",

View File

@ -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
}

View File

@ -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
}

View File

@ -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"
}

View File

@ -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()

View File

@ -1,27 +1,18 @@
{
"cards_label": "",
"category": "",
"charts": [
{
"chart_name": "Purchase Order Trends",
"label": "Purchase Order Trends"
}
],
"charts_label": "",
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Buying\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Purchase Order Trends\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Material Request\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Analytics\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Purchase Order Analysis\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Buying\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Items & Pricing\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Supplier\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Supplier Scorecard\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Regional\", \"col\": 4}}]",
"creation": "2020-01-28 11:50:26.195467",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "buying",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "Buying",
"links": [
{
@ -518,15 +509,12 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:15:56.218427",
"modified": "2021-08-05 12:15:56.218428",
"modified_by": "Administrator",
"module": "Buying",
"name": "Buying",
"onboarding": "Buying",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],
@ -572,6 +560,5 @@
"type": "Dashboard"
}
],
"shortcuts_label": "",
"title": "Buying"
}

View File

@ -1686,17 +1686,58 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
def update_invoice_status():
"""Updates status as Overdue for applicable invoices. Runs daily."""
today = getdate()
for doctype in ("Sales Invoice", "Purchase Invoice"):
frappe.db.sql("""
update `tab{}` as dt set dt.status = 'Overdue'
where dt.docstatus = 1
and dt.status != 'Overdue'
and dt.outstanding_amount > 0
and (dt.grand_total - dt.outstanding_amount) <
(select sum(payment_amount) from `tabPayment Schedule` as ps
where ps.parent = dt.name and ps.due_date < %s)
""".format(doctype), getdate())
UPDATE `tab{doctype}` invoice SET invoice.status = 'Overdue'
WHERE invoice.docstatus = 1
AND invoice.status REGEXP '^Unpaid|^Partly Paid'
AND invoice.outstanding_amount > 0
AND (
{or_condition}
(
(
CASE
WHEN invoice.party_account_currency = invoice.currency
THEN (
CASE
WHEN invoice.disable_rounded_total
THEN invoice.grand_total
ELSE invoice.rounded_total
END
)
ELSE (
CASE
WHEN invoice.disable_rounded_total
THEN invoice.base_grand_total
ELSE invoice.base_rounded_total
END
)
END
) - invoice.outstanding_amount
) < (
SELECT SUM(
CASE
WHEN invoice.party_account_currency = invoice.currency
THEN ps.payment_amount
ELSE ps.base_payment_amount
END
)
FROM `tabPayment Schedule` ps
WHERE ps.parent = invoice.name
AND ps.due_date < %(today)s
)
)
""".format(
doctype=doctype,
or_condition=(
"invoice.is_pos AND invoice.due_date < %(today)s OR"
if doctype == "Sales Invoice"
else ""
)
), {"today": today}
)
@frappe.whitelist()
def get_payment_terms(terms_template, posting_date=None, grand_total=None, base_grand_total=None, bill_date=None):

View File

@ -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)

View File

@ -33,6 +33,7 @@ class Opportunity(TransactionBase):
self.validate_item_details()
self.validate_uom_is_integer("uom", "qty")
self.validate_cust_name()
self.map_fields()
if not self.title:
self.title = self.customer_name
@ -43,6 +44,15 @@ class Opportunity(TransactionBase):
else:
self.calculate_totals()
def map_fields(self):
for field in self.meta.fields:
if not self.get(field.fieldname):
try:
value = frappe.db.get_value(self.opportunity_from, self.party_name, field.fieldname)
frappe.db.set(self, field.fieldname, value)
except Exception:
continue
def calculate_totals(self):
total = base_total = 0
for item in self.get('items'):

View File

@ -1,5 +1,4 @@
{
"category": "",
"charts": [
{
"chart_name": "Territory Wise Sales"
@ -7,18 +6,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"CRM\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Lead\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Opportunity\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Customer\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Sales Analytics\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Sales Pipeline\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Maintenance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Campaign\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
"creation": "2020-01-23 14:48:30.183272",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "crm",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "CRM",
"links": [
{
@ -421,15 +414,12 @@
"type": "Link"
}
],
"modified": "2021-08-19 19:08:08.728876",
"modified": "2021-08-20 12:15:56.913092",
"modified_by": "Administrator",
"module": "CRM",
"name": "CRM",
"onboarding": "CRM",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],

View File

@ -1,5 +1,4 @@
{
"category": "",
"charts": [
{
"chart_name": "Program Enrollments",
@ -8,18 +7,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Education\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Program Enrollments\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Instructor\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Program\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Course\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Fees\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Course Scheduling Tool\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Student Attendance Tool\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Student and Instructor\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Content Masters\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Admission\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fees\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Schedule\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"LMS Activity\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assessment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Assessment Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
"creation": "2020-03-02 17:22:57.066401",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "education",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "Education",
"links": [
{
@ -699,15 +692,12 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:15:57.929275",
"modified": "2021-08-05 12:15:57.929276",
"modified_by": "Administrator",
"module": "Education",
"name": "Education",
"onboarding": "Education",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "Education",
"roles": [],

View File

@ -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
}

View File

@ -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

View File

@ -5,5 +5,16 @@ frappe.ui.form.on('TaxJar Settings', {
is_sandbox: (frm) => {
frm.toggle_reqd("api_key", !frm.doc.is_sandbox);
frm.toggle_reqd("sandbox_api_key", frm.doc.is_sandbox);
}
},
refresh: (frm) => {
frm.add_custom_button(__('Update Nexus List'), function() {
frm.call({
doc: frm.doc,
method: 'update_nexus_list'
});
});
},
});

View File

@ -6,8 +6,8 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"is_sandbox",
"taxjar_calculate_tax",
"is_sandbox",
"taxjar_create_transactions",
"credentials",
"api_key",
@ -16,7 +16,10 @@
"configuration",
"tax_account_head",
"configuration_cb",
"shipping_account_head"
"shipping_account_head",
"section_break_12",
"nexus_address",
"nexus"
],
"fields": [
{
@ -54,6 +57,7 @@
},
{
"default": "0",
"depends_on": "taxjar_calculate_tax",
"fieldname": "is_sandbox",
"fieldtype": "Check",
"label": "Sandbox Mode"
@ -69,6 +73,7 @@
},
{
"default": "0",
"depends_on": "taxjar_calculate_tax",
"fieldname": "taxjar_create_transactions",
"fieldtype": "Check",
"label": "Create TaxJar Transaction"
@ -82,11 +87,28 @@
{
"fieldname": "cb_keys",
"fieldtype": "Column Break"
},
{
"fieldname": "section_break_12",
"fieldtype": "Section Break",
"label": "Nexus List"
},
{
"fieldname": "nexus_address",
"fieldtype": "HTML",
"label": "Nexus Address"
},
{
"fieldname": "nexus",
"fieldtype": "Table",
"label": "Nexus",
"options": "TaxJar Nexus",
"read_only": 1
}
],
"issingle": 1,
"links": [],
"modified": "2020-04-30 04:38:03.311089",
"modified": "2021-10-06 10:59:13.475442",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "TaxJar Settings",

View File

@ -4,9 +4,98 @@
from __future__ import unicode_literals
# import frappe
import json
import os
import frappe
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.model.document import Document
from frappe.permissions import add_permission, update_permission_property
from erpnext.erpnext_integrations.taxjar_integration import get_client
class TaxJarSettings(Document):
pass
def on_update(self):
TAXJAR_CREATE_TRANSACTIONS = frappe.db.get_single_value("TaxJar Settings", "taxjar_create_transactions")
TAXJAR_CALCULATE_TAX = frappe.db.get_single_value("TaxJar Settings", "taxjar_calculate_tax")
TAXJAR_SANDBOX_MODE = frappe.db.get_single_value("TaxJar Settings", "is_sandbox")
fields_already_exist = frappe.db.exists('Custom Field', {'dt': ('in', ['Item','Sales Invoice Item']), 'fieldname':'product_tax_category'})
fields_hidden = frappe.get_value('Custom Field', {'dt': ('in', ['Sales Invoice Item'])}, 'hidden')
if (TAXJAR_CREATE_TRANSACTIONS or TAXJAR_CALCULATE_TAX or TAXJAR_SANDBOX_MODE):
if not fields_already_exist:
add_product_tax_categories()
make_custom_fields()
add_permissions()
frappe.enqueue('erpnext.regional.united_states.setup.add_product_tax_categories', now=False)
elif fields_already_exist and fields_hidden:
toggle_tax_category_fields(hidden='0')
elif fields_already_exist:
toggle_tax_category_fields(hidden='1')
def validate(self):
self.calculate_taxes_validation_for_create_transactions()
@frappe.whitelist()
def update_nexus_list(self):
client = get_client()
nexus = client.nexus_regions()
new_nexus_list = [frappe._dict(address) for address in nexus]
self.set('nexus', [])
self.set('nexus', new_nexus_list)
self.save()
def calculate_taxes_validation_for_create_transactions(self):
if not self.taxjar_calculate_tax and (self.taxjar_create_transactions or self.is_sandbox):
frappe.throw(frappe._('Before enabling <b>Create Transaction</b> or <b>Sandbox Mode</b>, you need to check the <b>Enable Tax Calculation</b> box'))
def toggle_tax_category_fields(hidden):
frappe.set_value('Custom Field', {'dt':'Sales Invoice Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
frappe.set_value('Custom Field', {'dt':'Item', 'fieldname':'product_tax_category'}, 'hidden', hidden)
def add_product_tax_categories():
with open(os.path.join(os.path.dirname(__file__), 'product_tax_category_data.json'), 'r') as f:
tax_categories = json.loads(f.read())
create_tax_categories(tax_categories['categories'])
def create_tax_categories(data):
for d in data:
if not frappe.db.exists('Product Tax Category',{'product_tax_code':d.get('product_tax_code')}):
tax_category = frappe.new_doc('Product Tax Category')
tax_category.description = d.get("description")
tax_category.product_tax_code = d.get("product_tax_code")
tax_category.category_name = d.get("name")
tax_category.db_insert()
def make_custom_fields(update=True):
custom_fields = {
'Sales Invoice Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='description', options='Product Tax Category',
label='Product Tax Category', fetch_from='item_code.product_tax_category'),
dict(fieldname='tax_collectable', fieldtype='Currency', insert_after='net_amount',
label='Tax Collectable', read_only=1),
dict(fieldname='taxable_amount', fieldtype='Currency', insert_after='tax_collectable',
label='Taxable Amount', read_only=1)
],
'Item': [
dict(fieldname='product_tax_category', fieldtype='Link', insert_after='item_group', options='Product Tax Category',
label='Product Tax Category')
]
}
create_custom_fields(custom_fields, update=update)
def add_permissions():
doctype = "Product Tax Category"
for role in ('Accounts Manager', 'Accounts User', 'System Manager','Item Manager', 'Stock Manager'):
add_permission(doctype, role, 0)
update_permission_property(doctype, role, 0, 'write', 1)
update_permission_property(doctype, role, 0, 'create', 1)

View File

@ -4,7 +4,7 @@ import frappe
import taxjar
from frappe import _
from frappe.contacts.doctype.address.address import get_company_address
from frappe.utils import cint
from frappe.utils import cint, flt
from erpnext import get_default_company
@ -103,7 +103,7 @@ def get_tax_data(doc):
shipping = sum([tax.tax_amount for tax in doc.taxes if tax.account_head == SHIP_ACCOUNT_HEAD])
line_items = [get_line_item_dict(item) for item in doc.items]
line_items = [get_line_item_dict(item, doc.docstatus) for item in doc.items]
if from_shipping_state not in SUPPORTED_STATE_CODES:
from_shipping_state = get_state_code(from_address, 'Company')
@ -139,14 +139,21 @@ def get_state_code(address, location):
return state_code
def get_line_item_dict(item):
return dict(
def get_line_item_dict(item, docstatus):
tax_dict = dict(
id = item.get('idx'),
quantity = item.get('qty'),
unit_price = item.get('rate'),
product_tax_code = item.get('product_tax_category')
)
if docstatus == 1:
tax_dict.update({
'sales_tax':item.get('tax_collectable')
})
return tax_dict
def set_sales_tax(doc, method):
if not TAXJAR_CALCULATE_TAX:
return
@ -164,6 +171,9 @@ def set_sales_tax(doc, method):
setattr(doc, "taxes", [tax for tax in doc.taxes if tax.account_head != TAX_ACCOUNT_HEAD])
return
# check if delivering within a nexus
check_for_nexus(doc, tax_dict)
tax_data = validate_tax_request(tax_dict)
if tax_data is not None:
if not tax_data.amount_to_collect:
@ -191,6 +201,17 @@ def set_sales_tax(doc, method):
doc.run_method("calculate_taxes_and_totals")
def check_for_nexus(doc, tax_dict):
if not frappe.db.get_value('TaxJar Nexus', {'region_code': tax_dict["to_state"]}):
for item in doc.get("items"):
item.tax_collectable = flt(0)
item.taxable_amount = flt(0)
for tax in doc.taxes:
if tax.account_head == TAX_ACCOUNT_HEAD:
doc.taxes.remove(tax)
return
def check_sales_tax_exemption(doc):
# if the party is exempt from sales tax, then set all tax account heads to zero
sales_tax_exempted = hasattr(doc, "exempt_from_sales_tax") and doc.exempt_from_sales_tax \

View File

@ -1,20 +1,13 @@
{
"category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Marketplace\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Payments\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
"creation": "2020-08-20 19:30:48.138801",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "integration",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "ERPNext Integrations",
"links": [
{
@ -119,15 +112,12 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:15:58.740246",
"modified": "2021-08-05 12:15:58.740247",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations",
"onboarding": "",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],

View File

@ -1,20 +1,13 @@
{
"category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Integrations Settings\", \"col\": 4}}]",
"creation": "2020-07-31 10:38:54.021237",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "setting",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "ERPNext Integrations Settings",
"links": [
{
@ -81,15 +74,12 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:15:58.951704",
"modified": "2021-08-05 12:15:58.951705",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "ERPNext Integrations Settings",
"onboarding": "",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],

View File

@ -250,6 +250,7 @@ doc_events = {
"validate": "erpnext.regional.india.utils.validate_tax_category"
},
"Sales Invoice": {
"after_insert": "erpnext.regional.saudi_arabia.utils.create_qr_code",
"on_submit": [
"erpnext.regional.create_transaction_log",
"erpnext.regional.italy.utils.sales_invoice_on_submit",
@ -259,7 +260,10 @@ doc_events = {
"erpnext.regional.italy.utils.sales_invoice_on_cancel",
"erpnext.erpnext_integrations.taxjar_integration.delete_transaction"
],
"on_trash": "erpnext.regional.check_deletion_permission",
"on_trash": [
"erpnext.regional.check_deletion_permission",
"erpnext.regional.saudi_arabia.utils.delete_qr_code_file"
],
"validate": [
"erpnext.regional.india.utils.validate_document_name",
"erpnext.regional.india.utils.update_taxable_values"

View File

@ -74,7 +74,6 @@ class TestDailyWorkSummary(unittest.TestCase):
from `tabEmail Queue` as q, `tabEmail Queue Recipient` as r \
where q.name = r.parent""", as_dict=1)
frappe.db.commit()
def setup_groups(self, hour=None):
# setup email to trigger at this hour

View File

@ -55,6 +55,7 @@ def make_employee(user, company=None, **kwargs):
"email": user,
"first_name": user,
"new_password": "password",
"send_welcome_email": 0,
"roles": [{"doctype": "Has Role", "role": "Employee"}]
}).insert()

View File

@ -9,7 +9,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate
from erpnext.hr.utils import update_employee, validate_active_employee
from erpnext.hr.utils import update_employee_work_history, validate_active_employee
class EmployeePromotion(Document):
@ -23,10 +23,10 @@ class EmployeePromotion(Document):
def on_submit(self):
employee = frappe.get_doc("Employee", self.employee)
employee = update_employee(employee, self.promotion_details, date=self.promotion_date)
employee = update_employee_work_history(employee, self.promotion_details, date=self.promotion_date)
employee.save()
def on_cancel(self):
employee = frappe.get_doc("Employee", self.employee)
employee = update_employee(employee, self.promotion_details, cancel=True)
employee = update_employee_work_history(employee, self.promotion_details, cancel=True)
employee.save()

View File

@ -9,7 +9,7 @@ from frappe import _
from frappe.model.document import Document
from frappe.utils import getdate
from erpnext.hr.utils import update_employee
from erpnext.hr.utils import update_employee_work_history
class EmployeeTransfer(Document):
@ -24,7 +24,7 @@ class EmployeeTransfer(Document):
new_employee = frappe.copy_doc(employee)
new_employee.name = None
new_employee.employee_number = None
new_employee = update_employee(new_employee, self.transfer_details, date=self.transfer_date)
new_employee = update_employee_work_history(new_employee, self.transfer_details, date=self.transfer_date)
if self.new_company and self.company != self.new_company:
new_employee.internal_work_history = []
new_employee.date_of_joining = self.transfer_date
@ -39,7 +39,7 @@ class EmployeeTransfer(Document):
employee.db_set("relieving_date", self.transfer_date)
employee.db_set("status", "Left")
else:
employee = update_employee(employee, self.transfer_details, date=self.transfer_date)
employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date)
if self.new_company and self.company != self.new_company:
employee.company = self.new_company
employee.date_of_joining = self.transfer_date
@ -56,7 +56,7 @@ class EmployeeTransfer(Document):
employee.status = "Active"
employee.relieving_date = ''
else:
employee = update_employee(employee, self.transfer_details, cancel=True)
employee = update_employee_work_history(employee, self.transfer_details, date=self.transfer_date, cancel=True)
if self.new_company != self.company:
employee.company = self.company
employee.save()

View File

@ -4,6 +4,7 @@
from __future__ import unicode_literals
import unittest
from datetime import date
import frappe
from frappe.utils import add_days, getdate
@ -15,7 +16,12 @@ class TestEmployeeTransfer(unittest.TestCase):
def setUp(self):
make_employee("employee2@transfers.com")
make_employee("employee3@transfers.com")
frappe.db.sql("""delete from `tabEmployee Transfer`""")
create_company()
create_employee()
create_employee_transfer()
def tearDown(self):
frappe.db.rollback()
def test_submit_before_transfer_date(self):
transfer_obj = frappe.get_doc({
@ -57,3 +63,77 @@ class TestEmployeeTransfer(unittest.TestCase):
self.assertTrue(transfer.new_employee_id)
self.assertEqual(frappe.get_value("Employee", transfer.new_employee_id, "status"), "Active")
self.assertEqual(frappe.get_value("Employee", transfer.employee, "status"), "Left")
def test_employee_history(self):
name = frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name")
doc = frappe.get_doc("Employee",name)
count = 0
department = ["Accounts - TC", "Management - TC"]
designation = ["Accountant", "Manager"]
dt = [getdate("01-10-2021"), date.today()]
for data in doc.internal_work_history:
self.assertEqual(data.department, department[count])
self.assertEqual(data.designation, designation[count])
self.assertEqual(data.from_date, dt[count])
count = count + 1
data = frappe.db.get_list("Employee Transfer", filters={"employee":name}, fields=["*"])
doc = frappe.get_doc("Employee Transfer", data[0]["name"])
doc.cancel()
employee_doc = frappe.get_doc("Employee",name)
for data in employee_doc.internal_work_history:
self.assertEqual(data.designation, designation[0])
self.assertEqual(data.department, department[0])
self.assertEqual(data.from_date, dt[0])
def create_employee():
doc = frappe.get_doc({
"doctype": "Employee",
"first_name": "John",
"company": "Test Company",
"gender": "Male",
"date_of_birth": getdate("30-09-1980"),
"date_of_joining": getdate("01-10-2021"),
"department": "Accounts - TC",
"designation": "Accountant"
})
doc.save()
def create_company():
exists = frappe.db.exists("Company", "Test Company")
if not exists:
doc = frappe.get_doc({
"doctype": "Company",
"company_name": "Test Company",
"default_currency": "INR",
"country": "India"
})
doc.save()
def create_employee_transfer():
doc = frappe.get_doc({
"doctype": "Employee Transfer",
"employee": frappe.get_value("Employee", {"first_name": "John", "company": "Test Company"}, "name"),
"transfer_date": date.today(),
"transfer_details": [
{
"property": "Designation",
"current": "Accountant",
"new": "Manager",
"fieldname": "designation"
},
{
"property": "Department",
"current": "Accounts - TC",
"new": "Management - TC",
"fieldname": "department"
}
]
})
doc.save()
doc.submit()

View File

@ -10,6 +10,26 @@ frappe.ui.form.on('Expense Claim', {
},
company: function(frm) {
erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
var expenses = frm.doc.expenses;
for (var i = 0; i < expenses.length; i++) {
var expense = expenses[i];
if (!expense.expense_type) {
continue;
}
frappe.call({
method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center",
args: {
"expense_claim_type": expense.expense_type,
"company": frm.doc.company
},
callback: function(r) {
if (r.message) {
expense.default_account = r.message.account;
expense.cost_center = r.message.cost_center;
}
}
});
}
},
});

View File

@ -97,7 +97,7 @@ class ShiftType(Document):
assigned_employees = [x[0] for x in assigned_employees]
if consider_default_shift:
filters = {'default_shift': self.name}
filters = {'default_shift': self.name, 'status': ['!=', 'Inactive']}
default_shift_employees = frappe.get_all('Employee', 'name', filters, as_list=True)
default_shift_employees = [x[0] for x in default_shift_employees]
return list(set(assigned_employees+default_shift_employees))

View File

@ -182,10 +182,11 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
records= frappe.db.sql("""
SELECT
employee, leave_type, from_date, to_date, leaves, transaction_name,
is_carry_forward, is_expired
transaction_type, is_carry_forward, is_expired
FROM `tabLeave Ledger Entry`
WHERE employee=%(employee)s AND leave_type=%(leave_type)s
AND docstatus=1
AND transaction_type = 'Leave Allocation'
AND (from_date between %(from_date)s AND %(to_date)s
OR to_date between %(from_date)s AND %(to_date)s
OR (from_date < %(from_date)s AND to_date > %(to_date)s))

View File

@ -29,7 +29,15 @@ def set_employee_name(doc):
if doc.employee and not doc.employee_name:
doc.employee_name = frappe.db.get_value("Employee", doc.employee, "employee_name")
def update_employee(employee, details, date=None, cancel=False):
def update_employee_work_history(employee, details, date=None, cancel=False):
if not employee.internal_work_history and not cancel:
employee.append("internal_work_history", {
"branch": employee.branch,
"designation": employee.designation,
"department": employee.department,
"from_date": employee.date_of_joining
})
internal_work_history = {}
for item in details:
field = frappe.get_meta("Employee").get_field(item.fieldname)
@ -44,11 +52,35 @@ def update_employee(employee, details, date=None, cancel=False):
setattr(employee, item.fieldname, new_data)
if item.fieldname in ["department", "designation", "branch"]:
internal_work_history[item.fieldname] = item.new
if internal_work_history and not cancel:
internal_work_history["from_date"] = date
employee.append("internal_work_history", internal_work_history)
if cancel:
delete_employee_work_history(details, employee, date)
return employee
def delete_employee_work_history(details, employee, date):
filters = {}
for d in details:
for history in employee.internal_work_history:
if d.property == "Department" and history.department == d.new:
department = d.new
filters["department"] = department
if d.property == "Designation" and history.designation == d.new:
designation = d.new
filters["designation"] = designation
if d.property == "Branch" and history.branch == d.new:
branch = d.new
filters["branch"] = branch
if date and date == history.from_date:
filters["from_date"] = date
if filters:
frappe.db.delete("Employee Internal Work History", filters)
@frappe.whitelist()
def get_employee_fields_label():
fields = []

View File

@ -1,5 +1,4 @@
{
"category": "",
"charts": [
{
"chart_name": "Outgoing Salary",
@ -8,18 +7,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Human Resource\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": \"Outgoing Salary\", \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Employee\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Leave Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Job Applicant\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Monthly Attendance Sheet\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Employee Lifecycle\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Shift Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Leaves\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Attendance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Expense Claims\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Fleet Management\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Recruitment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loans\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Training\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Performance\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Key Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Other Reports\", \"col\": 4}}]",
"creation": "2020-03-02 15:48:58.322521",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "hr",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "HR",
"links": [
{
@ -942,15 +935,12 @@
"type": "Link"
}
],
"modified": "2021-08-31 12:18:59.842918",
"modified": "2021-08-31 12:18:59.842919",
"modified_by": "Administrator",
"module": "HR",
"name": "HR",
"onboarding": "Human Resource",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],

View File

@ -336,7 +336,6 @@
},
{
"depends_on": "eval:doc.is_secured_loan",
"fetch_from": "loan_application.maximum_loan_amount",
"fieldname": "maximum_loan_amount",
"fieldtype": "Currency",
"label": "Maximum Loan Amount",
@ -362,7 +361,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2021-10-20 08:28:16.796105",
"modified": "2021-10-12 18:10:32.360818",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan",

View File

@ -137,16 +137,23 @@ class Loan(AccountsController):
frappe.throw(_("Loan amount is mandatory"))
def link_loan_security_pledge(self):
if self.is_secured_loan:
loan_security_pledge = frappe.db.get_value('Loan Security Pledge', {'loan_application': self.loan_application},
'name')
if self.is_secured_loan and self.loan_application:
maximum_loan_value = frappe.db.get_value('Loan Security Pledge',
{
'loan_application': self.loan_application,
'status': 'Requested'
},
'sum(maximum_loan_value)'
)
if loan_security_pledge:
frappe.db.set_value('Loan Security Pledge', loan_security_pledge, {
'loan': self.name,
'status': 'Pledged',
'pledge_time': now_datetime()
})
if maximum_loan_value:
frappe.db.sql("""
UPDATE `tabLoan Security Pledge`
SET loan = %s, pledge_time = %s, status = 'Pledged'
WHERE status = 'Requested' and loan_application = %s
""", (self.name, now_datetime(), self.loan_application))
self.db_set('maximum_loan_amount', maximum_loan_value)
def unlink_loan_security_pledge(self):
pledges = frappe.get_all('Loan Security Pledge', fields=['name'], filters={'loan': self.name})

View File

@ -131,9 +131,10 @@ def create_loan(source_name, target_doc=None, submit=0):
def update_accounts(source_doc, target_doc, source_parent):
account_details = frappe.get_all("Loan Type",
fields=["mode_of_payment", "payment_account","loan_account", "interest_income_account", "penalty_income_account"],
filters = {'name': source_doc.loan_type}
)[0]
filters = {'name': source_doc.loan_type})[0]
if source_doc.is_secured_loan:
target_doc.maximum_loan_amount = 0
target_doc.mode_of_payment = account_details.mode_of_payment
target_doc.payment_account = account_details.payment_account

View File

@ -197,7 +197,7 @@ def get_disbursal_amount(loan, on_current_security_price=0):
security_value = get_total_pledged_security_value(loan)
if loan_details.is_secured_loan and not on_current_security_price:
security_value = flt(loan_details.maximum_loan_amount)
security_value = get_maximum_amount_as_per_pledged_security(loan)
if not security_value and not loan_details.is_secured_loan:
security_value = flt(loan_details.loan_amount)
@ -208,3 +208,6 @@ def get_disbursal_amount(loan, on_current_security_price=0):
disbursal_amount = loan_details.loan_amount - loan_details.disbursed_amount
return disbursal_amount
def get_maximum_amount_as_per_pledged_security(loan):
return flt(frappe.db.get_value('Loan Security Pledge', {'loan': loan}, 'sum(maximum_loan_value)'))

View File

@ -6,7 +6,7 @@ from __future__ import unicode_literals
import frappe
from frappe import _
from frappe.utils import add_days, add_months, cint, date_diff, flt, get_datetime, getdate
from frappe.utils import add_days, cint, date_diff, flt, get_datetime, getdate
from six import iteritems
import erpnext

View File

@ -1,20 +1,13 @@
{
"category": "",
"charts": [],
"content": "[{\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Loan Application\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Loan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Processes\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Disbursement and Repayment\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Loan Security\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}]",
"creation": "2020-03-12 16:35:55.299820",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "loan",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "Loans",
"links": [
{
@ -245,15 +238,12 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:18:13.350904",
"modified": "2021-08-05 12:18:13.350905",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loans",
"onboarding": "",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "",
"roles": [],

View File

@ -199,11 +199,15 @@ class MaintenanceSchedule(TransactionBase):
if chk:
throw(_("Maintenance Schedule {0} exists against {1}").format(chk[0][0], d.sales_order))
def validate_no_of_visits(self):
return len(self.schedules) != sum(d.no_of_visits for d in self.items)
def validate(self):
self.validate_end_date_visits()
self.validate_maintenance_detail()
self.validate_dates_with_periodicity()
self.validate_sales_order()
if not self.schedules or self.validate_no_of_visits():
self.generate_schedule()
def on_update(self):

View File

@ -1133,8 +1133,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters):
query_filters["has_variants"] = 0
if filters and filters.get("is_stock_item"):
or_cond_filters["is_stock_item"] = 1
or_cond_filters["has_variants"] = 1
query_filters["is_stock_item"] = 1
return frappe.get_list("Item",
fields = fields, filters=query_filters,

View File

@ -4,13 +4,14 @@
import unittest
from collections import deque
from functools import partial
import frappe
from frappe.test_runner import make_test_records
from frappe.utils import cstr, flt
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
from erpnext.manufacturing.doctype.bom.bom import make_variant_bom
from erpnext.manufacturing.doctype.bom.bom import item_query, make_variant_bom
from erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool import update_cost
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import (
@ -375,6 +376,16 @@ class TestBOM(unittest.TestCase):
# FG Items in Scrap/Loss Table should have Is Process Loss set
self.assertRaises(frappe.ValidationError, bom_doc.submit)
def test_bom_item_query(self):
query = partial(item_query, doctype="Item", txt="", searchfield="name", start=0, page_len=20, filters={"is_stock_item": 1})
test_items = query(txt="_Test")
filtered = query(txt="_Test Item 2")
self.assertNotEqual(len(test_items), len(filtered), msg="Item filtering showing excessive results")
self.assertTrue(0 < len(filtered) <= 3, msg="Item filtering showing excessive results")
def get_default_bom(item_code="_Test FG Item 2"):
return frappe.db.get_value("BOM", {"item": item_code, "is_active": 1, "is_default": 1})

View File

@ -424,7 +424,7 @@ class ProductionPlan(Document):
po = frappe.new_doc('Purchase Order')
po.supplier = supplier
po.schedule_date = getdate(po_list[0].schedule_date) if po_list[0].schedule_date else nowdate()
po.is_subcontracted_item = 'Yes'
po.is_subcontracted = 'Yes'
for row in po_list:
args = {
'item_code': row.production_item,

View File

@ -24,7 +24,7 @@ def get_data(filters):
}
fields = ["name", "status", "work_order", "production_item", "item_name", "posting_date",
"total_completed_qty", "workstation", "operation", "employee_name", "total_time_in_mins"]
"total_completed_qty", "workstation", "operation", "total_time_in_mins"]
for field in ["work_order", "workstation", "operation", "company"]:
if filters.get(field):
@ -45,7 +45,7 @@ def get_data(filters):
job_card_time_details = {}
for job_card_data in frappe.get_all("Job Card Time Log",
fields=["min(from_time) as from_time", "max(to_time) as to_time", "parent"],
filters=job_card_time_filter, group_by="parent", debug=1):
filters=job_card_time_filter, group_by="parent"):
job_card_time_details[job_card_data.parent] = job_card_data
res = []
@ -172,12 +172,6 @@ def get_columns(filters):
"options": "Operation",
"width": 110
},
{
"label": _("Employee Name"),
"fieldname": "employee_name",
"fieldtype": "Data",
"width": 110
},
{
"label": _("Total Completed Qty"),
"fieldname": "total_completed_qty",

View File

@ -9,9 +9,9 @@
"filters": [],
"idx": 0,
"is_standard": "Yes",
"modified": "2021-08-24 16:38:15.233395",
"modified": "2021-10-20 22:03:57.606612",
"modified_by": "Administrator",
"module": "Stock",
"module": "Manufacturing",
"name": "Process Loss Report",
"owner": "Administrator",
"prepared_report": 0,
@ -21,9 +21,6 @@
"roles": [
{
"role": "Manufacturing User"
},
{
"role": "Stock User"
}
]
}

View File

@ -111,7 +111,7 @@ def run_query(query_args: QueryArgs) -> Data:
{work_order_filter}
GROUP BY
se.work_order
""".format(**query_args), query_args, as_dict=1, debug=1)
""".format(**query_args), query_args, as_dict=1)
def update_data_with_total_pl_value(data: Data) -> None:
for row in data:

View File

@ -0,0 +1,64 @@
import unittest
from typing import List, Tuple
import frappe
from erpnext.tests.utils import ReportFilters, ReportName, execute_script_report
DEFAULT_FILTERS = {
"company": "_Test Company",
"from_date": "2010-01-01",
"to_date": "2030-01-01",
"warehouse": "_Test Warehouse - _TC",
}
REPORT_FILTER_TEST_CASES: List[Tuple[ReportName, ReportFilters]] = [
("BOM Explorer", {"bom": frappe.get_last_doc("BOM").name}),
("BOM Operations Time", {}),
("BOM Stock Calculated", {"bom": frappe.get_last_doc("BOM").name, "qty_to_make": 2}),
("BOM Stock Report", {"bom": frappe.get_last_doc("BOM").name, "qty_to_produce": 2}),
("Cost of Poor Quality Report", {}),
("Downtime Analysis", {}),
(
"Exponential Smoothing Forecasting",
{
"based_on_document": "Sales Order",
"based_on_field": "Qty",
"no_of_years": 3,
"periodicity": "Yearly",
"smoothing_constant": 0.3,
},
),
("Job Card Summary", {"fiscal_year": "2021-2022"}),
("Production Analytics", {"range": "Monthly"}),
("Quality Inspection Summary", {}),
("Process Loss Report", {}),
("Work Order Stock Report", {}),
("Work Order Summary", {"fiscal_year": "2021-2022", "age": 0}),
]
if frappe.db.a_row_exists("Production Plan"):
REPORT_FILTER_TEST_CASES.append(
("Production Plan Summary", {"production_plan": frappe.get_last_doc("Production Plan").name})
)
OPTIONAL_FILTERS = {
"warehouse": "_Test Warehouse - _TC",
"item": "_Test Item",
"item_group": "_Test Item Group",
}
class TestManufacturingReports(unittest.TestCase):
def test_execute_all_manufacturing_reports(self):
"""Test that all script report in manufacturing modules are executable with supported filters"""
for report, filter in REPORT_FILTER_TEST_CASES:
execute_script_report(
report_name=report,
module="Manufacturing",
filters=filter,
default_filters=DEFAULT_FILTERS,
optional_filters=OPTIONAL_FILTERS if filter.get("_optional") else None,
)

View File

@ -1,5 +1,4 @@
{
"category": "",
"charts": [
{
"chart_name": "Produced Quantity"
@ -7,18 +6,12 @@
],
"content": "[{\"type\": \"onboarding\", \"data\": {\"onboarding_name\":\"Manufacturing\", \"col\": 12}}, {\"type\": \"chart\", \"data\": {\"chart_name\": null, \"col\": 12}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Your Shortcuts\", \"level\": 4, \"col\": 12}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Item\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Plan\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Forecasting\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Work Order Summary\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"BOM Stock Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Production Planning Report\", \"col\": 4}}, {\"type\": \"shortcut\", \"data\": {\"shortcut_name\": \"Dashboard\", \"col\": 4}}, {\"type\": \"spacer\", \"data\": {\"col\": 12}}, {\"type\": \"header\", \"data\": {\"text\": \"Reports & Masters\", \"level\": 4, \"col\": 12}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Production\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Bill of Materials\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Reports\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Tools\", \"col\": 4}}, {\"type\": \"card\", \"data\": {\"card_name\": \"Settings\", \"col\": 4}}]",
"creation": "2020-03-02 17:11:37.032604",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
"doctype": "Workspace",
"extends": "",
"extends_another_page": 0,
"for_user": "",
"hide_custom": 0,
"icon": "organization",
"idx": 0,
"is_default": 0,
"is_standard": 0,
"label": "Manufacturing",
"links": [
{
@ -304,15 +297,12 @@
"type": "Link"
}
],
"modified": "2021-08-05 12:16:00.825741",
"modified": "2021-08-05 12:16:00.825742",
"modified_by": "Administrator",
"module": "Manufacturing",
"name": "Manufacturing",
"onboarding": "Manufacturing",
"owner": "Administrator",
"parent_page": "",
"pin_to_bottom": 0,
"pin_to_top": 0,
"public": 1,
"restrict_to_domain": "Manufacturing",
"roles": [],

Some files were not shown because too many files have changed in this diff Show More