Merge branch 'develop' into purchase-invoice-to-purchase-receipt-develop
This commit is contained in:
commit
840c921229
38
.github/helper/semgrep_rules/README.md
vendored
Normal file
38
.github/helper/semgrep_rules/README.md
vendored
Normal file
@ -0,0 +1,38 @@
|
||||
# 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
|
28
.github/helper/semgrep_rules/frappe_correctness.py
vendored
Normal file
28
.github/helper/semgrep_rules/frappe_correctness.py
vendored
Normal file
@ -0,0 +1,28 @@
|
||||
import frappe
|
||||
from frappe import _, flt
|
||||
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
def on_submit(self):
|
||||
if self.value_of_goods == 0:
|
||||
frappe.throw(_('Value of goods cannot be 0'))
|
||||
# ruleid: frappe-modifying-after-submit
|
||||
self.status = 'Submitted'
|
||||
|
||||
def on_submit(self):
|
||||
if flt(self.per_billed) < 100:
|
||||
self.update_billing_status()
|
||||
else:
|
||||
# todook: frappe-modifying-after-submit
|
||||
self.status = "Completed"
|
||||
self.db_set("status", "Completed")
|
||||
|
||||
class TestDoc(Document):
|
||||
pass
|
||||
|
||||
def validate(self):
|
||||
#ruleid: frappe-modifying-child-tables-while-iterating
|
||||
for item in self.child_table:
|
||||
if item.value < 0:
|
||||
self.remove(item)
|
74
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
Normal file
74
.github/helper/semgrep_rules/frappe_correctness.yml
vendored
Normal file
@ -0,0 +1,74 @@
|
||||
# This file specifies rules for correctness according to how frappe doctype data model works.
|
||||
|
||||
rules:
|
||||
- id: frappe-modifying-after-submit
|
||||
patterns:
|
||||
- pattern: self.$ATTR = ...
|
||||
- pattern-inside: |
|
||||
def on_submit(self, ...):
|
||||
...
|
||||
- metavariable-regex:
|
||||
metavariable: '$ATTR'
|
||||
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
|
||||
regex: '^(?!status_updater)(.*)$'
|
||||
message: |
|
||||
Doctype modified after submission. Please check if modification of self.$ATTR is commited to database.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-modifying-after-cancel
|
||||
patterns:
|
||||
- pattern: self.$ATTR = ...
|
||||
- pattern-inside: |
|
||||
def on_cancel(self, ...):
|
||||
...
|
||||
- metavariable-regex:
|
||||
metavariable: '$ATTR'
|
||||
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
|
||||
message: |
|
||||
Doctype modified after cancellation. Please check if modification of self.$ATTR is 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:
|
||||
exclude:
|
||||
- test_*.py
|
||||
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
|
6
.github/helper/semgrep_rules/security.py
vendored
Normal file
6
.github/helper/semgrep_rules/security.py
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
def function_name(input):
|
||||
# ruleid: frappe-codeinjection-eval
|
||||
eval(input)
|
||||
|
||||
# ok: frappe-codeinjection-eval
|
||||
eval("1 + 1")
|
25
.github/helper/semgrep_rules/security.yml
vendored
Normal file
25
.github/helper/semgrep_rules/security.yml
vendored
Normal file
@ -0,0 +1,25 @@
|
||||
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
|
||||
|
||||
- id: frappe-sqli-format-strings
|
||||
patterns:
|
||||
- pattern-inside: |
|
||||
@frappe.whitelist()
|
||||
def $FUNC(...):
|
||||
...
|
||||
- pattern-either:
|
||||
- pattern: frappe.db.sql("..." % ...)
|
||||
- pattern: frappe.db.sql(f"...", ...)
|
||||
- pattern: frappe.db.sql("...".format(...), ...)
|
||||
message: |
|
||||
Detected use of raw string formatting for SQL queries. This can lead to sql injection vulnerabilities. Refer security guidelines - https://github.com/frappe/erpnext/wiki/Code-Security-Guidelines
|
||||
languages: [python]
|
||||
severity: WARNING
|
37
.github/helper/semgrep_rules/translate.js
vendored
Normal file
37
.github/helper/semgrep_rules/translate.js
vendored
Normal file
@ -0,0 +1,37 @@
|
||||
// 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])
|
53
.github/helper/semgrep_rules/translate.py
vendored
Normal file
53
.github/helper/semgrep_rules/translate.py
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
# 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
|
||||
_('')
|
63
.github/helper/semgrep_rules/translate.yml
vendored
Normal file
63
.github/helper/semgrep_rules/translate.yml
vendored
Normal file
@ -0,0 +1,63 @@
|
||||
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*'
|
||||
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
|
31
.github/helper/semgrep_rules/ux.py
vendored
Normal file
31
.github/helper/semgrep_rules/ux.py
vendored
Normal file
@ -0,0 +1,31 @@
|
||||
import frappe
|
||||
from frappe import msgprint, throw, _
|
||||
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
throw("Error Occured")
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
frappe.throw("Error Occured")
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
frappe.msgprint("Useful message")
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
msgprint("Useful message")
|
||||
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
translatedmessage = _("Hello")
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
throw(translatedmessage)
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
msgprint(translatedmessage)
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
msgprint(_("Helpful message"))
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
frappe.throw(_("Error occured"))
|
15
.github/helper/semgrep_rules/ux.yml
vendored
Normal file
15
.github/helper/semgrep_rules/ux.yml
vendored
Normal file
@ -0,0 +1,15 @@
|
||||
rules:
|
||||
- id: frappe-missing-translate-function
|
||||
pattern-either:
|
||||
- patterns:
|
||||
- pattern: frappe.msgprint("...", ...)
|
||||
- pattern-not: frappe.msgprint(_("..."), ...)
|
||||
- pattern-not: frappe.msgprint(__("..."), ...)
|
||||
- patterns:
|
||||
- pattern: frappe.throw("...", ...)
|
||||
- pattern-not: 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, javascript, json]
|
||||
severity: ERROR
|
24
.github/workflows/semgrep.yml
vendored
Normal file
24
.github/workflows/semgrep.yml
vendored
Normal file
@ -0,0 +1,24 @@
|
||||
name: Semgrep
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Setup python3
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Run semgrep
|
||||
run: |
|
||||
python -m pip install -q semgrep
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity ERROR --config=.github/helper/semgrep_rules --quiet --error $files
|
||||
semgrep --config="r/python.lang.correctness" --quiet --error $files
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
|
@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '13.0.1'
|
||||
__version__ = '13.0.0-dev'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
@ -12,6 +12,7 @@
|
||||
"frozen_accounts_modifier",
|
||||
"determine_address_tax_category_from",
|
||||
"over_billing_allowance",
|
||||
"role_allowed_to_over_bill",
|
||||
"column_break_4",
|
||||
"credit_controller",
|
||||
"check_supplier_invoice_uniqueness",
|
||||
@ -226,6 +227,13 @@
|
||||
"fieldname": "delete_linked_ledger_entries",
|
||||
"fieldtype": "Check",
|
||||
"label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role are allowed to over bill above the allowance percentage",
|
||||
"fieldname": "role_allowed_to_over_bill",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Over Bill ",
|
||||
"options": "Role"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@ -233,7 +241,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-05 13:04:00.118892",
|
||||
"modified": "2021-03-11 18:52:05.601996",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Accounts Settings",
|
||||
|
@ -175,22 +175,24 @@
|
||||
},
|
||||
{
|
||||
"fieldname": "deposit",
|
||||
"oldfieldname": "debit",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Deposit"
|
||||
"label": "Deposit",
|
||||
"oldfieldname": "debit",
|
||||
"options": "currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "withdrawal",
|
||||
"oldfieldname": "credit",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Withdrawal"
|
||||
"label": "Withdrawal",
|
||||
"oldfieldname": "credit",
|
||||
"options": "currency"
|
||||
}
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-30 19:40:54.221070",
|
||||
"modified": "2021-04-14 17:31:58.963529",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Bank Transaction",
|
||||
|
@ -38,22 +38,22 @@
|
||||
{% endif %}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.debit, filters.presentation_currency) }}</td>
|
||||
{{ frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}</td>
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}</td>
|
||||
{{ frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}</td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or " " }}</b></td>
|
||||
<td style="text-align: right">
|
||||
{{ row.account and frappe.utils.fmt_money(row.debit, filters.presentation_currency) }}
|
||||
{{ row.account and frappe.utils.fmt_money(row.debit, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
<td style="text-align: right">
|
||||
{{ row.account and frappe.utils.fmt_money(row.credit, filters.presentation_currency) }}
|
||||
{{ row.account and frappe.utils.fmt_money(row.credit, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
{% endif %}
|
||||
<td style="text-align: right">
|
||||
{{ frappe.utils.fmt_money(row.balance, filters.presentation_currency) }}
|
||||
{{ frappe.utils.fmt_money(row.balance, currency=filters.presentation_currency) }}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
|
@ -9,7 +9,7 @@ from frappe.utils import cstr
|
||||
from frappe.model.naming import make_autoname
|
||||
from frappe.model.document import Document
|
||||
|
||||
pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group'
|
||||
pricing_rule_fields = ['apply_on', 'mixed_conditions', 'is_cumulative', 'other_item_code', 'other_item_group',
|
||||
'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from',
|
||||
'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier',
|
||||
'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules']
|
||||
@ -111,4 +111,4 @@ def get_args_for_pricing_rule(doc):
|
||||
for d in pricing_rule_fields:
|
||||
args[d] = doc.get(d)
|
||||
|
||||
return args
|
||||
return args
|
||||
|
@ -118,6 +118,7 @@
|
||||
"in_words",
|
||||
"total_advance",
|
||||
"outstanding_amount",
|
||||
"disable_rounded_total",
|
||||
"advances_section",
|
||||
"allocate_advances_automatically",
|
||||
"get_advances",
|
||||
@ -1109,6 +1110,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@ -1120,6 +1122,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@ -1168,6 +1171,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@ -1180,6 +1184,7 @@
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@ -1945,6 +1950,13 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Set Target Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "grand_total",
|
||||
"fieldname": "disable_rounded_total",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Rounded Total"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
@ -1957,7 +1969,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-03-31 15:42:26.261540",
|
||||
"modified": "2021-04-15 23:57:58.766651",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
@ -46,7 +46,6 @@ class SalesInvoice(SellingController):
|
||||
'target_parent_dt': 'Sales Order',
|
||||
'target_parent_field': 'per_billed',
|
||||
'source_field': 'amount',
|
||||
'join_field': 'so_detail',
|
||||
'percent_join_field': 'sales_order',
|
||||
'status_field': 'billing_status',
|
||||
'keyword': 'Billed',
|
||||
@ -276,7 +275,7 @@ class SalesInvoice(SellingController):
|
||||
pluck="pos_closing_entry"
|
||||
)
|
||||
if pos_closing_entry:
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}. ").format(
|
||||
msg = _("To cancel a {} you need to cancel the POS Closing Entry {}.").format(
|
||||
frappe.bold("Consolidated Sales Invoice"),
|
||||
get_link_to_form("POS Closing Entry", pos_closing_entry[0])
|
||||
)
|
||||
@ -549,12 +548,12 @@ class SalesInvoice(SellingController):
|
||||
frappe.throw(_("Debit To is required"), title=_("Account Missing"))
|
||||
|
||||
if account.report_type != "Balance Sheet":
|
||||
msg = _("Please ensure {} account is a Balance Sheet account. ").format(frappe.bold("Debit To"))
|
||||
msg = _("Please ensure {} account is a Balance Sheet account.").format(frappe.bold("Debit To")) + " "
|
||||
msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
if self.customer and account.account_type != "Receivable":
|
||||
msg = _("Please ensure {} account is a Receivable account. ").format(frappe.bold("Debit To"))
|
||||
msg = _("Please ensure {} account is a Receivable account.").format(frappe.bold("Debit To")) + " "
|
||||
msg += _("Change the account type to Receivable or select a different account.")
|
||||
frappe.throw(msg, title=_("Invalid Account"))
|
||||
|
||||
|
@ -1879,7 +1879,17 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
|
||||
def test_einvoice_submission_without_irn(self):
|
||||
# init
|
||||
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
|
||||
einvoice_settings = frappe.get_doc('E Invoice Settings')
|
||||
einvoice_settings.enable = 1
|
||||
einvoice_settings.applicable_from = nowdate()
|
||||
einvoice_settings.append('credentials', {
|
||||
'company': '_Test Company',
|
||||
'gstin': '27AAECE4835E1ZR',
|
||||
'username': 'test',
|
||||
'password': 'test'
|
||||
})
|
||||
einvoice_settings.save()
|
||||
|
||||
country = frappe.flags.country
|
||||
frappe.flags.country = 'India'
|
||||
|
||||
@ -1890,7 +1900,8 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
si.submit()
|
||||
|
||||
# reset
|
||||
frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
|
||||
einvoice_settings = frappe.get_doc('E Invoice Settings')
|
||||
einvoice_settings.enable = 0
|
||||
frappe.flags.country = country
|
||||
|
||||
def test_einvoice_json(self):
|
||||
|
@ -251,7 +251,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
|
||||
threshold = tax_details.get('threshold', 0)
|
||||
cumulative_threshold = tax_details.get('cumulative_threshold', 0)
|
||||
|
||||
if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
|
||||
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
|
||||
if ldc and is_valid_certificate(
|
||||
ldc.valid_from, ldc.valid_upto,
|
||||
inv.posting_date, tax_deducted,
|
||||
|
@ -87,50 +87,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
|
||||
for d in invoices:
|
||||
d.cancel()
|
||||
|
||||
def test_single_threshold_tds_with_previous_vouchers(self):
|
||||
invoices = []
|
||||
frappe.db.set_value("Supplier", "Test TDS Supplier2", "tax_withholding_category", "Single Threshold TDS")
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
pi = create_purchase_invoice(supplier="Test TDS Supplier2")
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
|
||||
self.assertEqual(pi.grand_total, 8000)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
d.cancel()
|
||||
|
||||
def test_single_threshold_tds_with_previous_vouchers_and_no_tds(self):
|
||||
invoices = []
|
||||
doc = create_supplier(supplier_name = "Test TDS Supplier ABC",
|
||||
tax_withholding_category="Single Threshold TDS")
|
||||
supplier = doc.name
|
||||
|
||||
pi = create_purchase_invoice(supplier=supplier)
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
# TDS not applied
|
||||
pi = create_purchase_invoice(supplier=supplier, do_not_apply_tds=True)
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
pi = create_purchase_invoice(supplier=supplier)
|
||||
pi.submit()
|
||||
invoices.append(pi)
|
||||
|
||||
self.assertEqual(pi.taxes_and_charges_deducted, 2000)
|
||||
self.assertEqual(pi.grand_total, 8000)
|
||||
|
||||
# delete invoices to avoid clashing
|
||||
for d in invoices:
|
||||
d.cancel()
|
||||
|
||||
def test_cumulative_threshold_tcs(self):
|
||||
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
|
||||
invoices = []
|
||||
|
@ -56,6 +56,8 @@
|
||||
"base_net_amount",
|
||||
"warehouse_and_reference",
|
||||
"warehouse",
|
||||
"actual_qty",
|
||||
"company_total_stock",
|
||||
"material_request",
|
||||
"material_request_item",
|
||||
"sales_order",
|
||||
@ -743,6 +745,22 @@
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "actual_qty",
|
||||
"fieldtype": "Float",
|
||||
"label": "Available Qty at Warehouse",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "company_total_stock",
|
||||
"fieldtype": "Float",
|
||||
"label": "Available Qty at Company",
|
||||
"no_copy": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"collapsible": 1,
|
||||
"fieldname": "discount_and_margin_section",
|
||||
@ -791,7 +809,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-02-23 01:00:27.132705",
|
||||
"modified": "2021-03-22 11:46:12.357435",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
7
erpnext/change_log/v13/v13.0.2.md
Normal file
7
erpnext/change_log/v13/v13.0.2.md
Normal file
@ -0,0 +1,7 @@
|
||||
## Version 13.0.2 Release Notes
|
||||
|
||||
### Fixes
|
||||
- fix: frappe.whitelist for doc methods ([#25231](https://github.com/frappe/erpnext/pull/25231))
|
||||
- fix: incorrect incoming rate for the sales return ([#25306](https://github.com/frappe/erpnext/pull/25306))
|
||||
- fix(e-invoicing): validations & tax calculation fixes ([#25314](https://github.com/frappe/erpnext/pull/25314))
|
||||
- fix: update scheduler check time ([#25295](https://github.com/frappe/erpnext/pull/25295))
|
@ -717,7 +717,9 @@ class AccountsController(TransactionBase):
|
||||
total_billed_amt = abs(total_billed_amt)
|
||||
max_allowed_amt = abs(max_allowed_amt)
|
||||
|
||||
if total_billed_amt - max_allowed_amt > 0.01:
|
||||
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
||||
|
||||
if total_billed_amt - max_allowed_amt > 0.01 and role_allowed_to_over_bill not in frappe.get_roles():
|
||||
frappe.throw(_("Cannot overbill for Item {0} in row {1} more than {2}. To allow over-billing, please set allowance in Accounts Settings")
|
||||
.format(item.item_code, item.idx, max_allowed_amt))
|
||||
|
||||
|
@ -6,6 +6,7 @@ import frappe
|
||||
from frappe import _, msgprint
|
||||
from frappe.utils import flt,cint, cstr, getdate
|
||||
from six import iteritems
|
||||
from collections import OrderedDict
|
||||
from erpnext.accounts.party import get_party_details
|
||||
from erpnext.stock.get_item_details import get_conversion_factor
|
||||
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
|
||||
@ -391,10 +392,12 @@ class BuyingController(StockController):
|
||||
|
||||
batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
|
||||
qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
|
||||
|
||||
for batch_data in batches_qty:
|
||||
qty = batch_data['qty']
|
||||
raw_material.batch_no = batch_data['batch']
|
||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
||||
if qty > 0:
|
||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
||||
else:
|
||||
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
|
||||
|
||||
@ -1056,7 +1059,7 @@ def get_transferred_batch_qty_map(purchase_order, fg_item):
|
||||
for batch_data in transferred_batches:
|
||||
key = ((batch_data.item_code, fg_item)
|
||||
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
|
||||
transferred_batch_qty_map.setdefault(key, {})
|
||||
transferred_batch_qty_map.setdefault(key, OrderedDict())
|
||||
transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
|
||||
|
||||
return transferred_batch_qty_map
|
||||
@ -1109,8 +1112,14 @@ def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty
|
||||
if available_qty >= required_qty:
|
||||
available_batches.append({'batch': batch, 'qty': required_qty})
|
||||
break
|
||||
else:
|
||||
elif available_qty != 0:
|
||||
available_batches.append({'batch': batch, 'qty': available_qty})
|
||||
required_qty -= available_qty
|
||||
|
||||
for row in available_batches:
|
||||
if backflushed_batches.get(row.get('batch'), 0) > 0:
|
||||
backflushed_batches[row.get('batch')] += row.get('qty')
|
||||
else:
|
||||
backflushed_batches[row.get('batch')] = row.get('qty')
|
||||
|
||||
return available_batches
|
||||
|
@ -713,7 +713,9 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
|
||||
return [(d,) for d in set(taxes)]
|
||||
|
||||
|
||||
def get_fields(doctype, fields=[]):
|
||||
def get_fields(doctype, fields=None):
|
||||
if fields is None:
|
||||
fields = []
|
||||
meta = frappe.get_meta(doctype)
|
||||
fields.extend(meta.get_search_fields())
|
||||
|
||||
|
@ -5,6 +5,7 @@ from __future__ import unicode_literals
|
||||
import frappe, erpnext
|
||||
from frappe import _
|
||||
from frappe.model.meta import get_field_precision
|
||||
from erpnext.stock.utils import get_incoming_rate
|
||||
from frappe.utils import flt, get_datetime, format_datetime
|
||||
|
||||
class StockOverReturnError(frappe.ValidationError): pass
|
||||
@ -389,10 +390,24 @@ def make_return_doc(doctype, source_name, target_doc=None):
|
||||
|
||||
return doclist
|
||||
|
||||
def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None, item_row=None, voucher_detail_no=None):
|
||||
def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None,
|
||||
item_row=None, voucher_detail_no=None, sle=None):
|
||||
if not return_against:
|
||||
return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against")
|
||||
|
||||
if not return_against and voucher_type == 'Sales Invoice' and sle:
|
||||
return get_incoming_rate({
|
||||
"item_code": sle.item_code,
|
||||
"warehouse": sle.warehouse,
|
||||
"posting_date": sle.get('posting_date'),
|
||||
"posting_time": sle.get('posting_time'),
|
||||
"qty": sle.actual_qty,
|
||||
"serial_no": sle.get('serial_no'),
|
||||
"company": sle.company,
|
||||
"voucher_type": sle.voucher_type,
|
||||
"voucher_no": sle.voucher_no
|
||||
}, raise_error_if_no_rate=False)
|
||||
|
||||
return_against_item_field = get_return_against_item_fields(voucher_type)
|
||||
|
||||
filters = get_filters(voucher_type, voucher_no, voucher_detail_no,
|
||||
|
@ -311,14 +311,16 @@ class SellingController(StockController):
|
||||
|
||||
items = self.get("items") + (self.get("packed_items") or [])
|
||||
for d in items:
|
||||
if not cint(self.get("is_return")):
|
||||
if not self.get("return_against"):
|
||||
# Get incoming rate based on original item cost based on valuation method
|
||||
qty = flt(d.get('stock_qty') or d.get('actual_qty'))
|
||||
|
||||
d.incoming_rate = get_incoming_rate({
|
||||
"item_code": d.item_code,
|
||||
"warehouse": d.warehouse,
|
||||
"posting_date": self.get('posting_date') or self.get('transaction_date'),
|
||||
"posting_time": self.get('posting_time') or nowtime(),
|
||||
"qty": -1 * flt(d.get('stock_qty') or d.get('actual_qty')),
|
||||
"qty": qty if cint(self.get("is_return")) else (-1 * qty),
|
||||
"serial_no": d.get('serial_no'),
|
||||
"company": self.company,
|
||||
"voucher_type": self.doctype,
|
||||
|
@ -201,10 +201,14 @@ class StatusUpdater(Document):
|
||||
get_allowance_for(item['item_code'], self.item_allowance,
|
||||
self.global_qty_allowance, self.global_amount_allowance, qty_or_amount)
|
||||
|
||||
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) /
|
||||
item[args['target_ref_field']]) * 100
|
||||
role_allowed_to_over_deliver_receive = frappe.db.get_single_value('Stock Settings', 'role_allowed_to_over_deliver_receive')
|
||||
role_allowed_to_over_bill = frappe.db.get_single_value('Accounts Settings', 'role_allowed_to_over_bill')
|
||||
role = role_allowed_to_over_deliver_receive if qty_or_amount == 'qty' else role_allowed_to_over_bill
|
||||
|
||||
if overflow_percent - allowance > 0.01:
|
||||
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) /
|
||||
item[args['target_ref_field']]) * 100
|
||||
|
||||
if overflow_percent - allowance > 0.01 and role not in frappe.get_roles():
|
||||
item['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100)
|
||||
item['reduce_by'] = item[args['target_field']] - item['max_allowed']
|
||||
|
||||
@ -371,10 +375,12 @@ class StatusUpdater(Document):
|
||||
ref_doc.db_set("per_billed", per_billed)
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
def get_allowance_for(item_code, item_allowance={}, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"):
|
||||
def get_allowance_for(item_code, item_allowance=None, global_qty_allowance=None, global_amount_allowance=None, qty_or_amount="qty"):
|
||||
"""
|
||||
Returns the allowance for the item, if not set, returns global allowance
|
||||
"""
|
||||
if item_allowance is None:
|
||||
item_allowance = {}
|
||||
if qty_or_amount == "qty":
|
||||
if item_allowance.get(item_code, frappe._dict()).get("qty"):
|
||||
return item_allowance[item_code].qty, item_allowance, global_qty_allowance, global_amount_allowance
|
||||
|
@ -117,7 +117,6 @@ class StockController(AccountsController):
|
||||
"account": expense_account,
|
||||
"against": warehouse_account[sle.warehouse]["account"],
|
||||
"cost_center": item_row.cost_center,
|
||||
"project": item_row.project or self.get('project'),
|
||||
"remarks": self.get("remarks") or "Accounting Entry for Stock",
|
||||
"credit": flt(sle.stock_value_difference, precision),
|
||||
"project": item_row.get("project") or self.get("project"),
|
||||
@ -483,7 +482,7 @@ class StockController(AccountsController):
|
||||
)
|
||||
message += "<br><br>"
|
||||
rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule)
|
||||
message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link)
|
||||
message += _("Please adjust the qty or edit {0} to proceed.").format(rule_link)
|
||||
return message
|
||||
|
||||
def repost_future_sle_and_gle(self):
|
||||
|
@ -41,7 +41,7 @@ class CourseEnrollment(Document):
|
||||
frappe.throw(_("Student is already enrolled via Course Enrollment {0}").format(
|
||||
get_link_to_form("Course Enrollment", enrollment)), title=_('Duplicate Entry'))
|
||||
|
||||
def add_quiz_activity(self, quiz_name, quiz_response, answers, score, status):
|
||||
def add_quiz_activity(self, quiz_name, quiz_response, answers, score, status, time_taken):
|
||||
result = {k: ('Correct' if v else 'Wrong') for k,v in answers.items()}
|
||||
result_data = []
|
||||
for key in answers:
|
||||
@ -66,7 +66,8 @@ class CourseEnrollment(Document):
|
||||
"activity_date": frappe.utils.datetime.datetime.now(),
|
||||
"result": result_data,
|
||||
"score": score,
|
||||
"status": status
|
||||
"status": status,
|
||||
"time_taken": time_taken
|
||||
}).insert(ignore_permissions = True)
|
||||
|
||||
def add_activity(self, content_type, content):
|
||||
|
@ -1,4 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_import": 1,
|
||||
"allow_rename": 1,
|
||||
"autoname": "field:title",
|
||||
@ -12,7 +13,10 @@
|
||||
"quiz_configuration_section",
|
||||
"passing_score",
|
||||
"max_attempts",
|
||||
"grading_basis"
|
||||
"grading_basis",
|
||||
"column_break_7",
|
||||
"is_time_bound",
|
||||
"duration"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
@ -58,9 +62,26 @@
|
||||
"fieldtype": "Select",
|
||||
"label": "Grading Basis",
|
||||
"options": "Latest Highest Score\nLatest Attempt"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_time_bound",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Time-Bound"
|
||||
},
|
||||
{
|
||||
"depends_on": "is_time_bound",
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Duration",
|
||||
"label": "Duration"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"modified": "2019-06-12 12:23:57.020508",
|
||||
"links": [],
|
||||
"modified": "2020-12-24 15:41:35.043262",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Quiz",
|
||||
|
@ -1,490 +1,163 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"actions": [],
|
||||
"autoname": "format:EDU-QA-{YYYY}-{#####}",
|
||||
"beta": 1,
|
||||
"creation": "2018-10-15 15:48:40.482821",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enrollment",
|
||||
"student",
|
||||
"column_break_3",
|
||||
"course",
|
||||
"section_break_5",
|
||||
"quiz",
|
||||
"column_break_7",
|
||||
"status",
|
||||
"section_break_9",
|
||||
"result",
|
||||
"section_break_11",
|
||||
"activity_date",
|
||||
"score",
|
||||
"column_break_14",
|
||||
"time_taken"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "enrollment",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Enrollment",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Course Enrollment",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 1,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "enrollment.student",
|
||||
"fieldname": "student",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Student",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Student",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "enrollment.course",
|
||||
"fieldname": "course",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Course",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Course",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 1,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_5",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "quiz",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Quiz",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Quiz",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 1,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_7",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Status",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "\nPass\nFail",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "section_break_9",
|
||||
"fieldtype": "Section Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "result",
|
||||
"fieldtype": "Table",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Result",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Quiz Result",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 1,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "activity_date",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Activity Date",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 1,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "score",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Score",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 1,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "time_taken",
|
||||
"fieldtype": "Duration",
|
||||
"label": "Time Taken",
|
||||
"set_only_once": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "section_break_11",
|
||||
"fieldtype": "Section Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_14",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 0,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-11-25 19:05:52.434437",
|
||||
"links": [],
|
||||
"modified": "2020-12-24 15:41:20.085380",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Education",
|
||||
"name": "Quiz Activity",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Academics User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "LMS User",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"amend": 0,
|
||||
"cancel": 0,
|
||||
"create": 0,
|
||||
"delete": 0,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"if_owner": 0,
|
||||
"import": 0,
|
||||
"permlevel": 0,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Instructor",
|
||||
"set_user_permissions": 0,
|
||||
"share": 1,
|
||||
"submit": 0,
|
||||
"write": 0
|
||||
"share": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
"track_changes": 1
|
||||
}
|
@ -114,7 +114,7 @@ class Student(Document):
|
||||
status = check_content_completion(content.name, content.doctype, course_enrollment_name)
|
||||
progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status})
|
||||
elif content.doctype == 'Quiz':
|
||||
status, score, result = check_quiz_completion(content, course_enrollment_name)
|
||||
status, score, result, time_taken = check_quiz_completion(content, course_enrollment_name)
|
||||
progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status, 'score': score, 'result': result})
|
||||
return progress
|
||||
|
||||
|
@ -194,7 +194,7 @@ def add_activity(course, content_type, content, program):
|
||||
return enrollment.add_activity(content_type, content)
|
||||
|
||||
@frappe.whitelist()
|
||||
def evaluate_quiz(quiz_response, quiz_name, course, program):
|
||||
def evaluate_quiz(quiz_response, quiz_name, course, program, time_taken):
|
||||
import json
|
||||
|
||||
student = get_current_student()
|
||||
@ -209,7 +209,7 @@ def evaluate_quiz(quiz_response, quiz_name, course, program):
|
||||
if student:
|
||||
enrollment = get_or_create_course_enrollment(course, program)
|
||||
if quiz.allowed_attempt(enrollment, quiz_name):
|
||||
enrollment.add_quiz_activity(quiz_name, quiz_response, result, score, status)
|
||||
enrollment.add_quiz_activity(quiz_name, quiz_response, result, score, status, time_taken)
|
||||
return {'result': result, 'score': score, 'status': status}
|
||||
else:
|
||||
return None
|
||||
@ -219,8 +219,9 @@ def get_quiz(quiz_name, course):
|
||||
try:
|
||||
quiz = frappe.get_doc("Quiz", quiz_name)
|
||||
questions = quiz.get_questions()
|
||||
duration = quiz.duration
|
||||
except:
|
||||
frappe.throw(_("Quiz {0} does not exist").format(quiz_name))
|
||||
frappe.throw(_("Quiz {0} does not exist").format(quiz_name), frappe.DoesNotExistError)
|
||||
return None
|
||||
|
||||
questions = [{
|
||||
@ -232,12 +233,20 @@ def get_quiz(quiz_name, course):
|
||||
} for question in questions]
|
||||
|
||||
if has_super_access():
|
||||
return {'questions': questions, 'activity': None}
|
||||
return {
|
||||
'questions': questions,
|
||||
'activity': None,
|
||||
'duration':duration
|
||||
}
|
||||
|
||||
student = get_current_student()
|
||||
course_enrollment = get_enrollment("course", course, student.name)
|
||||
status, score, result = check_quiz_completion(quiz, course_enrollment)
|
||||
return {'questions': questions, 'activity': {'is_complete': status, 'score': score, 'result': result}}
|
||||
status, score, result, time_taken = check_quiz_completion(quiz, course_enrollment)
|
||||
return {
|
||||
'questions': questions,
|
||||
'activity': {'is_complete': status, 'score': score, 'result': result, 'time_taken': time_taken},
|
||||
'duration': quiz.duration
|
||||
}
|
||||
|
||||
def get_topic_progress(topic, course_name, program):
|
||||
"""
|
||||
@ -361,15 +370,23 @@ def check_content_completion(content_name, content_type, enrollment_name):
|
||||
return False
|
||||
|
||||
def check_quiz_completion(quiz, enrollment_name):
|
||||
attempts = frappe.get_all("Quiz Activity", filters={'enrollment': enrollment_name, 'quiz': quiz.name}, fields=["name", "activity_date", "score", "status"])
|
||||
attempts = frappe.get_all("Quiz Activity",
|
||||
filters={
|
||||
'enrollment': enrollment_name,
|
||||
'quiz': quiz.name
|
||||
},
|
||||
fields=["name", "activity_date", "score", "status", "time_taken"]
|
||||
)
|
||||
status = False if quiz.max_attempts == 0 else bool(len(attempts) >= quiz.max_attempts)
|
||||
score = None
|
||||
result = None
|
||||
time_taken = None
|
||||
if attempts:
|
||||
if quiz.grading_basis == 'Last Highest Score':
|
||||
attempts = sorted(attempts, key = lambda i: int(i.score), reverse=True)
|
||||
score = attempts[0]['score']
|
||||
result = attempts[0]['status']
|
||||
time_taken = attempts[0]['time_taken']
|
||||
if result == 'Pass':
|
||||
status = True
|
||||
return status, score, result
|
||||
return status, score, result, time_taken
|
@ -307,6 +307,8 @@ auto_cancel_exempted_doctypes= [
|
||||
"Inpatient Medication Entry"
|
||||
]
|
||||
|
||||
after_migrate = ["erpnext.setup.install.update_select_perm_after_install"]
|
||||
|
||||
scheduler_events = {
|
||||
"cron": {
|
||||
"0/30 * * * *": [
|
||||
|
@ -66,7 +66,7 @@ class CompensatoryLeaveRequest(Document):
|
||||
|
||||
else:
|
||||
leave_allocation = self.create_leave_allocation(leave_period, date_difference)
|
||||
self.leave_allocation=leave_allocation.name
|
||||
self.db_set("leave_allocation", leave_allocation.name)
|
||||
else:
|
||||
frappe.throw(_("There is no leave period in between {0} and {1}").format(format_date(self.work_from_date), format_date(self.work_end_date)))
|
||||
|
||||
@ -124,4 +124,4 @@ class CompensatoryLeaveRequest(Document):
|
||||
))
|
||||
allocation.insert(ignore_permissions=True)
|
||||
allocation.submit()
|
||||
return allocation
|
||||
return allocation
|
||||
|
@ -218,8 +218,7 @@
|
||||
"fieldname": "leave_policy_assignment",
|
||||
"fieldtype": "Link",
|
||||
"label": "Leave Policy Assignment",
|
||||
"options": "Leave Policy Assignment",
|
||||
"read_only": 1
|
||||
"options": "Leave Policy Assignment"
|
||||
},
|
||||
{
|
||||
"fetch_from": "employee.company",
|
||||
@ -236,7 +235,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-04 18:46:13.184104",
|
||||
"modified": "2021-04-14 15:28:26.335104",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Leave Allocation",
|
||||
|
@ -43,7 +43,6 @@ def get_charts():
|
||||
return [{
|
||||
"doctype": "Dashboard Chart",
|
||||
"based_on": "modified",
|
||||
"time_interval": "Yearly",
|
||||
"chart_type": "Sum",
|
||||
"chart_name": _("Produced Quantity"),
|
||||
"name": "Produced Quantity",
|
||||
@ -60,7 +59,6 @@ def get_charts():
|
||||
}, {
|
||||
"doctype": "Dashboard Chart",
|
||||
"based_on": "creation",
|
||||
"time_interval": "Yearly",
|
||||
"chart_type": "Sum",
|
||||
"chart_name": _("Completed Operation"),
|
||||
"name": "Completed Operation",
|
||||
@ -238,4 +236,4 @@ def get_number_cards():
|
||||
"label": _("Monthly Quality Inspections"),
|
||||
"show_percentage_stats": 1,
|
||||
"stats_time_interval": "Weekly"
|
||||
}]
|
||||
}]
|
||||
|
@ -53,7 +53,9 @@ class BOMUpdateTool(Document):
|
||||
rate=%s, amount=stock_qty*%s where bom_no = %s and docstatus < 2 and parenttype='BOM'""",
|
||||
(self.new_bom, unit_cost, unit_cost, self.current_bom))
|
||||
|
||||
def get_parent_boms(self, bom, bom_list=[]):
|
||||
def get_parent_boms(self, bom, bom_list=None):
|
||||
if bom_list is None:
|
||||
bom_list = []
|
||||
data = frappe.db.sql("""SELECT DISTINCT parent FROM `tabBOM Item`
|
||||
WHERE bom_no = %s AND docstatus < 2 AND parenttype='BOM'""", bom)
|
||||
|
||||
@ -106,4 +108,4 @@ def update_cost():
|
||||
for bom in bom_list:
|
||||
frappe.get_doc("BOM", bom).update_cost(update_parent=False, from_child_bom=True)
|
||||
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
frappe.db.auto_commit_on_many_writes = 0
|
||||
|
@ -561,7 +561,6 @@ def get_material_request_items(row, sales_order, company,
|
||||
'item_name': row.item_name,
|
||||
'quantity': required_qty,
|
||||
'required_bom_qty': total_qty,
|
||||
'description': row.description,
|
||||
'stock_uom': row.get("stock_uom"),
|
||||
'warehouse': warehouse or row.get('source_warehouse') \
|
||||
or row.get('default_warehouse') or item_group_defaults.get("default_warehouse"),
|
||||
@ -766,7 +765,7 @@ def get_items_for_material_requests(doc, warehouses=None):
|
||||
to_enable = frappe.bold(_("Ignore Existing Projected Quantity"))
|
||||
warehouse = frappe.bold(doc.get('for_warehouse'))
|
||||
message = _("As there are sufficient raw materials, Material Request is not required for Warehouse {0}.").format(warehouse) + "<br><br>"
|
||||
message += _(" If you still want to proceed, please enable {0}.").format(to_enable)
|
||||
message += _("If you still want to proceed, please enable {0}.").format(to_enable)
|
||||
|
||||
frappe.msgprint(message, title=_("Note"))
|
||||
|
||||
|
@ -693,7 +693,7 @@ execute:frappe.reload_doctype('Dashboard')
|
||||
execute:frappe.reload_doc('desk', 'doctype', 'number_card_link')
|
||||
execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts')
|
||||
erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo
|
||||
erpnext.patches.v13_0.set_company_field_in_healthcare_doctypes #2020-05-25
|
||||
erpnext.patches.v13_0.set_company_field_in_healthcare_doctypes #2021-04-16
|
||||
erpnext.patches.v12_0.update_bom_in_so_mr
|
||||
execute:frappe.delete_doc("Report", "Department Analytics")
|
||||
execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True)
|
||||
@ -771,3 +771,5 @@ erpnext.patches.v12_0.add_gst_category_in_delivery_note
|
||||
erpnext.patches.v12_0.purchase_receipt_status
|
||||
erpnext.patches.v13_0.fix_non_unique_represents_company
|
||||
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
|
||||
erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021
|
||||
erpnext.patches.v13_0.update_shipment_status
|
||||
|
@ -12,5 +12,5 @@ def execute():
|
||||
select dl.link_name from `tabAddress` a, `tabDynamic Link` dl
|
||||
where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company'
|
||||
""", (creds.get('gstin')))
|
||||
if company_name and len(company_name) == 1:
|
||||
if company_name and len(company_name) > 0:
|
||||
frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0])
|
24
erpnext/patches/v13_0/make_non_standard_user_type.py
Normal file
24
erpnext/patches/v13_0/make_non_standard_user_type.py
Normal file
@ -0,0 +1,24 @@
|
||||
# Copyright (c) 2019, Frappe and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from six import iteritems
|
||||
from erpnext.setup.install import add_non_standard_user_types
|
||||
|
||||
def execute():
|
||||
doctype_dict = {
|
||||
'projects': ['Timesheet'],
|
||||
'payroll': ['Salary Slip', 'Employee Tax Exemption Declaration', 'Employee Tax Exemption Proof Submission'],
|
||||
'hr': ['Employee', 'Expense Claim', 'Leave Application', 'Attendance Request', 'Compensatory Leave Request']
|
||||
}
|
||||
|
||||
for module, doctypes in iteritems(doctype_dict):
|
||||
for doctype in doctypes:
|
||||
frappe.reload_doc(module, 'doctype', doctype)
|
||||
|
||||
|
||||
frappe.flags.ignore_select_perm = True
|
||||
frappe.flags.update_select_perm_after_migrate = True
|
||||
|
||||
add_non_standard_user_types()
|
@ -3,7 +3,7 @@ import frappe
|
||||
|
||||
def execute():
|
||||
company = frappe.db.get_single_value('Global Defaults', 'default_company')
|
||||
doctypes = ['Clinical Procedure', 'Inpatient Record', 'Lab Test', 'Sample Collection' 'Patient Appointment', 'Patient Encounter', 'Vital Signs', 'Therapy Session', 'Therapy Plan', 'Patient Assessment']
|
||||
doctypes = ['Clinical Procedure', 'Inpatient Record', 'Lab Test', 'Sample Collection', 'Patient Appointment', 'Patient Encounter', 'Vital Signs', 'Therapy Session', 'Therapy Plan', 'Patient Assessment']
|
||||
for entry in doctypes:
|
||||
if frappe.db.exists('DocType', entry):
|
||||
frappe.reload_doc('Healthcare', 'doctype', entry)
|
||||
|
14
erpnext/patches/v13_0/update_shipment_status.py
Normal file
14
erpnext/patches/v13_0/update_shipment_status.py
Normal file
@ -0,0 +1,14 @@
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("stock", "doctype", "shipment")
|
||||
|
||||
# update submitted status
|
||||
frappe.db.sql("""UPDATE `tabShipment`
|
||||
SET status = "Submitted"
|
||||
WHERE status = "Draft" AND docstatus = 1""")
|
||||
|
||||
# update cancelled status
|
||||
frappe.db.sql("""UPDATE `tabShipment`
|
||||
SET status = "Cancelled"
|
||||
WHERE status = "Draft" AND docstatus = 2""")
|
@ -151,6 +151,10 @@ frappe.ui.form.on('Payroll Entry', {
|
||||
filters['company'] = frm.doc.company;
|
||||
filters['start_date'] = frm.doc.start_date;
|
||||
filters['end_date'] = frm.doc.end_date;
|
||||
filters['salary_slip_based_on_timesheet'] = frm.doc.salary_slip_based_on_timesheet;
|
||||
filters['payroll_frequency'] = frm.doc.payroll_frequency;
|
||||
filters['payroll_payable_account'] = frm.doc.payroll_payable_account;
|
||||
filters['currency'] = frm.doc.currency;
|
||||
|
||||
if (frm.doc.department) {
|
||||
filters['department'] = frm.doc.department;
|
||||
|
@ -52,49 +52,32 @@ class PayrollEntry(Document):
|
||||
Returns list of active employees based on selected criteria
|
||||
and for which salary structure exists
|
||||
"""
|
||||
cond = self.get_filter_condition()
|
||||
cond += self.get_joining_relieving_condition()
|
||||
self.check_mandatory()
|
||||
filters = self.make_filters()
|
||||
cond = get_filter_condition(filters)
|
||||
cond += get_joining_relieving_condition(self.start_date, self.end_date)
|
||||
|
||||
condition = ''
|
||||
if self.payroll_frequency:
|
||||
condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": self.payroll_frequency}
|
||||
|
||||
sal_struct = frappe.db.sql_list("""
|
||||
select
|
||||
name from `tabSalary Structure`
|
||||
where
|
||||
docstatus = 1 and
|
||||
is_active = 'Yes'
|
||||
and company = %(company)s
|
||||
and currency = %(currency)s and
|
||||
ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s
|
||||
{condition}""".format(condition=condition),
|
||||
{"company": self.company, "currency": self.currency, "salary_slip_based_on_timesheet":self.salary_slip_based_on_timesheet})
|
||||
|
||||
sal_struct = get_sal_struct(self.company, self.currency, self.salary_slip_based_on_timesheet, condition)
|
||||
if sal_struct:
|
||||
cond += "and t2.salary_structure IN %(sal_struct)s "
|
||||
cond += "and t2.payroll_payable_account = %(payroll_payable_account)s "
|
||||
cond += "and %(from_date)s >= t2.from_date"
|
||||
emp_list = frappe.db.sql("""
|
||||
select
|
||||
distinct t1.name as employee, t1.employee_name, t1.department, t1.designation
|
||||
from
|
||||
`tabEmployee` t1, `tabSalary Structure Assignment` t2
|
||||
where
|
||||
t1.name = t2.employee
|
||||
and t2.docstatus = 1
|
||||
%s order by t2.from_date desc
|
||||
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True)
|
||||
|
||||
emp_list = self.remove_payrolled_employees(emp_list)
|
||||
emp_list = get_emp_list(sal_struct, cond, self.end_date, self.payroll_payable_account)
|
||||
emp_list = remove_payrolled_employees(emp_list, self.start_date, self.end_date)
|
||||
return emp_list
|
||||
|
||||
def remove_payrolled_employees(self, emp_list):
|
||||
for employee_details in emp_list:
|
||||
if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}):
|
||||
emp_list.remove(employee_details)
|
||||
def make_filters(self):
|
||||
filters = frappe._dict()
|
||||
filters['company'] = self.company
|
||||
filters['branch'] = self.branch
|
||||
filters['department'] = self.department
|
||||
filters['designation'] = self.designation
|
||||
|
||||
return emp_list
|
||||
return filters
|
||||
|
||||
@frappe.whitelist()
|
||||
def fill_employee_details(self):
|
||||
@ -122,23 +105,6 @@ class PayrollEntry(Document):
|
||||
if self.validate_attendance:
|
||||
return self.validate_employee_attendance()
|
||||
|
||||
def get_filter_condition(self):
|
||||
self.check_mandatory()
|
||||
|
||||
cond = ''
|
||||
for f in ['company', 'branch', 'department', 'designation']:
|
||||
if self.get(f):
|
||||
cond += " and t1." + f + " = " + frappe.db.escape(self.get(f))
|
||||
|
||||
return cond
|
||||
|
||||
def get_joining_relieving_condition(self):
|
||||
cond = """
|
||||
and ifnull(t1.date_of_joining, '0000-00-00') <= '%(end_date)s'
|
||||
and ifnull(t1.relieving_date, '2199-12-31') >= '%(start_date)s'
|
||||
""" % {"start_date": self.start_date, "end_date": self.end_date}
|
||||
return cond
|
||||
|
||||
def check_mandatory(self):
|
||||
for fieldname in ['company', 'start_date', 'end_date']:
|
||||
if not self.get(fieldname):
|
||||
@ -451,6 +417,53 @@ class PayrollEntry(Document):
|
||||
marked_days = attendances[0][0]
|
||||
return marked_days
|
||||
|
||||
def get_sal_struct(company, currency, salary_slip_based_on_timesheet, condition):
|
||||
return frappe.db.sql_list("""
|
||||
select
|
||||
name from `tabSalary Structure`
|
||||
where
|
||||
docstatus = 1 and
|
||||
is_active = 'Yes'
|
||||
and company = %(company)s
|
||||
and currency = %(currency)s and
|
||||
ifnull(salary_slip_based_on_timesheet,0) = %(salary_slip_based_on_timesheet)s
|
||||
{condition}""".format(condition=condition),
|
||||
{"company": company, "currency": currency, "salary_slip_based_on_timesheet": salary_slip_based_on_timesheet})
|
||||
|
||||
def get_filter_condition(filters):
|
||||
cond = ''
|
||||
for f in ['company', 'branch', 'department', 'designation']:
|
||||
if filters.get(f):
|
||||
cond += " and t1." + f + " = " + frappe.db.escape(filters.get(f))
|
||||
|
||||
return cond
|
||||
|
||||
def get_joining_relieving_condition(start_date, end_date):
|
||||
cond = """
|
||||
and ifnull(t1.date_of_joining, '0000-00-00') <= '%(end_date)s'
|
||||
and ifnull(t1.relieving_date, '2199-12-31') >= '%(start_date)s'
|
||||
""" % {"start_date": start_date, "end_date": end_date}
|
||||
return cond
|
||||
|
||||
def get_emp_list(sal_struct, cond, end_date, payroll_payable_account):
|
||||
return frappe.db.sql("""
|
||||
select
|
||||
distinct t1.name as employee, t1.employee_name, t1.department, t1.designation
|
||||
from
|
||||
`tabEmployee` t1, `tabSalary Structure Assignment` t2
|
||||
where
|
||||
t1.name = t2.employee
|
||||
and t2.docstatus = 1
|
||||
%s order by t2.from_date desc
|
||||
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": end_date, "payroll_payable_account": payroll_payable_account}, as_dict=True)
|
||||
|
||||
def remove_payrolled_employees(emp_list, start_date, end_date):
|
||||
for employee_details in emp_list:
|
||||
if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": start_date, "end_date": end_date, "docstatus": 1}):
|
||||
emp_list.remove(employee_details)
|
||||
|
||||
return emp_list
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_start_end_dates(payroll_frequency, start_date=None, company=None):
|
||||
'''Returns dict of start and end dates for given payroll frequency based on start_date'''
|
||||
@ -639,39 +652,41 @@ def get_payroll_entries_for_jv(doctype, txt, searchfield, start, page_len, filte
|
||||
'start': start, 'page_len': page_len
|
||||
})
|
||||
|
||||
def get_employee_with_existing_salary_slip(start_date, end_date, company):
|
||||
return frappe.db.sql_list("""
|
||||
select employee from `tabSalary Slip`
|
||||
where
|
||||
(start_date between %(start_date)s and %(end_date)s
|
||||
or
|
||||
end_date between %(start_date)s and %(end_date)s
|
||||
or
|
||||
%(start_date)s between start_date and end_date)
|
||||
and company = %(company)s
|
||||
and docstatus = 1
|
||||
""", {'start_date': start_date, 'end_date': end_date, 'company': company})
|
||||
def get_employee_list(filters):
|
||||
cond = get_filter_condition(filters)
|
||||
cond += get_joining_relieving_condition(filters.start_date, filters.end_date)
|
||||
condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": filters.payroll_frequency}
|
||||
sal_struct = get_sal_struct(filters.company, filters.currency, filters.salary_slip_based_on_timesheet, condition)
|
||||
if sal_struct:
|
||||
cond += "and t2.salary_structure IN %(sal_struct)s "
|
||||
cond += "and t2.payroll_payable_account = %(payroll_payable_account)s "
|
||||
cond += "and %(from_date)s >= t2.from_date"
|
||||
emp_list = get_emp_list(sal_struct, cond, filters.end_date, filters.payroll_payable_account)
|
||||
emp_list = remove_payrolled_employees(emp_list, filters.start_date, filters.end_date)
|
||||
return emp_list
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
filters = frappe._dict(filters)
|
||||
conditions = []
|
||||
exclude_employees = []
|
||||
include_employees = []
|
||||
emp_cond = ''
|
||||
if filters.start_date and filters.end_date:
|
||||
employee_list = get_employee_with_existing_salary_slip(filters.start_date, filters.end_date, filters.company)
|
||||
employee_list = get_employee_list(filters)
|
||||
emp = filters.get('employees')
|
||||
include_employees = [employee.employee for employee in employee_list if employee.employee not in emp]
|
||||
filters.pop('start_date')
|
||||
filters.pop('end_date')
|
||||
filters.pop('salary_slip_based_on_timesheet')
|
||||
filters.pop('payroll_frequency')
|
||||
filters.pop('payroll_payable_account')
|
||||
filters.pop('currency')
|
||||
if filters.employees is not None:
|
||||
filters.pop('employees')
|
||||
if employee_list:
|
||||
exclude_employees.extend(employee_list)
|
||||
if emp:
|
||||
exclude_employees.extend(emp)
|
||||
if exclude_employees:
|
||||
emp_cond += 'and employee not in %(exclude_employees)s'
|
||||
|
||||
if include_employees:
|
||||
emp_cond += 'and employee in %(include_employees)s'
|
||||
|
||||
return frappe.db.sql("""select name, employee_name from `tabEmployee`
|
||||
where status = 'Active'
|
||||
@ -695,4 +710,4 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
'_txt': txt.replace("%", ""),
|
||||
'start': start,
|
||||
'page_len': page_len,
|
||||
'exclude_employees': exclude_employees})
|
||||
'include_employees': include_employees})
|
||||
|
@ -40,7 +40,9 @@ frappe.ui.form.on("Salary Slip", {
|
||||
frm.set_query("employee", function() {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.employee_query",
|
||||
filters: frm.doc.company
|
||||
filters: {
|
||||
company: frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
@ -11,15 +11,16 @@
|
||||
"project",
|
||||
"issue",
|
||||
"type",
|
||||
"color",
|
||||
"is_group",
|
||||
"is_template",
|
||||
"column_break0",
|
||||
"status",
|
||||
"priority",
|
||||
"task_weight",
|
||||
"completed_by",
|
||||
"color",
|
||||
"parent_task",
|
||||
"completed_by",
|
||||
"completed_on",
|
||||
"sb_timeline",
|
||||
"exp_start_date",
|
||||
"expected_time",
|
||||
@ -358,6 +359,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.status == \"Completed\"",
|
||||
"fieldname": "completed_by",
|
||||
"fieldtype": "Link",
|
||||
"label": "Completed By",
|
||||
@ -381,6 +383,13 @@
|
||||
"fieldname": "duration",
|
||||
"fieldtype": "Int",
|
||||
"label": "Duration (Days)"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval: doc.status == \"Completed\"",
|
||||
"fieldname": "completed_on",
|
||||
"fieldtype": "Date",
|
||||
"label": "Completed On",
|
||||
"mandatory_depends_on": "eval: doc.status == \"Completed\""
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-check",
|
||||
@ -388,7 +397,7 @@
|
||||
"is_tree": 1,
|
||||
"links": [],
|
||||
"max_attachments": 5,
|
||||
"modified": "2020-12-28 11:32:58.714991",
|
||||
"modified": "2021-04-16 12:46:51.556741",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Task",
|
||||
|
@ -36,6 +36,7 @@ class Task(NestedSet):
|
||||
self.validate_status()
|
||||
self.update_depends_on()
|
||||
self.validate_dependencies_for_template_task()
|
||||
self.validate_completed_on()
|
||||
|
||||
def validate_dates(self):
|
||||
if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
|
||||
@ -100,6 +101,10 @@ class Task(NestedSet):
|
||||
dependent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(task.task)
|
||||
frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
|
||||
|
||||
def validate_completed_on(self):
|
||||
if self.completed_on and getdate(self.completed_on) > getdate():
|
||||
frappe.throw(_("Completed On cannot be greater than Today"))
|
||||
|
||||
def update_depends_on(self):
|
||||
depends_on_tasks = self.depends_on_tasks or ""
|
||||
for d in self.depends_on:
|
||||
|
@ -0,0 +1,41 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.query_reports["Delayed Tasks Summary"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname": "from_date",
|
||||
"label": __("From Date"),
|
||||
"fieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "to_date",
|
||||
"label": __("To Date"),
|
||||
"fieldtype": "Date"
|
||||
},
|
||||
{
|
||||
"fieldname": "priority",
|
||||
"label": __("Priority"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["", "Low", "Medium", "High", "Urgent"]
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"label": __("Status"),
|
||||
"fieldtype": "Select",
|
||||
"options": ["", "Open", "Working","Pending Review","Overdue","Completed"]
|
||||
},
|
||||
],
|
||||
"formatter": function(value, row, column, data, default_formatter) {
|
||||
value = default_formatter(value, row, column, data);
|
||||
if (column.id == "delay") {
|
||||
if (data["delay"] > 0) {
|
||||
value = `<p style="color: red; font-weight: bold">${value}</p>`;
|
||||
} else {
|
||||
value = `<p style="color: green; font-weight: bold">${value}</p>`;
|
||||
}
|
||||
}
|
||||
return value
|
||||
}
|
||||
};
|
@ -0,0 +1,29 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-03-25 15:03:19.857418",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-04-15 15:49:35.432486",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Delayed Tasks Summary",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "Task",
|
||||
"report_name": "Delayed Tasks Summary",
|
||||
"report_type": "Script Report",
|
||||
"roles": [
|
||||
{
|
||||
"role": "Projects User"
|
||||
},
|
||||
{
|
||||
"role": "Projects Manager"
|
||||
}
|
||||
]
|
||||
}
|
@ -0,0 +1,133 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.utils import date_diff, nowdate
|
||||
|
||||
def execute(filters=None):
|
||||
columns, data = [], []
|
||||
data = get_data(filters)
|
||||
columns = get_columns()
|
||||
charts = get_chart_data(data)
|
||||
return columns, data, None, charts
|
||||
|
||||
def get_data(filters):
|
||||
conditions = get_conditions(filters)
|
||||
tasks = frappe.get_all("Task",
|
||||
filters = conditions,
|
||||
fields = ["name", "subject", "exp_start_date", "exp_end_date",
|
||||
"status", "priority", "completed_on", "progress"],
|
||||
order_by="creation"
|
||||
)
|
||||
for task in tasks:
|
||||
if task.exp_end_date:
|
||||
if task.completed_on:
|
||||
task.delay = date_diff(task.completed_on, task.exp_end_date)
|
||||
elif task.status == "Completed":
|
||||
# task is completed but completed on is not set (for older tasks)
|
||||
task.delay = 0
|
||||
else:
|
||||
# task not completed
|
||||
task.delay = date_diff(nowdate(), task.exp_end_date)
|
||||
else:
|
||||
# task has no end date, hence no delay
|
||||
task.delay = 0
|
||||
|
||||
# Sort by descending order of delay
|
||||
tasks.sort(key=lambda x: x["delay"], reverse=True)
|
||||
return tasks
|
||||
|
||||
def get_conditions(filters):
|
||||
conditions = frappe._dict()
|
||||
keys = ["priority", "status"]
|
||||
for key in keys:
|
||||
if filters.get(key):
|
||||
conditions[key] = filters.get(key)
|
||||
if filters.get("from_date"):
|
||||
conditions.exp_end_date = [">=", filters.get("from_date")]
|
||||
if filters.get("to_date"):
|
||||
conditions.exp_start_date = ["<=", filters.get("to_date")]
|
||||
return conditions
|
||||
|
||||
def get_chart_data(data):
|
||||
delay, on_track = 0, 0
|
||||
for entry in data:
|
||||
if entry.get("delay") > 0:
|
||||
delay = delay + 1
|
||||
else:
|
||||
on_track = on_track + 1
|
||||
charts = {
|
||||
"data": {
|
||||
"labels": ["On Track", "Delayed"],
|
||||
"datasets": [
|
||||
{
|
||||
"name": "Delayed",
|
||||
"values": [on_track, delay]
|
||||
}
|
||||
]
|
||||
},
|
||||
"type": "percentage",
|
||||
"colors": ["#84D5BA", "#CB4B5F"]
|
||||
}
|
||||
return charts
|
||||
|
||||
def get_columns():
|
||||
columns = [
|
||||
{
|
||||
"fieldname": "name",
|
||||
"fieldtype": "Link",
|
||||
"label": "Task",
|
||||
"options": "Task",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"fieldname": "subject",
|
||||
"fieldtype": "Data",
|
||||
"label": "Subject",
|
||||
"width": 200
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Data",
|
||||
"label": "Status",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"fieldname": "priority",
|
||||
"fieldtype": "Data",
|
||||
"label": "Priority",
|
||||
"width": 80
|
||||
},
|
||||
{
|
||||
"fieldname": "progress",
|
||||
"fieldtype": "Data",
|
||||
"label": "Progress (%)",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"fieldname": "exp_start_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected Start Date",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"fieldname": "exp_end_date",
|
||||
"fieldtype": "Date",
|
||||
"label": "Expected End Date",
|
||||
"width": 150
|
||||
},
|
||||
{
|
||||
"fieldname": "completed_on",
|
||||
"fieldtype": "Date",
|
||||
"label": "Actual End Date",
|
||||
"width": 130
|
||||
},
|
||||
{
|
||||
"fieldname": "delay",
|
||||
"fieldtype": "Data",
|
||||
"label": "Delay (In Days)",
|
||||
"width": 120
|
||||
}
|
||||
]
|
||||
return columns
|
@ -0,0 +1,54 @@
|
||||
from __future__ import unicode_literals
|
||||
import unittest
|
||||
import frappe
|
||||
from frappe.utils import nowdate, add_days, add_months
|
||||
from erpnext.projects.doctype.task.test_task import create_task
|
||||
from erpnext.projects.report.delayed_tasks_summary.delayed_tasks_summary import execute
|
||||
|
||||
class TestDelayedTasksSummary(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUp(self):
|
||||
task1 = create_task("_Test Task 98", add_days(nowdate(), -10), nowdate())
|
||||
create_task("_Test Task 99", add_days(nowdate(), -10), add_days(nowdate(), -1))
|
||||
|
||||
task1.status = "Completed"
|
||||
task1.completed_on = add_days(nowdate(), -1)
|
||||
task1.save()
|
||||
|
||||
def test_delayed_tasks_summary(self):
|
||||
filters = frappe._dict({
|
||||
"from_date": add_months(nowdate(), -1),
|
||||
"to_date": nowdate(),
|
||||
"priority": "Low",
|
||||
"status": "Open"
|
||||
})
|
||||
expected_data = [
|
||||
{
|
||||
"subject": "_Test Task 99",
|
||||
"status": "Open",
|
||||
"priority": "Low",
|
||||
"delay": 1
|
||||
},
|
||||
{
|
||||
"subject": "_Test Task 98",
|
||||
"status": "Completed",
|
||||
"priority": "Low",
|
||||
"delay": -1
|
||||
}
|
||||
]
|
||||
report = execute(filters)
|
||||
data = list(filter(lambda x: x.subject == "_Test Task 99", report[1]))[0]
|
||||
|
||||
for key in ["subject", "status", "priority", "delay"]:
|
||||
self.assertEqual(expected_data[0].get(key), data.get(key))
|
||||
|
||||
filters.status = "Completed"
|
||||
report = execute(filters)
|
||||
data = list(filter(lambda x: x.subject == "_Test Task 98", report[1]))[0]
|
||||
|
||||
for key in ["subject", "status", "priority", "delay"]:
|
||||
self.assertEqual(expected_data[1].get(key), data.get(key))
|
||||
|
||||
def tearDown(self):
|
||||
for task in ["_Test Task 98", "_Test Task 99"]:
|
||||
frappe.get_doc("Task", {"subject": task}).delete()
|
@ -15,6 +15,7 @@
|
||||
"hide_custom": 0,
|
||||
"icon": "project",
|
||||
"idx": 0,
|
||||
"is_default": 0,
|
||||
"is_standard": 1,
|
||||
"label": "Projects",
|
||||
"links": [
|
||||
@ -148,9 +149,19 @@
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
},
|
||||
{
|
||||
"dependencies": "Task",
|
||||
"hidden": 0,
|
||||
"is_query_report": 1,
|
||||
"label": "Delayed Tasks Summary",
|
||||
"link_to": "Delayed Tasks Summary",
|
||||
"link_type": "Report",
|
||||
"onboard": 0,
|
||||
"type": "Link"
|
||||
}
|
||||
],
|
||||
"modified": "2020-12-01 13:38:37.856224",
|
||||
"modified": "2021-03-26 16:32:00.628561",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Projects",
|
||||
|
@ -216,7 +216,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
|
||||
child: item,
|
||||
args: {
|
||||
item_code: item.item_code,
|
||||
warehouse: item.warehouse
|
||||
warehouse: item.warehouse,
|
||||
company: doc.company
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -1103,6 +1103,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
|
||||
to_currency: to_currency,
|
||||
args: args
|
||||
},
|
||||
freeze: true,
|
||||
freeze_message: __("Fetching exchange rates ..."),
|
||||
callback: function(r) {
|
||||
callback(flt(r.message));
|
||||
}
|
||||
|
@ -20,6 +20,16 @@ class Quiz {
|
||||
}
|
||||
|
||||
make(data) {
|
||||
if (data.duration) {
|
||||
const timer_display = document.createElement("div");
|
||||
timer_display.classList.add("lms-timer", "float-right", "font-weight-bold");
|
||||
document.getElementsByClassName("lms-title")[0].appendChild(timer_display);
|
||||
if (!data.activity || (data.activity && !data.activity.is_complete)) {
|
||||
this.initialiseTimer(data.duration);
|
||||
this.is_time_bound = true;
|
||||
this.time_taken = 0;
|
||||
}
|
||||
}
|
||||
data.questions.forEach(question_data => {
|
||||
let question_wrapper = document.createElement('div');
|
||||
let question = new Question({
|
||||
@ -37,12 +47,51 @@ class Quiz {
|
||||
indicator = 'green'
|
||||
message = 'You have already cleared the quiz.'
|
||||
}
|
||||
|
||||
if (data.activity.time_taken) {
|
||||
this.calculate_and_display_time(data.activity.time_taken, "Time Taken - ");
|
||||
}
|
||||
this.set_quiz_footer(message, indicator, data.activity.score)
|
||||
}
|
||||
else {
|
||||
this.make_actions();
|
||||
}
|
||||
window.addEventListener('beforeunload', (event) => {
|
||||
event.preventDefault();
|
||||
event.returnValue = '';
|
||||
});
|
||||
}
|
||||
|
||||
initialiseTimer(duration) {
|
||||
this.time_left = duration;
|
||||
var self = this;
|
||||
var old_diff;
|
||||
this.calculate_and_display_time(this.time_left, "Time Left - ");
|
||||
this.start_time = new Date().getTime();
|
||||
this.timer = setInterval(function () {
|
||||
var diff = (new Date().getTime() - self.start_time)/1000;
|
||||
var variation = old_diff ? diff - old_diff : diff;
|
||||
old_diff = diff;
|
||||
self.time_left -= variation;
|
||||
self.time_taken += variation;
|
||||
self.calculate_and_display_time(self.time_left, "Time Left - ");
|
||||
if (self.time_left <= 0) {
|
||||
clearInterval(self.timer);
|
||||
self.time_taken -= 1;
|
||||
self.submit();
|
||||
}
|
||||
}, 1000);
|
||||
}
|
||||
|
||||
calculate_and_display_time(second, text) {
|
||||
var timer_display = document.getElementsByClassName("lms-timer")[0];
|
||||
var hours = this.append_zero(Math.floor(second / 3600));
|
||||
var minutes = this.append_zero(Math.floor(second % 3600 / 60));
|
||||
var seconds = this.append_zero(Math.ceil(second % 3600 % 60));
|
||||
timer_display.innerText = text + hours + ":" + minutes + ":" + seconds;
|
||||
}
|
||||
|
||||
append_zero(time) {
|
||||
return time > 9 ? time : "0" + time;
|
||||
}
|
||||
|
||||
make_actions() {
|
||||
@ -57,6 +106,10 @@ class Quiz {
|
||||
}
|
||||
|
||||
submit() {
|
||||
if (this.is_time_bound) {
|
||||
clearInterval(this.timer);
|
||||
$(".lms-timer").text("");
|
||||
}
|
||||
this.submit_btn.innerText = 'Evaluating..'
|
||||
this.submit_btn.disabled = true
|
||||
this.disable()
|
||||
@ -64,7 +117,8 @@ class Quiz {
|
||||
quiz_name: this.name,
|
||||
quiz_response: this.get_selected(),
|
||||
course: this.course,
|
||||
program: this.program
|
||||
program: this.program,
|
||||
time_taken: this.is_time_bound ? this.time_taken : ""
|
||||
}).then(res => {
|
||||
this.submit_btn.remove()
|
||||
if (!res.message) {
|
||||
@ -157,7 +211,7 @@ class Question {
|
||||
return input;
|
||||
}
|
||||
|
||||
let make_label = function(name, value) {
|
||||
let make_label = function (name, value) {
|
||||
let label = document.createElement('label');
|
||||
label.classList.add('form-check-label');
|
||||
label.htmlFor = name;
|
||||
@ -166,14 +220,14 @@ class Question {
|
||||
}
|
||||
|
||||
let make_option = function (wrapper, option) {
|
||||
let option_div = document.createElement('div')
|
||||
option_div.classList.add('form-check', 'pb-1')
|
||||
let option_div = document.createElement('div');
|
||||
option_div.classList.add('form-check', 'pb-1');
|
||||
let input = make_input(option.name, option.option);
|
||||
let label = make_label(option.name, option.option);
|
||||
option_div.appendChild(input)
|
||||
option_div.appendChild(label)
|
||||
wrapper.appendChild(option_div)
|
||||
return {input: input, ...option}
|
||||
option_div.appendChild(input);
|
||||
option_div.appendChild(label);
|
||||
wrapper.appendChild(option_div);
|
||||
return { input: input, ...option };
|
||||
}
|
||||
|
||||
let options_wrapper = document.createElement('div')
|
||||
|
@ -291,17 +291,15 @@ $.extend(erpnext.utils, {
|
||||
return options[0];
|
||||
}
|
||||
},
|
||||
copy_parent_value_in_all_row: function(doc, dt, dn, table_fieldname, fieldname, parent_fieldname) {
|
||||
var d = locals[dt][dn];
|
||||
if(d[fieldname]){
|
||||
var cl = doc[table_fieldname] || [];
|
||||
for(var i = 0; i < cl.length; i++) {
|
||||
overrides_parent_value_in_all_rows: function(doc, dt, dn, table_fieldname, fieldname, parent_fieldname) {
|
||||
if (doc[parent_fieldname]) {
|
||||
let cl = doc[table_fieldname] || [];
|
||||
for (let i = 0; i < cl.length; i++) {
|
||||
cl[i][fieldname] = doc[parent_fieldname];
|
||||
}
|
||||
frappe.refresh_field(table_fieldname);
|
||||
}
|
||||
refresh_field(table_fieldname);
|
||||
},
|
||||
|
||||
create_new_doc: function (doctype, update_fields) {
|
||||
frappe.model.with_doctype(doctype, function() {
|
||||
var new_doc = frappe.model.get_new_doc(doctype);
|
||||
|
@ -353,9 +353,9 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
||||
return row.on_grid_fields_dict.batch_no.get_value();
|
||||
}
|
||||
});
|
||||
if (selected_batches.includes(val)) {
|
||||
if (selected_batches.includes(batch_no)) {
|
||||
this.set_value("");
|
||||
frappe.throw(__('Batch {0} already selected.', [val]));
|
||||
frappe.throw(__('Batch {0} already selected.', [batch_no]));
|
||||
}
|
||||
|
||||
if (me.warehouse_details.name) {
|
||||
|
@ -38,12 +38,13 @@ def validate_eligibility(doc):
|
||||
einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01'
|
||||
if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from):
|
||||
return False
|
||||
|
||||
|
||||
invalid_company = not frappe.db.get_value('E Invoice User', { 'company': doc.get('company') })
|
||||
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
|
||||
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
|
||||
no_taxes_applied = not doc.get('taxes')
|
||||
|
||||
if invalid_supply_type or company_transaction or no_taxes_applied:
|
||||
if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied:
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -400,7 +401,7 @@ def validate_totals(einvoice):
|
||||
if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1:
|
||||
frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.'))
|
||||
|
||||
if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - total_item_value) > 1:
|
||||
if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - flt(value_details['OthChrg']) - total_item_value) > 1:
|
||||
frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.'))
|
||||
|
||||
calculated_invoice_value = \
|
||||
@ -466,21 +467,24 @@ def make_einvoice(invoice):
|
||||
try:
|
||||
einvoice = safe_json_load(einvoice)
|
||||
einvoice = santize_einvoice_fields(einvoice)
|
||||
validate_totals(einvoice)
|
||||
|
||||
except Exception:
|
||||
log_error(einvoice)
|
||||
link_to_error_list = '<a href="List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
|
||||
frappe.throw(
|
||||
_('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
|
||||
invoice.name, link_to_error_list),
|
||||
title=_('E Invoice Creation Failed')
|
||||
)
|
||||
show_link_to_error_log(invoice, einvoice)
|
||||
|
||||
validate_totals(einvoice)
|
||||
|
||||
return einvoice
|
||||
|
||||
def show_link_to_error_log(invoice, einvoice):
|
||||
err_log = log_error(einvoice)
|
||||
link_to_error_log = get_link_to_form('Error Log', err_log.name, 'Error Log')
|
||||
frappe.throw(
|
||||
_('An error occurred while creating e-invoice for {}. Please check {} for more information.').format(
|
||||
invoice.name, link_to_error_log),
|
||||
title=_('E Invoice Creation Failed')
|
||||
)
|
||||
|
||||
def log_error(data=None):
|
||||
if not isinstance(data, dict):
|
||||
if isinstance(data, six.string_types):
|
||||
data = json.loads(data)
|
||||
|
||||
seperator = "--" * 50
|
||||
@ -587,7 +591,7 @@ class GSPConnector():
|
||||
self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
|
||||
|
||||
def get_seller_gstin(self):
|
||||
gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
|
||||
gstin = frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
|
||||
if not gstin:
|
||||
frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
|
||||
return gstin
|
||||
|
@ -561,7 +561,7 @@ def get_json(filters, report_name, data):
|
||||
|
||||
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year)
|
||||
|
||||
gst_json = {"gstin": "", "version": "GST2.2.9",
|
||||
gst_json = {"version": "GST2.2.9",
|
||||
"hash": "hash", "gstin": gstin, "fp": fp}
|
||||
|
||||
res = {}
|
||||
|
@ -4,11 +4,8 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from erpnext.regional.united_arab_emirates.setup import make_custom_fields, add_print_formats
|
||||
from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax
|
||||
|
||||
|
||||
def setup(company=None, patch=True):
|
||||
make_custom_fields()
|
||||
add_print_formats()
|
||||
|
||||
if company:
|
||||
create_sales_tax(company)
|
@ -6,7 +6,6 @@ from __future__ import unicode_literals
|
||||
import frappe, os, json
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.permissions import add_permission, update_permission_property
|
||||
from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax
|
||||
from erpnext.payroll.doctype.gratuity_rule.gratuity_rule import get_gratuity_rule
|
||||
|
||||
def setup(company=None, patch=True):
|
||||
@ -16,9 +15,6 @@ def setup(company=None, patch=True):
|
||||
add_permissions()
|
||||
create_gratuity_rule()
|
||||
|
||||
if company:
|
||||
create_sales_tax(company)
|
||||
|
||||
def make_custom_fields():
|
||||
is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated',
|
||||
fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description',
|
||||
|
@ -38,11 +38,19 @@ class Customer(TransactionBase):
|
||||
set_name_by_naming_series(self)
|
||||
|
||||
def get_customer_name(self):
|
||||
if frappe.db.get_value("Customer", self.customer_name):
|
||||
|
||||
if frappe.db.get_value("Customer", self.customer_name) and not frappe.flags.in_import:
|
||||
count = frappe.db.sql("""select ifnull(MAX(CAST(SUBSTRING_INDEX(name, ' ', -1) AS UNSIGNED)), 0) from tabCustomer
|
||||
where name like %s""", "%{0} - %".format(self.customer_name), as_list=1)[0][0]
|
||||
count = cint(count) + 1
|
||||
return "{0} - {1}".format(self.customer_name, cstr(count))
|
||||
|
||||
new_customer_name = "{0} - {1}".format(self.customer_name, cstr(count))
|
||||
|
||||
msgprint(_("Changed customer name to '{}' as '{}' already exists.")
|
||||
.format(new_customer_name, self.customer_name),
|
||||
title=_("Note"), indicator="yellow")
|
||||
|
||||
return new_customer_name
|
||||
|
||||
return self.customer_name
|
||||
|
||||
|
@ -98,6 +98,7 @@
|
||||
"rounded_total",
|
||||
"in_words",
|
||||
"advance_paid",
|
||||
"disable_rounded_total",
|
||||
"packing_list",
|
||||
"packed_items",
|
||||
"payment_schedule_section",
|
||||
@ -901,6 +902,7 @@
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@ -912,6 +914,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@ -961,6 +964,7 @@
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@ -973,6 +977,7 @@
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"hide_days": 1,
|
||||
@ -1474,13 +1479,20 @@
|
||||
"label": "Represents Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "grand_total",
|
||||
"fieldname": "disable_rounded_total",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Rounded Total"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-file-text",
|
||||
"idx": 105,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-20 23:40:39.929296",
|
||||
"modified": "2021-04-15 23:55:13.439068",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Selling",
|
||||
"name": "Sales Order",
|
||||
|
@ -159,6 +159,31 @@ erpnext.PointOfSale.ItemSelector = class {
|
||||
bind_events() {
|
||||
const me = this;
|
||||
window.onScan = onScan;
|
||||
|
||||
onScan.decodeKeyEvent = function (oEvent) {
|
||||
var iCode = this._getNormalizedKeyNum(oEvent);
|
||||
switch (true) {
|
||||
case iCode >= 48 && iCode <= 90: // numbers and letters
|
||||
case iCode >= 106 && iCode <= 111: // operations on numeric keypad (+, -, etc.)
|
||||
case (iCode >= 160 && iCode <= 164) || iCode == 170: // ^ ! # $ *
|
||||
case iCode >= 186 && iCode <= 194: // (; = , - . / `)
|
||||
case iCode >= 219 && iCode <= 222: // ([ \ ] ')
|
||||
if (oEvent.key !== undefined && oEvent.key !== '') {
|
||||
return oEvent.key;
|
||||
}
|
||||
|
||||
var sDecoded = String.fromCharCode(iCode);
|
||||
switch (oEvent.shiftKey) {
|
||||
case false: sDecoded = sDecoded.toLowerCase(); break;
|
||||
case true: sDecoded = sDecoded.toUpperCase(); break;
|
||||
}
|
||||
return sDecoded;
|
||||
case iCode >= 96 && iCode <= 105: // numbers on numeric keypad
|
||||
return 0 + (iCode - 96);
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
onScan.attachTo(document, {
|
||||
onScan: (sScancode) => {
|
||||
if (this.search_field && this.$component.is(':visible')) {
|
||||
|
@ -204,11 +204,11 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
print_receipt() {
|
||||
const frm = this.events.get_frm();
|
||||
frappe.utils.print(
|
||||
frm.doctype,
|
||||
frm.docname,
|
||||
this.doc.doctype,
|
||||
this.doc.name,
|
||||
frm.pos_print_format,
|
||||
frm.doc.letter_head,
|
||||
frm.doc.language || frappe.boot.lang
|
||||
this.doc.letter_head,
|
||||
this.doc.language || frappe.boot.lang
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -17,6 +17,7 @@ from frappe.utils.nestedset import NestedSet
|
||||
from past.builtins import cmp
|
||||
import functools
|
||||
from erpnext.accounts.doctype.account.account import get_account_currency
|
||||
from erpnext.setup.setup_wizard.operations.taxes_setup import setup_taxes_and_charges
|
||||
|
||||
class Company(NestedSet):
|
||||
nsm_parent_field = 'parent_company'
|
||||
@ -68,11 +69,7 @@ class Company(NestedSet):
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_default_tax_template(self):
|
||||
from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax
|
||||
create_sales_tax({
|
||||
'country': self.country,
|
||||
'company_name': self.name
|
||||
})
|
||||
setup_taxes_and_charges(self.name, self.country)
|
||||
|
||||
def validate_default_accounts(self):
|
||||
accounts = [
|
||||
|
@ -15,7 +15,7 @@ def delete_company_transactions(company_name):
|
||||
frappe.only_for("System Manager")
|
||||
doc = frappe.get_doc("Company", company_name)
|
||||
|
||||
if frappe.session.user != doc.owner:
|
||||
if frappe.session.user != doc.owner and frappe.session.user != 'Administrator':
|
||||
frappe.throw(_("Transactions can only be deleted by the creator of the Company"),
|
||||
frappe.PermissionError)
|
||||
|
||||
|
@ -8,9 +8,11 @@ from erpnext.accounts.doctype.cash_flow_mapper.default_cash_flow_mapper import D
|
||||
from .default_success_action import get_default_success_action
|
||||
from frappe import _
|
||||
from frappe.utils import cint
|
||||
from frappe.installer import update_site_config
|
||||
from frappe.desk.page.setup_wizard.setup_wizard import add_all_roles_to
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_field
|
||||
from erpnext.setup.default_energy_point_rules import get_default_energy_point_rules
|
||||
from six import iteritems
|
||||
|
||||
default_mail_footer = """<div style="padding: 7px; text-align: right; color: #888"><small>Sent via
|
||||
<a style="color: #888" href="http://erpnext.org">ERPNext</a></div>"""
|
||||
@ -29,6 +31,7 @@ def after_install():
|
||||
add_company_to_session_defaults()
|
||||
add_standard_navbar_items()
|
||||
add_app_name()
|
||||
add_non_standard_user_types()
|
||||
frappe.db.commit()
|
||||
|
||||
|
||||
@ -164,3 +167,81 @@ def add_standard_navbar_items():
|
||||
|
||||
def add_app_name():
|
||||
frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext')
|
||||
|
||||
def add_non_standard_user_types():
|
||||
user_types = get_user_types_data()
|
||||
|
||||
user_type_limit = {}
|
||||
for user_type, data in iteritems(user_types):
|
||||
user_type_limit.setdefault(frappe.scrub(user_type), 10)
|
||||
|
||||
update_site_config('user_type_doctype_limit', user_type_limit)
|
||||
|
||||
for user_type, data in iteritems(user_types):
|
||||
create_custom_role(data)
|
||||
create_user_type(user_type, data)
|
||||
|
||||
def get_user_types_data():
|
||||
return {
|
||||
'Employee Self Service': {
|
||||
'role': 'Employee Self Service',
|
||||
'apply_user_permission_on': 'Employee',
|
||||
'user_id_field': 'user_id',
|
||||
'doctypes': {
|
||||
'Salary Slip': ['read'],
|
||||
'Employee': ['read', 'write'],
|
||||
'Expense Claim': ['read', 'write', 'create', 'delete'],
|
||||
'Leave Application': ['read', 'write', 'create', 'delete'],
|
||||
'Attendance Request': ['read', 'write', 'create', 'delete'],
|
||||
'Compensatory Leave Request': ['read', 'write', 'create', 'delete'],
|
||||
'Employee Tax Exemption Declaration': ['read', 'write', 'create', 'delete'],
|
||||
'Employee Tax Exemption Proof Submission': ['read', 'write', 'create', 'delete'],
|
||||
'Timesheet': ['read', 'write', 'create', 'delete', 'submit', 'cancel', 'amend']
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
def create_custom_role(data):
|
||||
if data.get('role') and not frappe.db.exists('Role', data.get('role')):
|
||||
frappe.get_doc({
|
||||
'doctype': 'Role',
|
||||
'role_name': data.get('role'),
|
||||
'desk_access': 1,
|
||||
'is_custom': 1
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
def create_user_type(user_type, data):
|
||||
if frappe.db.exists('User Type', user_type):
|
||||
doc = frappe.get_cached_doc('User Type', user_type)
|
||||
doc.user_doctypes = []
|
||||
else:
|
||||
doc = frappe.new_doc('User Type')
|
||||
doc.update({
|
||||
'name': user_type,
|
||||
'role': data.get('role'),
|
||||
'user_id_field': data.get('user_id_field'),
|
||||
'apply_user_permission_on': data.get('apply_user_permission_on')
|
||||
})
|
||||
|
||||
create_role_permissions_for_doctype(doc, data)
|
||||
doc.save(ignore_permissions=True)
|
||||
|
||||
def create_role_permissions_for_doctype(doc, data):
|
||||
for doctype, perms in iteritems(data.get('doctypes')):
|
||||
args = {'document_type': doctype}
|
||||
for perm in perms:
|
||||
args[perm] = 1
|
||||
|
||||
doc.append('user_doctypes', args)
|
||||
|
||||
def update_select_perm_after_install():
|
||||
if not frappe.flags.update_select_perm_after_migrate:
|
||||
return
|
||||
|
||||
frappe.flags.ignore_select_perm = False
|
||||
for row in frappe.get_all('User Type', filters= {'is_standard': 0}):
|
||||
print('Updating user type :- ', row.name)
|
||||
doc = frappe.get_doc('User Type', row.name)
|
||||
doc.save()
|
||||
|
||||
frappe.flags.update_select_perm_after_migrate = False
|
||||
|
@ -481,14 +481,250 @@
|
||||
},
|
||||
|
||||
"Germany": {
|
||||
"Germany VAT 19%": {
|
||||
"account_name": "VAT 19%",
|
||||
"tax_rate": 19.00,
|
||||
"default": 1
|
||||
},
|
||||
"Germany VAT 7%": {
|
||||
"account_name": "VAT 7%",
|
||||
"tax_rate": 7.00
|
||||
"chart_of_accounts": {
|
||||
"SKR04 mit Kontonummern": {
|
||||
"sales_tax_templates": [
|
||||
{
|
||||
"title": "Umsatzsteuer 19%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Umsatzsteuer 19%",
|
||||
"account_number": "3806",
|
||||
"tax_rate": 19.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Umsatzsteuer 7%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Umsatzsteuer 7%",
|
||||
"account_number": "3801",
|
||||
"tax_rate": 7.00
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"purchase_tax_templates": [
|
||||
{
|
||||
"title": "Abziehbare Vorsteuer 19%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Abziehbare Vorsteuer 19%",
|
||||
"account_number": "1406",
|
||||
"root_type": "Asset",
|
||||
"tax_rate": 19.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Abziehbare Vorsteuer 7%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Abziehbare Vorsteuer 7%",
|
||||
"account_number": "1401",
|
||||
"root_type": "Asset",
|
||||
"tax_rate": 7.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Innergemeinschaftlicher Erwerb 19% Umsatzsteuer und 19% Vorsteuer",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Abziehbare Vorsteuer nach § 13b UStG 19%",
|
||||
"account_number": "1407",
|
||||
"root_type": "Asset",
|
||||
"tax_rate": 19.00
|
||||
},
|
||||
"add_deduct_tax": "Add"
|
||||
},
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Umsatzsteuer nach § 13b UStG 19%",
|
||||
"account_number": "3837",
|
||||
"root_type": "Liability",
|
||||
"tax_rate": 19.00
|
||||
},
|
||||
"add_deduct_tax": "Deduct"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"SKR03 mit Kontonummern": {
|
||||
"sales_tax_templates": [
|
||||
{
|
||||
"title": "Umsatzsteuer 19%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Umsatzsteuer 19%",
|
||||
"account_number": "1776",
|
||||
"tax_rate": 19.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Umsatzsteuer 7%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Umsatzsteuer 7%",
|
||||
"account_number": "1771",
|
||||
"tax_rate": 7.00
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"purchase_tax_templates": [
|
||||
{
|
||||
"title": "Abziehbare Vorsteuer 19%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Abziehbare Vorsteuer 19%",
|
||||
"account_number": "1576",
|
||||
"root_type": "Asset",
|
||||
"tax_rate": 19.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Abziehbare Vorsteuer 7%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Abziehbare Vorsteuer 7%",
|
||||
"account_number": "1571",
|
||||
"root_type": "Asset",
|
||||
"tax_rate": 7.00
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"Standard with Numbers": {
|
||||
"sales_tax_templates": [
|
||||
{
|
||||
"title": "Umsatzsteuer 19%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Umsatzsteuer 19%",
|
||||
"account_number": "2301",
|
||||
"tax_rate": 19.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Umsatzsteuer 7%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Umsatzsteuer 7%",
|
||||
"account_number": "2302",
|
||||
"tax_rate": 7.00
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"purchase_tax_templates": [
|
||||
{
|
||||
"title": "Abziehbare Vorsteuer 19%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Abziehbare Vorsteuer 19%",
|
||||
"account_number": "1501",
|
||||
"root_type": "Asset",
|
||||
"tax_rate": 19.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Abziehbare Vorsteuer 7%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Abziehbare Vorsteuer 7%",
|
||||
"account_number": "1502",
|
||||
"root_type": "Asset",
|
||||
"tax_rate": 7.00
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"*": {
|
||||
"sales_tax_templates": [
|
||||
{
|
||||
"title": "Umsatzsteuer 19%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Umsatzsteuer 19%",
|
||||
"tax_rate": 19.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Umsatzsteuer 7%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Umsatzsteuer 7%",
|
||||
"tax_rate": 7.00
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"purchase_tax_templates": [
|
||||
{
|
||||
"title": "Abziehbare Vorsteuer 19%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Abziehbare Vorsteuer 19%",
|
||||
"tax_rate": 19.00,
|
||||
"root_type": "Asset"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Abziehbare Vorsteuer 7%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "Abziehbare Vorsteuer 7%",
|
||||
"root_type": "Asset",
|
||||
"tax_rate": 7.00
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
@ -580,26 +816,135 @@
|
||||
},
|
||||
|
||||
"India": {
|
||||
"In State GST": {
|
||||
"account_name": ["SGST", "CGST"],
|
||||
"tax_rate": [9.00, 9.00],
|
||||
"default": 1
|
||||
},
|
||||
"Out of State GST": {
|
||||
"account_name": "IGST",
|
||||
"tax_rate": 18.00
|
||||
},
|
||||
"VAT 5%": {
|
||||
"account_name": "VAT 5%",
|
||||
"tax_rate": 5.00
|
||||
},
|
||||
"VAT 4%": {
|
||||
"account_name": "VAT 4%",
|
||||
"tax_rate": 4.00
|
||||
},
|
||||
"VAT 14%": {
|
||||
"account_name": "VAT 14%",
|
||||
"tax_rate": 14.00
|
||||
"chart_of_accounts": {
|
||||
"*": {
|
||||
"item_tax_templates": [
|
||||
{
|
||||
"title": "In State GST",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "SGST",
|
||||
"tax_rate": 9.00
|
||||
}
|
||||
},
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "CGST",
|
||||
"tax_rate": 9.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Out of State GST",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "IGST",
|
||||
"tax_rate": 18.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "VAT 5%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "VAT 5%",
|
||||
"tax_rate": 5.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "VAT 4%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "VAT 4%",
|
||||
"tax_rate": 4.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "VAT 14%",
|
||||
"taxes": [
|
||||
{
|
||||
"tax_type": {
|
||||
"account_name": "VAT 14%",
|
||||
"tax_rate": 14.00
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"*": [
|
||||
{
|
||||
"title": "In State GST",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "SGST",
|
||||
"tax_rate": 9.00
|
||||
}
|
||||
},
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "CGST",
|
||||
"tax_rate": 9.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "Out of State GST",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "IGST",
|
||||
"tax_rate": 18.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "VAT 5%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "VAT 5%",
|
||||
"tax_rate": 5.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "VAT 4%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "VAT 4%",
|
||||
"tax_rate": 4.00
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"title": "VAT 14%",
|
||||
"taxes": [
|
||||
{
|
||||
"account_head": {
|
||||
"account_name": "VAT 14%",
|
||||
"tax_rate": 14.00
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -1,123 +1,232 @@
|
||||
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# License: GNU General Public License v3. See license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, copy, os, json
|
||||
from frappe.utils import flt
|
||||
from erpnext.accounts.doctype.account.account import RootNotEditable
|
||||
|
||||
def create_sales_tax(args):
|
||||
country_wise_tax = get_country_wise_tax(args.get("country"))
|
||||
if country_wise_tax and len(country_wise_tax) > 0:
|
||||
for sales_tax, tax_data in country_wise_tax.items():
|
||||
make_tax_account_and_template(
|
||||
args.get("company_name"),
|
||||
tax_data.get('account_name'),
|
||||
tax_data.get('tax_rate'), sales_tax)
|
||||
import os
|
||||
import json
|
||||
|
||||
def make_tax_account_and_template(company, account_name, tax_rate, template_name=None):
|
||||
if not isinstance(account_name, (list, tuple)):
|
||||
account_name = [account_name]
|
||||
tax_rate = [tax_rate]
|
||||
import frappe
|
||||
from frappe import _
|
||||
|
||||
accounts = []
|
||||
for i, name in enumerate(account_name):
|
||||
tax_account = make_tax_account(company, account_name[i], tax_rate[i])
|
||||
if tax_account:
|
||||
accounts.append(tax_account)
|
||||
|
||||
try:
|
||||
if accounts:
|
||||
make_sales_and_purchase_tax_templates(accounts, template_name)
|
||||
make_item_tax_templates(accounts, template_name)
|
||||
except frappe.NameError:
|
||||
if frappe.message_log: frappe.message_log.pop()
|
||||
except RootNotEditable:
|
||||
pass
|
||||
def setup_taxes_and_charges(company_name: str, country: str):
|
||||
file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'country_wise_tax.json')
|
||||
with open(file_path, 'r') as json_file:
|
||||
tax_data = json.load(json_file)
|
||||
|
||||
def make_tax_account(company, account_name, tax_rate):
|
||||
tax_group = get_tax_account_group(company)
|
||||
if tax_group:
|
||||
try:
|
||||
return frappe.get_doc({
|
||||
"doctype":"Account",
|
||||
"company": company,
|
||||
"parent_account": tax_group,
|
||||
"account_name": account_name,
|
||||
"is_group": 0,
|
||||
"report_type": "Balance Sheet",
|
||||
"root_type": "Liability",
|
||||
"account_type": "Tax",
|
||||
"tax_rate": flt(tax_rate) if tax_rate else None
|
||||
}).insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
except frappe.NameError:
|
||||
if frappe.message_log: frappe.message_log.pop()
|
||||
abbr = frappe.get_cached_value('Company', company, 'abbr')
|
||||
account = '{0} - {1}'.format(account_name, abbr)
|
||||
return frappe.get_doc('Account', account)
|
||||
country_wise_tax = tax_data.get(country)
|
||||
|
||||
def make_sales_and_purchase_tax_templates(accounts, template_name=None):
|
||||
if not template_name:
|
||||
template_name = accounts[0].name
|
||||
if not country_wise_tax:
|
||||
return
|
||||
|
||||
sales_tax_template = {
|
||||
"doctype": "Sales Taxes and Charges Template",
|
||||
"title": template_name,
|
||||
"company": accounts[0].company,
|
||||
'taxes': []
|
||||
if 'chart_of_accounts' not in country_wise_tax:
|
||||
country_wise_tax = simple_to_detailed(country_wise_tax)
|
||||
|
||||
from_detailed_data(company_name, country_wise_tax.get('chart_of_accounts'))
|
||||
|
||||
|
||||
def simple_to_detailed(templates):
|
||||
"""
|
||||
Convert a simple taxes object into a more detailed data structure.
|
||||
|
||||
Example input:
|
||||
|
||||
{
|
||||
"France VAT 20%": {
|
||||
"account_name": "VAT 20%",
|
||||
"tax_rate": 20,
|
||||
"default": 1
|
||||
},
|
||||
"France VAT 10%": {
|
||||
"account_name": "VAT 10%",
|
||||
"tax_rate": 10
|
||||
}
|
||||
}
|
||||
|
||||
for account in accounts:
|
||||
sales_tax_template['taxes'].append({
|
||||
"category": "Total",
|
||||
"charge_type": "On Net Total",
|
||||
"account_head": account.name,
|
||||
"description": "{0} @ {1}".format(account.account_name, account.tax_rate),
|
||||
"rate": account.tax_rate
|
||||
})
|
||||
# Sales
|
||||
frappe.get_doc(copy.deepcopy(sales_tax_template)).insert(ignore_permissions=True)
|
||||
|
||||
# Purchase
|
||||
purchase_tax_template = copy.deepcopy(sales_tax_template)
|
||||
purchase_tax_template["doctype"] = "Purchase Taxes and Charges Template"
|
||||
|
||||
doc = frappe.get_doc(purchase_tax_template)
|
||||
doc.insert(ignore_permissions=True)
|
||||
|
||||
def make_item_tax_templates(accounts, template_name=None):
|
||||
if not template_name:
|
||||
template_name = accounts[0].name
|
||||
|
||||
item_tax_template = {
|
||||
"doctype": "Item Tax Template",
|
||||
"title": template_name,
|
||||
"company": accounts[0].company,
|
||||
'taxes': []
|
||||
"""
|
||||
return {
|
||||
'chart_of_accounts': {
|
||||
'*': {
|
||||
'item_tax_templates': [{
|
||||
'title': title,
|
||||
'taxes': [{
|
||||
'tax_type': {
|
||||
'account_name': data.get('account_name'),
|
||||
'tax_rate': data.get('tax_rate')
|
||||
}
|
||||
}]
|
||||
} for title, data in templates.items()],
|
||||
'*': [{
|
||||
'title': title,
|
||||
'is_default': data.get('default', 0),
|
||||
'taxes': [{
|
||||
'account_head': {
|
||||
'account_name': data.get('account_name'),
|
||||
'tax_rate': data.get('tax_rate')
|
||||
}
|
||||
}]
|
||||
} for title, data in templates.items()]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
for account in accounts:
|
||||
item_tax_template['taxes'].append({
|
||||
"tax_type": account.name,
|
||||
"tax_rate": account.tax_rate
|
||||
})
|
||||
def from_detailed_data(company_name, data):
|
||||
"""Create Taxes and Charges Templates from detailed data."""
|
||||
coa_name = frappe.db.get_value('Company', company_name, 'chart_of_accounts')
|
||||
tax_templates = data.get(coa_name) or data.get('*')
|
||||
sales_tax_templates = tax_templates.get('sales_tax_templates') or tax_templates.get('*')
|
||||
purchase_tax_templates = tax_templates.get('purchase_tax_templates') or tax_templates.get('*')
|
||||
item_tax_templates = tax_templates.get('item_tax_templates') or tax_templates.get('*')
|
||||
|
||||
# Items
|
||||
frappe.get_doc(copy.deepcopy(item_tax_template)).insert(ignore_permissions=True)
|
||||
if sales_tax_templates:
|
||||
for template in sales_tax_templates:
|
||||
make_taxes_and_charges_template(company_name, 'Sales Taxes and Charges Template', template)
|
||||
|
||||
def get_tax_account_group(company):
|
||||
tax_group = frappe.db.get_value("Account",
|
||||
{"account_name": "Duties and Taxes", "is_group": 1, "company": company})
|
||||
if not tax_group:
|
||||
tax_group = frappe.db.get_value("Account", {"is_group": 1, "root_type": "Liability",
|
||||
"account_type": "Tax", "company": company})
|
||||
if purchase_tax_templates:
|
||||
for template in purchase_tax_templates:
|
||||
make_taxes_and_charges_template(company_name, 'Purchase Taxes and Charges Template', template)
|
||||
|
||||
return tax_group
|
||||
if item_tax_templates:
|
||||
for template in item_tax_templates:
|
||||
make_item_tax_template(company_name, template)
|
||||
|
||||
def get_country_wise_tax(country):
|
||||
data = {}
|
||||
with open (os.path.join(os.path.dirname(__file__), "..", "data", "country_wise_tax.json")) as countrywise_tax:
|
||||
data = json.load(countrywise_tax).get(country)
|
||||
|
||||
return data
|
||||
def make_taxes_and_charges_template(company_name, doctype, template):
|
||||
template['company'] = company_name
|
||||
template['doctype'] = doctype
|
||||
|
||||
if frappe.db.exists(doctype, {'title': template.get('title'), 'company': company_name}):
|
||||
return
|
||||
|
||||
for tax_row in template.get('taxes'):
|
||||
account_data = tax_row.get('account_head')
|
||||
tax_row_defaults = {
|
||||
'category': 'Total',
|
||||
'charge_type': 'On Net Total'
|
||||
}
|
||||
|
||||
# if account_head is a dict, search or create the account and get it's name
|
||||
if isinstance(account_data, dict):
|
||||
tax_row_defaults['description'] = '{0} @ {1}'.format(account_data.get('account_name'), account_data.get('tax_rate'))
|
||||
tax_row_defaults['rate'] = account_data.get('tax_rate')
|
||||
account = get_or_create_account(company_name, account_data)
|
||||
tax_row['account_head'] = account.name
|
||||
|
||||
# use the default value if nothing other is specified
|
||||
for fieldname, default_value in tax_row_defaults.items():
|
||||
if fieldname not in tax_row:
|
||||
tax_row[fieldname] = default_value
|
||||
|
||||
return frappe.get_doc(template).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def make_item_tax_template(company_name, template):
|
||||
"""Create an Item Tax Template.
|
||||
|
||||
This requires a separate method because Item Tax Template is structured
|
||||
differently from Sales and Purchase Tax Templates.
|
||||
"""
|
||||
doctype = 'Item Tax Template'
|
||||
template['company'] = company_name
|
||||
template['doctype'] = doctype
|
||||
|
||||
if frappe.db.exists(doctype, {'title': template.get('title'), 'company': company_name}):
|
||||
return
|
||||
|
||||
for tax_row in template.get('taxes'):
|
||||
account_data = tax_row.get('tax_type')
|
||||
|
||||
# if tax_type is a dict, search or create the account and get it's name
|
||||
if isinstance(account_data, dict):
|
||||
account = get_or_create_account(company_name, account_data)
|
||||
tax_row['tax_type'] = account.name
|
||||
if 'tax_rate' not in tax_row:
|
||||
tax_row['tax_rate'] = account_data.get('tax_rate')
|
||||
|
||||
return frappe.get_doc(template).insert(ignore_permissions=True)
|
||||
|
||||
|
||||
def get_or_create_account(company_name, account):
|
||||
"""
|
||||
Check if account already exists. If not, create it.
|
||||
Return a tax account or None.
|
||||
"""
|
||||
default_root_type = 'Liability'
|
||||
root_type = account.get('root_type', default_root_type)
|
||||
|
||||
existing_accounts = frappe.get_list('Account',
|
||||
filters={
|
||||
'company': company_name,
|
||||
'root_type': root_type
|
||||
},
|
||||
or_filters={
|
||||
'account_name': account.get('account_name'),
|
||||
'account_number': account.get('account_number')
|
||||
}
|
||||
)
|
||||
|
||||
if existing_accounts:
|
||||
return frappe.get_doc('Account', existing_accounts[0].name)
|
||||
|
||||
tax_group = get_or_create_tax_group(company_name, root_type)
|
||||
|
||||
account['doctype'] = 'Account'
|
||||
account['company'] = company_name
|
||||
account['parent_account'] = tax_group
|
||||
account['report_type'] = 'Balance Sheet'
|
||||
account['account_type'] = 'Tax'
|
||||
account['root_type'] = root_type
|
||||
account['is_group'] = 0
|
||||
|
||||
return frappe.get_doc(account).insert(ignore_permissions=True, ignore_mandatory=True)
|
||||
|
||||
|
||||
def get_or_create_tax_group(company_name, root_type):
|
||||
# Look for a group account of type 'Tax'
|
||||
tax_group_name = frappe.db.get_value('Account', {
|
||||
'is_group': 1,
|
||||
'root_type': root_type,
|
||||
'account_type': 'Tax',
|
||||
'company': company_name
|
||||
})
|
||||
|
||||
if tax_group_name:
|
||||
return tax_group_name
|
||||
|
||||
# Look for a group account named 'Duties and Taxes' or 'Tax Assets'
|
||||
account_name = _('Duties and Taxes') if root_type == 'Liability' else _('Tax Assets')
|
||||
tax_group_name = frappe.db.get_value('Account', {
|
||||
'is_group': 1,
|
||||
'root_type': root_type,
|
||||
'account_name': account_name,
|
||||
'company': company_name
|
||||
})
|
||||
|
||||
if tax_group_name:
|
||||
return tax_group_name
|
||||
|
||||
# Create a new group account named 'Duties and Taxes' or 'Tax Assets' just
|
||||
# below the root account
|
||||
root_account = frappe.get_list('Account', {
|
||||
'is_group': 1,
|
||||
'root_type': root_type,
|
||||
'company': company_name,
|
||||
'report_type': 'Balance Sheet',
|
||||
'parent_account': ('is', 'not set')
|
||||
}, limit=1)[0]
|
||||
|
||||
tax_group_account = frappe.get_doc({
|
||||
'doctype': 'Account',
|
||||
'company': company_name,
|
||||
'is_group': 1,
|
||||
'report_type': 'Balance Sheet',
|
||||
'root_type': root_type,
|
||||
'account_type': 'Tax',
|
||||
'account_name': account_name,
|
||||
'parent_account': root_account.name
|
||||
}).insert(ignore_permissions=True)
|
||||
|
||||
tax_group_name = tax_group_account.name
|
||||
|
||||
return tax_group_name
|
||||
|
@ -9,5 +9,4 @@ def complete():
|
||||
'data', 'test_mfg.json'), 'r') as f:
|
||||
data = json.loads(f.read())
|
||||
|
||||
#setup_wizard.create_sales_tax(data)
|
||||
setup_complete(data)
|
||||
|
@ -230,12 +230,12 @@ def update_cart_address(address_type, address_name):
|
||||
if address_type.lower() == "billing":
|
||||
quotation.customer_address = address_name
|
||||
quotation.address_display = address_display
|
||||
quotation.shipping_address_name == quotation.shipping_address_name or address_name
|
||||
quotation.shipping_address_name = quotation.shipping_address_name or address_name
|
||||
address_doc = next((doc for doc in get_billing_addresses() if doc["name"] == address_name), None)
|
||||
elif address_type.lower() == "shipping":
|
||||
quotation.shipping_address_name = address_name
|
||||
quotation.shipping_address = address_display
|
||||
quotation.customer_address == quotation.customer_address or address_name
|
||||
quotation.customer_address = quotation.customer_address or address_name
|
||||
address_doc = next((doc for doc in get_shipping_addresses() if doc["name"] == address_name), None)
|
||||
apply_cart_settings(quotation=quotation)
|
||||
|
||||
|
@ -99,6 +99,7 @@
|
||||
"rounding_adjustment",
|
||||
"rounded_total",
|
||||
"in_words",
|
||||
"disable_rounded_total",
|
||||
"terms_section_break",
|
||||
"tc_name",
|
||||
"terms",
|
||||
@ -768,6 +769,7 @@
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounding Adjustment (Company Currency)",
|
||||
@ -777,6 +779,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "base_rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounded Total (Company Currency)",
|
||||
@ -819,6 +822,7 @@
|
||||
"width": "150px"
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "rounding_adjustment",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounding Adjustment",
|
||||
@ -829,6 +833,7 @@
|
||||
},
|
||||
{
|
||||
"bold": 1,
|
||||
"depends_on": "eval:!doc.disable_rounded_total",
|
||||
"fieldname": "rounded_total",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Rounded Total",
|
||||
@ -1271,13 +1276,20 @@
|
||||
"label": "Represents Company",
|
||||
"options": "Company",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "grand_total",
|
||||
"fieldname": "disable_rounded_total",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Rounded Total"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-truck",
|
||||
"idx": 146,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-26 17:07:59.194403",
|
||||
"modified": "2021-04-15 23:55:49.620641",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Delivery Note",
|
||||
|
@ -582,6 +582,7 @@ class TestPurchaseReceipt(unittest.TestCase):
|
||||
serial_no=serial_no, basic_rate=100, do_not_submit=True)
|
||||
se.submit()
|
||||
|
||||
se.cancel()
|
||||
dn.cancel()
|
||||
pr1.cancel()
|
||||
|
||||
|
@ -14,6 +14,7 @@ from frappe import _, ValidationError
|
||||
from erpnext.controllers.stock_controller import StockController
|
||||
from six import string_types
|
||||
from six.moves import map
|
||||
|
||||
class SerialNoCannotCreateDirectError(ValidationError): pass
|
||||
class SerialNoCannotCannotChangeError(ValidationError): pass
|
||||
class SerialNoNotRequiredError(ValidationError): pass
|
||||
@ -322,11 +323,35 @@ def validate_serial_no(sle, item_det):
|
||||
frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code),
|
||||
SerialNoRequiredError)
|
||||
elif serial_nos:
|
||||
# SLE is being cancelled and has serial nos
|
||||
for serial_no in serial_nos:
|
||||
sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse"], as_dict=1)
|
||||
if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse:
|
||||
frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}")
|
||||
.format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse))
|
||||
check_serial_no_validity_on_cancel(serial_no, sle)
|
||||
|
||||
def check_serial_no_validity_on_cancel(serial_no, sle):
|
||||
sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse", "company", "status"], as_dict=1)
|
||||
sr_link = frappe.utils.get_link_to_form("Serial No", serial_no)
|
||||
doc_link = frappe.utils.get_link_to_form(sle.voucher_type, sle.voucher_no)
|
||||
actual_qty = cint(sle.actual_qty)
|
||||
is_stock_reco = sle.voucher_type == "Stock Reconciliation"
|
||||
msg = None
|
||||
|
||||
if sr and (actual_qty < 0 or is_stock_reco) and sr.warehouse != sle.warehouse:
|
||||
# receipt(inward) is being cancelled
|
||||
msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the warehouse {3}").format(
|
||||
sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse))
|
||||
elif sr and actual_qty > 0 and not is_stock_reco:
|
||||
# delivery is being cancelled, check for warehouse.
|
||||
if sr.warehouse:
|
||||
# serial no is active in another warehouse/company.
|
||||
msg = _("Cannot cancel {0} {1} as Serial No {2} is active in warehouse {3}").format(
|
||||
sle.voucher_type, doc_link, sr_link, frappe.bold(sr.warehouse))
|
||||
elif sr.company != sle.company and sr.status == "Delivered":
|
||||
# serial no is inactive (allowed) or delivered from another company (block).
|
||||
msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the company {3}").format(
|
||||
sle.voucher_type, doc_link, sr_link, frappe.bold(sle.company))
|
||||
|
||||
if msg:
|
||||
frappe.throw(msg, title=_("Cannot cancel"))
|
||||
|
||||
def validate_material_transfer_entry(sle_doc):
|
||||
sle_doc.update({
|
||||
|
@ -40,16 +40,139 @@ class TestSerialNo(unittest.TestCase):
|
||||
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
|
||||
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
|
||||
|
||||
create_delivery_note(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0])
|
||||
dn = create_delivery_note(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0])
|
||||
|
||||
serial_no = frappe.get_doc("Serial No", serial_nos[0])
|
||||
|
||||
# check Serial No details after delivery
|
||||
self.assertEqual(serial_no.status, "Delivered")
|
||||
self.assertEqual(serial_no.warehouse, None)
|
||||
self.assertEqual(serial_no.company, "_Test Company")
|
||||
self.assertEqual(serial_no.delivery_document_type, "Delivery Note")
|
||||
self.assertEqual(serial_no.delivery_document_no, dn.name)
|
||||
|
||||
wh = create_warehouse("_Test Warehouse", company="_Test Company 1")
|
||||
make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0],
|
||||
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1, serial_no=serial_nos[0],
|
||||
company="_Test Company 1", warehouse=wh)
|
||||
|
||||
serial_no = frappe.db.get_value("Serial No", serial_nos[0], ["warehouse", "company"], as_dict=1)
|
||||
serial_no.reload()
|
||||
|
||||
# check Serial No details after purchase in second company
|
||||
self.assertEqual(serial_no.status, "Active")
|
||||
self.assertEqual(serial_no.warehouse, wh)
|
||||
self.assertEqual(serial_no.company, "_Test Company 1")
|
||||
self.assertEqual(serial_no.purchase_document_type, "Purchase Receipt")
|
||||
self.assertEqual(serial_no.purchase_document_no, pr.name)
|
||||
|
||||
def test_inter_company_transfer_intermediate_cancellation(self):
|
||||
"""
|
||||
Receive into and Deliver Serial No from one company.
|
||||
Then Receive into and Deliver from second company.
|
||||
Try to cancel intermediate receipts/deliveries to test if it is blocked.
|
||||
"""
|
||||
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
|
||||
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
|
||||
|
||||
sn_doc = frappe.get_doc("Serial No", serial_nos[0])
|
||||
|
||||
# check Serial No details after purchase in first company
|
||||
self.assertEqual(sn_doc.status, "Active")
|
||||
self.assertEqual(sn_doc.company, "_Test Company")
|
||||
self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC")
|
||||
self.assertEqual(sn_doc.purchase_document_no, se.name)
|
||||
|
||||
dn = create_delivery_note(item_code="_Test Serialized Item With Series",
|
||||
qty=1, serial_no=serial_nos[0])
|
||||
sn_doc.reload()
|
||||
# check Serial No details after delivery from **first** company
|
||||
self.assertEqual(sn_doc.status, "Delivered")
|
||||
self.assertEqual(sn_doc.company, "_Test Company")
|
||||
self.assertEqual(sn_doc.warehouse, None)
|
||||
self.assertEqual(sn_doc.delivery_document_no, dn.name)
|
||||
|
||||
# try cancelling the first Serial No Receipt, even though it is delivered
|
||||
# block cancellation is Serial No is out of the warehouse
|
||||
self.assertRaises(frappe.ValidationError, se.cancel)
|
||||
|
||||
# receive serial no in second company
|
||||
wh = create_warehouse("_Test Warehouse", company="_Test Company 1")
|
||||
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series",
|
||||
qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh)
|
||||
sn_doc.reload()
|
||||
|
||||
self.assertEqual(sn_doc.warehouse, wh)
|
||||
# try cancelling the delivery from the first company
|
||||
# block cancellation as Serial No belongs to different company
|
||||
self.assertRaises(frappe.ValidationError, dn.cancel)
|
||||
|
||||
# deliver from second company
|
||||
dn_2 = create_delivery_note(item_code="_Test Serialized Item With Series",
|
||||
qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh)
|
||||
sn_doc.reload()
|
||||
|
||||
# check Serial No details after delivery from **second** company
|
||||
self.assertEqual(sn_doc.status, "Delivered")
|
||||
self.assertEqual(sn_doc.company, "_Test Company 1")
|
||||
self.assertEqual(sn_doc.warehouse, None)
|
||||
self.assertEqual(sn_doc.delivery_document_no, dn_2.name)
|
||||
|
||||
# cannot cancel any intermediate document before last Delivery Note
|
||||
self.assertRaises(frappe.ValidationError, se.cancel)
|
||||
self.assertRaises(frappe.ValidationError, dn.cancel)
|
||||
self.assertRaises(frappe.ValidationError, pr.cancel)
|
||||
|
||||
def test_inter_company_transfer_fallback_on_cancel(self):
|
||||
"""
|
||||
Test Serial No state changes on cancellation.
|
||||
If Delivery cancelled, it should fall back on last Receipt in the same company.
|
||||
If Receipt is cancelled, it should be Inactive in the same company.
|
||||
"""
|
||||
# Receipt in **first** company
|
||||
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
|
||||
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
|
||||
sn_doc = frappe.get_doc("Serial No", serial_nos[0])
|
||||
|
||||
# Delivery from first company
|
||||
dn = create_delivery_note(item_code="_Test Serialized Item With Series",
|
||||
qty=1, serial_no=serial_nos[0])
|
||||
|
||||
# Receipt in **second** company
|
||||
wh = create_warehouse("_Test Warehouse", company="_Test Company 1")
|
||||
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series",
|
||||
qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh)
|
||||
|
||||
# Delivery from second company
|
||||
dn_2 = create_delivery_note(item_code="_Test Serialized Item With Series",
|
||||
qty=1, serial_no=serial_nos[0], company="_Test Company 1", warehouse=wh)
|
||||
sn_doc.reload()
|
||||
|
||||
self.assertEqual(sn_doc.status, "Delivered")
|
||||
self.assertEqual(sn_doc.company, "_Test Company 1")
|
||||
self.assertEqual(sn_doc.delivery_document_no, dn_2.name)
|
||||
|
||||
dn_2.cancel()
|
||||
sn_doc.reload()
|
||||
# Fallback on Purchase Receipt if Delivery is cancelled
|
||||
self.assertEqual(sn_doc.status, "Active")
|
||||
self.assertEqual(sn_doc.company, "_Test Company 1")
|
||||
self.assertEqual(sn_doc.warehouse, wh)
|
||||
self.assertEqual(sn_doc.purchase_document_no, pr.name)
|
||||
|
||||
pr.cancel()
|
||||
sn_doc.reload()
|
||||
# Inactive in same company if Receipt cancelled
|
||||
self.assertEqual(sn_doc.status, "Inactive")
|
||||
self.assertEqual(sn_doc.company, "_Test Company 1")
|
||||
self.assertEqual(sn_doc.warehouse, None)
|
||||
|
||||
dn.cancel()
|
||||
sn_doc.reload()
|
||||
# Fallback on Purchase Receipt in FIRST company if
|
||||
# Delivery from FIRST company is cancelled
|
||||
self.assertEqual(sn_doc.status, "Active")
|
||||
self.assertEqual(sn_doc.company, "_Test Company")
|
||||
self.assertEqual(sn_doc.warehouse, "_Test Warehouse - _TC")
|
||||
self.assertEqual(sn_doc.purchase_document_no, se.name)
|
||||
|
||||
def tearDown(self):
|
||||
frappe.db.rollback()
|
@ -363,43 +363,6 @@ frappe.ui.form.on('Shipment', {
|
||||
if (frm.doc.pickup_date < frappe.datetime.get_today()) {
|
||||
frappe.throw(__("Pickup Date cannot be before this day"));
|
||||
}
|
||||
if (frm.doc.pickup_date == frappe.datetime.get_today()) {
|
||||
var pickup_time = frm.events.get_pickup_time(frm);
|
||||
frm.set_value("pickup_from", pickup_time);
|
||||
frm.trigger('set_pickup_to_time');
|
||||
}
|
||||
},
|
||||
pickup_from: function(frm) {
|
||||
var pickup_time = frm.events.get_pickup_time(frm);
|
||||
if (frm.doc.pickup_from && frm.doc.pickup_date == frappe.datetime.get_today()) {
|
||||
let current_hour = pickup_time.split(':')[0];
|
||||
let current_min = pickup_time.split(':')[1];
|
||||
let pickup_hour = frm.doc.pickup_from.split(':')[0];
|
||||
let pickup_min = frm.doc.pickup_from.split(':')[1];
|
||||
if (pickup_hour < current_hour || (pickup_hour == current_hour && pickup_min < current_min)) {
|
||||
frm.set_value("pickup_from", pickup_time);
|
||||
frappe.throw(__("Pickup Time cannot be in the past"));
|
||||
}
|
||||
}
|
||||
frm.trigger('set_pickup_to_time');
|
||||
},
|
||||
get_pickup_time: function() {
|
||||
let current_hour = new Date().getHours();
|
||||
let current_min = new Date().toLocaleString('en-US', {minute: 'numeric'});
|
||||
if (current_min < 30) {
|
||||
current_min = '30';
|
||||
} else {
|
||||
current_min = '00';
|
||||
current_hour = Number(current_hour)+1;
|
||||
}
|
||||
let pickup_time = current_hour +':'+ current_min;
|
||||
return pickup_time;
|
||||
},
|
||||
set_pickup_to_time: function(frm) {
|
||||
let pickup_to_hour = Number(frm.doc.pickup_from.split(':')[0])+5;
|
||||
let pickup_to_min = frm.doc.pickup_from.split(':')[1];
|
||||
let pickup_to = pickup_to_hour +':'+ pickup_to_min;
|
||||
frm.set_value("pickup_to", pickup_to);
|
||||
},
|
||||
clear_pickup_fields: function(frm) {
|
||||
let fields = ["pickup_address_name", "pickup_contact_name", "pickup_address", "pickup_contact", "pickup_contact_email", "pickup_contact_person"];
|
||||
|
@ -275,14 +275,16 @@
|
||||
"default": "09:00",
|
||||
"fieldname": "pickup_from",
|
||||
"fieldtype": "Time",
|
||||
"label": "Pickup from"
|
||||
"label": "Pickup from",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_on_submit": 1,
|
||||
"default": "17:00",
|
||||
"fieldname": "pickup_to",
|
||||
"fieldtype": "Time",
|
||||
"label": "Pickup to"
|
||||
"label": "Pickup to",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_36",
|
||||
@ -431,7 +433,7 @@
|
||||
],
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-12-25 15:02:34.891976",
|
||||
"modified": "2021-04-13 17:14:18.181818",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Shipment",
|
||||
@ -469,4 +471,4 @@
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
||||
}
|
||||
|
@ -23,10 +23,10 @@ class Shipment(Document):
|
||||
frappe.throw(_('Please enter Shipment Parcel information'))
|
||||
if self.value_of_goods == 0:
|
||||
frappe.throw(_('Value of goods cannot be 0'))
|
||||
self.status = 'Submitted'
|
||||
self.db_set('status', 'Submitted')
|
||||
|
||||
def on_cancel(self):
|
||||
self.status = 'Cancelled'
|
||||
self.db_set('status', 'Cancelled')
|
||||
|
||||
def validate_weight(self):
|
||||
for parcel in self.shipment_parcel:
|
||||
|
@ -398,7 +398,7 @@ class StockReconciliation(StockController):
|
||||
merge_similar_entries = {}
|
||||
|
||||
for d in sl_entries:
|
||||
if not d.serial_no or d.actual_qty < 0:
|
||||
if not d.serial_no or flt(d.get("actual_qty")) < 0:
|
||||
new_sl_entries.append(d)
|
||||
continue
|
||||
|
||||
|
@ -32,7 +32,7 @@ class TestStockReconciliation(unittest.TestCase):
|
||||
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
|
||||
# [[qty, valuation_rate, posting_date,
|
||||
# posting_time, expected_stock_value, bin_qty, bin_valuation]]
|
||||
|
||||
|
||||
input_data = [
|
||||
[50, 1000, "2012-12-26", "12:00"],
|
||||
[25, 900, "2012-12-26", "12:00"],
|
||||
@ -86,7 +86,7 @@ class TestStockReconciliation(unittest.TestCase):
|
||||
se1.cancel()
|
||||
|
||||
def test_get_items(self):
|
||||
create_warehouse("_Test Warehouse Group 1",
|
||||
create_warehouse("_Test Warehouse Group 1",
|
||||
{"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"})
|
||||
create_warehouse("_Test Warehouse Ledger 1",
|
||||
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC", "company": "_Test Company"})
|
||||
|
@ -13,6 +13,7 @@
|
||||
"column_break_4",
|
||||
"valuation_method",
|
||||
"over_delivery_receipt_allowance",
|
||||
"role_allowed_to_over_deliver_receive",
|
||||
"action_if_quality_inspection_is_not_submitted",
|
||||
"show_barcode_field",
|
||||
"clean_description_html",
|
||||
@ -234,6 +235,13 @@
|
||||
"fieldname": "disable_serial_no_and_batch_selector",
|
||||
"fieldtype": "Check",
|
||||
"label": "Disable Serial No And Batch Selector"
|
||||
},
|
||||
{
|
||||
"description": "Users with this role are allowed to over deliver/receive against orders above the allowance percentage",
|
||||
"fieldname": "role_allowed_to_over_deliver_receive",
|
||||
"fieldtype": "Link",
|
||||
"label": "Role Allowed to Over Deliver/Receive",
|
||||
"options": "Role"
|
||||
}
|
||||
],
|
||||
"icon": "icon-cog",
|
||||
@ -241,7 +249,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-18 13:15:38.352796",
|
||||
"modified": "2021-03-11 18:48:14.513055",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Stock Settings",
|
||||
|
@ -309,8 +309,6 @@ def get_basic_details(args, item, overwrite_warehouse=True):
|
||||
"update_stock": args.get("update_stock") if args.get('doctype') in ['Sales Invoice', 'Purchase Invoice'] else 0,
|
||||
"delivered_by_supplier": item.delivered_by_supplier if args.get("doctype") in ["Sales Order", "Sales Invoice"] else 0,
|
||||
"is_fixed_asset": item.is_fixed_asset,
|
||||
"weight_per_unit":item.weight_per_unit,
|
||||
"weight_uom":item.weight_uom,
|
||||
"last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0,
|
||||
"transaction_date": args.get("transaction_date"),
|
||||
"against_blanket_order": args.get("against_blanket_order"),
|
||||
@ -611,8 +609,12 @@ def get_price_list_rate(args, item_doc, out):
|
||||
meta = frappe.get_meta(args.parenttype or args.doctype)
|
||||
|
||||
if meta.get_field("currency") or args.get('currency'):
|
||||
pl_details = get_price_list_currency_and_exchange_rate(args)
|
||||
args.update(pl_details)
|
||||
if not args.get("price_list_currency") or not args.get("plc_conversion_rate"):
|
||||
# if currency and plc_conversion_rate exist then
|
||||
# `get_price_list_currency_and_exchange_rate` has already been called
|
||||
pl_details = get_price_list_currency_and_exchange_rate(args)
|
||||
args.update(pl_details)
|
||||
|
||||
if meta.get_field("currency"):
|
||||
validate_conversion_rate(args, meta)
|
||||
|
||||
@ -922,10 +924,19 @@ def get_projected_qty(item_code, warehouse):
|
||||
{"item_code": item_code, "warehouse": warehouse}, "projected_qty")}
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_bin_details(item_code, warehouse):
|
||||
return frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse},
|
||||
def get_bin_details(item_code, warehouse, company=None):
|
||||
bin_details = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse},
|
||||
["projected_qty", "actual_qty", "reserved_qty"], as_dict=True, cache=True) \
|
||||
or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
|
||||
if company:
|
||||
bin_details['company_total_stock'] = get_company_total_stock(item_code, company)
|
||||
return bin_details
|
||||
|
||||
def get_company_total_stock(item_code, company):
|
||||
return frappe.db.sql("""SELECT sum(actual_qty) from
|
||||
(`tabBin` INNER JOIN `tabWarehouse` ON `tabBin`.warehouse = `tabWarehouse`.name)
|
||||
WHERE `tabWarehouse`.company = '{0}' and `tabBin`.item_code = '{1}'"""
|
||||
.format(company, item_code))[0][0]
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_serial_no_details(item_code, warehouse, stock_qty, serial_no):
|
||||
@ -993,6 +1004,8 @@ def apply_price_list(args, as_doc=False):
|
||||
args = process_args(args)
|
||||
|
||||
parent = get_price_list_currency_and_exchange_rate(args)
|
||||
args.update(parent)
|
||||
|
||||
children = []
|
||||
|
||||
if "items" in args:
|
||||
@ -1057,7 +1070,7 @@ def get_price_list_currency_and_exchange_rate(args):
|
||||
return frappe._dict({
|
||||
"price_list_currency": price_list_currency,
|
||||
"price_list_uom_dependant": price_list_uom_dependant,
|
||||
"plc_conversion_rate": plc_conversion_rate
|
||||
"plc_conversion_rate": plc_conversion_rate or 1
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -55,19 +55,31 @@ def get_item_info(filters):
|
||||
|
||||
|
||||
def get_consumed_items(condition):
|
||||
purpose_to_exclude = [
|
||||
"Material Transfer for Manufacture",
|
||||
"Material Transfer",
|
||||
"Send to Subcontractor"
|
||||
]
|
||||
|
||||
condition += """
|
||||
and (
|
||||
purpose is NULL
|
||||
or purpose not in ({})
|
||||
)
|
||||
""".format(', '.join([f"'{p}'" for p in purpose_to_exclude]))
|
||||
condition = condition.replace("posting_date", "sle.posting_date")
|
||||
|
||||
consumed_items = frappe.db.sql("""
|
||||
select item_code, abs(sum(actual_qty)) as consumed_qty
|
||||
from `tabStock Ledger Entry`
|
||||
where actual_qty < 0
|
||||
from `tabStock Ledger Entry` as sle left join `tabStock Entry` as se
|
||||
on sle.voucher_no = se.name
|
||||
where
|
||||
actual_qty < 0
|
||||
and voucher_type not in ('Delivery Note', 'Sales Invoice')
|
||||
%s
|
||||
group by item_code
|
||||
""" % condition, as_dict=1)
|
||||
|
||||
consumed_items_map = {}
|
||||
for item in consumed_items:
|
||||
consumed_items_map.setdefault(item.item_code, item.consumed_qty)
|
||||
group by item_code""" % condition, as_dict=1)
|
||||
|
||||
consumed_items_map = {item.item_code : item.consumed_qty for item in consumed_items}
|
||||
return consumed_items_map
|
||||
|
||||
def get_delivered_items(condition):
|
||||
|
@ -372,7 +372,8 @@ class update_entries_after(object):
|
||||
elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"):
|
||||
if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
|
||||
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return # don't move this import to top
|
||||
rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code, voucher_detail_no=sle.voucher_detail_no)
|
||||
rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code,
|
||||
voucher_detail_no=sle.voucher_detail_no, sle = sle)
|
||||
else:
|
||||
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
|
||||
rate_field = "valuation_rate"
|
||||
@ -603,7 +604,7 @@ class update_entries_after(object):
|
||||
batch = self.wh_data.stock_queue[index]
|
||||
if qty_to_pop >= batch[0]:
|
||||
# consume current batch
|
||||
qty_to_pop = qty_to_pop - batch[0]
|
||||
qty_to_pop = _round_off_if_near_zero(qty_to_pop - batch[0])
|
||||
self.wh_data.stock_queue.pop(index)
|
||||
if not self.wh_data.stock_queue and qty_to_pop:
|
||||
# stock finished, qty still remains to be withdrawn
|
||||
@ -617,8 +618,8 @@ class update_entries_after(object):
|
||||
batch[0] = batch[0] - qty_to_pop
|
||||
qty_to_pop = 0
|
||||
|
||||
stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))
|
||||
stock_qty = sum((flt(batch[0]) for batch in self.wh_data.stock_queue))
|
||||
stock_value = _round_off_if_near_zero(sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue)))
|
||||
stock_qty = _round_off_if_near_zero(sum((flt(batch[0]) for batch in self.wh_data.stock_queue)))
|
||||
|
||||
if stock_qty:
|
||||
self.wh_data.valuation_rate = stock_value / flt(stock_qty)
|
||||
@ -857,3 +858,12 @@ def get_future_sle_with_negative_qty(args):
|
||||
order by timestamp(posting_date, posting_time) asc
|
||||
limit 1
|
||||
""", args, as_dict=1)
|
||||
|
||||
def _round_off_if_near_zero(number: float, precision: int = 6) -> float:
|
||||
""" Rounds off the number to zero only if number is close to zero for decimal
|
||||
specified in precision. Precision defaults to 6.
|
||||
"""
|
||||
if flt(number) < (1.0 / (10**precision)):
|
||||
return 0
|
||||
|
||||
return flt(number)
|
||||
|
@ -18,7 +18,6 @@ def get_level():
|
||||
"Delivery Note": 5,
|
||||
"Employee": 3,
|
||||
"Instructor": 5,
|
||||
"Instructor": 5,
|
||||
"Issue": 5,
|
||||
"Item": 5,
|
||||
"Journal Entry": 3,
|
||||
|
@ -62,7 +62,7 @@
|
||||
{{_('Back to Course')}}
|
||||
</a>
|
||||
</div>
|
||||
<div>
|
||||
<div class="lms-title">
|
||||
<h2>{{ content.name }} <span class="small text-muted">({{ position + 1 }}/{{length}})</span></h2>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
@ -169,14 +169,51 @@
|
||||
const next_url = '/lms/course?name={{ course }}&program={{ program }}'
|
||||
{% endif %}
|
||||
frappe.ready(() => {
|
||||
const quiz = new Quiz(document.getElementById('quiz-wrapper'), {
|
||||
name: '{{ content.name }}',
|
||||
course: '{{ course }}',
|
||||
program: '{{ program }}',
|
||||
quiz_exit_button: quiz_exit_button,
|
||||
next_url: next_url
|
||||
})
|
||||
window.quiz = quiz;
|
||||
{% if content.is_time_bound %}
|
||||
var duration = get_duration("{{content.duration}}")
|
||||
var d = frappe.msgprint({
|
||||
title: __('Important Notice'),
|
||||
indicator: "red",
|
||||
message: __(`This is a Time-Bound Quiz. <br><br>
|
||||
A timer for <b>${duration}</b> will start, once you click on <b>Proceed</b>. <br><br>
|
||||
If you fail to submit before the time is up, the Quiz will be submitted automatically.`),
|
||||
primary_action: {
|
||||
label: __("Proceed"),
|
||||
action: () => {
|
||||
create_quiz();
|
||||
d.hide();
|
||||
}
|
||||
},
|
||||
secondary_action: {
|
||||
action: () => {
|
||||
d.hide();
|
||||
window.location.href = "/lms/course?name={{ course }}&program={{ program }}";
|
||||
},
|
||||
label: __("Go Back"),
|
||||
}
|
||||
});
|
||||
{% else %}
|
||||
create_quiz();
|
||||
{% endif %}
|
||||
function create_quiz() {
|
||||
const quiz = new Quiz(document.getElementById('quiz-wrapper'), {
|
||||
name: '{{ content.name }}',
|
||||
course: '{{ course }}',
|
||||
program: '{{ program }}',
|
||||
quiz_exit_button: quiz_exit_button,
|
||||
next_url: next_url
|
||||
})
|
||||
window.quiz = quiz;
|
||||
}
|
||||
function get_duration(seconds){
|
||||
var hours = append_zero(Math.floor(seconds / 3600));
|
||||
var minutes = append_zero(Math.floor(seconds % 3600 / 60));
|
||||
var seconds = append_zero(Math.floor(seconds % 3600 % 60));
|
||||
return `${hours}:${minutes}:${seconds}`;
|
||||
}
|
||||
function append_zero(time) {
|
||||
return time > 9 ? time : "0" + time;
|
||||
}
|
||||
})
|
||||
{% endif %}
|
||||
|
||||
|
@ -42,7 +42,9 @@
|
||||
<section class="top-section" style="padding: 6rem 0rem;">
|
||||
<div class='container pb-5'>
|
||||
<h1>{{ education_settings.portal_title }}</h1>
|
||||
<p class='lead'>{{ education_settings.description }}</p>
|
||||
{% if education_settings.description %}
|
||||
<p class='lead'>{{ education_settings.description }}</p>
|
||||
{% endif %}
|
||||
<p class="mt-4">
|
||||
{% if frappe.session.user == 'Guest' %}
|
||||
<a class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
|
||||
@ -51,13 +53,15 @@
|
||||
</div>
|
||||
<div class='container'>
|
||||
<div class="row mt-5">
|
||||
{% for program in featured_programs %}
|
||||
{{ program_card(program.program, program.has_access) }}
|
||||
{% endfor %}
|
||||
{% if featured_programs %}
|
||||
{% for program in featured_programs %}
|
||||
{{ program_card(program.program, program.has_access) }}
|
||||
{% endfor %}
|
||||
{% for n in range( (3 - (featured_programs|length)) %3) %}
|
||||
{{ null_card() }}
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<p class="lead">You have not enrolled in any program. Contact your Instructor.</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
@ -35,7 +35,7 @@ def get_contents(topic, course, program):
|
||||
progress.append({'content': content, 'content_type': content.doctype, 'completed': status})
|
||||
elif content.doctype == 'Quiz':
|
||||
if student:
|
||||
status, score, result = utils.check_quiz_completion(content, course_enrollment.name)
|
||||
status, score, result, time_taken = utils.check_quiz_completion(content, course_enrollment.name)
|
||||
else:
|
||||
status = False
|
||||
score = None
|
||||
|
Loading…
Reference in New Issue
Block a user