diff --git a/.flake8 b/.flake8
index 399b176e1d0..56c9b9a3699 100644
--- a/.flake8
+++ b/.flake8
@@ -29,4 +29,5 @@ ignore =
B950,
W191,
-max-line-length = 200
\ No newline at end of file
+max-line-length = 200
+exclude=.github/helper/semgrep_rules
diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs
index c820d233a5e..e7fa354a2e9 100644
--- a/.git-blame-ignore-revs
+++ b/.git-blame-ignore-revs
@@ -10,3 +10,6 @@
# Replace use of Class.extend with native JS class
1fe891b287a1b3f225d29ee3d07e7b1824aba9e7
+
+# This commit just changes spaces to tabs for indentation in some files
+5f473611bd6ed57703716244a054d3fb5ba9cd23
diff --git a/.github/helper/install.sh b/.github/helper/install.sh
index 7b0f944c669..f7a71223436 100644
--- a/.github/helper/install.sh
+++ b/.github/helper/install.sh
@@ -44,3 +44,4 @@ sed -i 's/redis_socketio:/# redis_socketio:/g' Procfile
bench get-app erpnext "${GITHUB_WORKSPACE}"
bench start &
bench --site test_site reinstall --yes
+bench build --app frappe
diff --git a/.github/helper/semgrep_rules/frappe_correctness.py b/.github/helper/semgrep_rules/frappe_correctness.py
index 4798b927f83..745e6463b8a 100644
--- a/.github/helper/semgrep_rules/frappe_correctness.py
+++ b/.github/helper/semgrep_rules/frappe_correctness.py
@@ -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"
diff --git a/.github/helper/semgrep_rules/frappe_correctness.yml b/.github/helper/semgrep_rules/frappe_correctness.yml
index 54df0624806..faab3344a62 100644
--- a/.github/helper/semgrep_rules/frappe_correctness.yml
+++ b/.github/helper/semgrep_rules/frappe_correctness.yml
@@ -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
diff --git a/.github/helper/semgrep_rules/translate.js b/.github/helper/semgrep_rules/translate.js
index 7b92fe2dffb..9cdfb75d0be 100644
--- a/.github/helper/semgrep_rules/translate.js
+++ b/.github/helper/semgrep_rules/translate.js
@@ -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])
diff --git a/.github/helper/semgrep_rules/translate.py b/.github/helper/semgrep_rules/translate.py
index bd6cd9126c9..9de6aa94f01 100644
--- a/.github/helper/semgrep_rules/translate.py
+++ b/.github/helper/semgrep_rules/translate.py
@@ -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
diff --git a/.github/helper/semgrep_rules/translate.yml b/.github/helper/semgrep_rules/translate.yml
index 3737da5a7e2..5f03fb9fd00 100644
--- a/.github/helper/semgrep_rules/translate.yml
+++ b/.github/helper/semgrep_rules/translate.yml
@@ -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.
diff --git a/.github/helper/semgrep_rules/ux.js b/.github/helper/semgrep_rules/ux.js
new file mode 100644
index 00000000000..ae73f9cc603
--- /dev/null
+++ b/.github/helper/semgrep_rules/ux.js
@@ -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") }}. ');
diff --git a/.github/helper/semgrep_rules/ux.py b/.github/helper/semgrep_rules/ux.py
index 4a744574350..a00d3cd8aef 100644
--- a/.github/helper/semgrep_rules/ux.py
+++ b/.github/helper/semgrep_rules/ux.py
@@ -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"))
diff --git a/.github/helper/semgrep_rules/ux.yml b/.github/helper/semgrep_rules/ux.yml
index ed06a6a80c9..dd667f36c0f 100644
--- a/.github/helper/semgrep_rules/ux.yml
+++ b/.github/helper/semgrep_rules/ux.yml
@@ -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
diff --git a/.github/workflows/patch.yml b/.github/workflows/patch.yml
index 7c9e0272c95..b96a3d6bbed 100644
--- a/.github/workflows/patch.yml
+++ b/.github/workflows/patch.yml
@@ -66,4 +66,8 @@ jobs:
run: bash ${GITHUB_WORKSPACE}/.github/helper/install.sh
- name: Run Patch Tests
- run: cd ~/frappe-bench/ && wget http://build.erpnext.com/20171108_190013_955977f8_database.sql.gz && bench --site test_site --force restore ~/frappe-bench/20171108_190013_955977f8_database.sql.gz && bench --site test_site migrate
+ run: |
+ cd ~/frappe-bench/
+ wget https://erpnext.com/files/v10-erpnext.sql.gz
+ bench --site test_site --force restore ~/frappe-bench/v10-erpnext.sql.gz
+ bench --site test_site migrate
diff --git a/.github/workflows/semgrep.yml b/.github/workflows/semgrep.yml
index df082632365..389524e9684 100644
--- a/.github/workflows/semgrep.yml
+++ b/.github/workflows/semgrep.yml
@@ -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
diff --git a/.github/workflows/server-tests.yml b/.github/workflows/server-tests.yml
index 92685e2177d..69afa15187d 100644
--- a/.github/workflows/server-tests.yml
+++ b/.github/workflows/server-tests.yml
@@ -1,6 +1,10 @@
name: Server
-on: [pull_request, workflow_dispatch]
+on:
+ pull_request:
+ workflow_dispatch:
+ push:
+ branches: [ develop ]
jobs:
test:
@@ -87,6 +91,7 @@ jobs:
coveralls
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_TOKEN }}
COVERALLS_FLAG_NAME: run-${{ matrix.container }}
COVERALLS_SERVICE_NAME: ${{ github.event_name == 'pull_request' && 'github' || 'github-actions' }}
COVERALLS_PARALLEL: true
diff --git a/.pylintrc b/.pylintrc
deleted file mode 100644
index 4b2ea0a5647..00000000000
--- a/.pylintrc
+++ /dev/null
@@ -1 +0,0 @@
-disable=access-member-before-definition
\ No newline at end of file
diff --git a/README.md b/README.md
index 0a556f57b41..c6fc2512445 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
{{ _("Customer: ") }} {{filters.party[0] }}
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
index 6dc46430e06..088c190f451 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.js
@@ -19,7 +19,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
frappe.show_alert({message: __('Emails Queued'), indicator: 'blue'});
}
else{
- frappe.msgprint('No Records for these settings.')
+ frappe.msgprint(__('No Records for these settings.'))
}
}
});
@@ -33,7 +33,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
type: 'GET',
success: function(result) {
if(jQuery.isEmptyObject(result)){
- frappe.msgprint('No Records for these settings.');
+ frappe.msgprint(__('No Records for these settings.'));
}
else{
window.location = url;
@@ -92,7 +92,7 @@ frappe.ui.form.on('Process Statement Of Accounts', {
frm.refresh_field('customers');
}
else{
- frappe.throw('No Customers found with selected options.');
+ frappe.throw(__('No Customers found with selected options.'));
}
}
}
@@ -129,4 +129,4 @@ frappe.ui.form.on('Process Statement Of Accounts Customer', {
}
})
}
-});
\ No newline at end of file
+});
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
index 922f6367df4..295a3b86a9e 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.json
@@ -286,7 +286,7 @@
}
],
"links": [],
- "modified": "2021-05-13 12:44:19.574844",
+ "modified": "2021-05-21 11:14:22.426672",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Process Statement Of Accounts",
diff --git a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
index cf79c03935d..0b0ee904ff9 100644
--- a/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
+++ b/erpnext/accounts/doctype/process_statement_of_accounts/process_statement_of_accounts.py
@@ -94,10 +94,11 @@ 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 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})
statement_dict[entry.customer] = html
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index d3d3ffa17fa..00ef7d5c184 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -592,11 +592,12 @@
"label": "Raw Materials Supplied"
},
{
+ "depends_on": "update_stock",
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Supplied Items",
- "options": "Purchase Receipt Item Supplied",
- "read_only": 1
+ "no_copy": 1,
+ "options": "Purchase Receipt Item Supplied"
},
{
"fieldname": "section_break_26",
@@ -837,6 +838,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)",
@@ -883,6 +885,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment",
@@ -1380,7 +1383,7 @@
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2021-04-30 22:45:58.334107",
+ "modified": "2021-06-15 18:20:56.806195",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice",
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
index 83e9f7583e9..45d89ad1c87 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -68,9 +68,6 @@ class PurchaseInvoice(BuyingController):
super(PurchaseInvoice, self).validate()
- # apply tax withholding only if checked and applicable
- self.set_tax_withholding()
-
if not self.is_return:
self.po_required()
self.pr_required()
@@ -251,11 +248,9 @@ class PurchaseInvoice(BuyingController):
if self.update_stock and (not item.from_warehouse):
if for_validate and item.expense_account and item.expense_account != warehouse_account[item.warehouse]["account"]:
- msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]))
- msg += _("because account {} is not linked to warehouse {} ").format(frappe.bold(item.expense_account), frappe.bold(item.warehouse))
- msg += _("or it is not the default inventory account")
+ msg = _("Row {0}: Expense Head changed to {1} because account {2} is not linked to warehouse {3} or it is not the default inventory account").format(
+ item.idx, frappe.bold(warehouse_account[item.warehouse]["account"]), frappe.bold(item.expense_account), frappe.bold(item.warehouse))
frappe.msgprint(msg, title=_("Expense Head Changed"))
-
item.expense_account = warehouse_account[item.warehouse]["account"]
else:
# check if 'Stock Received But Not Billed' account is credited in Purchase receipt or not
@@ -266,8 +261,8 @@ class PurchaseInvoice(BuyingController):
if negative_expense_booked_in_pr:
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
- msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
- msg += _("because expense is booked against this account in Purchase Receipt {}").format(frappe.bold(item.purchase_receipt))
+ msg = _("Row {0}: Expense Head changed to {1} because expense is booked against this account in Purchase Receipt {2}").format(
+ item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.purchase_receipt))
frappe.msgprint(msg, title=_("Expense Head Changed"))
item.expense_account = stock_not_billed_account
@@ -275,8 +270,9 @@ class PurchaseInvoice(BuyingController):
# If no purchase receipt present then book expense in 'Stock Received But Not Billed'
# This is done in cases when Purchase Invoice is created before Purchase Receipt
if for_validate and item.expense_account and item.expense_account != stock_not_billed_account:
- msg = _("Row {}: Expense Head changed to {} ").format(item.idx, frappe.bold(stock_not_billed_account))
- msg += _("as no Purchase Receipt is created against Item {}. ").format(frappe.bold(item.item_code))
+ msg = _("Row {0}: Expense Head changed to {1} as no Purchase Receipt is created against Item {2}.").format(
+ item.idx, frappe.bold(stock_not_billed_account), frappe.bold(item.item_code))
+ msg += "
"
msg += _("This is done to handle accounting for cases when Purchase Receipt is created after Purchase Invoice")
frappe.msgprint(msg, title=_("Expense Head Changed"))
@@ -308,8 +304,8 @@ class PurchaseInvoice(BuyingController):
if not d.purchase_order:
msg = _("Purchase Order Required for item {}").format(frappe.bold(d.item_code))
msg += "
"
- msg += _("To submit the invoice without purchase order please set {} ").format(frappe.bold(_('Purchase Order Required')))
- msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
+ msg += _("To submit the invoice without purchase order please set {0} as {1} in {2}").format(
+ frappe.bold(_('Purchase Order Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Order"))
def pr_required(self):
@@ -323,8 +319,8 @@ class PurchaseInvoice(BuyingController):
if not d.purchase_receipt and d.item_code in stock_items:
msg = _("Purchase Receipt Required for item {}").format(frappe.bold(d.item_code))
msg += "
"
- msg += _("To submit the invoice without purchase receipt please set {} ").format(frappe.bold(_('Purchase Receipt Required')))
- msg += _("as {} in {}").format(frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
+ msg += _("To submit the invoice without purchase receipt please set {0} as {1} in {2}").format(
+ frappe.bold(_('Purchase Receipt Required')), frappe.bold('No'), get_link_to_form('Buying Settings', 'Buying Settings', 'Buying Settings'))
throw(msg, title=_("Mandatory Purchase Receipt"))
def validate_write_off_account(self):
@@ -404,6 +400,7 @@ class PurchaseInvoice(BuyingController):
# because updating ordered qty in bin depends upon updated ordered qty in PO
if self.update_stock == 1:
self.update_stock_ledger()
+ self.set_consumed_qty_in_po()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
@@ -456,6 +453,8 @@ class PurchaseInvoice(BuyingController):
self.make_tax_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
+ self.allocate_advance_taxes(gl_entries)
+
gl_entries = make_regional_gl_entries(gl_entries, self)
gl_entries = merge_similar_entries(gl_entries)
@@ -1000,6 +999,7 @@ class PurchaseInvoice(BuyingController):
if self.update_stock == 1:
self.update_stock_ledger()
self.delete_auto_created_batches()
+ self.set_consumed_qty_in_po()
self.make_gl_entries_on_cancel()
@@ -1090,6 +1090,7 @@ class PurchaseInvoice(BuyingController):
for d in self.taxes:
if d.account_head == tax_withholding_details.get("account_head"):
d.update(tax_withholding_details)
+
accounts.append(d.account_head)
if not accounts or tax_withholding_details.get("account_head") not in accounts:
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index 53db689c84a..ff433b962ff 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_tra
from erpnext.projects.doctype.project.test_project import make_project
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
from erpnext.stock.doctype.item.test_item import create_item
+from erpnext.buying.doctype.supplier.test_supplier import create_supplier
test_dependencies = ["Item", "Cost Center", "Payment Term", "Payment Terms Template"]
test_ignore = ["Serial No"]
@@ -620,8 +621,10 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(actual_qty_0, get_qty_after_transaction())
def test_subcontracting_via_purchase_invoice(self):
+ from erpnext.buying.doctype.purchase_order.test_purchase_order import update_backflush_based_on
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+ update_backflush_based_on('BOM')
make_stock_entry(item_code="_Test Item", target="_Test Warehouse 1 - _TC", qty=100, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse 1 - _TC",
qty=100, basic_rate=100)
@@ -631,7 +634,7 @@ class TestPurchaseInvoice(unittest.TestCase):
self.assertEqual(len(pi.get("supplied_items")), 2)
- rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
+ rm_supp_cost = sum(d.amount for d in pi.get("supplied_items"))
self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self):
@@ -950,6 +953,102 @@ class TestPurchaseInvoice(unittest.TestCase):
acc_settings.submit_journal_entriessubmit_journal_entries = 0
acc_settings.save()
+ def test_purchase_invoice_advance_taxes(self):
+ from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
+ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+ from erpnext.buying.doctype.purchase_order.purchase_order import get_mapped_purchase_invoice
+
+ # create a new supplier to test
+ supplier = create_supplier(supplier_name = '_Test TDS Advance Supplier',
+ tax_withholding_category = 'TDS - 194 - Dividends - Individual')
+
+ # Update tax withholding category with current fiscal year and rate details
+ update_tax_witholding_category('_Test Company', 'TDS Payable - _TC', nowdate())
+
+ # Create Purchase Order with TDS applied
+ po = create_purchase_order(do_not_save=1, supplier=supplier.name, rate=3000)
+ po.apply_tds = 1
+ po.tax_withholding_category = 'TDS - 194 - Dividends - Individual'
+ po.save()
+ po.submit()
+
+ # Update Unrealized Profit / Loss Account which is used as default advance tax account
+ frappe.db.set_value('Company', '_Test Company', 'unrealized_profit_loss_account', '_Test Account Excise Duty - _TC')
+
+ # Create Payment Entry Against the order
+ payment_entry = get_payment_entry(dt='Purchase Order', dn=po.name)
+ payment_entry.paid_from = 'Cash - _TC'
+ payment_entry.save()
+ payment_entry.submit()
+
+ # Check GLE for Payment Entry
+ expected_gle = [
+ ['_Test Account Excise Duty - _TC', 3000, 0],
+ ['Cash - _TC', 0, 27000],
+ ['Creditors - _TC', 27000, 0],
+ ['TDS Payable - _TC', 0, 3000],
+ ]
+
+ gl_entries = frappe.db.sql("""select account, debit, credit
+ from `tabGL Entry`
+ where voucher_type='Payment Entry' and voucher_no=%s
+ order by account asc""", (payment_entry.name), as_dict=1)
+
+ for i, gle in enumerate(gl_entries):
+ self.assertEqual(expected_gle[i][0], gle.account)
+ self.assertEqual(expected_gle[i][1], gle.debit)
+ self.assertEqual(expected_gle[i][2], gle.credit)
+
+ # Create Purchase Invoice against Purchase Order
+ purchase_invoice = get_mapped_purchase_invoice(po.name)
+ purchase_invoice.allocate_advances_automatically = 1
+ purchase_invoice.items[0].expense_account = '_Test Account Cost for Goods Sold - _TC'
+ purchase_invoice.save()
+ purchase_invoice.submit()
+
+ # Check GLE for Purchase Invoice
+ # Zero net effect on final TDS Payable on invoice
+ expected_gle = [
+ ['_Test Account Cost for Goods Sold - _TC', 30000, 0],
+ ['_Test Account Excise Duty - _TC', 0, 3000],
+ ['Creditors - _TC', 0, 27000],
+ ['TDS Payable - _TC', 3000, 3000]
+ ]
+
+ gl_entries = frappe.db.sql("""select account, debit, credit
+ from `tabGL Entry`
+ where voucher_type='Purchase Invoice' and voucher_no=%s
+ order by account asc""", (purchase_invoice.name), as_dict=1)
+
+ for i, gle in enumerate(gl_entries):
+ self.assertEqual(expected_gle[i][0], gle.account)
+ self.assertEqual(expected_gle[i][1], gle.debit)
+ self.assertEqual(expected_gle[i][2], gle.credit)
+
+def update_tax_witholding_category(company, account, date):
+ from erpnext.accounts.utils import get_fiscal_year
+
+ fiscal_year = get_fiscal_year(date=date, company=company)
+
+ if not frappe.db.get_value('Tax Withholding Rate',
+ {'parent': 'TDS - 194 - Dividends - Individual', 'fiscal_year': fiscal_year[0]}):
+ tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
+ tds_category.append('rates', {
+ 'fiscal_year': fiscal_year[0],
+ 'tax_withholding_rate': 10,
+ 'single_threshold': 2500,
+ 'cumulative_threshold': 0
+ })
+ tds_category.save()
+
+ if not frappe.db.get_value('Tax Withholding Account',
+ {'parent': 'TDS - 194 - Dividends - Individual', 'account': account}):
+ tds_category = frappe.get_doc('Tax Withholding Category', 'TDS - 194 - Dividends - Individual')
+ tds_category.append('accounts', {
+ 'company': company,
+ 'account': account
+ })
+ tds_category.save()
def unlink_payment_on_cancel_of_invoice(enable=1):
accounts_settings = frappe.get_doc("Accounts Settings")
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
index 10e1c73ea90..29f72470b32 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -272,7 +272,7 @@
"fieldname": "rate",
"fieldtype": "Currency",
"in_list_view": 1,
- "label": "Rate ",
+ "label": "Rate",
"oldfieldname": "import_rate",
"oldfieldtype": "Currency",
"options": "currency",
@@ -854,7 +854,7 @@
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2021-03-30 09:02:39.256602",
+ "modified": "2021-06-16 19:33:51.099386",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
index f9fdc4b605a..1fa68e0a8a8 100644
--- a/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
+++ b/erpnext/accounts/doctype/purchase_taxes_and_charges/purchase_taxes_and_charges.json
@@ -12,6 +12,7 @@
"charge_type",
"row_id",
"included_in_print_rate",
+ "included_in_paid_amount",
"col_break1",
"account_head",
"description",
@@ -21,6 +22,7 @@
"cost_center",
"dimension_col_break",
"section_break_9",
+ "currency",
"tax_amount",
"tax_amount_after_discount_amount",
"total",
@@ -205,12 +207,28 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "account_head.account_currency",
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Account Currency",
+ "options": "Currency",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:['Purchase Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
+ "description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry",
+ "fieldname": "included_in_paid_amount",
+ "fieldtype": "Check",
+ "label": "Considered In Paid Amount"
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-06-14 01:43:50.750455",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Taxes and Charges",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 16e70dd7ac4..bb556516702 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -17,7 +17,7 @@ erpnext.accounts.SalesInvoiceController = class SalesInvoiceController extends e
var me = this;
super.onload();
- this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet'];
+ this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice', 'Timesheet', 'POS Invoice Merge Log', 'POS Closing Entry'];
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',
@@ -897,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);
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 52bc409b6e5..e7dd6b8a606 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -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
},
{
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index ff1e14b30a3..4655ea82222 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -531,7 +531,7 @@ class SalesInvoice(SellingController):
# set pos values in items
for item in self.get("items"):
if item.get('item_code'):
- profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos)
+ profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos, update_data=True)
for fname, val in iteritems(profile_details):
if (not for_validate) or (for_validate and not item.get(fname)):
item.set(fname, val)
@@ -842,6 +842,8 @@ class SalesInvoice(SellingController):
self.make_tax_gl_entries(gl_entries)
self.make_internal_transfer_gl_entries(gl_entries)
+ self.allocate_advance_taxes(gl_entries)
+
self.make_item_gl_entries(gl_entries)
# merge gl entries before adding pos entries
@@ -849,7 +851,6 @@ class SalesInvoice(SellingController):
self.make_loyalty_point_redemption_gle(gl_entries)
self.make_pos_gl_entries(gl_entries)
- self.make_gle_for_change_amount(gl_entries)
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
@@ -987,7 +988,13 @@ class SalesInvoice(SellingController):
def make_pos_gl_entries(self, gl_entries):
if cint(self.is_pos):
+
+ skip_change_gl_entries = not cint(frappe.db.get_single_value('Accounts Settings', 'post_change_gl_entries'))
+
for payment_mode in self.payments:
+ if skip_change_gl_entries and payment_mode.account == self.account_for_change_amount:
+ payment_mode.base_amount -= flt(self.change_amount)
+
if payment_mode.amount:
# POS, make payment entries
gl_entries.append(
@@ -1019,8 +1026,11 @@ class SalesInvoice(SellingController):
}, payment_mode_account_currency, item=self)
)
+ if not skip_change_gl_entries:
+ self.make_gle_for_change_amount(gl_entries)
+
def make_gle_for_change_amount(self, gl_entries):
- if cint(self.is_pos) and self.change_amount:
+ if self.change_amount:
if self.account_for_change_amount:
gl_entries.append(
self.get_gl_dict({
@@ -1141,7 +1151,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:
@@ -1172,26 +1181,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)
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 9059d0b0404..114b7d2d352 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -713,7 +713,7 @@ class TestSalesInvoice(unittest.TestCase):
si.submit()
self.assertEqual(si.paid_amount, 100.0)
- self.pos_gl_entry(si, pos, 50)
+ self.validate_pos_gl_entry(si, pos, 50)
def test_pos_returns_with_repayment(self):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return
@@ -749,7 +749,7 @@ class TestSalesInvoice(unittest.TestCase):
make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
- pr = make_purchase_receipt(company= "_Test Company with perpetual inventory",
+ make_purchase_receipt(company= "_Test Company with perpetual inventory",
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
@@ -770,7 +770,45 @@ class TestSalesInvoice(unittest.TestCase):
self.assertEqual(pos.grand_total, 100.0)
self.assertEqual(pos.write_off_amount, -5)
- def pos_gl_entry(self, si, pos, cash_amount):
+ def test_pos_with_no_gl_entry_for_change_amount(self):
+ frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 0)
+
+ make_pos_profile(company="_Test Company with perpetual inventory", income_account = "Sales - TCP1",
+ expense_account = "Cost of Goods Sold - TCP1", warehouse="Stores - TCP1", cost_center = "Main - TCP1", write_off_account="_Test Write Off - TCP1")
+
+ make_purchase_receipt(company= "_Test Company with perpetual inventory",
+ item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
+
+ pos = create_sales_invoice(company= "_Test Company with perpetual inventory",
+ debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1",
+ income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1",
+ cost_center = "Main - TCP1", do_not_save=True)
+
+ pos.is_pos = 1
+ pos.update_stock = 1
+
+ taxes = get_taxes_and_charges()
+ pos.taxes = []
+ for tax in taxes:
+ pos.append("taxes", tax)
+
+ pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50})
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 60})
+
+ pos.insert()
+ pos.submit()
+
+ self.assertEqual(pos.grand_total, 100.0)
+ self.assertEqual(pos.change_amount, 10)
+
+ self.validate_pos_gl_entry(pos, pos, 60, validate_without_change_gle=True)
+
+ frappe.db.set_value('Accounts Settings', None, 'post_change_gl_entries', 1)
+
+ def validate_pos_gl_entry(self, si, pos, cash_amount, validate_without_change_gle=False):
+ if validate_without_change_gle:
+ cash_amount -= pos.change_amount
+
# check stock ledger entries
sle = frappe.db.sql("""select * from `tabStock Ledger Entry`
where voucher_type = 'Sales Invoice' and voucher_no = %s""",
@@ -933,12 +971,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
@@ -1905,69 +1937,80 @@ class TestSalesInvoice(unittest.TestCase):
frappe.flags.country = country
def test_einvoice_json(self):
- from erpnext.regional.india.e_invoice.utils import make_einvoice
+ from erpnext.regional.india.e_invoice.utils import make_einvoice, validate_totals
- si = make_sales_invoice_for_ewaybill()
- si.naming_series = 'INV-2020-.#####'
- si.items = []
- si.append("items", {
- "item_code": "_Test Item",
- "uom": "Nos",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 2000,
- "rate": 12,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- })
- si.append("items", {
- "item_code": "_Test Item 2",
- "uom": "Nos",
- "warehouse": "_Test Warehouse - _TC",
- "qty": 420,
- "rate": 15,
- "income_account": "Sales - _TC",
- "expense_account": "Cost of Goods Sold - _TC",
- "cost_center": "_Test Cost Center - _TC",
- })
+ si = get_sales_invoice_for_e_invoice()
si.discount_amount = 100
si.save()
einvoice = make_einvoice(si)
-
- total_item_ass_value = 0
- total_item_cgst_value = 0
- total_item_sgst_value = 0
- total_item_igst_value = 0
- total_item_value = 0
-
- for item in einvoice['ItemList']:
- total_item_ass_value += item['AssAmt']
- total_item_cgst_value += item['CgstAmt']
- total_item_sgst_value += item['SgstAmt']
- total_item_igst_value += item['IgstAmt']
- total_item_value += item['TotItemVal']
-
- self.assertTrue(item['AssAmt'], item['TotAmt'] - item['Discount'])
- self.assertTrue(item['TotItemVal'], item['AssAmt'] + item['CgstAmt'] + item['SgstAmt'] + item['IgstAmt'])
-
- value_details = einvoice['ValDtls']
-
- self.assertEqual(einvoice['Version'], '1.1')
- self.assertEqual(value_details['AssVal'], total_item_ass_value)
- self.assertEqual(value_details['CgstVal'], total_item_cgst_value)
- self.assertEqual(value_details['SgstVal'], total_item_sgst_value)
- self.assertEqual(value_details['IgstVal'], total_item_igst_value)
-
- calculated_invoice_value = \
- value_details['AssVal'] + value_details['CgstVal'] \
- + value_details['SgstVal'] + value_details['IgstVal'] \
- + value_details['OthChrg'] - value_details['Discount']
-
- self.assertTrue(value_details['TotInvVal'] - calculated_invoice_value < 0.1)
-
- self.assertEqual(value_details['TotInvVal'], si.base_grand_total)
self.assertTrue(einvoice['EwbDtls'])
+ validate_totals(einvoice)
+
+ si.apply_discount_on = 'Net Total'
+ si.save()
+ einvoice = make_einvoice(si)
+ validate_totals(einvoice)
+
+ [d.set('included_in_print_rate', 1) for d in si.taxes]
+ si.save()
+ einvoice = make_einvoice(si)
+ validate_totals(einvoice)
+
+def get_sales_invoice_for_e_invoice():
+ si = make_sales_invoice_for_ewaybill()
+ si.naming_series = 'INV-2020-.#####'
+ si.items = []
+ si.append("items", {
+ "item_code": "_Test Item",
+ "uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 2000,
+ "rate": 12,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ })
+
+ si.append("items", {
+ "item_code": "_Test Item 2",
+ "uom": "Nos",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 420,
+ "rate": 15,
+ "income_account": "Sales - _TC",
+ "expense_account": "Cost of Goods Sold - _TC",
+ "cost_center": "_Test Cost Center - _TC",
+ })
+
+ return si
+
+ def test_item_tax_net_range(self):
+ item = create_item("T Shirt")
+
+ item.set('taxes', [])
+ item.append("taxes", {
+ "item_tax_template": "_Test Account Excise Duty @ 10 - _TC",
+ "minimum_net_rate": 0,
+ "maximum_net_rate": 500
+ })
+
+ item.append("taxes", {
+ "item_tax_template": "_Test Account Excise Duty @ 12 - _TC",
+ "minimum_net_rate": 501,
+ "maximum_net_rate": 1000
+ })
+
+ item.save()
+
+ sales_invoice = create_sales_invoice(item = "T Shirt", rate=700, do_not_submit=True)
+ self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 12 - _TC")
+
+ # Apply discount
+ sales_invoice.apply_discount_on = 'Net Total'
+ sales_invoice.discount_amount = 300
+ sales_invoice.save()
+ self.assertEqual(sales_invoice.items[0].item_tax_template, "_Test Account Excise Duty @ 10 - _TC")
def make_test_address_for_ewaybill():
if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
@@ -2091,27 +2134,6 @@ def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
doc.assertEqual(expected_gle[i][2], gle.credit)
doc.assertEqual(getdate(expected_gle[i][3]), gle.posting_date)
- def test_item_tax_validity(self):
- item = frappe.get_doc("Item", "_Test Item 2")
-
- if item.taxes:
- item.taxes = []
- item.save()
-
- item.append("taxes", {
- "item_tax_template": "_Test Item Tax Template 1 - _TC",
- "valid_from": add_days(nowdate(), 1)
- })
-
- item.save()
-
- sales_invoice = create_sales_invoice(item = "_Test Item 2", do_not_save=1)
- sales_invoice.items[0].item_tax_template = "_Test Item Tax Template 1 - _TC"
- self.assertRaises(frappe.ValidationError, sales_invoice.save)
-
- item.taxes = []
- item.save()
-
def create_sales_invoice(**args):
si = frappe.new_doc("Sales Invoice")
args = frappe._dict(args)
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json
index 3c8cb6b8518..1b7a0fe562e 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges/sales_taxes_and_charges.json
@@ -1,8 +1,10 @@
{
+ "actions": [],
"creation": "2013-04-24 11:39:32",
"doctype": "DocType",
"document_type": "Setup",
"editable_grid": 1,
+ "engine": "InnoDB",
"field_order": [
"charge_type",
"row_id",
@@ -10,12 +12,14 @@
"col_break_1",
"description",
"included_in_print_rate",
+ "included_in_paid_amount",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break",
"section_break_8",
"rate",
"section_break_9",
+ "currency",
"tax_amount",
"total",
"tax_amount_after_discount_amount",
@@ -23,8 +27,7 @@
"base_tax_amount",
"base_total",
"base_tax_amount_after_discount_amount",
- "item_wise_tax_detail",
- "parenttype"
+ "item_wise_tax_detail"
],
"fields": [
{
@@ -173,17 +176,6 @@
"oldfieldtype": "Small Text",
"read_only": 1
},
- {
- "fieldname": "parenttype",
- "fieldtype": "Data",
- "hidden": 1,
- "in_filter": 1,
- "label": "Parenttype",
- "oldfieldname": "parenttype",
- "oldfieldtype": "Data",
- "print_hide": 1,
- "search_index": 1
- },
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
@@ -192,15 +184,34 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "fetch_from": "account_head.account_currency",
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Account Currency",
+ "options": "Currency",
+ "read_only": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:['Sales Taxes and Charges Template', 'Payment Entry'].includes(parent.doctype)",
+ "description": "If checked, the tax amount will be considered as already included in the Paid Amount in Payment Entry",
+ "fieldname": "included_in_paid_amount",
+ "fieldtype": "Check",
+ "label": "Considered In Paid Amount"
}
],
"idx": 1,
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-05-25 22:59:38.740883",
+ "links": [],
+ "modified": "2021-06-14 01:44:36.899147",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Taxes and Charges",
"owner": "Administrator",
"permissions": [],
+ "sort_field": "modified",
"sort_order": "ASC"
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
index 5c1cbaa4aaa..b9ee4a0963f 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -49,7 +49,7 @@ def get_party_tax_withholding_details(inv, tax_withholding_category=None):
if not parties:
parties.append(party)
- fiscal_year = get_fiscal_year(inv.posting_date, company=inv.company)
+ fiscal_year = get_fiscal_year(inv.get('posting_date') or inv.get('transaction_date'), company=inv.company)
tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
if not tax_details:
@@ -154,7 +154,7 @@ def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, p
tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
tax_amount = 0
- posting_date = inv.posting_date
+ posting_date = inv.get('posting_date') or inv.get('transaction_date')
if party_type == 'Supplier':
ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
if tax_deducted:
@@ -257,7 +257,7 @@ def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_dedu
if ((threshold and inv.net_total >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
if ldc and is_valid_certificate(
ldc.valid_from, ldc.valid_upto,
- inv.posting_date, tax_deducted,
+ inv.get('posting_date') or inv.get('transaction_date'), tax_deducted,
inv.net_total, ldc.certificate_limit
):
tds_amount = get_ltds_amount(supp_credit_amt, 0, ldc.certificate_limit, ldc.rate, tax_details)
diff --git a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
index 0cea7612dd3..dd26be7c992 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/test_tax_withholding_category.py
@@ -112,7 +112,7 @@ class TestTaxWithholdingCategory(unittest.TestCase):
si = create_sales_invoice(customer = "Test TCS Customer", rate=5000)
si.submit()
- tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
+ tcs_charged = sum(d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC')
self.assertEqual(tcs_charged, 500)
invoices.append(si)
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index f1717c50d8d..59009ae6215 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -143,7 +143,7 @@ def make_entry(args, adv_adj, update_outstanding, from_repost=False):
validate_expense_against_budget(args)
def validate_cwip_accounts(gl_map):
- cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")])
+ cwip_enabled = any(cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting"))
if cwip_enabled and gl_map[0].voucher_type == "Journal Entry":
cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount
@@ -185,10 +185,10 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision):
for d in gl_map:
if d.account == round_off_account:
round_off_gle = d
- if d.debit_in_account_currency:
- debit_credit_diff -= flt(d.debit_in_account_currency)
+ if d.debit:
+ debit_credit_diff -= flt(d.debit)
else:
- debit_credit_diff += flt(d.credit_in_account_currency)
+ debit_credit_diff += flt(d.credit)
round_off_account_exists = True
if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)):
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index e01cb6e151e..e025fc69054 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -457,7 +457,7 @@ def validate_party_frozen_disabled(party_type, party_name):
frappe.throw(_("{0} {1} is frozen").format(party_type, party_name), PartyFrozen)
elif party_type == "Employee":
- if frappe.db.get_value("Employee", party_name, "status") == "Left":
+ if frappe.db.get_value("Employee", party_name, "status") != "Active":
frappe.msgprint(_("{0} {1} is not active").format(party_type, party_name), alert=True)
def get_timeline_data(doctype, name):
diff --git a/erpnext/accounts/report/account_balance/account_balance.py b/erpnext/accounts/report/account_balance/account_balance.py
index 65e7d789bb0..be64c327fdf 100644
--- a/erpnext/accounts/report/account_balance/account_balance.py
+++ b/erpnext/accounts/report/account_balance/account_balance.py
@@ -58,11 +58,9 @@ def get_conditions(filters):
def get_data(filters):
data = []
-
conditions = get_conditions(filters)
-
accounts = frappe.db.get_all("Account", fields=["name", "account_currency"],
- filters=conditions)
+ filters=conditions, order_by='name')
for d in accounts:
balance = get_balance_on(d.name, date=filters.report_date)
diff --git a/erpnext/accounts/report/account_balance/test_account_balance.py b/erpnext/accounts/report/account_balance/test_account_balance.py
index b6ced312d09..14ddf4a30fc 100644
--- a/erpnext/accounts/report/account_balance/test_account_balance.py
+++ b/erpnext/accounts/report/account_balance/test_account_balance.py
@@ -23,7 +23,7 @@ class TestAccountBalance(unittest.TestCase):
expected_data = [
{
- "account": 'Sales - _TC2',
+ "account": 'Direct Income - _TC2',
"currency": 'EUR',
"balance": -100.0,
},
@@ -32,21 +32,21 @@ class TestAccountBalance(unittest.TestCase):
"currency": 'EUR',
"balance": -100.0,
},
- {
- "account": 'Service - _TC2',
- "currency": 'EUR',
- "balance": 0.0,
- },
- {
- "account": 'Direct Income - _TC2',
- "currency": 'EUR',
- "balance": -100.0,
- },
{
"account": 'Indirect Income - _TC2',
"currency": 'EUR',
"balance": 0.0,
},
+ {
+ "account": 'Sales - _TC2',
+ "currency": 'EUR',
+ "balance": -100.0,
+ },
+ {
+ "account": 'Service - _TC2',
+ "currency": 'EUR',
+ "balance": 0.0,
+ }
]
self.assertEqual(expected_data, report[1])
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
index db605f7285a..a11b77a6f64 100755
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py
@@ -584,6 +584,7 @@ class ReceivablePayableReport(object):
`tabGL Entry`
where
docstatus < 2
+ and is_cancelled = 0
and party_type=%s
and (party is not null and party != '')
{1} {2} {3}"""
diff --git a/erpnext/accounts/report/cash_flow/custom_cash_flow.py b/erpnext/accounts/report/cash_flow/custom_cash_flow.py
index fe2bc725e00..c11c15390b3 100644
--- a/erpnext/accounts/report/cash_flow/custom_cash_flow.py
+++ b/erpnext/accounts/report/cash_flow/custom_cash_flow.py
@@ -32,7 +32,7 @@ def get_accounts_in_mappers(mapping_names):
join `tabCash Flow Mapping` cfm on cfma.parent=cfm.name
where cfma.parent in (%s)
order by cfm.is_working_capital
- ''', (', '.join(['"%s"' % d for d in mapping_names])))
+ ''', (', '.join('"%s"' % d for d in mapping_names)))
def setup_mappers(mappers):
@@ -83,8 +83,8 @@ def setup_mappers(mappers):
account_types_labels = sorted(
set(
- [(d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
- for d in account_types]
+ (d['label'], d['is_working_capital'], d['is_income_tax_liability'], d['is_income_tax_expense'])
+ for d in account_types
),
key=lambda x: x[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:
@@ -375,7 +375,7 @@ def _get_account_type_based_data(filters, account_names, period_list, accumulate
total = 0
for period in period_list:
start_date = get_start_date(period, accumulated_values, company)
- accounts = ', '.join(['"%s"' % d for d in account_names])
+ accounts = ', '.join('"%s"' % d for d in account_names)
if opening_balances:
date_info = dict(date=start_date)
diff --git a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
index 10b32fea562..c79d7401e6a 100644
--- a/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
+++ b/erpnext/accounts/report/customer_ledger_summary/customer_ledger_summary.py
@@ -145,7 +145,7 @@ class PartyLedgerSummaryReport(object):
out = []
for party, row in iteritems(self.party_data):
if row.opening_balance or row.invoiced_amount or row.paid_amount or row.return_amount or row.closing_amount:
- total_party_adjustment = sum([amount for amount in itervalues(self.party_adjustment_details.get(party, {}))])
+ total_party_adjustment = sum(amount for amount in itervalues(self.party_adjustment_details.get(party, {})))
row.paid_amount -= total_party_adjustment
adjustments = self.party_adjustment_details.get(party, {})
diff --git a/erpnext/patches/v10_0/__init__.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/__init__.py
similarity index 100%
rename from erpnext/patches/v10_0/__init__.py
rename to erpnext/accounts/report/dimension_wise_accounts_balance_report/__init__.py
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
new file mode 100644
index 00000000000..6a0394861b8
--- /dev/null
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.js
@@ -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
+}
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.json b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.json
new file mode 100644
index 00000000000..6141944f9d2
--- /dev/null
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.json
@@ -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": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
new file mode 100644
index 00000000000..de7ed4926e1
--- /dev/null
+++ b/erpnext/accounts/report/dimension_wise_accounts_balance_report/dimension_wise_accounts_balance_report.py
@@ -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
diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py
index d20ddbde5c6..39ff8045181 100644
--- a/erpnext/accounts/report/financial_statements.py
+++ b/erpnext/accounts/report/financial_statements.py
@@ -369,7 +369,7 @@ def set_gl_entries_by_account(
if accounts:
additional_conditions += " and account in ({})"\
- .format(", ".join([frappe.db.escape(d) for d in accounts]))
+ .format(", ".join(frappe.db.escape(d) for d in accounts))
gl_filters = {
"company": company,
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.js b/erpnext/accounts/report/general_ledger/general_ledger.js
index fb0d3599261..84f786814de 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.js
+++ b/erpnext/accounts/report/general_ledger/general_ledger.js
@@ -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"
}
]
}
diff --git a/erpnext/accounts/report/general_ledger/general_ledger.py b/erpnext/accounts/report/general_ledger/general_ledger.py
index b5d7992604f..562df4f6f7d 100644
--- a/erpnext/accounts/report/general_ledger/general_ledger.py
+++ b/erpnext/accounts/report/general_ledger/general_ledger.py
@@ -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()
diff --git a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
index cb4d9b43dbd..685419a17e4 100644
--- a/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
+++ b/erpnext/accounts/report/item_wise_purchase_register/item_wise_purchase_register.py
@@ -334,7 +334,7 @@ def get_aii_accounts():
def get_purchase_receipts_against_purchase_order(item_list):
po_pr_map = frappe._dict()
- po_item_rows = list(set([d.po_detail for d in item_list]))
+ po_item_rows = list(set(d.po_detail for d in item_list))
if po_item_rows:
purchase_receipts = frappe.db.sql("""
diff --git a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
index 928b373effe..2e794da8425 100644
--- a/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
+++ b/erpnext/accounts/report/item_wise_sales_register/item_wise_sales_register.py
@@ -23,7 +23,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency)
- mode_of_payments = get_mode_of_payments(set([d.parent for d in item_list]))
+ mode_of_payments = get_mode_of_payments(set(d.parent for d in item_list))
so_dn_map = get_delivery_notes_against_sales_order(item_list)
data = []
diff --git a/erpnext/accounts/report/pos_register/pos_register.py b/erpnext/accounts/report/pos_register/pos_register.py
index cfbd7fd0c8b..6a42bb4fb65 100644
--- a/erpnext/accounts/report/pos_register/pos_register.py
+++ b/erpnext/accounts/report/pos_register/pos_register.py
@@ -77,14 +77,14 @@ def get_pos_entries(filters, group_by_field):
), filters, as_dict=1)
def concat_mode_of_payments(pos_entries):
- mode_of_payments = get_mode_of_payments(set([d.pos_invoice for d in pos_entries]))
+ mode_of_payments = get_mode_of_payments(set(d.pos_invoice for d in pos_entries))
for entry in pos_entries:
if mode_of_payments.get(entry.pos_invoice):
entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, []))
def add_subtotal_row(data, group_invoices, group_by_field, group_by_value):
- grand_total = sum([d.grand_total for d in group_invoices])
- paid_amount = sum([d.paid_amount for d in group_invoices])
+ grand_total = sum(d.grand_total for d in group_invoices)
+ paid_amount = sum(d.paid_amount for d in group_invoices)
data.append({
group_by_field: group_by_value,
"grand_total": grand_total,
diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py
index 8ac749d6290..10edd41aa88 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.py
+++ b/erpnext/accounts/report/purchase_register/purchase_register.py
@@ -26,7 +26,7 @@ def _execute(filters=None, additional_table_columns=None, additional_query_colum
invoice_expense_map, invoice_tax_map = get_invoice_tax_map(invoice_list,
invoice_expense_map, expense_accounts)
invoice_po_pr_map = get_invoice_po_pr_map(invoice_list)
- suppliers = list(set([d.supplier for d in invoice_list]))
+ suppliers = list(set(d.supplier for d in invoice_list))
supplier_details = get_supplier_details(suppliers)
company_currency = frappe.get_cached_value('Company', filters.company, "default_currency")
@@ -120,13 +120,13 @@ def get_columns(invoice_list, additional_table_columns):
and docstatus = 1 and (account_head is not null and account_head != '')
and category in ('Total', 'Valuation and Total')
and parent in (%s) order by account_head""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabPurchase Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
expense_columns = [(account + ":Currency/currency:120") for account in expense_accounts]
unrealized_profit_loss_account_columns = [(account + ":Currency/currency:120") for account in unrealized_profit_loss_accounts]
@@ -208,7 +208,7 @@ def get_invoice_expense_map(invoice_list):
from `tabPurchase Invoice Item`
where parent in (%s)
group by parent, expense_account
- """ % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
+ """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_expense_map = {}
for d in expense_details:
@@ -221,7 +221,7 @@ def get_internal_invoice_map(invoice_list):
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabPurchase Invoice` where name in (%s)
and is_internal_supplier = 1 and company = represents_company""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
internal_invoice_map = {}
for d in unrealized_amount_details:
@@ -238,7 +238,7 @@ def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
where parent in (%s) and category in ('Total', 'Valuation and Total')
and base_tax_amount_after_discount_amount != 0
group by parent, account_head, add_deduct_tax
- """ % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
+ """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_tax_map = {}
for d in tax_details:
@@ -258,7 +258,7 @@ def get_invoice_po_pr_map(invoice_list):
select parent, purchase_order, purchase_receipt, po_detail, project
from `tabPurchase Invoice Item`
where parent in (%s)
- """ % ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
+ """ % ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_po_pr_map = {}
for d in pi_items:
diff --git a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py
index c234da0fe39..ff774681a29 100644
--- a/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py
+++ b/erpnext/accounts/report/sales_payment_summary/sales_payment_summary.py
@@ -158,7 +158,7 @@ def get_sales_invoice_data(filters):
def get_mode_of_payments(filters):
mode_of_payments = {}
invoice_list = get_invoices(filters)
- invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
+ invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
if invoice_list:
inv_mop = frappe.db.sql("""select a.owner,a.posting_date, ifnull(b.mode_of_payment, '') as mode_of_payment
from `tabSales Invoice` a, `tabSales Invoice Payment` b
@@ -197,7 +197,7 @@ def get_invoices(filters):
def get_mode_of_payment_details(filters):
mode_of_payment_details = {}
invoice_list = get_invoices(filters)
- invoice_list_names = ",".join(['"' + invoice['name'] + '"' for invoice in invoice_list])
+ invoice_list_names = ",".join('"' + invoice['name'] + '"' for invoice in invoice_list)
if invoice_list:
inv_mop_detail = frappe.db.sql("""select a.owner, a.posting_date,
ifnull(b.mode_of_payment, '') as mode_of_payment, sum(b.base_amount) as paid_amount
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index cb2c98b64ae..909959323f7 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -248,19 +248,19 @@ def get_columns(invoice_list, additional_table_columns):
income_accounts = frappe.db.sql_list("""select distinct income_account
from `tabSales Invoice Item` where docstatus = 1 and parent in (%s)
order by income_account""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
tax_accounts = frappe.db.sql_list("""select distinct account_head
from `tabSales Taxes and Charges` where parenttype = 'Sales Invoice'
and docstatus = 1 and base_tax_amount_after_discount_amount != 0
and parent in (%s) order by account_head""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
unrealized_profit_loss_accounts = frappe.db.sql_list("""SELECT distinct unrealized_profit_loss_account
from `tabSales Invoice` where docstatus = 1 and name in (%s)
and ifnull(unrealized_profit_loss_account, '') != ''
order by unrealized_profit_loss_account""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]))
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list))
for account in income_accounts:
income_columns.append({
@@ -406,7 +406,7 @@ def get_invoices(filters, additional_query_columns):
def get_invoice_income_map(invoice_list):
income_details = frappe.db.sql("""select parent, income_account, sum(base_net_amount) as amount
from `tabSales Invoice Item` where parent in (%s) group by parent, income_account""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_income_map = {}
for d in income_details:
@@ -419,7 +419,7 @@ def get_internal_invoice_map(invoice_list):
unrealized_amount_details = frappe.db.sql("""SELECT name, unrealized_profit_loss_account,
base_net_total as amount from `tabSales Invoice` where name in (%s)
and is_internal_customer = 1 and company = represents_company""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
internal_invoice_map = {}
for d in unrealized_amount_details:
@@ -432,7 +432,7 @@ def get_invoice_tax_map(invoice_list, invoice_income_map, income_accounts):
tax_details = frappe.db.sql("""select parent, account_head,
sum(base_tax_amount_after_discount_amount) as tax_amount
from `tabSales Taxes and Charges` where parent in (%s) group by parent, account_head""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_tax_map = {}
for d in tax_details:
@@ -451,7 +451,7 @@ def get_invoice_so_dn_map(invoice_list):
si_items = frappe.db.sql("""select parent, sales_order, delivery_note, so_detail
from `tabSales Invoice Item` where parent in (%s)
and (ifnull(sales_order, '') != '' or ifnull(delivery_note, '') != '')""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_so_dn_map = {}
for d in si_items:
@@ -475,7 +475,7 @@ def get_invoice_cc_wh_map(invoice_list):
si_items = frappe.db.sql("""select parent, cost_center, warehouse
from `tabSales Invoice Item` where parent in (%s)
and (ifnull(cost_center, '') != '' or ifnull(warehouse, '') != '')""" %
- ', '.join(['%s']*len(invoice_list)), tuple([inv.name for inv in invoice_list]), as_dict=1)
+ ', '.join(['%s']*len(invoice_list)), tuple(inv.name for inv in invoice_list), as_dict=1)
invoice_cc_wh_map = {}
for d in si_items:
diff --git a/erpnext/accounts/report/tax_detail/test_tax_detail.py b/erpnext/accounts/report/tax_detail/test_tax_detail.py
index d3b8de5d191..743ddba0240 100644
--- a/erpnext/accounts/report/tax_detail/test_tax_detail.py
+++ b/erpnext/accounts/report/tax_detail/test_tax_detail.py
@@ -71,10 +71,8 @@ class TestTaxDetail(unittest.TestCase):
def rm_testdocs(self):
"Remove the Company and all data"
- from erpnext.setup.doctype.company.delete_company_transactions import delete_company_transactions
- delete_company_transactions(self.company.name)
- self.company.delete()
-
+ from erpnext.setup.doctype.company.company import create_transaction_deletion_request
+ create_transaction_deletion_request(self.company.name)
def test_report(self):
self.load_testdocs()
diff --git a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
index a8280c1b18e..e15715dccd8 100644
--- a/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
+++ b/erpnext/accounts/report/tds_computation_summary/tds_computation_summary.py
@@ -78,7 +78,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
and company=%s and posting_date between %s and %s
""", (supplier, company, from_date, to_date), as_dict=1)
- supplier_credit_amount = flt(sum([d.credit for d in entries]))
+ supplier_credit_amount = flt(sum(d.credit for d in entries))
vouchers = [d.voucher_no for d in entries]
vouchers += get_advance_vouchers([supplier], company=company,
@@ -91,7 +91,7 @@ def get_invoice_and_tds_amount(supplier, account, company, from_date, to_date, f
from `tabGL Entry`
where account=%s and posting_date between %s and %s
and company=%s and credit > 0 and voucher_no in ({0})
- """.format(', '.join(["'%s'" % d for d in vouchers])),
+ """.format(', '.join("'%s'" % d for d in vouchers)),
(account, from_date, to_date, company))[0][0])
date_range_filter = [fiscal_year, from_date, to_date]
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
index 344539eef6d..72de318a48c 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.js
@@ -54,6 +54,32 @@ frappe.query_reports["TDS Payable Monthly"] = {
frappe.query_report.refresh();
}
},
+ {
+ "fieldname":"purchase_order",
+ "label": __("Purchase Order"),
+ "fieldtype": "Link",
+ "options": "Purchase Order",
+ "get_query": function() {
+ return {
+ "filters": {
+ "name": ["in", frappe.query_report.invoices]
+ }
+ }
+ },
+ on_change: function() {
+ let supplier = frappe.query_report.get_filter_value('supplier');
+ if(!supplier) return; // return if no supplier selected
+
+ // filter invoices based on selected supplier
+ let invoices = [];
+ frappe.query_report.invoice_data.map(d => {
+ if(d.supplier==supplier)
+ invoices.push(d.name)
+ });
+ frappe.query_report.invoices = invoices;
+ frappe.query_report.refresh();
+ }
+ },
{
"fieldname":"from_date",
"label": __("From Date"),
@@ -75,15 +101,17 @@ frappe.query_reports["TDS Payable Monthly"] = {
onload: function(report) {
// fetch all tds applied invoices
frappe.call({
- "method": "erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly.get_tds_invoices",
+ "method": "erpnext.accounts.report.tds_payable_monthly.tds_payable_monthly.get_tds_invoices_and_orders",
callback: function(r) {
let invoices = [];
+
r.message.map(d => {
invoices.push(d.name);
});
- report["invoice_data"] = r.message;
+ report["invoice_data"] = r.message.invoices;
report["invoices"] = invoices;
+
}
});
}
diff --git a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
index a9fb237a048..ceefa31cfa1 100644
--- a/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
+++ b/erpnext/accounts/report/tds_payable_monthly/tds_payable_monthly.py
@@ -11,11 +11,14 @@ def execute(filters=None):
validate_filters(filters)
set_filters(filters)
+ # TDS payment entries
+ payment_entries = get_payment_entires(filters)
+
columns = get_columns(filters)
- if not filters["invoices"]:
+ if not filters.get("invoices"):
return columns, []
- res = get_result(filters)
+ res = get_result(filters, payment_entries)
return columns, res
@@ -27,8 +30,9 @@ def validate_filters(filters):
def set_filters(filters):
invoices = []
- if not filters["invoices"]:
- filters["invoices"] = get_tds_invoices()
+ if not filters.get("invoices"):
+ filters["invoices"] = get_tds_invoices_and_orders()
+
if filters.supplier and filters.purchase_invoice:
for d in filters["invoices"]:
if d.name == filters.purchase_invoice and d.supplier == filters.supplier:
@@ -41,13 +45,29 @@ def set_filters(filters):
for d in filters["invoices"]:
if d.name == filters.purchase_invoice:
invoices.append(d)
+ elif filters.supplier and filters.purchase_order:
+ for d in filters.get("invoices"):
+ if d.name == filters.purchase_order and d.supplier == filters.supplier:
+ invoices.append(d)
+ elif filters.supplier and not filters.purchase_order:
+ for d in filters.get("invoices"):
+ if d.supplier == filters.supplier:
+ invoices.append(d)
+ elif filters.purchase_order and not filters.supplier:
+ for d in filters.get("invoices"):
+ if d.name == filters.purchase_order:
+ invoices.append(d)
filters["invoices"] = invoices if invoices else filters["invoices"]
filters.naming_series = frappe.db.get_single_value('Buying Settings', 'supp_master_name')
-def get_result(filters):
- supplier_map, tds_docs = get_supplier_map(filters)
- gle_map = get_gle_map(filters)
+ #print(filters.get('invoices'))
+
+def get_result(filters, payment_entries):
+ supplier_map, tds_docs = get_supplier_map(filters, payment_entries)
+ documents = [d.get('name') for d in filters.get('invoices')] + [d.get('name') for d in payment_entries]
+
+ gle_map = get_gle_map(filters, documents)
out = []
for d in gle_map:
@@ -62,10 +82,11 @@ def get_result(filters):
for k in gle_map[d]:
if k.party == supplier_map[d] and k.credit > 0:
- total_amount_credited += k.credit
- elif account_list and k.account == account and k.credit > 0:
- tds_deducted = k.credit
- total_amount_credited += k.credit
+ total_amount_credited += (k.credit - k.debit)
+ elif account_list and k.account == account and (k.credit - k.debit) > 0:
+ tds_deducted = (k.credit - k.debit)
+ total_amount_credited += (k.credit - k.debit)
+ voucher_type = k.voucher_type
rate = [i.tax_withholding_rate for i in tds_doc.rates
if i.fiscal_year == gle_map[d][0].fiscal_year]
@@ -73,32 +94,36 @@ def get_result(filters):
if rate and len(rate) > 0 and tds_deducted:
rate = rate[0]
- if getdate(filters.from_date) <= gle_map[d][0].posting_date \
- and getdate(filters.to_date) >= gle_map[d][0].posting_date:
- row = [supplier.pan, supplier.name]
+ row = [supplier.pan, supplier.name]
- if filters.naming_series == 'Naming Series':
- row.append(supplier.supplier_name)
+ if filters.naming_series == 'Naming Series':
+ row.append(supplier.supplier_name)
- row.extend([tds_doc.name, supplier.supplier_type, rate, total_amount_credited,
- tds_deducted, gle_map[d][0].posting_date, "Purchase Invoice", d])
- out.append(row)
+ row.extend([tds_doc.name, supplier.supplier_type, rate, total_amount_credited,
+ tds_deducted, gle_map[d][0].posting_date, voucher_type, d])
+ out.append(row)
return out
-def get_supplier_map(filters):
+def get_supplier_map(filters, payment_entries):
# create a supplier_map of the form {"purchase_invoice": {supplier_name, pan, tds_name}}
# pre-fetch all distinct applicable tds docs
supplier_map, tds_docs = {}, {}
pan = "pan" if frappe.db.has_column("Supplier", "pan") else "tax_id"
+ supplier_list = [d.supplier for d in filters["invoices"]]
+
supplier_detail = frappe.db.get_all('Supplier',
- {"name": ["in", [d.supplier for d in filters["invoices"]]]},
+ {"name": ["in", supplier_list]},
["tax_withholding_category", "name", pan+" as pan", "supplier_type", "supplier_name"])
for d in filters["invoices"]:
supplier_map[d.get("name")] = [k for k in supplier_detail
if k.name == d.get("supplier")][0]
+ for d in payment_entries:
+ supplier_map[d.get("name")] = [k for k in supplier_detail
+ if k.name == d.get("supplier")][0]
+
for d in supplier_detail:
if d.get("tax_withholding_category") not in tds_docs:
tds_docs[d.get("tax_withholding_category")] = \
@@ -106,13 +131,19 @@ def get_supplier_map(filters):
return supplier_map, tds_docs
-def get_gle_map(filters):
+def get_gle_map(filters, documents):
# create gle_map of the form
# {"purchase_invoice": list of dict of all gle created for this invoice}
gle_map = {}
- gle = frappe.db.get_all('GL Entry',\
- {"voucher_no": ["in", [d.get("name") for d in filters["invoices"]]], 'is_cancelled': 0},
- ["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date"])
+
+ gle = frappe.db.get_all('GL Entry',
+ {
+ "voucher_no": ["in", documents],
+ 'is_cancelled': 0,
+ 'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
+ },
+ ["fiscal_year", "credit", "debit", "account", "voucher_no", "posting_date", "voucher_type"],
+ )
for d in gle:
if not d.voucher_no in gle_map:
@@ -201,8 +232,26 @@ def get_columns(filters):
return columns
+def get_payment_entires(filters):
+ filter_dict = {
+ 'posting_date': ("between", [filters.get('from_date'), filters.get('to_date')]),
+ 'party_type': 'Supplier',
+ 'apply_tax_withholding_amount': 1
+ }
+
+ if filters.get('purchase_invoice') or filters.get('purchase_order'):
+ parent = frappe.db.get_all('Payment Entry Reference',
+ {'reference_name': ('in', [d.get('name') for d in filters.get('invoices')])}, ['parent'])
+
+ filter_dict.update({'name': ('in', [d.get('parent') for d in parent])})
+
+ payment_entries = frappe.get_all('Payment Entry', fields=['name', 'party_name as supplier'],
+ filters=filter_dict)
+
+ return payment_entries
+
@frappe.whitelist()
-def get_tds_invoices():
+def get_tds_invoices_and_orders():
# fetch tds applicable supplier and fetch invoices for these suppliers
suppliers = [d.name for d in frappe.db.get_list("Supplier",
{"tax_withholding_category": ["!=", ""]}, ["name"])]
@@ -210,7 +259,12 @@ def get_tds_invoices():
invoices = frappe.db.get_list("Purchase Invoice",
{"supplier": ["in", suppliers]}, ["name", "supplier"])
+ orders = frappe.db.get_list("Purchase Order",
+ {"supplier": ["in", suppliers]}, ["name", "supplier"])
+
+ invoices = invoices + orders
invoices = [d for d in invoices if d.supplier]
+
frappe.cache().hset("invoices", frappe.session.user, invoices)
return invoices
diff --git a/erpnext/accounts/report/utils.py b/erpnext/accounts/report/utils.py
index 9de8d19f2a4..ba461edaf86 100644
--- a/erpnext/accounts/report/utils.py
+++ b/erpnext/accounts/report/utils.py
@@ -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
@@ -142,6 +139,6 @@ def get_invoiced_item_gross_margin(sales_invoice=None, item_code=None, company=N
gross_profit_data = GrossProfitGenerator(filters)
result = gross_profit_data.grouped_data
if not with_item_data:
- result = sum([d.gross_profit for d in result])
+ result = sum(d.gross_profit for d in result)
return result
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 5a64e27ccb7..66a9b601257 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -635,7 +635,7 @@ def get_held_invoices(party_type, party):
'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()',
as_dict=1
)
- held_invoices = set([d['name'] for d in held_invoices])
+ held_invoices = set(d['name'] for d in held_invoices)
return held_invoices
diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
index b04b03ca79c..821fa4d2af4 100644
--- a/erpnext/accounts/workspace/accounting/accounting.json
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -707,6 +707,7 @@
"link_to": "GST Settings",
"link_type": "DocType",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -717,6 +718,7 @@
"link_to": "GST HSN Code",
"link_type": "DocType",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -727,6 +729,7 @@
"link_to": "GSTR-1",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -737,6 +740,7 @@
"link_to": "GSTR-2",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -747,6 +751,7 @@
"link_to": "GSTR 3B Report",
"link_type": "DocType",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -757,6 +762,7 @@
"link_to": "GST Sales Register",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -767,6 +773,7 @@
"link_to": "GST Purchase Register",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -777,6 +784,7 @@
"link_to": "GST Itemised Sales Register",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -787,6 +795,7 @@
"link_to": "GST Itemised Purchase Register",
"link_type": "Report",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -797,6 +806,7 @@
"link_to": "C-Form",
"link_type": "DocType",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -807,6 +817,7 @@
"link_to": "Lower Deduction Certificate",
"link_type": "DocType",
"onboard": 0,
+ "only_for": "India",
"type": "Link"
},
{
@@ -1065,7 +1076,7 @@
"type": "Link"
}
],
- "modified": "2021-05-13 13:44:56.249888",
+ "modified": "2021-06-10 03:17:31.427945",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting",
@@ -1120,4 +1131,4 @@
"type": "Dashboard"
}
]
-}
+}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py
index 3cd4b802c11..8845f24d104 100644
--- a/erpnext/assets/doctype/asset/test_asset.py
+++ b/erpnext/assets/doctype/asset/test_asset.py
@@ -470,7 +470,7 @@ class TestAsset(unittest.TestCase):
})
asset.insert()
accumulated_depreciation_after_full_schedule = \
- max([d.accumulated_depreciation_amount for d in asset.get("schedules")])
+ max(d.accumulated_depreciation_amount for d in asset.get("schedules"))
asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) -
flt(accumulated_depreciation_after_full_schedule))
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
index 14308277c14..2f6b5ee2dc9 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -92,7 +92,7 @@ class AssetValueAdjustment(Document):
d.value_after_depreciation = asset_value
if d.depreciation_method in ("Straight Line", "Manual"):
- end_date = max([s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx])
+ end_date = max(s.schedule_date for s in asset.schedules if cint(s.finance_book_id) == d.idx)
total_days = date_diff(end_date, self.date)
rate_per_day = flt(d.value_after_depreciation) / flt(total_days)
from_date = self.date
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 384bbc53857..521432d296b 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -45,6 +45,47 @@ frappe.ui.form.on("Purchase Order", {
});
erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+
+ apply_tds: function(frm) {
+ if (!frm.doc.apply_tds) {
+ frm.set_value("tax_withholding_category", '');
+ } else {
+ frm.set_value("tax_withholding_category", frm.supplier_tds);
+ }
+ },
+
+ refresh: function(frm) {
+ frm.trigger('get_materials_from_supplier');
+ },
+
+ get_materials_from_supplier: function(frm) {
+ let po_details = [];
+
+ if (frm.doc.supplied_items && (frm.doc.per_received == 100 || frm.doc.status === 'Closed')) {
+ frm.doc.supplied_items.forEach(d => {
+ if (d.total_supplied_qty && d.total_supplied_qty != d.consumed_qty) {
+ po_details.push(d.name)
+ }
+ });
+ }
+
+ if (po_details && po_details.length) {
+ frm.add_custom_button(__('Return of Components'), () => {
+ frm.call({
+ method: 'erpnext.buying.doctype.purchase_order.purchase_order.get_materials_from_supplier',
+ freeze: true,
+ freeze_message: __('Creating Stock Entry'),
+ args: { purchase_order: frm.doc.name, po_details: po_details },
+ callback: function(r) {
+ if (r && r.message) {
+ const doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ }
+ }
+ });
+ }, __('Create'));
+ }
}
});
@@ -209,7 +250,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
}
has_unsupplied_items() {
- return this.frm.doc['supplied_items'].some(item => item.required_qty != item.supplied_qty)
+ return this.frm.doc['supplied_items'].some(item => item.required_qty > item.supplied_qty);
}
make_stock_entry() {
@@ -313,7 +354,8 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
if(me.values) {
me.values.sub_con_rm_items.map((row,i) => {
if (!row.item_code || !row.rm_item_code || !row.warehouse || !row.qty || row.qty === 0) {
- frappe.throw(__("Item Code, warehouse, quantity are required on row {0}", [i+1]));
+ let row_id = i+1;
+ frappe.throw(__("Item Code, warehouse and quantity are required on row {0}", [row_id]));
}
})
me._make_rm_stock_entry(me.dialog.fields_dict.sub_con_rm_items.grid.get_selected_children())
@@ -504,12 +546,14 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends e
],
primary_action: function() {
var data = d.get_values();
+ let reason_for_hold = 'Reason for hold: ' + data.reason_for_hold;
+
frappe.call({
method: "frappe.desk.form.utils.add_comment",
args: {
reference_doctype: me.frm.doctype,
reference_name: me.frm.docname,
- content: __('Reason for hold: ')+data.reason_for_hold,
+ content: __(reason_for_hold),
comment_email: frappe.session.user,
comment_by: frappe.session.user_fullname
},
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index ee2beea67f9..bb0ad60cabc 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -14,6 +14,8 @@
"supplier",
"get_items_from_open_material_requests",
"supplier_name",
+ "apply_tds",
+ "tax_withholding_category",
"column_break1",
"company",
"transaction_date",
@@ -142,7 +144,9 @@
{
"fieldname": "supplier_section",
"fieldtype": "Section Break",
- "options": "fa fa-user"
+ "options": "fa fa-user",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -152,7 +156,9 @@
"hidden": 1,
"label": "Title",
"no_copy": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "naming_series",
@@ -164,7 +170,9 @@
"options": "PUR-ORD-.YYYY.-",
"print_hide": 1,
"reqd": 1,
- "set_only_once": 1
+ "set_only_once": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"bold": 1,
@@ -178,14 +186,18 @@
"options": "Supplier",
"print_hide": 1,
"reqd": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.supplier && doc.docstatus===0 && (!(doc.items && doc.items.length) || (doc.items.length==1 && !doc.items[0].item_code))",
"description": "Fetch items based on Default Supplier.",
"fieldname": "get_items_from_open_material_requests",
"fieldtype": "Button",
- "label": "Get Items from Open Material Requests"
+ "label": "Get Items from Open Material Requests",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"bold": 1,
@@ -194,7 +206,9 @@
"fieldtype": "Data",
"in_global_search": 1,
"label": "Supplier Name",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "company",
@@ -206,13 +220,17 @@
"options": "Company",
"print_hide": 1,
"remember_last_selected_value": 1,
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break1",
"fieldtype": "Column Break",
"oldfieldtype": "Column Break",
"print_width": "50%",
+ "show_days": 1,
+ "show_seconds": 1,
"width": "50%"
},
{
@@ -224,27 +242,35 @@
"oldfieldname": "transaction_date",
"oldfieldtype": "Date",
"reqd": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"fieldname": "schedule_date",
"fieldtype": "Date",
- "label": "Required By"
+ "label": "Required By",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.docstatus===1",
"fieldname": "order_confirmation_no",
"fieldtype": "Data",
- "label": "Order Confirmation No"
+ "label": "Order Confirmation No",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval:doc.order_confirmation_no",
"fieldname": "order_confirmation_date",
"fieldtype": "Date",
- "label": "Order Confirmation Date"
+ "label": "Order Confirmation Date",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "amended_from",
@@ -256,19 +282,25 @@
"oldfieldtype": "Data",
"options": "Purchase Order",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "drop_ship",
"fieldtype": "Section Break",
- "label": "Drop Ship"
+ "label": "Drop Ship",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
"options": "Customer",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"bold": 1,
@@ -276,31 +308,41 @@
"fieldtype": "Data",
"label": "Customer Name",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_19",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "customer_contact_person",
"fieldtype": "Link",
"label": "Customer Contact",
- "options": "Contact"
+ "options": "Contact",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "customer_contact_display",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Customer Contact",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "customer_contact_mobile",
"fieldtype": "Small Text",
"hidden": 1,
"label": "Customer Mobile No",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "customer_contact_email",
@@ -308,27 +350,35 @@
"hidden": 1,
"label": "Customer Contact Email",
"options": "Email",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "section_addresses",
"fieldtype": "Section Break",
- "label": "Address and Contact"
+ "label": "Address and Contact",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "supplier_address",
"fieldtype": "Link",
"label": "Supplier Address",
"options": "Address",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "contact_person",
"fieldtype": "Link",
"label": "Supplier Contact",
"options": "Contact",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "address_display",
@@ -355,32 +405,42 @@
"label": "Contact Email",
"options": "Email",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "col_break_address",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "shipping_address",
"fieldtype": "Link",
"label": "Company Shipping Address",
"options": "Address",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "shipping_address_display",
"fieldtype": "Small Text",
"label": "Shipping Address Details",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "currency_and_price_list",
"fieldtype": "Section Break",
"label": "Currency and Price List",
- "options": "fa fa-tag"
+ "options": "fa fa-tag",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "currency",
@@ -390,7 +450,9 @@
"oldfieldtype": "Select",
"options": "Currency",
"print_hide": 1,
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "conversion_rate",
@@ -400,18 +462,24 @@
"oldfieldtype": "Currency",
"precision": "9",
"print_hide": 1,
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "cb_price_list",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "buying_price_list",
"fieldtype": "Link",
"label": "Price List",
"options": "Price List",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "price_list_currency",
@@ -419,14 +487,18 @@
"label": "Price List Currency",
"options": "Currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "plc_conversion_rate",
"fieldtype": "Float",
"label": "Price List Exchange Rate",
"precision": "9",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
@@ -435,7 +507,9 @@
"label": "Ignore Pricing Rule",
"no_copy": 1,
"permlevel": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "sec_warehouse",
@@ -448,11 +522,15 @@
"fieldtype": "Link",
"label": "Set Target Warehouse",
"options": "Warehouse",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "col_break_warehouse",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "No",
@@ -461,26 +539,34 @@
"in_standard_filter": 1,
"label": "Supply Raw Materials",
"options": "No\nYes",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.is_subcontracted==\"Yes\"",
"fieldname": "supplier_warehouse",
"fieldtype": "Link",
"label": "Supplier Warehouse",
- "options": "Warehouse"
+ "options": "Warehouse",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "items_section",
"fieldtype": "Section Break",
"hide_border": 1,
"oldfieldtype": "Section Break",
- "options": "fa fa-shopping-cart"
+ "options": "fa fa-shopping-cart",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "scan_barcode",
"fieldtype": "Data",
- "label": "Scan Barcode"
+ "label": "Scan Barcode",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_bulk_edit": 1,
@@ -490,46 +576,61 @@
"oldfieldname": "po_details",
"oldfieldtype": "Table",
"options": "Purchase Order Item",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "section_break_48",
"fieldtype": "Section Break",
- "label": "Pricing Rules"
+ "label": "Pricing Rules",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "pricing_rules",
"fieldtype": "Table",
"label": "Purchase Order Pricing Rule",
"options": "Pricing Rule Detail",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible_depends_on": "supplied_items",
"fieldname": "raw_material_details",
"fieldtype": "Section Break",
- "label": "Raw Materials Supplied"
+ "label": "Raw Materials Supplied",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "supplied_items",
"fieldtype": "Table",
"label": "Supplied Items",
+ "no_copy": 1,
"oldfieldname": "po_raw_material_details",
"oldfieldtype": "Table",
"options": "Purchase Order Item Supplied",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "sb_last_purchase",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "total_qty",
"fieldtype": "Float",
"label": "Total Quantity",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "base_total",
@@ -537,7 +638,9 @@
"label": "Total (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "base_net_total",
@@ -548,18 +651,24 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_26",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "total",
"fieldtype": "Currency",
"label": "Total",
"options": "currency",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "net_total",
@@ -569,20 +678,26 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "total_net_weight",
"fieldtype": "Float",
"label": "Total Net Weight",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "taxes_section",
"fieldtype": "Section Break",
"oldfieldtype": "Section Break",
- "options": "fa fa-money"
+ "options": "fa fa-money",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "taxes_and_charges",
@@ -591,18 +706,24 @@
"oldfieldname": "purchase_other_charges",
"oldfieldtype": "Link",
"options": "Purchase Taxes and Charges Template",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_50",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "shipping_rule",
"fieldtype": "Link",
"label": "Shipping Rule",
"options": "Shipping Rule",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "section_break_52",
@@ -615,13 +736,17 @@
"label": "Purchase Taxes and Charges",
"oldfieldname": "purchase_tax_details",
"oldfieldtype": "Table",
- "options": "Purchase Taxes and Charges"
+ "options": "Purchase Taxes and Charges",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "sec_tax_breakup",
"fieldtype": "Section Break",
- "label": "Tax Breakup"
+ "label": "Tax Breakup",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "other_charges_calculation",
@@ -630,14 +755,18 @@
"no_copy": 1,
"oldfieldtype": "HTML",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "totals",
"fieldtype": "Section Break",
"label": "Taxes and Charges",
"oldfieldtype": "Section Break",
- "options": "fa fa-money"
+ "options": "fa fa-money",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "base_taxes_and_charges_added",
@@ -648,7 +777,9 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "base_taxes_and_charges_deducted",
@@ -659,7 +790,9 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "base_total_taxes_and_charges",
@@ -671,11 +804,15 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_39",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "taxes_and_charges_added",
@@ -686,7 +823,9 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "taxes_and_charges_deducted",
@@ -697,7 +836,9 @@
"oldfieldtype": "Currency",
"options": "currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "total_taxes_and_charges",
@@ -706,14 +847,18 @@
"label": "Total Taxes and Charges",
"options": "currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"collapsible_depends_on": "apply_discount_on",
"fieldname": "discount_section",
"fieldtype": "Section Break",
- "label": "Additional Discount"
+ "label": "Additional Discount",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "Grand Total",
@@ -721,7 +866,9 @@
"fieldtype": "Select",
"label": "Apply Additional Discount On",
"options": "\nGrand Total\nNet Total",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "base_discount_amount",
@@ -729,24 +876,32 @@
"label": "Additional Discount Amount (Company Currency)",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_45",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "additional_discount_percentage",
"fieldtype": "Float",
"label": "Additional Discount Percentage",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "discount_amount",
"fieldtype": "Currency",
"label": "Additional Discount Amount",
"options": "currency",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "totals_section",
@@ -762,16 +917,21 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency)",
"no_copy": 1,
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"description": "In Words will be visible once you save the Purchase Order.",
@@ -782,7 +942,9 @@
"oldfieldname": "in_words",
"oldfieldtype": "Data",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "base_rounded_total",
@@ -792,12 +954,16 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break4",
"fieldtype": "Column Break",
- "oldfieldtype": "Column Break"
+ "oldfieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "grand_total",
@@ -807,29 +973,38 @@
"oldfieldname": "grand_total_import",
"oldfieldtype": "Currency",
"options": "currency",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment",
"no_copy": 1,
"options": "currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "rounded_total",
"fieldtype": "Currency",
"label": "Rounded Total",
"options": "currency",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"fieldname": "disable_rounded_total",
"fieldtype": "Check",
- "label": "Disable Rounded Total"
+ "label": "Disable Rounded Total",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "in_words",
@@ -839,7 +1014,9 @@
"oldfieldname": "in_words_import",
"oldfieldtype": "Data",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "advance_paid",
@@ -848,19 +1025,25 @@
"no_copy": 1,
"options": "party_account_currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "payment_schedule_section",
"fieldtype": "Section Break",
- "label": "Payment Terms"
+ "label": "Payment Terms",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "payment_terms_template",
"fieldtype": "Link",
"label": "Payment Terms Template",
- "options": "Payment Terms Template"
+ "options": "Payment Terms Template",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "payment_schedule",
@@ -868,7 +1051,9 @@
"label": "Payment Schedule",
"no_copy": 1,
"options": "Payment Schedule",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
@@ -877,7 +1062,9 @@
"fieldtype": "Section Break",
"label": "Terms and Conditions",
"oldfieldtype": "Section Break",
- "options": "fa fa-legal"
+ "options": "fa fa-legal",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "tc_name",
@@ -886,21 +1073,27 @@
"oldfieldname": "tc_name",
"oldfieldtype": "Link",
"options": "Terms and Conditions",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "terms",
"fieldtype": "Text Editor",
"label": "Terms and Conditions",
"oldfieldname": "terms",
- "oldfieldtype": "Text Editor"
+ "oldfieldtype": "Text Editor",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information",
- "oldfieldtype": "Section Break"
+ "oldfieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "Draft",
@@ -915,7 +1108,9 @@
"print_hide": 1,
"read_only": 1,
"reqd": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "ref_sq",
@@ -926,7 +1121,9 @@
"oldfieldtype": "Data",
"options": "Supplier Quotation",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "party_account_currency",
@@ -936,18 +1133,24 @@
"no_copy": 1,
"options": "Currency",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "inter_company_order_reference",
"fieldtype": "Link",
"label": "Inter Company Order Reference",
"options": "Sales Order",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_74",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:!doc.__islocal",
@@ -957,7 +1160,9 @@
"label": "% Received",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:!doc.__islocal",
@@ -967,7 +1172,9 @@
"label": "% Billed",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
@@ -977,6 +1184,8 @@
"oldfieldtype": "Column Break",
"print_hide": 1,
"print_width": "50%",
+ "show_days": 1,
+ "show_seconds": 1,
"width": "50%"
},
{
@@ -987,7 +1196,9 @@
"oldfieldname": "letter_head",
"oldfieldtype": "Select",
"options": "Letter Head",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -999,11 +1210,15 @@
"oldfieldtype": "Link",
"options": "Print Heading",
"print_hide": 1,
- "report_hide": 1
+ "report_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_86",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -1011,19 +1226,25 @@
"fieldname": "group_same_items",
"fieldtype": "Check",
"label": "Group same items",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "language",
"fieldtype": "Data",
"label": "Print Language",
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
"fieldname": "subscription_section",
"fieldtype": "Section Break",
- "label": "Subscription Section"
+ "label": "Subscription Section",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -1031,7 +1252,9 @@
"fieldtype": "Date",
"label": "From Date",
"no_copy": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
@@ -1039,11 +1262,15 @@
"fieldtype": "Date",
"label": "To Date",
"no_copy": 1,
- "print_hide": 1
+ "print_hide": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_97",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "auto_repeat",
@@ -1052,27 +1279,35 @@
"no_copy": 1,
"options": "Auto Repeat",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"allow_on_submit": 1,
"depends_on": "eval: doc.auto_repeat",
"fieldname": "update_auto_repeat_reference",
"fieldtype": "Button",
- "label": "Update Auto Repeat Reference"
+ "label": "Update Auto Repeat Reference",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "tax_category",
"fieldtype": "Link",
"label": "Tax Category",
- "options": "Tax Category"
+ "options": "Tax Category",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "supplied_items",
"fieldname": "set_reserve_warehouse",
"fieldtype": "Link",
"label": "Set Reserve Warehouse",
- "options": "Warehouse"
+ "options": "Warehouse",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"collapsible": 1,
@@ -1082,7 +1317,9 @@
},
{
"fieldname": "column_break_75",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "billing_address",
@@ -1118,13 +1355,30 @@
"label": "Represents Company",
"options": "Company",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "apply_tds",
+ "fieldtype": "Check",
+ "label": "Apply Tax Withholding Amount",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "depends_on": "eval: doc.apply_tds",
+ "fieldname": "tax_withholding_category",
+ "fieldtype": "Link",
+ "label": "Tax Withholding Category",
+ "options": "Tax Withholding Category",
+ "show_days": 1,
+ "show_seconds": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2021-01-20 22:07:23.487138",
+ "modified": "2021-05-30 15:17:53.663648",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order",
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py
index ef9372eeb65..eaa502ff7f0 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -14,11 +14,11 @@ from frappe.desk.notifications import clear_doctype_notifications
from erpnext.buying.utils import validate_for_items, check_on_hold_or_closed_status
from erpnext.stock.utils import get_bin
from erpnext.accounts.party import get_party_account_currency
-from six import string_types
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
-from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_party, update_linked_doc,\
- unlink_inter_company_doc
+from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
+from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_party,
+ update_linked_doc, unlink_inter_company_doc)
form_grid_templates = {
"items": "templates/form_grid/item_grid.html"
@@ -39,11 +39,18 @@ class PurchaseOrder(BuyingController):
'percent_join_field': 'material_request'
}]
+ def onload(self):
+ supplier_tds = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category")
+ self.set_onload("supplier_tds", supplier_tds)
+
def validate(self):
super(PurchaseOrder, self).validate()
self.set_status()
+ # apply tax withholding only if checked and applicable
+ self.set_tax_withholding()
+
self.validate_supplier()
self.validate_schedule_date()
validate_for_items(self)
@@ -87,6 +94,33 @@ class PurchaseOrder(BuyingController):
if cint(frappe.db.get_single_value('Buying Settings', 'maintain_same_rate')):
self.validate_rate_with_reference_doc([["Supplier Quotation", "supplier_quotation", "supplier_quotation_item"]])
+ def set_tax_withholding(self):
+ if not self.apply_tds:
+ return
+
+ tax_withholding_details = get_party_tax_withholding_details(self, self.tax_withholding_category)
+
+ if not tax_withholding_details:
+ return
+
+ accounts = []
+ for d in self.taxes:
+ if d.account_head == tax_withholding_details.get("account_head"):
+ d.update(tax_withholding_details)
+ accounts.append(d.account_head)
+
+ if not accounts or tax_withholding_details.get("account_head") not in accounts:
+ self.append("taxes", tax_withholding_details)
+
+ to_remove = [d for d in self.taxes
+ if not d.tax_amount and d.account_head == tax_withholding_details.get("account_head")]
+
+ for d in to_remove:
+ self.remove(d)
+
+ # calculate totals again after applying TDS
+ self.calculate_taxes_and_totals()
+
def validate_supplier(self):
prevent_po = frappe.db.get_value("Supplier", self.supplier, 'prevent_pos')
if prevent_po:
@@ -104,7 +138,7 @@ class PurchaseOrder(BuyingController):
def validate_minimum_order_qty(self):
if not self.get("items"): return
- items = list(set([d.item_code for d in self.get("items")]))
+ items = list(set(d.item_code for d in self.get("items")))
itemwise_min_order_qty = frappe._dict(frappe.db.sql("""select name, min_order_qty
from tabItem where name in ({0})""".format(", ".join(["%s"] * len(items))), items))
@@ -291,10 +325,10 @@ class PurchaseOrder(BuyingController):
so.notify_update()
def has_drop_ship_item(self):
- return any([d.delivered_by_supplier for d in self.items])
+ return any(d.delivered_by_supplier for d in self.items)
def is_against_so(self):
- return any([d.sales_order for d in self.items if d.sales_order])
+ return any(d.sales_order for d in self.items if d.sales_order)
def set_received_qty_for_drop_ship_items(self):
for item in self.items:
@@ -468,9 +502,11 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions
@frappe.whitelist()
def make_rm_stock_entry(purchase_order, rm_items):
- if isinstance(rm_items, string_types):
+ rm_items_list = rm_items
+
+ if isinstance(rm_items, str):
rm_items_list = json.loads(rm_items)
- else:
+ elif not rm_items:
frappe.throw(_("No Items available for transfer"))
if rm_items_list:
@@ -508,6 +544,8 @@ def make_rm_stock_entry(purchase_order, rm_items):
'qty': rm_item_data["qty"],
'from_warehouse': rm_item_data["warehouse"],
'stock_uom': rm_item_data["stock_uom"],
+ 'serial_no': rm_item_data.get('serial_no'),
+ 'batch_no': rm_item_data.get('batch_no'),
'main_item_code': rm_item_data["item_code"],
'allow_alternative_item': item_wh.get(rm_item_code, {}).get('allow_alternative_item')
}
@@ -547,3 +585,58 @@ def update_status(status, name):
def make_inter_company_sales_order(source_name, target_doc=None):
from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_inter_company_transaction
return make_inter_company_transaction("Purchase Order", source_name, target_doc)
+
+@frappe.whitelist()
+def get_materials_from_supplier(purchase_order, po_details):
+ if isinstance(po_details, str):
+ po_details = json.loads(po_details)
+
+ doc = frappe.get_cached_doc('Purchase Order', purchase_order)
+ doc.initialized_fields()
+ doc.purchase_orders = [doc.name]
+ doc.get_available_materials()
+
+ if not doc.available_materials:
+ frappe.throw(_('Materials are already received against the purchase order {0}')
+ .format(purchase_order))
+
+ return make_return_stock_entry_for_subcontract(doc.available_materials, doc, po_details)
+
+def make_return_stock_entry_for_subcontract(available_materials, po_doc, po_details):
+ ste_doc = frappe.new_doc('Stock Entry')
+ ste_doc.purpose = 'Material Transfer'
+ ste_doc.purchase_order = po_doc.name
+ ste_doc.company = po_doc.company
+ ste_doc.is_return = 1
+
+ for key, value in available_materials.items():
+ if not value.qty:
+ continue
+
+ if value.batch_no:
+ for batch_no, qty in value.batch_no.items():
+ if qty > 0:
+ add_items_in_ste(ste_doc, value, value.qty, po_details, batch_no)
+ else:
+ add_items_in_ste(ste_doc, value, value.qty, po_details)
+
+ ste_doc.set_stock_entry_type()
+ ste_doc.calculate_rate_and_amount()
+
+ return ste_doc
+
+def add_items_in_ste(ste_doc, row, qty, po_details, batch_no=None):
+ item = ste_doc.append('items', row.item_details)
+
+ po_detail = list(set(row.po_details).intersection(po_details))
+ item.update({
+ 'qty': qty,
+ 'batch_no': batch_no,
+ 'basic_rate': row.item_details['rate'],
+ 'po_detail': po_detail[0] if po_detail else '',
+ 's_warehouse': row.item_details['t_warehouse'],
+ 't_warehouse': row.item_details['s_warehouse'],
+ 'item_code': row.item_details['rm_item_code'],
+ 'subcontracted_item': row.item_details['main_item_code'],
+ 'serial_no': '\n'.join(row.serial_no) if row.serial_no else ''
+ })
\ No newline at end of file
diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
index aaa98f2f1f4..8563b97ab74 100644
--- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py
@@ -20,7 +20,6 @@ from erpnext.controllers.status_updater import OverAllowanceError
from erpnext.manufacturing.doctype.blanket_order.test_blanket_order import make_blanket_order
from erpnext.stock.doctype.batch.test_batch import make_new_batch
-from erpnext.controllers.buying_controller import get_backflushed_subcontracted_raw_materials
class TestPurchaseOrder(unittest.TestCase):
def test_make_purchase_receipt(self):
@@ -359,7 +358,7 @@ class TestPurchaseOrder(unittest.TestCase):
update_child_qty_rate('Purchase Order', trans_item, po.name)
po.reload()
- total_reqd_qty_after_change = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")])
+ total_reqd_qty_after_change = sum(d.get("required_qty") for d in po.as_dict().get("supplied_items"))
self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty)
@@ -771,7 +770,7 @@ class TestPurchaseOrder(unittest.TestCase):
self.assertEqual(bin11.reserved_qty_for_sub_contract, bin1.reserved_qty_for_sub_contract)
def test_exploded_items_in_subcontracted(self):
- item_code = "_Test Subcontracted FG Item 1"
+ item_code = "_Test Subcontracted FG Item 11"
make_subcontracted_item(item_code=item_code)
po = create_purchase_order(item_code=item_code, qty=1,
@@ -848,79 +847,6 @@ class TestPurchaseOrder(unittest.TestCase):
for item in rm_items:
transferred_rm_map[item.get('rm_item_code')] = item
- for item in pr.get('supplied_items'):
- self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty)
-
- update_backflush_based_on("BOM")
-
- def test_backflushed_based_on_for_multiple_batches(self):
- item_code = "_Test Subcontracted FG Item 2"
- make_item('Sub Contracted Raw Material 2', {
- 'is_stock_item': 1,
- 'is_sub_contracted_item': 1
- })
-
- make_subcontracted_item(item_code=item_code, has_batch_no=1, create_new_batch=1,
- raw_materials=["Sub Contracted Raw Material 2"])
-
- update_backflush_based_on("Material Transferred for Subcontract")
-
- order_qty = 500
- po = create_purchase_order(item_code=item_code, qty=order_qty,
- is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
-
- make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Sub Contracted Raw Material 2", qty=552, basic_rate=100)
-
- rm_items = [
- {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 2","item_name":"_Test Item",
- "qty":552,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}]
-
- rm_item_string = json.dumps(rm_items)
- se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string))
- se.submit()
-
- for batch in ["ABCD1", "ABCD2", "ABCD3", "ABCD4"]:
- make_new_batch(batch_id=batch, item_code=item_code)
-
- pr = make_purchase_receipt(po.name)
-
- # partial receipt
- pr.get('items')[0].qty = 30
- pr.get('items')[0].batch_no = "ABCD1"
-
- purchase_order = po.name
- purchase_order_item = po.items[0].name
-
- for batch_no, qty in {"ABCD2": 60, "ABCD3": 70, "ABCD4":40}.items():
- pr.append("items", {
- "item_code": pr.get('items')[0].item_code,
- "item_name": pr.get('items')[0].item_name,
- "uom": pr.get('items')[0].uom,
- "stock_uom": pr.get('items')[0].stock_uom,
- "warehouse": pr.get('items')[0].warehouse,
- "conversion_factor": pr.get('items')[0].conversion_factor,
- "cost_center": pr.get('items')[0].cost_center,
- "rate": pr.get('items')[0].rate,
- "qty": qty,
- "batch_no": batch_no,
- "purchase_order": purchase_order,
- "purchase_order_item": purchase_order_item
- })
-
- pr.submit()
-
- pr1 = make_purchase_receipt(po.name)
- pr1.get('items')[0].qty = 300
- pr1.get('items')[0].batch_no = "ABCD1"
- pr1.save()
-
- pr_key = ("Sub Contracted Raw Material 2", po.name)
- consumed_qty = get_backflushed_subcontracted_raw_materials([po.name]).get(pr_key)
-
- self.assertTrue(pr1.supplied_items[0].consumed_qty > 0)
- self.assertTrue(pr1.supplied_items[0].consumed_qty, flt(552.0) - flt(consumed_qty))
-
update_backflush_based_on("BOM")
def test_supplied_qty_against_subcontracted_po(self):
@@ -1111,28 +1037,35 @@ def create_purchase_order(**args):
po.schedule_date = add_days(nowdate(), 1)
po.company = args.company or "_Test Company"
- po.supplier = args.customer or "_Test Supplier"
+ po.supplier = args.supplier or "_Test Supplier"
po.is_subcontracted = args.is_subcontracted or "No"
po.currency = args.currency or frappe.get_cached_value('Company', po.company, "default_currency")
po.conversion_factor = args.conversion_factor or 1
po.supplier_warehouse = args.supplier_warehouse or None
- po.append("items", {
- "item_code": args.item or args.item_code or "_Test Item",
- "warehouse": args.warehouse or "_Test Warehouse - _TC",
- "qty": args.qty or 10,
- "rate": args.rate or 500,
- "schedule_date": add_days(nowdate(), 1),
- "include_exploded_items": args.get('include_exploded_items', 1),
- "against_blanket_order": args.against_blanket_order
- })
+ if args.rm_items:
+ for row in args.rm_items:
+ po.append("items", row)
+ else:
+ po.append("items", {
+ "item_code": args.item or args.item_code or "_Test Item",
+ "warehouse": args.warehouse or "_Test Warehouse - _TC",
+ "qty": args.qty or 10,
+ "rate": args.rate or 500,
+ "schedule_date": add_days(nowdate(), 1),
+ "include_exploded_items": args.get('include_exploded_items', 1),
+ "against_blanket_order": args.against_blanket_order
+ })
+
+ po.set_missing_values()
if not args.do_not_save:
po.insert()
if not args.do_not_submit:
if po.is_subcontracted == "Yes":
supp_items = po.get("supplied_items")
for d in supp_items:
- d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
+ if not d.reserve_warehouse:
+ d.reserve_warehouse = args.warehouse or "_Test Warehouse - _TC"
po.submit()
return po
diff --git a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
index d7ea9c1ccc8..60247bd90bb 100644
--- a/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
+++ b/erpnext/buying/doctype/purchase_order_item_supplied/purchase_order_item_supplied.json
@@ -6,21 +6,25 @@
"engine": "InnoDB",
"field_order": [
"main_item_code",
- "bom_detail_no",
+ "rm_item_code",
+ "column_break_3",
"stock_uom",
+ "reserve_warehouse",
"conversion_factor",
"column_break_6",
- "rm_item_code",
+ "bom_detail_no",
"reference_name",
- "reserve_warehouse",
"section_break2",
"rate",
"col_break2",
"amount",
"section_break1",
"required_qty",
+ "supplied_qty",
"col_break1",
- "supplied_qty"
+ "consumed_qty",
+ "returned_qty",
+ "total_supplied_qty"
],
"fields": [
{
@@ -125,6 +129,8 @@
"fieldtype": "Float",
"in_list_view": 1,
"label": "Supplied Qty",
+ "no_copy": 1,
+ "print_hide": 1,
"read_only": 1
},
{
@@ -142,13 +148,42 @@
{
"fieldname": "col_break2",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "consumed_qty",
+ "fieldtype": "Float",
+ "label": "Consumed Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "returned_qty",
+ "fieldtype": "Float",
+ "label": "Returned Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "total_supplied_qty",
+ "fieldtype": "Float",
+ "hidden": 1,
+ "label": "Total Supplied Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
}
],
"hide_toolbar": 1,
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-06-09 15:17:58.128242",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item Supplied",
diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
index dc00bca5cc5..f9cd72015a6 100644
--- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
+++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json
@@ -6,10 +6,11 @@
"engine": "InnoDB",
"field_order": [
"main_item_code",
- "description",
+ "rm_item_code",
+ "item_name",
"bom_detail_no",
"col_break1",
- "rm_item_code",
+ "description",
"stock_uom",
"conversion_factor",
"reference_name",
@@ -25,7 +26,8 @@
"secbreak_3",
"batch_no",
"col_break4",
- "serial_no"
+ "serial_no",
+ "purchase_order"
],
"fields": [
{
@@ -52,7 +54,6 @@
"fieldname": "description",
"fieldtype": "Text Editor",
"in_global_search": 1,
- "in_list_view": 1,
"label": "Description",
"oldfieldname": "description",
"oldfieldtype": "Data",
@@ -81,18 +82,20 @@
"fieldname": "required_qty",
"fieldtype": "Float",
"in_list_view": 1,
- "label": "Required Qty",
+ "label": "Available Qty For Consumption",
"oldfieldname": "required_qty",
"oldfieldtype": "Currency",
+ "print_hide": 1,
"read_only": 1
},
{
+ "columns": 2,
"fieldname": "consumed_qty",
"fieldtype": "Float",
- "label": "Consumed Qty",
+ "in_list_view": 1,
+ "label": "Qty to Be Consumed",
"oldfieldname": "consumed_qty",
"oldfieldtype": "Currency",
- "read_only": 1,
"reqd": 1
},
{
@@ -183,12 +186,28 @@
{
"fieldname": "col_break4",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "purchase_order",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "label": "Purchase Order",
+ "no_copy": 1,
+ "options": "Purchase Order",
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-18 17:26:09.703215",
+ "modified": "2021-06-19 19:33:04.431213",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Receipt Item Supplied",
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
index b530d1ab241..a4ce84e1cf9 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -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
@@ -316,19 +317,21 @@ def add_items(sq_doc, supplier, items):
create_rfq_items(sq_doc, supplier, data)
def create_rfq_items(sq_doc, supplier, data):
- sq_doc.append('items', {
- "item_code": data.item_code,
- "item_name": data.item_name,
- "description": data.description,
- "qty": data.qty,
- "rate": data.rate,
- "conversion_factor": data.conversion_factor if data.conversion_factor else None,
- "supplier_part_no": frappe.db.get_value("Item Supplier", {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no"),
- "warehouse": data.warehouse or '',
+ args = {}
+
+ for field in ['item_code', 'item_name', 'description', 'qty', 'rate', 'conversion_factor',
+ 'warehouse', 'material_request', 'material_request_item', 'stock_qty']:
+ args[field] = data.get(field)
+
+ args.update({
"request_for_quotation_item": data.name,
- "request_for_quotation": data.parent
+ "request_for_quotation": data.parent,
+ "supplier_part_no": frappe.db.get_value("Item Supplier",
+ {'parent': data.item_code, 'supplier': supplier}, "supplier_part_no")
})
+ sq_doc.append('items', args)
+
@frappe.whitelist()
def get_pdf(doctype, name, supplier):
doc = get_rfq_doc(doctype, name, supplier)
@@ -390,7 +393,7 @@ def get_item_from_material_requests_based_on_supplier(source_name, target_doc =
def get_supplier_tag():
if not frappe.cache().hget("Supplier", "Tags"):
filters = {"document_type": "Supplier"}
- tags = list(set([tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag]))
+ tags = list(set(tag.tag for tag in frappe.get_all("Tag Link", filters=filters, fields=["tag"]) if tag))
frappe.cache().hset("Supplier", "Tags", tags)
return frappe.cache().hget("Supplier", "Tags")
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 4cc5753cbd0..38b8dfdf48d 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -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",
diff --git a/erpnext/patches/v10_1/__init__.py b/erpnext/buying/doctype/supplier_item_group/__init__.py
similarity index 100%
rename from erpnext/patches/v10_1/__init__.py
rename to erpnext/buying/doctype/supplier_item_group/__init__.py
diff --git a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.js b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.js
new file mode 100644
index 00000000000..f7da90d98d6
--- /dev/null
+++ b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.js
@@ -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) {
+
+ // }
+});
diff --git a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.json b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.json
new file mode 100644
index 00000000000..1971458f61e
--- /dev/null
+++ b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py
new file mode 100644
index 00000000000..3a2e5d6dcef
--- /dev/null
+++ b/erpnext/buying/doctype/supplier_item_group/supplier_item_group.py
@@ -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."))
\ No newline at end of file
diff --git a/erpnext/buying/doctype/supplier_item_group/test_supplier_item_group.py b/erpnext/buying/doctype/supplier_item_group/test_supplier_item_group.py
new file mode 100644
index 00000000000..c75044d44eb
--- /dev/null
+++ b/erpnext/buying/doctype/supplier_item_group/test_supplier_item_group.py
@@ -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
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index 40fbe2c26e5..0a51a8e9a1c 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -576,6 +576,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "base_rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment (Company Currency",
@@ -620,6 +621,7 @@
"read_only": 1
},
{
+ "depends_on": "eval:!doc.disable_rounded_total",
"fieldname": "rounding_adjustment",
"fieldtype": "Currency",
"label": "Rounding Adjustment",
@@ -802,7 +804,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-12-03 15:18:29.073368",
+ "modified": "2021-04-19 00:58:20.995491",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
diff --git a/erpnext/patches/v11_1/__init__.py b/erpnext/buying/report/subcontract_order_summary/__init__.py
similarity index 100%
rename from erpnext/patches/v11_1/__init__.py
rename to erpnext/buying/report/subcontract_order_summary/__init__.py
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
new file mode 100644
index 00000000000..5ba52f1b21e
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.js
@@ -0,0 +1,45 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Subcontract Order Summary"] = {
+ "filters": [
+ {
+ label: __("Company"),
+ fieldname: "company",
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ label: __("From Date"),
+ fieldname:"from_date",
+ fieldtype: "Date",
+ default: frappe.datetime.add_months(frappe.datetime.get_today(), -1),
+ reqd: 1
+ },
+ {
+ label: __("To Date"),
+ fieldname:"to_date",
+ fieldtype: "Date",
+ default: frappe.datetime.get_today(),
+ reqd: 1
+ },
+ {
+ label: __("Purchase Order"),
+ fieldname: "name",
+ fieldtype: "Link",
+ options: "Purchase Order",
+ get_query: function() {
+ return {
+ filters: {
+ docstatus: 1,
+ is_subcontracted: 'Yes',
+ company: frappe.query_report.get_filter_value('company')
+ }
+ }
+ }
+ }
+ ]
+};
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
new file mode 100644
index 00000000000..526a8d8ad01
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.json
@@ -0,0 +1,32 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-05-31 14:43:32.417694",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-05-31 14:43:32.417694",
+ "modified_by": "Administrator",
+ "module": "Buying",
+ "name": "Subcontract Order Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Purchase Order",
+ "report_name": "Subcontract Order Summary",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Purchase Manager"
+ },
+ {
+ "role": "Purchase User"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
new file mode 100644
index 00000000000..0c0d4f0531d
--- /dev/null
+++ b/erpnext/buying/report/subcontract_order_summary/subcontract_order_summary.py
@@ -0,0 +1,152 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _
+
+def execute(filters=None):
+ columns, data = [], []
+ columns = get_columns()
+ data = get_data(filters)
+
+ return columns, data
+
+def get_data(report_filters):
+ data = []
+ orders = get_subcontracted_orders(report_filters)
+
+ if orders:
+ supplied_items = get_supplied_items(orders, report_filters)
+ po_details = prepare_subcontracted_data(orders, supplied_items)
+ get_subcontracted_data(po_details, data)
+
+ return data
+
+def get_subcontracted_orders(report_filters):
+ fields = ['`tabPurchase Order Item`.`parent` as po_id', '`tabPurchase Order Item`.`item_code`',
+ '`tabPurchase Order Item`.`item_name`', '`tabPurchase Order Item`.`qty`', '`tabPurchase Order Item`.`name`',
+ '`tabPurchase Order Item`.`received_qty`', '`tabPurchase Order`.`status`']
+
+ filters = get_filters(report_filters)
+
+ return frappe.get_all('Purchase Order', fields = fields, filters=filters) or []
+
+def get_filters(report_filters):
+ filters = [['Purchase Order', 'docstatus', '=', 1], ['Purchase Order', 'is_subcontracted', '=', 'Yes'],
+ ['Purchase Order', 'transaction_date', 'between', (report_filters.from_date, report_filters.to_date)]]
+
+ for field in ['name', 'company']:
+ if report_filters.get(field):
+ filters.append(['Purchase Order', field, '=', report_filters.get(field)])
+
+ return filters
+
+def get_supplied_items(orders, report_filters):
+ if not orders:
+ return []
+
+ fields = ['parent', 'main_item_code', 'rm_item_code', 'required_qty',
+ 'supplied_qty', 'returned_qty', 'total_supplied_qty', 'consumed_qty', 'reference_name']
+
+ filters = {'parent': ('in', [d.po_id for d in orders]), 'docstatus': 1}
+
+ supplied_items = {}
+ for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters):
+ new_key = (row.parent, row.reference_name, row.main_item_code)
+
+ supplied_items.setdefault(new_key, []).append(row)
+
+ return supplied_items
+
+def prepare_subcontracted_data(orders, supplied_items):
+ po_details = {}
+ for row in orders:
+ key = (row.po_id, row.name, row.item_code)
+ if key not in po_details:
+ po_details.setdefault(key, frappe._dict({'po_item': row, 'supplied_items': []}))
+
+ details = po_details[key]
+
+ if supplied_items.get(key):
+ for supplied_item in supplied_items[key]:
+ details['supplied_items'].append(supplied_item)
+
+ return po_details
+
+def get_subcontracted_data(po_details, data):
+ for key, details in po_details.items():
+ res = details.po_item
+ for index, row in enumerate(details.supplied_items):
+ if index != 0:
+ res = {}
+
+ res.update(row)
+ data.append(res)
+
+def get_columns():
+ return [
+ {
+ "label": _("Purchase Order"),
+ "fieldname": "po_id",
+ "fieldtype": "Link",
+ "options": "Purchase Order",
+ "width": 100
+ },
+ {
+ "label": _("Status"),
+ "fieldname": "status",
+ "fieldtype": "Data",
+ "width": 80
+ },
+ {
+ "label": _("Subcontracted Item"),
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 160
+ },
+ {
+ "label": _("Order Qty"),
+ "fieldname": "qty",
+ "fieldtype": "Float",
+ "width": 90
+ },
+ {
+ "label": _("Received Qty"),
+ "fieldname": "received_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Supplied Item"),
+ "fieldname": "rm_item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 160
+ },
+ {
+ "label": _("Required Qty"),
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Supplied Qty"),
+ "fieldname": "supplied_qty",
+ "fieldtype": "Float",
+ "width": 110
+ },
+ {
+ "label": _("Consumed Qty"),
+ "fieldname": "consumed_qty",
+ "fieldtype": "Float",
+ "width": 120
+ },
+ {
+ "label": _("Returned Qty"),
+ "fieldname": "returned_qty",
+ "fieldtype": "Float",
+ "width": 110
+ }
+ ]
\ No newline at end of file
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py
index de2ae8fc73d..68426abbb04 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/subcontracted_raw_materials_to_be_transferred.py
@@ -9,10 +9,10 @@ def execute(filters=None):
if filters.from_date >= filters.to_date:
frappe.msgprint(_("To Date must be greater than From Date"))
- data = []
columns = get_columns()
- get_data(data , filters)
- return columns, data
+ data = get_data(filters)
+
+ return columns, data or []
def get_columns():
return [
@@ -21,13 +21,12 @@ def get_columns():
"fieldtype": "Link",
"fieldname": "purchase_order",
"options": "Purchase Order",
- "width": 150
+ "width": 200
},
{
"label": _("Date"),
"fieldtype": "Date",
"fieldname": "date",
- "hidden": 1,
"width": 150
},
{
@@ -41,97 +40,58 @@ def get_columns():
"label": _("Item Code"),
"fieldtype": "Data",
"fieldname": "rm_item_code",
- "width": 100
+ "width": 150
},
{
"label": _("Required Quantity"),
"fieldtype": "Float",
- "fieldname": "r_qty",
- "width": 100
+ "fieldname": "reqd_qty",
+ "width": 150
},
{
"label": _("Transferred Quantity"),
"fieldtype": "Float",
- "fieldname": "t_qty",
- "width": 100
+ "fieldname": "transferred_qty",
+ "width": 200
},
{
"label": _("Pending Quantity"),
"fieldtype": "Float",
"fieldname": "p_qty",
- "width": 100
+ "width": 150
}
]
-def get_data(data, filters):
- po = get_po(filters)
- po_transferred_qty_map = frappe._dict(get_transferred_quantity([v.name for v in po]))
+def get_data(filters):
+ po_rm_item_details = get_po_items_to_supply(filters)
- sub_items = get_purchase_order_item_supplied([v.name for v in po])
+ data = []
+ for row in po_rm_item_details:
+ transferred_qty = row.get("transferred_qty") or 0
+ if transferred_qty < row.get("reqd_qty", 0):
+ pending_qty = frappe.utils.flt(row.get("reqd_qty", 0) - transferred_qty)
+ row.p_qty = pending_qty if pending_qty > 0 else 0
+ data.append(row)
- for order in po:
- for item in sub_items:
- if order.name == item.parent and order.name in po_transferred_qty_map and \
- item.required_qty != po_transferred_qty_map.get(order.name).get(item.rm_item_code):
- transferred_qty = po_transferred_qty_map.get(order.name).get(item.rm_item_code) \
- if po_transferred_qty_map.get(order.name).get(item.rm_item_code) else 0
- row ={
- 'purchase_order': item.parent,
- 'date': order.transaction_date,
- 'supplier': order.supplier,
- 'rm_item_code': item.rm_item_code,
- 'r_qty': item.required_qty,
- 't_qty':transferred_qty,
- 'p_qty':item.required_qty - transferred_qty
- }
+ return data
- data.append(row)
-
- return(data)
-
-def get_po(filters):
- record_filters = [
- ["is_subcontracted", "=", "Yes"],
- ["supplier", "=", filters.supplier],
- ["transaction_date", "<=", filters.to_date],
- ["transaction_date", ">=", filters.from_date],
- ["docstatus", "=", 1]
- ]
- return frappe.get_all("Purchase Order", filters=record_filters, fields=["name", "transaction_date", "supplier"])
-
-def get_transferred_quantity(po_name):
- stock_entries = get_stock_entry(po_name)
- stock_entries_detail = get_stock_entry_detail([v.name for v in stock_entries])
- po_transferred_qty_map = {}
-
-
- for entry in stock_entries:
- for details in stock_entries_detail:
- if details.parent == entry.name:
- details["Purchase_order"] = entry.purchase_order
- if entry.purchase_order not in po_transferred_qty_map:
- po_transferred_qty_map[entry.purchase_order] = {}
- po_transferred_qty_map[entry.purchase_order][details.item_code] = details.qty
- else:
- po_transferred_qty_map[entry.purchase_order][details.item_code] = po_transferred_qty_map[entry.purchase_order].get(details.item_code, 0) + details.qty
-
- return po_transferred_qty_map
-
-
-def get_stock_entry(po):
- return frappe.get_all("Stock Entry", filters=[
- ('purchase_order', 'IN', po),
- ('stock_entry_type', '=', 'Send to Subcontractor'),
- ('docstatus', '=', 1)
- ], fields=["name", "purchase_order"])
-
-def get_stock_entry_detail(se):
- return frappe.get_all("Stock Entry Detail", filters=[
- ["parent", "in", se]
+def get_po_items_to_supply(filters):
+ return frappe.db.get_all(
+ "Purchase Order",
+ fields=[
+ "name as purchase_order",
+ "transaction_date as date",
+ "supplier as supplier",
+ "`tabPurchase Order Item Supplied`.rm_item_code as rm_item_code",
+ "`tabPurchase Order Item Supplied`.required_qty as reqd_qty",
+ "`tabPurchase Order Item Supplied`.supplied_qty as transferred_qty"
],
- fields=["parent", "item_code", "qty"])
-
-def get_purchase_order_item_supplied(po):
- return frappe.get_all("Purchase Order Item Supplied", filters=[
- ('parent', 'IN', po)
- ], fields=['parent', 'rm_item_code', 'required_qty'])
+ filters = [
+ ["Purchase Order", "per_received", "<", "100"],
+ ["Purchase Order", "is_subcontracted", "=", "Yes"],
+ ["Purchase Order", "supplier", "=", filters.supplier],
+ ["Purchase Order", "transaction_date", "<=", filters.to_date],
+ ["Purchase Order", "transaction_date", ">=", filters.from_date],
+ ["Purchase Order", "docstatus", "=", 1]
+ ]
+ )
\ No newline at end of file
diff --git a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
index c1fc6fb82ff..2448e17c50f 100644
--- a/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
+++ b/erpnext/buying/report/subcontracted_raw_materials_to_be_transferred/test_subcontracted_raw_materials_to_be_transferred.py
@@ -12,34 +12,80 @@ import json, frappe, unittest
class TestSubcontractedItemToBeTransferred(unittest.TestCase):
def test_pending_and_transferred_qty(self):
- po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes')
+ po = create_purchase_order(item_code='_Test FG Item', is_subcontracted='Yes', supplier_warehouse="_Test Warehouse 1 - _TC")
+
+ # Material Receipt of RMs
make_stock_entry(item_code='_Test Item', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
make_stock_entry(item_code='_Test Item Home Desktop 100', target='_Test Warehouse - _TC', qty=100, basic_rate=100)
- transfer_subcontracted_raw_materials(po.name)
- col, data = execute(filters=frappe._dict({'supplier': po.supplier,
- 'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
- 'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))}))
- self.assertEqual(data[0]['purchase_order'], po.name)
- self.assertIn(data[0]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100'])
- self.assertIn(data[0]['p_qty'], [9, 18])
- self.assertIn(data[0]['t_qty'], [1, 2])
- self.assertEqual(data[1]['purchase_order'], po.name)
- self.assertIn(data[1]['rm_item_code'], ['_Test Item', '_Test Item Home Desktop 100'])
- self.assertIn(data[1]['p_qty'], [9, 18])
- self.assertIn(data[1]['t_qty'], [1, 2])
+ se = transfer_subcontracted_raw_materials(po)
+ col, data = execute(filters=frappe._dict(
+ {
+ 'supplier': po.supplier,
+ 'from_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=-10)),
+ 'to_date': frappe.utils.get_datetime(frappe.utils.add_to_date(po.transaction_date, days=10))
+ }
+ ))
+ po.reload()
+
+ po_data = [row for row in data if row.get('purchase_order') == po.name]
+ # Alphabetically sort to be certain of order
+ po_data = sorted(po_data, key = lambda i: i['rm_item_code'])
+
+ self.assertEqual(len(po_data), 2)
+ self.assertEqual(po_data[0]['purchase_order'], po.name)
+
+ self.assertEqual(po_data[0]['rm_item_code'], '_Test Item')
+ self.assertEqual(po_data[0]['p_qty'], 8)
+ self.assertEqual(po_data[0]['transferred_qty'], 2)
+
+ self.assertEqual(po_data[1]['rm_item_code'], '_Test Item Home Desktop 100')
+ self.assertEqual(po_data[1]['p_qty'], 19)
+ self.assertEqual(po_data[1]['transferred_qty'], 1)
+
+ se.cancel()
+ po.cancel()
def transfer_subcontracted_raw_materials(po):
+ # Order of supplied items fetched in PO is flaky
+ transfer_qty_map = {
+ '_Test Item': 2,
+ '_Test Item Home Desktop 100': 1
+ }
+
+ item_1 = po.supplied_items[0].rm_item_code
+ item_2 = po.supplied_items[1].rm_item_code
+
rm_item = [
- {'item_code': '_Test Item', 'rm_item_code': '_Test Item', 'item_name': '_Test Item', 'qty': 1,
- 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 100, 'stock_uom': 'Nos'},
- {'item_code': '_Test Item Home Desktop 100', 'rm_item_code': '_Test Item Home Desktop 100', 'item_name': '_Test Item Home Desktop 100', 'qty': 2,
- 'warehouse': '_Test Warehouse - _TC', 'rate': 100, 'amount': 200, 'stock_uom': 'Nos'}]
+ {
+ 'name': po.supplied_items[0].name,
+ 'item_code': item_1,
+ 'rm_item_code': item_1,
+ 'item_name': item_1,
+ 'qty': transfer_qty_map[item_1],
+ 'warehouse': '_Test Warehouse - _TC',
+ 'rate': 100,
+ 'amount': 100 * transfer_qty_map[item_1],
+ 'stock_uom': 'Nos'
+ },
+ {
+ 'name': po.supplied_items[1].name,
+ 'item_code': item_2,
+ 'rm_item_code': item_2,
+ 'item_name': item_2,
+ 'qty': transfer_qty_map[item_2],
+ 'warehouse': '_Test Warehouse - _TC',
+ 'rate': 100,
+ 'amount': 100 * transfer_qty_map[item_2],
+ 'stock_uom': 'Nos'
+ }
+ ]
rm_item_string = json.dumps(rm_item)
- se = frappe.get_doc(make_rm_stock_entry(po, rm_item_string))
- se.from_warehouse = '_Test Warehouse 1 - _TC'
- se.to_warehouse = '_Test Warehouse 1 - _TC'
+ se = frappe.get_doc(make_rm_stock_entry(po.name, rm_item_string))
+ se.from_warehouse = '_Test Warehouse - _TC'
+ se.to_warehouse = '_Test Warehouse - _TC'
se.stock_entry_type = 'Send to Subcontractor'
se.save()
se.submit()
+ return se
diff --git a/erpnext/change_log/v13/v13_4_0.md b/erpnext/change_log/v13/v13_4_0.md
new file mode 100644
index 00000000000..eaf4f762d49
--- /dev/null
+++ b/erpnext/change_log/v13/v13_4_0.md
@@ -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))
diff --git a/erpnext/change_log/v13/v13_5_0.md b/erpnext/change_log/v13/v13_5_0.md
new file mode 100644
index 00000000000..64c323a23e5
--- /dev/null
+++ b/erpnext/change_log/v13/v13_5_0.md
@@ -0,0 +1,54 @@
+# Version 13.5.0 Release Notes
+
+### Features & Enhancements
+
+- Tax deduction against advance payments ([#25831](https://github.com/frappe/erpnext/pull/25831))
+- Cost-center wise period closing entry ([#25766](https://github.com/frappe/erpnext/pull/25766))
+- Create Quality Inspections from account and stock documents ([#25221](https://github.com/frappe/erpnext/pull/25221))
+- Item Taxes based on net rate ([#25961](https://github.com/frappe/erpnext/pull/25961))
+- Enable/disable gl entry posting for change given in pos ([#25822](https://github.com/frappe/erpnext/pull/25822))
+- Add Inactive status to Employee ([#26029](https://github.com/frappe/erpnext/pull/26029))
+- Added check box to combine items with same BOM ([#25478](https://github.com/frappe/erpnext/pull/25478))
+- Item Tax Templates for Germany ([#25858](https://github.com/frappe/erpnext/pull/25858))
+- Refactored leave balance report ([#25771](https://github.com/frappe/erpnext/pull/25771))
+- Refactored Vehicle Expenses Report ([#25727](https://github.com/frappe/erpnext/pull/25727))
+- Refactored maintenance schedule and visit document ([#25358](https://github.com/frappe/erpnext/pull/25358))
+
+### Fixes
+
+- Cannot add same item with different rates ([#25849](https://github.com/frappe/erpnext/pull/25849))
+- Show only company addresses for ITC reversal entry ([#25866](https://github.com/frappe/erpnext/pull/25866))
+- Hiding Rounding Adjustment field ([#25380](https://github.com/frappe/erpnext/pull/25380))
+- Auto tax calculations in Payment Entry ([#26055](https://github.com/frappe/erpnext/pull/26055))
+- Not able to select the item code in work order ([#25915](https://github.com/frappe/erpnext/pull/25915))
+- Cannot reset plaid link for a bank account ([#25869](https://github.com/frappe/erpnext/pull/25869))
+- Student invalid password reset link ([#25826](https://github.com/frappe/erpnext/pull/25826))
+- Multiple pos issues ([#25928](https://github.com/frappe/erpnext/pull/25928))
+- Add Product Bundles to POS ([#25860](https://github.com/frappe/erpnext/pull/25860))
+- Enable Parallel tests ([#25862](https://github.com/frappe/erpnext/pull/25862))
+- Service item check on e-Invoicing ([#25986](https://github.com/frappe/erpnext/pull/25986))
+- Choose correct Salary Structure Assignment when getting data for formula eval ([#25981](https://github.com/frappe/erpnext/pull/25981))
+- Ignore internal transfer invoices from GST Reports ([#25969](https://github.com/frappe/erpnext/pull/25969))
+- Taxable value for invoices with additional discount ([#26056](https://github.com/frappe/erpnext/pull/26056))
+- Validate negative allocated amount in Payment Entry ([#25799](https://github.com/frappe/erpnext/pull/25799))
+- Allow all System Managers to delete company transactions ([#25834](https://github.com/frappe/erpnext/pull/25834))
+- Wrong round off gl entry posted in case of purchase invoice ([#25775](https://github.com/frappe/erpnext/pull/25775))
+- Use dictionary filter instead of list ([#25874](https://github.com/frappe/erpnext/pull/25874))
+- Ageing error in PSOA ([#25855](https://github.com/frappe/erpnext/pull/25855))
+- On click of duplicate button system has not copied the difference account ([#25988](https://github.com/frappe/erpnext/pull/25988))
+- Assign Product Bundle's conversion_factor to Pack… ([#25840](https://github.com/frappe/erpnext/pull/25840))
+- Rename Loan Management workspace to Loans ([#25856](https://github.com/frappe/erpnext/pull/25856))
+- Fix stock quantity calculation when negative_stock_allowe… ([#25859](https://github.com/frappe/erpnext/pull/25859))
+- Update cost center from pos profile ([#25971](https://github.com/frappe/erpnext/pull/25971))
+- Ensure website theme is applied correctly ([#25863](https://github.com/frappe/erpnext/pull/25863))
+- Only display GST card in Accounting Workspace if it's in India ([#26000](https://github.com/frappe/erpnext/pull/26000))
+- Incorrect gstin fetched incase of branch company address ([#25841](https://github.com/frappe/erpnext/pull/25841))
+- Sort account balances by account name ([#26009](https://github.com/frappe/erpnext/pull/26009))
+- Custom conversion factor field not mapped from job card to stock entry ([#25956](https://github.com/frappe/erpnext/pull/25956))
+- Chart of accounts importer always error ([#25882](https://github.com/frappe/erpnext/pull/25882))
+- Create POS Invoice for Product Bundles ([#25847](https://github.com/frappe/erpnext/pull/25847))
+- Wrap dates in getdate for leave application ([#25899](https://github.com/frappe/erpnext/pull/25899))
+- Closing entry shows incorrect expected amount ([#25868](https://github.com/frappe/erpnext/pull/25868))
+- Add Hold status column in the Issue Summary Report ([#25828](https://github.com/frappe/erpnext/pull/25828))
+- Rendering of broken image on pos ([#25872](https://github.com/frappe/erpnext/pull/25872))
+- Timeout error in the repost item valuation ([#25854](https://github.com/frappe/erpnext/pull/25854))
\ No newline at end of file
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 913458fa355..243939b275f 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -116,6 +116,8 @@ class AccountsController(TransactionBase):
if self.doctype == 'Purchase Invoice':
self.calculate_paid_amount()
+ # apply tax withholding only if checked and applicable
+ self.set_tax_withholding()
if self.doctype in ['Purchase Invoice', 'Sales Invoice']:
pos_check_field = "is_pos" if self.doctype=="Sales Invoice" else "is_paid"
@@ -225,7 +227,7 @@ class AccountsController(TransactionBase):
def validate_date_with_fiscal_year(self):
if self.meta.get_field("fiscal_year"):
- date_field = ""
+ date_field = None
if self.meta.get_field("posting_date"):
date_field = "posting_date"
elif self.meta.get_field("transaction_date"):
@@ -368,6 +370,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)
@@ -603,8 +610,8 @@ class AccountsController(TransactionBase):
order_field = "purchase_order"
order_doctype = "Purchase Order"
- order_list = list(set([d.get(order_field)
- for d in self.get("items") if d.get(order_field)]))
+ order_list = list(set(d.get(order_field)
+ for d in self.get("items") if d.get(order_field)))
journal_entries = get_advance_journal_entries(party_type, party, party_account,
amount_field, order_doctype, order_list, include_unallocated)
@@ -628,8 +635,8 @@ class AccountsController(TransactionBase):
def validate_advance_entries(self):
order_field = "sales_order" if self.doctype == "Sales Invoice" else "purchase_order"
- order_list = list(set([d.get(order_field)
- for d in self.get("items") if d.get(order_field)]))
+ order_list = list(set(d.get(order_field)
+ for d in self.get("items") if d.get(order_field)))
if not order_list: return
@@ -695,6 +702,7 @@ class AccountsController(TransactionBase):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
if self.doctype in ["Sales Invoice", "Purchase Invoice"]:
+ self.update_allocated_advance_taxes_on_cancel()
if frappe.db.get_single_value('Accounts Settings', 'unlink_payment_on_cancellation_of_invoice'):
unlink_ref_doc_from_payment_entries(self)
@@ -702,6 +710,87 @@ class AccountsController(TransactionBase):
if frappe.db.get_single_value('Accounts Settings', 'unlink_advance_payment_on_cancelation_of_order'):
unlink_ref_doc_from_payment_entries(self)
+ def get_tax_map(self):
+ tax_map = {}
+ for tax in self.get('taxes'):
+ tax_map.setdefault(tax.account_head, 0.0)
+ tax_map[tax.account_head] += tax.tax_amount
+
+ return tax_map
+
+ def update_allocated_advance_taxes_on_cancel(self):
+ if self.get('advances'):
+ tax_accounts = [d.account_head for d in self.get('taxes')]
+ allocated_tax_map = frappe._dict(frappe.get_all('GL Entry', fields=['account', 'sum(credit - debit)'],
+ filters={'voucher_no': self.name, 'account': ('in', tax_accounts)},
+ group_by='account', as_list=1))
+
+ tax_map = self.get_tax_map()
+
+ for pe in self.get('advances'):
+ if pe.reference_type == 'Payment Entry':
+ pe = frappe.get_doc('Payment Entry', pe.reference_name)
+ for tax in pe.get('taxes'):
+ allocated_amount = tax_map.get(tax.account_head) - allocated_tax_map.get(tax.account_head)
+ if allocated_amount > tax.tax_amount:
+ allocated_amount = tax.tax_amount
+
+ if allocated_amount:
+ frappe.db.set_value('Advance Taxes and Charges', tax.name, 'allocated_amount',
+ tax.allocated_amount - allocated_amount)
+ tax_map[tax.account_head] -= allocated_amount
+ allocated_tax_map[tax.account_head] -= allocated_amount
+
+ def allocate_advance_taxes(self, gl_entries):
+ tax_map = self.get_tax_map()
+ for pe in self.get("advances"):
+ if pe.reference_type == "Payment Entry" and \
+ frappe.db.get_value('Payment Entry', pe.reference_name, 'advance_tax_account'):
+ pe = frappe.get_doc("Payment Entry", pe.reference_name)
+ for tax in pe.get("taxes"):
+ account_currency = get_account_currency(tax.account_head)
+
+ if self.doctype == "Purchase Invoice":
+ dr_or_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
+ rev_dr_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
+ else:
+ dr_or_cr = "debit" if tax.add_deduct_tax == "Add" else "credit"
+ rev_dr_cr = "credit" if tax.add_deduct_tax == "Add" else "debit"
+
+ party = self.supplier if self.doctype == "Purchase Invoice" else self.customer
+ unallocated_amount = tax.tax_amount - tax.allocated_amount
+ if tax_map.get(tax.account_head):
+ amount = tax_map.get(tax.account_head)
+ if amount < unallocated_amount:
+ unallocated_amount = amount
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": tax.account_head,
+ "against": party,
+ dr_or_cr: unallocated_amount,
+ dr_or_cr + "_in_account_currency": unallocated_amount
+ if account_currency==self.company_currency
+ else unallocated_amount,
+ "cost_center": tax.cost_center
+ }, account_currency, item=tax))
+
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": pe.advance_tax_account,
+ "against": party,
+ rev_dr_cr: unallocated_amount,
+ rev_dr_cr + "_in_account_currency": unallocated_amount
+ if account_currency==self.company_currency
+ else unallocated_amount,
+ "cost_center": tax.cost_center
+ }, account_currency, item=tax))
+
+ frappe.db.set_value("Advance Taxes and Charges", tax.name, "allocated_amount",
+ tax.allocated_amount + unallocated_amount)
+
+ tax_map[tax.account_head] -= unallocated_amount
+
def validate_multiple_billing(self, ref_dt, item_ref_dn, based_on, parentfield):
from erpnext.controllers.status_updater import get_allowance_for
item_allowance = {}
@@ -1103,7 +1192,7 @@ def validate_conversion_rate(currency, conversion_rate, conversion_rate_label, c
def validate_taxes_and_charges(tax):
- if tax.charge_type in ['Actual', 'On Net Total'] and tax.row_id:
+ if tax.charge_type in ['Actual', 'On Net Total', 'On Paid Amount'] and tax.row_id:
frappe.throw(_("Can refer row only if the charge type is 'On Previous Row Amount' or 'Previous Row Total'"))
elif tax.charge_type in ['On Previous Row Amount', 'On Previous Row Total']:
if cint(tax.idx) == 1:
@@ -1120,20 +1209,19 @@ def validate_taxes_and_charges(tax):
def validate_inclusive_tax(tax, doc):
def _on_previous_row_error(row_range):
- throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx,
- row_range))
+ throw(_("To include tax in row {0} in Item rate, taxes in rows {1} must also be included").format(tax.idx, row_range))
if cint(getattr(tax, "included_in_print_rate", None)):
if tax.charge_type == "Actual":
# inclusive tax cannot be of type Actual
- throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate").format(tax.idx))
+ throw(_("Charge of type 'Actual' in row {0} cannot be included in Item Rate or Paid Amount").format(tax.idx))
elif tax.charge_type == "On Previous Row Amount" and \
not cint(doc.get("taxes")[cint(tax.row_id) - 1].included_in_print_rate):
# referred row should also be inclusive
_on_previous_row_error(tax.row_id)
elif tax.charge_type == "On Previous Row Total" and \
not all([cint(t.included_in_print_rate) for t in doc.get("taxes")[:cint(tax.row_id) - 1]]):
- # all rows about the reffered tax should be inclusive
+ # all rows about the referred tax should be inclusive
_on_previous_row_error("1 - %d" % (tax.row_id,))
elif tax.get("category") == "Valuation":
frappe.throw(_("Valuation type charges can not be marked as Inclusive"))
@@ -1235,7 +1323,6 @@ def get_advance_payment_entries(party_type, party, party_account, order_doctype,
return list(payment_entries_against_order) + list(unallocated_payment_entries)
-
def update_invoice_status():
# Daily update the status of the invoices
@@ -1444,6 +1531,7 @@ def validate_and_delete_children(parent, data):
for d in deleted_children:
update_bin_on_delete(d, parent.doctype)
+
@frappe.whitelist()
def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, child_docname="items"):
def check_doc_permissions(doc, perm_type='create'):
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 3f2d3390c05..6a550e0e975 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -11,16 +11,17 @@ from erpnext.accounts.party import get_party_details
from erpnext.stock.get_item_details import get_conversion_factor
from erpnext.buying.utils import validate_for_items, update_last_purchase_rate
from erpnext.stock.stock_ledger import get_valuation_rate
-from erpnext.stock.doctype.stock_entry.stock_entry import get_used_alternative_items
from erpnext.stock.doctype.serial_no.serial_no import get_auto_serial_nos, auto_make_serial_nos, get_serial_nos
from frappe.contacts.doctype.address.address import get_address_display
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
-from erpnext.controllers.stock_controller import StockController
from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
from erpnext.stock.utils import get_incoming_rate
-class BuyingController(StockController):
+from erpnext.controllers.stock_controller import StockController
+from erpnext.controllers.subcontracting import Subcontracting
+
+class BuyingController(StockController, Subcontracting):
def get_feed(self):
if self.get("supplier_name"):
@@ -57,6 +58,11 @@ class BuyingController(StockController):
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
self.update_valuation_rate()
+ def onload(self):
+ super(BuyingController, self).onload()
+ self.set_onload("backflush_based_on", frappe.db.get_single_value('Buying Settings',
+ 'backflush_raw_materials_of_subcontract_based_on'))
+
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)
@@ -171,18 +177,19 @@ class BuyingController(StockController):
TODO: rename item_tax_amount to valuation_tax_amount
"""
+ stock_and_asset_items = []
stock_and_asset_items = self.get_stock_items() + self.get_asset_items()
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1
for d in self.get("items"):
- if d.item_code and d.item_code in stock_and_asset_items:
+ if (d.item_code and d.item_code in stock_and_asset_items):
stock_and_asset_items_qty += flt(d.qty)
stock_and_asset_items_amount += flt(d.base_net_amount)
last_item_idx = d.idx
- total_valuation_amount = sum([flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
- if d.category in ["Valuation", "Valuation and Total"]])
+ total_valuation_amount = sum(flt(d.base_tax_amount_after_discount_amount) for d in self.get("taxes")
+ if d.category in ["Valuation", "Valuation and Total"])
valuation_amount_adjustment = total_valuation_amount
for i, item in enumerate(self.get("items")):
@@ -255,7 +262,7 @@ class BuyingController(StockController):
supplied_items_cost = 0.0
for d in self.get("supplied_items"):
if d.reference_name == item_row_id:
- if reset_outgoing_rate and frappe.db.get_value('Item', d.rm_item_code, 'is_stock_item'):
+ if reset_outgoing_rate and frappe.get_cached_value('Item', d.rm_item_code, 'is_stock_item'):
rate = get_incoming_rate({
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
@@ -285,11 +292,13 @@ class BuyingController(StockController):
if item in self.sub_contracted_items and not item.bom:
frappe.throw(_("Please select BOM in BOM field for Item {0}").format(item.item_code))
- if self.doctype == "Purchase Order":
- for supplied_item in self.get("supplied_items"):
- if not supplied_item.reserve_warehouse:
- frappe.throw(_("Reserved Warehouse is mandatory for Item {0} in Raw Materials supplied").format(frappe.bold(supplied_item.rm_item_code)))
+ if self.doctype != "Purchase Order":
+ return
+ for row in self.get("supplied_items"):
+ if not row.reserve_warehouse:
+ msg = f"Reserved Warehouse is mandatory for the Item {frappe.bold(row.rm_item_code)} in Raw Materials supplied"
+ frappe.throw(_(msg))
else:
for item in self.get("items"):
if item.bom:
@@ -297,23 +306,7 @@ class BuyingController(StockController):
def create_raw_materials_supplied(self, raw_material_table):
if self.is_subcontracted=="Yes":
- parent_items = []
- backflush_raw_materials_based_on = frappe.db.get_single_value("Buying Settings",
- "backflush_raw_materials_of_subcontract_based_on")
- if (self.doctype == 'Purchase Receipt' and
- backflush_raw_materials_based_on != 'BOM'):
- self.update_raw_materials_supplied_based_on_stock_entries()
- else:
- for item in self.get("items"):
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- item.rm_supp_cost = 0.0
- if item.bom and item.item_code in self.sub_contracted_items:
- self.update_raw_materials_supplied_based_on_bom(item, raw_material_table)
-
- if [item.item_code, item.name] not in parent_items:
- parent_items.append([item.item_code, item.name])
-
- self.cleanup_raw_materials_supplied(parent_items, raw_material_table)
+ self.set_materials_for_subcontracted_items(raw_material_table)
elif self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
for item in self.get("items"):
@@ -322,176 +315,6 @@ class BuyingController(StockController):
if self.is_subcontracted == "No" and self.get("supplied_items"):
self.set('supplied_items', [])
- def update_raw_materials_supplied_based_on_stock_entries(self):
- self.set('supplied_items', [])
-
- purchase_orders = set([d.purchase_order for d in self.items])
-
- # qty of raw materials backflushed (for each item per purchase order)
- backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders)
-
- # qty of "finished good" item yet to be received
- qty_to_be_received_map = get_qty_to_be_received(purchase_orders)
-
- for item in self.get('items'):
- if not item.purchase_order:
- continue
-
- # reset raw_material cost
- item.rm_supp_cost = 0
-
- # qty of raw materials transferred to the supplier
- transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code)
-
- non_stock_items = get_non_stock_items(item.purchase_order, item.item_code)
-
- item_key = '{}{}'.format(item.item_code, item.purchase_order)
-
- fg_yet_to_be_received = qty_to_be_received_map.get(item_key)
-
- if not fg_yet_to_be_received:
- frappe.throw(_("Row #{0}: Item {1} is already fully received in Purchase Order {2}")
- .format(item.idx, frappe.bold(item.item_code),
- frappe.utils.get_link_to_form("Purchase Order", item.purchase_order)),
- title=_("Limit Crossed"))
-
- transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code)
- # backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code)
-
- for raw_material in transferred_raw_materials + non_stock_items:
- rm_item_key = (raw_material.rm_item_code, item.item_code, item.purchase_order)
- raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {})
-
- consumed_qty = raw_material_data.get('qty', 0)
- consumed_serial_nos = raw_material_data.get('serial_no', '')
- consumed_batch_nos = raw_material_data.get('batch_nos', '')
-
- transferred_qty = raw_material.qty
-
- rm_qty_to_be_consumed = transferred_qty - consumed_qty
-
- # backflush all remaining transferred qty in the last Purchase Receipt
- if fg_yet_to_be_received == item.qty:
- qty = rm_qty_to_be_consumed
- else:
- qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty
-
- if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'):
- qty = frappe.utils.ceil(qty)
-
- if qty > rm_qty_to_be_consumed:
- qty = rm_qty_to_be_consumed
-
- if not qty: continue
-
- if raw_material.serial_nos:
- set_serial_nos(raw_material, consumed_serial_nos, qty)
-
- if raw_material.batch_nos:
- backflushed_batch_qty_map = raw_material_data.get('consumed_batch', {})
-
- batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code,
- qty, transferred_batch_qty_map, backflushed_batch_qty_map, item.purchase_order)
-
- for batch_data in batches_qty:
- qty = batch_data['qty']
- raw_material.batch_no = batch_data['batch']
- if qty > 0:
- self.append_raw_material_to_be_backflushed(item, raw_material, qty)
- else:
- self.append_raw_material_to_be_backflushed(item, raw_material, qty)
-
- def append_raw_material_to_be_backflushed(self, fg_item_row, raw_material_data, qty):
- rm = self.append('supplied_items', {})
- rm.update(raw_material_data)
-
- if not rm.main_item_code:
- rm.main_item_code = fg_item_row.item_code
-
- rm.reference_name = fg_item_row.name
- rm.required_qty = qty
- rm.consumed_qty = qty
-
- def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
- exploded_item = 1
- if hasattr(item, 'include_exploded_items'):
- exploded_item = item.get('include_exploded_items')
-
- bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
-
- used_alternative_items = []
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order:
- used_alternative_items = get_used_alternative_items(purchase_order = item.purchase_order)
-
- raw_materials_cost = 0
- items = list(set([d.item_code for d in bom_items]))
- item_wh = frappe._dict(frappe.db.sql("""select i.item_code, id.default_warehouse
- from `tabItem` i, `tabItem Default` id
- where id.parent=i.name and id.company=%s and i.name in ({0})"""
- .format(", ".join(["%s"] * len(items))), [self.company] + items))
-
- for bom_item in bom_items:
- if self.doctype == "Purchase Order":
- reserve_warehouse = bom_item.source_warehouse or item_wh.get(bom_item.item_code)
- if frappe.db.get_value("Warehouse", reserve_warehouse, "company") != self.company:
- reserve_warehouse = None
-
- conversion_factor = item.conversion_factor
- if (self.doctype in ["Purchase Receipt", "Purchase Invoice"] and item.purchase_order and
- bom_item.item_code in used_alternative_items):
- alternative_item_data = used_alternative_items.get(bom_item.item_code)
- bom_item.item_code = alternative_item_data.item_code
- bom_item.item_name = alternative_item_data.item_name
- bom_item.stock_uom = alternative_item_data.stock_uom
- conversion_factor = alternative_item_data.conversion_factor
- bom_item.description = alternative_item_data.description
-
- # check if exists
- exists = 0
- for d in self.get(raw_material_table):
- if d.main_item_code == item.item_code and d.rm_item_code == bom_item.item_code \
- and d.reference_name == item.name:
- rm, exists = d, 1
- break
-
- if not exists:
- rm = self.append(raw_material_table, {})
-
- required_qty = flt(flt(bom_item.qty_consumed_per_unit) * (flt(item.qty) + getattr(item, 'rejected_qty', 0)) *
- flt(conversion_factor), rm.precision("required_qty"))
- rm.reference_name = item.name
- rm.bom_detail_no = bom_item.name
- rm.main_item_code = item.item_code
- rm.rm_item_code = bom_item.item_code
- rm.stock_uom = bom_item.stock_uom
- rm.required_qty = required_qty
- rm.rate = bom_item.rate
- rm.conversion_factor = conversion_factor
-
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
- rm.consumed_qty = required_qty
- rm.description = bom_item.description
- if item.batch_no and frappe.db.get_value("Item", rm.rm_item_code, "has_batch_no") and not rm.batch_no:
- rm.batch_no = item.batch_no
- elif not rm.reserve_warehouse:
- rm.reserve_warehouse = reserve_warehouse
-
- def cleanup_raw_materials_supplied(self, parent_items, raw_material_table):
- """Remove all those child items which are no longer present in main item table"""
- delete_list = []
- for d in self.get(raw_material_table):
- if [d.main_item_code, d.reference_name] not in parent_items:
- # mark for deletion from doclist
- delete_list.append(d)
-
- # delete from doclist
- if delete_list:
- rm_supplied_details = self.get(raw_material_table)
- self.set(raw_material_table, [])
- for d in rm_supplied_details:
- if d not in delete_list:
- self.append(raw_material_table, d)
-
@property
def sub_contracted_items(self):
if not hasattr(self, "_sub_contracted_items"):
@@ -683,7 +506,8 @@ class BuyingController(StockController):
self.process_fixed_asset()
self.update_fixed_asset(field)
- update_last_purchase_rate(self, is_submit = 1)
+ if self.doctype in ['Purchase Order', 'Purchase Receipt']:
+ update_last_purchase_rate(self, is_submit = 1)
def on_cancel(self):
super(BuyingController, self).on_cancel()
@@ -691,7 +515,9 @@ class BuyingController(StockController):
if self.get('is_return'):
return
- update_last_purchase_rate(self, is_submit = 0)
+ if self.doctype in ['Purchase Order', 'Purchase Receipt']:
+ update_last_purchase_rate(self, is_submit = 0)
+
if self.doctype in ['Purchase Receipt', 'Purchase Invoice']:
field = 'purchase_invoice' if self.doctype == 'Purchase Invoice' else 'purchase_receipt'
@@ -863,104 +689,6 @@ class BuyingController(StockController):
else:
validate_item_type(self, "is_purchase_item", "purchase")
-
-def get_items_from_bom(item_code, bom, exploded_item=1):
- doctype = "BOM Item" if not exploded_item else "BOM Explosion Item"
-
- bom_items = frappe.db.sql("""select t2.item_code, t2.name,
- t2.rate, t2.stock_uom, t2.source_warehouse, t2.description,
- t2.stock_qty / ifnull(t1.quantity, 1) as qty_consumed_per_unit
- from
- `tabBOM` t1, `tab{0}` t2, tabItem t3
- where
- t2.parent = t1.name and t1.item = %s
- and t1.docstatus = 1 and t1.is_active = 1 and t1.name = %s
- and t2.sourced_by_supplier = 0
- and t2.item_code = t3.name""".format(doctype),
- (item_code, bom), as_dict=1)
-
- if not bom_items:
- msgprint(_("Specified BOM {0} does not exist for Item {1}").format(bom, item_code), raise_exception=1)
-
- return bom_items
-
-def get_subcontracted_raw_materials_from_se(purchase_order, fg_item):
- common_query = """
- SELECT
- sed.item_code AS rm_item_code,
- SUM(sed.qty) AS qty,
- sed.description,
- sed.stock_uom,
- sed.subcontracted_item AS main_item_code,
- {serial_no_concat_syntax} AS serial_nos,
- {batch_no_concat_syntax} AS batch_nos
- FROM `tabStock Entry` se,`tabStock Entry Detail` sed
- WHERE
- se.name = sed.parent
- AND se.docstatus=1
- AND se.purpose='Send to Subcontractor'
- AND se.purchase_order = %s
- AND IFNULL(sed.t_warehouse, '') != ''
- AND IFNULL(sed.subcontracted_item, '') in ('', %s)
- GROUP BY sed.item_code, sed.subcontracted_item
- """
- raw_materials = frappe.db.multisql({
- 'mariadb': common_query.format(
- serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)",
- batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)"
- ),
- 'postgres': common_query.format(
- serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')",
- batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')"
- )
- }, (purchase_order, fg_item), as_dict=1)
-
- return raw_materials
-
-def get_backflushed_subcontracted_raw_materials(purchase_orders):
- purchase_receipts = frappe.get_all("Purchase Receipt Item",
- fields = ["purchase_order", "item_code", "name", "parent"],
- filters={"docstatus": 1, "purchase_order": ("in", list(purchase_orders))})
-
- distinct_purchase_receipts = {}
- for pr in purchase_receipts:
- key = (pr.purchase_order, pr.item_code, pr.parent)
- distinct_purchase_receipts.setdefault(key, []).append(pr.name)
-
- backflushed_raw_materials_map = frappe._dict()
- for args, references in iteritems(distinct_purchase_receipts):
- purchase_receipt_supplied_items = get_supplied_items(args[1], args[2], references)
-
- for data in purchase_receipt_supplied_items:
- pr_key = (data.rm_item_code, data.main_item_code, args[0])
- if pr_key not in backflushed_raw_materials_map:
- backflushed_raw_materials_map.setdefault(pr_key, frappe._dict({
- "qty": 0.0,
- "serial_no": [],
- "batch_no": [],
- "consumed_batch": {}
- }))
-
- row = backflushed_raw_materials_map.get(pr_key)
- row.qty += data.consumed_qty
-
- for field in ["serial_no", "batch_no"]:
- if data.get(field):
- row[field].append(data.get(field))
-
- if data.get("batch_no"):
- if data.get("batch_no") in row.consumed_batch:
- row.consumed_batch[data.get("batch_no")] += data.consumed_qty
- else:
- row.consumed_batch[data.get("batch_no")] = data.consumed_qty
-
- return backflushed_raw_materials_map
-
-def get_supplied_items(item_code, purchase_receipt, references):
- return frappe.get_all("Purchase Receipt Item Supplied",
- fields=["rm_item_code", "main_item_code", "consumed_qty", "serial_no", "batch_no"],
- filters={"main_item_code": item_code, "parent": purchase_receipt, "reference_name": ("in", references)})
-
def get_asset_item_details(asset_items):
asset_items_data = {}
for d in frappe.get_all('Item', fields = ["name", "auto_create_assets", "asset_naming_series"],
@@ -992,135 +720,3 @@ def validate_item_type(doc, fieldname, message):
error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master").format(items, message)
frappe.throw(error_message)
-
-def get_qty_to_be_received(purchase_orders):
- return frappe._dict(frappe.db.sql("""
- SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key,
- SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received
- FROM `tabPurchase Order Item` poi
- WHERE
- poi.`parent` in %s
- GROUP BY poi.`item_code`, poi.`parent`
- HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`)
- """, (purchase_orders)))
-
-def get_non_stock_items(purchase_order, fg_item_code):
- return frappe.db.sql("""
- SELECT
- pois.main_item_code,
- pois.rm_item_code,
- item.description,
- pois.required_qty AS qty,
- pois.rate,
- 1 as non_stock_item,
- pois.stock_uom
- FROM `tabPurchase Order Item Supplied` pois, `tabItem` item
- WHERE
- pois.`rm_item_code` = item.`name`
- AND item.is_stock_item = 0
- AND pois.`parent` = %s
- AND pois.`main_item_code` = %s
- """, (purchase_order, fg_item_code), as_dict=1)
-
-
-def set_serial_nos(raw_material, consumed_serial_nos, qty):
- serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \
- set(get_serial_nos(consumed_serial_nos))
- if serial_nos and qty <= len(serial_nos):
- raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)])
-
-def get_transferred_batch_qty_map(purchase_order, fg_item):
- # returns
- # {
- # (item_code, fg_code): {
- # batch1: 10, # qty
- # batch2: 16
- # },
- # }
- transferred_batch_qty_map = {}
- transferred_batches = frappe.db.sql("""
- SELECT
- sed.batch_no,
- SUM(sed.qty) AS qty,
- sed.item_code,
- sed.subcontracted_item
- FROM `tabStock Entry` se,`tabStock Entry Detail` sed
- WHERE
- se.name = sed.parent
- AND se.docstatus=1
- AND se.purpose='Send to Subcontractor'
- AND se.purchase_order = %s
- AND ifnull(sed.subcontracted_item, '') in ('', %s)
- AND sed.batch_no IS NOT NULL
- GROUP BY
- sed.batch_no,
- sed.item_code
- """, (purchase_order, fg_item), as_dict=1)
-
- for batch_data in transferred_batches:
- key = ((batch_data.item_code, fg_item)
- if batch_data.subcontracted_item else (batch_data.item_code, purchase_order))
- transferred_batch_qty_map.setdefault(key, OrderedDict())
- transferred_batch_qty_map[key][batch_data.batch_no] = batch_data.qty
-
- return transferred_batch_qty_map
-
-def get_backflushed_batch_qty_map(purchase_order, fg_item):
- # returns
- # {
- # (item_code, fg_code): {
- # batch1: 10, # qty
- # batch2: 16
- # },
- # }
- backflushed_batch_qty_map = {}
- backflushed_batches = frappe.db.sql("""
- SELECT
- pris.batch_no,
- SUM(pris.consumed_qty) AS qty,
- pris.rm_item_code AS item_code
- FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris
- WHERE
- pr.name = pri.parent
- AND pri.parent = pris.parent
- AND pri.purchase_order = %s
- AND pri.item_code = pris.main_item_code
- AND pr.docstatus = 1
- AND pris.main_item_code = %s
- AND pris.batch_no IS NOT NULL
- GROUP BY
- pris.rm_item_code, pris.batch_no
- """, (purchase_order, fg_item), as_dict=1)
-
- for batch_data in backflushed_batches:
- backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {})
- backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty
-
- return backflushed_batch_qty_map
-
-def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batches, po):
- # Returns available batches to be backflushed based on requirements
- transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {})
- if not transferred_batches:
- transferred_batches = transferred_batch_qty_map.get((item_code, po), {})
-
- available_batches = []
-
- for (batch, transferred_qty) in transferred_batches.items():
- backflushed_qty = backflushed_batches.get(batch, 0)
- available_qty = transferred_qty - backflushed_qty
-
- if available_qty >= required_qty:
- available_batches.append({'batch': batch, 'qty': required_qty})
- break
- elif available_qty != 0:
- available_batches.append({'batch': batch, 'qty': available_qty})
- required_qty -= available_qty
-
- for row in available_batches:
- if backflushed_batches.get(row.get('batch'), 0) > 0:
- backflushed_batches[row.get('batch')] += row.get('qty')
- else:
- backflushed_batches[row.get('batch')] = row.get('qty')
-
- return available_batches
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index b31724fa487..7bd739a6ad3 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -4,6 +4,7 @@
from __future__ import unicode_literals
import frappe
import erpnext
+import json
from frappe.desk.reportview import get_match_cond, get_filters_cond
from frappe.utils import nowdate, getdate
from collections import defaultdict
@@ -87,7 +88,7 @@ def customer_query(doctype, txt, searchfield, start, page_len, filters):
fields = get_fields("Customer", fields)
searchfields = frappe.get_meta("Customer").get_search_fields()
- searchfields = " or ".join([field + " like %(txt)s" for field in searchfields])
+ searchfields = " or ".join(field + " like %(txt)s" for field in searchfields)
return frappe.db.sql("""select {fields} from `tabCustomer`
where docstatus < 2
@@ -198,6 +199,9 @@ def tax_account_query(doctype, txt, searchfield, start, page_len, filters):
def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=False):
conditions = []
+ if isinstance(filters, str):
+ filters = json.loads(filters)
+
#Get searchfields from meta and use in Item Link field query
meta = frappe.get_meta("Item", cached=True)
searchfields = meta.get_search_fields()
@@ -216,11 +220,23 @@ 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 and isinstance(filters, dict) and 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,
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 54156f379c5..7f28289760c 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -428,7 +428,7 @@ class SellingController(StockController):
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
def get_po_nos(self, ref_doctype, ref_fieldname, po_nos):
- doc_list = list(set([d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)]))
+ doc_list = list(set(d.get(ref_fieldname) for d in self.items if d.get(ref_fieldname)))
if doc_list:
po_nos += [d.po_no for d in frappe.get_all(ref_doctype, 'po_no', filters = {'name': ('in', doc_list)}) if d.get('po_no')]
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index ed3aee5c1a1..943f7aaeb12 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -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": [
@@ -299,8 +299,8 @@ class StatusUpdater(Document):
args['name'] = self.get(args['percent_join_field_parent'])
self._update_percent_field(args, update_modified)
else:
- distinct_transactions = set([d.get(args['percent_join_field'])
- for d in self.get_all_children(args['source_dt'])])
+ distinct_transactions = set(d.get(args['percent_join_field'])
+ for d in self.get_all_children(args['source_dt']))
for name in distinct_transactions:
if name:
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 41ca404d9b8..35097b97b99 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -1,17 +1,21 @@
# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
-from __future__ import unicode_literals
-import frappe, erpnext
-from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
-from frappe import _
-import frappe.defaults
+import json
from collections import defaultdict
-from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
+
+import frappe
+import frappe.defaults
+from frappe import _
+from frappe.utils import cint, cstr, flt, get_link_to_form, getdate
+
+import erpnext
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
+from erpnext.accounts.utils import check_if_stock_and_account_balance_synced, get_fiscal_year
from erpnext.controllers.accounts_controller import AccountsController
-from erpnext.stock.stock_ledger import get_valuation_rate
from erpnext.stock import get_warehouse_account_map
+from erpnext.stock.stock_ledger import get_valuation_rate
+
class QualityInspectionRequiredError(frappe.ValidationError): pass
class QualityInspectionRejectedError(frappe.ValidationError): pass
@@ -189,7 +193,6 @@ class StockController(AccountsController):
if hasattr(self, "items"):
item_doclist = self.get("items")
elif self.doctype == "Stock Reconciliation":
- import json
item_doclist = []
data = json.loads(self.reconciliation_json)
for row in data[data.index(self.head_row)+1:]:
@@ -310,7 +313,7 @@ class StockController(AccountsController):
def get_serialized_items(self):
serialized_items = []
- item_codes = list(set([d.item_code for d in self.get("items")]))
+ item_codes = list(set(d.item_code for d in self.get("items")))
if item_codes:
serialized_items = frappe.db.sql_list("""select name from `tabItem`
where has_serial_no=1 and name in ({})""".format(", ".join(["%s"]*len(item_codes))),
@@ -319,10 +322,10 @@ class StockController(AccountsController):
return serialized_items
def validate_warehouse(self):
- from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
+ from erpnext.stock.utils import validate_disabled_warehouse, validate_warehouse_company
- warehouses = list(set([d.warehouse for d in
- self.get("items") if getattr(d, "warehouse", None)]))
+ warehouses = list(set(d.warehouse for d in
+ self.get("items") if getattr(d, "warehouse", None)))
target_warehouses = list(set([d.target_warehouse for d in
self.get("items") if getattr(d, "target_warehouse", None)]))
@@ -498,20 +501,106 @@ class StockController(AccountsController):
check_if_stock_and_account_balance_synced(self.posting_date,
self.company, self.doctype, self.name)
+@frappe.whitelist()
+def make_quality_inspections(doctype, docname, items):
+ if isinstance(items, str):
+ items = json.loads(items)
+
+ inspections = []
+ for item in items:
+ if flt(item.get("sample_size")) > flt(item.get("qty")):
+ frappe.throw(_("{item_name}'s Sample Size ({sample_size}) cannot be greater than the Accepted Quantity ({accepted_quantity})").format(
+ item_name=item.get("item_name"),
+ sample_size=item.get("sample_size"),
+ accepted_quantity=item.get("qty")
+ ))
+
+ quality_inspection = frappe.get_doc({
+ "doctype": "Quality Inspection",
+ "inspection_type": "Incoming",
+ "inspected_by": frappe.session.user,
+ "reference_type": doctype,
+ "reference_name": docname,
+ "item_code": item.get("item_code"),
+ "description": item.get("description"),
+ "sample_size": flt(item.get("sample_size")),
+ "item_serial_no": item.get("serial_no").split("\n")[0] if item.get("serial_no") else None,
+ "batch_no": item.get("batch_no")
+ }).insert()
+ quality_inspection.save()
+ inspections.append(quality_inspection.name)
+
+ return inspections
+
def is_reposting_pending():
return frappe.db.exists("Repost Item Valuation",
{'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
+def future_sle_exists(args, sl_entries=None):
+ key = (args.voucher_type, args.voucher_no)
-def future_sle_exists(args):
- sl_entries = frappe.get_all("Stock Ledger Entry",
+ if validate_future_sle_not_exists(args, key, sl_entries):
+ return False
+ elif get_cached_data(args, key):
+ return True
+
+ if not sl_entries:
+ sl_entries = get_sle_entries_against_voucher(args)
+ if not sl_entries:
+ return
+
+ or_conditions = get_conditions_to_validate_future_sle(sl_entries)
+
+ data = frappe.db.sql("""
+ select item_code, warehouse, count(name) as total_row
+ from `tabStock Ledger Entry`
+ where
+ ({})
+ and timestamp(posting_date, posting_time)
+ >= timestamp(%(posting_date)s, %(posting_time)s)
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ GROUP BY
+ item_code, warehouse
+ """.format(" or ".join(or_conditions)), args, as_dict=1)
+
+ for d in data:
+ frappe.local.future_sle[key][(d.item_code, d.warehouse)] = d.total_row
+
+ return len(data)
+
+def validate_future_sle_not_exists(args, key, sl_entries=None):
+ item_key = ''
+ if args.get('item_code'):
+ item_key = (args.get('item_code'), args.get('warehouse'))
+
+ if not sl_entries and hasattr(frappe.local, 'future_sle'):
+ if (not frappe.local.future_sle.get(key) or
+ (item_key and item_key not in frappe.local.future_sle.get(key))):
+ return True
+
+def get_cached_data(args, key):
+ if not hasattr(frappe.local, 'future_sle'):
+ frappe.local.future_sle = {}
+
+ if key not in frappe.local.future_sle:
+ frappe.local.future_sle[key] = frappe._dict({})
+
+ if args.get('item_code'):
+ item_key = (args.get('item_code'), args.get('warehouse'))
+ count = frappe.local.future_sle[key].get(item_key)
+
+ return True if (count or count == 0) else False
+ else:
+ return frappe.local.future_sle[key]
+
+def get_sle_entries_against_voucher(args):
+ return frappe.get_all("Stock Ledger Entry",
filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
fields=["item_code", "warehouse"],
order_by="creation asc")
- if not sl_entries:
- return
-
+def get_conditions_to_validate_future_sle(sl_entries):
warehouse_items_map = {}
for entry in sl_entries:
if entry.warehouse not in warehouse_items_map:
@@ -522,23 +611,10 @@ def future_sle_exists(args):
or_conditions = []
for warehouse, items in warehouse_items_map.items():
or_conditions.append(
- "warehouse = '{}' and item_code in ({})".format(
- warehouse,
- ", ".join(frappe.db.escape(item) for item in items)
- )
- )
+ f"""warehouse = {frappe.db.escape(warehouse)}
+ and item_code in ({', '.join(frappe.db.escape(item) for item in items)})""")
- return frappe.db.sql("""
- select name
- from `tabStock Ledger Entry`
- where
- ({})
- and timestamp(posting_date, posting_time)
- >= timestamp(%(posting_date)s, %(posting_time)s)
- and voucher_no != %(voucher_no)s
- and is_cancelled = 0
- limit 1
- """.format(" or ".join(or_conditions)), args)
+ return or_conditions
def create_repost_item_valuation_entry(args):
args = frappe._dict(args)
diff --git a/erpnext/controllers/subcontracting.py b/erpnext/controllers/subcontracting.py
new file mode 100644
index 00000000000..36ae1102164
--- /dev/null
+++ b/erpnext/controllers/subcontracting.py
@@ -0,0 +1,393 @@
+import frappe
+import copy
+from frappe import _
+from frappe.utils import flt, cint, get_link_to_form
+from collections import defaultdict
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+class Subcontracting():
+ def set_materials_for_subcontracted_items(self, raw_material_table):
+ if self.doctype == 'Purchase Invoice' and not self.update_stock:
+ return
+
+ self.raw_material_table = raw_material_table
+ self.__identify_change_in_item_table()
+ self.__prepare_supplied_items()
+ self.__validate_supplied_items()
+
+ def __prepare_supplied_items(self):
+ self.initialized_fields()
+ self.__get_purchase_orders()
+ self.__get_pending_qty_to_receive()
+ self.get_available_materials()
+ self.__remove_changed_rows()
+ self.__set_supplied_items()
+
+ def initialized_fields(self):
+ self.available_materials = frappe._dict()
+ self.__transferred_items = frappe._dict()
+ self.alternative_item_details = frappe._dict()
+ self.__get_backflush_based_on()
+
+ def __get_backflush_based_on(self):
+ self.backflush_based_on = frappe.db.get_single_value("Buying Settings",
+ "backflush_raw_materials_of_subcontract_based_on")
+
+ def __get_purchase_orders(self):
+ self.purchase_orders = []
+
+ if self.doctype == 'Purchase Order':
+ return
+
+ self.purchase_orders = [d.purchase_order for d in self.items if d.purchase_order]
+
+ def __identify_change_in_item_table(self):
+ self.__changed_name = []
+ self.__reference_name = []
+
+ if self.doctype == 'Purchase Order' or self.is_new():
+ self.set(self.raw_material_table, [])
+ return
+
+ item_dict = self.__get_data_before_save()
+ if not item_dict:
+ return True
+
+ for n_row in self.items:
+ self.__reference_name.append(n_row.name)
+ if (n_row.name not in item_dict) or (n_row.item_code, n_row.qty) != item_dict[n_row.name]:
+ self.__changed_name.append(n_row.name)
+
+ if item_dict.get(n_row.name):
+ del item_dict[n_row.name]
+
+ self.__changed_name.extend(item_dict.keys())
+
+ def __get_data_before_save(self):
+ item_dict = {}
+ if self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self._doc_before_save:
+ for row in self._doc_before_save.get('items'):
+ item_dict[row.name] = (row.item_code, row.qty)
+
+ return item_dict
+
+ def get_available_materials(self):
+ ''' Get the available raw materials which has been transferred to the supplier.
+ available_materials = {
+ (item_code, subcontracted_item, purchase_order): {
+ 'qty': 1, 'serial_no': [ABC], 'batch_no': {'batch1': 1}, 'data': item_details
+ }
+ }
+ '''
+ if not self.purchase_orders:
+ return
+
+ for row in self.__get_transferred_items():
+ key = (row.rm_item_code, row.main_item_code, row.purchase_order)
+
+ if key not in self.available_materials:
+ self.available_materials.setdefault(key, frappe._dict({'qty': 0, 'serial_no': [],
+ 'batch_no': defaultdict(float), 'item_details': row, 'po_details': []})
+ )
+
+ details = self.available_materials[key]
+ details.qty += row.qty
+ details.po_details.append(row.po_detail)
+
+ if row.serial_no:
+ details.serial_no.extend(get_serial_nos(row.serial_no))
+
+ if row.batch_no:
+ details.batch_no[row.batch_no] += row.qty
+
+ self.__set_alternative_item_details(row)
+
+ self.__transferred_items = copy.deepcopy(self.available_materials)
+ for doctype in ['Purchase Receipt', 'Purchase Invoice']:
+ self.__update_consumed_materials(doctype)
+
+ def __update_consumed_materials(self, doctype, return_consumed_items=False):
+ '''Deduct the consumed materials from the available materials.'''
+
+ pr_items = self.__get_received_items(doctype)
+ if not pr_items:
+ return ([], {}) if return_consumed_items else None
+
+ pr_items = {d.name: d.get(self.get('po_field') or 'purchase_order') for d in pr_items}
+ consumed_materials = self.__get_consumed_items(doctype, pr_items.keys())
+
+ if return_consumed_items:
+ return (consumed_materials, pr_items)
+
+ for row in consumed_materials:
+ key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
+ if not self.available_materials.get(key):
+ continue
+
+ self.available_materials[key]['qty'] -= row.consumed_qty
+ if row.serial_no:
+ self.available_materials[key]['serial_no'] = list(
+ set(self.available_materials[key]['serial_no']) - set(get_serial_nos(row.serial_no))
+ )
+
+ if row.batch_no:
+ self.available_materials[key]['batch_no'][row.batch_no] -= row.consumed_qty
+
+ def __get_transferred_items(self):
+ fields = ['`tabStock Entry`.`purchase_order`']
+ alias_dict = {'item_code': 'rm_item_code', 'subcontracted_item': 'main_item_code', 'basic_rate': 'rate'}
+
+ child_table_fields = ['item_code', 'item_name', 'description', 'qty', 'basic_rate', 'amount',
+ 'serial_no', 'uom', 'subcontracted_item', 'stock_uom', 'batch_no', 'conversion_factor',
+ 's_warehouse', 't_warehouse', 'item_group', 'po_detail']
+
+ if self.backflush_based_on == 'BOM':
+ child_table_fields.append('original_item')
+
+ for field in child_table_fields:
+ fields.append(f'`tabStock Entry Detail`.`{field}` As {alias_dict.get(field, field)}')
+
+ filters = [['Stock Entry', 'docstatus', '=', 1], ['Stock Entry', 'purpose', '=', 'Send to Subcontractor'],
+ ['Stock Entry', 'purchase_order', 'in', self.purchase_orders]]
+
+ return frappe.get_all('Stock Entry', fields = fields, filters=filters)
+
+ def __get_received_items(self, doctype):
+ fields = []
+ self.po_field = 'purchase_order'
+
+ for field in ['name', self.po_field, 'parent']:
+ fields.append(f'`tab{doctype} Item`.`{field}`')
+
+ filters = [[doctype, 'docstatus', '=', 1], [f'{doctype} Item', self.po_field, 'in', self.purchase_orders]]
+ if doctype == 'Purchase Invoice':
+ filters.append(['Purchase Invoice', 'update_stock', "=", 1])
+
+ return frappe.get_all(f'{doctype}', fields = fields, filters = filters)
+
+ def __get_consumed_items(self, doctype, pr_items):
+ return frappe.get_all('Purchase Receipt Item Supplied',
+ fields = ['serial_no', 'rm_item_code', 'reference_name', 'batch_no', 'consumed_qty', 'main_item_code'],
+ filters = {'docstatus': 1, 'reference_name': ('in', list(pr_items)), 'parenttype': doctype})
+
+ def __set_alternative_item_details(self, row):
+ if row.get('original_item'):
+ self.alternative_item_details[row.get('original_item')] = row
+
+ def __get_pending_qty_to_receive(self):
+ '''Get qty to be received against the purchase order.'''
+
+ self.qty_to_be_received = defaultdict(float)
+
+ if self.doctype != 'Purchase Order' and self.backflush_based_on != 'BOM' and self.purchase_orders:
+ for row in frappe.get_all('Purchase Order Item',
+ fields = ['item_code', '(qty - received_qty) as qty', 'parent', 'name'],
+ filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}):
+
+ self.qty_to_be_received[(row.item_code, row.parent)] += row.qty
+
+ def __get_materials_from_bom(self, item_code, bom_no, exploded_item=0):
+ doctype = 'BOM Item' if not exploded_item else 'BOM Explosion Item'
+ fields = [f'`tab{doctype}`.`stock_qty` / `tabBOM`.`quantity` as qty_consumed_per_unit']
+
+ alias_dict = {'item_code': 'rm_item_code', 'name': 'bom_detail_no', 'source_warehouse': 'reserve_warehouse'}
+ for field in ['item_code', 'name', 'rate', 'stock_uom',
+ 'source_warehouse', 'description', 'item_name', 'stock_uom']:
+ fields.append(f'`tab{doctype}`.`{field}` As {alias_dict.get(field, field)}')
+
+ filters = [[doctype, 'parent', '=', bom_no], [doctype, 'docstatus', '=', 1],
+ ['BOM', 'item', '=', item_code], [doctype, 'sourced_by_supplier', '=', 0]]
+
+ return frappe.get_all('BOM', fields = fields, filters=filters, order_by = f'`tab{doctype}`.`idx`') or []
+
+ def __remove_changed_rows(self):
+ if not self.__changed_name:
+ return
+
+ i=1
+ self.set(self.raw_material_table, [])
+ for d in self._doc_before_save.supplied_items:
+ if d.reference_name in self.__changed_name:
+ continue
+
+ if (d.reference_name not in self.__reference_name):
+ continue
+
+ d.idx = i
+ self.append('supplied_items', d)
+
+ i += 1
+
+ def __set_supplied_items(self):
+ self.bom_items = {}
+
+ has_supplied_items = True if self.get(self.raw_material_table) else False
+ for row in self.items:
+ if (self.doctype != 'Purchase Order' and ((self.__changed_name and row.name not in self.__changed_name)
+ or (has_supplied_items and not self.__changed_name))):
+ continue
+
+ if self.doctype == 'Purchase Order' or self.backflush_based_on == 'BOM':
+ for bom_item in self.__get_materials_from_bom(row.item_code, row.bom, row.get('include_exploded_items')):
+ qty = (flt(bom_item.qty_consumed_per_unit) * flt(row.qty) * row.conversion_factor)
+ bom_item.main_item_code = row.item_code
+ self.__update_reserve_warehouse(bom_item, row)
+ self.__set_alternative_item(bom_item)
+ self.__add_supplied_item(row, bom_item, qty)
+
+ elif self.backflush_based_on != 'BOM':
+ for key, transfer_item in self.available_materials.items():
+ if (key[1], key[2]) == (row.item_code, row.purchase_order) and transfer_item.qty > 0:
+ qty = self.__get_qty_based_on_material_transfer(row, transfer_item) or 0
+ transfer_item.qty -= qty
+ self.__add_supplied_item(row, transfer_item.get('item_details'), qty)
+
+ if self.qty_to_be_received:
+ self.qty_to_be_received[(row.item_code, row.purchase_order)] -= row.qty
+
+ def __update_reserve_warehouse(self, row, item):
+ if self.doctype == 'Purchase Order':
+ row.reserve_warehouse = (self.set_reserve_warehouse or item.warehouse)
+
+ def __get_qty_based_on_material_transfer(self, item_row, transfer_item):
+ key = (item_row.item_code, item_row.purchase_order)
+
+ if self.qty_to_be_received == item_row.qty:
+ return transfer_item.qty
+
+ if self.qty_to_be_received:
+ qty = (flt(item_row.qty) * flt(transfer_item.qty)) / flt(self.qty_to_be_received.get(key, 0))
+ transfer_item.item_details.required_qty = transfer_item.qty
+
+ if (transfer_item.serial_no or frappe.get_cached_value('UOM',
+ transfer_item.item_details.stock_uom, 'must_be_whole_number')):
+ return frappe.utils.ceil(qty)
+
+ return qty
+
+ def __set_alternative_item(self, bom_item):
+ if self.alternative_item_details.get(bom_item.rm_item_code):
+ bom_item.update(self.alternative_item_details[bom_item.rm_item_code])
+
+ def __add_supplied_item(self, item_row, bom_item, qty):
+ bom_item.conversion_factor = item_row.conversion_factor
+ rm_obj = self.append(self.raw_material_table, bom_item)
+ rm_obj.reference_name = item_row.name
+
+ if self.doctype == 'Purchase Order':
+ rm_obj.required_qty = qty
+ else:
+ rm_obj.consumed_qty = 0
+ rm_obj.purchase_order = item_row.purchase_order
+ self.__set_batch_nos(bom_item, item_row, rm_obj, qty)
+
+ def __set_batch_nos(self, bom_item, item_row, rm_obj, qty):
+ key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
+
+ if (self.available_materials.get(key) and self.available_materials[key]['batch_no']):
+ new_rm_obj = None
+ for batch_no, batch_qty in self.available_materials[key]['batch_no'].items():
+ if batch_qty >= qty:
+ self.__set_batch_no_as_per_qty(item_row, rm_obj, batch_no, qty)
+ self.available_materials[key]['batch_no'][batch_no] -= qty
+ return
+
+ elif qty > 0 and batch_qty > 0:
+ qty -= batch_qty
+ new_rm_obj = self.append(self.raw_material_table, bom_item)
+ new_rm_obj.reference_name = item_row.name
+ self.__set_batch_no_as_per_qty(item_row, new_rm_obj, batch_no, batch_qty)
+ self.available_materials[key]['batch_no'][batch_no] = 0
+
+ if abs(qty) > 0 and not new_rm_obj:
+ self.__set_consumed_qty(rm_obj, qty)
+ else:
+ self.__set_consumed_qty(rm_obj, qty, bom_item.required_qty or qty)
+ self.__set_serial_nos(item_row, rm_obj)
+
+ def __set_consumed_qty(self, rm_obj, consumed_qty, required_qty=0):
+ rm_obj.required_qty = required_qty
+ rm_obj.consumed_qty = consumed_qty
+
+ def __set_batch_no_as_per_qty(self, item_row, rm_obj, batch_no, qty):
+ rm_obj.update({'consumed_qty': qty, 'batch_no': batch_no,
+ 'required_qty': qty, 'purchase_order': item_row.purchase_order})
+
+ self.__set_serial_nos(item_row, rm_obj)
+
+ def __set_serial_nos(self, item_row, rm_obj):
+ key = (rm_obj.rm_item_code, item_row.item_code, item_row.purchase_order)
+ if (self.available_materials.get(key) and self.available_materials[key]['serial_no']):
+ used_serial_nos = self.available_materials[key]['serial_no'][0: cint(rm_obj.consumed_qty)]
+ rm_obj.serial_no = '\n'.join(used_serial_nos)
+
+ # Removed the used serial nos from the list
+ for sn in used_serial_nos:
+ self.available_materials[key]['serial_no'].remove(sn)
+
+ def set_consumed_qty_in_po(self):
+ # Update consumed qty back in the purchase order
+ if self.is_subcontracted != 'Yes':
+ return
+
+ self.__get_purchase_orders()
+ itemwise_consumed_qty = defaultdict(float)
+ for doctype in ['Purchase Receipt', 'Purchase Invoice']:
+ consumed_items, pr_items = self.__update_consumed_materials(doctype, return_consumed_items=True)
+
+ for row in consumed_items:
+ key = (row.rm_item_code, row.main_item_code, pr_items.get(row.reference_name))
+ itemwise_consumed_qty[key] += row.consumed_qty
+
+ self.__update_consumed_qty_in_po(itemwise_consumed_qty)
+
+ def __update_consumed_qty_in_po(self, itemwise_consumed_qty):
+ fields = ['main_item_code', 'rm_item_code', 'parent', 'supplied_qty', 'name']
+ filters = {'docstatus': 1, 'parent': ('in', self.purchase_orders)}
+
+ for row in frappe.get_all('Purchase Order Item Supplied', fields = fields, filters=filters, order_by='idx'):
+ key = (row.rm_item_code, row.main_item_code, row.parent)
+ consumed_qty = itemwise_consumed_qty.get(key, 0)
+
+ if row.supplied_qty < consumed_qty:
+ consumed_qty = row.supplied_qty
+
+ itemwise_consumed_qty[key] -= consumed_qty
+ frappe.db.set_value('Purchase Order Item Supplied', row.name, 'consumed_qty', consumed_qty)
+
+ def __validate_supplied_items(self):
+ if self.doctype not in ['Purchase Invoice', 'Purchase Receipt']:
+ return
+
+ for row in self.get(self.raw_material_table):
+ self.__validate_consumed_qty(row)
+
+ key = (row.rm_item_code, row.main_item_code, row.purchase_order)
+ if not self.__transferred_items or not self.__transferred_items.get(key):
+ return
+
+ self.__validate_batch_no(row, key)
+ self.__validate_serial_no(row, key)
+
+ def __validate_consumed_qty(self, row):
+ if self.backflush_based_on != 'BOM' and flt(row.consumed_qty) == 0.0:
+ msg = f'Row {row.idx}: the consumed qty cannot be zero for the item {frappe.bold(row.rm_item_code)}'
+
+ frappe.throw(_(msg),title=_('Consumed Items Qty Check'))
+
+ def __validate_batch_no(self, row, key):
+ if row.get('batch_no') and row.get('batch_no') not in self.__transferred_items.get(key).get('batch_no'):
+ link = get_link_to_form('Purchase Order', row.purchase_order)
+ msg = f'The Batch No {frappe.bold(row.get("batch_no"))} has not supplied against the Purchase Order {link}'
+ frappe.throw(_(msg), title=_("Incorrect Batch Consumed"))
+
+ def __validate_serial_no(self, row, key):
+ if row.get('serial_no'):
+ serial_nos = get_serial_nos(row.get('serial_no'))
+ incorrect_sn = set(serial_nos).difference(self.__transferred_items.get(key).get('serial_no'))
+
+ if incorrect_sn:
+ incorrect_sn = "\n".join(incorrect_sn)
+ link = get_link_to_form('Purchase Order', row.purchase_order)
+ msg = f'The Serial Nos {incorrect_sn} has not supplied against the Purchase Order {link}'
+ frappe.throw(_(msg), title=_("Incorrect Serial Number Consumed"))
\ No newline at end of file
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 9fae49482dd..56da5b71da0 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -54,6 +54,7 @@ class calculate_taxes_and_totals(object):
if item.item_code and item.get('item_tax_template'):
item_doc = frappe.get_cached_doc("Item", item.item_code)
args = {
+ 'net_rate': item.net_rate or item.rate,
'tax_category': self.doc.get('tax_category'),
'posting_date': self.doc.get('posting_date'),
'bill_date': self.doc.get('bill_date'),
@@ -77,10 +78,12 @@ class calculate_taxes_and_totals(object):
taxes = _get_item_tax_template(args, item_taxes + item_group_taxes, for_validate=True)
- if item.item_tax_template not in taxes:
- frappe.throw(_("Row {0}: Invalid Item Tax Template for item {1}").format(
- item.idx, frappe.bold(item.item_code)
- ))
+ if taxes:
+ if item.item_tax_template not in taxes:
+ item.item_tax_template = taxes[0]
+ frappe.msgprint(_("Row {0}: Item Tax template updated as per validity and rate applied").format(
+ item.idx, frappe.bold(item.item_code)
+ ))
def validate_conversion_rate(self):
# validate conversion rate
@@ -375,10 +378,10 @@ class calculate_taxes_and_totals(object):
def manipulate_grand_total_for_inclusive_tax(self):
# if fully inclusive taxes and diff
- if self.doc.get("taxes") and any([cint(t.included_in_print_rate) for t in self.doc.get("taxes")]):
+ if self.doc.get("taxes") and any(cint(t.included_in_print_rate) for t in self.doc.get("taxes")):
last_tax = self.doc.get("taxes")[-1]
- non_inclusive_tax_amount = sum([flt(d.tax_amount_after_discount_amount)
- for d in self.doc.get("taxes") if not d.included_in_print_rate])
+ non_inclusive_tax_amount = sum(flt(d.tax_amount_after_discount_amount)
+ for d in self.doc.get("taxes") if not d.included_in_print_rate)
diff = self.doc.total + non_inclusive_tax_amount \
- flt(last_tax.total, last_tax.precision("total"))
@@ -518,8 +521,8 @@ class calculate_taxes_and_totals(object):
def calculate_total_advance(self):
if self.doc.docstatus < 2:
- total_allocated_amount = sum([flt(adv.allocated_amount, adv.precision("allocated_amount"))
- for adv in self.doc.get("advances")])
+ total_allocated_amount = sum(flt(adv.allocated_amount, adv.precision("allocated_amount"))
+ for adv in self.doc.get("advances"))
self.doc.total_advance = flt(total_allocated_amount, self.doc.precision("total_advance"))
@@ -619,7 +622,7 @@ class calculate_taxes_and_totals(object):
if self.doc.doctype == "Sales Invoice" \
and self.doc.paid_amount > self.doc.grand_total and not self.doc.is_return \
- and any([d.type == "Cash" for d in self.doc.payments]):
+ and any(d.type == "Cash" for d in self.doc.payments):
grand_total = self.doc.rounded_total or self.doc.grand_total
base_grand_total = self.doc.base_rounded_total or self.doc.base_grand_total
@@ -655,7 +658,13 @@ class calculate_taxes_and_totals(object):
item.margin_type = None
item.margin_rate_or_amount = 0.0
- if item.margin_type and item.margin_rate_or_amount:
+ if not item.pricing_rules and flt(item.rate) > flt(item.price_list_rate):
+ item.margin_type = "Amount"
+ item.margin_rate_or_amount = flt(item.rate - item.price_list_rate,
+ item.precision("margin_rate_or_amount"))
+ item.rate_with_margin = item.rate
+
+ elif item.margin_type and item.margin_rate_or_amount:
margin_value = item.margin_rate_or_amount if item.margin_type == 'Amount' else flt(item.price_list_rate) * flt(item.margin_rate_or_amount) / 100
rate_with_margin = flt(item.price_list_rate) + flt(margin_value)
base_rate_with_margin = flt(rate_with_margin) * flt(self.doc.conversion_rate)
@@ -683,7 +692,6 @@ class calculate_taxes_and_totals(object):
self.calculate_paid_amount()
-
def get_itemised_tax_breakup_html(doc):
if not doc.taxes:
return
diff --git a/erpnext/controllers/tests/test_mapper.py b/erpnext/controllers/tests/test_mapper.py
index 66459fdbf8a..7a4b2d36148 100644
--- a/erpnext/controllers/tests/test_mapper.py
+++ b/erpnext/controllers/tests/test_mapper.py
@@ -26,8 +26,8 @@ class TestMapper(unittest.TestCase):
# Assert that all inserted items are present in updated sales order
src_items = item_list_1 + item_list_2 + item_list_3
- self.assertEqual(set([d for d in src_items]),
- set([d.item_code for d in updated_so.items]))
+ self.assertEqual(set(d for d in src_items),
+ set(d.item_code for d in updated_so.items))
def make_quotation(self, item_list, customer):
diff --git a/erpnext/controllers/website_list_for_contact.py b/erpnext/controllers/website_list_for_contact.py
index ecf041efd17..7c072e4fad3 100644
--- a/erpnext/controllers/website_list_for_contact.py
+++ b/erpnext/controllers/website_list_for_contact.py
@@ -113,7 +113,7 @@ def post_process(doctype, data):
doc.set_indicator()
doc.status_display = ", ".join(doc.status_display)
- doc.items_preview = ", ".join([d.item_name for d in doc.items if d.item_name])
+ doc.items_preview = ", ".join(d.item_name for d in doc.items if d.item_name)
result.append(doc)
return result
diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py
index 2009ebf7cba..df73f09c493 100644
--- a/erpnext/crm/doctype/appointment/appointment.py
+++ b/erpnext/crm/doctype/appointment/appointment.py
@@ -38,7 +38,7 @@ class Appointment(Document):
number_of_agents = frappe.db.get_single_value('Appointment Booking Settings', 'number_of_agents')
if not number_of_agents == 0:
if (number_of_appointments_in_same_slot >= number_of_agents):
- frappe.throw('Time slot is not available')
+ frappe.throw(_('Time slot is not available'))
# Link lead
if not self.party:
lead = self.find_lead_by_email()
@@ -75,10 +75,10 @@ class Appointment(Document):
subject=_('Appointment Confirmation'))
if frappe.session.user == "Guest":
frappe.msgprint(
- 'Please check your email to confirm the appointment')
+ _('Please check your email to confirm the appointment'))
else :
frappe.msgprint(
- 'Appointment was created. But no lead was found. Please check the email to confirm')
+ _('Appointment was created. But no lead was found. Please check the email to confirm'))
def on_change(self):
# Sync Calendar
@@ -91,7 +91,7 @@ class Appointment(Document):
def set_verified(self, email):
if not email == self.customer_email:
- frappe.throw('Email verification failed.')
+ frappe.throw(_('Email verification failed.'))
# Create new lead
self.create_lead_and_link()
# Remove unverified status
@@ -184,7 +184,7 @@ class Appointment(Document):
appointment_event.insert(ignore_permissions=True)
self.calendar_event = appointment_event.name
self.save(ignore_permissions=True)
-
+
def _get_verify_url(self):
verify_route = '/book_appointment/verify'
params = {
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index 2e09a76c0f6..4ba41402449 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -280,7 +280,6 @@
"read_only": 1
},
{
- "depends_on": "eval:",
"fieldname": "territory",
"fieldtype": "Link",
"label": "Territory",
@@ -431,7 +430,7 @@
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
- "modified": "2021-01-06 19:42:46.190051",
+ "modified": "2021-06-04 10:11:22.831139",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
diff --git a/erpnext/education/doctype/assessment_result/assessment_result.js b/erpnext/education/doctype/assessment_result/assessment_result.js
index 617a873b820..c35f607a99d 100644
--- a/erpnext/education/doctype/assessment_result/assessment_result.js
+++ b/erpnext/education/doctype/assessment_result/assessment_result.js
@@ -6,7 +6,8 @@ frappe.ui.form.on('Assessment Result', {
if (!frm.doc.__islocal) {
frm.trigger('setup_chart');
}
- frm.set_df_property('details', 'read_only', 1);
+
+ frm.get_field('details').grid.cannot_add_rows = true;
frm.set_query('course', function() {
return {
diff --git a/erpnext/education/doctype/course_enrollment/course_enrollment.py b/erpnext/education/doctype/course_enrollment/course_enrollment.py
index 2b3acf1b93b..ce88990a70d 100644
--- a/erpnext/education/doctype/course_enrollment/course_enrollment.py
+++ b/erpnext/education/doctype/course_enrollment/course_enrollment.py
@@ -91,4 +91,4 @@ def check_activity_exists(enrollment, content_type, content):
if activity:
return activity[0].name
else:
- return None
\ No newline at end of file
+ return None
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
index 6a0dcf460a7..0f2ea96a583 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.py
@@ -75,7 +75,7 @@ class CourseSchedulingTool(Document):
"""Validates if Course Start Date is greater than Course End Date"""
if self.course_start_date > self.course_end_date:
frappe.throw(
- "Course Start Date cannot be greater than Course End Date.")
+ _("Course Start Date cannot be greater than Course End Date."))
def delete_course_schedule(self, rescheduled, reschedule_errors):
"""Delete all course schedule within the Date range and specified filters"""
diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py
index 2dc0f634f0f..6be9e7104b2 100644
--- a/erpnext/education/doctype/student/student.py
+++ b/erpnext/education/doctype/student/student.py
@@ -74,7 +74,6 @@ class Student(Document):
student_user.flags.ignore_permissions = True
student_user.add_roles("Student")
student_user.save()
- update_password_link = student_user.reset_password()
def update_applicant_status(self):
"""Updates Student Applicant status to Admitted"""
diff --git a/erpnext/education/utils.py b/erpnext/education/utils.py
index 8f51fef8476..9db8a4a90df 100644
--- a/erpnext/education/utils.py
+++ b/erpnext/education/utils.py
@@ -219,7 +219,6 @@ def get_quiz(quiz_name, course):
try:
quiz = frappe.get_doc("Quiz", quiz_name)
questions = quiz.get_questions()
- duration = quiz.duration
except:
frappe.throw(_("Quiz {0} does not exist").format(quiz_name), frappe.DoesNotExistError)
return None
@@ -236,15 +235,17 @@ def get_quiz(quiz_name, course):
return {
'questions': questions,
'activity': None,
- 'duration':duration
+ 'is_time_bound': quiz.is_time_bound,
+ 'duration': quiz.duration
}
student = get_current_student()
course_enrollment = get_enrollment("course", course, student.name)
status, score, result, time_taken = check_quiz_completion(quiz, course_enrollment)
return {
- 'questions': questions,
+ 'questions': questions,
'activity': {'is_complete': status, 'score': score, 'result': result, 'time_taken': time_taken},
+ 'is_time_bound': quiz.is_time_bound,
'duration': quiz.duration
}
@@ -372,9 +373,9 @@ def check_content_completion(content_name, content_type, enrollment_name):
def check_quiz_completion(quiz, enrollment_name):
attempts = frappe.get_all("Quiz Activity",
filters={
- 'enrollment': enrollment_name,
+ 'enrollment': enrollment_name,
'quiz': quiz.name
- },
+ },
fields=["name", "activity_date", "score", "status", "time_taken"]
)
status = False if quiz.max_attempts == 0 else bool(len(attempts) >= quiz.max_attempts)
@@ -389,4 +390,4 @@ def check_quiz_completion(quiz, enrollment_name):
time_taken = attempts[0]['time_taken']
if result == 'Pass':
status = True
- return status, score, result, time_taken
\ No newline at end of file
+ return status, score, result, time_taken
diff --git a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py
index f713684d37c..7fd3b34fd5d 100755
--- a/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py
+++ b/erpnext/erpnext_integrations/doctype/amazon_mws_settings/amazon_mws_api.py
@@ -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))
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
index 5f990cdd034..42d4b9b2b43 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
@@ -99,5 +99,7 @@ class PlaidConnector():
response = self.client.Transactions.get(self.access_token, start_date=start_date, end_date=end_date, offset=len(transactions))
transactions.extend(response["transactions"])
return transactions
+ except ItemError as e:
+ raise e
except Exception:
frappe.log_error(frappe.get_traceback(), _("Plaid transactions sync error"))
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
index bbc2ca8846c..37bf2824505 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
@@ -16,6 +16,10 @@ frappe.ui.form.on('Plaid Settings', {
new erpnext.integrations.plaidLink(frm);
});
+ frm.add_custom_button(__('Reset Plaid Link'), () => {
+ new erpnext.integrations.plaidLink(frm);
+ });
+
frm.add_custom_button(__("Sync Now"), () => {
frappe.call({
method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index 16c65733f0b..3ef069b5e20 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -12,6 +12,7 @@ from frappe.desk.doctype.tag.tag import add_tag
from frappe.model.document import Document
from frappe.utils import add_months, formatdate, getdate, today
+from plaid.errors import ItemError
class PlaidSettings(Document):
@staticmethod
@@ -51,7 +52,7 @@ def add_institution(token, response):
})
bank.insert()
except Exception:
- frappe.throw(frappe.get_traceback())
+ frappe.log_error(frappe.get_traceback(), title=_('Plaid Link Error'))
else:
bank = frappe.get_doc("Bank", response["institution"]["name"])
bank.plaid_access_token = access_token
@@ -83,16 +84,21 @@ def add_bank_accounts(response, bank, company):
if not acc_subtype:
add_account_subtype(account["subtype"])
- if not frappe.db.exists("Bank Account", dict(integration_id=account["id"])):
+ existing_bank_account = frappe.db.exists("Bank Account", {
+ 'account_name': account["name"],
+ 'bank': bank["bank_name"]
+ })
+
+ if not existing_bank_account:
try:
new_account = frappe.get_doc({
"doctype": "Bank Account",
"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
@@ -103,10 +109,27 @@ def add_bank_accounts(response, bank, company):
except frappe.UniqueValidationError:
frappe.msgprint(_("Bank account {0} already exists and could not be created again").format(account["name"]))
except Exception:
- frappe.throw(frappe.get_traceback())
+ frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error"))
+ frappe.throw(_("There was an error creating Bank Account while linking with Plaid."),
+ title=_("Plaid Link Failed"))
else:
- result.append(frappe.db.get_value("Bank Account", dict(integration_id=account["id"]), "name"))
+ try:
+ existing_account = frappe.get_doc('Bank Account', existing_bank_account)
+ existing_account.update({
+ "bank": bank["bank_name"],
+ "account_name": account["name"],
+ "account_type": account.get("type", ""),
+ "account_subtype": account.get("subtype", ""),
+ "mask": account.get("mask", ""),
+ "integration_id": account["id"]
+ })
+ existing_account.save()
+ result.append(existing_bank_account)
+ except Exception:
+ frappe.log_error(frappe.get_traceback(), title=_("Plaid Link Error"))
+ frappe.throw(_("There was an error updating Bank Account {} while linking with Plaid.").format(
+ existing_bank_account), title=_("Plaid Link Failed"))
return result
@@ -172,9 +195,16 @@ def get_transactions(bank, bank_account=None, start_date=None, end_date=None):
account_id = None
plaid = PlaidConnector(access_token)
- transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
- return transactions
+ try:
+ transactions = plaid.get_transactions(start_date=start_date, end_date=end_date, account_id=account_id)
+ except ItemError as e:
+ if e.code == "ITEM_LOGIN_REQUIRED":
+ msg = _("There was an error syncing transactions.") + " "
+ msg += _("Please refresh or reset the Plaid linking of the Bank {}.").format(bank) + " "
+ frappe.log_error(msg, title=_("Plaid Link Refresh Required"))
+
+ return transactions or []
def new_bank_transaction(transaction):
@@ -183,11 +213,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"
diff --git a/erpnext/healthcare/doctype/patient/patient.py b/erpnext/healthcare/doctype/patient/patient.py
index 789d452c070..56a34007ffb 100644
--- a/erpnext/healthcare/doctype/patient/patient.py
+++ b/erpnext/healthcare/doctype/patient/patient.py
@@ -33,21 +33,20 @@ class Patient(Document):
self.reload() # self.notify_update()
def on_update(self):
- if self.customer:
- customer = frappe.get_doc('Customer', self.customer)
- if self.customer_group:
- customer.customer_group = self.customer_group
- if self.territory:
- customer.territory = self.territory
-
- customer.customer_name = self.patient_name
- customer.default_price_list = self.default_price_list
- customer.default_currency = self.default_currency
- customer.language = self.language
- customer.ignore_mandatory = True
- customer.save(ignore_permissions=True)
- else:
- if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
+ if frappe.db.get_single_value('Healthcare Settings', 'link_customer_to_patient'):
+ if self.customer:
+ customer = frappe.get_doc('Customer', self.customer)
+ if self.customer_group:
+ customer.customer_group = self.customer_group
+ if self.territory:
+ customer.territory = self.territory
+ customer.customer_name = self.patient_name
+ customer.default_price_list = self.default_price_list
+ customer.default_currency = self.default_currency
+ customer.language = self.language
+ customer.ignore_mandatory = True
+ customer.save(ignore_permissions=True)
+ else:
create_customer(self)
def set_full_name(self):
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index a1d37e2812d..3da606b68b8 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -228,6 +228,7 @@ standard_queries = {
doc_events = {
"*": {
+ "validate": "erpnext.support.doctype.service_level_agreement.service_level_agreement.apply",
"on_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.create_medical_record",
"on_update_after_submit": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.update_medical_record",
"on_cancel": "erpnext.healthcare.doctype.patient_history_settings.patient_history_settings.delete_medical_record"
@@ -242,6 +243,9 @@ doc_events = {
"on_update": ["erpnext.hr.doctype.employee.employee.update_user_permissions",
"erpnext.portal.utils.set_default_role"]
},
+ "Communication": {
+ "on_update": "erpnext.support.doctype.service_level_agreement.service_level_agreement.update_hold_time"
+ },
("Sales Taxes and Charges Template", 'Price List'): {
"on_update": "erpnext.shopping_cart.doctype.shopping_cart_settings.shopping_cart_settings.validate_cart_settings"
},
@@ -269,10 +273,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"],
@@ -330,8 +336,10 @@ scheduler_events = {
"erpnext.projects.doctype.project.project.hourly_reminder",
"erpnext.projects.doctype.project.project.collect_project_status",
"erpnext.hr.doctype.shift_type.shift_type.process_auto_attendance_for_all_shifts",
- "erpnext.support.doctype.issue.issue.set_service_level_agreement_variance",
"erpnext.erpnext_integrations.connectors.shopify_connection.sync_old_orders",
+ "erpnext.support.doctype.service_level_agreement.service_level_agreement.set_service_level_agreement_variance"
+ ],
+ "hourly_long": [
"erpnext.stock.doctype.repost_item_valuation.repost_item_valuation.repost_entries"
],
"daily": [
@@ -366,10 +374,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"
@@ -426,7 +432,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'
},
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index 5123d6a5a78..5442ed56c31 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -207,7 +207,7 @@
"label": "Status",
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "Active\nLeft",
+ "options": "Active\nInactive\nLeft",
"reqd": 1,
"search_index": 1
},
@@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2021-01-02 16:54:33.477439",
+ "modified": "2021-06-12 11:31:37.730760",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index ed7d5884347..bc5694226ad 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -37,7 +37,7 @@ class Employee(NestedSet):
def validate(self):
from erpnext.controllers.status_updater import validate_status
- validate_status(self.status, ["Active", "Temporary Leave", "Left"])
+ validate_status(self.status, ["Active", "Inactive", "Left"])
self.employee = self.name
self.set_employee_name()
@@ -478,7 +478,7 @@ def get_employee_emails(employee_list):
@frappe.whitelist()
def get_children(doctype, parent=None, company=None, is_root=False, is_tree=False):
- filters = [['status', '!=', 'Left']]
+ filters = [['status', '=', 'Active']]
if company and company != 'All Companies':
filters.append(['company', '=', company])
diff --git a/erpnext/hr/doctype/employee/employee_dashboard.py b/erpnext/hr/doctype/employee/employee_dashboard.py
index 02033321648..285374d9f69 100644
--- a/erpnext/hr/doctype/employee/employee_dashboard.py
+++ b/erpnext/hr/doctype/employee/employee_dashboard.py
@@ -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']
+ },
]
}
diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js
index 44837030be8..6679e318c24 100644
--- a/erpnext/hr/doctype/employee/employee_list.js
+++ b/erpnext/hr/doctype/employee/employee_list.js
@@ -3,7 +3,7 @@ frappe.listview_settings['Employee'] = {
filters: [["status","=", "Active"]],
get_indicator: function(doc) {
var indicator = [__(doc.status), frappe.utils.guess_colour(doc.status), "status,=," + doc.status];
- indicator[1] = {"Active": "green", "Temporary Leave": "red", "Left": "gray"}[doc.status];
+ indicator[1] = {"Active": "green", "Inactive": "red", "Left": "gray"}[doc.status];
return indicator;
}
};
diff --git a/erpnext/hr/doctype/employee_promotion/employee_promotion.py b/erpnext/hr/doctype/employee_promotion/employee_promotion.py
index 49949212689..83fb235f92c 100644
--- a/erpnext/hr/doctype/employee_promotion/employee_promotion.py
+++ b/erpnext/hr/doctype/employee_promotion/employee_promotion.py
@@ -11,12 +11,12 @@ from erpnext.hr.utils import update_employee
class EmployeePromotion(Document):
def validate(self):
- if frappe.get_value("Employee", self.employee, "status") == "Left":
- frappe.throw(_("Cannot promote Employee with status Left"))
+ if frappe.get_value("Employee", self.employee, "status") != "Active":
+ frappe.throw(_("Cannot promote Employee with status Left or Inactive"))
def before_submit(self):
if getdate(self.promotion_date) > getdate():
- frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date "),
+ frappe.throw(_("Employee Promotion cannot be submitted before Promotion Date"),
frappe.DocstatusTransitionError)
def on_submit(self):
diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
index 3539970a32a..6eec9fa12a9 100644
--- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
@@ -11,12 +11,12 @@ from erpnext.hr.utils import update_employee
class EmployeeTransfer(Document):
def validate(self):
- if frappe.get_value("Employee", self.employee, "status") == "Left":
- frappe.throw(_("Cannot transfer Employee with status Left"))
+ if frappe.get_value("Employee", self.employee, "status") != "Active":
+ frappe.throw(_("Cannot transfer Employee with status Left or Inactive"))
def before_submit(self):
if getdate(self.transfer_date) > getdate():
- frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date "),
+ frappe.throw(_("Employee Transfer cannot be submitted before Transfer Date"),
frappe.DocstatusTransitionError)
def on_submit(self):
diff --git a/erpnext/hr/doctype/hr_settings/hr_settings.json b/erpnext/hr/doctype/hr_settings/hr_settings.json
index 3db6c239ef0..2396a8eee92 100644
--- a/erpnext/hr/doctype/hr_settings/hr_settings.json
+++ b/erpnext/hr/doctype/hr_settings/hr_settings.json
@@ -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",
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 0bf551e178c..cee6f374fdc 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -4,8 +4,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, \
- comma_or, get_fullname, add_days, nowdate, get_datetime_str
+from frappe.utils import cint, cstr, date_diff, flt, formatdate, getdate, get_link_to_form, get_fullname, add_days, nowdate
from erpnext.hr.utils import set_employee_name, get_leave_period, share_doc_with_approver
from erpnext.hr.doctype.leave_block_list.leave_block_list import get_applicable_block_dates
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
@@ -85,7 +84,7 @@ class LeaveApplication(Document):
def validate_dates(self):
if frappe.db.get_single_value("HR Settings", "restrict_backdated_leave_application"):
- if self.from_date and self.from_date < frappe.utils.today():
+ if self.from_date and getdate(self.from_date) < getdate():
allowed_role = frappe.db.get_single_value("HR Settings", "role_allowed_to_create_backdated_leave_application")
if allowed_role not in frappe.get_roles():
frappe.throw(_("Only users with the {0} role can create backdated leave applications").format(allowed_role))
@@ -248,9 +247,9 @@ class LeaveApplication(Document):
self.throw_overlap_error(d)
def throw_overlap_error(self, d):
- msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee,
- d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \
- + """
{0}""".format(d["name"])
+ form_link = get_link_to_form("Leave Application", d.name)
+ msg = _("Employee {0} has already applied for {1} between {2} and {3} : {4}").format(self.employee,
+ d['leave_type'], formatdate(d['from_date']), formatdate(d['to_date']), form_link)
frappe.throw(msg, OverlapError)
def get_total_leaves_on_half_day(self):
@@ -356,7 +355,7 @@ class LeaveApplication(Document):
sender = dict()
sender['email'] = frappe.get_doc('User', frappe.session.user).email
- sender['full_name'] = frappe.utils.get_fullname(sender['email'])
+ sender['full_name'] = get_fullname(sender['email'])
try:
frappe.sendmail(
@@ -823,4 +822,4 @@ def get_leave_approver(employee):
leave_approver = frappe.db.get_value('Department Approver', {'parent': department,
'parentfield': 'leave_approvers', 'idx': 1}, 'approver')
- return leave_approver
\ No newline at end of file
+ return leave_approver
diff --git a/erpnext/hr/doctype/leave_application/test_leave_application.py b/erpnext/hr/doctype/leave_application/test_leave_application.py
index a4a96b813ee..2832e2fad3b 100644
--- a/erpnext/hr/doctype/leave_application/test_leave_application.py
+++ b/erpnext/hr/doctype/leave_application/test_leave_application.py
@@ -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):
diff --git a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
index e0ffa5dd41a..c1da8b47ffa 100644
--- a/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
+++ b/erpnext/hr/doctype/leave_encashment/test_leave_encashment.py
@@ -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)
diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
index e0ec4be2dce..ff7f0422e03 100644
--- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
+++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
@@ -7,7 +7,7 @@ def get_data():
'transactions': [
{
'label': _('Leaves'),
- 'items': ['Leave Allocation']
+ 'items': ['Leave Policy Assignment', 'Leave Allocation']
},
]
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
index 7c32a0dde09..0aaf4cf616e 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.js
@@ -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 += "
";
- msg += "| "+__('Leave Type')+" | "+__("Leave Allocation")+" | "+__("Leaves Granted")+" |
|---|
";
- for (let key in leave_allocations) {
- msg += "
| "+key+" | "+leave_allocations[key]["name"]+" | "+leave_allocations[key]["leaves"]+" |
";
- }
- msg += "
";
- 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) {
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
index 462b81df1d5..d7cb1c88c92 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.py
@@ -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
-
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py
new file mode 100644
index 00000000000..4bb0535cf8c
--- /dev/null
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_dashboard.py
@@ -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']
+ },
+ ]
+ }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
index 468f243885c..8fe4b8f8efa 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment_list.js
@@ -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 () {
diff --git a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
index 838e794795f..9a14e3588d0 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
+++ b/erpnext/hr/doctype/leave_policy_assignment/test_leave_policy_assignment.py
@@ -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()
diff --git a/erpnext/hr/doctype/training_event/training_event.js b/erpnext/hr/doctype/training_event/training_event.js
index 12bc920b187..b7d34b178a0 100644
--- a/erpnext/hr/doctype/training_event/training_event.js
+++ b/erpnext/hr/doctype/training_event/training_event.js
@@ -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]
+ }
+ };
+ });
+ }
+});
diff --git a/erpnext/hr/doctype/training_event_employee/training_event_employee.json b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
index e3a40649b4d..2d313e9faca 100644
--- a/erpnext/hr/doctype/training_event_employee/training_event_employee.json
+++ b/erpnext/hr/doctype/training_event_employee/training_event_employee.json
@@ -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"
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
index cf0048c1a76..ed52c4e1222 100644
--- a/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
+++ b/erpnext/hr/doctype/vehicle_log/test_vehicle_log.py
@@ -5,7 +5,7 @@ from __future__ import unicode_literals
import frappe
import unittest
-from frappe.utils import nowdate,flt, cstr,random_string
+from frappe.utils import nowdate, flt, cstr, random_string
from erpnext.hr.doctype.employee.test_employee import make_employee
from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim
@@ -18,23 +18,13 @@ class TestVehicleLog(unittest.TestCase):
self.employee_id = make_employee("testdriver@example.com", company="_Test Company")
self.license_plate = get_vehicle(self.employee_id)
-
+
def tearDown(self):
frappe.delete_doc("Vehicle", self.license_plate, force=1)
frappe.delete_doc("Employee", self.employee_id, force=1)
def test_make_vehicle_log_and_syncing_of_odometer_value(self):
- vehicle_log = frappe.get_doc({
- "doctype": "Vehicle Log",
- "license_plate": cstr(self.license_plate),
- "employee": self.employee_id,
- "date":frappe.utils.nowdate(),
- "odometer":5010,
- "fuel_qty":frappe.utils.flt(50),
- "price": frappe.utils.flt(500)
- })
- vehicle_log.save()
- vehicle_log.submit()
+ vehicle_log = make_vehicle_log(self.license_plate, self.employee_id)
#checking value of vehicle odometer value on submit.
vehicle = frappe.get_doc("Vehicle", self.license_plate)
@@ -51,19 +41,9 @@ class TestVehicleLog(unittest.TestCase):
self.assertEqual(vehicle.last_odometer, current_odometer - distance_travelled)
vehicle_log.delete()
-
+
def test_vehicle_log_fuel_expense(self):
- vehicle_log = frappe.get_doc({
- "doctype": "Vehicle Log",
- "license_plate": cstr(self.license_plate),
- "employee": self.employee_id,
- "date": frappe.utils.nowdate(),
- "odometer":5010,
- "fuel_qty":frappe.utils.flt(50),
- "price": frappe.utils.flt(500)
- })
- vehicle_log.save()
- vehicle_log.submit()
+ vehicle_log = make_vehicle_log(self.license_plate, self.employee_id)
expense_claim = make_expense_claim(vehicle_log.name)
fuel_expense = expense_claim.expenses[0].amount
@@ -73,6 +53,18 @@ class TestVehicleLog(unittest.TestCase):
frappe.delete_doc("Expense Claim", expense_claim.name)
frappe.delete_doc("Vehicle Log", vehicle_log.name)
+ def test_vehicle_log_with_service_expenses(self):
+ vehicle_log = make_vehicle_log(self.license_plate, self.employee_id, with_services=True)
+
+ expense_claim = make_expense_claim(vehicle_log.name)
+ expenses = expense_claim.expenses[0].amount
+ self.assertEqual(expenses, 27000)
+
+ vehicle_log.cancel()
+ frappe.delete_doc("Expense Claim", expense_claim.name)
+ frappe.delete_doc("Vehicle Log", vehicle_log.name)
+
+
def get_vehicle(employee_id):
license_plate=random_string(10).upper()
vehicle = frappe.get_doc({
@@ -81,15 +73,46 @@ def get_vehicle(employee_id):
"make": "Maruti",
"model": "PCM",
"employee": employee_id,
- "last_odometer":5000,
- "acquisition_date":frappe.utils.nowdate(),
+ "last_odometer": 5000,
+ "acquisition_date": nowdate(),
"location": "Mumbai",
"chassis_no": "1234ABCD",
"uom": "Litre",
- "vehicle_value":frappe.utils.flt(500000)
+ "vehicle_value": flt(500000)
})
try:
vehicle.insert()
except frappe.DuplicateEntryError:
pass
- return license_plate
\ No newline at end of file
+ return license_plate
+
+
+def make_vehicle_log(license_plate, employee_id, with_services=False):
+ vehicle_log = frappe.get_doc({
+ "doctype": "Vehicle Log",
+ "license_plate": cstr(license_plate),
+ "employee": employee_id,
+ "date": nowdate(),
+ "odometer": 5010,
+ "fuel_qty": flt(50),
+ "price": flt(500)
+ })
+
+ if with_services:
+ vehicle_log.append("service_detail", {
+ "service_item": "Oil Change",
+ "type": "Inspection",
+ "frequency": "Mileage",
+ "expense_amount": flt(500)
+ })
+ vehicle_log.append("service_detail", {
+ "service_item": "Wheels",
+ "type": "Change",
+ "frequency": "Half Yearly",
+ "expense_amount": flt(1500)
+ })
+
+ vehicle_log.save()
+ vehicle_log.submit()
+
+ return vehicle_log
\ No newline at end of file
diff --git a/erpnext/hr/doctype/vehicle_log/vehicle_log.json b/erpnext/hr/doctype/vehicle_log/vehicle_log.json
index 619e295ebe8..4ea904542d8 100644
--- a/erpnext/hr/doctype/vehicle_log/vehicle_log.json
+++ b/erpnext/hr/doctype/vehicle_log/vehicle_log.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2016-09-03 14:14:51.788550",
"doctype": "DocType",
@@ -10,7 +11,6 @@
"naming_series",
"license_plate",
"employee",
- "column_break_4",
"column_break_7",
"model",
"make",
@@ -65,10 +65,6 @@
"options": "Employee",
"reqd": 1
},
- {
- "fieldname": "column_break_4",
- "fieldtype": "Column Break"
- },
{
"fieldname": "column_break_7",
"fieldtype": "Column Break"
@@ -142,7 +138,6 @@
{
"fieldname": "service_detail",
"fieldtype": "Table",
- "label": "Service Detail",
"options": "Vehicle Service"
},
{
@@ -158,7 +153,7 @@
"fetch_from": "license_plate.last_odometer",
"fieldname": "last_odometer",
"fieldtype": "Int",
- "label": "last Odometer Value ",
+ "label": "Last Odometer Value ",
"read_only": 1,
"reqd": 1
},
@@ -168,7 +163,8 @@
}
],
"is_submittable": 1,
- "modified": "2020-03-18 16:45:45.060761",
+ "links": [],
+ "modified": "2021-05-17 00:10:21.188352",
"modified_by": "Administrator",
"module": "HR",
"name": "Vehicle Log",
diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.json b/erpnext/hr/notification/training_scheduled/training_scheduled.json
index 966b8875723..e49541e3214 100644
--- a/erpnext/hr/notification/training_scheduled/training_scheduled.json
+++ b/erpnext/hr/notification/training_scheduled/training_scheduled.json
@@ -11,16 +11,18 @@
"event": "Submit",
"idx": 0,
"is_standard": 1,
- "message": "\n\n
\n
\n \n | \n \n \n \n - {{ doc.introduction }}
\n - {{_(\"Event Location\")}}: {{ doc.location }}
\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 - {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
\n - \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
\n {% else %}\n - {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
\n - {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
\n {% endif %}\n \n {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}\n \n | \n | \n
\n
\n
",
- "modified": "2019-11-29 15:38:31.805409",
+ "message": "\n\n
\n
\n \n | \n \n \n {{ doc.introduction }}\n \n - {{_(\"Event Location\")}}: {{ doc.location }}
\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 - {{_(\"Date\")}}: {{ start.strftime(\"%A, %d %b %Y\") }}
\n - \n {{_(\"Timing\")}}: {{ start.strftime(\"%I:%M %p\") + ' to ' + end.strftime(\"%I:%M %p\") }}\n
\n {% else %}\n - {{_(\"Start Time\")}}: {{ start.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
\n - {{_(\"End Time\")}}: {{ end.strftime(\"%A, %d %b %Y at %I:%M %p\") }}\n
\n {% endif %}\n - {{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
\n {% if doc.is_mandatory %}\n - Note: This Training Event is mandatory
\n {% endif %}\n \n \n | \n | \n
\n
\n
",
+ "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 }}"
}
\ No newline at end of file
diff --git a/erpnext/hr/notification/training_scheduled/training_scheduled.md b/erpnext/hr/notification/training_scheduled/training_scheduled.md
index 374038ac202..418fd4990e3 100644
--- a/erpnext/hr/notification/training_scheduled/training_scheduled.md
+++ b/erpnext/hr/notification/training_scheduled/training_scheduled.md
@@ -35,6 +35,9 @@
{% endif %}
{{ _('Event Link') }}: {{ frappe.utils.get_link_to_form(doc.doctype, doc.name) }}
+ {% if doc.is_mandatory %}
+
Note: This Training Event is mandatory
+ {% endif %}
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js
index 05728a297b2..8bb3457190e 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.js
@@ -37,5 +37,22 @@ frappe.query_reports["Employee Leave Balance"] = {
"fieldtype": "Link",
"options": "Employee",
}
- ]
+ ],
+
+ onload: () => {
+ frappe.call({
+ type: "GET",
+ method: "erpnext.hr.utils.get_leave_period",
+ args: {
+ "from_date": frappe.defaults.get_default("year_start_date"),
+ "to_date": frappe.defaults.get_default("year_end_date"),
+ "company": frappe.defaults.get_user_default("Company")
+ },
+ freeze: true,
+ callback: (data) => {
+ frappe.query_report.set_filter_value("from_date", data.message[0].from_date);
+ frappe.query_report.set_filter_value("to_date", data.message[0].to_date);
+ }
+ });
+ }
}
diff --git a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
index 06f9160363c..4dd4570e8ca 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -6,15 +6,16 @@ import frappe
from frappe.utils import flt, add_days
from frappe import _
from erpnext.hr.doctype.leave_application.leave_application import get_leaves_for_period, get_leave_balance_on
+from itertools import groupby
def execute(filters=None):
if filters.to_date <= filters.from_date:
- frappe.throw(_('"From date" can not be greater than or equal to "To date"'))
+ frappe.throw(_('"From Date" can not be greater than or equal to "To Date"'))
columns = get_columns()
data = get_data(filters)
-
- return columns, data
+ charts = get_chart_data(data)
+ return columns, data, None, charts
def get_columns():
columns = [{
@@ -31,9 +32,10 @@ def get_columns():
'options': 'Employee'
}, {
'label': _('Employee Name'),
- 'fieldtype': 'Data',
+ 'fieldtype': 'Dynamic Link',
'fieldname': 'employee_name',
'width': 100,
+ 'options': 'employee'
}, {
'label': _('Opening Balance'),
'fieldtype': 'float',
@@ -64,8 +66,7 @@ def get_columns():
return columns
def get_data(filters):
- leave_types = frappe.db.sql_list("SELECT `name` FROM `tabLeave Type` ORDER BY `name` ASC")
-
+ leave_types = frappe.db.get_list('Leave Type', pluck='name', order_by='name')
conditions = get_conditions(filters)
user = frappe.session.user
@@ -113,12 +114,8 @@ def get_data(filters):
# not be shown on the basis of days left it create in user mind for carry_forward leave
row.closing_balance = (new_allocation + opening - (row.leaves_expired + leaves_taken))
-
-
row.indent = 1
data.append(row)
- new_leaves_allocated = 0
-
return data
@@ -129,27 +126,37 @@ def get_conditions(filters):
if filters.get('employee'):
conditions['name'] = filters.get('employee')
- if filters.get('employee'):
- conditions['name'] = filters.get('employee')
-
if filters.get('company'):
conditions['company'] = filters.get('company')
+ if filters.get('department'):
+ conditions['department'] = filters.get('department')
+
return conditions
def get_department_leave_approver_map(department=None):
- conditions=''
- if department:
- conditions="and (department_name = '%(department)s' or parent_department = '%(department)s')"%{'department': department}
# get current department and all its child
- department_list = frappe.db.sql_list(""" SELECT name FROM `tabDepartment` WHERE disabled=0 {0}""".format(conditions)) #nosec
-
+ department_list = frappe.get_list('Department',
+ filters={
+ 'disabled': 0
+ },
+ or_filters={
+ 'name': department,
+ 'parent_department': department
+ },
+ fields=['name'],
+ pluck='name'
+ )
# retrieve approvers list from current department and from its subsequent child departments
- approver_list = frappe.get_all('Department Approver', filters={
- 'parentfield': 'leave_approvers',
- 'parent': ('in', department_list)
- }, fields=['parent', 'approver'], as_list=1)
+ approver_list = frappe.get_all('Department Approver',
+ filters={
+ 'parentfield': 'leave_approvers',
+ 'parent': ('in', department_list)
+ },
+ fields=['parent', 'approver'],
+ as_list=1
+ )
approvers = {}
@@ -190,3 +197,40 @@ def get_allocated_and_expired_leaves(from_date, to_date, employee, leave_type):
new_allocation += record.leaves
return new_allocation, expired_leaves
+
+def get_chart_data(data):
+ labels = []
+ datasets = []
+ employee_data = data
+
+ if data and data[0].get('employee_name'):
+ get_dataset_for_chart(employee_data, datasets, labels)
+
+ chart = {
+ 'data': {
+ 'labels': labels,
+ 'datasets': datasets
+ },
+ 'type': 'bar',
+ 'colors': ['#456789', '#EE8888', '#7E77BF']
+ }
+
+ return chart
+
+def get_dataset_for_chart(employee_data, datasets, labels):
+ leaves = []
+ employee_data = sorted(employee_data, key=lambda k: k['employee_name'])
+
+ for key, group in groupby(employee_data, lambda x: x['employee_name']):
+ for grp in group:
+ if grp.closing_balance:
+ leaves.append(frappe._dict({
+ 'leave_type': grp.leave_type,
+ 'closing_balance': grp.closing_balance
+ }))
+
+ if leaves:
+ labels.append(key)
+
+ for leave in leaves:
+ datasets.append({'name': leave.leave_type, 'values': [leave.closing_balance]})
diff --git a/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py b/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py
new file mode 100644
index 00000000000..26e0f26392e
--- /dev/null
+++ b/erpnext/hr/report/vehicle_expenses/test_vehicle_expenses.py
@@ -0,0 +1,73 @@
+# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import unittest
+import frappe
+from frappe.utils import getdate
+from erpnext.hr.doctype.employee.test_employee import make_employee
+from erpnext.hr.doctype.vehicle_log.vehicle_log import make_expense_claim
+from erpnext.hr.doctype.vehicle_log.test_vehicle_log import get_vehicle, make_vehicle_log
+from erpnext.hr.report.vehicle_expenses.vehicle_expenses import execute
+from erpnext.accounts.utils import get_fiscal_year
+
+class TestVehicleExpenses(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ frappe.db.sql('delete from `tabVehicle Log`')
+
+ employee_id = frappe.db.sql('''select name from `tabEmployee` where name="testdriver@example.com"''')
+ self.employee_id = employee_id[0][0] if employee_id else None
+ if not self.employee_id:
+ self.employee_id = make_employee('testdriver@example.com', company='_Test Company')
+
+ self.license_plate = get_vehicle(self.employee_id)
+
+ def test_vehicle_expenses_based_on_fiscal_year(self):
+ vehicle_log = make_vehicle_log(self.license_plate, self.employee_id, with_services=True)
+ expense_claim = make_expense_claim(vehicle_log.name)
+
+ # Based on Fiscal Year
+ filters = {
+ 'filter_based_on': 'Fiscal Year',
+ 'fiscal_year': get_fiscal_year(getdate())[0]
+ }
+
+ report = execute(filters)
+
+ expected_data = [{
+ 'vehicle': self.license_plate,
+ 'make': 'Maruti',
+ 'model': 'PCM',
+ 'location': 'Mumbai',
+ 'log_name': vehicle_log.name,
+ 'odometer': 5010,
+ 'date': getdate(),
+ 'fuel_qty': 50.0,
+ 'fuel_price': 500.0,
+ 'fuel_expense': 25000.0,
+ 'service_expense': 2000.0,
+ 'employee': self.employee_id
+ }]
+
+ self.assertEqual(report[1], expected_data)
+
+ # Based on Date Range
+ fiscal_year = get_fiscal_year(getdate(), as_dict=True)
+ filters = {
+ 'filter_based_on': 'Date Range',
+ 'from_date': fiscal_year.year_start_date,
+ 'to_date': fiscal_year.year_end_date
+ }
+
+ report = execute(filters)
+ self.assertEqual(report[1], expected_data)
+
+ # clean up
+ vehicle_log.cancel()
+ frappe.delete_doc('Expense Claim', expense_claim.name)
+ frappe.delete_doc('Vehicle Log', vehicle_log.name)
+
+ def tearDown(self):
+ frappe.delete_doc('Vehicle', self.license_plate, force=1)
+ frappe.delete_doc('Employee', self.employee_id, force=1)
diff --git a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js
index b66bebbec1f..879acd18ef4 100644
--- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js
+++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.js
@@ -1,31 +1,52 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.require("assets/erpnext/js/financial_statements.js", function() {
- frappe.query_reports["Vehicle Expenses"] = {
- "filters": [
- {
- "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({
- from_date: fy.year_start_date,
- to_date: fy.year_end_date
- });
- });
- }
- }
- ]
- }
-});
+frappe.query_reports["Vehicle Expenses"] = {
+ "filters": [
+ {
+ "fieldname": "filter_based_on",
+ "label": __("Filter Based On"),
+ "fieldtype": "Select",
+ "options": ["Fiscal Year", "Date Range"],
+ "default": ["Fiscal Year"],
+ "reqd": 1
+ },
+ {
+ "fieldname": "fiscal_year",
+ "label": __("Fiscal Year"),
+ "fieldtype": "Link",
+ "options": "Fiscal Year",
+ "default": frappe.defaults.get_user_default("fiscal_year"),
+ "depends_on": "eval: doc.filter_based_on == 'Fiscal Year'",
+ "reqd": 1
+ },
+ {
+ "fieldname": "from_date",
+ "label": __("From Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "depends_on": "eval: doc.filter_based_on == 'Date Range'",
+ "default": frappe.datetime.add_months(frappe.datetime.nowdate(), -12)
+ },
+ {
+ "fieldname": "to_date",
+ "label": __("To Date"),
+ "fieldtype": "Date",
+ "reqd": 1,
+ "depends_on": "eval: doc.filter_based_on == 'Date Range'",
+ "default": frappe.datetime.nowdate()
+ },
+ {
+ "fieldname": "vehicle",
+ "label": __("Vehicle"),
+ "fieldtype": "Link",
+ "options": "Vehicle"
+ },
+ {
+ "fieldname": "employee",
+ "label": __("Employee"),
+ "fieldtype": "Link",
+ "options": "Employee"
+ }
+ ]
+};
diff --git a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.json b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.json
index 2ab0c143b8b..1a3e5a93bb2 100644
--- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.json
+++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.json
@@ -1,20 +1,23 @@
{
- "add_total_row": 0,
- "apply_user_permissions": 1,
- "creation": "2016-09-09 03:33:40.605734",
- "disabled": 0,
- "docstatus": 0,
- "doctype": "Report",
- "idx": 2,
- "is_standard": "Yes",
- "modified": "2017-02-24 19:59:18.641284",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Vehicle Expenses",
- "owner": "Administrator",
- "ref_doctype": "Vehicle",
- "report_name": "Vehicle Expenses",
- "report_type": "Script Report",
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2016-09-09 03:33:40.605734",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 2,
+ "is_standard": "Yes",
+ "modified": "2021-05-16 22:48:22.767535",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Vehicle Expenses",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Vehicle",
+ "report_name": "Vehicle Expenses",
+ "report_type": "Script Report",
"roles": [
{
"role": "Fleet Manager"
diff --git a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py
index eab58ffbbcf..d847cbb5c9b 100644
--- a/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py
+++ b/erpnext/hr/report/vehicle_expenses/vehicle_expenses.py
@@ -5,86 +5,209 @@ from __future__ import unicode_literals
import frappe
import erpnext
from frappe import _
-from frappe.utils import flt,cstr
+from frappe.utils import flt
from erpnext.accounts.report.financial_statements import get_period_list
def execute(filters=None):
- columns, data, chart = [], [], []
- if filters.get('fiscal_year'):
- company = erpnext.get_default_company()
- period_list = get_period_list(filters.get('fiscal_year'), filters.get('fiscal_year'),
- '', '', 'Fiscal Year', 'Monthly', company=company)
- columns=get_columns()
- data=get_log_data(filters)
- chart=get_chart_data(data,period_list)
+ filters = frappe._dict(filters or {})
+
+ columns = get_columns()
+ data = get_vehicle_log_data(filters)
+ chart = get_chart_data(data, filters)
+
return columns, data, None, chart
def get_columns():
- columns = [_("License") + ":Link/Vehicle:100", _('Create') + ":data:50",
- _("Model") + ":data:50", _("Location") + ":data:100",
- _("Log") + ":Link/Vehicle Log:100", _("Odometer") + ":Int:80",
- _("Date") + ":Date:100", _("Fuel Qty") + ":Float:80",
- _("Fuel Price") + ":Float:100",_("Fuel Expense") + ":Float:100",
- _("Service Expense") + ":Float:100"
+ return [
+ {
+ 'fieldname': 'vehicle',
+ 'fieldtype': 'Link',
+ 'label': _('Vehicle'),
+ 'options': 'Vehicle',
+ 'width': 150
+ },
+ {
+ 'fieldname': 'make',
+ 'fieldtype': 'Data',
+ 'label': _('Make'),
+ 'width': 100
+ },
+ {
+ 'fieldname': 'model',
+ 'fieldtype': 'Data',
+ 'label': _('Model'),
+ 'width': 80
+ },
+ {
+ 'fieldname': 'location',
+ 'fieldtype': 'Data',
+ 'label': _('Location'),
+ 'width': 100
+ },
+ {
+ 'fieldname': 'log_name',
+ 'fieldtype': 'Link',
+ 'label': _('Vehicle Log'),
+ 'options': 'Vehicle Log',
+ 'width': 100
+ },
+ {
+ 'fieldname': 'odometer',
+ 'fieldtype': 'Int',
+ 'label': _('Odometer Value'),
+ 'width': 120
+ },
+ {
+ 'fieldname': 'date',
+ 'fieldtype': 'Date',
+ 'label': _('Date'),
+ 'width': 100
+ },
+ {
+ 'fieldname': 'fuel_qty',
+ 'fieldtype': 'Float',
+ 'label': _('Fuel Qty'),
+ 'width': 80
+ },
+ {
+ 'fieldname': 'fuel_price',
+ 'fieldtype': 'Float',
+ 'label': _('Fuel Price'),
+ 'width': 100
+ },
+ {
+ 'fieldname': 'fuel_expense',
+ 'fieldtype': 'Currency',
+ 'label': _('Fuel Expense'),
+ 'width': 150
+ },
+ {
+ 'fieldname': 'service_expense',
+ 'fieldtype': 'Currency',
+ 'label': _('Service Expense'),
+ 'width': 150
+ },
+ {
+ 'fieldname': 'employee',
+ 'fieldtype': 'Link',
+ 'label': _('Employee'),
+ 'options': 'Employee',
+ 'width': 150
+ }
]
+
return columns
-def get_log_data(filters):
- fy = frappe.db.get_value('Fiscal Year', filters.get('fiscal_year'), ['year_start_date', 'year_end_date'], as_dict=True)
- data = frappe.db.sql("""select
- vhcl.license_plate as "License", vhcl.make as "Make", vhcl.model as "Model",
- vhcl.location as "Location", log.name as "Log", log.odometer as "Odometer",
- log.date as "Date", log.fuel_qty as "Fuel Qty", log.price as "Fuel Price",
- log.fuel_qty * log.price as "Fuel Expense"
- from
+
+def get_vehicle_log_data(filters):
+ start_date, end_date = get_period_dates(filters)
+ conditions, values = get_conditions(filters)
+
+ data = frappe.db.sql("""
+ SELECT
+ vhcl.license_plate as vehicle, vhcl.make, vhcl.model,
+ vhcl.location, log.name as log_name, log.odometer,
+ log.date, log.employee, log.fuel_qty,
+ log.price as fuel_price,
+ log.fuel_qty * log.price as fuel_expense
+ FROM
`tabVehicle` vhcl,`tabVehicle Log` log
- where
- vhcl.license_plate = log.license_plate and log.docstatus = 1 and date between %s and %s
- order by date""" ,(fy.year_start_date, fy.year_end_date), as_dict=1)
- dl=list(data)
- for row in dl:
- row["Service Expense"]= get_service_expense(row["Log"])
- return dl
+ WHERE
+ vhcl.license_plate = log.license_plate
+ and log.docstatus = 1
+ and date between %(start_date)s and %(end_date)s
+ {0}
+ ORDER BY date""".format(conditions), values, as_dict=1)
+
+ for row in data:
+ row['service_expense'] = get_service_expense(row.log_name)
+
+ return data
+
+
+def get_conditions(filters):
+ conditions = ''
+
+ start_date, end_date = get_period_dates(filters)
+ values = {
+ 'start_date': start_date,
+ 'end_date': end_date
+ }
+
+ if filters.employee:
+ conditions += ' and log.employee = %(employee)s'
+ values['employee'] = filters.employee
+
+ if filters.vehicle:
+ conditions += ' and vhcl.license_plate = %(vehicle)s'
+ values['vehicle'] = filters.vehicle
+
+ return conditions, values
+
+
+def get_period_dates(filters):
+ if filters.filter_based_on == 'Fiscal Year' and filters.fiscal_year:
+ fy = frappe.db.get_value('Fiscal Year', filters.fiscal_year,
+ ['year_start_date', 'year_end_date'], as_dict=True)
+ return fy.year_start_date, fy.year_end_date
+ else:
+ return filters.from_date, filters.to_date
+
def get_service_expense(logname):
- expense_amount = frappe.db.sql("""select sum(expense_amount)
- from `tabVehicle Log` log,`tabVehicle Service` ser
- where ser.parent=log.name and log.name=%s""",logname)
- return flt(expense_amount[0][0]) if expense_amount else 0
+ expense_amount = frappe.db.sql("""
+ SELECT sum(expense_amount)
+ FROM
+ `tabVehicle Log` log, `tabVehicle Service` service
+ WHERE
+ service.parent=log.name and log.name=%s
+ """, logname)
+
+ return flt(expense_amount[0][0]) if expense_amount else 0.0
+
+
+def get_chart_data(data, filters):
+ period_list = get_period_list(filters.fiscal_year, filters.fiscal_year,
+ filters.from_date, filters.to_date, filters.filter_based_on, 'Monthly')
+
+ fuel_data, service_data = [], []
-def get_chart_data(data,period_list):
- fuel_exp_data,service_exp_data,fueldata,servicedata = [],[],[],[]
- service_exp_data = []
- fueldata = []
for period in period_list:
- total_fuel_exp=0
- total_ser_exp=0
- for row in data:
- if row["Date"] <= period.to_date and row["Date"] >= period.from_date:
- total_fuel_exp+=flt(row["Fuel Expense"])
- total_ser_exp+=flt(row["Service Expense"])
- fueldata.append([period.key,total_fuel_exp])
- servicedata.append([period.key,total_ser_exp])
+ total_fuel_exp = 0
+ total_service_exp = 0
+
+ for row in data:
+ if row.date <= period.to_date and row.date >= period.from_date:
+ total_fuel_exp += flt(row.fuel_expense)
+ total_service_exp += flt(row.service_expense)
+
+ fuel_data.append([period.key, total_fuel_exp])
+ service_data.append([period.key, total_service_exp])
+
+ labels = [period.label for period in period_list]
+ fuel_exp_data= [row[1] for row in fuel_data]
+ service_exp_data= [row[1] for row in service_data]
- labels = [period.key for period in period_list]
- fuel_exp_data= [row[1] for row in fueldata]
- service_exp_data= [row[1] for row in servicedata]
datasets = []
if fuel_exp_data:
datasets.append({
- 'name': 'Fuel Expenses',
+ 'name': _('Fuel Expenses'),
'values': fuel_exp_data
})
+
if service_exp_data:
datasets.append({
- 'name': 'Service Expenses',
+ 'name': _('Service Expenses'),
'values': service_exp_data
})
+
chart = {
- "data": {
+ 'data': {
'labels': labels,
'datasets': datasets
- }
+ },
+ 'type': 'line',
+ 'fieldtype': 'Currency'
}
- chart["type"] = "line"
+
return chart
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index 2540b3db63b..ebb17343471 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -269,6 +269,7 @@ def get_total_exemption_amount(declarations):
total_exemption_amount = sum([flt(d.total_exemption_amount) for d in exemptions.values()])
return total_exemption_amount
+@frappe.whitelist()
def get_leave_period(from_date, to_date, company):
leave_period = frappe.db.sql("""
select name, from_date, to_date
@@ -500,13 +501,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):
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index 230475f2d14..ff7fbbdf49a 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -60,8 +60,9 @@ class Loan(AccountsController):
self.monthly_repayment_amount = get_monthly_repayment_amount(self.repayment_method, self.loan_amount, self.rate_of_interest, self.repayment_periods)
def check_sanctioned_amount_limit(self):
- total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
+ if sanctioned_amount_limit:
+ total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
@@ -155,9 +156,29 @@ def update_total_amount_paid(doc):
frappe.db.set_value("Loan", doc.name, "total_amount_paid", total_amount_paid)
def get_total_loan_amount(applicant_type, applicant, company):
- return frappe.db.get_value('Loan',
- {'applicant_type': applicant_type, 'company': company, 'applicant': applicant, 'docstatus': 1},
- 'sum(loan_amount)')
+ pending_amount = 0
+ loan_details = frappe.db.get_all("Loan",
+ filters={"applicant_type": applicant_type, "company": company, "applicant": applicant, "docstatus": 1,
+ "status": ("!=", "Closed")},
+ fields=["status", "total_payment", "disbursed_amount", "total_interest_payable", "total_principal_paid",
+ "written_off_amount"])
+
+ interest_amount = flt(frappe.db.get_value("Loan Interest Accrual", {"applicant_type": applicant_type,
+ "company": company, "applicant": applicant, "docstatus": 1}, "sum(interest_amount - paid_interest_amount)"))
+
+ for loan in loan_details:
+ if loan.status in ("Disbursed", "Loan Closure Requested"):
+ pending_amount += flt(loan.total_payment) - flt(loan.total_interest_payable) \
+ - flt(loan.total_principal_paid) - flt(loan.written_off_amount)
+ elif loan.status == "Partially Disbursed":
+ pending_amount += flt(loan.disbursed_amount) - flt(loan.total_interest_payable) \
+ - flt(loan.total_principal_paid) - flt(loan.written_off_amount)
+ elif loan.status == "Sanctioned":
+ pending_amount += flt(loan.total_payment)
+
+ pending_amount += interest_amount
+
+ return pending_amount
def get_sanctioned_amount_limit(applicant_type, applicant, company):
return frappe.db.get_value('Sanctioned Loan Amount',
@@ -264,7 +285,7 @@ def make_loan_write_off(loan, company=None, posting_date=None, amount=0, as_dict
pending_amount = amounts['pending_principal_amount']
if amount and (amount > pending_amount):
- frappe.throw('Write Off amount cannot be greater than pending loan amount')
+ frappe.throw(_('Write Off amount cannot be greater than pending loan amount'))
if not amount:
amount = pending_amount
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index fa4707ce2b4..314f58dd15e 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -49,7 +49,11 @@ class TestLoan(unittest.TestCase):
if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
- self.applicant2 = frappe.db.get_value("Customer", {'name': '_Test Loan Customer'}, 'name')
+ if not frappe.db.exists("Customer", "_Test Loan Customer 1"):
+ frappe.get_doc(get_customer_dict("_Test Loan Customer 1")).insert(ignore_permissions=True)
+
+ self.applicant2 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer"}, "name")
+ self.applicant3 = frappe.db.get_value("Customer", {"name": "_Test Loan Customer 1"}, "name")
create_loan(self.applicant1, "Personal Loan", 280000, "Repay Over Number of Periods", 20)
@@ -125,6 +129,38 @@ class TestLoan(unittest.TestCase):
self.assertTrue(gl_entries1)
self.assertTrue(gl_entries2)
+ def test_sanctioned_amount_limit(self):
+ # Clear loan docs before checking
+ frappe.db.sql("DELETE FROM `tabLoan` where applicant = '_Test Loan Customer 1'")
+ frappe.db.sql("DELETE FROM `tabLoan Application` where applicant = '_Test Loan Customer 1'")
+ frappe.db.sql("DELETE FROM `tabLoan Security Pledge` where applicant = '_Test Loan Customer 1'")
+
+ if not frappe.db.get_value("Sanctioned Loan Amount", filters={"applicant_type": "Customer",
+ "applicant": "_Test Loan Customer 1", "company": "_Test Company"}):
+ frappe.get_doc({
+ "doctype": "Sanctioned Loan Amount",
+ "applicant_type": "Customer",
+ "applicant": "_Test Loan Customer 1",
+ "sanctioned_amount_limit": 1500000,
+ "company": "_Test Company"
+ }).insert(ignore_permissions=True)
+
+ # Make First Loan
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+ loan = create_demand_loan(self.applicant3, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan.submit()
+
+ # Make second loan greater than the sanctioned amount
+ loan_application = create_loan_application('_Test Company', self.applicant3, 'Demand Loan', pledge,
+ do_not_save=True)
+ self.assertRaises(frappe.ValidationError, loan_application.save)
+
def test_regular_loan_repayment(self):
pledge = [{
"loan_security": "Test Security 1",
@@ -367,7 +403,7 @@ class TestLoan(unittest.TestCase):
unpledge_request.load_from_db()
self.assertEqual(unpledge_request.docstatus, 1)
- def test_santined_loan_security_unpledge(self):
+ def test_sanctioned_loan_security_unpledge(self):
pledge = [{
"loan_security": "Test Security 1",
"qty": 4000.00
@@ -858,7 +894,7 @@ def create_repayment_entry(loan, applicant, posting_date, paid_amount):
return lr
def create_loan_application(company, applicant, loan_type, proposed_pledges, repayment_method=None,
- repayment_periods=None, posting_date=None):
+ repayment_periods=None, posting_date=None, do_not_save=False):
loan_application = frappe.new_doc('Loan Application')
loan_application.applicant_type = 'Customer'
loan_application.company = company
@@ -874,6 +910,9 @@ def create_loan_application(company, applicant, loan_type, proposed_pledges, rep
for pledge in proposed_pledges:
loan_application.append('proposed_pledges', pledge)
+ if do_not_save:
+ return loan_application
+
loan_application.save()
loan_application.submit()
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
index 9c0147e55ba..d8f3577b2c3 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -46,9 +46,11 @@ class LoanApplication(Document):
frappe.throw(_("Loan Amount exceeds maximum loan amount of {0} as per proposed securities").format(self.maximum_loan_amount))
def check_sanctioned_amount_limit(self):
- total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
sanctioned_amount_limit = get_sanctioned_amount_limit(self.applicant_type, self.applicant, self.company)
+ if sanctioned_amount_limit:
+ total_loan_amount = get_total_loan_amount(self.applicant_type, self.applicant, self.company)
+
if sanctioned_amount_limit and flt(self.loan_amount) + flt(total_loan_amount) > flt(sanctioned_amount_limit):
frappe.throw(_("Sanctioned Amount limit crossed for {0} {1}").format(self.applicant_type, frappe.bold(self.applicant)))
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 3d99b1f3040..b8b1a40b5fd 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -235,70 +235,71 @@ class LoanRepayment(AccountsController):
else:
remarks = _("Repayment against Loan: ") + self.against_loan
- if self.total_penalty_paid:
+ if not loan_details.repay_from_salary:
+ if self.total_penalty_paid:
+ gle_map.append(
+ self.get_gl_dict({
+ "account": loan_details.loan_account,
+ "against": loan_details.payment_account,
+ "debit": self.total_penalty_paid,
+ "debit_in_account_currency": self.total_penalty_paid,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": _("Penalty against loan:") + self.against_loan,
+ "cost_center": self.cost_center,
+ "party_type": self.applicant_type,
+ "party": self.applicant,
+ "posting_date": getdate(self.posting_date)
+ })
+ )
+
+ gle_map.append(
+ self.get_gl_dict({
+ "account": loan_details.penalty_income_account,
+ "against": loan_details.payment_account,
+ "credit": self.total_penalty_paid,
+ "credit_in_account_currency": self.total_penalty_paid,
+ "against_voucher_type": "Loan",
+ "against_voucher": self.against_loan,
+ "remarks": _("Penalty against loan:") + self.against_loan,
+ "cost_center": self.cost_center,
+ "posting_date": getdate(self.posting_date)
+ })
+ )
+
gle_map.append(
self.get_gl_dict({
- "account": loan_details.loan_account,
- "against": loan_details.payment_account,
- "debit": self.total_penalty_paid,
- "debit_in_account_currency": self.total_penalty_paid,
+ "account": loan_details.payment_account,
+ "against": loan_details.loan_account + ", " + loan_details.interest_income_account
+ + ", " + loan_details.penalty_income_account,
+ "debit": self.amount_paid,
+ "debit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": _("Penalty against loan:") + self.against_loan,
+ "remarks": remarks,
"cost_center": self.cost_center,
- "party_type": self.applicant_type,
- "party": self.applicant,
"posting_date": getdate(self.posting_date)
})
)
gle_map.append(
self.get_gl_dict({
- "account": loan_details.penalty_income_account,
+ "account": loan_details.loan_account,
+ "party_type": loan_details.applicant_type,
+ "party": loan_details.applicant,
"against": loan_details.payment_account,
- "credit": self.total_penalty_paid,
- "credit_in_account_currency": self.total_penalty_paid,
+ "credit": self.amount_paid,
+ "credit_in_account_currency": self.amount_paid,
"against_voucher_type": "Loan",
"against_voucher": self.against_loan,
- "remarks": _("Penalty against loan:") + self.against_loan,
+ "remarks": remarks,
"cost_center": self.cost_center,
"posting_date": getdate(self.posting_date)
})
)
- gle_map.append(
- self.get_gl_dict({
- "account": loan_details.payment_account,
- "against": loan_details.loan_account + ", " + loan_details.interest_income_account
- + ", " + loan_details.penalty_income_account,
- "debit": self.amount_paid,
- "debit_in_account_currency": self.amount_paid,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": remarks,
- "cost_center": self.cost_center,
- "posting_date": getdate(self.posting_date)
- })
- )
-
- gle_map.append(
- self.get_gl_dict({
- "account": loan_details.loan_account,
- "party_type": loan_details.applicant_type,
- "party": loan_details.applicant,
- "against": loan_details.payment_account,
- "credit": self.amount_paid,
- "credit_in_account_currency": self.amount_paid,
- "against_voucher_type": "Loan",
- "against_voucher": self.against_loan,
- "remarks": remarks,
- "cost_center": self.cost_center,
- "posting_date": getdate(self.posting_date)
- })
- )
-
- if gle_map:
- make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
+ if gle_map:
+ make_gl_entries(gle_map, cancel=cancel, adv_adj=adv_adj, merge_entries=False)
def create_repayment_entry(loan, applicant, company, posting_date, loan_type,
payment_type, interest_payable, payable_principal_amount, amount_paid, penalty_amount=None):
diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json
index 18559dceef7..d0b67f7c64a 100644
--- a/erpnext/loan_management/workspace/loan_management/loan_management.json
+++ b/erpnext/loan_management/workspace/loan_management/loan_management.json
@@ -12,7 +12,7 @@
"idx": 0,
"is_default": 0,
"is_standard": 1,
- "label": "Loan Management",
+ "label": "Loans",
"links": [
{
"hidden": 0,
@@ -220,10 +220,10 @@
"type": "Link"
}
],
- "modified": "2021-02-18 17:31:53.586508",
+ "modified": "2021-05-25 17:31:53.586508",
"modified_by": "Administrator",
"module": "Loan Management",
- "name": "Loan Management",
+ "name": "Loans",
"owner": "Administrator",
"pin_to_bottom": 0,
"pin_to_top": 0,
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js
index 2371d9652cc..546a68f268b 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.js
@@ -2,40 +2,36 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.maintenance");
-
frappe.ui.form.on('Maintenance Schedule', {
- setup: function(frm) {
+ setup: function (frm) {
frm.set_query('contact_person', erpnext.queries.contact_query);
frm.set_query('customer_address', erpnext.queries.address_query);
frm.set_query('customer', erpnext.queries.customer);
-
- frm.add_fetch('item_code', 'item_name', 'item_name');
- frm.add_fetch('item_code', 'description', 'description');
},
- onload: function(frm) {
+ onload: function (frm) {
if (!frm.doc.status) {
- frm.set_value({status:'Draft'});
+ frm.set_value({ status: 'Draft' });
}
if (frm.doc.__islocal) {
- frm.set_value({transaction_date: frappe.datetime.get_today()});
+ frm.set_value({ transaction_date: frappe.datetime.get_today() });
}
},
- refresh: function(frm) {
+ refresh: function (frm) {
setTimeout(() => {
frm.toggle_display('generate_schedule', !(frm.is_new()));
frm.toggle_display('schedule', !(frm.is_new()));
- },10);
+ }, 10);
},
- customer: function(frm) {
+ customer: function (frm) {
erpnext.utils.get_party_details(frm)
},
- customer_address: function(frm) {
+ customer_address: function (frm) {
erpnext.utils.get_address_display(frm, 'customer_address', 'address_display');
},
- contact_person: function(frm) {
+ contact_person: function (frm) {
erpnext.utils.get_contact_details(frm);
},
- generate_schedule: function(frm) {
+ generate_schedule: function (frm) {
if (frm.is_new()) {
frappe.msgprint(__('Please save first'));
} else {
@@ -53,7 +49,7 @@ erpnext.maintenance.MaintenanceSchedule = class MaintenanceSchedule extends frap
if (this.frm.doc.docstatus === 0) {
this.frm.add_custom_button(__('Sales Order'),
- function() {
+ function () {
erpnext.utils.map_current_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_maintenance_schedule",
source_doctype: "Sales Order",
@@ -68,13 +64,79 @@ erpnext.maintenance.MaintenanceSchedule = class MaintenanceSchedule extends frap
});
}, __("Get Items From"));
} else if (this.frm.doc.docstatus === 1) {
- this.frm.add_custom_button(__('Create Maintenance Visit'), function() {
- frappe.model.open_mapped_doc({
- method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit",
- source_name: me.frm.doc.name,
- frm: me.frm
+ let schedules = me.frm.doc.schedules;
+ let flag = schedules.some(schedule => schedule.completion_status === "Pending");
+ if (flag) {
+ this.frm.add_custom_button(__('Maintenance Visit'), function () {
+ let options = "";
+
+ me.frm.call('get_pending_data', {data_type: "items"}).then(r => {
+ options = r.message;
+
+ let schedule_id = "";
+ let d = new frappe.ui.Dialog({
+ title: __("Enter Visit Details"),
+ fields: [{
+ fieldtype: "Select",
+ fieldname: "item_name",
+ label: __("Item Name"),
+ options: options,
+ reqd: 1,
+ onchange: function () {
+ let field = d.get_field("scheduled_date");
+ me.frm.call('get_pending_data',
+ {
+ item_name: this.value,
+ data_type: "date"
+ }).then(r => {
+ field.df.options = r.message;
+ field.refresh();
+ });
+ }
+ },
+ {
+ label: __('Scheduled Date'),
+ fieldname: 'scheduled_date',
+ fieldtype: 'Select',
+ options: "",
+ reqd: 1,
+ onchange: function () {
+ let field = d.get_field('item_name');
+ me.frm.call(
+ 'get_pending_data',
+ {
+ item_name: field.value,
+ s_date: this.value,
+ data_type: "id"
+ }).then(r => {
+ schedule_id = r.message;
+ });
+ }
+ },
+ ],
+ primary_action_label: 'Create Visit',
+ primary_action(values) {
+ frappe.call({
+ method: "erpnext.maintenance.doctype.maintenance_schedule.maintenance_schedule.make_maintenance_visit",
+ args: {
+ item_name: values.item_name,
+ s_id: schedule_id,
+ source_name: me.frm.doc.name,
+ },
+ callback: function (r) {
+ if (!r.exc) {
+ frappe.model.sync(r.message);
+ frappe.set_route("Form", r.message.doctype, r.message.name);
+ }
+ }
+ });
+ d.hide();
+ }
+ });
+ d.show();
});
- }, __('Create'));
+ }, __('Create'));
+ }
}
}
@@ -92,25 +154,10 @@ erpnext.maintenance.MaintenanceSchedule = class MaintenanceSchedule extends frap
set_no_of_visits(doc, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
-
- if (item.start_date && item.end_date && item.periodicity) {
- if(item.start_date > item.end_date) {
- frappe.msgprint(__("Row {0}:Start Date must be before End Date", [item.idx]));
- return;
- }
-
- var date_diff = frappe.datetime.get_diff(item.end_date, item.start_date) + 1;
-
- var days_in_period = {
- "Weekly": 7,
- "Monthly": 30,
- "Quarterly": 91,
- "Half Yearly": 182,
- "Yearly": 365
- }
-
- var no_of_visits = cint(date_diff / days_in_period[item.periodicity]);
- frappe.model.set_value(item.doctype, item.name, "no_of_visits", no_of_visits);
+ let me = this;
+ if (item.start_date && item.periodicity) {
+ me.frm.call('validate_end_date_visits');
+
}
}
};
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json
index 606d22f52b7..4f89a679c82 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.json
@@ -1,852 +1,264 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "naming_series:",
- "beta": 0,
- "creation": "2013-01-10 16:34:30",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 0,
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2013-01-10 16:34:30",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+ "customer_details",
+ "naming_series",
+ "customer",
+ "column_break0",
+ "status",
+ "transaction_date",
+ "items_section",
+ "items",
+ "schedule",
+ "generate_schedule",
+ "schedules",
+ "contact_info",
+ "customer_name",
+ "contact_person",
+ "contact_mobile",
+ "contact_email",
+ "contact_display",
+ "column_break_17",
+ "customer_address",
+ "address_display",
+ "territory",
+ "customer_group",
+ "company",
+ "amended_from"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "customer_details",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "oldfieldtype": "Section Break",
- "options": "fa fa-user",
- "permlevel": 0,
- "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": "customer_details",
+ "fieldtype": "Section Break",
+ "oldfieldtype": "Section Break",
+ "options": "fa fa-user"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fieldname": "naming_series",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Series",
- "length": 0,
- "no_copy": 1,
- "options": "MAT-MSH-.YYYY.-",
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "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": 1,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "no_copy": 1,
+ "options": "MAT-MSH-.YYYY.-",
+ "print_hide": 1,
+ "reqd": 1,
+ "set_only_once": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "customer",
- "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": 1,
- "label": "Customer",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "customer",
- "oldfieldtype": "Link",
- "options": "Customer",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Customer",
+ "oldfieldname": "customer",
+ "oldfieldtype": "Link",
+ "options": "Customer",
+ "print_hide": 1,
+ "search_index": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break0",
- "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,
- "oldfieldtype": "Column Break",
- "permlevel": 0,
- "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_break0",
+ "fieldtype": "Column Break",
+ "oldfieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Draft",
- "fieldname": "status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 1,
- "label": "Status",
- "length": 0,
- "no_copy": 1,
- "oldfieldname": "status",
- "oldfieldtype": "Select",
- "options": "\nDraft\nSubmitted\nCancelled",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_standard_filter": 1,
+ "label": "Status",
+ "no_copy": 1,
+ "oldfieldname": "status",
+ "oldfieldtype": "Select",
+ "options": "\nDraft\nSubmitted\nCancelled",
+ "read_only": 1,
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "transaction_date",
- "fieldtype": "Date",
- "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": "Transaction Date",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "transaction_date",
- "oldfieldtype": "Date",
- "permlevel": 0,
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "transaction_date",
+ "fieldtype": "Date",
+ "label": "Transaction Date",
+ "oldfieldname": "transaction_date",
+ "oldfieldtype": "Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "items_section",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "oldfieldtype": "Section Break",
- "options": "fa fa-shopping-cart",
- "permlevel": 0,
- "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": "items_section",
+ "fieldtype": "Section Break",
+ "oldfieldtype": "Section Break",
+ "options": "fa fa-shopping-cart"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "items",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Items",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "item_maintenance_detail",
- "oldfieldtype": "Table",
- "options": "Maintenance Schedule Item",
- "permlevel": 0,
- "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,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Items",
+ "oldfieldname": "item_maintenance_detail",
+ "oldfieldtype": "Table",
+ "options": "Maintenance Schedule Item",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "schedule",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Schedule",
- "length": 0,
- "no_copy": 0,
- "oldfieldtype": "Section Break",
- "options": "fa fa-time",
- "permlevel": 0,
- "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": "schedule",
+ "fieldtype": "Section Break",
+ "label": "Schedule",
+ "oldfieldtype": "Section Break",
+ "options": "fa fa-time"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "generate_schedule",
- "fieldtype": "Button",
- "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": "Generate Schedule",
- "length": 0,
- "no_copy": 0,
- "oldfieldtype": "Button",
- "permlevel": 0,
- "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": "generate_schedule",
+ "fieldtype": "Button",
+ "label": "Generate Schedule",
+ "oldfieldtype": "Button"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "schedules",
- "fieldtype": "Table",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Schedules",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "schedules",
- "oldfieldtype": "Table",
- "options": "Maintenance Schedule Detail",
- "permlevel": 0,
- "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": "schedules",
+ "fieldtype": "Table",
+ "label": "Schedules",
+ "oldfieldname": "schedules",
+ "oldfieldtype": "Table",
+ "options": "Maintenance Schedule Detail"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "contact_info",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Contact Info",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "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": "contact_info",
+ "fieldtype": "Section Break",
+ "label": "Contact Info"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 1,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "customer",
- "fieldname": "customer_name",
- "fieldtype": "Data",
- "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": "Customer Name",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "customer_name",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "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
- },
+ "bold": 1,
+ "depends_on": "customer",
+ "fieldname": "customer_name",
+ "fieldtype": "Data",
+ "in_global_search": 1,
+ "in_list_view": 1,
+ "label": "Customer Name",
+ "oldfieldname": "customer_name",
+ "oldfieldtype": "Data",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "customer",
- "fieldname": "contact_person",
- "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": "Contact Person",
- "length": 0,
- "no_copy": 0,
- "options": "Contact",
- "permlevel": 0,
- "print_hide": 1,
- "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
- },
+ "depends_on": "customer",
+ "fieldname": "contact_person",
+ "fieldtype": "Link",
+ "label": "Contact Person",
+ "options": "Contact",
+ "print_hide": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "customer",
- "fieldname": "contact_mobile",
- "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": "Mobile No",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "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
- },
+ "depends_on": "customer",
+ "fieldname": "contact_mobile",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Mobile No",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "customer",
- "fieldname": "contact_email",
- "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": "Contact Email",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "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
- },
+ "depends_on": "customer",
+ "fieldname": "contact_email",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Contact Email",
+ "print_hide": 1,
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "contact_display",
- "fieldtype": "Small Text",
- "hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Contact",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "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": "contact_display",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "in_global_search": 1,
+ "label": "Contact",
+ "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_17",
- "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,
- "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_17",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "customer",
- "fieldname": "customer_address",
- "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": "Customer Address",
- "length": 0,
- "no_copy": 0,
- "options": "Address",
- "permlevel": 0,
- "print_hide": 1,
- "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
- },
+ "depends_on": "customer",
+ "fieldname": "customer_address",
+ "fieldtype": "Link",
+ "label": "Customer Address",
+ "options": "Address",
+ "print_hide": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "address_display",
- "fieldtype": "Small Text",
- "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": "Address",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "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": "address_display",
+ "fieldtype": "Small Text",
+ "hidden": 1,
+ "label": "Address",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "customer",
- "description": "",
- "fieldname": "territory",
- "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": "Territory",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "territory",
- "oldfieldtype": "Link",
- "options": "Territory",
- "permlevel": 0,
- "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
- },
+ "depends_on": "customer",
+ "fieldname": "territory",
+ "fieldtype": "Link",
+ "label": "Territory",
+ "oldfieldname": "territory",
+ "oldfieldtype": "Link",
+ "options": "Territory"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "customer",
- "description": "",
- "fieldname": "customer_group",
- "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": "Customer Group",
- "length": 0,
- "no_copy": 0,
- "options": "Customer Group",
- "permlevel": 0,
- "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
- },
+ "depends_on": "customer",
+ "fieldname": "customer_group",
+ "fieldtype": "Link",
+ "label": "Customer Group",
+ "options": "Customer Group"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 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": 0,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "oldfieldname": "company",
- "oldfieldtype": "Link",
- "options": "Company",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 1,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "oldfieldname": "company",
+ "oldfieldtype": "Link",
+ "options": "Company",
+ "remember_last_selected_value": 1,
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "amended_from",
- "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": "Amended From",
- "length": 0,
- "no_copy": 1,
- "options": "Maintenance Schedule",
- "permlevel": 0,
- "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
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Maintenance Schedule",
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-calendar",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2020-09-18 17:26:09.703215",
- "modified_by": "Administrator",
- "module": "Maintenance",
- "name": "Maintenance Schedule",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-calendar",
+ "idx": 1,
+ "is_submittable": 1,
+ "links": [
+ {
+ "group": "Visits",
+ "link_doctype": "Maintenance Visit",
+ "link_fieldname": "maintenance_schedule"
+ }
+ ],
+ "modified": "2021-05-27 16:05:10.746465",
+ "modified_by": "Administrator",
+ "module": "Maintenance",
+ "name": "Maintenance Schedule",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Maintenance Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Maintenance Manager",
+ "share": 1,
+ "submit": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "search_fields": "status,customer,customer_name",
- "show_name_in_global_search": 0,
- "sort_order": "DESC",
- "timeline_field": "customer",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "search_fields": "status,customer,customer_name",
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "timeline_field": "customer"
}
\ No newline at end of file
diff --git a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
index 0aefe19c8d8..d6e42f3ee1c 100644
--- a/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
+++ b/erpnext/maintenance/doctype/maintenance_schedule/maintenance_schedule.py
@@ -4,12 +4,13 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import add_days, getdate, cint, cstr
+from frappe.utils import add_days, getdate, cint, cstr, date_diff, formatdate
from frappe import throw, _
from erpnext.utilities.transaction_base import TransactionBase, delete_events
from erpnext.stock.utils import get_valid_serial_nos
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class MaintenanceSchedule(TransactionBase):
@frappe.whitelist()
@@ -32,8 +33,40 @@ class MaintenanceSchedule(TransactionBase):
child.idx = count
count = count + 1
child.sales_person = d.sales_person
+ child.completion_status = "Pending"
+ child.item_reference = d.name
+
+ @frappe.whitelist()
+ def validate_end_date_visits(self):
+ days_in_period = {
+ "Weekly": 7,
+ "Monthly": 30,
+ "Quarterly": 91,
+ "Half Yearly": 182,
+ "Yearly": 365
+ }
+ for item in self.items:
+ if item.periodicity and item.start_date:
+ if not item.end_date:
+ if item.no_of_visits:
+ item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
+ else:
+ item.end_date = add_days(item.start_date, days_in_period[item.periodicity])
+
+ diff = date_diff(item.end_date, item.start_date) + 1
+ no_of_visits = cint(diff / days_in_period[item.periodicity])
+
+ if not item.no_of_visits or item.no_of_visits == 0:
+ item.end_date = add_days(item.start_date, days_in_period[item.periodicity])
+ diff = date_diff(item.end_date, item.start_date) + 1
+ item.no_of_visits = cint(diff / days_in_period[item.periodicity])
+
+ elif item.no_of_visits > no_of_visits:
+ item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
+
+ elif item.no_of_visits < no_of_visits:
+ item.end_date = add_days(item.start_date, item.no_of_visits * days_in_period[item.periodicity])
- self.save()
def on_submit(self):
if not self.get('schedules'):
@@ -58,9 +91,10 @@ class MaintenanceSchedule(TransactionBase):
if no_email_sp:
frappe.msgprint(
- frappe._("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format(
+ _("Setting Events to {0}, since the Employee attached to the below Sales Persons does not have a User ID{1}").format(
self.owner, "