Merge branch 'version-13-pre-release' into version-13
This commit is contained in:
commit
d0ba0217e4
3
.flake8
3
.flake8
@ -29,4 +29,5 @@ ignore =
|
||||
B950,
|
||||
W191,
|
||||
|
||||
max-line-length = 200
|
||||
max-line-length = 200
|
||||
exclude=.github/helper/semgrep_rules
|
||||
|
@ -4,25 +4,61 @@ from frappe import _, flt
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
# ruleid: frappe-modifying-but-not-comitting
|
||||
def on_submit(self):
|
||||
if self.value_of_goods == 0:
|
||||
frappe.throw(_('Value of goods cannot be 0'))
|
||||
# ruleid: frappe-modifying-after-submit
|
||||
self.status = 'Submitted'
|
||||
|
||||
|
||||
# ok: frappe-modifying-but-not-comitting
|
||||
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")
|
||||
if self.value_of_goods == 0:
|
||||
frappe.throw(_('Value of goods cannot be 0'))
|
||||
self.status = 'Submitted'
|
||||
self.db_set('status', 'Submitted')
|
||||
|
||||
class TestDoc(Document):
|
||||
pass
|
||||
# ok: frappe-modifying-but-not-comitting
|
||||
def on_submit(self):
|
||||
if self.value_of_goods == 0:
|
||||
frappe.throw(_('Value of goods cannot be 0'))
|
||||
x = "y"
|
||||
self.status = x
|
||||
self.db_set('status', x)
|
||||
|
||||
def validate(self):
|
||||
#ruleid: frappe-modifying-child-tables-while-iterating
|
||||
for item in self.child_table:
|
||||
if item.value < 0:
|
||||
self.remove(item)
|
||||
|
||||
# ok: frappe-modifying-but-not-comitting
|
||||
def on_submit(self):
|
||||
x = "y"
|
||||
self.status = x
|
||||
self.save()
|
||||
|
||||
# ruleid: frappe-modifying-but-not-comitting-other-method
|
||||
class DoctypeClass(Document):
|
||||
def on_submit(self):
|
||||
self.good_method()
|
||||
self.tainted_method()
|
||||
|
||||
def tainted_method(self):
|
||||
self.status = "uptate"
|
||||
|
||||
|
||||
# ok: frappe-modifying-but-not-comitting-other-method
|
||||
class DoctypeClass(Document):
|
||||
def on_submit(self):
|
||||
self.good_method()
|
||||
self.tainted_method()
|
||||
|
||||
def tainted_method(self):
|
||||
self.status = "update"
|
||||
self.db_set("status", "update")
|
||||
|
||||
# ok: frappe-modifying-but-not-comitting-other-method
|
||||
class DoctypeClass(Document):
|
||||
def on_submit(self):
|
||||
self.good_method()
|
||||
self.tainted_method()
|
||||
self.save()
|
||||
|
||||
def tainted_method(self):
|
||||
self.status = "uptate"
|
||||
|
@ -1,32 +1,93 @@
|
||||
# This file specifies rules for correctness according to how frappe doctype data model works.
|
||||
|
||||
rules:
|
||||
- id: frappe-modifying-after-submit
|
||||
- id: frappe-modifying-but-not-comitting
|
||||
patterns:
|
||||
- pattern: self.$ATTR = ...
|
||||
- pattern-inside: |
|
||||
def on_submit(self, ...):
|
||||
- pattern: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
- pattern-not: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
...
|
||||
self.db_set(..., self.$ATTR, ...)
|
||||
- pattern-not: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = $SOME_VAR
|
||||
...
|
||||
self.db_set(..., $SOME_VAR, ...)
|
||||
- pattern-not: |
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = $SOME_VAR
|
||||
...
|
||||
self.save()
|
||||
- metavariable-regex:
|
||||
metavariable: '$ATTR'
|
||||
# this is negative look-ahead, add more attrs to ignore like (ignore|ignore_this_too|ignore_me)
|
||||
regex: '^(?!status_updater)(.*)$'
|
||||
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
|
||||
- metavariable-regex:
|
||||
metavariable: "$METHOD"
|
||||
regex: "(on_submit|on_cancel)"
|
||||
message: |
|
||||
Doctype modified after submission. Please check if modification of self.$ATTR is commited to database.
|
||||
DocType modified in self.$METHOD. Please check if modification of self.$ATTR is commited to database.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-modifying-after-cancel
|
||||
- id: frappe-modifying-but-not-comitting-other-method
|
||||
patterns:
|
||||
- pattern: self.$ATTR = ...
|
||||
- pattern-inside: |
|
||||
def on_cancel(self, ...):
|
||||
- pattern: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
- metavariable-regex:
|
||||
metavariable: '$ATTR'
|
||||
regex: '^(?!ignore_linked_doctypes|status_updater)(.*)$'
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
- pattern-not: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
...
|
||||
self.db_set(..., self.$ATTR, ...)
|
||||
- pattern-not: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = $SOME_VAR
|
||||
...
|
||||
self.db_set(..., $SOME_VAR, ...)
|
||||
- pattern-not: |
|
||||
class $DOCTYPE(...):
|
||||
def $METHOD(self, ...):
|
||||
...
|
||||
self.$ANOTHER_METHOD()
|
||||
...
|
||||
self.save()
|
||||
def $ANOTHER_METHOD(self, ...):
|
||||
...
|
||||
self.$ATTR = ...
|
||||
- metavariable-regex:
|
||||
metavariable: "$METHOD"
|
||||
regex: "(on_submit|on_cancel)"
|
||||
message: |
|
||||
Doctype modified after cancellation. Please check if modification of self.$ATTR is commited to database.
|
||||
self.$ANOTHER_METHOD is called from self.$METHOD, check if changes to self.$ATTR are commited to database.
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
|
7
.github/helper/semgrep_rules/translate.js
vendored
7
.github/helper/semgrep_rules/translate.js
vendored
@ -35,3 +35,10 @@ __('You have' + 'subscribers in your mailing list.')
|
||||
// ruleid: frappe-translation-js-splitting
|
||||
__('You have {0} subscribers' +
|
||||
'in your mailing list', [subscribers.length])
|
||||
|
||||
// ok: frappe-translation-js-splitting
|
||||
__("Ctrl+Enter to add comment")
|
||||
|
||||
// ruleid: frappe-translation-js-splitting
|
||||
__('You have {0} subscribers \
|
||||
in your mailing list', [subscribers.length])
|
||||
|
8
.github/helper/semgrep_rules/translate.py
vendored
8
.github/helper/semgrep_rules/translate.py
vendored
@ -51,3 +51,11 @@ _(f"what" + f"this is also not cool")
|
||||
_("")
|
||||
# ruleid: frappe-translation-empty-string
|
||||
_('')
|
||||
|
||||
|
||||
class Test:
|
||||
# ok: frappe-translation-python-splitting
|
||||
def __init__(
|
||||
args
|
||||
):
|
||||
pass
|
||||
|
9
.github/helper/semgrep_rules/translate.yml
vendored
9
.github/helper/semgrep_rules/translate.yml
vendored
@ -42,9 +42,10 @@ rules:
|
||||
|
||||
- id: frappe-translation-python-splitting
|
||||
pattern-either:
|
||||
- pattern: _(...) + ... + _(...)
|
||||
- pattern: _(...) + _(...)
|
||||
- pattern: _("..." + "...")
|
||||
- pattern-regex: '_\([^\)]*\\\s*'
|
||||
- pattern-regex: '[\s\.]_\([^\)]*\\\s*' # lines broken by `\`
|
||||
- pattern-regex: '[\s\.]_\(\s*\n' # line breaks allowed by python for using ( )
|
||||
message: |
|
||||
Do not split strings inside translate function. Do not concatenate using translate functions.
|
||||
Please refer: https://frappeframework.com/docs/user/en/translations
|
||||
@ -53,8 +54,8 @@ rules:
|
||||
|
||||
- id: frappe-translation-js-splitting
|
||||
pattern-either:
|
||||
- pattern-regex: '__\([^\)]*[\+\\]\s*'
|
||||
- pattern: __('...' + '...')
|
||||
- pattern-regex: '__\([^\)]*[\\]\s+'
|
||||
- pattern: __('...' + '...', ...)
|
||||
- pattern: __('...') + __('...')
|
||||
message: |
|
||||
Do not split strings inside translate function. Do not concatenate using translate functions.
|
||||
|
9
.github/helper/semgrep_rules/ux.js
vendored
Normal file
9
.github/helper/semgrep_rules/ux.js
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
|
||||
// ok: frappe-missing-translate-function-js
|
||||
frappe.msgprint('{{ _("Both login and password required") }}');
|
||||
|
||||
// ruleid: frappe-missing-translate-function-js
|
||||
frappe.msgprint('What');
|
||||
|
||||
// ok: frappe-missing-translate-function-js
|
||||
frappe.throw(' {{ _("Both login and password required") }}. ');
|
18
.github/helper/semgrep_rules/ux.py
vendored
18
.github/helper/semgrep_rules/ux.py
vendored
@ -2,30 +2,30 @@ import frappe
|
||||
from frappe import msgprint, throw, _
|
||||
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
# ruleid: frappe-missing-translate-function-python
|
||||
throw("Error Occured")
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
# ruleid: frappe-missing-translate-function-python
|
||||
frappe.throw("Error Occured")
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
# ruleid: frappe-missing-translate-function-python
|
||||
frappe.msgprint("Useful message")
|
||||
|
||||
# ruleid: frappe-missing-translate-function
|
||||
# ruleid: frappe-missing-translate-function-python
|
||||
msgprint("Useful message")
|
||||
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
# ok: frappe-missing-translate-function-python
|
||||
translatedmessage = _("Hello")
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
# ok: frappe-missing-translate-function-python
|
||||
throw(translatedmessage)
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
# ok: frappe-missing-translate-function-python
|
||||
msgprint(translatedmessage)
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
# ok: frappe-missing-translate-function-python
|
||||
msgprint(_("Helpful message"))
|
||||
|
||||
# ok: frappe-missing-translate-function
|
||||
# ok: frappe-missing-translate-function-python
|
||||
frappe.throw(_("Error occured"))
|
||||
|
23
.github/helper/semgrep_rules/ux.yml
vendored
23
.github/helper/semgrep_rules/ux.yml
vendored
@ -1,15 +1,30 @@
|
||||
rules:
|
||||
- id: frappe-missing-translate-function
|
||||
- id: frappe-missing-translate-function-python
|
||||
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]
|
||||
languages: [python]
|
||||
severity: ERROR
|
||||
|
||||
- id: frappe-missing-translate-function-js
|
||||
pattern-either:
|
||||
- patterns:
|
||||
- pattern: frappe.msgprint("...", ...)
|
||||
- pattern-not: frappe.msgprint(__("..."), ...)
|
||||
# ignore microtemplating e.g. msgprint("{{ _("server side translation") }}")
|
||||
- pattern-not: frappe.msgprint("=~/\{\{.*\_.*\}\}/i", ...)
|
||||
- patterns:
|
||||
- pattern: frappe.throw("...", ...)
|
||||
- pattern-not: frappe.throw(__("..."), ...)
|
||||
# ignore microtemplating
|
||||
- pattern-not: frappe.throw("=~/\{\{.*\_.*\}\}/i", ...)
|
||||
message: |
|
||||
All user facing text must be wrapped in translate function. Please refer to translation documentation. https://frappeframework.com/docs/user/en/guides/basics/translations
|
||||
languages: [javascript]
|
||||
severity: ERROR
|
||||
|
12
.github/workflows/semgrep.yml
vendored
12
.github/workflows/semgrep.yml
vendored
@ -4,6 +4,8 @@ on:
|
||||
pull_request:
|
||||
branches:
|
||||
- develop
|
||||
- version-13-hotfix
|
||||
- version-13-pre-release
|
||||
jobs:
|
||||
semgrep:
|
||||
name: Frappe Linter
|
||||
@ -14,11 +16,19 @@ jobs:
|
||||
uses: actions/setup-python@v2
|
||||
with:
|
||||
python-version: 3.8
|
||||
- name: Run semgrep
|
||||
|
||||
- name: Setup semgrep
|
||||
run: |
|
||||
python -m pip install -q semgrep
|
||||
git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q
|
||||
|
||||
- name: Semgrep errors
|
||||
run: |
|
||||
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
|
||||
|
||||
- name: Semgrep warnings
|
||||
run: |
|
||||
files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF)
|
||||
[[ -d .github/helper/semgrep_rules ]] && semgrep --severity WARNING --severity INFO --config=.github/helper/semgrep_rules --quiet $files
|
||||
|
@ -5,7 +5,7 @@ import frappe
|
||||
from erpnext.hooks import regional_overrides
|
||||
from frappe.utils import getdate
|
||||
|
||||
__version__ = '13.3.1'
|
||||
__version__ = '13.4.0'
|
||||
|
||||
def get_default_company(user=None):
|
||||
'''Get default company for user'''
|
||||
|
@ -41,7 +41,7 @@ def build_conditions(process_type, account, company):
|
||||
if account:
|
||||
conditions += "AND %s='%s'"%(deferred_account, account)
|
||||
elif company:
|
||||
conditions += "AND p.company='%s'"%(company)
|
||||
conditions += f"AND p.company = {frappe.db.escape(company)}"
|
||||
|
||||
return conditions
|
||||
|
||||
@ -360,12 +360,10 @@ def make_gl_entries(doc, credit_account, debit_account, against,
|
||||
frappe.flags.deferred_accounting_error = True
|
||||
|
||||
def send_mail(deferred_process):
|
||||
title = _("Error while processing deferred accounting for {0}".format(deferred_process))
|
||||
content = _("""
|
||||
Deferred accounting failed for some invoices:
|
||||
Please check Process Deferred Accounting {0}
|
||||
and submit manually after resolving errors
|
||||
""").format(get_link_to_form('Process Deferred Accounting', deferred_process))
|
||||
title = _("Error while processing deferred accounting for {0}").format(deferred_process)
|
||||
link = get_link_to_form('Process Deferred Accounting', deferred_process)
|
||||
content = _("Deferred accounting failed for some invoices:") + "\n"
|
||||
content += _("Please check Process Deferred Accounting {0} and submit manually after resolving errors.").format(link)
|
||||
sendmail_to_system_managers(title, content)
|
||||
|
||||
def book_revenue_via_journal_entry(doc, credit_account, debit_account, against,
|
||||
|
@ -75,8 +75,13 @@ class GLEntry(Document):
|
||||
def pl_must_have_cost_center(self):
|
||||
if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
|
||||
if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
|
||||
frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.")
|
||||
.format(self.voucher_type, self.voucher_no, self.account))
|
||||
msg = _("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}.").format(
|
||||
self.voucher_type, self.voucher_no, self.account)
|
||||
msg += " "
|
||||
msg += _("Please set the cost center field in {0} or setup a default Cost Center for the Company.").format(
|
||||
self.voucher_type)
|
||||
|
||||
frappe.throw(msg, title=_("Missing Cost Center"))
|
||||
|
||||
def validate_dimensions_for_pl_and_bs(self):
|
||||
account_type = frappe.db.get_value("Account", self.account, "report_type")
|
||||
|
@ -1,196 +1,82 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2018-01-02 15:48:58.768352",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2018-01-02 15:48:58.768352",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"company",
|
||||
"cgst_account",
|
||||
"sgst_account",
|
||||
"igst_account",
|
||||
"cess_account",
|
||||
"is_reverse_charge_account"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Company",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Company",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 1,
|
||||
"fieldname": "company",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Company",
|
||||
"options": "Company",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "cgst_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "CGST Account",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 2,
|
||||
"fieldname": "cgst_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "CGST Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "sgst_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "SGST Account",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 2,
|
||||
"fieldname": "sgst_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "SGST Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "igst_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "IGST Account",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 1,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"columns": 2,
|
||||
"fieldname": "igst_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "IGST Account",
|
||||
"options": "Account",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "cess_account",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "CESS Account",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Account",
|
||||
"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,
|
||||
"unique": 0
|
||||
"columns": 2,
|
||||
"fieldname": "cess_account",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "CESS Account",
|
||||
"options": "Account"
|
||||
},
|
||||
{
|
||||
"columns": 1,
|
||||
"default": "0",
|
||||
"fieldname": "is_reverse_charge_account",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Reverse Charge Account"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2018-01-02 15:52:22.335988",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GST Account",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-09 12:30:25.889993",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "GST Account",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -39,7 +39,11 @@ class JournalEntry(AccountsController):
|
||||
self.validate_multi_currency()
|
||||
self.set_amounts_in_company_currency()
|
||||
self.validate_debit_credit_amount()
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
# Do not validate while importing via data import
|
||||
if not frappe.flags.in_import:
|
||||
self.validate_total_debit_and_credit()
|
||||
|
||||
self.validate_against_jv()
|
||||
self.validate_reference_doc()
|
||||
self.set_against_account()
|
||||
|
17
erpnext/accounts/doctype/journal_entry/regional/india.js
Normal file
17
erpnext/accounts/doctype/journal_entry/regional/india.js
Normal file
@ -0,0 +1,17 @@
|
||||
frappe.ui.form.on("Journal Entry", {
|
||||
refresh: function(frm) {
|
||||
frm.set_query('company_address', function(doc) {
|
||||
if(!doc.company) {
|
||||
frappe.throw(__('Please set Company'));
|
||||
}
|
||||
|
||||
return {
|
||||
query: 'frappe.contacts.doctype.address.address.address_query',
|
||||
filters: {
|
||||
link_doctype: 'Company',
|
||||
link_name: doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
@ -46,6 +46,7 @@
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "closing_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
@ -57,7 +58,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2020-10-23 16:45:43.662034",
|
||||
"modified": "2021-05-19 20:08:44.523861",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "POS Closing Entry Detail",
|
||||
|
@ -455,32 +455,26 @@ class POSInvoice(SalesInvoice):
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_availability(item_code, warehouse):
|
||||
latest_sle = frappe.db.sql("""select qty_after_transaction
|
||||
from `tabStock Ledger Entry`
|
||||
bin_qty = frappe.db.sql("""select actual_qty from `tabBin`
|
||||
where item_code = %s and warehouse = %s
|
||||
order by posting_date desc, posting_time desc
|
||||
limit 1""", (item_code, warehouse), as_dict=1)
|
||||
|
||||
pos_sales_qty = get_pos_reserved_qty(item_code, warehouse)
|
||||
|
||||
sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0
|
||||
bin_qty = bin_qty[0].actual_qty or 0 if bin_qty else 0
|
||||
|
||||
if sle_qty and pos_sales_qty:
|
||||
return sle_qty - pos_sales_qty
|
||||
else:
|
||||
return sle_qty
|
||||
return bin_qty - pos_sales_qty
|
||||
|
||||
def get_pos_reserved_qty(item_code, warehouse):
|
||||
reserved_qty = frappe.db.sql("""select sum(p_item.qty) as qty
|
||||
from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item
|
||||
where p.name = p_item.parent
|
||||
and p.consolidated_invoice is NULL
|
||||
and p.docstatus = 1
|
||||
and ifnull(p.consolidated_invoice, '') = ''
|
||||
and p_item.docstatus = 1
|
||||
and p_item.item_code = %s
|
||||
and p_item.warehouse = %s
|
||||
""", (item_code, warehouse), as_dict=1)
|
||||
|
||||
|
||||
return reserved_qty[0].qty or 0 if reserved_qty else 0
|
||||
|
||||
@frappe.whitelist()
|
||||
|
@ -42,8 +42,9 @@ class POSInvoiceMergeLog(Document):
|
||||
if return_against_status != "Consolidated":
|
||||
# if return entry is not getting merged in the current pos closing and if it is not consolidated
|
||||
bold_unconsolidated = frappe.bold("not Consolidated")
|
||||
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}. ")
|
||||
msg = (_("Row #{}: Original Invoice {} of return invoice {} is {}.")
|
||||
.format(d.idx, bold_return_against, bold_pos_invoice, bold_unconsolidated))
|
||||
msg += " "
|
||||
msg += _("Original invoice should be consolidated before or along with the return invoice.")
|
||||
msg += "<br><br>"
|
||||
msg += _("You can add original invoice {} manually to proceed.").format(bold_return_against)
|
||||
@ -56,12 +57,12 @@ class POSInvoiceMergeLog(Document):
|
||||
sales = [d for d in pos_invoice_docs if d.get('is_return') == 0]
|
||||
|
||||
sales_invoice, credit_note = "", ""
|
||||
if sales:
|
||||
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
||||
|
||||
if returns:
|
||||
credit_note = self.process_merging_into_credit_note(returns)
|
||||
|
||||
if sales:
|
||||
sales_invoice = self.process_merging_into_sales_invoice(sales)
|
||||
|
||||
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
|
||||
|
||||
self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
|
||||
@ -274,9 +275,9 @@ def create_merge_logs(invoice_by_customer, closing_entry=None):
|
||||
closing_entry.db_set('error_message', '')
|
||||
closing_entry.update_opening_entry()
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
message_log = frappe.message_log.pop()
|
||||
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
|
||||
error_message = safe_load_json(message_log)
|
||||
|
||||
if closing_entry:
|
||||
@ -300,9 +301,9 @@ def cancel_merge_logs(merge_logs, closing_entry=None):
|
||||
closing_entry.db_set('error_message', '')
|
||||
closing_entry.update_opening_entry(for_cancel=True)
|
||||
|
||||
except Exception:
|
||||
except Exception as e:
|
||||
frappe.db.rollback()
|
||||
message_log = frappe.message_log.pop()
|
||||
message_log = frappe.message_log.pop() if frappe.message_log else str(e)
|
||||
error_message = safe_load_json(message_log)
|
||||
|
||||
if closing_entry:
|
||||
@ -348,11 +349,9 @@ def job_already_enqueued(job_name):
|
||||
return True
|
||||
|
||||
def safe_load_json(message):
|
||||
JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
|
||||
|
||||
try:
|
||||
json_message = json.loads(message).get('message')
|
||||
except JSONDecodeError:
|
||||
except Exception:
|
||||
json_message = message
|
||||
|
||||
return json_message
|
@ -1,24 +1,42 @@
|
||||
<h1 class="text-center" style="page-break-before:always">{{ filters.party[0] }}</h1>
|
||||
<h3 class="text-center">{{ _("Statement of Accounts") }}</h3>
|
||||
<div class="page-break">
|
||||
<div id="header-html" class="hidden-pdf">
|
||||
{% if letter_head %}
|
||||
<div class="letter-head text-center">{{ letter_head.content }}</div>
|
||||
<hr style="height:2px;border-width:0;color:black;background-color:black;">
|
||||
{% endif %}
|
||||
</div>
|
||||
<div id="footer-html" class="visible-pdf">
|
||||
{% if letter_head.footer %}
|
||||
<div class="letter-head-footer">
|
||||
<hr style="border-width:0;color:black;background-color:black;padding-bottom:2px;">
|
||||
{{ letter_head.footer }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
<h2 class="text-center">{{ _("STATEMENTS OF ACCOUNTS") }}</h2>
|
||||
<div>
|
||||
<h5 style="float: left;">{{ _("Customer: ") }} <b>{{filters.party[0] }}</b></h5>
|
||||
<h5 style="float: right;">
|
||||
{{ _("Date: ") }}
|
||||
<b>{{ frappe.format(filters.from_date, 'Date')}}
|
||||
{{ _("to") }}
|
||||
{{ frappe.format(filters.to_date, 'Date')}}</b>
|
||||
</h5>
|
||||
</div>
|
||||
<br>
|
||||
|
||||
<h5 class="text-center">
|
||||
{{ frappe.format(filters.from_date, 'Date')}}
|
||||
{{ _("to") }}
|
||||
{{ frappe.format(filters.to_date, 'Date')}}
|
||||
</h5>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{{ _("Date") }}</th>
|
||||
<th style="width: 15%">{{ _("Ref") }}</th>
|
||||
<th style="width: 25%">{{ _("Party") }}</th>
|
||||
<th style="width: 15%">{{ _("Debit") }}</th>
|
||||
<th style="width: 15%">{{ _("Credit") }}</th>
|
||||
<th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">{{ _("Date") }}</th>
|
||||
<th style="width: 15%">{{ _("Reference") }}</th>
|
||||
<th style="width: 25%">{{ _("Remarks") }}</th>
|
||||
<th style="width: 15%">{{ _("Debit") }}</th>
|
||||
<th style="width: 15%">{{ _("Credit") }}</th>
|
||||
<th style="width: 18%">{{ _("Balance (Dr - Cr)") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for row in data %}
|
||||
<tr>
|
||||
{% if(row.posting_date) %}
|
||||
@ -58,32 +76,34 @@
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
<br><br>
|
||||
{% if ageing %}
|
||||
<h3 class="text-center">{{ _("Ageing Report Based On ") }} {{ ageing.ageing_based_on }}</h3>
|
||||
<h5 class="text-center">
|
||||
{{ _("Up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
|
||||
</h5>
|
||||
<br>
|
||||
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 12%">30 Days</th>
|
||||
<th style="width: 15%">60 Days</th>
|
||||
<th style="width: 25%">90 Days</th>
|
||||
<th style="width: 15%">120 Days</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
<p class="text-right text-muted">Printed On {{ frappe.format(frappe.utils.get_datetime(), 'Datetime') }}</p>
|
||||
</table>
|
||||
<br>
|
||||
{% if ageing %}
|
||||
<h4 class="text-center">{{ _("Ageing Report based on ") }} {{ ageing.ageing_based_on }}
|
||||
{{ _("up to " ) }} {{ frappe.format(filters.to_date, 'Date')}}
|
||||
</h4>
|
||||
<table class="table table-bordered">
|
||||
<thead>
|
||||
<tr>
|
||||
<th style="width: 25%">30 Days</th>
|
||||
<th style="width: 25%">60 Days</th>
|
||||
<th style="width: 25%">90 Days</th>
|
||||
<th style="width: 25%">120 Days</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range1, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range2, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range3, currency=filters.presentation_currency) }}</td>
|
||||
<td>{{ frappe.utils.fmt_money(ageing.range4, currency=filters.presentation_currency) }}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
{% endif %}
|
||||
{% if terms_and_conditions %}
|
||||
<div>
|
||||
{{ terms_and_conditions }}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
@ -1,6 +1,5 @@
|
||||
{
|
||||
"actions": [],
|
||||
"allow_workflow": 1,
|
||||
"autoname": "Prompt",
|
||||
"creation": "2020-05-22 16:46:18.712954",
|
||||
"doctype": "DocType",
|
||||
@ -28,9 +27,11 @@
|
||||
"customers",
|
||||
"preferences",
|
||||
"orientation",
|
||||
"section_break_14",
|
||||
"include_ageing",
|
||||
"ageing_based_on",
|
||||
"section_break_14",
|
||||
"letter_head",
|
||||
"terms_and_conditions",
|
||||
"section_break_1",
|
||||
"enable_auto_email",
|
||||
"section_break_18",
|
||||
@ -270,10 +271,22 @@
|
||||
"fieldname": "body",
|
||||
"fieldtype": "Text Editor",
|
||||
"label": "Body"
|
||||
},
|
||||
{
|
||||
"fieldname": "letter_head",
|
||||
"fieldtype": "Link",
|
||||
"label": "Letter Head",
|
||||
"options": "Letter Head"
|
||||
},
|
||||
{
|
||||
"fieldname": "terms_and_conditions",
|
||||
"fieldtype": "Link",
|
||||
"label": "Terms and Conditions",
|
||||
"options": "Terms and Conditions"
|
||||
}
|
||||
],
|
||||
"links": [],
|
||||
"modified": "2020-08-08 08:47:09.185728",
|
||||
"modified": "2021-05-21 10:14:22.426672",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Process Statement Of Accounts",
|
||||
|
@ -64,6 +64,9 @@ def get_report_pdf(doc, consolidated=True):
|
||||
tax_id = frappe.get_doc('Customer', entry.customer).tax_id
|
||||
presentation_currency = get_party_account_currency('Customer', entry.customer, doc.company) \
|
||||
or doc.currency or get_company_currency(doc.company)
|
||||
if doc.letter_head:
|
||||
from frappe.www.printview import get_letter_head
|
||||
letter_head = get_letter_head(doc, 0)
|
||||
|
||||
filters= frappe._dict({
|
||||
'from_date': doc.from_date,
|
||||
@ -91,7 +94,10 @@ def get_report_pdf(doc, consolidated=True):
|
||||
continue
|
||||
|
||||
html = frappe.render_template(template_path, \
|
||||
{"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None})
|
||||
{"filters": filters, "data": res, "ageing": ageing[0] if (doc.include_ageing and ageing) else None,
|
||||
"letter_head": letter_head if doc.letter_head else None,
|
||||
"terms_and_conditions": frappe.db.get_value('Terms and Conditions', doc.terms_and_conditions, 'terms')
|
||||
if doc.terms_and_conditions else None})
|
||||
|
||||
html = frappe.render_template(base_template_path, {"body": html, \
|
||||
"css": get_print_style(), "title": "Statement For " + entry.customer})
|
||||
|
@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte
|
||||
var me = this;
|
||||
this._super();
|
||||
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice'];
|
||||
this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet'];
|
||||
if(!this.frm.doc.__islocal && !this.frm.doc.customer && this.frm.doc.debit_to) {
|
||||
// show debit_to in print format
|
||||
this.frm.set_df_property("debit_to", "print_hide", 0);
|
||||
@ -582,6 +582,16 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
};
|
||||
});
|
||||
|
||||
frm.set_query("adjustment_against", function() {
|
||||
return {
|
||||
filters: {
|
||||
company: frm.doc.company,
|
||||
customer: frm.doc.customer,
|
||||
docstatus: 1
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
frm.custom_make_buttons = {
|
||||
'Delivery Note': 'Delivery',
|
||||
'Sales Invoice': 'Return / Credit Note',
|
||||
@ -685,14 +695,16 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
},
|
||||
|
||||
project: function(frm){
|
||||
frm.call({
|
||||
method: "add_timesheet_data",
|
||||
doc: frm.doc,
|
||||
callback: function(r, rt) {
|
||||
refresh_field(['timesheets'])
|
||||
}
|
||||
})
|
||||
frm.refresh();
|
||||
if (!frm.doc.is_return) {
|
||||
frm.call({
|
||||
method: "add_timesheet_data",
|
||||
doc: frm.doc,
|
||||
callback: function(r, rt) {
|
||||
refresh_field(['timesheets'])
|
||||
}
|
||||
})
|
||||
frm.refresh();
|
||||
}
|
||||
},
|
||||
|
||||
onload: function(frm) {
|
||||
@ -807,14 +819,27 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
}
|
||||
},
|
||||
|
||||
add_timesheet_row: function(frm, row, exchange_rate) {
|
||||
frm.add_child('timesheets', {
|
||||
'activity_type': row.activity_type,
|
||||
'description': row.description,
|
||||
'time_sheet': row.parent,
|
||||
'billing_hours': row.billing_hours,
|
||||
'billing_amount': flt(row.billing_amount) * flt(exchange_rate),
|
||||
'timesheet_detail': row.name
|
||||
});
|
||||
frm.refresh_field('timesheets');
|
||||
calculate_total_billing_amount(frm);
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.project) {
|
||||
if (frm.doc.docstatus===0 && !frm.doc.is_return) {
|
||||
frm.add_custom_button(__('Fetch Timesheet'), function() {
|
||||
let d = new frappe.ui.Dialog({
|
||||
title: __('Fetch Timesheet'),
|
||||
fields: [
|
||||
{
|
||||
"label" : "From",
|
||||
"label" : __("From"),
|
||||
"fieldname": "from_time",
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
@ -824,11 +849,18 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
fieldname: 'col_break_1',
|
||||
},
|
||||
{
|
||||
"label" : "To",
|
||||
"label" : __("To"),
|
||||
"fieldname": "to_time",
|
||||
"fieldtype": "Date",
|
||||
"reqd": 1,
|
||||
}
|
||||
},
|
||||
{
|
||||
"label" : __("Project"),
|
||||
"fieldname": "project",
|
||||
"fieldtype": "Link",
|
||||
"options": "Project",
|
||||
"default": frm.doc.project
|
||||
},
|
||||
],
|
||||
primary_action: function() {
|
||||
let data = d.get_values();
|
||||
@ -837,27 +869,35 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
args: {
|
||||
from_time: data.from_time,
|
||||
to_time: data.to_time,
|
||||
project: frm.doc.project
|
||||
project: data.project
|
||||
},
|
||||
callback: function(r) {
|
||||
if(!r.exc) {
|
||||
if(r.message.length > 0) {
|
||||
frm.clear_table('timesheets')
|
||||
r.message.forEach((d) => {
|
||||
frm.add_child('timesheets',{
|
||||
'time_sheet': d.parent,
|
||||
'billing_hours': d.billing_hours,
|
||||
'billing_amount': d.billing_amt,
|
||||
'timesheet_detail': d.name
|
||||
if (!r.exc && r.message.length > 0) {
|
||||
frm.clear_table('timesheets')
|
||||
r.message.forEach((d) => {
|
||||
let exchange_rate = 1.0;
|
||||
if (frm.doc.currency != d.currency) {
|
||||
frappe.call({
|
||||
method: 'erpnext.setup.utils.get_exchange_rate',
|
||||
args: {
|
||||
from_currency: d.currency,
|
||||
to_currency: frm.doc.currency
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
exchange_rate = r.message;
|
||||
frm.events.add_timesheet_row(frm, d, exchange_rate);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
frm.refresh_field('timesheets')
|
||||
}
|
||||
else {
|
||||
frappe.msgprint(__('No Timesheet Found.'))
|
||||
}
|
||||
d.hide();
|
||||
} else {
|
||||
frm.events.add_timesheet_row(frm, d, exchange_rate);
|
||||
}
|
||||
});
|
||||
} else {
|
||||
frappe.msgprint(__('No Timesheets found with the selected filters.'))
|
||||
}
|
||||
d.hide();
|
||||
}
|
||||
});
|
||||
},
|
||||
@ -867,6 +907,10 @@ frappe.ui.form.on('Sales Invoice', {
|
||||
})
|
||||
}
|
||||
|
||||
if (frm.doc.is_debit_note) {
|
||||
frm.set_df_property('return_against', 'label', 'Adjustment Against');
|
||||
}
|
||||
|
||||
if (frappe.boot.active_domains.includes("Healthcare")) {
|
||||
frm.set_df_property("patient", "hidden", 0);
|
||||
frm.set_df_property("patient_name", "hidden", 0);
|
||||
|
@ -16,6 +16,7 @@
|
||||
"is_pos",
|
||||
"is_consolidated",
|
||||
"is_return",
|
||||
"is_debit_note",
|
||||
"update_billed_amount_in_sales_order",
|
||||
"column_break1",
|
||||
"company",
|
||||
@ -392,7 +393,7 @@
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "return_against",
|
||||
"depends_on": "eval:doc.return_against || doc.is_debit_note",
|
||||
"fieldname": "return_against",
|
||||
"fieldtype": "Link",
|
||||
"hide_days": 1,
|
||||
@ -401,7 +402,7 @@
|
||||
"no_copy": 1,
|
||||
"options": "Sales Invoice",
|
||||
"print_hide": 1,
|
||||
"read_only": 1,
|
||||
"read_only_depends_on": "eval:doc.is_return",
|
||||
"search_index": 1
|
||||
},
|
||||
{
|
||||
@ -748,6 +749,7 @@
|
||||
{
|
||||
"collapsible": 1,
|
||||
"collapsible_depends_on": "eval:doc.total_billing_amount > 0",
|
||||
"depends_on": "eval: !doc.is_return",
|
||||
"fieldname": "time_sheet_list",
|
||||
"fieldtype": "Section Break",
|
||||
"hide_days": 1,
|
||||
@ -770,6 +772,7 @@
|
||||
"hide_days": 1,
|
||||
"hide_seconds": 1,
|
||||
"label": "Total Billing Amount",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -1951,6 +1954,12 @@
|
||||
"label": "Set Target Warehouse",
|
||||
"options": "Warehouse"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "is_debit_note",
|
||||
"fieldtype": "Check",
|
||||
"label": "Is Debit Note"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "grand_total",
|
||||
@ -1969,7 +1978,7 @@
|
||||
"link_fieldname": "consolidated_invoice"
|
||||
}
|
||||
],
|
||||
"modified": "2021-04-15 23:57:58.766651",
|
||||
"modified": "2021-05-20 22:48:33.988881",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice",
|
||||
|
@ -125,6 +125,8 @@ class SalesInvoice(SellingController):
|
||||
self.validate_multiple_billing("Delivery Note", "dn_detail", "amount", "items")
|
||||
if not self.is_return:
|
||||
self.validate_serial_numbers()
|
||||
else:
|
||||
self.timesheets = []
|
||||
self.update_packing_list()
|
||||
self.set_billing_hours_and_amount()
|
||||
self.update_timesheet_billing_for_project()
|
||||
@ -337,7 +339,7 @@ class SalesInvoice(SellingController):
|
||||
|
||||
if "Healthcare" in active_domains:
|
||||
manage_invoice_submit_cancel(self, "on_cancel")
|
||||
|
||||
self.unlink_sales_invoice_from_timesheets()
|
||||
self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
|
||||
|
||||
def update_status_updater_args(self):
|
||||
@ -393,6 +395,18 @@ class SalesInvoice(SellingController):
|
||||
if validate_against_credit_limit:
|
||||
check_credit_limit(self.customer, self.company, bypass_credit_limit_check_at_sales_order)
|
||||
|
||||
def unlink_sales_invoice_from_timesheets(self):
|
||||
for row in self.timesheets:
|
||||
timesheet = frappe.get_doc('Timesheet', row.time_sheet)
|
||||
for time_log in timesheet.time_logs:
|
||||
if time_log.sales_invoice == self.name:
|
||||
time_log.sales_invoice = None
|
||||
timesheet.calculate_total_amounts()
|
||||
timesheet.calculate_percentage_billed()
|
||||
timesheet.flags.ignore_validate_update_after_submit = True
|
||||
timesheet.set_status()
|
||||
timesheet.db_update_all()
|
||||
|
||||
@frappe.whitelist()
|
||||
def set_missing_values(self, for_validate=False):
|
||||
pos = self.set_pos_fields(for_validate)
|
||||
@ -427,7 +441,7 @@ class SalesInvoice(SellingController):
|
||||
timesheet.calculate_percentage_billed()
|
||||
timesheet.flags.ignore_validate_update_after_submit = True
|
||||
timesheet.set_status()
|
||||
timesheet.save()
|
||||
timesheet.db_update_all()
|
||||
|
||||
def update_time_sheet_detail(self, timesheet, args, sales_invoice):
|
||||
for data in timesheet.time_logs:
|
||||
@ -741,8 +755,10 @@ class SalesInvoice(SellingController):
|
||||
self.append('timesheets', {
|
||||
'time_sheet': data.parent,
|
||||
'billing_hours': data.billing_hours,
|
||||
'billing_amount': data.billing_amt,
|
||||
'timesheet_detail': data.name
|
||||
'billing_amount': data.billing_amount,
|
||||
'timesheet_detail': data.name,
|
||||
'activity_type': data.activity_type,
|
||||
'description': data.description
|
||||
})
|
||||
|
||||
self.calculate_billing_amount_for_timesheet()
|
||||
@ -1121,7 +1137,6 @@ class SalesInvoice(SellingController):
|
||||
"""
|
||||
self.set_serial_no_against_delivery_note()
|
||||
self.validate_serial_against_delivery_note()
|
||||
self.validate_serial_against_sales_invoice()
|
||||
|
||||
def set_serial_no_against_delivery_note(self):
|
||||
for item in self.items:
|
||||
@ -1152,26 +1167,6 @@ class SalesInvoice(SellingController):
|
||||
frappe.throw(_("Row {0}: {1} Serial numbers required for Item {2}. You have provided {3}.").format(
|
||||
item.idx, item.qty, item.item_code, len(si_serial_nos)))
|
||||
|
||||
def validate_serial_against_sales_invoice(self):
|
||||
""" check if serial number is already used in other sales invoice """
|
||||
for item in self.items:
|
||||
if not item.serial_no:
|
||||
continue
|
||||
|
||||
for serial_no in item.serial_no.split("\n"):
|
||||
serial_no_details = frappe.db.get_value("Serial No", serial_no,
|
||||
["sales_invoice", "item_code"], as_dict=1)
|
||||
|
||||
if not serial_no_details:
|
||||
continue
|
||||
|
||||
if serial_no_details.sales_invoice and serial_no_details.item_code == item.item_code \
|
||||
and self.name != serial_no_details.sales_invoice:
|
||||
sales_invoice_company = frappe.db.get_value("Sales Invoice", serial_no_details.sales_invoice, "company")
|
||||
if sales_invoice_company == self.company:
|
||||
frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}")
|
||||
.format(serial_no, serial_no_details.sales_invoice))
|
||||
|
||||
def update_project(self):
|
||||
if self.project:
|
||||
project = frappe.get_doc("Project", self.project)
|
||||
|
@ -933,12 +933,6 @@ class TestSalesInvoice(unittest.TestCase):
|
||||
self.assertFalse(frappe.db.get_value("Serial No", serial_nos[0], "warehouse"))
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0],
|
||||
"delivery_document_no"), si.name)
|
||||
self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "sales_invoice"),
|
||||
si.name)
|
||||
|
||||
# check if the serial number is already linked with any other Sales Invoice
|
||||
_si = frappe.copy_doc(si.as_dict())
|
||||
self.assertRaises(frappe.ValidationError, _si.insert)
|
||||
|
||||
return si
|
||||
|
||||
|
@ -1,172 +1,78 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2016-06-14 19:21:34.321662",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"actions": [],
|
||||
"creation": "2016-06-14 19:21:34.321662",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"activity_type",
|
||||
"description",
|
||||
"billing_hours",
|
||||
"billing_amount",
|
||||
"time_sheet",
|
||||
"timesheet_detail"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "time_sheet",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Time Sheet",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Timesheet",
|
||||
"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
|
||||
},
|
||||
"fieldname": "time_sheet",
|
||||
"fieldtype": "Link",
|
||||
"in_global_search": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Time Sheet",
|
||||
"options": "Timesheet",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "billing_hours",
|
||||
"fieldtype": "Float",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Billing Hours",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "billing_hours",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Billing Hours",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "billing_amount",
|
||||
"fieldtype": "Currency",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Billing Amount",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "billing_amount",
|
||||
"fieldtype": "Currency",
|
||||
"in_list_view": 1,
|
||||
"label": "Billing Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "timesheet_detail",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Timesheet Detail",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 1,
|
||||
"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_on_submit": 1,
|
||||
"fieldname": "timesheet_detail",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1,
|
||||
"label": "Timesheet Detail",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "activity_type",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Activity Type",
|
||||
"options": "Activity Type",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "description",
|
||||
"fieldtype": "Small Text",
|
||||
"in_list_view": 1,
|
||||
"label": "Description",
|
||||
"read_only": 1
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-02-18 18:50:44.770361",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Timesheet",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-20 22:33:57.234846",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Sales Invoice Timesheet",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -165,7 +165,7 @@ def add_data_for_operating_activities(
|
||||
if profit_data:
|
||||
profit_data.update({
|
||||
"indent": 1,
|
||||
"parent_account": get_mapper_for(light_mappers, position=0)['section_header']
|
||||
"parent_account": get_mapper_for(light_mappers, position=1)['section_header']
|
||||
})
|
||||
data.append(profit_data)
|
||||
section_data.append(profit_data)
|
||||
@ -312,10 +312,10 @@ def add_data_for_other_activities(
|
||||
def compute_data(filters, company_currency, profit_data, period_list, light_mappers, full_mapper):
|
||||
data = []
|
||||
|
||||
operating_activities_mapper = get_mapper_for(light_mappers, position=0)
|
||||
operating_activities_mapper = get_mapper_for(light_mappers, position=1)
|
||||
other_mappers = [
|
||||
get_mapper_for(light_mappers, position=1),
|
||||
get_mapper_for(light_mappers, position=2)
|
||||
get_mapper_for(light_mappers, position=2),
|
||||
get_mapper_for(light_mappers, position=3)
|
||||
]
|
||||
|
||||
if operating_activities_mapper:
|
||||
|
@ -0,0 +1,81 @@
|
||||
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
/* eslint-disable */
|
||||
|
||||
frappe.require("assets/erpnext/js/financial_statements.js", function() {
|
||||
frappe.query_reports["Dimension-wise Accounts Balance Report"] = {
|
||||
"filters": [
|
||||
{
|
||||
"fieldname": "company",
|
||||
"label": __("Company"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Company",
|
||||
"default": frappe.defaults.get_user_default("Company"),
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "fiscal_year",
|
||||
"label": __("Fiscal Year"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Fiscal Year",
|
||||
"default": frappe.defaults.get_user_default("fiscal_year"),
|
||||
"reqd": 1,
|
||||
"on_change": function(query_report) {
|
||||
var fiscal_year = query_report.get_values().fiscal_year;
|
||||
if (!fiscal_year) {
|
||||
return;
|
||||
}
|
||||
frappe.model.with_doc("Fiscal Year", fiscal_year, function(r) {
|
||||
var fy = frappe.model.get_doc("Fiscal Year", fiscal_year);
|
||||
frappe.query_report.set_filter_value({
|
||||
from_date: fy.year_start_date,
|
||||
to_date: fy.year_end_date
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
{
|
||||
"fieldname": "from_date",
|
||||
"label": __("From Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.defaults.get_user_default("year_start_date"),
|
||||
},
|
||||
{
|
||||
"fieldname": "to_date",
|
||||
"label": __("To Date"),
|
||||
"fieldtype": "Date",
|
||||
"default": frappe.defaults.get_user_default("year_end_date"),
|
||||
},
|
||||
{
|
||||
"fieldname": "finance_book",
|
||||
"label": __("Finance Book"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Finance Book",
|
||||
},
|
||||
{
|
||||
"fieldname": "dimension",
|
||||
"label": __("Select Dimension"),
|
||||
"fieldtype": "Select",
|
||||
"options": get_accounting_dimension_options(),
|
||||
"reqd": 1,
|
||||
},
|
||||
],
|
||||
"formatter": erpnext.financial_statements.formatter,
|
||||
"tree": true,
|
||||
"name_field": "account",
|
||||
"parent_field": "parent_account",
|
||||
"initial_depth": 3
|
||||
}
|
||||
|
||||
});
|
||||
|
||||
function get_accounting_dimension_options() {
|
||||
let options =["", "Cost Center", "Project"];
|
||||
frappe.db.get_list('Accounting Dimension',
|
||||
{fields:['document_type']}).then((res) => {
|
||||
res.forEach((dimension) => {
|
||||
options.push(dimension.document_type);
|
||||
});
|
||||
});
|
||||
return options
|
||||
}
|
@ -0,0 +1,22 @@
|
||||
{
|
||||
"add_total_row": 0,
|
||||
"columns": [],
|
||||
"creation": "2021-04-09 16:48:59.548018",
|
||||
"disable_prepared_report": 0,
|
||||
"disabled": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "Report",
|
||||
"filters": [],
|
||||
"idx": 0,
|
||||
"is_standard": "Yes",
|
||||
"modified": "2021-04-09 16:48:59.548018",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Accounts",
|
||||
"name": "Dimension-wise Accounts Balance Report",
|
||||
"owner": "Administrator",
|
||||
"prepared_report": 0,
|
||||
"ref_doctype": "GL Entry",
|
||||
"report_name": "Dimension-wise Accounts Balance Report",
|
||||
"report_type": "Script Report",
|
||||
"roles": []
|
||||
}
|
@ -0,0 +1,213 @@
|
||||
# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe, erpnext
|
||||
from frappe import _
|
||||
from frappe.utils import (flt, cstr)
|
||||
|
||||
from erpnext.accounts.report.financial_statements import filter_accounts, filter_out_zero_value_rows
|
||||
from erpnext.accounts.report.trial_balance.trial_balance import validate_filters
|
||||
|
||||
from six import itervalues
|
||||
|
||||
def execute(filters=None):
|
||||
validate_filters(filters)
|
||||
dimension_items_list = get_dimension_items_list(filters.dimension, filters.company)
|
||||
|
||||
if not dimension_items_list:
|
||||
return [], []
|
||||
|
||||
dimension_items_list = [''.join(d) for d in dimension_items_list]
|
||||
columns = get_columns(dimension_items_list)
|
||||
data = get_data(filters, dimension_items_list)
|
||||
|
||||
return columns, data
|
||||
|
||||
def get_data(filters, dimension_items_list):
|
||||
company_currency = erpnext.get_company_currency(filters.company)
|
||||
acc = frappe.db.sql("""
|
||||
select
|
||||
name, account_number, parent_account, lft, rgt, root_type,
|
||||
report_type, account_name, include_in_gross, account_type, is_group
|
||||
from
|
||||
`tabAccount`
|
||||
where
|
||||
company=%s
|
||||
order by lft""", (filters.company), as_dict=True)
|
||||
|
||||
if not acc:
|
||||
return None
|
||||
|
||||
accounts, accounts_by_name, parent_children_map = filter_accounts(acc)
|
||||
|
||||
min_lft, max_rgt = frappe.db.sql("""select min(lft), max(rgt) from `tabAccount`
|
||||
where company=%s""", (filters.company))[0]
|
||||
|
||||
account = frappe.db.sql_list("""select name from `tabAccount`
|
||||
where lft >= %s and rgt <= %s and company = %s""", (min_lft, max_rgt, filters.company))
|
||||
|
||||
gl_entries_by_account = {}
|
||||
set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account)
|
||||
format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list)
|
||||
accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list)
|
||||
out = prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list)
|
||||
out = filter_out_zero_value_rows(out, parent_children_map)
|
||||
|
||||
return out
|
||||
|
||||
def set_gl_entries_by_account(dimension_items_list, filters, account, gl_entries_by_account):
|
||||
for item in dimension_items_list:
|
||||
condition = get_condition(filters.from_date, item, filters.dimension)
|
||||
if account:
|
||||
condition += " and account in ({})"\
|
||||
.format(", ".join([frappe.db.escape(d) for d in account]))
|
||||
|
||||
gl_filters = {
|
||||
"company": filters.get("company"),
|
||||
"from_date": filters.get("from_date"),
|
||||
"to_date": filters.get("to_date"),
|
||||
"finance_book": cstr(filters.get("finance_book"))
|
||||
}
|
||||
|
||||
gl_filters['item'] = ''.join(item)
|
||||
|
||||
if filters.get("include_default_book_entries"):
|
||||
gl_filters["company_fb"] = frappe.db.get_value("Company",
|
||||
filters.company, 'default_finance_book')
|
||||
|
||||
for key, value in filters.items():
|
||||
if value:
|
||||
gl_filters.update({
|
||||
key: value
|
||||
})
|
||||
|
||||
gl_entries = frappe.db.sql("""
|
||||
select
|
||||
posting_date, account, debit, credit, is_opening, fiscal_year,
|
||||
debit_in_account_currency, credit_in_account_currency, account_currency
|
||||
from
|
||||
`tabGL Entry`
|
||||
where
|
||||
company=%(company)s
|
||||
{condition}
|
||||
and posting_date <= %(to_date)s
|
||||
and is_cancelled = 0
|
||||
order by account, posting_date""".format(
|
||||
condition=condition),
|
||||
gl_filters, as_dict=True) #nosec
|
||||
|
||||
for entry in gl_entries:
|
||||
entry['dimension_item'] = ''.join(item)
|
||||
gl_entries_by_account.setdefault(entry.account, []).append(entry)
|
||||
|
||||
def format_gl_entries(gl_entries_by_account, accounts_by_name, dimension_items_list):
|
||||
|
||||
for entries in itervalues(gl_entries_by_account):
|
||||
for entry in entries:
|
||||
d = accounts_by_name.get(entry.account)
|
||||
if not d:
|
||||
frappe.msgprint(
|
||||
_("Could not retrieve information for {0}.").format(entry.account), title="Error",
|
||||
raise_exception=1
|
||||
)
|
||||
for item in dimension_items_list:
|
||||
if item == entry.dimension_item:
|
||||
d[frappe.scrub(item)] = d.get(frappe.scrub(item), 0.0) + flt(entry.debit) - flt(entry.credit)
|
||||
|
||||
def prepare_data(accounts, filters, parent_children_map, company_currency, dimension_items_list):
|
||||
data = []
|
||||
|
||||
for d in accounts:
|
||||
has_value = False
|
||||
total = 0
|
||||
row = {
|
||||
"account": d.name,
|
||||
"parent_account": d.parent_account,
|
||||
"indent": d.indent,
|
||||
"from_date": filters.from_date,
|
||||
"to_date": filters.to_date,
|
||||
"currency": company_currency,
|
||||
"account_name": ('{} - {}'.format(d.account_number, d.account_name)
|
||||
if d.account_number else d.account_name)
|
||||
}
|
||||
|
||||
for item in dimension_items_list:
|
||||
row[frappe.scrub(item)] = flt(d.get(frappe.scrub(item), 0.0), 3)
|
||||
|
||||
if abs(row[frappe.scrub(item)]) >= 0.005:
|
||||
# ignore zero values
|
||||
has_value = True
|
||||
total += flt(d.get(frappe.scrub(item), 0.0), 3)
|
||||
|
||||
row["has_value"] = has_value
|
||||
row["total"] = total
|
||||
data.append(row)
|
||||
|
||||
return data
|
||||
|
||||
def accumulate_values_into_parents(accounts, accounts_by_name, dimension_items_list):
|
||||
"""accumulate children's values in parent accounts"""
|
||||
for d in reversed(accounts):
|
||||
if d.parent_account:
|
||||
for item in dimension_items_list:
|
||||
accounts_by_name[d.parent_account][frappe.scrub(item)] = \
|
||||
accounts_by_name[d.parent_account].get(frappe.scrub(item), 0.0) + d.get(frappe.scrub(item), 0.0)
|
||||
|
||||
def get_condition(from_date, item, dimension):
|
||||
conditions = []
|
||||
|
||||
if from_date:
|
||||
conditions.append("posting_date >= %(from_date)s")
|
||||
if dimension:
|
||||
if dimension not in ['Cost Center', 'Project']:
|
||||
if dimension in ['Customer', 'Supplier']:
|
||||
dimension = 'Party'
|
||||
else:
|
||||
dimension = 'Voucher No'
|
||||
txt = "{0} = %(item)s".format(frappe.scrub(dimension))
|
||||
conditions.append(txt)
|
||||
|
||||
return " and {}".format(" and ".join(conditions)) if conditions else ""
|
||||
|
||||
def get_dimension_items_list(dimension, company):
|
||||
meta = frappe.get_meta(dimension, cached=False)
|
||||
fieldnames = [d.fieldname for d in meta.get("fields")]
|
||||
filters = {}
|
||||
if 'company' in fieldnames:
|
||||
filters['company'] = company
|
||||
return frappe.get_all(dimension, filters, as_list=True)
|
||||
|
||||
def get_columns(dimension_items_list, accumulated_values=1, company=None):
|
||||
columns = [{
|
||||
"fieldname": "account",
|
||||
"label": _("Account"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Account",
|
||||
"width": 300
|
||||
}]
|
||||
if company:
|
||||
columns.append({
|
||||
"fieldname": "currency",
|
||||
"label": _("Currency"),
|
||||
"fieldtype": "Link",
|
||||
"options": "Currency",
|
||||
"hidden": 1
|
||||
})
|
||||
for item in dimension_items_list:
|
||||
columns.append({
|
||||
"fieldname": frappe.scrub(item),
|
||||
"label": item,
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 150
|
||||
})
|
||||
columns.append({
|
||||
"fieldname": "total",
|
||||
"label": "Total",
|
||||
"fieldtype": "Currency",
|
||||
"options": "currency",
|
||||
"width": 150
|
||||
})
|
||||
|
||||
return columns
|
@ -166,6 +166,11 @@ frappe.query_reports["General Ledger"] = {
|
||||
"fieldname": "show_cancelled_entries",
|
||||
"label": __("Show Cancelled Entries"),
|
||||
"fieldtype": "Check"
|
||||
},
|
||||
{
|
||||
"fieldname": "show_net_values_in_party_account",
|
||||
"label": __("Show Net Values in Party Account"),
|
||||
"fieldtype": "Check"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
@ -344,6 +344,9 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
consolidated_gle = OrderedDict()
|
||||
group_by = group_by_field(filters.get('group_by'))
|
||||
|
||||
if filters.get('show_net_values_in_party_account'):
|
||||
account_type_map = get_account_type_map(filters.get('company'))
|
||||
|
||||
def update_value_in_dict(data, key, gle):
|
||||
data[key].debit += flt(gle.debit)
|
||||
data[key].credit += flt(gle.credit)
|
||||
@ -351,6 +354,24 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
data[key].debit_in_account_currency += flt(gle.debit_in_account_currency)
|
||||
data[key].credit_in_account_currency += flt(gle.credit_in_account_currency)
|
||||
|
||||
if filters.get('show_net_values_in_party_account') and \
|
||||
account_type_map.get(data[key].account) in ('Receivable', 'Payable'):
|
||||
net_value = flt(data[key].debit) - flt(data[key].credit)
|
||||
net_value_in_account_currency = flt(data[key].debit_in_account_currency) \
|
||||
- flt(data[key].credit_in_account_currency)
|
||||
|
||||
if net_value < 0:
|
||||
dr_or_cr = 'credit'
|
||||
rev_dr_or_cr = 'debit'
|
||||
else:
|
||||
dr_or_cr = 'debit'
|
||||
rev_dr_or_cr = 'credit'
|
||||
|
||||
data[key][dr_or_cr] = abs(net_value)
|
||||
data[key][dr_or_cr+'_in_account_currency'] = abs(net_value_in_account_currency)
|
||||
data[key][rev_dr_or_cr] = 0
|
||||
data[key][rev_dr_or_cr+'_in_account_currency'] = 0
|
||||
|
||||
if data[key].against_voucher and gle.against_voucher:
|
||||
data[key].against_voucher += ', ' + gle.against_voucher
|
||||
|
||||
@ -388,6 +409,12 @@ def get_accountwise_gle(filters, accounting_dimensions, gl_entries, gle_map):
|
||||
|
||||
return totals, entries
|
||||
|
||||
def get_account_type_map(company):
|
||||
account_type_map = frappe._dict(frappe.get_all('Account', fields=['name', 'account_type'],
|
||||
filters={'company': company}, as_list=1))
|
||||
|
||||
return account_type_map
|
||||
|
||||
def get_result_as_list(data, filters):
|
||||
balance, balance_in_account_currency = 0, 0
|
||||
inv_details = get_supplier_invoice_details()
|
||||
|
@ -81,8 +81,7 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
|
||||
presentation_currency = currency_info['presentation_currency']
|
||||
company_currency = currency_info['company_currency']
|
||||
|
||||
pl_accounts = [d.name for d in frappe.get_list('Account',
|
||||
filters={'report_type': 'Profit and Loss', 'company': company})]
|
||||
account_currencies = list(set(entry['account_currency'] for entry in gl_entries))
|
||||
|
||||
for entry in gl_entries:
|
||||
account = entry['account']
|
||||
@ -92,10 +91,15 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
|
||||
credit_in_account_currency = flt(entry['credit_in_account_currency'])
|
||||
account_currency = entry['account_currency']
|
||||
|
||||
if account_currency != presentation_currency:
|
||||
value = debit or credit
|
||||
if len(account_currencies) == 1 and account_currency == presentation_currency:
|
||||
if entry.get('debit'):
|
||||
entry['debit'] = debit_in_account_currency
|
||||
|
||||
date = entry['posting_date'] if account in pl_accounts else currency_info['report_date']
|
||||
if entry.get('credit'):
|
||||
entry['credit'] = credit_in_account_currency
|
||||
else:
|
||||
value = debit or credit
|
||||
date = currency_info['report_date']
|
||||
converted_value = convert(value, presentation_currency, company_currency, date)
|
||||
|
||||
if entry.get('debit'):
|
||||
@ -104,13 +108,6 @@ def convert_to_presentation_currency(gl_entries, currency_info, company):
|
||||
if entry.get('credit'):
|
||||
entry['credit'] = converted_value
|
||||
|
||||
elif account_currency == presentation_currency:
|
||||
if entry.get('debit'):
|
||||
entry['debit'] = debit_in_account_currency
|
||||
|
||||
if entry.get('credit'):
|
||||
entry['credit'] = credit_in_account_currency
|
||||
|
||||
converted_gl_list.append(entry)
|
||||
|
||||
return converted_gl_list
|
||||
|
@ -4,7 +4,7 @@
|
||||
frappe.ui.form.on('Asset Category', {
|
||||
onload: function(frm) {
|
||||
frm.add_fetch('company_name', 'accumulated_depreciation_account', 'accumulated_depreciation_account');
|
||||
frm.add_fetch('company_name', 'depreciation_expense_account', 'accumulated_depreciation_account');
|
||||
frm.add_fetch('company_name', 'depreciation_expense_account', 'depreciation_expense_account');
|
||||
|
||||
frm.set_query('fixed_asset_account', 'accounts', function(doc, cdt, cdn) {
|
||||
var d = locals[cdt][cdn];
|
||||
|
@ -62,6 +62,7 @@ class RequestforQuotation(BuyingController):
|
||||
for supplier in self.suppliers:
|
||||
supplier.email_sent = 0
|
||||
supplier.quote_status = 'Pending'
|
||||
self.send_to_supplier()
|
||||
|
||||
def on_cancel(self):
|
||||
frappe.db.set(self, 'status', 'Cancelled')
|
||||
@ -81,7 +82,7 @@ class RequestforQuotation(BuyingController):
|
||||
def send_to_supplier(self):
|
||||
"""Sends RFQ mail to involved suppliers."""
|
||||
for rfq_supplier in self.suppliers:
|
||||
if rfq_supplier.send_email:
|
||||
if rfq_supplier.email_id is not None and rfq_supplier.send_email:
|
||||
self.validate_email_id(rfq_supplier)
|
||||
|
||||
# make new user if required
|
||||
|
@ -383,8 +383,14 @@
|
||||
"icon": "fa fa-user",
|
||||
"idx": 370,
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"modified": "2021-01-06 19:51:40.939087",
|
||||
"links": [
|
||||
{
|
||||
"group": "Item Group",
|
||||
"link_doctype": "Supplier Item Group",
|
||||
"link_fieldname": "supplier"
|
||||
}
|
||||
],
|
||||
"modified": "2021-05-18 15:10:11.087191",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier",
|
||||
|
@ -0,0 +1,8 @@
|
||||
// Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Supplier Item Group', {
|
||||
// refresh: function(frm) {
|
||||
|
||||
// }
|
||||
});
|
@ -0,0 +1,77 @@
|
||||
{
|
||||
"actions": [],
|
||||
"creation": "2021-05-07 18:16:40.621421",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"supplier",
|
||||
"item_group"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "supplier",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Supplier",
|
||||
"options": "Supplier",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "item_group",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Item Group",
|
||||
"options": "Item Group",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-19 13:48:16.742303",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Supplier Item Group",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Purchase Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,18 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
|
||||
class SupplierItemGroup(Document):
|
||||
def validate(self):
|
||||
exists = frappe.db.exists({
|
||||
'doctype': 'Supplier Item Group',
|
||||
'supplier': self.supplier,
|
||||
'item_group': self.item_group
|
||||
})
|
||||
if exists:
|
||||
frappe.throw(_("Item Group has already been linked to this supplier."))
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2021, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestSupplierItemGroup(unittest.TestCase):
|
||||
pass
|
54
erpnext/change_log/v13/v13_4_0.md
Normal file
54
erpnext/change_log/v13/v13_4_0.md
Normal file
@ -0,0 +1,54 @@
|
||||
# Version 13.4.0 Release Notes
|
||||
|
||||
### Features & Enhancements
|
||||
|
||||
- Multiple GST enhancement and fixes ([#25249](https://github.com/frappe/erpnext/pull/25249))
|
||||
- Linking supplier with an item group for filtering items ([#25683](https://github.com/frappe/erpnext/pull/25683))
|
||||
- Leave Policy Assignment Refactor ([#24327](https://github.com/frappe/erpnext/pull/24327))
|
||||
- Dimension-wise Accounts Balance Report ([#25260](https://github.com/frappe/erpnext/pull/25260))
|
||||
- Show net values in Party Accounts ([#25714](https://github.com/frappe/erpnext/pull/25714))
|
||||
- Add pending qty section to batch/serial selector dialog ([#25519](https://github.com/frappe/erpnext/pull/25519))
|
||||
- enhancements in Training Event ([#25782](https://github.com/frappe/erpnext/pull/25782))
|
||||
- Refactored timesheet ([#25701](https://github.com/frappe/erpnext/pull/25701))
|
||||
|
||||
### Fixes
|
||||
|
||||
- Process Statement of Accounts formatting ([#25777](https://github.com/frappe/erpnext/pull/25777))
|
||||
- Removed serial no validation for sales invoice ([#25817](https://github.com/frappe/erpnext/pull/25817))
|
||||
- Fetch email id from dialog box in pos past order summary ([#25808](https://github.com/frappe/erpnext/pull/25808))
|
||||
- Don't map set warehouse from delivery note to purchase receipt ([#25672](https://github.com/frappe/erpnext/pull/25672))
|
||||
- Apply permission while selecting projects ([#25765](https://github.com/frappe/erpnext/pull/25765))
|
||||
- Error on adding bank account to plaid ([#25658](https://github.com/frappe/erpnext/pull/25658))
|
||||
- Set disable rounded total if it is globally enabled ([#25789](https://github.com/frappe/erpnext/pull/25789))
|
||||
- Wrong amount on CR side in general ledger report for customer when different account currencies are involved ([#25654](https://github.com/frappe/erpnext/pull/25654))
|
||||
- Stock move dialog duplicate submit actions (V13) ([#25486](https://github.com/frappe/erpnext/pull/25486))
|
||||
- Cashflow mapper not showing data ([#25815](https://github.com/frappe/erpnext/pull/25815))
|
||||
- Ignore rounding diff while importing JV using data import ([#25816](https://github.com/frappe/erpnext/pull/25816))
|
||||
- Woocommerce order sync issue ([#25688](https://github.com/frappe/erpnext/pull/25688))
|
||||
- Expected amount in pos closing payments table ([#25737](https://github.com/frappe/erpnext/pull/25737))
|
||||
- Show only company addresses for ITC reversal entry ([#25867](https://github.com/frappe/erpnext/pull/25867))
|
||||
- Timeout error while loading warehouse tree ([#25694](https://github.com/frappe/erpnext/pull/25694))
|
||||
- Plaid Withdrawals and Deposits are recorded incorrectly ([#25784](https://github.com/frappe/erpnext/pull/25784))
|
||||
- Return case for item with available qty equal to one ([#25760](https://github.com/frappe/erpnext/pull/25760))
|
||||
- The status of repost item valuation showing In Progress since long time ([#25754](https://github.com/frappe/erpnext/pull/25754))
|
||||
- Updated applicable charges form in landed cost voucher ([#25732](https://github.com/frappe/erpnext/pull/25732))
|
||||
- Rearrange buttons for Company DocType ([#25617](https://github.com/frappe/erpnext/pull/25617))
|
||||
- Show uom for item in selector dialog ([#25697](https://github.com/frappe/erpnext/pull/25697))
|
||||
- Warehouse not found in stock entry ([#25776](https://github.com/frappe/erpnext/pull/25776))
|
||||
- Use dictionary filter instead of list (bp #25874 pre-release) ([#25875](https://github.com/frappe/erpnext/pull/25875))
|
||||
- Send emails on rfq submit ([#25695](https://github.com/frappe/erpnext/pull/25695))
|
||||
- Cannot bypass e-invoicing for non gst item invoices ([#25759](https://github.com/frappe/erpnext/pull/25759))
|
||||
- Validation message of quality inspection in purchase receipt ([#25666](https://github.com/frappe/erpnext/pull/25666))
|
||||
- Dialog variable assignment after definition in POS ([#25681](https://github.com/frappe/erpnext/pull/25681))
|
||||
- Wrong quantity after transaction for parallel stock transactions ([#25779](https://github.com/frappe/erpnext/pull/25779))
|
||||
- Item Variant Details Report ([#25797](https://github.com/frappe/erpnext/pull/25797))
|
||||
- Duplicate stock entry on multiple click ([#25742](https://github.com/frappe/erpnext/pull/25742))
|
||||
- Bank statement import via google sheet ([#25676](https://github.com/frappe/erpnext/pull/25676))
|
||||
- Change today to now to get data for reposting ([#25702](https://github.com/frappe/erpnext/pull/25702))
|
||||
- Parameter for get_filtered_list_for_consolidated_report in consolidated balance sheet ([#25698](https://github.com/frappe/erpnext/pull/25698))
|
||||
- Ageing error in PSOA ([#25857](https://github.com/frappe/erpnext/pull/25857))
|
||||
- Breaking cost center validation ([#25660](https://github.com/frappe/erpnext/pull/25660))
|
||||
- Project filter for Kanban Board ([#25744](https://github.com/frappe/erpnext/pull/25744))
|
||||
- Show allow zero valuation only when auto checked ([#25778](https://github.com/frappe/erpnext/pull/25778))
|
||||
- Missing cost center message on creating gl entries ([#25755](https://github.com/frappe/erpnext/pull/25755))
|
||||
- Address template with upper filter throws jinja error ([#25756](https://github.com/frappe/erpnext/pull/25756))
|
@ -368,6 +368,11 @@ class AccountsController(TransactionBase):
|
||||
if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'):
|
||||
item.set('is_fixed_asset', ret.get('is_fixed_asset', 0))
|
||||
|
||||
# Double check for cost center
|
||||
# Items add via promotional scheme may not have cost center set
|
||||
if hasattr(item, 'cost_center') and not item.get('cost_center'):
|
||||
item.set('cost_center', self.get('cost_center') or erpnext.get_default_cost_center(self.company))
|
||||
|
||||
if ret.get("pricing_rules"):
|
||||
self.apply_pricing_rule_on_items(item, ret)
|
||||
self.set_pricing_rule_details(item, ret)
|
||||
@ -1006,7 +1011,7 @@ class AccountsController(TransactionBase):
|
||||
else:
|
||||
grand_total -= self.get("total_advance")
|
||||
base_grand_total = flt(grand_total * self.get("conversion_rate"), self.precision("base_grand_total"))
|
||||
print(grand_total, base_grand_total)
|
||||
|
||||
if total != flt(grand_total, self.precision("grand_total")) or \
|
||||
base_total != flt(base_grand_total, self.precision("base_grand_total")):
|
||||
frappe.throw(_("Total Payment Amount in Payment Schedule must be equal to Grand / Rounded Total"))
|
||||
|
@ -204,7 +204,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
|
||||
if "description" in searchfields:
|
||||
searchfields.remove("description")
|
||||
|
||||
|
||||
columns = ''
|
||||
extra_searchfields = [field for field in searchfields
|
||||
if not field in ["name", "item_group", "description"]]
|
||||
@ -216,11 +216,22 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals
|
||||
if not field in searchfields]
|
||||
searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
|
||||
|
||||
if filters.get('supplier'):
|
||||
item_group_list = frappe.get_all('Supplier Item Group', filters = {'supplier': filters.get('supplier')}, fields = ['item_group'])
|
||||
|
||||
item_groups = []
|
||||
for i in item_group_list:
|
||||
item_groups.append(i.item_group)
|
||||
|
||||
del filters['supplier']
|
||||
|
||||
if item_groups:
|
||||
filters['item_group'] = ['in', item_groups]
|
||||
|
||||
description_cond = ''
|
||||
if frappe.db.count('Item', cache=True) < 50000:
|
||||
# scan description only if items are less than 50000
|
||||
description_cond = 'or tabItem.description LIKE %(txt)s'
|
||||
|
||||
return frappe.db.sql("""select tabItem.name,
|
||||
if(length(tabItem.item_name) > 40,
|
||||
concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name,
|
||||
|
@ -76,12 +76,12 @@ status_map = {
|
||||
["Stopped", "eval:self.status == 'Stopped'"],
|
||||
["Cancelled", "eval:self.docstatus == 2"],
|
||||
["Pending", "eval:self.status != 'Stopped' and self.per_ordered == 0 and self.docstatus == 1"],
|
||||
["Partially Ordered", "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1"],
|
||||
["Ordered", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
|
||||
["Transferred", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Transfer'"],
|
||||
["Issued", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Material Issue'"],
|
||||
["Received", "eval:self.status != 'Stopped' and self.per_received == 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
|
||||
["Partially Received", "eval:self.status != 'Stopped' and self.per_received > 0 and self.per_received < 100 and self.docstatus == 1 and self.material_request_type == 'Purchase'"],
|
||||
["Partially Ordered", "eval:self.status != 'Stopped' and self.per_ordered < 100 and self.per_ordered > 0 and self.docstatus == 1"],
|
||||
["Manufactured", "eval:self.status != 'Stopped' and self.per_ordered == 100 and self.docstatus == 1 and self.material_request_type == 'Manufacture'"]
|
||||
],
|
||||
"Bank Transaction": [
|
||||
|
@ -7,6 +7,7 @@
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import urllib
|
||||
from urllib.parse import quote
|
||||
import hashlib
|
||||
import hmac
|
||||
import base64
|
||||
@ -68,8 +69,9 @@ def calc_md5(string):
|
||||
"""
|
||||
md = hashlib.md5()
|
||||
md.update(string)
|
||||
return base64.encodestring(md.digest()).strip('\n') if six.PY2 \
|
||||
else base64.encodebytes(md.digest()).decode().strip()
|
||||
return base64.encodebytes(md.digest()).decode().strip()
|
||||
|
||||
|
||||
|
||||
def remove_empty(d):
|
||||
"""
|
||||
@ -177,7 +179,6 @@ class MWS(object):
|
||||
'SignatureMethod': 'HmacSHA256',
|
||||
}
|
||||
params.update(extra_data)
|
||||
quote = urllib.quote if six.PY2 else urllib.parse.quote
|
||||
request_description = '&'.join(['%s=%s' % (k, quote(params[k], safe='-_.~')) for k in sorted(params)])
|
||||
signature = self.calc_signature(method, request_description)
|
||||
url = '%s%s?%s&Signature=%s' % (self.domain, self.uri, request_description, quote(signature))
|
||||
|
@ -90,9 +90,9 @@ def add_bank_accounts(response, bank, company):
|
||||
"bank": bank["bank_name"],
|
||||
"account": default_gl_account.account,
|
||||
"account_name": account["name"],
|
||||
"account_type": account["type"] or "",
|
||||
"account_subtype": account["subtype"] or "",
|
||||
"mask": account["mask"] or "",
|
||||
"account_type": account.get("type", ""),
|
||||
"account_subtype": account.get("subtype", ""),
|
||||
"mask": account.get("mask", ""),
|
||||
"integration_id": account["id"],
|
||||
"is_company_account": 1,
|
||||
"company": company
|
||||
@ -183,11 +183,11 @@ def new_bank_transaction(transaction):
|
||||
bank_account = frappe.db.get_value("Bank Account", dict(integration_id=transaction["account_id"]))
|
||||
|
||||
if float(transaction["amount"]) >= 0:
|
||||
debit = float(transaction["amount"])
|
||||
credit = 0
|
||||
else:
|
||||
debit = 0
|
||||
credit = abs(float(transaction["amount"]))
|
||||
credit = float(transaction["amount"])
|
||||
else:
|
||||
debit = abs(float(transaction["amount"]))
|
||||
credit = 0
|
||||
|
||||
status = "Pending" if transaction["pending"] == "True" else "Settled"
|
||||
|
||||
|
@ -268,10 +268,12 @@ doc_events = {
|
||||
},
|
||||
"Purchase Invoice": {
|
||||
"validate": [
|
||||
"erpnext.regional.india.utils.update_grand_total_for_rcm",
|
||||
"erpnext.regional.india.utils.validate_reverse_charge_transaction",
|
||||
"erpnext.regional.india.utils.update_itc_availed_fields",
|
||||
"erpnext.regional.united_arab_emirates.utils.update_grand_total_for_rcm",
|
||||
"erpnext.regional.united_arab_emirates.utils.validate_returns"
|
||||
]
|
||||
"erpnext.regional.united_arab_emirates.utils.validate_returns",
|
||||
"erpnext.regional.india.utils.update_taxable_values"
|
||||
]
|
||||
},
|
||||
"Payment Entry": {
|
||||
"on_submit": ["erpnext.regional.create_transaction_log", "erpnext.accounts.doctype.payment_request.payment_request.update_payment_req_status", "erpnext.accounts.doctype.dunning.dunning.resolve_dunning"],
|
||||
@ -365,10 +367,8 @@ scheduler_events = {
|
||||
"erpnext.setup.doctype.email_digest.email_digest.send",
|
||||
"erpnext.manufacturing.doctype.bom_update_tool.bom_update_tool.update_latest_price_in_all_boms",
|
||||
"erpnext.hr.doctype.leave_ledger_entry.leave_ledger_entry.process_expired_allocation",
|
||||
"erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.automatically_allocate_leaves_based_on_leave_policy",
|
||||
"erpnext.hr.utils.generate_leave_encashment",
|
||||
"erpnext.hr.utils.allocate_earned_leaves",
|
||||
"erpnext.hr.utils.grant_leaves_automatically",
|
||||
"erpnext.loan_management.doctype.process_loan_security_shortfall.process_loan_security_shortfall.create_process_loan_security_shortfall",
|
||||
"erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual.process_loan_interest_accrual_for_term_loans",
|
||||
"erpnext.crm.doctype.lead.lead.daily_open_lead"
|
||||
@ -425,7 +425,6 @@ regional_overrides = {
|
||||
'erpnext.controllers.taxes_and_totals.get_regional_round_off_accounts': 'erpnext.regional.india.utils.get_regional_round_off_accounts',
|
||||
'erpnext.hr.utils.calculate_annual_eligible_hra_exemption': 'erpnext.regional.india.utils.calculate_annual_eligible_hra_exemption',
|
||||
'erpnext.hr.utils.calculate_hra_exemption_for_period': 'erpnext.regional.india.utils.calculate_hra_exemption_for_period',
|
||||
'erpnext.accounts.doctype.purchase_invoice.purchase_invoice.make_regional_gl_entries': 'erpnext.regional.india.utils.make_regional_gl_entries',
|
||||
'erpnext.controllers.accounts_controller.validate_einvoice_fields': 'erpnext.regional.india.e_invoice.utils.validate_einvoice_fields',
|
||||
'erpnext.assets.doctype.asset.asset.get_depreciation_amount': 'erpnext.regional.india.utils.get_depreciation_amount'
|
||||
},
|
||||
|
@ -11,8 +11,12 @@ def get_data():
|
||||
},
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Leave and Attendance'),
|
||||
'items': ['Attendance', 'Attendance Request', 'Leave Application', 'Leave Allocation', 'Employee Checkin']
|
||||
'label': _('Attendance'),
|
||||
'items': ['Attendance', 'Attendance Request', 'Employee Checkin']
|
||||
},
|
||||
{
|
||||
'label': _('Leave'),
|
||||
'items': ['Leave Application', 'Leave Allocation', 'Leave Policy Assignment']
|
||||
},
|
||||
{
|
||||
'label': _('Lifecycle'),
|
||||
@ -30,10 +34,6 @@ def get_data():
|
||||
'label': _('Benefit'),
|
||||
'items': ['Employee Benefit Application', 'Employee Benefit Claim']
|
||||
},
|
||||
{
|
||||
'label': _('Evaluation'),
|
||||
'items': ['Appraisal']
|
||||
},
|
||||
{
|
||||
'label': _('Payroll'),
|
||||
'items': ['Salary Structure Assignment', 'Salary Slip', 'Additional Salary', 'Timesheet','Employee Incentive', 'Retention Bonus', 'Bank Account']
|
||||
@ -42,5 +42,9 @@ def get_data():
|
||||
'label': _('Training'),
|
||||
'items': ['Training Event', 'Training Result', 'Training Feedback', 'Employee Skill Map']
|
||||
},
|
||||
{
|
||||
'label': _('Evaluation'),
|
||||
'items': ['Appraisal']
|
||||
},
|
||||
]
|
||||
}
|
||||
|
@ -23,7 +23,6 @@
|
||||
"show_leaves_of_all_department_members_in_calendar",
|
||||
"auto_leave_encashment",
|
||||
"restrict_backdated_leave_application",
|
||||
"automatically_allocate_leaves_based_on_leave_policy",
|
||||
"hiring_settings",
|
||||
"check_vacancies"
|
||||
],
|
||||
@ -133,12 +132,6 @@
|
||||
"label": "Role Allowed to Create Backdated Leave Application",
|
||||
"options": "Role"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "automatically_allocate_leaves_based_on_leave_policy",
|
||||
"fieldtype": "Check",
|
||||
"label": "Automatically Allocate Leaves Based On Leave Policy"
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "send_leave_notification",
|
||||
@ -155,7 +148,7 @@
|
||||
"idx": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2021-04-26 10:52:56.192773",
|
||||
"modified": "2021-05-11 10:52:56.192773",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "HR Settings",
|
||||
|
@ -446,8 +446,6 @@ class TestLeaveApplication(unittest.TestCase):
|
||||
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||
|
||||
frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
|
||||
|
||||
from erpnext.hr.utils import allocate_earned_leaves
|
||||
i = 0
|
||||
while(i<14):
|
||||
|
@ -44,10 +44,6 @@ class TestLeaveEncashment(unittest.TestCase):
|
||||
salary_structure = make_salary_structure("Salary Structure for Encashment", "Monthly", self.employee,
|
||||
other_details={"leave_encashment_amount_per_day": 50})
|
||||
|
||||
#grant Leaves
|
||||
frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0]).grant_leave_alloc_for_employee()
|
||||
|
||||
|
||||
def tearDown(self):
|
||||
for dt in ["Leave Period", "Leave Allocation", "Leave Ledger Entry", "Additional Salary", "Leave Encashment", "Salary Structure", "Leave Policy"]:
|
||||
frappe.db.sql("delete from `tab%s`" % dt)
|
||||
|
@ -7,7 +7,7 @@ def get_data():
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Leaves'),
|
||||
'items': ['Leave Allocation']
|
||||
'items': ['Leave Policy Assignment', 'Leave Allocation']
|
||||
},
|
||||
]
|
||||
}
|
@ -4,35 +4,22 @@
|
||||
frappe.ui.form.on('Leave Policy Assignment', {
|
||||
onload: function(frm) {
|
||||
frm.ignore_doctypes_on_cancel_all = ["Leave Ledger Entry"];
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
if (frm.doc.docstatus === 1 && frm.doc.leaves_allocated === 0) {
|
||||
frm.add_custom_button(__("Grant Leave"), function() {
|
||||
|
||||
frappe.call({
|
||||
doc: frm.doc,
|
||||
method: "grant_leave_alloc_for_employee",
|
||||
callback: function(r) {
|
||||
let leave_allocations = r.message;
|
||||
let msg = frm.events.get_success_message(leave_allocations);
|
||||
frappe.msgprint(msg);
|
||||
cur_frm.refresh();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
get_success_message: function(leave_allocations) {
|
||||
let msg = __("Leaves has been granted successfully");
|
||||
msg += "<br><table class='table table-bordered'>";
|
||||
msg += "<tr><th>"+__('Leave Type')+"</th><th>"+__("Leave Allocation")+"</th><th>"+__("Leaves Granted")+"</th><tr>";
|
||||
for (let key in leave_allocations) {
|
||||
msg += "<tr><th>"+key+"</th><td>"+leave_allocations[key]["name"]+"</td><td>"+leave_allocations[key]["leaves"]+"</td></tr>";
|
||||
}
|
||||
msg += "</table>";
|
||||
return msg;
|
||||
frm.set_query('leave_policy', function() {
|
||||
return {
|
||||
filters: {
|
||||
"docstatus": 1
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query('leave_period', function() {
|
||||
return {
|
||||
filters: {
|
||||
"is_active": 1,
|
||||
"company": frm.doc.company
|
||||
}
|
||||
};
|
||||
});
|
||||
},
|
||||
|
||||
assignment_based_on: function(frm) {
|
||||
|
@ -17,6 +17,9 @@ class LeavePolicyAssignment(Document):
|
||||
self.validate_policy_assignment_overlap()
|
||||
self.set_dates()
|
||||
|
||||
def on_submit(self):
|
||||
self.grant_leave_alloc_for_employee()
|
||||
|
||||
def set_dates(self):
|
||||
if self.assignment_based_on == "Leave Period":
|
||||
self.effective_from, self.effective_to = frappe.db.get_value("Leave Period", self.leave_period, ["from_date", "to_date"])
|
||||
@ -75,7 +78,7 @@ class LeavePolicyAssignment(Document):
|
||||
from_date=self.effective_from,
|
||||
to_date=self.effective_to,
|
||||
new_leaves_allocated=new_leaves_allocated,
|
||||
leave_period=self.leave_period or None,
|
||||
leave_period=self.leave_period if self.assignment_based_on == "Leave Policy" else '',
|
||||
leave_policy_assignment = self.name,
|
||||
leave_policy = self.leave_policy,
|
||||
carry_forward=carry_forward
|
||||
@ -131,22 +134,6 @@ class LeavePolicyAssignment(Document):
|
||||
return new_leaves_allocated
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def grant_leave_for_multiple_employees(leave_policy_assignments):
|
||||
leave_policy_assignments = json.loads(leave_policy_assignments)
|
||||
not_granted = []
|
||||
for assignment in leave_policy_assignments:
|
||||
try:
|
||||
frappe.get_doc("Leave Policy Assignment", assignment).grant_leave_alloc_for_employee()
|
||||
except Exception:
|
||||
not_granted.append(assignment)
|
||||
|
||||
if len(not_granted):
|
||||
msg = _("Leave not Granted for Assignments:")+ bold(comma_and(not_granted)) + _(". Please Check documents")
|
||||
else:
|
||||
msg = _("Leave granted Successfully")
|
||||
frappe.msgprint(msg)
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_assignment_for_multiple_employees(employees, data):
|
||||
|
||||
@ -166,29 +153,18 @@ def create_assignment_for_multiple_employees(employees, data):
|
||||
assignment.effective_to = getdate(data.effective_to) or None
|
||||
assignment.leave_period = data.leave_period or None
|
||||
assignment.carry_forward = data.carry_forward
|
||||
|
||||
assignment.save()
|
||||
assignment.submit()
|
||||
try:
|
||||
assignment.submit()
|
||||
except frappe.exceptions.ValidationError:
|
||||
continue
|
||||
|
||||
frappe.db.commit()
|
||||
|
||||
docs_name.append(assignment.name)
|
||||
|
||||
return docs_name
|
||||
|
||||
|
||||
def automatically_allocate_leaves_based_on_leave_policy():
|
||||
today = getdate()
|
||||
automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_single_value(
|
||||
'HR Settings', 'automatically_allocate_leaves_based_on_leave_policy'
|
||||
)
|
||||
|
||||
pending_assignments = frappe.get_list(
|
||||
"Leave Policy Assignment",
|
||||
filters = {"docstatus": 1, "leaves_allocated": 0, "effective_from": today}
|
||||
)
|
||||
|
||||
if len(pending_assignments) and automatically_allocate_leaves_based_on_leave_policy:
|
||||
for assignment in pending_assignments:
|
||||
frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
|
||||
|
||||
|
||||
def get_leave_type_details():
|
||||
leave_type_details = frappe._dict()
|
||||
leave_types = frappe.get_all("Leave Type",
|
||||
@ -197,4 +173,3 @@ def get_leave_type_details():
|
||||
for d in leave_types:
|
||||
leave_type_details.setdefault(d.name, d)
|
||||
return leave_type_details
|
||||
|
||||
|
@ -0,0 +1,13 @@
|
||||
from __future__ import unicode_literals
|
||||
from frappe import _
|
||||
|
||||
def get_data():
|
||||
return {
|
||||
'fieldname': 'leave_policy_assignment',
|
||||
'transactions': [
|
||||
{
|
||||
'label': _('Leaves'),
|
||||
'items': ['Leave Allocation']
|
||||
},
|
||||
]
|
||||
}
|
@ -6,6 +6,7 @@ frappe.listview_settings['Leave Policy Assignment'] = {
|
||||
doctype: "Employee",
|
||||
target: cur_list,
|
||||
setters: {
|
||||
employee_name: '',
|
||||
company: '',
|
||||
department: '',
|
||||
},
|
||||
@ -92,37 +93,6 @@ frappe.listview_settings['Leave Policy Assignment'] = {
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
list_view.page.add_inner_button(__("Grant Leaves"), function () {
|
||||
me.dialog = new frappe.ui.form.MultiSelectDialog({
|
||||
doctype: "Leave Policy Assignment",
|
||||
target: cur_list,
|
||||
setters: {
|
||||
company: '',
|
||||
employee: '',
|
||||
},
|
||||
get_query() {
|
||||
return {
|
||||
filters: {
|
||||
docstatus: ['=', 1],
|
||||
leaves_allocated: ['=', 0]
|
||||
}
|
||||
};
|
||||
},
|
||||
add_filters_group: 1,
|
||||
primary_action_label: "Grant Leaves",
|
||||
action(leave_policy_assignments) {
|
||||
frappe.call({
|
||||
method: 'erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment.grant_leave_for_multiple_employees',
|
||||
async: false,
|
||||
args: {
|
||||
leave_policy_assignments: leave_policy_assignments
|
||||
}
|
||||
});
|
||||
me.dialog.hide();
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
set_effective_date: function () {
|
||||
|
@ -35,7 +35,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||
|
||||
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
|
||||
leave_policy_assignment_doc.grant_leave_alloc_for_employee()
|
||||
leave_policy_assignment_doc.reload()
|
||||
|
||||
self.assertEqual(leave_policy_assignment_doc.leaves_allocated, 1)
|
||||
@ -73,7 +72,6 @@ class TestLeavePolicyAssignment(unittest.TestCase):
|
||||
leave_policy_assignments = create_assignment_for_multiple_employees([employee.name], frappe._dict(data))
|
||||
|
||||
leave_policy_assignment_doc = frappe.get_doc("Leave Policy Assignment", leave_policy_assignments[0])
|
||||
leave_policy_assignment_doc.grant_leave_alloc_for_employee()
|
||||
leave_policy_assignment_doc.reload()
|
||||
|
||||
|
||||
|
@ -2,23 +2,41 @@
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Training Event', {
|
||||
onload_post_render: function(frm) {
|
||||
onload_post_render: function (frm) {
|
||||
frm.get_field("employees").grid.set_multiple_add("employee");
|
||||
},
|
||||
refresh: function(frm) {
|
||||
if(!frm.doc.__islocal) {
|
||||
frm.add_custom_button(__("Training Result"), function() {
|
||||
refresh: function (frm) {
|
||||
if (!frm.doc.__islocal) {
|
||||
frm.add_custom_button(__("Training Result"), function () {
|
||||
frappe.route_options = {
|
||||
training_event: frm.doc.name
|
||||
}
|
||||
};
|
||||
frappe.set_route("List", "Training Result");
|
||||
});
|
||||
frm.add_custom_button(__("Training Feedback"), function() {
|
||||
frm.add_custom_button(__("Training Feedback"), function () {
|
||||
frappe.route_options = {
|
||||
training_event: frm.doc.name
|
||||
}
|
||||
};
|
||||
frappe.set_route("List", "Training Feedback");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Training Event Employee", {
|
||||
employee: function (frm) {
|
||||
let emp = [];
|
||||
for (let d in frm.doc.employees) {
|
||||
if (frm.doc.employees[d].employee) {
|
||||
emp.push(frm.doc.employees[d].employee);
|
||||
}
|
||||
}
|
||||
frm.set_query("employee", "employees", function () {
|
||||
return {
|
||||
filters: {
|
||||
name: ["NOT IN", emp]
|
||||
}
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -1,241 +1,80 @@
|
||||
{
|
||||
"allow_copy": 0,
|
||||
"allow_events_in_timeline": 0,
|
||||
"allow_guest_to_view": 0,
|
||||
"allow_import": 0,
|
||||
"allow_rename": 0,
|
||||
"beta": 0,
|
||||
"creation": "2016-08-08 05:33:39.965305",
|
||||
"custom": 0,
|
||||
"docstatus": 0,
|
||||
"doctype": "DocType",
|
||||
"document_type": "",
|
||||
"editable_grid": 1,
|
||||
"actions": [],
|
||||
"creation": "2016-08-08 05:33:39.965305",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"employee",
|
||||
"employee_name",
|
||||
"department",
|
||||
"column_break_3",
|
||||
"status",
|
||||
"attendance",
|
||||
"is_mandatory"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Employee",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Employee",
|
||||
"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
|
||||
},
|
||||
"fieldname": "employee",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Employee",
|
||||
"options": "Employee"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "employee.employee_name",
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Read Only",
|
||||
"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": "Employee Name",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "",
|
||||
"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
|
||||
},
|
||||
"fetch_from": "employee.employee_name",
|
||||
"fieldname": "employee_name",
|
||||
"fieldtype": "Read Only",
|
||||
"label": "Employee Name"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fetch_from": "employee.department",
|
||||
"fieldname": "department",
|
||||
"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": "Department",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Department",
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 1,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fetch_from": "employee.department",
|
||||
"fieldname": "department",
|
||||
"fieldtype": "Link",
|
||||
"label": "Department",
|
||||
"options": "Department",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 0,
|
||||
"in_standard_filter": 0,
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"permlevel": 0,
|
||||
"precision": "",
|
||||
"print_hide": 0,
|
||||
"print_hide_if_no_value": 0,
|
||||
"read_only": 0,
|
||||
"remember_last_selected_value": 0,
|
||||
"report_hide": 0,
|
||||
"reqd": 0,
|
||||
"search_index": 0,
|
||||
"set_only_once": 0,
|
||||
"translatable": 0,
|
||||
"unique": 0
|
||||
},
|
||||
"fieldname": "column_break_3",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 1,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"default": "Open",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Status",
|
||||
"length": 0,
|
||||
"no_copy": 1,
|
||||
"options": "Open\nInvited\nCompleted\nFeedback Submitted",
|
||||
"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_on_submit": 1,
|
||||
"default": "Open",
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Status",
|
||||
"no_copy": 1,
|
||||
"options": "Open\nInvited\nCompleted\nFeedback Submitted"
|
||||
},
|
||||
{
|
||||
"allow_bulk_edit": 0,
|
||||
"allow_in_quick_entry": 0,
|
||||
"allow_on_submit": 0,
|
||||
"bold": 0,
|
||||
"collapsible": 0,
|
||||
"columns": 0,
|
||||
"fieldname": "attendance",
|
||||
"fieldtype": "Select",
|
||||
"hidden": 0,
|
||||
"ignore_user_permissions": 0,
|
||||
"ignore_xss_filter": 0,
|
||||
"in_filter": 0,
|
||||
"in_global_search": 0,
|
||||
"in_list_view": 1,
|
||||
"in_standard_filter": 0,
|
||||
"label": "Attendance",
|
||||
"length": 0,
|
||||
"no_copy": 0,
|
||||
"options": "Mandatory\nOptional",
|
||||
"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
|
||||
"fieldname": "attendance",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Attendance",
|
||||
"options": "Present\nAbsent"
|
||||
},
|
||||
{
|
||||
"columns": 2,
|
||||
"default": "1",
|
||||
"fieldname": "is_mandatory",
|
||||
"fieldtype": "Check",
|
||||
"in_list_view": 1,
|
||||
"label": "Is Mandatory"
|
||||
}
|
||||
],
|
||||
"has_web_view": 0,
|
||||
"hide_heading": 0,
|
||||
"hide_toolbar": 0,
|
||||
"idx": 0,
|
||||
"image_view": 0,
|
||||
"in_create": 0,
|
||||
"is_submittable": 0,
|
||||
"issingle": 0,
|
||||
"istable": 1,
|
||||
"max_attachments": 0,
|
||||
"modified": "2019-01-30 11:28:16.170333",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Training Event Employee",
|
||||
"name_case": "",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"read_only": 0,
|
||||
"read_only_onload": 0,
|
||||
"show_name_in_global_search": 0,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 0,
|
||||
"track_seen": 0,
|
||||
"track_views": 0
|
||||
],
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-05-21 12:41:59.336237",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Training Event Employee",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC"
|
||||
}
|
@ -11,16 +11,18 @@
|
||||
"event": "Submit",
|
||||
"idx": 0,
|
||||
"is_standard": 1,
|
||||
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{ doc.introduction }}</li>\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>{{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n {% endif %}\n </ul>\n {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>",
|
||||
"modified": "2019-11-29 15:38:31.805409",
|
||||
"message": "<table class=\"panel-header\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div class=\"text-medium text-muted\">\n <span>{{_(\"Training Event:\")}} {{ doc.event_name }}</span>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>\n\n<table class=\"panel-body\" border=\"0\" cellpadding=\"0\" cellspacing=\"0\" width=\"100%\">\n <tr height=\"10\"></tr>\n <tr>\n <td width=\"15\"></td>\n <td>\n <div>\n {{ doc.introduction }}\n <ul class=\"list-unstyled\" style=\"line-height: 1.7\">\n <li>{{_(\"Event Location\")}}: <b>{{ doc.location }}</b></li>\n {% set start = frappe.utils.get_datetime(doc.start_time) %}\n {% set end = frappe.utils.get_datetime(doc.end_time) %}\n {% if start.date() == end.date() %}\n <li>{{_(\"Date\")}}: <b>{{ start.strftime(\"%A, %d %b %Y\") }}</b></li>\n <li>\n {{_(\"Timing\")}}: <b>{{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}</b>\n </li>\n {% else %}\n <li>{{_(\"Start Time\")}}: <b>{{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n <li>{{_(\"End Time\")}}: <b>{{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}</b>\n </li>\n {% endif %}\n <li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>\n {% if doc.is_mandatory %}\n <li>Note: This Training Event is mandatory</li>\n {% endif %}\n </ul>\n </div>\n </td>\n <td width=\"15\"></td>\n </tr>\n <tr height=\"10\"></tr>\n</table>",
|
||||
"modified": "2021-05-24 16:29:13.165930",
|
||||
"modified_by": "Administrator",
|
||||
"module": "HR",
|
||||
"name": "Training Scheduled",
|
||||
"owner": "Administrator",
|
||||
"recipients": [
|
||||
{
|
||||
"email_by_document_field": "employee_emails"
|
||||
"receiver_by_document_field": "employee_emails"
|
||||
}
|
||||
],
|
||||
"send_system_notification": 0,
|
||||
"send_to_all_assignees": 0,
|
||||
"subject": "Training Scheduled: {{ doc.name }}"
|
||||
}
|
@ -35,6 +35,9 @@
|
||||
</li>
|
||||
{% endif %}
|
||||
<li>{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}</li>
|
||||
{% if doc.is_mandatory %}
|
||||
<li>Note: This Training Event is mandatory</li>
|
||||
{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</td>
|
||||
|
@ -500,13 +500,6 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co
|
||||
total_claimed_amount = sum_of_claimed_amount[0].total_amount
|
||||
return total_claimed_amount
|
||||
|
||||
def grant_leaves_automatically():
|
||||
automatically_allocate_leaves_based_on_leave_policy = frappe.db.get_singles_value("HR Settings", "automatically_allocate_leaves_based_on_leave_policy")
|
||||
if automatically_allocate_leaves_based_on_leave_policy:
|
||||
lpa = frappe.db.get_all("Leave Policy Assignment", filters={"effective_from": getdate(), "docstatus": 1, "leaves_allocated":0})
|
||||
for assignment in lpa:
|
||||
frappe.get_doc("Leave Policy Assignment", assignment.name).grant_leave_alloc_for_employee()
|
||||
|
||||
def share_doc_with_approver(doc, user):
|
||||
# if approver does not have permissions, share
|
||||
if not frappe.has_permission(doc=doc, ptype="submit", user=user):
|
||||
|
@ -76,9 +76,9 @@ frappe.ui.form.on("Work Order", {
|
||||
frm.set_query("production_item", function() {
|
||||
return {
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters:[
|
||||
['is_stock_item', '=',1]
|
||||
]
|
||||
filters: {
|
||||
"is_stock_item": 1,
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -769,6 +769,7 @@ erpnext.patches.v13_0.rename_discharge_date_in_ip_record
|
||||
erpnext.patches.v12_0.create_taxable_value_field
|
||||
erpnext.patches.v12_0.add_gst_category_in_delivery_note
|
||||
erpnext.patches.v12_0.purchase_receipt_status
|
||||
erpnext.patches.v12_0.create_itc_reversal_custom_fields
|
||||
erpnext.patches.v13_0.fix_non_unique_represents_company
|
||||
erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing
|
||||
erpnext.patches.v13_0.make_non_standard_user_type #13-04-2021
|
||||
@ -778,3 +779,5 @@ erpnext.patches.v12_0.add_ewaybill_validity_field
|
||||
erpnext.patches.v13_0.germany_make_custom_fields
|
||||
erpnext.patches.v13_0.germany_fill_debtor_creditor_number
|
||||
erpnext.patches.v13_0.set_pos_closing_as_failed
|
||||
erpnext.patches.v13_0.update_timesheet_changes
|
||||
erpnext.patches.v13_0.set_training_event_attendance
|
||||
|
115
erpnext/patches/v12_0/create_itc_reversal_custom_fields.py
Normal file
115
erpnext/patches/v12_0/create_itc_reversal_custom_fields.py
Normal file
@ -0,0 +1,115 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
|
||||
from frappe.custom.doctype.property_setter.property_setter import make_property_setter
|
||||
from erpnext.regional.india.utils import get_gst_accounts
|
||||
|
||||
def execute():
|
||||
company = frappe.get_all('Company', filters = {'country': 'India'}, fields=['name'])
|
||||
if not company:
|
||||
return
|
||||
|
||||
frappe.reload_doc("regional", "doctype", "gst_settings")
|
||||
frappe.reload_doc("accounts", "doctype", "gst_account")
|
||||
|
||||
journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
|
||||
make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
|
||||
|
||||
custom_fields = {
|
||||
'Journal Entry': [
|
||||
dict(fieldname='reversal_type', label='Reversal Type',
|
||||
fieldtype='Select', insert_after='voucher_type', print_hide=1,
|
||||
options="As per rules 42 & 43 of CGST Rules\nOthers",
|
||||
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
|
||||
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
|
||||
dict(fieldname='company_address', label='Company Address',
|
||||
fieldtype='Link', options='Address', insert_after='reversal_type',
|
||||
print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
|
||||
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
|
||||
dict(fieldname='company_gstin', label='Company GSTIN',
|
||||
fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1,
|
||||
fetch_from='company_address.gstin',
|
||||
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
|
||||
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'")
|
||||
],
|
||||
'Purchase Invoice': [
|
||||
dict(fieldname='eligibility_for_itc', label='Eligibility For ITC',
|
||||
fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1,
|
||||
options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC',
|
||||
default="All Other ITC")
|
||||
],
|
||||
'Purchase Invoice Item': [
|
||||
dict(fieldname='taxable_value', label='Taxable Value',
|
||||
fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency",
|
||||
print_hide=1)
|
||||
]
|
||||
}
|
||||
|
||||
create_custom_fields(custom_fields, update=True)
|
||||
|
||||
# Patch ITC Availed fields from Data to Currency
|
||||
# Patch Availed ITC for current fiscal_year
|
||||
|
||||
gst_accounts = get_gst_accounts(only_non_reverse_charge=1)
|
||||
|
||||
frappe.db.sql("""
|
||||
UPDATE `tabCustom Field` SET fieldtype='Currency', options='Company:company:default_currency'
|
||||
WHERE dt = 'Purchase Invoice' and fieldname in ('itc_integrated_tax', 'itc_state_tax', 'itc_central_tax',
|
||||
'itc_cess_amount')
|
||||
""")
|
||||
|
||||
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_integrated_tax = '0'
|
||||
WHERE trim(coalesce(itc_integrated_tax, '')) = '' """)
|
||||
|
||||
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_state_tax = '0'
|
||||
WHERE trim(coalesce(itc_state_tax, '')) = '' """)
|
||||
|
||||
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_central_tax = '0'
|
||||
WHERE trim(coalesce(itc_central_tax, '')) = '' """)
|
||||
|
||||
frappe.db.sql("""UPDATE `tabPurchase Invoice` set itc_cess_amount = '0'
|
||||
WHERE trim(coalesce(itc_cess_amount, '')) = '' """)
|
||||
|
||||
# Get purchase invoices
|
||||
invoices = frappe.get_all('Purchase Invoice',
|
||||
{'posting_date': ('>=', '2021-04-01'), 'eligibility_for_itc': ('!=', 'Ineligible')},
|
||||
['name'])
|
||||
|
||||
amount_map = {}
|
||||
|
||||
if invoices:
|
||||
invoice_list = set([d.name for d in invoices])
|
||||
|
||||
# Get GST applied
|
||||
amounts = frappe.db.sql("""
|
||||
SELECT parent, account_head, sum(base_tax_amount_after_discount_amount) as amount
|
||||
FROM `tabPurchase Taxes and Charges`
|
||||
where parent in %s
|
||||
GROUP BY parent, account_head
|
||||
""", (invoice_list), as_dict=1)
|
||||
|
||||
for d in amounts:
|
||||
amount_map.setdefault(d.parent,
|
||||
{
|
||||
'itc_integrated_tax': 0,
|
||||
'itc_state_tax': 0,
|
||||
'itc_central_tax': 0,
|
||||
'itc_cess_amount': 0
|
||||
})
|
||||
|
||||
if d.account_head in gst_accounts.get('igst_account'):
|
||||
amount_map[d.parent]['itc_integrated_tax'] += d.amount
|
||||
if d.account_head in gst_accounts.get('cgst_account'):
|
||||
amount_map[d.parent]['itc_central_tax'] += d.amount
|
||||
if d.account_head in gst_accounts.get('sgst_account'):
|
||||
amount_map[d.parent]['itc_state_tax'] += d.amount
|
||||
if d.account_head in gst_accounts.get('cess_account'):
|
||||
amount_map[d.parent]['itc_cess_amount'] += d.amount
|
||||
|
||||
for invoice, values in amount_map.items():
|
||||
frappe.db.set_value('Purchase Invoice', invoice, {
|
||||
'itc_integrated_tax': values.get('itc_integrated_tax'),
|
||||
'itc_central_tax': values.get('itc_central_tax'),
|
||||
'itc_state_tax': values['itc_state_tax'],
|
||||
'itc_cess_amount': values['itc_cess_amount'],
|
||||
})
|
9
erpnext/patches/v13_0/set_training_event_attendance.py
Normal file
9
erpnext/patches/v13_0/set_training_event_attendance.py
Normal file
@ -0,0 +1,9 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc('hr', 'doctype', 'training_event')
|
||||
frappe.reload_doc('hr', 'doctype', 'training_event_employee')
|
||||
|
||||
frappe.db.sql("update `tabTraining Event Employee` set `attendance` = 'Present'")
|
||||
frappe.db.sql("update `tabTraining Event Employee` set `is_mandatory` = 1 where `attendance` = 'Mandatory'")
|
25
erpnext/patches/v13_0/update_timesheet_changes.py
Normal file
25
erpnext/patches/v13_0/update_timesheet_changes.py
Normal file
@ -0,0 +1,25 @@
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe.model.utils.rename_field import rename_field
|
||||
|
||||
def execute():
|
||||
frappe.reload_doc("projects", "doctype", "timesheet")
|
||||
frappe.reload_doc("projects", "doctype", "timesheet_detail")
|
||||
|
||||
if frappe.db.has_column("Timesheet Detail", "billable"):
|
||||
rename_field("Timesheet Detail", "billable", "is_billable")
|
||||
|
||||
base_currency = frappe.defaults.get_global_default('currency')
|
||||
|
||||
frappe.db.sql("""UPDATE `tabTimesheet Detail`
|
||||
SET base_billing_rate = billing_rate,
|
||||
base_billing_amount = billing_amount,
|
||||
base_costing_rate = costing_rate,
|
||||
base_costing_amount = costing_amount""")
|
||||
|
||||
frappe.db.sql("""UPDATE `tabTimesheet`
|
||||
SET currency = '{0}',
|
||||
exchange_rate = 1.0,
|
||||
base_total_billable_amount = total_billable_amount,
|
||||
base_total_billed_amount = total_billed_amount,
|
||||
base_total_costing_amount = total_costing_amount""".format(base_currency))
|
@ -51,7 +51,7 @@ def execute():
|
||||
|
||||
def get_timelog_data(data):
|
||||
return {
|
||||
'billable': data.billable,
|
||||
'is_billable': data.billable,
|
||||
'from_time': data.from_time,
|
||||
'hours': data.hours,
|
||||
'to_time': data.to_time,
|
||||
|
@ -1,4 +1,8 @@
|
||||
frappe.ui.form.on("Activity Type", {
|
||||
onload: function(frm) {
|
||||
frm.set_currency_labels(["billing_rate", "costing_rate"], frappe.defaults.get_global_default('currency'));
|
||||
},
|
||||
|
||||
refresh: function(frm) {
|
||||
frm.add_custom_button(__("Activity Cost per Employee"), function() {
|
||||
frappe.route_options = {"activity_type": frm.doc.name};
|
||||
|
@ -87,7 +87,7 @@ frappe.ui.form.on("Project", {
|
||||
|
||||
frm.add_custom_button(__("Kanban Board"), () => {
|
||||
frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
|
||||
project: frm.doc.project_name
|
||||
project: frm.doc.name
|
||||
}).then(() => {
|
||||
frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
|
||||
});
|
||||
|
@ -523,8 +523,9 @@ def update_project_sales_billing():
|
||||
def create_kanban_board_if_not_exists(project):
|
||||
from frappe.desk.doctype.kanban_board.kanban_board import quick_kanban_board
|
||||
|
||||
if not frappe.db.exists('Kanban Board', project):
|
||||
quick_kanban_board('Task', project, 'status', project)
|
||||
project = frappe.get_doc('Project', project)
|
||||
if not frappe.db.exists('Kanban Board', project.project_name):
|
||||
quick_kanban_board('Task', project.project_name, 'status', project.name)
|
||||
|
||||
return True
|
||||
|
||||
|
@ -5,12 +5,6 @@ frappe.provide("erpnext.projects");
|
||||
|
||||
frappe.ui.form.on("Task", {
|
||||
setup: function (frm) {
|
||||
frm.set_query("project", function () {
|
||||
return {
|
||||
query: "erpnext.projects.doctype.task.task.get_project"
|
||||
}
|
||||
});
|
||||
|
||||
frm.make_methods = {
|
||||
'Timesheet': () => frappe.model.open_mapped_doc({
|
||||
method: 'erpnext.projects.doctype.task.task.make_timesheet',
|
||||
|
@ -37,7 +37,7 @@ class TestTimesheet(unittest.TestCase):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
|
||||
make_salary_structure_for_timesheet(emp)
|
||||
timesheet = make_timesheet(emp, simulate=True, billable=1)
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||
|
||||
self.assertEqual(timesheet.total_hours, 2)
|
||||
self.assertEqual(timesheet.total_billable_hours, 2)
|
||||
@ -49,7 +49,7 @@ class TestTimesheet(unittest.TestCase):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
|
||||
make_salary_structure_for_timesheet(emp)
|
||||
timesheet = make_timesheet(emp, simulate=True, billable=0)
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=0)
|
||||
|
||||
self.assertEqual(timesheet.total_hours, 2)
|
||||
self.assertEqual(timesheet.total_billable_hours, 0)
|
||||
@ -61,7 +61,7 @@ class TestTimesheet(unittest.TestCase):
|
||||
emp = make_employee("test_employee_6@salary.com", company="_Test Company")
|
||||
|
||||
salary_structure = make_salary_structure_for_timesheet(emp)
|
||||
timesheet = make_timesheet(emp, simulate = True, billable=1)
|
||||
timesheet = make_timesheet(emp, simulate = True, is_billable=1)
|
||||
salary_slip = make_salary_slip(timesheet.name)
|
||||
salary_slip.submit()
|
||||
|
||||
@ -82,7 +82,7 @@ class TestTimesheet(unittest.TestCase):
|
||||
def test_sales_invoice_from_timesheet(self):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
|
||||
timesheet = make_timesheet(emp, simulate=True, billable=1)
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1)
|
||||
sales_invoice = make_sales_invoice(timesheet.name, '_Test Item', '_Test Customer')
|
||||
sales_invoice.due_date = nowdate()
|
||||
sales_invoice.submit()
|
||||
@ -100,7 +100,7 @@ class TestTimesheet(unittest.TestCase):
|
||||
emp = make_employee("test_employee_6@salary.com")
|
||||
project = frappe.get_value("Project", {"project_name": "_Test Project"})
|
||||
|
||||
timesheet = make_timesheet(emp, simulate=True, billable=1, project=project, company='_Test Company')
|
||||
timesheet = make_timesheet(emp, simulate=True, is_billable=1, project=project, company='_Test Company')
|
||||
sales_invoice = create_sales_invoice(do_not_save=True)
|
||||
sales_invoice.project = project
|
||||
sales_invoice.submit()
|
||||
@ -171,13 +171,13 @@ def make_salary_structure_for_timesheet(employee, company=None):
|
||||
return salary_structure
|
||||
|
||||
|
||||
def make_timesheet(employee, simulate=False, billable = 0, activity_type="_Test Activity Type", project=None, task=None, company=None):
|
||||
def make_timesheet(employee, simulate=False, is_billable = 0, activity_type="_Test Activity Type", project=None, task=None, company=None):
|
||||
update_activity_type(activity_type)
|
||||
timesheet = frappe.new_doc("Timesheet")
|
||||
timesheet.employee = employee
|
||||
timesheet.company = company or '_Test Company'
|
||||
timesheet_detail = timesheet.append('time_logs', {})
|
||||
timesheet_detail.billable = billable
|
||||
timesheet_detail.is_billable = is_billable
|
||||
timesheet_detail.activity_type = activity_type
|
||||
timesheet_detail.from_time = now_datetime()
|
||||
timesheet_detail.hours = 2
|
||||
|
@ -90,17 +90,99 @@ frappe.ui.form.on("Timesheet", {
|
||||
}
|
||||
if(frm.doc.per_billed > 0) {
|
||||
frm.fields_dict["time_logs"].grid.toggle_enable("billing_hours", false);
|
||||
frm.fields_dict["time_logs"].grid.toggle_enable("billable", false);
|
||||
frm.fields_dict["time_logs"].grid.toggle_enable("is_billable", false);
|
||||
}
|
||||
frm.trigger('setup_filters');
|
||||
frm.trigger('set_dynamic_field_label');
|
||||
},
|
||||
|
||||
customer: function(frm) {
|
||||
frm.set_query('parent_project', function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"customer": doc.customer
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.set_query('project', 'time_logs', function(doc) {
|
||||
return {
|
||||
filters: {
|
||||
"customer": doc.customer
|
||||
}
|
||||
};
|
||||
});
|
||||
frm.refresh();
|
||||
},
|
||||
|
||||
currency: function(frm) {
|
||||
let base_currency = frappe.defaults.get_global_default('currency');
|
||||
if (base_currency != frm.doc.currency) {
|
||||
frappe.call({
|
||||
method: "erpnext.setup.utils.get_exchange_rate",
|
||||
args: {
|
||||
from_currency: frm.doc.currency,
|
||||
to_currency: base_currency
|
||||
},
|
||||
callback: function(r) {
|
||||
if (r.message) {
|
||||
frm.set_value('exchange_rate', flt(r.message));
|
||||
frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency + " = [?] " + base_currency);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
frm.trigger('set_dynamic_field_label');
|
||||
},
|
||||
|
||||
exchange_rate: function(frm) {
|
||||
$.each(frm.doc.time_logs, function(i, d) {
|
||||
calculate_billing_costing_amount(frm, d.doctype, d.name);
|
||||
});
|
||||
calculate_time_and_amount(frm);
|
||||
},
|
||||
|
||||
set_dynamic_field_label: function(frm) {
|
||||
let base_currency = frappe.defaults.get_global_default('currency');
|
||||
frm.set_currency_labels(["base_total_costing_amount", "base_total_billable_amount", "base_total_billed_amount"], base_currency);
|
||||
frm.set_currency_labels(["total_costing_amount", "total_billable_amount", "total_billed_amount"], frm.doc.currency);
|
||||
|
||||
frm.toggle_display(["base_total_costing_amount", "base_total_billable_amount", "base_total_billed_amount"],
|
||||
frm.doc.currency != base_currency);
|
||||
|
||||
if (frm.doc.time_logs.length > 0) {
|
||||
frm.set_currency_labels(["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], base_currency, "time_logs");
|
||||
frm.set_currency_labels(["billing_rate", "billing_amount", "costing_rate", "costing_amount"], frm.doc.currency, "time_logs");
|
||||
|
||||
let time_logs_grid = frm.fields_dict.time_logs.grid;
|
||||
$.each(["base_billing_rate", "base_billing_amount", "base_costing_rate", "base_costing_amount"], function(i, d) {
|
||||
if (frappe.meta.get_docfield(time_logs_grid.doctype, d))
|
||||
time_logs_grid.set_column_disp(d, frm.doc.currency != base_currency);
|
||||
});
|
||||
}
|
||||
frm.refresh_fields();
|
||||
},
|
||||
|
||||
make_invoice: function(frm) {
|
||||
let fields = [{
|
||||
"fieldtype": "Link",
|
||||
"label": __("Item Code"),
|
||||
"fieldname": "item_code",
|
||||
"options": "Item"
|
||||
}];
|
||||
|
||||
if (!frm.doc.customer) {
|
||||
fields.push({
|
||||
"fieldtype": "Link",
|
||||
"label": __("Customer"),
|
||||
"fieldname": "customer",
|
||||
"options": "Customer",
|
||||
"default": frm.doc.customer
|
||||
});
|
||||
}
|
||||
|
||||
let dialog = new frappe.ui.Dialog({
|
||||
title: __("Select Item (optional)"),
|
||||
fields: [
|
||||
{"fieldtype": "Link", "label": __("Item Code"), "fieldname": "item_code", "options":"Item"},
|
||||
{"fieldtype": "Link", "label": __("Customer"), "fieldname": "customer", "options":"Customer"}
|
||||
]
|
||||
title: __("Create Sales Invoice"),
|
||||
fields: fields
|
||||
});
|
||||
|
||||
dialog.set_primary_action(__('Create Sales Invoice'), () => {
|
||||
@ -113,7 +195,8 @@ frappe.ui.form.on("Timesheet", {
|
||||
args: {
|
||||
"source_name": frm.doc.name,
|
||||
"item_code": args.item_code,
|
||||
"customer": args.customer
|
||||
"customer": frm.doc.customer || args.customer,
|
||||
"currency": frm.doc.currency
|
||||
},
|
||||
freeze: true,
|
||||
callback: function(r) {
|
||||
@ -136,8 +219,7 @@ frappe.ui.form.on("Timesheet", {
|
||||
|
||||
parent_project: function(frm) {
|
||||
set_project_in_timelog(frm);
|
||||
},
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
frappe.ui.form.on("Timesheet Detail", {
|
||||
@ -171,35 +253,34 @@ frappe.ui.form.on("Timesheet Detail", {
|
||||
if(frm.doc.parent_project) {
|
||||
frappe.model.set_value(cdt, cdn, 'project', frm.doc.parent_project);
|
||||
}
|
||||
|
||||
var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row');
|
||||
$trigger_again.on('click', () => {
|
||||
$('.form-grid')
|
||||
.find('[data-fieldname="timer"]')
|
||||
.append(frappe.render_template("timesheet"));
|
||||
frm.trigger("control_timer");
|
||||
});
|
||||
},
|
||||
|
||||
hours: function(frm, cdt, cdn) {
|
||||
calculate_end_time(frm, cdt, cdn);
|
||||
calculate_billing_costing_amount(frm, cdt, cdn);
|
||||
calculate_time_and_amount(frm);
|
||||
},
|
||||
|
||||
billing_hours: function(frm, cdt, cdn) {
|
||||
calculate_billing_costing_amount(frm, cdt, cdn);
|
||||
calculate_time_and_amount(frm);
|
||||
},
|
||||
|
||||
billing_rate: function(frm, cdt, cdn) {
|
||||
calculate_billing_costing_amount(frm, cdt, cdn);
|
||||
calculate_time_and_amount(frm);
|
||||
},
|
||||
|
||||
costing_rate: function(frm, cdt, cdn) {
|
||||
calculate_billing_costing_amount(frm, cdt, cdn);
|
||||
calculate_time_and_amount(frm);
|
||||
},
|
||||
|
||||
billable: function(frm, cdt, cdn) {
|
||||
is_billable: function(frm, cdt, cdn) {
|
||||
update_billing_hours(frm, cdt, cdn);
|
||||
update_time_rates(frm, cdt, cdn);
|
||||
calculate_billing_costing_amount(frm, cdt, cdn);
|
||||
calculate_time_and_amount(frm);
|
||||
},
|
||||
|
||||
activity_type: function(frm, cdt, cdn) {
|
||||
@ -207,7 +288,8 @@ frappe.ui.form.on("Timesheet Detail", {
|
||||
method: "erpnext.projects.doctype.timesheet.timesheet.get_activity_cost",
|
||||
args: {
|
||||
employee: frm.doc.employee,
|
||||
activity_type: frm.selected_doc.activity_type
|
||||
activity_type: frm.selected_doc.activity_type,
|
||||
currency: frm.doc.currency
|
||||
},
|
||||
callback: function(r){
|
||||
if(r.message){
|
||||
@ -239,9 +321,9 @@ var calculate_end_time = function(frm, cdt, cdn) {
|
||||
}
|
||||
};
|
||||
|
||||
var update_billing_hours = function(frm, cdt, cdn){
|
||||
var child = locals[cdt][cdn];
|
||||
if(!child.billable) {
|
||||
var update_billing_hours = function(frm, cdt, cdn) {
|
||||
let child = frappe.get_doc(cdt, cdn);
|
||||
if (!child.is_billable) {
|
||||
frappe.model.set_value(cdt, cdn, 'billing_hours', 0.0);
|
||||
} else {
|
||||
// bill all hours by default
|
||||
@ -249,40 +331,44 @@ var update_billing_hours = function(frm, cdt, cdn){
|
||||
}
|
||||
};
|
||||
|
||||
var update_time_rates = function(frm, cdt, cdn){
|
||||
var child = locals[cdt][cdn];
|
||||
if(!child.billable){
|
||||
var update_time_rates = function(frm, cdt, cdn) {
|
||||
let child = frappe.get_doc(cdt, cdn);
|
||||
if (!child.is_billable) {
|
||||
frappe.model.set_value(cdt, cdn, 'billing_rate', 0.0);
|
||||
}
|
||||
};
|
||||
|
||||
var calculate_billing_costing_amount = function(frm, cdt, cdn){
|
||||
var child = locals[cdt][cdn];
|
||||
var billing_amount = 0.0;
|
||||
var costing_amount = 0.0;
|
||||
|
||||
if(child.billing_hours && child.billable){
|
||||
billing_amount = (child.billing_hours * child.billing_rate);
|
||||
var calculate_billing_costing_amount = function(frm, cdt, cdn) {
|
||||
let row = frappe.get_doc(cdt, cdn);
|
||||
let billing_amount = 0.0;
|
||||
let base_billing_amount = 0.0;
|
||||
let exchange_rate = flt(frm.doc.exchange_rate);
|
||||
frappe.model.set_value(cdt, cdn, 'base_billing_rate', flt(row.billing_rate) * exchange_rate);
|
||||
frappe.model.set_value(cdt, cdn, 'base_costing_rate', flt(row.costing_rate) * exchange_rate);
|
||||
if (row.billing_hours && row.is_billable) {
|
||||
base_billing_amount = flt(row.billing_hours) * flt(row.base_billing_rate);
|
||||
billing_amount = flt(row.billing_hours) * flt(row.billing_rate);
|
||||
}
|
||||
costing_amount = flt(child.costing_rate * child.hours);
|
||||
|
||||
frappe.model.set_value(cdt, cdn, 'base_billing_amount', base_billing_amount);
|
||||
frappe.model.set_value(cdt, cdn, 'base_costing_amount', flt(row.base_costing_rate) * flt(row.hours));
|
||||
frappe.model.set_value(cdt, cdn, 'billing_amount', billing_amount);
|
||||
frappe.model.set_value(cdt, cdn, 'costing_amount', costing_amount);
|
||||
calculate_time_and_amount(frm);
|
||||
frappe.model.set_value(cdt, cdn, 'costing_amount', flt(row.costing_rate) * flt(row.hours));
|
||||
};
|
||||
|
||||
var calculate_time_and_amount = function(frm) {
|
||||
var tl = frm.doc.time_logs || [];
|
||||
var total_working_hr = 0;
|
||||
var total_billing_hr = 0;
|
||||
var total_billable_amount = 0;
|
||||
var total_costing_amount = 0;
|
||||
let tl = frm.doc.time_logs || [];
|
||||
let total_working_hr = 0;
|
||||
let total_billing_hr = 0;
|
||||
let total_billable_amount = 0;
|
||||
let total_costing_amount = 0;
|
||||
for(var i=0; i<tl.length; i++) {
|
||||
if (tl[i].hours) {
|
||||
total_working_hr += tl[i].hours;
|
||||
total_billable_amount += tl[i].billing_amount;
|
||||
total_costing_amount += tl[i].costing_amount;
|
||||
|
||||
if(tl[i].billable){
|
||||
if (tl[i].is_billable) {
|
||||
total_billing_hr += tl[i].billing_hours;
|
||||
}
|
||||
}
|
||||
|
@ -11,6 +11,9 @@
|
||||
"title",
|
||||
"naming_series",
|
||||
"company",
|
||||
"customer",
|
||||
"currency",
|
||||
"exchange_rate",
|
||||
"sales_invoice",
|
||||
"column_break_3",
|
||||
"salary_slip",
|
||||
@ -30,11 +33,14 @@
|
||||
"total_hours",
|
||||
"billing_details",
|
||||
"total_billable_hours",
|
||||
"total_billed_hours",
|
||||
"total_costing_amount",
|
||||
"base_total_billable_amount",
|
||||
"base_total_billed_amount",
|
||||
"base_total_costing_amount",
|
||||
"column_break_10",
|
||||
"total_billed_hours",
|
||||
"total_billable_amount",
|
||||
"total_billed_amount",
|
||||
"total_costing_amount",
|
||||
"per_billed",
|
||||
"section_break_18",
|
||||
"note",
|
||||
@ -176,7 +182,6 @@
|
||||
"default": "0",
|
||||
"fieldname": "total_hours",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Total Working Hours",
|
||||
"read_only": 1
|
||||
},
|
||||
@ -199,7 +204,6 @@
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "total_billed_hours",
|
||||
"fieldtype": "Float",
|
||||
"in_list_view": 1,
|
||||
"label": "Total Billed Hours",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
@ -209,6 +213,7 @@
|
||||
"fieldname": "total_costing_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Costing Amount",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -222,6 +227,7 @@
|
||||
"fieldname": "total_billable_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Billable Amount",
|
||||
"options": "currency",
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
@ -229,6 +235,7 @@
|
||||
"fieldname": "total_billed_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Billed Amount",
|
||||
"options": "currency",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
@ -236,6 +243,7 @@
|
||||
"allow_on_submit": 1,
|
||||
"fieldname": "per_billed",
|
||||
"fieldtype": "Percent",
|
||||
"in_list_view": 1,
|
||||
"label": "% Amount Billed",
|
||||
"no_copy": 1,
|
||||
"print_hide": 1,
|
||||
@ -265,13 +273,53 @@
|
||||
"fieldtype": "Link",
|
||||
"label": "Project",
|
||||
"options": "Project"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer",
|
||||
"fieldtype": "Link",
|
||||
"label": "Customer",
|
||||
"options": "Customer"
|
||||
},
|
||||
{
|
||||
"fetch_from": "customer.default_currency",
|
||||
"fetch_if_empty": 1,
|
||||
"fieldname": "currency",
|
||||
"fieldtype": "Link",
|
||||
"label": "Currency",
|
||||
"options": "Currency"
|
||||
},
|
||||
{
|
||||
"fieldname": "base_total_costing_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Costing Amount",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_total_billable_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Billable Amount",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "base_total_billed_amount",
|
||||
"fieldtype": "Currency",
|
||||
"label": "Total Billed Amount",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "exchange_rate",
|
||||
"fieldtype": "Float",
|
||||
"label": "Exchange Rate"
|
||||
}
|
||||
],
|
||||
"icon": "fa fa-clock-o",
|
||||
"idx": 1,
|
||||
"is_submittable": 1,
|
||||
"links": [],
|
||||
"modified": "2021-01-08 20:51:14.590080",
|
||||
"modified": "2021-05-18 16:10:08.249619",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Projects",
|
||||
"name": "Timesheet",
|
||||
|
@ -14,6 +14,7 @@ from frappe.model.document import Document
|
||||
from erpnext.manufacturing.doctype.workstation.workstation import (check_if_within_operating_hours,
|
||||
WorkstationHolidayError)
|
||||
from erpnext.manufacturing.doctype.manufacturing_settings.manufacturing_settings import get_mins_between_operations
|
||||
from erpnext.setup.utils import get_exchange_rate
|
||||
|
||||
class OverlapError(frappe.ValidationError): pass
|
||||
class OverWorkLoggedError(frappe.ValidationError): pass
|
||||
@ -37,9 +38,9 @@ class Timesheet(Document):
|
||||
self.total_hours = 0.0
|
||||
self.total_billable_hours = 0.0
|
||||
self.total_billed_hours = 0.0
|
||||
self.total_billable_amount = 0.0
|
||||
self.total_costing_amount = 0.0
|
||||
self.total_billed_amount = 0.0
|
||||
self.total_billable_amount = self.base_total_billable_amount = 0.0
|
||||
self.total_costing_amount = self.base_total_costing_amount = 0.0
|
||||
self.total_billed_amount = self.base_total_billed_amount = 0.0
|
||||
|
||||
for d in self.get("time_logs"):
|
||||
self.update_billing_hours(d)
|
||||
@ -47,10 +48,13 @@ class Timesheet(Document):
|
||||
|
||||
self.total_hours += flt(d.hours)
|
||||
self.total_costing_amount += flt(d.costing_amount)
|
||||
if d.billable:
|
||||
self.base_total_costing_amount += flt(d.base_costing_amount)
|
||||
if d.is_billable:
|
||||
self.total_billable_hours += flt(d.billing_hours)
|
||||
self.total_billable_amount += flt(d.billing_amount)
|
||||
self.base_total_billable_amount += flt(d.base_billing_amount)
|
||||
self.total_billed_amount += flt(d.billing_amount) if d.sales_invoice else 0.0
|
||||
self.base_total_billed_amount += flt(d.base_billing_amount) if d.sales_invoice else 0.0
|
||||
self.total_billed_hours += flt(d.billing_hours) if d.sales_invoice else 0.0
|
||||
|
||||
def calculate_percentage_billed(self):
|
||||
@ -59,7 +63,7 @@ class Timesheet(Document):
|
||||
self.per_billed = (self.total_billed_amount * 100) / self.total_billable_amount
|
||||
|
||||
def update_billing_hours(self, args):
|
||||
if args.billable:
|
||||
if args.is_billable:
|
||||
if flt(args.billing_hours) == 0.0:
|
||||
args.billing_hours = args.hours
|
||||
else:
|
||||
@ -133,16 +137,20 @@ class Timesheet(Document):
|
||||
def validate_time_logs(self):
|
||||
for data in self.get('time_logs'):
|
||||
self.validate_overlap(data)
|
||||
self.validate_task_project()
|
||||
self.set_project(data)
|
||||
self.validate_project(data)
|
||||
|
||||
def validate_overlap(self, data):
|
||||
settings = frappe.get_single('Projects Settings')
|
||||
self.validate_overlap_for("user", data, self.user, settings.ignore_user_time_overlap)
|
||||
self.validate_overlap_for("employee", data, self.employee, settings.ignore_employee_time_overlap)
|
||||
|
||||
def validate_task_project(self):
|
||||
for log in self.time_logs:
|
||||
log.project = log.project or frappe.db.get_value("Task", log.task, "project")
|
||||
def set_project(self, data):
|
||||
data.project = data.project or frappe.db.get_value("Task", data.task, "project")
|
||||
|
||||
def validate_project(self, data):
|
||||
if self.parent_project and self.parent_project != data.project:
|
||||
frappe.throw(_("Row {0}: Project must be same as the one set in the Timesheet: {1}.").format(data.idx, self.parent_project))
|
||||
|
||||
def validate_overlap_for(self, fieldname, args, value, ignore_validation=False):
|
||||
if not value or ignore_validation:
|
||||
@ -189,7 +197,7 @@ class Timesheet(Document):
|
||||
|
||||
def update_cost(self):
|
||||
for data in self.time_logs:
|
||||
if data.activity_type or data.billable:
|
||||
if data.activity_type or data.is_billable:
|
||||
rate = get_activity_cost(self.employee, data.activity_type)
|
||||
hours = data.billing_hours or 0
|
||||
costing_hours = data.billing_hours or data.hours or 0
|
||||
@ -200,20 +208,29 @@ class Timesheet(Document):
|
||||
data.costing_amount = data.costing_rate * costing_hours
|
||||
|
||||
def update_time_rates(self, ts_detail):
|
||||
if not ts_detail.billable:
|
||||
if not ts_detail.is_billable:
|
||||
ts_detail.billing_rate = 0.0
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_projectwise_timesheet_data(project, parent=None, from_time=None, to_time=None):
|
||||
def get_projectwise_timesheet_data(project=None, parent=None, from_time=None, to_time=None):
|
||||
condition = ''
|
||||
if project:
|
||||
condition += "and tsd.project = %(project)s"
|
||||
if parent:
|
||||
condition = "AND parent = %(parent)s"
|
||||
condition += "AND tsd.parent = %(parent)s"
|
||||
if from_time and to_time:
|
||||
condition += "AND CAST(from_time as DATE) BETWEEN %(from_time)s AND %(to_time)s"
|
||||
condition += "AND CAST(tsd.from_time as DATE) BETWEEN %(from_time)s AND %(to_time)s"
|
||||
|
||||
return frappe.db.sql("""select name, parent, billing_hours, billing_amount as billing_amt
|
||||
from `tabTimesheet Detail` where parenttype = 'Timesheet' and docstatus=1 and project = %(project)s {0} and billable = 1
|
||||
and sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1)
|
||||
return frappe.db.sql("""SELECT tsd.name as name,
|
||||
tsd.parent as parent, tsd.billing_hours as billing_hours,
|
||||
tsd.billing_amount as billing_amount, tsd.activity_type as activity_type,
|
||||
tsd.description as description, ts.currency as currency
|
||||
FROM `tabTimesheet Detail` tsd
|
||||
INNER JOIN `tabTimesheet` ts ON ts.name = tsd.parent
|
||||
WHERE tsd.parenttype = 'Timesheet'
|
||||
and tsd.docstatus=1 {0}
|
||||
and tsd.is_billable = 1
|
||||
and tsd.sales_invoice is null""".format(condition), {'project': project, 'parent': parent, 'from_time': from_time, 'to_time': to_time}, as_dict=1)
|
||||
|
||||
@frappe.whitelist()
|
||||
@frappe.validate_and_sanitize_search_inputs
|
||||
@ -250,7 +267,7 @@ def get_timesheet_data(name, project):
|
||||
}
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_sales_invoice(source_name, item_code=None, customer=None):
|
||||
def make_sales_invoice(source_name, item_code=None, customer=None, currency=None):
|
||||
target = frappe.new_doc("Sales Invoice")
|
||||
timesheet = frappe.get_doc('Timesheet', source_name)
|
||||
|
||||
@ -268,6 +285,9 @@ def make_sales_invoice(source_name, item_code=None, customer=None):
|
||||
if customer:
|
||||
target.customer = customer
|
||||
|
||||
if currency:
|
||||
target.currency = currency
|
||||
|
||||
if item_code:
|
||||
target.append('items', {
|
||||
'item_code': item_code,
|
||||
@ -275,11 +295,16 @@ def make_sales_invoice(source_name, item_code=None, customer=None):
|
||||
'rate': billing_rate
|
||||
})
|
||||
|
||||
target.append('timesheets', {
|
||||
'time_sheet': timesheet.name,
|
||||
'billing_hours': hours,
|
||||
'billing_amount': billing_amount
|
||||
})
|
||||
for time_log in timesheet.time_logs:
|
||||
if time_log.is_billable:
|
||||
target.append('timesheets', {
|
||||
'time_sheet': timesheet.name,
|
||||
'billing_hours': time_log.billing_hours,
|
||||
'billing_amount': time_log.billing_amount,
|
||||
'timesheet_detail': time_log.name,
|
||||
'activity_type': time_log.activity_type,
|
||||
'description': time_log.description
|
||||
})
|
||||
|
||||
target.run_method("calculate_billing_amount_for_timesheet")
|
||||
target.run_method("set_missing_values")
|
||||
@ -309,12 +334,17 @@ def set_missing_values(time_sheet, target):
|
||||
})
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_activity_cost(employee=None, activity_type=None):
|
||||
def get_activity_cost(employee=None, activity_type=None, currency=None):
|
||||
base_currency = frappe.defaults.get_global_default('currency')
|
||||
rate = frappe.db.get_values("Activity Cost", {"employee": employee,
|
||||
"activity_type": activity_type}, ["costing_rate", "billing_rate"], as_dict=True)
|
||||
if not rate:
|
||||
rate = frappe.db.get_values("Activity Type", {"activity_type": activity_type},
|
||||
["costing_rate", "billing_rate"], as_dict=True)
|
||||
if rate and currency and currency!=base_currency:
|
||||
exchange_rate = get_exchange_rate(base_currency, currency)
|
||||
rate[0]["costing_rate"] = rate[0]["costing_rate"] * exchange_rate
|
||||
rate[0]["billing_rate"] = rate[0]["billing_rate"] * exchange_rate
|
||||
|
||||
return rate[0] if rate else {}
|
||||
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -126,7 +126,7 @@ def get_timesheet_details(filters, timesheet_list):
|
||||
timesheet_details = frappe.get_all(
|
||||
"Timesheet Detail",
|
||||
filters = timesheet_details_filter,
|
||||
fields=["from_time", "to_time", "hours", "billable", "billing_hours", "billing_rate", "parent"]
|
||||
fields=["from_time", "to_time", "hours", "is_billable", "billing_hours", "billing_rate", "parent"]
|
||||
)
|
||||
|
||||
timesheet_details_map = frappe._dict()
|
||||
@ -139,7 +139,7 @@ def get_billable_and_total_duration(activity, start_time, end_time):
|
||||
precision = frappe.get_precision("Timesheet Detail", "hours")
|
||||
activity_duration = time_diff_in_hours(end_time, start_time)
|
||||
billing_duration = 0.0
|
||||
if activity.billable:
|
||||
if activity.is_billable:
|
||||
billing_duration = activity.billing_hours
|
||||
if activity_duration != activity.billing_hours:
|
||||
billing_duration = activity_duration * activity.billing_hours / activity.hours
|
||||
|
@ -140,7 +140,7 @@ class EmployeeHoursReport:
|
||||
additional_filters += f"AND tt.{field} = '{self.filters.get(field)}'"
|
||||
|
||||
self.filtered_time_logs = frappe.db.sql('''
|
||||
SELECT tt.employee AS employee, ttd.hours AS hours, ttd.billable AS billable, ttd.project AS project
|
||||
SELECT tt.employee AS employee, ttd.hours AS hours, ttd.is_billable AS is_billable, ttd.project AS project
|
||||
FROM `tabTimesheet Detail` AS ttd
|
||||
JOIN `tabTimesheet` AS tt
|
||||
ON ttd.parent = tt.name
|
||||
@ -153,14 +153,14 @@ class EmployeeHoursReport:
|
||||
def generate_stats_by_employee(self):
|
||||
self.stats_by_employee = frappe._dict()
|
||||
|
||||
for emp, hours, billable, project in self.filtered_time_logs:
|
||||
for emp, hours, is_billable, project in self.filtered_time_logs:
|
||||
self.stats_by_employee.setdefault(
|
||||
emp, frappe._dict()
|
||||
).setdefault('billed_hours', 0.0)
|
||||
|
||||
self.stats_by_employee[emp].setdefault('non_billed_hours', 0.0)
|
||||
|
||||
if billable:
|
||||
if is_billable:
|
||||
self.stats_by_employee[emp]['billed_hours'] += flt(hours, 2)
|
||||
else:
|
||||
self.stats_by_employee[emp]['non_billed_hours'] += flt(hours, 2)
|
||||
|
@ -31,7 +31,7 @@ class TestEmployeeUtilization(unittest.TestCase):
|
||||
timesheet1.append("time_logs", {
|
||||
"activity_type": get_random("Activity Type"),
|
||||
"hours": 5,
|
||||
"billable": 1,
|
||||
"is_billable": 1,
|
||||
"from_time": '2021-04-01 13:30:00.000000',
|
||||
"to_time": '2021-04-01 18:30:00.000000'
|
||||
})
|
||||
@ -46,7 +46,7 @@ class TestEmployeeUtilization(unittest.TestCase):
|
||||
timesheet2.append("time_logs", {
|
||||
"activity_type": get_random("Activity Type"),
|
||||
"hours": 10,
|
||||
"billable": 0,
|
||||
"is_billable": 0,
|
||||
"from_time": '2021-04-01 13:30:00.000000',
|
||||
"to_time": '2021-04-01 23:30:00.000000',
|
||||
"project": cls.test_project.name
|
||||
|
@ -8,20 +8,20 @@ from erpnext.projects.doctype.timesheet.timesheet import make_salary_slip, make_
|
||||
from erpnext.projects.report.project_profitability.project_profitability import execute
|
||||
|
||||
class TestProjectProfitability(unittest.TestCase):
|
||||
@classmethod
|
||||
|
||||
def setUp(self):
|
||||
emp = make_employee('test_employee_9@salary.com', company='_Test Company')
|
||||
if not frappe.db.exists('Salary Component', 'Timesheet Component'):
|
||||
frappe.get_doc({'doctype': 'Salary Component', 'salary_component': 'Timesheet Component'}).insert()
|
||||
make_salary_structure_for_timesheet(emp, company='_Test Company')
|
||||
self.timesheet = make_timesheet(emp, simulate = True, billable=1)
|
||||
self.timesheet = make_timesheet(emp, simulate = True, is_billable=1)
|
||||
self.salary_slip = make_salary_slip(self.timesheet.name)
|
||||
self.salary_slip.submit()
|
||||
self.sales_invoice = make_sales_invoice(self.timesheet.name, '_Test Item', '_Test Customer')
|
||||
self.sales_invoice.due_date = nowdate()
|
||||
self.sales_invoice.submit()
|
||||
|
||||
frappe.db.set_value("HR Settings", "HR Settings", "standard_working_hours", 8)
|
||||
frappe.db.set_value('HR Settings', None, 'standard_working_hours', 8)
|
||||
|
||||
def test_project_profitability(self):
|
||||
filters = {
|
||||
@ -55,4 +55,4 @@ class TestProjectProfitability(unittest.TestCase):
|
||||
def tearDown(self):
|
||||
frappe.get_doc("Sales Invoice", self.sales_invoice.name).cancel()
|
||||
frappe.get_doc("Salary Slip", self.salary_slip.name).cancel()
|
||||
frappe.get_doc("Timesheet", self.timesheet.name).cancel()
|
||||
frappe.get_doc("Timesheet", self.timesheet.name).cancel()
|
||||
|
@ -84,13 +84,13 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({
|
||||
if (me.frm.doc.is_subcontracted == "Yes") {
|
||||
return{
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters:{ 'is_sub_contracted_item': 1 }
|
||||
filters:{ 'supplier': me.frm.doc.supplier, 'is_sub_contracted_item': 1 }
|
||||
}
|
||||
}
|
||||
else {
|
||||
return{
|
||||
query: "erpnext.controllers.queries.item_query",
|
||||
filters: {'is_purchase_item': 1}
|
||||
filters: { 'supplier': me.frm.doc.supplier, 'is_purchase_item': 1 }
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -724,6 +724,18 @@ frappe.form.link_formatters['Employee'] = function(value, doc) {
|
||||
}
|
||||
}
|
||||
|
||||
frappe.form.link_formatters['Project'] = function(value, doc) {
|
||||
if (doc && value && doc.project_name && doc.project_name !== value && doc.project === value) {
|
||||
return value + ': ' + doc.project_name;
|
||||
} else if (!value && doc.doctype && doc.project_name) {
|
||||
// format blank value in child table
|
||||
return doc.project;
|
||||
} else {
|
||||
// if value is blank in report view or project name and name are the same, return as is
|
||||
return value;
|
||||
}
|
||||
};
|
||||
|
||||
// add description on posting time
|
||||
$(document).on('app_ready', function() {
|
||||
if(!frappe.datetime.is_timezone_same()) {
|
||||
|
@ -74,9 +74,18 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
||||
fieldname: 'qty',
|
||||
fieldtype:'Float',
|
||||
read_only: me.has_batch && !me.has_serial_no,
|
||||
label: __(me.has_batch && !me.has_serial_no ? 'Total Qty' : 'Qty'),
|
||||
label: __(me.has_batch && !me.has_serial_no ? 'Selected Qty' : 'Qty'),
|
||||
default: flt(me.item.stock_qty),
|
||||
},
|
||||
...get_pending_qty_fields(me),
|
||||
{
|
||||
fieldname: 'uom',
|
||||
read_only: 1,
|
||||
fieldtype: 'Link',
|
||||
options: 'UOM',
|
||||
label: __('UOM'),
|
||||
default: me.item.uom
|
||||
},
|
||||
{
|
||||
fieldname: 'auto_fetch_button',
|
||||
fieldtype:'Button',
|
||||
@ -173,6 +182,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
||||
|
||||
if (this.has_batch && !this.has_serial_no) {
|
||||
this.update_total_qty();
|
||||
this.update_pending_qtys();
|
||||
}
|
||||
|
||||
this.dialog.show();
|
||||
@ -313,7 +323,21 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
||||
|
||||
qty_field.set_input(total_qty);
|
||||
},
|
||||
update_pending_qtys: function() {
|
||||
const pending_qty_field = this.dialog.fields_dict.pending_qty;
|
||||
const total_selected_qty_field = this.dialog.fields_dict.total_selected_qty;
|
||||
|
||||
if (!pending_qty_field || !total_selected_qty_field) return;
|
||||
|
||||
const me = this;
|
||||
const required_qty = this.dialog.fields_dict.required_qty.value;
|
||||
const selected_qty = this.dialog.fields_dict.qty.value;
|
||||
const total_selected_qty = selected_qty + calc_total_selected_qty(me);
|
||||
const pending_qty = required_qty - total_selected_qty;
|
||||
|
||||
pending_qty_field.set_input(pending_qty);
|
||||
total_selected_qty_field.set_input(total_selected_qty);
|
||||
},
|
||||
get_batch_fields: function() {
|
||||
var me = this;
|
||||
|
||||
@ -415,6 +439,7 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
||||
}
|
||||
|
||||
me.update_total_qty();
|
||||
me.update_pending_qtys();
|
||||
}
|
||||
},
|
||||
],
|
||||
@ -511,3 +536,60 @@ erpnext.SerialNoBatchSelector = Class.extend({
|
||||
];
|
||||
}
|
||||
});
|
||||
|
||||
function get_pending_qty_fields(me) {
|
||||
if (!check_can_calculate_pending_qty(me)) return [];
|
||||
const { frm: { doc: { fg_completed_qty }}, item: { item_code, stock_qty }} = me;
|
||||
const { qty_consumed_per_unit } = erpnext.stock.bom.items[item_code];
|
||||
|
||||
const total_selected_qty = calc_total_selected_qty(me);
|
||||
const required_qty = flt(fg_completed_qty) * flt(qty_consumed_per_unit);
|
||||
const pending_qty = required_qty - (flt(stock_qty) + total_selected_qty);
|
||||
|
||||
const pending_qty_fields = [
|
||||
{ fieldtype: 'Section Break', label: __('Pending Quantity') },
|
||||
{
|
||||
fieldname: 'required_qty',
|
||||
read_only: 1,
|
||||
fieldtype: 'Float',
|
||||
label: __('Required Qty'),
|
||||
default: required_qty
|
||||
},
|
||||
{ fieldtype: 'Column Break' },
|
||||
{
|
||||
fieldname: 'total_selected_qty',
|
||||
read_only: 1,
|
||||
fieldtype: 'Float',
|
||||
label: __('Total Selected Qty'),
|
||||
default: total_selected_qty
|
||||
},
|
||||
{ fieldtype: 'Column Break' },
|
||||
{
|
||||
fieldname: 'pending_qty',
|
||||
read_only: 1,
|
||||
fieldtype: 'Float',
|
||||
label: __('Pending Qty'),
|
||||
default: pending_qty
|
||||
},
|
||||
];
|
||||
return pending_qty_fields;
|
||||
}
|
||||
|
||||
function calc_total_selected_qty(me) {
|
||||
const { frm: { doc: { items }}, item: { name, item_code }} = me;
|
||||
const totalSelectedQty = items
|
||||
.filter( item => ( item.name !== name ) && ( item.item_code === item_code ) )
|
||||
.map( item => flt(item.qty) )
|
||||
.reduce( (i, j) => i + j, 0);
|
||||
return totalSelectedQty;
|
||||
}
|
||||
|
||||
function check_can_calculate_pending_qty(me) {
|
||||
const { frm: { doc }, item } = me;
|
||||
const docChecks = doc.bom_no
|
||||
&& doc.fg_completed_qty
|
||||
&& erpnext.stock.bom
|
||||
&& erpnext.stock.bom.name === doc.bom_no;
|
||||
const itemChecks = !!item;
|
||||
return docChecks && itemChecks;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
{{ address_line1 }}<br>
|
||||
{% if address_line2 %}{{ address_line2 }}<br>{% endif -%}
|
||||
{{ city }}, {% if state %}{{ state }}{% endif -%}{% if pincode %} {{ pincode }}<br>{% endif -%}
|
||||
{% if country != "United States" %}{{ country|upper }}{% endif -%}
|
||||
{% if country != "United States" %}{{ country }}{% endif -%}
|
||||
|
@ -172,7 +172,7 @@
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><b>(A) {{__("ITC Available (whether in full op part)")}}</b></td>
|
||||
<td><b>(A) {{__("ITC Available (whether in full or part)")}}</b></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
<td></td>
|
||||
|
@ -3,148 +3,21 @@
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import os
|
||||
import json
|
||||
import frappe
|
||||
from six import iteritems
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
import json
|
||||
from six import iteritems
|
||||
from frappe.utils import flt, getdate
|
||||
from frappe.utils import flt, cstr
|
||||
from erpnext.regional.india import state_numbers
|
||||
|
||||
class GSTR3BReport(Document):
|
||||
def before_save(self):
|
||||
|
||||
def validate(self):
|
||||
self.get_data()
|
||||
|
||||
def get_data(self):
|
||||
|
||||
self.report_dict = {
|
||||
"gstin": "",
|
||||
"ret_period": "",
|
||||
"inward_sup": {
|
||||
"isup_details": [
|
||||
{
|
||||
"ty": "GST",
|
||||
"intra": 0,
|
||||
"inter": 0
|
||||
},
|
||||
{
|
||||
"ty": "NONGST",
|
||||
"inter": 0,
|
||||
"intra": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"sup_details": {
|
||||
"osup_zero": {
|
||||
"csamt": 0,
|
||||
"txval": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
"osup_nil_exmp": {
|
||||
"txval": 0
|
||||
},
|
||||
"osup_det": {
|
||||
"samt": 0,
|
||||
"csamt": 0,
|
||||
"txval": 0,
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
"isup_rev": {
|
||||
"samt": 0,
|
||||
"csamt": 0,
|
||||
"txval": 0,
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
"osup_nongst": {
|
||||
"txval": 0,
|
||||
}
|
||||
},
|
||||
"inter_sup": {
|
||||
"unreg_details": [],
|
||||
"comp_details": [],
|
||||
"uin_details": []
|
||||
},
|
||||
"itc_elg": {
|
||||
"itc_avl": [
|
||||
{
|
||||
"csamt": 0,
|
||||
"samt": 0,
|
||||
"ty": "IMPG",
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
{
|
||||
"csamt": 0,
|
||||
"samt": 0,
|
||||
"ty": "IMPS",
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
{
|
||||
"samt": 0,
|
||||
"csamt": 0,
|
||||
"ty": "ISRC",
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
{
|
||||
"ty": "ISD",
|
||||
"iamt": 0,
|
||||
"camt": 0,
|
||||
"samt": 0,
|
||||
"csamt": 0
|
||||
},
|
||||
{
|
||||
"samt": 0,
|
||||
"csamt": 0,
|
||||
"ty": "OTH",
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
}
|
||||
],
|
||||
"itc_rev": [
|
||||
{
|
||||
"ty": "RUL",
|
||||
"iamt": 0,
|
||||
"camt": 0,
|
||||
"samt": 0,
|
||||
"csamt": 0
|
||||
},
|
||||
{
|
||||
"ty": "OTH",
|
||||
"iamt": 0,
|
||||
"camt": 0,
|
||||
"samt": 0,
|
||||
"csamt": 0
|
||||
}
|
||||
],
|
||||
"itc_net": {
|
||||
"samt": 0,
|
||||
"csamt": 0,
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
"itc_inelg": [
|
||||
{
|
||||
"ty": "RUL",
|
||||
"iamt": 0,
|
||||
"camt": 0,
|
||||
"samt": 0,
|
||||
"csamt": 0
|
||||
},
|
||||
{
|
||||
"ty": "OTH",
|
||||
"iamt": 0,
|
||||
"camt": 0,
|
||||
"samt": 0,
|
||||
"csamt": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
self.report_dict = json.loads(get_json('gstr_3b_report_template'))
|
||||
|
||||
self.gst_details = self.get_company_gst_details()
|
||||
self.report_dict["gstin"] = self.gst_details.get("gstin")
|
||||
@ -152,23 +25,19 @@ class GSTR3BReport(Document):
|
||||
self.month_no = get_period(self.month)
|
||||
self.account_heads = self.get_account_heads()
|
||||
|
||||
outward_supply_tax_amounts = self.get_tax_amounts("Sales Invoice")
|
||||
inward_supply_tax_amounts = self.get_tax_amounts("Purchase Invoice", reverse_charge="Y")
|
||||
self.get_outward_supply_details("Sales Invoice")
|
||||
self.set_outward_taxable_supplies()
|
||||
|
||||
self.get_outward_supply_details("Purchase Invoice", reverse_charge=True)
|
||||
self.set_supplies_liable_to_reverse_charge()
|
||||
|
||||
itc_details = self.get_itc_details()
|
||||
|
||||
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_det", ["Registered Regular"])
|
||||
self.prepare_data("Sales Invoice", outward_supply_tax_amounts, "sup_details", "osup_zero", ["SEZ", "Deemed Export", "Overseas"])
|
||||
self.prepare_data("Purchase Invoice", inward_supply_tax_amounts, "sup_details", "isup_rev", ["Unregistered", "Overseas"], reverse_charge="Y")
|
||||
self.report_dict["sup_details"]["osup_nil_exmp"]["txval"] = flt(self.get_nil_rated_supply_value(), 2)
|
||||
self.set_itc_details(itc_details)
|
||||
|
||||
inter_state_supplies = self.get_inter_state_supplies(self.gst_details.get("gst_state_number"))
|
||||
self.get_itc_reversal_entries()
|
||||
inward_nil_exempt = self.get_inward_nil_exempt(self.gst_details.get("gst_state"))
|
||||
self.set_inter_state_supply(inter_state_supplies)
|
||||
self.set_inward_nil_exempt(inward_nil_exempt)
|
||||
|
||||
self.missing_field_invoices = self.get_missing_field_invoices()
|
||||
|
||||
self.json_output = frappe.as_json(self.report_dict)
|
||||
|
||||
def set_inward_nil_exempt(self, inward_nil_exempt):
|
||||
@ -178,189 +47,95 @@ class GSTR3BReport(Document):
|
||||
self.report_dict["inward_sup"]["isup_details"][1]["intra"] = flt(inward_nil_exempt.get("non_gst").get("intra"), 2)
|
||||
|
||||
def set_itc_details(self, itc_details):
|
||||
|
||||
itc_type_map = {
|
||||
itc_eligible_type_map = {
|
||||
'IMPG': 'Import Of Capital Goods',
|
||||
'IMPS': 'Import Of Service',
|
||||
'ISRC': 'ITC on Reverse Charge',
|
||||
'ISD': 'Input Service Distributor',
|
||||
'OTH': 'All Other ITC'
|
||||
}
|
||||
|
||||
itc_ineligible_map = {
|
||||
'RUL': 'Ineligible As Per Section 17(5)',
|
||||
'OTH': 'Ineligible Others'
|
||||
}
|
||||
|
||||
net_itc = self.report_dict["itc_elg"]["itc_net"]
|
||||
|
||||
for d in self.report_dict["itc_elg"]["itc_avl"]:
|
||||
|
||||
itc_type = itc_type_map.get(d["ty"])
|
||||
|
||||
if d["ty"] == 'ISRC':
|
||||
reverse_charge = ["Y"]
|
||||
itc_type = 'All Other ITC'
|
||||
gst_category = ['Unregistered', 'Overseas']
|
||||
else:
|
||||
gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
|
||||
reverse_charge = ["N", "Y"]
|
||||
|
||||
for account_head in self.account_heads:
|
||||
for category in gst_category:
|
||||
for charge_type in reverse_charge:
|
||||
for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
|
||||
d[key[0]] += flt(itc_details.get((category, itc_type, charge_type, account_head.get(key[1])), {}).get("amount"), 2)
|
||||
|
||||
itc_type = itc_eligible_type_map.get(d["ty"])
|
||||
for key in ['iamt', 'camt', 'samt', 'csamt']:
|
||||
d[key] = flt(itc_details.get(itc_type, {}).get(key))
|
||||
net_itc[key] += flt(d[key], 2)
|
||||
|
||||
for account_head in self.account_heads:
|
||||
itc_inelg = self.report_dict["itc_elg"]["itc_inelg"][1]
|
||||
for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
|
||||
itc_inelg[key[0]] = flt(itc_details.get(("Ineligible", "N", account_head.get(key[1])), {}).get("amount"), 2)
|
||||
for d in self.report_dict["itc_elg"]["itc_inelg"]:
|
||||
itc_type = itc_ineligible_map.get(d["ty"])
|
||||
for key in ['iamt', 'camt', 'samt', 'csamt']:
|
||||
d[key] = flt(itc_details.get(itc_type, {}).get(key))
|
||||
|
||||
def prepare_data(self, doctype, tax_details, supply_type, supply_category, gst_category_list, reverse_charge="N"):
|
||||
def get_itc_reversal_entries(self):
|
||||
reversal_entries = frappe.db.sql("""
|
||||
SELECT ja.account, j.reversal_type, sum(credit_in_account_currency) as amount
|
||||
FROM `tabJournal Entry` j, `tabJournal Entry Account` ja
|
||||
where j.docstatus = 1
|
||||
and j.is_opening = 'No'
|
||||
and ja.parent = j.name
|
||||
and j.voucher_type = 'Reversal Of ITC'
|
||||
and month(j.posting_date) = %s and year(j.posting_date) = %s
|
||||
and j.company = %s and j.company_gstin = %s
|
||||
GROUP BY ja.account, j.reversal_type""", (self.month_no, self.year, self.company,
|
||||
self.gst_details.get("gstin")), as_dict=1)
|
||||
|
||||
account_map = {
|
||||
'sgst_account': 'samt',
|
||||
'cess_account': 'csamt',
|
||||
'cgst_account': 'camt',
|
||||
'igst_account': 'iamt'
|
||||
}
|
||||
net_itc = self.report_dict["itc_elg"]["itc_net"]
|
||||
|
||||
txval = 0
|
||||
total_taxable_value = self.get_total_taxable_value(doctype, reverse_charge)
|
||||
for entry in reversal_entries:
|
||||
if entry.reversal_type == 'As per rules 42 & 43 of CGST Rules':
|
||||
index = 0
|
||||
else:
|
||||
index = 1
|
||||
|
||||
for gst_category in gst_category_list:
|
||||
txval += total_taxable_value.get(gst_category,0)
|
||||
for account_head in self.account_heads:
|
||||
for account_type, account_name in iteritems(account_head):
|
||||
if account_map.get(account_type) in self.report_dict.get(supply_type).get(supply_category):
|
||||
self.report_dict[supply_type][supply_category][account_map.get(account_type)] += \
|
||||
flt(tax_details.get((account_name, gst_category), {}).get("amount"), 2)
|
||||
|
||||
self.report_dict[supply_type][supply_category]["txval"] += flt(txval, 2)
|
||||
|
||||
def set_inter_state_supply(self, inter_state_supply):
|
||||
osup_det = self.report_dict["sup_details"]["osup_det"]
|
||||
|
||||
for key, value in iteritems(inter_state_supply):
|
||||
if key[0] == "Unregistered":
|
||||
self.report_dict["inter_sup"]["unreg_details"].append(value)
|
||||
|
||||
if key[0] == "Registered Composition":
|
||||
self.report_dict["inter_sup"]["comp_details"].append(value)
|
||||
|
||||
if key[0] == "UIN Holders":
|
||||
self.report_dict["inter_sup"]["uin_details"].append(value)
|
||||
|
||||
def get_total_taxable_value(self, doctype, reverse_charge):
|
||||
|
||||
return frappe._dict(frappe.db.sql("""
|
||||
select gst_category, sum(net_total) as total
|
||||
from `tab{doctype}`
|
||||
where docstatus = 1 and month(posting_date) = %s
|
||||
and year(posting_date) = %s and reverse_charge = %s
|
||||
and company = %s and company_gstin = %s
|
||||
group by gst_category
|
||||
""" #nosec
|
||||
.format(doctype = doctype), (self.month_no, self.year, reverse_charge, self.company, self.gst_details.get("gstin"))))
|
||||
for key in ['camt', 'samt', 'iamt', 'csamt']:
|
||||
if entry.account in self.account_heads.get(key):
|
||||
self.report_dict["itc_elg"]["itc_rev"][index][key] += flt(entry.amount)
|
||||
net_itc[key] -= flt(entry.amount)
|
||||
|
||||
def get_itc_details(self):
|
||||
itc_amount = frappe.db.sql("""
|
||||
select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount,
|
||||
t.account_head, s.eligibility_for_itc, s.reverse_charge
|
||||
from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t
|
||||
where s.docstatus = 1 and t.parent = s.name
|
||||
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
|
||||
and s.company_gstin = %s
|
||||
group by t.account_head, s.gst_category, s.eligibility_for_itc
|
||||
""",
|
||||
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
|
||||
itc_amounts = frappe.db.sql("""
|
||||
SELECT eligibility_for_itc, sum(itc_integrated_tax) as itc_integrated_tax,
|
||||
sum(itc_central_tax) as itc_central_tax,
|
||||
sum(itc_state_tax) as itc_state_tax,
|
||||
sum(itc_cess_amount) as itc_cess_amount
|
||||
FROM `tabPurchase Invoice`
|
||||
WHERE docstatus = 1
|
||||
and is_opening = 'No'
|
||||
and month(posting_date) = %s and year(posting_date) = %s and company = %s
|
||||
and company_gstin = %s
|
||||
GROUP BY eligibility_for_itc
|
||||
""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
|
||||
|
||||
itc_details = {}
|
||||
|
||||
for d in itc_amount:
|
||||
itc_details.setdefault((d.gst_category, d.eligibility_for_itc, d.reverse_charge, d.account_head),{
|
||||
"amount": d.tax_amount
|
||||
for d in itc_amounts:
|
||||
itc_details.setdefault(d.eligibility_for_itc, {
|
||||
'iamt': d.itc_integrated_tax,
|
||||
'camt': d.itc_central_tax,
|
||||
'samt': d.itc_state_tax,
|
||||
'csamt': d.itc_cess_amount
|
||||
})
|
||||
|
||||
return itc_details
|
||||
|
||||
def get_nil_rated_supply_value(self):
|
||||
|
||||
return frappe.db.sql("""
|
||||
select sum(i.base_amount) as total from
|
||||
`tabSales Invoice Item` i, `tabSales Invoice` s
|
||||
where s.docstatus = 1 and i.parent = s.name and i.is_nil_exempt = 1
|
||||
and month(s.posting_date) = %s and year(s.posting_date) = %s
|
||||
and s.company = %s and s.company_gstin = %s""",
|
||||
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)[0].total
|
||||
|
||||
def get_inter_state_supplies(self, state_number):
|
||||
inter_state_supply_tax = frappe.db.sql(""" select t.account_head, t.tax_amount_after_discount_amount as tax_amount,
|
||||
s.name, s.net_total, s.place_of_supply, s.gst_category from `tabSales Invoice` s, `tabSales Taxes and Charges` t
|
||||
where t.parent = s.name and s.docstatus = 1 and month(s.posting_date) = %s and year(s.posting_date) = %s
|
||||
and s.company = %s and s.company_gstin = %s and s.gst_category in ('Unregistered', 'Registered Composition', 'UIN Holders')
|
||||
""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
|
||||
|
||||
inter_state_supply_tax_mapping = {}
|
||||
inter_state_supply_details = {}
|
||||
|
||||
for d in inter_state_supply_tax:
|
||||
inter_state_supply_tax_mapping.setdefault(d.name, {
|
||||
'place_of_supply': d.place_of_supply,
|
||||
'taxable_value': d.net_total,
|
||||
'gst_category': d.gst_category,
|
||||
'camt': 0.0,
|
||||
'samt': 0.0,
|
||||
'iamt': 0.0,
|
||||
'csamt': 0.0
|
||||
})
|
||||
|
||||
if d.account_head in [a.cgst_account for a in self.account_heads]:
|
||||
inter_state_supply_tax_mapping[d.name]['camt'] += d.tax_amount
|
||||
|
||||
if d.account_head in [a.sgst_account for a in self.account_heads]:
|
||||
inter_state_supply_tax_mapping[d.name]['samt'] += d.tax_amount
|
||||
|
||||
if d.account_head in [a.igst_account for a in self.account_heads]:
|
||||
inter_state_supply_tax_mapping[d.name]['iamt'] += d.tax_amount
|
||||
|
||||
if d.account_head in [a.cess_account for a in self.account_heads]:
|
||||
inter_state_supply_tax_mapping[d.name]['csamt'] += d.tax_amount
|
||||
|
||||
for key, value in iteritems(inter_state_supply_tax_mapping):
|
||||
if value.get('place_of_supply'):
|
||||
osup_det = self.report_dict["sup_details"]["osup_det"]
|
||||
osup_det["txval"] = flt(osup_det["txval"] + value['taxable_value'], 2)
|
||||
osup_det["iamt"] = flt(osup_det["iamt"] + value['iamt'], 2)
|
||||
osup_det["camt"] = flt(osup_det["camt"] + value['camt'], 2)
|
||||
osup_det["samt"] = flt(osup_det["samt"] + value['samt'], 2)
|
||||
osup_det["csamt"] = flt(osup_det["csamt"] + value['csamt'], 2)
|
||||
|
||||
if state_number != value.get('place_of_supply').split("-")[0]:
|
||||
inter_state_supply_details.setdefault((value.get('gst_category'), value.get('place_of_supply')), {
|
||||
"txval": 0.0,
|
||||
"pos": value.get('place_of_supply').split("-")[0],
|
||||
"iamt": 0.0
|
||||
})
|
||||
|
||||
inter_state_supply_details[(value.get('gst_category'), value.get('place_of_supply'))]['txval'] += value['taxable_value']
|
||||
inter_state_supply_details[(value.get('gst_category'), value.get('place_of_supply'))]['iamt'] += value['iamt']
|
||||
|
||||
return inter_state_supply_details
|
||||
|
||||
def get_inward_nil_exempt(self, state):
|
||||
inward_nil_exempt = frappe.db.sql(""" select p.place_of_supply, sum(i.base_amount) as base_amount,
|
||||
i.is_nil_exempt, i.is_non_gst from `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
||||
where p.docstatus = 1 and p.name = i.parent
|
||||
inward_nil_exempt = frappe.db.sql("""
|
||||
SELECT p.place_of_supply, sum(i.base_amount) as base_amount, i.is_nil_exempt, i.is_non_gst
|
||||
FROM `tabPurchase Invoice` p , `tabPurchase Invoice Item` i
|
||||
WHERE p.docstatus = 1 and p.name = i.parent
|
||||
and p.is_opening = 'No'
|
||||
and p.gst_category != 'Registered Composition'
|
||||
and (i.is_nil_exempt = 1 or i.is_non_gst = 1) and
|
||||
month(p.posting_date) = %s and year(p.posting_date) = %s and p.company = %s and p.company_gstin = %s
|
||||
group by p.place_of_supply, i.is_nil_exempt, i.is_non_gst""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
|
||||
|
||||
inward_nil_exempt += frappe.db.sql("""SELECT sum(base_net_total) as base_amount, gst_category, place_of_supply
|
||||
FROM `tabPurchase Invoice`
|
||||
WHERE docstatus = 1 and gst_category = 'Registered Composition'
|
||||
and month(posting_date) = %s and year(posting_date) = %s
|
||||
and company = %s and company_gstin = %s
|
||||
group by place_of_supply""", (self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
|
||||
and (i.is_nil_exempt = 1 or i.is_non_gst = 1 or p.gst_category = 'Registered Composition') and
|
||||
month(p.posting_date) = %s and year(p.posting_date) = %s
|
||||
and p.company = %s and p.company_gstin = %s
|
||||
GROUP BY p.place_of_supply, i.is_nil_exempt, i.is_non_gst""",
|
||||
(self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
|
||||
|
||||
inward_nil_exempt_details = {
|
||||
"gst": {
|
||||
@ -388,37 +163,193 @@ class GSTR3BReport(Document):
|
||||
|
||||
return inward_nil_exempt_details
|
||||
|
||||
def get_tax_amounts(self, doctype, reverse_charge="N"):
|
||||
def get_outward_supply_details(self, doctype, reverse_charge=None):
|
||||
self.get_outward_tax_invoices(doctype, reverse_charge=reverse_charge)
|
||||
self.get_outward_items(doctype)
|
||||
self.get_outward_tax_details(doctype)
|
||||
|
||||
def get_outward_tax_invoices(self, doctype, reverse_charge=None):
|
||||
self.invoices = []
|
||||
self.invoice_detail_map = {}
|
||||
condition = ''
|
||||
|
||||
if reverse_charge:
|
||||
condition += "AND reverse_charge = 'Y'"
|
||||
|
||||
invoice_details = frappe.db.sql("""
|
||||
SELECT
|
||||
name, gst_category, export_type, place_of_supply
|
||||
FROM
|
||||
`tab{doctype}`
|
||||
WHERE
|
||||
docstatus = 1
|
||||
AND month(posting_date) = %s
|
||||
AND year(posting_date) = %s
|
||||
AND company = %s
|
||||
AND company_gstin = %s
|
||||
AND is_opening = 'No'
|
||||
{reverse_charge}
|
||||
ORDER BY name
|
||||
""".format(doctype=doctype, reverse_charge=condition), (self.month_no, self.year,
|
||||
self.company, self.gst_details.get("gstin")), as_dict=1)
|
||||
|
||||
for d in invoice_details:
|
||||
self.invoice_detail_map.setdefault(d.name, d)
|
||||
self.invoices.append(d.name)
|
||||
|
||||
def get_outward_items(self, doctype):
|
||||
self.invoice_items = frappe._dict()
|
||||
self.is_nil_exempt = []
|
||||
self.is_non_gst = []
|
||||
|
||||
if self.get('invoices'):
|
||||
item_details = frappe.db.sql("""
|
||||
SELECT
|
||||
item_code, parent, taxable_value, base_net_amount, item_tax_rate,
|
||||
is_nil_exempt, is_non_gst
|
||||
FROM
|
||||
`tab%s Item`
|
||||
WHERE parent in (%s)
|
||||
""" % (doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1)
|
||||
|
||||
for d in item_details:
|
||||
if d.item_code not in self.invoice_items.get(d.parent, {}):
|
||||
self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code,
|
||||
sum((i.get('taxable_value', 0) or i.get('base_net_amount', 0)) for i in item_details
|
||||
if i.item_code == d.item_code and i.parent == d.parent))
|
||||
|
||||
if d.is_nil_exempt and d.item_code not in self.is_nil_exempt:
|
||||
self.is_nil_exempt.append(d.item_code)
|
||||
|
||||
if d.is_non_gst and d.item_code not in self.is_non_gst:
|
||||
self.is_non_gst.append(d.item_code)
|
||||
|
||||
def get_outward_tax_details(self, doctype):
|
||||
if doctype == "Sales Invoice":
|
||||
tax_template = 'Sales Taxes and Charges'
|
||||
elif doctype == "Purchase Invoice":
|
||||
tax_template = 'Purchase Taxes and Charges'
|
||||
|
||||
tax_amounts = frappe.db.sql("""
|
||||
select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head
|
||||
from `tab{doctype}` s , `tab{template}` t
|
||||
where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s
|
||||
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
|
||||
and s.company_gstin = %s
|
||||
group by t.account_head, s.gst_category
|
||||
""" #nosec
|
||||
.format(doctype=doctype, template=tax_template),
|
||||
(reverse_charge, self.month_no, self.year, self.company, self.gst_details.get("gstin")), as_dict=1)
|
||||
self.items_based_on_tax_rate = {}
|
||||
self.invoice_cess = frappe._dict()
|
||||
self.cgst_sgst_invoices = []
|
||||
|
||||
tax_details = {}
|
||||
if self.get('invoices'):
|
||||
tax_details = frappe.db.sql("""
|
||||
SELECT
|
||||
parent, account_head, item_wise_tax_detail, base_tax_amount_after_discount_amount
|
||||
FROM `tab%s`
|
||||
WHERE
|
||||
parenttype = %s and docstatus = 1
|
||||
and parent in (%s)
|
||||
ORDER BY account_head
|
||||
""" % (tax_template, '%s', ', '.join(['%s']*len(self.invoices))),
|
||||
tuple([doctype] + list(self.invoices)))
|
||||
|
||||
for d in tax_amounts:
|
||||
tax_details.setdefault(
|
||||
(d.account_head,d.gst_category),{
|
||||
"amount": d.get("tax_amount"),
|
||||
}
|
||||
)
|
||||
for parent, account, item_wise_tax_detail, tax_amount in tax_details:
|
||||
if account in self.account_heads.get('csamt'):
|
||||
self.invoice_cess.setdefault(parent, tax_amount)
|
||||
else:
|
||||
if item_wise_tax_detail:
|
||||
try:
|
||||
item_wise_tax_detail = json.loads(item_wise_tax_detail)
|
||||
cgst_or_sgst = False
|
||||
if account in self.account_heads.get('camt') \
|
||||
or account in self.account_heads.get('samt'):
|
||||
cgst_or_sgst = True
|
||||
|
||||
return tax_details
|
||||
for item_code, tax_amounts in item_wise_tax_detail.items():
|
||||
if not (cgst_or_sgst or account in self.account_heads.get('iamt') or
|
||||
(item_code in self.is_non_gst + self.is_nil_exempt)):
|
||||
continue
|
||||
|
||||
tax_rate = tax_amounts[0]
|
||||
if tax_rate:
|
||||
if cgst_or_sgst:
|
||||
tax_rate *= 2
|
||||
if parent not in self.cgst_sgst_invoices:
|
||||
self.cgst_sgst_invoices.append(parent)
|
||||
|
||||
rate_based_dict = self.items_based_on_tax_rate\
|
||||
.setdefault(parent, {}).setdefault(tax_rate, [])
|
||||
if item_code not in rate_based_dict:
|
||||
rate_based_dict.append(item_code)
|
||||
except ValueError:
|
||||
continue
|
||||
|
||||
|
||||
if self.get('invoice_items'):
|
||||
# Build itemised tax for export invoices, nil and exempted where tax table is blank
|
||||
for invoice, items in iteritems(self.invoice_items):
|
||||
if invoice not in self.items_based_on_tax_rate and (self.invoice_detail_map.get(invoice, {}).get('export_type')
|
||||
== "Without Payment of Tax"):
|
||||
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
|
||||
|
||||
def set_outward_taxable_supplies(self):
|
||||
inter_state_supply_details = {}
|
||||
|
||||
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
|
||||
for rate, items in items_based_on_rate.items():
|
||||
for item_code, taxable_value in self.invoice_items.get(inv).items():
|
||||
if item_code in items:
|
||||
if item_code in self.is_nil_exempt:
|
||||
self.report_dict['sup_details']['osup_nil_exmp']['txval'] += taxable_value
|
||||
elif item_code in self.is_non_gst:
|
||||
self.report_dict['sup_details']['osup_nongst']['txval'] += taxable_value
|
||||
elif rate == 0:
|
||||
self.report_dict['sup_details']['osup_zero']['txval'] += taxable_value
|
||||
#self.report_dict['sup_details']['osup_zero'][key] += tax_amount
|
||||
else:
|
||||
if inv in self.cgst_sgst_invoices:
|
||||
tax_rate = rate/2
|
||||
self.report_dict['sup_details']['osup_det']['camt'] += (taxable_value * tax_rate /100)
|
||||
self.report_dict['sup_details']['osup_det']['samt'] += (taxable_value * tax_rate /100)
|
||||
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
|
||||
else:
|
||||
self.report_dict['sup_details']['osup_det']['iamt'] += (taxable_value * rate /100)
|
||||
self.report_dict['sup_details']['osup_det']['txval'] += taxable_value
|
||||
|
||||
gst_category = self.invoice_detail_map.get(inv, {}).get('gst_category')
|
||||
place_of_supply = self.invoice_detail_map.get(inv, {}).get('place_of_supply') or '00-Other Territory'
|
||||
|
||||
if gst_category in ['Unregistered', 'Registered Composition', 'UIN Holders'] and \
|
||||
self.gst_details.get("gst_state") != place_of_supply.split("-")[1]:
|
||||
inter_state_supply_details.setdefault((gst_category, place_of_supply), {
|
||||
"txval": 0.0,
|
||||
"pos": place_of_supply.split("-")[0],
|
||||
"iamt": 0.0
|
||||
})
|
||||
inter_state_supply_details[(gst_category, place_of_supply)]['txval'] += taxable_value
|
||||
inter_state_supply_details[(gst_category, place_of_supply)]['iamt'] += (taxable_value * rate /100)
|
||||
|
||||
self.set_inter_state_supply(inter_state_supply_details)
|
||||
|
||||
def set_supplies_liable_to_reverse_charge(self):
|
||||
for inv, items_based_on_rate in self.items_based_on_tax_rate.items():
|
||||
for rate, items in items_based_on_rate.items():
|
||||
for item_code, taxable_value in self.invoice_items.get(inv).items():
|
||||
if item_code in items:
|
||||
if inv in self.cgst_sgst_invoices:
|
||||
tax_rate = rate/2
|
||||
self.report_dict['sup_details']['isup_rev']['camt'] += (taxable_value * tax_rate /100)
|
||||
self.report_dict['sup_details']['isup_rev']['samt'] += (taxable_value * tax_rate /100)
|
||||
self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value
|
||||
else:
|
||||
self.report_dict['sup_details']['isup_rev']['iamt'] += (taxable_value * rate /100)
|
||||
self.report_dict['sup_details']['isup_rev']['txval'] += taxable_value
|
||||
|
||||
def set_inter_state_supply(self, inter_state_supply):
|
||||
for key, value in iteritems(inter_state_supply):
|
||||
if key[0] == "Unregistered":
|
||||
self.report_dict["inter_sup"]["unreg_details"].append(value)
|
||||
|
||||
if key[0] == "Registered Composition":
|
||||
self.report_dict["inter_sup"]["comp_details"].append(value)
|
||||
|
||||
if key[0] == "UIN Holders":
|
||||
self.report_dict["inter_sup"]["uin_details"].append(value)
|
||||
|
||||
def get_company_gst_details(self):
|
||||
|
||||
gst_details = frappe.get_all("Address",
|
||||
fields=["gstin", "gst_state", "gst_state_number"],
|
||||
filters={
|
||||
@ -431,20 +362,28 @@ class GSTR3BReport(Document):
|
||||
frappe.throw(_("Please enter GSTIN and state for the Company Address {0}").format(self.company_address))
|
||||
|
||||
def get_account_heads(self):
|
||||
account_map = {
|
||||
'sgst_account': 'samt',
|
||||
'cess_account': 'csamt',
|
||||
'cgst_account': 'camt',
|
||||
'igst_account': 'iamt'
|
||||
}
|
||||
|
||||
account_heads = frappe.get_all("GST Account",
|
||||
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"],
|
||||
filters={
|
||||
"company":self.company
|
||||
})
|
||||
account_heads = {}
|
||||
gst_settings_accounts = frappe.get_all("GST Account",
|
||||
filters={'company': self.company, 'is_reverse_charge_account': 0},
|
||||
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
|
||||
|
||||
if account_heads:
|
||||
return account_heads
|
||||
else:
|
||||
frappe.throw(_("Please set account heads in GST Settings for Compnay {0}").format(self.company))
|
||||
if not gst_settings_accounts:
|
||||
frappe.throw(_("Please set GST Accounts in GST Settings"))
|
||||
|
||||
for d in gst_settings_accounts:
|
||||
for acc, val in d.items():
|
||||
account_heads.setdefault(account_map.get(acc), []).append(val)
|
||||
|
||||
return account_heads
|
||||
|
||||
def get_missing_field_invoices(self):
|
||||
|
||||
missing_field_invoices = []
|
||||
|
||||
for doctype in ["Sales Invoice", "Purchase Invoice"]:
|
||||
@ -456,26 +395,32 @@ class GSTR3BReport(Document):
|
||||
party_type = 'Supplier'
|
||||
party = 'supplier'
|
||||
|
||||
docnames = frappe.db.sql("""
|
||||
select t1.name from `tab{doctype}` t1, `tab{party_type}` t2
|
||||
where t1.docstatus = 1 and month(t1.posting_date) = %s and year(t1.posting_date) = %s
|
||||
docnames = frappe.db.sql(
|
||||
"""
|
||||
SELECT t1.name FROM `tab{doctype}` t1, `tab{party_type}` t2
|
||||
WHERE t1.docstatus = 1 and t1.is_opening = 'No'
|
||||
and month(t1.posting_date) = %s and year(t1.posting_date) = %s
|
||||
and t1.company = %s and t1.place_of_supply IS NULL and t1.{party} = t2.name and
|
||||
t2.gst_category != 'Overseas'
|
||||
""".format(doctype = doctype, party_type = party_type, party=party), (self.month_no, self.year, self.company), as_dict=1) #nosec
|
||||
""".format(doctype = doctype, party_type = party_type,
|
||||
party=party) ,(self.month_no, self.year, self.company), as_dict=1) #nosec
|
||||
|
||||
for d in docnames:
|
||||
missing_field_invoices.append(d.name)
|
||||
|
||||
return ",".join(missing_field_invoices)
|
||||
|
||||
def get_state_code(state):
|
||||
def get_json(template):
|
||||
file_path = os.path.join(os.path.dirname(__file__), '{template}.json'.format(template=template))
|
||||
with open(file_path, 'r') as f:
|
||||
return cstr(f.read())
|
||||
|
||||
def get_state_code(state):
|
||||
state_code = state_numbers.get(state)
|
||||
|
||||
return state_code
|
||||
|
||||
def get_period(month, year=None):
|
||||
|
||||
month_no = {
|
||||
"January": 1,
|
||||
"February": 2,
|
||||
@ -499,13 +444,11 @@ def get_period(month, year=None):
|
||||
|
||||
@frappe.whitelist()
|
||||
def view_report(name):
|
||||
|
||||
json_data = frappe.get_value("GSTR 3B Report", name, 'json_output')
|
||||
return json.loads(json_data)
|
||||
|
||||
@frappe.whitelist()
|
||||
def make_json(name):
|
||||
|
||||
json_data = frappe.get_value("GSTR 3B Report", name, 'json_output')
|
||||
file_name = "GST3B.json"
|
||||
frappe.local.response.filename = file_name
|
||||
|
@ -0,0 +1,127 @@
|
||||
{
|
||||
"gstin": "",
|
||||
"ret_period": "",
|
||||
"inward_sup": {
|
||||
"isup_details": [
|
||||
{
|
||||
"ty": "GST",
|
||||
"intra": 0,
|
||||
"inter": 0
|
||||
},
|
||||
{
|
||||
"ty": "NONGST",
|
||||
"inter": 0,
|
||||
"intra": 0
|
||||
}
|
||||
]
|
||||
},
|
||||
"sup_details": {
|
||||
"osup_zero": {
|
||||
"csamt": 0,
|
||||
"txval": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
"osup_nil_exmp": {
|
||||
"txval": 0
|
||||
},
|
||||
"osup_det": {
|
||||
"samt": 0,
|
||||
"csamt": 0,
|
||||
"txval": 0,
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
"isup_rev": {
|
||||
"samt": 0,
|
||||
"csamt": 0,
|
||||
"txval": 0,
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
"osup_nongst": {
|
||||
"txval": 0
|
||||
}
|
||||
},
|
||||
"inter_sup": {
|
||||
"unreg_details": [],
|
||||
"comp_details": [],
|
||||
"uin_details": []
|
||||
},
|
||||
"itc_elg": {
|
||||
"itc_avl": [
|
||||
{
|
||||
"csamt": 0,
|
||||
"samt": 0,
|
||||
"ty": "IMPG",
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
{
|
||||
"csamt": 0,
|
||||
"samt": 0,
|
||||
"ty": "IMPS",
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
{
|
||||
"samt": 0,
|
||||
"csamt": 0,
|
||||
"ty": "ISRC",
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
{
|
||||
"ty": "ISD",
|
||||
"iamt": 0,
|
||||
"camt": 0,
|
||||
"samt": 0,
|
||||
"csamt": 0
|
||||
},
|
||||
{
|
||||
"samt": 0,
|
||||
"csamt": 0,
|
||||
"ty": "OTH",
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
}
|
||||
],
|
||||
"itc_rev": [
|
||||
{
|
||||
"ty": "RUL",
|
||||
"iamt": 0,
|
||||
"camt": 0,
|
||||
"samt": 0,
|
||||
"csamt": 0
|
||||
},
|
||||
{
|
||||
"ty": "OTH",
|
||||
"iamt": 0,
|
||||
"camt": 0,
|
||||
"samt": 0,
|
||||
"csamt": 0
|
||||
}
|
||||
],
|
||||
"itc_net": {
|
||||
"samt": 0,
|
||||
"csamt": 0,
|
||||
"camt": 0,
|
||||
"iamt": 0
|
||||
},
|
||||
"itc_inelg": [
|
||||
{
|
||||
"ty": "RUL",
|
||||
"iamt": 0,
|
||||
"camt": 0,
|
||||
"samt": 0,
|
||||
"csamt": 0
|
||||
},
|
||||
{
|
||||
"ty": "OTH",
|
||||
"iamt": 0,
|
||||
"camt": 0,
|
||||
"samt": 0,
|
||||
"csamt": 0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -60,8 +60,7 @@ class TestGSTR3BReport(unittest.TestCase):
|
||||
|
||||
output = json.loads(report.json_output)
|
||||
|
||||
self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 36),
|
||||
self.assertEqual(output["sup_details"]["osup_zero"]["iamt"], 18),
|
||||
self.assertEqual(output["sup_details"]["osup_det"]["iamt"], 54)
|
||||
self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18),
|
||||
self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100),
|
||||
self.assertEqual(output["inward_sup"]["isup_details"][0]["intra"], 250)
|
||||
|
@ -55,8 +55,7 @@ def get_datev_csv(data, filters, csv_class):
|
||||
quoting=QUOTE_NONNUMERIC
|
||||
)
|
||||
|
||||
if not six.PY2:
|
||||
data = data.encode('latin_1', errors='replace')
|
||||
data = data.encode('latin_1', errors='replace')
|
||||
|
||||
header = get_header(filters, csv_class)
|
||||
header = ';'.join(header).encode('latin_1', errors='replace')
|
||||
|
@ -43,8 +43,9 @@ def validate_eligibility(doc):
|
||||
invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
|
||||
company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
|
||||
no_taxes_applied = not doc.get('taxes')
|
||||
has_non_gst_item = any(d for d in doc.get('items', []) if d.get('is_non_gst'))
|
||||
|
||||
if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied:
|
||||
if invalid_company or invalid_supply_type or company_transaction or no_taxes_applied or has_non_gst_item:
|
||||
return False
|
||||
|
||||
return True
|
||||
@ -533,11 +534,9 @@ def santize_einvoice_fields(einvoice):
|
||||
return einvoice
|
||||
|
||||
def safe_json_load(json_string):
|
||||
JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
|
||||
|
||||
try:
|
||||
return json.loads(json_string)
|
||||
except JSONDecodeError as e:
|
||||
except json.JSONDecodeError as e:
|
||||
# print a snippet of 40 characters around the location where error occured
|
||||
pos = e.pos
|
||||
start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
|
||||
|
@ -114,9 +114,12 @@ def add_print_formats():
|
||||
|
||||
def make_property_setters(patch=False):
|
||||
# GST rules do not allow for an invoice no. bigger than 16 characters
|
||||
journal_entry_types = frappe.get_meta("Journal Entry").get_options("voucher_type").split("\n") + ['Reversal Of ITC']
|
||||
|
||||
if not patch:
|
||||
make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '')
|
||||
make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '')
|
||||
make_property_setter('Journal Entry', 'voucher_type', 'options', '\n'.join(journal_entry_types), '')
|
||||
|
||||
def make_custom_fields(update=True):
|
||||
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
|
||||
@ -198,15 +201,20 @@ def make_custom_fields(update=True):
|
||||
purchase_invoice_itc_fields = [
|
||||
dict(fieldname='eligibility_for_itc', label='Eligibility For ITC',
|
||||
fieldtype='Select', insert_after='reason_for_issuing_document', print_hide=1,
|
||||
options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nIneligible\nAll Other ITC', default="All Other ITC"),
|
||||
options='Input Service Distributor\nImport Of Service\nImport Of Capital Goods\nITC on Reverse Charge\nIneligible As Per Section 17(5)\nIneligible Others\nAll Other ITC',
|
||||
default="All Other ITC"),
|
||||
dict(fieldname='itc_integrated_tax', label='Availed ITC Integrated Tax',
|
||||
fieldtype='Data', insert_after='eligibility_for_itc', print_hide=1),
|
||||
fieldtype='Currency', insert_after='eligibility_for_itc',
|
||||
options='Company:company:default_currency', print_hide=1),
|
||||
dict(fieldname='itc_central_tax', label='Availed ITC Central Tax',
|
||||
fieldtype='Data', insert_after='itc_integrated_tax', print_hide=1),
|
||||
fieldtype='Currency', insert_after='itc_integrated_tax',
|
||||
options='Company:company:default_currency', print_hide=1),
|
||||
dict(fieldname='itc_state_tax', label='Availed ITC State/UT Tax',
|
||||
fieldtype='Data', insert_after='itc_central_tax', print_hide=1),
|
||||
fieldtype='Currency', insert_after='itc_central_tax',
|
||||
options='Company:company:default_currency', print_hide=1),
|
||||
dict(fieldname='itc_cess_amount', label='Availed ITC Cess',
|
||||
fieldtype='Data', insert_after='itc_state_tax', print_hide=1),
|
||||
fieldtype='Currency', insert_after='itc_state_tax',
|
||||
options='Company:company:default_currency', print_hide=1),
|
||||
]
|
||||
|
||||
sales_invoice_gst_fields = [
|
||||
@ -236,6 +244,23 @@ def make_custom_fields(update=True):
|
||||
depends_on="eval:doc.gst_category=='Overseas' "),
|
||||
]
|
||||
|
||||
journal_entry_fields = [
|
||||
dict(fieldname='reversal_type', label='Reversal Type',
|
||||
fieldtype='Select', insert_after='voucher_type', print_hide=1,
|
||||
options="As per rules 42 & 43 of CGST Rules\nOthers",
|
||||
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
|
||||
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
|
||||
dict(fieldname='company_address', label='Company Address',
|
||||
fieldtype='Link', options='Address', insert_after='reversal_type',
|
||||
print_hide=1, depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
|
||||
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'"),
|
||||
dict(fieldname='company_gstin', label='Company GSTIN',
|
||||
fieldtype='Data', read_only=1, insert_after='company_address', print_hide=1,
|
||||
fetch_from='company_address.gstin',
|
||||
depends_on="eval:doc.voucher_type=='Reversal Of ITC'",
|
||||
mandatory_depends_on="eval:doc.voucher_type=='Reversal Of ITC'")
|
||||
]
|
||||
|
||||
inter_state_gst_field = [
|
||||
dict(fieldname='is_inter_state', label='Is Inter State',
|
||||
fieldtype='Check', insert_after='disabled', print_hide=1),
|
||||
@ -430,13 +455,13 @@ def make_custom_fields(update=True):
|
||||
|
||||
dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type',
|
||||
print_hide=1, hidden=1),
|
||||
|
||||
|
||||
dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section',
|
||||
no_copy=1, print_hide=1),
|
||||
|
||||
|
||||
dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
|
||||
dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',
|
||||
no_copy=1, print_hide=1),
|
||||
|
||||
dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date',
|
||||
@ -469,6 +494,7 @@ def make_custom_fields(update=True):
|
||||
'Purchase Receipt': purchase_invoice_gst_fields,
|
||||
'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
|
||||
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category,
|
||||
'Journal Entry': journal_entry_fields,
|
||||
'Sales Order': sales_invoice_gst_fields,
|
||||
'Tax Category': inter_state_gst_field,
|
||||
'Item': [
|
||||
@ -486,7 +512,7 @@ def make_custom_fields(update=True):
|
||||
'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
|
||||
'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value],
|
||||
'Material Request Item': [hsn_sac_field, nil_rated_exempt, is_non_gst],
|
||||
'Salary Component': [
|
||||
dict(fieldname= 'component_type',
|
||||
|
@ -204,8 +204,6 @@ def get_regional_address_details(party_details, doctype, company):
|
||||
|
||||
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
|
||||
master_doctype = "Sales Taxes and Charges Template"
|
||||
|
||||
get_tax_template_for_sez(party_details, master_doctype, company, 'Customer')
|
||||
get_tax_template_based_on_category(master_doctype, company, party_details)
|
||||
|
||||
if party_details.get('taxes_and_charges'):
|
||||
@ -216,7 +214,6 @@ def get_regional_address_details(party_details, doctype, company):
|
||||
|
||||
elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"):
|
||||
master_doctype = "Purchase Taxes and Charges Template"
|
||||
get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier')
|
||||
get_tax_template_based_on_category(master_doctype, company, party_details)
|
||||
|
||||
if party_details.get('taxes_and_charges'):
|
||||
@ -283,20 +280,6 @@ def get_tax_template(master_doctype, company, is_inter_state, state_code):
|
||||
{'company': company, 'disabled': 0, 'tax_category': tax_category.name}, 'name')
|
||||
return default_tax
|
||||
|
||||
def get_tax_template_for_sez(party_details, master_doctype, company, party_type):
|
||||
|
||||
gst_details = frappe.db.get_value(party_type, {'name': party_details.get(frappe.scrub(party_type))},
|
||||
['gst_category', 'export_type'], as_dict=1)
|
||||
|
||||
if gst_details:
|
||||
if gst_details.gst_category == 'SEZ' and gst_details.export_type == 'With Payment of Tax':
|
||||
default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0,
|
||||
"gst_state": number_state_mapping[party_details.company_gstin[:2]]})
|
||||
|
||||
party_details["taxes_and_charges"] = default_tax
|
||||
party_details.taxes = get_taxes_and_charges(master_doctype, default_tax)
|
||||
|
||||
|
||||
def calculate_annual_eligible_hra_exemption(doc):
|
||||
basic_component, hra_component = frappe.db.get_value('Company', doc.company, ["basic_component", "hra_component"])
|
||||
if not (basic_component and hra_component):
|
||||
@ -697,13 +680,22 @@ def validate_state_code(state_code, address):
|
||||
return int(state_code)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_gst_accounts(company, account_wise=False):
|
||||
def get_gst_accounts(company=None, account_wise=False, only_reverse_charge=0, only_non_reverse_charge=0):
|
||||
filters={"parent": "GST Settings"}
|
||||
|
||||
if company:
|
||||
filters.update({'company': company})
|
||||
if only_reverse_charge:
|
||||
filters.update({'is_reverse_charge_account': 1})
|
||||
elif only_non_reverse_charge:
|
||||
filters.update({'is_reverse_charge_account': 0})
|
||||
|
||||
gst_accounts = frappe._dict()
|
||||
gst_settings_accounts = frappe.get_all("GST Account",
|
||||
filters={"parent": "GST Settings", "company": company},
|
||||
filters=filters,
|
||||
fields=["cgst_account", "sgst_account", "igst_account", "cess_account"])
|
||||
|
||||
if not gst_settings_accounts and not frappe.flags.in_test:
|
||||
if not gst_settings_accounts and not frappe.flags.in_test and not frappe.flags.in_migrate:
|
||||
frappe.throw(_("Please set GST Accounts in GST Settings"))
|
||||
|
||||
for d in gst_settings_accounts:
|
||||
@ -715,101 +707,63 @@ def get_gst_accounts(company, account_wise=False):
|
||||
|
||||
return gst_accounts
|
||||
|
||||
def update_grand_total_for_rcm(doc, method):
|
||||
def validate_reverse_charge_transaction(doc, method):
|
||||
country = frappe.get_cached_value('Company', doc.company, 'country')
|
||||
|
||||
if country != 'India':
|
||||
return
|
||||
|
||||
gst_tax, base_gst_tax = get_gst_tax_amount(doc)
|
||||
|
||||
if not base_gst_tax:
|
||||
return
|
||||
base_gst_tax = 0
|
||||
base_reverse_charge_booked = 0
|
||||
|
||||
if doc.reverse_charge == 'Y':
|
||||
doc.taxes_and_charges_added -= gst_tax
|
||||
doc.total_taxes_and_charges -= gst_tax
|
||||
doc.base_taxes_and_charges_added -= base_gst_tax
|
||||
doc.base_total_taxes_and_charges -= base_gst_tax
|
||||
gst_accounts = get_gst_accounts(doc.company, only_reverse_charge=1)
|
||||
reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
|
||||
+ gst_accounts.get('igst_account')
|
||||
|
||||
update_totals(gst_tax, base_gst_tax, doc)
|
||||
|
||||
def update_totals(gst_tax, base_gst_tax, doc):
|
||||
doc.base_grand_total -= base_gst_tax
|
||||
doc.grand_total -= gst_tax
|
||||
|
||||
if doc.meta.get_field("rounded_total"):
|
||||
if doc.is_rounded_total_disabled():
|
||||
doc.outstanding_amount = doc.grand_total
|
||||
else:
|
||||
doc.rounded_total = round_based_on_smallest_currency_fraction(doc.grand_total,
|
||||
doc.currency, doc.precision("rounded_total"))
|
||||
|
||||
doc.rounding_adjustment += flt(doc.rounded_total - doc.grand_total,
|
||||
doc.precision("rounding_adjustment"))
|
||||
|
||||
doc.outstanding_amount = doc.rounded_total or doc.grand_total
|
||||
|
||||
doc.in_words = money_in_words(doc.grand_total, doc.currency)
|
||||
doc.base_in_words = money_in_words(doc.base_grand_total, erpnext.get_company_currency(doc.company))
|
||||
doc.set_payment_schedule()
|
||||
|
||||
def make_regional_gl_entries(gl_entries, doc):
|
||||
country = frappe.get_cached_value('Company', doc.company, 'country')
|
||||
|
||||
if country != 'India':
|
||||
return gl_entries
|
||||
|
||||
gst_tax, base_gst_tax = get_gst_tax_amount(doc)
|
||||
|
||||
if not base_gst_tax:
|
||||
return gl_entries
|
||||
|
||||
if doc.reverse_charge == 'Y':
|
||||
gst_accounts = get_gst_accounts(doc.company)
|
||||
gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
|
||||
gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1)
|
||||
non_reverse_charge_accounts = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
|
||||
+ gst_accounts.get('igst_account')
|
||||
|
||||
for tax in doc.get('taxes'):
|
||||
if tax.category not in ("Total", "Valuation and Total"):
|
||||
continue
|
||||
if tax.account_head in non_reverse_charge_accounts:
|
||||
if tax.add_deduct_tax == 'Add':
|
||||
base_gst_tax += tax.base_tax_amount_after_discount_amount
|
||||
else:
|
||||
base_gst_tax += tax.base_tax_amount_after_discount_amount
|
||||
elif tax.account_head in reverse_charge_accounts:
|
||||
if tax.add_deduct_tax == 'Add':
|
||||
base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount
|
||||
else:
|
||||
base_reverse_charge_booked += tax.base_tax_amount_after_discount_amount
|
||||
|
||||
dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
|
||||
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
|
||||
account_currency = get_account_currency(tax.account_head)
|
||||
if base_gst_tax != base_reverse_charge_booked:
|
||||
msg = _("Booked reverse charge is not equal to applied tax amount")
|
||||
msg += "<br>"
|
||||
msg += _("Please refer {gst_document_link} to learn more about how to setup and create reverse charge invoice").format(
|
||||
gst_document_link='<a href="https://docs.erpnext.com/docs/user/manual/en/regional/india/gst-setup">GST Documentation</a>')
|
||||
|
||||
gl_entries.append(doc.get_gl_dict(
|
||||
{
|
||||
"account": tax.account_head,
|
||||
"cost_center": tax.cost_center,
|
||||
"posting_date": doc.posting_date,
|
||||
"against": doc.supplier,
|
||||
dr_or_cr: tax.base_tax_amount_after_discount_amount,
|
||||
dr_or_cr + "_in_account_currency": tax.base_tax_amount_after_discount_amount \
|
||||
if account_currency==doc.company_currency \
|
||||
else tax.tax_amount_after_discount_amount
|
||||
}, account_currency, item=tax)
|
||||
)
|
||||
frappe.throw(msg)
|
||||
|
||||
return gl_entries
|
||||
def update_itc_availed_fields(doc, method):
|
||||
country = frappe.get_cached_value('Company', doc.company, 'country')
|
||||
|
||||
def get_gst_tax_amount(doc):
|
||||
gst_accounts = get_gst_accounts(doc.company)
|
||||
gst_account_list = gst_accounts.get('cgst_account', []) + gst_accounts.get('sgst_account', []) \
|
||||
+ gst_accounts.get('igst_account', [])
|
||||
if country != 'India':
|
||||
return
|
||||
|
||||
base_gst_tax = 0
|
||||
gst_tax = 0
|
||||
# Initialize values
|
||||
doc.itc_integrated_tax = doc.itc_state_tax = doc.itc_central_tax = doc.itc_cess_amount = 0
|
||||
gst_accounts = get_gst_accounts(doc.company, only_non_reverse_charge=1)
|
||||
|
||||
for tax in doc.get('taxes'):
|
||||
if tax.category not in ("Total", "Valuation and Total"):
|
||||
continue
|
||||
|
||||
if flt(tax.base_tax_amount_after_discount_amount) and tax.account_head in gst_account_list:
|
||||
base_gst_tax += tax.base_tax_amount_after_discount_amount
|
||||
gst_tax += tax.tax_amount_after_discount_amount
|
||||
|
||||
return gst_tax, base_gst_tax
|
||||
if tax.account_head in gst_accounts.get('igst_account', []):
|
||||
doc.itc_integrated_tax += flt(tax.base_tax_amount_after_discount_amount)
|
||||
if tax.account_head in gst_accounts.get('sgst_account', []):
|
||||
doc.itc_state_tax += flt(tax.base_tax_amount_after_discount_amount)
|
||||
if tax.account_head in gst_accounts.get('cgst_account', []):
|
||||
doc.itc_central_tax += flt(tax.base_tax_amount_after_discount_amount)
|
||||
if tax.account_head in gst_accounts.get('cess_account', []):
|
||||
doc.itc_cess_amount += flt(tax.base_tax_amount_after_discount_amount)
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_regional_round_off_accounts(company, account_list):
|
||||
|
@ -46,7 +46,13 @@ frappe.query_reports["GSTR-1"] = {
|
||||
"label": __("Type of Business"),
|
||||
"fieldtype": "Select",
|
||||
"reqd": 1,
|
||||
"options": ["B2B", "B2C Large", "B2C Small", "CDNR", "EXPORT"],
|
||||
"options": [
|
||||
{ "value": "B2B", "label": __("B2B Invoices - 4A, 4B, 4C, 6B, 6C") },
|
||||
{ "value": "B2C Large", "label": __("B2C(Large) Invoices - 5A, 5B") },
|
||||
{ "value": "B2C Small", "label": __("B2C(Small) Invoices - 7") },
|
||||
{ "value": "CDNR-REG", "label": __("Credit/Debit Notes (Registered) - 9B") },
|
||||
{ "value": "EXPORT", "label": __("Export Invoice - 6A") }
|
||||
],
|
||||
"default": "B2B"
|
||||
}
|
||||
],
|
||||
|
@ -32,6 +32,7 @@ class Gstr1Report(object):
|
||||
reverse_charge,
|
||||
return_against,
|
||||
is_return,
|
||||
is_debit_note,
|
||||
gst_category,
|
||||
export_type,
|
||||
port_code,
|
||||
@ -42,7 +43,7 @@ class Gstr1Report(object):
|
||||
|
||||
def run(self):
|
||||
self.get_columns()
|
||||
self.gst_accounts = get_gst_accounts(self.filters.company)
|
||||
self.gst_accounts = get_gst_accounts(self.filters.company, only_non_reverse_charge=1)
|
||||
self.get_invoice_data()
|
||||
|
||||
if self.invoices:
|
||||
@ -62,9 +63,9 @@ class Gstr1Report(object):
|
||||
for rate, items in items_based_on_rate.items():
|
||||
row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items)
|
||||
|
||||
if self.filters.get("type_of_business") == "CDNR":
|
||||
if self.filters.get("type_of_business") == "CDNR-REG":
|
||||
row.append("Y" if invoice_details.posting_date <= date(2017, 7, 1) else "N")
|
||||
row.append("C" if invoice_details.return_against else "R")
|
||||
row.append("C" if invoice_details.is_return else "D")
|
||||
|
||||
if taxable_value:
|
||||
self.data.append(row)
|
||||
@ -105,7 +106,7 @@ class Gstr1Report(object):
|
||||
def get_row_data_for_invoice(self, invoice, invoice_details, tax_rate, items):
|
||||
row = []
|
||||
for fieldname in self.invoice_fields:
|
||||
if self.filters.get("type_of_business") == "CDNR" and fieldname == "invoice_value":
|
||||
if self.filters.get("type_of_business") == "CDNR-REG" and fieldname == "invoice_value":
|
||||
row.append(abs(invoice_details.base_rounded_total) or abs(invoice_details.base_grand_total))
|
||||
elif fieldname == "invoice_value":
|
||||
row.append(invoice_details.base_rounded_total or invoice_details.base_grand_total)
|
||||
@ -171,7 +172,7 @@ class Gstr1Report(object):
|
||||
|
||||
|
||||
if self.filters.get("type_of_business") == "B2B":
|
||||
conditions += "and ifnull(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') and is_return != 1"
|
||||
conditions += "AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ') AND is_return != 1"
|
||||
|
||||
if self.filters.get("type_of_business") in ("B2C Large", "B2C Small"):
|
||||
b2c_limit = frappe.db.get_single_value('GST Settings', 'b2c_limit')
|
||||
@ -179,19 +180,19 @@ class Gstr1Report(object):
|
||||
frappe.throw(_("Please set B2C Limit in GST Settings."))
|
||||
|
||||
if self.filters.get("type_of_business") == "B2C Large":
|
||||
conditions += """ and ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
|
||||
and grand_total > {0} and is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
|
||||
conditions += """ AND ifnull(SUBSTR(place_of_supply, 1, 2),'') != ifnull(SUBSTR(company_gstin, 1, 2),'')
|
||||
AND grand_total > {0} AND is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
|
||||
|
||||
elif self.filters.get("type_of_business") == "B2C Small":
|
||||
conditions += """ and (
|
||||
conditions += """ AND (
|
||||
SUBSTR(place_of_supply, 1, 2) = SUBSTR(company_gstin, 1, 2)
|
||||
or grand_total <= {0}) and is_return != 1 and gst_category ='Unregistered' """.format(flt(b2c_limit))
|
||||
OR grand_total <= {0}) and is_return != 1 AND gst_category ='Unregistered' """.format(flt(b2c_limit))
|
||||
|
||||
elif self.filters.get("type_of_business") == "CDNR":
|
||||
conditions += """ and is_return = 1 """
|
||||
elif self.filters.get("type_of_business") == "CDNR-REG":
|
||||
conditions += """ AND (is_return = 1 OR is_debit_note = 1) AND IFNULL(gst_category, '') in ('Registered Regular', 'Deemed Export', 'SEZ')"""
|
||||
|
||||
elif self.filters.get("type_of_business") == "EXPORT":
|
||||
conditions += """ and is_return !=1 and gst_category = 'Overseas' """
|
||||
conditions += """ AND is_return !=1 and gst_category = 'Overseas' """
|
||||
return conditions
|
||||
|
||||
def get_invoice_items(self):
|
||||
@ -403,7 +404,7 @@ class Gstr1Report(object):
|
||||
"width": 100
|
||||
}
|
||||
]
|
||||
elif self.filters.get("type_of_business") == "CDNR":
|
||||
elif self.filters.get("type_of_business") == "CDNR-REG":
|
||||
self.invoice_columns = [
|
||||
{
|
||||
"fieldname": "customer_gstin",
|
||||
@ -437,6 +438,17 @@ class Gstr1Report(object):
|
||||
"options": "Sales Invoice",
|
||||
"width":120
|
||||
},
|
||||
{
|
||||
"fieldname": "reverse_charge",
|
||||
"label": "Reverse Charge",
|
||||
"fieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "export_type",
|
||||
"label": "Export Type",
|
||||
"fieldtype": "Data",
|
||||
"hidden": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "reason_for_issuing_document",
|
||||
"label": "Reason For Issuing document",
|
||||
@ -449,6 +461,11 @@ class Gstr1Report(object):
|
||||
"fieldtype": "Data",
|
||||
"width": 120
|
||||
},
|
||||
{
|
||||
"fieldname": "gst_category",
|
||||
"label": "GST Category",
|
||||
"fieldtype": "Data"
|
||||
},
|
||||
{
|
||||
"fieldname": "invoice_value",
|
||||
"label": "Invoice Value",
|
||||
@ -458,10 +475,10 @@ class Gstr1Report(object):
|
||||
]
|
||||
self.other_columns = [
|
||||
{
|
||||
"fieldname": "cess_amount",
|
||||
"label": "Cess Amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 100
|
||||
"fieldname": "cess_amount",
|
||||
"label": "Cess Amount",
|
||||
"fieldtype": "Currency",
|
||||
"width": 100
|
||||
},
|
||||
{
|
||||
"fieldname": "pre_gst",
|
||||
@ -589,6 +606,12 @@ def get_json(filters, report_name, data):
|
||||
|
||||
out = get_export_json(res)
|
||||
gst_json["exp"] = out
|
||||
elif filters["type_of_business"] == 'CDNR-REG':
|
||||
for item in report_data[:-1]:
|
||||
res.setdefault(item["customer_gstin"], {}).setdefault(item["invoice_number"],[]).append(item)
|
||||
|
||||
out = get_cdnr_reg_json(res, gstin)
|
||||
gst_json["cdnr"] = out
|
||||
|
||||
return {
|
||||
'report_name': report_name,
|
||||
@ -628,7 +651,6 @@ def get_b2b_json(res, gstin):
|
||||
return out
|
||||
|
||||
def get_b2cs_json(data, gstin):
|
||||
|
||||
company_state_number = gstin[0:2]
|
||||
|
||||
out = []
|
||||
@ -713,6 +735,54 @@ def get_export_json(res):
|
||||
|
||||
return out
|
||||
|
||||
def get_cdnr_reg_json(res, gstin):
|
||||
out = []
|
||||
|
||||
for gst_in in res:
|
||||
cdnr_item, inv = {"ctin": gst_in, "nt": []}, []
|
||||
if not gst_in: continue
|
||||
|
||||
for number, invoice in iteritems(res[gst_in]):
|
||||
if not invoice[0]["place_of_supply"]:
|
||||
frappe.throw(_("""{0} not entered in Invoice {1}.
|
||||
Please update and try again""").format(frappe.bold("Place Of Supply"),
|
||||
frappe.bold(invoice[0]['invoice_number'])))
|
||||
|
||||
inv_item = {
|
||||
"nt_num": invoice[0]["invoice_number"],
|
||||
"nt_dt": getdate(invoice[0]["posting_date"]).strftime('%d-%m-%Y'),
|
||||
"val": abs(flt(invoice[0]["invoice_value"])),
|
||||
"ntty": invoice[0]["document_type"],
|
||||
"pos": "%02d" % int(invoice[0]["place_of_supply"].split('-')[0]),
|
||||
"rchrg": invoice[0]["reverse_charge"],
|
||||
"inv_type": get_invoice_type_for_cdnr(invoice[0])
|
||||
}
|
||||
|
||||
inv_item["itms"] = []
|
||||
for item in invoice:
|
||||
inv_item["itms"].append(get_rate_and_tax_details(item, gstin))
|
||||
|
||||
inv.append(inv_item)
|
||||
|
||||
if not inv: continue
|
||||
cdnr_item["nt"] = inv
|
||||
out.append(cdnr_item)
|
||||
|
||||
return out
|
||||
|
||||
def get_invoice_type_for_cdnr(row):
|
||||
if row.get('gst_category') == 'SEZ':
|
||||
if row.get('export_type') == 'WPAY':
|
||||
invoice_type = 'SEWP'
|
||||
else:
|
||||
invoice_type = 'SEWOP'
|
||||
elif row.get('gst_category') == 'Deemed Export':
|
||||
row.invoice_type = 'DE'
|
||||
elif row.get('gst_category') == 'Registered Regular':
|
||||
invoice_type = 'R'
|
||||
|
||||
return invoice_type
|
||||
|
||||
def get_basic_invoice_detail(row):
|
||||
return {
|
||||
"inum": row["invoice_number"],
|
||||
|
@ -241,7 +241,7 @@ erpnext.PointOfSale.PastOrderSummary = class {
|
||||
|
||||
send_email() {
|
||||
const frm = this.events.get_frm();
|
||||
const recipients = this.email_dialog.get_values().recipients;
|
||||
const recipients = this.email_dialog.get_values().email_id;
|
||||
const doc = this.doc || frm.doc;
|
||||
const print_format = frm.pos_print_format;
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user