Merge branch 'develop' into purchase-invoice-to-purchase-receipt-develop

This commit is contained in:
Nabin Hait 2021-04-19 13:30:19 +05:30 committed by GitHub
commit 840c921229
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
94 changed files with 2171 additions and 837 deletions

38
.github/helper/semgrep_rules/README.md vendored Normal file
View 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

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

View 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

View File

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

View 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

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

View 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
_('')

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

View File

@ -5,7 +5,7 @@ import frappe
from erpnext.hooks import regional_overrides from erpnext.hooks import regional_overrides
from frappe.utils import getdate from frappe.utils import getdate
__version__ = '13.0.1' __version__ = '13.0.0-dev'
def get_default_company(user=None): def get_default_company(user=None):
'''Get default company for user''' '''Get default company for user'''

View File

@ -12,6 +12,7 @@
"frozen_accounts_modifier", "frozen_accounts_modifier",
"determine_address_tax_category_from", "determine_address_tax_category_from",
"over_billing_allowance", "over_billing_allowance",
"role_allowed_to_over_bill",
"column_break_4", "column_break_4",
"credit_controller", "credit_controller",
"check_supplier_invoice_uniqueness", "check_supplier_invoice_uniqueness",
@ -226,6 +227,13 @@
"fieldname": "delete_linked_ledger_entries", "fieldname": "delete_linked_ledger_entries",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction" "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", "icon": "icon-cog",
@ -233,7 +241,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-01-05 13:04:00.118892", "modified": "2021-03-11 18:52:05.601996",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Accounts Settings", "name": "Accounts Settings",

View File

@ -175,22 +175,24 @@
}, },
{ {
"fieldname": "deposit", "fieldname": "deposit",
"oldfieldname": "debit",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Deposit" "label": "Deposit",
"oldfieldname": "debit",
"options": "currency"
}, },
{ {
"fieldname": "withdrawal", "fieldname": "withdrawal",
"oldfieldname": "credit",
"fieldtype": "Currency", "fieldtype": "Currency",
"in_list_view": 1, "in_list_view": 1,
"label": "Withdrawal" "label": "Withdrawal",
"oldfieldname": "credit",
"options": "currency"
} }
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-30 19:40:54.221070", "modified": "2021-04-14 17:31:58.963529",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Bank Transaction", "name": "Bank Transaction",

View File

@ -38,22 +38,22 @@
{% endif %} {% endif %}
</td> </td>
<td style="text-align: right"> <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"> <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 %} {% else %}
<td></td> <td></td>
<td></td> <td></td>
<td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</b></td> <td><b>{{ frappe.format(row.account, {fieldtype: "Link"}) or "&nbsp;" }}</b></td>
<td style="text-align: right"> <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>
<td style="text-align: right"> <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> </td>
{% endif %} {% endif %}
<td style="text-align: right"> <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> </td>
</tr> </tr>
{% endfor %} {% endfor %}

View File

@ -9,7 +9,7 @@ from frappe.utils import cstr
from frappe.model.naming import make_autoname from frappe.model.naming import make_autoname
from frappe.model.document import Document 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', 'apply_rule_on_other', 'other_brand', 'selling', 'buying', 'applicable_for', 'valid_from',
'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier', 'valid_upto', 'customer', 'customer_group', 'territory', 'sales_partner', 'campaign', 'supplier',
'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules'] 'supplier_group', 'company', 'currency', 'apply_multiple_pricing_rules']

View File

@ -118,6 +118,7 @@
"in_words", "in_words",
"total_advance", "total_advance",
"outstanding_amount", "outstanding_amount",
"disable_rounded_total",
"advances_section", "advances_section",
"allocate_advances_automatically", "allocate_advances_automatically",
"get_advances", "get_advances",
@ -1109,6 +1110,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment", "fieldname": "base_rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -1120,6 +1122,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounded_total", "fieldname": "base_rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -1168,6 +1171,7 @@
"reqd": 1 "reqd": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment", "fieldname": "rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -1180,6 +1184,7 @@
}, },
{ {
"bold": 1, "bold": 1,
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounded_total", "fieldname": "rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -1945,6 +1950,13 @@
"fieldtype": "Link", "fieldtype": "Link",
"label": "Set Target Warehouse", "label": "Set Target Warehouse",
"options": "Warehouse" "options": "Warehouse"
},
{
"default": "0",
"depends_on": "grand_total",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
@ -1957,7 +1969,7 @@
"link_fieldname": "consolidated_invoice" "link_fieldname": "consolidated_invoice"
} }
], ],
"modified": "2021-03-31 15:42:26.261540", "modified": "2021-04-15 23:57:58.766651",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Accounts", "module": "Accounts",
"name": "Sales Invoice", "name": "Sales Invoice",

View File

@ -46,7 +46,6 @@ class SalesInvoice(SellingController):
'target_parent_dt': 'Sales Order', 'target_parent_dt': 'Sales Order',
'target_parent_field': 'per_billed', 'target_parent_field': 'per_billed',
'source_field': 'amount', 'source_field': 'amount',
'join_field': 'so_detail',
'percent_join_field': 'sales_order', 'percent_join_field': 'sales_order',
'status_field': 'billing_status', 'status_field': 'billing_status',
'keyword': 'Billed', 'keyword': 'Billed',
@ -276,7 +275,7 @@ class SalesInvoice(SellingController):
pluck="pos_closing_entry" pluck="pos_closing_entry"
) )
if 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"), frappe.bold("Consolidated Sales Invoice"),
get_link_to_form("POS Closing Entry", pos_closing_entry[0]) 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")) frappe.throw(_("Debit To is required"), title=_("Account Missing"))
if account.report_type != "Balance Sheet": 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.") msg += _("You can change the parent account to a Balance Sheet account or select a different account.")
frappe.throw(msg, title=_("Invalid Account")) frappe.throw(msg, title=_("Invalid Account"))
if self.customer and account.account_type != "Receivable": 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.") msg += _("Change the account type to Receivable or select a different account.")
frappe.throw(msg, title=_("Invalid Account")) frappe.throw(msg, title=_("Invalid Account"))

View File

@ -1879,7 +1879,17 @@ class TestSalesInvoice(unittest.TestCase):
def test_einvoice_submission_without_irn(self): def test_einvoice_submission_without_irn(self):
# init # 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 country = frappe.flags.country
frappe.flags.country = 'India' frappe.flags.country = 'India'
@ -1890,7 +1900,8 @@ class TestSalesInvoice(unittest.TestCase):
si.submit() si.submit()
# reset # 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 frappe.flags.country = country
def test_einvoice_json(self): def test_einvoice_json(self):

View File

@ -251,7 +251,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
threshold = tax_details.get('threshold', 0) threshold = tax_details.get('threshold', 0)
cumulative_threshold = tax_details.get('cumulative_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( if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto, ldc.valid_from, ldc.valid_upto,
inv.posting_date, tax_deducted, inv.posting_date, tax_deducted,

View File

@ -87,50 +87,6 @@ class TestTaxWithholdingCategory(unittest.TestCase):
for d in invoices: for d in invoices:
d.cancel() 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): def test_cumulative_threshold_tcs(self):
frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS") frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
invoices = [] invoices = []

View File

@ -56,6 +56,8 @@
"base_net_amount", "base_net_amount",
"warehouse_and_reference", "warehouse_and_reference",
"warehouse", "warehouse",
"actual_qty",
"company_total_stock",
"material_request", "material_request",
"material_request_item", "material_request_item",
"sales_order", "sales_order",
@ -743,6 +745,22 @@
"options": "currency", "options": "currency",
"read_only": 1 "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, "collapsible": 1,
"fieldname": "discount_and_margin_section", "fieldname": "discount_and_margin_section",
@ -791,7 +809,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"istable": 1, "istable": 1,
"links": [], "links": [],
"modified": "2021-02-23 01:00:27.132705", "modified": "2021-03-22 11:46:12.357435",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Buying", "module": "Buying",
"name": "Purchase Order Item", "name": "Purchase Order Item",

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

View File

@ -717,7 +717,9 @@ class AccountsController(TransactionBase):
total_billed_amt = abs(total_billed_amt) total_billed_amt = abs(total_billed_amt)
max_allowed_amt = abs(max_allowed_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") 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)) .format(item.item_code, item.idx, max_allowed_amt))

View File

@ -6,6 +6,7 @@ import frappe
from frappe import _, msgprint from frappe import _, msgprint
from frappe.utils import flt,cint, cstr, getdate from frappe.utils import flt,cint, cstr, getdate
from six import iteritems from six import iteritems
from collections import OrderedDict
from erpnext.accounts.party import get_party_details from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate 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, 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) qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
for batch_data in batches_qty: for batch_data in batches_qty:
qty = batch_data['qty'] qty = batch_data['qty']
raw_material.batch_no = batch_data['batch'] 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: else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty) 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: for batch_data in transferred_batches:
key = ((batch_data.item_code, fg_item) key = ((batch_data.item_code, fg_item)
if batch_data.subcontracted_item else (batch_data.item_code, purchase_order)) 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 transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
return transferred_batch_qty_map 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: if available_qty >= required_qty:
available_batches.append({'batch': batch, 'qty': required_qty}) available_batches.append({'batch': batch, 'qty': required_qty})
break break
else: elif available_qty != 0:
available_batches.append({'batch': batch, 'qty': available_qty}) available_batches.append({'batch': batch, 'qty': available_qty})
required_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 return available_batches

View File

@ -713,7 +713,9 @@ def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
return [(d,) for d in set(taxes)] 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) meta = frappe.get_meta(doctype)
fields.extend(meta.get_search_fields()) fields.extend(meta.get_search_fields())

View File

@ -5,6 +5,7 @@ from __future__ import unicode_literals
import frappe, erpnext import frappe, erpnext
from frappe import _ from frappe import _
from frappe.model.meta import get_field_precision 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 from frappe.utils import flt, get_datetime, format_datetime
class StockOverReturnError(frappe.ValidationError): pass class StockOverReturnError(frappe.ValidationError): pass
@ -389,10 +390,24 @@ def make_return_doc(doctype, source_name, target_doc=None):
return doclist 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: if not return_against:
return_against = frappe.get_cached_value(voucher_type, voucher_no, "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) return_against_item_field = get_return_against_item_fields(voucher_type)
filters = get_filters(voucher_type, voucher_no, voucher_detail_no, filters = get_filters(voucher_type, voucher_no, voucher_detail_no,

View File

@ -311,14 +311,16 @@ class SellingController(StockController):
items = self.get("items") + (self.get("packed_items") or []) items = self.get("items") + (self.get("packed_items") or [])
for d in items: 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 # 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({ d.incoming_rate = get_incoming_rate({
"item_code": d.item_code, "item_code": d.item_code,
"warehouse": d.warehouse, "warehouse": d.warehouse,
"posting_date": self.get('posting_date') or self.get('transaction_date'), "posting_date": self.get('posting_date') or self.get('transaction_date'),
"posting_time": self.get('posting_time') or nowtime(), "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'), "serial_no": d.get('serial_no'),
"company": self.company, "company": self.company,
"voucher_type": self.doctype, "voucher_type": self.doctype,

View File

@ -201,10 +201,14 @@ class StatusUpdater(Document):
get_allowance_for(item['item_code'], self.item_allowance, get_allowance_for(item['item_code'], self.item_allowance,
self.global_qty_allowance, self.global_amount_allowance, qty_or_amount) self.global_qty_allowance, self.global_amount_allowance, qty_or_amount)
overflow_percent = ((item[args['target_field']] - item[args['target_ref_field']]) / role_allowed_to_over_deliver_receive = frappe.db.get_single_value('Stock Settings', 'role_allowed_to_over_deliver_receive')
item[args['target_ref_field']]) * 100 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['max_allowed'] = flt(item[args['target_ref_field']] * (100+allowance)/100)
item['reduce_by'] = item[args['target_field']] - item['max_allowed'] 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.db_set("per_billed", per_billed)
ref_doc.set_status(update=True) 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 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 qty_or_amount == "qty":
if item_allowance.get(item_code, frappe._dict()).get("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 return item_allowance[item_code].qty, item_allowance, global_qty_allowance, global_amount_allowance

View File

@ -117,7 +117,6 @@ class StockController(AccountsController):
"account": expense_account, "account": expense_account,
"against": warehouse_account[sle.warehouse]["account"], "against": warehouse_account[sle.warehouse]["account"],
"cost_center": item_row.cost_center, "cost_center": item_row.cost_center,
"project": item_row.project or self.get('project'),
"remarks": self.get("remarks") or "Accounting Entry for Stock", "remarks": self.get("remarks") or "Accounting Entry for Stock",
"credit": flt(sle.stock_value_difference, precision), "credit": flt(sle.stock_value_difference, precision),
"project": item_row.get("project") or self.get("project"), "project": item_row.get("project") or self.get("project"),
@ -483,7 +482,7 @@ class StockController(AccountsController):
) )
message += "<br><br>" message += "<br><br>"
rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule) 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 return message
def repost_future_sle_and_gle(self): def repost_future_sle_and_gle(self):

View File

@ -41,7 +41,7 @@ class CourseEnrollment(Document):
frappe.throw(_("Student is already enrolled via Course Enrollment {0}").format( frappe.throw(_("Student is already enrolled via Course Enrollment {0}").format(
get_link_to_form("Course Enrollment", enrollment)), title=_('Duplicate Entry')) 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 = {k: ('Correct' if v else 'Wrong') for k,v in answers.items()}
result_data = [] result_data = []
for key in answers: for key in answers:
@ -66,7 +66,8 @@ class CourseEnrollment(Document):
"activity_date": frappe.utils.datetime.datetime.now(), "activity_date": frappe.utils.datetime.datetime.now(),
"result": result_data, "result": result_data,
"score": score, "score": score,
"status": status "status": status,
"time_taken": time_taken
}).insert(ignore_permissions = True) }).insert(ignore_permissions = True)
def add_activity(self, content_type, content): def add_activity(self, content_type, content):

View File

@ -1,4 +1,5 @@
{ {
"actions": [],
"allow_import": 1, "allow_import": 1,
"allow_rename": 1, "allow_rename": 1,
"autoname": "field:title", "autoname": "field:title",
@ -12,7 +13,10 @@
"quiz_configuration_section", "quiz_configuration_section",
"passing_score", "passing_score",
"max_attempts", "max_attempts",
"grading_basis" "grading_basis",
"column_break_7",
"is_time_bound",
"duration"
], ],
"fields": [ "fields": [
{ {
@ -58,9 +62,26 @@
"fieldtype": "Select", "fieldtype": "Select",
"label": "Grading Basis", "label": "Grading Basis",
"options": "Latest Highest Score\nLatest Attempt" "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", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Quiz", "name": "Quiz",

View File

@ -1,490 +1,163 @@
{ {
"allow_copy": 0, "actions": [],
"allow_events_in_timeline": 0,
"allow_guest_to_view": 0,
"allow_import": 0,
"allow_rename": 0,
"autoname": "format:EDU-QA-{YYYY}-{#####}", "autoname": "format:EDU-QA-{YYYY}-{#####}",
"beta": 1, "beta": 1,
"creation": "2018-10-15 15:48:40.482821", "creation": "2018-10-15 15:48:40.482821",
"custom": 0,
"docstatus": 0,
"doctype": "DocType", "doctype": "DocType",
"document_type": "",
"editable_grid": 1, "editable_grid": 1,
"engine": "InnoDB", "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": [ "fields": [
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "enrollment", "fieldname": "enrollment",
"fieldtype": "Link", "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", "label": "Enrollment",
"length": 0,
"no_copy": 0,
"options": "Course Enrollment", "options": "Course Enrollment",
"permlevel": 0, "set_only_once": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "enrollment.student", "fetch_from": "enrollment.student",
"fieldname": "student", "fieldname": "student",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Student", "label": "Student",
"length": 0,
"no_copy": 0,
"options": "Student", "options": "Student",
"permlevel": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_3", "fieldname": "column_break_3",
"fieldtype": "Column Break", "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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fetch_from": "enrollment.course", "fetch_from": "enrollment.course",
"fieldname": "course", "fieldname": "course",
"fieldtype": "Link", "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", "label": "Course",
"length": 0,
"no_copy": 0,
"options": "Course", "options": "Course",
"permlevel": 0,
"precision": "",
"print_hide": 0,
"print_hide_if_no_value": 0,
"read_only": 1, "read_only": 1,
"remember_last_selected_value": 0, "set_only_once": 1
"report_hide": 0,
"reqd": 0,
"search_index": 0,
"set_only_once": 1,
"translatable": 0,
"unique": 0
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_5", "fieldname": "section_break_5",
"fieldtype": "Section Break", "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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "quiz", "fieldname": "quiz",
"fieldtype": "Link", "fieldtype": "Link",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Quiz", "label": "Quiz",
"length": 0,
"no_copy": 0,
"options": "Quiz", "options": "Quiz",
"permlevel": 0, "set_only_once": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "column_break_7", "fieldname": "column_break_7",
"fieldtype": "Column Break", "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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "status", "fieldname": "status",
"fieldtype": "Select", "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", "label": "Status",
"length": 0,
"no_copy": 0,
"options": "\nPass\nFail", "options": "\nPass\nFail",
"permlevel": 0, "read_only": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "section_break_9", "fieldname": "section_break_9",
"fieldtype": "Section Break", "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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "result", "fieldname": "result",
"fieldtype": "Table", "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", "label": "Result",
"length": 0,
"no_copy": 0,
"options": "Quiz Result", "options": "Quiz Result",
"permlevel": 0, "set_only_once": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "activity_date", "fieldname": "activity_date",
"fieldtype": "Data", "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", "label": "Activity Date",
"length": 0, "set_only_once": 1
"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
}, },
{ {
"allow_bulk_edit": 0,
"allow_in_quick_entry": 0,
"allow_on_submit": 0,
"bold": 0,
"collapsible": 0,
"columns": 0,
"fieldname": "score", "fieldname": "score",
"fieldtype": "Data", "fieldtype": "Data",
"hidden": 0,
"ignore_user_permissions": 0,
"ignore_xss_filter": 0,
"in_filter": 0,
"in_global_search": 0,
"in_list_view": 1, "in_list_view": 1,
"in_standard_filter": 0,
"label": "Score", "label": "Score",
"length": 0, "set_only_once": 1
"no_copy": 0, },
"permlevel": 0, {
"precision": "", "fieldname": "time_taken",
"print_hide": 0, "fieldtype": "Duration",
"print_hide_if_no_value": 0, "label": "Time Taken",
"read_only": 0, "set_only_once": 1
"remember_last_selected_value": 0, },
"report_hide": 0, {
"reqd": 0, "fieldname": "section_break_11",
"search_index": 0, "fieldtype": "Section Break"
"set_only_once": 1, },
"translatable": 0, {
"unique": 0 "fieldname": "column_break_14",
"fieldtype": "Column Break"
} }
], ],
"has_web_view": 0, "links": [],
"hide_heading": 0, "modified": "2020-12-24 15:41:20.085380",
"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",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Education", "module": "Education",
"name": "Quiz Activity", "name": "Quiz Activity",
"name_case": "",
"owner": "Administrator", "owner": "Administrator",
"permissions": [ "permissions": [
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Academics User", "role": "Academics User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 1, "create": 1,
"delete": 1, "delete": 1,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "LMS User", "role": "LMS User",
"set_user_permissions": 0,
"share": 1, "share": 1,
"submit": 0,
"write": 1 "write": 1
}, },
{ {
"amend": 0,
"cancel": 0,
"create": 0,
"delete": 0,
"email": 1, "email": 1,
"export": 1, "export": 1,
"if_owner": 0,
"import": 0,
"permlevel": 0,
"print": 1, "print": 1,
"read": 1, "read": 1,
"report": 1, "report": 1,
"role": "Instructor", "role": "Instructor",
"set_user_permissions": 0, "share": 1
"share": 1,
"submit": 0,
"write": 0
} }
], ],
"quick_entry": 1, "quick_entry": 1,
"read_only": 0,
"read_only_onload": 0,
"show_name_in_global_search": 0,
"sort_field": "modified", "sort_field": "modified",
"sort_order": "DESC", "sort_order": "DESC",
"track_changes": 1, "track_changes": 1
"track_seen": 0,
"track_views": 0
} }

View File

@ -114,7 +114,7 @@ class Student(Document):
status = check_content_completion(content.name, content.doctype, course_enrollment_name) status = check_content_completion(content.name, content.doctype, course_enrollment_name)
progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status}) progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status})
elif content.doctype == 'Quiz': 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}) progress.append({'content': content.name, 'content_type': content.doctype, 'is_complete': status, 'score': score, 'result': result})
return progress return progress

View File

@ -194,7 +194,7 @@ def add_activity(course, content_type, content, program):
return enrollment.add_activity(content_type, content) return enrollment.add_activity(content_type, content)
@frappe.whitelist() @frappe.whitelist()
def evaluate_quiz(quiz_response, quiz_name, course, program): def evaluate_quiz(quiz_response, quiz_name, course, program, time_taken):
import json import json
student = get_current_student() student = get_current_student()
@ -209,7 +209,7 @@ def evaluate_quiz(quiz_response, quiz_name, course, program):
if student: if student:
enrollment = get_or_create_course_enrollment(course, program) enrollment = get_or_create_course_enrollment(course, program)
if quiz.allowed_attempt(enrollment, quiz_name): 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} return {'result': result, 'score': score, 'status': status}
else: else:
return None return None
@ -219,8 +219,9 @@ def get_quiz(quiz_name, course):
try: try:
quiz = frappe.get_doc("Quiz", quiz_name) quiz = frappe.get_doc("Quiz", quiz_name)
questions = quiz.get_questions() questions = quiz.get_questions()
duration = quiz.duration
except: 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 return None
questions = [{ questions = [{
@ -232,12 +233,20 @@ def get_quiz(quiz_name, course):
} for question in questions] } for question in questions]
if has_super_access(): if has_super_access():
return {'questions': questions, 'activity': None} return {
'questions': questions,
'activity': None,
'duration':duration
}
student = get_current_student() student = get_current_student()
course_enrollment = get_enrollment("course", course, student.name) course_enrollment = get_enrollment("course", course, student.name)
status, score, result = check_quiz_completion(quiz, course_enrollment) status, score, result, time_taken = check_quiz_completion(quiz, course_enrollment)
return {'questions': questions, 'activity': {'is_complete': status, 'score': score, 'result': result}} 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): def get_topic_progress(topic, course_name, program):
""" """
@ -361,15 +370,23 @@ def check_content_completion(content_name, content_type, enrollment_name):
return False return False
def check_quiz_completion(quiz, enrollment_name): 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) status = False if quiz.max_attempts == 0 else bool(len(attempts) >= quiz.max_attempts)
score = None score = None
result = None result = None
time_taken = None
if attempts: if attempts:
if quiz.grading_basis == 'Last Highest Score': if quiz.grading_basis == 'Last Highest Score':
attempts = sorted(attempts, key = lambda i: int(i.score), reverse=True) attempts = sorted(attempts, key = lambda i: int(i.score), reverse=True)
score = attempts[0]['score'] score = attempts[0]['score']
result = attempts[0]['status'] result = attempts[0]['status']
time_taken = attempts[0]['time_taken']
if result == 'Pass': if result == 'Pass':
status = True status = True
return status, score, result return status, score, result, time_taken

View File

@ -307,6 +307,8 @@ auto_cancel_exempted_doctypes= [
"Inpatient Medication Entry" "Inpatient Medication Entry"
] ]
after_migrate = ["erpnext.setup.install.update_select_perm_after_install"]
scheduler_events = { scheduler_events = {
"cron": { "cron": {
"0/30 * * * *": [ "0/30 * * * *": [

View File

@ -66,7 +66,7 @@ class CompensatoryLeaveRequest(Document):
else: else:
leave_allocation = self.create_leave_allocation(leave_period, date_difference) 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: 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))) 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)))

View File

@ -218,8 +218,7 @@
"fieldname": "leave_policy_assignment", "fieldname": "leave_policy_assignment",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Leave Policy Assignment", "label": "Leave Policy Assignment",
"options": "Leave Policy Assignment", "options": "Leave Policy Assignment"
"read_only": 1
}, },
{ {
"fetch_from": "employee.company", "fetch_from": "employee.company",
@ -236,7 +235,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-01-04 18:46:13.184104", "modified": "2021-04-14 15:28:26.335104",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "HR", "module": "HR",
"name": "Leave Allocation", "name": "Leave Allocation",

View File

@ -43,7 +43,6 @@ def get_charts():
return [{ return [{
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"based_on": "modified", "based_on": "modified",
"time_interval": "Yearly",
"chart_type": "Sum", "chart_type": "Sum",
"chart_name": _("Produced Quantity"), "chart_name": _("Produced Quantity"),
"name": "Produced Quantity", "name": "Produced Quantity",
@ -60,7 +59,6 @@ def get_charts():
}, { }, {
"doctype": "Dashboard Chart", "doctype": "Dashboard Chart",
"based_on": "creation", "based_on": "creation",
"time_interval": "Yearly",
"chart_type": "Sum", "chart_type": "Sum",
"chart_name": _("Completed Operation"), "chart_name": _("Completed Operation"),
"name": "Completed Operation", "name": "Completed Operation",

View File

@ -53,7 +53,9 @@ class BOMUpdateTool(Document):
rate=%s, amount=stock_qty*%s where bom_no = %s and docstatus < 2 and parenttype='BOM'""", 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)) (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` data = frappe.db.sql("""SELECT DISTINCT parent FROM `tabBOM Item`
WHERE bom_no = %s AND docstatus < 2 AND parenttype='BOM'""", bom) WHERE bom_no = %s AND docstatus < 2 AND parenttype='BOM'""", bom)

View File

@ -561,7 +561,6 @@ def get_material_request_items(row, sales_order, company,
'item_name': row.item_name, 'item_name': row.item_name,
'quantity': required_qty, 'quantity': required_qty,
'required_bom_qty': total_qty, 'required_bom_qty': total_qty,
'description': row.description,
'stock_uom': row.get("stock_uom"), 'stock_uom': row.get("stock_uom"),
'warehouse': warehouse or row.get('source_warehouse') \ 'warehouse': warehouse or row.get('source_warehouse') \
or row.get('default_warehouse') or item_group_defaults.get("default_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")) to_enable = frappe.bold(_("Ignore Existing Projected Quantity"))
warehouse = frappe.bold(doc.get('for_warehouse')) 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 = _("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")) frappe.msgprint(message, title=_("Note"))

View File

@ -693,7 +693,7 @@ execute:frappe.reload_doctype('Dashboard')
execute:frappe.reload_doc('desk', 'doctype', 'number_card_link') execute:frappe.reload_doc('desk', 'doctype', 'number_card_link')
execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts') execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts')
erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo 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 erpnext.patches.v12_0.update_bom_in_so_mr
execute:frappe.delete_doc("Report", "Department Analytics") execute:frappe.delete_doc("Report", "Department Analytics")
execute:frappe.rename_doc("Desk Page", "Loan Management", "Loan", force=True) 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.v12_0.purchase_receipt_status
erpnext.patches.v13_0.fix_non_unique_represents_company erpnext.patches.v13_0.fix_non_unique_represents_company
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing 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

View File

@ -12,5 +12,5 @@ def execute():
select dl.link_name from `tabAddress` a, `tabDynamic Link` dl select dl.link_name from `tabAddress` a, `tabDynamic Link` dl
where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company' where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company'
""", (creds.get('gstin'))) """, (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]) frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0])

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

View File

@ -3,7 +3,7 @@ import frappe
def execute(): def execute():
company = frappe.db.get_single_value('Global Defaults', 'default_company') 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: for entry in doctypes:
if frappe.db.exists('DocType', entry): if frappe.db.exists('DocType', entry):
frappe.reload_doc('Healthcare', 'doctype', entry) frappe.reload_doc('Healthcare', 'doctype', entry)

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

View File

@ -151,6 +151,10 @@ frappe.ui.form.on('Payroll Entry', {
filters['company'] = frm.doc.company; filters['company'] = frm.doc.company;
filters['start_date'] = frm.doc.start_date; filters['start_date'] = frm.doc.start_date;
filters['end_date'] = frm.doc.end_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) { if (frm.doc.department) {
filters['department'] = frm.doc.department; filters['department'] = frm.doc.department;

View File

@ -52,49 +52,32 @@ class PayrollEntry(Document):
Returns list of active employees based on selected criteria Returns list of active employees based on selected criteria
and for which salary structure exists and for which salary structure exists
""" """
cond = self.get_filter_condition() self.check_mandatory()
cond += self.get_joining_relieving_condition() filters = self.make_filters()
cond = get_filter_condition(filters)
cond += get_joining_relieving_condition(self.start_date, self.end_date)
condition = '' condition = ''
if self.payroll_frequency: if self.payroll_frequency:
condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": self.payroll_frequency} condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": self.payroll_frequency}
sal_struct = frappe.db.sql_list(""" sal_struct = get_sal_struct(self.company, self.currency, self.salary_slip_based_on_timesheet, condition)
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})
if sal_struct: if sal_struct:
cond += "and t2.salary_structure IN %(sal_struct)s " cond += "and t2.salary_structure IN %(sal_struct)s "
cond += "and t2.payroll_payable_account = %(payroll_payable_account)s " cond += "and t2.payroll_payable_account = %(payroll_payable_account)s "
cond += "and %(from_date)s >= t2.from_date" cond += "and %(from_date)s >= t2.from_date"
emp_list = frappe.db.sql(""" emp_list = get_emp_list(sal_struct, cond, self.end_date, self.payroll_payable_account)
select emp_list = remove_payrolled_employees(emp_list, self.start_date, self.end_date)
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)
return emp_list return emp_list
def remove_payrolled_employees(self, emp_list): def make_filters(self):
for employee_details in emp_list: filters = frappe._dict()
if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}): filters['company'] = self.company
emp_list.remove(employee_details) filters['branch'] = self.branch
filters['department'] = self.department
filters['designation'] = self.designation
return emp_list return filters
@frappe.whitelist() @frappe.whitelist()
def fill_employee_details(self): def fill_employee_details(self):
@ -122,23 +105,6 @@ class PayrollEntry(Document):
if self.validate_attendance: if self.validate_attendance:
return self.validate_employee_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): def check_mandatory(self):
for fieldname in ['company', 'start_date', 'end_date']: for fieldname in ['company', 'start_date', 'end_date']:
if not self.get(fieldname): if not self.get(fieldname):
@ -451,6 +417,53 @@ class PayrollEntry(Document):
marked_days = attendances[0][0] marked_days = attendances[0][0]
return marked_days 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() @frappe.whitelist()
def get_start_end_dates(payroll_frequency, start_date=None, company=None): 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''' '''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 'start': start, 'page_len': page_len
}) })
def get_employee_with_existing_salary_slip(start_date, end_date, company): def get_employee_list(filters):
return frappe.db.sql_list(""" cond = get_filter_condition(filters)
select employee from `tabSalary Slip` cond += get_joining_relieving_condition(filters.start_date, filters.end_date)
where condition = """and payroll_frequency = '%(payroll_frequency)s'"""% {"payroll_frequency": filters.payroll_frequency}
(start_date between %(start_date)s and %(end_date)s sal_struct = get_sal_struct(filters.company, filters.currency, filters.salary_slip_based_on_timesheet, condition)
or if sal_struct:
end_date between %(start_date)s and %(end_date)s cond += "and t2.salary_structure IN %(sal_struct)s "
or cond += "and t2.payroll_payable_account = %(payroll_payable_account)s "
%(start_date)s between start_date and end_date) cond += "and %(from_date)s >= t2.from_date"
and company = %(company)s emp_list = get_emp_list(sal_struct, cond, filters.end_date, filters.payroll_payable_account)
and docstatus = 1 emp_list = remove_payrolled_employees(emp_list, filters.start_date, filters.end_date)
""", {'start_date': start_date, 'end_date': end_date, 'company': company}) return emp_list
@frappe.whitelist() @frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs @frappe.validate_and_sanitize_search_inputs
def employee_query(doctype, txt, searchfield, start, page_len, filters): def employee_query(doctype, txt, searchfield, start, page_len, filters):
filters = frappe._dict(filters) filters = frappe._dict(filters)
conditions = [] conditions = []
exclude_employees = [] include_employees = []
emp_cond = '' emp_cond = ''
if filters.start_date and filters.end_date: 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') 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('start_date')
filters.pop('end_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: if filters.employees is not None:
filters.pop('employees') filters.pop('employees')
if employee_list:
exclude_employees.extend(employee_list) if include_employees:
if emp: emp_cond += 'and employee in %(include_employees)s'
exclude_employees.extend(emp)
if exclude_employees:
emp_cond += 'and employee not in %(exclude_employees)s'
return frappe.db.sql("""select name, employee_name from `tabEmployee` return frappe.db.sql("""select name, employee_name from `tabEmployee`
where status = 'Active' where status = 'Active'
@ -695,4 +710,4 @@ def employee_query(doctype, txt, searchfield, start, page_len, filters):
'_txt': txt.replace("%", ""), '_txt': txt.replace("%", ""),
'start': start, 'start': start,
'page_len': page_len, 'page_len': page_len,
'exclude_employees': exclude_employees}) 'include_employees': include_employees})

View File

@ -40,7 +40,9 @@ frappe.ui.form.on("Salary Slip", {
frm.set_query("employee", function() { frm.set_query("employee", function() {
return { return {
query: "erpnext.controllers.queries.employee_query", query: "erpnext.controllers.queries.employee_query",
filters: frm.doc.company filters: {
company: frm.doc.company
}
}; };
}); });
}, },

View File

@ -11,15 +11,16 @@
"project", "project",
"issue", "issue",
"type", "type",
"color",
"is_group", "is_group",
"is_template", "is_template",
"column_break0", "column_break0",
"status", "status",
"priority", "priority",
"task_weight", "task_weight",
"completed_by",
"color",
"parent_task", "parent_task",
"completed_by",
"completed_on",
"sb_timeline", "sb_timeline",
"exp_start_date", "exp_start_date",
"expected_time", "expected_time",
@ -358,6 +359,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval: doc.status == \"Completed\"",
"fieldname": "completed_by", "fieldname": "completed_by",
"fieldtype": "Link", "fieldtype": "Link",
"label": "Completed By", "label": "Completed By",
@ -381,6 +383,13 @@
"fieldname": "duration", "fieldname": "duration",
"fieldtype": "Int", "fieldtype": "Int",
"label": "Duration (Days)" "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", "icon": "fa fa-check",
@ -388,7 +397,7 @@
"is_tree": 1, "is_tree": 1,
"links": [], "links": [],
"max_attachments": 5, "max_attachments": 5,
"modified": "2020-12-28 11:32:58.714991", "modified": "2021-04-16 12:46:51.556741",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Task", "name": "Task",

View File

@ -36,6 +36,7 @@ class Task(NestedSet):
self.validate_status() self.validate_status()
self.update_depends_on() self.update_depends_on()
self.validate_dependencies_for_template_task() self.validate_dependencies_for_template_task()
self.validate_completed_on()
def validate_dates(self): 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): 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) 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)) 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): def update_depends_on(self):
depends_on_tasks = self.depends_on_tasks or "" depends_on_tasks = self.depends_on_tasks or ""
for d in self.depends_on: for d in self.depends_on:

View File

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

View File

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

View File

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

View File

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

View File

@ -15,6 +15,7 @@
"hide_custom": 0, "hide_custom": 0,
"icon": "project", "icon": "project",
"idx": 0, "idx": 0,
"is_default": 0,
"is_standard": 1, "is_standard": 1,
"label": "Projects", "label": "Projects",
"links": [ "links": [
@ -148,9 +149,19 @@
"link_type": "Report", "link_type": "Report",
"onboard": 0, "onboard": 0,
"type": "Link" "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", "modified_by": "Administrator",
"module": "Projects", "module": "Projects",
"name": "Projects", "name": "Projects",

View File

@ -216,7 +216,8 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
child: item, child: item,
args: { args: {
item_code: item.item_code, item_code: item.item_code,
warehouse: item.warehouse warehouse: item.warehouse,
company: doc.company
} }
}); });
} }

View File

@ -1103,6 +1103,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({
to_currency: to_currency, to_currency: to_currency,
args: args args: args
}, },
freeze: true,
freeze_message: __("Fetching exchange rates ..."),
callback: function(r) { callback: function(r) {
callback(flt(r.message)); callback(flt(r.message));
} }

View File

@ -20,6 +20,16 @@ class Quiz {
} }
make(data) { 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 => { data.questions.forEach(question_data => {
let question_wrapper = document.createElement('div'); let question_wrapper = document.createElement('div');
let question = new Question({ let question = new Question({
@ -37,12 +47,51 @@ class Quiz {
indicator = 'green' indicator = 'green'
message = 'You have already cleared the quiz.' 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) this.set_quiz_footer(message, indicator, data.activity.score)
} }
else { else {
this.make_actions(); 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() { make_actions() {
@ -57,6 +106,10 @@ class Quiz {
} }
submit() { submit() {
if (this.is_time_bound) {
clearInterval(this.timer);
$(".lms-timer").text("");
}
this.submit_btn.innerText = 'Evaluating..' this.submit_btn.innerText = 'Evaluating..'
this.submit_btn.disabled = true this.submit_btn.disabled = true
this.disable() this.disable()
@ -64,7 +117,8 @@ class Quiz {
quiz_name: this.name, quiz_name: this.name,
quiz_response: this.get_selected(), quiz_response: this.get_selected(),
course: this.course, course: this.course,
program: this.program program: this.program,
time_taken: this.is_time_bound ? this.time_taken : ""
}).then(res => { }).then(res => {
this.submit_btn.remove() this.submit_btn.remove()
if (!res.message) { if (!res.message) {
@ -157,7 +211,7 @@ class Question {
return input; return input;
} }
let make_label = function(name, value) { let make_label = function (name, value) {
let label = document.createElement('label'); let label = document.createElement('label');
label.classList.add('form-check-label'); label.classList.add('form-check-label');
label.htmlFor = name; label.htmlFor = name;
@ -166,14 +220,14 @@ class Question {
} }
let make_option = function (wrapper, option) { let make_option = function (wrapper, option) {
let option_div = document.createElement('div') let option_div = document.createElement('div');
option_div.classList.add('form-check', 'pb-1') option_div.classList.add('form-check', 'pb-1');
let input = make_input(option.name, option.option); let input = make_input(option.name, option.option);
let label = make_label(option.name, option.option); let label = make_label(option.name, option.option);
option_div.appendChild(input) option_div.appendChild(input);
option_div.appendChild(label) option_div.appendChild(label);
wrapper.appendChild(option_div) wrapper.appendChild(option_div);
return {input: input, ...option} return { input: input, ...option };
} }
let options_wrapper = document.createElement('div') let options_wrapper = document.createElement('div')

View File

@ -291,17 +291,15 @@ $.extend(erpnext.utils, {
return options[0]; return options[0];
} }
}, },
copy_parent_value_in_all_row: function(doc, dt, dn, table_fieldname, fieldname, parent_fieldname) { overrides_parent_value_in_all_rows: function(doc, dt, dn, table_fieldname, fieldname, parent_fieldname) {
var d = locals[dt][dn]; if (doc[parent_fieldname]) {
if(d[fieldname]){ let cl = doc[table_fieldname] || [];
var cl = doc[table_fieldname] || []; for (let i = 0; i < cl.length; i++) {
for(var i = 0; i < cl.length; i++) {
cl[i][fieldname] = doc[parent_fieldname]; cl[i][fieldname] = doc[parent_fieldname];
} }
frappe.refresh_field(table_fieldname);
} }
refresh_field(table_fieldname);
}, },
create_new_doc: function (doctype, update_fields) { create_new_doc: function (doctype, update_fields) {
frappe.model.with_doctype(doctype, function() { frappe.model.with_doctype(doctype, function() {
var new_doc = frappe.model.get_new_doc(doctype); var new_doc = frappe.model.get_new_doc(doctype);

View File

@ -353,9 +353,9 @@ erpnext.SerialNoBatchSelector = Class.extend({
return row.on_grid_fields_dict.batch_no.get_value(); return row.on_grid_fields_dict.batch_no.get_value();
} }
}); });
if (selected_batches.includes(val)) { if (selected_batches.includes(batch_no)) {
this.set_value(""); this.set_value("");
frappe.throw(__('Batch {0} already selected.', [val])); frappe.throw(__('Batch {0} already selected.', [batch_no]));
} }
if (me.warehouse_details.name) { if (me.warehouse_details.name) {

View File

@ -39,11 +39,12 @@ def validate_eligibility(doc):
if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from): if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from):
return False 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'] 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') company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
no_taxes_applied = not doc.get('taxes') 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 False
return True return True
@ -400,7 +401,7 @@ def validate_totals(einvoice):
if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1: 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.')) 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.')) 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 = \ calculated_invoice_value = \
@ -466,21 +467,24 @@ def make_einvoice(invoice):
try: try:
einvoice = safe_json_load(einvoice) einvoice = safe_json_load(einvoice)
einvoice = santize_einvoice_fields(einvoice) einvoice = santize_einvoice_fields(einvoice)
validate_totals(einvoice)
except Exception: except Exception:
log_error(einvoice) show_link_to_error_log(invoice, einvoice)
link_to_error_list = '<a href="List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
frappe.throw( validate_totals(einvoice)
_('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')
)
return 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): def log_error(data=None):
if not isinstance(data, dict): if isinstance(data, six.string_types):
data = json.loads(data) data = json.loads(data)
seperator = "--" * 50 seperator = "--" * 50
@ -587,7 +591,7 @@ class GSPConnector():
self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
def get_seller_gstin(self): 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: if not gstin:
frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.')) frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
return gstin return gstin

View File

@ -561,7 +561,7 @@ def get_json(filters, report_name, data):
fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) 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} "hash": "hash", "gstin": gstin, "fp": fp}
res = {} res = {}

View File

@ -4,11 +4,8 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from erpnext.regional.united_arab_emirates.setup import make_custom_fields, add_print_formats 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): def setup(company=None, patch=True):
make_custom_fields() make_custom_fields()
add_print_formats() add_print_formats()
if company:
create_sales_tax(company)

View File

@ -6,7 +6,6 @@ from __future__ import unicode_literals
import frappe, os, json import frappe, os, json
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.permissions import add_permission, update_permission_property 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 from erpnext.payroll.doctype.gratuity_rule.gratuity_rule import get_gratuity_rule
def setup(company=None, patch=True): def setup(company=None, patch=True):
@ -16,9 +15,6 @@ def setup(company=None, patch=True):
add_permissions() add_permissions()
create_gratuity_rule() create_gratuity_rule()
if company:
create_sales_tax(company)
def make_custom_fields(): def make_custom_fields():
is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated', is_zero_rated = dict(fieldname='is_zero_rated', label='Is Zero Rated',
fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description', fieldtype='Check', fetch_from='item_code.is_zero_rated', insert_after='description',

View File

@ -38,11 +38,19 @@ class Customer(TransactionBase):
set_name_by_naming_series(self) set_name_by_naming_series(self)
def get_customer_name(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 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] where name like %s""", "%{0} - %".format(self.customer_name), as_list=1)[0][0]
count = cint(count) + 1 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 return self.customer_name

View File

@ -98,6 +98,7 @@
"rounded_total", "rounded_total",
"in_words", "in_words",
"advance_paid", "advance_paid",
"disable_rounded_total",
"packing_list", "packing_list",
"packed_items", "packed_items",
"payment_schedule_section", "payment_schedule_section",
@ -901,6 +902,7 @@
"width": "150px" "width": "150px"
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment", "fieldname": "base_rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -912,6 +914,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounded_total", "fieldname": "base_rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -961,6 +964,7 @@
"width": "150px" "width": "150px"
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment", "fieldname": "rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -973,6 +977,7 @@
}, },
{ {
"bold": 1, "bold": 1,
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounded_total", "fieldname": "rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"hide_days": 1, "hide_days": 1,
@ -1474,13 +1479,20 @@
"label": "Represents Company", "label": "Represents Company",
"options": "Company", "options": "Company",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"depends_on": "grand_total",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
} }
], ],
"icon": "fa fa-file-text", "icon": "fa fa-file-text",
"idx": 105, "idx": 105,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2021-01-20 23:40:39.929296", "modified": "2021-04-15 23:55:13.439068",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Selling", "module": "Selling",
"name": "Sales Order", "name": "Sales Order",

View File

@ -159,6 +159,31 @@ erpnext.PointOfSale.ItemSelector = class {
bind_events() { bind_events() {
const me = this; const me = this;
window.onScan = onScan; 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.attachTo(document, {
onScan: (sScancode) => { onScan: (sScancode) => {
if (this.search_field && this.$component.is(':visible')) { if (this.search_field && this.$component.is(':visible')) {

View File

@ -204,11 +204,11 @@ erpnext.PointOfSale.PastOrderSummary = class {
print_receipt() { print_receipt() {
const frm = this.events.get_frm(); const frm = this.events.get_frm();
frappe.utils.print( frappe.utils.print(
frm.doctype, this.doc.doctype,
frm.docname, this.doc.name,
frm.pos_print_format, frm.pos_print_format,
frm.doc.letter_head, this.doc.letter_head,
frm.doc.language || frappe.boot.lang this.doc.language || frappe.boot.lang
); );
} }

View File

@ -17,6 +17,7 @@ from frappe.utils.nestedset import NestedSet
from past.builtins import cmp from past.builtins import cmp
import functools import functools
from erpnext.accounts.doctype.account.account import get_account_currency 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): class Company(NestedSet):
nsm_parent_field = 'parent_company' nsm_parent_field = 'parent_company'
@ -68,11 +69,7 @@ class Company(NestedSet):
@frappe.whitelist() @frappe.whitelist()
def create_default_tax_template(self): def create_default_tax_template(self):
from erpnext.setup.setup_wizard.operations.taxes_setup import create_sales_tax setup_taxes_and_charges(self.name, self.country)
create_sales_tax({
'country': self.country,
'company_name': self.name
})
def validate_default_accounts(self): def validate_default_accounts(self):
accounts = [ accounts = [

View File

@ -15,7 +15,7 @@ def delete_company_transactions(company_name):
frappe.only_for("System Manager") frappe.only_for("System Manager")
doc = frappe.get_doc("Company", company_name) 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.throw(_("Transactions can only be deleted by the creator of the Company"),
frappe.PermissionError) frappe.PermissionError)

View File

@ -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 .default_success_action import get_default_success_action
from frappe import _ from frappe import _
from frappe.utils import cint 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.desk.page.setup_wizard.setup_wizard import add_all_roles_to
from frappe.custom.doctype.custom_field.custom_field import create_custom_field 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 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 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>""" <a style="color: #888" href="http://erpnext.org">ERPNext</a></div>"""
@ -29,6 +31,7 @@ def after_install():
add_company_to_session_defaults() add_company_to_session_defaults()
add_standard_navbar_items() add_standard_navbar_items()
add_app_name() add_app_name()
add_non_standard_user_types()
frappe.db.commit() frappe.db.commit()
@ -164,3 +167,81 @@ def add_standard_navbar_items():
def add_app_name(): def add_app_name():
frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext') 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

View File

@ -481,14 +481,250 @@
}, },
"Germany": { "Germany": {
"Germany VAT 19%": { "chart_of_accounts": {
"account_name": "VAT 19%", "SKR04 mit Kontonummern": {
"tax_rate": 19.00, "sales_tax_templates": [
"default": 1 {
}, "title": "Umsatzsteuer 19%",
"Germany VAT 7%": { "taxes": [
"account_name": "VAT 7%", {
"tax_rate": 7.00 "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": { "India": {
"In State GST": { "chart_of_accounts": {
"account_name": ["SGST", "CGST"], "*": {
"tax_rate": [9.00, 9.00], "item_tax_templates": [
"default": 1 {
}, "title": "In State GST",
"Out of State GST": { "taxes": [
"account_name": "IGST", {
"tax_rate": 18.00 "tax_type": {
}, "account_name": "SGST",
"VAT 5%": { "tax_rate": 9.00
"account_name": "VAT 5%", }
"tax_rate": 5.00 },
}, {
"VAT 4%": { "tax_type": {
"account_name": "VAT 4%", "account_name": "CGST",
"tax_rate": 4.00 "tax_rate": 9.00
}, }
"VAT 14%": { }
"account_name": "VAT 14%", ]
"tax_rate": 14.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
}
}
]
}
]
}
} }
}, },

View File

@ -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 # License: GNU General Public License v3. See license.txt
from __future__ import unicode_literals 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): import os
country_wise_tax = get_country_wise_tax(args.get("country")) import json
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)
def make_tax_account_and_template(company, account_name, tax_rate, template_name=None): import frappe
if not isinstance(account_name, (list, tuple)): from frappe import _
account_name = [account_name]
tax_rate = [tax_rate]
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: def setup_taxes_and_charges(company_name: str, country: str):
if accounts: file_path = os.path.join(os.path.dirname(__file__), '..', 'data', 'country_wise_tax.json')
make_sales_and_purchase_tax_templates(accounts, template_name) with open(file_path, 'r') as json_file:
make_item_tax_templates(accounts, template_name) tax_data = json.load(json_file)
except frappe.NameError:
if frappe.message_log: frappe.message_log.pop()
except RootNotEditable:
pass
def make_tax_account(company, account_name, tax_rate): country_wise_tax = tax_data.get(country)
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)
def make_sales_and_purchase_tax_templates(accounts, template_name=None): if not country_wise_tax:
if not template_name: return
template_name = accounts[0].name
sales_tax_template = { if 'chart_of_accounts' not in country_wise_tax:
"doctype": "Sales Taxes and Charges Template", country_wise_tax = simple_to_detailed(country_wise_tax)
"title": template_name,
"company": accounts[0].company, from_detailed_data(company_name, country_wise_tax.get('chart_of_accounts'))
'taxes': []
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: return {
sales_tax_template['taxes'].append({ 'chart_of_accounts': {
"category": "Total", '*': {
"charge_type": "On Net Total", 'item_tax_templates': [{
"account_head": account.name, 'title': title,
"description": "{0} @ {1}".format(account.account_name, account.tax_rate), 'taxes': [{
"rate": account.tax_rate 'tax_type': {
}) 'account_name': data.get('account_name'),
# Sales 'tax_rate': data.get('tax_rate')
frappe.get_doc(copy.deepcopy(sales_tax_template)).insert(ignore_permissions=True) }
}]
# Purchase } for title, data in templates.items()],
purchase_tax_template = copy.deepcopy(sales_tax_template) '*': [{
purchase_tax_template["doctype"] = "Purchase Taxes and Charges Template" 'title': title,
'is_default': data.get('default', 0),
doc = frappe.get_doc(purchase_tax_template) 'taxes': [{
doc.insert(ignore_permissions=True) 'account_head': {
'account_name': data.get('account_name'),
def make_item_tax_templates(accounts, template_name=None): 'tax_rate': data.get('tax_rate')
if not template_name: }
template_name = accounts[0].name }]
} for title, data in templates.items()]
item_tax_template = { }
"doctype": "Item Tax Template", }
"title": template_name,
"company": accounts[0].company,
'taxes': []
} }
for account in accounts: def from_detailed_data(company_name, data):
item_tax_template['taxes'].append({ """Create Taxes and Charges Templates from detailed data."""
"tax_type": account.name, coa_name = frappe.db.get_value('Company', company_name, 'chart_of_accounts')
"tax_rate": account.tax_rate 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 if sales_tax_templates:
frappe.get_doc(copy.deepcopy(item_tax_template)).insert(ignore_permissions=True) 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): if purchase_tax_templates:
tax_group = frappe.db.get_value("Account", for template in purchase_tax_templates:
{"account_name": "Duties and Taxes", "is_group": 1, "company": company}) make_taxes_and_charges_template(company_name, 'Purchase Taxes and Charges Template', template)
if not tax_group:
tax_group = frappe.db.get_value("Account", {"is_group": 1, "root_type": "Liability",
"account_type": "Tax", "company": company})
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

View File

@ -9,5 +9,4 @@ def complete():
'data', 'test_mfg.json'), 'r') as f: 'data', 'test_mfg.json'), 'r') as f:
data = json.loads(f.read()) data = json.loads(f.read())
#setup_wizard.create_sales_tax(data)
setup_complete(data) setup_complete(data)

View File

@ -230,12 +230,12 @@ def update_cart_address(address_type, address_name):
if address_type.lower() == "billing": if address_type.lower() == "billing":
quotation.customer_address = address_name quotation.customer_address = address_name
quotation.address_display = address_display 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) address_doc = next((doc for doc in get_billing_addresses() if doc["name"] == address_name), None)
elif address_type.lower() == "shipping": elif address_type.lower() == "shipping":
quotation.shipping_address_name = address_name quotation.shipping_address_name = address_name
quotation.shipping_address = address_display 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) address_doc = next((doc for doc in get_shipping_addresses() if doc["name"] == address_name), None)
apply_cart_settings(quotation=quotation) apply_cart_settings(quotation=quotation)

View File

@ -99,6 +99,7 @@
"rounding_adjustment", "rounding_adjustment",
"rounded_total", "rounded_total",
"in_words", "in_words",
"disable_rounded_total",
"terms_section_break", "terms_section_break",
"tc_name", "tc_name",
"terms", "terms",
@ -768,6 +769,7 @@
"width": "150px" "width": "150px"
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment", "fieldname": "base_rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)", "label": "Rounding Adjustment (Company Currency)",
@ -777,6 +779,7 @@
"read_only": 1 "read_only": 1
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounded_total", "fieldname": "base_rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounded Total (Company Currency)", "label": "Rounded Total (Company Currency)",
@ -819,6 +822,7 @@
"width": "150px" "width": "150px"
}, },
{ {
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment", "fieldname": "rounding_adjustment",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounding Adjustment", "label": "Rounding Adjustment",
@ -829,6 +833,7 @@
}, },
{ {
"bold": 1, "bold": 1,
"depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounded_total", "fieldname": "rounded_total",
"fieldtype": "Currency", "fieldtype": "Currency",
"label": "Rounded Total", "label": "Rounded Total",
@ -1271,13 +1276,20 @@
"label": "Represents Company", "label": "Represents Company",
"options": "Company", "options": "Company",
"read_only": 1 "read_only": 1
},
{
"default": "0",
"depends_on": "grand_total",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
"label": "Disable Rounded Total"
} }
], ],
"icon": "fa fa-truck", "icon": "fa fa-truck",
"idx": 146, "idx": 146,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-26 17:07:59.194403", "modified": "2021-04-15 23:55:49.620641",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Delivery Note", "name": "Delivery Note",

View File

@ -582,6 +582,7 @@ class TestPurchaseReceipt(unittest.TestCase):
serial_no=serial_no, basic_rate=100, do_not_submit=True) serial_no=serial_no, basic_rate=100, do_not_submit=True)
se.submit() se.submit()
se.cancel()
dn.cancel() dn.cancel()
pr1.cancel() pr1.cancel()

View File

@ -14,6 +14,7 @@ from frappe import _, ValidationError
from erpnext.controllers.stock_controller import StockController from erpnext.controllers.stock_controller import StockController
from six import string_types from six import string_types
from six.moves import map from six.moves import map
class SerialNoCannotCreateDirectError(ValidationError): pass class SerialNoCannotCreateDirectError(ValidationError): pass
class SerialNoCannotCannotChangeError(ValidationError): pass class SerialNoCannotCannotChangeError(ValidationError): pass
class SerialNoNotRequiredError(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), frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code),
SerialNoRequiredError) SerialNoRequiredError)
elif serial_nos: elif serial_nos:
# SLE is being cancelled and has serial nos
for serial_no in serial_nos: for serial_no in serial_nos:
sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse"], as_dict=1) check_serial_no_validity_on_cancel(serial_no, sle)
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}") def check_serial_no_validity_on_cancel(serial_no, sle):
.format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse)) 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): def validate_material_transfer_entry(sle_doc):
sle_doc.update({ sle_doc.update({

View File

@ -40,16 +40,139 @@ class TestSerialNo(unittest.TestCase):
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
serial_nos = get_serial_nos(se.get("items")[0].serial_no) 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") 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) 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.warehouse, wh)
self.assertEqual(serial_no.company, "_Test Company 1") 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): def tearDown(self):
frappe.db.rollback() frappe.db.rollback()

View File

@ -363,43 +363,6 @@ frappe.ui.form.on('Shipment', {
if (frm.doc.pickup_date < frappe.datetime.get_today()) { if (frm.doc.pickup_date < frappe.datetime.get_today()) {
frappe.throw(__("Pickup Date cannot be before this day")); 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) { clear_pickup_fields: function(frm) {
let fields = ["pickup_address_name", "pickup_contact_name", "pickup_address", "pickup_contact", "pickup_contact_email", "pickup_contact_person"]; let fields = ["pickup_address_name", "pickup_contact_name", "pickup_address", "pickup_contact", "pickup_contact_email", "pickup_contact_person"];

View File

@ -275,14 +275,16 @@
"default": "09:00", "default": "09:00",
"fieldname": "pickup_from", "fieldname": "pickup_from",
"fieldtype": "Time", "fieldtype": "Time",
"label": "Pickup from" "label": "Pickup from",
"reqd": 1
}, },
{ {
"allow_on_submit": 1, "allow_on_submit": 1,
"default": "17:00", "default": "17:00",
"fieldname": "pickup_to", "fieldname": "pickup_to",
"fieldtype": "Time", "fieldtype": "Time",
"label": "Pickup to" "label": "Pickup to",
"reqd": 1
}, },
{ {
"fieldname": "column_break_36", "fieldname": "column_break_36",
@ -431,7 +433,7 @@
], ],
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2020-12-25 15:02:34.891976", "modified": "2021-04-13 17:14:18.181818",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Shipment", "name": "Shipment",

View File

@ -23,10 +23,10 @@ class Shipment(Document):
frappe.throw(_('Please enter Shipment Parcel information')) frappe.throw(_('Please enter Shipment Parcel information'))
if self.value_of_goods == 0: if self.value_of_goods == 0:
frappe.throw(_('Value of goods cannot be 0')) frappe.throw(_('Value of goods cannot be 0'))
self.status = 'Submitted' self.db_set('status', 'Submitted')
def on_cancel(self): def on_cancel(self):
self.status = 'Cancelled' self.db_set('status', 'Cancelled')
def validate_weight(self): def validate_weight(self):
for parcel in self.shipment_parcel: for parcel in self.shipment_parcel:

View File

@ -398,7 +398,7 @@ class StockReconciliation(StockController):
merge_similar_entries = {} merge_similar_entries = {}
for d in sl_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) new_sl_entries.append(d)
continue continue

View File

@ -13,6 +13,7 @@
"column_break_4", "column_break_4",
"valuation_method", "valuation_method",
"over_delivery_receipt_allowance", "over_delivery_receipt_allowance",
"role_allowed_to_over_deliver_receive",
"action_if_quality_inspection_is_not_submitted", "action_if_quality_inspection_is_not_submitted",
"show_barcode_field", "show_barcode_field",
"clean_description_html", "clean_description_html",
@ -234,6 +235,13 @@
"fieldname": "disable_serial_no_and_batch_selector", "fieldname": "disable_serial_no_and_batch_selector",
"fieldtype": "Check", "fieldtype": "Check",
"label": "Disable Serial No And Batch Selector" "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", "icon": "icon-cog",
@ -241,7 +249,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2021-01-18 13:15:38.352796", "modified": "2021-03-11 18:48:14.513055",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@ -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, "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, "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, "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, "last_purchase_rate": item.last_purchase_rate if args.get("doctype") in ["Purchase Order"] else 0,
"transaction_date": args.get("transaction_date"), "transaction_date": args.get("transaction_date"),
"against_blanket_order": args.get("against_blanket_order"), "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) meta = frappe.get_meta(args.parenttype or args.doctype)
if meta.get_field("currency") or args.get('currency'): if meta.get_field("currency") or args.get('currency'):
pl_details = get_price_list_currency_and_exchange_rate(args) if not args.get("price_list_currency") or not args.get("plc_conversion_rate"):
args.update(pl_details) # 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"): if meta.get_field("currency"):
validate_conversion_rate(args, meta) validate_conversion_rate(args, meta)
@ -922,10 +924,19 @@ def get_projected_qty(item_code, warehouse):
{"item_code": item_code, "warehouse": warehouse}, "projected_qty")} {"item_code": item_code, "warehouse": warehouse}, "projected_qty")}
@frappe.whitelist() @frappe.whitelist()
def get_bin_details(item_code, warehouse): def get_bin_details(item_code, warehouse, company=None):
return frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse}, bin_details = frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse},
["projected_qty", "actual_qty", "reserved_qty"], as_dict=True, cache=True) \ ["projected_qty", "actual_qty", "reserved_qty"], as_dict=True, cache=True) \
or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0} 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() @frappe.whitelist()
def get_serial_no_details(item_code, warehouse, stock_qty, serial_no): 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) args = process_args(args)
parent = get_price_list_currency_and_exchange_rate(args) parent = get_price_list_currency_and_exchange_rate(args)
args.update(parent)
children = [] children = []
if "items" in args: if "items" in args:
@ -1057,7 +1070,7 @@ def get_price_list_currency_and_exchange_rate(args):
return frappe._dict({ return frappe._dict({
"price_list_currency": price_list_currency, "price_list_currency": price_list_currency,
"price_list_uom_dependant": price_list_uom_dependant, "price_list_uom_dependant": price_list_uom_dependant,
"plc_conversion_rate": plc_conversion_rate "plc_conversion_rate": plc_conversion_rate or 1
}) })
@frappe.whitelist() @frappe.whitelist()

View File

@ -55,19 +55,31 @@ def get_item_info(filters):
def get_consumed_items(condition): 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(""" consumed_items = frappe.db.sql("""
select item_code, abs(sum(actual_qty)) as consumed_qty select item_code, abs(sum(actual_qty)) as consumed_qty
from `tabStock Ledger Entry` from `tabStock Ledger Entry` as sle left join `tabStock Entry` as se
where actual_qty < 0 on sle.voucher_no = se.name
where
actual_qty < 0
and voucher_type not in ('Delivery Note', 'Sales Invoice') and voucher_type not in ('Delivery Note', 'Sales Invoice')
%s %s
group by item_code group by item_code""" % condition, as_dict=1)
""" % condition, as_dict=1)
consumed_items_map = {}
for item in consumed_items:
consumed_items_map.setdefault(item.item_code, item.consumed_qty)
consumed_items_map = {item.item_code : item.consumed_qty for item in consumed_items}
return consumed_items_map return consumed_items_map
def get_delivered_items(condition): def get_delivered_items(condition):

View File

@ -372,7 +372,8 @@ class update_entries_after(object):
elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"): 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"): 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 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: else:
if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"): if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
rate_field = "valuation_rate" rate_field = "valuation_rate"
@ -603,7 +604,7 @@ class update_entries_after(object):
batch = self.wh_data.stock_queue[index] batch = self.wh_data.stock_queue[index]
if qty_to_pop >= batch[0]: if qty_to_pop >= batch[0]:
# consume current batch # 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) self.wh_data.stock_queue.pop(index)
if not self.wh_data.stock_queue and qty_to_pop: if not self.wh_data.stock_queue and qty_to_pop:
# stock finished, qty still remains to be withdrawn # stock finished, qty still remains to be withdrawn
@ -617,8 +618,8 @@ class update_entries_after(object):
batch[0] = batch[0] - qty_to_pop batch[0] = batch[0] - qty_to_pop
qty_to_pop = 0 qty_to_pop = 0
stock_value = sum((flt(batch[0]) * flt(batch[1]) 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 = sum((flt(batch[0]) 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: if stock_qty:
self.wh_data.valuation_rate = stock_value / flt(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 order by timestamp(posting_date, posting_time) asc
limit 1 limit 1
""", args, as_dict=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)

View File

@ -18,7 +18,6 @@ def get_level():
"Delivery Note": 5, "Delivery Note": 5,
"Employee": 3, "Employee": 3,
"Instructor": 5, "Instructor": 5,
"Instructor": 5,
"Issue": 5, "Issue": 5,
"Item": 5, "Item": 5,
"Journal Entry": 3, "Journal Entry": 3,

View File

@ -62,7 +62,7 @@
{{_('Back to Course')}} {{_('Back to Course')}}
</a> </a>
</div> </div>
<div> <div class="lms-title">
<h2>{{ content.name }} <span class="small text-muted">({{ position + 1 }}/{{length}})</span></h2> <h2>{{ content.name }} <span class="small text-muted">({{ position + 1 }}/{{length}})</span></h2>
</div> </div>
{% endmacro %} {% endmacro %}
@ -169,14 +169,51 @@
const next_url = '/lms/course?name={{ course }}&program={{ program }}' const next_url = '/lms/course?name={{ course }}&program={{ program }}'
{% endif %} {% endif %}
frappe.ready(() => { frappe.ready(() => {
const quiz = new Quiz(document.getElementById('quiz-wrapper'), { {% if content.is_time_bound %}
name: '{{ content.name }}', var duration = get_duration("{{content.duration}}")
course: '{{ course }}', var d = frappe.msgprint({
program: '{{ program }}', title: __('Important Notice'),
quiz_exit_button: quiz_exit_button, indicator: "red",
next_url: next_url 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>
window.quiz = quiz; 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 %} {% endif %}

View File

@ -42,7 +42,9 @@
<section class="top-section" style="padding: 6rem 0rem;"> <section class="top-section" style="padding: 6rem 0rem;">
<div class='container pb-5'> <div class='container pb-5'>
<h1>{{ education_settings.portal_title }}</h1> <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"> <p class="mt-4">
{% if frappe.session.user == 'Guest' %} {% if frappe.session.user == 'Guest' %}
<a class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a> <a class="btn btn-primary btn-lg" href="/login#signup">{{_('Sign Up')}}</a>
@ -51,13 +53,15 @@
</div> </div>
<div class='container'> <div class='container'>
<div class="row mt-5"> <div class="row mt-5">
{% for program in featured_programs %}
{{ program_card(program.program, program.has_access) }}
{% endfor %}
{% if featured_programs %} {% 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) %} {% for n in range( (3 - (featured_programs|length)) %3) %}
{{ null_card() }} {{ null_card() }}
{% endfor %} {% endfor %}
{% else %}
<p class="lead">You have not enrolled in any program. Contact your Instructor.</p>
{% endif %} {% endif %}
</div> </div>
</div> </div>

View File

@ -35,7 +35,7 @@ def get_contents(topic, course, program):
progress.append({'content': content, 'content_type': content.doctype, 'completed': status}) progress.append({'content': content, 'content_type': content.doctype, 'completed': status})
elif content.doctype == 'Quiz': elif content.doctype == 'Quiz':
if student: 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: else:
status = False status = False
score = None score = None