Merge branch 'develop' of https://github.com/frappe/erpnext into other_app_acc_dims
diff --git a/.eslintrc b/.eslintrc
index d6f0f49..3b6ab74 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -92,6 +92,7 @@
"cur_page": true,
"cur_list": true,
"cur_tree": true,
+ "cur_pos": true,
"msg_dialog": true,
"is_null": true,
"in_list": true,
@@ -149,6 +150,7 @@
"it": true,
"context": true,
"before": true,
- "beforeEach": true
+ "beforeEach": true,
+ "onScan": true
}
}
diff --git a/.github/helper/documentation.py b/.github/helper/documentation.py
index b603ed5..9cc4663 100644
--- a/.github/helper/documentation.py
+++ b/.github/helper/documentation.py
@@ -21,8 +21,8 @@
if word.startswith('http') and uri_validator(word):
parsed_url = urlparse(word)
if parsed_url.netloc == "github.com":
- _, org, repo, _type, ref = parsed_url.path.split('/')
- if org == "frappe" and repo in docs_repos:
+ parts = parsed_url.path.split('/')
+ if len(parts) == 5 and parts[1] == "frappe" and parts[2] in docs_repos:
return True
diff --git a/.github/helper/translation.py b/.github/helper/translation.py
index 340f4f8..9146b3b 100644
--- a/.github/helper/translation.py
+++ b/.github/helper/translation.py
@@ -2,7 +2,7 @@
import sys
errors_encounter = 0
-pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,\s*(.)*?\s*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
+pattern = re.compile(r"_\(([\"']{,3})(?P<message>((?!\1).)*)\1(\s*,\s*context\s*=\s*([\"'])(?P<py_context>((?!\5).)*)\5)*(\s*,(\s*?.*?\n*?)*(,\s*([\"'])(?P<js_context>((?!\11).)*)\11)*)*\)")
words_pattern = re.compile(r"_{1,2}\([\"'`]{1,3}.*?[a-zA-Z]")
start_pattern = re.compile(r"_{1,2}\([f\"'`]{1,3}")
f_string_pattern = re.compile(r"_\(f[\"']")
@@ -28,7 +28,7 @@
has_f_string = f_string_pattern.search(line)
if has_f_string:
errors_encounter += 1
- print(f'\nF-strings are not supported for translations at line number {line_number + 1}\n{line.strip()[:100]}')
+ print(f'\nF-strings are not supported for translations at line number {line_number}\n{line.strip()[:100]}')
continue
else:
continue
@@ -36,7 +36,7 @@
match = pattern.search(line)
error_found = False
- if not match and line.endswith(',\n'):
+ if not match and line.endswith((',\n', '[\n')):
# concat remaining text to validate multiline pattern
line = "".join(file_lines[line_number - 1:])
line = line[start_matches.start() + 1:]
@@ -44,11 +44,11 @@
if not match:
error_found = True
- print(f'\nTranslation syntax error at line number {line_number + 1}\n{line.strip()[:100]}')
+ print(f'\nTranslation syntax error at line number {line_number}\n{line.strip()[:100]}')
if not error_found and not words_pattern.search(line):
error_found = True
- print(f'\nTranslation is useless because it has no words at line number {line_number + 1}\n{line.strip()[:100]}')
+ print(f'\nTranslation is useless because it has no words at line number {line_number}\n{line.strip()[:100]}')
if error_found:
errors_encounter += 1
diff --git a/erpnext/.stylelintrc b/erpnext/.stylelintrc
new file mode 100644
index 0000000..1e05d1f
--- /dev/null
+++ b/erpnext/.stylelintrc
@@ -0,0 +1,9 @@
+{
+ "extends": ["stylelint-config-recommended"],
+ "plugins": ["stylelint-scss"],
+ "rules": {
+ "at-rule-no-unknown": null,
+ "scss/at-rule-no-unknown": true,
+ "no-descending-specificity": null
+ }
+}
\ No newline at end of file
diff --git a/erpnext/__init__.py b/erpnext/__init__.py
index 38d8a62..199a183 100644
--- a/erpnext/__init__.py
+++ b/erpnext/__init__.py
@@ -109,7 +109,7 @@
'''
if company or frappe.flags.company:
return frappe.get_cached_value('Company',
- company or frappe.flags.company, 'country')
+ company or frappe.flags.company, 'country')
elif frappe.flags.country:
return frappe.flags.country
else:
@@ -132,16 +132,10 @@
return caller
-def get_last_membership():
+def get_last_membership(member):
'''Returns last membership if exists'''
last_membership = frappe.get_all('Membership', 'name,to_date,membership_type',
- dict(member=frappe.session.user, paid=1), order_by='to_date desc', limit=1)
+ dict(member=member, paid=1), order_by='to_date desc', limit=1)
- return last_membership and last_membership[0]
-
-def is_member():
- '''Returns true if the user is still a member'''
- last_membership = get_last_membership()
- if last_membership and getdate(last_membership.to_date) > getdate():
- return True
- return False
+ if last_membership:
+ return last_membership[0]
diff --git a/erpnext/accounts/desk_page/accounting/accounting.json b/erpnext/accounts/desk_page/accounting/accounting.json
deleted file mode 100644
index a18dbff..0000000
--- a/erpnext/accounts/desk_page/accounting/accounting.json
+++ /dev/null
@@ -1,161 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Accounting Masters",
- "links": "[\n {\n \"description\": \"Company (not Customer or Supplier) master.\",\n \"label\": \"Company\",\n \"name\": \"Company\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tree of financial accounts.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Chart of Accounts\",\n \"name\": \"Account\",\n \"onboard\": 1,\n \"route\": \"#Tree/Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Accounts Settings\",\n \"name\": \"Accounts Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Financial / accounting year.\",\n \"label\": \"Fiscal Year\",\n \"name\": \"Fiscal Year\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Accounting Dimension\",\n \"name\": \"Accounting Dimension\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Finance Book\",\n \"name\": \"Finance Book\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Accounting Period\",\n \"name\": \"Accounting Period\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Payment Terms based on conditions\",\n \"label\": \"Payment Term\",\n \"name\": \"Payment Term\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "General Ledger",
- "links": "[\n {\n \"description\": \"Accounting journal entries.\",\n \"label\": \"Journal Entry\",\n \"name\": \"Journal Entry\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Make journal entries from a template.\",\n \"label\": \"Journal Entry Template\",\n \"name\": \"Journal Entry Template\",\n \"type\": \"doctype\"\n },\n \n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"General Ledger\",\n \"name\": \"General Ledger\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Customer Ledger Summary\",\n \"name\": \"Customer Ledger Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Supplier Ledger Summary\",\n \"name\": \"Supplier Ledger Summary\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Accounts Receivable",
- "links": "[\n {\n \"description\": \"Bills raised to Customers.\",\n \"label\": \"Sales Invoice\",\n \"name\": \"Sales Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customer database.\",\n \"label\": \"Customer\",\n \"name\": \"Customer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Bank/Cash transactions against party or for internal transfer\",\n \"label\": \"Payment Entry\",\n \"name\": \"Payment Entry\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Payment Request\",\n \"label\": \"Payment Request\",\n \"name\": \"Payment Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Receivable\",\n \"name\": \"Accounts Receivable\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Receivable Summary\",\n \"name\": \"Accounts Receivable Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Register\",\n \"name\": \"Sales Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales Register\",\n \"name\": \"Item-wise Sales Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Delivered Items To Be Billed\",\n \"name\": \"Delivered Items To Be Billed\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Accounts Payable",
- "links": "[\n {\n \"description\": \"Bills raised by Suppliers.\",\n \"label\": \"Purchase Invoice\",\n \"name\": \"Purchase Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Supplier database.\",\n \"label\": \"Supplier\",\n \"name\": \"Supplier\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Bank/Cash transactions against party or for internal transfer\",\n \"label\": \"Payment Entry\",\n \"name\": \"Payment Entry\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Payable\",\n \"name\": \"Accounts Payable\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Accounts Payable Summary\",\n \"name\": \"Accounts Payable Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Purchase Register\",\n \"name\": \"Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Purchase Register\",\n \"name\": \"Item-wise Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Received Items To Be Billed\",\n \"name\": \"Received Items To Be Billed\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Trial Balance for Party\",\n \"name\": \"Trial Balance for Party\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Journal Entry\"\n ],\n \"doctype\": \"Journal Entry\",\n \"is_query_report\": true,\n \"label\": \"Payment Period Based On Invoice Date\",\n \"name\": \"Payment Period Based On Invoice Date\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Payment Summary\",\n \"name\": \"Sales Payment Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Address And Contacts\",\n \"name\": \"Address And Contacts\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"DATEV Export\",\n \"name\": \"DATEV\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Financial Statements",
- "links": "[\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Trial Balance\",\n \"name\": \"Trial Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Profit and Loss Statement\",\n \"name\": \"Profit and Loss Statement\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Balance Sheet\",\n \"name\": \"Balance Sheet\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Cash Flow\",\n \"name\": \"Cash Flow\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Consolidated Financial Statement\",\n \"name\": \"Consolidated Financial Statement\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Multi Currency",
- "links": "[\n {\n \"description\": \"Enable / disable currencies.\",\n \"label\": \"Currency\",\n \"name\": \"Currency\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Currency exchange rate master.\",\n \"label\": \"Currency Exchange\",\n \"name\": \"Currency Exchange\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Exchange Rate Revaluation master.\",\n \"label\": \"Exchange Rate Revaluation\",\n \"name\": \"Exchange Rate Revaluation\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Settings",
- "links": "[\n {\n \"description\": \"Setup Gateway accounts.\",\n \"label\": \"Payment Gateway Account\",\n \"name\": \"Payment Gateway Account\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Template of terms or contract.\",\n \"label\": \"Terms and Conditions Template\",\n \"name\": \"Terms and Conditions\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"e.g. Bank, Cash, Credit Card\",\n \"label\": \"Mode of Payment\",\n \"name\": \"Mode of Payment\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Bank Statement",
- "links": "[\n {\n \"label\": \"Bank\",\n \"name\": \"Bank\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Account\",\n \"name\": \"Bank Account\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Clearance\",\n \"name\": \"Bank Clearance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Bank Reconciliation\",\n \"name\": \"bank-reconciliation\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Reconciliation Statement\",\n \"name\": \"Bank Reconciliation Statement\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Subscription Management",
- "links": "[\n {\n \"label\": \"Subscription Plan\",\n \"name\": \"Subscription Plan\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Subscription\",\n \"name\": \"Subscription\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Subscription Settings\",\n \"name\": \"Subscription Settings\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Goods and Services Tax (GST India)",
- "links": "[\n {\n \"label\": \"GST Settings\",\n \"name\": \"GST Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"GST HSN Code\",\n \"name\": \"GST HSN Code\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-1\",\n \"name\": \"GSTR-1\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GSTR-2\",\n \"name\": \"GSTR-2\",\n \"type\": \"report\"\n },\n {\n \"label\": \"GSTR 3B Report\",\n \"name\": \"GSTR 3B Report\",\n \"type\": \"doctype\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Sales Register\",\n \"name\": \"GST Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Purchase Register\",\n \"name\": \"GST Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Sales Register\",\n \"name\": \"GST Itemised Sales Register\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"GST Itemised Purchase Register\",\n \"name\": \"GST Itemised Purchase Register\",\n \"type\": \"report\"\n },\n {\n \"country\": \"India\",\n \"description\": \"C-Form records\",\n \"label\": \"C-Form\",\n \"name\": \"C-Form\",\n \"type\": \"doctype\"\n },\n {\n \"country\": \"India\",\n \"label\": \"Lower Deduction Certificate\",\n \"name\": \"Lower Deduction Certificate\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Share Management",
- "links": "[\n {\n \"description\": \"List of available Shareholders with folio numbers\",\n \"label\": \"Shareholder\",\n \"name\": \"Shareholder\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"List of all share transactions\",\n \"label\": \"Share Transfer\",\n \"name\": \"Share Transfer\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Share Transfer\"\n ],\n \"doctype\": \"Share Transfer\",\n \"is_query_report\": true,\n \"label\": \"Share Ledger\",\n \"name\": \"Share Ledger\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Share Transfer\"\n ],\n \"doctype\": \"Share Transfer\",\n \"is_query_report\": true,\n \"label\": \"Share Balance\",\n \"name\": \"Share Balance\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Cost Center and Budgeting",
- "links": "[\n {\n \"description\": \"Tree of financial Cost Centers.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Chart of Cost Centers\",\n \"name\": \"Cost Center\",\n \"route\": \"#Tree/Cost Center\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Define budget for a financial year.\",\n \"label\": \"Budget\",\n \"name\": \"Budget\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Accounting Dimension\",\n \"name\": \"Accounting Dimension\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Cost Center\"\n ],\n \"doctype\": \"Cost Center\",\n \"is_query_report\": true,\n \"label\": \"Budget Variance Report\",\n \"name\": \"Budget Variance Report\",\n \"type\": \"report\"\n },\n {\n \"description\": \"Seasonality for setting budgets, targets etc.\",\n \"label\": \"Monthly Distribution\",\n \"name\": \"Monthly Distribution\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Opening and Closing",
- "links": "[\n {\n \"label\": \"Opening Invoice Creation Tool\",\n \"name\": \"Opening Invoice Creation Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Chart of Accounts Importer\",\n \"name\": \"Chart of Accounts Importer\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Close Balance Sheet and book Profit or Loss.\",\n \"label\": \"Period Closing Voucher\",\n \"name\": \"Period Closing Voucher\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Taxes",
- "links": "[\n {\n \"description\": \"Tax template for selling transactions.\",\n \"label\": \"Sales Taxes and Charges Template\",\n \"name\": \"Sales Taxes and Charges Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tax template for buying transactions.\",\n \"label\": \"Purchase Taxes and Charges Template\",\n \"name\": \"Purchase Taxes and Charges Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tax template for item tax rates.\",\n \"label\": \"Item Tax Template\",\n \"name\": \"Item Tax Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tax Category for overriding tax rates.\",\n \"label\": \"Tax Category\",\n \"name\": \"Tax Category\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tax Rule for transactions.\",\n \"label\": \"Tax Rule\",\n \"name\": \"Tax Rule\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tax Withholding rates to be applied on transactions.\",\n \"label\": \"Tax Withholding Category\",\n \"name\": \"Tax Withholding Category\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Profitability",
- "links": "[\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Gross Profit\",\n \"name\": \"Gross Profit\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"GL Entry\"\n ],\n \"doctype\": \"GL Entry\",\n \"is_query_report\": true,\n \"label\": \"Profitability Analysis\",\n \"name\": \"Profitability Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Invoice Trends\",\n \"name\": \"Sales Invoice Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Invoice\"\n ],\n \"doctype\": \"Purchase Invoice\",\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Value-Added Tax (VAT UAE)",
- "links": "[\n {\n \"country\": \"United Arab Emirates\",\n \"label\": \"UAE VAT Settings\",\n \"name\": \"UAE VAT Settings\",\n \"type\": \"doctype\"\n },\n {\n \"country\": \"United Arab Emirates\",\n \"is_query_report\": true,\n \"label\": \"UAE VAT 201\",\n \"name\": \"UAE VAT 201\",\n \"type\": \"report\"\n }\n\n]"
- }
- ],
- "category": "Modules",
- "charts": [
- {
- "chart_name": "Profit and Loss",
- "label": "Profit and Loss"
- }
- ],
- "creation": "2020-03-02 15:41:59.515192",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Accounting",
- "modified": "2020-11-11 18:35:11.542909",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Accounting",
- "onboarding": "Accounts",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "label": "Chart Of Accounts",
- "link_to": "Account",
- "type": "DocType"
- },
- {
- "label": "Sales Invoice",
- "link_to": "Sales Invoice",
- "type": "DocType"
- },
- {
- "label": "Purchase Invoice",
- "link_to": "Purchase Invoice",
- "type": "DocType"
- },
- {
- "label": "Journal Entry",
- "link_to": "Journal Entry",
- "type": "DocType"
- },
- {
- "label": "Payment Entry",
- "link_to": "Payment Entry",
- "type": "DocType"
- },
- {
- "label": "Accounts Receivable",
- "link_to": "Accounts Receivable",
- "type": "Report"
- },
- {
- "label": "General Ledger",
- "link_to": "General Ledger",
- "type": "Report"
- },
- {
- "label": "Trial Balance",
- "link_to": "Trial Balance",
- "type": "Report"
- },
- {
- "label": "Dashboard",
- "link_to": "Accounts",
- "type": "Dashboard"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/account/account_tree.js b/erpnext/accounts/doctype/account/account_tree.js
index 7bbc1c9..7516134 100644
--- a/erpnext/accounts/doctype/account/account_tree.js
+++ b/erpnext/accounts/doctype/account/account_tree.js
@@ -120,17 +120,17 @@
} else {
treeview.new_node();
}
- }, "octicon octicon-plus");
+ }, "add");
},
onrender: function(node) {
- if(frappe.boot.user.can_read.indexOf("GL Entry") !== -1){
+ if (frappe.boot.user.can_read.indexOf("GL Entry") !== -1) {
// show Dr if positive since balance is calculated as debit - credit else show Cr
let balance = node.data.balance_in_account_currency || node.data.balance;
let dr_or_cr = balance > 0 ? "Dr": "Cr";
if (node.data && node.data.balance!==undefined) {
- $('<span class="balance-area pull-right text-muted small">'
+ $('<span class="balance-area pull-right">'
+ (node.data.balance_in_account_currency ?
(format_currency(Math.abs(node.data.balance_in_account_currency),
node.data.account_currency) + " / ") : "")
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json
index 3fc109b..849df18 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/de_kontenplan_SKR04_with_account_number.json
@@ -910,98 +910,8 @@
},
"is_group": 1
},
- "Passiva": {
+ "Passiva - Verbindlichkeiten": {
"root_type": "Liability",
- "A - Eigenkapital": {
- "account_type": "Equity",
- "is_group": 1,
- "I - Gezeichnetes Kapital": {
- "account_type": "Equity",
- "is_group": 1,
- "Gezeichnetes Kapital": {
- "account_type": "Equity",
- "account_number": "2900"
- },
- "Ausstehende Einlagen auf das gezeichnete Kapital": {
- "account_number": "2910",
- "is_group": 1
- }
- },
- "II - Kapitalr\u00fccklage": {
- "account_type": "Equity",
- "is_group": 1,
- "Kapitalr\u00fccklage": {
- "account_number": "2920"
- }
- },
- "III - Gewinnr\u00fccklagen": {
- "account_type": "Equity",
- "1 - gesetzliche R\u00fccklage": {
- "account_type": "Equity",
- "is_group": 1,
- "Gesetzliche R\u00fccklage": {
- "account_number": "2930"
- }
- },
- "2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
- "account_type": "Equity",
- "is_group": 1
- },
- "3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
- "account_type": "Equity",
- "is_group": 1,
- "Satzungsm\u00e4\u00dfige R\u00fccklagen": {
- "account_number": "2950"
- }
- },
- "4 - andere Gewinnr\u00fccklagen": {
- "account_type": "Equity",
- "is_group": 1,
- "Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
- "is_group": 1,
- "Gewinnr\u00fccklagen (BilMoG)": {
- "account_number": "2963"
- },
- "Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
- "account_number": "2964"
- },
- "Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
- "account_number": "2965"
- },
- "Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
- "account_number": "2966"
- }
- },
- "Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
- "account_number": "2967"
- },
- "Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
- "account_number": "2968"
- },
- "Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
- "account_number": "2969"
- }
- },
- "is_group": 1
- },
- "IV - Gewinnvortrag/Verlustvortrag": {
- "account_type": "Equity",
- "is_group": 1,
- "Gewinnvortrag vor Verwendung": {
- "account_number": "2970"
- },
- "Verlustvortrag vor Verwendung": {
- "account_number": "2978"
- }
- },
- "V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
- "account_type": "Equity",
- "is_group": 1
- },
- "Einlagen stiller Gesellschafter": {
- "account_number": "9295"
- }
- },
"B - R\u00fcckstellungen": {
"is_group": 1,
"1 - R\u00fcckstellungen f. Pensionen und \u00e4hnliche Verplicht.": {
@@ -1618,6 +1528,143 @@
},
"is_group": 1
},
+ "Passiva - Eigenkapital": {
+ "root_type": "Equity",
+ "A - Eigenkapital": {
+ "account_type": "Equity",
+ "is_group": 1,
+ "I - Gezeichnetes Kapital": {
+ "account_type": "Equity",
+ "is_group": 1,
+ "Gezeichnetes Kapital": {
+ "account_number": "2900",
+ "account_type": "Equity"
+ },
+ "Gesch\u00e4ftsguthaben der verbleibenden Mitglieder": {
+ "account_number": "2901"
+ },
+ "Gesch\u00e4ftsguthaben der ausscheidenden Mitglieder": {
+ "account_number": "2902"
+ },
+ "Gesch\u00e4ftsguthaben aus gek\u00fcndigten Gesch\u00e4ftsanteilen": {
+ "account_number": "2903"
+ },
+ "R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
+ "account_number": "2906"
+ },
+ "Gegenkonto R\u00fcckst\u00e4ndige f\u00e4llige Einzahlungen auf Gesch\u00e4ftsanteile, vermerkt": {
+ "account_number": "2907"
+ },
+ "Kapitalerh\u00f6hung aus Gesellschaftsmitteln": {
+ "account_number": "2908"
+ },
+ "Ausstehende Einlagen auf das gezeichnete Kapital, nicht eingefordert": {
+ "account_number": "2910"
+ }
+ },
+ "II - Kapitalr\u00fccklage": {
+ "account_type": "Equity",
+ "is_group": 1,
+ "Kapitalr\u00fccklage": {
+ "account_number": "2920"
+ },
+ "Kapitalr\u00fccklage durch Ausgabe von Anteilen \u00fcber Nennbetrag": {
+ "account_number": "2925"
+ },
+ "Kapitalr\u00fccklage durch Ausgabe von Schuldverschreibungen": {
+ "account_number": "2926"
+ },
+ "Kapitalr\u00fccklage durch Zuzahlungen gegen Gew\u00e4hrung eines Vorzugs": {
+ "account_number": "2927"
+ },
+ "Kapitalr\u00fccklage durch Zuzahlungen in das Eigenkapital": {
+ "account_number": "2928"
+ },
+ "Nachschusskapital (Gegenkonto 1299)": {
+ "account_number": "2929"
+ }
+ },
+ "III - Gewinnr\u00fccklagen": {
+ "account_type": "Equity",
+ "1 - gesetzliche R\u00fccklage": {
+ "account_type": "Equity",
+ "is_group": 1,
+ "Gesetzliche R\u00fccklage": {
+ "account_number": "2930"
+ }
+ },
+ "2 - R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
+ "account_type": "Equity",
+ "is_group": 1,
+ "R\u00fccklage f. Anteile an einem herrschenden oder mehrheitlich beteiligten Unternehmen": {
+ "account_number": "2935"
+ }
+ },
+ "3 - satzungsm\u00e4\u00dfige R\u00fccklagen": {
+ "account_type": "Equity",
+ "is_group": 1,
+ "Satzungsm\u00e4\u00dfige R\u00fccklagen": {
+ "account_number": "2950"
+ }
+ },
+ "4 - andere Gewinnr\u00fccklagen": {
+ "account_type": "Equity",
+ "is_group": 1,
+ "Andere Gewinnr\u00fccklagen": {
+ "account_number": "2960"
+ },
+ "Andere Gewinnr\u00fccklagen aus dem Erwerb eigener Anteile": {
+ "account_number": "2961"
+ },
+ "Eigenkapitalanteil von Wertaufholungen": {
+ "account_number": "2962"
+ },
+ "Gewinnr\u00fccklagen aus den \u00dcbergangsvorschriften BilMoG": {
+ "is_group": 1,
+ "Gewinnr\u00fccklagen (BilMoG)": {
+ "account_number": "2963"
+ },
+ "Gewinnr\u00fccklagen aus Zuschreibung Sachanlageverm\u00f6gen (BilMoG)": {
+ "account_number": "2964"
+ },
+ "Gewinnr\u00fccklagen aus Zuschreibung Finanzanlageverm\u00f6gen (BilMoG)": {
+ "account_number": "2965"
+ },
+ "Gewinnr\u00fccklagen aus Aufl\u00f6sung der Sonderposten mit R\u00fccklageanteil (BilMoG)": {
+ "account_number": "2966"
+ }
+ },
+ "Latente Steuern (Gewinnr\u00fccklage Haben) aus erfolgsneutralen Verrechnungen": {
+ "account_number": "2967"
+ },
+ "Latente Steuern (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
+ "account_number": "2968"
+ },
+ "Rechnungsabgrenzungsposten (Gewinnr\u00fccklage Soll) aus erfolgsneutralen Verrechnungen": {
+ "account_number": "2969"
+ }
+ },
+ "is_group": 1
+ },
+ "IV - Gewinnvortrag/Verlustvortrag": {
+ "account_type": "Equity",
+ "is_group": 1,
+ "Gewinnvortrag vor Verwendung": {
+ "account_number": "2970"
+ },
+ "Verlustvortrag vor Verwendung": {
+ "account_number": "2978"
+ }
+ },
+ "V - Jahres\u00fcberschu\u00df/Jahresfehlbetrag": {
+ "account_type": "Equity",
+ "is_group": 1
+ },
+ "Einlagen stiller Gesellschafter": {
+ "account_number": "9295"
+ }
+ }
+ },
"1 - Umsatzerl\u00f6se": {
"root_type": "Income",
"is_group": 1,
diff --git a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
index 6c83e3b..acb11e5 100644
--- a/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
+++ b/erpnext/accounts/doctype/account/chart_of_accounts/verified/standard_chart_of_accounts_with_account_number.py
@@ -245,6 +245,9 @@
"account_number": "2200"
},
_("Duties and Taxes"): {
+ _("TDS Payable"): {
+ "account_number": "2310"
+ },
"account_type": "Tax",
"is_group": 1,
"account_number": "2300"
diff --git a/erpnext/accounts/doctype/account/test_account.py b/erpnext/accounts/doctype/account/test_account.py
index 0605d89..533eda3 100644
--- a/erpnext/accounts/doctype/account/test_account.py
+++ b/erpnext/accounts/doctype/account/test_account.py
@@ -172,7 +172,7 @@
frappe.delete_doc("Account", doc)
-def _make_test_records(verbose):
+def _make_test_records(verbose=None):
from frappe.test_runner import make_test_objects
accounts = [
@@ -254,7 +254,8 @@
account_name = kwargs.get('account_name'),
account_type = kwargs.get('account_type'),
parent_account = kwargs.get('parent_account'),
- company = kwargs.get('company')
+ company = kwargs.get('company'),
+ account_currency = kwargs.get('account_currency')
))
account.save()
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
index 9a6c389..65c5ff1 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.js
@@ -2,7 +2,6 @@
// For license information, please see license.txt
frappe.ui.form.on('Accounting Dimension', {
-
refresh: function(frm) {
frm.set_query('document_type', () => {
let invalid_doctypes = frappe.model.core_doctypes_list;
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json
index cf55d55..5858f10 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.json
@@ -30,6 +30,7 @@
"fieldtype": "Link",
"label": "Reference Document Type",
"options": "DocType",
+ "read_only_depends_on": "eval:!doc.__islocal",
"reqd": 1
},
{
@@ -48,7 +49,7 @@
}
],
"links": [],
- "modified": "2020-03-22 20:34:39.805728",
+ "modified": "2021-02-08 16:37:53.936656",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounting Dimension",
diff --git a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
index ed1a0b6..0ebf0eb 100644
--- a/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/accounting_dimension.py
@@ -29,15 +29,25 @@
if exists and self.is_new():
frappe.throw("Document Type already used as a dimension")
+ if not self.is_new():
+ self.validate_document_type_change()
+
+ def validate_document_type_change(self):
+ doctype_before_save = frappe.db.get_value("Accounting Dimension", self.name, "document_type")
+ if doctype_before_save != self.document_type:
+ message = _("Cannot change Reference Document Type.")
+ message += _("Please create a new Accounting Dimension if required.")
+ frappe.throw(message)
+
def after_insert(self):
if frappe.flags.in_test:
make_dimension_in_accounting_doctypes(doc=self)
else:
- frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self)
+ frappe.enqueue(make_dimension_in_accounting_doctypes, doc=self, queue='long')
def on_trash(self):
if frappe.flags.in_test:
- delete_accounting_dimension(doc=self)
+ delete_accounting_dimension(doc=self, queue='long')
else:
frappe.enqueue(delete_accounting_dimension, doc=self)
@@ -48,9 +58,13 @@
if not self.fieldname:
self.fieldname = scrub(self.label)
+ def on_update(self):
+ frappe.flags.accounting_dimensions = None
+
def make_dimension_in_accounting_doctypes(doc, doclist=None):
if not doclist:
doclist = get_doctypes_with_dimensions()
+
doc_count = len(get_accounting_dimensions())
count = 0
@@ -72,6 +86,7 @@
meta = frappe.get_meta(doctype, cached=False)
fieldnames = [d.fieldname for d in meta.get("fields")]
+
if df['fieldname'] not in fieldnames:
if doctype == "Budget":
add_dimension_to_budget_doctype(df.copy(), doc)
@@ -168,12 +183,14 @@
return frappe.get_hooks("accounting_dimension_doctypes")
def get_accounting_dimensions(as_list=True):
- accounting_dimensions = frappe.get_all("Accounting Dimension", fields=["label", "fieldname", "disabled", "document_type"])
+ if frappe.flags.accounting_dimensions is None:
+ frappe.flags.accounting_dimensions = frappe.get_all("Accounting Dimension",
+ fields=["label", "fieldname", "disabled", "document_type"])
if as_list:
- return [d.fieldname for d in accounting_dimensions]
+ return [d.fieldname for d in frappe.flags.accounting_dimensions]
else:
- return accounting_dimensions
+ return frappe.flags.accounting_dimensions
def get_checks_for_pl_and_bs_accounts():
dimensions = frappe.db.sql("""SELECT p.label, p.disabled, p.fieldname, c.default_dimension, c.company, c.mandatory_for_pl, c.mandatory_for_bs
@@ -195,7 +212,7 @@
return all_dimensions
@frappe.whitelist()
-def get_dimension_filters():
+def get_dimensions(with_cost_center_and_project=False):
dimension_filters = frappe.db.sql("""
SELECT label, fieldname, document_type
FROM `tabAccounting Dimension`
@@ -206,6 +223,18 @@
FROM `tabAccounting Dimension Detail` c, `tabAccounting Dimension` p
WHERE c.parent = p.name""", as_dict=1)
+ if with_cost_center_and_project:
+ dimension_filters.extend([
+ {
+ 'fieldname': 'cost_center',
+ 'document_type': 'Cost Center'
+ },
+ {
+ 'fieldname': 'project',
+ 'document_type': 'Project'
+ }
+ ])
+
default_dimensions_map = {}
for dimension in default_dimensions:
default_dimensions_map.setdefault(dimension.company, {})
diff --git a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
index 104880f..fc1d7e3 100644
--- a/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
+++ b/erpnext/accounts/doctype/accounting_dimension/test_accounting_dimension.py
@@ -11,37 +11,7 @@
class TestAccountingDimension(unittest.TestCase):
def setUp(self):
- frappe.set_user("Administrator")
-
- if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
- dimension = frappe.get_doc({
- "doctype": "Accounting Dimension",
- "document_type": "Department",
- }).insert()
- else:
- dimension1 = frappe.get_doc("Accounting Dimension", "Department")
- dimension1.disabled = 0
- dimension1.save()
-
- if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
- dimension1 = frappe.get_doc({
- "doctype": "Accounting Dimension",
- "document_type": "Location",
- })
-
- dimension1.append("dimension_defaults", {
- "company": "_Test Company",
- "reference_document": "Location",
- "default_dimension": "Block 1",
- "mandatory_for_bs": 1
- })
-
- dimension1.insert()
- dimension1.save()
- else:
- dimension1 = frappe.get_doc("Accounting Dimension", "Location")
- dimension1.disabled = 0
- dimension1.save()
+ create_dimension()
def test_dimension_against_sales_invoice(self):
si = create_sales_invoice(do_not_save=1)
@@ -101,6 +71,38 @@
def tearDown(self):
disable_dimension()
+def create_dimension():
+ frappe.set_user("Administrator")
+
+ if not frappe.db.exists("Accounting Dimension", {"document_type": "Department"}):
+ frappe.get_doc({
+ "doctype": "Accounting Dimension",
+ "document_type": "Department",
+ }).insert()
+ else:
+ dimension = frappe.get_doc("Accounting Dimension", "Department")
+ dimension.disabled = 0
+ dimension.save()
+
+ if not frappe.db.exists("Accounting Dimension", {"document_type": "Location"}):
+ dimension1 = frappe.get_doc({
+ "doctype": "Accounting Dimension",
+ "document_type": "Location",
+ })
+
+ dimension1.append("dimension_defaults", {
+ "company": "_Test Company",
+ "reference_document": "Location",
+ "default_dimension": "Block 1",
+ "mandatory_for_bs": 1
+ })
+
+ dimension1.insert()
+ dimension1.save()
+ else:
+ dimension1 = frappe.get_doc("Accounting Dimension", "Location")
+ dimension1.disabled = 0
+ dimension1.save()
def disable_dimension():
dimension1 = frappe.get_doc("Accounting Dimension", "Department")
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/accounts/doctype/accounting_dimension_filter/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/accounts/doctype/accounting_dimension_filter/__init__.py
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
new file mode 100644
index 0000000..74b7b51
--- /dev/null
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.js
@@ -0,0 +1,82 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Accounting Dimension Filter', {
+ refresh: function(frm, cdt, cdn) {
+ if (frm.doc.accounting_dimension) {
+ frm.set_df_property('dimensions', 'label', frm.doc.accounting_dimension, cdn, 'dimension_value');
+ }
+
+ let help_content =
+ `<table class="table table-bordered" style="background-color: #f9f9f9;">
+ <tr><td>
+ <p>
+ <i class="fa fa-hand-right"></i>
+ {{__('Note: On checking Is Mandatory the accounting dimension will become mandatory against that specific account for all accounting transactions')}}
+ </p>
+ </td></tr>
+ </table>`;
+
+ frm.set_df_property('dimension_filter_help', 'options', help_content);
+ },
+ onload: function(frm) {
+ frm.set_query('applicable_on_account', 'accounts', function() {
+ return {
+ filters: {
+ 'company': frm.doc.company
+ }
+ };
+ });
+
+ frappe.db.get_list('Accounting Dimension',
+ {fields: ['document_type']}).then((res) => {
+ let options = ['Cost Center', 'Project'];
+
+ res.forEach((dimension) => {
+ options.push(dimension.document_type);
+ });
+
+ frm.set_df_property('accounting_dimension', 'options', options);
+ });
+
+ frm.trigger('setup_filters');
+ },
+
+ setup_filters: function(frm) {
+ let filters = {};
+
+ if (frm.doc.accounting_dimension) {
+ frappe.model.with_doctype(frm.doc.accounting_dimension, function() {
+ if (frappe.model.is_tree(frm.doc.accounting_dimension)) {
+ filters['is_group'] = 0;
+ }
+
+ if (frappe.meta.has_field(frm.doc.accounting_dimension, 'company')) {
+ filters['company'] = frm.doc.company;
+ }
+
+ frm.set_query('dimension_value', 'dimensions', function() {
+ return {
+ filters: filters
+ };
+ });
+ });
+ }
+ },
+
+ accounting_dimension: function(frm) {
+ frm.clear_table("dimensions");
+ let row = frm.add_child("dimensions");
+ row.accounting_dimension = frm.doc.accounting_dimension;
+ frm.refresh_field("dimensions");
+ frm.trigger('setup_filters');
+ },
+});
+
+frappe.ui.form.on('Allowed Dimension', {
+ dimensions_add: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ row.accounting_dimension = frm.doc.accounting_dimension;
+ frm.refresh_field("dimensions");
+ }
+});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json
new file mode 100644
index 0000000..0f3fbc0
--- /dev/null
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.json
@@ -0,0 +1,158 @@
+{
+ "actions": [],
+ "autoname": "format:{accounting_dimension}-{#####}",
+ "creation": "2020-11-08 18:28:11.906146",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "accounting_dimension",
+ "disabled",
+ "column_break_2",
+ "company",
+ "allow_or_restrict",
+ "section_break_4",
+ "accounts",
+ "column_break_6",
+ "dimensions",
+ "section_break_10",
+ "dimension_filter_help"
+ ],
+ "fields": [
+ {
+ "fieldname": "accounting_dimension",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "label": "Accounting Dimension",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "hide_border": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "allow_or_restrict",
+ "fieldtype": "Select",
+ "label": "Allow Or Restrict Dimension",
+ "options": "Allow\nRestrict",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "accounts",
+ "fieldtype": "Table",
+ "label": "Applicable On Account",
+ "options": "Applicable On Account",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "depends_on": "eval:doc.accounting_dimension",
+ "fieldname": "dimensions",
+ "fieldtype": "Table",
+ "label": "Applicable Dimension",
+ "options": "Allowed Dimension",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "disabled",
+ "fieldtype": "Check",
+ "label": "Disabled",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "dimension_filter_help",
+ "fieldtype": "HTML",
+ "label": "Dimension Filter Help",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "section_break_10",
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-02-03 12:04:58.678402",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Accounting Dimension Filter",
+ "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": "Accounts User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
new file mode 100644
index 0000000..6aef9ca
--- /dev/null
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/accounting_dimension_filter.py
@@ -0,0 +1,61 @@
+# -*- coding: utf-8 -*-
+# Copyright, (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _, scrub
+from frappe.model.document import Document
+
+class AccountingDimensionFilter(Document):
+ def validate(self):
+ self.validate_applicable_accounts()
+
+ def validate_applicable_accounts(self):
+ accounts = frappe.db.sql(
+ """
+ SELECT a.applicable_on_account as account
+ FROM `tabApplicable On Account` a, `tabAccounting Dimension Filter` d
+ WHERE d.name = a.parent
+ and d.name != %s
+ and d.accounting_dimension = %s
+ """, (self.name, self.accounting_dimension), as_dict=1)
+
+ account_list = [d.account for d in accounts]
+
+ for account in self.get('accounts'):
+ if account.applicable_on_account in account_list:
+ frappe.throw(_("Row {0}: {1} account already applied for Accounting Dimension {2}").format(
+ account.idx, frappe.bold(account.applicable_on_account), frappe.bold(self.accounting_dimension)))
+
+def get_dimension_filter_map():
+ filters = frappe.db.sql("""
+ SELECT
+ a.applicable_on_account, d.dimension_value, p.accounting_dimension,
+ p.allow_or_restrict, a.is_mandatory
+ FROM
+ `tabApplicable On Account` a, `tabAllowed Dimension` d,
+ `tabAccounting Dimension Filter` p
+ WHERE
+ p.name = a.parent
+ AND p.disabled = 0
+ AND p.name = d.parent
+ """, as_dict=1)
+
+ dimension_filter_map = {}
+
+ for f in filters:
+ f.fieldname = scrub(f.accounting_dimension)
+
+ build_map(dimension_filter_map, f.fieldname, f.applicable_on_account, f.dimension_value,
+ f.allow_or_restrict, f.is_mandatory)
+
+ return dimension_filter_map
+
+def build_map(map_object, dimension, account, filter_value, allow_or_restrict, is_mandatory):
+ map_object.setdefault((dimension, account), {
+ 'allowed_dimensions': [],
+ 'is_mandatory': is_mandatory,
+ 'allow_or_restrict': allow_or_restrict
+ })
+ map_object[(dimension, account)]['allowed_dimensions'].append(filter_value)
diff --git a/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
new file mode 100644
index 0000000..7877abd
--- /dev/null
+++ b/erpnext/accounts/doctype/accounting_dimension_filter/test_accounting_dimension_filter.py
@@ -0,0 +1,99 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
+from erpnext.accounts.doctype.accounting_dimension.test_accounting_dimension import create_dimension, disable_dimension
+from erpnext.exceptions import InvalidAccountDimensionError, MandatoryAccountDimensionError
+
+class TestAccountingDimensionFilter(unittest.TestCase):
+ def setUp(self):
+ create_dimension()
+ create_accounting_dimension_filter()
+ self.invoice_list = []
+
+ def test_allowed_dimension_validation(self):
+ si = create_sales_invoice(do_not_save=1)
+ si.items[0].cost_center = 'Main - _TC'
+ si.department = 'Accounts - _TC'
+ si.location = 'Block 1'
+ si.save()
+
+ self.assertRaises(InvalidAccountDimensionError, si.submit)
+ self.invoice_list.append(si)
+
+ def test_mandatory_dimension_validation(self):
+ si = create_sales_invoice(do_not_save=1)
+ si.department = ''
+ si.location = 'Block 1'
+
+ # Test with no department for Sales Account
+ si.items[0].department = ''
+ si.items[0].cost_center = '_Test Cost Center 2 - _TC'
+ si.save()
+
+ self.assertRaises(MandatoryAccountDimensionError, si.submit)
+ self.invoice_list.append(si)
+
+ def tearDown(self):
+ disable_dimension_filter()
+ disable_dimension()
+
+ for si in self.invoice_list:
+ si.load_from_db()
+ if si.docstatus == 1:
+ si.cancel()
+
+def create_accounting_dimension_filter():
+ if not frappe.db.get_value('Accounting Dimension Filter',
+ {'accounting_dimension': 'Cost Center'}):
+ frappe.get_doc({
+ 'doctype': 'Accounting Dimension Filter',
+ 'accounting_dimension': 'Cost Center',
+ 'allow_or_restrict': 'Allow',
+ 'company': '_Test Company',
+ 'accounts': [{
+ 'applicable_on_account': 'Sales - _TC',
+ }],
+ 'dimensions': [{
+ 'accounting_dimension': 'Cost Center',
+ 'dimension_value': '_Test Cost Center 2 - _TC'
+ }]
+ }).insert()
+ else:
+ doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
+ doc.disabled = 0
+ doc.save()
+
+ if not frappe.db.get_value('Accounting Dimension Filter',
+ {'accounting_dimension': 'Department'}):
+ frappe.get_doc({
+ 'doctype': 'Accounting Dimension Filter',
+ 'accounting_dimension': 'Department',
+ 'allow_or_restrict': 'Allow',
+ 'company': '_Test Company',
+ 'accounts': [{
+ 'applicable_on_account': 'Sales - _TC',
+ 'is_mandatory': 1
+ }],
+ 'dimensions': [{
+ 'accounting_dimension': 'Department',
+ 'dimension_value': 'Accounts - _TC'
+ }]
+ }).insert()
+ else:
+ doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
+ doc.disabled = 0
+ doc.save()
+
+def disable_dimension_filter():
+ doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Cost Center'})
+ doc.disabled = 1
+ doc.save()
+
+ doc = frappe.get_doc('Accounting Dimension Filter', {'accounting_dimension': 'Department'})
+ doc.disabled = 1
+ doc.save()
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js
index 0627675..541901c 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.js
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.js
@@ -6,3 +6,46 @@
}
});
+
+frappe.tour['Accounts Settings'] = [
+ {
+ fieldname: "acc_frozen_upto",
+ title: "Accounts Frozen Upto",
+ description: __("Freeze accounting transactions up to specified date, nobody can make/modify entry except the specified Role."),
+ },
+ {
+ fieldname: "frozen_accounts_modifier",
+ title: "Role Allowed to Set Frozen Accounts & Edit Frozen Entries",
+ description: __("Users with this Role are allowed to set frozen accounts and create/modify accounting entries against frozen accounts.")
+ },
+ {
+ fieldname: "determine_address_tax_category_from",
+ title: "Determine Address Tax Category From",
+ description: __("Tax category can be set on Addresses. An address can be Shipping or Billing address. Set which addres to select when applying Tax Category.")
+ },
+ {
+ fieldname: "over_billing_allowance",
+ title: "Over Billing Allowance Percentage",
+ description: __("The percentage by which you can overbill transactions. For example, if the order value is $100 for an Item and percentage here is set as 10% then you are allowed to bill for $110.")
+ },
+ {
+ fieldname: "credit_controller",
+ title: "Credit Controller",
+ description: __("Select the role that is allowed to submit transactions that exceed credit limits set. The credit limit can be set in the Customer form.")
+ },
+ {
+ fieldname: "make_payment_via_journal_entry",
+ title: "Make Payment via Journal Entry",
+ description: __("When checked, if user proceeds to make payment from an invoice, the system will open a Journal Entry instead of a Payment Entry.")
+ },
+ {
+ fieldname: "unlink_payment_on_cancellation_of_invoice",
+ title: "Unlink Payment on Cancellation of Invoice",
+ description: __("If checked, system will unlink the payment against the respective invoice.")
+ },
+ {
+ fieldname: "unlink_advance_payment_on_cancelation_of_order",
+ title: "Unlink Advance Payment on Cancellation of Order",
+ description: __("Similar to the previous option, this unlinks any advance payments made against Purchase/Sales Orders.")
+ }
+];
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
index 41f9ce0..a3c29b6 100644
--- a/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
+++ b/erpnext/accounts/doctype/accounts_settings/accounts_settings.json
@@ -21,6 +21,7 @@
"book_asset_depreciation_entry_automatically",
"add_taxes_from_item_tax_template",
"automatically_fetch_payment_terms",
+ "delete_linked_ledger_entries",
"deferred_accounting_settings_section",
"automatically_process_deferred_accounting_entry",
"book_deferred_entries_based_on",
@@ -219,6 +220,12 @@
"fieldtype": "Select",
"label": "Book Deferred Entries Based On",
"options": "Days\nMonths"
+ },
+ {
+ "default": "0",
+ "fieldname": "delete_linked_ledger_entries",
+ "fieldtype": "Check",
+ "label": "Delete Accounting and Stock Ledger Entries on deletion of Transaction"
}
],
"icon": "icon-cog",
@@ -226,7 +233,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-10-13 11:32:52.268826",
+ "modified": "2021-01-05 13:04:00.118892",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts Settings",
@@ -254,4 +261,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/accounts/doctype/allowed_dimension/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/accounts/doctype/allowed_dimension/__init__.py
diff --git a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json
new file mode 100644
index 0000000..7fe2a3c
--- /dev/null
+++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.json
@@ -0,0 +1,43 @@
+{
+ "actions": [],
+ "creation": "2020-11-08 18:22:36.001131",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "accounting_dimension",
+ "dimension_value"
+ ],
+ "fields": [
+ {
+ "fieldname": "accounting_dimension",
+ "fieldtype": "Link",
+ "label": "Accounting Dimension",
+ "options": "DocType",
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "dimension_value",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "options": "accounting_dimension",
+ "show_days": 1,
+ "show_seconds": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-11-23 09:56:19.744200",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Allowed Dimension",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py
new file mode 100644
index 0000000..c2afc1a
--- /dev/null
+++ b/erpnext/accounts/doctype/allowed_dimension/allowed_dimension.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class AllowedDimension(Document):
+ pass
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/accounts/doctype/applicable_on_account/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/accounts/doctype/applicable_on_account/__init__.py
diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json
new file mode 100644
index 0000000..95e98d0
--- /dev/null
+++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.json
@@ -0,0 +1,46 @@
+{
+ "actions": [],
+ "creation": "2020-11-08 18:20:00.944449",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "applicable_on_account",
+ "is_mandatory"
+ ],
+ "fields": [
+ {
+ "fieldname": "applicable_on_account",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Accounts",
+ "options": "Account",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "columns": 2,
+ "default": "0",
+ "fieldname": "is_mandatory",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Is Mandatory",
+ "show_days": 1,
+ "show_seconds": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-11-22 19:55:13.324136",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Applicable On Account",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py
new file mode 100644
index 0000000..0fccaf3
--- /dev/null
+++ b/erpnext/accounts/doctype/applicable_on_account/applicable_on_account.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class ApplicableOnAccount(Document):
+ pass
diff --git a/erpnext/accounts/doctype/bank/bank.js b/erpnext/accounts/doctype/bank/bank.js
index de9498e..49b2b18 100644
--- a/erpnext/accounts/doctype/bank/bank.js
+++ b/erpnext/accounts/doctype/bank/bank.js
@@ -1,5 +1,6 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide('erpnext.integrations');
frappe.ui.form.on('Bank', {
onload: function(frm) {
@@ -20,7 +21,12 @@
frm.set_df_property('address_and_contact', 'hidden', 0);
frappe.contacts.render_address_and_contact(frm);
}
- },
+ if (frm.doc.plaid_access_token) {
+ frm.add_custom_button(__('Refresh Plaid Link'), () => {
+ new erpnext.integrations.refreshPlaidLink(frm.doc.plaid_access_token);
+ });
+ }
+ }
});
@@ -40,4 +46,79 @@
frm.doc.name).options = options;
frm.fields_dict.bank_transaction_mapping.grid.refresh();
-};
\ No newline at end of file
+};
+
+erpnext.integrations.refreshPlaidLink = class refreshPlaidLink {
+ constructor(access_token) {
+ this.access_token = access_token;
+ this.plaidUrl = 'https://cdn.plaid.com/link/v2/stable/link-initialize.js';
+ this.init_config();
+ }
+
+ async init_config() {
+ this.plaid_env = await frappe.db.get_single_value('Plaid Settings', 'plaid_env');
+ this.token = await this.get_link_token_for_update();
+ this.init_plaid();
+ }
+
+ async get_link_token_for_update() {
+ const token = frappe.xcall(
+ 'erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_link_token_for_update',
+ { access_token: this.access_token }
+ )
+ if (!token) {
+ frappe.throw(__('Cannot retrieve link token for update. Check Error Log for more information'));
+ }
+ return token;
+ }
+
+ init_plaid() {
+ const me = this;
+ me.loadScript(me.plaidUrl)
+ .then(() => {
+ me.onScriptLoaded(me);
+ })
+ .then(() => {
+ if (me.linkHandler) {
+ me.linkHandler.open();
+ }
+ })
+ .catch((error) => {
+ me.onScriptError(error);
+ });
+ }
+
+ loadScript(src) {
+ return new Promise(function (resolve, reject) {
+ if (document.querySelector("script[src='" + src + "']")) {
+ resolve();
+ return;
+ }
+ const el = document.createElement('script');
+ el.type = 'text/javascript';
+ el.async = true;
+ el.src = src;
+ el.addEventListener('load', resolve);
+ el.addEventListener('error', reject);
+ el.addEventListener('abort', reject);
+ document.head.appendChild(el);
+ });
+ }
+
+ onScriptLoaded(me) {
+ me.linkHandler = Plaid.create({
+ env: me.plaid_env,
+ token: me.token,
+ onSuccess: me.plaid_success
+ });
+ }
+
+ onScriptError(error) {
+ frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
+ console.log(error);
+ }
+
+ plaid_success(token, response) {
+ frappe.show_alert({ message: __('Plaid Link Updated'), indicator: 'green' });
+ }
+};
diff --git a/erpnext/accounts/doctype/bank_account/bank_account.json b/erpnext/accounts/doctype/bank_account/bank_account.json
index b42f1f9..de67ab1 100644
--- a/erpnext/accounts/doctype/bank_account/bank_account.json
+++ b/erpnext/accounts/doctype/bank_account/bank_account.json
@@ -86,6 +86,7 @@
},
{
"default": "0",
+ "description": "Setting the account as a Company Account is necessary for Bank Reconciliation",
"fieldname": "is_company_account",
"fieldtype": "Check",
"label": "Is Company Account"
@@ -207,7 +208,7 @@
}
],
"links": [],
- "modified": "2020-07-17 13:59:50.795412",
+ "modified": "2020-10-23 16:48:06.303658",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Account",
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/accounts/doctype/bank_reconciliation_tool/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/accounts/doctype/bank_reconciliation_tool/__init__.py
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
new file mode 100644
index 0000000..297dd43
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.js
@@ -0,0 +1,162 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+frappe.ui.form.on("Bank Reconciliation Tool", {
+ setup: function (frm) {
+ frm.set_query("bank_account", function () {
+ return {
+ filters: {
+ company: ["in", frm.doc.company],
+ },
+ };
+ });
+ },
+
+ refresh: function (frm) {
+ frappe.require("assets/js/bank-reconciliation-tool.min.js", () =>
+ frm.trigger("make_reconciliation_tool")
+ );
+ frm.upload_statement_button = frm.page.set_secondary_action(
+ __("Upload Bank Statement"),
+ () =>
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_statement_import.bank_statement_import.upload_bank_statement",
+ args: {
+ dt: frm.doc.doctype,
+ dn: frm.doc.name,
+ company: frm.doc.company,
+ bank_account: frm.doc.bank_account,
+ },
+ callback: function (r) {
+ if (!r.exc) {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route(
+ "Form",
+ doc[0].doctype,
+ doc[0].name
+ );
+ }
+ },
+ })
+ );
+ },
+
+ after_save: function (frm) {
+ frm.trigger("make_reconciliation_tool");
+ },
+
+ bank_account: function (frm) {
+ frappe.db.get_value(
+ "Bank Account",
+ frm.bank_account,
+ "account",
+ (r) => {
+ frappe.db.get_value(
+ "Account",
+ r.account,
+ "account_currency",
+ (r) => {
+ frm.currency = r.account_currency;
+ }
+ );
+ }
+ );
+ frm.trigger("get_account_opening_balance");
+ },
+
+ bank_statement_from_date: function (frm) {
+ frm.trigger("get_account_opening_balance");
+ },
+
+ make_reconciliation_tool(frm) {
+ frm.get_field("reconciliation_tool_cards").$wrapper.empty();
+ if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
+ frm.trigger("get_cleared_balance").then(() => {
+ if (
+ frm.doc.bank_account &&
+ frm.doc.bank_statement_from_date &&
+ frm.doc.bank_statement_to_date &&
+ frm.doc.bank_statement_closing_balance
+ ) {
+ frm.trigger("render_chart");
+ frm.trigger("render");
+ frappe.utils.scroll_to(
+ frm.get_field("reconciliation_tool_cards").$wrapper,
+ true,
+ 30
+ );
+ }
+ });
+ }
+ },
+
+ get_account_opening_balance(frm) {
+ if (frm.doc.bank_account && frm.doc.bank_statement_from_date) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
+ args: {
+ bank_account: frm.doc.bank_account,
+ till_date: frm.doc.bank_statement_from_date,
+ },
+ callback: (response) => {
+ frm.set_value("account_opening_balance", response.message);
+ },
+ });
+ }
+ },
+
+ get_cleared_balance(frm) {
+ if (frm.doc.bank_account && frm.doc.bank_statement_to_date) {
+ return frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
+ args: {
+ bank_account: frm.doc.bank_account,
+ till_date: frm.doc.bank_statement_to_date,
+ },
+ callback: (response) => {
+ frm.cleared_balance = response.message;
+ },
+ });
+ }
+ },
+
+ render_chart(frm) {
+ frm.cards_manager = new erpnext.accounts.bank_reconciliation.NumberCardManager(
+ {
+ $reconciliation_tool_cards: frm.get_field(
+ "reconciliation_tool_cards"
+ ).$wrapper,
+ bank_statement_closing_balance:
+ frm.doc.bank_statement_closing_balance,
+ cleared_balance: frm.cleared_balance,
+ currency: frm.currency,
+ }
+ );
+ },
+
+ render(frm) {
+ if (frm.doc.bank_account) {
+ frm.bank_reconciliation_data_table_manager = new erpnext.accounts.bank_reconciliation.DataTableManager(
+ {
+ company: frm.doc.company,
+ bank_account: frm.doc.bank_account,
+ $reconciliation_tool_dt: frm.get_field(
+ "reconciliation_tool_dt"
+ ).$wrapper,
+ $no_bank_transactions: frm.get_field(
+ "no_bank_transactions"
+ ).$wrapper,
+ bank_statement_from_date: frm.doc.bank_statement_from_date,
+ bank_statement_to_date: frm.doc.bank_statement_to_date,
+ bank_statement_closing_balance:
+ frm.doc.bank_statement_closing_balance,
+ cards_manager: frm.cards_manager,
+ }
+ );
+ }
+ },
+});
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json
new file mode 100644
index 0000000..4837db3
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.json
@@ -0,0 +1,113 @@
+{
+ "actions": [],
+ "creation": "2020-12-02 10:13:02.148040",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "bank_account",
+ "column_break_1",
+ "bank_statement_from_date",
+ "bank_statement_to_date",
+ "column_break_2",
+ "account_opening_balance",
+ "bank_statement_closing_balance",
+ "section_break_1",
+ "reconciliation_tool_cards",
+ "reconciliation_tool_dt",
+ "no_bank_transactions"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "fieldname": "bank_account",
+ "fieldtype": "Link",
+ "label": "Bank Account",
+ "options": "Bank Account"
+ },
+ {
+ "fieldname": "column_break_1",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: doc.bank_account",
+ "fieldname": "bank_statement_from_date",
+ "fieldtype": "Date",
+ "label": "Bank Statement From Date"
+ },
+ {
+ "depends_on": "eval: doc.bank_statement_from_date",
+ "fieldname": "bank_statement_to_date",
+ "fieldtype": "Date",
+ "label": "Bank Statement To Date"
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: doc.bank_statement_from_date",
+ "fieldname": "account_opening_balance",
+ "fieldtype": "Currency",
+ "label": "Account Opening Balance",
+ "options": "Currency",
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.bank_statement_to_date",
+ "fieldname": "bank_statement_closing_balance",
+ "fieldtype": "Currency",
+ "label": "Bank Statement Closing Balance",
+ "options": "Currency"
+ },
+ {
+ "depends_on": "eval: doc.bank_statement_closing_balance",
+ "fieldname": "section_break_1",
+ "fieldtype": "Section Break",
+ "label": "Reconcile"
+ },
+ {
+ "fieldname": "reconciliation_tool_cards",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "reconciliation_tool_dt",
+ "fieldtype": "HTML"
+ },
+ {
+ "fieldname": "no_bank_transactions",
+ "fieldtype": "HTML",
+ "options": "<div class=\"text-muted text-center\">No Matching Bank Transactions Found</div>"
+ }
+ ],
+ "hide_toolbar": 1,
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-02-02 01:35:53.043578",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Bank Reconciliation Tool",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
new file mode 100644
index 0000000..8a17233
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/bank_reconciliation_tool.py
@@ -0,0 +1,452 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import json
+
+import frappe
+from frappe.model.document import Document
+from frappe import _
+from frappe.utils import flt
+
+from erpnext import get_company_currency
+from erpnext.accounts.utils import get_balance_on
+from erpnext.accounts.report.bank_reconciliation_statement.bank_reconciliation_statement import get_entries, get_amounts_not_reflected_in_system
+from erpnext.accounts.doctype.bank_transaction.bank_transaction import get_paid_amount
+
+
+class BankReconciliationTool(Document):
+ pass
+
+@frappe.whitelist()
+def get_bank_transactions(bank_account, from_date = None, to_date = None):
+ # returns bank transactions for a bank account
+ filters = []
+ filters.append(['bank_account', '=', bank_account])
+ filters.append(['docstatus', '=', 1])
+ filters.append(['unallocated_amount', '>', 0])
+ if to_date:
+ filters.append(['date', '<=', to_date])
+ if from_date:
+ filters.append(['date', '>=', from_date])
+ transactions = frappe.get_all(
+ 'Bank Transaction',
+ fields = ['date', 'deposit', 'withdrawal', 'currency',
+ 'description', 'name', 'bank_account', 'company',
+ 'unallocated_amount', 'reference_number', 'party_type', 'party'],
+ filters = filters
+ )
+ return transactions
+
+@frappe.whitelist()
+def get_account_balance(bank_account, till_date):
+ # returns account balance till the specified date
+ account = frappe.db.get_value('Bank Account', bank_account, 'account')
+ filters = frappe._dict({
+ "account": account,
+ "report_date": till_date,
+ "include_pos_transactions": 1
+ })
+ data = get_entries(filters)
+
+ balance_as_per_system = get_balance_on(filters["account"], filters["report_date"])
+
+ total_debit, total_credit = 0,0
+ for d in data:
+ total_debit += flt(d.debit)
+ total_credit += flt(d.credit)
+
+ amounts_not_reflected_in_system = get_amounts_not_reflected_in_system(filters)
+
+ bank_bal = flt(balance_as_per_system) - flt(total_debit) + flt(total_credit) \
+ + amounts_not_reflected_in_system
+
+ return bank_bal
+
+
+@frappe.whitelist()
+def update_bank_transaction(bank_transaction_name, reference_number, party_type=None, party=None):
+ # updates bank transaction based on the new parameters provided by the user from Vouchers
+ bank_transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
+ bank_transaction.reference_number = reference_number
+ bank_transaction.party_type = party_type
+ bank_transaction.party = party
+ bank_transaction.save()
+ return frappe.db.get_all('Bank Transaction',
+ filters={
+ 'name': bank_transaction_name
+ },
+ fields=['date', 'deposit', 'withdrawal', 'currency',
+ 'description', 'name', 'bank_account', 'company',
+ 'unallocated_amount', 'reference_number',
+ 'party_type', 'party'],
+ )[0]
+
+
+@frappe.whitelist()
+def create_journal_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, posting_date=None, entry_type=None,
+ second_account=None, mode_of_payment=None, party_type=None, party=None, allow_edit=None):
+ # Create a new journal entry based on the bank transaction
+ bank_transaction = frappe.db.get_values(
+ "Bank Transaction", bank_transaction_name,
+ fieldname=["name", "deposit", "withdrawal", "bank_account"] ,
+ as_dict=True
+ )[0]
+ company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
+ account_type = frappe.db.get_value("Account", second_account, "account_type")
+ if account_type in ["Receivable", "Payable"]:
+ if not (party_type and party):
+ frappe.throw(_("Party Type and Party is required for Receivable / Payable account {0}").format( second_account))
+ accounts = []
+ # Multi Currency?
+ accounts.append({
+ "account": second_account,
+ "credit_in_account_currency": bank_transaction.deposit
+ if bank_transaction.deposit > 0
+ else 0,
+ "debit_in_account_currency":bank_transaction.withdrawal
+ if bank_transaction.withdrawal > 0
+ else 0,
+ "party_type":party_type,
+ "party":party,
+ })
+
+ accounts.append({
+ "account": company_account,
+ "bank_account": bank_transaction.bank_account,
+ "credit_in_account_currency": bank_transaction.withdrawal
+ if bank_transaction.withdrawal > 0
+ else 0,
+ "debit_in_account_currency":bank_transaction.deposit
+ if bank_transaction.deposit > 0
+ else 0,
+ })
+
+ company = frappe.get_value("Account", company_account, "company")
+
+ journal_entry_dict = {
+ "voucher_type" : entry_type,
+ "company" : company,
+ "posting_date" : posting_date,
+ "cheque_date" : reference_date,
+ "cheque_no" : reference_number,
+ "mode_of_payment" : mode_of_payment
+ }
+ journal_entry = frappe.new_doc('Journal Entry')
+ journal_entry.update(journal_entry_dict)
+ journal_entry.set("accounts", accounts)
+
+
+ if allow_edit:
+ return journal_entry
+
+ journal_entry.insert()
+ journal_entry.submit()
+
+ if bank_transaction.deposit > 0:
+ paid_amount = bank_transaction.deposit
+ else:
+ paid_amount = bank_transaction.withdrawal
+
+ vouchers = json.dumps([{
+ "payment_doctype":"Journal Entry",
+ "payment_name":journal_entry.name,
+ "amount":paid_amount}])
+
+ return reconcile_vouchers(bank_transaction.name, vouchers)
+
+@frappe.whitelist()
+def create_payment_entry_bts( bank_transaction_name, reference_number=None, reference_date=None, party_type=None, party=None, posting_date=None,
+ mode_of_payment=None, project=None, cost_center=None, allow_edit=None):
+ # Create a new payment entry based on the bank transaction
+ bank_transaction = frappe.db.get_values(
+ "Bank Transaction", bank_transaction_name,
+ fieldname=["name", "unallocated_amount", "deposit", "bank_account"] ,
+ as_dict=True
+ )[0]
+ paid_amount = bank_transaction.unallocated_amount
+ payment_type = "Receive" if bank_transaction.deposit > 0 else "Pay"
+
+ company_account = frappe.get_value("Bank Account", bank_transaction.bank_account, "account")
+ company = frappe.get_value("Account", company_account, "company")
+ payment_entry_dict = {
+ "company" : company,
+ "payment_type" : payment_type,
+ "reference_no" : reference_number,
+ "reference_date" : reference_date,
+ "party_type" : party_type,
+ "party" : party,
+ "posting_date" : posting_date,
+ "paid_amount": paid_amount,
+ "received_amount": paid_amount
+ }
+ payment_entry = frappe.new_doc("Payment Entry")
+
+
+ payment_entry.update(payment_entry_dict)
+
+ if mode_of_payment:
+ payment_entry.mode_of_payment = mode_of_payment
+ if project:
+ payment_entry.project = project
+ if cost_center:
+ payment_entry.cost_center = cost_center
+ if payment_type == "Receive":
+ payment_entry.paid_to = company_account
+ else:
+ payment_entry.paid_from = company_account
+
+ payment_entry.validate()
+
+ if allow_edit:
+ return payment_entry
+
+ payment_entry.insert()
+
+ payment_entry.submit()
+ vouchers = json.dumps([{
+ "payment_doctype":"Payment Entry",
+ "payment_name":payment_entry.name,
+ "amount":paid_amount}])
+ return reconcile_vouchers(bank_transaction.name, vouchers)
+
+@frappe.whitelist()
+def reconcile_vouchers(bank_transaction_name, vouchers):
+ # updated clear date of all the vouchers based on the bank transaction
+ vouchers = json.loads(vouchers)
+ transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
+ if transaction.unallocated_amount == 0:
+ frappe.throw(_("This bank transaction is already fully reconciled"))
+ total_amount = 0
+ for voucher in vouchers:
+ voucher['payment_entry'] = frappe.get_doc(voucher['payment_doctype'], voucher['payment_name'])
+ total_amount += get_paid_amount(frappe._dict({
+ 'payment_document': voucher['payment_doctype'],
+ 'payment_entry': voucher['payment_name'],
+ }), transaction.currency)
+
+ if total_amount > transaction.unallocated_amount:
+ frappe.throw(_("The Sum Total of Amounts of All Selected Vouchers Should be Less than the Unallocated Amount of the Bank Transaction"))
+ account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
+
+ for voucher in vouchers:
+ gl_entry = frappe.db.get_value("GL Entry", dict(account=account, voucher_type=voucher['payment_doctype'], voucher_no=voucher['payment_name']), ['credit', 'debit'], as_dict=1)
+ gl_amount, transaction_amount = (gl_entry.credit, transaction.deposit) if gl_entry.credit > 0 else (gl_entry.debit, transaction.withdrawal)
+ allocated_amount = gl_amount if gl_amount >= transaction_amount else transaction_amount
+
+ transaction.append("payment_entries", {
+ "payment_document": voucher['payment_entry'].doctype,
+ "payment_entry": voucher['payment_entry'].name,
+ "allocated_amount": allocated_amount
+ })
+
+ transaction.save()
+ transaction.update_allocations()
+ return frappe.get_doc("Bank Transaction", bank_transaction_name)
+
+@frappe.whitelist()
+def get_linked_payments(bank_transaction_name, document_types = None):
+ # get all matching payments for a bank transaction
+ transaction = frappe.get_doc("Bank Transaction", bank_transaction_name)
+ bank_account = frappe.db.get_values(
+ "Bank Account",
+ transaction.bank_account,
+ ["account", "company"],
+ as_dict=True)[0]
+ (account, company) = (bank_account.account, bank_account.company)
+ matching = check_matching(account, company, transaction, document_types)
+ return matching
+
+def check_matching(bank_account, company, transaction, document_types):
+ # combine all types of vocuhers
+ subquery = get_queries(bank_account, company, transaction, document_types)
+ filters = {
+ "amount": transaction.unallocated_amount,
+ "payment_type" : "Receive" if transaction.deposit > 0 else "Pay",
+ "reference_no": transaction.reference_number,
+ "party_type": transaction.party_type,
+ "party": transaction.party,
+ "bank_account": bank_account
+ }
+
+ matching_vouchers = []
+ for query in subquery:
+ matching_vouchers.extend(
+ frappe.db.sql(query, filters,)
+ )
+
+ return sorted(matching_vouchers, key = lambda x: x[0], reverse=True) if matching_vouchers else []
+
+def get_queries(bank_account, company, transaction, document_types):
+ # get queries to get matching vouchers
+ amount_condition = "=" if "exact_match" in document_types else "<="
+ account_from_to = "paid_to" if transaction.deposit > 0 else "paid_from"
+ queries = []
+
+ if "payment_entry" in document_types:
+ pe_amount_matching = get_pe_matching_query(amount_condition, account_from_to, transaction)
+ queries.extend([pe_amount_matching])
+
+ if "journal_entry" in document_types:
+ je_amount_matching = get_je_matching_query(amount_condition, transaction)
+ queries.extend([je_amount_matching])
+
+ if transaction.deposit > 0 and "sales_invoice" in document_types:
+ si_amount_matching = get_si_matching_query(amount_condition)
+ queries.extend([si_amount_matching])
+
+ if transaction.withdrawal > 0:
+ if "purchase_invoice" in document_types:
+ pi_amount_matching = get_pi_matching_query(amount_condition)
+ queries.extend([pi_amount_matching])
+
+ if "expense_claim" in document_types:
+ ec_amount_matching = get_ec_matching_query(bank_account, company, amount_condition)
+ queries.extend([ec_amount_matching])
+
+ return queries
+
+def get_pe_matching_query(amount_condition, account_from_to, transaction):
+ # get matching payment entries query
+ if transaction.deposit > 0:
+ currency_field = "paid_to_account_currency as currency"
+ else:
+ currency_field = "paid_from_account_currency as currency"
+ return f"""
+ SELECT
+ (CASE WHEN reference_no=%(reference_no)s THEN 1 ELSE 0 END
+ + CASE WHEN (party_type = %(party_type)s AND party = %(party)s ) THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Payment Entry' as doctype,
+ name,
+ paid_amount,
+ reference_no,
+ reference_date,
+ party,
+ party_type,
+ posting_date,
+ {currency_field}
+ FROM
+ `tabPayment Entry`
+ WHERE
+ paid_amount {amount_condition} %(amount)s
+ AND docstatus = 1
+ AND payment_type IN (%(payment_type)s, 'Internal Transfer')
+ AND ifnull(clearance_date, '') = ""
+ AND {account_from_to} = %(bank_account)s
+ """
+
+
+def get_je_matching_query(amount_condition, transaction):
+ # get matching journal entry query
+ cr_or_dr = "credit" if transaction.withdrawal > 0 else "debit"
+ return f"""
+
+ SELECT
+ (CASE WHEN je.cheque_no=%(reference_no)s THEN 1 ELSE 0 END
+ + 1) AS rank ,
+ 'Journal Entry' as doctype,
+ je.name,
+ jea.{cr_or_dr}_in_account_currency as paid_amount,
+ je.cheque_no as reference_no,
+ je.cheque_date as reference_date,
+ je.pay_to_recd_from as party,
+ jea.party_type,
+ je.posting_date,
+ jea.account_currency as currency
+ FROM
+ `tabJournal Entry Account` as jea
+ JOIN
+ `tabJournal Entry` as je
+ ON
+ jea.parent = je.name
+ WHERE
+ (je.clearance_date is null or je.clearance_date='0000-00-00')
+ AND jea.account = %(bank_account)s
+ AND jea.{cr_or_dr}_in_account_currency {amount_condition} %(amount)s
+ AND je.docstatus = 1
+ """
+
+
+def get_si_matching_query(amount_condition):
+ # get matchin sales invoice query
+ return f"""
+ SELECT
+ ( CASE WHEN si.customer = %(party)s THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Sales Invoice' as doctype,
+ si.name,
+ sip.amount as paid_amount,
+ '' as reference_no,
+ '' as reference_date,
+ si.customer as party,
+ 'Customer' as party_type,
+ si.posting_date,
+ si.currency
+
+ FROM
+ `tabSales Invoice Payment` as sip
+ JOIN
+ `tabSales Invoice` as si
+ ON
+ sip.parent = si.name
+ WHERE (sip.clearance_date is null or sip.clearance_date='0000-00-00')
+ AND sip.account = %(bank_account)s
+ AND sip.amount {amount_condition} %(amount)s
+ AND si.docstatus = 1
+ """
+
+def get_pi_matching_query(amount_condition):
+ # get matching purchase invoice query
+ return f"""
+ SELECT
+ ( CASE WHEN supplier = %(party)s THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Purchase Invoice' as doctype,
+ name,
+ paid_amount,
+ '' as reference_no,
+ '' as reference_date,
+ supplier as party,
+ 'Supplier' as party_type,
+ posting_date,
+ currency
+ FROM
+ `tabPurchase Invoice`
+ WHERE
+ paid_amount {amount_condition} %(amount)s
+ AND docstatus = 1
+ AND is_paid = 1
+ AND ifnull(clearance_date, '') = ""
+ AND cash_bank_account = %(bank_account)s
+ """
+
+def get_ec_matching_query(bank_account, company, amount_condition):
+ # get matching Expense Claim query
+ mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account",
+ filters={"default_account": bank_account}, fields=["parent"])]
+ mode_of_payments = '(\'' + '\', \''.join(mode_of_payments) + '\' )'
+ company_currency = get_company_currency(company)
+ return f"""
+ SELECT
+ ( CASE WHEN employee = %(party)s THEN 1 ELSE 0 END
+ + 1 ) AS rank,
+ 'Expense Claim' as doctype,
+ name,
+ total_sanctioned_amount as paid_amount,
+ '' as reference_no,
+ '' as reference_date,
+ employee as party,
+ 'Employee' as party_type,
+ posting_date,
+ '{company_currency}' as currency
+ FROM
+ `tabExpense Claim`
+ WHERE
+ total_sanctioned_amount {amount_condition} %(amount)s
+ AND docstatus = 1
+ AND is_paid = 1
+ AND ifnull(clearance_date, '') = ""
+ AND mode_of_payment in {mode_of_payments}
+ """
diff --git a/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
new file mode 100644
index 0000000..d96950a
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_reconciliation_tool/test_bank_reconciliation_tool.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestBankReconciliationTool(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/accounts/doctype/bank_statement_import/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/bank_statement_settings/__init__.py
rename to erpnext/accounts/doctype/bank_statement_import/__init__.py
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.css b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.css
new file mode 100644
index 0000000..5206540
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.css
@@ -0,0 +1,3 @@
+.warnings .warning {
+ margin-bottom: 40px;
+}
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
new file mode 100644
index 0000000..ad4ff9e
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.js
@@ -0,0 +1,574 @@
+// Copyright (c) 2019, Frappe Technologies and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on("Bank Statement Import", {
+ setup(frm) {
+ frappe.realtime.on("data_import_refresh", ({ data_import }) => {
+ frm.import_in_progress = false;
+ if (data_import !== frm.doc.name) return;
+ frappe.model.clear_doc("Bank Statement Import", frm.doc.name);
+ frappe.model
+ .with_doc("Bank Statement Import", frm.doc.name)
+ .then(() => {
+ frm.refresh();
+ });
+ });
+ frappe.realtime.on("data_import_progress", (data) => {
+ frm.import_in_progress = true;
+ if (data.data_import !== frm.doc.name) {
+ return;
+ }
+ let percent = Math.floor((data.current * 100) / data.total);
+ let seconds = Math.floor(data.eta);
+ let minutes = Math.floor(data.eta / 60);
+ let eta_message =
+ // prettier-ignore
+ seconds < 60
+ ? __('About {0} seconds remaining', [seconds])
+ : minutes === 1
+ ? __('About {0} minute remaining', [minutes])
+ : __('About {0} minutes remaining', [minutes]);
+
+ let message;
+ if (data.success) {
+ let message_args = [data.current, data.total, eta_message];
+ message =
+ frm.doc.import_type === "Insert New Records"
+ ? __("Importing {0} of {1}, {2}", message_args)
+ : __("Updating {0} of {1}, {2}", message_args);
+ }
+ if (data.skipping) {
+ message = __(
+ "Skipping {0} of {1}, {2}",
+ [
+ data.current,
+ data.total,
+ eta_message,
+ ]
+ );
+ }
+ frm.dashboard.show_progress(
+ __("Import Progress"),
+ percent,
+ message
+ );
+ frm.page.set_indicator(__("In Progress"), "orange");
+
+ // hide progress when complete
+ if (data.current === data.total) {
+ setTimeout(() => {
+ frm.dashboard.hide();
+ frm.refresh();
+ }, 2000);
+ }
+ });
+
+ frm.set_query("reference_doctype", () => {
+ return {
+ filters: {
+ name: ["in", frappe.boot.user.can_import],
+ },
+ };
+ });
+
+ frm.get_field("import_file").df.options = {
+ restrictions: {
+ allowed_file_types: [".csv", ".xls", ".xlsx"],
+ },
+ };
+
+ frm.has_import_file = () => {
+ return frm.doc.import_file || frm.doc.google_sheets_url;
+ };
+ },
+
+ refresh(frm) {
+ frm.page.hide_icon_group();
+ frm.trigger("update_indicators");
+ frm.trigger("import_file");
+ frm.trigger("show_import_log");
+ frm.trigger("show_import_warnings");
+ frm.trigger("toggle_submit_after_import");
+ frm.trigger("show_import_status");
+ frm.trigger("show_report_error_button");
+
+ if (frm.doc.status === "Partial Success") {
+ frm.add_custom_button(__("Export Errored Rows"), () =>
+ frm.trigger("export_errored_rows")
+ );
+ }
+
+ if (frm.doc.status.includes("Success")) {
+ frm.add_custom_button(
+ __("Go to {0} List", [frm.doc.reference_doctype]),
+ () => frappe.set_route("List", frm.doc.reference_doctype)
+ );
+ }
+ },
+
+ onload_post_render(frm) {
+ frm.trigger("update_primary_action");
+ },
+
+ update_primary_action(frm) {
+ if (frm.is_dirty()) {
+ frm.enable_save();
+ return;
+ }
+ frm.disable_save();
+ if (frm.doc.status !== "Success") {
+ if (!frm.is_new() && frm.has_import_file()) {
+ let label =
+ frm.doc.status === "Pending"
+ ? __("Start Import")
+ : __("Retry");
+ frm.page.set_primary_action(label, () =>
+ frm.events.start_import(frm)
+ );
+ } else {
+ frm.page.set_primary_action(__("Save"), () => frm.save());
+ }
+ }
+ },
+
+ update_indicators(frm) {
+ const indicator = frappe.get_indicator(frm.doc);
+ if (indicator) {
+ frm.page.set_indicator(indicator[0], indicator[1]);
+ } else {
+ frm.page.clear_indicator();
+ }
+ },
+
+ show_import_status(frm) {
+ let import_log = JSON.parse(frm.doc.import_log || "[]");
+ let successful_records = import_log.filter((log) => log.success);
+ let failed_records = import_log.filter((log) => !log.success);
+ if (successful_records.length === 0) return;
+
+ let message;
+ if (failed_records.length === 0) {
+ let message_args = [successful_records.length];
+ if (frm.doc.import_type === "Insert New Records") {
+ message =
+ successful_records.length > 1
+ ? __("Successfully imported {0} records.", message_args)
+ : __("Successfully imported {0} record.", message_args);
+ } else {
+ message =
+ successful_records.length > 1
+ ? __("Successfully updated {0} records.", message_args)
+ : __("Successfully updated {0} record.", message_args);
+ }
+ } else {
+ let message_args = [successful_records.length, import_log.length];
+ if (frm.doc.import_type === "Insert New Records") {
+ message =
+ successful_records.length > 1
+ ? __(
+ "Successfully imported {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
+ message_args
+ )
+ : __(
+ "Successfully imported {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",
+ message_args
+ );
+ } else {
+ message =
+ successful_records.length > 1
+ ? __(
+ "Successfully updated {0} records out of {1}. Click on Export Errored Rows, fix the errors and import again.",
+ message_args
+ )
+ : __(
+ "Successfully updated {0} record out of {1}. Click on Export Errored Rows, fix the errors and import again.",
+ message_args
+ );
+ }
+ }
+ frm.dashboard.set_headline(message);
+ },
+
+ show_report_error_button(frm) {
+ if (frm.doc.status === "Error") {
+ frappe.db
+ .get_list("Error Log", {
+ filters: { method: frm.doc.name },
+ fields: ["method", "error"],
+ order_by: "creation desc",
+ limit: 1,
+ })
+ .then((result) => {
+ if (result.length > 0) {
+ frm.add_custom_button("Report Error", () => {
+ let fake_xhr = {
+ responseText: JSON.stringify({
+ exc: result[0].error,
+ }),
+ };
+ frappe.request.report_error(fake_xhr, {});
+ });
+ }
+ });
+ }
+ },
+
+ start_import(frm) {
+ frm.call({
+ method: "form_start_import",
+ args: { data_import: frm.doc.name },
+ btn: frm.page.btn_primary,
+ }).then((r) => {
+ if (r.message === true) {
+ frm.disable_save();
+ }
+ });
+ },
+
+ download_template() {
+ let method =
+ "/api/method/frappe.core.doctype.data_import.data_import.download_template";
+
+ open_url_post(method, {
+ doctype: "Bank Transaction",
+ export_records: "5_records",
+ export_fields: {
+ "Bank Transaction": [
+ "date",
+ "deposit",
+ "withdrawal",
+ "description",
+ "reference_number",
+ ],
+ },
+ });
+ },
+
+ reference_doctype(frm) {
+ frm.trigger("toggle_submit_after_import");
+ },
+
+ toggle_submit_after_import(frm) {
+ frm.toggle_display("submit_after_import", false);
+ let doctype = frm.doc.reference_doctype;
+ if (doctype) {
+ frappe.model.with_doctype(doctype, () => {
+ let meta = frappe.get_meta(doctype);
+ frm.toggle_display("submit_after_import", meta.is_submittable);
+ });
+ }
+ },
+
+ google_sheets_url(frm) {
+ if (!frm.is_dirty()) {
+ frm.trigger("import_file");
+ } else {
+ frm.trigger("update_primary_action");
+ }
+ },
+
+ refresh_google_sheet(frm) {
+ frm.trigger("import_file");
+ },
+
+ import_file(frm) {
+ frm.toggle_display("section_import_preview", frm.has_import_file());
+ if (!frm.has_import_file()) {
+ frm.get_field("import_preview").$wrapper.empty();
+ return;
+ } else {
+ frm.trigger("update_primary_action");
+ }
+
+ // load import preview
+ frm.get_field("import_preview").$wrapper.empty();
+ $('<span class="text-muted">')
+ .html(__("Loading import file..."))
+ .appendTo(frm.get_field("import_preview").$wrapper);
+
+ frm.call({
+ method: "get_preview_from_template",
+ args: {
+ data_import: frm.doc.name,
+ import_file: frm.doc.import_file,
+ google_sheets_url: frm.doc.google_sheets_url,
+ },
+ error_handlers: {
+ TimestampMismatchError() {
+ // ignore this error
+ },
+ },
+ }).then((r) => {
+ let preview_data = r.message;
+ frm.events.show_import_preview(frm, preview_data);
+ frm.events.show_import_warnings(frm, preview_data);
+ });
+ },
+ // method: 'frappe.core.doctype.data_import.data_import.get_preview_from_template',
+
+ show_import_preview(frm, preview_data) {
+ let import_log = JSON.parse(frm.doc.import_log || "[]");
+
+ if (
+ frm.import_preview &&
+ frm.import_preview.doctype === frm.doc.reference_doctype
+ ) {
+ frm.import_preview.preview_data = preview_data;
+ frm.import_preview.import_log = import_log;
+ frm.import_preview.refresh();
+ return;
+ }
+
+ frappe.require("/assets/js/data_import_tools.min.js", () => {
+ frm.import_preview = new frappe.data_import.ImportPreview({
+ wrapper: frm.get_field("import_preview").$wrapper,
+ doctype: frm.doc.reference_doctype,
+ preview_data,
+ import_log,
+ frm,
+ events: {
+ remap_column(changed_map) {
+ let template_options = JSON.parse(
+ frm.doc.template_options || "{}"
+ );
+ template_options.column_to_field_map =
+ template_options.column_to_field_map || {};
+ Object.assign(
+ template_options.column_to_field_map,
+ changed_map
+ );
+ frm.set_value(
+ "template_options",
+ JSON.stringify(template_options)
+ );
+ frm.save().then(() => frm.trigger("import_file"));
+ },
+ },
+ });
+ });
+ },
+
+ export_errored_rows(frm) {
+ open_url_post(
+ "/api/method/frappe.core.doctype.data_import.data_import.download_errored_template",
+ {
+ data_import_name: frm.doc.name,
+ }
+ );
+ },
+
+ show_import_warnings(frm, preview_data) {
+ let columns = preview_data.columns;
+ let warnings = JSON.parse(frm.doc.template_warnings || "[]");
+ warnings = warnings.concat(preview_data.warnings || []);
+
+ frm.toggle_display("import_warnings_section", warnings.length > 0);
+ if (warnings.length === 0) {
+ frm.get_field("import_warnings").$wrapper.html("");
+ return;
+ }
+
+ // group warnings by row
+ let warnings_by_row = {};
+ let other_warnings = [];
+ for (let warning of warnings) {
+ if (warning.row) {
+ warnings_by_row[warning.row] =
+ warnings_by_row[warning.row] || [];
+ warnings_by_row[warning.row].push(warning);
+ } else {
+ other_warnings.push(warning);
+ }
+ }
+
+ let html = "";
+ html += Object.keys(warnings_by_row)
+ .map((row_number) => {
+ let message = warnings_by_row[row_number]
+ .map((w) => {
+ if (w.field) {
+ let label =
+ w.field.label +
+ (w.field.parent !== frm.doc.reference_doctype
+ ? ` (${w.field.parent})`
+ : "");
+ return `<li>${label}: ${w.message}</li>`;
+ }
+ return `<li>${w.message}</li>`;
+ })
+ .join("");
+ return `
+ <div class="warning" data-row="${row_number}">
+ <h5 class="text-uppercase">${__("Row {0}", [row_number])}</h5>
+ <div class="body"><ul>${message}</ul></div>
+ </div>
+ `;
+ })
+ .join("");
+
+ html += other_warnings
+ .map((warning) => {
+ let header = "";
+ if (warning.col) {
+ let column_number = `<span class="text-uppercase">${__(
+ "Column {0}",
+ [warning.col]
+ )}</span>`;
+ let column_header = columns[warning.col].header_title;
+ header = `${column_number} (${column_header})`;
+ }
+ return `
+ <div class="warning" data-col="${warning.col}">
+ <h5>${header}</h5>
+ <div class="body">${warning.message}</div>
+ </div>
+ `;
+ })
+ .join("");
+ frm.get_field("import_warnings").$wrapper.html(`
+ <div class="row">
+ <div class="col-sm-10 warnings">${html}</div>
+ </div>
+ `);
+ },
+
+ show_failed_logs(frm) {
+ frm.trigger("show_import_log");
+ },
+
+ show_import_log(frm) {
+ let import_log = JSON.parse(frm.doc.import_log || "[]");
+ let logs = import_log;
+ frm.toggle_display("import_log", false);
+ frm.toggle_display("import_log_section", logs.length > 0);
+
+ if (logs.length === 0) {
+ frm.get_field("import_log_preview").$wrapper.empty();
+ return;
+ }
+
+ let rows = logs
+ .map((log) => {
+ let html = "";
+ if (log.success) {
+ if (frm.doc.import_type === "Insert New Records") {
+ html = __(
+ "Successfully imported {0}", [
+ `<span class="underline">${frappe.utils.get_form_link(
+ frm.doc.reference_doctype,
+ log.docname,
+ true
+ )}<span>`,
+ ]
+ );
+ } else {
+ html = __(
+ "Successfully updated {0}", [
+ `<span class="underline">${frappe.utils.get_form_link(
+ frm.doc.reference_doctype,
+ log.docname,
+ true
+ )}<span>`,
+ ]
+ );
+ }
+ } else {
+ let messages = log.messages
+ .map(JSON.parse)
+ .map((m) => {
+ let title = m.title
+ ? `<strong>${m.title}</strong>`
+ : "";
+ let message = m.message
+ ? `<div>${m.message}</div>`
+ : "";
+ return title + message;
+ })
+ .join("");
+ let id = frappe.dom.get_unique_id();
+ html = `${messages}
+ <button class="btn btn-default btn-xs" type="button" data-toggle="collapse" data-target="#${id}" aria-expanded="false" aria-controls="${id}" style="margin-top: 15px;">
+ ${__("Show Traceback")}
+ </button>
+ <div class="collapse" id="${id}" style="margin-top: 15px;">
+ <div class="well">
+ <pre>${log.exception}</pre>
+ </div>
+ </div>`;
+ }
+ let indicator_color = log.success ? "green" : "red";
+ let title = log.success ? __("Success") : __("Failure");
+
+ if (frm.doc.show_failed_logs && log.success) {
+ return "";
+ }
+
+ return `<tr>
+ <td>${log.row_indexes.join(", ")}</td>
+ <td>
+ <div class="indicator ${indicator_color}">${title}</div>
+ </td>
+ <td>
+ ${html}
+ </td>
+ </tr>`;
+ })
+ .join("");
+
+ if (!rows && frm.doc.show_failed_logs) {
+ rows = `<tr><td class="text-center text-muted" colspan=3>
+ ${__("No failed logs")}
+ </td></tr>`;
+ }
+
+ frm.get_field("import_log_preview").$wrapper.html(`
+ <table class="table table-bordered">
+ <tr class="text-muted">
+ <th width="10%">${__("Row Number")}</th>
+ <th width="10%">${__("Status")}</th>
+ <th width="80%">${__("Message")}</th>
+ </tr>
+ ${rows}
+ </table>
+ `);
+ },
+
+ show_missing_link_values(frm, missing_link_values) {
+ let can_be_created_automatically = missing_link_values.every(
+ (d) => d.has_one_mandatory_field
+ );
+
+ let html = missing_link_values
+ .map((d) => {
+ let doctype = d.doctype;
+ let values = d.missing_values;
+ return `
+ <h5>${doctype}</h5>
+ <ul>${values.map((v) => `<li>${v}</li>`).join("")}</ul>
+ `;
+ })
+ .join("");
+
+ if (can_be_created_automatically) {
+ // prettier-ignore
+ let message = __('There are some linked records which needs to be created before we can import your file. Do you want to create the following missing records automatically?');
+ frappe.confirm(message + html, () => {
+ frm.call("create_missing_link_values", {
+ missing_link_values,
+ }).then((r) => {
+ let records = r.message;
+ frappe.msgprint(__(
+ "Created {0} records successfully.", [
+ records.length,
+ ]
+ ));
+ });
+ });
+ } else {
+ frappe.msgprint(
+ // prettier-ignore
+ __('The following records needs to be created before we can import your file.') + html
+ );
+ }
+ },
+});
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json
new file mode 100644
index 0000000..5e913cc
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.json
@@ -0,0 +1,227 @@
+{
+ "actions": [],
+ "autoname": "format:Bank Statement Import on {creation}",
+ "beta": 1,
+ "creation": "2019-08-04 14:16:08.318714",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "company",
+ "bank_account",
+ "bank",
+ "column_break_4",
+ "google_sheets_url",
+ "refresh_google_sheet",
+ "html_5",
+ "import_file",
+ "download_template",
+ "status",
+ "template_options",
+ "import_warnings_section",
+ "template_warnings",
+ "import_warnings",
+ "section_import_preview",
+ "import_preview",
+ "import_log_section",
+ "import_log",
+ "show_failed_logs",
+ "import_log_preview",
+ "reference_doctype",
+ "import_type",
+ "submit_after_import",
+ "mute_emails"
+ ],
+ "fields": [
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "bank_account",
+ "fieldtype": "Link",
+ "label": "Bank Account",
+ "options": "Bank Account",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "depends_on": "eval:doc.bank_account",
+ "fetch_from": "bank_account.bank",
+ "fieldname": "bank",
+ "fieldtype": "Link",
+ "label": "Bank",
+ "options": "Bank",
+ "read_only": 1,
+ "set_only_once": 1
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "download_template",
+ "fieldtype": "Button",
+ "label": "Download Template"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "import_file",
+ "fieldtype": "Attach",
+ "in_list_view": 1,
+ "label": "Import File"
+ },
+ {
+ "fieldname": "import_preview",
+ "fieldtype": "HTML",
+ "label": "Import Preview"
+ },
+ {
+ "fieldname": "section_import_preview",
+ "fieldtype": "Section Break",
+ "label": "Preview"
+ },
+ {
+ "fieldname": "template_options",
+ "fieldtype": "Code",
+ "hidden": 1,
+ "label": "Template Options",
+ "options": "JSON",
+ "read_only": 1
+ },
+ {
+ "fieldname": "import_log",
+ "fieldtype": "Code",
+ "label": "Import Log",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "import_log_section",
+ "fieldtype": "Section Break",
+ "label": "Import Log"
+ },
+ {
+ "fieldname": "import_log_preview",
+ "fieldtype": "HTML",
+ "label": "Import Log Preview"
+ },
+ {
+ "default": "Pending",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "label": "Status",
+ "options": "Pending\nSuccess\nPartial Success\nError",
+ "read_only": 1
+ },
+ {
+ "fieldname": "template_warnings",
+ "fieldtype": "Code",
+ "hidden": 1,
+ "label": "Template Warnings",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "import_warnings_section",
+ "fieldtype": "Section Break",
+ "label": "Import File Errors and Warnings"
+ },
+ {
+ "fieldname": "import_warnings",
+ "fieldtype": "HTML",
+ "label": "Import Warnings"
+ },
+ {
+ "default": "0",
+ "fieldname": "show_failed_logs",
+ "fieldtype": "Check",
+ "label": "Show Failed Logs"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal && !doc.import_file",
+ "fieldname": "html_5",
+ "fieldtype": "HTML",
+ "options": "<h5 class=\"text-muted uppercase\">Or</h5>"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal && !doc.import_file\n",
+ "description": "Must be a publicly accessible Google Sheets URL",
+ "fieldname": "google_sheets_url",
+ "fieldtype": "Data",
+ "label": "Import from Google Sheets"
+ },
+ {
+ "depends_on": "eval:doc.google_sheets_url && !doc.__unsaved",
+ "fieldname": "refresh_google_sheet",
+ "fieldtype": "Button",
+ "label": "Refresh Google Sheet"
+ },
+ {
+ "default": "Bank Transaction",
+ "fieldname": "reference_doctype",
+ "fieldtype": "Link",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "Document Type",
+ "options": "DocType",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "default": "Insert New Records",
+ "fieldname": "import_type",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "in_list_view": 1,
+ "label": "Import Type",
+ "options": "\nInsert New Records\nUpdate Existing Records",
+ "reqd": 1,
+ "set_only_once": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "submit_after_import",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Submit After Import",
+ "set_only_once": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "mute_emails",
+ "fieldtype": "Check",
+ "hidden": 1,
+ "label": "Don't Send Emails",
+ "set_only_once": 1
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "hide_toolbar": 1,
+ "links": [],
+ "modified": "2021-02-10 19:29:59.027325",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Bank Statement Import",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
new file mode 100644
index 0000000..9f41b13
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import.py
@@ -0,0 +1,205 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2019, Frappe Technologies and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import csv
+import json
+import re
+
+import openpyxl
+from openpyxl.styles import Font
+from openpyxl.utils import get_column_letter
+from six import string_types
+
+import frappe
+from frappe.core.doctype.data_import.importer import Importer, ImportFile
+from frappe.utils.background_jobs import enqueue
+from frappe.utils.xlsxutils import handle_html, ILLEGAL_CHARACTERS_RE
+from frappe import _
+
+from frappe.core.doctype.data_import.data_import import DataImport
+
+class BankStatementImport(DataImport):
+ def __init__(self, *args, **kwargs):
+ super(BankStatementImport, self).__init__(*args, **kwargs)
+
+ def validate(self):
+ doc_before_save = self.get_doc_before_save()
+ if (
+ not (self.import_file or self.google_sheets_url)
+ or (doc_before_save and doc_before_save.import_file != self.import_file)
+ or (doc_before_save and doc_before_save.google_sheets_url != self.google_sheets_url)
+ ):
+
+ template_options_dict = {}
+ column_to_field_map = {}
+ bank = frappe.get_doc("Bank", self.bank)
+ for i in bank.bank_transaction_mapping:
+ column_to_field_map[i.file_field] = i.bank_transaction_field
+ template_options_dict["column_to_field_map"] = column_to_field_map
+ self.template_options = json.dumps(template_options_dict)
+
+ self.template_warnings = ""
+
+ self.validate_import_file()
+ self.validate_google_sheets_url()
+
+ def start_import(self):
+
+ from frappe.core.page.background_jobs.background_jobs import get_info
+ from frappe.utils.scheduler import is_scheduler_inactive
+
+ if is_scheduler_inactive() and not frappe.flags.in_test:
+ frappe.throw(
+ _("Scheduler is inactive. Cannot import data."), title=_("Scheduler Inactive")
+ )
+
+ enqueued_jobs = [d.get("job_name") for d in get_info()]
+
+ if self.name not in enqueued_jobs:
+ enqueue(
+ start_import,
+ queue="default",
+ timeout=6000,
+ event="data_import",
+ job_name=self.name,
+ data_import=self.name,
+ bank_account=self.bank_account,
+ import_file_path=self.import_file,
+ bank=self.bank,
+ template_options=self.template_options,
+ now=frappe.conf.developer_mode or frappe.flags.in_test,
+ )
+ return True
+
+ return False
+
+@frappe.whitelist()
+def get_preview_from_template(data_import, import_file=None, google_sheets_url=None):
+ return frappe.get_doc("Bank Statement Import", data_import).get_preview_from_template(
+ import_file, google_sheets_url
+ )
+
+@frappe.whitelist()
+def form_start_import(data_import):
+ return frappe.get_doc("Bank Statement Import", data_import).start_import()
+
+@frappe.whitelist()
+def download_errored_template(data_import_name):
+ data_import = frappe.get_doc("Bank Statement Import", data_import_name)
+ data_import.export_errored_rows()
+
+def start_import(data_import, bank_account, import_file_path, bank, template_options):
+ """This method runs in background job"""
+
+ update_mapping_db(bank, template_options)
+
+ data_import = frappe.get_doc("Bank Statement Import", data_import)
+
+ import_file = ImportFile("Bank Transaction", file = import_file_path, import_type="Insert New Records")
+ data = import_file.raw_data
+
+ add_bank_account(data, bank_account)
+ write_files(import_file, data)
+
+ try:
+ i = Importer(data_import.reference_doctype, data_import=data_import)
+ i.import_data()
+ except Exception:
+ frappe.db.rollback()
+ data_import.db_set("status", "Error")
+ frappe.log_error(title=data_import.name)
+ finally:
+ frappe.flags.in_import = False
+
+ frappe.publish_realtime("data_import_refresh", {"data_import": data_import.name})
+
+def update_mapping_db(bank, template_options):
+ bank = frappe.get_doc("Bank", bank)
+ for d in bank.bank_transaction_mapping:
+ d.delete()
+
+ for d in json.loads(template_options)["column_to_field_map"].items():
+ bank.append("bank_transaction_mapping", {"bank_transaction_field": d[1] ,"file_field": d[0]} )
+
+ bank.save()
+
+def add_bank_account(data, bank_account):
+ bank_account_loc = None
+ if "Bank Account" not in data[0]:
+ data[0].append("Bank Account")
+ else:
+ for loc, header in enumerate(data[0]):
+ if header == "Bank Account":
+ bank_account_loc = loc
+
+ for row in data[1:]:
+ if bank_account_loc:
+ row[bank_account_loc] = bank_account
+ else:
+ row.append(bank_account)
+
+def write_files(import_file, data):
+ full_file_path = import_file.file_doc.get_full_path()
+ parts = import_file.file_doc.get_extension()
+ extension = parts[1]
+ extension = extension.lstrip(".")
+
+ if extension == "csv":
+ with open(full_file_path, 'w', newline='') as file:
+ writer = csv.writer(file)
+ writer.writerows(data)
+ elif extension == "xlsx" or "xls":
+ write_xlsx(data, "trans", file_path = full_file_path)
+
+def write_xlsx(data, sheet_name, wb=None, column_widths=None, file_path=None):
+ # from xlsx utils with changes
+ column_widths = column_widths or []
+ if wb is None:
+ wb = openpyxl.Workbook(write_only=True)
+
+ ws = wb.create_sheet(sheet_name, 0)
+
+ for i, column_width in enumerate(column_widths):
+ if column_width:
+ ws.column_dimensions[get_column_letter(i + 1)].width = column_width
+
+ row1 = ws.row_dimensions[1]
+ row1.font = Font(name='Calibri', bold=True)
+
+ for row in data:
+ clean_row = []
+ for item in row:
+ if isinstance(item, string_types) and (sheet_name not in ['Data Import Template', 'Data Export']):
+ value = handle_html(item)
+ else:
+ value = item
+
+ if isinstance(item, string_types) and next(ILLEGAL_CHARACTERS_RE.finditer(value), None):
+ # Remove illegal characters from the string
+ value = re.sub(ILLEGAL_CHARACTERS_RE, '', value)
+
+ clean_row.append(value)
+
+ ws.append(clean_row)
+
+ wb.save(file_path)
+ return True
+
+@frappe.whitelist()
+def upload_bank_statement(**args):
+ args = frappe._dict(args)
+ bsi = frappe.new_doc("Bank Statement Import")
+
+ if args.company:
+ bsi.update({
+ "company": args.company,
+ })
+
+ if args.bank_account:
+ bsi.update({
+ "bank_account": args.bank_account
+ })
+
+ return bsi
diff --git a/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js
new file mode 100644
index 0000000..6c75402
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/bank_statement_import_list.js
@@ -0,0 +1,36 @@
+let imports_in_progress = [];
+
+frappe.listview_settings['Bank Statement Import'] = {
+ onload(listview) {
+ frappe.realtime.on('data_import_progress', data => {
+ if (!imports_in_progress.includes(data.data_import)) {
+ imports_in_progress.push(data.data_import);
+ }
+ });
+ frappe.realtime.on('data_import_refresh', data => {
+ imports_in_progress = imports_in_progress.filter(
+ d => d !== data.data_import
+ );
+ listview.refresh();
+ });
+ },
+ get_indicator: function(doc) {
+ var colors = {
+ 'Pending': 'orange',
+ 'Not Started': 'orange',
+ 'Partial Success': 'orange',
+ 'Success': 'green',
+ 'In Progress': 'orange',
+ 'Error': 'red'
+ };
+ let status = doc.status;
+ if (imports_in_progress.includes(doc.name)) {
+ status = 'In Progress';
+ }
+ if (status == 'Pending') {
+ status = 'Not Started';
+ }
+ return [__(status), colors[status], 'status,=,' + doc.status];
+ },
+ hide_name_column: true
+};
diff --git a/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py b/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py
new file mode 100644
index 0000000..cd58314
--- /dev/null
+++ b/erpnext/accounts/doctype/bank_statement_import/test_bank_statement_import.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestBankStatementImport(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.js b/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.js
deleted file mode 100644
index 46aa4f2..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, sathishpy@gmail.com and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Bank Statement Settings', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.json b/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.json
deleted file mode 100644
index 53fbf7d..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.json
+++ /dev/null
@@ -1,272 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "beta": 0,
- "creation": "2017-11-13 13:38:10.863592",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank",
- "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": "Bank Account",
- "length": 0,
- "no_copy": 0,
- "options": "Bank",
- "permlevel": 0,
- "precision": "",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "'%d/%m/%Y'",
- "fieldname": "date_format",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Date Format",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "statement_header_mapping",
- "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": "Statement Header Mapping",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "header_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": "Statement Headers",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Settings Item",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "transaction_data_mapping",
- "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": "Transaction Data Mapping",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mapped_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": "Mapped Items",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Transaction Settings Item",
- "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
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-04-07 18:57:04.048423",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.py b/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.py
deleted file mode 100644
index 6c4dd1b..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings/bank_statement_settings.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementSettings(Document):
- def autoname(self):
- self.name = self.bank + "-Statement-Settings"
diff --git a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.js b/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.js
deleted file mode 100644
index f2381c0..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Bank Statement Settings", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially([
- // insert a new Bank Statement Settings
- () => frappe.tests.make('Bank Statement Settings', [
- // values to be set
- {key: 'value'}
- ]),
- () => {
- assert.equal(cur_frm.doc.key, 'value');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.py b/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.py
deleted file mode 100644
index aa7fe83..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings/test_bank_statement_settings.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import frappe
-import unittest
-
-class TestBankStatementSettings(unittest.TestCase):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.json b/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.json
deleted file mode 100644
index 7c93f26..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.json
+++ /dev/null
@@ -1,101 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-01-08 00:16:42.762980",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mapped_header",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Mapped Header",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "stmt_header",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Bank Header",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-01-08 00:19:14.841134",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Settings Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py b/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py
deleted file mode 100644
index 9438e9a..0000000
--- a/erpnext/accounts/doctype/bank_statement_settings_item/bank_statement_settings_item.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementSettingsItem(Document):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/__init__.py
+++ /dev/null
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.js b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.js
deleted file mode 100644
index 736ed35..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.js
+++ /dev/null
@@ -1,100 +0,0 @@
-// Copyright (c) 2017, sathishpy@gmail.com and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Bank Statement Transaction Entry', {
- setup: function(frm) {
- frm.events.account_filters(frm)
- frm.events.invoice_filter(frm)
- },
- refresh: function(frm) {
- frm.set_df_property("bank_account", "read_only", frm.doc.__islocal ? 0 : 1);
- frm.set_df_property("from_date", "read_only", frm.doc.__islocal ? 0 : 1);
- frm.set_df_property("to_date", "read_only", frm.doc.__islocal ? 0 : 1);
- },
- invoke_doc_function(frm, method) {
- frappe.call({
- doc: frm.doc,
- method: method,
- callback: function(r) {
- if(!r.exe) {
- frm.refresh_fields();
- }
- }
- });
- },
- account_filters: function(frm) {
- frm.fields_dict['bank_account'].get_query = function(doc, dt, dn) {
- return {
- filters:[
- ["Account", "account_type", "in", ["Bank"]]
- ]
- }
- };
- frm.fields_dict['receivable_account'].get_query = function(doc, dt, dn) {
- return {
- filters: {"account_type": "Receivable"}
- }
- };
- frm.fields_dict['payable_account'].get_query = function(doc, dt, dn) {
- return {
- filters: {"account_type": "Payable"}
- }
- };
- },
-
- invoice_filter: function(frm) {
- frm.set_query("invoice", "payment_invoice_items", function(doc, cdt, cdn) {
- let row = locals[cdt][cdn]
- if (row.party_type == "Customer") {
- return {
- filters:[[row.invoice_type, "customer", "in", [row.party]],
- [row.invoice_type, "status", "!=", "Cancelled" ],
- [row.invoice_type, "posting_date", "<", row.transaction_date ],
- [row.invoice_type, "outstanding_amount", ">", 0 ]]
- }
- } else if (row.party_type == "Supplier") {
- return {
- filters:[[row.invoice_type, "supplier", "in", [row.party]],
- [row.invoice_type, "status", "!=", "Cancelled" ],
- [row.invoice_type, "posting_date", "<", row.transaction_date ],
- [row.invoice_type, "outstanding_amount", ">", 0 ]]
- }
- }
- });
- },
-
- match_invoices: function(frm) {
- frm.events.invoke_doc_function(frm, "populate_matching_invoices");
- },
- create_payments: function(frm) {
- frm.events.invoke_doc_function(frm, "create_payment_entries");
- },
- submit_payments: function(frm) {
- frm.events.invoke_doc_function(frm, "submit_payment_entries");
- },
-});
-
-
-frappe.ui.form.on('Bank Statement Transaction Invoice Item', {
- party_type: function(frm, cdt, cdn) {
- let row = locals[cdt][cdn];
- if (row.party_type == "Customer") {
- row.invoice_type = "Sales Invoice";
- } else if (row.party_type == "Supplier") {
- row.invoice_type = "Purchase Invoice";
- } else if (row.party_type == "Account") {
- row.invoice_type = "Journal Entry";
- }
- refresh_field("invoice_type", row.name, "payment_invoice_items");
-
- },
- invoice_type: function(frm, cdt, cdn) {
- let row = locals[cdt][cdn];
- if (row.invoice_type == "Purchase Invoice") {
- row.party_type = "Supplier";
- } else if (row.invoice_type == "Sales Invoice") {
- row.party_type = "Customer";
- }
- refresh_field("party_type", row.name, "payment_invoice_items");
- }
-});
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.json b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.json
deleted file mode 100644
index fb80169..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.json
+++ /dev/null
@@ -1,792 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "beta": 0,
- "creation": "2017-11-07 13:48:13.123185",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank_account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Bank Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "from_date",
- "fieldtype": "Date",
- "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": "From Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "to_date",
- "fieldtype": "Date",
- "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": "To Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank_settings",
- "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": "Bank Statement Settings",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Settings",
- "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
- },
- {
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank",
- "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": "Bank",
- "length": 0,
- "no_copy": 0,
- "options": "Bank",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "receivable_account",
- "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": "Receivable Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payable_account",
- "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": "Payable Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank_statement",
- "fieldtype": "Attach",
- "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": "Bank Statement",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "section_break_6",
- "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": "Bank Transaction Entries",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "new_transaction_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": "New Transactions",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Transaction Payment Item",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.new_transaction_items && doc.new_transaction_items.length",
- "fieldname": "section_break_9",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fieldname": "match_invoices",
- "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": "Match Transaction to Invoices",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_14",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "create_payments",
- "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": "Create New Payment/Journal Entry",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_16",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "submit_payments",
- "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": "Submit/Reconcile Payments",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "eval:doc.new_transaction_items && doc.new_transaction_items.length",
- "fieldname": "section_break_18",
- "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": "Matching Invoices",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_invoice_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": "Payment Invoice Items",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Transaction Invoice Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reconciled_transactions",
- "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": "Reconciled Transactions",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reconciled_transaction_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": "Reconciled Transactions",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Transaction Payment Item",
- "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
- },
- {
- "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": "Bank Statement Transaction Entry",
- "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
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-09-14 18:04:44.170455",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Transaction Entry",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
- "write": 1
- },
- {
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
- "write": 1
- }
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py
deleted file mode 100644
index 27dd8e4..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/bank_statement_transaction_entry.py
+++ /dev/null
@@ -1,443 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com 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
-from erpnext.accounts.utils import get_outstanding_invoices
-from frappe.utils import nowdate
-from datetime import datetime
-import csv, os, re, io
-import difflib
-import copy
-
-class BankStatementTransactionEntry(Document):
- def autoname(self):
- self.name = self.bank_account + "-" + self.from_date + "-" + self.to_date
- if self.bank:
- mapper_name = self.bank + "-Statement-Settings"
- if not frappe.db.exists("Bank Statement Settings", mapper_name):
- self.create_settings(self.bank)
- self.bank_settings = mapper_name
-
- def create_settings(self, bank):
- mapper = frappe.new_doc("Bank Statement Settings")
- mapper.bank = bank
- mapper.date_format = "%Y-%m-%d"
- mapper.bank_account = self.bank_account
- for header in ["Date", "Particulars", "Withdrawals", "Deposits", "Balance"]:
- header_item = mapper.append("header_items", {})
- header_item.mapped_header = header_item.stmt_header = header
- mapper.save()
-
- def on_update(self):
- if (not self.bank_statement):
- self.reconciled_transaction_items = self.new_transaction_items = []
- return
-
- if len(self.new_transaction_items + self.reconciled_transaction_items) == 0:
- self.populate_payment_entries()
- else:
- self.match_invoice_to_payment()
-
- def validate(self):
- if not self.new_transaction_items:
- self.populate_payment_entries()
-
- def get_statement_headers(self):
- if not self.bank_settings:
- frappe.throw(_("Bank Data mapper doesn't exist"))
- mapper_doc = frappe.get_doc("Bank Statement Settings", self.bank_settings)
- headers = {entry.mapped_header:entry.stmt_header for entry in mapper_doc.header_items}
- return headers
-
- def populate_payment_entries(self):
- if self.bank_statement is None: return
- file_url = self.bank_statement
- if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0):
- frappe.throw(_("Transactions already retreived from the statement"))
-
- date_format = frappe.get_value("Bank Statement Settings", self.bank_settings, "date_format")
- if (date_format is None):
- date_format = '%Y-%m-%d'
- if self.bank_settings:
- mapped_items = frappe.get_doc("Bank Statement Settings", self.bank_settings).mapped_items
- statement_headers = self.get_statement_headers()
- transactions = get_transaction_entries(file_url, statement_headers)
- for entry in transactions:
- date = entry[statement_headers["Date"]].strip()
- #print("Processing entry DESC:{0}-W:{1}-D:{2}-DT:{3}".format(entry["Particulars"], entry["Withdrawals"], entry["Deposits"], entry["Date"]))
- if (not date): continue
- transaction_date = datetime.strptime(date, date_format).date()
- if (self.from_date and transaction_date < datetime.strptime(self.from_date, '%Y-%m-%d').date()): continue
- if (self.to_date and transaction_date > datetime.strptime(self.to_date, '%Y-%m-%d').date()): continue
- bank_entry = self.append('new_transaction_items', {})
- bank_entry.transaction_date = transaction_date
- bank_entry.description = entry[statement_headers["Particulars"]]
-
- mapped_item = next((entry for entry in mapped_items if entry.mapping_type == "Transaction" and frappe.safe_decode(entry.bank_data.lower()) in frappe.safe_decode(bank_entry.description.lower())), None)
- if (mapped_item is not None):
- bank_entry.party_type = mapped_item.mapped_data_type
- bank_entry.party = mapped_item.mapped_data
- else:
- bank_entry.party_type = "Supplier" if not entry[statement_headers["Deposits"]].strip() else "Customer"
- party_list = frappe.get_all(bank_entry.party_type, fields=["name"])
- parties = [party.name for party in party_list]
- matches = difflib.get_close_matches(frappe.safe_decode(bank_entry.description.lower()), parties, 1, 0.4)
- if len(matches) > 0: bank_entry.party = matches[0]
- bank_entry.amount = -float(entry[statement_headers["Withdrawals"]]) if not entry[statement_headers["Deposits"]].strip() else float(entry[statement_headers["Deposits"]])
- self.map_unknown_transactions()
- self.map_transactions_on_journal_entry()
-
- def map_transactions_on_journal_entry(self):
- for entry in self.new_transaction_items:
- vouchers = frappe.db.sql("""select name, posting_date from `tabJournal Entry`
- where posting_date='{0}' and total_credit={1} and cheque_no='{2}' and docstatus != 2
- """.format(entry.transaction_date, abs(entry.amount), frappe.safe_decode(entry.description)), as_dict=True)
- if (len(vouchers) == 1):
- entry.reference_name = vouchers[0].name
-
- def populate_matching_invoices(self):
- self.payment_invoice_items = []
- self.map_unknown_transactions()
- added_invoices = []
- for entry in self.new_transaction_items:
- if (not entry.party or entry.party_type == "Account"): continue
- account = self.receivable_account if entry.party_type == "Customer" else self.payable_account
- invoices = get_outstanding_invoices(entry.party_type, entry.party, account)
- transaction_date = datetime.strptime(entry.transaction_date, "%Y-%m-%d").date()
- outstanding_invoices = [invoice for invoice in invoices if invoice.posting_date <= transaction_date]
- amount = abs(entry.amount)
- matching_invoices = [invoice for invoice in outstanding_invoices if invoice.outstanding_amount == amount]
- sorted(outstanding_invoices, key=lambda k: k['posting_date'])
- for e in (matching_invoices + outstanding_invoices):
- added = next((inv for inv in added_invoices if inv == e.get('voucher_no')), None)
- if (added is not None): continue
- ent = self.append('payment_invoice_items', {})
- ent.transaction_date = entry.transaction_date
- ent.payment_description = frappe.safe_decode(entry.description)
- ent.party_type = entry.party_type
- ent.party = entry.party
- ent.invoice = e.get('voucher_no')
- added_invoices += [ent.invoice]
- ent.invoice_type = "Sales Invoice" if entry.party_type == "Customer" else "Purchase Invoice"
- ent.invoice_date = e.get('posting_date')
- ent.outstanding_amount = e.get('outstanding_amount')
- ent.allocated_amount = min(float(e.get('outstanding_amount')), amount)
- amount -= float(e.get('outstanding_amount'))
- if (amount <= 5): break
- self.match_invoice_to_payment()
- self.populate_matching_vouchers()
- self.map_transactions_on_journal_entry()
-
- def match_invoice_to_payment(self):
- added_payments = []
- for entry in self.new_transaction_items:
- if (not entry.party or entry.party_type == "Account"): continue
- entry.account = self.receivable_account if entry.party_type == "Customer" else self.payable_account
- amount = abs(entry.amount)
- payment, matching_invoices = None, []
- for inv_entry in self.payment_invoice_items:
- if (inv_entry.payment_description != frappe.safe_decode(entry.description) or inv_entry.transaction_date != entry.transaction_date): continue
- if (inv_entry.party != entry.party): continue
- matching_invoices += [inv_entry.invoice_type + "|" + inv_entry.invoice]
- payment = get_payments_matching_invoice(inv_entry.invoice, entry.amount, entry.transaction_date)
- doc = frappe.get_doc(inv_entry.invoice_type, inv_entry.invoice)
- inv_entry.invoice_date = doc.posting_date
- inv_entry.outstanding_amount = doc.outstanding_amount
- inv_entry.allocated_amount = min(float(doc.outstanding_amount), amount)
- amount -= inv_entry.allocated_amount
- if (amount < 0): break
-
- amount = abs(entry.amount)
- if (payment is None):
- order_doctype = "Sales Order" if entry.party_type=="Customer" else "Purchase Order"
- from erpnext.controllers.accounts_controller import get_advance_payment_entries
- payment_entries = get_advance_payment_entries(entry.party_type, entry.party, entry.account, order_doctype, against_all_orders=True)
- payment_entries += self.get_matching_payments(entry.party, amount, entry.transaction_date)
- payment = next((payment for payment in payment_entries if payment.amount == amount and payment not in added_payments), None)
- if (payment is None):
- print("Failed to find payments for {0}:{1}".format(entry.party, amount))
- continue
- added_payments += [payment]
- entry.reference_type = payment.reference_type
- entry.reference_name = payment.reference_name
- entry.mode_of_payment = "Wire Transfer"
- entry.outstanding_amount = min(amount, 0)
- if (entry.payment_reference is None):
- entry.payment_reference = frappe.safe_decode(entry.description)
- entry.invoices = ",".join(matching_invoices)
- #print("Matching payment is {0}:{1}".format(entry.reference_type, entry.reference_name))
-
- def get_matching_payments(self, party, amount, pay_date):
- query = """select 'Payment Entry' as reference_type, name as reference_name, paid_amount as amount
- from `tabPayment Entry` where party='{0}' and paid_amount={1} and posting_date='{2}' and docstatus != 2
- """.format(party, amount, pay_date)
- matching_payments = frappe.db.sql(query, as_dict=True)
- return matching_payments
-
- def map_unknown_transactions(self):
- for entry in self.new_transaction_items:
- if (entry.party): continue
- inv_type = "Sales Invoice" if (entry.amount > 0) else "Purchase Invoice"
- party_type = "customer" if (entry.amount > 0) else "supplier"
-
- query = """select posting_date, name, {0}, outstanding_amount
- from `tab{1}` where ROUND(outstanding_amount)={2} and posting_date < '{3}'
- """.format(party_type, inv_type, round(abs(entry.amount)), entry.transaction_date)
- invoices = frappe.db.sql(query, as_dict = True)
- if(len(invoices) > 0):
- entry.party = invoices[0].get(party_type)
-
- def populate_matching_vouchers(self):
- for entry in self.new_transaction_items:
- if (not entry.party or entry.reference_name): continue
- print("Finding matching voucher for {0}".format(frappe.safe_decode(entry.description)))
- amount = abs(entry.amount)
- invoices = []
- vouchers = get_matching_journal_entries(self.from_date, self.to_date, entry.party, self.bank_account, amount)
- if len(vouchers) == 0: continue
- for voucher in vouchers:
- added = next((entry.invoice for entry in self.payment_invoice_items if entry.invoice == voucher.voucher_no), None)
- if (added):
- print("Found voucher {0}".format(added))
- continue
- print("Adding voucher {0} {1} {2}".format(voucher.voucher_no, voucher.posting_date, voucher.debit))
- ent = self.append('payment_invoice_items', {})
- ent.invoice_date = voucher.posting_date
- ent.invoice_type = "Journal Entry"
- ent.invoice = voucher.voucher_no
- ent.payment_description = frappe.safe_decode(entry.description)
- ent.allocated_amount = max(voucher.debit, voucher.credit)
-
- invoices += [ent.invoice_type + "|" + ent.invoice]
- entry.reference_type = "Journal Entry"
- entry.mode_of_payment = "Wire Transfer"
- entry.reference_name = ent.invoice
- #entry.account = entry.party
- entry.invoices = ",".join(invoices)
- break
-
-
- def create_payment_entries(self):
- for payment_entry in self.new_transaction_items:
- if (not payment_entry.party): continue
- if (payment_entry.reference_name): continue
- print("Creating payment entry for {0}".format(frappe.safe_decode(payment_entry.description)))
- if (payment_entry.party_type == "Account"):
- payment = self.create_journal_entry(payment_entry)
- invoices = [payment.doctype + "|" + payment.name]
- payment_entry.invoices = ",".join(invoices)
- else:
- payment = self.create_payment_entry(payment_entry)
- invoices = [entry.reference_doctype + "|" + entry.reference_name for entry in payment.references if entry is not None]
- payment_entry.invoices = ",".join(invoices)
- payment_entry.mode_of_payment = payment.mode_of_payment
- payment_entry.account = self.receivable_account if payment_entry.party_type == "Customer" else self.payable_account
- payment_entry.reference_name = payment.name
- payment_entry.reference_type = payment.doctype
- frappe.msgprint(_("Successfully created payment entries"))
-
- def create_payment_entry(self, pe):
- payment = frappe.new_doc("Payment Entry")
- payment.posting_date = pe.transaction_date
- payment.payment_type = "Receive" if pe.party_type == "Customer" else "Pay"
- payment.mode_of_payment = "Wire Transfer"
- payment.party_type = pe.party_type
- payment.party = pe.party
- payment.paid_to = self.bank_account if pe.party_type == "Customer" else self.payable_account
- payment.paid_from = self.receivable_account if pe.party_type == "Customer" else self.bank_account
- payment.paid_amount = payment.received_amount = abs(pe.amount)
- payment.reference_no = pe.description
- payment.reference_date = pe.transaction_date
- payment.save()
- for inv_entry in self.payment_invoice_items:
- if (pe.description != inv_entry.payment_description or pe.transaction_date != inv_entry.transaction_date): continue
- if (pe.party != inv_entry.party): continue
- reference = payment.append("references", {})
- reference.reference_doctype = inv_entry.invoice_type
- reference.reference_name = inv_entry.invoice
- reference.allocated_amount = inv_entry.allocated_amount
- print ("Adding invoice {0} {1}".format(reference.reference_name, reference.allocated_amount))
- payment.setup_party_account_field()
- payment.set_missing_values()
- #payment.set_exchange_rate()
- #payment.set_amounts()
- #print("Created payment entry {0}".format(payment.as_dict()))
- payment.save()
- return payment
-
- def create_journal_entry(self, pe):
- je = frappe.new_doc("Journal Entry")
- je.is_opening = "No"
- je.voucher_type = "Bank Entry"
- je.cheque_no = pe.description
- je.cheque_date = pe.transaction_date
- je.remark = pe.description
- je.posting_date = pe.transaction_date
- if (pe.amount < 0):
- je.append("accounts", {"account": pe.party, "debit_in_account_currency": abs(pe.amount)})
- je.append("accounts", {"account": self.bank_account, "credit_in_account_currency": abs(pe.amount)})
- else:
- je.append("accounts", {"account": pe.party, "credit_in_account_currency": pe.amount})
- je.append("accounts", {"account": self.bank_account, "debit_in_account_currency": pe.amount})
- je.save()
- return je
-
- def update_payment_entry(self, payment):
- lst = []
- invoices = payment.invoices.strip().split(',')
- if (len(invoices) == 0): return
- amount = float(abs(payment.amount))
- for invoice_entry in invoices:
- if (not invoice_entry.strip()): continue
- invs = invoice_entry.split('|')
- invoice_type, invoice = invs[0], invs[1]
- outstanding_amount = frappe.get_value(invoice_type, invoice, 'outstanding_amount')
-
- lst.append(frappe._dict({
- 'voucher_type': payment.reference_type,
- 'voucher_no' : payment.reference_name,
- 'against_voucher_type' : invoice_type,
- 'against_voucher' : invoice,
- 'account' : payment.account,
- 'party_type': payment.party_type,
- 'party': frappe.get_value("Payment Entry", payment.reference_name, "party"),
- 'unadjusted_amount' : float(amount),
- 'allocated_amount' : min(outstanding_amount, amount)
- }))
- amount -= outstanding_amount
- if lst:
- from erpnext.accounts.utils import reconcile_against_document
- try:
- reconcile_against_document(lst)
- except:
- frappe.throw(_("Exception occurred while reconciling {0}").format(payment.reference_name))
-
- def submit_payment_entries(self):
- for payment in self.new_transaction_items:
- if payment.reference_name is None: continue
- doc = frappe.get_doc(payment.reference_type, payment.reference_name)
- if doc.docstatus == 1:
- if (payment.reference_type == "Journal Entry"): continue
- if doc.unallocated_amount == 0: continue
- print("Reconciling payment {0}".format(payment.reference_name))
- self.update_payment_entry(payment)
- else:
- print("Submitting payment {0}".format(payment.reference_name))
- if (payment.reference_type == "Payment Entry"):
- if (payment.payment_reference):
- doc.reference_no = payment.payment_reference
- doc.mode_of_payment = payment.mode_of_payment
- doc.save()
- doc.submit()
- self.move_reconciled_entries()
- self.populate_matching_invoices()
-
- def move_reconciled_entries(self):
- idx = 0
- while idx < len(self.new_transaction_items):
- entry = self.new_transaction_items[idx]
- try:
- print("Checking transaction {0}: {2} in {1} entries".format(idx, len(self.new_transaction_items), frappe.safe_decode(entry.description)))
- except UnicodeEncodeError:
- pass
- idx += 1
- if entry.reference_name is None: continue
- doc = frappe.get_doc(entry.reference_type, entry.reference_name)
- if doc.docstatus == 1 and (entry.reference_type == "Journal Entry" or doc.unallocated_amount == 0):
- self.remove(entry)
- rc_entry = self.append('reconciled_transaction_items', {})
- dentry = entry.as_dict()
- dentry.pop('idx', None)
- rc_entry.update(dentry)
- idx -= 1
-
-
-def get_matching_journal_entries(from_date, to_date, account, against, amount):
- query = """select voucher_no, posting_date, account, against, debit_in_account_currency as debit, credit_in_account_currency as credit
- from `tabGL Entry`
- where posting_date between '{0}' and '{1}' and account = '{2}' and against = '{3}' and debit = '{4}'
- """.format(from_date, to_date, account, against, amount)
- jv_entries = frappe.db.sql(query, as_dict=True)
- #print("voucher query:{0}\n Returned {1} entries".format(query, len(jv_entries)))
- return jv_entries
-
-def get_payments_matching_invoice(invoice, amount, pay_date):
- query = """select pe.name as reference_name, per.reference_doctype as reference_type, per.outstanding_amount, per.allocated_amount
- from `tabPayment Entry Reference` as per JOIN `tabPayment Entry` as pe on pe.name = per.parent
- where per.reference_name='{0}' and (posting_date='{1}' or reference_date='{1}') and pe.docstatus != 2
- """.format(invoice, pay_date)
- payments = frappe.db.sql(query, as_dict=True)
- if (len(payments) == 0): return
- payment = next((payment for payment in payments if payment.allocated_amount == amount), payments[0])
- #Hack: Update the reference type which is set to invoice type
- payment.reference_type = "Payment Entry"
- return payment
-
-def is_headers_present(headers, row):
- for header in headers:
- if header not in row:
- return False
- return True
-
-def get_header_index(headers, row):
- header_index = {}
- for header in headers:
- if header in row:
- header_index[header] = row.index(header)
- return header_index
-
-def get_transaction_info(headers, header_index, row):
- transaction = {}
- for header in headers:
- transaction[header] = row[header_index[header]]
- if (transaction[header] == None):
- transaction[header] = ""
- return transaction
-
-def get_transaction_entries(file_url, headers):
- header_index = {}
- rows, transactions = [], []
-
- if (file_url.lower().endswith("xlsx")):
- from frappe.utils.xlsxutils import read_xlsx_file_from_attached_file
- rows = read_xlsx_file_from_attached_file(file_url=file_url)
- elif (file_url.lower().endswith("csv")):
- from frappe.utils.csvutils import read_csv_content
- _file = frappe.get_doc("File", {"file_url": file_url})
- filepath = _file.get_full_path()
- with open(filepath,'rb') as csvfile:
- rows = read_csv_content(csvfile.read())
- elif (file_url.lower().endswith("xls")):
- filename = file_url.split("/")[-1]
- rows = get_rows_from_xls_file(filename)
- else:
- frappe.throw(_("Only .csv and .xlsx files are supported currently"))
-
- stmt_headers = headers.values()
- for row in rows:
- if len(row) == 0 or row[0] == None or not row[0]: continue
- #print("Processing row {0}".format(row))
- if header_index:
- transaction = get_transaction_info(stmt_headers, header_index, row)
- transactions.append(transaction)
- elif is_headers_present(stmt_headers, row):
- header_index = get_header_index(stmt_headers, row)
- return transactions
-
-def get_rows_from_xls_file(filename):
- _file = frappe.get_doc("File", {"file_name": filename})
- filepath = _file.get_full_path()
- import xlrd
- book = xlrd.open_workbook(filepath)
- sheets = book.sheets()
- rows = []
- for row in range(1, sheets[0].nrows):
- row_values = []
- for col in range(1, sheets[0].ncols):
- row_values.append(sheets[0].cell_value(row, col))
- rows.append(row_values)
- return rows
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.js b/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.js
deleted file mode 100644
index 46d570f..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Bank Statement Transaction Entry", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially([
- // insert a new Bank Statement Transaction Entry
- () => frappe.tests.make('Bank Statement Transaction Entry', [
- // values to be set
- {key: 'value'}
- ]),
- () => {
- assert.equal(cur_frm.doc.key, 'value');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.py b/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.py
deleted file mode 100644
index 4589483..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_entry/test_bank_statement_transaction_entry.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import frappe
-import unittest
-
-class TestBankStatementTransactionEntry(unittest.TestCase):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/__init__.py
+++ /dev/null
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.json b/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.json
deleted file mode 100644
index d96c94d..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.json
+++ /dev/null
@@ -1,365 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-07 13:58:53.827058",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "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,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 4,
- "fieldname": "payment_description",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Payment Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "party_type",
- "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": "Party Type",
- "length": 0,
- "no_copy": 0,
- "options": "Customer\nSupplier\nAccount",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "party",
- "fieldtype": "Dynamic 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": "Party",
- "length": 0,
- "no_copy": 0,
- "options": "party_type",
- "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
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_4",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "invoice_date",
- "fieldtype": "Date",
- "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": "Invoice Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "invoice_type",
- "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": "Invoice Type",
- "length": 0,
- "no_copy": 0,
- "options": "Sales Invoice\nPurchase Invoice\nJournal Entry",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "invoice",
- "fieldtype": "Dynamic 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": "invoice",
- "length": 0,
- "no_copy": 0,
- "options": "invoice_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "outstanding_amount",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Outstanding Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "allocated_amount",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Allocated Amount",
- "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
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-09-14 19:03:30.949831",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Transaction Invoice Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py b/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py
deleted file mode 100644
index cb1b158..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_invoice_item/bank_statement_transaction_invoice_item.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementTransactionInvoiceItem(Document):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/__init__.py
+++ /dev/null
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.json b/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.json
deleted file mode 100644
index 177dccd..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.json
+++ /dev/null
@@ -1,494 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-07 14:03:05.651413",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "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": 1,
- "in_standard_filter": 0,
- "label": "Transaction Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 4,
- "fieldname": "description",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "amount",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Amount",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "party_type",
- "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": "Party Type",
- "length": 0,
- "no_copy": 0,
- "options": "Customer\nSupplier\nAccount",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "party",
- "fieldtype": "Dynamic 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": "Party",
- "length": 0,
- "no_copy": 0,
- "options": "party_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "reference_type",
- "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": "Reference Type",
- "length": 0,
- "no_copy": 0,
- "options": "Payment Entry\nJournal Entry",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "account",
- "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": "Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mode_of_payment",
- "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": "Mode of Payment",
- "length": 0,
- "no_copy": 0,
- "options": "Mode of Payment",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "outstanding_amount",
- "fieldtype": "Currency",
- "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": "outstanding_amount",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_10",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 2,
- "fieldname": "reference_name",
- "fieldtype": "Dynamic 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": "Reference Name",
- "length": 0,
- "no_copy": 0,
- "options": "reference_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "payment_reference",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Payment Reference",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "invoices",
- "fieldtype": "Text",
- "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": "Invoices",
- "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,
- "unique": 0
- }
- ],
- "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": "2017-11-15 19:18:52.876221",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Transaction Payment Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py b/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py
deleted file mode 100644
index 9840c0d..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_payment_item/bank_statement_transaction_payment_item.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementTransactionPaymentItem(Document):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/__init__.py
+++ /dev/null
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.js b/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.js
deleted file mode 100644
index 46aa4f2..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.js
+++ /dev/null
@@ -1,8 +0,0 @@
-// Copyright (c) 2017, sathishpy@gmail.com and contributors
-// For license information, please see license.txt
-
-frappe.ui.form.on('Bank Statement Settings', {
- refresh: function(frm) {
-
- }
-});
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.json b/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.json
deleted file mode 100644
index 474bb90..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.json
+++ /dev/null
@@ -1,266 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 1,
- "beta": 0,
- "creation": "2017-11-13 13:38:10.863592",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank_account",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Bank Account",
- "length": 0,
- "no_copy": 0,
- "options": "Account",
- "permlevel": 0,
- "precision": "",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "'%d/%m/%Y'",
- "fieldname": "date_format",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Date Format",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "statement_header_mapping",
- "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": "Statement Header Mapping",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "header_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": "Statement Headers",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Settings Item",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "transaction_data_mapping",
- "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": "Transaction Data Mapping",
- "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,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mapped_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": "Mapped Items",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Statement Transaction Settings Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-01-12 10:34:32.840487",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- },
- {
- "amend": 0,
- "apply_user_permissions": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Accounts Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
- "write": 1
- }
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.py b/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.py
deleted file mode 100644
index de9a85f..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/bank_statement_transaction_settings.py
+++ /dev/null
@@ -1,11 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementSettings(Document):
- def autoname(self):
- self.name = self.bank_account + "-Mappings"
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.js b/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.js
deleted file mode 100644
index f2381c0..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.js
+++ /dev/null
@@ -1,23 +0,0 @@
-/* eslint-disable */
-// rename this file from _test_[name] to test_[name] to activate
-// and remove above this line
-
-QUnit.test("test: Bank Statement Settings", function (assert) {
- let done = assert.async();
-
- // number of asserts
- assert.expect(1);
-
- frappe.run_serially([
- // insert a new Bank Statement Settings
- () => frappe.tests.make('Bank Statement Settings', [
- // values to be set
- {key: 'value'}
- ]),
- () => {
- assert.equal(cur_frm.doc.key, 'value');
- },
- () => done()
- ]);
-
-});
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.py b/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.py
deleted file mode 100644
index aa7fe83..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings/test_bank_statement_transaction_settings.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and Contributors
-# See license.txt
-from __future__ import unicode_literals
-
-import frappe
-import unittest
-
-class TestBankStatementSettings(unittest.TestCase):
- pass
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py b/erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py
deleted file mode 100644
index e69de29..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/__init__.py
+++ /dev/null
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.json b/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.json
deleted file mode 100644
index 47c3209..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.json
+++ /dev/null
@@ -1,166 +0,0 @@
-{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-11-13 13:42:00.335432",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
- "fields": [
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Transaction",
- "fieldname": "mapping_type",
- "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": "Mapping Type",
- "length": 0,
- "no_copy": 0,
- "options": "Transaction",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "bank_data",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Bank Data",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Account",
- "fieldname": "mapped_data_type",
- "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": "Mapped Data Type",
- "length": 0,
- "no_copy": 0,
- "options": "Account\nCustomer\nSupplier\nAccount",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "mapped_data",
- "fieldtype": "Dynamic 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": "Mapped Data",
- "length": 0,
- "no_copy": 0,
- "options": "mapped_data_type",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- }
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-01-08 00:13:49.973501",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "Bank Statement Transaction Settings Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py b/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py
deleted file mode 100644
index bf0a590..0000000
--- a/erpnext/accounts/doctype/bank_statement_transaction_settings_item/bank_statement_transaction_settings_item.py
+++ /dev/null
@@ -1,10 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2017, sathishpy@gmail.com and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.model.document import Document
-
-class BankStatementTransactionSettingsItem(Document):
- pass
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
index 8b1bab1..3758b52 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.js
@@ -1,32 +1,70 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-frappe.ui.form.on('Bank Transaction', {
+frappe.ui.form.on("Bank Transaction", {
onload(frm) {
- frm.set_query('payment_document', 'payment_entries', function() {
+ frm.set_query("payment_document", "payment_entries", function () {
return {
- "filters": {
- "name": ["in", ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice", "Expense Claim"]]
- }
+ filters: {
+ name: [
+ "in",
+ [
+ "Payment Entry",
+ "Journal Entry",
+ "Sales Invoice",
+ "Purchase Invoice",
+ "Expense Claim",
+ ],
+ ],
+ },
};
});
- }
+ },
+ bank_account: function (frm) {
+ set_bank_statement_filter(frm);
+ },
+
+ setup: function (frm) {
+ frm.set_query("party_type", function () {
+ return {
+ filters: {
+ name: ["in", Object.keys(frappe.boot.party_account_types)],
+ },
+ };
+ });
+ },
});
-frappe.ui.form.on('Bank Transaction Payments', {
- payment_entries_remove: function(frm, cdt, cdn) {
+frappe.ui.form.on("Bank Transaction Payments", {
+ payment_entries_remove: function (frm, cdt, cdn) {
update_clearance_date(frm, cdt, cdn);
- }
+ },
});
const update_clearance_date = (frm, cdt, cdn) => {
if (frm.doc.docstatus === 1) {
- frappe.xcall('erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment',
- {doctype: cdt, docname: cdn})
- .then(e => {
+ frappe
+ .xcall(
+ "erpnext.accounts.doctype.bank_transaction.bank_transaction.unclear_reference_payment",
+ { doctype: cdt, docname: cdn }
+ )
+ .then((e) => {
if (e == "success") {
- frappe.show_alert({message:__("Document {0} successfully uncleared", [e]), indicator:'green'});
+ frappe.show_alert({
+ message: __("Document {0} successfully uncleared", [e]),
+ indicator: "green",
+ });
}
});
}
-};
\ No newline at end of file
+};
+
+function set_bank_statement_filter(frm) {
+ frm.set_query("bank_statement", function () {
+ return {
+ filters: {
+ bank_account: frm.doc.bank_account,
+ },
+ };
+ });
+}
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
index 39937bb..69ee497 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.json
@@ -1,833 +1,245 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
+ "actions": [],
"allow_import": 1,
- "allow_rename": 0,
"autoname": "naming_series:",
- "beta": 0,
"creation": "2018-10-22 18:19:02.784533",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "date",
+ "column_break_2",
+ "status",
+ "bank_account",
+ "company",
+ "section_break_4",
+ "deposit",
+ "withdrawal",
+ "column_break_7",
+ "currency",
+ "section_break_10",
+ "description",
+ "section_break_14",
+ "reference_number",
+ "transaction_id",
+ "payment_entries",
+ "section_break_18",
+ "allocated_amount",
+ "amended_from",
+ "column_break_17",
+ "unallocated_amount",
+ "party_section",
+ "party_type",
+ "party"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "ACC-BTN-.YYYY.-",
- "fetch_if_empty": 0,
"fieldname": "naming_series",
"fieldtype": "Select",
"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": "Series",
- "length": 0,
"no_copy": 1,
"options": "ACC-BTN-.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
+ "set_only_once": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "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": "Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Date"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Pending",
- "fetch_if_empty": 0,
"fieldname": "status",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
"in_standard_filter": 1,
"label": "Status",
- "length": 0,
- "no_copy": 0,
- "options": "\nPending\nSettled\nUnreconciled\nReconciled",
- "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
+ "options": "\nPending\nSettled\nUnreconciled\nReconciled"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "bank_account",
"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": "Bank Account",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Bank Account"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
"fetch_from": "bank_account.company",
- "fetch_if_empty": 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": 1,
"label": "Company",
- "length": 0,
- "no_copy": 0,
"options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "debit",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Debit",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
- "fieldname": "credit",
- "fieldtype": "Currency",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Credit",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_7",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "currency",
"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": "Currency",
- "length": 0,
- "no_copy": 0,
- "options": "Currency",
- "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
+ "options": "Currency"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_10",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "description",
"fieldtype": "Small Text",
- "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": "Description",
- "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
+ "label": "Description"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_14",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "allow_on_submit": 1,
"fieldname": "reference_number",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Reference Number",
- "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
+ "label": "Reference Number"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "transaction_id",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Transaction ID",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
"read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
"unique": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
"allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "payment_entries",
"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": "Payment Entries",
- "length": 0,
- "no_copy": 0,
- "options": "Bank Transaction Payments",
- "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
+ "options": "Bank Transaction Payments"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_18",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Section Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "allocated_amount",
"fieldtype": "Currency",
- "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": "Allocated Amount",
- "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
+ "label": "Allocated Amount"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 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": "Bank Transaction",
- "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
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 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,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_if_empty": 0,
"fieldname": "unallocated_amount",
"fieldtype": "Currency",
- "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": "Unallocated Amount",
- "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
+ "label": "Unallocated Amount"
+ },
+ {
+ "fieldname": "party_section",
+ "fieldtype": "Section Break",
+ "label": "Payment From / To"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "party_type",
+ "fieldtype": "Link",
+ "label": "Party Type",
+ "options": "DocType"
+ },
+ {
+ "allow_on_submit": 1,
+ "fieldname": "party",
+ "fieldtype": "Dynamic Link",
+ "label": "Party",
+ "options": "party_type"
+ },
+ {
+ "fieldname": "deposit",
+ "oldfieldname": "debit",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Deposit"
+ },
+ {
+ "fieldname": "withdrawal",
+ "oldfieldname": "credit",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Withdrawal"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
"is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-05-11 05:27:55.244721",
+ "links": [],
+ "modified": "2020-12-30 19:40:54.221070",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Bank Transaction",
- "name_case": "",
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
},
{
- "amend": 0,
"cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts Manager",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Accounts User",
- "set_user_permissions": 0,
"share": 1,
"submit": 1,
"write": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "date",
"sort_order": "DESC",
"title_field": "bank_account",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
index 0e45db3..5246baa 100644
--- a/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction.py
@@ -11,7 +11,7 @@
class BankTransaction(StatusUpdater):
def after_insert(self):
- self.unallocated_amount = abs(flt(self.credit) - flt(self.debit))
+ self.unallocated_amount = abs(flt(self.withdrawal) - flt(self.deposit))
def on_submit(self):
self.clear_linked_payment_entries()
@@ -30,13 +30,13 @@
if allocated_amount:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", flt(allocated_amount))
- frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.credit) - flt(self.debit)) - flt(allocated_amount))
+ frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)) - flt(allocated_amount))
else:
frappe.db.set_value(self.doctype, self.name, "allocated_amount", 0)
- frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.credit) - flt(self.debit)))
+ frappe.db.set_value(self.doctype, self.name, "unallocated_amount", abs(flt(self.withdrawal) - flt(self.deposit)))
- amount = self.debit or self.credit
+ amount = self.deposit or self.withdrawal
if amount == self.allocated_amount:
frappe.db.set_value(self.doctype, self.name, "status", "Reconciled")
@@ -44,18 +44,11 @@
def clear_linked_payment_entries(self):
for payment_entry in self.payment_entries:
- allocated_amount = get_total_allocated_amount(payment_entry)
- paid_amount = get_paid_amount(payment_entry, self.currency)
+ if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
+ self.clear_simple_entry(payment_entry)
- if paid_amount and allocated_amount:
- if flt(allocated_amount[0]["allocated_amount"]) > flt(paid_amount):
- frappe.throw(_("The total allocated amount ({0}) is greated than the paid amount ({1}).").format(flt(allocated_amount[0]["allocated_amount"]), flt(paid_amount)))
- else:
- if payment_entry.payment_document in ["Payment Entry", "Journal Entry", "Purchase Invoice", "Expense Claim"]:
- self.clear_simple_entry(payment_entry)
-
- elif payment_entry.payment_document == "Sales Invoice":
- self.clear_sales_invoice(payment_entry)
+ elif payment_entry.payment_document == "Sales Invoice":
+ self.clear_sales_invoice(payment_entry)
def clear_simple_entry(self, payment_entry):
frappe.db.set_value(payment_entry.payment_document, payment_entry.payment_entry, "clearance_date", self.date)
@@ -112,3 +105,4 @@
frappe.db.set_value(doc.payment_document, doc.payment_entry, "clearance_date", None)
return doc.payment_entry
+
diff --git a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
index 2754633..3b14e4e 100644
--- a/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
+++ b/erpnext/accounts/doctype/bank_transaction/test_bank_transaction.py
@@ -5,17 +5,20 @@
import frappe
import unittest
+import json
from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
-from erpnext.accounts.page.bank_reconciliation.bank_reconciliation import reconcile, get_linked_payments
+from erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool import reconcile_vouchers, get_linked_payments
+from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
test_dependencies = ["Item", "Cost Center"]
class TestBankTransaction(unittest.TestCase):
def setUp(self):
+ make_pos_profile()
add_transactions()
- add_payments()
+ add_vouchers()
def tearDown(self):
for bt in frappe.get_all("Bank Transaction"):
@@ -27,20 +30,27 @@
frappe.db.sql("""delete from `tabPayment Entry Reference`""")
frappe.db.sql("""delete from `tabPayment Entry`""")
+ # Delete POS Profile
+ frappe.db.sql("delete from `tabPOS Profile`")
+
frappe.flags.test_bank_transactions_created = False
frappe.flags.test_payments_created = False
# This test checks if ERPNext is able to provide a linked payment for a bank transaction based on the amount of the bank transaction.
def test_linked_payments(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic"))
- linked_payments = get_linked_payments(bank_transaction.name)
- self.assertTrue(linked_payments[0].party == "Conrad Electronic")
+ linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
+ self.assertTrue(linked_payments[0][6] == "Conrad Electronic")
# This test validates a simple reconciliation leading to the clearance of the bank transaction and the payment
def test_reconcile(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
- reconcile(bank_transaction.name, "Payment Entry", payment.name)
+ vouchers = json.dumps([{
+ "payment_doctype":"Payment Entry",
+ "payment_name":payment.name,
+ "amount":bank_transaction.unallocated_amount}])
+ reconcile_vouchers(bank_transaction.name, vouchers)
unallocated_amount = frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount")
self.assertTrue(unallocated_amount == 0)
@@ -48,45 +58,40 @@
clearance_date = frappe.db.get_value("Payment Entry", payment.name, "clearance_date")
self.assertTrue(clearance_date is not None)
- # Check if ERPNext can correctly fetch a linked payment based on the party
- def test_linked_payments_based_on_party(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G"))
- linked_payments = get_linked_payments(bank_transaction.name)
- self.assertTrue(len(linked_payments)==1)
-
# Check if ERPNext can correctly filter a linked payments based on the debit/credit amount
def test_debit_credit_output(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
- linked_payments = get_linked_payments(bank_transaction.name)
- self.assertTrue(linked_payments[0].payment_type == "Pay")
+ linked_payments = get_linked_payments(bank_transaction.name, ['payment_entry', 'exact_match'])
+ print(linked_payments)
+ self.assertTrue(linked_payments[0][3])
# Check error if already reconciled
def test_already_reconciled(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
- reconcile(bank_transaction.name, "Payment Entry", payment.name)
+ vouchers = json.dumps([{
+ "payment_doctype":"Payment Entry",
+ "payment_name":payment.name,
+ "amount":bank_transaction.unallocated_amount}])
+ reconcile_vouchers(bank_transaction.name, vouchers)
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G"))
payment = frappe.get_doc("Payment Entry", dict(party="Mr G", paid_amount=1200))
- self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name)
-
- # Raise an error if creditor transaction vs creditor payment
- def test_invalid_creditor_reconcilation(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"))
- payment = frappe.get_doc("Payment Entry", dict(party="Conrad Electronic", paid_amount=690))
- self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name)
-
- # Raise an error if debitor transaction vs debitor payment
- def test_invalid_debitor_reconcilation(self):
- bank_transaction = frappe.get_doc("Bank Transaction", dict(description="Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07"))
- payment = frappe.get_doc("Payment Entry", dict(party="Fayva", paid_amount=109080))
- self.assertRaises(frappe.ValidationError, reconcile, bank_transaction=bank_transaction.name, payment_doctype="Payment Entry", payment_name=payment.name)
+ vouchers = json.dumps([{
+ "payment_doctype":"Payment Entry",
+ "payment_name":payment.name,
+ "amount":bank_transaction.unallocated_amount}])
+ self.assertRaises(frappe.ValidationError, reconcile_vouchers, bank_transaction_name=bank_transaction.name, vouchers=vouchers)
# Raise an error if debitor transaction vs debitor payment
def test_clear_sales_invoice(self):
bank_transaction = frappe.get_doc("Bank Transaction", dict(description="I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio"))
payment = frappe.get_doc("Sales Invoice", dict(customer="Fayva", status=["=", "Paid"]))
- reconcile(bank_transaction.name, "Sales Invoice", payment.name)
+ vouchers = json.dumps([{
+ "payment_doctype":"Sales Invoice",
+ "payment_name":payment.name,
+ "amount":bank_transaction.unallocated_amount}])
+ reconcile_vouchers(bank_transaction.name, vouchers=vouchers)
self.assertEqual(frappe.db.get_value("Bank Transaction", bank_transaction.name, "unallocated_amount"), 0)
self.assertTrue(frappe.db.get_value("Sales Invoice Payment", dict(parent=payment.name), "clearance_date") is not None)
@@ -121,7 +126,7 @@
"doctype": "Bank Transaction",
"description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G",
"date": "2018-10-23",
- "debit": 1200,
+ "deposit": 1200,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
@@ -131,7 +136,7 @@
"doctype": "Bank Transaction",
"description":"1512567 BG/000003025 OPSKATTUZWXXX AT776000000098709849 Herr G",
"date": "2018-10-23",
- "debit": 1700,
+ "deposit": 1700,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
@@ -141,7 +146,7 @@
"doctype": "Bank Transaction",
"description":"Re 95282925234 FE/000002917 AT171513000281183046 Conrad Electronic",
"date": "2018-10-26",
- "debit": 690,
+ "withdrawal": 690,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
@@ -151,7 +156,7 @@
"doctype": "Bank Transaction",
"description":"Auszahlung Karte MC/000002916 AUTOMAT 698769 K002 27.10. 14:07",
"date": "2018-10-27",
- "debit": 3900,
+ "deposit": 3900,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
@@ -161,7 +166,7 @@
"doctype": "Bank Transaction",
"description":"I2015000011 VD/000002514 ATWWXXX AT4701345000003510057 Bio",
"date": "2018-10-27",
- "credit": 109080,
+ "withdrawal": 109080,
"currency": "INR",
"bank_account": "Checking Account - Citi Bank"
}).insert()
@@ -169,7 +174,7 @@
frappe.flags.test_bank_transactions_created = True
-def add_payments():
+def add_vouchers():
if frappe.flags.test_payments_created:
return
@@ -187,6 +192,7 @@
pass
pi = make_purchase_invoice(supplier="Conrad Electronic", qty=1, rate=690)
+
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Conrad Oct 18"
pe.reference_date = "2018-10-24"
@@ -237,10 +243,15 @@
except frappe.DuplicateEntryError:
pass
- pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900)
+ pi = make_purchase_invoice(supplier="Poore Simon's", qty=1, rate=3900, is_paid=1, do_not_save =1)
+ pi.cash_bank_account = "_Test Bank - _TC"
+ pi.insert()
+ pi.submit()
pe = get_payment_entry("Purchase Invoice", pi.name, bank_account="_Test Bank - _TC")
pe.reference_no = "Poore Simon's Oct 18"
pe.reference_date = "2018-10-28"
+ pe.paid_amount = 690
+ pe.received_amount = 690
pe.insert()
pe.submit()
@@ -290,4 +301,4 @@
si.save()
si.submit()
- frappe.flags.test_payments_created = True
\ No newline at end of file
+ frappe.flags.test_payments_created = True
diff --git a/erpnext/accounts/doctype/budget/budget.js b/erpnext/accounts/doctype/budget/budget.js
index cadf1e7..e162e32 100644
--- a/erpnext/accounts/doctype/budget/budget.js
+++ b/erpnext/accounts/doctype/budget/budget.js
@@ -1,24 +1,9 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Budget', {
onload: function(frm) {
- frm.set_query("cost_center", function() {
- return {
- filters: {
- company: frm.doc.company
- }
- }
- })
-
- frm.set_query("project", function() {
- return {
- filters: {
- company: frm.doc.company
- }
- }
- })
-
frm.set_query("account", "accounts", function() {
return {
filters: {
@@ -26,16 +11,18 @@
report_type: "Profit and Loss",
is_group: 0
}
- }
- })
-
+ };
+ });
+
frm.set_query("monthly_distribution", function() {
return {
filters: {
fiscal_year: frm.doc.fiscal_year
}
- }
- })
+ };
+ });
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
diff --git a/erpnext/accounts/doctype/budget/test_budget.py b/erpnext/accounts/doctype/budget/test_budget.py
index 0f115f9..c5ec23c 100644
--- a/erpnext/accounts/doctype/budget/test_budget.py
+++ b/erpnext/accounts/doctype/budget/test_budget.py
@@ -122,8 +122,10 @@
frappe.db.set_value("Budget", budget.name, "action_if_accumulated_monthly_budget_exceeded", "Stop")
+ project = frappe.get_value("Project", {"project_name": "_Test Project"})
+
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate())
+ "_Test Bank - _TC", 40000, "_Test Cost Center - _TC", project=project, posting_date=nowdate())
self.assertRaises(BudgetError, jv.submit)
@@ -147,8 +149,11 @@
budget = make_budget(budget_against="Project")
+ project = frappe.get_value("Project", {"project_name": "_Test Project"})
+
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 250000, "_Test Cost Center - _TC", project="_Test Project", posting_date=nowdate())
+ "_Test Bank - _TC", 250000, "_Test Cost Center - _TC",
+ project=project, posting_date=nowdate())
self.assertRaises(BudgetError, jv.submit)
@@ -159,10 +164,10 @@
budget = make_budget(budget_against="Cost Center")
month = now_datetime().month
- if month > 10:
- month = 10
+ if month > 9:
+ month = 9
- for i in range(month):
+ for i in range(month+1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
"_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True)
@@ -181,12 +186,14 @@
budget = make_budget(budget_against="Project")
month = now_datetime().month
- if month > 10:
- month = 10
+ if month > 9:
+ month = 9
- for i in range(month):
+ project = frappe.get_value("Project", {"project_name": "_Test Project"})
+ for i in range(month + 1):
jv = make_journal_entry("_Test Account Cost for Goods Sold - _TC",
- "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True, project="_Test Project")
+ "_Test Bank - _TC", 20000, "_Test Cost Center - _TC", posting_date=nowdate(), submit=True,
+ project=project)
self.assertTrue(frappe.db.get_value("GL Entry",
{"voucher_type": "Journal Entry", "voucher_no": jv.name}))
@@ -289,7 +296,7 @@
budget = frappe.new_doc("Budget")
if budget_against == "Project":
- budget.project = "_Test Project"
+ budget.project = frappe.get_value("Project", {"project_name": "_Test Project"})
else:
budget.cost_center =cost_center or "_Test Cost Center - _TC"
diff --git a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
index 340b9dd..622bd33 100644
--- a/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
+++ b/erpnext/accounts/doctype/coupon_code/test_coupon_code.py
@@ -28,22 +28,22 @@
"item_group": "_Test Item Group",
"item_name": "_Test Tesla Car",
"apply_warehouse_wise_reorder_level": 0,
- "warehouse":"Stores - TCP1",
+ "warehouse":"Stores - _TC",
"gst_hsn_code": "999800",
"valuation_rate": 5000,
"standard_rate":5000,
"item_defaults": [{
- "company": "_Test Company with perpetual inventory",
- "default_warehouse": "Stores - TCP1",
+ "company": "_Test Company",
+ "default_warehouse": "Stores - _TC",
"default_price_list":"_Test Price List",
- "expense_account": "Cost of Goods Sold - TCP1",
- "buying_cost_center": "Main - TCP1",
- "selling_cost_center": "Main - TCP1",
- "income_account": "Sales - TCP1"
+ "expense_account": "Cost of Goods Sold - _TC",
+ "buying_cost_center": "Main - _TC",
+ "selling_cost_center": "Main - _TC",
+ "income_account": "Sales - _TC"
}],
"show_in_website": 1,
"route":"-test-tesla-car",
- "website_warehouse": "Stores - TCP1"
+ "website_warehouse": "Stores - _TC"
})
item.insert()
# create test item price
@@ -65,12 +65,12 @@
"items": [{
"item_code": "_Test Tesla Car"
}],
- "warehouse":"Stores - TCP1",
+ "warehouse":"Stores - _TC",
"coupon_code_based":1,
"selling": 1,
"rate_or_discount": "Discount Percentage",
"discount_percentage": 30,
- "company": "_Test Company with perpetual inventory",
+ "company": "_Test Company",
"currency":"INR",
"for_price_list":"_Test Price List"
})
@@ -85,7 +85,7 @@
})
sales_partner.insert()
# create test item coupon code
- if not frappe.db.exists("Coupon Code","SAVE30"):
+ if not frappe.db.exists("Coupon Code", "SAVE30"):
coupon_code = frappe.get_doc({
"doctype": "Coupon Code",
"coupon_name":"SAVE30",
@@ -102,35 +102,27 @@
test_create_test_data()
def tearDown(self):
- frappe.set_user("Administrator")
+ frappe.set_user("Administrator")
- def test_1_check_coupon_code_used_before_so(self):
- coupon_code = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
- # reset used coupon code count
- coupon_code.used=0
- coupon_code.save()
- # check no coupon code is used before sales order is made
- self.assertEqual(coupon_code.get("used"),0)
+ def test_sales_order_with_coupon_code(self):
+ frappe.db.set_value("Coupon Code", "SAVE30", "used", 0)
- def test_2_sales_order_with_coupon_code(self):
- so = make_sales_order(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
- customer="_Test Customer", selling_price_list="_Test Price List", item_code="_Test Tesla Car", rate=5000,qty=1,
+ so = make_sales_order(company='_Test Company', warehouse='Stores - _TC',
+ customer="_Test Customer", selling_price_list="_Test Price List",
+ item_code="_Test Tesla Car", rate=5000, qty=1,
do_not_submit=True)
- so = frappe.get_doc('Sales Order', so.name)
- # check item price before coupon code is applied
self.assertEqual(so.items[0].rate, 5000)
+
so.coupon_code='SAVE30'
so.sales_partner='_Test Coupon Partner'
so.save()
+
# check item price after coupon code is applied
self.assertEqual(so.items[0].rate, 3500)
- so.submit()
- def test_3_check_coupon_code_used_after_so(self):
- doc = frappe.get_doc("Coupon Code", frappe.db.get_value("Coupon Code", {"coupon_name":"SAVE30"}))
- # check no coupon code is used before sales order is made
- self.assertEqual(doc.get("used"),1)
+ so.submit()
+ self.assertEqual(frappe.db.get_value("Coupon Code", "SAVE30", "used"), 1)
diff --git a/erpnext/accounts/doctype/gl_entry/gl_entry.py b/erpnext/accounts/doctype/gl_entry/gl_entry.py
index def9ed6..ce76d0a 100644
--- a/erpnext/accounts/doctype/gl_entry/gl_entry.py
+++ b/erpnext/accounts/doctype/gl_entry/gl_entry.py
@@ -11,8 +11,10 @@
from erpnext.accounts.party import validate_party_gle_currency, validate_party_frozen_disabled
from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.utils import get_fiscal_year
-from erpnext.exceptions import InvalidAccountCurrency
+from erpnext.exceptions import InvalidAccountCurrency, InvalidAccountDimensionError, MandatoryAccountDimensionError
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_checks_for_pl_and_bs_accounts
+from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map
+from six import iteritems
exclude_from_linked_with = True
class GLEntry(Document):
@@ -25,27 +27,30 @@
def validate(self):
self.flags.ignore_submit_comment = True
- self.check_mandatory()
self.validate_and_set_fiscal_year()
self.pl_must_have_cost_center()
- self.validate_cost_center()
- self.check_pl_account()
- self.validate_party()
- self.validate_currency()
+ if not self.flags.from_repost:
+ self.check_mandatory()
+ self.validate_cost_center()
+ self.check_pl_account()
+ self.validate_party()
+ self.validate_currency()
- def on_update_with_args(self, adv_adj, update_outstanding = 'Yes'):
- self.validate_account_details(adv_adj)
- self.validate_dimensions_for_pl_and_bs()
+ def on_update(self):
+ adv_adj = self.flags.adv_adj
+ if not self.flags.from_repost:
+ self.validate_account_details(adv_adj)
+ self.validate_dimensions_for_pl_and_bs()
+ self.validate_allowed_dimensions()
+ validate_balance_type(self.account, adv_adj)
+ validate_frozen_account(self.account, adv_adj)
- validate_frozen_account(self.account, adv_adj)
- validate_balance_type(self.account, adv_adj)
-
- # Update outstanding amt on against voucher
- if self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees'] \
- and self.against_voucher and update_outstanding == 'Yes':
- update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
- self.against_voucher)
+ # Update outstanding amt on against voucher
+ if (self.against_voucher_type in ['Journal Entry', 'Sales Invoice', 'Purchase Invoice', 'Fees']
+ and self.against_voucher and self.flags.update_outstanding == 'Yes'):
+ update_outstanding_amt(self.account, self.party_type, self.party, self.against_voucher_type,
+ self.against_voucher)
def check_mandatory(self):
mandatory = ['account','voucher_type','voucher_no','company']
@@ -53,7 +58,7 @@
if not self.get(k):
frappe.throw(_("{0} is required").format(_(self.meta.get_label(k))))
- account_type = frappe.db.get_value("Account", self.account, "account_type")
+ account_type = frappe.get_cached_value("Account", self.account, "account_type")
if not (self.party_type and self.party):
if account_type == "Receivable":
frappe.throw(_("{0} {1}: Customer is required against Receivable account {2}")
@@ -68,17 +73,15 @@
.format(self.voucher_type, self.voucher_no, self.account))
def pl_must_have_cost_center(self):
- if frappe.db.get_value("Account", self.account, "report_type") == "Profit and Loss":
+ if frappe.get_cached_value("Account", self.account, "report_type") == "Profit and Loss":
if not self.cost_center and self.voucher_type != 'Period Closing Voucher':
frappe.throw(_("{0} {1}: Cost Center is required for 'Profit and Loss' account {2}. Please set up a default Cost Center for the Company.")
.format(self.voucher_type, self.voucher_no, self.account))
def validate_dimensions_for_pl_and_bs(self):
-
account_type = frappe.db.get_value("Account", self.account, "report_type")
for dimension in get_checks_for_pl_and_bs_accounts():
-
if account_type == "Profit and Loss" \
and self.company == dimension.company and dimension.mandatory_for_pl and not dimension.disabled:
if not self.get(dimension.fieldname):
@@ -91,6 +94,25 @@
frappe.throw(_("Accounting Dimension <b>{0}</b> is required for 'Balance Sheet' account {1}.")
.format(dimension.label, self.account))
+ def validate_allowed_dimensions(self):
+ dimension_filter_map = get_dimension_filter_map()
+ for key, value in iteritems(dimension_filter_map):
+ dimension = key[0]
+ account = key[1]
+
+ if self.account == account:
+ if value['is_mandatory'] and not self.get(dimension):
+ frappe.throw(_("{0} is mandatory for account {1}").format(
+ frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), MandatoryAccountDimensionError)
+
+ if value['allow_or_restrict'] == 'Allow':
+ if self.get(dimension) and self.get(dimension) not in value['allowed_dimensions']:
+ frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
+ else:
+ if self.get(dimension) and self.get(dimension) in value['allowed_dimensions']:
+ frappe.throw(_("Invalid value {0} for {1} against account {2}").format(
+ frappe.bold(self.get(dimension)), frappe.bold(frappe.unscrub(dimension)), frappe.bold(self.account)), InvalidAccountDimensionError)
def check_pl_account(self):
if self.is_opening=='Yes' and \
@@ -106,8 +128,8 @@
from tabAccount where name=%s""", self.account, as_dict=1)[0]
if ret.is_group==1:
- frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in
- transactions''').format(self.voucher_type, self.voucher_no, self.account))
+ frappe.throw(_('''{0} {1}: Account {2} is a Group Account and group accounts cannot be used in transactions''')
+ .format(self.voucher_type, self.voucher_no, self.account))
if ret.docstatus==2:
frappe.throw(_("{0} {1}: Account {2} is inactive")
@@ -118,26 +140,18 @@
.format(self.voucher_type, self.voucher_no, self.account, self.company))
def validate_cost_center(self):
- if not hasattr(self, "cost_center_company"):
- self.cost_center_company = {}
+ if not self.cost_center: return
- def _get_cost_center_company():
- if not self.cost_center_company.get(self.cost_center):
- self.cost_center_company[self.cost_center] = frappe.db.get_value(
- "Cost Center", self.cost_center, "company")
+ is_group, company = frappe.get_cached_value('Cost Center',
+ self.cost_center, ['is_group', 'company'])
- return self.cost_center_company[self.cost_center]
-
- def _check_is_group():
- return cint(frappe.get_cached_value('Cost Center', self.cost_center, 'is_group'))
-
- if self.cost_center and _get_cost_center_company() != self.company:
+ if company != self.company:
frappe.throw(_("{0} {1}: Cost Center {2} does not belong to Company {3}")
.format(self.voucher_type, self.voucher_no, self.cost_center, self.company))
- if self.cost_center and _check_is_group():
- frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot
- be used in transactions""").format(self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
+ if (self.voucher_type != 'Period Closing Voucher' and is_group):
+ frappe.throw(_("""{0} {1}: Cost Center {2} is a group cost center and group cost centers cannot be used in transactions""").format(
+ self.voucher_type, self.voucher_no, frappe.bold(self.cost_center)))
def validate_party(self):
validate_party_frozen_disabled(self.party_type, self.party)
@@ -147,7 +161,7 @@
account_currency = get_account_currency(self.account)
if not self.account_currency:
- self.account_currency = company_currency
+ self.account_currency = account_currency or company_currency
if account_currency != self.account_currency:
frappe.throw(_("{0} {1}: Accounting Entry for {2} can only be made in currency: {3}")
@@ -161,7 +175,6 @@
if not self.fiscal_year:
self.fiscal_year = get_fiscal_year(self.posting_date, company=self.company)[0]
-
def validate_balance_type(account, adv_adj=False):
if not adv_adj and account:
balance_must_be = frappe.db.get_value("Account", account, "balance_must_be")
@@ -227,7 +240,7 @@
def validate_frozen_account(account, adv_adj=None):
- frozen_account = frappe.db.get_value("Account", account, "freeze_account")
+ frozen_account = frappe.get_cached_value("Account", account, "freeze_account")
if frozen_account == 'Yes' and not adv_adj:
frozen_accounts_modifier = frappe.db.get_value( 'Accounts Settings', None,
'frozen_accounts_modifier')
diff --git a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
index acc308e..3d80a97 100644
--- a/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
+++ b/erpnext/accounts/doctype/item_tax_template/item_tax_template_dashboard.py
@@ -20,7 +20,8 @@
'items': ['Purchase Invoice', 'Purchase Order', 'Purchase Receipt']
},
{
- 'items': ['Item']
+ 'label': _('Stock'),
+ 'items': ['Item Groups', 'Item']
}
]
}
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js
index ff12967..37b03f3 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.js
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js
@@ -120,6 +120,8 @@
}
}
});
+
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
voucher_type: function(frm){
@@ -197,6 +199,7 @@
this.load_defaults();
this.setup_queries();
this.setup_balance_formatter();
+ erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
onload_post_render: function() {
@@ -222,15 +225,6 @@
return erpnext.journal_entry.account_query(me.frm);
});
- me.frm.set_query("cost_center", "accounts", function(doc, cdt, cdn) {
- return {
- filters: {
- company: me.frm.doc.company,
- is_group: 0
- }
- };
- });
-
me.frm.set_query("party_type", "accounts", function(doc, cdt, cdn) {
const row = locals[cdt][cdn];
@@ -406,6 +400,8 @@
}
}
cur_frm.cscript.update_totals(doc);
+
+ erpnext.accounts.dimensions.copy_dimension_from_first_row(this.frm, cdt, cdn, 'accounts');
},
});
diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.py b/erpnext/accounts/doctype/journal_entry/journal_entry.py
index cd71273..3419bb6 100644
--- a/erpnext/accounts/doctype/journal_entry/journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/journal_entry.py
@@ -6,14 +6,18 @@
from frappe.utils import cstr, flt, fmt_money, formatdate, getdate, nowdate, cint, get_link_to_form
from frappe import msgprint, _, scrub
from erpnext.controllers.accounts_controller import AccountsController
-from erpnext.accounts.utils import get_balance_on, get_account_currency
+from erpnext.accounts.utils import get_balance_on, get_stock_accounts, get_stock_and_account_balance, \
+ get_account_currency, check_if_stock_and_account_balance_synced
from erpnext.accounts.party import get_party_account
from erpnext.hr.doctype.expense_claim.expense_claim import update_reimbursed_amount
-from erpnext.accounts.doctype.invoice_discounting.invoice_discounting import get_party_account_based_on_invoice_discounting
+from erpnext.accounts.doctype.invoice_discounting.invoice_discounting \
+ import get_party_account_based_on_invoice_discounting
from erpnext.accounts.deferred_revenue import get_deferred_booking_accounts
from six import string_types, iteritems
+class StockAccountInvalidTransaction(frappe.ValidationError): pass
+
class JournalEntry(AccountsController):
def __init__(self, *args, **kwargs):
super(JournalEntry, self).__init__(*args, **kwargs)
@@ -46,6 +50,7 @@
self.validate_empty_accounts_table()
self.set_account_and_party_balance()
self.validate_inter_company_accounts()
+ self.validate_stock_accounts()
if not self.title:
self.title = self.get_title()
@@ -57,6 +62,8 @@
self.update_expense_claim()
self.update_inter_company_jv()
self.update_invoice_discounting()
+ check_if_stock_and_account_balance_synced(self.posting_date,
+ self.company, self.doctype, self.name)
def on_cancel(self):
from erpnext.accounts.utils import unlink_ref_doc_from_payment_entries
@@ -96,6 +103,16 @@
if self.total_credit != doc.total_debit or self.total_debit != doc.total_credit:
frappe.throw(_("Total Credit/ Debit Amount should be same as linked Journal Entry"))
+ def validate_stock_accounts(self):
+ stock_accounts = get_stock_accounts(self.company, self.doctype, self.name)
+ for account in stock_accounts:
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
+ self.posting_date, self.company)
+
+ if account_bal == stock_bal:
+ frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
+ .format(account), StockAccountInvalidTransaction)
+
def update_inter_company_jv(self):
if self.voucher_type == "Inter Company Journal Entry" and self.inter_company_journal_entry_reference:
frappe.db.set_value("Journal Entry", self.inter_company_journal_entry_reference,\
@@ -212,11 +229,11 @@
if d.reference_type=="Journal Entry":
account_root_type = frappe.db.get_value("Account", d.account, "root_type")
if account_root_type == "Asset" and flt(d.debit) > 0:
- frappe.throw(_("For {0}, only credit accounts can be linked against another debit entry")
- .format(d.account))
+ frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets credited")
+ .format(d.idx, d.account))
elif account_root_type == "Liability" and flt(d.credit) > 0:
- frappe.throw(_("For {0}, only debit accounts can be linked against another credit entry")
- .format(d.account))
+ frappe.throw(_("Row #{0}: For {1}, you can select reference document only if account gets debited")
+ .format(d.idx, d.account))
if d.reference_name == self.name:
frappe.throw(_("You can not enter current voucher in 'Against Journal Entry' column"))
@@ -1060,4 +1077,4 @@
},
}, target_doc)
- return doclist
\ No newline at end of file
+ return doclist
diff --git a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
index 53c0758..5f003e0 100644
--- a/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
+++ b/erpnext/accounts/doctype/journal_entry/test_journal_entry.py
@@ -6,7 +6,7 @@
from frappe.utils import flt, nowdate
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.exceptions import InvalidAccountCurrency
-from erpnext.accounts.general_ledger import StockAccountInvalidTransaction
+from erpnext.accounts.doctype.journal_entry.journal_entry import StockAccountInvalidTransaction
class TestJournalEntry(unittest.TestCase):
def test_journal_entry_with_against_jv(self):
@@ -75,54 +75,46 @@
elif test_voucher.doctype in ["Sales Order", "Purchase Order"]:
# if test_voucher is a Sales Order/Purchase Order, test error on cancellation of test_voucher
+ frappe.db.set_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order", 0)
submitted_voucher = frappe.get_doc(test_voucher.doctype, test_voucher.name)
self.assertRaises(frappe.LinkExistsError, submitted_voucher.cancel)
def test_jv_against_stock_account(self):
- from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
- set_perpetual_inventory()
+ company = "_Test Company with perpetual inventory"
+ stock_account = get_inventory_account(company)
- jv = frappe.copy_doc({
- "cheque_date": nowdate(),
- "cheque_no": "33",
- "company": "_Test Company with perpetual inventory",
- "doctype": "Journal Entry",
- "accounts": [
- {
- "account": "Debtors - TCP1",
- "party_type": "Customer",
- "party": "_Test Customer",
- "credit_in_account_currency": 400.0,
- "debit_in_account_currency": 0.0,
- "doctype": "Journal Entry Account",
- "parentfield": "accounts",
- "cost_center": "Main - TCP1"
- },
- {
- "account": "_Test Bank - TCP1",
- "credit_in_account_currency": 0.0,
- "debit_in_account_currency": 400.0,
- "doctype": "Journal Entry Account",
- "parentfield": "accounts",
- "cost_center": "Main - TCP1"
- }
- ],
- "naming_series": "_T-Journal Entry-",
- "posting_date": nowdate(),
- "user_remark": "test",
- "voucher_type": "Bank Entry"
- })
+ from erpnext.accounts.utils import get_stock_and_account_balance
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(stock_account, nowdate(), company)
+ diff = flt(account_bal) - flt(stock_bal)
- jv.get("accounts")[0].update({
- "account": get_inventory_account('_Test Company with perpetual inventory'),
- "company": "_Test Company with perpetual inventory",
- "party_type": None,
- "party": None
+ if not diff:
+ diff = 100
+
+ jv = frappe.new_doc("Journal Entry")
+ jv.company = company
+ jv.posting_date = nowdate()
+ jv.append("accounts", {
+ "account": stock_account,
+ "cost_center": "Main - TCP1",
+ "debit_in_account_currency": 0 if diff > 0 else abs(diff),
+ "credit_in_account_currency": diff if diff > 0 else 0
})
+
+ jv.append("accounts", {
+ "account": "Stock Adjustment - TCP1",
+ "cost_center": "Main - TCP1",
+ "debit_in_account_currency": diff if diff > 0 else 0,
+ "credit_in_account_currency": 0 if diff > 0 else abs(diff)
+ })
+ jv.insert()
- self.assertRaises(StockAccountInvalidTransaction, jv.submit)
- jv.cancel()
- set_perpetual_inventory(0)
+ if account_bal == stock_bal:
+ self.assertRaises(StockAccountInvalidTransaction, jv.submit)
+ frappe.db.rollback()
+ else:
+ jv.submit()
+ jv.cancel()
def test_multi_currency(self):
jv = make_journal_entry("_Test Bank USD - _TC",
@@ -168,7 +160,7 @@
self.assertFalse(gle)
def test_reverse_journal_entry(self):
- from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
+ from erpnext.accounts.doctype.journal_entry.journal_entry import make_reverse_journal_entry
jv = make_journal_entry("_Test Bank USD - _TC",
"Sales - _TC", 100, exchange_rate=50, save=False)
@@ -307,15 +299,20 @@
def test_jv_with_project(self):
from erpnext.projects.doctype.project.test_project import make_project
- project = make_project({
- 'project_name': 'Journal Entry Project',
- 'project_template_name': 'Test Project Template',
- 'start_date': '2020-01-01'
- })
+
+ if not frappe.db.exists("Project", {"project_name": "Journal Entry Project"}):
+ project = make_project({
+ 'project_name': 'Journal Entry Project',
+ 'project_template_name': 'Test Project Template',
+ 'start_date': '2020-01-01'
+ })
+ project_name = project.name
+ else:
+ project_name = frappe.get_value("Project", {"project_name": "_Test Project"})
jv = make_journal_entry("_Test Cash - _TC", "_Test Bank - _TC", 100, save=False)
for d in jv.accounts:
- d.project = project.project_name
+ d.project = project_name
jv.voucher_type = "Bank Entry"
jv.multi_currency = 0
jv.cheque_no = "112233"
@@ -325,10 +322,10 @@
expected_values = {
"_Test Cash - _TC": {
- "project": project.project_name
+ "project": project_name
},
"_Test Bank - _TC": {
- "project": project.project_name
+ "project": project_name
}
}
diff --git a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
index 524a671..f90f867 100644
--- a/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
+++ b/erpnext/accounts/doctype/loyalty_program/loyalty_program.js
@@ -1,6 +1,8 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
+
frappe.ui.form.on('Loyalty Program', {
setup: function(frm) {
var help_content =
@@ -46,20 +48,17 @@
};
});
- frm.set_query("cost_center", function() {
- return {
- filters: {
- company: frm.doc.company
- }
- };
- });
-
frm.set_value("company", frappe.defaults.get_user_default("Company"));
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
if (frm.doc.loyalty_program_type === "Single Tier Program" && frm.doc.collection_rules.length > 1) {
frappe.throw(__("Please select the Multiple Tier Program type for more than one collection rules."));
}
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
});
diff --git a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
index 5278d8b..3199488 100644
--- a/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
+++ b/erpnext/accounts/doctype/loyalty_program/test_loyalty_program.py
@@ -8,12 +8,10 @@
from frappe.utils import today, cint, flt, getdate
from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points
from erpnext.accounts.party import get_dashboard_info
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestLoyaltyProgram(unittest.TestCase):
@classmethod
def setUpClass(self):
- set_perpetual_inventory(0)
# create relevant item, customer, loyalty program, etc
create_records()
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
index 3ce5701..b2e8626 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.js
@@ -36,6 +36,8 @@
frm.dashboard.show_progress(data.title, (data.count / data.total) * 100, data.message);
frm.page.set_indicator(__('In Progress'), 'orange');
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
@@ -100,6 +102,7 @@
}
})
}
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
invoice_type: function(frm) {
@@ -118,7 +121,8 @@
frappe.render_template('opening_invoice_creation_tool_dashboard', {
data: opening_invoices_summary,
max_count: max_count
- })
+ }),
+ __("Opening Invoices Summary")
);
section.on('click', '.invoice-link', function() {
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
index d51856a..76027a3 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool.py
@@ -64,11 +64,11 @@
prepare_invoice_summary(doctype, invoices)
return invoices_summary, max_count
-
+
def validate_company(self):
if not self.company:
frappe.throw(_("Please select the Company"))
-
+
def set_missing_values(self, row):
row.qty = row.qty or 1.0
row.temporary_opening_account = row.temporary_opening_account or get_temporary_opening_account(self.company)
@@ -155,7 +155,8 @@
"posting_date": row.posting_date,
frappe.scrub(row.party_type): row.party,
"is_pos": 0,
- "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice"
+ "doctype": "Sales Invoice" if self.invoice_type == "Sales" else "Purchase Invoice",
+ "update_stock": 0
})
accounting_dimension = get_accounting_dimensions()
@@ -209,7 +210,7 @@
frappe.db.commit()
if errors:
frappe.msgprint(_("You had {} errors while creating opening invoices. Check {} for more details")
- .format(errors, "<a href='#List/Error Log' class='variant-click'>Error Log</a>"), indicator="red", title=_("Error Occured"))
+ .format(errors, "<a href='/app/List/Error Log' class='variant-click'>Error Log</a>"), indicator="red", title=_("Error Occured"))
return names
def publish(index, total, doctype):
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html
index 5b136d4..afbcfa5 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/opening_invoice_creation_tool_dashboard.html
@@ -1,4 +1,3 @@
-<h5 style="margin-top: 0px;">{{ __("Opening Invoices Summary") }}</h5>
{% $.each(data, (company, summary) => { %}
<h6 style="margin: 15px 0px -10px 0px;"><a class="company-link"> {{ company }}</a></h6>
diff --git a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
index 54229f5..bdfe532 100644
--- a/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
+++ b/erpnext/accounts/doctype/opening_invoice_creation_tool/test_opening_invoice_creation_tool.py
@@ -7,17 +7,24 @@
import unittest
test_dependencies = ["Customer", "Supplier"]
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter
from erpnext.accounts.doctype.opening_invoice_creation_tool.opening_invoice_creation_tool import get_temporary_opening_account
class TestOpeningInvoiceCreationTool(unittest.TestCase):
- def make_invoices(self, invoice_type="Sales"):
+ def setUp(self):
+ if not frappe.db.exists("Company", "_Test Opening Invoice Company"):
+ make_company()
+
+ def make_invoices(self, invoice_type="Sales", company=None, party_1=None, party_2=None):
doc = frappe.get_single("Opening Invoice Creation Tool")
- args = get_opening_invoice_creation_dict(invoice_type=invoice_type)
+ args = get_opening_invoice_creation_dict(invoice_type=invoice_type, company=company,
+ party_1=party_1, party_2=party_2)
doc.update(args)
return doc.make_invoices()
def test_opening_sales_invoice_creation(self):
- invoices = self.make_invoices()
+ property_setter = make_property_setter("Sales Invoice", "update_stock", "default", 1, "Check")
+ invoices = self.make_invoices(company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
@@ -27,6 +34,13 @@
}
self.check_expected_values(invoices, expected_value)
+ si = frappe.get_doc("Sales Invoice", invoices[0])
+
+ # Check if update stock is not enabled
+ self.assertEqual(si.update_stock, 0)
+
+ property_setter.delete()
+
def check_expected_values(self, invoices, expected_value, invoice_type="Sales"):
doctype = "Sales Invoice" if invoice_type == "Sales" else "Purchase Invoice"
@@ -36,7 +50,7 @@
self.assertEqual(si.get(field, ""), expected_value[invoice_idx][field_idx])
def test_opening_purchase_invoice_creation(self):
- invoices = self.make_invoices(invoice_type="Purchase")
+ invoices = self.make_invoices(invoice_type="Purchase", company="_Test Opening Invoice Company")
self.assertEqual(len(invoices), 2)
expected_value = {
@@ -46,6 +60,32 @@
}
self.check_expected_values(invoices, expected_value, "Purchase")
+ def test_opening_sales_invoice_creation_with_missing_debit_account(self):
+ company = "_Test Opening Invoice Company"
+ party_1, party_2 = make_customer("Customer A"), make_customer("Customer B")
+
+ old_default_receivable_account = frappe.db.get_value("Company", company, "default_receivable_account")
+ frappe.db.set_value("Company", company, "default_receivable_account", "")
+
+ if not frappe.db.exists("Cost Center", "_Test Opening Invoice Company - _TOIC"):
+ cc = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "_Test Opening Invoice Company",
+ "is_group": 1, "company": "_Test Opening Invoice Company"})
+ cc.insert(ignore_mandatory=True)
+ cc2 = frappe.get_doc({"doctype": "Cost Center", "cost_center_name": "Main", "is_group": 0,
+ "company": "_Test Opening Invoice Company", "parent_cost_center": cc.name})
+ cc2.insert()
+
+ frappe.db.set_value("Company", company, "cost_center", "Main - _TOIC")
+
+ self.make_invoices(company="_Test Opening Invoice Company", party_1=party_1, party_2=party_2)
+
+ # Check if missing debit account error raised
+ error_log = frappe.db.exists("Error Log", {"error": ["like", "%erpnext.controllers.accounts_controller.AccountMissingError%"]})
+ self.assertTrue(error_log)
+
+ # teardown
+ frappe.db.set_value("Company", company, "default_receivable_account", old_default_receivable_account)
+
def get_opening_invoice_creation_dict(**args):
party = "Customer" if args.get("invoice_type", "Sales") == "Sales" else "Supplier"
company = args.get("company", "_Test Company")
@@ -57,7 +97,7 @@
{
"qty": 1.0,
"outstanding_amount": 300,
- "party": "_Test {0}".format(party),
+ "party": args.get("party_1") or "_Test {0}".format(party),
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
@@ -66,7 +106,7 @@
{
"qty": 2.0,
"outstanding_amount": 250,
- "party": "_Test {0} 1".format(party),
+ "party": args.get("party_2") or "_Test {0} 1".format(party),
"item_name": "Opening Item",
"due_date": "2016-09-10",
"posting_date": "2016-09-05",
@@ -76,4 +116,31 @@
})
invoice_dict.update(args)
- return invoice_dict
\ No newline at end of file
+ return invoice_dict
+
+def make_company():
+ if frappe.db.exists("Company", "_Test Opening Invoice Company"):
+ return frappe.get_doc("Company", "_Test Opening Invoice Company")
+
+ company = frappe.new_doc("Company")
+ company.company_name = "_Test Opening Invoice Company"
+ company.abbr = "_TOIC"
+ company.default_currency = "INR"
+ company.country = "India"
+ company.insert()
+ return company
+
+def make_customer(customer=None):
+ customer_name = customer or "Opening Customer"
+ customer = frappe.get_doc({
+ "doctype": "Customer",
+ "customer_name": customer_name,
+ "customer_group": "All Customer Groups",
+ "customer_type": "Company",
+ "territory": "All Territories"
+ })
+ if not frappe.db.exists("Customer", customer_name):
+ customer.insert(ignore_permissions=True)
+ return customer.name
+ else:
+ return frappe.db.exists("Customer", customer_name)
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.js b/erpnext/accounts/doctype/payment_entry/payment_entry.js
index e117471..f5c488d 100644
--- a/erpnext/accounts/doctype/payment_entry/payment_entry.js
+++ b/erpnext/accounts/doctype/payment_entry/payment_entry.js
@@ -1,6 +1,7 @@
// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
{% include "erpnext/public/js/controllers/accounts.js" %}
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Payment Entry', {
onload: function(frm) {
@@ -8,6 +9,8 @@
if (!frm.doc.paid_from) frm.set_value("paid_from_account_currency", null);
if (!frm.doc.paid_to) frm.set_value("paid_to_account_currency", null);
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
setup: function(frm) {
@@ -88,15 +91,6 @@
}
});
- frm.set_query("cost_center", "deductions", function() {
- return {
- filters: {
- "is_group": 0,
- "company": frm.doc.company
- }
- }
- });
-
frm.set_query("reference_doctype", "references", function() {
if (frm.doc.party_type=="Customer") {
var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"];
@@ -167,6 +161,7 @@
company: function(frm) {
frm.events.hide_unhide_fields(frm);
frm.events.set_dynamic_labels(frm);
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
contact_person: function(frm) {
@@ -401,6 +396,8 @@
set_account_currency_and_balance: function(frm, account, currency_field,
balance_field, callback_function) {
+
+ var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
if (frm.doc.posting_date && account) {
frappe.call({
method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_account_details",
@@ -427,6 +424,14 @@
if(!frm.doc.paid_amount && frm.doc.received_amount)
frm.events.received_amount(frm);
+
+ if (frm.doc.paid_from_account_currency == frm.doc.paid_to_account_currency
+ && frm.doc.paid_amount != frm.doc.received_amount) {
+ if (company_currency != frm.doc.paid_from_account_currency &&
+ frm.doc.payment_type == "Pay") {
+ frm.doc.paid_amount = frm.doc.received_amount;
+ }
+ }
}
},
() => {
diff --git a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
index 791b03a..f7a15c0 100644
--- a/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
+++ b/erpnext/accounts/doctype/payment_reconciliation/payment_reconciliation.py
@@ -88,19 +88,19 @@
voucher_type = ('Sales Invoice'
if self.party_type == 'Customer' else "Purchase Invoice")
- return frappe.db.sql(""" SELECT `tab{doc}`.name as reference_name, %(voucher_type)s as reference_type,
- (sum(`tabGL Entry`.{dr_or_cr}) - sum(`tabGL Entry`.{reconciled_dr_or_cr})) as amount,
+ return frappe.db.sql(""" SELECT doc.name as reference_name, %(voucher_type)s as reference_type,
+ (sum(gl.{dr_or_cr}) - sum(gl.{reconciled_dr_or_cr})) as amount,
account_currency as currency
- FROM `tab{doc}`, `tabGL Entry`
+ FROM `tab{doc}` doc, `tabGL Entry` gl
WHERE
- (`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no)
- and `tab{doc}`.{party_type_field} = %(party)s
- and `tab{doc}`.is_return = 1 and `tab{doc}`.return_against IS NULL
- and `tabGL Entry`.against_voucher_type = %(voucher_type)s
- and `tab{doc}`.docstatus = 1 and `tabGL Entry`.party = %(party)s
- and `tabGL Entry`.party_type = %(party_type)s and `tabGL Entry`.account = %(account)s
- and `tabGL Entry`.is_cancelled = 0
- GROUP BY `tab{doc}`.name
+ (doc.name = gl.against_voucher or doc.name = gl.voucher_no)
+ and doc.{party_type_field} = %(party)s
+ and doc.is_return = 1 and ifnull(doc.return_against, "") = ""
+ and gl.against_voucher_type = %(voucher_type)s
+ and doc.docstatus = 1 and gl.party = %(party)s
+ and gl.party_type = %(party_type)s and gl.account = %(account)s
+ and gl.is_cancelled = 0
+ GROUP BY doc.name
Having
amount > 0
""".format(
@@ -113,7 +113,7 @@
'party_type': self.party_type,
'voucher_type': voucher_type,
'account': self.receivable_payable_account
- }, as_dict=1)
+ }, as_dict=1, debug=1)
def add_payment_entries(self, entries):
self.set('payments', [])
diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py
index 1b97050..53ac996 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/payment_request.py
@@ -3,6 +3,7 @@
# For license information, please see license.txt
from __future__ import unicode_literals
+import json
import frappe
from frappe import _
from frappe.model.document import Document
@@ -82,18 +83,37 @@
self.make_communication_entry()
elif self.payment_channel == "Phone":
- controller = get_payment_gateway_controller(self.payment_gateway)
- payment_record = dict(
- reference_doctype="Payment Request",
- reference_docname=self.name,
- payment_reference=self.reference_name,
- grand_total=self.grand_total,
- sender=self.email_to,
- currency=self.currency,
- payment_gateway=self.payment_gateway
- )
- controller.validate_transaction_currency(self.currency)
- controller.request_for_payment(**payment_record)
+ self.request_phone_payment()
+
+ def request_phone_payment(self):
+ controller = get_payment_gateway_controller(self.payment_gateway)
+ request_amount = self.get_request_amount()
+
+ payment_record = dict(
+ reference_doctype="Payment Request",
+ reference_docname=self.name,
+ payment_reference=self.reference_name,
+ request_amount=request_amount,
+ sender=self.email_to,
+ currency=self.currency,
+ payment_gateway=self.payment_gateway
+ )
+
+ controller.validate_transaction_currency(self.currency)
+ controller.request_for_payment(**payment_record)
+
+ def get_request_amount(self):
+ data_of_completed_requests = frappe.get_all("Integration Request", filters={
+ 'reference_doctype': self.doctype,
+ 'reference_docname': self.name,
+ 'status': 'Completed'
+ }, pluck="data")
+
+ if not data_of_completed_requests:
+ return self.grand_total
+
+ request_amounts = sum([json.loads(d).get('request_amount') for d in data_of_completed_requests])
+ return request_amounts
def on_cancel(self):
self.check_if_payment_entry_exists()
@@ -351,8 +371,8 @@
if args.order_type == "Shopping Cart" or args.mute_email:
pr.flags.mute_email = True
+ pr.insert(ignore_permissions=True)
if args.submit_doc:
- pr.insert(ignore_permissions=True)
pr.submit()
if args.order_type == "Shopping Cart":
@@ -412,8 +432,8 @@
def get_gateway_details(args):
"""return gateway and payment account of default payment gateway"""
- if args.get("payment_gateway"):
- return get_payment_gateway_account(args.get("payment_gateway"))
+ if args.get("payment_gateway_account"):
+ return get_payment_gateway_account(args.get("payment_gateway_account"))
if args.order_type == "Shopping Cart":
payment_gateway_account = frappe.get_doc("Shopping Cart Settings").payment_gateway_account
diff --git a/erpnext/accounts/doctype/payment_request/payment_request_list.js b/erpnext/accounts/doctype/payment_request/payment_request_list.js
index 72833d2..85d729c 100644
--- a/erpnext/accounts/doctype/payment_request/payment_request_list.js
+++ b/erpnext/accounts/doctype/payment_request/payment_request_list.js
@@ -2,7 +2,7 @@
add_fields: ["status"],
get_indicator: function(doc) {
if(doc.status == "Draft") {
- return [__("Draft"), "darkgrey", "status,=,Draft"];
+ return [__("Draft"), "gray", "status,=,Draft"];
}
if(doc.status == "Requested") {
return [__("Requested"), "green", "status,=,Requested"];
@@ -19,5 +19,5 @@
else if(doc.status == "Cancelled") {
return [__("Cancelled"), "red", "status,=,Cancelled"];
}
- }
+ }
}
diff --git a/erpnext/accounts/doctype/payment_request/test_payment_request.py b/erpnext/accounts/doctype/payment_request/test_payment_request.py
index 8a10e2c..5eba62c 100644
--- a/erpnext/accounts/doctype/payment_request/test_payment_request.py
+++ b/erpnext/accounts/doctype/payment_request/test_payment_request.py
@@ -45,7 +45,8 @@
def test_payment_request_linkings(self):
so_inr = make_sales_order(currency="INR")
- pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com")
+ pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
+ payment_gateway_account="_Test Gateway - INR")
self.assertEqual(pr.reference_doctype, "Sales Order")
self.assertEqual(pr.reference_name, so_inr.name)
@@ -54,7 +55,8 @@
conversion_rate = get_exchange_rate("USD", "INR")
si_usd = create_sales_invoice(currency="USD", conversion_rate=conversion_rate)
- pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com")
+ pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
+ payment_gateway_account="_Test Gateway - USD")
self.assertEqual(pr.reference_doctype, "Sales Invoice")
self.assertEqual(pr.reference_name, si_usd.name)
@@ -68,7 +70,7 @@
so_inr = make_sales_order(currency="INR")
pr = make_payment_request(dt="Sales Order", dn=so_inr.name, recipient_id="saurabh@erpnext.com",
- mute_email=1, submit_doc=1, return_doc=1)
+ mute_email=1, payment_gateway_account="_Test Gateway - INR", submit_doc=1, return_doc=1)
pe = pr.set_as_paid()
so_inr = frappe.get_doc("Sales Order", so_inr.name)
@@ -79,7 +81,7 @@
currency="USD", conversion_rate=50)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
- mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1)
+ mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
pe = pr.set_as_paid()
@@ -106,7 +108,7 @@
currency="USD", conversion_rate=50)
pr = make_payment_request(dt="Sales Invoice", dn=si_usd.name, recipient_id="saurabh@erpnext.com",
- mute_email=1, payment_gateway="_Test Gateway - USD", submit_doc=1, return_doc=1)
+ mute_email=1, payment_gateway_account="_Test Gateway - USD", submit_doc=1, return_doc=1)
pe = pr.create_payment_entry()
pr.load_from_db()
diff --git a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
index 7dd5b01..a74fa06 100644
--- a/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
+++ b/erpnext/accounts/doctype/period_closing_voucher/period_closing_voucher.py
@@ -8,7 +8,7 @@
from erpnext.accounts.utils import get_account_currency
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import (get_accounting_dimensions,
- get_dimension_filters)
+ get_dimensions)
class PeriodClosingVoucher(AccountsController):
def validate(self):
@@ -58,7 +58,7 @@
for dimension in accounting_dimensions:
dimension_fields.append('t1.{0}'.format(dimension))
- dimension_filters, default_dimensions = get_dimension_filters()
+ dimension_filters, default_dimensions = get_dimensions()
pl_accounts = self.get_pl_balances(dimension_fields)
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
index 57baac7..9ea616f 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
@@ -3,6 +3,7 @@
frappe.ui.form.on('POS Closing Entry', {
onload: function(frm) {
+ frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
frm.set_query("pos_profile", function(doc) {
return {
filters: { 'user': doc.user }
@@ -20,7 +21,7 @@
return { filters: { 'status': 'Open', 'docstatus': 1 } };
});
- if (frm.doc.docstatus === 0) frm.set_value("period_end_date", frappe.datetime.now_datetime());
+ if (frm.doc.docstatus === 0 && !frm.doc.amended_from) frm.set_value("period_end_date", frappe.datetime.now_datetime());
if (frm.doc.docstatus === 1) set_html_data(frm);
},
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
index 32bca3b..a9b91e0 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.json
@@ -6,11 +6,13 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
+ "period_details_section",
"period_start_date",
"period_end_date",
"column_break_3",
"posting_date",
"pos_opening_entry",
+ "status",
"section_break_5",
"company",
"column_break_7",
@@ -64,7 +66,8 @@
},
{
"fieldname": "section_break_5",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "User Details"
},
{
"fieldname": "company",
@@ -120,7 +123,7 @@
"collapsible_depends_on": "eval:doc.docstatus==0",
"fieldname": "section_break_13",
"fieldtype": "Section Break",
- "label": "Details"
+ "label": "Totals"
},
{
"default": "0",
@@ -184,11 +187,32 @@
"label": "POS Opening Entry",
"options": "POS Opening Entry",
"reqd": 1
+ },
+ {
+ "allow_on_submit": 1,
+ "default": "Draft",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "hidden": 1,
+ "label": "Status",
+ "options": "Draft\nSubmitted\nQueued\nCancelled",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "period_details_section",
+ "fieldtype": "Section Break",
+ "label": "Period Details"
}
],
"is_submittable": 1,
- "links": [],
- "modified": "2020-05-29 15:03:22.226113",
+ "links": [
+ {
+ "link_doctype": "POS Invoice Merge Log",
+ "link_fieldname": "pos_closing_entry"
+ }
+ ],
+ "modified": "2021-02-01 13:47:20.722104",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Closing Entry",
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
index 2b91c74..f5224a2 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
@@ -6,13 +6,12 @@
import frappe
import json
from frappe import _
-from frappe.model.document import Document
-from frappe.utils import getdate, get_datetime, flt
-from collections import defaultdict
+from frappe.utils import get_datetime, flt
+from erpnext.controllers.status_updater import StatusUpdater
from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data
-from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices, unconsolidate_pos_invoices
-class POSClosingEntry(Document):
+class POSClosingEntry(StatusUpdater):
def validate(self):
if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open":
frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry"))
@@ -21,11 +20,16 @@
self.validate_pos_invoices()
def validate_pos_closing(self):
- user = frappe.get_all("POS Closing Entry",
- filters = { "user": self.user, "docstatus": 1, "pos_profile": self.pos_profile },
- or_filters = {
- "period_start_date": ("between", [self.period_start_date, self.period_end_date]),
- "period_end_date": ("between", [self.period_start_date, self.period_end_date])
+ user = frappe.db.sql("""
+ SELECT name FROM `tabPOS Closing Entry`
+ WHERE
+ user = %(user)s AND docstatus = 1 AND pos_profile = %(profile)s AND
+ (period_start_date between %(start)s and %(end)s OR period_end_date between %(start)s and %(end)s)
+ """, {
+ 'user': self.user,
+ 'profile': self.pos_profile,
+ 'start': self.period_start_date,
+ 'end': self.period_end_date
})
if user:
@@ -57,20 +61,29 @@
if not invalid_rows:
return
- error_list = [_("Row #{}: {}").format(row.get('idx'), row.get('msg')) for row in invalid_rows]
- frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
+ error_list = []
+ for row in invalid_rows:
+ for msg in row.get('msg'):
+ error_list.append(_("Row #{}: {}").format(row.get('idx'), msg))
- def on_submit(self):
- merge_pos_invoices(self.pos_transactions)
- opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
- opening_entry.pos_closing_entry = self.name
- opening_entry.set_status()
- opening_entry.save()
+ frappe.throw(error_list, title=_("Invalid POS Invoices"), as_list=True)
def get_payment_reconciliation_details(self):
currency = frappe.get_cached_value('Company', self.company, "default_currency")
return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html",
{"data": self, "currency": currency})
+
+ def on_submit(self):
+ consolidate_pos_invoices(closing_entry=self)
+
+ def on_cancel(self):
+ unconsolidate_pos_invoices(closing_entry=self)
+
+ def update_opening_entry(self, for_cancel=False):
+ opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry)
+ opening_entry.pos_closing_entry = self.name if not for_cancel else None
+ opening_entry.set_status()
+ opening_entry.save()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js
new file mode 100644
index 0000000..20fd610
--- /dev/null
+++ b/erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry_list.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
+// License: GNU General Public License v3. See license.txt
+
+// render
+frappe.listview_settings['POS Closing Entry'] = {
+ get_indicator: function(doc) {
+ var status_color = {
+ "Draft": "red",
+ "Submitted": "blue",
+ "Queued": "orange",
+ "Cancelled": "red"
+
+ };
+ return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
+ }
+};
diff --git a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
index 8de54d5..40db09e 100644
--- a/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
+++ b/erpnext/accounts/doctype/pos_closing_entry/test_pos_closing_entry.py
@@ -13,7 +13,6 @@
class TestPOSClosingEntry(unittest.TestCase):
def test_pos_closing_entry(self):
test_user, pos_profile = init_user_and_profile()
-
opening_entry = create_opening_entry(pos_profile, test_user.name)
pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
@@ -45,6 +44,49 @@
frappe.set_user("Administrator")
frappe.db.sql("delete from `tabPOS Profile`")
+ def test_cancelling_of_pos_closing_entry(self):
+ test_user, pos_profile = init_user_and_profile()
+ opening_entry = create_opening_entry(pos_profile, test_user.name)
+
+ pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1)
+ pos_inv1.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500
+ })
+ pos_inv1.submit()
+
+ pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1)
+ pos_inv2.append('payments', {
+ 'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200
+ })
+ pos_inv2.submit()
+
+ pcv_doc = make_closing_entry_from_opening(opening_entry)
+ payment = pcv_doc.payment_reconciliation[0]
+
+ self.assertEqual(payment.mode_of_payment, 'Cash')
+
+ for d in pcv_doc.payment_reconciliation:
+ if d.mode_of_payment == 'Cash':
+ d.closing_amount = 6700
+
+ pcv_doc.submit()
+
+ pos_inv1.load_from_db()
+ self.assertRaises(frappe.ValidationError, pos_inv1.cancel)
+
+ si_doc = frappe.get_doc("Sales Invoice", pos_inv1.consolidated_invoice)
+ self.assertRaises(frappe.ValidationError, si_doc.cancel)
+
+ pcv_doc.load_from_db()
+ pcv_doc.cancel()
+ si_doc.load_from_db()
+ pos_inv1.load_from_db()
+ self.assertEqual(si_doc.docstatus, 2)
+ self.assertEqual(pos_inv1.status, 'Paid')
+
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabPOS Profile`")
+
def init_user_and_profile(**args):
user = 'test@example.com'
test_user = frappe.get_doc('User', user)
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
index 86062d1..493bd44 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.js
@@ -2,6 +2,7 @@
// For license information, please see license.txt
{% include 'erpnext/selling/sales_common.js' %};
+frappe.provide("erpnext.accounts");
erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend({
setup(doc) {
@@ -9,12 +10,19 @@
this._super(doc);
},
+ company: function() {
+ erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
+ },
+
onload(doc) {
this._super();
+ this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice Merge Log'];
if(doc.__islocal && doc.is_pos && frappe.get_route_str() !== 'point-of-sale') {
this.frm.script_manager.trigger("is_pos");
this.frm.refresh_fields();
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
refresh(doc) {
@@ -187,18 +195,43 @@
},
request_for_payment: function (frm) {
+ if (!frm.doc.contact_mobile) {
+ frappe.throw(__('Please enter mobile number first.'));
+ }
+ frm.dirty();
frm.save().then(() => {
- frappe.dom.freeze();
- frappe.call({
- method: 'create_payment_request',
- doc: frm.doc,
- })
+ frappe.dom.freeze(__('Waiting for payment...'));
+ frappe
+ .call({
+ method: 'create_payment_request',
+ doc: frm.doc
+ })
.fail(() => {
frappe.dom.unfreeze();
- frappe.msgprint('Payment request failed');
+ frappe.msgprint(__('Payment request failed'));
})
- .then(() => {
- frappe.msgprint('Payment request sent successfully');
+ .then(({ message }) => {
+ const payment_request_name = message.name;
+ setTimeout(() => {
+ frappe.db.get_value('Payment Request', payment_request_name, ['status', 'grand_total']).then(({ message }) => {
+ if (message.status != 'Paid') {
+ frappe.dom.unfreeze();
+ frappe.msgprint({
+ message: __('Payment Request took too long to respond. Please try requesting for payment again.'),
+ title: __('Request Timeout')
+ });
+ } else if (frappe.dom.freeze_count != 0) {
+ frappe.dom.unfreeze();
+ cur_frm.reload_doc();
+ cur_pos.payment.events.submit_invoice();
+
+ frappe.show_alert({
+ message: __("Payment of {0} received successfully.", [format_currency(message.grand_total, frm.doc.currency, 0)]),
+ indicator: 'green'
+ });
+ }
+ });
+ }, 60000);
});
});
}
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
index 5bc57b4..7459c11 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.json
@@ -13,11 +13,11 @@
"customer",
"customer_name",
"tax_id",
- "is_pos",
"pos_profile",
- "offline_pos_name",
- "is_return",
"consolidated_invoice",
+ "is_pos",
+ "is_return",
+ "update_billed_amount_in_sales_order",
"column_break1",
"company",
"posting_date",
@@ -25,10 +25,7 @@
"set_posting_time",
"due_date",
"amended_from",
- "returns",
"return_against",
- "column_break_21",
- "update_billed_amount_in_sales_order",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -183,8 +180,7 @@
"column_break_140",
"auto_repeat",
"update_auto_repeat_reference",
- "against_income_account",
- "pos_total_qty"
+ "against_income_account"
],
"fields": [
{
@@ -266,14 +262,6 @@
"print_hide": 1
},
{
- "fieldname": "offline_pos_name",
- "fieldtype": "Data",
- "hidden": 1,
- "label": "Offline POS Name",
- "print_hide": 1,
- "read_only": 1
- },
- {
"allow_on_submit": 1,
"default": "0",
"fieldname": "is_return",
@@ -350,25 +338,15 @@
},
{
"depends_on": "return_against",
- "fieldname": "returns",
- "fieldtype": "Section Break",
- "label": "Returns"
- },
- {
- "depends_on": "return_against",
"fieldname": "return_against",
"fieldtype": "Link",
- "label": "Return Against POS Invoice",
+ "label": "Return Against",
"no_copy": 1,
"options": "POS Invoice",
"print_hide": 1,
"read_only": 1
},
{
- "fieldname": "column_break_21",
- "fieldtype": "Column Break"
- },
- {
"default": "0",
"depends_on": "eval: doc.is_return && doc.return_against",
"fieldname": "update_billed_amount_in_sales_order",
@@ -587,19 +565,21 @@
},
{
"fieldname": "sec_warehouse",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Warehouse"
},
{
"depends_on": "update_stock",
"fieldname": "set_warehouse",
"fieldtype": "Link",
- "label": "Set Source Warehouse",
+ "label": "Source Warehouse",
"options": "Warehouse",
"print_hide": 1
},
{
"fieldname": "items_section",
"fieldtype": "Section Break",
+ "label": "Items",
"oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart"
},
@@ -1501,7 +1481,7 @@
"allow_on_submit": 1,
"fieldname": "sales_team",
"fieldtype": "Table",
- "label": "Sales Team1",
+ "label": "Sales Team",
"oldfieldname": "sales_team",
"oldfieldtype": "Table",
"options": "Sales Team",
@@ -1561,15 +1541,6 @@
"report_hide": 1
},
{
- "fieldname": "pos_total_qty",
- "fieldtype": "Float",
- "hidden": 1,
- "label": "Total Qty",
- "print_hide": 1,
- "print_hide_if_no_value": 1,
- "read_only": 1
- },
- {
"allow_on_submit": 1,
"fieldname": "consolidated_invoice",
"fieldtype": "Link",
@@ -1581,7 +1552,7 @@
"icon": "fa fa-file-text",
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 13:56:51.056083",
+ "modified": "2021-02-01 15:03:33.800707",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice",
@@ -1626,7 +1597,6 @@
"role": "All"
}
],
- "quick_entry": 1,
"search_fields": "posting_date, due_date, customer, base_grand_total, outstanding_amount",
"show_name_in_global_search": 1,
"sort_field": "modified",
diff --git a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
index d486ff6..76e0092 100644
--- a/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/pos_invoice.py
@@ -6,10 +6,9 @@
import frappe
from frappe import _
from frappe.model.document import Document
-from erpnext.controllers.selling_controller import SellingController
-from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
from erpnext.accounts.utils import get_account_currency
from erpnext.accounts.party import get_party_account, get_due_date
+from frappe.utils import cint, flt, getdate, nowdate, get_link_to_form
from erpnext.accounts.doctype.payment_request.payment_request import make_payment_request
from erpnext.accounts.doctype.loyalty_program.loyalty_program import validate_loyalty_points
from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos, get_serial_nos
@@ -58,6 +57,22 @@
self.apply_loyalty_points()
self.check_phone_payments()
self.set_status(update=True)
+
+ def before_cancel(self):
+ if self.consolidated_invoice and frappe.db.get_value('Sales Invoice', self.consolidated_invoice, 'docstatus') == 1:
+ pos_closing_entry = frappe.get_all(
+ "POS Invoice Reference",
+ ignore_permissions=True,
+ filters={ 'pos_invoice': self.name },
+ pluck="parent",
+ limit=1
+ )
+ frappe.throw(
+ _('You need to cancel POS Closing Entry {} to be able to cancel this document.').format(
+ get_link_to_form("POS Closing Entry", pos_closing_entry[0])
+ ),
+ title=_('Not Allowed')
+ )
def on_cancel(self):
# run on cancel method of selling controller
@@ -78,7 +93,7 @@
mode_of_payment=pay.mode_of_payment, status="Paid"),
fieldname="grand_total")
- if pay.amount != paid_amt:
+ if paid_amt and pay.amount != paid_amt:
return frappe.throw(_("Payment related to {0} is not completed").format(pay.mode_of_payment))
def validate_stock_availablility(self):
@@ -164,10 +179,18 @@
if d.get("serial_no"):
serial_nos = get_serial_nos(d.serial_no)
for sr in serial_nos:
- serial_no_exists = frappe.db.exists("POS Invoice Item", {
- "parent": self.return_against,
- "serial_no": ["like", d.get("serial_no")]
- })
+ serial_no_exists = frappe.db.sql("""
+ SELECT name
+ FROM `tabPOS Invoice Item`
+ WHERE
+ parent = %s
+ and (serial_no = %s
+ or serial_no like %s
+ or serial_no like %s
+ or serial_no like %s
+ )
+ """, (self.return_against, sr, sr+'\n%', '%\n'+sr, '%\n'+sr+'\n%'))
+
if not serial_no_exists:
bold_return_against = frappe.bold(self.return_against)
bold_serial_no = frappe.bold(sr)
@@ -175,7 +198,7 @@
_("Row #{}: Serial No {} cannot be returned since it was not transacted in original invoice {}")
.format(d.idx, bold_serial_no, bold_return_against)
)
-
+
def validate_non_stock_items(self):
for d in self.get("items"):
is_stock_item = frappe.get_cached_value("Item", d.get("item_code"), "is_stock_item")
@@ -267,6 +290,8 @@
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {}
+ if not pos_profile:
+ frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
self.pos_profile = pos_profile.get('name')
profile = {}
@@ -275,7 +300,7 @@
if not self.get('payments') and not for_validate:
update_multi_mode_option(self, profile)
-
+
if self.is_return and not for_validate:
add_return_modes(self, profile)
@@ -295,9 +320,14 @@
self.set(fieldname, profile.get(fieldname))
if self.customer:
- customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group'])
+ customer_price_list, customer_group, customer_currency = frappe.db.get_value(
+ "Customer", self.customer, ['default_price_list', 'customer_group', 'default_currency']
+ )
customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list')
selling_price_list = customer_price_list or customer_group_price_list or profile.get('selling_price_list')
+ if customer_currency != profile.get('currency'):
+ self.set('currency', customer_currency)
+
else:
selling_price_list = profile.get('selling_price_list')
@@ -362,22 +392,48 @@
if not self.contact_mobile:
frappe.throw(_("Please enter the phone number first"))
- payment_gateway = frappe.db.get_value("Payment Gateway Account", {
- "payment_account": pay.account,
- })
- record = {
- "payment_gateway": payment_gateway,
- "dt": "POS Invoice",
- "dn": self.name,
- "payment_request_type": "Inward",
- "party_type": "Customer",
- "party": self.customer,
- "mode_of_payment": pay.mode_of_payment,
- "recipient_id": self.contact_mobile,
- "submit_doc": True
- }
+ pay_req = self.get_existing_payment_request(pay)
+ if not pay_req:
+ pay_req = self.get_new_payment_request(pay)
+ pay_req.submit()
+ else:
+ pay_req.request_phone_payment()
- return make_payment_request(**record)
+ return pay_req
+
+ def get_new_payment_request(self, mop):
+ payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
+ "payment_account": mop.account,
+ }, ["name"])
+
+ args = {
+ "dt": "POS Invoice",
+ "dn": self.name,
+ "recipient_id": self.contact_mobile,
+ "mode_of_payment": mop.mode_of_payment,
+ "payment_gateway_account": payment_gateway_account,
+ "payment_request_type": "Inward",
+ "party_type": "Customer",
+ "party": self.customer,
+ "return_doc": True
+ }
+ return make_payment_request(**args)
+
+ def get_existing_payment_request(self, pay):
+ payment_gateway_account = frappe.db.get_value("Payment Gateway Account", {
+ "payment_account": pay.account,
+ }, ["name"])
+
+ args = {
+ 'doctype': 'Payment Request',
+ 'reference_doctype': 'POS Invoice',
+ 'reference_name': self.name,
+ 'payment_gateway_account': payment_gateway_account,
+ 'email_to': self.contact_mobile
+ }
+ pr = frappe.db.exists(args)
+ if pr:
+ return frappe.get_doc('Payment Request', pr[0][0])
@frappe.whitelist()
def get_stock_availability(item_code, warehouse):
diff --git a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
index c179360..15875af 100644
--- a/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
+++ b/erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
@@ -198,6 +198,65 @@
self.assertEqual(pos_return.get('payments')[0].amount, -500)
self.assertEqual(pos_return.get('payments')[1].amount, -500)
+ def test_pos_return_for_serialized_item(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ se = make_serialized_item(company='_Test Company',
+ target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+
+ serial_nos = get_serial_nos(se.get("items")[0].serial_no)
+
+ pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
+ account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
+ expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
+ item=se.get("items")[0].item_code, rate=1000, do_not_save=1)
+
+ pos.get("items")[0].serial_no = serial_nos[0]
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+
+ pos.insert()
+ pos.submit()
+
+ pos_return = make_sales_return(pos.name)
+
+ pos_return.insert()
+ pos_return.submit()
+ self.assertEqual(pos_return.get('items')[0].serial_no, serial_nos[0])
+
+ def test_partial_pos_returns(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+ se = make_serialized_item(company='_Test Company',
+ target_warehouse="Stores - _TC", cost_center='Main - _TC', expense_account='Cost of Goods Sold - _TC')
+
+ serial_nos = get_serial_nos(se.get("items")[0].serial_no)
+
+ pos = create_pos_invoice(company='_Test Company', debit_to='Debtors - _TC',
+ account_for_change_amount='Cash - _TC', warehouse='Stores - _TC', income_account='Sales - _TC',
+ expense_account='Cost of Goods Sold - _TC', cost_center='Main - _TC',
+ item=se.get("items")[0].item_code, qty=2, rate=1000, do_not_save=1)
+
+ pos.get("items")[0].serial_no = serial_nos[0] + "\n" + serial_nos[1]
+ pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 1000, 'default': 1})
+
+ pos.insert()
+ pos.submit()
+
+ pos_return1 = make_sales_return(pos.name)
+
+ # partial return 1
+ pos_return1.get('items')[0].qty = -1
+ pos_return1.get('items')[0].serial_no = serial_nos[0]
+ pos_return1.insert()
+ pos_return1.submit()
+
+ # partial return 2
+ pos_return2 = make_sales_return(pos.name)
+ self.assertEqual(pos_return2.get('items')[0].qty, -1)
+ self.assertEqual(pos_return2.get('items')[0].serial_no, serial_nos[1])
+
def test_pos_change_amount(self):
pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC",
income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105,
@@ -290,7 +349,7 @@
def test_merging_into_sales_invoice_with_discount(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
- from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+ from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
@@ -306,7 +365,7 @@
})
pos_inv2.submit()
- merge_pos_invoices()
+ consolidate_pos_invoices()
pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
@@ -315,7 +374,7 @@
def test_merging_into_sales_invoice_with_discount_and_inclusive_tax(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
- from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+ from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
frappe.db.sql("delete from `tabPOS Invoice`")
test_user, pos_profile = init_user_and_profile()
@@ -348,7 +407,7 @@
})
pos_inv2.submit()
- merge_pos_invoices()
+ consolidate_pos_invoices()
pos_inv.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total")
@@ -357,7 +416,7 @@
def test_merging_with_validate_selling_price(self):
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
- from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+ from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"):
frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1)
@@ -393,7 +452,7 @@
})
pos_inv2.submit()
- merge_pos_invoices()
+ consolidate_pos_invoices()
pos_inv2.load_from_db()
rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total")
diff --git a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
index 2b6e7de..8b71eb0 100644
--- a/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
+++ b/erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
@@ -87,6 +87,7 @@
"edit_references",
"sales_order",
"so_detail",
+ "pos_invoice_item",
"column_break_74",
"delivery_note",
"dn_detail",
@@ -790,11 +791,20 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
+ },
+ {
+ "fieldname": "pos_invoice_item",
+ "fieldtype": "Data",
+ "ignore_user_permissions": 1,
+ "label": "POS Invoice Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-07-22 13:40:34.418346",
+ "modified": "2021-01-04 17:34:49.924531",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Item",
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json
index 8f97639..da2984f 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.json
@@ -7,6 +7,8 @@
"field_order": [
"posting_date",
"customer",
+ "column_break_3",
+ "pos_closing_entry",
"section_break_3",
"pos_invoices",
"references_section",
@@ -76,11 +78,22 @@
"label": "Consolidated Credit Note",
"options": "Sales Invoice",
"read_only": 1
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "pos_closing_entry",
+ "fieldtype": "Link",
+ "label": "POS Closing Entry",
+ "options": "POS Closing Entry"
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-05-29 15:08:41.317100",
+ "modified": "2020-12-01 11:53:57.267579",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Invoice Merge Log",
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
index add27e9..40f77b4 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/pos_invoice_merge_log.py
@@ -5,10 +5,13 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate
-from frappe.model.document import Document
-from frappe.model.mapper import map_doc
from frappe.model import default_fields
+from frappe.model.document import Document
+from frappe.utils import flt, getdate, nowdate
+from frappe.utils.background_jobs import enqueue
+from frappe.model.mapper import map_doc, map_child_doc
+from frappe.utils.scheduler import is_scheduler_inactive
+from frappe.core.page.background_jobs.background_jobs import get_info
from six import iteritems
@@ -26,7 +29,7 @@
for d in self.pos_invoices:
status, docstatus, is_return, return_against = frappe.db.get_value(
'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against'])
-
+
bold_pos_invoice = frappe.bold(d.pos_invoice)
bold_status = frappe.bold(status)
if docstatus != 1:
@@ -55,17 +58,23 @@
sales_invoice, credit_note = "", ""
if sales:
sales_invoice = self.process_merging_into_sales_invoice(sales)
-
+
if returns:
credit_note = self.process_merging_into_credit_note(returns)
self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log
- self.update_pos_invoices(sales_invoice, credit_note)
+ self.update_pos_invoices(pos_invoice_docs, sales_invoice, credit_note)
+
+ def on_cancel(self):
+ pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices]
+
+ self.update_pos_invoices(pos_invoice_docs)
+ self.cancel_linked_invoices()
def process_merging_into_sales_invoice(self, data):
sales_invoice = self.get_new_sales_invoice()
-
+
sales_invoice = self.merge_pos_invoice_into(sales_invoice, data)
sales_invoice.is_consolidated = 1
@@ -83,25 +92,25 @@
credit_note.is_consolidated = 1
# TODO: return could be against multiple sales invoice which could also have been consolidated?
- credit_note.return_against = self.consolidated_invoice
+ # credit_note.return_against = self.consolidated_invoice
credit_note.save()
credit_note.submit()
self.consolidated_credit_note = credit_note.name
return credit_note.name
-
+
def merge_pos_invoice_into(self, invoice, data):
items, payments, taxes = [], [], []
loyalty_amount_sum, loyalty_points_sum = 0, 0
for doc in data:
map_doc(doc, invoice, table_map={ "doctype": invoice.doctype })
-
+
if doc.redeem_loyalty_points:
invoice.loyalty_redemption_account = doc.loyalty_redemption_account
invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center
loyalty_points_sum += doc.loyalty_points
loyalty_amount_sum += doc.loyalty_amount
-
+
for item in doc.get('items'):
found = False
for i in items:
@@ -109,10 +118,13 @@
i.uom == item.uom and i.net_rate == item.net_rate):
found = True
i.qty = i.qty + item.qty
+
if not found:
item.rate = item.net_rate
- items.append(item)
-
+ item.price_list_rate = 0
+ si_item = map_child_doc(item, invoice, {"doctype": "Sales Invoice Item"})
+ items.append(si_item)
+
for tax in doc.get('taxes'):
found = False
for t in taxes:
@@ -147,9 +159,11 @@
invoice.set('taxes', taxes)
invoice.additional_discount_percentage = 0
invoice.discount_amount = 0.0
+ invoice.taxes_and_charges = None
+ invoice.ignore_pricing_rule = 1
return invoice
-
+
def get_new_sales_invoice(self):
sales_invoice = frappe.new_doc('Sales Invoice')
sales_invoice.customer = self.customer
@@ -159,17 +173,21 @@
return sales_invoice
- def update_pos_invoices(self, sales_invoice, credit_note):
- for d in self.pos_invoices:
- doc = frappe.get_doc('POS Invoice', d.pos_invoice)
- if not doc.is_return:
- doc.update({'consolidated_invoice': sales_invoice})
- else:
- doc.update({'consolidated_invoice': credit_note})
+ def update_pos_invoices(self, invoice_docs, sales_invoice='', credit_note=''):
+ for doc in invoice_docs:
+ doc.load_from_db()
+ doc.update({ 'consolidated_invoice': None if self.docstatus==2 else (credit_note if doc.is_return else sales_invoice) })
doc.set_status(update=True)
doc.save()
-def get_all_invoices():
+ def cancel_linked_invoices(self):
+ for si_name in [self.consolidated_invoice, self.consolidated_credit_note]:
+ if not si_name: continue
+ si = frappe.get_doc('Sales Invoice', si_name)
+ si.flags.ignore_validate = True
+ si.cancel()
+
+def get_all_unconsolidated_invoices():
filters = {
'consolidated_invoice': [ 'in', [ '', None ]],
'status': ['not in', ['Consolidated']],
@@ -177,33 +195,95 @@
}
pos_invoices = frappe.db.get_all('POS Invoice', filters=filters,
fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer'])
-
+
return pos_invoices
-def get_invoices_customer_map(pos_invoices):
+def get_invoice_customer_map(pos_invoices):
# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] }
pos_invoice_customer_map = {}
for invoice in pos_invoices:
customer = invoice.get('customer')
pos_invoice_customer_map.setdefault(customer, [])
pos_invoice_customer_map[customer].append(invoice)
-
+
return pos_invoice_customer_map
-def merge_pos_invoices(pos_invoices=[]):
- if not pos_invoices:
- pos_invoices = get_all_invoices()
-
- pos_invoice_map = get_invoices_customer_map(pos_invoices)
- create_merge_logs(pos_invoice_map)
+def consolidate_pos_invoices(pos_invoices=[], closing_entry={}):
+ invoices = pos_invoices or closing_entry.get('pos_transactions') or get_all_unconsolidated_invoices()
+ invoice_by_customer = get_invoice_customer_map(invoices)
-def create_merge_logs(pos_invoice_customer_map):
- for customer, invoices in iteritems(pos_invoice_customer_map):
+ if len(invoices) >= 5 and closing_entry:
+ closing_entry.set_status(update=True, status='Queued')
+ enqueue_job(create_merge_logs, invoice_by_customer, closing_entry)
+ else:
+ create_merge_logs(invoice_by_customer, closing_entry)
+
+def unconsolidate_pos_invoices(closing_entry):
+ merge_logs = frappe.get_all(
+ 'POS Invoice Merge Log',
+ filters={ 'pos_closing_entry': closing_entry.name },
+ pluck='name'
+ )
+
+ if len(merge_logs) >= 5:
+ closing_entry.set_status(update=True, status='Queued')
+ enqueue_job(cancel_merge_logs, merge_logs, closing_entry)
+ else:
+ cancel_merge_logs(merge_logs, closing_entry)
+
+def create_merge_logs(invoice_by_customer, closing_entry={}):
+ for customer, invoices in iteritems(invoice_by_customer):
merge_log = frappe.new_doc('POS Invoice Merge Log')
merge_log.posting_date = getdate(nowdate())
merge_log.customer = customer
+ merge_log.pos_closing_entry = closing_entry.get('name', None)
merge_log.set('pos_invoices', invoices)
merge_log.save(ignore_permissions=True)
merge_log.submit()
+
+ if closing_entry:
+ closing_entry.set_status(update=True, status='Submitted')
+ closing_entry.update_opening_entry()
+def cancel_merge_logs(merge_logs, closing_entry={}):
+ for log in merge_logs:
+ merge_log = frappe.get_doc('POS Invoice Merge Log', log)
+ merge_log.flags.ignore_permissions = True
+ merge_log.cancel()
+
+ if closing_entry:
+ closing_entry.set_status(update=True, status='Cancelled')
+ closing_entry.update_opening_entry(for_cancel=True)
+
+def enqueue_job(job, invoice_by_customer, closing_entry):
+ check_scheduler_status()
+
+ job_name = closing_entry.get("name")
+ if not job_already_enqueued(job_name):
+ enqueue(
+ job,
+ queue="long",
+ timeout=10000,
+ event="processing_merge_logs",
+ job_name=job_name,
+ closing_entry=closing_entry,
+ invoice_by_customer=invoice_by_customer,
+ now=frappe.conf.developer_mode or frappe.flags.in_test
+ )
+
+ if job == create_merge_logs:
+ msg = _('POS Invoices will be consolidated in a background process')
+ else:
+ msg = _('POS Invoices will be unconsolidated in a background process')
+
+ frappe.msgprint(msg, alert=1)
+
+def check_scheduler_status():
+ if is_scheduler_inactive() and not frappe.flags.in_test:
+ frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive"))
+
+def job_already_enqueued(job_name):
+ enqueued_jobs = [d.get("job_name") for d in get_info()]
+ if job_name in enqueued_jobs:
+ return True
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
index 0f34272..db046c9 100644
--- a/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
+++ b/erpnext/accounts/doctype/pos_invoice_merge_log/test_pos_invoice_merge_log.py
@@ -7,7 +7,7 @@
import unittest
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return
-from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices
+from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices
from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile
class TestPOSInvoiceMergeLog(unittest.TestCase):
@@ -34,7 +34,7 @@
})
pos_inv3.submit()
- merge_pos_invoices()
+ consolidate_pos_invoices()
pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
@@ -79,7 +79,7 @@
pos_inv_cn.paid_amount = -300
pos_inv_cn.submit()
- merge_pos_invoices()
+ consolidate_pos_invoices()
pos_inv.load_from_db()
self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice))
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
index acac1c4..cb5b3a5 100644
--- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry.py
@@ -6,7 +6,6 @@
import frappe
from frappe import _
from frappe.utils import cint, get_link_to_form
-from frappe.model.document import Document
from erpnext.controllers.status_updater import StatusUpdater
class POSOpeningEntry(StatusUpdater):
diff --git a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js
index 6c26ded..1ad3c91 100644
--- a/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js
+++ b/erpnext/accounts/doctype/pos_opening_entry/pos_opening_entry_list.js
@@ -5,7 +5,7 @@
frappe.listview_settings['POS Opening Entry'] = {
get_indicator: function(doc) {
var status_color = {
- "Draft": "grey",
+ "Draft": "red",
"Open": "orange",
"Closed": "green",
"Cancelled": "red"
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.js b/erpnext/accounts/doctype/pos_profile/pos_profile.js
index 7f4f755..efdeb1a 100755
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.js
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.js
@@ -57,6 +57,8 @@
}
};
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
@@ -67,6 +69,7 @@
company: function(frm) {
frm.trigger("toggle_display_account_head");
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
toggle_display_account_head: function(frm) {
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile.json b/erpnext/accounts/doctype/pos_profile/pos_profile.json
index 570111a..8afa0ab 100644
--- a/erpnext/accounts/doctype/pos_profile/pos_profile.json
+++ b/erpnext/accounts/doctype/pos_profile/pos_profile.json
@@ -6,15 +6,11 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
- "disabled",
- "section_break_2",
- "customer",
"company",
+ "customer",
"country",
+ "disabled",
"column_break_9",
- "update_stock",
- "ignore_pricing_rule",
- "hide_unavailable_items",
"warehouse",
"campaign",
"company_address",
@@ -23,8 +19,17 @@
"section_break_11",
"payments",
"section_break_14",
- "item_groups",
+ "hide_images",
+ "hide_unavailable_items",
+ "auto_add_item_to_cart",
"column_break_16",
+ "update_stock",
+ "ignore_pricing_rule",
+ "allow_rate_change",
+ "allow_discount_change",
+ "section_break_23",
+ "item_groups",
+ "column_break_25",
"customer_groups",
"section_break_16",
"print_format",
@@ -56,10 +61,6 @@
"label": "Disabled"
},
{
- "fieldname": "section_break_2",
- "fieldtype": "Section Break"
- },
- {
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
@@ -124,7 +125,8 @@
},
{
"fieldname": "section_break_14",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Configuration"
},
{
"description": "Only show Items from these Item Groups",
@@ -306,6 +308,7 @@
"default": "1",
"fieldname": "update_stock",
"fieldtype": "Check",
+ "hidden": 1,
"label": "Update Stock",
"read_only": 1
},
@@ -314,13 +317,67 @@
"fieldname": "hide_unavailable_items",
"fieldtype": "Check",
"label": "Hide Unavailable Items"
+ },
+ {
+ "default": "0",
+ "fieldname": "hide_images",
+ "fieldtype": "Check",
+ "label": "Hide Images"
+ },
+ {
+ "default": "0",
+ "fieldname": "auto_add_item_to_cart",
+ "fieldtype": "Check",
+ "label": "Automatically Add Filtered Item To Cart"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_rate_change",
+ "fieldtype": "Check",
+ "label": "Allow User to Edit Rate"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_discount_change",
+ "fieldtype": "Check",
+ "label": "Allow User to Edit Discount"
+ },
+ {
+ "fieldname": "section_break_23",
+ "fieldtype": "Section Break",
+ "label": "Filters"
+ },
+ {
+ "fieldname": "column_break_25",
+ "fieldtype": "Column Break"
}
],
"icon": "icon-cog",
"idx": 1,
"index_web_pages_for_search": 1,
- "links": [],
- "modified": "2020-10-29 13:18:38.795925",
+ "links": [
+ {
+ "group": "Invoices",
+ "link_doctype": "Sales Invoice",
+ "link_fieldname": "pos_profile"
+ },
+ {
+ "group": "Invoices",
+ "link_doctype": "POS Invoice",
+ "link_fieldname": "pos_profile"
+ },
+ {
+ "group": "Opening & Closing",
+ "link_doctype": "POS Opening Entry",
+ "link_fieldname": "pos_profile"
+ },
+ {
+ "group": "Opening & Closing",
+ "link_doctype": "POS Closing Entry",
+ "link_fieldname": "pos_profile"
+ }
+ ],
+ "modified": "2021-02-01 13:52:51.081311",
"modified_by": "Administrator",
"module": "Accounts",
"name": "POS Profile",
diff --git a/erpnext/accounts/doctype/pos_profile/pos_profile_dashboard.py b/erpnext/accounts/doctype/pos_profile/pos_profile_dashboard.py
deleted file mode 100644
index 2e4632a..0000000
--- a/erpnext/accounts/doctype/pos_profile/pos_profile_dashboard.py
+++ /dev/null
@@ -1,14 +0,0 @@
-from __future__ import unicode_literals
-
-from frappe import _
-
-
-def get_data():
- return {
- 'fieldname': 'pos_profile',
- 'transactions': [
- {
- 'items': ['Sales Invoice', 'POS Closing Entry', 'POS Opening Entry']
- }
- ]
- }
diff --git a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
index edf8659..62dc1fc 100644
--- a/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
+++ b/erpnext/accounts/doctype/pos_profile/test_pos_profile.py
@@ -70,6 +70,7 @@
""".format(cond=cond), tuple([company] + args_list), as_dict=1)
def make_pos_profile(**args):
+ frappe.db.sql("delete from `tabPOS Payment Method`")
frappe.db.sql("delete from `tabPOS Profile`")
args = frappe._dict(args)
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
index cc8ed4b..d08a854 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json
@@ -406,6 +406,7 @@
"fieldtype": "Column Break"
},
{
+ "default": "0",
"depends_on": "eval:doc.rate_or_discount==\"Rate\"",
"fieldname": "rate",
"fieldtype": "Currency",
@@ -469,6 +470,7 @@
"options": "UOM"
},
{
+ "description": "If rate is zero them item will be treated as \"Free Item\"",
"fieldname": "free_item_rate",
"fieldtype": "Currency",
"label": "Rate"
@@ -563,7 +565,7 @@
"icon": "fa fa-gift",
"idx": 1,
"links": [],
- "modified": "2020-10-28 16:53:14.416172",
+ "modified": "2020-12-04 00:36:24.698219",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Pricing Rule",
diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
index 55a5b0e..0565264 100644
--- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py
@@ -345,9 +345,13 @@
if ((pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == args.currency)
or (pricing_rule.margin_type == 'Percentage')):
item_details.margin_type = pricing_rule.margin_type
- item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
item_details.has_margin = True
+ if pricing_rule.apply_multiple_pricing_rules and item_details.margin_rate_or_amount is not None:
+ item_details.margin_rate_or_amount += pricing_rule.margin_rate_or_amount
+ else:
+ item_details.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
+
if pricing_rule.rate_or_discount == 'Rate':
pricing_rule_rate = 0.0
if pricing_rule.currency == args.currency:
diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
index ec0a485..f28cee7 100644
--- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
+++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py
@@ -56,6 +56,7 @@
self.assertEqual(details.get("discount_percentage"), 10)
prule = frappe.get_doc(test_record.copy())
+ prule.priority = 1
prule.applicable_for = "Customer"
prule.title = "_Test Pricing Rule for Customer"
self.assertRaises(MandatoryError, prule.insert)
@@ -261,6 +262,7 @@
"rate_or_discount": "Discount Percentage",
"rate": 0,
"discount_percentage": 17.5,
+ "priority": 1,
"company": "_Test Company"
}).insert()
@@ -521,6 +523,22 @@
frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete()
item.delete()
+ def test_pricing_rule_for_transaction(self):
+ make_item("Water Flask 1")
+ frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule')
+ make_pricing_rule(selling=1, min_qty=5, price_or_product_discount="Product",
+ apply_on="Transaction", free_item="Water Flask 1", free_qty=1, free_item_rate=10)
+
+ si = create_sales_invoice(qty=5, do_not_submit=True)
+ self.assertEquals(len(si.items), 2)
+ self.assertEquals(si.items[1].rate, 10)
+
+ si1 = create_sales_invoice(qty=2, do_not_submit=True)
+ self.assertEquals(len(si1.items), 1)
+
+ for doc in [si, si1]:
+ doc.delete()
+
def make_pricing_rule(**args):
args = frappe._dict(args)
@@ -539,20 +557,24 @@
"rate_or_discount": args.rate_or_discount or "Discount Percentage",
"discount_percentage": args.discount_percentage or 0.0,
"rate": args.rate or 0.0,
- "margin_type": args.margin_type,
"margin_rate_or_amount": args.margin_rate_or_amount or 0.0,
"condition": args.condition or '',
+ "priority": 1,
"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0
})
- if args.get("priority"):
- doc.priority = args.get("priority")
+ for field in ["free_item", "free_qty", "free_item_rate", "priority",
+ "margin_type", "price_or_product_discount"]:
+ if args.get(field):
+ doc.set(field, args.get(field))
apply_on = doc.apply_on.replace(' ', '_').lower()
child_table = {'Item Code': 'items', 'Item Group': 'item_groups', 'Brand': 'brands'}
- doc.append(child_table.get(doc.apply_on), {
- apply_on: args.get(apply_on) or "_Test Item"
- })
+
+ if doc.apply_on != "Transaction":
+ doc.append(child_table.get(doc.apply_on), {
+ apply_on: args.get(apply_on) or "_Test Item"
+ })
doc.insert(ignore_permissions=True)
if args.get(apply_on) and apply_on != "item_code":
diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py
index b003328..d163335 100644
--- a/erpnext/accounts/doctype/pricing_rule/utils.py
+++ b/erpnext/accounts/doctype/pricing_rule/utils.py
@@ -41,10 +41,11 @@
if not pricing_rules: return []
if apply_multiple_pricing_rules(pricing_rules):
- pricing_rules = sorted_by_priority(pricing_rules)
+ pricing_rules = sorted_by_priority(pricing_rules, args, doc)
for pricing_rule in pricing_rules:
- pricing_rule = filter_pricing_rules(args, pricing_rule, doc)
- if pricing_rule:
+ if isinstance(pricing_rule, list):
+ rules.extend(pricing_rule)
+ else:
rules.append(pricing_rule)
else:
pricing_rule = filter_pricing_rules(args, pricing_rules, doc)
@@ -53,17 +54,22 @@
return rules
-def sorted_by_priority(pricing_rules):
+def sorted_by_priority(pricing_rules, args, doc=None):
# If more than one pricing rules, then sort by priority
pricing_rules_list = []
pricing_rule_dict = {}
- for pricing_rule in pricing_rules:
- if not pricing_rule.get("priority"): continue
- pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule)
+ for pricing_rule in pricing_rules:
+ pricing_rule = filter_pricing_rules(args, pricing_rule, doc)
+ if pricing_rule:
+ if not pricing_rule.get('priority'):
+ pricing_rule['priority'] = 1
+
+ if pricing_rule.get('apply_multiple_pricing_rules'):
+ pricing_rule_dict.setdefault(cint(pricing_rule.get("priority")), []).append(pricing_rule)
for key in sorted(pricing_rule_dict):
- pricing_rules_list.append(pricing_rule_dict.get(key))
+ pricing_rules_list.extend(pricing_rule_dict.get(key))
return pricing_rules_list or pricing_rules
@@ -144,9 +150,7 @@
if not apply_multiple_rule: return False
- if (apply_multiple_rule
- and len(apply_multiple_rule) == len(pricing_rules)):
- return True
+ return True
def _get_tree_conditions(args, parenttype, table, allow_blank=True):
field = frappe.scrub(parenttype)
@@ -164,7 +168,15 @@
frappe.throw(_("Invalid {0}").format(args.get(field)))
parent_groups = frappe.db.sql_list("""select name from `tab%s`
- where lft<=%s and rgt>=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
+ where lft>=%s and rgt<=%s""" % (parenttype, '%s', '%s'), (lft, rgt))
+
+ if parenttype in ["Customer Group", "Item Group", "Territory"]:
+ parent_field = "parent_{0}".format(frappe.scrub(parenttype))
+ root_name = frappe.db.get_list(parenttype,
+ {"is_group": 1, parent_field: ("is", "not set")}, "name", as_list=1)
+
+ if root_name and root_name[0][0]:
+ parent_groups.append(root_name[0][0])
if parent_groups:
if allow_blank: parent_groups.append('')
@@ -256,18 +268,6 @@
if max_priority:
pricing_rules = list(filter(lambda x: cint(x.priority)==max_priority, pricing_rules))
- # apply internal priority
- all_fields = ["item_code", "item_group", "brand", "customer", "customer_group", "territory",
- "supplier", "supplier_group", "campaign", "sales_partner", "variant_of"]
-
- if len(pricing_rules) > 1:
- for field_set in [["item_code", "variant_of", "item_group", "brand"],
- ["customer", "customer_group", "territory"], ["supplier", "supplier_group"]]:
- remaining_fields = list(set(all_fields) - set(field_set))
- if if_all_rules_same(pricing_rules, remaining_fields):
- pricing_rules = apply_internal_priority(pricing_rules, field_set, args)
- break
-
if pricing_rules and not isinstance(pricing_rules, list):
pricing_rules = list(pricing_rules)
@@ -457,6 +457,9 @@
pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty,
doc.total, pricing_rules)
+ if not pricing_rules:
+ remove_free_item(doc)
+
for d in pricing_rules:
if d.price_or_product_discount == 'Price':
if d.apply_discount_on:
@@ -480,6 +483,12 @@
get_product_discount_rule(d, item_details, doc=doc)
apply_pricing_rule_for_free_items(doc, item_details.free_item_data)
doc.set_missing_values()
+ doc.calculate_taxes_and_totals()
+
+def remove_free_item(doc):
+ for d in doc.items:
+ if d.is_free_item:
+ doc.remove(d)
def get_applied_pricing_rules(pricing_rules):
if pricing_rules:
@@ -492,7 +501,7 @@
def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None):
free_item = pricing_rule.free_item
- if pricing_rule.same_item:
+ if pricing_rule.same_item and pricing_rule.get("apply_on") != 'Transaction':
free_item = item_details.item_code or args.item_code
if not free_item:
diff --git a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py
index 31356c6..e08a0e5 100644
--- a/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py
+++ b/erpnext/accounts/doctype/process_deferred_accounting/test_process_deferred_accounting.py
@@ -21,7 +21,7 @@
item.no_of_months = 12
item.save()
- si = create_sales_invoice(item=item.name, posting_date="2019-01-10", do_not_submit=True)
+ si = create_sales_invoice(item=item.name, update_stock=0, posting_date="2019-01-10", do_not_submit=True)
si.items[0].enable_deferred_revenue = 1
si.items[0].service_start_date = "2019-01-10"
si.items[0].service_end_date = "2019-03-15"
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
index 1d41d0f..06aa20b 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js
@@ -15,7 +15,22 @@
return (doc.qty<=doc.received_qty) ? "green" : "orange";
});
}
+
+ this.frm.set_query("unrealized_profit_loss_account", function() {
+ return {
+ filters: {
+ company: doc.company,
+ is_group: 0,
+ root_type: "Liability",
+ }
+ };
+ });
},
+
+ company: function() {
+ erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
+ },
+
onload: function() {
this._super();
@@ -31,6 +46,8 @@
if (this.frm.doc.supplier && this.frm.doc.__islocal) {
this.frm.trigger('supplier');
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
refresh: function(doc) {
@@ -258,8 +275,11 @@
supplier: function() {
var me = this;
- if(this.frm.updating_party_details)
+
+ // Do not update if inter company reference is there as the details will already be updated
+ if(this.frm.updating_party_details || this.frm.doc.inter_company_invoice_reference)
return;
+
erpnext.utils.get_party_details(this.frm, "erpnext.accounts.party.get_party_details",
{
posting_date: this.frm.doc.posting_date,
@@ -488,7 +508,7 @@
frappe.ui.form.on("Purchase Invoice", {
setup: function(frm) {
frm.custom_make_buttons = {
- 'Purchase Invoice': 'Debit Note',
+ 'Purchase Invoice': 'Return / Debit Note',
'Payment Entry': 'Payment'
}
@@ -501,15 +521,6 @@
}
}
}
-
- frm.set_query("cost_center", function() {
- return {
- filters: {
- company: frm.doc.company,
- is_group: 0
- }
- };
- });
},
onload: function(frm) {
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
index 2df77a8..451c936 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.json
@@ -1,6 +1,5 @@
{
"actions": [],
- "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-21 16:16:39",
@@ -58,8 +57,8 @@
"set_warehouse",
"rejected_warehouse",
"col_break_warehouse",
+ "set_from_warehouse",
"is_subcontracted",
- "supplier_warehouse",
"items_section",
"update_stock",
"scan_barcode",
@@ -127,6 +126,7 @@
"write_off_cost_center",
"advances_section",
"allocate_advances_automatically",
+ "adjust_advance_taxes",
"get_advances",
"advances",
"payment_schedule_section",
@@ -152,9 +152,11 @@
"is_opening",
"against_expense_account",
"column_break_63",
+ "unrealized_profit_loss_account",
"status",
"inter_company_invoice_reference",
"is_internal_supplier",
+ "represents_company",
"remarks",
"subscription_section",
"from_date",
@@ -513,6 +515,7 @@
},
{
"depends_on": "update_stock",
+ "description": "Sets 'Accepted Warehouse' in each row of the items table.",
"fieldname": "set_warehouse",
"fieldtype": "Link",
"label": "Set Accepted Warehouse",
@@ -542,17 +545,6 @@
"print_hide": 1
},
{
- "depends_on": "eval:doc.is_subcontracted==\"Yes\"",
- "fieldname": "supplier_warehouse",
- "fieldtype": "Link",
- "label": "Supplier Warehouse",
- "no_copy": 1,
- "options": "Warehouse",
- "print_hide": 1,
- "print_width": "50px",
- "width": "50px"
- },
- {
"fieldname": "items_section",
"fieldtype": "Section Break",
"oldfieldtype": "Section Break",
@@ -1223,14 +1215,16 @@
"fieldtype": "Select",
"in_standard_filter": 1,
"label": "Status",
- "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled",
+ "options": "\nDraft\nReturn\nDebit Note Issued\nSubmitted\nPaid\nUnpaid\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1
},
{
"fieldname": "inter_company_invoice_reference",
"fieldtype": "Link",
"label": "Inter Company Invoice Reference",
+ "no_copy": 1,
"options": "Sales Invoice",
+ "print_hide": 1,
"read_only": 1
},
{
@@ -1330,13 +1324,49 @@
"fieldtype": "Link",
"label": "Project",
"options": "Project"
+ },
+ {
+ "default": "0",
+ "description": "Taxes paid while advance payment will be adjusted against this invoice",
+ "fieldname": "adjust_advance_taxes",
+ "fieldtype": "Check",
+ "label": "Adjust Advance Taxes"
+ },
+ {
+ "depends_on": "eval:doc.is_internal_supplier",
+ "description": "Unrealized Profit / Loss account for intra-company transfers",
+ "fieldname": "unrealized_profit_loss_account",
+ "fieldtype": "Link",
+ "label": "Unrealized Profit / Loss Account",
+ "options": "Account"
+ },
+ {
+ "depends_on": "eval:doc.is_internal_supplier",
+ "description": "Company which internal supplier represents",
+ "fetch_from": "supplier.represents_company",
+ "fieldname": "represents_company",
+ "fieldtype": "Link",
+ "label": "Represents Company",
+ "options": "Company"
+ },
+ {
+ "depends_on": "eval:doc.update_stock && (doc.is_subcontracted==\"Yes\" || doc.is_internal_supplier)",
+ "description": "Sets 'From Warehouse' in each row of the items table.",
+ "fieldname": "set_from_warehouse",
+ "fieldtype": "Link",
+ "label": "Set From Warehouse",
+ "no_copy": 1,
+ "options": "Warehouse",
+ "print_hide": 1,
+ "print_width": "50px",
+ "width": "50px"
}
],
"icon": "fa fa-file-text",
"idx": 204,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 13:57:18.266978",
+ "modified": "2020-12-26 20:49:03.305063",
"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 91c4dfb..dacd50a 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py
@@ -147,6 +147,11 @@
throw(_("Conversion rate cannot be 0 or 1"))
def validate_credit_to_acc(self):
+ if not self.credit_to:
+ self.credit_to = get_party_account("Supplier", self.supplier, self.company)
+ if not self.credit_to:
+ self.raise_missing_debit_credit_account_error("Supplier", self.supplier)
+
account = frappe.db.get_value("Account", self.credit_to,
["account_type", "report_type", "account_currency"], as_dict=True)
@@ -201,8 +206,8 @@
["Purchase Receipt", "purchase_receipt", "pr_detail"]
])
- def validate_warehouse(self):
- if self.update_stock:
+ def validate_warehouse(self, for_validate=True):
+ if self.update_stock and for_validate:
for d in self.get('items'):
if not d.warehouse:
frappe.throw(_("Warehouse required at Row No {0}, please set default warehouse for the item {1} for the company {2}").
@@ -228,7 +233,7 @@
if self.update_stock:
self.validate_item_code()
- self.validate_warehouse()
+ self.validate_warehouse(for_validate)
if auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
@@ -405,10 +410,13 @@
# this sequence because outstanding may get -negative
self.make_gl_entries()
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
self.update_project()
update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference)
- def make_gl_entries(self, gl_entries=None):
+ def make_gl_entries(self, gl_entries=None, from_repost=False):
if not gl_entries:
gl_entries = self.get_gl_entries()
@@ -416,7 +424,7 @@
update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes"
if self.docstatus == 1:
- make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False)
+ make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -431,9 +439,11 @@
self.auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if self.auto_accounting_for_stock:
self.stock_received_but_not_billed = self.get_company_default("stock_received_but_not_billed")
+ self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
else:
self.stock_received_but_not_billed = None
- self.expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
+ self.expenses_included_in_valuation = None
+
self.negative_expense_to_be_booked = 0.0
gl_entries = []
@@ -444,6 +454,7 @@
self.get_asset_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
+ self.make_internal_transfer_gl_entries(gl_entries)
gl_entries = make_regional_gl_entries(gl_entries, self)
@@ -452,7 +463,6 @@
self.make_payment_gl_entries(gl_entries)
self.make_write_off_gl_entry(gl_entries)
self.make_gle_for_rounding_adjustment(gl_entries)
-
return gl_entries
def check_asset_cwip_enabled(self):
@@ -469,31 +479,30 @@
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
- if grand_total:
- # Didnot use base_grand_total to book rounding loss gle
- grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
- self.precision("grand_total"))
- gl_entries.append(
- self.get_gl_dict({
- "account": self.credit_to,
- "party_type": "Supplier",
- "party": self.supplier,
- "due_date": self.due_date,
- "against": self.against_expense_account,
- "credit": grand_total_in_company_currency,
- "credit_in_account_currency": grand_total_in_company_currency \
- if self.party_account_currency==self.company_currency else grand_total,
- "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
- "against_voucher_type": self.doctype,
- "project": self.project,
- "cost_center": self.cost_center
- }, self.party_account_currency, item=self)
- )
+ if grand_total and not self.is_internal_transfer():
+ # Did not use base_grand_total to book rounding loss gle
+ grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
+ self.precision("grand_total"))
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.credit_to,
+ "party_type": "Supplier",
+ "party": self.supplier,
+ "due_date": self.due_date,
+ "against": self.against_expense_account,
+ "credit": grand_total_in_company_currency,
+ "credit_in_account_currency": grand_total_in_company_currency \
+ if self.party_account_currency==self.company_currency else grand_total,
+ "against_voucher": self.return_against if cint(self.is_return) and self.return_against else self.name,
+ "against_voucher_type": self.doctype,
+ "project": self.project,
+ "cost_center": self.cost_center
+ }, self.party_account_currency, item=self)
+ )
def make_item_gl_entries(self, gl_entries):
# item gl entries
stock_items = self.get_stock_items()
- expenses_included_in_valuation = self.get_company_default("expenses_included_in_valuation")
if self.update_stock and self.auto_accounting_for_stock:
warehouse_account = get_warehouse_account_map(self.company)
@@ -502,8 +511,8 @@
voucher_wise_stock_value = {}
if self.update_stock:
for d in frappe.get_all('Stock Ledger Entry',
- fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}):
- voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference)
+ fields = ["voucher_detail_no", "stock_value_difference", "warehouse"], filters={'voucher_no': self.name}):
+ voucher_wise_stock_value.setdefault((d.voucher_detail_no, d.warehouse), d.stock_value_difference)
valuation_tax_accounts = [d.account_head for d in self.get("taxes")
if d.category in ('Valuation', 'Total and Valuation')
@@ -521,7 +530,6 @@
item, voucher_wise_stock_value, account_currency)
if item.from_warehouse:
-
gl_entries.append(self.get_gl_dict({
"account": warehouse_account[item.warehouse]['account'],
"against": warehouse_account[item.from_warehouse]["account"],
@@ -541,28 +549,31 @@
"debit": -1 * flt(item.base_net_amount, item.precision("base_net_amount")),
}, warehouse_account[item.from_warehouse]["account_currency"], item=item))
- gl_entries.append(
- self.get_gl_dict({
- "account": item.expense_account,
- "against": self.supplier,
- "debit": flt(item.base_net_amount, item.precision("base_net_amount")),
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "cost_center": item.cost_center,
- "project": item.project
- }, account_currency, item=item)
- )
+ # Do not book expense for transfer within same company transfer
+ if not self.is_internal_transfer():
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": item.expense_account,
+ "against": self.supplier,
+ "debit": flt(item.base_net_amount, item.precision("base_net_amount")),
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "cost_center": item.cost_center,
+ "project": item.project
+ }, account_currency, item=item)
+ )
else:
- gl_entries.append(
- self.get_gl_dict({
- "account": item.expense_account,
- "against": self.supplier,
- "debit": warehouse_debit_amount,
- "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "cost_center": item.cost_center,
- "project": item.project or self.project
- }, account_currency, item=item)
- )
+ if not self.is_internal_transfer():
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": item.expense_account,
+ "against": self.supplier,
+ "debit": warehouse_debit_amount,
+ "remarks": self.get("remarks") or _("Accounting Entry for Stock"),
+ "cost_center": item.cost_center,
+ "project": item.project or self.project
+ }, account_currency, item=item)
+ )
# Amount added through landed-cost-voucher
if landed_cost_entries:
@@ -572,7 +583,8 @@
"against": item.expense_account,
"cost_center": item.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(amount),
+ "credit": flt(amount["base_amount"]),
+ "credit_in_account_currency": flt(amount["amount"]),
"project": item.project or self.project
}, item=item))
@@ -614,13 +626,14 @@
if expense_booked_in_pr:
expense_account = service_received_but_not_billed_account
- gl_entries.append(self.get_gl_dict({
- "account": expense_account,
- "against": self.supplier,
- "debit": amount,
- "cost_center": item.cost_center,
- "project": item.project or self.project
- }, account_currency, item=item))
+ if not self.is_internal_transfer():
+ gl_entries.append(self.get_gl_dict({
+ "account": expense_account,
+ "against": self.supplier,
+ "debit": amount,
+ "cost_center": item.cost_center,
+ "project": item.project or self.project
+ }, account_currency, item=item))
# If asset is bought through this document and not linked to PR
if self.update_stock and item.landed_cost_voucher_amount:
@@ -785,10 +798,10 @@
# Stock ledger value is not matching with the warehouse amount
if (self.update_stock and voucher_wise_stock_value.get(item.name) and
- warehouse_debit_amount != flt(voucher_wise_stock_value.get(item.name), net_amt_precision)):
+ warehouse_debit_amount != flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)):
cost_of_goods_sold_account = self.get_company_default("default_expense_account")
- stock_amount = flt(voucher_wise_stock_value.get(item.name), net_amt_precision)
+ stock_amount = flt(voucher_wise_stock_value.get((item.name, item.warehouse)), net_amt_precision)
stock_adjustment_amt = warehouse_debit_amount - stock_amount
gl_entries.append(
@@ -827,7 +840,8 @@
}, account_currency, item=tax)
)
# accumulate valuation tax
- if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount):
+ if self.is_opening == "No" and tax.category in ("Valuation", "Valuation and Total") and flt(tax.base_tax_amount_after_discount_amount) \
+ and not self.is_internal_transfer():
if self.auto_accounting_for_stock and not tax.cost_center:
frappe.throw(_("Cost Center is required in row {0} in Taxes table for type {1}").format(tax.idx, _(tax.category)))
valuation_tax.setdefault(tax.name, 0)
@@ -871,8 +885,19 @@
"against": self.supplier,
"credit": valuation_tax[tax.name],
"remarks": self.remarks or "Accounting Entry for Stock"
- }, item=tax)
- )
+ }, item=tax))
+
+ def make_internal_transfer_gl_entries(self, gl_entries):
+ if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
+ account_currency = get_account_currency(self.unrealized_profit_loss_account)
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.unrealized_profit_loss_account,
+ "against": self.supplier,
+ "credit": flt(self.total_taxes_and_charges),
+ "credit_in_account_currency": flt(self.base_total_taxes_and_charges),
+ "cost_center": self.cost_center
+ }, account_currency, item=self))
def make_payment_gl_entries(self, gl_entries):
# Make Cash GL Entries
@@ -977,11 +1002,15 @@
self.delete_auto_created_batches()
self.make_gl_entries_on_cancel()
+
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
self.update_project()
frappe.db.set(self, 'status', 'Cancelled')
unlink_inter_company_doc(self.doctype, self.name, self.inter_company_invoice_reference)
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_project(self):
project_list = []
@@ -1032,7 +1061,9 @@
updated_pr += update_billed_amount_based_on_po(d.po_detail, update_modified)
for pr in set(updated_pr):
- frappe.get_doc("Purchase Receipt", pr).update_billing_percentage(update_modified=update_modified)
+ from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_billing_percentage
+ pr_doc = frappe.get_doc("Purchase Receipt", pr)
+ update_billing_percentage(pr_doc, update_modified=update_modified)
def on_recurring(self, reference_doc, auto_repeat_doc):
self.due_date = None
@@ -1088,7 +1119,9 @@
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
- if outstanding_amount > 0 and due_date < nowdate:
+ if self.is_internal_transfer():
+ self.status = 'Internal Transfer'
+ elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue"
elif outstanding_amount > 0 and due_date >= nowdate:
self.status = "Unpaid"
diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
index 86c2e40..914a245 100644
--- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
+++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice_list.js
@@ -4,23 +4,25 @@
// render
frappe.listview_settings['Purchase Invoice'] = {
add_fields: ["supplier", "supplier_name", "base_grand_total", "outstanding_amount", "due_date", "company",
- "currency", "is_return", "release_date", "on_hold"],
+ "currency", "is_return", "release_date", "on_hold", "represents_company", "is_internal_supplier"],
get_indicator: function(doc) {
- if( (flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
+ if ((flt(doc.outstanding_amount) <= 0) && doc.docstatus == 1 && doc.status == 'Debit Note Issued') {
return [__("Debit Note Issued"), "darkgrey", "outstanding_amount,<=,0"];
- } else if(flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
+ } else if (flt(doc.outstanding_amount) > 0 && doc.docstatus==1) {
if(cint(doc.on_hold) && !doc.release_date) {
return [__("On Hold"), "darkgrey"];
- } else if(cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
+ } else if (cint(doc.on_hold) && doc.release_date && frappe.datetime.get_diff(doc.release_date, frappe.datetime.nowdate()) > 0) {
return [__("Temporarily on Hold"), "darkgrey"];
- } else if(frappe.datetime.get_diff(doc.due_date) < 0) {
+ } else if (frappe.datetime.get_diff(doc.due_date) < 0) {
return [__("Overdue"), "red", "outstanding_amount,>,0|due_date,<,Today"];
} else {
return [__("Unpaid"), "orange", "outstanding_amount,>,0|due_date,>=,Today"];
}
- } else if(cint(doc.is_return)) {
- return [__("Return"), "darkgrey", "is_return,=,Yes"];
- } else if(flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
+ } else if (cint(doc.is_return)) {
+ return [__("Return"), "gray", "is_return,=,Yes"];
+ } else if (doc.company == doc.represents_company && doc.is_internal_supplier) {
+ return [__("Internal Transfer"), "darkgrey", "outstanding_amount,=,0"];
+ } else if (flt(doc.outstanding_amount)==0 && doc.docstatus==1) {
return [__("Paid"), "green", "outstanding_amount,=,0"];
}
}
diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
index f2499d2..2c088ce 100644
--- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
+++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py
@@ -9,8 +9,7 @@
from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
from frappe.utils import cint, flt, today, nowdate, add_days, getdate
import frappe.defaults
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory, \
- test_records as pr_test_records, make_purchase_receipt, get_taxes
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt, get_taxes
from erpnext.controllers.accounts_controller import get_payment_terms
from erpnext.exceptions import InvalidCurrency
from erpnext.stock.doctype.stock_entry.test_stock_entry import get_qty_after_transaction
@@ -33,13 +32,10 @@
def test_gl_entries_without_perpetual_inventory(self):
frappe.db.set_value("Company", "_Test Company", "round_off_account", "Round Off - _TC")
- wrapper = frappe.copy_doc(test_records[0])
- set_perpetual_inventory(0, wrapper.company)
- self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(wrapper.company)))
- wrapper.insert()
- wrapper.submit()
- wrapper.load_from_db()
- dl = wrapper
+ pi = frappe.copy_doc(test_records[0])
+ self.assertTrue(not cint(erpnext.is_perpetual_inventory_enabled(pi.company)))
+ pi.insert()
+ pi.submit()
expected_gl_entries = {
"_Test Payable - _TC": [0, 1512.0],
@@ -54,12 +50,16 @@
"Round Off - _TC": [0, 0.3]
}
gl_entries = frappe.db.sql("""select account, debit, credit from `tabGL Entry`
- where voucher_type = 'Purchase Invoice' and voucher_no = %s""", dl.name, as_dict=1)
+ where voucher_type = 'Purchase Invoice' and voucher_no = %s""", pi.name, as_dict=1)
for d in gl_entries:
self.assertEqual([d.debit, d.credit], expected_gl_entries.get(d.account))
def test_gl_entries_with_perpetual_inventory(self):
- pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10)
+ pi = make_purchase_invoice(company="_Test Company with perpetual inventory",
+ warehouse= "Stores - TCP1", cost_center = "Main - TCP1",
+ expense_account ="_Test Account Cost for Goods Sold - TCP1",
+ get_taxes_and_charges=True, qty=10)
+
self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
self.check_gle_for_pi(pi.name)
@@ -198,8 +198,6 @@
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", get_taxes_and_charges=True,)
- self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
-
pi = make_purchase_invoice(company="_Test Company with perpetual inventory", supplier_warehouse="Work In Progress - TCP1", warehouse= "Stores - TCP1", cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1", get_taxes_and_charges=True, qty=10,do_not_save= "True")
for d in pi.items:
@@ -247,17 +245,11 @@
self.assertRaises(frappe.CannotChangeConstantError, pi.save)
- def test_gl_entries_with_aia_for_non_stock_items(self):
- pi = frappe.copy_doc(test_records[1])
- set_perpetual_inventory(1, pi.company)
- self.assertTrue(cint(erpnext.is_perpetual_inventory_enabled(pi.company)), 1)
- pi.get("items")[0].item_code = "_Test Non Stock Item"
- pi.get("items")[0].expense_account = "_Test Account Cost for Goods Sold - _TC"
- pi.get("taxes").pop(0)
- pi.get("taxes").pop(1)
- pi.insert()
- pi.submit()
- pi.load_from_db()
+ def test_gl_entries_for_non_stock_items_with_perpetual_inventory(self):
+ pi = make_purchase_invoice(item_code = "_Test Non Stock Item",
+ company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
+ cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
+
self.assertTrue(pi.status, "Unpaid")
gl_entries = frappe.db.sql("""select account, debit, credit
@@ -265,17 +257,15 @@
order by account asc""", pi.name, as_dict=1)
self.assertTrue(gl_entries)
- expected_values = sorted([
- ["_Test Payable - _TC", 0, 620],
- ["_Test Account Cost for Goods Sold - _TC", 500.0, 0],
- ["_Test Account VAT - _TC", 120.0, 0],
- ])
+ expected_values = [
+ ["_Test Account Cost for Goods Sold - TCP1", 250.0, 0],
+ ["Creditors - TCP1", 0, 250]
+ ]
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[i][0], gle.account)
self.assertEqual(expected_values[i][1], gle.debit)
self.assertEqual(expected_values[i][2], gle.credit)
- set_perpetual_inventory(0, pi.company)
def test_purchase_invoice_calculation(self):
pi = frappe.copy_doc(test_records[0])
@@ -436,33 +426,39 @@
)
def test_total_purchase_cost_for_project(self):
- make_project({'project_name':'_Test Project'})
+ if not frappe.db.exists("Project", {"project_name": "_Test Project for Purchase"}):
+ project = make_project({'project_name':'_Test Project for Purchase'})
+ else:
+ project = frappe.get_doc("Project", {"project_name": "_Test Project for Purchase"})
existing_purchase_cost = frappe.db.sql("""select sum(base_net_amount)
- from `tabPurchase Invoice Item` where project = '_Test Project' and docstatus=1""")
+ from `tabPurchase Invoice Item`
+ where project = '{0}'
+ and docstatus=1""".format(project.name))
existing_purchase_cost = existing_purchase_cost and existing_purchase_cost[0][0] or 0
- pi = make_purchase_invoice(currency="USD", conversion_rate=60, project="_Test Project")
- self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"),
+ pi = make_purchase_invoice(currency="USD", conversion_rate=60, project=project.name)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000)
- pi1 = make_purchase_invoice(qty=10, project="_Test Project")
- self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"),
+ pi1 = make_purchase_invoice(qty=10, project=project.name)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15500)
pi1.cancel()
- self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"),
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"),
existing_purchase_cost + 15000)
pi.cancel()
- self.assertEqual(frappe.db.get_value("Project", "_Test Project", "total_purchase_cost"), existing_purchase_cost)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_purchase_cost"), existing_purchase_cost)
- def test_return_purchase_invoice(self):
- set_perpetual_inventory()
+ def test_return_purchase_invoice_with_perpetual_inventory(self):
+ pi = make_purchase_invoice(company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
+ cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
- pi = make_purchase_invoice()
-
- return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2)
+ return_pi = make_purchase_invoice(is_return=1, return_against=pi.name, qty=-2,
+ company = "_Test Company with perpetual inventory", warehouse= "Stores - TCP1",
+ cost_center = "Main - TCP1", expense_account ="_Test Account Cost for Goods Sold - TCP1")
# check gl entries for return
@@ -473,19 +469,15 @@
self.assertTrue(gl_entries)
expected_values = {
- "Creditors - _TC": [100.0, 0.0],
- "Stock Received But Not Billed - _TC": [0.0, 100.0],
+ "Creditors - TCP1": [100.0, 0.0],
+ "Stock Received But Not Billed - TCP1": [0.0, 100.0],
}
for gle in gl_entries:
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
- set_perpetual_inventory(0)
-
def test_multi_currency_gle(self):
- set_perpetual_inventory(0)
-
pi = make_purchase_invoice(supplier="_Test Supplier USD", credit_to="_Test Payable USD - _TC",
currency="USD", conversion_rate=50)
@@ -640,10 +632,9 @@
self.assertEqual(len(pi.get("supplied_items")), 2)
rm_supp_cost = sum([d.amount for d in pi.get("supplied_items")])
- self.assertEqual(pi.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
+ self.assertEqual(flt(pi.get("items")[0].rm_supp_cost, 2), flt(rm_supp_cost, 2))
def test_rejected_serial_no(self):
- set_perpetual_inventory(0)
pi = make_purchase_invoice(item_code="_Test Serialized Item With Series", received_qty=2, qty=1,
rejected_qty=1, rate=500, update_stock=1,
rejected_warehouse = "_Test Rejected Warehouse - _TC")
@@ -874,17 +865,17 @@
})
pi = make_purchase_invoice(credit_to="Creditors - _TC" ,do_not_save=1)
- pi.items[0].project = item_project.project_name
- pi.project = project.project_name
+ pi.items[0].project = item_project.name
+ pi.project = project.name
pi.submit()
expected_values = {
"Creditors - _TC": {
- "project": project.project_name
+ "project": project.name
},
"_Test Account Cost for Goods Sold - _TC": {
- "project": item_project.project_name
+ "project": item_project.name
}
}
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 f6d76e5..07e75ac 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "hash",
"creation": "2013-05-22 12:43:10",
"doctype": "DocType",
@@ -39,6 +40,7 @@
"base_rate",
"base_amount",
"pricing_rules",
+ "stock_uom_rate",
"is_free_item",
"section_break_22",
"net_rate",
@@ -87,6 +89,7 @@
"po_detail",
"purchase_receipt",
"pr_detail",
+ "sales_invoice_item",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -553,8 +556,8 @@
"fieldtype": "Link",
"hidden": 1,
"label": "Brand",
- "print_hide": 1,
- "options": "Brand"
+ "options": "Brand",
+ "print_hide": 1
},
{
"fetch_from": "item_code.item_group",
@@ -562,9 +565,9 @@
"fieldname": "item_group",
"fieldtype": "Link",
"label": "Item Group",
+ "options": "Item Group",
"print_hide": 1,
- "read_only": 1,
- "options": "Item Group"
+ "read_only": 1
},
{
"description": "Tax detail table fetched from item master as a string and stored in this field.\nUsed for Taxes and Charges",
@@ -759,10 +762,11 @@
"read_only": 1
},
{
+ "depends_on": "eval:parent.is_internal_supplier && parent.update_stock",
"fieldname": "from_warehouse",
"fieldtype": "Link",
"ignore_user_permissions": 1,
- "label": "Supplier Warehouse",
+ "label": "From Warehouse",
"options": "Warehouse"
},
{
@@ -779,11 +783,28 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.uom != doc.stock_uom",
+ "fieldname": "stock_uom_rate",
+ "fieldtype": "Currency",
+ "label": "Rate of Stock UOM",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "sales_invoice_item",
+ "fieldtype": "Data",
+ "label": "Sales Invoice Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
- "modified": "2020-08-20 11:48:01.398356",
+ "links": [],
+ "modified": "2021-01-30 21:43:21.488258",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Purchase Invoice Item",
@@ -791,4 +812,4 @@
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC"
-}
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
index 56576df..50ec7d8 100644
--- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
+++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.py
@@ -6,8 +6,5 @@
from frappe.model.document import Document
-from erpnext.controllers.print_settings import print_settings_for_item_table
-
class PurchaseInvoiceItem(Document):
- def __setup__(self):
- print_settings_for_item_table(self)
+ pass
diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js
index 6336db1..f54bce8 100644
--- a/erpnext/accounts/doctype/sales_invoice/regional/india.js
+++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js
@@ -1,6 +1,8 @@
{% include "erpnext/regional/india/taxes.js" %}
+{% include "erpnext/regional/india/e_invoice/einvoice.js" %}
erpnext.setup_auto_gst_taxation('Sales Invoice');
+erpnext.setup_einvoice_actions('Sales Invoice')
frappe.ui.form.on("Sales Invoice", {
setup: function(frm) {
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
index 502e65e..d3e8a44 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js
@@ -5,18 +5,22 @@
cur_frm.pformat.print_heading = 'Invoice';
{% include 'erpnext/selling/sales_common.js' %};
-
-
frappe.provide("erpnext.accounts");
+
+
erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.extend({
setup: function(doc) {
this.setup_posting_date_time_check();
this._super(doc);
},
+ company: function() {
+ erpnext.accounts.dimensions.update_dimension(this.frm, this.frm.doctype);
+ },
onload: function() {
var me = this;
this._super();
+ this.frm.ignore_doctypes_on_cancel_all = ['POS Invoice'];
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);
@@ -33,6 +37,7 @@
me.frm.refresh_fields();
}
erpnext.queries.setup_warehouse_query(this.frm);
+ erpnext.accounts.dimensions.setup_dimension_filters(this.frm, this.frm.doctype);
},
refresh: function(doc, dt, dn) {
@@ -126,16 +131,15 @@
this.set_default_print_format();
if (doc.docstatus == 1 && !doc.inter_company_invoice_reference) {
- frappe.model.with_doc("Customer", me.frm.doc.customer, function() {
- var customer = frappe.model.get_doc("Customer", me.frm.doc.customer);
- var internal = customer.is_internal_customer;
- var disabled = customer.disabled;
- if (internal == 1 && disabled == 0) {
- me.frm.add_custom_button("Inter Company Invoice", function() {
- me.make_inter_company_invoice();
- }, __('Create'));
- }
- });
+ let internal = me.frm.doc.is_internal_customer;
+ if (internal) {
+ let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Invoice" :
+ "Inter Company Purchase Invoice";
+
+ me.frm.add_custom_button(button_label, function() {
+ me.make_inter_company_invoice();
+ }, __('Create'));
+ }
}
},
@@ -571,18 +575,19 @@
};
});
- frm.set_query("cost_center", function() {
+ frm.set_query("unrealized_profit_loss_account", function() {
return {
filters: {
company: frm.doc.company,
- is_group: 0
+ is_group: 0,
+ root_type: "Liability",
}
};
});
frm.custom_make_buttons = {
'Delivery Note': 'Delivery',
- 'Sales Invoice': 'Sales Return',
+ 'Sales Invoice': 'Return / Credit Note',
'Payment Request': 'Payment Request',
'Payment Entry': 'Payment'
},
@@ -664,12 +669,12 @@
};
},
// When multiple companies are set up. in case company name is changed set default company address
- company:function(frm){
- if (frm.doc.company)
- {
+ company: function(frm){
+ if (frm.doc.company) {
frappe.call({
- method:"erpnext.setup.doctype.company.company.get_default_company_address",
- args:{name:frm.doc.company, existing_address: frm.doc.company_address},
+ method: "erpnext.setup.doctype.company.company.get_default_company_address",
+ args: {name:frm.doc.company, existing_address: frm.doc.company_address || ""},
+ debounce: 2000,
callback: function(r){
if (r.message){
frm.set_value("company_address",r.message)
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
index 17fbe2d..720a917 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.json
@@ -1,6 +1,5 @@
{
"actions": [],
- "allow_auto_repeat": 1,
"allow_import": 1,
"autoname": "naming_series:",
"creation": "2013-05-24 19:29:05",
@@ -13,11 +12,11 @@
"customer",
"customer_name",
"tax_id",
+ "pos_profile",
"is_pos",
"is_consolidated",
- "pos_profile",
- "offline_pos_name",
"is_return",
+ "update_billed_amount_in_sales_order",
"column_break1",
"company",
"company_tax_id",
@@ -25,11 +24,8 @@
"posting_time",
"set_posting_time",
"due_date",
- "amended_from",
- "returns",
"return_against",
- "column_break_21",
- "update_billed_amount_in_sales_order",
+ "amended_from",
"accounting_dimensions_section",
"project",
"dimension_col_break",
@@ -61,6 +57,8 @@
"ignore_pricing_rule",
"sec_warehouse",
"set_warehouse",
+ "column_break_55",
+ "set_target_warehouse",
"items_section",
"update_stock",
"scan_barcode",
@@ -158,6 +156,7 @@
"more_information",
"inter_company_invoice_reference",
"is_internal_customer",
+ "represents_company",
"customer_group",
"campaign",
"is_discounted",
@@ -171,6 +170,7 @@
"c_form_applicable",
"c_form_no",
"column_break8",
+ "unrealized_profit_loss_account",
"remarks",
"sales_team_section_break",
"sales_partner",
@@ -185,8 +185,7 @@
"column_break_140",
"auto_repeat",
"update_auto_repeat_reference",
- "against_income_account",
- "pos_total_qty"
+ "against_income_account"
],
"fields": [
{
@@ -294,16 +293,6 @@
"print_hide": 1
},
{
- "fieldname": "offline_pos_name",
- "fieldtype": "Data",
- "hidden": 1,
- "hide_days": 1,
- "hide_seconds": 1,
- "label": "Offline POS Name",
- "print_hide": 1,
- "read_only": 1
- },
- {
"default": "0",
"fieldname": "is_return",
"fieldtype": "Check",
@@ -403,19 +392,11 @@
},
{
"depends_on": "return_against",
- "fieldname": "returns",
- "fieldtype": "Section Break",
- "hide_days": 1,
- "hide_seconds": 1,
- "label": "Returns"
- },
- {
- "depends_on": "return_against",
"fieldname": "return_against",
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Return Against Sales Invoice",
+ "label": "Return Against",
"no_copy": 1,
"options": "Sales Invoice",
"print_hide": 1,
@@ -423,12 +404,6 @@
"search_index": 1
},
{
- "fieldname": "column_break_21",
- "fieldtype": "Column Break",
- "hide_days": 1,
- "hide_seconds": 1
- },
- {
"default": "0",
"depends_on": "eval: doc.is_return && doc.return_against",
"fieldname": "update_billed_amount_in_sales_order",
@@ -675,7 +650,8 @@
"fieldname": "sec_warehouse",
"fieldtype": "Section Break",
"hide_days": 1,
- "hide_seconds": 1
+ "hide_seconds": 1,
+ "label": "Warehouse"
},
{
"depends_on": "update_stock",
@@ -683,7 +659,7 @@
"fieldtype": "Link",
"hide_days": 1,
"hide_seconds": 1,
- "label": "Set Source Warehouse",
+ "label": "Source Warehouse",
"options": "Warehouse",
"print_hide": 1
},
@@ -692,6 +668,7 @@
"fieldtype": "Section Break",
"hide_days": 1,
"hide_seconds": 1,
+ "label": "Items",
"oldfieldtype": "Section Break",
"options": "fa fa-shopping-cart"
},
@@ -1655,7 +1632,7 @@
"in_standard_filter": 1,
"label": "Status",
"no_copy": 1,
- "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled",
+ "options": "\nDraft\nReturn\nCredit Note Issued\nSubmitted\nPaid\nUnpaid\nUnpaid and Discounted\nOverdue and Discounted\nOverdue\nCancelled\nInternal Transfer",
"print_hide": 1,
"read_only": 1
},
@@ -1902,17 +1879,6 @@
"report_hide": 1
},
{
- "fieldname": "pos_total_qty",
- "fieldtype": "Float",
- "hidden": 1,
- "hide_days": 1,
- "hide_seconds": 1,
- "label": "Total Qty",
- "print_hide": 1,
- "print_hide_if_no_value": 1,
- "read_only": 1
- },
- {
"collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
@@ -1950,13 +1916,49 @@
"fieldtype": "Data",
"label": "Company Tax ID",
"read_only": 1
+ },
+ {
+ "depends_on": "eval:doc.is_internal_customer",
+ "description": "Unrealized Profit / Loss account for intra-company transfers",
+ "fieldname": "unrealized_profit_loss_account",
+ "fieldtype": "Link",
+ "label": "Unrealized Profit / Loss Account",
+ "options": "Account"
+ },
+ {
+ "depends_on": "eval:doc.is_internal_customer",
+ "description": "Company which internal customer represents",
+ "fetch_from": "customer.represents_company",
+ "fieldname": "represents_company",
+ "fieldtype": "Link",
+ "label": "Represents Company",
+ "options": "Company",
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_55",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval: doc.is_internal_customer && doc.update_stock",
+ "fieldname": "set_target_warehouse",
+ "fieldtype": "Link",
+ "label": "Set Target Warehouse",
+ "options": "Warehouse"
}
],
"icon": "fa fa-file-text",
"idx": 181,
"is_submittable": 1,
- "links": [],
- "modified": "2020-10-30 13:57:45.086303",
+ "links": [
+ {
+ "custom": 1,
+ "group": "Reference",
+ "link_doctype": "POS Invoice",
+ "link_fieldname": "consolidated_invoice"
+ }
+ ],
+ "modified": "2021-02-01 15:42:26.261540",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice",
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
index af6c696..4217711 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py
@@ -4,9 +4,9 @@
from __future__ import unicode_literals
import frappe, erpnext
import frappe.defaults
-from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form
+from frappe.utils import cint, flt, getdate, add_days, cstr, nowdate, get_link_to_form, formatdate
from frappe import _, msgprint, throw
-from erpnext.accounts.party import get_party_account, get_due_date
+from erpnext.accounts.party import get_party_account, get_due_date, get_party_details
from frappe.model.mapper import get_mapped_doc
from erpnext.controllers.selling_controller import SellingController
from erpnext.accounts.utils import get_account_currency
@@ -21,6 +21,9 @@
from erpnext.accounts.doctype.loyalty_program.loyalty_program import \
get_loyalty_program_details_with_points, get_loyalty_details, validate_loyalty_points
from erpnext.accounts.deferred_revenue import validate_service_stop_date
+from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details
+from frappe.model.utils import get_fetch_values
+from frappe.contacts.doctype.address.address import get_address_display
from erpnext.healthcare.utils import manage_invoice_submit_cancel
@@ -53,7 +56,7 @@
"""Set indicator for portal"""
if self.outstanding_amount < 0:
self.indicator_title = _("Credit Note Issued")
- self.indicator_color = "darkgrey"
+ self.indicator_color = "gray"
elif self.outstanding_amount > 0 and getdate(self.due_date) >= getdate(nowdate()):
self.indicator_color = "orange"
self.indicator_title = _("Unpaid")
@@ -62,7 +65,7 @@
self.indicator_title = _("Overdue")
elif cint(self.is_return) == 1:
self.indicator_title = _("Return")
- self.indicator_color = "darkgrey"
+ self.indicator_color = "gray"
else:
self.indicator_color = "green"
self.indicator_title = _("Paid")
@@ -73,6 +76,8 @@
if not self.is_pos:
self.so_dn_required()
+
+ self.set_tax_withholding()
self.validate_proj_cust()
self.validate_pos_return()
@@ -151,6 +156,32 @@
if cost_center_company != self.company:
frappe.throw(_("Row #{0}: Cost Center {1} does not belong to company {2}").format(frappe.bold(item.idx), frappe.bold(item.cost_center), frappe.bold(self.company)))
+ def set_tax_withholding(self):
+ tax_withholding_details = get_party_tax_withholding_details(self)
+
+ if not tax_withholding_details:
+ return
+
+ accounts = []
+ tax_withholding_account = tax_withholding_details.get("account_head")
+
+ for d in self.taxes:
+ if d.account_head == tax_withholding_account:
+ d.update(tax_withholding_details)
+ accounts.append(d.account_head)
+
+ if not accounts or tax_withholding_account not in accounts:
+ self.append("taxes", tax_withholding_details)
+
+ to_remove = [d for d in self.taxes
+ if not d.tax_amount and d.charge_type == "Actual" and d.account_head == tax_withholding_account]
+
+ for d in to_remove:
+ self.remove(d)
+
+ # calculate totals again after applying TDS
+ self.calculate_taxes_and_totals()
+
def before_save(self):
set_account_for_mode_of_payment(self)
@@ -180,6 +211,9 @@
# this sequence because outstanding may get -ve
self.make_gl_entries()
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
if not self.is_return:
self.update_billing_status_for_zero_amount_refdoc("Delivery Note")
self.update_billing_status_for_zero_amount_refdoc("Sales Order")
@@ -228,9 +262,27 @@
if len(self.payments) == 0 and self.is_pos:
frappe.throw(_("At least one mode of payment is required for POS invoice."))
- def before_cancel(self):
- self.update_time_sheet(None)
+ def check_if_consolidated_invoice(self):
+ # since POS Invoice extends Sales Invoice, we explicitly check if doctype is Sales Invoice
+ if self.doctype == "Sales Invoice" and self.is_consolidated:
+ invoice_or_credit_note = "consolidated_credit_note" if self.is_return else "consolidated_invoice"
+ pos_closing_entry = frappe.get_all(
+ "POS Invoice Merge Log",
+ filters={ invoice_or_credit_note: self.name },
+ pluck="pos_closing_entry"
+ )
+ if pos_closing_entry:
+ msg = _("To cancel a {} you need to cancel the POS Closing Entry {}. ").format(
+ frappe.bold("Consolidated Sales Invoice"),
+ get_link_to_form("POS Closing Entry", pos_closing_entry[0])
+ )
+ frappe.throw(msg, title=_("Not Allowed"))
+ def before_cancel(self):
+ self.check_if_consolidated_invoice()
+
+ super(SalesInvoice, self).before_cancel()
+ self.update_time_sheet(None)
def on_cancel(self):
super(SalesInvoice, self).on_cancel()
@@ -258,6 +310,10 @@
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
+
+ if self.update_stock == 1:
+ self.repost_future_sle_and_gle()
+
frappe.db.set(self, 'status', 'Cancelled')
if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction":
@@ -279,7 +335,7 @@
if "Healthcare" in active_domains:
manage_invoice_submit_cancel(self, "on_cancel")
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def update_status_updater_args(self):
if cint(self.update_stock):
@@ -405,6 +461,8 @@
from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile
if not self.pos_profile:
pos_profile = get_pos_profile(self.company) or {}
+ if not pos_profile:
+ frappe.throw(_("No POS Profile found. Please create a New POS Profile first"))
self.pos_profile = pos_profile.get('name')
pos = {}
@@ -424,7 +482,9 @@
if not for_validate and not self.customer:
self.customer = pos.customer
- self.ignore_pricing_rule = pos.ignore_pricing_rule
+ if not for_validate:
+ self.ignore_pricing_rule = pos.ignore_pricing_rule
+
if pos.get('account_for_change_amount'):
self.account_for_change_amount = pos.get('account_for_change_amount')
@@ -472,6 +532,11 @@
return frappe.db.sql("select abbr from tabCompany where name=%s", self.company)[0][0]
def validate_debit_to_acc(self):
+ if not self.debit_to:
+ self.debit_to = get_party_account("Customer", self.customer, self.company)
+ if not self.debit_to:
+ self.raise_missing_debit_credit_account_error("Customer", self.customer)
+
account = frappe.get_cached_value("Account", self.debit_to,
["account_type", "report_type", "account_currency"], as_dict=True)
@@ -535,7 +600,12 @@
self.against_income_account = ','.join(against_acc)
def add_remarks(self):
- if not self.remarks: self.remarks = 'No Remarks'
+ if not self.remarks:
+ if self.po_no and self.po_date:
+ self.remarks = _("Against Customer Order {0} dated {1}").format(self.po_no,
+ formatdate(self.po_date))
+ else:
+ self.remarks = _("No Remarks")
def validate_auto_set_posting_time(self):
# Don't auto set the posting date and time if invoice is amended
@@ -715,22 +785,20 @@
if d.delivery_note and frappe.db.get_value("Delivery Note", d.delivery_note, "docstatus") != 1:
throw(_("Delivery Note {0} is not submitted").format(d.delivery_note))
- def make_gl_entries(self, gl_entries=None):
- from erpnext.accounts.general_ledger import make_reverse_gl_entries
+ def make_gl_entries(self, gl_entries=None, from_repost=False):
+ from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries
auto_accounting_for_stock = erpnext.is_perpetual_inventory_enabled(self.company)
if not gl_entries:
gl_entries = self.get_gl_entries()
if gl_entries:
- from erpnext.accounts.general_ledger import make_gl_entries
-
# if POS and amount is written off, updating outstanding amt after posting all gl entries
update_outstanding = "No" if (cint(self.is_pos) or self.write_off_account or
cint(self.redeem_loyalty_points)) else "Yes"
if self.docstatus == 1:
- make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False)
+ make_gl_entries(gl_entries, update_outstanding=update_outstanding, merge_entries=False, from_repost=from_repost)
elif self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -751,6 +819,7 @@
self.make_customer_gl_entry(gl_entries)
self.make_tax_gl_entries(gl_entries)
+ self.make_internal_transfer_gl_entries(gl_entries)
self.make_item_gl_entries(gl_entries)
@@ -770,7 +839,7 @@
# Checked both rounding_adjustment and rounded_total
# because rounded_total had value even before introcution of posting GLE based on rounded total
grand_total = self.rounded_total if (self.rounding_adjustment and self.rounded_total) else self.grand_total
- if grand_total:
+ if grand_total and not self.is_internal_transfer():
# Didnot use base_grand_total to book rounding loss gle
grand_total_in_company_currency = flt(grand_total * self.conversion_rate,
self.precision("grand_total"))
@@ -809,6 +878,18 @@
}, account_currency, item=tax)
)
+ def make_internal_transfer_gl_entries(self, gl_entries):
+ if self.is_internal_transfer() and flt(self.base_total_taxes_and_charges):
+ account_currency = get_account_currency(self.unrealized_profit_loss_account)
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": self.unrealized_profit_loss_account,
+ "against": self.customer,
+ "debit": flt(self.total_taxes_and_charges),
+ "debit_in_account_currency": flt(self.base_total_taxes_and_charges),
+ "cost_center": self.cost_center
+ }, account_currency, item=self))
+
def make_item_gl_entries(self, gl_entries):
# income account gl entries
for item in self.get("items"):
@@ -831,22 +912,24 @@
asset.db_set("disposal_date", self.posting_date)
asset.set_status("Sold" if self.docstatus==1 else None)
else:
- income_account = (item.income_account
- if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
+ # Do not book income for transfer within same company
+ if not self.is_internal_transfer():
+ income_account = (item.income_account
+ if (not item.enable_deferred_revenue or self.is_return) else item.deferred_revenue_account)
- account_currency = get_account_currency(income_account)
- gl_entries.append(
- self.get_gl_dict({
- "account": income_account,
- "against": self.customer,
- "credit": flt(item.base_net_amount, item.precision("base_net_amount")),
- "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount"))
- if account_currency==self.company_currency
- else flt(item.net_amount, item.precision("net_amount"))),
- "cost_center": item.cost_center,
- "project": item.project or self.project
- }, account_currency, item=item)
- )
+ account_currency = get_account_currency(income_account)
+ gl_entries.append(
+ self.get_gl_dict({
+ "account": income_account,
+ "against": self.customer,
+ "credit": flt(item.base_net_amount, item.precision("base_net_amount")),
+ "credit_in_account_currency": (flt(item.base_net_amount, item.precision("base_net_amount"))
+ if account_currency==self.company_currency
+ else flt(item.net_amount, item.precision("net_amount"))),
+ "cost_center": item.cost_center,
+ "project": item.project or self.project
+ }, account_currency, item=item)
+ )
# expense account gl entries
if cint(self.update_stock) and \
@@ -1258,7 +1341,9 @@
if self.docstatus == 2:
status = "Cancelled"
elif self.docstatus == 1:
- if outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed':
+ if self.is_internal_transfer():
+ self.status = 'Internal Transfer'
+ elif outstanding_amount > 0 and due_date < nowdate and self.is_discounted and discountng_status=='Disbursed':
self.status = "Overdue and Discounted"
elif outstanding_amount > 0 and due_date < nowdate:
self.status = "Overdue"
@@ -1500,7 +1585,7 @@
details = get_inter_company_details(doc, doctype)
price_list = doc.selling_price_list if doctype in ["Sales Invoice", "Sales Order", "Delivery Note"] else doc.buying_price_list
valid_price_list = frappe.db.get_value("Price List", {"name": price_list, "buying": 1, "selling": 1})
- if not valid_price_list:
+ if not valid_price_list and not doc.is_internal_transfer():
frappe.throw(_("Selected Price List should have buying and selling fields checked."))
party = details.get("party")
@@ -1523,15 +1608,21 @@
if doctype in ["Sales Invoice", "Sales Order"]:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Purchase Invoice" if doctype == "Sales Invoice" else "Purchase Order"
+ target_detail_field = "sales_invoice_item" if doctype == "Sales Invoice" else "sales_order_item"
+ source_document_warehouse_field = 'target_warehouse'
+ target_document_warehouse_field = 'from_warehouse'
else:
source_doc = frappe.get_doc(doctype, source_name)
target_doctype = "Sales Invoice" if doctype == "Purchase Invoice" else "Sales Order"
+ source_document_warehouse_field = 'from_warehouse'
+ target_document_warehouse_field = 'target_warehouse'
validate_inter_company_transaction(source_doc, doctype)
details = get_inter_company_details(source_doc, doctype)
def set_missing_values(source, target):
target.run_method("set_missing_values")
+ set_purchase_references(target)
def update_details(source_doc, target_doc, source_parent):
target_doc.inter_company_invoice_reference = source_doc.name
@@ -1539,41 +1630,184 @@
currency = frappe.db.get_value('Supplier', details.get('party'), 'default_currency')
target_doc.company = details.get("company")
target_doc.supplier = details.get("party")
+ target_doc.is_internal_supplier = 1
+ target_doc.ignore_pricing_rule = 1
target_doc.buying_price_list = source_doc.selling_price_list
+ # Invert Addresses
+ update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address)
+ update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address)
+
if currency:
target_doc.currency = currency
+
+ update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company,
+ doctype=target_doc.doctype, party_address=target_doc.supplier_address,
+ company_address=target_doc.shipping_address)
+
else:
currency = frappe.db.get_value('Customer', details.get('party'), 'default_currency')
target_doc.company = details.get("company")
target_doc.customer = details.get("party")
target_doc.selling_price_list = source_doc.buying_price_list
+ update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address)
+ update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address)
+ update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address)
+
if currency:
target_doc.currency = currency
+ update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company,
+ doctype=target_doc.doctype, party_address=target_doc.customer_address,
+ company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name)
+
+ item_field_map = {
+ "doctype": target_doctype + " Item",
+ "field_no_map": [
+ "income_account",
+ "expense_account",
+ "cost_center",
+ "warehouse"
+ ],
+ "field_map": {
+ 'rate': 'rate',
+ }
+ }
+
+ if doctype in ["Sales Invoice", "Sales Order"]:
+ item_field_map["field_map"].update({
+ "name": target_detail_field,
+ })
+
+ if source_doc.get('update_stock'):
+ item_field_map["field_map"].update({
+ source_document_warehouse_field: target_document_warehouse_field,
+ 'batch_no': 'batch_no',
+ 'serial_no': 'serial_no'
+ })
+
doclist = get_mapped_doc(doctype, source_name, {
doctype: {
"doctype": target_doctype,
"postprocess": update_details,
+ "set_target_warehouse": "set_from_warehouse",
"field_no_map": [
- "taxes_and_charges"
+ "taxes_and_charges",
+ "set_warehouse",
+ "shipping_address"
]
},
- doctype +" Item": {
- "doctype": target_doctype + " Item",
- "field_no_map": [
- "income_account",
- "expense_account",
- "cost_center",
- "warehouse"
- ]
- }
+ doctype +" Item": item_field_map
}, target_doc, set_missing_values)
return doclist
+def set_purchase_references(doc):
+ # add internal PO or PR links if any
+ if doc.is_internal_transfer():
+ if doc.doctype == 'Purchase Receipt':
+ so_item_map = get_delivery_note_details(doc.inter_company_invoice_reference)
+
+ if so_item_map:
+ pd_item_map, parent_child_map, warehouse_map = \
+ get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item')
+
+ update_pr_items(doc, so_item_map, pd_item_map, parent_child_map, warehouse_map)
+
+ elif doc.doctype == 'Purchase Invoice':
+ dn_item_map, so_item_map = get_sales_invoice_details(doc.inter_company_invoice_reference)
+ # First check for Purchase receipt
+ if list(dn_item_map.values()):
+ pd_item_map, parent_child_map, warehouse_map = \
+ get_pd_details('Purchase Receipt Item', dn_item_map, 'delivery_note_item')
+
+ update_pi_items(doc, 'pr_detail', 'purchase_receipt',
+ dn_item_map, pd_item_map, parent_child_map, warehouse_map)
+
+ if list(so_item_map.values()):
+ pd_item_map, parent_child_map, warehouse_map = \
+ get_pd_details('Purchase Order Item', so_item_map, 'sales_order_item')
+
+ update_pi_items(doc, 'po_detail', 'purchase_order',
+ so_item_map, pd_item_map, parent_child_map, warehouse_map)
+
+def update_pi_items(doc, detail_field, parent_field, sales_item_map,
+ purchase_item_map, parent_child_map, warehouse_map):
+ for item in doc.get('items'):
+ item.set(detail_field, purchase_item_map.get(sales_item_map.get(item.sales_invoice_item)))
+ item.set(parent_field, parent_child_map.get(sales_item_map.get(item.sales_invoice_item)))
+ if doc.update_stock:
+ item.warehouse = warehouse_map.get(sales_item_map.get(item.sales_invoice_item))
+
+def update_pr_items(doc, sales_item_map, purchase_item_map, parent_child_map, warehouse_map):
+ for item in doc.get('items'):
+ item.purchase_order_item = purchase_item_map.get(sales_item_map.get(item.delivery_note_item))
+ item.warehouse = warehouse_map.get(sales_item_map.get(item.delivery_note_item))
+ item.purchase_order = parent_child_map.get(sales_item_map.get(item.delivery_note_item))
+
+def get_delivery_note_details(internal_reference):
+ so_item_map = {}
+
+ si_item_details = frappe.get_all('Delivery Note Item', fields=['name', 'so_detail'],
+ filters={'parent': internal_reference})
+
+ for d in si_item_details:
+ so_item_map.setdefault(d.name, d.so_detail)
+
+ return so_item_map
+
+def get_sales_invoice_details(internal_reference):
+ dn_item_map = {}
+ so_item_map = {}
+
+ si_item_details = frappe.get_all('Sales Invoice Item', fields=['name', 'so_detail',
+ 'dn_detail'], filters={'parent': internal_reference})
+
+ for d in si_item_details:
+ if d.dn_detail:
+ dn_item_map.setdefault(d.name, d.dn_detail)
+ if d.so_detail:
+ so_item_map.setdefault(d.name, d.so_detail)
+
+ return dn_item_map, so_item_map
+
+def get_pd_details(doctype, sd_detail_map, sd_detail_field):
+ pd_item_map = {}
+ accepted_warehouse_map = {}
+ parent_child_map = {}
+
+ pd_item_details = frappe.get_all(doctype,
+ fields=[sd_detail_field, 'name', 'warehouse', 'parent'], filters={sd_detail_field: ('in', list(sd_detail_map.values()))})
+
+ for d in pd_item_details:
+ pd_item_map.setdefault(d.get(sd_detail_field), d.name)
+ parent_child_map.setdefault(d.get(sd_detail_field), d.parent)
+ accepted_warehouse_map.setdefault(d.get(sd_detail_field), d.warehouse)
+
+ return pd_item_map, parent_child_map, accepted_warehouse_map
+
+def update_taxes(doc, party=None, party_type=None, company=None, doctype=None, party_address=None,
+ company_address=None, shipping_address_name=None, master_doctype=None):
+ # Update Party Details
+ party_details = get_party_details(party=party, party_type=party_type, company=company,
+ doctype=doctype, party_address=party_address, company_address=company_address,
+ shipping_address=shipping_address_name)
+
+ # Update taxes and charges if any
+ doc.taxes_and_charges = party_details.get('taxes_and_charges')
+ doc.set('taxes', party_details.get('taxes'))
+
+def update_address(doc, address_field, address_display_field, address_name):
+ doc.set(address_field, address_name)
+ fetch_values = get_fetch_values(doc.doctype, address_field, address_name)
+
+ for key, value in fetch_values.items():
+ doc.set(key, value)
+
+ doc.set(address_display_field, get_address_display(doc.get(address_field)))
+
@frappe.whitelist()
def get_loyalty_programs(customer):
''' sets applicable loyalty program to the customer or returns a list of applicable programs '''
@@ -1649,6 +1883,7 @@
where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""",
(company, mode_of_payment), as_dict=1)
+@frappe.whitelist()
def create_dunning(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount
diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js
index 05d49df..1a01cb5 100644
--- a/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js
+++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice_list.js
@@ -10,12 +10,12 @@
"Draft": "grey",
"Unpaid": "orange",
"Paid": "green",
- "Return": "darkgrey",
- "Credit Note Issued": "darkgrey",
+ "Return": "gray",
+ "Credit Note Issued": "gray",
"Unpaid and Discounted": "orange",
"Overdue and Discounted": "red",
- "Overdue": "red"
-
+ "Overdue": "red",
+ "Internal Transfer": "darkgrey"
};
return [__(doc.status), status_color[doc.status], "status,=,"+doc.status];
},
diff --git a/erpnext/accounts/doctype/sales_invoice/test_records.json b/erpnext/accounts/doctype/sales_invoice/test_records.json
index 11ebe6a..ee6419d 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_records.json
+++ b/erpnext/accounts/doctype/sales_invoice/test_records.json
@@ -17,7 +17,8 @@
"description": "138-CMS Shoe",
"doctype": "Sales Invoice Item",
"income_account": "Sales - _TC",
- "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "expense_account": "_Test Account Cost for Goods Sold - _TC",
+ "item_code": "138-CMS Shoe",
"item_name": "138-CMS Shoe",
"parentfield": "items",
"qty": 1.0,
diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
index 9660c95..7cd1828 100644
--- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
+++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py
@@ -10,7 +10,6 @@
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry, get_qty_after_transaction
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice
from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.exceptions import InvalidAccountCurrency, InvalidCurrency
from erpnext.stock.doctype.serial_no.serial_no import SerialNoWarehouseError
from frappe.model.naming import make_autoname
@@ -23,6 +22,7 @@
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice
+from erpnext.stock.utils import get_incoming_rate
class TestSalesInvoice(unittest.TestCase):
def make(self):
@@ -659,7 +659,6 @@
def test_sales_invoice_gl_entry_without_perpetual_inventory(self):
si = frappe.copy_doc(test_records[1])
- set_perpetual_inventory(0, si.company)
si.insert()
si.submit()
@@ -690,7 +689,8 @@
self.assertTrue(gle)
def test_pos_gl_entry_with_perpetual_inventory(self):
- make_pos_profile()
+ 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", item_code= "_Test FG Item",warehouse= "Stores - TCP1",cost_center= "Main - TCP1")
@@ -746,7 +746,8 @@
self.assertEqual(pos_return.get('payments')[0].amount, -1000)
def test_pos_change_amount(self):
- make_pos_profile()
+ 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",
item_code= "_Test FG Item",warehouse= "Stores - TCP1", cost_center= "Main - TCP1")
@@ -813,7 +814,6 @@
frappe.db.sql("delete from `tabPOS Profile`")
def test_pos_si_without_payment(self):
- set_perpetual_inventory()
make_pos_profile()
pos = copy.deepcopy(test_records[1])
@@ -827,9 +827,8 @@
self.assertRaises(frappe.ValidationError, si.submit)
def test_sales_invoice_gl_entry_with_perpetual_inventory_no_item_code(self):
- set_perpetual_inventory()
-
- si = frappe.get_doc(test_records[1])
+ si = create_sales_invoice(company="_Test Company with perpetual inventory", debit_to = "Debtors - TCP1",
+ income_account="Sales - TCP1", cost_center = "Main - TCP1", do_not_save=True)
si.get("items")[0].item_code = None
si.insert()
si.submit()
@@ -840,24 +839,16 @@
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
- [si.debit_to, 630.0, 0.0],
- [test_records[1]["items"][0]["income_account"], 0.0, 500.0],
- [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
- [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
+ ["Debtors - TCP1", 100.0, 0.0],
+ ["Sales - TCP1", 0.0, 100.0]
])
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
- set_perpetual_inventory(0)
-
def test_sales_invoice_gl_entry_with_perpetual_inventory_non_stock_item(self):
- set_perpetual_inventory()
- si = frappe.get_doc(test_records[1])
- si.get("items")[0].item_code = "_Test Non Stock Item"
- si.insert()
- si.submit()
+ si = create_sales_invoice(item="_Test Non Stock Item")
gl_entries = frappe.db.sql("""select account, debit, credit
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
@@ -865,17 +856,14 @@
self.assertTrue(gl_entries)
expected_values = dict((d[0], d) for d in [
- [si.debit_to, 630.0, 0.0],
- [test_records[1]["items"][0]["income_account"], 0.0, 500.0],
- [test_records[1]["taxes"][0]["account_head"], 0.0, 80.0],
- [test_records[1]["taxes"][1]["account_head"], 0.0, 50.0],
+ [si.debit_to, 100.0, 0.0],
+ [test_records[1]["items"][0]["income_account"], 0.0, 100.0]
])
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account][0], gle.account)
self.assertEqual(expected_values[gle.account][1], gle.debit)
self.assertEqual(expected_values[gle.account][2], gle.credit)
- set_perpetual_inventory(0)
def _insert_purchase_receipt(self):
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import test_records \
@@ -1104,7 +1092,6 @@
self.assertEqual(si.grand_total, 859.43)
def test_multi_currency_gle(self):
- set_perpetual_inventory(0)
si = create_sales_invoice(customer="_Test Customer USD", debit_to="_Test Receivable USD - _TC",
currency="USD", conversion_rate=50)
@@ -1571,7 +1558,7 @@
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
-
+
def test_sales_invoice_with_project_link(self):
from erpnext.projects.doctype.project.test_project import make_project
@@ -1587,17 +1574,17 @@
})
sales_invoice = create_sales_invoice(do_not_save=1)
- sales_invoice.items[0].project = item_project.project_name
- sales_invoice.project = project.project_name
+ sales_invoice.items[0].project = item_project.name
+ sales_invoice.project = project.name
sales_invoice.submit()
expected_values = {
"Debtors - _TC": {
- "project": project.project_name
+ "project": project.name
},
"Sales - _TC": {
- "project": item_project.project_name
+ "project": item_project.name
}
}
@@ -1605,9 +1592,9 @@
debit_in_account_currency, credit_in_account_currency
from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s
order by account asc""", sales_invoice.name, as_dict=1)
-
+
self.assertTrue(gl_entries)
-
+
for gle in gl_entries:
self.assertEqual(expected_values[gle.account]["project"], gle.project)
@@ -1774,99 +1761,95 @@
si.submit()
target_doc = make_inter_company_transaction("Sales Invoice", si.name)
+ target_doc.items[0].update({
+ "expense_account": "Cost of Goods Sold - _TC1",
+ "cost_center": "Main - _TC1",
+ "warehouse": "Stores - _TC1"
+ })
target_doc.submit()
self.assertEqual(target_doc.company, "_Test Company 1")
self.assertEqual(target_doc.supplier, "_Test Internal Supplier")
+ def test_internal_transfer_gl_entry(self):
+ ## Create internal transfer account
+ account = create_account(account_name="Unrealized Profit",
+ parent_account="Current Liabilities - TCP1", company="_Test Company with perpetual inventory")
+
+ frappe.db.set_value('Company', '_Test Company with perpetual inventory',
+ 'unrealized_profit_loss_account', account)
+
+ customer = create_internal_customer("_Test Internal Customer 2", "_Test Company with perpetual inventory",
+ "_Test Company with perpetual inventory")
+
+ create_internal_supplier("_Test Internal Supplier 2", "_Test Company with perpetual inventory",
+ "_Test Company with perpetual inventory")
+
+ si = create_sales_invoice(
+ company = "_Test Company with perpetual inventory",
+ customer = customer,
+ debit_to = "Debtors - TCP1",
+ warehouse = "Stores - TCP1",
+ income_account = "Sales - TCP1",
+ expense_account = "Cost of Goods Sold - TCP1",
+ cost_center = "Main - TCP1",
+ currency = "INR",
+ do_not_save = 1
+ )
+
+ si.selling_price_list = "_Test Price List Rest of the World"
+ si.update_stock = 1
+ si.items[0].target_warehouse = 'Work In Progress - TCP1'
+ add_taxes(si)
+ si.save()
+
+ rate = 0.0
+ for d in si.get('items'):
+ rate = get_incoming_rate({
+ "item_code": d.item_code,
+ "warehouse": d.warehouse,
+ "posting_date": si.posting_date,
+ "posting_time": si.posting_time,
+ "qty": -1 * flt(d.get('stock_qty')),
+ "serial_no": d.serial_no,
+ "company": si.company,
+ "voucher_type": 'Sales Invoice',
+ "voucher_no": si.name,
+ "allow_zero_valuation": d.get("allow_zero_valuation")
+ }, raise_error_if_no_rate=False)
+
+ rate = flt(rate, 2)
+
+ si.submit()
+
+ target_doc = make_inter_company_transaction("Sales Invoice", si.name)
+ target_doc.company = '_Test Company with perpetual inventory'
+ target_doc.items[0].warehouse = 'Finished Goods - TCP1'
+ add_taxes(target_doc)
+ target_doc.save()
+ target_doc.submit()
+
+ tax_amount = flt(rate * (12/100), 2)
+ si_gl_entries = [
+ ["_Test Account Excise Duty - TCP1", 0.0, tax_amount, nowdate()],
+ ["Unrealized Profit - TCP1", tax_amount, 0.0, nowdate()]
+ ]
+
+ check_gl_entries(self, si.name, si_gl_entries, add_days(nowdate(), -1))
+
+ pi_gl_entries = [
+ ["_Test Account Excise Duty - TCP1", tax_amount , 0.0, nowdate()],
+ ["Unrealized Profit - TCP1", 0.0, tax_amount, nowdate()]
+ ]
+
+ # Sale and Purchase both should be at valuation rate
+ self.assertEqual(si.items[0].rate, rate)
+ self.assertEqual(target_doc.items[0].rate, rate)
+
+ check_gl_entries(self, target_doc.name, pi_gl_entries, add_days(nowdate(), -1))
+
def test_eway_bill_json(self):
- if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
- address = frappe.get_doc({
- "address_line1": "_Test Address Line 1",
- "address_title": "_Test Address for Eway bill",
- "address_type": "Billing",
- "city": "_Test City",
- "state": "Test State",
- "country": "India",
- "doctype": "Address",
- "is_primary_address": 1,
- "phone": "+91 0000000000",
- "gstin": "27AAECE4835E1ZR",
- "gst_state": "Maharashtra",
- "gst_state_number": "27",
- "pincode": "401108"
- }).insert()
-
- address.append("links", {
- "link_doctype": "Company",
- "link_name": "_Test Company"
- })
-
- address.save()
-
- if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
- address = frappe.get_doc({
- "address_line1": "_Test Address Line 1",
- "address_title": "_Test Customer-Address for Eway bill",
- "address_type": "Shipping",
- "city": "_Test City",
- "state": "Test State",
- "country": "India",
- "doctype": "Address",
- "is_primary_address": 1,
- "phone": "+91 0000000000",
- "gst_state": "Maharashtra",
- "gst_state_number": "27",
- "pincode": "410038"
- }).insert()
-
- address.append("links", {
- "link_doctype": "Customer",
- "link_name": "_Test Customer"
- })
-
- address.save()
-
- gst_settings = frappe.get_doc("GST Settings")
-
- gst_account = frappe.get_all(
- "GST Account",
- fields=["cgst_account", "sgst_account", "igst_account"],
- filters = {"company": "_Test Company"})
-
- if not gst_account:
- gst_settings.append("gst_accounts", {
- "company": "_Test Company",
- "cgst_account": "CGST - _TC",
- "sgst_account": "SGST - _TC",
- "igst_account": "IGST - _TC",
- })
-
- gst_settings.save()
-
- si = create_sales_invoice(do_not_save =1, rate = '60000')
-
- si.distance = 2000
- si.company_address = "_Test Address for Eway bill-Billing"
- si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
- si.vehicle_no = "KA12KA1234"
- si.gst_category = "Registered Regular"
-
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "CGST - _TC",
- "cost_center": "Main - _TC",
- "description": "CGST @ 9.0",
- "rate": 9
- })
-
- si.append("taxes", {
- "charge_type": "On Net Total",
- "account_head": "SGST - _TC",
- "cost_center": "Main - _TC",
- "description": "SGST @ 9.0",
- "rate": 9
- })
+ si = make_sales_invoice_for_ewaybill()
si.submit()
@@ -1883,6 +1866,197 @@
self.assertEqual(data['billLists'][0]['vehicleNo'], 'KA12KA1234')
self.assertEqual(data['billLists'][0]['itemList'][0]['taxableAmount'], 60000)
+ def test_einvoice_submission_without_irn(self):
+ # init
+ frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 1)
+ country = frappe.flags.country
+ frappe.flags.country = 'India'
+
+ si = make_sales_invoice_for_ewaybill()
+ self.assertRaises(frappe.ValidationError, si.submit)
+
+ si.irn = 'test_irn'
+ si.submit()
+
+ # reset
+ frappe.db.set_value('E Invoice Settings', 'E Invoice Settings', 'enable', 0)
+ frappe.flags.country = country
+
+ def test_einvoice_json(self):
+ from erpnext.regional.india.e_invoice.utils import make_einvoice
+
+ 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.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'])
+
+def make_test_address_for_ewaybill():
+ if not frappe.db.exists('Address', '_Test Address for Eway bill-Billing'):
+ address = frappe.get_doc({
+ "address_line1": "_Test Address Line 1",
+ "address_title": "_Test Address for Eway bill",
+ "address_type": "Billing",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 1,
+ "phone": "+910000000000",
+ "gstin": "27AAECE4835E1ZR",
+ "gst_state": "Maharashtra",
+ "gst_state_number": "27",
+ "pincode": "401108"
+ }).insert()
+
+ address.append("links", {
+ "link_doctype": "Company",
+ "link_name": "_Test Company"
+ })
+
+ address.save()
+
+ if not frappe.db.exists('Address', '_Test Customer-Address for Eway bill-Shipping'):
+ address = frappe.get_doc({
+ "address_line1": "_Test Address Line 1",
+ "address_title": "_Test Customer-Address for Eway bill",
+ "address_type": "Shipping",
+ "city": "_Test City",
+ "state": "Test State",
+ "country": "India",
+ "doctype": "Address",
+ "is_primary_address": 1,
+ "phone": "+910000000000",
+ "gstin": "27AACCM7806M1Z3",
+ "gst_state": "Maharashtra",
+ "gst_state_number": "27",
+ "pincode": "410038"
+ }).insert()
+
+ address.append("links", {
+ "link_doctype": "Customer",
+ "link_name": "_Test Customer"
+ })
+
+ address.save()
+
+def make_test_transporter_for_ewaybill():
+ if not frappe.db.exists('Supplier', '_Test Transporter'):
+ frappe.get_doc({
+ "doctype": "Supplier",
+ "supplier_name": "_Test Transporter",
+ "country": "India",
+ "supplier_group": "_Test Supplier Group",
+ "supplier_type": "Company",
+ "is_transporter": 1
+ }).insert()
+
+def make_sales_invoice_for_ewaybill():
+ make_test_address_for_ewaybill()
+ make_test_transporter_for_ewaybill()
+
+ gst_settings = frappe.get_doc("GST Settings")
+
+ gst_account = frappe.get_all(
+ "GST Account",
+ fields=["cgst_account", "sgst_account", "igst_account"],
+ filters = {"company": "_Test Company"}
+ )
+
+ if not gst_account:
+ gst_settings.append("gst_accounts", {
+ "company": "_Test Company",
+ "cgst_account": "CGST - _TC",
+ "sgst_account": "SGST - _TC",
+ "igst_account": "IGST - _TC",
+ })
+
+ gst_settings.save()
+
+ si = create_sales_invoice(do_not_save=1, rate='60000')
+
+ si.distance = 2000
+ si.company_address = "_Test Address for Eway bill-Billing"
+ si.customer_address = "_Test Customer-Address for Eway bill-Shipping"
+ si.vehicle_no = "KA12KA1234"
+ si.gst_category = "Registered Regular"
+ si.mode_of_transport = 'Road'
+ si.transporter = '_Test Transporter'
+
+ si.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "CGST - _TC",
+ "cost_center": "Main - _TC",
+ "description": "CGST @ 9.0",
+ "rate": 9
+ })
+
+ si.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "SGST - _TC",
+ "cost_center": "Main - _TC",
+ "description": "SGST @ 9.0",
+ "rate": 9
+ })
+
+ return si
+
def check_gl_entries(doc, voucher_no, expected_gle, posting_date):
gl_entries = frappe.db.sql("""select account, debit, credit, posting_date
from `tabGL Entry`
@@ -1935,14 +2109,19 @@
si.append("items", {
"item_code": args.item or args.item_code or "_Test Item",
+ "item_name": args.item_name or "_Test Item",
+ "description": args.description or "_Test Item",
"gst_hsn_code": "999800",
"warehouse": args.warehouse or "_Test Warehouse - _TC",
"qty": args.qty or 1,
+ "uom": args.uom or "Nos",
+ "stock_uom": args.uom or "Nos",
"rate": args.rate if args.get("rate") is not None else 100,
"income_account": args.income_account or "Sales - _TC",
"expense_account": args.expense_account or "Cost of Goods Sold - _TC",
"cost_center": args.cost_center or "_Test Cost Center - _TC",
- "serial_no": args.serial_no
+ "serial_no": args.serial_no,
+ "conversion_factor": 1
})
if not args.do_not_save:
@@ -2037,4 +2216,57 @@
"parentfield": "taxes",
"rate": 2,
"row_id": 1
- }]
\ No newline at end of file
+ }]
+
+def create_internal_customer(customer_name, represents_company, allowed_to_interact_with):
+ if not frappe.db.exists("Customer", customer_name):
+ customer = frappe.get_doc({
+ "customer_group": "_Test Customer Group",
+ "customer_name": customer_name,
+ "customer_type": "Individual",
+ "doctype": "Customer",
+ "territory": "_Test Territory",
+ "is_internal_customer": 1,
+ "represents_company": represents_company
+ })
+
+ customer.append("companies", {
+ "company": allowed_to_interact_with
+ })
+
+ customer.insert()
+ customer_name = customer.name
+ else:
+ customer_name = frappe.db.get_value("Customer", customer_name)
+
+ return customer_name
+
+def create_internal_supplier(supplier_name, represents_company, allowed_to_interact_with):
+ if not frappe.db.exists("Supplier", supplier_name):
+ supplier = frappe.get_doc({
+ "supplier_group": "_Test Supplier Group",
+ "supplier_name": supplier_name,
+ "doctype": "Supplier",
+ "is_internal_supplier": 1,
+ "represents_company": represents_company
+ })
+
+ supplier.append("companies", {
+ "company": allowed_to_interact_with
+ })
+
+ supplier.insert()
+ supplier_name = supplier.name
+ else:
+ supplier_name = frappe.db.exists("Supplier", supplier_name)
+
+ return supplier_name
+
+def add_taxes(doc):
+ doc.append('taxes', {
+ 'account_head': '_Test Account Excise Duty - TCP1',
+ "charge_type": "On Net Total",
+ "cost_center": "Main - TCP1",
+ "description": "Excise Duty",
+ "rate": 12
+ })
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
index fb3dd6a..b403c7b 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "hash",
"creation": "2013-06-04 11:02:19",
"doctype": "DocType",
@@ -44,6 +45,7 @@
"base_rate",
"base_amount",
"pricing_rules",
+ "stock_uom_rate",
"is_free_item",
"section_break_21",
"net_rate",
@@ -51,6 +53,7 @@
"column_break_24",
"base_net_rate",
"base_net_amount",
+ "incoming_rate",
"drop_ship",
"delivered_by_supplier",
"accounting",
@@ -563,11 +566,12 @@
"print_hide": 1
},
{
+ "depends_on": "eval: parent.is_internal_customer && parent.update_stock",
"fieldname": "target_warehouse",
"fieldtype": "Link",
"hidden": 1,
"ignore_user_permissions": 1,
- "label": "Customer Warehouse (Optional)",
+ "label": "Target Warehouse",
"no_copy": 1,
"options": "Warehouse",
"print_hide": 1
@@ -792,20 +796,36 @@
"options": "Project"
},
{
- "depends_on": "eval:parent.update_stock == 1",
- "fieldname": "sales_invoice_item",
- "fieldtype": "Data",
- "ignore_user_permissions": 1,
- "label": "Sales Invoice Item",
- "no_copy": 1,
- "print_hide": 1,
- "read_only": 1
- }
+ "depends_on": "eval:parent.update_stock == 1",
+ "fieldname": "sales_invoice_item",
+ "fieldtype": "Data",
+ "ignore_user_permissions": 1,
+ "label": "Sales Invoice Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Currency",
+ "label": "Incoming Rate",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.uom != doc.stock_uom",
+ "fieldname": "stock_uom_rate",
+ "fieldtype": "Currency",
+ "label": "Rate of Stock UOM",
+ "options": "currency",
+ "read_only": 1
+ }
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-08-20 11:24:41.749986",
+ "modified": "2021-01-30 21:42:37.796771",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Sales Invoice Item",
diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
index 7a62f8e..a73b03a 100644
--- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
+++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.py
@@ -5,8 +5,6 @@
import frappe
from frappe.model.document import Document
-from erpnext.controllers.print_settings import print_settings_for_item_table
class SalesInvoiceItem(Document):
- def __setup__(self):
- print_settings_for_item_table(self)
+ pass
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js
index 97a6fdd..0e01188 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.js
@@ -5,3 +5,25 @@
{% include "erpnext/public/js/controllers/accounts.js" %}
+frappe.tour['Sales Taxes and Charges Template'] = [
+ {
+ fieldname: "title",
+ title: __("Title"),
+ description: __("A name by which you will identify this template. You can change this later."),
+ },
+ {
+ fieldname: "company",
+ title: __("Company"),
+ description: __("Company for which this tax template will be applicable"),
+ },
+ {
+ fieldname: "is_default",
+ title: __("Is this Default?"),
+ description: __("Set this template as the default for all sales transactions"),
+ },
+ {
+ fieldname: "taxes",
+ title: __("Taxes Table"),
+ description: __("You can add a row for a tax rule here. These rules can be applied on the net total, or can be a flat amount."),
+ }
+];
diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
index b46de6c..429a9f3 100644
--- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
+++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.py
@@ -34,6 +34,9 @@
validate_disabled(doc)
+ # Validate with existing taxes and charges template for unique tax category
+ validate_for_tax_category(doc)
+
for tax in doc.get("taxes"):
validate_taxes_and_charges(tax)
validate_inclusive_tax(tax, doc)
@@ -41,3 +44,7 @@
def validate_disabled(doc):
if doc.is_default and doc.disabled:
frappe.throw(_("Disabled template must not be default template"))
+
+def validate_for_tax_category(doc):
+ if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0}):
+ frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category)))
diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js
index d0904ee..8e4b806 100644
--- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.js
+++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.js
@@ -1,16 +1,18 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
-frappe.ui.form.on('Shipping Rule', {
- refresh: function(frm) {
- frm.set_query("cost_center", function() {
- return {
- filters: {
- company: frm.doc.company
- }
- }
- })
+frappe.provide('erpnext.accounts.dimensions');
+frappe.ui.form.on('Shipping Rule', {
+ onload: function(frm) {
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
+ refresh: function(frm) {
frm.set_query("account", function() {
return {
filters: {
diff --git a/erpnext/accounts/doctype/subscription/subscription.py b/erpnext/accounts/doctype/subscription/subscription.py
index 552a5d4..e023b47 100644
--- a/erpnext/accounts/doctype/subscription/subscription.py
+++ b/erpnext/accounts/doctype/subscription/subscription.py
@@ -446,7 +446,7 @@
if not self.generate_invoice_at_period_start:
return False
- if self.is_new_subscription():
+ if self.is_new_subscription() and getdate() >= getdate(self.current_invoice_start):
return True
# Check invoice dates and make sure it doesn't have outstanding invoices
diff --git a/erpnext/accounts/doctype/subscription/subscription_list.js b/erpnext/accounts/doctype/subscription/subscription_list.js
index a4edb77..c7325fb 100644
--- a/erpnext/accounts/doctype/subscription/subscription_list.js
+++ b/erpnext/accounts/doctype/subscription/subscription_list.js
@@ -11,7 +11,7 @@
} else if(doc.status === 'Unpaid') {
return [__("Unpaid"), "red"];
} else if(doc.status === 'Cancelled') {
- return [__("Cancelled"), "darkgrey"];
+ return [__("Cancelled"), "gray"];
}
}
};
\ 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 32ad4cb..961bdb1 100644
--- a/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
+++ b/erpnext/accounts/doctype/tax_withholding_category/tax_withholding_category.py
@@ -12,37 +12,62 @@
class TaxWithholdingCategory(Document):
pass
-def get_party_tax_withholding_details(ref_doc, tax_withholding_category=None):
+def get_party_details(inv):
+ party_type, party = '', ''
+ if inv.doctype == 'Sales Invoice':
+ party_type = 'Customer'
+ party = inv.customer
+ else:
+ party_type = 'Supplier'
+ party = inv.supplier
+
+ return party_type, party
+
+def get_party_tax_withholding_details(inv, tax_withholding_category=None):
pan_no = ''
- suppliers = []
+ parties = []
+ party_type, party = get_party_details(inv)
if not tax_withholding_category:
- tax_withholding_category, pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, ['tax_withholding_category', 'pan'])
+ tax_withholding_category, pan_no = frappe.db.get_value(party_type, party, ['tax_withholding_category', 'pan'])
if not tax_withholding_category:
return
+ # if tax_withholding_category passed as an argument but not pan_no
if not pan_no:
- pan_no = frappe.db.get_value('Supplier', ref_doc.supplier, 'pan')
+ pan_no = frappe.db.get_value(party_type, party, 'pan')
# Get others suppliers with the same PAN No
if pan_no:
- suppliers = [d.name for d in frappe.get_all('Supplier', fields=['name'], filters={'pan': pan_no})]
+ parties = frappe.get_all(party_type, filters={ 'pan': pan_no }, pluck='name')
- if not suppliers:
- suppliers.append(ref_doc.supplier)
+ if not parties:
+ parties.append(party)
- fy = get_fiscal_year(ref_doc.posting_date, company=ref_doc.company)
- tax_details = get_tax_withholding_details(tax_withholding_category, fy[0], ref_doc.company)
+ fiscal_year = get_fiscal_year(inv.posting_date, company=inv.company)
+ tax_details = get_tax_withholding_details(tax_withholding_category, fiscal_year[0], inv.company)
+
if not tax_details:
frappe.throw(_('Please set associated account in Tax Withholding Category {0} against Company {1}')
- .format(tax_withholding_category, ref_doc.company))
+ .format(tax_withholding_category, inv.company))
- tds_amount = get_tds_amount(suppliers, ref_doc.net_total, ref_doc.company,
- tax_details, fy, ref_doc.posting_date, pan_no)
+ if party_type == 'Customer' and not tax_details.cumulative_threshold:
+ # TCS is only chargeable on sum of invoiced value
+ frappe.throw(_('Tax Withholding Category {} against Company {} for Customer {} should have Cumulative Threshold value.')
+ .format(tax_withholding_category, inv.company, party))
- tax_row = get_tax_row(tax_details, tds_amount)
+ tax_amount, tax_deducted = get_tax_amount(
+ party_type, parties,
+ inv, tax_details,
+ fiscal_year, pan_no
+ )
+
+ if party_type == 'Supplier':
+ tax_row = get_tax_row_for_tds(tax_details, tax_amount)
+ else:
+ tax_row = get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted)
return tax_row
@@ -69,147 +94,254 @@
frappe.throw(_("No Tax Withholding data found for the current Fiscal Year."))
-def get_tax_row(tax_details, tds_amount):
-
- return {
+def get_tax_row_for_tcs(inv, tax_details, tax_amount, tax_deducted):
+ row = {
"category": "Total",
- "add_deduct_tax": "Deduct",
"charge_type": "Actual",
- "account_head": tax_details.account_head,
+ "tax_amount": tax_amount,
"description": tax_details.description,
- "tax_amount": tds_amount
+ "account_head": tax_details.account_head
}
-def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_details, posting_date, pan_no=None):
- fiscal_year, year_start_date, year_end_date = fiscal_year_details
- tds_amount = 0
- tds_deducted = 0
+ if tax_deducted:
+ # TCS already deducted on previous invoices
+ # So, TCS will be calculated by 'Previous Row Total'
- def _get_tds(amount, rate):
- if amount <= 0:
- return 0
-
- return amount * rate / 100
-
- ldc_name = frappe.db.get_value('Lower Deduction Certificate',
- {
- 'pan_no': pan_no,
- 'fiscal_year': fiscal_year
- }, 'name')
- ldc = ''
-
- if ldc_name:
- ldc = frappe.get_doc('Lower Deduction Certificate', ldc_name)
-
- entries = frappe.db.sql("""
- select voucher_no, credit
- from `tabGL Entry`
- where company = %s and
- party in %s and fiscal_year=%s and credit > 0
- and is_opening = 'No'
- """, (company, tuple(suppliers), fiscal_year), as_dict=1)
-
- vouchers = [d.voucher_no for d in entries]
- advance_vouchers = get_advance_vouchers(suppliers, fiscal_year=fiscal_year, company=company)
-
- tds_vouchers = vouchers + advance_vouchers
-
- if tds_vouchers:
- tds_deducted = frappe.db.sql("""
- SELECT sum(credit) FROM `tabGL Entry`
- WHERE
- account=%s and fiscal_year=%s and credit > 0
- and voucher_no in ({0})""". format(','.join(['%s'] * len(tds_vouchers))),
- ((tax_details.account_head, fiscal_year) + tuple(tds_vouchers)))
-
- tds_deducted = tds_deducted[0][0] if tds_deducted and tds_deducted[0][0] else 0
-
- if tds_deducted:
- if ldc:
- limit_consumed = frappe.db.get_value('Purchase Invoice',
- {
- 'supplier': ('in', suppliers),
- 'apply_tds': 1,
- 'docstatus': 1
- }, 'sum(net_total)')
-
- if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, limit_consumed, net_total,
- ldc.certificate_limit):
-
- tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
+ taxes_excluding_tcs = [d for d in inv.taxes if d.account_head != tax_details.account_head]
+ if taxes_excluding_tcs:
+ # chargeable amount is the total amount after other charges are applied
+ row.update({
+ "charge_type": "On Previous Row Total",
+ "row_id": len(taxes_excluding_tcs),
+ "rate": tax_details.rate
+ })
else:
- tds_amount = _get_tds(net_total, tax_details.rate)
- else:
- supplier_credit_amount = frappe.get_all('Purchase Invoice',
- fields = ['sum(net_total)'],
- filters = {'name': ('in', vouchers), 'docstatus': 1, "apply_tds": 1}, as_list=1)
+ # if only TCS is to be charged, then net total is chargeable amount
+ row.update({
+ "charge_type": "On Net Total",
+ "rate": tax_details.rate
+ })
- supplier_credit_amount = (supplier_credit_amount[0][0]
- if supplier_credit_amount and supplier_credit_amount[0][0] else 0)
+ return row
- jv_supplier_credit_amt = frappe.get_all('Journal Entry Account',
- fields = ['sum(credit_in_account_currency)'],
- filters = {
- 'parent': ('in', vouchers), 'docstatus': 1,
- 'party': ('in', suppliers),
- 'reference_type': ('not in', ['Purchase Invoice'])
- }, as_list=1)
+def get_tax_row_for_tds(tax_details, tax_amount):
+ return {
+ "category": "Total",
+ "charge_type": "Actual",
+ "tax_amount": tax_amount,
+ "add_deduct_tax": "Deduct",
+ "description": tax_details.description,
+ "account_head": tax_details.account_head
+ }
- supplier_credit_amount += (jv_supplier_credit_amt[0][0]
- if jv_supplier_credit_amt and jv_supplier_credit_amt[0][0] else 0)
+def get_lower_deduction_certificate(fiscal_year, pan_no):
+ ldc_name = frappe.db.get_value('Lower Deduction Certificate', { 'pan_no': pan_no, 'fiscal_year': fiscal_year }, 'name')
+ if ldc_name:
+ return frappe.get_doc('Lower Deduction Certificate', ldc_name)
- supplier_credit_amount += net_total
+def get_tax_amount(party_type, parties, inv, tax_details, fiscal_year_details, pan_no=None):
+ fiscal_year = fiscal_year_details[0]
- debit_note_amount = get_debit_note_amount(suppliers, year_start_date, year_end_date)
- supplier_credit_amount -= debit_note_amount
+ vouchers = get_invoice_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
+ advance_vouchers = get_advance_vouchers(parties, fiscal_year, inv.company, party_type=party_type)
+ taxable_vouchers = vouchers + advance_vouchers
- if ((tax_details.get('threshold', 0) and supplier_credit_amount >= tax_details.threshold)
- or (tax_details.get('cumulative_threshold', 0) and supplier_credit_amount >= tax_details.cumulative_threshold)):
+ tax_deducted = 0
+ if taxable_vouchers:
+ tax_deducted = get_deducted_tax(taxable_vouchers, fiscal_year, tax_details)
- if ldc and is_valid_certificate(ldc.valid_from, ldc.valid_upto, posting_date, tds_deducted, net_total,
- ldc.certificate_limit):
- tds_amount = get_ltds_amount(supplier_credit_amount, 0, ldc.certificate_limit, ldc.rate,
- tax_details)
+ tax_amount = 0
+ posting_date = inv.posting_date
+ if party_type == 'Supplier':
+ ldc = get_lower_deduction_certificate(fiscal_year, pan_no)
+ if tax_deducted:
+ net_total = inv.net_total
+ if ldc:
+ tax_amount = get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total)
else:
- tds_amount = _get_tds(supplier_credit_amount, tax_details.rate)
+ tax_amount = net_total * tax_details.rate / 100 if net_total > 0 else 0
+ else:
+ tax_amount = get_tds_amount(
+ ldc, parties, inv, tax_details,
+ fiscal_year_details, tax_deducted, vouchers
+ )
+
+ elif party_type == 'Customer':
+ if tax_deducted:
+ # if already TCS is charged, then amount will be calculated based on 'Previous Row Total'
+ tax_amount = 0
+ else:
+ # if no TCS has been charged in FY,
+ # then chargeable value is "prev invoices + advances" value which cross the threshold
+ tax_amount = get_tcs_amount(
+ parties, inv, tax_details,
+ fiscal_year_details, vouchers, advance_vouchers
+ )
+
+ return tax_amount, tax_deducted
+
+def get_invoice_vouchers(parties, fiscal_year, company, party_type='Supplier'):
+ dr_or_cr = 'credit' if party_type == 'Supplier' else 'debit'
+
+ filters = {
+ dr_or_cr: ['>', 0],
+ 'company': company,
+ 'party_type': party_type,
+ 'party': ['in', parties],
+ 'fiscal_year': fiscal_year,
+ 'is_opening': 'No',
+ 'is_cancelled': 0
+ }
+
+ return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck="voucher_no") or [""]
+
+def get_advance_vouchers(parties, fiscal_year=None, company=None, from_date=None, to_date=None, party_type='Supplier'):
+ # for advance vouchers, debit and credit is reversed
+ dr_or_cr = 'debit' if party_type == 'Supplier' else 'credit'
+
+ filters = {
+ dr_or_cr: ['>', 0],
+ 'is_opening': 'No',
+ 'is_cancelled': 0,
+ 'party_type': party_type,
+ 'party': ['in', parties],
+ 'against_voucher': ['is', 'not set']
+ }
+
+ if fiscal_year:
+ filters['fiscal_year'] = fiscal_year
+ if company:
+ filters['company'] = company
+ if from_date and to_date:
+ filters['posting_date'] = ['between', (from_date, to_date)]
+
+ return frappe.get_all('GL Entry', filters=filters, distinct=1, pluck='voucher_no') or [""]
+
+def get_deducted_tax(taxable_vouchers, fiscal_year, tax_details):
+ # check if TDS / TCS account is already charged on taxable vouchers
+ filters = {
+ 'is_cancelled': 0,
+ 'credit': ['>', 0],
+ 'fiscal_year': fiscal_year,
+ 'account': tax_details.account_head,
+ 'voucher_no': ['in', taxable_vouchers],
+ }
+ field = "sum(credit)"
+
+ return frappe.db.get_value('GL Entry', filters, field) or 0.0
+
+def get_tds_amount(ldc, parties, inv, tax_details, fiscal_year_details, tax_deducted, vouchers):
+ tds_amount = 0
+
+ supp_credit_amt = frappe.db.get_value('Purchase Invoice', {
+ 'name': ('in', vouchers), 'docstatus': 1, 'apply_tds': 1
+ }, 'sum(net_total)') or 0.0
+
+ supp_jv_credit_amt = frappe.db.get_value('Journal Entry Account', {
+ 'parent': ('in', vouchers), 'docstatus': 1,
+ 'party': ('in', parties), 'reference_type': ('!=', 'Purchase Invoice')
+ }, 'sum(credit_in_account_currency)') or 0.0
+
+ supp_credit_amt += supp_jv_credit_amt
+ supp_credit_amt += inv.net_total
+
+ debit_note_amount = get_debit_note_amount(parties, fiscal_year_details, inv.company)
+ supp_credit_amt -= debit_note_amount
+
+ threshold = tax_details.get('threshold', 0)
+ cumulative_threshold = tax_details.get('cumulative_threshold', 0)
+
+ if ((threshold and supp_credit_amt >= threshold) or (cumulative_threshold and supp_credit_amt >= cumulative_threshold)):
+ if ldc and is_valid_certificate(
+ ldc.valid_from, ldc.valid_upto,
+ inv.posting_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)
+ else:
+ tds_amount = supp_credit_amt * tax_details.rate / 100 if supp_credit_amt > 0 else 0
return tds_amount
-def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=None, to_date=None):
- condition = "fiscal_year=%s" % fiscal_year
+def get_tcs_amount(parties, inv, tax_details, fiscal_year_details, vouchers, adv_vouchers):
+ tcs_amount = 0
+ fiscal_year, _, _ = fiscal_year_details
+
+ # sum of debit entries made from sales invoices
+ invoiced_amt = frappe.db.get_value('GL Entry', {
+ 'is_cancelled': 0,
+ 'party': ['in', parties],
+ 'company': inv.company,
+ 'voucher_no': ['in', vouchers],
+ }, 'sum(debit)') or 0.0
+
+ # sum of credit entries made from PE / JV with unset 'against voucher'
+ advance_amt = frappe.db.get_value('GL Entry', {
+ 'is_cancelled': 0,
+ 'party': ['in', parties],
+ 'company': inv.company,
+ 'voucher_no': ['in', adv_vouchers],
+ }, 'sum(credit)') or 0.0
+
+ # sum of credit entries made from sales invoice
+ credit_note_amt = frappe.db.get_value('GL Entry', {
+ 'is_cancelled': 0,
+ 'credit': ['>', 0],
+ 'party': ['in', parties],
+ 'fiscal_year': fiscal_year,
+ 'company': inv.company,
+ 'voucher_type': 'Sales Invoice',
+ }, 'sum(credit)') or 0.0
+
+ cumulative_threshold = tax_details.get('cumulative_threshold', 0)
+
+ current_invoice_total = get_invoice_total_without_tcs(inv, tax_details)
+ total_invoiced_amt = current_invoice_total + invoiced_amt + advance_amt - credit_note_amt
+
+ if ((cumulative_threshold and total_invoiced_amt >= cumulative_threshold)):
+ chargeable_amt = total_invoiced_amt - cumulative_threshold
+ tcs_amount = chargeable_amt * tax_details.rate / 100 if chargeable_amt > 0 else 0
+
+ return tcs_amount
+
+def get_invoice_total_without_tcs(inv, tax_details):
+ tcs_tax_row = [d for d in inv.taxes if d.account_head == tax_details.account_head]
+ tcs_tax_row_amount = tcs_tax_row[0].base_tax_amount if tcs_tax_row else 0
+
+ return inv.grand_total - tcs_tax_row_amount
+
+def get_tds_amount_from_ldc(ldc, parties, fiscal_year, pan_no, tax_details, posting_date, net_total):
+ tds_amount = 0
+ limit_consumed = frappe.db.get_value('Purchase Invoice', {
+ 'supplier': ('in', parties),
+ 'apply_tds': 1,
+ 'docstatus': 1
+ }, 'sum(net_total)')
+
+ if is_valid_certificate(
+ ldc.valid_from, ldc.valid_upto,
+ posting_date, limit_consumed,
+ net_total, ldc.certificate_limit
+ ):
+ tds_amount = get_ltds_amount(net_total, limit_consumed, ldc.certificate_limit, ldc.rate, tax_details)
+
+ return tds_amount
+
+def get_debit_note_amount(suppliers, fiscal_year_details, company=None):
+ _, year_start_date, year_end_date = fiscal_year_details
+
+ filters = {
+ 'supplier': ['in', suppliers],
+ 'is_return': 1,
+ 'docstatus': 1,
+ 'posting_date': ['between', (year_start_date, year_end_date)]
+ }
+ fields = ['abs(sum(net_total)) as net_total']
if company:
- condition += "and company =%s" % (company)
- if from_date and to_date:
- condition += "and posting_date between %s and %s" % (from_date, to_date)
+ filters['company'] = company
- ## Appending the same supplier again if length of suppliers list is 1
- ## since tuple of single element list contains None, For example ('Test Supplier 1', )
- ## and the below query fails
- if len(suppliers) == 1:
- suppliers.append(suppliers[0])
-
- return frappe.db.sql_list("""
- select distinct voucher_no
- from `tabGL Entry`
- where party in %s and %s and debit > 0
- and is_opening = 'No'
- """, (tuple(suppliers), condition)) or []
-
-def get_debit_note_amount(suppliers, year_start_date, year_end_date, company=None):
- condition = "and 1=1"
- if company:
- condition = " and company=%s " % company
-
- if len(suppliers) == 1:
- suppliers.append(suppliers[0])
-
- return flt(frappe.db.sql("""
- select abs(sum(net_total))
- from `tabPurchase Invoice`
- where supplier in %s and is_return=1 and docstatus=1
- and posting_date between %s and %s %s
- """, (tuple(suppliers), year_start_date, year_end_date, condition)))
+ return frappe.get_all('Purchase Invoice', filters, fields)[0].get('net_total') or 0.0
def get_ltds_amount(current_amount, deducted_amount, certificate_limit, rate, tax_details):
if current_amount < (certificate_limit - deducted_amount):
@@ -227,4 +359,4 @@
certificate_limit > deducted_amount):
valid = True
- return valid
\ No newline at end of file
+ return valid
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 ef77674..9ce8e3f 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
@@ -9,7 +9,7 @@
from erpnext.accounts.utils import get_fiscal_year
from erpnext.buying.doctype.supplier.test_supplier import create_supplier
-test_dependencies = ["Supplier Group"]
+test_dependencies = ["Supplier Group", "Customer Group"]
class TestTaxWithholdingCategory(unittest.TestCase):
@classmethod
@@ -18,6 +18,9 @@
create_records()
create_tax_with_holding_category()
+ def tearDown(self):
+ cancel_invoices()
+
def test_cumulative_threshold_tds(self):
frappe.db.set_value("Supplier", "Test TDS Supplier", "tax_withholding_category", "Cumulative Threshold TDS")
invoices = []
@@ -128,9 +131,59 @@
for d in invoices:
d.cancel()
+ def test_cumulative_threshold_tcs(self):
+ frappe.db.set_value("Customer", "Test TCS Customer", "tax_withholding_category", "Cumulative Threshold TCS")
+ invoices = []
+
+ # create invoices for lower than single threshold tax rate
+ for _ in range(2):
+ si = create_sales_invoice(customer = "Test TCS Customer")
+ si.submit()
+ invoices.append(si)
+
+ # create another invoice whose total when added to previously created invoice,
+ # surpasses cumulative threshhold
+ si = create_sales_invoice(customer = "Test TCS Customer", rate=12000)
+ si.submit()
+
+ # assert tax collection on total invoice amount created until now
+ tcs_charged = sum([d.base_tax_amount for d in si.taxes if d.account_head == 'TCS - _TC'])
+ self.assertEqual(tcs_charged, 200)
+ self.assertEqual(si.grand_total, 12200)
+ invoices.append(si)
+
+ # TCS is already collected once, so going forward system will collect TCS on every invoice
+ 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'])
+ self.assertEqual(tcs_charged, 500)
+ invoices.append(si)
+
+ #delete invoices to avoid clashing
+ for d in invoices:
+ d.cancel()
+
+def cancel_invoices():
+ purchase_invoices = frappe.get_all("Purchase Invoice", {
+ 'supplier': ['in', ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']],
+ 'docstatus': 1
+ }, pluck="name")
+
+ sales_invoices = frappe.get_all("Sales Invoice", {
+ 'customer': 'Test TCS Customer',
+ 'docstatus': 1
+ }, pluck="name")
+
+ for d in purchase_invoices:
+ frappe.get_doc('Purchase Invoice', d).cancel()
+
+ for d in sales_invoices:
+ frappe.get_doc('Sales Invoice', d).cancel()
+
def create_purchase_invoice(**args):
# return sales invoice doc object
- item = frappe.get_doc('Item', {'item_name': 'TDS Item'})
+ item = frappe.db.get_value('Item', {'item_name': 'TDS Item'}, "name")
args = frappe._dict(args)
pi = frappe.get_doc({
@@ -145,7 +198,7 @@
"taxes": [],
"items": [{
'doctype': 'Purchase Invoice Item',
- 'item_code': item.name,
+ 'item_code': item,
'qty': args.qty or 1,
'rate': args.rate or 10000,
'cost_center': 'Main - _TC',
@@ -156,6 +209,33 @@
pi.save()
return pi
+def create_sales_invoice(**args):
+ # return sales invoice doc object
+ item = frappe.db.get_value('Item', {'item_name': 'TCS Item'}, "name")
+
+ args = frappe._dict(args)
+ si = frappe.get_doc({
+ "doctype": "Sales Invoice",
+ "posting_date": today(),
+ "customer": args.customer,
+ "company": '_Test Company',
+ "taxes_and_charges": "",
+ "currency": "INR",
+ "debit_to": "Debtors - _TC",
+ "taxes": [],
+ "items": [{
+ 'doctype': 'Sales Invoice Item',
+ 'item_code': item,
+ 'qty': args.qty or 1,
+ 'rate': args.rate or 10000,
+ 'cost_center': 'Main - _TC',
+ 'expense_account': 'Cost of Goods Sold - _TC'
+ }]
+ })
+
+ si.save()
+ return si
+
def create_records():
# create a new suppliers
for name in ['Test TDS Supplier', 'Test TDS Supplier1', 'Test TDS Supplier2']:
@@ -168,7 +248,17 @@
"doctype": "Supplier",
}).insert()
- # create an item
+ for name in ['Test TCS Customer']:
+ if frappe.db.exists('Customer', name):
+ continue
+
+ frappe.get_doc({
+ "customer_group": "_Test Customer Group",
+ "customer_name": name,
+ "doctype": "Customer"
+ }).insert()
+
+ # create item
if not frappe.db.exists('Item', "TDS Item"):
frappe.get_doc({
"doctype": "Item",
@@ -178,7 +268,16 @@
"is_stock_item": 0,
}).insert()
- # create an account
+ if not frappe.db.exists('Item', "TCS Item"):
+ frappe.get_doc({
+ "doctype": "Item",
+ "item_code": "TCS Item",
+ "item_name": "TCS Item",
+ "item_group": "All Item Groups",
+ "is_stock_item": 1
+ }).insert()
+
+ # create tds account
if not frappe.db.exists("Account", "TDS - _TC"):
frappe.get_doc({
'doctype': 'Account',
@@ -189,6 +288,17 @@
'root_type': 'Asset'
}).insert()
+ # create tcs account
+ if not frappe.db.exists("Account", "TCS - _TC"):
+ frappe.get_doc({
+ 'doctype': 'Account',
+ 'company': '_Test Company',
+ 'account_name': 'TCS',
+ 'parent_account': 'Duties and Taxes - _TC',
+ 'report_type': 'Balance Sheet',
+ 'root_type': 'Liability'
+ }).insert()
+
def create_tax_with_holding_category():
fiscal_year = get_fiscal_year(today(), company="_Test Company")[0]
@@ -210,6 +320,23 @@
}]
}).insert()
+ if not frappe.db.exists("Tax Withholding Category", "Cumulative Threshold TCS"):
+ frappe.get_doc({
+ "doctype": "Tax Withholding Category",
+ "name": "Cumulative Threshold TCS",
+ "category_name": "10% TCS",
+ "rates": [{
+ 'fiscal_year': fiscal_year,
+ 'tax_withholding_rate': 10,
+ 'single_threshold': 0,
+ 'cumulative_threshold': 30000.00
+ }],
+ "accounts": [{
+ 'company': '_Test Company',
+ 'account': 'TCS - _TC'
+ }]
+ }).insert()
+
# Single thresold
if not frappe.db.exists("Tax Withholding Category", "Single Threshold TDS"):
frappe.get_doc({
diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py
index 9a091bf..dac0c21 100644
--- a/erpnext/accounts/general_ledger.py
+++ b/erpnext/accounts/general_ledger.py
@@ -5,23 +5,19 @@
import frappe, erpnext
from frappe.utils import flt, cstr, cint, comma_and, today, getdate, formatdate, now
from frappe import _
-from erpnext.accounts.utils import get_stock_and_account_balance
from frappe.model.meta import get_field_precision
from erpnext.accounts.doctype.budget.budget import validate_expense_against_budget
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
-
class ClosedAccountingPeriod(frappe.ValidationError): pass
-class StockAccountInvalidTransaction(frappe.ValidationError): pass
-class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
-def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes'):
+def make_gl_entries(gl_map, cancel=False, adv_adj=False, merge_entries=True, update_outstanding='Yes', from_repost=False):
if gl_map:
if not cancel:
validate_accounting_period(gl_map)
gl_map = process_gl_map(gl_map, merge_entries)
if gl_map and len(gl_map) > 1:
- save_entries(gl_map, adv_adj, update_outstanding)
+ save_entries(gl_map, adv_adj, update_outstanding, from_repost)
else:
frappe.throw(_("Incorrect number of General Ledger Entries found. You might have selected a wrong Account in the transaction."))
else:
@@ -48,9 +44,9 @@
frappe.throw(_("You cannot create or cancel any accounting entries with in the closed Accounting Period {0}")
.format(frappe.bold(accounting_periods[0].name)), ClosedAccountingPeriod)
-def process_gl_map(gl_map, merge_entries=True):
+def process_gl_map(gl_map, merge_entries=True, precision=None):
if merge_entries:
- gl_map = merge_similar_entries(gl_map)
+ gl_map = merge_similar_entries(gl_map, precision)
for entry in gl_map:
# toggle debit, credit if negative entry
if flt(entry.debit) < 0:
@@ -73,7 +69,7 @@
return gl_map
-def merge_similar_entries(gl_map):
+def merge_similar_entries(gl_map, precision=None):
merged_gl_map = []
accounting_dimensions = get_accounting_dimensions()
for entry in gl_map:
@@ -92,7 +88,9 @@
company = gl_map[0].company if gl_map else erpnext.get_default_company()
company_currency = erpnext.get_company_currency(company)
- precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
+
+ if not precision:
+ precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency)
# filter zero debit and credit entries
merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map)
@@ -119,8 +117,9 @@
if same_head:
return e
-def save_entries(gl_map, adv_adj, update_outstanding):
- validate_cwip_accounts(gl_map)
+def save_entries(gl_map, adv_adj, update_outstanding, from_repost=False):
+ if not from_repost:
+ validate_cwip_accounts(gl_map)
round_off_debit_credit(gl_map)
@@ -128,76 +127,19 @@
check_freezing_date(gl_map[0]["posting_date"], adv_adj)
for entry in gl_map:
- make_entry(entry, adv_adj, update_outstanding)
+ make_entry(entry, adv_adj, update_outstanding, from_repost)
- # check against budget
- validate_expense_against_budget(entry)
-
- validate_account_for_perpetual_inventory(gl_map)
-
-
-def make_entry(args, adv_adj, update_outstanding):
+def make_entry(args, adv_adj, update_outstanding, from_repost=False):
gle = frappe.new_doc("GL Entry")
gle.update(args)
gle.flags.ignore_permissions = 1
- gle.insert()
- gle.run_method("on_update_with_args", adv_adj, update_outstanding)
+ gle.flags.from_repost = from_repost
+ gle.flags.adv_adj = adv_adj
+ gle.flags.update_outstanding = update_outstanding or 'Yes'
gle.submit()
- # check against budget
- validate_expense_against_budget(args)
-
-def validate_account_for_perpetual_inventory(gl_map):
- if cint(erpnext.is_perpetual_inventory_enabled(gl_map[0].company)):
- account_list = [gl_entries.account for gl_entries in gl_map]
-
- aii_accounts = [d.name for d in frappe.get_all("Account",
- filters={'account_type': 'Stock', 'is_group': 0, 'company': gl_map[0].company})]
-
- for account in account_list:
- if account not in aii_accounts:
- continue
-
- # Always use current date to get stock and account balance as there can future entries for
- # other items
- account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
- getdate(), gl_map[0].company)
-
- if gl_map[0].voucher_type=="Journal Entry":
- # In case of Journal Entry, there are no corresponding SL entries,
- # hence deducting currency amount
- account_bal -= flt(gl_map[0].debit) - flt(gl_map[0].credit)
- if account_bal == stock_bal:
- frappe.throw(_("Account: {0} can only be updated via Stock Transactions")
- .format(account), StockAccountInvalidTransaction)
-
- elif abs(account_bal - stock_bal) > 0.1:
- precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
- currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency"))
-
- diff = flt(stock_bal - account_bal, precision)
- error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format(
- stock_bal, account_bal, frappe.bold(account))
- error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff))
- stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account")
-
- db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency')
- db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency')
-
- journal_entry_args = {
- 'accounts':[
- {'account': account, db_or_cr_warehouse_account : abs(diff)},
- {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }]
- }
-
- frappe.msgprint(msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
- raise_exception=StockValueAndAccountBalanceOutOfSync,
- title=_('Values Out Of Sync'),
- primary_action={
- 'label': _('Make Journal Entry'),
- 'client_action': 'erpnext.route_to_adjustment_jv',
- 'args': journal_entry_args
- })
+ if not from_repost:
+ 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")])
@@ -254,7 +196,7 @@
if not round_off_gle:
for k in ["voucher_type", "voucher_no", "company",
- "posting_date", "remarks", "is_opening"]:
+ "posting_date", "remarks"]:
round_off_gle[k] = gl_map[0][k]
round_off_gle.update({
@@ -266,6 +208,7 @@
"cost_center": round_off_cost_center,
"party_type": None,
"party": None,
+ "is_opening": "No",
"against_voucher_type": None,
"against_voucher": None
})
diff --git a/erpnext/accounts/module_onboarding/accounts/accounts.json b/erpnext/accounts/module_onboarding/accounts/accounts.json
index 570d2bd..6b5c5a1 100644
--- a/erpnext/accounts/module_onboarding/accounts/accounts.json
+++ b/erpnext/accounts/module_onboarding/accounts/accounts.json
@@ -13,7 +13,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/accounts",
"idx": 0,
"is_complete": 0,
- "modified": "2020-07-08 14:06:09.033880",
+ "modified": "2020-10-30 15:41:15.547225",
"modified_by": "Administrator",
"module": "Accounts",
"name": "Accounts",
diff --git a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json
index 48637bf..fc49bd6 100644
--- a/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json
+++ b/erpnext/accounts/onboarding_step/chart_of_accounts/chart_of_accounts.json
@@ -1,19 +1,24 @@
{
"action": "Go to Page",
+ "action_label": "View Chart of Accounts",
+ "callback_message": "You can continue with the onboarding after exploring this page",
+ "callback_title": "Awesome Work",
"creation": "2020-05-13 19:58:20.928127",
+ "description": "# Chart Of Accounts\n\nThe Chart of Accounts is the blueprint of the accounts in your organization.\nIt is a tree view of the names of the Accounts (Ledgers and Groups) that a Company requires to manage its books of accounts. ERPNext sets up a simple chart of accounts for each Company you create, but you can modify it according to your needs and legal requirements.\n\nFor each company, Chart of Accounts signifies the way to classify the accounting entries, mostly\nbased on statutory (tax, compliance to government regulations) requirements.\n\nThere's a brief video tutorial about chart of accounts in the next step.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
+ "intro_video_url": "https://www.youtube.com/embed/AcfMCT7wLLo",
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 17:40:28.410447",
+ "modified": "2020-10-30 14:35:59.474920",
"modified_by": "Administrator",
"name": "Chart of Accounts",
"owner": "Administrator",
"path": "Tree/Account",
"reference_document": "Account",
+ "show_form_tour": 0,
"show_full_form": 0,
"title": "Review Chart of Accounts",
"validate_action": 0
diff --git a/erpnext/accounts/onboarding_step/configure_account_settings/configure_account_settings.json b/erpnext/accounts/onboarding_step/configure_account_settings/configure_account_settings.json
index c8be357..c84430a 100644
--- a/erpnext/accounts/onboarding_step/configure_account_settings/configure_account_settings.json
+++ b/erpnext/accounts/onboarding_step/configure_account_settings/configure_account_settings.json
@@ -1,18 +1,19 @@
{
- "action": "Create Entry",
+ "action": "Show Form Tour",
"creation": "2020-05-14 17:53:00.876946",
+ "description": "# Account Settings\n\nThis is a crucial piece of configuration. There are various account settings in ERPNext to restrict and configure actions in the Accounting module.\n\nThe following settings are avaialble for you to configure\n\n1. Account Freezing \n2. Credit and Overbilling\n3. Invoicing and Tax Automations\n4. Balance Sheet configurations\n\nThere's much more, you can check it all out in this step",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 1,
"is_skipped": 0,
- "modified": "2020-05-14 18:06:25.212923",
+ "modified": "2020-10-19 14:40:55.584484",
"modified_by": "Administrator",
"name": "Configure Account Settings",
"owner": "Administrator",
"reference_document": "Accounts Settings",
+ "show_form_tour": 0,
"show_full_form": 1,
"title": "Configure Account Settings",
"validate_action": 1
diff --git a/erpnext/accounts/onboarding_step/create_a_customer/create_a_customer.json b/erpnext/accounts/onboarding_step/create_a_customer/create_a_customer.json
index 5a403b0..0b6750c 100644
--- a/erpnext/accounts/onboarding_step/create_a_customer/create_a_customer.json
+++ b/erpnext/accounts/onboarding_step/create_a_customer/create_a_customer.json
@@ -1,18 +1,19 @@
{
"action": "Create Entry",
"creation": "2020-05-14 17:46:41.831517",
+ "description": "## Who is a Customer?\n\nA customer, who is sometimes known as a client, buyer, or purchaser is the one who receives goods, services, products, or ideas, from a seller for a monetary consideration.\n\nEvery customer needs to be assigned a unique id. Customer name itself can be the id or you can set a naming series for ids to be generated in Selling Settings.\n\nJust like the supplier, let's quickly create a customer.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-06-01 13:16:19.731719",
+ "modified": "2020-10-30 15:28:46.659660",
"modified_by": "Administrator",
"name": "Create a Customer",
"owner": "Administrator",
"reference_document": "Customer",
+ "show_form_tour": 0,
"show_full_form": 0,
"title": "Create a Customer",
"validate_action": 1
diff --git a/erpnext/accounts/onboarding_step/create_a_product/create_a_product.json b/erpnext/accounts/onboarding_step/create_a_product/create_a_product.json
index d2068e1..d76f645 100644
--- a/erpnext/accounts/onboarding_step/create_a_product/create_a_product.json
+++ b/erpnext/accounts/onboarding_step/create_a_product/create_a_product.json
@@ -1,19 +1,21 @@
{
"action": "Create Entry",
"creation": "2020-05-12 18:16:06.624554",
+ "description": "## Products and Services\n\nDepending on the nature of your business, you might be selling products or services to your clients or even both. \nERPNext is optimized for itemized management of your sales and purchase.\n\nThe **Item Master** is where you can add all your sales items. If you are in services, you can create an Item for each service that you offer. If you run a manufacturing business, the same master is used for keeping a record of raw materials, sub-assemblies etc.\n\nCompleting the Item Master is very essential for the successful implementation of ERPNext. We have a brief video introducing the item master for you, you can watch it in the next step.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
+ "intro_video_url": "https://www.youtube.com/watch?v=Sl5UFA5H5EQ",
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-12 18:30:02.489949",
+ "modified": "2020-10-30 15:20:30.133495",
"modified_by": "Administrator",
"name": "Create a Product",
"owner": "Administrator",
"reference_document": "Item",
+ "show_form_tour": 0,
"show_full_form": 0,
- "title": "Create a Product",
+ "title": "Create a Sales Item",
"validate_action": 1
}
\ No newline at end of file
diff --git a/erpnext/accounts/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/accounts/onboarding_step/create_a_supplier/create_a_supplier.json
index 7a64224..64bc7bb 100644
--- a/erpnext/accounts/onboarding_step/create_a_supplier/create_a_supplier.json
+++ b/erpnext/accounts/onboarding_step/create_a_supplier/create_a_supplier.json
@@ -1,18 +1,19 @@
{
"action": "Create Entry",
"creation": "2020-05-14 22:09:10.043554",
+ "description": "## Who is a Supplier?\n\nSuppliers are companies or individuals who provide you with products or services. ERPNext has comprehensive features for purchase cycles. \n\nLet's quickly create a supplier with the minimal details required. You need the name of the supplier, assign the supplier to a group, and select the type of the supplier, viz. Company or Individual.",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 22:09:10.043554",
+ "modified": "2020-10-30 15:26:48.315772",
"modified_by": "Administrator",
"name": "Create a Supplier",
"owner": "Administrator",
"reference_document": "Supplier",
+ "show_form_tour": 0,
"show_full_form": 0,
"title": "Create a Supplier",
"validate_action": 1
diff --git a/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json b/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json
index 3a2b8d3..ddbc89e 100644
--- a/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json
+++ b/erpnext/accounts/onboarding_step/create_your_first_purchase_invoice/create_your_first_purchase_invoice.json
@@ -1,18 +1,19 @@
{
"action": "Create Entry",
"creation": "2020-05-14 22:10:07.049704",
+ "description": "# What's a Purchase Invoice?\n\nA Purchase Invoice is a bill you receive from your Suppliers against which you need to make the payment.\nPurchase Invoice is the exact opposite of your Sales Invoice. Here you accrue expenses to your Supplier. \n\nThe following is what a typical purchase cycle looks like, however you can create a purchase invoice directly as well.\n\n![Purchase Flow](https://docs.erpnext.com/docs/assets/img/accounts/pi-flow.png)\n\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 22:10:07.049704",
+ "modified": "2020-10-30 15:30:26.337773",
"modified_by": "Administrator",
"name": "Create Your First Purchase Invoice",
"owner": "Administrator",
"reference_document": "Purchase Invoice",
+ "show_form_tour": 0,
"show_full_form": 1,
"title": "Create Your First Purchase Invoice ",
"validate_action": 1
diff --git a/erpnext/accounts/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json b/erpnext/accounts/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json
index 473de50..9e7dd67 100644
--- a/erpnext/accounts/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json
+++ b/erpnext/accounts/onboarding_step/create_your_first_sales_invoice/create_your_first_sales_invoice.json
@@ -1,18 +1,19 @@
{
"action": "Create Entry",
"creation": "2020-05-14 17:48:21.019019",
+ "description": "# All about sales invoice\n\nA Sales Invoice is a bill that you send to your Customers against which the Customer makes the payment. Sales Invoice is an accounting transaction. On submission of Sales Invoice, the system updates the receivable and books income against a Customer Account.\n\nHere's the flow of how a sales invoice is generally created\n\n\n![Sales Flow](https://docs.erpnext.com/docs/assets/img/accounts/so-flow.png)",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 17:48:21.019019",
+ "modified": "2020-10-16 12:59:16.987507",
"modified_by": "Administrator",
"name": "Create Your First Sales Invoice",
"owner": "Administrator",
"reference_document": "Sales Invoice",
+ "show_form_tour": 0,
"show_full_form": 1,
"title": "Create Your First Sales Invoice ",
"validate_action": 1
diff --git a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json
index 8e00067..a492201 100644
--- a/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json
+++ b/erpnext/accounts/onboarding_step/setup_taxes/setup_taxes.json
@@ -1,18 +1,20 @@
{
"action": "Create Entry",
+ "action_label": "Make a Sales Tax Template",
"creation": "2020-05-13 19:29:43.844463",
+ "description": "# Setting up Taxes\n\nAny sophisticated accounting system, including ERPNext will have automatic tax calculations for your transactions. These calculations are based on user defined rules in compliance to local rules and regulations.\n\nERPNext allows this via *Tax Templates*. These templates can be used in Sales Orders and Sales Invoices. Other types of charges that may apply to your invoices (like shipping, insurance etc.) can also be configured as taxes.\n\nFor Tax Accounts that you want to use in the tax templates, go to:\n\n`> Accounting > Taxes > Sales Taxes and Charges Template`\n\nYou can read more about these templates in our documentation [here](https://docs.erpnext.com/docs/user/manual/en/selling/sales-taxes-and-charges-template)\n",
"docstatus": 0,
"doctype": "Onboarding Step",
"idx": 0,
"is_complete": 0,
- "is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 17:40:16.014413",
+ "modified": "2020-10-30 14:54:18.087383",
"modified_by": "Administrator",
"name": "Setup Taxes",
"owner": "Administrator",
"reference_document": "Sales Taxes and Charges Template",
+ "show_form_tour": 1,
"show_full_form": 1,
"title": "Lets create a Tax Template for Sales ",
"validate_action": 0
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
deleted file mode 100644
index 6ae81d7..0000000
--- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.js
+++ /dev/null
@@ -1,583 +0,0 @@
-frappe.provide("erpnext.accounts");
-
-frappe.pages['bank-reconciliation'].on_page_load = function(wrapper) {
- new erpnext.accounts.bankReconciliation(wrapper);
-}
-
-erpnext.accounts.bankReconciliation = class BankReconciliation {
- constructor(wrapper) {
- this.page = frappe.ui.make_app_page({
- parent: wrapper,
- title: __("Bank Reconciliation"),
- single_column: true
- });
- this.parent = wrapper;
- this.page = this.parent.page;
-
- this.check_plaid_status();
- this.make();
- }
-
- make() {
- const me = this;
-
- me.$main_section = $(`<div class="reconciliation page-main-content"></div>`).appendTo(me.page.main);
- const empty_state = __("Upload a bank statement, link or reconcile a bank account")
- me.$main_section.append(`<div class="flex justify-center align-center text-muted"
- style="height: 50vh; display: flex;"><h5 class="text-muted">${empty_state}</h5></div>`)
-
- me.page.add_field({
- fieldtype: 'Link',
- label: __('Company'),
- fieldname: 'company',
- options: "Company",
- onchange: function() {
- if (this.value) {
- me.company = this.value;
- } else {
- me.company = null;
- me.bank_account = null;
- }
- }
- })
- me.page.add_field({
- fieldtype: 'Link',
- label: __('Bank Account'),
- fieldname: 'bank_account',
- options: "Bank Account",
- get_query: function() {
- if(!me.company) {
- frappe.throw(__("Please select company first"));
- return
- }
-
- return {
- filters: {
- "company": me.company
- }
- }
- },
- onchange: function() {
- if (this.value) {
- me.bank_account = this.value;
- me.add_actions();
- } else {
- me.bank_account = null;
- me.page.hide_actions_menu();
- }
- }
- })
- }
-
- check_plaid_status() {
- const me = this;
- frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => {
- if (r && r.enabled === "1") {
- me.plaid_status = "active"
- } else {
- me.plaid_status = "inactive"
- }
- })
- }
-
- add_actions() {
- const me = this;
-
- me.page.show_menu()
-
- me.page.add_menu_item(__("Upload a statement"), function() {
- me.clear_page_content();
- new erpnext.accounts.bankTransactionUpload(me);
- }, true)
-
- if (me.plaid_status==="active") {
- me.page.add_menu_item(__("Synchronize this account"), function() {
- me.clear_page_content();
- new erpnext.accounts.bankTransactionSync(me);
- }, true)
- }
-
- me.page.add_menu_item(__("Reconcile this account"), function() {
- me.clear_page_content();
- me.make_reconciliation_tool();
- }, true)
- }
-
- clear_page_content() {
- const me = this;
- $(me.page.body).find('.frappe-list').remove();
- me.$main_section.empty();
- }
-
- make_reconciliation_tool() {
- const me = this;
- frappe.model.with_doctype("Bank Transaction", () => {
- erpnext.accounts.ReconciliationList = new erpnext.accounts.ReconciliationTool({
- parent: me.parent,
- doctype: "Bank Transaction"
- });
- })
- }
-}
-
-
-erpnext.accounts.bankTransactionUpload = class bankTransactionUpload {
- constructor(parent) {
- this.parent = parent;
- this.data = [];
-
- const assets = [
- "/assets/frappe/css/frappe-datatable.css",
- "/assets/frappe/js/lib/clusterize.min.js",
- "/assets/frappe/js/lib/Sortable.min.js",
- "/assets/frappe/js/lib/frappe-datatable.js"
- ];
-
- frappe.require(assets, () => {
- this.make();
- });
- }
-
- make() {
- const me = this;
- new frappe.ui.FileUploader({
- method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement',
- allow_multiple: 0,
- on_success: function(attachment, r) {
- if (!r.exc && r.message) {
- me.data = r.message;
- me.setup_transactions_dom();
- me.create_datatable();
- me.add_primary_action();
- }
- }
- })
- }
-
- setup_transactions_dom() {
- const me = this;
- me.parent.$main_section.append('<div class="transactions-table"></div>');
- }
-
- create_datatable() {
- try {
- this.datatable = new DataTable('.transactions-table', {
- columns: this.data.columns,
- data: this.data.data
- })
- }
- catch(err) {
- let msg = __("Your file could not be processed. It should be a standard CSV or XLSX file with headers in the first row.");
- frappe.throw(msg)
- }
-
- }
-
- add_primary_action() {
- const me = this;
- me.parent.page.set_primary_action(__("Submit"), function() {
- me.add_bank_entries()
- }, null, __("Creating bank entries..."))
- }
-
- add_bank_entries() {
- const me = this;
- frappe.xcall('erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.create_bank_entries',
- {columns: this.datatable.datamanager.columns, data: this.datatable.datamanager.data, bank_account: me.parent.bank_account}
- ).then((result) => {
- let result_title = result.errors == 0 ? __("{0} bank transaction(s) created", [result.success]) : __("{0} bank transaction(s) created and {1} errors", [result.success, result.errors])
- let result_msg = `
- <div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
- <h5 class="text-muted">${result_title}</h5>
- </div>`
- me.parent.page.clear_primary_action();
- me.parent.$main_section.empty();
- me.parent.$main_section.append(result_msg);
- if (result.errors == 0) {
- frappe.show_alert({message:__("All bank transactions have been created"), indicator:'green'});
- } else {
- frappe.show_alert({message:__("Please check the error log for details about the import errors"), indicator:'red'});
- }
- })
- }
-}
-
-erpnext.accounts.bankTransactionSync = class bankTransactionSync {
- constructor(parent) {
- this.parent = parent;
- this.data = [];
-
- this.init_config()
- }
-
- init_config() {
- const me = this;
- frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.get_plaid_configuration')
- .then(result => {
- me.plaid_env = result.plaid_env;
- me.client_name = result.client_name;
- me.link_token = result.link_token;
- me.sync_transactions();
- })
- }
-
- sync_transactions() {
- const me = this;
- frappe.db.get_value("Bank Account", me.parent.bank_account, "bank", (r) => {
- frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions', {
- bank: r.bank,
- bank_account: me.parent.bank_account,
- freeze: true
- })
- .then((result) => {
- let result_title = (result && result.length > 0)
- ? __("{0} bank transaction(s) created", [result.length])
- : __("This bank account is already synchronized");
-
- let result_msg = `
- <div class="flex justify-center align-center text-muted" style="height: 50vh; display: flex;">
- <h5 class="text-muted">${result_title}</h5>
- </div>`
-
- this.parent.$main_section.append(result_msg)
- frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' });
- })
- })
- }
-}
-
-
-erpnext.accounts.ReconciliationTool = class ReconciliationTool extends frappe.views.BaseList {
- constructor(opts) {
- super(opts);
- this.show();
- }
-
- setup_defaults() {
- super.setup_defaults();
-
- this.page_title = __("Bank Reconciliation");
- this.doctype = 'Bank Transaction';
- this.fields = ['date', 'description', 'debit', 'credit', 'currency']
-
- }
-
- setup_view() {
- this.render_header();
- }
-
- setup_side_bar() {
- //
- }
-
- make_standard_filters() {
- //
- }
-
- freeze() {
- this.$result.find('.list-count').html(`<span>${__('Refreshing')}...</span>`);
- }
-
- get_args() {
- const args = super.get_args();
-
- return Object.assign({}, args, {
- ...args.filters.push(["Bank Transaction", "docstatus", "=", 1],
- ["Bank Transaction", "unallocated_amount", ">", 0])
- });
-
- }
-
- update_data(r) {
- let data = r.message || [];
-
- if (this.start === 0) {
- this.data = data;
- } else {
- this.data = this.data.concat(data);
- }
- }
-
- render() {
- const me = this;
- this.$result.find('.list-row-container').remove();
- $('[data-fieldname="name"]').remove();
- me.data.map((value) => {
- const row = $('<div class="list-row-container">').data("data", value).appendTo(me.$result).get(0);
- new erpnext.accounts.ReconciliationRow(row, value);
- })
- }
-
- render_header() {
- const me = this;
- if ($(this.wrapper).find('.transaction-header').length === 0) {
- me.$result.append(frappe.render_template("bank_transaction_header"));
- }
- }
-}
-
-erpnext.accounts.ReconciliationRow = class ReconciliationRow {
- constructor(row, data) {
- this.data = data;
- this.row = row;
- this.make();
- this.bind_events();
- }
-
- make() {
- $(this.row).append(frappe.render_template("bank_transaction_row", this.data))
- }
-
- bind_events() {
- const me = this;
- $(me.row).on('click', '.clickable-section', function() {
- me.bank_entry = $(this).attr("data-name");
- me.show_dialog($(this).attr("data-name"));
- })
-
- $(me.row).on('click', '.new-reconciliation', function() {
- me.bank_entry = $(this).attr("data-name");
- me.show_dialog($(this).attr("data-name"));
- })
-
- $(me.row).on('click', '.new-payment', function() {
- me.bank_entry = $(this).attr("data-name");
- me.new_payment();
- })
-
- $(me.row).on('click', '.new-invoice', function() {
- me.bank_entry = $(this).attr("data-name");
- me.new_invoice();
- })
-
- $(me.row).on('click', '.new-expense', function() {
- me.bank_entry = $(this).attr("data-name");
- me.new_expense();
- })
- }
-
- new_payment() {
- const me = this;
- const paid_amount = me.data.credit > 0 ? me.data.credit : me.data.debit;
- const payment_type = me.data.credit > 0 ? "Receive": "Pay";
- const party_type = me.data.credit > 0 ? "Customer": "Supplier";
-
- frappe.new_doc("Payment Entry", {"payment_type": payment_type, "paid_amount": paid_amount,
- "party_type": party_type, "paid_from": me.data.bank_account})
- }
-
- new_invoice() {
- const me = this;
- const invoice_type = me.data.credit > 0 ? "Sales Invoice" : "Purchase Invoice";
-
- frappe.new_doc(invoice_type)
- }
-
- new_expense() {
- frappe.new_doc("Expense Claim")
- }
-
-
- show_dialog(data) {
- const me = this;
-
- frappe.db.get_value("Bank Account", me.data.bank_account, "account", (r) => {
- me.gl_account = r.account;
- })
-
- frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments',
- { bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") }
- ).then((result) => {
- me.make_dialog(result)
- })
- }
-
- make_dialog(data) {
- const me = this;
- me.selected_payment = null;
-
- const fields = [
- {
- fieldtype: 'Section Break',
- fieldname: 'section_break_1',
- label: __('Automatic Reconciliation')
- },
- {
- fieldtype: 'HTML',
- fieldname: 'payment_proposals'
- },
- {
- fieldtype: 'Section Break',
- fieldname: 'section_break_2',
- label: __('Search for a payment')
- },
- {
- fieldtype: 'Link',
- fieldname: 'payment_doctype',
- options: 'DocType',
- label: 'Payment DocType',
- get_query: () => {
- return {
- filters : {
- "name": ["in", ["Payment Entry", "Journal Entry", "Sales Invoice", "Purchase Invoice", "Expense Claim"]]
- }
- }
- },
- },
- {
- fieldtype: 'Column Break',
- fieldname: 'column_break_1',
- },
- {
- fieldtype: 'Dynamic Link',
- fieldname: 'payment_entry',
- options: 'payment_doctype',
- label: 'Payment Document',
- get_query: () => {
- let dt = this.dialog.fields_dict.payment_doctype.value;
- if (dt === "Payment Entry") {
- return {
- query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.payment_entry_query",
- filters : {
- "bank_account": this.data.bank_account,
- "company": this.data.company
- }
- }
- } else if (dt === "Journal Entry") {
- return {
- query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.journal_entry_query",
- filters : {
- "bank_account": this.data.bank_account,
- "company": this.data.company
- }
- }
- } else if (dt === "Sales Invoice") {
- return {
- query: "erpnext.accounts.page.bank_reconciliation.bank_reconciliation.sales_invoices_query"
- }
- } else if (dt === "Purchase Invoice") {
- return {
- filters : [
- ["Purchase Invoice", "ifnull(clearance_date, '')", "=", ""],
- ["Purchase Invoice", "docstatus", "=", 1],
- ["Purchase Invoice", "company", "=", this.data.company]
- ]
- }
- } else if (dt === "Expense Claim") {
- return {
- filters : [
- ["Expense Claim", "ifnull(clearance_date, '')", "=", ""],
- ["Expense Claim", "docstatus", "=", 1],
- ["Expense Claim", "company", "=", this.data.company]
- ]
- }
- }
- },
- onchange: function() {
- if (me.selected_payment !== this.value) {
- me.selected_payment = this.value;
- me.display_payment_details(this);
- }
- }
- },
- {
- fieldtype: 'Section Break',
- fieldname: 'section_break_3'
- },
- {
- fieldtype: 'HTML',
- fieldname: 'payment_details'
- },
- ];
-
- me.dialog = new frappe.ui.Dialog({
- title: __("Choose a corresponding payment"),
- fields: fields,
- size: "large"
- });
-
- const proposals_wrapper = me.dialog.fields_dict.payment_proposals.$wrapper;
- if (data && data.length > 0) {
- proposals_wrapper.append(frappe.render_template("linked_payment_header"));
- data.map(value => {
- proposals_wrapper.append(frappe.render_template("linked_payment_row", value))
- })
- } else {
- const empty_data_msg = __("ERPNext could not find any matching payment entry")
- proposals_wrapper.append(`<div class="text-center"><h5 class="text-muted">${empty_data_msg}</h5></div>`)
- }
-
- $(me.dialog.body).on('click', '.reconciliation-btn', (e) => {
- const payment_entry = $(e.target).attr('data-name');
- const payment_doctype = $(e.target).attr('data-doctype');
- frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.reconcile',
- {bank_transaction: me.bank_entry, payment_doctype: payment_doctype, payment_name: payment_entry})
- .then((result) => {
- setTimeout(function(){
- erpnext.accounts.ReconciliationList.refresh();
- }, 2000);
- me.dialog.hide();
- })
- })
-
- me.dialog.show();
- }
-
- display_payment_details(event) {
- const me = this;
- if (event.value) {
- let dt = me.dialog.fields_dict.payment_doctype.value;
- me.dialog.fields_dict['payment_details'].$wrapper.empty();
- frappe.db.get_doc(dt, event.value)
- .then(doc => {
- let displayed_docs = []
- let payment = []
- if (dt === "Payment Entry") {
- payment.currency = doc.payment_type == "Receive" ? doc.paid_to_account_currency : doc.paid_from_account_currency;
- payment.doctype = dt
- payment.posting_date = doc.posting_date;
- payment.party = doc.party;
- payment.reference_no = doc.reference_no;
- payment.reference_date = doc.reference_date;
- payment.paid_amount = doc.paid_amount;
- payment.name = doc.name;
- displayed_docs.push(payment);
- } else if (dt === "Journal Entry") {
- doc.accounts.forEach(payment => {
- if (payment.account === me.gl_account) {
- payment.doctype = dt;
- payment.posting_date = doc.posting_date;
- payment.party = doc.pay_to_recd_from;
- payment.reference_no = doc.cheque_no;
- payment.reference_date = doc.cheque_date;
- payment.currency = payment.account_currency;
- payment.paid_amount = payment.credit > 0 ? payment.credit : payment.debit;
- payment.name = doc.name;
- displayed_docs.push(payment);
- }
- })
- } else if (dt === "Sales Invoice") {
- doc.payments.forEach(payment => {
- if (payment.clearance_date === null || payment.clearance_date === "") {
- payment.doctype = dt;
- payment.posting_date = doc.posting_date;
- payment.party = doc.customer;
- payment.reference_no = doc.remarks;
- payment.currency = doc.currency;
- payment.paid_amount = payment.amount;
- payment.name = doc.name;
- displayed_docs.push(payment);
- }
- })
- }
-
- const details_wrapper = me.dialog.fields_dict.payment_details.$wrapper;
- details_wrapper.append(frappe.render_template("linked_payment_header"));
- displayed_docs.forEach(payment => {
- details_wrapper.append(frappe.render_template("linked_payment_row", payment));
- })
- })
- }
-
- }
-}
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.json b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.json
deleted file mode 100644
index feea368..0000000
--- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "content": null,
- "creation": "2018-11-24 12:03:14.646669",
- "docstatus": 0,
- "doctype": "Page",
- "idx": 0,
- "modified": "2018-11-24 12:03:14.646669",
- "modified_by": "Administrator",
- "module": "Accounts",
- "name": "bank-reconciliation",
- "owner": "Administrator",
- "page_name": "bank-reconciliation",
- "roles": [
- {
- "role": "System Manager"
- },
- {
- "role": "Accounts Manager"
- },
- {
- "role": "Accounts User"
- }
- ],
- "script": null,
- "standard": "Yes",
- "style": null,
- "system_page": 0,
- "title": "Bank Reconciliation"
-}
\ No newline at end of file
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py b/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py
deleted file mode 100644
index 8abe20c..0000000
--- a/erpnext/accounts/page/bank_reconciliation/bank_reconciliation.py
+++ /dev/null
@@ -1,369 +0,0 @@
-# -*- coding: utf-8 -*-
-# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
-# For license information, please see license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-import difflib
-from frappe.utils import flt
-from six import iteritems
-from erpnext import get_company_currency
-
-@frappe.whitelist()
-def reconcile(bank_transaction, payment_doctype, payment_name):
- transaction = frappe.get_doc("Bank Transaction", bank_transaction)
- payment_entry = frappe.get_doc(payment_doctype, payment_name)
-
- account = frappe.db.get_value("Bank Account", transaction.bank_account, "account")
- gl_entry = frappe.get_doc("GL Entry", dict(account=account, voucher_type=payment_doctype, voucher_no=payment_name))
-
- if payment_doctype == "Payment Entry" and payment_entry.unallocated_amount > transaction.unallocated_amount:
- frappe.throw(_("The unallocated amount of Payment Entry {0} is greater than the Bank Transaction's unallocated amount").format(payment_name))
-
- if transaction.unallocated_amount == 0:
- frappe.throw(_("This bank transaction is already fully reconciled"))
-
- if transaction.credit > 0 and gl_entry.credit > 0:
- frappe.throw(_("The selected payment entry should be linked with a debtor bank transaction"))
-
- if transaction.debit > 0 and gl_entry.debit > 0:
- frappe.throw(_("The selected payment entry should be linked with a creditor bank transaction"))
-
- add_payment_to_transaction(transaction, payment_entry, gl_entry)
-
- return 'reconciled'
-
-def add_payment_to_transaction(transaction, payment_entry, gl_entry):
- gl_amount, transaction_amount = (gl_entry.credit, transaction.debit) if gl_entry.credit > 0 else (gl_entry.debit, transaction.credit)
- allocated_amount = gl_amount if gl_amount <= transaction_amount else transaction_amount
- transaction.append("payment_entries", {
- "payment_document": payment_entry.doctype,
- "payment_entry": payment_entry.name,
- "allocated_amount": allocated_amount
- })
-
- transaction.save()
- transaction.update_allocations()
-
-@frappe.whitelist()
-def get_linked_payments(bank_transaction):
- transaction = frappe.get_doc("Bank Transaction", bank_transaction)
- bank_account = frappe.db.get_values("Bank Account", transaction.bank_account, ["account", "company"], as_dict=True)
-
- # Get all payment entries with a matching amount
- amount_matching = check_matching_amount(bank_account[0].account, bank_account[0].company, transaction)
-
- # Get some data from payment entries linked to a corresponding bank transaction
- description_matching = get_matching_descriptions_data(bank_account[0].company, transaction)
-
- if amount_matching:
- return check_amount_vs_description(amount_matching, description_matching)
-
- elif description_matching:
- description_matching = filter(lambda x: not x.get('clearance_date'), description_matching)
- if not description_matching:
- return []
-
- return sorted(list(description_matching), key = lambda x: x["posting_date"], reverse=True)
-
- else:
- return []
-
-def check_matching_amount(bank_account, company, transaction):
- payments = []
- amount = transaction.credit if transaction.credit > 0 else transaction.debit
-
- payment_type = "Receive" if transaction.credit > 0 else "Pay"
- account_from_to = "paid_to" if transaction.credit > 0 else "paid_from"
- currency_field = "paid_to_account_currency as currency" if transaction.credit > 0 else "paid_from_account_currency as currency"
-
- payment_entries = frappe.get_all("Payment Entry", fields=["'Payment Entry' as doctype", "name", "paid_amount", "payment_type", "reference_no", "reference_date",
- "party", "party_type", "posting_date", "{0}".format(currency_field)], filters=[["paid_amount", "like", "{0}%".format(amount)],
- ["docstatus", "=", "1"], ["payment_type", "=", [payment_type, "Internal Transfer"]], ["ifnull(clearance_date, '')", "=", ""], ["{0}".format(account_from_to), "=", "{0}".format(bank_account)]])
-
- jea_side = "debit" if transaction.credit > 0 else "credit"
- journal_entries = frappe.db.sql(f"""
- SELECT
- 'Journal Entry' as doctype, je.name, je.posting_date, je.cheque_no as reference_no,
- jea.account_currency as currency, je.pay_to_recd_from as party, je.cheque_date as reference_date,
- jea.{jea_side}_in_account_currency as paid_amount
- FROM
- `tabJournal Entry Account` as jea
- JOIN
- `tabJournal Entry` as je
- ON
- jea.parent = je.name
- WHERE
- (je.clearance_date is null or je.clearance_date='0000-00-00')
- AND
- jea.account = %(bank_account)s
- AND
- jea.{jea_side}_in_account_currency like %(txt)s
- AND
- je.docstatus = 1
- """, {
- 'bank_account': bank_account,
- 'txt': '%%%s%%' % amount
- }, as_dict=True)
-
- if transaction.credit > 0:
- sales_invoices = frappe.db.sql("""
- SELECT
- 'Sales Invoice' as doctype, si.name, si.customer as party,
- si.posting_date, sip.amount as paid_amount
- FROM
- `tabSales Invoice Payment` as sip
- JOIN
- `tabSales Invoice` as si
- ON
- sip.parent = si.name
- WHERE
- (sip.clearance_date is null or sip.clearance_date='0000-00-00')
- AND
- sip.account = %s
- AND
- sip.amount like %s
- AND
- si.docstatus = 1
- """, (bank_account, amount), as_dict=True)
- else:
- sales_invoices = []
-
- if transaction.debit > 0:
- purchase_invoices = frappe.get_all("Purchase Invoice",
- fields = ["'Purchase Invoice' as doctype", "name", "paid_amount", "supplier as party", "posting_date", "currency"],
- filters=[
- ["paid_amount", "like", "{0}%".format(amount)],
- ["docstatus", "=", "1"],
- ["is_paid", "=", "1"],
- ["ifnull(clearance_date, '')", "=", ""],
- ["cash_bank_account", "=", "{0}".format(bank_account)]
- ]
- )
-
- mode_of_payments = [x["parent"] for x in frappe.db.get_list("Mode of Payment Account",
- filters={"default_account": bank_account}, fields=["parent"])]
-
- company_currency = get_company_currency(company)
-
- expense_claims = frappe.get_all("Expense Claim",
- fields=["'Expense Claim' as doctype", "name", "total_sanctioned_amount as paid_amount",
- "employee as party", "posting_date", "'{0}' as currency".format(company_currency)],
- filters=[
- ["total_sanctioned_amount", "like", "{0}%".format(amount)],
- ["docstatus", "=", "1"],
- ["is_paid", "=", "1"],
- ["ifnull(clearance_date, '')", "=", ""],
- ["mode_of_payment", "in", "{0}".format(tuple(mode_of_payments))]
- ]
- )
- else:
- purchase_invoices = expense_claims = []
-
- for data in [payment_entries, journal_entries, sales_invoices, purchase_invoices, expense_claims]:
- if data:
- payments.extend(data)
-
- return payments
-
-def get_matching_descriptions_data(company, transaction):
- if not transaction.description :
- return []
-
- bank_transactions = frappe.db.sql("""
- SELECT
- bt.name, bt.description, bt.date, btp.payment_document, btp.payment_entry
- FROM
- `tabBank Transaction` as bt
- LEFT JOIN
- `tabBank Transaction Payments` as btp
- ON
- bt.name = btp.parent
- WHERE
- bt.allocated_amount > 0
- AND
- bt.docstatus = 1
- """, as_dict=True)
-
- selection = []
- for bank_transaction in bank_transactions:
- if bank_transaction.description:
- seq=difflib.SequenceMatcher(lambda x: x == " ", transaction.description, bank_transaction.description)
-
- if seq.ratio() > 0.6:
- bank_transaction["ratio"] = seq.ratio()
- selection.append(bank_transaction)
-
- document_types = set([x["payment_document"] for x in selection])
-
- links = {}
- for document_type in document_types:
- links[document_type] = [x["payment_entry"] for x in selection if x["payment_document"]==document_type]
-
-
- data = []
- company_currency = get_company_currency(company)
- for key, value in iteritems(links):
- if key == "Payment Entry":
- data.extend(frappe.get_all("Payment Entry", filters=[["name", "in", value]],
- fields=["'Payment Entry' as doctype", "posting_date", "party", "reference_no",
- "reference_date", "paid_amount", "paid_to_account_currency as currency", "clearance_date"]))
- if key == "Journal Entry":
- journal_entries = frappe.get_all("Journal Entry", filters=[["name", "in", value]],
- fields=["name", "'Journal Entry' as doctype", "posting_date",
- "pay_to_recd_from as party", "cheque_no as reference_no", "cheque_date as reference_date",
- "total_credit as paid_amount", "clearance_date"])
- for journal_entry in journal_entries:
- journal_entry_accounts = frappe.get_all("Journal Entry Account", filters={"parenttype": journal_entry["doctype"], "parent": journal_entry["name"]}, fields=["account_currency"])
- journal_entry["currency"] = journal_entry_accounts[0]["account_currency"] if journal_entry_accounts else company_currency
- data.extend(journal_entries)
- if key == "Sales Invoice":
- data.extend(frappe.get_all("Sales Invoice", filters=[["name", "in", value]], fields=["'Sales Invoice' as doctype", "posting_date", "customer_name as party", "paid_amount", "currency"]))
- if key == "Purchase Invoice":
- data.extend(frappe.get_all("Purchase Invoice", filters=[["name", "in", value]], fields=["'Purchase Invoice' as doctype", "posting_date", "supplier_name as party", "paid_amount", "currency"]))
- if key == "Expense Claim":
- expense_claims = frappe.get_all("Expense Claim", filters=[["name", "in", value]], fields=["'Expense Claim' as doctype", "posting_date", "employee_name as party", "total_amount_reimbursed as paid_amount"])
- data.extend([dict(x,**{"currency": company_currency}) for x in expense_claims])
-
- return data
-
-def check_amount_vs_description(amount_matching, description_matching):
- result = []
-
- if description_matching:
- for am_match in amount_matching:
- for des_match in description_matching:
- if des_match.get("clearance_date"):
- continue
-
- if am_match["party"] == des_match["party"]:
- if am_match not in result:
- result.append(am_match)
- continue
-
- if "reference_no" in am_match and "reference_no" in des_match:
- # Sequence Matcher does not handle None as input
- am_reference = am_match["reference_no"] or ""
- des_reference = des_match["reference_no"] or ""
-
- if difflib.SequenceMatcher(lambda x: x == " ", am_reference, des_reference).ratio() > 70:
- if am_match not in result:
- result.append(am_match)
- if result:
- return sorted(result, key = lambda x: x["posting_date"], reverse=True)
- else:
- return sorted(amount_matching, key = lambda x: x["posting_date"], reverse=True)
-
- else:
- return sorted(amount_matching, key = lambda x: x["posting_date"], reverse=True)
-
-def get_matching_transactions_payments(description_matching):
- payments = [x["payment_entry"] for x in description_matching]
-
- payment_by_ratio = {x["payment_entry"]: x["ratio"] for x in description_matching}
-
- if payments:
- reference_payment_list = frappe.get_all("Payment Entry", fields=["name", "paid_amount", "payment_type", "reference_no", "reference_date",
- "party", "party_type", "posting_date", "paid_to_account_currency"], filters=[["name", "in", payments]])
-
- return sorted(reference_payment_list, key=lambda x: payment_by_ratio[x["name"]])
-
- else:
- return []
-
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def payment_entry_query(doctype, txt, searchfield, start, page_len, filters):
- account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account")
- if not account:
- return
-
- return frappe.db.sql("""
- SELECT
- name, party, paid_amount, received_amount, reference_no
- FROM
- `tabPayment Entry`
- WHERE
- (clearance_date is null or clearance_date='0000-00-00')
- AND (paid_from = %(account)s or paid_to = %(account)s)
- AND (name like %(txt)s or party like %(txt)s)
- AND docstatus = 1
- ORDER BY
- if(locate(%(_txt)s, name), locate(%(_txt)s, name), 99999), name
- LIMIT
- %(start)s, %(page_len)s""",
- {
- 'txt': "%%%s%%" % txt,
- '_txt': txt.replace("%", ""),
- 'start': start,
- 'page_len': page_len,
- 'account': account
- }
- )
-
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def journal_entry_query(doctype, txt, searchfield, start, page_len, filters):
- account = frappe.db.get_value("Bank Account", filters.get("bank_account"), "account")
-
- return frappe.db.sql("""
- SELECT
- jea.parent, je.pay_to_recd_from,
- if(jea.debit_in_account_currency > 0, jea.debit_in_account_currency, jea.credit_in_account_currency)
- FROM
- `tabJournal Entry Account` as jea
- LEFT JOIN
- `tabJournal Entry` as je
- ON
- jea.parent = je.name
- WHERE
- (je.clearance_date is null or je.clearance_date='0000-00-00')
- AND
- jea.account = %(account)s
- AND
- (jea.parent like %(txt)s or je.pay_to_recd_from like %(txt)s)
- AND
- je.docstatus = 1
- ORDER BY
- if(locate(%(_txt)s, jea.parent), locate(%(_txt)s, jea.parent), 99999),
- jea.parent
- LIMIT
- %(start)s, %(page_len)s""",
- {
- 'txt': "%%%s%%" % txt,
- '_txt': txt.replace("%", ""),
- 'start': start,
- 'page_len': page_len,
- 'account': account
- }
- )
-
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def sales_invoices_query(doctype, txt, searchfield, start, page_len, filters):
- return frappe.db.sql("""
- SELECT
- sip.parent, si.customer, sip.amount, sip.mode_of_payment
- FROM
- `tabSales Invoice Payment` as sip
- LEFT JOIN
- `tabSales Invoice` as si
- ON
- sip.parent = si.name
- WHERE
- (sip.clearance_date is null or sip.clearance_date='0000-00-00')
- AND
- (sip.parent like %(txt)s or si.customer like %(txt)s)
- ORDER BY
- if(locate(%(_txt)s, sip.parent), locate(%(_txt)s, sip.parent), 99999),
- sip.parent
- LIMIT
- %(start)s, %(page_len)s""",
- {
- 'txt': "%%%s%%" % txt,
- '_txt': txt.replace("%", ""),
- 'start': start,
- 'page_len': page_len
- }
- )
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_transaction_header.html b/erpnext/accounts/page/bank_reconciliation/bank_transaction_header.html
deleted file mode 100644
index 94f183b..0000000
--- a/erpnext/accounts/page/bank_reconciliation/bank_transaction_header.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<div class="transaction-header">
- <div class="level list-row list-row-head text-muted small">
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ __("Date") }}
- </div>
- <div class="col-xs-11 col-sm-4 ellipsis list-subject">
- {{ __("Description") }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ __("Debit") }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ __("Credit") }}
- </div>
- <div class="col-sm-1 ellipsis hidden-xs">
- {{ __("Currency") }}
- </div>
- <div class="col-sm-1 ellipsis">
- </div>
- </div>
-</div>
diff --git a/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html b/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html
deleted file mode 100644
index 742b84c..0000000
--- a/erpnext/accounts/page/bank_reconciliation/bank_transaction_row.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<div class="list-row transaction-item">
- <div>
- <div class="clickable-section" data-name={{ name }}>
- <div class="col-sm-2 ellipsis hidden-xs">
- {%= frappe.datetime.str_to_user(date) %}
- </div>
- <div class="col-xs-8 col-sm-4 ellipsis list-subject">
- {{ description }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {%= format_currency(debit, currency) %}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {%= format_currency(credit, currency) %}
- </div>
- <div class="col-sm-1 ellipsis hidden-xs">
- {{ currency }}
- </div>
- </div>
- <div class="col-xs-3 col-sm-1">
- <div class="btn-group">
- <a class="dropdown-toggle btn btn-default btn-xs" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
- <span>Actions </span>
- <span class="caret"></span>
- </a>
- <ul class="dropdown-menu reports-dropdown" style="max-height: 300px; overflow-y: auto; right: 0px; left: auto;">
- <li><a class="new-reconciliation" data-name={{ name }}>{{ __("Reconcile") }}</a></li>
- <li class="divider"></li>
- <li><a class="new-payment" data-name={{ name }}>{{ __("New Payment") }}</a></li>
- <li><a class="new-invoice" data-name={{ name }}>{{ __("New Invoice") }}</a></li>
- <li><a class="new-expense" data-name={{ name }}>{{ __("New Expense") }}</a></li>
- </ul>
- </div>
- </div>
- </div>
-</div>
diff --git a/erpnext/accounts/page/bank_reconciliation/linked_payment_header.html b/erpnext/accounts/page/bank_reconciliation/linked_payment_header.html
deleted file mode 100644
index 4542c36..0000000
--- a/erpnext/accounts/page/bank_reconciliation/linked_payment_header.html
+++ /dev/null
@@ -1,21 +0,0 @@
-<div class="transaction-header">
- <div class="level list-row list-row-head text-muted small">
- <div class="col-xs-3 col-sm-2 ellipsis">
- {{ __("Payment Name") }}
- </div>
- <div class="col-xs-3 col-sm-2 ellipsis">
- {{ __("Reference Date") }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ __("Amount") }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ __("Party") }}
- </div>
- <div class="col-xs-3 col-sm-2 ellipsis">
- {{ __("Reference Number") }}
- </div>
- <div class="col-xs-2 col-sm-2">
- </div>
- </div>
-</div>
diff --git a/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html b/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html
deleted file mode 100644
index bdbc9fc..0000000
--- a/erpnext/accounts/page/bank_reconciliation/linked_payment_row.html
+++ /dev/null
@@ -1,36 +0,0 @@
-<div class="list-row">
- <div>
- <div class="col-xs-3 col-sm-2 ellipsis">
- {{ name }}
- </div>
- <div class="col-xs-3 col-sm-2 ellipsis">
- {% if (typeof reference_date !== "undefined") %}
- {%= frappe.datetime.str_to_user(reference_date) %}
- {% else %}
- {% if (typeof posting_date !== "undefined") %}
- {%= frappe.datetime.str_to_user(posting_date) %}
- {% endif %}
- {% endif %}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {{ format_currency(paid_amount, currency) }}
- </div>
- <div class="col-sm-2 ellipsis hidden-xs">
- {% if (typeof party !== "undefined") %}
- {{ party }}
- {% endif %}
- </div>
- <div class="col-xs-3 col-sm-2 ellipsis">
- {% if (typeof reference_no !== "undefined") %}
- {{ reference_no }}
- {% else %}
- {{ "" }}
- {% endif %}
- </div>
- <div class="col-xs-2 col-sm-2">
- <div class="text-right margin-bottom">
- <button class="btn btn-primary btn-xs reconciliation-btn" data-doctype="{{ doctype }}" data-name="{{ name }}">{{ __("Reconcile") }}</button>
- </div>
- </div>
- </div>
-</div>
\ No newline at end of file
diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py
index 64268b8..38b2284 100644
--- a/erpnext/accounts/party.py
+++ b/erpnext/accounts/party.py
@@ -39,7 +39,7 @@
party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype))
party = party_details[party_type.lower()]
- if not ignore_permissions and not frappe.has_permission(party_type, "read", party):
+ if not ignore_permissions and not (frappe.has_permission(party_type, "read", party) or frappe.has_permission(party_type, "select", party)):
frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError)
party = frappe.get_doc(party_type, party)
diff --git a/erpnext/accounts/page/bank_reconciliation/__init__.py b/erpnext/accounts/print_format/gst_e_invoice/__init__.py
similarity index 100%
rename from erpnext/accounts/page/bank_reconciliation/__init__.py
rename to erpnext/accounts/print_format/gst_e_invoice/__init__.py
diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
new file mode 100644
index 0000000..8eef2ad
--- /dev/null
+++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.html
@@ -0,0 +1,162 @@
+{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value -%}
+{%- set einvoice = json.loads(doc.signed_einvoice) -%}
+
+<div class="page-break">
+ <div {% if print_settings.repeat_header_footer %} id="header-html" class="hidden-pdf" {% endif %}>
+ {% if letter_head and not no_letterhead %}
+ <div class="letter-head">{{ letter_head }}</div>
+ {% endif %}
+ <div class="print-heading">
+ <h2>E Invoice<br><small>{{ doc.name }}</small></h2>
+ </div>
+ </div>
+ {% if print_settings.repeat_header_footer %}
+ <div id="footer-html" class="visible-pdf">
+ {% if not no_letterhead and footer %}
+ <div class="letter-head-footer">
+ {{ footer }}
+ </div>
+ {% endif %}
+ <p class="text-center small page-number visible-pdf">
+ {{ _("Page {0} of {1}").format('<span class="page"></span>', '<span class="topage"></span>') }}
+ </p>
+ </div>
+ {% endif %}
+ <div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
+ <h5 class="font-bold" style="margin-left: 15px; margin-top: 0px;">1. Transaction Details</h5>
+ <div class="col-xs-8 column-break">
+ <div class="row data-field">
+ <div class="col-xs-4"><label>IRN</label></div>
+ <div class="col-xs-8 value">{{ einvoice.Irn }}</div>
+ </div>
+ <div class="row data-field">
+ <div class="col-xs-4"><label>Ack. No</label></div>
+ <div class="col-xs-8 value">{{ einvoice.AckNo }}</div>
+ </div>
+ <div class="row data-field">
+ <div class="col-xs-4"><label>Ack. Date</label></div>
+ <div class="col-xs-8 value">{{ frappe.utils.format_datetime(einvoice.AckDt, "dd/MM/yyyy hh:mm:ss") }}</div>
+ </div>
+ <div class="row data-field">
+ <div class="col-xs-4"><label>Category</label></div>
+ <div class="col-xs-8 value">{{ einvoice.TranDtls.SupTyp }}</div>
+ </div>
+ <div class="row data-field">
+ <div class="col-xs-4"><label>Document Type</label></div>
+ <div class="col-xs-8 value">{{ einvoice.DocDtls.Typ }}</div>
+ </div>
+ <div class="row data-field">
+ <div class="col-xs-4"><label>Document No</label></div>
+ <div class="col-xs-8 value">{{ einvoice.DocDtls.No }}</div>
+ </div>
+ </div>
+ <div class="col-xs-4 column-break">
+ <img src="{{ doc.qrcode_image }}" width="175px" style="float: right;">
+ </div>
+ </div>
+ <div class="row section-break" style="border-bottom: 1px solid #d1d8dd; padding-bottom: 10px;">
+ <h5 class="font-bold" style="margin-left: 15px; margin-bottom: 0px;">2. Party Details</h5>
+ {%- set seller = einvoice.SellerDtls -%}
+ <div class="col-xs-6 column-break">
+ <h5 style="margin-bottom: 5px;">Seller</h5>
+ <p>{{ seller.Gstin }}</p>
+ <p>{{ seller.LglNm }}</p>
+ <p>{{ seller.Addr1 }}</p>
+ {%- if seller.Addr2 -%} <p>{{ seller.Addr2 }}</p> {% endif %}
+ <p>{{ seller.Loc }}</p>
+ <p>{{ frappe.db.get_value("Address", doc.company_address, "gst_state") }} - {{ seller.Pin }}</p>
+
+ {%- if einvoice.ShipDtls -%}
+ {%- set shipping = einvoice.ShipDtls -%}
+ <h5 style="margin-bottom: 5px;">Shipping</h5>
+ <p>{{ shipping.Gstin }}</p>
+ <p>{{ shipping.LglNm }}</p>
+ <p>{{ shipping.Addr1 }}</p>
+ {%- if shipping.Addr2 -%} <p>{{ shipping.Addr2 }}</p> {% endif %}
+ <p>{{ shipping.Loc }}</p>
+ <p>{{ frappe.db.get_value("Address", doc.shipping_address_name, "gst_state") }} - {{ shipping.Pin }}</p>
+ {% endif %}
+ </div>
+ {%- set buyer = einvoice.BuyerDtls -%}
+ <div class="col-xs-6 column-break">
+ <h5 style="margin-bottom: 5px;">Buyer</h5>
+ <p>{{ buyer.Gstin }}</p>
+ <p>{{ buyer.LglNm }}</p>
+ <p>{{ buyer.Addr1 }}</p>
+ {%- if buyer.Addr2 -%} <p>{{ buyer.Addr2 }}</p> {% endif %}
+ <p>{{ buyer.Loc }}</p>
+ <p>{{ frappe.db.get_value("Address", doc.customer_address, "gst_state") }} - {{ buyer.Pin }}</p>
+ </div>
+ </div>
+ <div style="overflow-x: auto;">
+ <h5 class="font-bold" style="margin-bottom: 0px;">3. Item Details</h5>
+ <table class="table table-bordered">
+ <thead>
+ <tr>
+ <th class="text-left" style="width: 3%;">Sr. No.</th>
+ <th class="text-left">Item</th>
+ <th class="text-left" style="width: 10%;">HSN Code</th>
+ <th class="text-left" style="width: 5%;">Qty</th>
+ <th class="text-left" style="width: 5%;">UOM</th>
+ <th class="text-left">Rate</th>
+ <th class="text-left" style="width: 5%;">Discount</th>
+ <th class="text-left">Taxable Amount</th>
+ <th class="text-left" style="width: 7%;">Tax Rate</th>
+ <th class="text-left" style="width: 5%;">Other Charges</th>
+ <th class="text-left">Total</th>
+ </tr>
+ </thead>
+ <tbody>
+ {% for item in einvoice.ItemList %}
+ <tr>
+ <td class="text-left" style="width: 3%;">{{ item.SlNo }}</td>
+ <td class="text-left">{{ item.PrdDesc }}</td>
+ <td class="text-left" style="width: 10%;">{{ item.HsnCd }}</td>
+ <td class="text-right" style="width: 5%;">{{ item.Qty }}</td>
+ <td class="text-left" style="width: 5%;">{{ item.Unit }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(item.UnitPrice, None, "INR") }}</td>
+ <td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(item.Discount, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(item.AssAmt, None, "INR") }}</td>
+ <td class="text-right" style="width: 7%;">{{ item.GstRt + item.CesRt }} %</td>
+ <td class="text-right" style="width: 5%;">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(item.TotItemVal, None, "INR") }}</td>
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+ <div style="overflow-x: auto;">
+ <h5 class="font-bold" style="margin-bottom: 0px;">4. Value Details</h5>
+ <table class="table table-bordered">
+ <thead>
+ <tr>
+ <th class="text-left">Taxable Amount</th>
+ <th class="text-left">CGST</th>
+ <th class="text-left"">SGST</th>
+ <th class="text-left">IGST</th>
+ <th class="text-left">CESS</th>
+ <th class="text-left" style="width: 10%;">State CESS</th>
+ <th class="text-left">Discount</th>
+ <th class="text-left" style="width: 10%;">Other Charges</th>
+ <th class="text-left" style="width: 10%;">Round Off</th>
+ <th class="text-left">Total Value</th>
+ </tr>
+ </thead>
+ <tbody>
+ {%- set value_details = einvoice.ValDtls -%}
+ <tr>
+ <td class="text-right">{{ frappe.utils.fmt_money(value_details.AssVal, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(value_details.CgstVal, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(value_details.SgstVal, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(value_details.IgstVal, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(value_details.CesVal, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(0, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(value_details.Discount, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(value_details.OthChrg, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(value_details.RndOffAmt, None, "INR") }}</td>
+ <td class="text-right">{{ frappe.utils.fmt_money(value_details.TotInvVal, None, "INR") }}</td>
+ </tr>
+ </tbody>
+ </table>
+ </div>
+</div>
\ No newline at end of file
diff --git a/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json
new file mode 100644
index 0000000..1001199
--- /dev/null
+++ b/erpnext/accounts/print_format/gst_e_invoice/gst_e_invoice.json
@@ -0,0 +1,24 @@
+{
+ "align_labels_right": 1,
+ "creation": "2020-10-10 18:01:21.032914",
+ "custom_format": 0,
+ "default_print_language": "en-US",
+ "disabled": 1,
+ "doc_type": "Sales Invoice",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "html": "",
+ "idx": 0,
+ "line_breaks": 1,
+ "modified": "2020-10-23 19:54:40.634936",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "GST E-Invoice",
+ "owner": "Administrator",
+ "print_format_builder": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 1,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/print_format/sales_invoice_return/sales_invoice_return.html b/erpnext/accounts/print_format/sales_invoice_return/sales_invoice_return.html
index 1d758e8..3d5a9b1 100644
--- a/erpnext/accounts/print_format/sales_invoice_return/sales_invoice_return.html
+++ b/erpnext/accounts/print_format/sales_invoice_return/sales_invoice_return.html
@@ -1,5 +1,5 @@
{%- from "templates/print_formats/standard_macros.html" import add_header, render_field, print_value, fieldmeta,
- get_width, get_align_class -%}
+ get_width, get_align_class with context -%}
{%- macro render_currency(df, doc) -%}
<div class="row {% if df.bold %}important{% endif %} data-field">
@@ -63,14 +63,19 @@
<tr>
<td class="table-sr">{{ d.idx }}</td>
{% for tdf in visible_columns %}
- {% if not d.flags.compact_item_print or tdf.fieldname in doc.get(df.fieldname)[0].flags.compact_item_fields %}
+ {% if not print_settings.compact_item_print or tdf.fieldname in doc.flags.compact_item_fields %}
<td class="{{ get_align_class(tdf) }}" {{ fieldmeta(df) }}>
{% if tdf.fieldname == 'qty' %}
<div class="value">{{ (d[tdf.fieldname])|abs }}</div></td>
{% elif tdf.fieldtype == 'Currency' %}
<div class="value">{{ frappe.utils.fmt_money((d[tdf.fieldname])|abs, currency=doc.currency) }}</div></td>
{% else %}
- <div class="value">{{ print_value(tdf, d, doc, visible_columns) }}</div></td>
+ {% if doc.child_print_templates %}
+ {%- set child_templates = doc.child_print_templates.get(df.fieldname) -%}
+ <div class="value">{{ print_value(tdf, d, doc, visible_columns, child_templates) }}</div></td>
+ {% else %}
+ <div class="value">{{ print_value(tdf, d, doc, visible_columns) }}</div></td>
+ {% endif %}
{% endif %}
{% endif %}
{% endfor %}
diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
index bb0d0a1..79a6aab 100644
--- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
+++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.html
@@ -42,11 +42,13 @@
{% if(filters.show_future_payments) { %}
{% var balance_row = data.slice(-1).pop();
- var range1 = report.columns[11].label;
- var range2 = report.columns[12].label;
- var range3 = report.columns[13].label;
- var range4 = report.columns[14].label;
- var range5 = report.columns[15].label;
+ var start = filters.based_on_payment_terms ? 13 : 11;
+ var range1 = report.columns[start].label;
+ var range2 = report.columns[start+1].label;
+ var range3 = report.columns[start+2].label;
+ var range4 = report.columns[start+3].label;
+ var range5 = report.columns[start+4].label;
+ var range6 = report.columns[start+5].label;
%}
{% if(balance_row) { %}
<table class="table table-bordered table-condensed">
@@ -70,20 +72,34 @@
<th>{%= __(range3) %}</th>
<th>{%= __(range4) %}</th>
<th>{%= __(range5) %}</th>
+ <th>{%= __(range6) %}</th>
<th>{%= __("Total") %}</th>
</tr>
</thead>
<tbody>
<tr>
<td>{%= __("Total Outstanding") %}</td>
- <td class="text-right">{%= format_number(balance_row["range1"], null, 2) %}</td>
- <td class="text-right">{%= format_currency(balance_row["range2"]) %}</td>
- <td class="text-right">{%= format_currency(balance_row["range3"]) %}</td>
- <td class="text-right">{%= format_currency(balance_row["range4"]) %}</td>
- <td class="text-right">{%= format_currency(balance_row["range5"]) %}</td>
+ <td class="text-right">
+ {%= format_number(balance_row["age"], null, 2) %}
+ </td>
+ <td class="text-right">
+ {%= format_currency(balance_row["range1"], data[data.length-1]["currency"]) %}
+ </td>
+ <td class="text-right">
+ {%= format_currency(balance_row["range2"], data[data.length-1]["currency"]) %}
+ </td>
+ <td class="text-right">
+ {%= format_currency(balance_row["range3"], data[data.length-1]["currency"]) %}
+ </td>
+ <td class="text-right">
+ {%= format_currency(balance_row["range4"], data[data.length-1]["currency"]) %}
+ </td>
+ <td class="text-right">
+ {%= format_currency(balance_row["range5"], data[data.length-1]["currency"]) %}
+ </td>
<td class="text-right">
{%= format_currency(flt(balance_row["outstanding"]), data[data.length-1]["currency"]) %}
- </td>
+ </td>
</tr>
<td>{%= __("Future Payments") %}</td>
<td></td>
@@ -91,6 +107,7 @@
<td></td>
<td></td>
<td></td>
+ <td></td>
<td class="text-right">
{%= format_currency(flt(balance_row[("future_amount")]), data[data.length-1]["currency"]) %}
</td>
@@ -101,6 +118,7 @@
<th></th>
<th></th>
<th></th>
+ <th></th>
<th class="text-right">
{%= format_currency(flt(balance_row["outstanding"] - balance_row[("future_amount")]), data[data.length-1]["currency"]) %}</th>
</tr>
@@ -218,15 +236,15 @@
<td></td>
<td style="text-align: right"><b>{%= __("Total") %}</b></td>
<td style="text-align: right">
- {%= format_currency(data[i]["invoiced"], data[0]["currency"] ) %}</td>
+ {%= format_currency(data[i]["invoiced"], data[i]["currency"] ) %}</td>
{% if(!filters.show_future_payments) { %}
<td style="text-align: right">
- {%= format_currency(data[i]["paid"], data[0]["currency"]) %}</td>
- <td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %} </td>
+ {%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
+ <td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %} </td>
{% } %}
<td style="text-align: right">
- {%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}</td>
+ {%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% if(filters.show_future_payments) { %}
{% if(report.report_name === "Accounts Receivable") { %}
@@ -234,8 +252,8 @@
{%= data[i]["po_no"] %}</td>
{% } %}
<td style="text-align: right">{%= data[i]["future_ref"] %}</td>
- <td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[0]["currency"]) %}</td>
- <td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[0]["currency"]) %}</td>
+ <td style="text-align: right">{%= format_currency(data[i]["future_amount"], data[i]["currency"]) %}</td>
+ <td style="text-align: right">{%= format_currency(data[i]["remaining_balance"], data[i]["currency"]) %}</td>
{% } %}
{% } %}
{% } else { %}
@@ -256,10 +274,10 @@
{% } else { %}
<td><b>{%= __("Total") %}</b></td>
{% } %}
- <td style="text-align: right">{%= format_currency(data[i]["invoiced"], data[0]["currency"]) %}</td>
- <td style="text-align: right">{%= format_currency(data[i]["paid"], data[0]["currency"]) %}</td>
- <td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[0]["currency"]) %}</td>
- <td style="text-align: right">{%= format_currency(data[i]["outstanding"], data[0]["currency"]) %}</td>
+ <td style="text-align: right">{%= format_currency(data[i]["invoiced"], data[i]["currency"]) %}</td>
+ <td style="text-align: right">{%= format_currency(data[i]["paid"], data[i]["currency"]) %}</td>
+ <td style="text-align: right">{%= format_currency(data[i]["credit_note"], data[i]["currency"]) %}</td>
+ <td style="text-align: right">{%= format_currency(data[i]["outstanding"], data[i]["currency"]) %}</td>
{% } %}
{% } %}
</tr>
diff --git a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
index 16bef56..2162a02 100644
--- a/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
+++ b/erpnext/accounts/report/asset_depreciation_ledger/asset_depreciation_ledger.py
@@ -47,21 +47,22 @@
for d in gl_entries:
asset_data = assets_details.get(d.against_voucher)
- if not asset_data.get("accumulated_depreciation_amount"):
- asset_data.accumulated_depreciation_amount = d.debit
- else:
- asset_data.accumulated_depreciation_amount += d.debit
+ if asset_data:
+ if not asset_data.get("accumulated_depreciation_amount"):
+ asset_data.accumulated_depreciation_amount = d.debit
+ else:
+ asset_data.accumulated_depreciation_amount += d.debit
- row = frappe._dict(asset_data)
- row.update({
- "depreciation_amount": d.debit,
- "depreciation_date": d.posting_date,
- "amount_after_depreciation": (flt(row.gross_purchase_amount) -
- flt(row.accumulated_depreciation_amount)),
- "depreciation_entry": d.voucher_no
- })
+ row = frappe._dict(asset_data)
+ row.update({
+ "depreciation_amount": d.debit,
+ "depreciation_date": d.posting_date,
+ "amount_after_depreciation": (flt(row.gross_purchase_amount) -
+ flt(row.accumulated_depreciation_amount)),
+ "depreciation_entry": d.voucher_no
+ })
- data.append(row)
+ data.append(row)
return data
diff --git a/erpnext/accounts/report/balance_sheet/balance_sheet.py b/erpnext/accounts/report/balance_sheet/balance_sheet.py
index a858c19..1729abc 100644
--- a/erpnext/accounts/report/balance_sheet/balance_sheet.py
+++ b/erpnext/accounts/report/balance_sheet/balance_sheet.py
@@ -147,7 +147,6 @@
{
"value": net_asset,
"label": "Total Asset",
- "indicator": "Green",
"datatype": "Currency",
"currency": currency
},
@@ -155,14 +154,12 @@
"value": net_liability,
"label": "Total Liability",
"datatype": "Currency",
- "indicator": "Red",
"currency": currency
},
{
"value": net_equity,
"label": "Total Equity",
"datatype": "Currency",
- "indicator": "Blue",
"currency": currency
},
{
diff --git a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
index 0861b20..79b0a6f 100644
--- a/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
+++ b/erpnext/accounts/report/bank_clearance_summary/bank_clearance_summary.py
@@ -15,15 +15,51 @@
return columns, data
def get_columns():
- return [
- _("Payment Document") + "::130",
- _("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":110",
- _("Posting Date") + ":Date:100",
- _("Cheque/Reference No") + "::120",
- _("Clearance Date") + ":Date:100",
- _("Against Account") + ":Link/Account:170",
- _("Amount") + ":Currency:120"
- ]
+ columns = [{
+ "label": _("Payment Document Type"),
+ "fieldname": "payment_document_type",
+ "fieldtype": "Link",
+ "options": "Doctype",
+ "width": 130
+ },
+ {
+ "label": _("Payment Entry"),
+ "fieldname": "payment_entry",
+ "fieldtype": "Dynamic Link",
+ "options": "payment_document_type",
+ "width": 140
+ },
+ {
+ "label": _("Posting Date"),
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "label": _("Cheque/Reference No"),
+ "fieldname": "cheque_no",
+ "width": 120
+ },
+ {
+ "label": _("Clearance Date"),
+ "fieldname": "clearance_date",
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "label": _("Against Account"),
+ "fieldname": "against",
+ "fieldtype": "Link",
+ "options": "Account",
+ "width": 170
+ },
+ {
+ "label": _("Amount"),
+ "fieldname": "amount",
+ "width": 120
+ }]
+
+ return columns
def get_conditions(filters):
conditions = ""
diff --git a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
index d011689..76f3c50 100644
--- a/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
+++ b/erpnext/accounts/report/consolidated_financial_statement/consolidated_financial_statement.py
@@ -222,7 +222,7 @@
set_gl_entries_by_account(start_date,
end_date, root.lft, root.rgt, filters,
- gl_entries_by_account, accounts_by_name, ignore_closing_entries=False)
+ gl_entries_by_account, accounts_by_name, accounts, ignore_closing_entries=False)
calculate_values(accounts_by_name, gl_entries_by_account, companies, start_date, filters)
accumulate_values_into_parents(accounts, accounts_by_name, companies)
@@ -339,7 +339,7 @@
return data
def set_gl_entries_by_account(from_date, to_date, root_lft, root_rgt, filters, gl_entries_by_account,
- accounts_by_name, ignore_closing_entries=False):
+ accounts_by_name, accounts, ignore_closing_entries=False):
"""Returns a dict like { "account": [gl entries], ... }"""
company_lft, company_rgt = frappe.get_cached_value('Company',
@@ -382,15 +382,31 @@
for entry in gl_entries:
key = entry.account_number or entry.account_name
- validate_entries(key, entry, accounts_by_name)
+ validate_entries(key, entry, accounts_by_name, accounts)
gl_entries_by_account.setdefault(key, []).append(entry)
return gl_entries_by_account
-def validate_entries(key, entry, accounts_by_name):
+def get_account_details(account):
+ return frappe.get_cached_value('Account', account, ['name', 'report_type', 'root_type', 'company',
+ 'is_group', 'account_name', 'account_number', 'parent_account', 'lft', 'rgt'], as_dict=1)
+
+def validate_entries(key, entry, accounts_by_name, accounts):
if key not in accounts_by_name:
- field = "Account number" if entry.account_number else "Account name"
- frappe.throw(_("{0} {1} is not present in the parent company").format(field, key))
+ args = get_account_details(entry.account)
+
+ if args.parent_account:
+ parent_args = get_account_details(args.parent_account)
+
+ args.update({
+ 'lft': parent_args.lft + 1,
+ 'rgt': parent_args.rgt - 1,
+ 'root_type': parent_args.root_type,
+ 'report_type': parent_args.report_type
+ })
+
+ accounts_by_name.setdefault(key, args)
+ accounts.append(args)
def get_additional_conditions(from_date, ignore_closing_entries, filters):
additional_conditions = []
diff --git a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py
index 3ffb3ac..515fd99 100644
--- a/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py
+++ b/erpnext/accounts/report/delivered_items_to_be_billed/delivered_items_to_be_billed.py
@@ -14,11 +14,93 @@
def get_column():
return [
- _("Delivery Note") + ":Link/Delivery Note:120", _("Status") + "::120", _("Date") + ":Date:100",
- _("Suplier") + ":Link/Customer:120", _("Customer Name") + "::120",
- _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120",
- _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Pending Amount") + ":Currency:100",
- _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120",
+ {
+ "label": _("Delivery Note"),
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "options": "Delivery Note",
+ "width": 160
+ },
+ {
+ "label": _("Date"),
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "label": _("Customer"),
+ "fieldname": "customer",
+ "fieldtype": "Link",
+ "options": "Customer",
+ "width": 120
+ },
+ {
+ "label": _("Customer Name"),
+ "fieldname": "customer_name",
+ "fieldtype": "Data",
+ "width": 120
+ },
+ {
+ "label": _("Item Code"),
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 120
+ },
+ {
+ "label": _("Amount"),
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "width": 100,
+ "options": "Company:company:default_currency"
+ },
+ {
+ "label": _("Billed Amount"),
+ "fieldname": "billed_amount",
+ "fieldtype": "Currency",
+ "width": 100,
+ "options": "Company:company:default_currency"
+ },
+ {
+ "label": _("Returned Amount"),
+ "fieldname": "returned_amount",
+ "fieldtype": "Currency",
+ "width": 120,
+ "options": "Company:company:default_currency"
+ },
+ {
+ "label": _("Pending Amount"),
+ "fieldname": "pending_amount",
+ "fieldtype": "Currency",
+ "width": 120,
+ "options": "Company:company:default_currency"
+ },
+ {
+ "label": _("Item Name"),
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "width": 120
+ },
+ {
+ "label": _("Description"),
+ "fieldname": "description",
+ "fieldtype": "Data",
+ "width": 120
+ },
+ {
+ "label": _("Project"),
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "options": "Project",
+ "width": 120
+ },
+ {
+ "label": _("Company"),
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "options": "Company",
+ "width": 120
+ }
]
def get_args():
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 3445df7..cb4d9b4 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
@@ -8,6 +8,7 @@
from erpnext.accounts.report.item_wise_sales_register.item_wise_sales_register import (get_tax_accounts,
get_grand_total, add_total_row, get_display_value, get_group_by_and_display_fields, add_sub_total_row,
get_group_by_conditions)
+from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details
def execute(filters=None):
return _execute(filters)
@@ -22,7 +23,7 @@
aii_account_map = get_aii_accounts()
if item_list:
itemised_tax, tax_columns = get_tax_accounts(item_list, columns, company_currency,
- doctype="Purchase Invoice", tax_doctype="Purchase Taxes and Charges")
+ doctype='Purchase Invoice', tax_doctype='Purchase Taxes and Charges')
po_pr_map = get_purchase_receipts_against_purchase_order(item_list)
@@ -34,22 +35,27 @@
if filters.get('group_by'):
grand_total = get_grand_total(filters, 'Purchase Invoice')
+ item_details = get_item_details()
+
for d in item_list:
if not d.stock_qty:
continue
+ item_record = item_details.get(d.item_code)
+
purchase_receipt = None
if d.purchase_receipt:
purchase_receipt = d.purchase_receipt
elif d.po_detail:
purchase_receipt = ", ".join(po_pr_map.get(d.po_detail, []))
- expense_account = d.expense_account or aii_account_map.get(d.company)
+ expense_account = d.unrealized_profit_loss_account or d.expense_account \
+ or aii_account_map.get(d.company)
row = {
'item_code': d.item_code,
- 'item_name': d.item_name,
- 'item_group': d.item_group,
+ 'item_name': item_record.item_name if item_record else d.item_name,
+ 'item_group': item_record.item_group if item_record else d.item_group,
'description': d.description,
'invoice': d.parent,
'posting_date': d.posting_date,
@@ -81,10 +87,10 @@
for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update({
- frappe.scrub(tax + ' Rate'): item_tax.get("tax_rate", 0),
- frappe.scrub(tax + ' Amount'): item_tax.get("tax_amount", 0),
+ frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0),
+ frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0),
})
- total_tax += flt(item_tax.get("tax_amount"))
+ total_tax += flt(item_tax.get('tax_amount'))
row.update({
'total_tax': total_tax,
@@ -309,8 +315,10 @@
select
`tabPurchase Invoice Item`.`name`, `tabPurchase Invoice Item`.`parent`,
`tabPurchase Invoice`.posting_date, `tabPurchase Invoice`.credit_to, `tabPurchase Invoice`.company,
- `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total, `tabPurchase Invoice Item`.`item_code`,
- `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`, `tabPurchase Invoice Item`.description,
+ `tabPurchase Invoice`.supplier, `tabPurchase Invoice`.remarks, `tabPurchase Invoice`.base_net_total,
+ `tabPurchase Invoice`.unrealized_profit_loss_account,
+ `tabPurchase Invoice Item`.`item_code`, `tabPurchase Invoice Item`.description,
+ `tabPurchase Invoice Item`.`item_name`, `tabPurchase Invoice Item`.`item_group`,
`tabPurchase Invoice Item`.`project`, `tabPurchase Invoice Item`.`purchase_order`,
`tabPurchase Invoice Item`.`purchase_receipt`, `tabPurchase Invoice Item`.`po_detail`,
`tabPurchase Invoice Item`.`expense_account`, `tabPurchase Invoice Item`.`stock_qty`,
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 a05dcd7..928b373 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
@@ -8,6 +8,7 @@
from frappe.model.meta import get_field_precision
from frappe.utils.xlsxutils import handle_html
from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments
+from erpnext.selling.report.item_wise_sales_history.item_wise_sales_history import get_item_details, get_customer_details
def execute(filters=None):
return _execute(filters)
@@ -16,7 +17,7 @@
if not filters: filters = {}
columns = get_columns(additional_table_columns, filters)
- company_currency = frappe.get_cached_value('Company', filters.get("company"), "default_currency")
+ company_currency = frappe.get_cached_value('Company', filters.get('company'), 'default_currency')
item_list = get_items(filters, additional_query_columns)
if item_list:
@@ -33,7 +34,13 @@
if filters.get('group_by'):
grand_total = get_grand_total(filters, 'Sales Invoice')
+ customer_details = get_customer_details()
+ item_details = get_item_details()
+
for d in item_list:
+ customer_record = customer_details.get(d.customer)
+ item_record = item_details.get(d.item_code)
+
delivery_note = None
if d.delivery_note:
delivery_note = d.delivery_note
@@ -45,14 +52,14 @@
row = {
'item_code': d.item_code,
- 'item_name': d.item_name,
- 'item_group': d.item_group,
+ 'item_name': item_record.item_name if item_record else d.item_name,
+ 'item_group': item_record.item_group if item_record else d.item_group,
'description': d.description,
'invoice': d.parent,
'posting_date': d.posting_date,
'customer': d.customer,
- 'customer_name': d.customer_name,
- 'customer_group': d.customer_group,
+ 'customer_name': customer_record.customer_name,
+ 'customer_group': customer_record.customer_group,
}
if additional_query_columns:
@@ -69,7 +76,7 @@
'company': d.company,
'sales_order': d.sales_order,
'delivery_note': d.delivery_note,
- 'income_account': d.income_account,
+ 'income_account': d.unrealized_profit_loss_account or d.income_account,
'cost_center': d.cost_center,
'stock_qty': d.stock_qty,
'stock_uom': d.stock_uom
@@ -90,10 +97,10 @@
for tax in tax_columns:
item_tax = itemised_tax.get(d.name, {}).get(tax, {})
row.update({
- frappe.scrub(tax + ' Rate'): item_tax.get("tax_rate", 0),
- frappe.scrub(tax + ' Amount'): item_tax.get("tax_amount", 0),
+ frappe.scrub(tax + ' Rate'): item_tax.get('tax_rate', 0),
+ frappe.scrub(tax + ' Amount'): item_tax.get('tax_amount', 0),
})
- total_tax += flt(item_tax.get("tax_amount"))
+ total_tax += flt(item_tax.get('tax_amount'))
row.update({
'total_tax': total_tax,
@@ -226,7 +233,7 @@
if filters.get('group_by') != 'Territory':
columns.extend([
{
- 'label': _("Territory"),
+ 'label': _('Territory'),
'fieldname': 'territory',
'fieldtype': 'Link',
'options': 'Territory',
@@ -372,15 +379,16 @@
select
`tabSales Invoice Item`.name, `tabSales Invoice Item`.parent,
`tabSales Invoice`.posting_date, `tabSales Invoice`.debit_to,
+ `tabSales Invoice`.unrealized_profit_loss_account,
`tabSales Invoice`.project, `tabSales Invoice`.customer, `tabSales Invoice`.remarks,
`tabSales Invoice`.territory, `tabSales Invoice`.company, `tabSales Invoice`.base_net_total,
- `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.item_name,
- `tabSales Invoice Item`.item_group, `tabSales Invoice Item`.description, `tabSales Invoice Item`.sales_order,
- `tabSales Invoice Item`.delivery_note, `tabSales Invoice Item`.income_account,
- `tabSales Invoice Item`.cost_center, `tabSales Invoice Item`.stock_qty,
- `tabSales Invoice Item`.stock_uom, `tabSales Invoice Item`.base_net_rate,
- `tabSales Invoice Item`.base_net_amount, `tabSales Invoice`.customer_name,
- `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
+ `tabSales Invoice Item`.item_code, `tabSales Invoice Item`.description,
+ `tabSales Invoice Item`.`item_name`, `tabSales Invoice Item`.`item_group`,
+ `tabSales Invoice Item`.sales_order, `tabSales Invoice Item`.delivery_note,
+ `tabSales Invoice Item`.income_account, `tabSales Invoice Item`.cost_center,
+ `tabSales Invoice Item`.stock_qty, `tabSales Invoice Item`.stock_uom,
+ `tabSales Invoice Item`.base_net_rate, `tabSales Invoice Item`.base_net_amount,
+ `tabSales Invoice`.customer_name, `tabSales Invoice`.customer_group, `tabSales Invoice Item`.so_detail,
`tabSales Invoice`.update_stock, `tabSales Invoice Item`.uom, `tabSales Invoice Item`.qty {0}
from `tabSales Invoice`, `tabSales Invoice Item`
where `tabSales Invoice`.name = `tabSales Invoice Item`.parent
@@ -417,14 +425,14 @@
return frappe.db.sql_list("select name from `tabPurchase Taxes and Charges` where add_deduct_tax = 'Deduct'")
def get_tax_accounts(item_list, columns, company_currency,
- doctype="Sales Invoice", tax_doctype="Sales Taxes and Charges"):
+ doctype='Sales Invoice', tax_doctype='Sales Taxes and Charges'):
import json
item_row_map = {}
tax_columns = []
invoice_item_row = {}
itemised_tax = {}
- tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field("tax_amount"),
+ tax_amount_precision = get_field_precision(frappe.get_meta(tax_doctype).get_field('tax_amount'),
currency=company_currency) or 2
for d in item_list:
@@ -469,8 +477,8 @@
tax_rate = tax_data
tax_amount = 0
- if charge_type == "Actual" and not tax_rate:
- tax_rate = "NA"
+ if charge_type == 'Actual' and not tax_rate:
+ tax_rate = 'NA'
item_net_amount = sum([flt(d.base_net_amount)
for d in item_row_map.get(parent, {}).get(item_code, [])])
@@ -484,17 +492,17 @@
if (doctype == 'Purchase Invoice' and name in deducted_tax) else tax_value)
itemised_tax.setdefault(d.name, {})[description] = frappe._dict({
- "tax_rate": tax_rate,
- "tax_amount": tax_value
+ 'tax_rate': tax_rate,
+ 'tax_amount': tax_value
})
except ValueError:
continue
- elif charge_type == "Actual" and tax_amount:
+ elif charge_type == 'Actual' and tax_amount:
for d in invoice_item_row.get(parent, []):
itemised_tax.setdefault(d.name, {})[description] = frappe._dict({
- "tax_rate": "NA",
- "tax_amount": flt((tax_amount * d.base_net_amount) / d.base_net_total,
+ 'tax_rate': 'NA',
+ 'tax_amount': flt((tax_amount * d.base_net_amount) / d.base_net_total,
tax_amount_precision)
})
@@ -563,7 +571,7 @@
})
total_row_map.setdefault('total_row', {
- subtotal_display_field: "Total",
+ subtotal_display_field: 'Total',
'stock_qty': 0.0,
'amount': 0.0,
'bold': 1,
diff --git a/erpnext/accounts/report/non_billed_report.py b/erpnext/accounts/report/non_billed_report.py
index a9e25bc..2e18ce1 100644
--- a/erpnext/accounts/report/non_billed_report.py
+++ b/erpnext/accounts/report/non_billed_report.py
@@ -17,18 +17,26 @@
return frappe.db.sql("""
Select
- `{parent_tab}`.name, `{parent_tab}`.status, `{parent_tab}`.{date_field}, `{parent_tab}`.{party}, `{parent_tab}`.{party}_name,
- {project_field}, `{child_tab}`.item_code, `{child_tab}`.base_amount,
+ `{parent_tab}`.name, `{parent_tab}`.{date_field},
+ `{parent_tab}`.{party}, `{parent_tab}`.{party}_name,
+ `{child_tab}`.item_code,
+ `{child_tab}`.base_amount,
(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)),
- (`{child_tab}`.base_amount - (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1))),
- `{child_tab}`.item_name, `{child_tab}`.description, `{parent_tab}`.company
+ (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0)),
+ (`{child_tab}`.base_amount -
+ (`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1)) -
+ (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))),
+ `{child_tab}`.item_name, `{child_tab}`.description,
+ {project_field}, `{parent_tab}`.company
from
`{parent_tab}`, `{child_tab}`
where
`{parent_tab}`.name = `{child_tab}`.parent and `{parent_tab}`.docstatus = 1
and `{parent_tab}`.status not in ('Closed', 'Completed')
- and `{child_tab}`.amount > 0 and round(`{child_tab}`.billed_amt *
- ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) < `{child_tab}`.base_amount
+ and `{child_tab}`.amount > 0
+ and (`{child_tab}`.base_amount -
+ round(`{child_tab}`.billed_amt * ifnull(`{parent_tab}`.conversion_rate, 1), {precision}) -
+ (`{child_tab}`.base_rate * ifnull(`{child_tab}`.returned_qty, 0))) > 0
order by
`{parent_tab}`.{order} {order_by}
""".format(parent_tab = 'tab' + doctype, child_tab = 'tab' + child_tab, precision= precision, party = party,
diff --git a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
index 57a1231..7195c7e 100644
--- a/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
+++ b/erpnext/accounts/report/payment_period_based_on_invoice_date/payment_period_based_on_invoice_date.py
@@ -59,23 +59,111 @@
def get_columns(filters):
return [
- _("Payment Document") + ":: 100",
- _("Payment Entry") + ":Dynamic Link/"+_("Payment Document")+":140",
- _("Party Type") + "::100",
- _("Party") + ":Dynamic Link/Party Type:140",
- _("Posting Date") + ":Date:100",
- _("Invoice") + (":Link/Purchase Invoice:130" if filters.get("payment_type") == _("Outgoing") else ":Link/Sales Invoice:130"),
- _("Invoice Posting Date") + ":Date:130",
- _("Payment Due Date") + ":Date:130",
- _("Debit") + ":Currency:120",
- _("Credit") + ":Currency:120",
- _("Remarks") + "::150",
- _("Age") +":Int:40",
- "0-30:Currency:100",
- "30-60:Currency:100",
- "60-90:Currency:100",
- _("90-Above") + ":Currency:100",
- _("Delay in payment (Days)") + "::150"
+ {
+ "fieldname": "payment_document",
+ "label": _("Payment Document Type"),
+ "fieldtype": "Data",
+ "width": 100
+ },
+ {
+ "fieldname": "payment_entry",
+ "label": _("Payment Document"),
+ "fieldtype": "Dynamic Link",
+ "options": "payment_document",
+ "width": 160
+ },
+ {
+ "fieldname": "party_type",
+ "label": _("Party Type"),
+ "fieldtype": "Data",
+ "width": 100
+ },
+ {
+ "fieldname": "party",
+ "label": _("Party"),
+ "fieldtype": "Dynamic Link",
+ "options": "party_type",
+ "width": 160
+ },
+ {
+ "fieldname": "posting_date",
+ "label": _("Posting Date"),
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "fieldname": "invoice",
+ "label": _("Invoice"),
+ "fieldtype": "Link",
+ "options": "Purchase Invoice" if filters.get("payment_type") == _("Outgoing") else "Sales Invoice",
+ "width": 160
+ },
+ {
+ "fieldname": "invoice_posting_date",
+ "label": _("Invoice Posting Date"),
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "fieldname": "due_date",
+ "label": _("Payment Due Date"),
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "fieldname": "debit",
+ "label": _("Debit"),
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "credit",
+ "label": _("Credit"),
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "remarks",
+ "label": _("Remarks"),
+ "fieldtype": "Data",
+ "width": 200
+ },
+ {
+ "fieldname": "age",
+ "label": _("Age"),
+ "fieldtype": "Int",
+ "width": 50
+ },
+ {
+ "fieldname": "range1",
+ "label": "0-30",
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "range2",
+ "label": "30-60",
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "range3",
+ "label": "60-90",
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "range4",
+ "label": _("90 Above"),
+ "fieldtype": "Currency",
+ "width": 140
+ },
+ {
+ "fieldname": "delay_in_payment",
+ "label": _("Delay in payment (Days)"),
+ "fieldtype": "Int",
+ "width": 100
+ }
]
def get_conditions(filters):
diff --git a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
index b34d037..fe261b3 100644
--- a/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
+++ b/erpnext/accounts/report/profit_and_loss_statement/profit_and_loss_statement.py
@@ -60,23 +60,25 @@
return [
{
- "value": net_profit,
- "indicator": "Green" if net_profit > 0 else "Red",
- "label": profit_label,
- "datatype": "Currency",
- "currency": currency
- },
- {
"value": net_income,
"label": income_label,
"datatype": "Currency",
"currency": currency
},
+ { "type": "separator", "value": "-"},
{
"value": net_expense,
"label": expense_label,
"datatype": "Currency",
"currency": currency
+ },
+ { "type": "separator", "value": "=", "color": "blue"},
+ {
+ "value": net_profit,
+ "indicator": "Green" if net_profit > 0 else "Red",
+ "label": profit_label,
+ "datatype": "Currency",
+ "currency": currency
}
]
diff --git a/erpnext/accounts/report/purchase_register/purchase_register.py b/erpnext/accounts/report/purchase_register/purchase_register.py
index 9399e70..8ac749d 100644
--- a/erpnext/accounts/report/purchase_register/purchase_register.py
+++ b/erpnext/accounts/report/purchase_register/purchase_register.py
@@ -14,13 +14,15 @@
if not filters: filters = {}
invoice_list = get_invoices(filters, additional_query_columns)
- columns, expense_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns)
+ columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts \
+ = get_columns(invoice_list, additional_table_columns)
if not invoice_list:
msgprint(_("No record found"))
return columns, invoice_list
invoice_expense_map = get_invoice_expense_map(invoice_list)
+ internal_invoice_map = get_internal_invoice_map(invoice_list)
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)
@@ -52,10 +54,17 @@
# map expense values
base_net_total = 0
for expense_acc in expense_accounts:
- expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc))
+ if inv.is_internal_supplier and inv.company == inv.represents_company:
+ expense_amount = 0
+ else:
+ expense_amount = flt(invoice_expense_map.get(inv.name, {}).get(expense_acc))
base_net_total += expense_amount
row.append(expense_amount)
+ # Add amount in unrealized account
+ for account in unrealized_profit_loss_accounts:
+ row.append(flt(internal_invoice_map.get((inv.name, account))))
+
# net total
row.append(base_net_total or inv.base_net_total)
@@ -96,7 +105,8 @@
"width": 80
}
]
- expense_accounts = tax_accounts = expense_columns = tax_columns = []
+ expense_accounts = tax_accounts = expense_columns = tax_columns = unrealized_profit_loss_accounts = \
+ unrealized_profit_loss_account_columns = []
if invoice_list:
expense_accounts = frappe.db.sql_list("""select distinct expense_account
@@ -112,17 +122,25 @@
and parent in (%s) order by account_head""" %
', '.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]))
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]
+
for account in tax_accounts:
if account not in expense_accounts:
tax_columns.append(account + ":Currency/currency:120")
- columns = columns + expense_columns + [_("Net Total") + ":Currency/currency:120"] + tax_columns + \
+ columns = columns + expense_columns + unrealized_profit_loss_account_columns + \
+ [_("Net Total") + ":Currency/currency:120"] + tax_columns + \
[_("Total Tax") + ":Currency/currency:120", _("Grand Total") + ":Currency/currency:120",
_("Rounded Total") + ":Currency/currency:120", _("Outstanding Amount") + ":Currency/currency:120"]
- return columns, expense_accounts, tax_accounts
+ return columns, expense_accounts, tax_accounts, unrealized_profit_loss_accounts
def get_conditions(filters):
conditions = ""
@@ -199,6 +217,19 @@
return invoice_expense_map
+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)
+
+ internal_invoice_map = {}
+ for d in unrealized_amount_details:
+ if d.unrealized_profit_loss_account:
+ internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount)
+
+ return internal_invoice_map
+
def get_invoice_tax_map(invoice_list, invoice_expense_map, expense_accounts):
tax_details = frappe.db.sql("""
select parent, account_head, case add_deduct_tax when "Add" then sum(base_tax_amount_after_discount_amount)
diff --git a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py
index 5e8d773..e9e9c9c 100644
--- a/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py
+++ b/erpnext/accounts/report/received_items_to_be_billed/received_items_to_be_billed.py
@@ -14,11 +14,93 @@
def get_column():
return [
- _("Purchase Receipt") + ":Link/Purchase Receipt:120", _("Status") + "::120", _("Date") + ":Date:100",
- _("Supplier") + ":Link/Supplier:120", _("Supplier Name") + "::120",
- _("Project") + ":Link/Project:120", _("Item Code") + ":Link/Item:120",
- _("Amount") + ":Currency:100", _("Billed Amount") + ":Currency:100", _("Amount to Bill") + ":Currency:100",
- _("Item Name") + "::120", _("Description") + "::120", _("Company") + ":Link/Company:120",
+ {
+ "label": _("Purchase Receipt"),
+ "fieldname": "name",
+ "fieldtype": "Link",
+ "options": "Purchase Receipt",
+ "width": 160
+ },
+ {
+ "label": _("Date"),
+ "fieldname": "date",
+ "fieldtype": "Date",
+ "width": 100
+ },
+ {
+ "label": _("Supplier"),
+ "fieldname": "supplier",
+ "fieldtype": "Link",
+ "options": "Supplier",
+ "width": 120
+ },
+ {
+ "label": _("Supplier Name"),
+ "fieldname": "supplier_name",
+ "fieldtype": "Data",
+ "width": 120
+ },
+ {
+ "label": _("Item Code"),
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "options": "Item",
+ "width": 120
+ },
+ {
+ "label": _("Amount"),
+ "fieldname": "amount",
+ "fieldtype": "Currency",
+ "width": 100,
+ "options": "Company:company:default_currency"
+ },
+ {
+ "label": _("Billed Amount"),
+ "fieldname": "billed_amount",
+ "fieldtype": "Currency",
+ "width": 100,
+ "options": "Company:company:default_currency"
+ },
+ {
+ "label": _("Returned Amount"),
+ "fieldname": "returned_amount",
+ "fieldtype": "Currency",
+ "width": 120,
+ "options": "Company:company:default_currency"
+ },
+ {
+ "label": _("Pending Amount"),
+ "fieldname": "pending_amount",
+ "fieldtype": "Currency",
+ "width": 120,
+ "options": "Company:company:default_currency"
+ },
+ {
+ "label": _("Item Name"),
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "width": 120
+ },
+ {
+ "label": _("Description"),
+ "fieldname": "description",
+ "fieldtype": "Data",
+ "width": 120
+ },
+ {
+ "label": _("Project"),
+ "fieldname": "project",
+ "fieldtype": "Link",
+ "options": "Project",
+ "width": 120
+ },
+ {
+ "label": _("Company"),
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "options": "Company",
+ "width": 120
+ }
]
def get_args():
diff --git a/erpnext/accounts/report/sales_register/sales_register.py b/erpnext/accounts/report/sales_register/sales_register.py
index b6e61b1..cb2c98b 100644
--- a/erpnext/accounts/report/sales_register/sales_register.py
+++ b/erpnext/accounts/report/sales_register/sales_register.py
@@ -15,13 +15,14 @@
if not filters: filters = frappe._dict({})
invoice_list = get_invoices(filters, additional_query_columns)
- columns, income_accounts, tax_accounts = get_columns(invoice_list, additional_table_columns)
+ columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts = get_columns(invoice_list, additional_table_columns)
if not invoice_list:
msgprint(_("No record found"))
return columns, invoice_list
invoice_income_map = get_invoice_income_map(invoice_list)
+ internal_invoice_map = get_internal_invoice_map(invoice_list)
invoice_income_map, invoice_tax_map = get_invoice_tax_map(invoice_list,
invoice_income_map, income_accounts)
#Cost Center & Warehouse Map
@@ -70,12 +71,22 @@
# map income values
base_net_total = 0
for income_acc in income_accounts:
- income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc))
+ if inv.is_internal_customer and inv.company == inv.represents_company:
+ income_amount = 0
+ else:
+ income_amount = flt(invoice_income_map.get(inv.name, {}).get(income_acc))
+
base_net_total += income_amount
row.update({
frappe.scrub(income_acc): income_amount
})
+ # Add amount in unrealized account
+ for account in unrealized_profit_loss_accounts:
+ row.update({
+ frappe.scrub(account): flt(internal_invoice_map.get((inv.name, account)))
+ })
+
# net total
row.update({'net_total': base_net_total or inv.base_net_total})
@@ -230,6 +241,8 @@
tax_accounts = []
income_columns = []
tax_columns = []
+ unrealized_profit_loss_accounts = []
+ unrealized_profit_loss_account_columns = []
if invoice_list:
income_accounts = frappe.db.sql_list("""select distinct income_account
@@ -243,12 +256,18 @@
and parent in (%s) order by account_head""" %
', '.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]))
+
for account in income_accounts:
income_columns.append({
"label": account,
"fieldname": frappe.scrub(account),
"fieldtype": "Currency",
- "options": 'currency',
+ "options": "currency",
"width": 120
})
@@ -258,15 +277,24 @@
"label": account,
"fieldname": frappe.scrub(account),
"fieldtype": "Currency",
- "options": 'currency',
+ "options": "currency",
"width": 120
})
+ for account in unrealized_profit_loss_accounts:
+ unrealized_profit_loss_account_columns.append({
+ "label": account,
+ "fieldname": frappe.scrub(account),
+ "fieldtype": "Currency",
+ "options": "currency",
+ "width": 120
+ })
+
net_total_column = [{
"label": _("Net Total"),
"fieldname": "net_total",
"fieldtype": "Currency",
- "options": 'currency',
+ "options": "currency",
"width": 120
}]
@@ -301,9 +329,10 @@
}
]
- columns = columns + income_columns + net_total_column + tax_columns + total_columns
+ columns = columns + income_columns + unrealized_profit_loss_account_columns + \
+ net_total_column + tax_columns + total_columns
- return columns, income_accounts, tax_accounts
+ return columns, income_accounts, tax_accounts, unrealized_profit_loss_accounts
def get_conditions(filters):
conditions = ""
@@ -368,7 +397,8 @@
return frappe.db.sql("""
select name, posting_date, debit_to, project, customer,
customer_name, owner, remarks, territory, tax_id, customer_group,
- base_net_total, base_grand_total, base_rounded_total, outstanding_amount {0}
+ base_net_total, base_grand_total, base_rounded_total, outstanding_amount,
+ is_internal_customer, represents_company, company {0}
from `tabSales Invoice`
where docstatus = 1 %s order by posting_date desc, name desc""".format(additional_query_columns or '') %
conditions, filters, as_dict=1)
@@ -385,6 +415,19 @@
return invoice_income_map
+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)
+
+ internal_invoice_map = {}
+ for d in unrealized_amount_details:
+ if d.unrealized_profit_loss_account:
+ internal_invoice_map.setdefault((d.name, d.unrealized_profit_loss_account), d.amount)
+
+ return internal_invoice_map
+
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
diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py
index 53677cd..89a05b1 100644
--- a/erpnext/accounts/utils.py
+++ b/erpnext/accounts/utils.py
@@ -12,11 +12,12 @@
from six import iteritems
# imported to enable erpnext.accounts.utils.get_account_currency
from erpnext.accounts.doctype.account.account import get_account_currency
+from frappe.model.meta import get_field_precision
from erpnext.stock.utils import get_stock_value_on
from erpnext.stock import get_warehouse_account_map
-
+class StockValueAndAccountBalanceOutOfSync(frappe.ValidationError): pass
class FiscalYearError(frappe.ValidationError): pass
@frappe.whitelist()
@@ -78,7 +79,10 @@
else:
return ((fy.name, fy.year_start_date, fy.year_end_date),)
- error_msg = _("""{0} {1} not in any active Fiscal Year.""").format(label, formatdate(transaction_date))
+ error_msg = _("""{0} {1} is not in any active Fiscal Year""").format(label, formatdate(transaction_date))
+ if company:
+ error_msg = _("""{0} for {1}""").format(error_msg, frappe.bold(company))
+
if verbose==1: frappe.msgprint(error_msg)
raise FiscalYearError(error_msg)
@@ -582,24 +586,6 @@
(dr_or_cr, dr_or_cr, '%s', '%s', '%s', dr_or_cr),
(d.diff, d.voucher_type, d.voucher_no))
-def get_stock_and_account_balance(account=None, posting_date=None, company=None):
- if not posting_date: posting_date = nowdate()
-
- warehouse_account = get_warehouse_account_map(company)
-
- account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True)
-
- related_warehouses = [wh for wh, wh_details in warehouse_account.items()
- if wh_details.account == account and not wh_details.is_group]
-
- total_stock_value = 0.0
- for warehouse in related_warehouses:
- value = get_stock_value_on(warehouse, posting_date)
- total_stock_value += value
-
- precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
- return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
-
def get_currency_precision():
precision = cint(frappe.db.get_default("currency_precision"))
if not precision:
@@ -900,14 +886,13 @@
return accounts
-def get_stock_accounts(company):
- return frappe.get_all("Account", filters = {
- "account_type": "Stock",
- "company": company
- })
-
def update_gl_entries_after(posting_date, posting_time, for_warehouses=None, for_items=None,
warehouse_account=None, company=None):
+ stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items, company)
+ repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company, warehouse_account)
+
+
+def repost_gle_for_stock_vouchers(stock_vouchers, posting_date, company=None, warehouse_account=None):
def _delete_gl_entries(voucher_type, voucher_no):
frappe.db.sql("""delete from `tabGL Entry`
where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no))
@@ -915,21 +900,21 @@
if not warehouse_account:
warehouse_account = get_warehouse_account_map(company)
- future_stock_vouchers = get_future_stock_vouchers(posting_date, posting_time, for_warehouses, for_items)
- gle = get_voucherwise_gl_entries(future_stock_vouchers, posting_date)
+ precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit")) or 2
- for voucher_type, voucher_no in future_stock_vouchers:
+ gle = get_voucherwise_gl_entries(stock_vouchers, posting_date)
+ for voucher_type, voucher_no in stock_vouchers:
existing_gle = gle.get((voucher_type, voucher_no), [])
- voucher_obj = frappe.get_doc(voucher_type, voucher_no)
+ voucher_obj = frappe.get_cached_doc(voucher_type, voucher_no)
expected_gle = voucher_obj.get_gl_entries(warehouse_account)
if expected_gle:
- if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle):
+ if not existing_gle or not compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
_delete_gl_entries(voucher_type, voucher_no)
- voucher_obj.make_gl_entries(gl_entries=expected_gle, repost_future_gle=False, from_repost=True)
+ voucher_obj.make_gl_entries(gl_entries=expected_gle, from_repost=True)
else:
_delete_gl_entries(voucher_type, voucher_no)
-def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None):
+def get_future_stock_vouchers(posting_date, posting_time, for_warehouses=None, for_items=None, company=None):
future_stock_vouchers = []
values = []
@@ -942,9 +927,16 @@
condition += " and warehouse in ({})".format(", ".join(["%s"] * len(for_warehouses)))
values += for_warehouses
+ if company:
+ condition += " and company = %s"
+ values.append(company)
+
for d in frappe.db.sql("""select distinct sle.voucher_type, sle.voucher_no
from `tabStock Ledger Entry` sle
- where timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s) {condition}
+ where
+ timestamp(sle.posting_date, sle.posting_time) >= timestamp(%s, %s)
+ and is_cancelled = 0
+ {condition}
order by timestamp(sle.posting_date, sle.posting_time) asc, creation asc for update""".format(condition=condition),
tuple([posting_date, posting_time] + values), as_dict=True):
future_stock_vouchers.append([d.voucher_type, d.voucher_no])
@@ -961,3 +953,107 @@
gl_entries.setdefault((d.voucher_type, d.voucher_no), []).append(d)
return gl_entries
+
+def compare_existing_and_expected_gle(existing_gle, expected_gle, precision):
+ matched = True
+ for entry in expected_gle:
+ account_existed = False
+ for e in existing_gle:
+ if entry.account == e.account:
+ account_existed = True
+ if (entry.account == e.account and entry.against_account == e.against_account
+ and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center)
+ and ( flt(entry.debit, precision) != flt(e.debit, precision) or
+ flt(entry.credit, precision) != flt(e.credit, precision))):
+ matched = False
+ break
+ if not account_existed:
+ matched = False
+ break
+ return matched
+
+def check_if_stock_and_account_balance_synced(posting_date, company, voucher_type=None, voucher_no=None):
+ if not cint(erpnext.is_perpetual_inventory_enabled(company)):
+ return
+
+ accounts = get_stock_accounts(company, voucher_type, voucher_no)
+ stock_adjustment_account = frappe.db.get_value("Company", company, "stock_adjustment_account")
+
+ for account in accounts:
+ account_bal, stock_bal, warehouse_list = get_stock_and_account_balance(account,
+ posting_date, company)
+
+ if abs(account_bal - stock_bal) > 0.1:
+ precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"),
+ currency=frappe.get_cached_value('Company', company, "default_currency"))
+
+ diff = flt(stock_bal - account_bal, precision)
+
+ error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses as on {3}.").format(
+ stock_bal, account_bal, frappe.bold(account), posting_date)
+ error_resolution = _("Please create an adjustment Journal Entry for amount {0} on {1}")\
+ .format(frappe.bold(diff), frappe.bold(posting_date))
+
+ frappe.msgprint(
+ msg="""{0}<br></br>{1}<br></br>""".format(error_reason, error_resolution),
+ raise_exception=StockValueAndAccountBalanceOutOfSync,
+ title=_('Values Out Of Sync'),
+ primary_action={
+ 'label': _('Make Journal Entry'),
+ 'client_action': 'erpnext.route_to_adjustment_jv',
+ 'args': get_journal_entry(account, stock_adjustment_account, diff)
+ })
+
+def get_stock_accounts(company, voucher_type=None, voucher_no=None):
+ stock_accounts = [d.name for d in frappe.db.get_all("Account", {
+ "account_type": "Stock",
+ "company": company,
+ "is_group": 0
+ })]
+ if voucher_type and voucher_no:
+ if voucher_type == "Journal Entry":
+ stock_accounts = [d.account for d in frappe.db.get_all("Journal Entry Account", {
+ "parent": voucher_no,
+ "account": ["in", stock_accounts]
+ }, "account")]
+
+ else:
+ stock_accounts = [d.account for d in frappe.db.get_all("GL Entry", {
+ "voucher_type": voucher_type,
+ "voucher_no": voucher_no,
+ "account": ["in", stock_accounts]
+ }, "account")]
+
+ return stock_accounts
+
+def get_stock_and_account_balance(account=None, posting_date=None, company=None):
+ if not posting_date: posting_date = nowdate()
+
+ warehouse_account = get_warehouse_account_map(company)
+
+ account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True)
+
+ related_warehouses = [wh for wh, wh_details in warehouse_account.items()
+ if wh_details.account == account and not wh_details.is_group]
+
+ total_stock_value = 0.0
+ for warehouse in related_warehouses:
+ value = get_stock_value_on(warehouse, posting_date)
+ total_stock_value += value
+
+ precision = frappe.get_precision("Journal Entry Account", "debit_in_account_currency")
+ return flt(account_balance, precision), flt(total_stock_value, precision), related_warehouses
+
+def get_journal_entry(account, stock_adjustment_account, amount):
+ db_or_cr_warehouse_account =('credit_in_account_currency' if amount < 0 else 'debit_in_account_currency')
+ db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if amount < 0 else 'credit_in_account_currency')
+
+ return {
+ 'accounts':[{
+ 'account': account,
+ db_or_cr_warehouse_account: abs(amount)
+ }, {
+ 'account': stock_adjustment_account,
+ db_or_cr_stock_adjustment_account : abs(amount)
+ }]
+ }
diff --git a/erpnext/accounts/workspace/accounting/accounting.json b/erpnext/accounts/workspace/accounting/accounting.json
new file mode 100644
index 0000000..8d24ca8
--- /dev/null
+++ b/erpnext/accounts/workspace/accounting/accounting.json
@@ -0,0 +1,1119 @@
+{
+ "category": "Modules",
+ "charts": [
+ {
+ "chart_name": "Profit and Loss",
+ "label": "Profit and Loss"
+ }
+ ],
+ "creation": "2020-03-02 15:41:59.515192",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "accounting",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Accounting",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Accounting Masters",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Company",
+ "link_to": "Company",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Chart of Accounts",
+ "link_to": "Account",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Accounts Settings",
+ "link_to": "Accounts Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fiscal Year",
+ "link_to": "Fiscal Year",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Accounting Dimension",
+ "link_to": "Accounting Dimension",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Finance Book",
+ "link_to": "Finance Book",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Accounting Period",
+ "link_to": "Accounting Period",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Payment Term",
+ "link_to": "Payment Term",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "General Ledger",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Journal Entry",
+ "link_to": "Journal Entry",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Journal Entry Template",
+ "link_to": "Journal Entry Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "General Ledger",
+ "link_to": "General Ledger",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Customer Ledger Summary",
+ "link_to": "Customer Ledger Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Supplier Ledger Summary",
+ "link_to": "Supplier Ledger Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Accounts Receivable",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Invoice",
+ "link_to": "Sales Invoice",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Customer",
+ "link_to": "Customer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Payment Entry",
+ "link_to": "Payment Entry",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Payment Request",
+ "link_to": "Payment Request",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Accounts Receivable",
+ "link_to": "Accounts Receivable",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Accounts Receivable Summary",
+ "link_to": "Accounts Receivable Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Register",
+ "link_to": "Sales Register",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Item-wise Sales Register",
+ "link_to": "Item-wise Sales Register",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Order Analysis",
+ "link_to": "Sales Order Analysis",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Delivered Items To Be Billed",
+ "link_to": "Delivered Items To Be Billed",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Accounts Payable",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Purchase Invoice",
+ "link_to": "Purchase Invoice",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier",
+ "link_to": "Supplier",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Payment Entry",
+ "link_to": "Payment Entry",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Accounts Payable",
+ "link_to": "Accounts Payable",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Accounts Payable Summary",
+ "link_to": "Accounts Payable Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Purchase Register",
+ "link_to": "Purchase Register",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Item-wise Purchase Register",
+ "link_to": "Item-wise Purchase Register",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Purchase Order Analysis",
+ "link_to": "Purchase Order Analysis",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Received Items To Be Billed",
+ "link_to": "Received Items To Be Billed",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Trial Balance for Party",
+ "link_to": "Trial Balance for Party",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Journal Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Payment Period Based On Invoice Date",
+ "link_to": "Payment Period Based On Invoice Date",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Partners Commission",
+ "link_to": "Sales Partners Commission",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Customer",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Customer Credit Balance",
+ "link_to": "Customer Credit Balance",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Payment Summary",
+ "link_to": "Sales Payment Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Address",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Address And Contacts",
+ "link_to": "Address And Contacts",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "DATEV Export",
+ "link_to": "DATEV",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Financial Statements",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Trial Balance",
+ "link_to": "Trial Balance",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Profit and Loss Statement",
+ "link_to": "Profit and Loss Statement",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Balance Sheet",
+ "link_to": "Balance Sheet",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Cash Flow",
+ "link_to": "Cash Flow",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Consolidated Financial Statement",
+ "link_to": "Consolidated Financial Statement",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Multi Currency",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Currency",
+ "link_to": "Currency",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Currency Exchange",
+ "link_to": "Currency Exchange",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Exchange Rate Revaluation",
+ "link_to": "Exchange Rate Revaluation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Payment Gateway Account",
+ "link_to": "Payment Gateway Account",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Terms and Conditions Template",
+ "link_to": "Terms and Conditions",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Mode of Payment",
+ "link_to": "Mode of Payment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bank Statement",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bank",
+ "link_to": "Bank",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bank Account",
+ "link_to": "Bank Account",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bank Clearance",
+ "link_to": "Bank Clearance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bank Reconciliation",
+ "link_to": "bank-reconciliation",
+ "link_type": "Page",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Bank Reconciliation Statement",
+ "link_to": "Bank Reconciliation Statement",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bank Statement Transaction Entry",
+ "link_to": "Bank Statement Transaction Entry",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bank Statement Settings",
+ "link_to": "Bank Statement Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Subscription Management",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Subscription Plan",
+ "link_to": "Subscription Plan",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Subscription",
+ "link_to": "Subscription",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Subscription Settings",
+ "link_to": "Subscription Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Goods and Services Tax (GST India)",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "GST Settings",
+ "link_to": "GST Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "GST HSN Code",
+ "link_to": "GST HSN Code",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "GSTR-1",
+ "link_to": "GSTR-1",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "GSTR-2",
+ "link_to": "GSTR-2",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "GSTR 3B Report",
+ "link_to": "GSTR 3B Report",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "GST Sales Register",
+ "link_to": "GST Sales Register",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "GST Purchase Register",
+ "link_to": "GST Purchase Register",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "GST Itemised Sales Register",
+ "link_to": "GST Itemised Sales Register",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "GST Itemised Purchase Register",
+ "link_to": "GST Itemised Purchase Register",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "C-Form",
+ "link_to": "C-Form",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Lower Deduction Certificate",
+ "link_to": "Lower Deduction Certificate",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Share Management",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shareholder",
+ "link_to": "Shareholder",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Share Transfer",
+ "link_to": "Share Transfer",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Share Transfer",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Share Ledger",
+ "link_to": "Share Ledger",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Share Transfer",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Share Balance",
+ "link_to": "Share Balance",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Cost Center and Budgeting",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Chart of Cost Centers",
+ "link_to": "Cost Center",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Budget",
+ "link_to": "Budget",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Accounting Dimension",
+ "link_to": "Accounting Dimension",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Cost Center",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Budget Variance Report",
+ "link_to": "Budget Variance Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Monthly Distribution",
+ "link_to": "Monthly Distribution",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Opening and Closing",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Opening Invoice Creation Tool",
+ "link_to": "Opening Invoice Creation Tool",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Chart of Accounts Importer",
+ "link_to": "Chart of Accounts Importer",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Period Closing Voucher",
+ "link_to": "Period Closing Voucher",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Taxes",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Taxes and Charges Template",
+ "link_to": "Sales Taxes and Charges Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Purchase Taxes and Charges Template",
+ "link_to": "Purchase Taxes and Charges Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Tax Template",
+ "link_to": "Item Tax Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tax Category",
+ "link_to": "Tax Category",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tax Rule",
+ "link_to": "Tax Rule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tax Withholding Category",
+ "link_to": "Tax Withholding Category",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Profitability",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Gross Profit",
+ "link_to": "Gross Profit",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "GL Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Profitability Analysis",
+ "link_to": "Profitability Analysis",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Invoice Trends",
+ "link_to": "Sales Invoice Trends",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Purchase Invoice Trends",
+ "link_to": "Purchase Invoice Trends",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:35.349024",
+ "modified_by": "Administrator",
+ "module": "Accounts",
+ "name": "Accounting",
+ "onboarding": "Accounts",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "label": "Chart Of Accounts",
+ "link_to": "Account",
+ "type": "DocType"
+ },
+ {
+ "label": "Sales Invoice",
+ "link_to": "Sales Invoice",
+ "type": "DocType"
+ },
+ {
+ "label": "Purchase Invoice",
+ "link_to": "Purchase Invoice",
+ "type": "DocType"
+ },
+ {
+ "label": "Journal Entry",
+ "link_to": "Journal Entry",
+ "type": "DocType"
+ },
+ {
+ "label": "Payment Entry",
+ "link_to": "Payment Entry",
+ "type": "DocType"
+ },
+ {
+ "label": "Accounts Receivable",
+ "link_to": "Accounts Receivable",
+ "type": "Report"
+ },
+ {
+ "label": "General Ledger",
+ "link_to": "General Ledger",
+ "type": "Report"
+ },
+ {
+ "label": "Trial Balance",
+ "link_to": "Trial Balance",
+ "type": "Report"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "Accounts",
+ "type": "Dashboard"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/agriculture/desk_page/agriculture/agriculture.json b/erpnext/agriculture/desk_page/agriculture/agriculture.json
deleted file mode 100644
index e0d2c9c..0000000
--- a/erpnext/agriculture/desk_page/agriculture/agriculture.json
+++ /dev/null
@@ -1,39 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Crops & Lands",
- "links": "[\n {\n \"label\": \"Crop\",\n \"name\": \"Crop\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Crop Cycle\",\n \"name\": \"Crop Cycle\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Location\",\n \"name\": \"Location\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Analytics",
- "links": "[\n {\n \"label\": \"Plant Analysis\",\n \"name\": \"Plant Analysis\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Soil Analysis\",\n \"name\": \"Soil Analysis\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Water Analysis\",\n \"name\": \"Water Analysis\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Soil Texture\",\n \"name\": \"Soil Texture\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Weather\",\n \"name\": \"Weather\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Agriculture Analysis Criteria\",\n \"name\": \"Agriculture Analysis Criteria\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Diseases & Fertilizers",
- "links": "[\n {\n \"label\": \"Disease\",\n \"name\": \"Disease\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fertilizer\",\n \"name\": \"Fertilizer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- }
- ],
- "category": "Domains",
- "charts": [],
- "creation": "2020-03-02 17:23:34.339274",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Agriculture",
- "modified": "2020-04-01 11:28:51.032822",
- "modified_by": "Administrator",
- "module": "Agriculture",
- "name": "Agriculture",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "restrict_to_domain": "Agriculture",
- "shortcuts": []
-}
\ No newline at end of file
diff --git a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
index cae150c..afbd9b4 100644
--- a/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
+++ b/erpnext/agriculture/doctype/crop_cycle/crop_cycle.py
@@ -48,7 +48,7 @@
def import_disease_tasks(self, disease, start_date):
disease_doc = frappe.get_doc('Disease', disease)
- self.create_task(disease_doc.treatment_task, self.name, start_date)
+ self.create_task(disease_doc.treatment_task, self.project, start_date)
def create_project(self, period, crop_tasks):
project = frappe.get_doc({
diff --git a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py
index 5510d5a..763b403 100644
--- a/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py
+++ b/erpnext/agriculture/doctype/crop_cycle/test_crop_cycle.py
@@ -71,4 +71,4 @@
def check_project_creation():
- return True if frappe.db.exists('Project', 'Basil from seed 2017') else False
+ return True if frappe.db.exists('Project', {'project_name': 'Basil from seed 2017'}) else False
diff --git a/erpnext/agriculture/workspace/agriculture/agriculture.json b/erpnext/agriculture/workspace/agriculture/agriculture.json
new file mode 100644
index 0000000..2cc2524
--- /dev/null
+++ b/erpnext/agriculture/workspace/agriculture/agriculture.json
@@ -0,0 +1,157 @@
+{
+ "category": "Domains",
+ "charts": [],
+ "creation": "2020-03-02 17:23:34.339274",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "agriculture",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Agriculture",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Crops & Lands",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Crop",
+ "link_to": "Crop",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Crop Cycle",
+ "link_to": "Crop Cycle",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Location",
+ "link_to": "Location",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Analytics",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Plant Analysis",
+ "link_to": "Plant Analysis",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Soil Analysis",
+ "link_to": "Soil Analysis",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Water Analysis",
+ "link_to": "Water Analysis",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Soil Texture",
+ "link_to": "Soil Texture",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Weather",
+ "link_to": "Weather",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Agriculture Analysis Criteria",
+ "link_to": "Agriculture Analysis Criteria",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Diseases & Fertilizers",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Disease",
+ "link_to": "Disease",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fertilizer",
+ "link_to": "Fertilizer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:38.477493",
+ "modified_by": "Administrator",
+ "module": "Agriculture",
+ "name": "Agriculture",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "restrict_to_domain": "Agriculture",
+ "shortcuts": []
+}
\ No newline at end of file
diff --git a/erpnext/assets/desk_page/assets/assets.json b/erpnext/assets/desk_page/assets/assets.json
deleted file mode 100644
index 449a5fa..0000000
--- a/erpnext/assets/desk_page/assets/assets.json
+++ /dev/null
@@ -1,66 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Assets",
- "links": "[\n {\n \"label\": \"Asset\",\n \"name\": \"Asset\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Location\",\n \"name\": \"Location\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Asset Category\",\n \"name\": \"Asset Category\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Transfer an asset from one warehouse to another\",\n \"label\": \"Asset Movement\",\n \"name\": \"Asset Movement\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Maintenance",
- "links": "[\n {\n \"label\": \"Asset Maintenance Team\",\n \"name\": \"Asset Maintenance Team\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Asset Maintenance Team\"\n ],\n \"label\": \"Asset Maintenance\",\n \"name\": \"Asset Maintenance\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Asset Maintenance\"\n ],\n \"label\": \"Asset Maintenance Log\",\n \"name\": \"Asset Maintenance Log\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Asset\"\n ],\n \"label\": \"Asset Value Adjustment\",\n \"name\": \"Asset Value Adjustment\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Asset\"\n ],\n \"label\": \"Asset Repair\",\n \"name\": \"Asset Repair\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Asset\"\n ],\n \"doctype\": \"Asset\",\n \"is_query_report\": true,\n \"label\": \"Asset Depreciation Ledger\",\n \"name\": \"Asset Depreciation Ledger\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Asset\"\n ],\n \"doctype\": \"Asset\",\n \"is_query_report\": true,\n \"label\": \"Asset Depreciations and Balances\",\n \"name\": \"Asset Depreciations and Balances\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Asset Maintenance\"\n ],\n \"doctype\": \"Asset Maintenance\",\n \"label\": \"Asset Maintenance\",\n \"name\": \"Asset Maintenance\",\n \"type\": \"report\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [
- {
- "chart_name": "Asset Value Analytics",
- "label": "Asset Value Analytics"
- }
- ],
- "creation": "2020-03-02 15:43:27.634865",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Assets",
- "modified": "2020-05-20 18:05:23.994795",
- "modified_by": "Administrator",
- "module": "Assets",
- "name": "Assets",
- "onboarding": "Assets",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "label": "Asset",
- "link_to": "Asset",
- "type": "DocType"
- },
- {
- "label": "Asset Category",
- "link_to": "Asset Category",
- "type": "DocType"
- },
- {
- "label": "Fixed Asset Register",
- "link_to": "Fixed Asset Register",
- "type": "Report"
- },
- {
- "label": "Dashboard",
- "link_to": "Asset",
- "type": "Dashboard"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js
index b2318a2..6f1bb28 100644
--- a/erpnext/assets/doctype/asset/asset.js
+++ b/erpnext/assets/doctype/asset/asset.js
@@ -2,6 +2,7 @@
// For license information, please see license.txt
frappe.provide("erpnext.asset");
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Asset', {
onload: function(frm) {
@@ -32,13 +33,11 @@
};
});
- frm.set_query("cost_center", function() {
- return {
- "filters": {
- "company": frm.doc.company,
- }
- };
- });
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
setup: function(frm) {
diff --git a/erpnext/assets/doctype/asset/asset.json b/erpnext/assets/doctype/asset/asset.json
index a3152ab..421b9a6 100644
--- a/erpnext/assets/doctype/asset/asset.json
+++ b/erpnext/assets/doctype/asset/asset.json
@@ -8,21 +8,20 @@
"document_type": "Document",
"engine": "InnoDB",
"field_order": [
- "is_existing_asset",
- "section_break_2",
- "naming_series",
+ "company",
"item_code",
"item_name",
- "asset_category",
"asset_owner",
"asset_owner_company",
+ "is_existing_asset",
"supplier",
"customer",
"image",
"journal_entry_for_scrap",
"column_break_3",
- "company",
+ "naming_series",
"asset_name",
+ "asset_category",
"location",
"custodian",
"department",
@@ -95,12 +94,14 @@
"reqd": 1
},
{
+ "depends_on": "item_code",
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Read Only",
"label": "Item Name"
},
{
+ "depends_on": "item_code",
"fetch_from": "item_code.asset_category",
"fieldname": "asset_category",
"fieldtype": "Link",
@@ -307,12 +308,13 @@
{
"depends_on": "calculate_depreciation",
"fieldname": "section_break_14",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Depreciation Schedule"
},
{
"fieldname": "schedules",
"fieldtype": "Table",
- "label": "Depreciation Schedules",
+ "label": "Depreciation Schedule",
"no_copy": 1,
"options": "Depreciation Schedule"
},
@@ -459,10 +461,6 @@
"label": "Allow Monthly Depreciation"
},
{
- "fieldname": "section_break_2",
- "fieldtype": "Section Break"
- },
- {
"collapsible": 1,
"collapsible_depends_on": "is_existing_asset",
"fieldname": "purchase_details_section",
@@ -480,14 +478,31 @@
{
"depends_on": "calculate_depreciation",
"fieldname": "section_break_36",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Finance Books"
}
],
"idx": 72,
"image_field": "image",
"is_submittable": 1,
- "links": [],
- "modified": "2020-07-28 15:04:44.452224",
+ "links": [
+ {
+ "group": "Maintenance",
+ "link_doctype": "Asset Maintenance",
+ "link_fieldname": "asset_name"
+ },
+ {
+ "group": "Repair",
+ "link_doctype": "Asset Repair",
+ "link_fieldname": "asset_name"
+ },
+ {
+ "group": "Value",
+ "link_doctype": "Asset Value Adjustment",
+ "link_fieldname": "asset"
+ }
+ ],
+ "modified": "2021-01-22 12:38:59.091510",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset",
@@ -527,5 +542,6 @@
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "title_field": "asset_name"
+ "title_field": "asset_name",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py
index 30abc66..e8e8ec6 100644
--- a/erpnext/assets/doctype/asset/asset.py
+++ b/erpnext/assets/doctype/asset/asset.py
@@ -133,9 +133,10 @@
if self.is_existing_asset: return
if self.gross_purchase_amount and self.gross_purchase_amount != self.purchase_receipt_amount:
- frappe.throw(_("Gross Purchase Amount should be {} to purchase amount of one single Asset. {}\
- Please do not book expense of multiple assets against one single Asset.")
- .format(frappe.bold("equal"), "<br>"), title=_("Invalid Gross Purchase Amount"))
+ error_message = _("Gross Purchase Amount should be <b>equal</b> to purchase amount of one single Asset.")
+ error_message += "<br>"
+ error_message += _("Please do not book expense of multiple assets against one single Asset.")
+ frappe.throw(error_message, title=_("Invalid Gross Purchase Amount"))
def make_asset_movement(self):
reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice'
@@ -471,7 +472,7 @@
asset_bought_with_invoice = (purchase_document == self.purchase_invoice)
fixed_asset_account = self.get_fixed_asset_account()
-
+
cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
cwip_account = self.get_cwip_account(cwip_enabled=cwip_enabled)
@@ -503,10 +504,10 @@
purchase_document = self.purchase_invoice if asset_bought_with_invoice else self.purchase_receipt
return purchase_document
-
+
def get_fixed_asset_account(self):
return get_asset_category_account('fixed_asset_account', None, self.name, None, self.asset_category, self.company)
-
+
def get_cwip_account(self, cwip_enabled=False):
cwip_account = None
try:
@@ -659,7 +660,7 @@
frappe.db.commit()
- frappe.msgprint(_("Asset Movement record {0} created").format("<a href='#Form/Asset Movement/{0}'>{0}</a>").format(movement_entry.name))
+ frappe.msgprint(_("Asset Movement record {0} created").format("<a href='/app/Form/Asset Movement/{0}'>{0}</a>").format(movement_entry.name))
@frappe.whitelist()
def get_item_details(item_code, asset_category):
diff --git a/erpnext/assets/doctype/asset/asset_dashboard.py b/erpnext/assets/doctype/asset/asset_dashboard.py
index b489899..a5cf238 100644
--- a/erpnext/assets/doctype/asset/asset_dashboard.py
+++ b/erpnext/assets/doctype/asset/asset_dashboard.py
@@ -2,20 +2,11 @@
def get_data():
return {
- 'fieldname': 'asset_name',
'non_standard_fieldnames': {
'Asset Movement': 'asset'
},
'transactions': [
{
- 'label': ['Maintenance'],
- 'items': ['Asset Maintenance', 'Asset Maintenance Log']
- },
- {
- 'label': ['Repair'],
- 'items': ['Asset Repair']
- },
- {
'label': ['Movement'],
'items': ['Asset Movement']
}
diff --git a/erpnext/assets/doctype/asset_category/asset_category.json b/erpnext/assets/doctype/asset_category/asset_category.json
index 7483b41..a25f546 100644
--- a/erpnext/assets/doctype/asset_category/asset_category.json
+++ b/erpnext/assets/doctype/asset_category/asset_category.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:asset_category_name",
@@ -64,7 +65,8 @@
"label": "Enable Capital Work in Progress Accounting"
}
],
- "modified": "2019-10-11 12:19:59.759136",
+ "links": [],
+ "modified": "2021-02-24 15:05:38.621803",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Category",
@@ -111,5 +113,6 @@
],
"show_name_in_global_search": 1,
"sort_field": "modified",
- "sort_order": "DESC"
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js
index 001fc26..70b8654 100644
--- a/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js
+++ b/erpnext/assets/doctype/asset_maintenance/asset_maintenance.js
@@ -40,14 +40,13 @@
if(!r.message) {
return;
}
- var section = frm.dashboard.add_section(`<h5 style="margin-top: 0px;">
- ${ __("Maintenance Log") }</a></h5>`);
+ const section = frm.dashboard.add_section('', __("Maintenance Log"));
var rows = $('<div></div>').appendTo(section);
// show
(r.message || []).forEach(function(d) {
$(`<div class='row' style='margin-bottom: 10px;'>
<div class='col-sm-3 small'>
- <a onclick="frappe.set_route('List', 'Asset Maintenance Log',
+ <a onclick="frappe.set_route('List', 'Asset Maintenance Log',
{'asset_name': '${d.asset_name}','maintenance_status': '${d.maintenance_status}' });">
${d.maintenance_status} <span class="badge">${d.count}</span>
</a>
diff --git a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json
index 7395bec..7d33176 100644
--- a/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json
+++ b/erpnext/assets/doctype/asset_maintenance_log/asset_maintenance_log.json
@@ -18,15 +18,13 @@
"task_name",
"maintenance_type",
"periodicity",
- "assign_to_name",
- "column_break_6",
- "due_date",
- "completion_date",
- "maintenance_status",
- "section_break_12",
"has_certificate",
"certificate_attachement",
- "section_break_6",
+ "column_break_6",
+ "maintenance_status",
+ "assign_to_name",
+ "due_date",
+ "completion_date",
"description",
"column_break_9",
"actions_performed",
@@ -70,7 +68,8 @@
},
{
"fieldname": "section_break_5",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Maintenance Details"
},
{
"fieldname": "task",
@@ -124,10 +123,6 @@
"reqd": 1
},
{
- "fieldname": "section_break_12",
- "fieldtype": "Section Break"
- },
- {
"default": "0",
"fetch_from": "task.certificate_required",
"fieldname": "has_certificate",
@@ -141,10 +136,6 @@
"label": "Certificate"
},
{
- "fieldname": "section_break_6",
- "fieldtype": "Column Break"
- },
- {
"fetch_from": "task.description",
"fieldname": "description",
"fieldtype": "Read Only",
@@ -179,9 +170,10 @@
"read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-05-28 20:51:48.238397",
+ "modified": "2021-01-22 12:33:45.888124",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Maintenance Log",
diff --git a/erpnext/assets/doctype/asset_maintenance_team/asset_maintenance_team.json b/erpnext/assets/doctype/asset_maintenance_team/asset_maintenance_team.json
index e2aa548..ffa04e5 100644
--- a/erpnext/assets/doctype/asset_maintenance_team/asset_maintenance_team.json
+++ b/erpnext/assets/doctype/asset_maintenance_team/asset_maintenance_team.json
@@ -1,282 +1,87 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:maintenance_team_name",
- "beta": 0,
- "creation": "2017-10-20 11:43:47.712616",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "field:maintenance_team_name",
+ "creation": "2017-10-20 11:43:47.712616",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "maintenance_team_name",
+ "maintenance_manager",
+ "maintenance_manager_name",
+ "column_break_2",
+ "company",
+ "section_break_2",
+ "maintenance_team_members"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "maintenance_team_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Maintenance Team Name",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "maintenance_team_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Maintenance Team Name",
+ "reqd": 1,
+ "unique": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "maintenance_manager",
- "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": "Maintenance Manager",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "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": "maintenance_manager",
+ "fieldtype": "Link",
+ "label": "Maintenance Manager",
+ "options": "User"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"fetch_from": "maintenance_manager.full_name",
- "fieldname": "maintenance_manager_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": "Maintenance Manager 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
- },
+ "fieldname": "maintenance_manager_name",
+ "fieldtype": "Read Only",
+ "label": "Maintenance Manager Name"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "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_2",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_2",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break",
+ "label": "Team"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "maintenance_team_members",
- "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": "Maintenance Team Members",
- "length": 0,
- "no_copy": 0,
- "options": "Maintenance Team Member",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldname": "maintenance_team_members",
+ "fieldtype": "Table",
+ "label": "Maintenance Team Members",
+ "options": "Maintenance Team Member",
+ "reqd": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-05-16 22:43:24.195349",
- "modified_by": "Administrator",
- "module": "Assets",
- "name": "Asset Maintenance Team",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-01-22 15:09:03.347345",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Maintenance Team",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Manufacturing User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing User",
+ "share": 1,
"write": 1
}
- ],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json
index 3472ab5..bdce639 100644
--- a/erpnext/assets/doctype/asset_movement/asset_movement.json
+++ b/erpnext/assets/doctype/asset_movement/asset_movement.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"autoname": "format:ACC-ASM-{YYYY}-{#####}",
"creation": "2016-04-25 18:00:23.559973",
@@ -91,8 +92,10 @@
"fieldtype": "Column Break"
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2019-11-23 13:28:47.256935",
+ "links": [],
+ "modified": "2021-01-22 12:30:55.295670",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Movement",
diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json
index 6df6e27..d338fc0 100644
--- a/erpnext/assets/doctype/asset_repair/asset_repair.json
+++ b/erpnext/assets/doctype/asset_repair/asset_repair.json
@@ -1,763 +1,208 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "naming_series:",
- "beta": 0,
- "creation": "2017-10-23 11:38:54.004355",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "naming_series:",
+ "creation": "2017-10-23 11:38:54.004355",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "naming_series",
+ "asset_name",
+ "column_break_2",
+ "item_code",
+ "item_name",
+ "section_break_5",
+ "failure_date",
+ "assign_to",
+ "assign_to_name",
+ "column_break_6",
+ "completion_date",
+ "repair_status",
+ "repair_cost",
+ "section_break_9",
+ "description",
+ "column_break_9",
+ "actions_performed",
+ "section_break_17",
+ "downtime",
+ "column_break_19",
+ "amended_from"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "asset_name",
- "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": "Asset Name",
- "length": 0,
- "no_copy": 0,
- "options": "Asset",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 1,
+ "fieldname": "asset_name",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Asset",
+ "options": "Asset",
+ "reqd": 1
+ },
{
- "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": 0,
- "options": "ACC-ASR-.YYYY.-",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "options": "ACC-ASR-.YYYY.-",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "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_2",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "asset_name.item_code",
- "fieldname": "item_code",
- "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": "Item Code",
- "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": "asset_name.item_code",
+ "fieldname": "item_code",
+ "fieldtype": "Read Only",
+ "label": "Item Code"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "asset_name.item_name",
- "fieldname": "item_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": "Item 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": "asset_name.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Read Only",
+ "label": "Item Name"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "label": "Repair Details"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 1,
- "fieldname": "failure_date",
- "fieldtype": "Datetime",
- "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": "Failure Date",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "columns": 1,
+ "fieldname": "failure_date",
+ "fieldtype": "Datetime",
+ "label": "Failure Date",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "assign_to",
- "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": "Assign To",
- "length": 0,
- "no_copy": 0,
- "options": "User",
- "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,
+ "fieldname": "assign_to",
+ "fieldtype": "Link",
+ "label": "Assign To",
+ "options": "User"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_from": "assign_to.full_name",
- "fieldname": "assign_to_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": "Assign To 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
- },
+ "allow_on_submit": 1,
+ "fetch_from": "assign_to.full_name",
+ "fieldname": "assign_to_name",
+ "fieldtype": "Read Only",
+ "label": "Assign To Name"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_6",
- "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_6",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "completion_date",
- "fieldtype": "Datetime",
- "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": "Completion Date",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "allow_on_submit": 1,
+ "fieldname": "completion_date",
+ "fieldtype": "Datetime",
+ "label": "Completion Date"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "Pending",
- "fieldname": "repair_status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Repair Status",
- "length": 0,
- "no_copy": 1,
- "options": "Pending\nCompleted\nCancelled",
- "permlevel": 0,
- "precision": "",
- "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,
- "width": ""
- },
+ "allow_on_submit": 1,
+ "default": "Pending",
+ "fieldname": "repair_status",
+ "fieldtype": "Select",
+ "label": "Repair Status",
+ "no_copy": 1,
+ "options": "Pending\nCompleted\nCancelled",
+ "print_hide": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_9",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_9",
+ "fieldtype": "Section Break",
+ "label": "Description"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Long Text",
- "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": "Error Description",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "description",
+ "fieldtype": "Long Text",
+ "label": "Error Description",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_9",
- "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_9",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "actions_performed",
- "fieldtype": "Long Text",
- "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": "Actions performed",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "allow_on_submit": 1,
+ "fieldname": "actions_performed",
+ "fieldtype": "Long Text",
+ "label": "Actions performed"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_17",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_17",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "downtime",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Downtime",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "allow_on_submit": 1,
+ "fieldname": "downtime",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Downtime",
+ "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_19",
- "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_19",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 1,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "repair_cost",
- "fieldtype": "Currency",
- "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": "Repair Cost",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "allow_on_submit": 1,
+ "fieldname": "repair_cost",
+ "fieldtype": "Currency",
+ "label": "Repair Cost"
+ },
{
- "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": "Asset Repair",
- "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": "Asset Repair",
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 1,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-08-21 14:44:27.181876",
- "modified_by": "Administrator",
- "module": "Assets",
- "name": "Asset Repair",
- "name_case": "",
- "owner": "Administrator",
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2021-01-22 15:08:12.495850",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Asset Repair",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Manufacturing Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing Manager",
+ "share": 1,
+ "submit": 1,
"write": 1
- },
+ },
{
- "amend": 1,
- "cancel": 1,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "Quality Manager",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 1,
+ "amend": 1,
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Quality Manager",
+ "share": 1,
+ "submit": 1,
"write": 1
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "title_field": "",
- "track_changes": 1,
- "track_seen": 1,
- "track_views": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1,
+ "track_seen": 1
}
\ No newline at end of file
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
index a6e6974..79c8861 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.js
@@ -1,6 +1,8 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
+
frappe.ui.form.on('Asset Value Adjustment', {
setup: function(frm) {
frm.add_fetch('company', 'cost_center', 'cost_center');
@@ -13,11 +15,19 @@
}
});
},
+
onload: function(frm) {
if(frm.is_new() && frm.doc.asset) {
frm.trigger("set_current_asset_value");
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
asset: function(frm) {
frm.trigger("set_current_asset_value");
},
diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json
index 3236e72..57e04e2 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2018-05-11 00:22:43.695151",
"doctype": "DocType",
"editable_grid": 1,
@@ -7,14 +8,16 @@
"company",
"asset",
"asset_category",
- "finance_book",
- "journal_entry",
"column_break_4",
"date",
+ "finance_book",
+ "amended_from",
+ "value_details_section",
"current_asset_value",
"new_asset_value",
+ "column_break_11",
"difference_amount",
- "amended_from",
+ "journal_entry",
"accounting_dimensions_section",
"cost_center",
"dimension_col_break"
@@ -108,10 +111,21 @@
{
"fieldname": "dimension_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "value_details_section",
+ "fieldtype": "Section Break",
+ "label": "Value Details"
+ },
+ {
+ "fieldname": "column_break_11",
+ "fieldtype": "Column Break"
}
],
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2019-11-22 14:09:25.800375",
+ "links": [],
+ "modified": "2021-01-22 14:10:23.085181",
"modified_by": "Administrator",
"module": "Assets",
"name": "Asset Value Adjustment",
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 fd702c7..1430827 100644
--- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
+++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py
@@ -13,19 +13,16 @@
class AssetValueAdjustment(Document):
def validate(self):
self.validate_date()
- self.set_difference_amount()
self.set_current_asset_value()
+ self.set_difference_amount()
def on_submit(self):
self.make_depreciation_entry()
self.reschedule_depreciations(self.new_asset_value)
def on_cancel(self):
- if self.journal_entry:
- frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry))
-
self.reschedule_depreciations(self.current_asset_value)
-
+
def validate_date(self):
asset_purchase_date = frappe.db.get_value('Asset', self.asset, 'purchase_date')
if getdate(self.date) < getdate(asset_purchase_date):
@@ -53,6 +50,7 @@
je.posting_date = self.date
je.company = self.company
je.remark = "Depreciation Entry against {0} worth {1}".format(self.asset, self.difference_amount)
+ je.finance_book = self.finance_book
credit_entry = {
"account": accumulated_depreciation_account,
@@ -78,7 +76,7 @@
debit_entry.update({
dimension['fieldname']: self.get(dimension['fieldname']) or dimension.get('default_dimension')
})
-
+
je.append("accounts", credit_entry)
je.append("accounts", debit_entry)
diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
index af08a2a..d1457b9 100644
--- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
+++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py
@@ -75,24 +75,23 @@
for asset in assets_record:
asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \
- flt(depreciation_amount_map.get(asset.name))
- if asset_value:
- row = {
- "asset_id": asset.asset_id,
- "asset_name": asset.asset_name,
- "status": asset.status,
- "department": asset.department,
- "cost_center": asset.cost_center,
- "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice),
- "gross_purchase_amount": asset.gross_purchase_amount,
- "opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
- "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
- "available_for_use_date": asset.available_for_use_date,
- "location": asset.location,
- "asset_category": asset.asset_category,
- "purchase_date": asset.purchase_date,
- "asset_value": asset_value
- }
- data.append(row)
+ row = {
+ "asset_id": asset.asset_id,
+ "asset_name": asset.asset_name,
+ "status": asset.status,
+ "department": asset.department,
+ "cost_center": asset.cost_center,
+ "vendor_name": pr_supplier_map.get(asset.purchase_receipt) or pi_supplier_map.get(asset.purchase_invoice),
+ "gross_purchase_amount": asset.gross_purchase_amount,
+ "opening_accumulated_depreciation": asset.opening_accumulated_depreciation,
+ "depreciated_amount": depreciation_amount_map.get(asset.asset_id) or 0.0,
+ "available_for_use_date": asset.available_for_use_date,
+ "location": asset.location,
+ "asset_category": asset.asset_category,
+ "purchase_date": asset.purchase_date,
+ "asset_value": asset_value
+ }
+ data.append(row)
return data
diff --git a/erpnext/assets/workspace/assets/assets.json b/erpnext/assets/workspace/assets/assets.json
new file mode 100644
index 0000000..c401581
--- /dev/null
+++ b/erpnext/assets/workspace/assets/assets.json
@@ -0,0 +1,193 @@
+{
+ "category": "Modules",
+ "charts": [
+ {
+ "chart_name": "Asset Value Analytics",
+ "label": "Asset Value Analytics"
+ }
+ ],
+ "creation": "2020-03-02 15:43:27.634865",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "assets",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Assets",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Assets",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Asset",
+ "link_to": "Asset",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Location",
+ "link_to": "Location",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Asset Category",
+ "link_to": "Asset Category",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Asset Movement",
+ "link_to": "Asset Movement",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Maintenance",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Asset Maintenance Team",
+ "link_to": "Asset Maintenance Team",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Asset Maintenance Team",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Asset Maintenance",
+ "link_to": "Asset Maintenance",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Asset Maintenance",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Asset Maintenance Log",
+ "link_to": "Asset Maintenance Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Asset",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Asset Value Adjustment",
+ "link_to": "Asset Value Adjustment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Asset",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Asset Repair",
+ "link_to": "Asset Repair",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Asset",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Asset Depreciation Ledger",
+ "link_to": "Asset Depreciation Ledger",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Asset",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Asset Depreciations and Balances",
+ "link_to": "Asset Depreciations and Balances",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Asset Maintenance",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Asset Maintenance",
+ "link_to": "Asset Maintenance",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:37.977119",
+ "modified_by": "Administrator",
+ "module": "Assets",
+ "name": "Assets",
+ "onboarding": "Assets",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "label": "Asset",
+ "link_to": "Asset",
+ "type": "DocType"
+ },
+ {
+ "label": "Asset Category",
+ "link_to": "Asset Category",
+ "type": "DocType"
+ },
+ {
+ "label": "Fixed Asset Register",
+ "link_to": "Fixed Asset Register",
+ "type": "Report"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "Asset",
+ "type": "Dashboard"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/buying/desk_page/buying/buying.json b/erpnext/buying/desk_page/buying/buying.json
deleted file mode 100644
index 2e870fe..0000000
--- a/erpnext/buying/desk_page/buying/buying.json
+++ /dev/null
@@ -1,113 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Buying",
- "links": "[ \n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Request for purchase.\",\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"description\": \"Purchase Orders given to Suppliers.\",\n \"label\": \"Purchase Order\",\n \"name\": \"Purchase Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Invoice\",\n \"name\": \"Purchase Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"description\": \"Request for quotation.\",\n \"label\": \"Request for Quotation\",\n \"name\": \"Request for Quotation\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"description\": \"Quotations received from Suppliers.\",\n \"label\": \"Supplier Quotation\",\n \"name\": \"Supplier Quotation\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Items & Pricing",
- "links": "[\n {\n \"description\": \"All Products or Services.\",\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Multiple Item prices.\",\n \"label\": \"Item Price\",\n \"name\": \"Item Price\",\n \"onboard\": 1,\n \"route\": \"#Report/Item Price\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Price List master.\",\n \"label\": \"Price List\",\n \"name\": \"Price List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Bundle items at time of sale.\",\n \"label\": \"Product Bundle\",\n \"name\": \"Product Bundle\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tree of Item Groups.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Item Group\",\n \"link\": \"Tree/Item Group\",\n \"name\": \"Item Group\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Rules for applying different promotional schemes.\",\n \"label\": \"Promotional Scheme\",\n \"name\": \"Promotional Scheme\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Rules for applying pricing and discount.\",\n \"label\": \"Pricing Rule\",\n \"name\": \"Pricing Rule\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Settings",
- "links": "[\n {\n \"description\": \"Default settings for buying transactions.\",\n \"label\": \"Buying Settings\",\n \"name\": \"Buying Settings\",\n \"settings\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tax template for buying transactions.\",\n \"label\": \"Purchase Taxes and Charges Template\",\n \"name\": \"Purchase Taxes and Charges Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Template of terms or contract.\",\n \"label\": \"Terms and Conditions Template\",\n \"name\": \"Terms and Conditions\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Supplier",
- "links": "[\n {\n \"description\": \"Supplier database.\",\n \"label\": \"Supplier\",\n \"name\": \"Supplier\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Supplier Group master.\",\n \"label\": \"Supplier Group\",\n \"name\": \"Supplier Group\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Contacts.\",\n \"label\": \"Contact\",\n \"name\": \"Contact\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Addresses.\",\n \"label\": \"Address\",\n \"name\": \"Address\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Supplier Scorecard",
- "links": "[\n {\n \"description\": \"All Supplier scorecards.\",\n \"label\": \"Supplier Scorecard\",\n \"name\": \"Supplier Scorecard\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Templates of supplier scorecard variables.\",\n \"label\": \"Supplier Scorecard Variable\",\n \"name\": \"Supplier Scorecard Variable\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Templates of supplier scorecard criteria.\",\n \"label\": \"Supplier Scorecard Criteria\",\n \"name\": \"Supplier Scorecard Criteria\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Templates of supplier standings.\",\n \"label\": \"Supplier Scorecard Standing\",\n \"name\": \"Supplier Scorecard Standing\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Key Reports",
- "links": "[\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Analytics\",\n \"name\": \"Purchase Analytics\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier-Wise Sales Analytics\",\n \"name\": \"Supplier-Wise Sales Analytics\",\n \"onboard\": 1,\n \"reference_doctype\": \"Stock Ledger Entry\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Requested Items to Order\",\n \"name\": \"Requested Items to Order\",\n \"onboard\": 1,\n \"reference_doctype\": \"Material Request\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Order Trends\",\n \"name\": \"Purchase Order Trends\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Procurement Tracker\",\n \"name\": \"Procurement Tracker\",\n \"onboard\": 1,\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Other Reports",
- "links": "[\n {\n \"is_query_report\": true,\n \"label\": \"Items To Be Requested\",\n \"name\": \"Items To Be Requested\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Item-wise Purchase History\",\n \"name\": \"Item-wise Purchase History\",\n \"onboard\": 1,\n \"reference_doctype\": \"Item\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"reference_doctype\": \"Purchase Receipt\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Purchase Invoice Trends\",\n \"name\": \"Purchase Invoice Trends\",\n \"reference_doctype\": \"Purchase Invoice\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Raw Materials To Be Transferred\",\n \"name\": \"Subcontracted Raw Materials To Be Transferred\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Subcontracted Item To Be Received\",\n \"name\": \"Subcontracted Item To Be Received\",\n \"reference_doctype\": \"Purchase Order\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier Quotation Comparison\",\n \"name\": \"Supplier Quotation Comparison\",\n \"onboard\": 1,\n \"reference_doctype\": \"Supplier Quotation\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Material Requests for which Supplier Quotations are not created\",\n \"name\": \"Material Requests for which Supplier Quotations are not created\",\n \"reference_doctype\": \"Material Request\",\n \"type\": \"report\"\n },\n {\n \"is_query_report\": true,\n \"label\": \"Supplier Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"reference_doctype\": \"Address\",\n \"route_options\": {\n \"party_type\": \"Supplier\"\n },\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Regional",
- "links": "[\n {\n \"description\": \"Import Italian Purchase Invoices\",\n \"label\": \"Import Supplier Invoice\",\n \"name\": \"Import Supplier Invoice\",\n \"type\": \"doctype\"\n } \n]"
- }
- ],
- "cards_label": "",
- "category": "Modules",
- "charts": [
- {
- "chart_name": "Purchase Order Trends",
- "label": "Purchase Order Trends"
- }
- ],
- "charts_label": "",
- "creation": "2020-01-28 11:50:26.195467",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Buying",
- "modified": "2020-09-30 14:40:55.638458",
- "modified_by": "Administrator",
- "module": "Buying",
- "name": "Buying",
- "onboarding": "Buying",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "color": "#cef6d1",
- "format": "{} Available",
- "label": "Item",
- "link_to": "Item",
- "stats_filter": "{\n \"disabled\": 0\n}",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} Pending",
- "label": "Material Request",
- "link_to": "Material Request",
- "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"Pending\"\n}",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} To Receive",
- "label": "Purchase Order",
- "link_to": "Purchase Order",
- "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\":[\"in\", [\"To Receive\", \"To Receive and Bill\"]]\n}",
- "type": "DocType"
- },
- {
- "label": "Purchase Analytics",
- "link_to": "Purchase Analytics",
- "type": "Report"
- },
- {
- "label": "Purchase Order Analysis",
- "link_to": "Purchase Order Analysis",
- "type": "Report"
- },
- {
- "label": "Dashboard",
- "link_to": "Buying",
- "type": "Dashboard"
- }
- ],
- "shortcuts_label": ""
-}
\ No newline at end of file
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js
index 47483c9..dd0f065 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.js
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.js
@@ -2,7 +2,7 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.buying");
-
+frappe.provide("erpnext.accounts.dimensions");
{% include 'erpnext/public/js/controllers/buying.js' %};
frappe.ui.form.on("Purchase Order", {
@@ -30,6 +30,10 @@
},
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
onload: function(frm) {
set_schedule_date(frm);
if (!frm.doc.transaction_date){
@@ -39,6 +43,8 @@
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
}
});
@@ -58,8 +64,8 @@
erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend({
setup: function() {
this.frm.custom_make_buttons = {
- 'Purchase Receipt': 'Receipt',
- 'Purchase Invoice': 'Invoice',
+ 'Purchase Receipt': 'Purchase Receipt',
+ 'Purchase Invoice': 'Purchase Invoice',
'Stock Entry': 'Material to Supplier',
'Payment Entry': 'Payment',
}
@@ -158,16 +164,16 @@
if (doc.docstatus === 1 && !doc.inter_company_order_reference) {
let me = this;
- frappe.model.with_doc("Supplier", me.frm.doc.supplier, () => {
- let supplier = frappe.model.get_doc("Supplier", me.frm.doc.supplier);
- let internal = supplier.is_internal_supplier;
- let disabled = supplier.disabled;
- if (internal === 1 && disabled === 0) {
- me.frm.add_custom_button("Inter Company Order", function() {
- me.make_inter_company_order(me.frm);
- }, __('Create'));
- }
- });
+ let internal = me.frm.doc.is_internal_supplier;
+ if (internal) {
+ let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Sales Order" :
+ "Inter Company Sales Order";
+
+ me.frm.add_custom_button(button_label, function() {
+ me.make_inter_company_order(me.frm);
+ }, __('Create'));
+ }
+
}
}
@@ -347,7 +353,8 @@
make_purchase_receipt: function() {
frappe.model.open_mapped_doc({
method: "erpnext.buying.doctype.purchase_order.purchase_order.make_purchase_receipt",
- frm: cur_frm
+ frm: cur_frm,
+ freeze_message: __("Creating Purchase Receipt ...")
})
},
@@ -374,7 +381,7 @@
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
- per_ordered: ["<", 99.99],
+ per_ordered: ["<", 100],
company: me.frm.doc.company
}
})
diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.json b/erpnext/buying/doctype/purchase_order/purchase_order.json
index 71231f6..ee2beea 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.json
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.json
@@ -134,6 +134,8 @@
"ref_sq",
"column_break_74",
"party_account_currency",
+ "is_internal_supplier",
+ "represents_company",
"inter_company_order_reference"
],
"fields": [
@@ -168,6 +170,7 @@
"bold": 1,
"fieldname": "supplier",
"fieldtype": "Link",
+ "in_global_search": 1,
"in_standard_filter": 1,
"label": "Supplier",
"oldfieldname": "supplier",
@@ -1100,13 +1103,28 @@
{
"fieldname": "items_col_break",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fetch_from": "supplier.is_internal_supplier",
+ "fieldname": "is_internal_supplier",
+ "fieldtype": "Check",
+ "label": "Is Internal Supplier"
+ },
+ {
+ "fetch_from": "supplier.represents_company",
+ "fieldname": "represents_company",
+ "fieldtype": "Link",
+ "label": "Represents Company",
+ "options": "Company",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 13:58:14.697921",
+ "modified": "2021-01-20 22:07:23.487138",
"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 c7efb8a..d32e98e 100644
--- a/erpnext/buying/doctype/purchase_order/purchase_order.py
+++ b/erpnext/buying/doctype/purchase_order/purchase_order.py
@@ -123,8 +123,8 @@
if self.is_subcontracted == "Yes":
for item in self.items:
if not item.bom:
- frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}"\
- .format(item.item_code, item.idx)))
+ frappe.throw(_("BOM is not specified for subcontracting item {0} at row {1}")
+ .format(item.item_code, item.idx))
def get_schedule_dates(self):
for d in self.get('items'):
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
index 10db240..75b2954 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json
@@ -40,6 +40,7 @@
"base_rate",
"base_amount",
"pricing_rules",
+ "stock_uom_rate",
"is_free_item",
"section_break_29",
"net_rate",
@@ -726,13 +727,21 @@
"fieldname": "more_info_section_break",
"fieldtype": "Section Break",
"label": "More Information"
+ },
+ {
+ "depends_on": "eval: doc.uom != doc.stock_uom",
+ "fieldname": "stock_uom_rate",
+ "fieldtype": "Currency",
+ "label": "Rate of Stock UOM",
+ "options": "currency",
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-10-30 11:59:47.670951",
+ "modified": "2021-01-30 21:44:41.816974",
"modified_by": "Administrator",
"module": "Buying",
"name": "Purchase Order Item",
diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py
index b711e36..8bdcd47 100644
--- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py
+++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.py
@@ -6,11 +6,8 @@
from frappe.model.document import Document
-from erpnext.controllers.print_settings import print_settings_for_item_table
-
class PurchaseOrderItem(Document):
- def __setup__(self):
- print_settings_for_item_table(self)
+ pass
def on_doctype_update():
frappe.db.add_index("Purchase Order Item", ["item_code", "warehouse"])
\ No newline at end of file
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
index c427242..b76c378 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.js
@@ -224,7 +224,7 @@
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
- per_ordered: ["<", 99.99],
+ per_ordered: ["<", 100],
company: me.frm.doc.company
}
})
@@ -280,7 +280,7 @@
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
- per_ordered: ["<", 99.99]
+ per_ordered: ["<", 100]
}
});
dialog.hide();
@@ -290,11 +290,17 @@
dialog.show();
}, __("Get Items From"));
+ // Link Material Requests
+ this.frm.add_custom_button(__('Link to Material Requests'),
+ function() {
+ erpnext.buying.link_to_mrs(me.frm);
+ }, __("Tools"));
+
// Get Suppliers
this.frm.add_custom_button(__('Get Suppliers'),
function() {
me.get_suppliers_button(me.frm);
- });
+ }, __("Tools"));
}
},
diff --git a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
index 3af6cf8..4ce4100 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.json
@@ -18,7 +18,6 @@
"suppliers",
"items_section",
"items",
- "link_to_mrs",
"supplier_response_section",
"salutation",
"subject",
@@ -118,13 +117,6 @@
"reqd": 1
},
{
- "depends_on": "eval:doc.docstatus===0 && (doc.items && doc.items.length)",
- "fieldname": "link_to_mrs",
- "fieldtype": "Button",
- "label": "Link to Material Requests"
- },
- {
- "depends_on": "eval:!doc.__islocal",
"fieldname": "supplier_response_section",
"fieldtype": "Section Break",
"label": "Email Details"
@@ -260,7 +252,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-04 22:04:29.017134",
+ "modified": "2020-11-05 22:04:29.017134",
"modified_by": "Administrator",
"module": "Buying",
"name": "Request for Quotation",
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 a51498e..7cf22f8 100644
--- a/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
+++ b/erpnext/buying/doctype/request_for_quotation/request_for_quotation.py
@@ -127,6 +127,10 @@
'link_doctype': 'Supplier',
'link_name': rfq_supplier.supplier
})
+ contact.append('email_ids', {
+ 'email_id': user.name,
+ 'is_primary': 1
+ })
if not contact.email_id and not contact.user:
contact.email_id = user.name
diff --git a/erpnext/buying/doctype/supplier/supplier.json b/erpnext/buying/doctype/supplier/supplier.json
index 40362b1..4cc5753 100644
--- a/erpnext/buying/doctype/supplier/supplier.json
+++ b/erpnext/buying/doctype/supplier/supplier.json
@@ -26,7 +26,6 @@
"supplier_group",
"supplier_type",
"pan",
- "language",
"allow_purchase_invoice_creation_without_purchase_order",
"allow_purchase_invoice_creation_without_purchase_receipt",
"disabled",
@@ -57,6 +56,7 @@
"website",
"supplier_details",
"column_break_30",
+ "language",
"is_frozen"
],
"fields": [
@@ -384,7 +384,7 @@
"idx": 370,
"image_field": "image",
"links": [],
- "modified": "2020-06-17 23:18:20",
+ "modified": "2021-01-06 19:51:40.939087",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier",
diff --git a/erpnext/buying/doctype/supplier/supplier.py b/erpnext/buying/doctype/supplier/supplier.py
index df143ee..edeb135 100644
--- a/erpnext/buying/doctype/supplier/supplier.py
+++ b/erpnext/buying/doctype/supplier/supplier.py
@@ -49,6 +49,15 @@
msgprint(_("Series is mandatory"), raise_exception=1)
validate_party_accounts(self)
+ self.validate_internal_supplier()
+
+ def validate_internal_supplier(self):
+ internal_supplier = frappe.db.get_value("Supplier",
+ {"is_internal_supplier": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name")
+
+ if internal_supplier:
+ frappe.throw(_("Internal Supplier for company {0} already exists").format(
+ frappe.bold(self.represents_company)))
def on_trash(self):
delete_contact_and_address('Supplier', self.name)
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
index a7cab50..a0187b0 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.js
@@ -44,12 +44,18 @@
material_request_type: "Purchase",
docstatus: 1,
status: ["!=", "Stopped"],
- per_ordered: ["<", 99.99],
+ per_ordered: ["<", 100],
company: me.frm.doc.company
}
})
}, __("Get Items From"));
+ // Link Material Requests
+ this.frm.add_custom_button(__('Link to Material Requests'),
+ function() {
+ erpnext.buying.link_to_mrs(me.frm);
+ }, __("Tools"));
+
this.frm.add_custom_button(__("Request for Quotation"),
function() {
if (!me.frm.doc.supplier) {
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
index b39c989..40fbe2c 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.json
@@ -35,7 +35,6 @@
"ignore_pricing_rule",
"items_section",
"items",
- "link_to_mrs",
"pricing_rule_details",
"pricing_rules",
"section_break_22",
@@ -323,12 +322,6 @@
"reqd": 1
},
{
- "depends_on": "eval:doc.docstatus===0 && (doc.items && doc.items.length)",
- "fieldname": "link_to_mrs",
- "fieldtype": "Button",
- "label": "Link to material requests"
- },
- {
"fieldname": "pricing_rule_details",
"fieldtype": "Section Break",
"label": "Pricing Rules"
@@ -806,9 +799,10 @@
],
"icon": "fa fa-shopping-cart",
"idx": 29,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 13:58:33.043971",
+ "modified": "2020-12-03 15:18:29.073368",
"modified_by": "Administrator",
"module": "Buying",
"name": "Supplier Quotation",
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
index ae5611f..6a4c02c 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation.py
@@ -71,7 +71,7 @@
doc_sup = doc_sup[0] if doc_sup else None
if not doc_sup:
frappe.throw(_("Supplier {0} not found in {1}").format(self.supplier,
- "<a href='desk#Form/Request for Quotation/{0}'> Request for Quotation {0} </a>".format(doc.name)))
+ "<a href='desk/app/Form/Request for Quotation/{0}'> Request for Quotation {0} </a>".format(doc.name)))
quote_status = _('Received')
for item in doc.items:
diff --git a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js
index 9f4fece..5ab6c98 100644
--- a/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js
+++ b/erpnext/buying/doctype/supplier_quotation/supplier_quotation_list.js
@@ -4,9 +4,9 @@
if(doc.status==="Ordered") {
return [__("Ordered"), "green", "status,=,Ordered"];
} else if(doc.status==="Rejected") {
- return [__("Lost"), "darkgrey", "status,=,Lost"];
+ return [__("Lost"), "gray", "status,=,Lost"];
} else if(doc.status==="Expired") {
- return [__("Expired"), "darkgrey", "status,=,Expired"];
+ return [__("Expired"), "gray", "status,=,Expired"];
}
}
};
diff --git a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py
index f24e5be..64dda87 100644
--- a/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py
+++ b/erpnext/buying/doctype/supplier_quotation_item/supplier_quotation_item.py
@@ -6,8 +6,5 @@
from frappe.model.document import Document
-from erpnext.controllers.print_settings import print_settings_for_item_table
-
class SupplierQuotationItem(Document):
- def __setup__(self):
- print_settings_for_item_table(self)
+ pass
diff --git a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js
index c50916e..dc5474e 100644
--- a/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js
+++ b/erpnext/buying/doctype/supplier_scorecard/supplier_scorecard_list.js
@@ -10,7 +10,7 @@
if (doc.indicator_color) {
return [__(doc.status), doc.indicator_color.toLowerCase(), "status,=," + doc.status];
} else {
- return [__("Unknown"), "darkgrey", "status,=,''"];
+ return [__("Unknown"), "gray", "status,=,''"];
}
},
diff --git a/erpnext/buying/report/purchase_analytics/purchase_analytics.js b/erpnext/buying/report/purchase_analytics/purchase_analytics.js
index e17973c..ba8535a 100644
--- a/erpnext/buying/report/purchase_analytics/purchase_analytics.js
+++ b/erpnext/buying/report/purchase_analytics/purchase_analytics.js
@@ -75,62 +75,70 @@
return Object.assign(options, {
checkboxColumn: true,
events: {
- onCheckRow: function(data) {
+ onCheckRow: function (data) {
+ if (!data) return;
+
+ const data_doctype = $(
+ data[2].html
+ )[0].attributes.getNamedItem("data-doctype").value;
+ const tree_type = frappe.query_report.filters[0].value;
+ if (data_doctype != tree_type) return;
+
row_name = data[2].content;
length = data.length;
- var tree_type = frappe.query_report.filters[0].value;
-
- if(tree_type == "Supplier" || tree_type == "Item") {
- row_values = data.slice(4,length-1).map(function (column) {
- return column.content;
- })
- }
- else {
- row_values = data.slice(3,length-1).map(function (column) {
- return column.content;
- })
+ if (tree_type == "Supplier") {
+ row_values = data
+ .slice(4, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
+ } else if (tree_type == "Item") {
+ row_values = data
+ .slice(5, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
+ } else {
+ row_values = data
+ .slice(3, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
}
- entry = {
- 'name':row_name,
- 'values':row_values
- }
+ entry = {
+ name: row_name,
+ values: row_values,
+ };
let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets;
- var found = false;
-
- for(var i=0; i < new_datasets.length;i++){
- if(new_datasets[i].name == row_name){
- found = true;
- new_datasets.splice(i,1);
- break;
+ let element_found = new_datasets.some((element, index, array)=>{
+ if(element.name == row_name){
+ array.splice(index, 1)
+ return true
}
- }
+ return false
+ })
- if(!found){
+ if (!element_found) {
new_datasets.push(entry);
}
-
let new_data = {
labels: raw_data.labels,
- datasets: new_datasets
- }
-
- setTimeout(() => {
- frappe.query_report.chart.update(new_data)
- },500)
-
-
- setTimeout(() => {
- frappe.query_report.chart.draw(true);
- }, 1000)
+ datasets: new_datasets,
+ };
+ chart_options = {
+ data: new_data,
+ type: "line",
+ };
+ frappe.query_report.render_chart(chart_options);
frappe.query_report.raw_chart_data = new_data;
},
- }
+ },
});
}
}
diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py
index 47b4866..a73cb0d 100644
--- a/erpnext/buying/utils.py
+++ b/erpnext/buying/utils.py
@@ -35,9 +35,10 @@
frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx))
# update last purchsae rate
- if last_purchase_rate:
- frappe.db.sql("""update `tabItem` set last_purchase_rate = %s where name = %s""",
- (flt(last_purchase_rate), d.item_code))
+ frappe.db.set_value('Item', d.item_code, 'last_purchase_rate', flt(last_purchase_rate))
+
+
+
def validate_for_items(doc):
items = []
diff --git a/erpnext/buying/workspace/buying/buying.json b/erpnext/buying/workspace/buying/buying.json
new file mode 100644
index 0000000..6c9c0f3
--- /dev/null
+++ b/erpnext/buying/workspace/buying/buying.json
@@ -0,0 +1,520 @@
+{
+ "cards_label": "",
+ "category": "Modules",
+ "charts": [
+ {
+ "chart_name": "Purchase Order Trends",
+ "label": "Purchase Order Trends"
+ }
+ ],
+ "charts_label": "",
+ "creation": "2020-01-28 11:50:26.195467",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "buying",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Buying",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Buying",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Material Request",
+ "link_to": "Material Request",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Supplier",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Purchase Order",
+ "link_to": "Purchase Order",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Supplier",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Purchase Invoice",
+ "link_to": "Purchase Invoice",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Supplier",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Request for Quotation",
+ "link_to": "Request for Quotation",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Supplier",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier Quotation",
+ "link_to": "Supplier Quotation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Items & Pricing",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item",
+ "link_to": "Item",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Price",
+ "link_to": "Item Price",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Price List",
+ "link_to": "Price List",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Product Bundle",
+ "link_to": "Product Bundle",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Group",
+ "link_to": "Item Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Promotional Scheme",
+ "link_to": "Promotional Scheme",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Pricing Rule",
+ "link_to": "Pricing Rule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Buying Settings",
+ "link_to": "Buying Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Purchase Taxes and Charges Template",
+ "link_to": "Purchase Taxes and Charges Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Terms and Conditions Template",
+ "link_to": "Terms and Conditions",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier",
+ "link_to": "Supplier",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier Group",
+ "link_to": "Supplier Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Contact",
+ "link_to": "Contact",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Address",
+ "link_to": "Address",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier Scorecard",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier Scorecard",
+ "link_to": "Supplier Scorecard",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier Scorecard Variable",
+ "link_to": "Supplier Scorecard Variable",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier Scorecard Criteria",
+ "link_to": "Supplier Scorecard Criteria",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier Scorecard Standing",
+ "link_to": "Supplier Scorecard Standing",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Key Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Purchase Analytics",
+ "link_to": "Purchase Analytics",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Purchase Order Analysis",
+ "link_to": "Purchase Order Analysis",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Supplier-Wise Sales Analytics",
+ "link_to": "Supplier-Wise Sales Analytics",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Items to Order and Receive",
+ "link_to": "Requested Items to Order and Receive",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Purchase Order Trends",
+ "link_to": "Purchase Order Trends",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Procurement Tracker",
+ "link_to": "Procurement Tracker",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Other Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Items To Be Requested",
+ "link_to": "Items To Be Requested",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Item-wise Purchase History",
+ "link_to": "Item-wise Purchase History",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Purchase Receipt Trends",
+ "link_to": "Purchase Receipt Trends",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Purchase Invoice Trends",
+ "link_to": "Purchase Invoice Trends",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Subcontracted Raw Materials To Be Transferred",
+ "link_to": "Subcontracted Raw Materials To Be Transferred",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Subcontracted Item To Be Received",
+ "link_to": "Subcontracted Item To Be Received",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Supplier Quotation Comparison",
+ "link_to": "Supplier Quotation Comparison",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Material Requests for which Supplier Quotations are not created",
+ "link_to": "Material Requests for which Supplier Quotations are not created",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Supplier Addresses And Contacts",
+ "link_to": "Address And Contacts",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Regional",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Import Supplier Invoice",
+ "link_to": "Import Supplier Invoice",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:38.615167",
+ "modified_by": "Administrator",
+ "module": "Buying",
+ "name": "Buying",
+ "onboarding": "Buying",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "color": "Green",
+ "format": "{} Available",
+ "label": "Item",
+ "link_to": "Item",
+ "stats_filter": "{\n \"disabled\": 0\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Yellow",
+ "format": "{} Pending",
+ "label": "Material Request",
+ "link_to": "Material Request",
+ "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"Pending\"\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Yellow",
+ "format": "{} To Receive",
+ "label": "Purchase Order",
+ "link_to": "Purchase Order",
+ "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\":[\"in\", [\"To Receive\", \"To Receive and Bill\"]]\n}",
+ "type": "DocType"
+ },
+ {
+ "label": "Purchase Analytics",
+ "link_to": "Purchase Analytics",
+ "type": "Report"
+ },
+ {
+ "label": "Purchase Order Analysis",
+ "link_to": "Purchase Order Analysis",
+ "type": "Report"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "Buying",
+ "type": "Dashboard"
+ }
+ ],
+ "shortcuts_label": ""
+}
\ No newline at end of file
diff --git a/erpnext/config/accounts.py b/erpnext/config/accounts.py
deleted file mode 100644
index 839c4ad..0000000
--- a/erpnext/config/accounts.py
+++ /dev/null
@@ -1,626 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-import frappe
-
-
-def get_data():
- config = [
- {
- "label": _("Accounts Receivable"),
- "items": [
- {
- "type": "doctype",
- "name": "Sales Invoice",
- "description": _("Bills raised to Customers."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Customer",
- "description": _("Customer database."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Payment Entry",
- "description": _("Bank/Cash transactions against party or for internal transfer")
- },
- {
- "type": "doctype",
- "name": "Payment Request",
- "description": _("Payment Request"),
- },
- {
- "type": "report",
- "name": "Accounts Receivable",
- "doctype": "Sales Invoice",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Accounts Receivable Summary",
- "doctype": "Sales Invoice",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Sales Register",
- "doctype": "Sales Invoice",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Item-wise Sales Register",
- "is_query_report": True,
- "doctype": "Sales Invoice"
- },
- {
- "type": "report",
- "name": "Ordered Items To Be Billed",
- "is_query_report": True,
- "doctype": "Sales Invoice"
- },
- {
- "type": "report",
- "name": "Delivered Items To Be Billed",
- "is_query_report": True,
- "doctype": "Sales Invoice"
- },
- ]
- },
- {
- "label": _("Accounts Payable"),
- "items": [
- {
- "type": "doctype",
- "name": "Purchase Invoice",
- "description": _("Bills raised by Suppliers."),
- "onboard": 1
- },
- {
- "type": "doctype",
- "name": "Supplier",
- "description": _("Supplier database."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Payment Entry",
- "description": _("Bank/Cash transactions against party or for internal transfer")
- },
- {
- "type": "report",
- "name": "Accounts Payable",
- "doctype": "Purchase Invoice",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Accounts Payable Summary",
- "doctype": "Purchase Invoice",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Purchase Register",
- "doctype": "Purchase Invoice",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Item-wise Purchase Register",
- "is_query_report": True,
- "doctype": "Purchase Invoice"
- },
- {
- "type": "report",
- "name": "Purchase Order Items To Be Billed",
- "is_query_report": True,
- "doctype": "Purchase Invoice"
- },
- {
- "type": "report",
- "name": "Received Items To Be Billed",
- "is_query_report": True,
- "doctype": "Purchase Invoice"
- },
- ]
- },
- {
- "label": _("Accounting Masters"),
- "items": [
- {
- "type": "doctype",
- "name": "Company",
- "description": _("Company (not Customer or Supplier) master."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Account",
- "icon": "fa fa-sitemap",
- "label": _("Chart of Accounts"),
- "route": "#Tree/Account",
- "description": _("Tree of financial accounts."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Accounts Settings",
- },
- {
- "type": "doctype",
- "name": "Fiscal Year",
- "description": _("Financial / accounting year.")
- },
- {
- "type": "doctype",
- "name": "Accounting Dimension",
- },
- {
- "type": "doctype",
- "name": "Finance Book",
- },
- {
- "type": "doctype",
- "name": "Accounting Period",
- },
- {
- "type": "doctype",
- "name": "Payment Term",
- "description": _("Payment Terms based on conditions")
- },
- ]
- },
- {
- "label": _("Banking and Payments"),
- "items": [
- {
- "type": "doctype",
- "label": _("Match Payments with Invoices"),
- "name": "Payment Reconciliation",
- "description": _("Match non-linked Invoices and Payments.")
- },
- {
- "type": "doctype",
- "label": _("Update Bank Clearance Dates"),
- "name": "Bank Clearance",
- "description": _("Update bank payment dates with journals.")
- },
- {
- "type": "doctype",
- "label": _("Invoice Discounting"),
- "name": "Invoice Discounting",
- },
- {
- "type": "report",
- "name": "Bank Reconciliation Statement",
- "is_query_report": True,
- "doctype": "Journal Entry"
- },{
- "type": "page",
- "name": "bank-reconciliation",
- "label": _("Bank Reconciliation"),
- "icon": "fa fa-bar-chart"
- },
- {
- "type": "report",
- "name": "Bank Clearance Summary",
- "is_query_report": True,
- "doctype": "Journal Entry"
- },
- {
- "type": "doctype",
- "name": "Bank Guarantee"
- },
- {
- "type": "doctype",
- "name": "Cheque Print Template",
- "description": _("Setup cheque dimensions for printing")
- },
- ]
- },
- {
- "label": _("General Ledger"),
- "items": [
- {
- "type": "doctype",
- "name": "Journal Entry",
- "description": _("Accounting journal entries.")
- },
- {
- "type": "report",
- "name": "General Ledger",
- "doctype": "GL Entry",
- "is_query_report": True,
- },
- {
- "type": "report",
- "name": "Customer Ledger Summary",
- "doctype": "Sales Invoice",
- "is_query_report": True,
- },
- {
- "type": "report",
- "name": "Supplier Ledger Summary",
- "doctype": "Sales Invoice",
- "is_query_report": True,
- },
- {
- "type": "doctype",
- "name": "Process Deferred Accounting"
- }
- ]
- },
- {
- "label": _("Taxes"),
- "items": [
- {
- "type": "doctype",
- "name": "Sales Taxes and Charges Template",
- "description": _("Tax template for selling transactions.")
- },
- {
- "type": "doctype",
- "name": "Purchase Taxes and Charges Template",
- "description": _("Tax template for buying transactions.")
- },
- {
- "type": "doctype",
- "name": "Item Tax Template",
- "description": _("Tax template for item tax rates.")
- },
- {
- "type": "doctype",
- "name": "Tax Category",
- "description": _("Tax Category for overriding tax rates.")
- },
- {
- "type": "doctype",
- "name": "Tax Rule",
- "description": _("Tax Rule for transactions.")
- },
- {
- "type": "doctype",
- "name": "Tax Withholding Category",
- "description": _("Tax Withholding rates to be applied on transactions.")
- },
- ]
- },
- {
- "label": _("Cost Center and Budgeting"),
- "items": [
- {
- "type": "doctype",
- "name": "Cost Center",
- "icon": "fa fa-sitemap",
- "label": _("Chart of Cost Centers"),
- "route": "#Tree/Cost Center",
- "description": _("Tree of financial Cost Centers."),
- },
- {
- "type": "doctype",
- "name": "Budget",
- "description": _("Define budget for a financial year.")
- },
- {
- "type": "doctype",
- "name": "Accounting Dimension",
- },
- {
- "type": "report",
- "name": "Budget Variance Report",
- "is_query_report": True,
- "doctype": "Cost Center"
- },
- {
- "type": "doctype",
- "name": "Monthly Distribution",
- "description": _("Seasonality for setting budgets, targets etc.")
- },
- ]
- },
- {
- "label": _("Financial Statements"),
- "items": [
- {
- "type": "report",
- "name": "Trial Balance",
- "doctype": "GL Entry",
- "is_query_report": True,
- },
- {
- "type": "report",
- "name": "Profit and Loss Statement",
- "doctype": "GL Entry",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Balance Sheet",
- "doctype": "GL Entry",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Cash Flow",
- "doctype": "GL Entry",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Consolidated Financial Statement",
- "doctype": "GL Entry",
- "is_query_report": True
- },
- ]
- },
- {
- "label": _("Opening and Closing"),
- "items": [
- {
- "type": "doctype",
- "name": "Opening Invoice Creation Tool",
- },
- {
- "type": "doctype",
- "name": "Chart of Accounts Importer",
- },
- {
- "type": "doctype",
- "name": "Period Closing Voucher",
- "description": _("Close Balance Sheet and book Profit or Loss.")
- },
- ]
-
- },
- {
- "label": _("Multi Currency"),
- "items": [
- {
- "type": "doctype",
- "name": "Currency",
- "description": _("Enable / disable currencies.")
- },
- {
- "type": "doctype",
- "name": "Currency Exchange",
- "description": _("Currency exchange rate master.")
- },
- {
- "type": "doctype",
- "name": "Exchange Rate Revaluation",
- "description": _("Exchange Rate Revaluation master.")
- },
- ]
- },
- {
- "label": _("Settings"),
- "icon": "fa fa-cog",
- "items": [
- {
- "type": "doctype",
- "name": "Payment Gateway Account",
- "description": _("Setup Gateway accounts.")
- },
- {
- "type": "doctype",
- "name": "Terms and Conditions",
- "label": _("Terms and Conditions Template"),
- "description": _("Template of terms or contract.")
- },
- {
- "type": "doctype",
- "name": "Mode of Payment",
- "description": _("e.g. Bank, Cash, Credit Card")
- },
- ]
- },
- {
- "label": _("Subscription Management"),
- "items": [
- {
- "type": "doctype",
- "name": "Subscriber",
- },
- {
- "type": "doctype",
- "name": "Subscription Plan",
- },
- {
- "type": "doctype",
- "name": "Subscription"
- },
- {
- "type": "doctype",
- "name": "Subscription Settings"
- }
- ]
- },
- {
- "label": _("Bank Statement"),
- "items": [
- {
- "type": "doctype",
- "label": _("Bank"),
- "name": "Bank",
- },
- {
- "type": "doctype",
- "label": _("Bank Account"),
- "name": "Bank Account",
- },
- {
- "type": "doctype",
- "name": "Bank Statement Transaction Entry",
- },
- {
- "type": "doctype",
- "label": _("Bank Statement Settings"),
- "name": "Bank Statement Settings",
- },
- ]
- },
- {
- "label": _("Profitability"),
- "items": [
- {
- "type": "report",
- "name": "Gross Profit",
- "doctype": "Sales Invoice",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Profitability Analysis",
- "doctype": "GL Entry",
- "is_query_report": True,
- },
- {
- "type": "report",
- "name": "Sales Invoice Trends",
- "is_query_report": True,
- "doctype": "Sales Invoice"
- },
- {
- "type": "report",
- "name": "Purchase Invoice Trends",
- "is_query_report": True,
- "doctype": "Purchase Invoice"
- },
- ]
- },
- {
- "label": _("Reports"),
- "icon": "fa fa-table",
- "items": [
- {
- "type": "report",
- "name": "Trial Balance for Party",
- "doctype": "GL Entry",
- "is_query_report": True,
- },
- {
- "type": "report",
- "name": "Payment Period Based On Invoice Date",
- "is_query_report": True,
- "doctype": "Journal Entry"
- },
- {
- "type": "report",
- "name": "Sales Partners Commission",
- "is_query_report": True,
- "doctype": "Sales Invoice"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Customer Credit Balance",
- "doctype": "Customer"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Sales Payment Summary",
- "doctype": "Sales Invoice"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Address And Contacts",
- "doctype": "Address"
- }
- ]
- },
- {
- "label": _("Share Management"),
- "icon": "fa fa-microchip ",
- "items": [
- {
- "type": "doctype",
- "name": "Shareholder",
- "description": _("List of available Shareholders with folio numbers")
- },
- {
- "type": "doctype",
- "name": "Share Transfer",
- "description": _("List of all share transactions"),
- },
- {
- "type": "report",
- "name": "Share Ledger",
- "doctype": "Share Transfer",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Share Balance",
- "doctype": "Share Transfer",
- "is_query_report": True
- }
- ]
- },
-
- ]
-
- gst = {
- "label": _("Goods and Services Tax (GST India)"),
- "items": [
- {
- "type": "doctype",
- "name": "GST Settings",
- },
- {
- "type": "doctype",
- "name": "GST HSN Code",
- },
- {
- "type": "report",
- "name": "GSTR-1",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "GSTR-2",
- "is_query_report": True
- },
- {
- "type": "doctype",
- "name": "GSTR 3B Report",
- },
- {
- "type": "report",
- "name": "GST Sales Register",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "GST Purchase Register",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "GST Itemised Sales Register",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "GST Itemised Purchase Register",
- "is_query_report": True
- },
- {
- "type": "doctype",
- "name": "C-Form",
- "description": _("C-Form records"),
- "country": "India"
- },
- ]
- }
-
-
- countries = frappe.get_all("Company", fields="country")
- countries = [country["country"] for country in countries]
- if "India" in countries:
- config.insert(9, gst)
- domains = frappe.get_active_domains()
- return config
diff --git a/erpnext/config/agriculture.py b/erpnext/config/agriculture.py
deleted file mode 100644
index 937d76e..0000000
--- a/erpnext/config/agriculture.py
+++ /dev/null
@@ -1,70 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Crops & Lands"),
- "items": [
- {
- "type": "doctype",
- "name": "Crop",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Crop Cycle",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Location",
- "onboard": 1,
- }
- ]
- },
- {
- "label": _("Diseases & Fertilizers"),
- "items": [
- {
- "type": "doctype",
- "name": "Disease",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Fertilizer",
- "onboard": 1,
- }
- ]
- },
- {
- "label": _("Analytics"),
- "items": [
- {
- "type": "doctype",
- "name": "Plant Analysis",
- },
- {
- "type": "doctype",
- "name": "Soil Analysis",
- },
- {
- "type": "doctype",
- "name": "Water Analysis",
- },
- {
- "type": "doctype",
- "name": "Soil Texture",
- },
- {
- "type": "doctype",
- "name": "Weather",
- },
- {
- "type": "doctype",
- "name": "Agriculture Analysis Criteria",
- }
- ]
- },
- ]
\ No newline at end of file
diff --git a/erpnext/config/assets.py b/erpnext/config/assets.py
deleted file mode 100644
index 4cf7cf0..0000000
--- a/erpnext/config/assets.py
+++ /dev/null
@@ -1,94 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Assets"),
- "items": [
- {
- "type": "doctype",
- "name": "Asset",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Location",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Asset Category",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Asset Movement",
- "description": _("Transfer an asset from one warehouse to another")
- },
- ]
- },
- {
- "label": _("Maintenance"),
- "items": [
- {
- "type": "doctype",
- "name": "Asset Maintenance Team",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Asset Maintenance",
- "onboard": 1,
- "dependencies": ["Asset Maintenance Team"],
- },
- {
- "type": "doctype",
- "name": "Asset Maintenance Tasks",
- "onboard": 1,
- "dependencies": ["Asset Maintenance"],
- },
- {
- "type": "doctype",
- "name": "Asset Maintenance Log",
- "dependencies": ["Asset Maintenance"],
- },
- {
- "type": "doctype",
- "name": "Asset Value Adjustment",
- "dependencies": ["Asset"],
- },
- {
- "type": "doctype",
- "name": "Asset Repair",
- "dependencies": ["Asset"],
- },
- ]
- },
- {
- "label": _("Reports"),
- "icon": "fa fa-table",
- "items": [
- {
- "type": "report",
- "name": "Asset Depreciation Ledger",
- "doctype": "Asset",
- "is_query_report": True,
- "dependencies": ["Asset"],
- },
- {
- "type": "report",
- "name": "Asset Depreciations and Balances",
- "doctype": "Asset",
- "is_query_report": True,
- "dependencies": ["Asset"],
- },
- {
- "type": "report",
- "name": "Asset Maintenance",
- "doctype": "Asset Maintenance",
- "dependencies": ["Asset Maintenance"]
- },
- ]
- }
- ]
diff --git a/erpnext/config/buying.py b/erpnext/config/buying.py
deleted file mode 100644
index b06bb76..0000000
--- a/erpnext/config/buying.py
+++ /dev/null
@@ -1,264 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-
-def get_data():
- config = [
- {
- "label": _("Purchasing"),
- "icon": "fa fa-star",
- "items": [
- {
- "type": "doctype",
- "name": "Material Request",
- "onboard": 1,
- "dependencies": ["Item"],
- "description": _("Request for purchase."),
- },
- {
- "type": "doctype",
- "name": "Purchase Order",
- "onboard": 1,
- "dependencies": ["Item", "Supplier"],
- "description": _("Purchase Orders given to Suppliers."),
- },
- {
- "type": "doctype",
- "name": "Purchase Invoice",
- "onboard": 1,
- "dependencies": ["Item", "Supplier"]
- },
- {
- "type": "doctype",
- "name": "Request for Quotation",
- "onboard": 1,
- "dependencies": ["Item", "Supplier"],
- "description": _("Request for quotation."),
- },
- {
- "type": "doctype",
- "name": "Supplier Quotation",
- "dependencies": ["Item", "Supplier"],
- "description": _("Quotations received from Suppliers."),
- },
- ]
- },
- {
- "label": _("Items and Pricing"),
- "items": [
- {
- "type": "doctype",
- "name": "Item",
- "onboard": 1,
- "description": _("All Products or Services."),
- },
- {
- "type": "doctype",
- "name": "Item Price",
- "description": _("Multiple Item prices."),
- "onboard": 1,
- "route": "#Report/Item Price"
- },
- {
- "type": "doctype",
- "name": "Price List",
- "description": _("Price List master.")
- },
- {
- "type": "doctype",
- "name": "Pricing Rule",
- "description": _("Rules for applying pricing and discount.")
- },
- {
- "type": "doctype",
- "name": "Product Bundle",
- "description": _("Bundle items at time of sale."),
- },
- {
- "type": "doctype",
- "name": "Item Group",
- "icon": "fa fa-sitemap",
- "label": _("Item Group"),
- "link": "Tree/Item Group",
- "description": _("Tree of Item Groups."),
- },
- {
- "type": "doctype",
- "name": "Promotional Scheme",
- "description": _("Rules for applying different promotional schemes.")
- }
- ]
- },
- {
- "label": _("Settings"),
- "icon": "fa fa-cog",
- "items": [
- {
- "type": "doctype",
- "name": "Buying Settings",
- "settings": 1,
- "description": _("Default settings for buying transactions.")
- },
- {
- "type": "doctype",
- "name": "Purchase Taxes and Charges Template",
- "description": _("Tax template for buying transactions.")
- },
- {
- "type": "doctype",
- "name":"Terms and Conditions",
- "label": _("Terms and Conditions Template"),
- "description": _("Template of terms or contract.")
- },
- ]
- },
- {
- "label": _("Supplier"),
- "items": [
- {
- "type": "doctype",
- "name": "Supplier",
- "onboard": 1,
- "description": _("Supplier database."),
- },
- {
- "type": "doctype",
- "name": "Supplier Group",
- "description": _("Supplier Group master.")
- },
- {
- "type": "doctype",
- "name": "Contact",
- "description": _("All Contacts."),
- },
- {
- "type": "doctype",
- "name": "Address",
- "description": _("All Addresses."),
- },
-
- ]
- },
- {
- "label": _("Key Reports"),
- "icon": "fa fa-table",
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Purchase Analytics",
- "reference_doctype": "Purchase Order",
- "onboard": 1
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Purchase Order Trends",
- "reference_doctype": "Purchase Order",
- "onboard": 1,
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Procurement Tracker",
- "reference_doctype": "Purchase Order",
- "onboard": 1,
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Requested Items To Order",
- "reference_doctype": "Material Request",
- "onboard": 1,
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Address And Contacts",
- "label": _("Supplier Addresses And Contacts"),
- "reference_doctype": "Address",
- "route_options": {
- "party_type": "Supplier"
- }
- }
- ]
- },
- {
- "label": _("Supplier Scorecard"),
- "items": [
- {
- "type": "doctype",
- "name": "Supplier Scorecard",
- "description": _("All Supplier scorecards."),
- },
- {
- "type": "doctype",
- "name": "Supplier Scorecard Variable",
- "description": _("Templates of supplier scorecard variables.")
- },
- {
- "type": "doctype",
- "name": "Supplier Scorecard Criteria",
- "description": _("Templates of supplier scorecard criteria."),
- },
- {
- "type": "doctype",
- "name": "Supplier Scorecard Standing",
- "description": _("Templates of supplier standings."),
- },
-
- ]
- },
- {
- "label": _("Other Reports"),
- "icon": "fa fa-list",
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Items To Be Requested",
- "reference_doctype": "Item",
- "onboard": 1,
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Item-wise Purchase History",
- "reference_doctype": "Item",
- "onboard": 1,
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Supplier-Wise Sales Analytics",
- "reference_doctype": "Stock Ledger Entry",
- "onboard": 1
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Material Requests for which Supplier Quotations are not created",
- "reference_doctype": "Material Request"
- }
- ]
- },
-
- ]
-
- regional = {
- "label": _("Regional"),
- "items": [
- {
- "type": "doctype",
- "name": "Import Supplier Invoice",
- "description": _("Import Italian Supplier Invoice."),
- "onboard": 1,
- }
- ]
- }
-
- countries = frappe.get_all("Company", fields="country")
- countries = [country["country"] for country in countries]
- if "Italy" in countries:
- config.append(regional)
- return config
\ No newline at end of file
diff --git a/erpnext/config/crm.py b/erpnext/config/crm.py
deleted file mode 100644
index 09c2a65..0000000
--- a/erpnext/config/crm.py
+++ /dev/null
@@ -1,236 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Sales Pipeline"),
- "icon": "fa fa-star",
- "items": [
- {
- "type": "doctype",
- "name": "Lead",
- "description": _("Database of potential customers."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Opportunity",
- "description": _("Potential opportunities for selling."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Customer",
- "description": _("Customer database."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Contact",
- "description": _("All Contacts."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Communication",
- "description": _("Record of all communications of type email, phone, chat, visit, etc."),
- },
- {
- "type": "doctype",
- "name": "Lead Source",
- "description": _("Track Leads by Lead Source.")
- },
- {
- "type": "doctype",
- "name": "Contract",
- "description": _("Helps you keep tracks of Contracts based on Supplier, Customer and Employee"),
- },
- {
- "type": "doctype",
- "name": "Appointment",
- "description" : _("Helps you manage appointments with your leads"),
- },
- {
- "type": "doctype",
- "name": "Newsletter",
- "label": _("Newsletter"),
- }
- ]
- },
- {
- "label": _("Reports"),
- "icon": "fa fa-list",
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Lead Details",
- "doctype": "Lead",
- "onboard": 1,
- },
- {
- "type": "page",
- "name": "sales-funnel",
- "label": _("Sales Funnel"),
- "icon": "fa fa-bar-chart",
- "onboard": 1,
- },
- {
- "type": "report",
- "name": "Prospects Engaged But Not Converted",
- "doctype": "Lead",
- "is_query_report": True,
- "onboard": 1,
- },
- {
- "type": "report",
- "name": "Minutes to First Response for Opportunity",
- "doctype": "Opportunity",
- "is_query_report": True,
- "dependencies": ["Opportunity"]
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Customer Addresses And Contacts",
- "doctype": "Contact",
- "dependencies": ["Customer"]
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Inactive Customers",
- "doctype": "Sales Order",
- "dependencies": ["Sales Order"]
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Campaign Efficiency",
- "doctype": "Lead",
- "dependencies": ["Lead"]
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Lead Owner Efficiency",
- "doctype": "Lead",
- "dependencies": ["Lead"]
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Territory-wise Sales",
- "doctype": "Opportunity",
- "dependencies": ["Opportunity"]
- }
- ]
- },
- {
- "label": _("Settings"),
- "icon": "fa fa-cog",
- "items": [
- {
- "type": "doctype",
- "label": _("Customer Group"),
- "name": "Customer Group",
- "icon": "fa fa-sitemap",
- "link": "Tree/Customer Group",
- "description": _("Manage Customer Group Tree."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "label": _("Territory"),
- "name": "Territory",
- "icon": "fa fa-sitemap",
- "link": "Tree/Territory",
- "description": _("Manage Territory Tree."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "label": _("Sales Person"),
- "name": "Sales Person",
- "icon": "fa fa-sitemap",
- "link": "Tree/Sales Person",
- "description": _("Manage Sales Person Tree."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Campaign",
- "description": _("Sales campaigns."),
- },
- {
- "type": "doctype",
- "name": "Email Campaign",
- "description": _("Sends Mails to lead or contact based on a Campaign schedule"),
- },
- {
- "type": "doctype",
- "name": "SMS Center",
- "description":_("Send mass SMS to your contacts"),
- },
- {
- "type": "doctype",
- "name": "SMS Log",
- "description":_("Logs for maintaining sms delivery status"),
- },
- {
- "type": "doctype",
- "name": "SMS Settings",
- "description": _("Setup SMS gateway settings")
- },
- {
- "type": "doctype",
- "label": _("Email Group"),
- "name": "Email Group",
- }
- ]
- },
- {
- "label": _("Maintenance"),
- "icon": "fa fa-star",
- "items": [
- {
- "type": "doctype",
- "name": "Maintenance Schedule",
- "description": _("Plan for maintenance visits."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Maintenance Visit",
- "description": _("Visit report for maintenance call."),
- },
- {
- "type": "report",
- "name": "Maintenance Schedules",
- "is_query_report": True,
- "doctype": "Maintenance Schedule"
- },
- {
- "type": "doctype",
- "name": "Warranty Claim",
- "description": _("Warranty Claim against Serial No."),
- },
- ]
- },
- # {
- # "label": _("Help"),
- # "items": [
- # {
- # "type": "help",
- # "label": _("Lead to Quotation"),
- # "youtube_id": "TxYX4r4JAKA"
- # },
- # {
- # "type": "help",
- # "label": _("Newsletters"),
- # "youtube_id": "muLKsCrrDRo"
- # },
- # ]
- # },
- ]
diff --git a/erpnext/config/desktop.py b/erpnext/config/desktop.py
deleted file mode 100644
index ce7c245..0000000
--- a/erpnext/config/desktop.py
+++ /dev/null
@@ -1,220 +0,0 @@
-# coding=utf-8
-
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- # Modules
- {
- "module_name": "Getting Started",
- "category": "Modules",
- "label": _("Getting Started"),
- "color": "#1abc9c",
- "icon": "fa fa-check-square-o",
- "type": "module",
- "disable_after_onboard": 1,
- "description": "Dive into the basics for your organisation's needs.",
- "onboard_present": 1
- },
- {
- "module_name": "Accounts",
- "category": "Modules",
- "label": _("Accounting"),
- "color": "#3498db",
- "icon": "octicon octicon-repo",
- "type": "module",
- "description": "Accounts, billing, payments, cost center and budgeting."
- },
- {
- "module_name": "Selling",
- "category": "Modules",
- "label": _("Selling"),
- "color": "#1abc9c",
- "icon": "octicon octicon-tag",
- "type": "module",
- "description": "Sales orders, quotations, customers and items."
- },
- {
- "module_name": "Buying",
- "category": "Modules",
- "label": _("Buying"),
- "color": "#c0392b",
- "icon": "octicon octicon-briefcase",
- "type": "module",
- "description": "Purchasing, suppliers, material requests, and items."
- },
- {
- "module_name": "Stock",
- "category": "Modules",
- "label": _("Stock"),
- "color": "#f39c12",
- "icon": "octicon octicon-package",
- "type": "module",
- "description": "Stock transactions, reports, serial numbers and batches."
- },
- {
- "module_name": "Assets",
- "category": "Modules",
- "label": _("Assets"),
- "color": "#4286f4",
- "icon": "octicon octicon-database",
- "type": "module",
- "description": "Asset movement, maintainance and tools."
- },
- {
- "module_name": "Projects",
- "category": "Modules",
- "label": _("Projects"),
- "color": "#8e44ad",
- "icon": "octicon octicon-rocket",
- "type": "module",
- "description": "Updates, Timesheets and Activities."
- },
- {
- "module_name": "CRM",
- "category": "Modules",
- "label": _("CRM"),
- "color": "#EF4DB6",
- "icon": "octicon octicon-broadcast",
- "type": "module",
- "description": "Sales pipeline, leads, opportunities and customers."
- },
- {
- "module_name": "Loan Management",
- "category": "Modules",
- "label": _("Loan Management"),
- "color": "#EF4DB6",
- "icon": "octicon octicon-repo",
- "type": "module",
- "description": "Loan Management for Customer and Employees"
- },
- {
- "module_name": "Support",
- "category": "Modules",
- "label": _("Support"),
- "color": "#1abc9c",
- "icon": "fa fa-check-square-o",
- "type": "module",
- "description": "User interactions, support issues and knowledge base."
- },
- {
- "module_name": "HR",
- "category": "Modules",
- "label": _("Human Resources"),
- "color": "#2ecc71",
- "icon": "octicon octicon-organization",
- "type": "module",
- "description": "Employees, attendance, payroll, leaves and shifts."
- },
- {
- "module_name": "Quality Management",
- "category": "Modules",
- "label": _("Quality"),
- "color": "#1abc9c",
- "icon": "fa fa-check-square-o",
- "type": "module",
- "description": "Quality goals, procedures, reviews and action."
- },
-
-
- # Category: "Domains"
- {
- "module_name": "Manufacturing",
- "category": "Domains",
- "label": _("Manufacturing"),
- "color": "#7f8c8d",
- "icon": "octicon octicon-tools",
- "type": "module",
- "description": "BOMS, work orders, operations, and timesheets."
- },
- {
- "module_name": "Retail",
- "category": "Domains",
- "label": _("Retail"),
- "color": "#7f8c8d",
- "icon": "octicon octicon-credit-card",
- "type": "module",
- "description": "Point of Sale and cashier closing."
- },
- {
- "module_name": "Education",
- "category": "Domains",
- "label": _("Education"),
- "color": "#428B46",
- "icon": "octicon octicon-mortar-board",
- "type": "module",
- "description": "Student admissions, fees, courses and scores."
- },
-
- {
- "module_name": "Healthcare",
- "category": "Domains",
- "label": _("Healthcare"),
- "color": "#FF888B",
- "icon": "fa fa-heartbeat",
- "type": "module",
- "description": "Patient appointments, procedures and tests."
- },
- {
- "module_name": "Agriculture",
- "category": "Domains",
- "label": _("Agriculture"),
- "color": "#8BC34A",
- "icon": "octicon octicon-globe",
- "type": "module",
- "description": "Crop cycles, land areas, soil and plant analysis."
- },
- {
- "module_name": "Hotels",
- "category": "Domains",
- "label": _("Hotels"),
- "color": "#EA81E8",
- "icon": "fa fa-bed",
- "type": "module",
- "description": "Hotel rooms, pricing, reservation and amenities."
- },
-
- {
- "module_name": "Non Profit",
- "category": "Domains",
- "label": _("Non Profit"),
- "color": "#DE2B37",
- "icon": "octicon octicon-heart",
- "type": "module",
- "description": "Volunteers, memberships, grants and chapters."
- },
- {
- "module_name": "Restaurant",
- "category": "Domains",
- "label": _("Restaurant"),
- "color": "#EA81E8",
- "icon": "fa fa-cutlery",
- "_doctype": "Restaurant",
- "type": "module",
- "link": "List/Restaurant",
- "description": "Menu, Orders and Table Reservations."
- },
-
- {
- "module_name": "Help",
- "category": "Administration",
- "label": _("Learn"),
- "color": "#FF888B",
- "icon": "octicon octicon-device-camera-video",
- "type": "module",
- "is_help": True,
- "description": "Explore Help Articles and Videos."
- },
- {
- "module_name": 'Marketplace',
- "category": "Places",
- "label": _('Marketplace'),
- "icon": "octicon octicon-star",
- "type": 'link',
- "link": '#marketplace/home',
- "color": '#FF4136',
- 'standard': 1,
- "description": "Publish items to other ERPNext users."
- },
- ]
diff --git a/erpnext/config/docs.py b/erpnext/config/docs.py
deleted file mode 100644
index 85e6006..0000000
--- a/erpnext/config/docs.py
+++ /dev/null
@@ -1,3 +0,0 @@
-from __future__ import unicode_literals
-
-source_link = "https://github.com/erpnext/foundation"
diff --git a/erpnext/config/education.py b/erpnext/config/education.py
index 4efaaa6..1c8ab10 100644
--- a/erpnext/config/education.py
+++ b/erpnext/config/education.py
@@ -173,7 +173,7 @@
{
"type": "doctype",
"name": "Course Schedule",
- "route": "#List/Course Schedule/Calendar"
+ "route": "/app/List/Course Schedule/Calendar"
},
{
"type": "doctype",
diff --git a/erpnext/config/getting_started.py b/erpnext/config/getting_started.py
deleted file mode 100644
index dc72316..0000000
--- a/erpnext/config/getting_started.py
+++ /dev/null
@@ -1,268 +0,0 @@
-from __future__ import unicode_literals
-import frappe
-from frappe import _
-
-active_domains = frappe.get_active_domains()
-
-def get_data():
- return [
- {
- "label": _("Accounting"),
- "items": [
- {
- "type": "doctype",
- "name": "Item",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Customer",
- "description": _("Customer database."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Supplier",
- "description": _("Supplier database."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Company",
- "description": _("Company (not Customer or Supplier) master."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Account",
- "icon": "fa fa-sitemap",
- "label": _("Chart of Accounts"),
- "route": "#Tree/Account",
- "description": _("Tree of financial accounts."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Opening Invoice Creation Tool",
- "description": _("Create Opening Sales and Purchase Invoices"),
- "onboard": 1,
- },
- ]
- },
- {
- "label": _("Data Import and Settings"),
- "items": [
- {
- "type": "doctype",
- "name": "Data Import",
- "label": _("Import Data"),
- "icon": "octicon octicon-cloud-upload",
- "description": _("Import Data from CSV / Excel files."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Chart of Accounts Importer",
- "label": _("Chart of Accounts Importer"),
- "description": _("Import Chart of Accounts from CSV / Excel files"),
- "onboard": 1
- },
- {
- "type": "doctype",
- "name": "Letter Head",
- "description": _("Letter Heads for print templates."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Email Account",
- "description": _("Add / Manage Email Accounts."),
- "onboard": 1,
- },
-
- ]
- },
- {
- "label": _("Stock"),
- "items": [
- {
- "type": "doctype",
- "name": "Warehouse",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Brand",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "UOM",
- "label": _("Unit of Measure") + " (UOM)",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Stock Reconciliation",
- "onboard": 1,
- },
- ]
- },
- {
- "label": _("CRM"),
- "items": [
- {
- "type": "doctype",
- "name": "Lead",
- "description": _("Database of potential customers."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "label": _("Customer Group"),
- "name": "Customer Group",
- "icon": "fa fa-sitemap",
- "link": "Tree/Customer Group",
- "description": _("Manage Customer Group Tree."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "label": _("Territory"),
- "name": "Territory",
- "icon": "fa fa-sitemap",
- "link": "Tree/Territory",
- "description": _("Manage Territory Tree."),
- "onboard": 1,
- },
- ]
- },
- {
- "label": _("Human Resources"),
- "items": [
- {
- "type": "doctype",
- "name": "Employee",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Employee Attendance Tool",
- "hide_count": True,
- "onboard": 1,
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Salary Structure",
- "onboard": 1,
- },
- ]
- },
- {
- "label": _("Education"),
- "condition": "Education" in active_domains,
- "items": [
- {
- "type": "doctype",
- "name": "Student",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Course",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Instructor",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Room",
- "onboard": 1,
- },
- ]
- },
- {
- "label": _("Healthcare"),
- "condition": "Healthcare" in active_domains,
- "items": [
- {
- "type": "doctype",
- "name": "Patient",
- "label": _("Patient"),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Physician",
- "label": _("Physician"),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Diagnosis",
- "label": _("Diagnosis"),
- "onboard": 1,
- }
- ]
- },
- {
- "label": _("Agriculture"),
- "condition": "Agriculture" in active_domains,
- "items": [
- {
- "type": "doctype",
- "name": "Crop",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Crop Cycle",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Location",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Fertilizer",
- "onboard": 1,
- }
- ]
- },
- {
- "label": _("Non Profit"),
- "condition": "Non Profit" in active_domains,
- "items": [
- {
- "type": "doctype",
- "name": "Member",
- "description": _("Member information."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Volunteer",
- "description": _("Volunteer information."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Chapter",
- "description": _("Chapter information."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Donor",
- "description": _("Donor information."),
- "onboard": 1,
- },
- ]
- }
- ]
\ No newline at end of file
diff --git a/erpnext/config/healthcare.py b/erpnext/config/healthcare.py
deleted file mode 100644
index da24d11..0000000
--- a/erpnext/config/healthcare.py
+++ /dev/null
@@ -1,254 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Masters"),
- "items": [
- {
- "type": "doctype",
- "name": "Patient",
- "label": _("Patient"),
- "onboard": 1
- },
- {
- "type": "doctype",
- "name": "Healthcare Practitioner",
- "label": _("Healthcare Practitioner"),
- "onboard": 1
- },
- {
- "type": "doctype",
- "name": "Practitioner Schedule",
- "label": _("Practitioner Schedule"),
- "onboard": 1
- },
- {
- "type": "doctype",
- "name": "Medical Department",
- "label": _("Medical Department"),
- },
- {
- "type": "doctype",
- "name": "Healthcare Service Unit Type",
- "label": _("Healthcare Service Unit Type")
- },
- {
- "type": "doctype",
- "name": "Healthcare Service Unit",
- "label": _("Healthcare Service Unit")
- },
- {
- "type": "doctype",
- "name": "Medical Code Standard",
- "label": _("Medical Code Standard")
- },
- {
- "type": "doctype",
- "name": "Medical Code",
- "label": _("Medical Code")
- }
- ]
- },
- {
- "label": _("Consultation Setup"),
- "items": [
- {
- "type": "doctype",
- "name": "Appointment Type",
- "label": _("Appointment Type"),
- },
- {
- "type": "doctype",
- "name": "Clinical Procedure Template",
- "label": _("Clinical Procedure Template")
- },
- {
- "type": "doctype",
- "name": "Prescription Dosage",
- "label": _("Prescription Dosage")
- },
- {
- "type": "doctype",
- "name": "Prescription Duration",
- "label": _("Prescription Duration")
- },
- {
- "type": "doctype",
- "name": "Antibiotic",
- "label": _("Antibiotic")
- }
- ]
- },
- {
- "label": _("Consultation"),
- "items": [
- {
- "type": "doctype",
- "name": "Patient Appointment",
- "label": _("Patient Appointment")
- },
- {
- "type": "doctype",
- "name": "Clinical Procedure",
- "label": _("Clinical Procedure")
- },
- {
- "type": "doctype",
- "name": "Patient Encounter",
- "label": _("Patient Encounter")
- },
- {
- "type": "doctype",
- "name": "Vital Signs",
- "label": _("Vital Signs")
- },
- {
- "type": "doctype",
- "name": "Complaint",
- "label": _("Complaint")
- },
- {
- "type": "doctype",
- "name": "Diagnosis",
- "label": _("Diagnosis")
- },
- {
- "type": "doctype",
- "name": "Fee Validity",
- "label": _("Fee Validity")
- }
- ]
- },
- {
- "label": _("Settings"),
- "items": [
- {
- "type": "doctype",
- "name": "Healthcare Settings",
- "label": _("Healthcare Settings"),
- "onboard": 1
- }
- ]
- },
- {
- "label": _("Laboratory Setup"),
- "items": [
- {
- "type": "doctype",
- "name": "Lab Test Template",
- "label": _("Lab Test Template")
- },
- {
- "type": "doctype",
- "name": "Lab Test Sample",
- "label": _("Lab Test Sample")
- },
- {
- "type": "doctype",
- "name": "Lab Test UOM",
- "label": _("Lab Test UOM")
- },
- {
- "type": "doctype",
- "name": "Sensitivity",
- "label": _("Sensitivity")
- }
- ]
- },
- {
- "label": _("Laboratory"),
- "items": [
- {
- "type": "doctype",
- "name": "Lab Test",
- "label": _("Lab Test")
- },
- {
- "type": "doctype",
- "name": "Sample Collection",
- "label": _("Sample Collection")
- },
- {
- "type": "doctype",
- "name": "Dosage Form",
- "label": _("Dosage Form")
- }
- ]
- },
- {
- "label": _("Records and History"),
- "items": [
- {
- "type": "page",
- "name": "patient_history",
- "label": _("Patient History"),
- },
- {
- "type": "doctype",
- "name": "Patient Medical Record",
- "label": _("Patient Medical Record")
- },
- {
- "type": "doctype",
- "name": "Inpatient Record",
- "label": _("Inpatient Record")
- }
- ]
- },
- {
- "label": _("Reports"),
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Patient Appointment Analytics",
- "doctype": "Patient Appointment"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Lab Test Report",
- "doctype": "Lab Test",
- "label": _("Lab Test Report")
- }
- ]
- },
- {
- "label": _("Rehabilitation"),
- "icon": "icon-cog",
- "items": [
- {
- "type": "doctype",
- "name": "Exercise Type",
- "label": _("Exercise Type")
- },
- {
- "type": "doctype",
- "name": "Exercise Difficulty Level",
- "label": _("Exercise Difficulty Level")
- },
- {
- "type": "doctype",
- "name": "Therapy Type",
- "label": _("Therapy Type")
- },
- {
- "type": "doctype",
- "name": "Therapy Plan",
- "label": _("Therapy Plan")
- },
- {
- "type": "doctype",
- "name": "Therapy Session",
- "label": _("Therapy Session")
- },
- {
- "type": "doctype",
- "name": "Motor Assessment Scale",
- "label": _("Motor Assessment Scale")
- }
- ]
- }
- ]
diff --git a/erpnext/config/help.py b/erpnext/config/help.py
deleted file mode 100644
index 922afb4..0000000
--- a/erpnext/config/help.py
+++ /dev/null
@@ -1,273 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("General"),
- "items": [
- {
- "type": "help",
- "label": _("Navigating"),
- "youtube_id": "YDoI2DF4Lmc"
- },
- {
- "type": "help",
- "label": _("Setup Wizard"),
- "youtube_id": "oIOf_zCFWKQ"
- },
- {
- "type": "help",
- "label": _("Customizing Forms"),
- "youtube_id": "pJhL9mmxV_U"
- },
- {
- "type": "help",
- "label": _("Report Builder"),
- "youtube_id": "TxJGUNarcQs"
- },
- ]
-
- },
- {
- "label": _("Settings"),
- "items": [
- {
- "type": "help",
- "label": _("Data Import and Export"),
- "youtube_id": "6wiriRKPhmg"
- },
- {
- "type": "help",
- "label": _("Opening Stock Balance"),
- "youtube_id": "nlHX0ZZ84Lw"
- },
- {
- "type": "help",
- "label": _("Setting up Email Account"),
- "youtube_id": "YFYe0DrB95o"
- },
- {
- "type": "help",
- "label": _("Printing and Branding"),
- "youtube_id": "cKZHcx1znMc"
- },
- {
- "type": "help",
- "label": _("Users and Permissions"),
- "youtube_id": "8Slw1hsTmUI"
- },
- {
- "type": "help",
- "label": _("Workflow"),
- "youtube_id": "yObJUg9FxFs"
- },
- {
- "type": "help",
- "label": _("File Manager"),
- "youtube_id": "4-osLW3E_Rk"
- },
- ]
- },
- {
- "label": _("Accounting"),
- "items": [
- {
- "type": "help",
- "label": _("Chart of Accounts"),
- "youtube_id": "DyR-DST-PyA"
- },
- {
- "type": "help",
- "label": _("Setting up Taxes"),
- "youtube_id": "nQ1zZdPgdaQ"
- },
- {
- "type": "help",
- "label": _("Opening Accounting Balance"),
- "youtube_id": "kdgM20Q-q68"
- },
- {
- "type": "help",
- "label": _("Advance Payments"),
- "youtube_id": "J46-6qtyZ9U"
- },
- ]
- },
- {
- "label": _("CRM"),
- "items": [
- {
- "type": "help",
- "label": _("Lead to Quotation"),
- "youtube_id": "TxYX4r4JAKA"
- },
- {
- "type": "help",
- "label": _("Newsletters"),
- "youtube_id": "muLKsCrrDRo"
- },
- ]
- },
- {
- "label": _("Selling"),
- "items": [
- {
- "type": "help",
- "label": _("Customer and Supplier"),
- "youtube_id": "anoGi_RpQ20"
- },
- {
- "type": "help",
- "label": _("Sales Order to Payment"),
- "youtube_id": "1eP90MWoDQM"
- },
- {
- "type": "help",
- "label": _("Point-of-Sale"),
- "youtube_id": "4WkelWkbP_c"
- },
- {
- "type": "help",
- "label": _("Product Bundle"),
- "youtube_id": "yk3kPrRyRRc"
- },
- {
- "type": "help",
- "label": _("Drop Ship"),
- "youtube_id": "hUc0hu_XLdo"
- },
- ]
- },
- {
- "label": _("Stock"),
- "items": [
- {
- "type": "help",
- "label": _("Items and Pricing"),
- "youtube_id": "qXaEwld4_Ps"
- },
- {
- "type": "help",
- "label": _("Item Variants"),
- "youtube_id": "OGBETlCzU5o"
- },
- {
- "type": "help",
- "label": _("Opening Stock Balance"),
- "youtube_id": "0yPgrtfeCTs"
- },
- {
- "type": "help",
- "label": _("Making Stock Entries"),
- "youtube_id": "Njt107hlY3I"
- },
- {
- "type": "help",
- "label": _("Serialized Inventory"),
- "youtube_id": "gvOVlEwFDAk"
- },
- {
- "type": "help",
- "label": _("Batch Inventory"),
- "youtube_id": "J0QKl7ABPKM"
- },
- {
- "type": "help",
- "label": _("Managing Subcontracting"),
- "youtube_id": "ThiMCC2DtKo"
- },
- {
- "type": "help",
- "label": _("Quality Inspection"),
- "youtube_id": "WmtcF3Y40Fs"
- },
- ]
- },
- {
- "label": _("Buying"),
- "items": [
- {
- "type": "help",
- "label": _("Customer and Supplier"),
- "youtube_id": "anoGi_RpQ20"
- },
- {
- "type": "help",
- "label": _("Material Request to Purchase Order"),
- "youtube_id": "55Gk2j7Q8Zw"
- },
- {
- "type": "help",
- "label": _("Purchase Order to Payment"),
- "youtube_id": "efFajTTQBa8"
- },
- {
- "type": "help",
- "label": _("Managing Subcontracting"),
- "youtube_id": "ThiMCC2DtKo"
- },
- ]
- },
- {
- "label": _("Manufacturing"),
- "items": [
- {
- "type": "help",
- "label": _("Bill of Materials"),
- "youtube_id": "hDV0c1OeWLo"
- },
- {
- "type": "help",
- "label": _("Work Order"),
- "youtube_id": "ZotgLyp2YFY"
- },
-
- ]
- },
- {
- "label": _("Human Resource"),
- "items": [
- {
- "type": "help",
- "label": _("Setting up Employees"),
- "youtube_id": "USfIUdZlUhw"
- },
- {
- "type": "help",
- "label": _("Leave Management"),
- "youtube_id": "fc0p_AXebc8"
- },
- {
- "type": "help",
- "label": _("Expense Claims"),
- "youtube_id": "5SZHJF--ZFY"
- }
- ]
- },
- {
- "label": _("Projects"),
- "items": [
- {
- "type": "help",
- "label": _("Managing Projects"),
- "youtube_id": "gCzShu9Niu4"
- },
- ]
- },
- {
- "label": _("Website"),
- "items": [
- {
- "type": "help",
- "label": _("Publish Items on Website"),
- "youtube_id": "W31LBBNzbgc"
- },
- {
- "type": "help",
- "label": _("Shopping Cart"),
- "youtube_id": "xkrYO-KFukM"
- },
- ]
- },
- ]
diff --git a/erpnext/config/hr.py b/erpnext/config/hr.py
deleted file mode 100644
index 9855a11..0000000
--- a/erpnext/config/hr.py
+++ /dev/null
@@ -1,470 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Employee"),
- "items": [
- {
- "type": "doctype",
- "name": "Employee",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Employment Type",
- },
- {
- "type": "doctype",
- "name": "Branch",
- },
- {
- "type": "doctype",
- "name": "Department",
- },
- {
- "type": "doctype",
- "name": "Designation",
- },
- {
- "type": "doctype",
- "name": "Employee Grade",
- },
- {
- "type": "doctype",
- "name": "Employee Group",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Employee Health Insurance"
- },
- ]
- },
- {
- "label": _("Attendance"),
- "items": [
- {
- "type": "doctype",
- "name": "Employee Attendance Tool",
- "hide_count": True,
- "onboard": 1,
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Attendance",
- "onboard": 1,
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Attendance Request",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Upload Attendance",
- "hide_count": True,
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Employee Checkin",
- "hide_count": True,
- "dependencies": ["Employee"]
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Monthly Attendance Sheet",
- "doctype": "Attendance"
- },
- ]
- },
- {
- "label": _("Leaves"),
- "items": [
- {
- "type": "doctype",
- "name": "Leave Application",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Leave Allocation",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Leave Policy",
- "dependencies": ["Leave Type"]
- },
- {
- "type": "doctype",
- "name": "Leave Period",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name":"Leave Type",
- },
- {
- "type": "doctype",
- "name": "Holiday List",
- },
- {
- "type": "doctype",
- "name": "Compensatory Leave Request",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Leave Encashment",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Leave Block List",
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Employee Leave Balance",
- "doctype": "Leave Application"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Leave Ledger Entry",
- "doctype": "Leave Ledger Entry"
- },
- ]
- },
- {
- "label": _("Payroll"),
- "items": [
- {
- "type": "doctype",
- "name": "Salary Structure",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Salary Structure Assignment",
- "onboard": 1,
- "dependencies": ["Salary Structure", "Employee"],
- },
- {
- "type": "doctype",
- "name": "Payroll Entry",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Salary Slip",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Payroll Period",
- },
- {
- "type": "doctype",
- "name": "Income Tax Slab",
- },
- {
- "type": "doctype",
- "name": "Salary Component",
- },
- {
- "type": "doctype",
- "name": "Additional Salary",
- },
- {
- "type": "doctype",
- "name": "Retention Bonus",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Employee Incentive",
- "dependencies": ["Employee"]
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Salary Register",
- "doctype": "Salary Slip"
- },
- ]
- },
- {
- "label": _("Employee Tax and Benefits"),
- "items": [
- {
- "type": "doctype",
- "name": "Employee Tax Exemption Declaration",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Employee Tax Exemption Proof Submission",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Employee Other Income",
- },
- {
- "type": "doctype",
- "name": "Employee Benefit Application",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Employee Benefit Claim",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Employee Tax Exemption Category",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Employee Tax Exemption Sub Category",
- "dependencies": ["Employee"]
- },
- ]
- },
- {
- "label": _("Employee Lifecycle"),
- "items": [
- {
- "type": "doctype",
- "name": "Employee Onboarding",
- "dependencies": ["Job Applicant"],
- },
- {
- "type": "doctype",
- "name": "Employee Skill Map",
- "dependencies": ["Employee"],
- },
- {
- "type": "doctype",
- "name": "Employee Promotion",
- "dependencies": ["Employee"],
- },
- {
- "type": "doctype",
- "name": "Employee Transfer",
- "dependencies": ["Employee"],
- },
- {
- "type": "doctype",
- "name": "Employee Separation",
- "dependencies": ["Employee"],
- },
- {
- "type": "doctype",
- "name": "Employee Onboarding Template",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Employee Separation Template",
- "dependencies": ["Employee"]
- },
- ]
- },
- {
- "label": _("Recruitment"),
- "items": [
- {
- "type": "doctype",
- "name": "Job Opening",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Job Applicant",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Job Offer",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Appointment Letter",
- },
- {
- "type": "doctype",
- "name": "Staffing Plan",
- },
- ]
- },
- {
- "label": _("Training"),
- "items": [
- {
- "type": "doctype",
- "name": "Training Program"
- },
- {
- "type": "doctype",
- "name": "Training Event"
- },
- {
- "type": "doctype",
- "name": "Training Result"
- },
- {
- "type": "doctype",
- "name": "Training Feedback"
- },
- ]
- },
- {
- "label": _("Performance"),
- "items": [
- {
- "type": "doctype",
- "name": "Appraisal",
- },
- {
- "type": "doctype",
- "name": "Appraisal Template",
- },
- {
- "type": "doctype",
- "name": "Energy Point Rule",
- },
- {
- "type": "doctype",
- "name": "Energy Point Log",
- },
- {
- "type": "link",
- "doctype": "Energy Point Log",
- "label": _("Energy Point Leaderboard"),
- "route": "#social/users"
- },
- ]
- },
- {
- "label": _("Expense Claims"),
- "items": [
- {
- "type": "doctype",
- "name": "Expense Claim",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Employee Advance",
- "dependencies": ["Employee"]
- },
- ]
- },
- {
- "label": _("Loans"),
- "items": [
- {
- "type": "doctype",
- "name": "Loan Application",
- "dependencies": ["Employee"]
- },
- {
- "type": "doctype",
- "name": "Loan"
- },
- {
- "type": "doctype",
- "name": "Loan Type",
- },
- ]
- },
- {
- "label": _("Shift Management"),
- "items": [
- {
- "type": "doctype",
- "name": "Shift Type",
- },
- {
- "type": "doctype",
- "name": "Shift Request",
- },
- {
- "type": "doctype",
- "name": "Shift Assignment",
- },
- ]
- },
- {
- "label": _("Fleet Management"),
- "items": [
- {
- "type": "doctype",
- "name": "Vehicle"
- },
- {
- "type": "doctype",
- "name": "Vehicle Log"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Vehicle Expenses",
- "doctype": "Vehicle"
- },
- ]
- },
- {
- "label": _("Settings"),
- "icon": "fa fa-cog",
- "items": [
- {
- "type": "doctype",
- "name": "HR Settings",
- },
- {
- "type": "doctype",
- "name": "Daily Work Summary Group"
- },
- {
- "type": "page",
- "name": "team-updates",
- "label": _("Team Updates")
- },
- ]
- },
- {
- "label": _("Reports"),
- "icon": "fa fa-list",
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Employee Birthday",
- "doctype": "Employee"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Employees working on a holiday",
- "doctype": "Employee"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Department Analytics",
- "doctype": "Employee"
- },
- ]
- },
- ]
diff --git a/erpnext/config/hub_node.py b/erpnext/config/hub_node.py
deleted file mode 100644
index 0afdeb5..0000000
--- a/erpnext/config/hub_node.py
+++ /dev/null
@@ -1,24 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Settings"),
- "items": [
- {
- "type": "doctype",
- "name": "Marketplace Settings"
- },
- ]
- },
- {
- "label": _("Marketplace"),
- "items": [
- {
- "type": "page",
- "name": "marketplace/home"
- },
- ]
- },
- ]
\ No newline at end of file
diff --git a/erpnext/config/integrations.py b/erpnext/config/integrations.py
deleted file mode 100644
index f8b3257..0000000
--- a/erpnext/config/integrations.py
+++ /dev/null
@@ -1,51 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Payments"),
- "icon": "fa fa-star",
- "items": [
- {
- "type": "doctype",
- "name": "GoCardless Settings",
- "description": _("GoCardless payment gateway settings"),
- },
- {
- "type": "doctype",
- "name": "GoCardless Mandate",
- "description": _("GoCardless SEPA Mandate"),
- }
- ]
- },
- {
- "label": _("Settings"),
- "items": [
- {
- "type": "doctype",
- "name": "Woocommerce Settings"
- },
- {
- "type": "doctype",
- "name": "Shopify Settings",
- "description": _("Connect Shopify with ERPNext"),
- },
- {
- "type": "doctype",
- "name": "Amazon MWS Settings",
- "description": _("Connect Amazon with ERPNext"),
- },
- {
- "type": "doctype",
- "name": "Plaid Settings",
- "description": _("Connect your bank accounts to ERPNext"),
- },
- {
- "type": "doctype",
- "name": "Exotel Settings",
- "description": _("Connect your Exotel Account to ERPNext and track call logs"),
- }
- ]
- }
- ]
diff --git a/erpnext/config/loan_management.py b/erpnext/config/loan_management.py
deleted file mode 100644
index a84f13a..0000000
--- a/erpnext/config/loan_management.py
+++ /dev/null
@@ -1,107 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-import frappe
-
-
-def get_data():
- return [
- {
- "label": _("Loan"),
- "items": [
- {
- "type": "doctype",
- "name": "Loan Type",
- "description": _("Loan Type for interest and penalty rates"),
- },
- {
- "type": "doctype",
- "name": "Loan Application",
- "description": _("Loan Applications from customers and employees."),
- },
- {
- "type": "doctype",
- "name": "Loan",
- "description": _("Loans provided to customers and employees."),
- },
-
- ]
- },
- {
- "label": _("Loan Security"),
- "items": [
- {
- "type": "doctype",
- "name": "Loan Security Type",
- },
- {
- "type": "doctype",
- "name": "Loan Security Price",
- },
- {
- "type": "doctype",
- "name": "Loan Security",
- },
- {
- "type": "doctype",
- "name": "Loan Security Pledge",
- },
- {
- "type": "doctype",
- "name": "Loan Security Unpledge",
- },
- {
- "type": "doctype",
- "name": "Loan Security Shortfall",
- },
- ]
- },
- {
- "label": _("Disbursement and Repayment"),
- "items": [
- {
- "type": "doctype",
- "name": "Loan Disbursement",
- },
- {
- "type": "doctype",
- "name": "Loan Repayment",
- },
- {
- "type": "doctype",
- "name": "Loan Interest Accrual"
- }
- ]
- },
- {
- "label": _("Loan Processes"),
- "items": [
- {
- "type": "doctype",
- "name": "Process Loan Security Shortfall",
- },
- {
- "type": "doctype",
- "name": "Process Loan Interest Accrual",
- }
- ]
- },
- {
- "label": _("Reports"),
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Loan Repayment and Closure",
- "route": "#query-report/Loan Repayment and Closure",
- "doctype": "Loan Repayment",
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Loan Security Status",
- "route": "#query-report/Loan Security Status",
- "doctype": "Loan Security Pledge",
- }
- ]
- }
- ]
\ No newline at end of file
diff --git a/erpnext/config/manufacturing.py b/erpnext/config/manufacturing.py
deleted file mode 100644
index 012f1ca..0000000
--- a/erpnext/config/manufacturing.py
+++ /dev/null
@@ -1,168 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Bill of Materials"),
- "items": [
- {
- "type": "doctype",
- "name": "Item",
- "description": _("All Products or Services."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "BOM",
- "description": _("Bill of Materials (BOM)"),
- "label": _("Bill of Materials"),
- "onboard": 1,
- "dependencies": ["Item"]
- },
- {
- "type": "doctype",
- "name": "BOM Browser",
- "icon": "fa fa-sitemap",
- "label": _("BOM Browser"),
- "description": _("Tree of Bill of Materials"),
- "link": "Tree/BOM",
- "onboard": 1,
- "dependencies": ["Item"]
- },
-
- {
- "type": "doctype",
- "name": "Workstation",
- "description": _("Where manufacturing operations are carried."),
- },
- {
- "type": "doctype",
- "name": "Operation",
- "description": _("Details of the operations carried out."),
- },
- {
- "type": "doctype",
- "name": "Routing"
- }
-
- ]
- },
- {
- "label": _("Production"),
- "icon": "fa fa-star",
- "items": [
- {
- "type": "doctype",
- "name": "Work Order",
- "description": _("Orders released for production."),
- "onboard": 1,
- "dependencies": ["Item", "BOM"]
- },
- {
- "type": "doctype",
- "name": "Production Plan",
- "description": _("Generate Material Requests (MRP) and Work Orders."),
- "onboard": 1,
- "dependencies": ["Item", "BOM"]
- },
- {
- "type": "doctype",
- "name": "Stock Entry",
- "onboard": 1,
- "dependencies": ["Item"]
- },
- {
- "type": "doctype",
- "name": "Timesheet",
- "description": _("Time Sheet for manufacturing."),
- "onboard": 1,
- "dependencies": ["Activity Type"]
- },
- {
- "type": "doctype",
- "name": "Job Card"
- }
- ]
- },
- {
- "label": _("Tools"),
- "icon": "fa fa-wrench",
- "items": [
- {
- "type": "doctype",
- "name": "BOM Update Tool",
- "description": _("Replace BOM and update latest price in all BOMs"),
- },
- {
- "type": "page",
- "label": _("BOM Comparison Tool"),
- "name": "bom-comparison-tool",
- "description": _("Compare BOMs for changes in Raw Materials and Operations"),
- "data_doctype": "BOM"
- },
- ]
- },
- {
- "label": _("Settings"),
- "items": [
- {
- "type": "doctype",
- "name": "Manufacturing Settings",
- "description": _("Global settings for all manufacturing processes."),
- }
- ]
- },
- {
- "label": _("Reports"),
- "icon": "fa fa-list",
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Work Order Summary",
- "doctype": "Work Order"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Issued Items Against Work Order",
- "doctype": "Work Order"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Production Analytics",
- "doctype": "Work Order"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "BOM Search",
- "doctype": "BOM"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "BOM Stock Report",
- "doctype": "BOM"
- }
- ]
- },
- {
- "label": _("Help"),
- "icon": "fa fa-facetime-video",
- "items": [
- {
- "type": "help",
- "label": _("Bill of Materials"),
- "youtube_id": "hDV0c1OeWLo"
- },
- {
- "type": "help",
- "label": _("Work Order"),
- "youtube_id": "ZotgLyp2YFY"
- },
- ]
- }
- ]
diff --git a/erpnext/config/non_profit.py b/erpnext/config/non_profit.py
deleted file mode 100644
index 42ec9d3..0000000
--- a/erpnext/config/non_profit.py
+++ /dev/null
@@ -1,101 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Chapter"),
- "icon": "fa fa-star",
- "items": [
- {
- "type": "doctype",
- "name": "Chapter",
- "description": _("Chapter information."),
- "onboard": 1,
- }
- ]
- },
- {
- "label": _("Membership"),
- "items": [
- {
- "type": "doctype",
- "name": "Member",
- "description": _("Member information."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Membership",
- "description": _("Memebership Details"),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Membership Type",
- "description": _("Memebership Type Details"),
- },
- ]
- },
- {
- "label": _("Volunteer"),
- "items": [
- {
- "type": "doctype",
- "name": "Volunteer",
- "description": _("Volunteer information."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Volunteer Type",
- "description": _("Volunteer Type information."),
- }
- ]
- },
- {
- "label": _("Donor"),
- "items": [
- {
- "type": "doctype",
- "name": "Donor",
- "description": _("Donor information."),
- },
- {
- "type": "doctype",
- "name": "Donor Type",
- "description": _("Donor Type information."),
- }
- ]
- },
- {
- "label": _("Loan Management"),
- "icon": "icon-list",
- "items": [
- {
- "type": "doctype",
- "name": "Loan Type",
- "description": _("Define various loan types")
- },
- {
- "type": "doctype",
- "name": "Loan Application",
- "description": _("Loan Application")
- },
- {
- "type": "doctype",
- "name": "Loan"
- },
- ]
- },
- {
- "label": _("Grant Application"),
- "items": [
- {
- "type": "doctype",
- "name": "Grant Application",
- "description": _("Grant information."),
- }
- ]
- }
- ]
diff --git a/erpnext/config/projects.py b/erpnext/config/projects.py
index 47700d1..ab4db96 100644
--- a/erpnext/config/projects.py
+++ b/erpnext/config/projects.py
@@ -16,13 +16,13 @@
{
"type": "doctype",
"name": "Task",
- "route": "#List/Task",
+ "route": "/app/List/Task",
"description": _("Project activity / task."),
"onboard": 1,
},
{
"type": "report",
- "route": "#List/Task/Gantt",
+ "route": "/app/List/Task/Gantt",
"doctype": "Task",
"name": "Gantt Chart",
"description": _("Gantt chart of all tasks."),
@@ -97,5 +97,5 @@
},
]
},
-
+
]
diff --git a/erpnext/config/quality_management.py b/erpnext/config/quality_management.py
deleted file mode 100644
index 35acdfa..0000000
--- a/erpnext/config/quality_management.py
+++ /dev/null
@@ -1,73 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Goal and Procedure"),
- "items": [
- {
- "type": "doctype",
- "name": "Quality Goal",
- "description":_("Quality Goal."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Quality Procedure",
- "description":_("Quality Procedure."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Quality Procedure",
- "icon": "fa fa-sitemap",
- "label": _("Tree of Procedures"),
- "route": "#Tree/Quality Procedure",
- "description": _("Tree of Quality Procedures."),
- },
- ]
- },
- {
- "label": _("Review and Action"),
- "items": [
- {
- "type": "doctype",
- "name": "Quality Review",
- "description":_("Quality Review"),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Quality Action",
- "description":_("Quality Action"),
- }
- ]
- },
- {
- "label": _("Meeting"),
- "items": [
- {
- "type": "doctype",
- "name": "Quality Meeting",
- "description":_("Quality Meeting"),
- }
- ]
- },
- {
- "label": _("Feedback"),
- "items": [
- {
- "type": "doctype",
- "name": "Quality Feedback",
- "description":_("Quality Feedback"),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Quality Feedback Template",
- "description":_("Quality Feedback Template"),
- }
- ]
- },
- ]
\ No newline at end of file
diff --git a/erpnext/config/retail.py b/erpnext/config/retail.py
deleted file mode 100644
index 738be7e..0000000
--- a/erpnext/config/retail.py
+++ /dev/null
@@ -1,48 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Retail Operations"),
- "items": [
- {
- "type": "doctype",
- "name": "POS Profile",
- "label": _("Point-of-Sale Profile"),
- "description": _("Setup default values for POS Invoices"),
- "onboard": 1,
- },
- {
- "type": "page",
- "name": "pos",
- "label": _("POS"),
- "description": _("Point of Sale"),
- "onboard": 1,
- "dependencies": ["POS Profile"]
- },
- {
- "type": "doctype",
- "name": "Cashier Closing",
- "description": _("Cashier Closing"),
- },
- {
- "type": "doctype",
- "name": "POS Settings",
- "description": _("Setup mode of POS (Online / Offline)")
- },
- {
- "type": "doctype",
- "name": "Loyalty Program",
- "label": _("Loyalty Program"),
- "description": _("To make Customer based incentive schemes.")
- },
- {
- "type": "doctype",
- "name": "Loyalty Point Entry",
- "label": _("Loyalty Point Entry"),
- "description": _("To view logs of Loyalty Points assigned to a Customer.")
- }
- ]
- }
- ]
\ No newline at end of file
diff --git a/erpnext/config/selling.py b/erpnext/config/selling.py
deleted file mode 100644
index 5db4cc2..0000000
--- a/erpnext/config/selling.py
+++ /dev/null
@@ -1,320 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Sales"),
- "icon": "fa fa-star",
- "items": [
- {
- "type": "doctype",
- "name": "Customer",
- "description": _("Customer Database."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Quotation",
- "description": _("Quotes to Leads or Customers."),
- "onboard": 1,
- "dependencies": ["Item", "Customer"],
- },
- {
- "type": "doctype",
- "name": "Sales Order",
- "description": _("Confirmed orders from Customers."),
- "onboard": 1,
- "dependencies": ["Item", "Customer"],
- },
- {
- "type": "doctype",
- "name": "Sales Invoice",
- "description": _("Invoices for Costumers."),
- "onboard": 1,
- "dependencies": ["Item", "Customer"],
- },
- {
- "type": "doctype",
- "name": "Blanket Order",
- "description": _("Blanket Orders from Costumers."),
- "onboard": 1,
- "dependencies": ["Item", "Customer"],
- },
- {
- "type": "doctype",
- "name": "Sales Partner",
- "description": _("Manage Sales Partners."),
- "dependencies": ["Item"],
- },
- {
- "type": "doctype",
- "label": _("Sales Person"),
- "name": "Sales Person",
- "icon": "fa fa-sitemap",
- "link": "Tree/Sales Person",
- "description": _("Manage Sales Person Tree."),
- "dependencies": ["Item", "Customer"],
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Territory Target Variance (Item Group-Wise)",
- "route": "#query-report/Territory Target Variance Item Group-Wise",
- "doctype": "Territory",
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Sales Person Target Variance (Item Group-Wise)",
- "route": "#query-report/Sales Person Target Variance Item Group-Wise",
- "doctype": "Sales Person",
- "dependencies": ["Sales Person"],
- },
- ]
- },
- {
- "label": _("Items and Pricing"),
- "items": [
- {
- "type": "doctype",
- "name": "Item",
- "description": _("All Products or Services."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Item Price",
- "description": _("Multiple Item prices."),
- "route": "#Report/Item Price",
- "dependencies": ["Item", "Price List"],
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Price List",
- "description": _("Price List master."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Item Group",
- "icon": "fa fa-sitemap",
- "label": _("Item Group"),
- "link": "Tree/Item Group",
- "description": _("Tree of Item Groups."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Product Bundle",
- "description": _("Bundle items at time of sale."),
- "dependencies": ["Item"],
- },
- {
- "type": "doctype",
- "name": "Promotional Scheme",
- "description": _("Rules for applying different promotional schemes.")
- },
- {
- "type": "doctype",
- "name": "Pricing Rule",
- "description": _("Rules for applying pricing and discount."),
- "dependencies": ["Item"],
- },
- {
- "type": "doctype",
- "name": "Shipping Rule",
- "description": _("Rules for adding shipping costs."),
- },
- {
- "type": "doctype",
- "name": "Coupon Code",
- "description": _("Define coupon codes."),
- }
- ]
- },
- {
- "label": _("Settings"),
- "icon": "fa fa-cog",
- "items": [
- {
- "type": "doctype",
- "name": "Selling Settings",
- "description": _("Default settings for selling transactions."),
- "settings": 1,
- },
- {
- "type": "doctype",
- "name":"Terms and Conditions",
- "label": _("Terms and Conditions Template"),
- "description": _("Template of terms or contract."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Sales Taxes and Charges Template",
- "description": _("Tax template for selling transactions."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Lead Source",
- "description": _("Track Leads by Lead Source.")
- },
- {
- "type": "doctype",
- "label": _("Customer Group"),
- "name": "Customer Group",
- "icon": "fa fa-sitemap",
- "link": "Tree/Customer Group",
- "description": _("Manage Customer Group Tree."),
- },
- {
- "type": "doctype",
- "name": "Contact",
- "description": _("All Contacts."),
- },
- {
- "type": "doctype",
- "name": "Address",
- "description": _("All Addresses."),
- },
- {
- "type": "doctype",
- "label": _("Territory"),
- "name": "Territory",
- "icon": "fa fa-sitemap",
- "link": "Tree/Territory",
- "description": _("Manage Territory Tree."),
- },
- {
- "type": "doctype",
- "name": "Campaign",
- "description": _("Sales campaigns."),
- },
- ]
- },
- {
- "label": _("Key Reports"),
- "icon": "fa fa-table",
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Sales Analytics",
- "doctype": "Sales Order",
- "onboard": 1,
- },
- {
- "type": "page",
- "name": "sales-funnel",
- "label": _("Sales Funnel"),
- "icon": "fa fa-bar-chart",
- "onboard": 1,
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Customer Acquisition and Loyalty",
- "doctype": "Customer",
- "icon": "fa fa-bar-chart",
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Inactive Customers",
- "doctype": "Sales Order"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Ordered Items To Be Delivered",
- "doctype": "Sales Order"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Sales Person-wise Transaction Summary",
- "doctype": "Sales Order"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Item-wise Sales History",
- "doctype": "Item"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Quotation Trends",
- "doctype": "Quotation"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Sales Order Trends",
- "doctype": "Sales Order"
- },
- ]
- },
- {
- "label": _("Other Reports"),
- "icon": "fa fa-list",
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Lead Details",
- "doctype": "Lead"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Address And Contacts",
- "label": _("Customer Addresses And Contacts"),
- "doctype": "Address",
- "route_options": {
- "party_type": "Customer"
- }
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "BOM Search",
- "doctype": "BOM"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Available Stock for Packing Items",
- "doctype": "Item",
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Pending SO Items For Purchase Request",
- "doctype": "Sales Order"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Customer Credit Balance",
- "doctype": "Customer"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Customers Without Any Sales Transactions",
- "doctype": "Customer"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Sales Partners Commission",
- "doctype": "Customer"
- }
- ]
- },
-
- ]
diff --git a/erpnext/config/settings.py b/erpnext/config/settings.py
deleted file mode 100644
index 323683a..0000000
--- a/erpnext/config/settings.py
+++ /dev/null
@@ -1,117 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-from frappe.desk.moduleview import add_setup_section
-
-def get_data():
- data = [
- {
- "label": _("Settings"),
- "icon": "fa fa-wrench",
- "items": [
- {
- "type": "doctype",
- "name": "Global Defaults",
- "label": _("ERPNext Settings"),
- "description": _("Set Default Values like Company, Currency, Current Fiscal Year, etc."),
- "hide_count": True,
- "settings": 1,
- }
- ]
- },
- {
- "label": _("Printing"),
- "icon": "fa fa-print",
- "items": [
- {
- "type": "doctype",
- "name": "Letter Head",
- "description": _("Letter Heads for print templates."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Print Heading",
- "description": _("Titles for print templates e.g. Proforma Invoice.")
- },
- {
- "type": "doctype",
- "name": "Address Template",
- "description": _("Country wise default Address Templates")
- },
- {
- "type": "doctype",
- "name": "Terms and Conditions",
- "description": _("Standard contract terms for Sales or Purchase.")
- },
- ]
- },
- {
- "label": _("Help"),
- "items": [
- {
- "type": "help",
- "name": _("Data Import and Export"),
- "youtube_id": "6wiriRKPhmg"
- },
- {
- "type": "help",
- "label": _("Setting up Email"),
- "youtube_id": "YFYe0DrB95o"
- },
- {
- "type": "help",
- "label": _("Printing and Branding"),
- "youtube_id": "cKZHcx1znMc"
- },
- {
- "type": "help",
- "label": _("Users and Permissions"),
- "youtube_id": "8Slw1hsTmUI"
- },
- {
- "type": "help",
- "label": _("Workflow"),
- "youtube_id": "yObJUg9FxFs"
- },
- ]
- },
- {
- "label": _("Customize"),
- "icon": "fa fa-glass",
- "items": [
- {
- "type": "doctype",
- "name": "Authorization Rule",
- "description": _("Create rules to restrict transactions based on values.")
- }
- ]
- },
- {
- "label": _("Email"),
- "icon": "fa fa-envelope",
- "items": [
- {
- "type": "doctype",
- "name": "Email Digest",
- "description": _("Create and manage daily, weekly and monthly email digests.")
- },
- {
- "type": "doctype",
- "name": "SMS Settings",
- "description": _("Setup SMS gateway settings")
- },
- ]
- }
- ]
-
- for module, label, icon in (
- ("accounts", _("Accounting"), "fa fa-money"),
- ("stock", _("Stock"), "fa fa-truck"),
- ("selling", _("Selling"), "fa fa-tag"),
- ("buying", _("Buying"), "fa fa-shopping-cart"),
- ("hr", _("Human Resources"), "fa fa-group"),
- ("support", _("Support"), "fa fa-phone")):
-
- add_setup_section(data, "erpnext", module, label, icon)
-
- return data
diff --git a/erpnext/config/stock.py b/erpnext/config/stock.py
deleted file mode 100644
index dd35f5a..0000000
--- a/erpnext/config/stock.py
+++ /dev/null
@@ -1,361 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Stock Transactions"),
- "items": [
- {
- "type": "doctype",
- "name": "Stock Entry",
- "onboard": 1,
- "dependencies": ["Item"],
- },
- {
- "type": "doctype",
- "name": "Delivery Note",
- "onboard": 1,
- "dependencies": ["Item", "Customer"],
- },
- {
- "type": "doctype",
- "name": "Purchase Receipt",
- "onboard": 1,
- "dependencies": ["Item", "Supplier"],
- },
- {
- "type": "doctype",
- "name": "Material Request",
- "onboard": 1,
- "dependencies": ["Item"],
- },
- {
- "type": "doctype",
- "name": "Pick List",
- "onboard": 1,
- "dependencies": ["Item"],
- },
- {
- "type": "doctype",
- "name": "Delivery Trip"
- },
- ]
- },
- {
- "label": _("Stock Reports"),
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Stock Ledger",
- "doctype": "Stock Ledger Entry",
- "onboard": 1,
- "dependencies": ["Item"],
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Stock Balance",
- "doctype": "Stock Ledger Entry",
- "onboard": 1,
- "dependencies": ["Item"],
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Stock Projected Qty",
- "doctype": "Item",
- "onboard": 1,
- "dependencies": ["Item"],
- },
- {
- "type": "page",
- "name": "stock-balance",
- "label": _("Stock Summary"),
- "dependencies": ["Item"],
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Stock Ageing",
- "doctype": "Item",
- "dependencies": ["Item"],
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Item Price Stock",
- "doctype": "Item",
- "dependencies": ["Item"],
- }
- ]
- },
- {
- "label": _("Settings"),
- "icon": "fa fa-cog",
- "items": [
- {
- "type": "doctype",
- "name": "Stock Settings",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Warehouse",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "UOM",
- "label": _("Unit of Measure") + " (UOM)",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Brand",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Item Attribute",
- },
- {
- "type": "doctype",
- "name": "Item Variant Settings",
- },
- ]
- },
- {
- "label": _("Items and Pricing"),
- "items": [
- {
- "type": "doctype",
- "name": "Item",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Product Bundle",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Item Group",
- "icon": "fa fa-sitemap",
- "label": _("Item Group"),
- "link": "Tree/Item Group",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Price List",
- },
- {
- "type": "doctype",
- "name": "Item Price",
- },
- {
- "type": "doctype",
- "name": "Shipping Rule",
- },
- {
- "type": "doctype",
- "name": "Pricing Rule",
- },
- {
- "type": "doctype",
- "name": "Item Alternative",
- },
- {
- "type": "doctype",
- "name": "Item Manufacturer",
- },
- {
- "type": "doctype",
- "name": "Item Variant Settings",
- },
- ]
- },
- {
- "label": _("Serial No and Batch"),
- "items": [
- {
- "type": "doctype",
- "name": "Serial No",
- "onboard": 1,
- "dependencies": ["Item"],
- },
- {
- "type": "doctype",
- "name": "Batch",
- "onboard": 1,
- "dependencies": ["Item"],
- },
- {
- "type": "doctype",
- "name": "Installation Note",
- "dependencies": ["Item"],
- },
- {
- "type": "report",
- "name": "Serial No Service Contract Expiry",
- "doctype": "Serial No"
- },
- {
- "type": "report",
- "name": "Serial No Status",
- "doctype": "Serial No"
- },
- {
- "type": "report",
- "name": "Serial No Warranty Expiry",
- "doctype": "Serial No"
- },
- ]
- },
- {
- "label": _("Tools"),
- "icon": "fa fa-wrench",
- "items": [
- {
- "type": "doctype",
- "name": "Stock Reconciliation",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Landed Cost Voucher",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Packing Slip",
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Quality Inspection",
- },
- {
- "type": "doctype",
- "name": "Quality Inspection Template",
- },
- {
- "type": "doctype",
- "name": "Quick Stock Balance",
- },
- ]
- },
- {
- "label": _("Key Reports"),
- "icon": "fa fa-table",
- "items": [
- {
- "type": "report",
- "is_query_report": False,
- "name": "Item-wise Price List Rate",
- "doctype": "Item Price",
- "onboard": 1,
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Stock Analytics",
- "doctype": "Stock Entry",
- "onboard": 1,
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Delivery Note Trends",
- "doctype": "Delivery Note"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Purchase Receipt Trends",
- "doctype": "Purchase Receipt"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Ordered Items To Be Delivered",
- "doctype": "Delivery Note"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Purchase Order Items To Be Received",
- "doctype": "Purchase Receipt"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Item Shortage Report",
- "doctype": "Bin"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Batch-Wise Balance History",
- "doctype": "Batch"
- },
- ]
- },
- {
- "label": _("Other Reports"),
- "icon": "fa fa-list",
- "items": [
- {
- "type": "report",
- "is_query_report": True,
- "name": "Requested Items To Be Transferred",
- "doctype": "Material Request"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Batch Item Expiry Status",
- "doctype": "Stock Ledger Entry"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Item Prices",
- "doctype": "Price List"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Itemwise Recommended Reorder Level",
- "doctype": "Item"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Item Variant Details",
- "doctype": "Item"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Subcontracted Raw Materials To Be Transferred",
- "doctype": "Purchase Order"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Subcontracted Item To Be Received",
- "doctype": "Purchase Order"
- },
- {
- "type": "report",
- "is_query_report": True,
- "name": "Stock and Account Value Comparison",
- "doctype": "Stock Ledger Entry"
- }
- ]
- },
-
- ]
diff --git a/erpnext/config/support.py b/erpnext/config/support.py
deleted file mode 100644
index 151c4f7..0000000
--- a/erpnext/config/support.py
+++ /dev/null
@@ -1,105 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Issues"),
- "items": [
- {
- "type": "doctype",
- "name": "Issue",
- "description": _("Support queries from customers."),
- "onboard": 1,
- },
- {
- "type": "doctype",
- "name": "Issue Type",
- "description": _("Issue Type."),
- },
- {
- "type": "doctype",
- "name": "Issue Priority",
- "description": _("Issue Priority."),
- }
- ]
- },
- {
- "label": _("Warranty"),
- "items": [
- {
- "type": "doctype",
- "name": "Warranty Claim",
- "description": _("Warranty Claim against Serial No."),
- },
- {
- "type": "doctype",
- "name": "Serial No",
- "description": _("Single unit of an Item."),
- },
- ]
- },
- {
- "label": _("Service Level Agreement"),
- "items": [
- {
- "type": "doctype",
- "name": "Service Level",
- "description": _("Service Level."),
- },
- {
- "type": "doctype",
- "name": "Service Level Agreement",
- "description": _("Service Level Agreement."),
- }
- ]
- },
- {
- "label": _("Maintenance"),
- "items": [
- {
- "type": "doctype",
- "name": "Maintenance Schedule",
- },
- {
- "type": "doctype",
- "name": "Maintenance Visit",
- },
- ]
- },
- {
- "label": _("Reports"),
- "icon": "fa fa-list",
- "items": [
- {
- "type": "page",
- "name": "support-analytics",
- "label": _("Support Analytics"),
- "icon": "fa fa-bar-chart"
- },
- {
- "type": "report",
- "name": "Minutes to First Response for Issues",
- "doctype": "Issue",
- "is_query_report": True
- },
- {
- "type": "report",
- "name": "Support Hours",
- "doctype": "Issue",
- "is_query_report": True
- },
- ]
- },
- {
- "label": _("Settings"),
- "icon": "fa fa-list",
- "items": [
- {
- "type": "doctype",
- "name": "Support Settings",
- "label": _("Support Settings"),
- },
- ]
- },
- ]
\ No newline at end of file
diff --git a/erpnext/config/website.py b/erpnext/config/website.py
deleted file mode 100644
index d31b057..0000000
--- a/erpnext/config/website.py
+++ /dev/null
@@ -1,33 +0,0 @@
-from __future__ import unicode_literals
-from frappe import _
-
-def get_data():
- return [
- {
- "label": _("Portal"),
- "items": [
- {
- "type": "doctype",
- "name": "Homepage",
- "description": _("Settings for website homepage"),
- },
- {
- "type": "doctype",
- "name": "Homepage Section",
- "description": _("Add cards or custom sections on homepage"),
- },
- {
- "type": "doctype",
- "name": "Products Settings",
- "description": _("Settings for website product listing"),
- },
- {
- "type": "doctype",
- "name": "Shopping Cart Settings",
- "label": _("Shopping Cart Settings"),
- "description": _("Settings for online shopping cart such as shipping rules, price list etc."),
- "hide_count": True
- }
- ]
- }
- ]
diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py
index 6108a61..12a81c7 100644
--- a/erpnext/controllers/accounts_controller.py
+++ b/erpnext/controllers/accounts_controller.py
@@ -22,6 +22,9 @@
from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions
from erpnext.stock.get_item_details import get_item_warehouse, _get_item_tax_template, get_item_tax_map
from erpnext.stock.doctype.packed_item.packed_item import make_packing_list
+from erpnext.controllers.print_settings import set_print_templates_for_item_table, set_print_templates_for_taxes
+
+class AccountMissingError(frappe.ValidationError): pass
force_item_fields = ("item_group", "brand", "stock_uom", "is_fixed_asset", "item_tax_rate", "pricing_rules")
@@ -29,6 +32,19 @@
def __init__(self, *args, **kwargs):
super(AccountsController, self).__init__(*args, **kwargs)
+ def get_print_settings(self):
+ print_setting_fields = []
+ items_field = self.meta.get_field('items')
+
+ if items_field and items_field.fieldtype == 'Table':
+ print_setting_fields += ['compact_item_print', 'print_uom_after_quantity']
+
+ taxes_field = self.meta.get_field('taxes')
+ if taxes_field and taxes_field.fieldtype == 'Table':
+ print_setting_fields += ['print_taxes_with_zero_amount']
+
+ return print_setting_fields
+
@property
def company_currency(self):
if not hasattr(self, "__company_currency"):
@@ -73,6 +89,9 @@
self.ensure_supplier_is_not_blocked()
self.validate_date_with_fiscal_year()
+ self.validate_inter_company_reference()
+
+ self.set_incoming_rate()
if self.meta.get_field("currency"):
self.calculate_taxes_and_totals()
@@ -105,10 +124,24 @@
else:
self.validate_deferred_start_and_end_date()
+ self.set_inter_company_account()
+
validate_regional(self)
+
+ validate_einvoice_fields(self)
+
if self.doctype != 'Material Request':
apply_pricing_rule_on_transaction(self)
+ def before_cancel(self):
+ validate_einvoice_fields(self)
+
+ def on_trash(self):
+ # delete sl and gl entries on deletion of transaction
+ if frappe.db.get_single_value('Accounts Settings', 'delete_linked_ledger_entries'):
+ frappe.db.sql("delete from `tabGL Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name))
+ frappe.db.sql("delete from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s", (self.doctype, self.name))
+
def validate_deferred_start_and_end_date(self):
for d in self.items:
if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"):
@@ -138,7 +171,7 @@
elif self.doctype in ("Quotation", "Purchase Order", "Sales Order"):
self.validate_non_invoice_documents_schedule()
- def before_print(self):
+ def before_print(self, settings=None):
if self.doctype in ['Purchase Order', 'Sales Order', 'Sales Invoice', 'Purchase Invoice',
'Supplier Quotation', 'Purchase Receipt', 'Delivery Note', 'Quotation']:
if self.get("group_same_items"):
@@ -151,6 +184,9 @@
else:
df.set("print_hide", 1)
+ set_print_templates_for_item_table(self, settings)
+ set_print_templates_for_taxes(self, settings)
+
def calculate_paid_amount(self):
if hasattr(self, "is_pos") or hasattr(self, "is_paid"):
is_paid = self.get("is_pos") or self.get("is_paid")
@@ -196,6 +232,17 @@
validate_fiscal_year(self.get(date_field), self.fiscal_year, self.company,
self.meta.get_label(date_field), self)
+ def validate_inter_company_reference(self):
+ if self.doctype not in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
+ return
+
+ if self.is_internal_transfer():
+ if not (self.get('inter_company_reference') or self.get('inter_company_invoice_reference')
+ or self.get('inter_company_order_reference')):
+ msg = _("Internal Sale or Delivery Reference missing. ")
+ msg += _("Please create purchase from internal sale or delivery document itself")
+ frappe.throw(msg, title=_("Internal Sales Reference Missing"))
+
def validate_due_date(self):
if self.get('is_pos'): return
@@ -272,6 +319,7 @@
args["doctype"] = self.doctype
args["name"] = self.name
args["child_docname"] = item.name
+ args["ignore_pricing_rule"] = self.ignore_pricing_rule if hasattr(self, 'ignore_pricing_rule') else 0
if not args.get("transaction_date"):
args["transaction_date"] = args.get("posting_date")
@@ -438,8 +486,10 @@
account_currency = get_account_currency(gl_dict.account)
if gl_dict.account and self.doctype not in ["Journal Entry",
- "Period Closing Voucher", "Payment Entry"]:
+ "Period Closing Voucher", "Payment Entry", "Purchase Receipt", "Purchase Invoice", "Stock Entry"]:
self.validate_account_currency(gl_dict.account, account_currency)
+
+ if gl_dict.account and self.doctype not in ["Journal Entry", "Period Closing Voucher", "Payment Entry"]:
set_balance_in_account_currency(gl_dict, account_currency, self.get("conversion_rate"),
self.company_currency)
@@ -735,6 +785,21 @@
return self._abbr
+ def raise_missing_debit_credit_account_error(self, party_type, party):
+ """Raise an error if debit to/credit to account does not exist."""
+ db_or_cr = frappe.bold("Debit To") if self.doctype == "Sales Invoice" else frappe.bold("Credit To")
+ rec_or_pay = "Receivable" if self.doctype == "Sales Invoice" else "Payable"
+
+ link_to_party = frappe.utils.get_link_to_form(party_type, party)
+ link_to_company = frappe.utils.get_link_to_form("Company", self.company)
+
+ message = _("{0} Account not found against Customer {1}.").format(db_or_cr, frappe.bold(party) or '')
+ message += "<br>" + _("Please set one of the following:") + "<br>"
+ message += "<br><ul><li>" + _("'Account' in the Accounting section of Customer {0}").format(link_to_party) + "</li>"
+ message += "<li>" + _("'Default {0} Account' in Company {1}").format(rec_or_pay, link_to_company) + "</li></ul>"
+
+ frappe.throw(message, title=_("Account Missing"), exc=AccountMissingError)
+
def validate_party(self):
party_type, party = self.get_party()
validate_party_frozen_disabled(party_type, party)
@@ -915,6 +980,38 @@
else:
return frappe.db.get_single_value("Global Defaults", "disable_rounded_total")
+ def set_inter_company_account(self):
+ """
+ Set intercompany account for inter warehouse transactions
+ This account will be used in case billing company and internal customer's
+ representation company is same
+ """
+
+ if self.is_internal_transfer() and not self.unrealized_profit_loss_account:
+ unrealized_profit_loss_account = frappe.db.get_value('Company', self.company, 'unrealized_profit_loss_account')
+
+ if not unrealized_profit_loss_account:
+ msg = _("Please select Unrealized Profit / Loss account or add default Unrealized Profit / Loss account account for company {0}").format(
+ frappe.bold(self.company))
+ frappe.throw(msg)
+
+ self.unrealized_profit_loss_account = unrealized_profit_loss_account
+
+ def is_internal_transfer(self):
+ """
+ It will an internal transfer if its an internal customer and representation
+ company is same as billing company
+ """
+ if self.doctype in ('Sales Invoice', 'Delivery Note', 'Sales Order'):
+ internal_party_field = 'is_internal_customer'
+ elif self.doctype in ('Purchase Invoice', 'Purchase Receipt', 'Purchase Order'):
+ internal_party_field = 'is_internal_supplier'
+
+ if self.get(internal_party_field) and (self.represents_company == self.company):
+ return True
+
+ return False
+
@frappe.whitelist()
def get_tax_rate(account_head):
return frappe.db.get_value("Account", account_head, ["tax_rate", "account_name"], as_dict=True)
@@ -1212,45 +1309,28 @@
})
tax_row.db_insert()
-def set_sales_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
+def set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, trans_item):
"""
- Returns a Sales Order Item child item containing the default values
+ Returns a Sales/Purchase Order Item child item containing the default values
"""
p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
- child_item = frappe.new_doc('Sales Order Item', p_doc, child_docname)
+ child_item = frappe.new_doc(child_doctype, p_doc, child_docname)
item = frappe.get_doc("Item", trans_item.get('item_code'))
- child_item.item_code = item.item_code
- child_item.item_name = item.item_name
- child_item.description = item.description
- child_item.delivery_date = trans_item.get('delivery_date') or p_doc.delivery_date
+ for field in ("item_code", "item_name", "description", "item_group"):
+ child_item.update({field: item.get(field)})
+ date_fieldname = "delivery_date" if child_doctype == "Sales Order Item" else "schedule_date"
+ child_item.update({date_fieldname: trans_item.get(date_fieldname) or p_doc.get(date_fieldname)})
child_item.uom = trans_item.get("uom") or item.stock_uom
conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
- set_child_tax_template_and_map(item, child_item, p_doc)
- add_taxes_from_tax_template(child_item, p_doc)
- child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
- if not child_item.warehouse:
- frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
- .format(frappe.bold("default warehouse"), frappe.bold(item.item_code)))
- return child_item
-
-
-def set_purchase_order_defaults(parent_doctype, parent_doctype_name, child_docname, trans_item):
- """
- Returns a Purchase Order Item child item containing the default values
- """
- p_doc = frappe.get_doc(parent_doctype, parent_doctype_name)
- child_item = frappe.new_doc('Purchase Order Item', p_doc, child_docname)
- item = frappe.get_doc("Item", trans_item.get('item_code'))
- child_item.item_code = item.item_code
- child_item.item_name = item.item_name
- child_item.description = item.description
- child_item.schedule_date = trans_item.get('schedule_date') or p_doc.schedule_date
- child_item.uom = trans_item.get("uom") or item.stock_uom
- conversion_factor = flt(get_conversion_factor(item.item_code, child_item.uom).get("conversion_factor"))
- child_item.conversion_factor = flt(trans_item.get('conversion_factor')) or conversion_factor
- child_item.base_rate = 1 # Initiallize value will update in parent validation
- child_item.base_amount = 1 # Initiallize value will update in parent validation
+ if child_doctype == "Purchase Order Item":
+ child_item.base_rate = 1 # Initiallize value will update in parent validation
+ child_item.base_amount = 1 # Initiallize value will update in parent validation
+ if child_doctype == "Sales Order Item":
+ child_item.warehouse = get_item_warehouse(item, p_doc, overwrite_warehouse=True)
+ if not child_item.warehouse:
+ frappe.throw(_("Cannot find {} for item {}. Please set the same in Item Master or Stock Settings.")
+ .format(frappe.bold("default warehouse"), frappe.bold(item.item_code)))
set_child_tax_template_and_map(item, child_item, p_doc)
add_taxes_from_tax_template(child_item, p_doc)
return child_item
@@ -1314,8 +1394,8 @@
)
def get_new_child_item(item_row):
- new_child_function = set_sales_order_defaults if parent_doctype == "Sales Order" else set_purchase_order_defaults
- return new_child_function(parent_doctype, parent_doctype_name, child_docname, item_row)
+ child_doctype = "Sales Order Item" if parent_doctype == "Sales Order" else "Purchase Order Item"
+ return set_order_defaults(parent_doctype, parent_doctype_name, child_doctype, child_docname, item_row)
def validate_quantity(child_item, d):
if parent_doctype == "Sales Order" and flt(d.get("qty")) < flt(child_item.delivered_qty):
@@ -1424,6 +1504,7 @@
parent.flags.ignore_validate_update_after_submit = True
parent.set_qty_as_per_stock_uom()
parent.calculate_taxes_and_totals()
+ parent.set_total_in_words()
if parent_doctype == "Sales Order":
make_packing_list(parent)
parent.set_gross_profit()
@@ -1467,3 +1548,7 @@
@erpnext.allow_regional
def validate_regional(doc):
pass
+
+@erpnext.allow_regional
+def validate_einvoice_fields(doc):
+ pass
diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py
index 9ee83e3..e469838 100644
--- a/erpnext/controllers/buying_controller.py
+++ b/erpnext/controllers/buying_controller.py
@@ -16,18 +16,10 @@
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):
- def __setup__(self):
- if hasattr(self, "taxes"):
- self.flags.print_taxes_with_zero_amount = cint(frappe.db.get_single_value("Print Settings",
- "print_taxes_with_zero_amount"))
- self.flags.show_inclusive_tax_in_print = self.is_inclusive_tax()
-
- self.print_templates = {
- "total": "templates/print_formats/includes/total.html",
- "taxes": "templates/print_formats/includes/taxes.html"
- }
def get_feed(self):
if self.get("supplier_name"):
@@ -62,7 +54,7 @@
self.set_landed_cost_voucher_amount()
if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
- self.update_valuation_rate("items")
+ self.update_valuation_rate()
def set_missing_values(self, for_validate=False):
super(BuyingController, self).set_missing_values(for_validate)
@@ -94,13 +86,18 @@
def validate_stock_or_nonstock_items(self):
if self.meta.get_field("taxes") and not self.get_stock_items() and not self.get_asset_items():
- tax_for_valuation = [d for d in self.get("taxes")
+ msg = _('Tax Category has been changed to "Total" because all the Items are non-stock items')
+ self.update_tax_category(msg)
+
+ def update_tax_category(self, msg):
+ tax_for_valuation = [d for d in self.get("taxes")
if d.category in ["Valuation", "Valuation and Total"]]
- if tax_for_valuation:
- for d in tax_for_valuation:
- d.category = 'Total'
- msgprint(_('Tax Category has been changed to "Total" because all the Items are non-stock items'))
+ if tax_for_valuation:
+ for d in tax_for_valuation:
+ d.category = 'Total'
+
+ msgprint(msg)
def validate_asset_return(self):
if self.doctype not in ['Purchase Receipt', 'Purchase Invoice'] or not self.is_return:
@@ -166,7 +163,7 @@
self.in_words = money_in_words(amount, self.currency)
# update valuation rate
- def update_valuation_rate(self, parentfield):
+ def update_valuation_rate(self, reset_outgoing_rate=True):
"""
item_tax_amount is the total tax amount applied on that item
stored for valuation
@@ -177,7 +174,7 @@
stock_and_asset_items_qty, stock_and_asset_items_amount = 0, 0
last_item_idx = 1
- for d in self.get(parentfield):
+ for d in self.get("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)
@@ -187,7 +184,7 @@
if d.category in ["Valuation", "Valuation and Total"]])
valuation_amount_adjustment = total_valuation_amount
- for i, item in enumerate(self.get(parentfield)):
+ for i, item in enumerate(self.get("items")):
if item.item_code and item.qty and item.item_code in stock_and_asset_items:
item_proportion = flt(item.base_net_amount) / stock_and_asset_items_amount if stock_and_asset_items_amount \
else flt(item.qty) / stock_and_asset_items_qty
@@ -205,16 +202,76 @@
item.conversion_factor = get_conversion_factor(item.item_code, item.uom).get("conversion_factor") or 1.0
qty_in_stock_uom = flt(item.qty * item.conversion_factor)
- rm_supp_cost = flt(item.rm_supp_cost) if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
-
- landed_cost_voucher_amount = flt(item.landed_cost_voucher_amount) \
- if self.doctype in ["Purchase Receipt", "Purchase Invoice"] else 0.0
-
- item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + rm_supp_cost
- + landed_cost_voucher_amount) / qty_in_stock_uom)
+ item.rm_supp_cost = self.get_supplied_items_cost(item.name, reset_outgoing_rate)
+ item.valuation_rate = ((item.base_net_amount + item.item_tax_amount + item.rm_supp_cost
+ + flt(item.landed_cost_voucher_amount)) / qty_in_stock_uom)
else:
item.valuation_rate = 0.0
+ def set_incoming_rate(self):
+ if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Purchase Order"):
+ return
+
+ ref_doctype_map = {
+ "Purchase Order": "Sales Order Item",
+ "Purchase Receipt": "Delivery Note Item",
+ "Purchase Invoice": "Sales Invoice Item",
+ }
+
+ ref_doctype = ref_doctype_map.get(self.doctype)
+ items = self.get("items")
+ for d in items:
+ if not cint(self.get("is_return")):
+ # Get outgoing rate based on original item cost based on valuation method
+
+ if not d.get(frappe.scrub(ref_doctype)):
+ outgoing_rate = get_incoming_rate({
+ "item_code": d.item_code,
+ "warehouse": d.get('from_warehouse'),
+ "posting_date": self.get('posting_date') or self.get('transation_date'),
+ "posting_time": self.get('posting_time'),
+ "qty": -1 * flt(d.get('stock_qty')),
+ "serial_no": d.get('serial_no'),
+ "company": self.company,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "allow_zero_valuation": d.get("allow_zero_valuation")
+ }, raise_error_if_no_rate=False)
+
+ rate = flt(outgoing_rate * d.conversion_factor, d.precision('rate'))
+ else:
+ rate = frappe.db.get_value(ref_doctype, d.get(frappe.scrub(ref_doctype)), 'rate')
+
+ if self.is_internal_transfer():
+ if rate != d.rate:
+ d.rate = rate
+ d.discount_percentage = 0
+ d.discount_amount = 0
+ frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
+ .format(d.idx), alert=1)
+
+ def get_supplied_items_cost(self, item_row_id, reset_outgoing_rate=True):
+ 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'):
+ rate = get_incoming_rate({
+ "item_code": d.rm_item_code,
+ "warehouse": self.supplier_warehouse,
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "qty": -1 * d.consumed_qty,
+ "serial_no": d.serial_no
+ })
+
+ if rate > 0:
+ d.rate = rate
+
+ d.amount = flt(flt(d.consumed_qty) * flt(d.rate), d.precision("amount"))
+ supplied_items_cost += flt(d.amount)
+
+ return supplied_items_cost
+
def validate_for_subcontracting(self):
if not self.is_subcontracted and self.sub_contracted_items:
frappe.throw(_("Please enter 'Is Subcontracted' as Yes or No"))
@@ -305,7 +362,7 @@
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_nos', '')
+ consumed_serial_nos = raw_material_data.get('serial_no', '')
consumed_batch_nos = raw_material_data.get('batch_nos', '')
transferred_qty = raw_material.qty
@@ -341,35 +398,17 @@
else:
self.append_raw_material_to_be_backflushed(item, raw_material, qty)
- def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, 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_doc.item_code
+ rm.main_item_code = fg_item_row.item_code
- rm.reference_name = fg_item_doc.name
+ rm.reference_name = fg_item_row.name
rm.required_qty = qty
rm.consumed_qty = qty
- if not raw_material_data.get('non_stock_item'):
- from erpnext.stock.utils import get_incoming_rate
- rm.rate = get_incoming_rate({
- "item_code": raw_material_data.rm_item_code,
- "warehouse": self.supplier_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1 * qty,
- "serial_no": rm.serial_no
- })
-
- if not rm.rate:
- rm.rate = get_valuation_rate(raw_material_data.rm_item_code, self.supplier_warehouse,
- self.doctype, self.name, currency=self.company_currency, company=self.company)
-
- rm.amount = qty * flt(rm.rate)
- fg_item_doc.rm_supp_cost += rm.amount
-
def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table):
exploded_item = 1
if hasattr(item, 'include_exploded_items'):
@@ -378,7 +417,7 @@
bom_items = get_items_from_bom(item.item_code, item.bom, exploded_item)
used_alternative_items = []
- if self.doctype == 'Purchase Receipt' and item.purchase_order:
+ 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
@@ -395,7 +434,7 @@
reserve_warehouse = None
conversion_factor = item.conversion_factor
- if (self.doctype == 'Purchase Receipt' and item.purchase_order and
+ 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
@@ -423,9 +462,7 @@
rm.rm_item_code = bom_item.item_code
rm.stock_uom = bom_item.stock_uom
rm.required_qty = required_qty
- if self.doctype == "Purchase Order" and not rm.reserve_warehouse:
- rm.reserve_warehouse = reserve_warehouse
-
+ rm.rate = bom_item.rate
rm.conversion_factor = conversion_factor
if self.doctype in ["Purchase Receipt", "Purchase Invoice"]:
@@ -433,29 +470,8 @@
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
-
- # get raw materials rate
- if self.doctype == "Purchase Receipt":
- from erpnext.stock.utils import get_incoming_rate
- rm.rate = get_incoming_rate({
- "item_code": bom_item.item_code,
- "warehouse": self.supplier_warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1 * required_qty,
- "serial_no": rm.serial_no
- })
- if not rm.rate:
- rm.rate = get_valuation_rate(bom_item.item_code, self.supplier_warehouse,
- self.doctype, self.name, currency=self.company_currency, company = self.company)
- else:
- rm.rate = bom_item.rate
-
- rm.amount = required_qty * flt(rm.rate)
- raw_materials_cost += flt(rm.amount)
-
- if self.doctype in ("Purchase Receipt", "Purchase Invoice"):
- item.rm_supp_cost = raw_materials_cost
+ 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"""
@@ -497,6 +513,10 @@
frappe.throw(_("Row {0}: Conversion Factor is mandatory").format(d.idx))
d.stock_qty = flt(d.qty) * flt(d.conversion_factor)
+ if self.doctype=="Purchase Receipt" and d.meta.get_field("received_stock_qty"):
+ # Set Received Qty in Stock UOM
+ d.received_stock_qty = flt(d.received_qty) * flt(d.conversion_factor, d.precision("conversion_factor"))
+
def validate_purchase_return(self):
for d in self.get("items"):
if self.is_return and flt(d.rejected_qty) != 0:
@@ -564,7 +584,10 @@
or (cint(self.is_return) and self.docstatus==2)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
- "warehouse": d.from_warehouse
+ "warehouse": d.from_warehouse,
+ "outgoing_rate": d.rate,
+ "recalculate_rate": 1,
+ "dependant_sle_voucher_detail_no": d.name
})
sl_entries.append(from_warehouse_sle)
@@ -574,28 +597,20 @@
"serial_no": cstr(d.serial_no).strip()
})
if self.is_return:
- filters = {
- "voucher_type": self.doctype,
- "voucher_no": self.return_against,
- "item_code": d.item_code
- }
-
- if (self.doctype == "Purchase Invoice" and self.update_stock
- and d.get("purchase_invoice_item")):
- filters["voucher_detail_no"] = d.purchase_invoice_item
- elif self.doctype == "Purchase Receipt" and d.get("purchase_receipt_item"):
- filters["voucher_detail_no"] = d.purchase_receipt_item
-
- original_incoming_rate = frappe.db.get_value("Stock Ledger Entry", filters, "incoming_rate")
+ outgoing_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
sle.update({
- "outgoing_rate": original_incoming_rate
+ "outgoing_rate": outgoing_rate,
+ "recalculate_rate": 1
})
+ if d.from_warehouse:
+ sle.dependant_sle_voucher_detail_no = d.name
else:
val_rate_db_precision = 6 if cint(self.precision("valuation_rate", d)) <= 6 else 9
incoming_rate = flt(d.valuation_rate, val_rate_db_precision)
sle.update({
- "incoming_rate": incoming_rate
+ "incoming_rate": incoming_rate,
+ "recalculate_rate": 1 if (self.is_subcontracted and d.bom) or d.from_warehouse else 0
})
sl_entries.append(sle)
@@ -603,7 +618,8 @@
or (cint(self.is_return) and self.docstatus==1)):
from_warehouse_sle = self.get_sl_entries(d, {
"actual_qty": -1 * pr_qty,
- "warehouse": d.from_warehouse
+ "warehouse": d.from_warehouse,
+ "recalculate_rate": 1
})
sl_entries.append(from_warehouse_sle)
@@ -651,6 +667,7 @@
"item_code": d.rm_item_code,
"warehouse": self.supplier_warehouse,
"actual_qty": -1*flt(d.consumed_qty),
+ "dependant_sle_voucher_detail_no": d.reference_name
}))
def on_submit(self):
@@ -842,6 +859,7 @@
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"
diff --git a/erpnext/controllers/print_settings.py b/erpnext/controllers/print_settings.py
index c41db25..e08c400 100644
--- a/erpnext/controllers/print_settings.py
+++ b/erpnext/controllers/print_settings.py
@@ -5,20 +5,34 @@
import frappe
from frappe.utils import cint
-def print_settings_for_item_table(doc):
-
+def set_print_templates_for_item_table(doc, settings):
doc.print_templates = {
- "qty": "templates/print_formats/includes/item_table_qty.html"
+ "items": "templates/print_formats/includes/items.html",
}
- doc.hide_in_print_layout = ["uom", "stock_uom"]
- doc.flags.compact_item_print = cint(frappe.db.get_single_value("Print Settings", "compact_item_print"))
+ doc.child_print_templates = {
+ "items": {
+ "qty": "templates/print_formats/includes/item_table_qty.html",
+ }
+ }
- if doc.flags.compact_item_print:
- doc.print_templates["description"] = "templates/print_formats/includes/item_table_description.html"
- doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"]
+ if doc.meta.get_field("items"):
+ doc.meta.get_field("items").hide_in_print_layout = ["uom", "stock_uom"]
+
+ doc.flags.compact_item_fields = ["description", "qty", "rate", "amount"]
+
+ if settings.compact_item_print:
+ doc.child_print_templates["items"]["description"] =\
+ "templates/print_formats/includes/item_table_description.html"
doc.flags.format_columns = format_columns
+def set_print_templates_for_taxes(doc, settings):
+ doc.flags.show_inclusive_tax_in_print = doc.is_inclusive_tax()
+ doc.print_templates.update({
+ "total": "templates/print_formats/includes/total.html",
+ "taxes": "templates/print_formats/includes/taxes.html"
+ })
+
def format_columns(display_columns, compact_fields):
compact_fields = compact_fields + ["image", "item_code", "item_name"]
final_columns = []
diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py
index 8fe3816..81f0ad3 100644
--- a/erpnext/controllers/queries.py
+++ b/erpnext/controllers/queries.py
@@ -493,6 +493,41 @@
'company': filters.get("company", "")
})
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
+def get_filtered_dimensions(doctype, txt, searchfield, start, page_len, filters):
+ from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import get_dimension_filter_map
+ dimension_filters = get_dimension_filter_map()
+ dimension_filters = dimension_filters.get((filters.get('dimension'),filters.get('account')))
+ query_filters = []
+
+ meta = frappe.get_meta(doctype)
+ if meta.is_tree:
+ query_filters.append(['is_group', '=', 0])
+
+ if meta.has_field('company'):
+ query_filters.append(['company', '=', filters.get('company')])
+
+ if txt:
+ query_filters.append([searchfield, 'LIKE', "%%%s%%" % txt])
+
+ if dimension_filters:
+ if dimension_filters['allow_or_restrict'] == 'Allow':
+ query_selector = 'in'
+ else:
+ query_selector = 'not in'
+
+ if len(dimension_filters['allowed_dimensions']) == 1:
+ dimensions = tuple(dimension_filters['allowed_dimensions'] * 2)
+ else:
+ dimensions = tuple(dimension_filters['allowed_dimensions'])
+
+ query_filters.append(['name', query_selector, dimensions])
+
+ output = frappe.get_all(doctype, filters=query_filters)
+ result = [d.name for d in output]
+
+ return [(d,) for d in set(result)]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
@@ -622,6 +657,34 @@
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
+def get_healthcare_service_units(doctype, txt, searchfield, start, page_len, filters):
+ query = """
+ select name
+ from `tabHealthcare Service Unit`
+ where
+ is_group = 0
+ and company = {company}
+ and name like {txt}""".format(
+ company = frappe.db.escape(filters.get('company')), txt = frappe.db.escape('%{0}%'.format(txt)))
+
+ if filters and filters.get('inpatient_record'):
+ from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
+ service_unit = get_current_healthcare_service_unit(filters.get('inpatient_record'))
+
+ # if the patient is admitted, then appointments should be allowed against the admission service unit,
+ # inspite of it being an Inpatient Occupancy service unit
+ if service_unit:
+ query += " and (allow_appointments = 1 or name = {service_unit})".format(service_unit = frappe.db.escape(service_unit))
+ else:
+ query += " and allow_appointments = 1"
+ else:
+ query += " and allow_appointments = 1"
+
+ return frappe.db.sql(query, filters)
+
+
+@frappe.whitelist()
+@frappe.validate_and_sanitize_search_inputs
def get_tax_template(doctype, txt, searchfield, start, page_len, filters):
item_doc = frappe.get_cached_doc('Item', filters.get('item_code'))
diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py
index afc5f81..de61b35 100644
--- a/erpnext/controllers/sales_and_purchase_return.py
+++ b/erpnext/controllers/sales_and_purchase_return.py
@@ -203,10 +203,40 @@
return items
+def get_returned_qty_map_for_row(row_name, doctype):
+ child_doctype = doctype + " Item"
+ reference_field = "dn_detail" if doctype == "Delivery Note" else frappe.scrub(child_doctype)
+
+ fields = [
+ "sum(abs(`tab{0}`.qty)) as qty".format(child_doctype),
+ "sum(abs(`tab{0}`.stock_qty)) as stock_qty".format(child_doctype)
+ ]
+
+ if doctype in ("Purchase Receipt", "Purchase Invoice"):
+ fields += [
+ "sum(abs(`tab{0}`.rejected_qty)) as rejected_qty".format(child_doctype),
+ "sum(abs(`tab{0}`.received_qty)) as received_qty".format(child_doctype)
+ ]
+
+ if doctype == "Purchase Receipt":
+ fields += ["sum(abs(`tab{0}`.received_stock_qty)) as received_stock_qty".format(child_doctype)]
+
+ data = frappe.db.get_list(doctype,
+ fields = fields,
+ filters = [
+ [doctype, "docstatus", "=", 1],
+ [doctype, "is_return", "=", 1],
+ [child_doctype, reference_field, "=", row_name]
+ ])
+
+ return data[0]
+
def make_return_doc(doctype, source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
company = frappe.db.get_value("Delivery Note", source_name, "company")
default_warehouse_for_sales_return = frappe.db.get_value("Company", company, "default_warehouse_for_sales_return")
+
def set_missing_values(source, target):
doc = frappe.get_doc(target)
doc.is_return = 1
@@ -230,6 +260,7 @@
if doc.get("is_return"):
if doc.doctype == 'Sales Invoice' or doc.doctype == 'POS Invoice':
+ doc.consolidated_invoice = ""
doc.set('payments', [])
for data in source.payments:
paid_amount = 0.00
@@ -261,30 +292,48 @@
doc.run_method("calculate_taxes_and_totals")
def update_item(source_doc, target_doc, source_parent):
- target_doc.qty = -1* source_doc.qty
+ target_doc.qty = -1 * source_doc.qty
+
+ if source_doc.serial_no:
+ returned_serial_nos = get_returned_serial_nos(source_doc, source_parent)
+ serial_nos = list(set(get_serial_nos(source_doc.serial_no)) - set(returned_serial_nos))
+ if serial_nos:
+ target_doc.serial_no = '\n'.join(serial_nos)
+
if doctype == "Purchase Receipt":
- target_doc.received_qty = -1* source_doc.received_qty
- target_doc.rejected_qty = -1* source_doc.rejected_qty
- target_doc.qty = -1* source_doc.qty
- target_doc.stock_qty = -1 * source_doc.stock_qty
+ returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
+ target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
+ target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
+
+ target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
+ target_doc.received_stock_qty = -1 * flt(source_doc.received_stock_qty - (returned_qty_map.get('received_stock_qty') or 0))
+
target_doc.purchase_order = source_doc.purchase_order
target_doc.purchase_order_item = source_doc.purchase_order_item
target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.purchase_receipt_item = source_doc.name
elif doctype == "Purchase Invoice":
- target_doc.received_qty = -1* source_doc.received_qty
- target_doc.rejected_qty = -1* source_doc.rejected_qty
- target_doc.qty = -1* source_doc.qty
- target_doc.stock_qty = -1 * source_doc.stock_qty
+ returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ target_doc.received_qty = -1 * flt(source_doc.received_qty - (returned_qty_map.get('received_qty') or 0))
+ target_doc.rejected_qty = -1 * flt(source_doc.rejected_qty - (returned_qty_map.get('rejected_qty') or 0))
+ target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
+
+ target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
target_doc.purchase_order = source_doc.purchase_order
target_doc.purchase_receipt = source_doc.purchase_receipt
target_doc.rejected_warehouse = source_doc.rejected_warehouse
target_doc.po_detail = source_doc.po_detail
target_doc.pr_detail = source_doc.pr_detail
target_doc.purchase_invoice_item = source_doc.name
+ target_doc.price_list_rate = 0
elif doctype == "Delivery Note":
+ returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
+ target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
+
target_doc.against_sales_order = source_doc.against_sales_order
target_doc.against_sales_invoice = source_doc.against_sales_invoice
target_doc.so_detail = source_doc.so_detail
@@ -294,12 +343,22 @@
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
elif doctype == "Sales Invoice" or doctype == "POS Invoice":
+ returned_qty_map = get_returned_qty_map_for_row(source_doc.name, doctype)
+ target_doc.qty = -1 * flt(source_doc.qty - (returned_qty_map.get('qty') or 0))
+ target_doc.stock_qty = -1 * flt(source_doc.stock_qty - (returned_qty_map.get('stock_qty') or 0))
+
target_doc.sales_order = source_doc.sales_order
target_doc.delivery_note = source_doc.delivery_note
target_doc.so_detail = source_doc.so_detail
target_doc.dn_detail = source_doc.dn_detail
target_doc.expense_account = source_doc.expense_account
- target_doc.sales_invoice_item = source_doc.name
+
+ if doctype == "Sales Invoice":
+ target_doc.sales_invoice_item = source_doc.name
+ else:
+ target_doc.pos_invoice_item = source_doc.name
+
+ target_doc.price_list_rate = 0
if default_warehouse_for_sales_return:
target_doc.warehouse = default_warehouse_for_sales_return
@@ -329,3 +388,63 @@
}, target_doc, set_missing_values)
return doclist
+
+def get_rate_for_return(voucher_type, voucher_no, item_code, return_against=None, item_row=None, voucher_detail_no=None):
+ if not return_against:
+ return_against = frappe.get_cached_value(voucher_type, voucher_no, "return_against")
+
+ return_against_item_field = get_return_against_item_fields(voucher_type)
+
+ filters = get_filters(voucher_type, voucher_no, voucher_detail_no,
+ return_against, item_code, return_against_item_field, item_row)
+
+ if voucher_type in ("Purchase Receipt", "Purchase Invoice"):
+ select_field = "incoming_rate"
+ else:
+ select_field = "abs(stock_value_difference / actual_qty)"
+
+ return flt(frappe.db.get_value("Stock Ledger Entry", filters, select_field))
+
+def get_return_against_item_fields(voucher_type):
+ return_against_item_fields = {
+ "Purchase Receipt": "purchase_receipt_item",
+ "Purchase Invoice": "purchase_invoice_item",
+ "Delivery Note": "dn_detail",
+ "Sales Invoice": "sales_invoice_item"
+ }
+ return return_against_item_fields[voucher_type]
+
+def get_filters(voucher_type, voucher_no, voucher_detail_no, return_against, item_code, return_against_item_field, item_row):
+ filters = {
+ "voucher_type": voucher_type,
+ "voucher_no": return_against,
+ "item_code": item_code
+ }
+
+ if item_row:
+ reference_voucher_detail_no = item_row.get(return_against_item_field)
+ else:
+ reference_voucher_detail_no = frappe.db.get_value(voucher_type + " Item", voucher_detail_no, return_against_item_field)
+
+ if reference_voucher_detail_no:
+ filters["voucher_detail_no"] = reference_voucher_detail_no
+
+ return filters
+
+def get_returned_serial_nos(child_doc, parent_doc):
+ from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+ return_ref_field = frappe.scrub(child_doc.doctype)
+ if child_doc.doctype == "Delivery Note Item":
+ return_ref_field = "dn_detail"
+
+ serial_nos = []
+
+ fields = ["`{0}`.`serial_no`".format("tab" + child_doc.doctype)]
+
+ filters = [[parent_doc.doctype, "return_against", "=", parent_doc.name], [parent_doc.doctype, "is_return", "=", 1],
+ [child_doc.doctype, return_ref_field, "=", child_doc.name], [parent_doc.doctype, "docstatus", "=", 1]]
+
+ for row in frappe.get_all(parent_doc.doctype, fields = fields, filters=filters):
+ serial_nos.extend(get_serial_nos(row.serial_no))
+
+ return serial_nos
\ No newline at end of file
diff --git a/erpnext/controllers/selling_controller.py b/erpnext/controllers/selling_controller.py
index 515239a..c61b67b 100644
--- a/erpnext/controllers/selling_controller.py
+++ b/erpnext/controllers/selling_controller.py
@@ -3,7 +3,7 @@
from __future__ import unicode_literals
import frappe
-from frappe.utils import cint, flt, cstr, comma_or, get_link_to_form
+from frappe.utils import cint, flt, cstr, get_link_to_form, nowtime
from frappe import _, throw
from erpnext.stock.get_item_details import get_bin_details
from erpnext.stock.utils import get_incoming_rate
@@ -13,18 +13,9 @@
from erpnext.controllers.accounts_controller import get_taxes_and_charges
from erpnext.controllers.stock_controller import StockController
+from erpnext.controllers.sales_and_purchase_return import get_rate_for_return
class SellingController(StockController):
- def __setup__(self):
- if hasattr(self, "taxes"):
- self.flags.print_taxes_with_zero_amount = cint(frappe.db.get_single_value("Print Settings",
- "print_taxes_with_zero_amount"))
- self.flags.show_inclusive_tax_in_print = self.is_inclusive_tax()
-
- self.print_templates = {
- "total": "templates/print_formats/includes/total.html",
- "taxes": "templates/print_formats/includes/taxes.html"
- }
def get_feed(self):
return _("To {0} | {1} {2}").format(self.customer_name, self.currency,
@@ -42,7 +33,7 @@
self.validate_max_discount()
self.validate_selling_price()
self.set_qty_as_per_stock_uom()
- self.set_po_nos()
+ self.set_po_nos(for_validate=True)
self.set_gross_profit()
set_default_income_account_for_item(self)
self.set_customer_address()
@@ -189,7 +180,7 @@
for it in self.get("items"):
if not it.item_code:
continue
-
+
last_purchase_rate, is_stock_item = frappe.get_cached_value("Item", it.item_code, ["last_purchase_rate", "is_stock_item"])
last_purchase_rate_in_sales_uom = last_purchase_rate * (it.conversion_factor or 1)
if flt(it.base_net_rate) < flt(last_purchase_rate_in_sales_uom):
@@ -230,7 +221,8 @@
'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
- 'delivery_note_item': d.get("dn_detail")
+ 'dn_detail': d.get("dn_detail"),
+ 'incoming_rate': p.get("incoming_rate")
}))
else:
il.append(frappe._dict({
@@ -248,7 +240,8 @@
'voucher_type': self.doctype,
'allow_zero_valuation': d.allow_zero_valuation_rate,
'sales_invoice_item': d.get("sales_invoice_item"),
- 'delivery_note_item': d.get("dn_detail")
+ 'dn_detail': d.get("dn_detail"),
+ 'incoming_rate': d.get("incoming_rate")
}))
return il
@@ -307,83 +300,122 @@
sales_order.update_reserved_qty(so_item_rows)
+ def set_incoming_rate(self):
+ if self.doctype not in ("Delivery Note", "Sales Invoice", "Sales Order"):
+ return
+
+ items = self.get("items") + (self.get("packed_items") or [])
+ for d in items:
+ if not cint(self.get("is_return")):
+ # Get incoming rate based on original item cost based on valuation method
+ d.incoming_rate = get_incoming_rate({
+ "item_code": d.item_code,
+ "warehouse": d.warehouse,
+ "posting_date": self.get('posting_date') or self.get('transaction_date'),
+ "posting_time": self.get('posting_time') or nowtime(),
+ "qty": -1 * flt(d.get('stock_qty') or d.get('actual_qty')),
+ "serial_no": d.get('serial_no'),
+ "company": self.company,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "allow_zero_valuation": d.get("allow_zero_valuation")
+ }, raise_error_if_no_rate=False)
+
+ # For internal transfers use incoming rate as the valuation rate
+ if self.is_internal_transfer():
+ rate = flt(d.incoming_rate * d.conversion_factor, d.precision('rate'))
+ if d.rate != rate:
+ d.rate = rate
+ d.discount_percentage = 0
+ d.discount_amount = 0
+ frappe.msgprint(_("Row {0}: Item rate has been updated as per valuation rate since its an internal stock transfer")
+ .format(d.idx), alert=1)
+
+ elif self.get("return_against"):
+ # Get incoming rate of return entry from reference document
+ # based on original item cost as per valuation method
+ d.incoming_rate = get_rate_for_return(self.doctype, self.name, d.item_code, self.return_against, item_row=d)
+
def update_stock_ledger(self):
self.update_reserved_qty()
sl_entries = []
+ # Loop over items and packed items table
for d in self.get_item_list():
if frappe.get_cached_value("Item", d.item_code, "is_stock_item") == 1 and flt(d.qty):
if flt(d.conversion_factor)==0.0:
d.conversion_factor = get_conversion_factor(d.item_code, d.uom).get("conversion_factor") or 1.0
- return_rate = 0
- if cint(self.is_return) and self.return_against and self.docstatus==1:
- against_document_no = (d.get("sales_invoice_item")
- if self.doctype == "Sales Invoice" else d.get("delivery_note_item"))
- return_rate = self.get_incoming_rate_for_return(d.item_code,
- self.return_against, against_document_no)
-
- # On cancellation or if return entry submission, make stock ledger entry for
+ # On cancellation or return entry submission, make stock ledger entry for
# target warehouse first, to update serial no values properly
if d.warehouse and ((not cint(self.is_return) and self.docstatus==1)
or (cint(self.is_return) and self.docstatus==2)):
- sl_entries.append(self.get_sl_entries(d, {
- "actual_qty": -1*flt(d.qty),
- "incoming_rate": return_rate
- }))
+ sl_entries.append(self.get_sle_for_source_warehouse(d))
if d.target_warehouse:
- target_warehouse_sle = self.get_sl_entries(d, {
- "actual_qty": flt(d.qty),
- "warehouse": d.target_warehouse
- })
-
- if self.docstatus == 1:
- if not cint(self.is_return):
- args = frappe._dict({
- "item_code": d.item_code,
- "warehouse": d.warehouse,
- "posting_date": self.posting_date,
- "posting_time": self.posting_time,
- "qty": -1*flt(d.qty),
- "serial_no": d.serial_no,
- "company": d.company,
- "voucher_type": d.voucher_type,
- "voucher_no": d.name,
- "allow_zero_valuation": d.allow_zero_valuation
- })
- target_warehouse_sle.update({
- "incoming_rate": get_incoming_rate(args)
- })
- else:
- target_warehouse_sle.update({
- "outgoing_rate": return_rate
- })
- sl_entries.append(target_warehouse_sle)
+ sl_entries.append(self.get_sle_for_target_warehouse(d))
if d.warehouse and ((not cint(self.is_return) and self.docstatus==2)
or (cint(self.is_return) and self.docstatus==1)):
- sl_entries.append(self.get_sl_entries(d, {
- "actual_qty": -1*flt(d.qty),
- "incoming_rate": return_rate
- }))
+ sl_entries.append(self.get_sle_for_source_warehouse(d))
+
self.make_sl_entries(sl_entries)
- def set_po_nos(self):
+ def get_sle_for_source_warehouse(self, item_row):
+ sle = self.get_sl_entries(item_row, {
+ "actual_qty": -1*flt(item_row.qty),
+ "incoming_rate": item_row.incoming_rate,
+ "recalculate_rate": cint(self.is_return)
+ })
+ if item_row.target_warehouse and not cint(self.is_return):
+ sle.dependant_sle_voucher_detail_no = item_row.name
+
+ return sle
+
+ def get_sle_for_target_warehouse(self, item_row):
+ sle = self.get_sl_entries(item_row, {
+ "actual_qty": flt(item_row.qty),
+ "warehouse": item_row.target_warehouse
+ })
+
+ if self.docstatus == 1:
+ if not cint(self.is_return):
+ sle.update({
+ "incoming_rate": item_row.incoming_rate,
+ "recalculate_rate": 1
+ })
+ else:
+ sle.update({
+ "outgoing_rate": item_row.incoming_rate
+ })
+ if item_row.warehouse:
+ sle.dependant_sle_voucher_detail_no = item_row.name
+
+ return sle
+
+ def set_po_nos(self, for_validate=False):
if self.doctype == 'Sales Invoice' and hasattr(self, "items"):
+ if for_validate and self.po_no:
+ return
self.set_pos_for_sales_invoice()
if self.doctype == 'Delivery Note' and hasattr(self, "items"):
+ if for_validate and self.po_no:
+ return
self.set_pos_for_delivery_note()
def set_pos_for_sales_invoice(self):
po_nos = []
+ if self.po_no:
+ po_nos.append(self.po_no)
self.get_po_nos('Sales Order', 'sales_order', po_nos)
self.get_po_nos('Delivery Note', 'delivery_note', po_nos)
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
def set_pos_for_delivery_note(self):
po_nos = []
+ if self.po_no:
+ po_nos.append(self.po_no)
self.get_po_nos('Sales Order', 'against_sales_order', po_nos)
self.get_po_nos('Sales Invoice', 'against_sales_invoice', po_nos)
self.po_no = ', '.join(list(set(x.strip() for x in ','.join(po_nos).split(','))))
@@ -414,9 +446,13 @@
check_list, chk_dupl_itm = [], []
if cint(frappe.db.get_single_value("Selling Settings", "allow_multiple_items")):
return
+ if self.doctype == "Sales Invoice" and self.is_consolidated:
+ return
+ if self.doctype == "POS Invoice":
+ return
for d in self.get('items'):
- if self.doctype in ["POS Invoice","Sales Invoice"]:
+ if self.doctype == "Sales Invoice":
stock_items = [d.item_code, d.description, d.warehouse, d.sales_order or d.delivery_note, d.batch_no or '']
non_stock_items = [d.item_code, d.description, d.sales_order or d.delivery_note]
elif self.doctype == "Delivery Note":
@@ -427,13 +463,19 @@
non_stock_items = [d.item_code, d.description]
if frappe.db.get_value("Item", d.item_code, "is_stock_item") == 1:
+ duplicate_items_msg = _("Item {0} entered multiple times.").format(frappe.bold(d.item_code))
+ duplicate_items_msg += "<br><br>"
+ duplicate_items_msg += _("Please enable {} in {} to allow same item in multiple rows").format(
+ frappe.bold("Allow Item to Be Added Multiple Times in a Transaction"),
+ get_link_to_form("Selling Settings", "Selling Settings")
+ )
if stock_items in check_list:
- frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
+ frappe.throw(duplicate_items_msg)
else:
check_list.append(stock_items)
else:
if non_stock_items in chk_dupl_itm:
- frappe.throw(_("Note: Item {0} entered multiple times").format(d.item_code))
+ frappe.throw(duplicate_items_msg)
else:
chk_dupl_itm.append(non_stock_items)
@@ -455,4 +497,4 @@
for d in obj.get("items"):
if d.item_code:
if getattr(d, "income_account", None):
- set_item_default(d.item_code, obj.company, 'income_account', d.income_account)
+ set_item_default(d.item_code, obj.company, 'income_account', d.income_account)
\ No newline at end of file
diff --git a/erpnext/controllers/status_updater.py b/erpnext/controllers/status_updater.py
index 9feac78..0987d09 100644
--- a/erpnext/controllers/status_updater.py
+++ b/erpnext/controllers/status_updater.py
@@ -58,6 +58,7 @@
"Delivery Note": [
["Draft", None],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
+ ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"],
@@ -65,6 +66,7 @@
"Purchase Receipt": [
["Draft", None],
["To Bill", "eval:self.per_billed < 100 and self.docstatus == 1"],
+ ["Return Issued", "eval:self.per_returned == 100 and self.docstatus == 1"],
["Completed", "eval:self.per_billed == 100 and self.docstatus == 1"],
["Cancelled", "eval:self.docstatus==2"],
["Closed", "eval:self.status=='Closed'"],
@@ -91,6 +93,12 @@
["Open", "eval:self.docstatus == 1 and not self.pos_closing_entry"],
["Closed", "eval:self.docstatus == 1 and self.pos_closing_entry"],
["Cancelled", "eval:self.docstatus == 2"],
+ ],
+ "POS Closing Entry": [
+ ["Draft", None],
+ ["Submitted", "eval:self.docstatus == 1"],
+ ["Queued", "eval:self.status == 'Queued'"],
+ ["Cancelled", "eval:self.docstatus == 2"],
]
}
@@ -232,7 +240,7 @@
self._update_children(args, update_modified)
- if "percent_join_field" in args:
+ if "percent_join_field" in args or "percent_join_field_parent" in args:
self._update_percent_field_in_targets(args, update_modified)
def _update_children(self, args, update_modified):
@@ -252,33 +260,43 @@
if not args.get("second_source_extra_cond"):
args["second_source_extra_cond"] = ""
- args['second_source_condition'] = """ + ifnull((select sum(%(second_source_field)s)
+ args['second_source_condition'] = frappe.db.sql(""" select ifnull((select sum(%(second_source_field)s)
from `tab%(second_source_dt)s`
where `%(second_join_field)s`="%(detail_id)s"
- and (`tab%(second_source_dt)s`.docstatus=1) %(second_source_extra_cond)s FOR UPDATE), 0)""" % args
+ and (`tab%(second_source_dt)s`.docstatus=1)
+ %(second_source_extra_cond)s), 0) """ % args)[0][0]
if args['detail_id']:
if not args.get("extra_cond"): args["extra_cond"] = ""
- frappe.db.sql("""update `tab%(target_dt)s`
- set %(target_field)s = (
+ args["source_dt_value"] = frappe.db.sql("""
(select ifnull(sum(%(source_field)s), 0)
from `tab%(source_dt)s` where `%(join_field)s`="%(detail_id)s"
and (docstatus=1 %(cond)s) %(extra_cond)s)
- %(second_source_condition)s
- )
- %(update_modified)s
+ """ % args)[0][0] or 0.0
+
+ if args['second_source_condition']:
+ args["source_dt_value"] += flt(args['second_source_condition'])
+
+ frappe.db.sql("""update `tab%(target_dt)s`
+ set %(target_field)s = %(source_dt_value)s %(update_modified)s
where name='%(detail_id)s'""" % args)
def _update_percent_field_in_targets(self, args, update_modified=True):
"""Update percent field in parent transaction"""
- distinct_transactions = set([d.get(args['percent_join_field'])
- for d in self.get_all_children(args['source_dt'])])
+ if args.get('percent_join_field_parent'):
+ # if reference to target doc where % is to be updated, is
+ # in source doc's parent form, consider percent_join_field_parent
+ 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'])])
- for name in distinct_transactions:
- if name:
- args['name'] = name
- self._update_percent_field(args, update_modified)
+ for name in distinct_transactions:
+ if name:
+ args['name'] = name
+ self._update_percent_field(args, update_modified)
def _update_percent_field(self, args, update_modified=True):
"""Update percent field in parent transaction"""
diff --git a/erpnext/controllers/stock_controller.py b/erpnext/controllers/stock_controller.py
index 2d2fff8..e0031c9 100644
--- a/erpnext/controllers/stock_controller.py
+++ b/erpnext/controllers/stock_controller.py
@@ -6,7 +6,8 @@
from frappe.utils import cint, flt, cstr, get_link_to_form, today, getdate
from frappe import _
import frappe.defaults
-from erpnext.accounts.utils import get_fiscal_year
+from collections import defaultdict
+from erpnext.accounts.utils import get_fiscal_year, check_if_stock_and_account_balance_synced
from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries, process_gl_map
from erpnext.controllers.accounts_controller import AccountsController
from erpnext.stock.stock_ledger import get_valuation_rate
@@ -23,8 +24,11 @@
self.validate_inspection()
self.validate_serialized_batch()
self.validate_customer_provided_item()
+ self.set_rate_of_stock_uom()
+ self.validate_internal_transfer()
+ self.validate_putaway_capacity()
- def make_gl_entries(self, gl_entries=None):
+ def make_gl_entries(self, gl_entries=None, from_repost=False):
if self.docstatus == 2:
make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name)
@@ -34,12 +38,12 @@
if self.docstatus==1:
if not gl_entries:
gl_entries = self.get_gl_entries(warehouse_account)
- make_gl_entries(gl_entries)
+ make_gl_entries(gl_entries, from_repost=from_repost)
elif self.doctype in ['Purchase Receipt', 'Purchase Invoice'] and self.docstatus == 1:
gl_entries = []
gl_entries = self.get_asset_gl_entry(gl_entries)
- make_gl_entries(gl_entries)
+ make_gl_entries(gl_entries, from_repost=from_repost)
def validate_serialized_batch(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -70,14 +74,14 @@
gl_list = []
warehouse_with_no_account = []
-
- precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
+ precision = self.get_debit_field_precision()
for item_row in voucher_details:
+
sle_list = sle_map.get(item_row.name)
if sle_list:
for sle in sle_list:
if warehouse_account.get(sle.warehouse):
- # from warehouse account/ target warehouse account
+ # from warehouse account
self.check_expense_account(item_row)
@@ -92,9 +96,16 @@
sle = self.update_stock_ledger_entries(sle)
+ # expense account/ target_warehouse / source_warehouse
+ if item_row.get('target_warehouse'):
+ warehouse = item_row.get('target_warehouse')
+ expense_account = warehouse_account[warehouse]["account"]
+ else:
+ expense_account = item_row.expense_account
+
gl_list.append(self.get_gl_dict({
"account": warehouse_account[sle.warehouse]["account"],
- "against": item_row.expense_account,
+ "against": expense_account,
"cost_center": item_row.cost_center,
"project": item_row.project or self.get('project'),
"remarks": self.get("remarks") or "Accounting Entry for Stock",
@@ -102,9 +113,8 @@
"is_opening": item_row.get("is_opening") or self.get("is_opening") or "No",
}, warehouse_account[sle.warehouse]["account_currency"], item=item_row))
- # expense account
gl_list.append(self.get_gl_dict({
- "account": item_row.expense_account,
+ "account": expense_account,
"against": warehouse_account[sle.warehouse]["account"],
"cost_center": item_row.cost_center,
"project": item_row.project or self.get('project'),
@@ -119,9 +129,15 @@
if warehouse_with_no_account:
for wh in warehouse_with_no_account:
if frappe.db.get_value("Warehouse", wh, "company"):
- frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
+ frappe.throw(_("Warehouse {0} is not linked to any account, please mention the account in the warehouse record or set default inventory account in company {1}.").format(wh, self.company))
- return process_gl_map(gl_list)
+ return process_gl_map(gl_list, precision=precision)
+
+ def get_debit_field_precision(self):
+ if not frappe.flags.debit_field_precision:
+ frappe.flags.debit_field_precision = frappe.get_precision("GL Entry", "debit_in_account_currency")
+
+ return frappe.flags.debit_field_precision
def update_stock_ledger_entries(self, sle):
sle.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
@@ -211,7 +227,7 @@
""", (self.doctype, self.name), as_dict=True)
for sle in stock_ledger_entries:
- stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
+ stock_ledger.setdefault(sle.voucher_detail_no, []).append(sle)
return stock_ledger
def make_batches(self, warehouse_field):
@@ -234,7 +250,7 @@
.format(item.idx, frappe.bold(item.item_code), msg), title=_("Expense Account Missing"))
else:
- is_expense_account = frappe.db.get_value("Account",
+ is_expense_account = frappe.get_cached_value("Account",
item.get("expense_account"), "report_type")=="Profit and Loss"
if self.doctype not in ("Purchase Receipt", "Purchase Invoice", "Stock Reconciliation", "Stock Entry") and not is_expense_account:
frappe.throw(_("Expense / Difference account ({0}) must be a 'Profit or Loss' account")
@@ -303,25 +319,8 @@
return serialized_items
- def get_incoming_rate_for_return(self, item_code, against_document, against_document_no=None):
- incoming_rate = 0.0
- cond = ''
- if against_document and item_code:
- if against_document_no:
- cond = " and voucher_detail_no = %s" %(frappe.db.escape(against_document_no))
-
- incoming_rate = frappe.db.sql("""select abs(stock_value_difference / actual_qty)
- from `tabStock Ledger Entry`
- where voucher_type = %s and voucher_no = %s
- and item_code = %s {0} limit 1""".format(cond),
- (self.doctype, against_document, item_code))
-
- incoming_rate = incoming_rate[0][0] if incoming_rate else 0.0
-
- return incoming_rate
-
def validate_warehouse(self):
- from erpnext.stock.utils import validate_warehouse_company
+ from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
warehouses = list(set([d.warehouse for d in
self.get("items") if getattr(d, "warehouse", None)]))
@@ -337,14 +336,19 @@
warehouses.extend(from_warehouse)
for w in warehouses:
+ validate_disabled_warehouse(w)
validate_warehouse_company(w, self.company)
def update_billing_percentage(self, update_modified=True):
+ target_ref_field = "amount"
+ if self.doctype == "Delivery Note":
+ target_ref_field = "amount - (returned_qty * rate)"
+
self._update_percent_field({
"target_dt": self.doctype + " Item",
"target_parent_dt": self.doctype,
"target_parent_field": "per_billed",
- "target_ref_field": "amount",
+ "target_ref_field": target_ref_field,
"target_field": "billed_amt",
"name": self.name,
}, update_modified)
@@ -399,19 +403,154 @@
if frappe.db.get_value('Item', d.item_code, 'is_customer_provided_item'):
d.allow_zero_valuation_rate = 1
-def compare_existing_and_expected_gle(existing_gle, expected_gle):
- matched = True
- for entry in expected_gle:
- account_existed = False
- for e in existing_gle:
- if entry.account == e.account:
- account_existed = True
- if entry.account == e.account and entry.against_account == e.against_account \
- and (not entry.cost_center or not e.cost_center or entry.cost_center == e.cost_center) \
- and (entry.debit != e.debit or entry.credit != e.credit):
- matched = False
- break
- if not account_existed:
- matched = False
+ def set_rate_of_stock_uom(self):
+ if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]:
+ for d in self.get("items"):
+ d.stock_uom_rate = d.rate / d.conversion_factor
+
+ def validate_internal_transfer(self):
+ if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \
+ and self.is_internal_transfer():
+ self.validate_in_transit_warehouses()
+ self.validate_multi_currency()
+ self.validate_packed_items()
+
+ def validate_in_transit_warehouses(self):
+ if (self.doctype == 'Sales Invoice' and self.get('update_stock')) or self.doctype == 'Delivery Note':
+ for item in self.get('items'):
+ if not item.target_warehouse:
+ frappe.throw(_("Row {0}: Target Warehouse is mandatory for internal transfers").format(item.idx))
+
+ if (self.doctype == 'Purchase Invoice' and self.get('update_stock')) or self.doctype == 'Purchase Receipt':
+ for item in self.get('items'):
+ if not item.from_warehouse:
+ frappe.throw(_("Row {0}: From Warehouse is mandatory for internal transfers").format(item.idx))
+
+ def validate_multi_currency(self):
+ if self.currency != self.company_currency:
+ frappe.throw(_("Internal transfers can only be done in company's default currency"))
+
+ def validate_packed_items(self):
+ if self.doctype in ('Sales Invoice', 'Delivery Note Item') and self.get('packed_items'):
+ frappe.throw(_("Packed Items cannot be transferred internally"))
+
+ def validate_putaway_capacity(self):
+ # if over receipt is attempted while 'apply putaway rule' is disabled
+ # and if rule was applied on the transaction, validate it.
+ from erpnext.stock.doctype.putaway_rule.putaway_rule import get_available_putaway_capacity
+ valid_doctype = self.doctype in ("Purchase Receipt", "Stock Entry", "Purchase Invoice",
+ "Stock Reconciliation")
+
+ if self.doctype == "Purchase Invoice" and self.get("update_stock") == 0:
+ valid_doctype = False
+
+ if valid_doctype:
+ rule_map = defaultdict(dict)
+ for item in self.get("items"):
+ warehouse_field = "t_warehouse" if self.doctype == "Stock Entry" else "warehouse"
+ rule = frappe.db.get_value("Putaway Rule",
+ {
+ "item_code": item.get("item_code"),
+ "warehouse": item.get(warehouse_field)
+ },
+ ["name", "disable"], as_dict=True)
+ if rule:
+ if rule.get("disabled"): continue # dont validate for disabled rule
+
+ if self.doctype == "Stock Reconciliation":
+ stock_qty = flt(item.qty)
+ else:
+ stock_qty = flt(item.transfer_qty) if self.doctype == "Stock Entry" else flt(item.stock_qty)
+
+ rule_name = rule.get("name")
+ if not rule_map[rule_name]:
+ rule_map[rule_name]["warehouse"] = item.get(warehouse_field)
+ rule_map[rule_name]["item"] = item.get("item_code")
+ rule_map[rule_name]["qty_put"] = 0
+ rule_map[rule_name]["capacity"] = get_available_putaway_capacity(rule_name)
+ rule_map[rule_name]["qty_put"] += flt(stock_qty)
+
+ for rule, values in rule_map.items():
+ if flt(values["qty_put"]) > flt(values["capacity"]):
+ message = self.prepare_over_receipt_message(rule, values)
+ frappe.throw(msg=message, title=_("Over Receipt"))
+
+ def prepare_over_receipt_message(self, rule, values):
+ message = _("{0} qty of Item {1} is being received into Warehouse {2} with capacity {3}.") \
+ .format(
+ frappe.bold(values["qty_put"]), frappe.bold(values["item"]),
+ frappe.bold(values["warehouse"]), frappe.bold(values["capacity"])
+ )
+ message += "<br><br>"
+ rule_link = frappe.utils.get_link_to_form("Putaway Rule", rule)
+ message += _(" Please adjust the qty or edit {0} to proceed.").format(rule_link)
+ return message
+
+ def repost_future_sle_and_gle(self):
+ args = frappe._dict({
+ "posting_date": self.posting_date,
+ "posting_time": self.posting_time,
+ "voucher_type": self.doctype,
+ "voucher_no": self.name,
+ "company": self.company
+ })
+ if check_if_future_sle_exists(args):
+ create_repost_item_valuation_entry(args)
+ elif not is_reposting_pending():
+ check_if_stock_and_account_balance_synced(self.posting_date,
+ self.company, self.doctype, self.name)
+
+def is_reposting_pending():
+ return frappe.db.exists("Repost Item Valuation",
+ {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
+
+
+def check_if_future_sle_exists(args):
+ sl_entries = frappe.db.get_all("Stock Ledger Entry",
+ filters={"voucher_type": args.voucher_type, "voucher_no": args.voucher_no},
+ fields=["item_code", "warehouse"],
+ order_by="creation asc")
+
+ distinct_item_warehouses = list(set([(d.item_code, d.warehouse) for d in sl_entries]))
+
+ sle_exists = False
+ for item_code, warehouse in distinct_item_warehouses:
+ args.update({
+ "item_code": item_code,
+ "warehouse": warehouse
+ })
+ if get_sle(args):
+ sle_exists = True
break
- return matched
+ return sle_exists
+
+def get_sle(args):
+ return frappe.db.sql("""
+ select name
+ from `tabStock Ledger Entry`
+ where
+ item_code=%(item_code)s
+ and warehouse=%(warehouse)s
+ 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
+ """, args)
+
+def create_repost_item_valuation_entry(args):
+ args = frappe._dict(args)
+ repost_entry = frappe.new_doc("Repost Item Valuation")
+ repost_entry.based_on = args.based_on
+ if not args.based_on:
+ repost_entry.based_on = 'Transaction' if args.voucher_no else "Item and Warehouse"
+ repost_entry.voucher_type = args.voucher_type
+ repost_entry.voucher_no = args.voucher_no
+ repost_entry.item_code = args.item_code
+ repost_entry.warehouse = args.warehouse
+ repost_entry.posting_date = args.posting_date
+ repost_entry.posting_time = args.posting_time
+ repost_entry.company = args.company
+ repost_entry.allow_zero_rate = args.allow_zero_rate
+ repost_entry.flags.ignore_links = True
+ repost_entry.save()
+ repost_entry.submit()
\ No newline at end of file
diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py
index 81d07c1..10271cb 100644
--- a/erpnext/controllers/taxes_and_totals.py
+++ b/erpnext/controllers/taxes_and_totals.py
@@ -10,10 +10,13 @@
validate_taxes_and_charges, validate_inclusive_tax
from erpnext.stock.get_item_details import _get_item_tax_template
from erpnext.accounts.doctype.pricing_rule.utils import get_applied_pricing_rules
+from erpnext.accounts.doctype.journal_entry.journal_entry import get_exchange_rate
class calculate_taxes_and_totals(object):
def __init__(self, doc):
self.doc = doc
+ frappe.flags.round_off_applicable_accounts = []
+ get_round_off_applicable_accounts(self.doc.company, frappe.flags.round_off_applicable_accounts)
self.calculate()
def calculate(self):
@@ -106,7 +109,7 @@
elif item.discount_amount and item.pricing_rules:
item.rate = item.price_list_rate - item.discount_amount
- if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item']:
+ if item.doctype in ['Quotation Item', 'Sales Order Item', 'Delivery Note Item', 'Sales Invoice Item', 'POS Invoice Item']:
item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item)
if flt(item.rate_with_margin) > 0:
item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate"))
@@ -238,9 +241,6 @@
self.doc.round_floats_in(self.doc, ["total", "base_total", "net_total", "base_net_total"])
- if self.doc.doctype == 'Sales Invoice' and self.doc.is_pos:
- self.doc.pos_total_qty = self.doc.total_qty
-
def calculate_taxes(self):
self.doc.rounding_adjustment = 0
# maintain actual tax rate based on idx
@@ -334,10 +334,18 @@
elif tax.charge_type == "On Item Quantity":
current_tax_amount = tax_rate * item.qty
+ current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount)
self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount)
return current_tax_amount
+ def get_final_current_tax_amount(self, tax, current_tax_amount):
+ # Some countries need individual tax components to be rounded
+ # Handeled via regional doctypess
+ if tax.account_head in frappe.flags.round_off_applicable_accounts:
+ current_tax_amount = round(current_tax_amount, 0)
+ return current_tax_amount
+
def set_item_wise_tax(self, item, tax, tax_rate, current_tax_amount):
# store tax breakup for each item
key = item.item_code or item.item_name
@@ -519,6 +527,17 @@
if self.doc.docstatus == 0:
self.calculate_outstanding_amount()
+ def is_internal_invoice(self):
+ """
+ Checks if its an internal transfer invoice
+ and decides if to calculate any out standing amount or not
+ """
+
+ if self.doc.doctype in ('Sales Invoice', 'Purchase Invoice') and self.doc.is_internal_transfer():
+ return True
+
+ return False
+
def calculate_outstanding_amount(self):
# NOTE:
# write_off_amount is only for POS Invoice
@@ -526,7 +545,8 @@
if self.doc.doctype == "Sales Invoice":
self.calculate_paid_amount()
- if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos'): return
+ if self.doc.is_return and self.doc.return_against and not self.doc.get('is_pos') or \
+ self.is_internal_invoice(): return
self.doc.round_floats_in(self.doc, ["grand_total", "total_advance", "write_off_amount"])
self._set_in_company_currency(self.doc, ['write_off_amount'])
@@ -603,7 +623,6 @@
self.doc.precision("base_write_off_amount"))
def calculate_margin(self, item):
-
rate_with_margin = 0.0
base_rate_with_margin = 0.0
if item.price_list_rate:
@@ -612,8 +631,8 @@
for d in get_applied_pricing_rules(item.pricing_rules):
pricing_rule = frappe.get_cached_doc('Pricing Rule', d)
- if (pricing_rule.margin_type in ['Amount', 'Percentage'] and pricing_rule.currency == self.doc.currency)\
- or (pricing_rule.margin_type == 'Percentage'):
+ if pricing_rule.margin_rate_or_amount and ((pricing_rule.currency == self.doc.currency and
+ pricing_rule.margin_type in ['Amount', 'Percentage']) or pricing_rule.margin_type == 'Percentage'):
item.margin_type = pricing_rule.margin_type
item.margin_rate_or_amount = pricing_rule.margin_rate_or_amount
has_margin = True
@@ -641,7 +660,8 @@
if default_mode_of_payment:
self.doc.append('payments', {
'mode_of_payment': default_mode_of_payment.mode_of_payment,
- 'amount': total_amount_to_pay
+ 'amount': total_amount_to_pay,
+ 'default': 1
})
else:
self.doc.is_pos = 0
@@ -683,6 +703,15 @@
)
)
+@frappe.whitelist()
+def get_round_off_applicable_accounts(company, account_list):
+ account_list = get_regional_round_off_accounts(company, account_list)
+
+ return account_list
+
+@erpnext.allow_regional
+def get_regional_round_off_accounts(company, account_list):
+ pass
@erpnext.allow_regional
def update_itemised_tax_data(doc):
@@ -745,3 +774,35 @@
for taxes in itemised_tax.values():
for tax_account in taxes:
taxes[tax_account]["tax_amount"] = flt(taxes[tax_account]["tax_amount"], precision)
+
+class init_landed_taxes_and_totals(object):
+ def __init__(self, doc):
+ self.doc = doc
+ self.tax_field = 'taxes' if self.doc.doctype == 'Landed Cost Voucher' else 'additional_costs'
+ self.set_account_currency()
+ self.set_exchange_rate()
+ self.set_amounts_in_company_currency()
+
+ def set_account_currency(self):
+ company_currency = erpnext.get_company_currency(self.doc.company)
+ for d in self.doc.get(self.tax_field):
+ if not d.account_currency:
+ account_currency = frappe.db.get_value('Account', d.expense_account, 'account_currency')
+ d.account_currency = account_currency or company_currency
+
+ def set_exchange_rate(self):
+ company_currency = erpnext.get_company_currency(self.doc.company)
+ for d in self.doc.get(self.tax_field):
+ if d.account_currency == company_currency:
+ d.exchange_rate = 1
+ elif not d.exchange_rate or d.exchange_rate == 1 or self.doc.posting_date:
+ d.exchange_rate = get_exchange_rate(self.doc.posting_date, account=d.expense_account,
+ account_currency=d.account_currency, company=self.doc.company)
+
+ if not d.exchange_rate:
+ frappe.throw(_("Row {0}: Exchange Rate is mandatory").format(d.idx))
+
+ def set_amounts_in_company_currency(self):
+ for d in self.doc.get(self.tax_field):
+ d.amount = flt(d.amount, d.precision("amount"))
+ d.base_amount = flt(d.amount * flt(d.exchange_rate), d.precision("base_amount"))
\ No newline at end of file
diff --git a/erpnext/controllers/tests/test_item_variant.py b/erpnext/controllers/tests/test_item_variant.py
index c257215..813f0a0 100644
--- a/erpnext/controllers/tests/test_item_variant.py
+++ b/erpnext/controllers/tests/test_item_variant.py
@@ -6,6 +6,7 @@
from erpnext.stock.doctype.item.test_item import set_item_variant_settings
from erpnext.controllers.item_variant import copy_attributes_to_variant, make_variant_item_code
+from erpnext.stock.doctype.quality_inspection.test_quality_inspection import create_quality_inspection_parameter
from six import string_types
@@ -56,6 +57,8 @@
qc = frappe.new_doc("Quality Inspection Template")
qc.quality_inspection_template_name = qc_template
+
+ create_quality_inspection_parameter("Moisture")
qc.append('item_quality_inspection_parameter', {
"specification": "Moisture",
"value": "< 5%",
diff --git a/erpnext/crm/desk_page/crm/crm.json b/erpnext/crm/desk_page/crm/crm.json
deleted file mode 100644
index d974beb..0000000
--- a/erpnext/crm/desk_page/crm/crm.json
+++ /dev/null
@@ -1,86 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Sales Pipeline",
- "links": "[\n {\n \"description\": \"Database of potential customers.\",\n \"label\": \"Lead\",\n \"name\": \"Lead\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Potential opportunities for selling.\",\n \"label\": \"Opportunity\",\n \"name\": \"Opportunity\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customer database.\",\n \"label\": \"Customer\",\n \"name\": \"Customer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Contacts.\",\n \"label\": \"Contact\",\n \"name\": \"Contact\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Record of all communications of type email, phone, chat, visit, etc.\",\n \"label\": \"Communication\",\n \"name\": \"Communication\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Track Leads by Lead Source.\",\n \"label\": \"Lead Source\",\n \"name\": \"Lead Source\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Helps you keep tracks of Contracts based on Supplier, Customer and Employee\",\n \"label\": \"Contract\",\n \"name\": \"Contract\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Helps you manage appointments with your leads\",\n \"label\": \"Appointment\",\n \"name\": \"Appointment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Newsletter\",\n \"name\": \"Newsletter\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Prospects Engaged But Not Converted\",\n \"name\": \"Prospects Engaged But Not Converted\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Opportunity\"\n ],\n \"doctype\": \"Opportunity\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Opportunity\",\n \"name\": \"First Response Time for Opportunity\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Campaign Efficiency\",\n \"name\": \"Campaign Efficiency\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Owner Efficiency\",\n \"name\": \"Lead Owner Efficiency\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Maintenance",
- "links": "[\n {\n \"description\": \"Plan for maintenance visits.\",\n \"label\": \"Maintenance Schedule\",\n \"name\": \"Maintenance Schedule\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Visit report for maintenance call.\",\n \"label\": \"Maintenance Visit\",\n \"name\": \"Maintenance Visit\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Warranty Claim against Serial No.\",\n \"label\": \"Warranty Claim\",\n \"name\": \"Warranty Claim\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Campaign",
- "links": "[\n {\n \"description\": \"Sales campaigns.\",\n \"label\": \"Campaign\",\n \"name\": \"Campaign\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Sends Mails to lead or contact based on a Campaign schedule\",\n \"label\": \"Email Campaign\",\n \"name\": \"Email Campaign\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create and Schedule social media posts\",\n \"label\": \"Social Media Post\",\n \"name\": \"Social Media Post\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Settings",
- "links": "[\n {\n \"description\": \"Manage Customer Group Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Customer Group\",\n \"link\": \"Tree/Customer Group\",\n \"name\": \"Customer Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Territory Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Territory\",\n \"link\": \"Tree/Territory\",\n \"name\": \"Territory\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Sales Person Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Sales Person\",\n \"link\": \"Tree/Sales Person\",\n \"name\": \"Sales Person\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Send mass SMS to your contacts\",\n \"label\": \"SMS Center\",\n \"name\": \"SMS Center\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Logs for maintaining sms delivery status\",\n \"label\": \"SMS Log\",\n \"name\": \"SMS Log\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Setup SMS gateway settings\",\n \"label\": \"SMS Settings\",\n \"name\": \"SMS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Email Group\",\n \"name\": \"Email Group\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Twitter Settings\",\n \"name\": \"Twitter Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"LinkedIn Settings\",\n \"name\": \"LinkedIn Settings\",\n \"type\": \"doctype\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [
- {
- "chart_name": "Territory Wise Sales"
- }
- ],
- "creation": "2020-01-23 14:48:30.183272",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "CRM",
- "modified": "2020-08-11 18:55:18.238900",
- "modified_by": "Administrator",
- "module": "CRM",
- "name": "CRM",
- "onboarding": "CRM",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "color": "#ffe8cd",
- "format": "{} Open",
- "label": "Lead",
- "link_to": "Lead",
- "stats_filter": "{\"status\":\"Open\"}",
- "type": "DocType"
- },
- {
- "color": "#cef6d1",
- "format": "{} Assigned",
- "label": "Opportunity",
- "link_to": "Opportunity",
- "stats_filter": "{\"_assign\": [\"like\", '%' + frappe.session.user + '%']}",
- "type": "DocType"
- },
- {
- "label": "Customer",
- "link_to": "Customer",
- "type": "DocType"
- },
- {
- "label": "Sales Analytics",
- "link_to": "Sales Analytics",
- "type": "Report"
- },
- {
- "label": "Dashboard",
- "link_to": "CRM",
- "type": "Dashboard"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py
index 63efeb3..2009ebf 100644
--- a/erpnext/crm/doctype/appointment/appointment.py
+++ b/erpnext/crm/doctype/appointment/appointment.py
@@ -126,7 +126,7 @@
add_assignemnt({
'doctype': self.doctype,
'name': self.name,
- 'assign_to': existing_assignee
+ 'assign_to': [existing_assignee]
})
return
if self._assign:
@@ -139,7 +139,7 @@
add_assignemnt({
'doctype': self.doctype,
'name': self.name,
- 'assign_to': agent
+ 'assign_to': [agent]
})
break
diff --git a/erpnext/crm/doctype/contract/contract.js b/erpnext/crm/doctype/contract/contract.js
index ee9e895..9968855 100644
--- a/erpnext/crm/doctype/contract/contract.js
+++ b/erpnext/crm/doctype/contract/contract.js
@@ -1,23 +1,31 @@
// Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
-cur_frm.add_fetch("contract_template", "contract_terms", "contract_terms");
-cur_frm.add_fetch("contract_template", "requires_fulfilment", "requires_fulfilment");
-
-// Add fulfilment terms from contract template into contract
frappe.ui.form.on("Contract", {
contract_template: function (frm) {
- // Populate the fulfilment terms table from a contract template, if any
if (frm.doc.contract_template) {
- frappe.model.with_doc("Contract Template", frm.doc.contract_template, function () {
- var tabletransfer = frappe.model.get_doc("Contract Template", frm.doc.contract_template);
-
- frm.doc.fulfilment_terms = [];
- $.each(tabletransfer.fulfilment_terms, function (index, row) {
- var d = frm.add_child("fulfilment_terms");
- d.requirement = row.requirement;
- frm.refresh_field("fulfilment_terms");
- });
+ frappe.call({
+ method: 'erpnext.crm.doctype.contract_template.contract_template.get_contract_template',
+ args: {
+ template_name: frm.doc.contract_template,
+ doc: frm.doc
+ },
+ callback: function(r) {
+ if (r && r.message) {
+ let contract_template = r.message.contract_template;
+ frm.set_value("contract_terms", r.message.contract_terms);
+ frm.set_value("requires_fulfilment", contract_template.requires_fulfilment);
+
+ if (frm.doc.requires_fulfilment) {
+ // Populate the fulfilment terms table from a contract template, if any
+ r.message.contract_template.fulfilment_terms.forEach(element => {
+ let d = frm.add_child("fulfilment_terms");
+ d.requirement = element.requirement;
+ });
+ frm.refresh_field("fulfilment_terms");
+ }
+ }
+ }
});
}
}
diff --git a/erpnext/crm/doctype/contract/contract.json b/erpnext/crm/doctype/contract/contract.json
index 0026e4a..de3230f 100755
--- a/erpnext/crm/doctype/contract/contract.json
+++ b/erpnext/crm/doctype/contract/contract.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"allow_import": 1,
"allow_rename": 1,
"creation": "2018-04-12 06:32:04.582486",
@@ -247,7 +248,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-03-30 06:56:07.257932",
+ "modified": "2020-12-07 11:15:58.385521",
"modified_by": "Administrator",
"module": "CRM",
"name": "Contract",
diff --git a/erpnext/crm/doctype/contract/contract_list.js b/erpnext/crm/doctype/contract/contract_list.js
index 2ef5900..26a2907 100644
--- a/erpnext/crm/doctype/contract/contract_list.js
+++ b/erpnext/crm/doctype/contract/contract_list.js
@@ -1,12 +1,12 @@
frappe.listview_settings['Contract'] = {
- add_fields: ["status"],
- get_indicator: function (doc) {
- if (doc.status == "Unsigned") {
- return [__(doc.status), "red", "status,=," + doc.status];
- } else if (doc.status == "Active") {
- return [__(doc.status), "green", "status,=," + doc.status];
- } else if (doc.status == "Inactive") {
- return [__(doc.status), "darkgrey", "status,=," + doc.status];
- }
- },
+ add_fields: ["status"],
+ get_indicator: function (doc) {
+ if (doc.status == "Unsigned") {
+ return [__(doc.status), "red", "status,=," + doc.status];
+ } else if (doc.status == "Active") {
+ return [__(doc.status), "green", "status,=," + doc.status];
+ } else if (doc.status == "Inactive") {
+ return [__(doc.status), "gray", "status,=," + doc.status];
+ }
+ },
};
\ No newline at end of file
diff --git a/erpnext/crm/doctype/contract_template/contract_template.json b/erpnext/crm/doctype/contract_template/contract_template.json
index 5e4582f..7cc5ec1 100644
--- a/erpnext/crm/doctype/contract_template/contract_template.json
+++ b/erpnext/crm/doctype/contract_template/contract_template.json
@@ -11,7 +11,9 @@
"contract_terms",
"sb_fulfilment",
"requires_fulfilment",
- "fulfilment_terms"
+ "fulfilment_terms",
+ "section_break_6",
+ "contract_template_help"
],
"fields": [
{
@@ -41,10 +43,20 @@
"fieldtype": "Table",
"label": "Fulfilment Terms and Conditions",
"options": "Contract Template Fulfilment Terms"
+ },
+ {
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "contract_template_help",
+ "fieldtype": "HTML",
+ "label": "Contract Template Help",
+ "options": "<h4>Contract Template Example</h4>\n\n<pre>Contract for Customer {{ party_name }}\n\n-Valid From : {{ start_date }} \n-Valid To : {{ end_date }}\n</pre>\n\n<h4>How to get fieldnames</h4>\n\n<p>The field names you can use in your Contract Template are the fields in the Contract for which you are creating the template. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Contract)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>"
}
],
"links": [],
- "modified": "2020-11-11 17:49:44.879363",
+ "modified": "2020-12-07 10:44:22.587047",
"modified_by": "Administrator",
"module": "CRM",
"name": "Contract Template",
diff --git a/erpnext/crm/doctype/contract_template/contract_template.py b/erpnext/crm/doctype/contract_template/contract_template.py
index 601ee9a..69fd86f 100644
--- a/erpnext/crm/doctype/contract_template/contract_template.py
+++ b/erpnext/crm/doctype/contract_template/contract_template.py
@@ -5,6 +5,27 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
+from frappe.utils.jinja import validate_template
+from six import string_types
+import json
class ContractTemplate(Document):
- pass
+ def validate(self):
+ if self.contract_terms:
+ validate_template(self.contract_terms)
+
+@frappe.whitelist()
+def get_contract_template(template_name, doc):
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+
+ contract_template = frappe.get_doc("Contract Template", template_name)
+ contract_terms = None
+
+ if contract_template.contract_terms:
+ contract_terms = frappe.render_template(contract_template.contract_terms, doc)
+
+ return {
+ 'contract_template': contract_template,
+ 'contract_terms': contract_terms
+ }
\ No newline at end of file
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 2df1793..1b33fd7 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -49,6 +49,7 @@
"phone",
"mobile_no",
"fax",
+ "website",
"more_info",
"type",
"market_segment",
@@ -56,8 +57,8 @@
"request_type",
"column_break3",
"company",
- "website",
"territory",
+ "language",
"unsubscribed",
"blog_subscriber",
"title"
@@ -447,13 +448,19 @@
"fieldtype": "Select",
"label": "Address Type",
"options": "Billing\nShipping\nOffice\nPersonal\nPlant\nPostal\nShop\nSubsidiary\nWarehouse\nCurrent\nPermanent\nOther"
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
}
],
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
"links": [],
- "modified": "2020-10-13 15:24:00.094811",
+ "modified": "2021-01-06 19:39:58.748978",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
diff --git a/erpnext/crm/doctype/lead/lead.py b/erpnext/crm/doctype/lead/lead.py
index 1439adb..d1d0968 100644
--- a/erpnext/crm/doctype/lead/lead.py
+++ b/erpnext/crm/doctype/lead/lead.py
@@ -176,7 +176,7 @@
"phone": self.mobile_no
})
- contact.insert()
+ contact.insert(ignore_permissions=True)
return contact
@@ -352,7 +352,7 @@
leads = frappe.get_all('Lead', or_filters={
'phone': ['like', '%{}'.format(number)],
'mobile_no': ['like', '%{}'.format(number)]
- }, limit=1)
+ }, limit=1, order_by="creation DESC")
lead = leads[0].name if leads else None
@@ -361,4 +361,4 @@
def daily_open_lead():
leads = frappe.get_all("Lead", filters = [["contact_date", "Between", [nowdate(), nowdate()]]])
for lead in leads:
- frappe.db.set_value("Lead", lead.name, "status", "Open")
\ No newline at end of file
+ frappe.db.set_value("Lead", lead.name, "status", "Open")
diff --git a/erpnext/crm/doctype/opportunity/opportunity.js b/erpnext/crm/doctype/opportunity/opportunity.js
index 08958b7..ac374a9 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.js
+++ b/erpnext/crm/doctype/opportunity/opportunity.js
@@ -24,6 +24,12 @@
frm.trigger('set_contact_link');
}
},
+ contact_date: function(frm) {
+ if(frm.doc.contact_date < frappe.datetime.now_datetime()){
+ frm.set_value("contact_date", "");
+ frappe.throw(__("Next follow up date should be greater than now."))
+ }
+ },
onload_post_render: function(frm) {
frm.get_field("items").grid.set_multiple_add("item_code", "qty");
diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json
index eee13f7..2e09a76 100644
--- a/erpnext/crm/doctype/opportunity/opportunity.json
+++ b/erpnext/crm/doctype/opportunity/opportunity.json
@@ -54,6 +54,7 @@
"campaign",
"column_break1",
"transaction_date",
+ "language",
"amended_from",
"lost_reasons"
],
@@ -419,12 +420,18 @@
"fieldtype": "Duration",
"label": "First Response Time",
"read_only": 1
+ },
+ {
+ "fieldname": "language",
+ "fieldtype": "Link",
+ "label": "Print Language",
+ "options": "Language"
}
],
"icon": "fa fa-info-sign",
"idx": 195,
"links": [],
- "modified": "2020-08-12 17:34:35.066961",
+ "modified": "2021-01-06 19:42:46.190051",
"modified_by": "Administrator",
"module": "CRM",
"name": "Opportunity",
diff --git a/erpnext/crm/doctype/utils.py b/erpnext/crm/doctype/utils.py
index 885ef05..f244daf 100644
--- a/erpnext/crm/doctype/utils.py
+++ b/erpnext/crm/doctype/utils.py
@@ -78,7 +78,9 @@
def strip_number(number):
if not number: return
- # strip 0 from the start of the number for proper number comparisions
+ # strip + and 0 from the start of the number for proper number comparisions
+ # eg. +7888383332 should match with 7888383332
# eg. 07888383332 should match with 7888383332
+ number = number.lstrip('+')
number = number.lstrip('0')
- return number
\ No newline at end of file
+ return number
diff --git a/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json
index 9f996d9..0ee9317 100644
--- a/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json
+++ b/erpnext/crm/onboarding_step/create_opportunity/create_opportunity.json
@@ -8,12 +8,12 @@
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 17:38:27.496696",
+ "modified": "2021-01-21 15:28:52.483839",
"modified_by": "Administrator",
"name": "Create Opportunity",
"owner": "Administrator",
"reference_document": "Opportunity",
- "show_full_form": 0,
+ "show_full_form": 1,
"title": "Create Opportunity",
"validate_action": 1
}
\ No newline at end of file
diff --git a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py
index b538a58..3a9d57d 100644
--- a/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py
+++ b/erpnext/crm/report/prospects_engaged_but_not_converted/prospects_engaged_but_not_converted.py
@@ -19,15 +19,50 @@
if not filters.get('lead_age'): filters["lead_age"] = 60
def get_columns():
- return [
- _("Lead") + ":Link/Lead:100",
- _("Name") + "::100",
- _("Organization") + "::100",
- _("Reference Document") + "::150",
- _("Reference Name") + ":Dynamic Link/"+_("Reference Document")+":120",
- _("Last Communication") + ":Data:200",
- _("Last Communication Date") + ":Date:180"
- ]
+ columns = [{
+ "label": _("Lead"),
+ "fieldname": "lead",
+ "fieldtype": "Link",
+ "options": "Lead",
+ "width": 130
+ },
+ {
+ "label": _("Name"),
+ "fieldname": "name",
+ "width": 120
+ },
+ {
+ "label": _("Organization"),
+ "fieldname": "organization",
+ "width": 120
+ },
+ {
+ "label": _("Reference Document Type"),
+ "fieldname": "reference_document_type",
+ "fieldtype": "Link",
+ "options": "Doctype",
+ "width": 100
+ },
+ {
+ "label": _("Reference Name"),
+ "fieldname": "reference_name",
+ "fieldtype": "Dynamic Link",
+ "options": "reference_document_type",
+ "width": 140
+ },
+ {
+ "label": _("Last Communication"),
+ "fieldname": "last_communication",
+ "fieldtype": "Data",
+ "width": 200
+ },
+ {
+ "label": _("Last Communication Date"),
+ "fieldname": "last_communication_date",
+ "fieldtype": "Date",
+ "width": 100
+ }]
+ return columns
def get_data(filters):
lead_details = []
diff --git a/erpnext/crm/workspace/crm/crm.json b/erpnext/crm/workspace/crm/crm.json
new file mode 100644
index 0000000..b4fb7d8
--- /dev/null
+++ b/erpnext/crm/workspace/crm/crm.json
@@ -0,0 +1,407 @@
+{
+ "category": "Modules",
+ "charts": [
+ {
+ "chart_name": "Territory Wise Sales"
+ }
+ ],
+ "creation": "2020-01-23 14:48:30.183272",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "crm",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "CRM",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Pipeline",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Lead",
+ "link_to": "Lead",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Opportunity",
+ "link_to": "Opportunity",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Customer",
+ "link_to": "Customer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Contact",
+ "link_to": "Contact",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Communication",
+ "link_to": "Communication",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Lead Source",
+ "link_to": "Lead Source",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Contract",
+ "link_to": "Contract",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Appointment",
+ "link_to": "Appointment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Newsletter",
+ "link_to": "Newsletter",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Lead",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Lead Details",
+ "link_to": "Lead Details",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Funnel",
+ "link_to": "sales-funnel",
+ "link_type": "Page",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Lead",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Prospects Engaged But Not Converted",
+ "link_to": "Prospects Engaged But Not Converted",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Opportunity",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "First Response Time for Opportunity",
+ "link_to": "First Response Time for Opportunity",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Inactive Customers",
+ "link_to": "Inactive Customers",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Lead",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Campaign Efficiency",
+ "link_to": "Campaign Efficiency",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Lead",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Lead Owner Efficiency",
+ "link_to": "Lead Owner Efficiency",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Maintenance",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Maintenance Schedule",
+ "link_to": "Maintenance Schedule",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Maintenance Visit",
+ "link_to": "Maintenance Visit",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Warranty Claim",
+ "link_to": "Warranty Claim",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Campaign",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Campaign",
+ "link_to": "Campaign",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email Campaign",
+ "link_to": "Email Campaign",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Social Media Post",
+ "link_to": "Social Media Post",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Customer Group",
+ "link_to": "Customer Group",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Territory",
+ "link_to": "Territory",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Person",
+ "link_to": "Sales Person",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "SMS Center",
+ "link_to": "SMS Center",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "SMS Log",
+ "link_to": "SMS Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "SMS Settings",
+ "link_to": "SMS Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email Group",
+ "link_to": "Email Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Twitter Settings",
+ "link_to": "Twitter Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "LinkedIn Settings",
+ "link_to": "LinkedIn Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:36.871352",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "CRM",
+ "onboarding": "CRM",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "color": "Blue",
+ "format": "{} Open",
+ "label": "Lead",
+ "link_to": "Lead",
+ "stats_filter": "{\"status\":\"Open\"}",
+ "type": "DocType"
+ },
+ {
+ "color": "Blue",
+ "format": "{} Assigned",
+ "label": "Opportunity",
+ "link_to": "Opportunity",
+ "stats_filter": "{\"_assign\": [\"like\", '%' + frappe.session.user + '%']}",
+ "type": "DocType"
+ },
+ {
+ "label": "Customer",
+ "link_to": "Customer",
+ "type": "DocType"
+ },
+ {
+ "label": "Sales Analytics",
+ "link_to": "Sales Analytics",
+ "type": "Report"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "CRM",
+ "type": "Dashboard"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/education/desk_page/education/education.json b/erpnext/education/desk_page/education/education.json
deleted file mode 100644
index 77ee8ec..0000000
--- a/erpnext/education/desk_page/education/education.json
+++ /dev/null
@@ -1,154 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Student and Instructor",
- "links": "[\n {\n \"label\": \"Student\",\n \"name\": \"Student\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Instructor\",\n \"name\": \"Instructor\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Guardian\",\n \"name\": \"Guardian\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Group\",\n \"name\": \"Student Group\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Log\",\n \"name\": \"Student Log\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Masters",
- "links": "[\n {\n \"label\": \"Program\",\n \"name\": \"Program\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course\",\n \"name\": \"Course\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Topic\",\n \"name\": \"Topic\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Room\",\n \"name\": \"Room\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Content Masters",
- "links": "[\n {\n \"label\": \"Article\",\n \"name\": \"Article\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Quiz\",\n \"name\": \"Quiz\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Settings",
- "links": "[\n {\n \"label\": \"Education Settings\",\n \"name\": \"Education Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Category\",\n \"name\": \"Student Category\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Batch Name\",\n \"name\": \"Student Batch Name\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Grading Scale\",\n \"name\": \"Grading Scale\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Academic Term\",\n \"name\": \"Academic Term\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Academic Year\",\n \"name\": \"Academic Year\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Admission",
- "links": "[\n {\n \"label\": \"Student Applicant\",\n \"name\": \"Student Applicant\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Admission\",\n \"name\": \"Student Admission\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Program Enrollment\",\n \"name\": \"Program Enrollment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course Enrollment\",\n \"name\": \"Course Enrollment\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Fees",
- "links": "[\n {\n \"label\": \"Fee Structure\",\n \"name\": \"Fee Structure\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fee Category\",\n \"name\": \"Fee Category\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fee Schedule\",\n \"name\": \"Fee Schedule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fees\",\n \"name\": \"Fees\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Fees\"\n ],\n \"doctype\": \"Fees\",\n \"is_query_report\": true,\n \"label\": \"Student Fee Collection Report\",\n \"name\": \"Student Fee Collection\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Fees\"\n ],\n \"doctype\": \"Fees\",\n \"is_query_report\": true,\n \"label\": \"Program wise Fee Collection Report\",\n \"name\": \"Program wise Fee Collection\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Schedule",
- "links": "[\n {\n \"label\": \"Course Schedule\",\n \"name\": \"Course Schedule\",\n \"route\": \"#List/Course Schedule/Calendar\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course Scheduling Tool\",\n \"name\": \"Course Scheduling Tool\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Attendance",
- "links": "[\n {\n \"label\": \"Student Attendance\",\n \"name\": \"Student Attendance\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Leave Application\",\n \"name\": \"Student Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Student Monthly Attendance Sheet\",\n \"name\": \"Student Monthly Attendance Sheet\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Absent Student Report\",\n \"name\": \"Absent Student Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Student Attendance\"\n ],\n \"doctype\": \"Student Attendance\",\n \"is_query_report\": true,\n \"label\": \"Student Batch-Wise Attendance\",\n \"name\": \"Student Batch-Wise Attendance\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "LMS Activity",
- "links": "[\n {\n \"label\": \"Course Enrollment\",\n \"name\": \"Course Enrollment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course Activity\",\n \"name\": \"Course Activity\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Quiz Activity\",\n \"name\": \"Quiz Activity\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Assessment",
- "links": "[\n {\n \"label\": \"Assessment Plan\",\n \"name\": \"Assessment Plan\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Group\",\n \"link\": \"Tree/Assessment Group\",\n \"name\": \"Assessment Group\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Result\",\n \"name\": \"Assessment Result\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Criteria\",\n \"name\": \"Assessment Criteria\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Assessment Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Assessment Result\"\n ],\n \"doctype\": \"Assessment Result\",\n \"is_query_report\": true,\n \"label\": \"Course wise Assessment Report\",\n \"name\": \"Course wise Assessment Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Assessment Result\"\n ],\n \"doctype\": \"Assessment Result\",\n \"is_query_report\": true,\n \"label\": \"Final Assessment Grades\",\n \"name\": \"Final Assessment Grades\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Assessment Plan\"\n ],\n \"doctype\": \"Assessment Plan\",\n \"is_query_report\": true,\n \"label\": \"Assessment Plan Status\",\n \"name\": \"Assessment Plan Status\",\n \"type\": \"report\"\n },\n {\n \"label\": \"Student Report Generation Tool\",\n \"name\": \"Student Report Generation Tool\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Tools",
- "links": "[\n {\n \"label\": \"Student Attendance Tool\",\n \"name\": \"Student Attendance Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Assessment Result Tool\",\n \"name\": \"Assessment Result Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Student Group Creation Tool\",\n \"name\": \"Student Group Creation Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Program Enrollment Tool\",\n \"name\": \"Program Enrollment Tool\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course Scheduling Tool\",\n \"name\": \"Course Scheduling Tool\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Other Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Program Enrollment\"\n ],\n \"doctype\": \"Program Enrollment\",\n \"is_query_report\": true,\n \"label\": \"Student and Guardian Contact Details\",\n \"name\": \"Student and Guardian Contact Details\",\n \"type\": \"report\"\n }\n]"
- }
- ],
- "category": "Domains",
- "charts": [
- {
- "chart_name": "Program Enrollments",
- "label": "Program Enrollments"
- }
- ],
- "creation": "2020-03-02 17:22:57.066401",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Education",
- "modified": "2020-07-27 19:35:18.832694",
- "modified_by": "Administrator",
- "module": "Education",
- "name": "Education",
- "onboarding": "Education",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "restrict_to_domain": "Education",
- "shortcuts": [
- {
- "color": "#cef6d1",
- "format": "{} Active",
- "label": "Student",
- "link_to": "Student",
- "stats_filter": "{\n \"enabled\": 1\n}",
- "type": "DocType"
- },
- {
- "color": "#cef6d1",
- "format": "{} Active",
- "label": "Instructor",
- "link_to": "Instructor",
- "stats_filter": "{\n \"status\": \"Active\"\n}",
- "type": "DocType"
- },
- {
- "color": "",
- "format": "",
- "label": "Program",
- "link_to": "Program",
- "stats_filter": "",
- "type": "DocType"
- },
- {
- "label": "Course",
- "link_to": "Course",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} Unpaid",
- "label": "Fees",
- "link_to": "Fees",
- "stats_filter": "{\n \"outstanding_amount\": [\"!=\", 0.0]\n}",
- "type": "DocType"
- },
- {
- "label": "Student Monthly Attendance Sheet",
- "link_to": "Student Monthly Attendance Sheet",
- "type": "Report"
- },
- {
- "label": "Course Scheduling Tool",
- "link_to": "Course Scheduling Tool",
- "type": "DocType"
- },
- {
- "label": "Student Attendance Tool",
- "link_to": "Student Attendance Tool",
- "type": "DocType"
- },
- {
- "label": "Dashboard",
- "link_to": "Education",
- "type": "Dashboard"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/education/doctype/assessment_result_tool/assessment_result_tool.js b/erpnext/education/doctype/assessment_result_tool/assessment_result_tool.js
index 3cd4512..053f0c2 100644
--- a/erpnext/education/doctype/assessment_result_tool/assessment_result_tool.js
+++ b/erpnext/education/doctype/assessment_result_tool/assessment_result_tool.js
@@ -128,7 +128,7 @@
result_table.find(`span[data-student=${assessment_result.student}].total-score-grade`).html(assessment_result.grade);
let link_span = result_table.find(`span[data-student=${assessment_result.student}].total-result-link`);
$(link_span).css("display", "block");
- $(link_span).find("a").attr("href", "#Form/Assessment Result/"+assessment_result.name);
+ $(link_span).find("a").attr("href", "/app/assessment-result/"+assessment_result.name);
}
});
}
diff --git a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js
index 20503f9..d57f46a 100644
--- a/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js
+++ b/erpnext/education/doctype/course_scheduling_tool/course_scheduling_tool.js
@@ -19,17 +19,22 @@
}
const { course_schedules } = r.message;
if (course_schedules) {
+ const course_schedules_html = course_schedules.map(c => `
+ <tr>
+ <td><a href="/app/course-schedule/${c.name}">${c.name}</a></td>
+ <td>${c.schedule_date}</td>
+ </tr>
+ `).join('');
+
const html = `
- <table class="table table-bordered">
- <caption>${__('Following course schedules were created')}</caption>
- <thead><tr><th>${__("Course")}</th><th>${__("Date")}</th></tr></thead>
- <tbody>
- ${course_schedules.map(
- c => `<tr><td><a href="#Form/Course Schedule/${c.name}">${c.name}</a></td>
- <td>${c.schedule_date}</td></tr>`
- ).join('')}
- </tbody>
- </table>`
+ <table class="table table-bordered">
+ <caption>${__('Following course schedules were created')}</caption>
+ <thead><tr><th>${__("Course")}</th><th>${__("Date")}</th></tr></thead>
+ <tbody>
+ ${course_schedules_html}
+ </tbody>
+ </table>
+ `;
frappe.msgprint(html);
}
diff --git a/erpnext/education/doctype/fee_schedule/fee_schedule.js b/erpnext/education/doctype/fee_schedule/fee_schedule.js
index 75dd446..0089957 100644
--- a/erpnext/education/doctype/fee_schedule/fee_schedule.js
+++ b/erpnext/education/doctype/fee_schedule/fee_schedule.js
@@ -1,6 +1,7 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on('Fee Schedule', {
setup: function(frm) {
frm.add_fetch('fee_structure', 'receivable_account', 'receivable_account');
@@ -8,6 +9,10 @@
frm.add_fetch('fee_structure', 'cost_center', 'cost_center');
},
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
onload: function(frm) {
frm.set_query('receivable_account', function(doc) {
return {
@@ -43,13 +48,15 @@
frm.reload_doc();
}
if (data.progress) {
- let progress_bar = $(cur_frm.dashboard.progress_area).find('.progress-bar');
+ let progress_bar = $(cur_frm.dashboard.progress_area.body).find('.progress-bar');
if (progress_bar) {
$(progress_bar).removeClass('progress-bar-danger').addClass('progress-bar-success progress-bar-striped');
$(progress_bar).css('width', data.progress+'%');
}
}
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
diff --git a/erpnext/education/doctype/fee_structure/fee_structure.js b/erpnext/education/doctype/fee_structure/fee_structure.js
index b331c6d..310c410 100644
--- a/erpnext/education/doctype/fee_structure/fee_structure.js
+++ b/erpnext/education/doctype/fee_structure/fee_structure.js
@@ -1,6 +1,8 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
+
frappe.ui.form.on('Fee Structure', {
setup: function(frm) {
frm.add_fetch('company', 'default_receivable_account', 'receivable_account');
@@ -8,6 +10,10 @@
frm.add_fetch('company', 'cost_center', 'cost_center');
},
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
onload: function(frm) {
frm.set_query('academic_term', function() {
return {
@@ -35,6 +41,8 @@
}
};
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
diff --git a/erpnext/education/doctype/fees/fees.js b/erpnext/education/doctype/fees/fees.js
index aaf42b4..ac66acd 100644
--- a/erpnext/education/doctype/fees/fees.js
+++ b/erpnext/education/doctype/fees/fees.js
@@ -1,6 +1,7 @@
// Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and contributors
// For license information, please see license.txt
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Fees", {
setup: function(frm) {
@@ -9,15 +10,19 @@
frm.add_fetch("fee_structure", "cost_center", "cost_center");
},
- onload: function(frm){
- frm.set_query("academic_term",function(){
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+
+ onload: function(frm) {
+ frm.set_query("academic_term", function() {
return{
- "filters":{
+ "filters": {
"academic_year": (frm.doc.academic_year)
}
};
});
- frm.set_query("fee_structure",function(){
+ frm.set_query("fee_structure", function() {
return{
"filters":{
"academic_year": (frm.doc.academic_year)
@@ -45,6 +50,8 @@
if (!frm.doc.posting_date) {
frm.doc.posting_date = frappe.datetime.get_today();
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
diff --git a/erpnext/education/doctype/program_enrollment/program_enrollment.py b/erpnext/education/doctype/program_enrollment/program_enrollment.py
index 6fbcd8a..d18c0f9 100644
--- a/erpnext/education/doctype/program_enrollment/program_enrollment.py
+++ b/erpnext/education/doctype/program_enrollment/program_enrollment.py
@@ -87,7 +87,7 @@
fees.submit()
fee_list.append(fees.name)
if fee_list:
- fee_list = ["""<a href="#Form/Fees/%s" target="_blank">%s</a>""" % \
+ fee_list = ["""<a href="/app/Form/Fees/%s" target="_blank">%s</a>""" % \
(fee, fee) for fee in fee_list]
msgprint(_("Fee Records Created - {0}").format(comma_and(fee_list)))
@@ -124,21 +124,24 @@
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_program_courses(doctype, txt, searchfield, start, page_len, filters):
- if filters.get('program'):
- return frappe.db.sql("""select course, course_name from `tabProgram Course`
- where parent = %(program)s and course like %(txt)s {match_cond}
- order by
- if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999),
- idx desc,
- `tabProgram Course`.course asc
- limit {start}, {page_len}""".format(
- match_cond=get_match_cond(doctype),
- start=start,
- page_len=page_len), {
- "txt": "%{0}%".format(txt),
- "_txt": txt.replace('%', ''),
- "program": filters['program']
- })
+ if not filters.get('program'):
+ frappe.msgprint(_("Please select a Program first."))
+ return []
+
+ return frappe.db.sql("""select course, course_name from `tabProgram Course`
+ where parent = %(program)s and course like %(txt)s {match_cond}
+ order by
+ if(locate(%(_txt)s, course), locate(%(_txt)s, course), 99999),
+ idx desc,
+ `tabProgram Course`.course asc
+ limit {start}, {page_len}""".format(
+ match_cond=get_match_cond(doctype),
+ start=start,
+ page_len=page_len), {
+ "txt": "%{0}%".format(txt),
+ "_txt": txt.replace('%', ''),
+ "program": filters['program']
+ })
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/education/doctype/student_admission/templates/student_admission.html b/erpnext/education/doctype/student_admission/templates/student_admission.html
index 7ff3906..f9ddac0 100644
--- a/erpnext/education/doctype/student_admission/templates/student_admission.html
+++ b/erpnext/education/doctype/student_admission/templates/student_admission.html
@@ -21,7 +21,7 @@
{% elif frappe.utils.getdate(doc.admission_start_date) > today %}
blue"> Application will open
{% else %}
- darkgrey
+ gray
{% endif %}
</span>
</div>
diff --git a/erpnext/education/doctype/student_admission/templates/student_admission_row.html b/erpnext/education/doctype/student_admission/templates/student_admission_row.html
index cf22436..99868d5 100644
--- a/erpnext/education/doctype/student_admission/templates/student_admission_row.html
+++ b/erpnext/education/doctype/student_admission/templates/student_admission_row.html
@@ -11,7 +11,7 @@
{% elif frappe.utils.getdate(doc.admission_start_date) > today %}
blue
{% else %}
- darkgrey
+ gray
{% endif %}
">{{ doc.title }}</span>
</div>
diff --git a/erpnext/education/workspace/education/education.json b/erpnext/education/workspace/education/education.json
new file mode 100644
index 0000000..bf74961
--- /dev/null
+++ b/erpnext/education/workspace/education/education.json
@@ -0,0 +1,701 @@
+{
+ "category": "Domains",
+ "charts": [
+ {
+ "chart_name": "Program Enrollments",
+ "label": "Program Enrollments"
+ }
+ ],
+ "creation": "2020-03-02 17:22:57.066401",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "education",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Education",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student and Instructor",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student",
+ "link_to": "Student",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Instructor",
+ "link_to": "Instructor",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Guardian",
+ "link_to": "Guardian",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Group",
+ "link_to": "Student Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Log",
+ "link_to": "Student Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Masters",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Program",
+ "link_to": "Program",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Course",
+ "link_to": "Course",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Topic",
+ "link_to": "Topic",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Room",
+ "link_to": "Room",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Content Masters",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Article",
+ "link_to": "Article",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Video",
+ "link_to": "Video",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quiz",
+ "link_to": "Quiz",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Education Settings",
+ "link_to": "Education Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Category",
+ "link_to": "Student Category",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Batch Name",
+ "link_to": "Student Batch Name",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Grading Scale",
+ "link_to": "Grading Scale",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Academic Term",
+ "link_to": "Academic Term",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Academic Year",
+ "link_to": "Academic Year",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Admission",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Applicant",
+ "link_to": "Student Applicant",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Admission",
+ "link_to": "Student Admission",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Program Enrollment",
+ "link_to": "Program Enrollment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Course Enrollment",
+ "link_to": "Course Enrollment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fees",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fee Structure",
+ "link_to": "Fee Structure",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fee Category",
+ "link_to": "Fee Category",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fee Schedule",
+ "link_to": "Fee Schedule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fees",
+ "link_to": "Fees",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Fees",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Student Fee Collection Report",
+ "link_to": "Student Fee Collection",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Fees",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Program wise Fee Collection Report",
+ "link_to": "Program wise Fee Collection",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Schedule",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Course Schedule",
+ "link_to": "Course Schedule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Course Scheduling Tool",
+ "link_to": "Course Scheduling Tool",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Attendance",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Attendance",
+ "link_to": "Student Attendance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Leave Application",
+ "link_to": "Student Leave Application",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Student Attendance",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Student Monthly Attendance Sheet",
+ "link_to": "Student Monthly Attendance Sheet",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Student Attendance",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Absent Student Report",
+ "link_to": "Absent Student Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Student Attendance",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Student Batch-Wise Attendance",
+ "link_to": "Student Batch-Wise Attendance",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "LMS Activity",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Course Enrollment",
+ "link_to": "Course Enrollment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Course Activity",
+ "link_to": "Course Activity",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quiz Activity",
+ "link_to": "Quiz Activity",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Assessment",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Assessment Plan",
+ "link_to": "Assessment Plan",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Assessment Group",
+ "link_to": "Assessment Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Assessment Result",
+ "link_to": "Assessment Result",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Assessment Criteria",
+ "link_to": "Assessment Criteria",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Assessment Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Assessment Result",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Course wise Assessment Report",
+ "link_to": "Course wise Assessment Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Assessment Result",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Final Assessment Grades",
+ "link_to": "Final Assessment Grades",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Assessment Plan",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Assessment Plan Status",
+ "link_to": "Assessment Plan Status",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Report Generation Tool",
+ "link_to": "Student Report Generation Tool",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tools",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Attendance Tool",
+ "link_to": "Student Attendance Tool",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Assessment Result Tool",
+ "link_to": "Assessment Result Tool",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student Group Creation Tool",
+ "link_to": "Student Group Creation Tool",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Program Enrollment Tool",
+ "link_to": "Program Enrollment Tool",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Course Scheduling Tool",
+ "link_to": "Course Scheduling Tool",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Other Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Program Enrollment",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Student and Guardian Contact Details",
+ "link_to": "Student and Guardian Contact Details",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:37.448989",
+ "modified_by": "Administrator",
+ "module": "Education",
+ "name": "Education",
+ "onboarding": "Education",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "restrict_to_domain": "Education",
+ "shortcuts": [
+ {
+ "color": "Grey",
+ "format": "{} Active",
+ "label": "Student",
+ "link_to": "Student",
+ "stats_filter": "{\n \"enabled\": 1\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "format": "{} Active",
+ "label": "Instructor",
+ "link_to": "Instructor",
+ "stats_filter": "{\n \"status\": \"Active\"\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "",
+ "format": "",
+ "label": "Program",
+ "link_to": "Program",
+ "stats_filter": "",
+ "type": "DocType"
+ },
+ {
+ "label": "Course",
+ "link_to": "Course",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "format": "{} Unpaid",
+ "label": "Fees",
+ "link_to": "Fees",
+ "stats_filter": "{\n \"outstanding_amount\": [\"!=\", 0.0]\n}",
+ "type": "DocType"
+ },
+ {
+ "label": "Student Monthly Attendance Sheet",
+ "link_to": "Student Monthly Attendance Sheet",
+ "type": "Report"
+ },
+ {
+ "label": "Course Scheduling Tool",
+ "link_to": "Course Scheduling Tool",
+ "type": "DocType"
+ },
+ {
+ "label": "Student Attendance Tool",
+ "link_to": "Student Attendance Tool",
+ "type": "DocType"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "Education",
+ "type": "Dashboard"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/connectors/shopify_connection.py b/erpnext/erpnext_integrations/connectors/shopify_connection.py
index efbaa71..f0a05ed 100644
--- a/erpnext/erpnext_integrations/connectors/shopify_connection.py
+++ b/erpnext/erpnext_integrations/connectors/shopify_connection.py
@@ -260,6 +260,15 @@
"""Shipping lines represents the shipping details,
each such shipping detail consists of a list of tax_lines"""
for shipping_charge in shipping_lines:
+ if shipping_charge.get("price"):
+ taxes.append({
+ "charge_type": _("Actual"),
+ "account_head": get_tax_account_head(shipping_charge),
+ "description": shipping_charge["title"],
+ "tax_amount": shipping_charge["price"],
+ "cost_center": shopify_settings.cost_center
+ })
+
for tax in shipping_charge.get("tax_lines"):
taxes.append({
"charge_type": _("Actual"),
diff --git a/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json
deleted file mode 100644
index ea3b129..0000000
--- a/erpnext/erpnext_integrations/desk_page/erpnext_integrations/erpnext_integrations.json
+++ /dev/null
@@ -1,40 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Marketplace",
- "links": "[\n {\n \"description\": \"Woocommerce marketplace settings\",\n \"label\": \"Woocommerce Settings\",\n \"name\": \"Woocommerce Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Amazon MWS settings\",\n \"label\": \"Amazon MWS Settings\",\n \"name\": \"Amazon MWS Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Shopify settings\",\n \"label\": \"Shopify Settings\",\n \"name\": \"Shopify Settings\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Payments",
- "links": "[\n {\n \"description\": \"GoCardless payment gateway settings\",\n \"label\": \"GoCardless Settings\",\n \"name\": \"GoCardless Settings\",\n \"type\": \"doctype\"\n }, {\n \"description\": \"M-Pesa payment gateway settings\",\n \"label\": \"M-Pesa Settings\",\n \"name\": \"Mpesa Settings\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Settings",
- "links": "[\n {\n \"description\": \"Plaid settings\",\n \"label\": \"Plaid Settings\",\n \"name\": \"Plaid Settings\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Exotel settings\",\n \"label\": \"Exotel Settings\",\n \"name\": \"Exotel Settings\",\n \"type\": \"doctype\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [],
- "creation": "2020-08-20 19:30:48.138801",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends": "Integrations",
- "extends_another_page": 1,
- "hide_custom": 1,
- "idx": 0,
- "is_standard": 1,
- "label": "ERPNext Integrations",
- "modified": "2020-10-29 19:54:46.228222",
- "modified_by": "Administrator",
- "module": "ERPNext Integrations",
- "name": "ERPNext Integrations",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": []
-}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/desk_page/erpnext_integrations_settings/erpnext_integrations_settings.json b/erpnext/erpnext_integrations/desk_page/erpnext_integrations_settings/erpnext_integrations_settings.json
deleted file mode 100644
index 3bbc36a..0000000
--- a/erpnext/erpnext_integrations/desk_page/erpnext_integrations_settings/erpnext_integrations_settings.json
+++ /dev/null
@@ -1,30 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Integrations Settings",
- "links": "[\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Woocommerce Settings\"\n\t},\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Shopify Settings\",\n\t\t\"description\": \"Connect Shopify with ERPNext\"\n\t},\n\t{\n\t \"type\": \"doctype\",\n\t\t\"name\": \"Amazon MWS Settings\",\n\t\t\"description\": \"Connect Amazon with ERPNext\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Plaid Settings\",\n\t\t\"description\": \"Connect your bank accounts to ERPNext\"\n\t},\n {\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Exotel Settings\",\n\t\t\"description\": \"Connect your Exotel Account to ERPNext and track call logs\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [],
- "creation": "2020-07-31 10:38:54.021237",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends": "Settings",
- "extends_another_page": 1,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "ERPNext Integrations Settings",
- "modified": "2020-07-31 10:44:39.374297",
- "modified_by": "Administrator",
- "module": "ERPNext Integrations",
- "name": "ERPNext Integrations Settings",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": []
-}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py
index d33b0a7..554c6b0 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_connector.py
@@ -5,7 +5,7 @@
class MpesaConnector():
def __init__(self, env="sandbox", app_key=None, app_secret=None, sandbox_url="https://sandbox.safaricom.co.ke",
- live_url="https://safaricom.co.ke"):
+ live_url="https://api.safaricom.co.ke"):
"""Setup configuration for Mpesa connector and generate new access token."""
self.env = env
self.app_key = app_key
@@ -102,14 +102,14 @@
"BusinessShortCode": business_shortcode,
"Password": encoded.decode("utf-8"),
"Timestamp": time,
- "TransactionType": "CustomerPayBillOnline",
"Amount": amount,
"PartyA": int(phone_number),
- "PartyB": business_shortcode,
+ "PartyB": reference_code,
"PhoneNumber": int(phone_number),
"CallBackURL": callback_url,
"AccountReference": reference_code,
- "TransactionDesc": description
+ "TransactionDesc": description,
+ "TransactionType": "CustomerPayBillOnline" if self.env == "sandbox" else "CustomerBuyGoodsOnline"
}
headers = {'Authorization': 'Bearer {0}'.format(self.authentication_token), 'Content-Type': "application/json"}
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
index fc7b310..407f826 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.json
@@ -11,8 +11,10 @@
"consumer_secret",
"initiator_name",
"till_number",
+ "transaction_limit",
"sandbox",
"column_break_4",
+ "business_shortcode",
"online_passkey",
"security_credential",
"get_account_balance",
@@ -84,10 +86,24 @@
"fieldname": "get_account_balance",
"fieldtype": "Button",
"label": "Get Account Balance"
+ },
+ {
+ "depends_on": "eval:(doc.sandbox==0)",
+ "fieldname": "business_shortcode",
+ "fieldtype": "Data",
+ "label": "Business Shortcode",
+ "mandatory_depends_on": "eval:(doc.sandbox==0)"
+ },
+ {
+ "default": "150000",
+ "fieldname": "transaction_limit",
+ "fieldtype": "Float",
+ "label": "Transaction Limit",
+ "non_negative": 1
}
],
"links": [],
- "modified": "2020-09-25 20:21:38.215494",
+ "modified": "2021-01-29 12:02:16.106942",
"modified_by": "Administrator",
"module": "ERPNext Integrations",
"name": "Mpesa Settings",
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
index 1cad84d..b571802 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/mpesa_settings.py
@@ -33,13 +33,34 @@
create_mode_of_payment('Mpesa-' + self.payment_gateway_name, payment_type="Phone")
def request_for_payment(self, **kwargs):
- if frappe.flags.in_test:
- from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload
- response = frappe._dict(get_payment_request_response_payload())
- else:
- response = frappe._dict(generate_stk_push(**kwargs))
+ args = frappe._dict(kwargs)
+ request_amounts = self.split_request_amount_according_to_transaction_limit(args)
- self.handle_api_response("CheckoutRequestID", kwargs, response)
+ for i, amount in enumerate(request_amounts):
+ args.request_amount = amount
+ if frappe.flags.in_test:
+ from erpnext.erpnext_integrations.doctype.mpesa_settings.test_mpesa_settings import get_payment_request_response_payload
+ response = frappe._dict(get_payment_request_response_payload(amount))
+ else:
+ response = frappe._dict(generate_stk_push(**args))
+
+ self.handle_api_response("CheckoutRequestID", args, response)
+
+ def split_request_amount_according_to_transaction_limit(self, args):
+ request_amount = args.request_amount
+ if request_amount > self.transaction_limit:
+ # make multiple requests
+ request_amounts = []
+ requests_to_be_made = frappe.utils.ceil(request_amount / self.transaction_limit) # 480/150 = ceil(3.2) = 4
+ for i in range(requests_to_be_made):
+ amount = self.transaction_limit
+ if i == requests_to_be_made - 1:
+ amount = request_amount - (self.transaction_limit * i) # for 4th request, 480 - (150 * 3) = 30
+ request_amounts.append(amount)
+ else:
+ request_amounts = [request_amount]
+
+ return request_amounts
def get_account_balance_info(self):
payload = dict(
@@ -67,7 +88,8 @@
req_name = getattr(response, global_id)
error = None
- create_request_log(request_dict, "Host", "Mpesa", req_name, error)
+ if not frappe.db.exists('Integration Request', req_name):
+ create_request_log(request_dict, "Host", "Mpesa", req_name, error)
if error:
frappe.throw(_(getattr(response, "errorMessage")), title=_("Transaction Error"))
@@ -80,6 +102,8 @@
mpesa_settings = frappe.get_doc("Mpesa Settings", args.payment_gateway[6:])
env = "production" if not mpesa_settings.sandbox else "sandbox"
+ # for sandbox, business shortcode is same as till number
+ business_shortcode = mpesa_settings.business_shortcode if env == "production" else mpesa_settings.till_number
connector = MpesaConnector(env=env,
app_key=mpesa_settings.consumer_key,
@@ -87,10 +111,12 @@
mobile_number = sanitize_mobile_number(args.sender)
- response = connector.stk_push(business_shortcode=mpesa_settings.till_number,
- passcode=mpesa_settings.get_password("online_passkey"), amount=args.grand_total,
+ response = connector.stk_push(
+ business_shortcode=business_shortcode, amount=args.request_amount,
+ passcode=mpesa_settings.get_password("online_passkey"),
callback_url=callback_url, reference_code=mpesa_settings.till_number,
- phone_number=mobile_number, description="POS Payment")
+ phone_number=mobile_number, description="POS Payment"
+ )
return response
@@ -108,29 +134,72 @@
transaction_response = frappe._dict(kwargs["Body"]["stkCallback"])
checkout_id = getattr(transaction_response, "CheckoutRequestID", "")
- request = frappe.get_doc("Integration Request", checkout_id)
- transaction_data = frappe._dict(loads(request.data))
+ integration_request = frappe.get_doc("Integration Request", checkout_id)
+ transaction_data = frappe._dict(loads(integration_request.data))
+ total_paid = 0 # for multiple integration request made against a pos invoice
+ success = False # for reporting successfull callback to point of sale ui
if transaction_response['ResultCode'] == 0:
- if request.reference_doctype and request.reference_docname:
+ if integration_request.reference_doctype and integration_request.reference_docname:
try:
- doc = frappe.get_doc(request.reference_doctype,
- request.reference_docname)
- doc.run_method("on_payment_authorized", 'Completed')
-
item_response = transaction_response["CallbackMetadata"]["Item"]
+ amount = fetch_param_value(item_response, "Amount", "Name")
mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
- frappe.db.set_value("POS Invoice", doc.reference_name, "mpesa_receipt_number", mpesa_receipt)
- request.handle_success(transaction_response)
+ pr = frappe.get_doc(integration_request.reference_doctype, integration_request.reference_docname)
+
+ mpesa_receipts, completed_payments = get_completed_integration_requests_info(
+ integration_request.reference_doctype,
+ integration_request.reference_docname,
+ checkout_id
+ )
+
+ total_paid = amount + sum(completed_payments)
+ mpesa_receipts = ', '.join(mpesa_receipts + [mpesa_receipt])
+
+ if total_paid >= pr.grand_total:
+ pr.run_method("on_payment_authorized", 'Completed')
+ success = True
+
+ frappe.db.set_value("POS Invoice", pr.reference_name, "mpesa_receipt_number", mpesa_receipts)
+ integration_request.handle_success(transaction_response)
except Exception:
- request.handle_failure(transaction_response)
+ integration_request.handle_failure(transaction_response)
frappe.log_error(frappe.get_traceback())
else:
- request.handle_failure(transaction_response)
+ integration_request.handle_failure(transaction_response)
- frappe.publish_realtime('process_phone_payment', doctype="POS Invoice",
- docname=transaction_data.payment_reference, user=request.owner, message=transaction_response)
+ frappe.publish_realtime(
+ event='process_phone_payment',
+ doctype="POS Invoice",
+ docname=transaction_data.payment_reference,
+ user=integration_request.owner,
+ message={
+ 'amount': total_paid,
+ 'success': success,
+ 'failure_message': transaction_response["ResultDesc"] if transaction_response['ResultCode'] != 0 else ''
+ },
+ )
+
+def get_completed_integration_requests_info(reference_doctype, reference_docname, checkout_id):
+ output_of_other_completed_requests = frappe.get_all("Integration Request", filters={
+ 'name': ['!=', checkout_id],
+ 'reference_doctype': reference_doctype,
+ 'reference_docname': reference_docname,
+ 'status': 'Completed'
+ }, pluck="output")
+
+ mpesa_receipts, completed_payments = [], []
+
+ for out in output_of_other_completed_requests:
+ out = frappe._dict(loads(out))
+ item_response = out["CallbackMetadata"]["Item"]
+ completed_amount = fetch_param_value(item_response, "Amount", "Name")
+ completed_mpesa_receipt = fetch_param_value(item_response, "MpesaReceiptNumber", "Name")
+ completed_payments.append(completed_amount)
+ mpesa_receipts.append(completed_mpesa_receipt)
+
+ return mpesa_receipts, completed_payments
def get_account_balance(request_payload):
"""Call account balance API to send the request to the Mpesa Servers."""
diff --git a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
index 4e86d36..2948796 100644
--- a/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
+++ b/erpnext/erpnext_integrations/doctype/mpesa_settings/test_mpesa_settings.py
@@ -9,6 +9,10 @@
from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice
class TestMpesaSettings(unittest.TestCase):
+ def tearDown(self):
+ frappe.db.sql('delete from `tabMpesa Settings`')
+ frappe.db.sql('delete from `tabIntegration Request` where integration_request_service = "Mpesa"')
+
def test_creation_of_payment_gateway(self):
create_mpesa_settings(payment_gateway_name="_Test")
@@ -40,10 +44,13 @@
}
}))
+ integration_request.delete()
+
def test_processing_of_callback_payload(self):
create_mpesa_settings(payment_gateway_name="Payment")
mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
+ frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
pos_invoice = create_pos_invoice(do_not_submit=1)
pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 500})
@@ -55,10 +62,16 @@
# test payment request creation
self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
- callback_response = get_payment_callback_payload()
+ # submitting payment request creates integration requests with random id
+ integration_req_ids = frappe.get_all("Integration Request", filters={
+ 'reference_doctype': pr.doctype,
+ 'reference_docname': pr.name,
+ }, pluck="name")
+
+ callback_response = get_payment_callback_payload(Amount=500, CheckoutRequestID=integration_req_ids[0])
verify_transaction(**callback_response)
# test creation of integration request
- integration_request = frappe.get_doc("Integration Request", "ws_CO_061020201133231972")
+ integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
# test integration request creation and successful update of the status on receiving callback response
self.assertTrue(integration_request)
@@ -68,6 +81,120 @@
integration_request.reload()
self.assertEquals(pos_invoice.mpesa_receipt_number, "LGR7OWQX0R")
self.assertEquals(integration_request.status, "Completed")
+
+ frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
+ integration_request.delete()
+ pr.reload()
+ pr.cancel()
+ pr.delete()
+ pos_invoice.delete()
+
+ def test_processing_of_multiple_callback_payload(self):
+ create_mpesa_settings(payment_gateway_name="Payment")
+ mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
+ frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
+ frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
+ frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
+
+ pos_invoice = create_pos_invoice(do_not_submit=1)
+ pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000})
+ pos_invoice.contact_mobile = "093456543894"
+ pos_invoice.currency = "KES"
+ pos_invoice.save()
+
+ pr = pos_invoice.create_payment_request()
+ # test payment request creation
+ self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
+
+ # submitting payment request creates integration requests with random id
+ integration_req_ids = frappe.get_all("Integration Request", filters={
+ 'reference_doctype': pr.doctype,
+ 'reference_docname': pr.name,
+ }, pluck="name")
+
+ # create random receipt nos and send it as response to callback handler
+ mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
+
+ integration_requests = []
+ for i in range(len(integration_req_ids)):
+ callback_response = get_payment_callback_payload(
+ Amount=500,
+ CheckoutRequestID=integration_req_ids[i],
+ MpesaReceiptNumber=mpesa_receipt_numbers[i]
+ )
+ # handle response manually
+ verify_transaction(**callback_response)
+ # test completion of integration request
+ integration_request = frappe.get_doc("Integration Request", integration_req_ids[i])
+ self.assertEquals(integration_request.status, "Completed")
+ integration_requests.append(integration_request)
+
+ # check receipt number once all the integration requests are completed
+ pos_invoice.reload()
+ self.assertEquals(pos_invoice.mpesa_receipt_number, ', '.join(mpesa_receipt_numbers))
+
+ frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
+ [d.delete() for d in integration_requests]
+ pr.reload()
+ pr.cancel()
+ pr.delete()
+ pos_invoice.delete()
+
+ def test_processing_of_only_one_succes_callback_payload(self):
+ create_mpesa_settings(payment_gateway_name="Payment")
+ mpesa_account = frappe.db.get_value("Payment Gateway Account", {"payment_gateway": 'Mpesa-Payment'}, "payment_account")
+ frappe.db.set_value("Account", mpesa_account, "account_currency", "KES")
+ frappe.db.set_value("Mpesa Settings", "Payment", "transaction_limit", "500")
+ frappe.db.set_value("Customer", "_Test Customer", "default_currency", "KES")
+
+ pos_invoice = create_pos_invoice(do_not_submit=1)
+ pos_invoice.append("payments", {'mode_of_payment': 'Mpesa-Payment', 'account': mpesa_account, 'amount': 1000})
+ pos_invoice.contact_mobile = "093456543894"
+ pos_invoice.currency = "KES"
+ pos_invoice.save()
+
+ pr = pos_invoice.create_payment_request()
+ # test payment request creation
+ self.assertEquals(pr.payment_gateway, "Mpesa-Payment")
+
+ # submitting payment request creates integration requests with random id
+ integration_req_ids = frappe.get_all("Integration Request", filters={
+ 'reference_doctype': pr.doctype,
+ 'reference_docname': pr.name,
+ }, pluck="name")
+
+ # create random receipt nos and send it as response to callback handler
+ mpesa_receipt_numbers = [frappe.utils.random_string(5) for d in integration_req_ids]
+
+ callback_response = get_payment_callback_payload(
+ Amount=500,
+ CheckoutRequestID=integration_req_ids[0],
+ MpesaReceiptNumber=mpesa_receipt_numbers[0]
+ )
+ # handle response manually
+ verify_transaction(**callback_response)
+ # test completion of integration request
+ integration_request = frappe.get_doc("Integration Request", integration_req_ids[0])
+ self.assertEquals(integration_request.status, "Completed")
+
+ # now one request is completed
+ # second integration request fails
+ # now retrying payment request should make only one integration request again
+ pr = pos_invoice.create_payment_request()
+ new_integration_req_ids = frappe.get_all("Integration Request", filters={
+ 'reference_doctype': pr.doctype,
+ 'reference_docname': pr.name,
+ 'name': ['not in', integration_req_ids]
+ }, pluck="name")
+
+ self.assertEquals(len(new_integration_req_ids), 1)
+
+ frappe.db.set_value("Customer", "_Test Customer", "default_currency", "")
+ frappe.db.sql("delete from `tabIntegration Request` where integration_request_service = 'Mpesa'")
+ pr.reload()
+ pr.cancel()
+ pr.delete()
+ pos_invoice.delete()
def create_mpesa_settings(payment_gateway_name="Express"):
if frappe.db.exists("Mpesa Settings", payment_gateway_name):
@@ -157,16 +284,19 @@
}
}
-def get_payment_request_response_payload():
+def get_payment_request_response_payload(Amount=500):
"""Response received after successfully calling the stk push process request API."""
+
+ CheckoutRequestID = frappe.utils.random_string(10)
+
return {
"MerchantRequestID": "8071-27184008-1",
- "CheckoutRequestID": "ws_CO_061020201133231972",
+ "CheckoutRequestID": CheckoutRequestID,
"ResultCode": 0,
"ResultDesc": "The service request is processed successfully.",
"CallbackMetadata": {
"Item": [
- { "Name": "Amount", "Value": 500.0 },
+ { "Name": "Amount", "Value": Amount },
{ "Name": "MpesaReceiptNumber", "Value": "LGR7OWQX0R" },
{ "Name": "TransactionDate", "Value": 20201006113336 },
{ "Name": "PhoneNumber", "Value": 254723575670 }
@@ -174,41 +304,26 @@
}
}
-
-def get_payment_callback_payload():
+def get_payment_callback_payload(Amount=500, CheckoutRequestID="ws_CO_061020201133231972", MpesaReceiptNumber="LGR7OWQX0R"):
"""Response received from the server as callback after calling the stkpush process request API."""
return {
"Body":{
- "stkCallback":{
- "MerchantRequestID":"19465-780693-1",
- "CheckoutRequestID":"ws_CO_061020201133231972",
- "ResultCode":0,
- "ResultDesc":"The service request is processed successfully.",
- "CallbackMetadata":{
- "Item":[
- {
- "Name":"Amount",
- "Value":500
- },
- {
- "Name":"MpesaReceiptNumber",
- "Value":"LGR7OWQX0R"
- },
- {
- "Name":"Balance"
- },
- {
- "Name":"TransactionDate",
- "Value":20170727154800
- },
- {
- "Name":"PhoneNumber",
- "Value":254721566839
+ "stkCallback":{
+ "MerchantRequestID":"19465-780693-1",
+ "CheckoutRequestID":CheckoutRequestID,
+ "ResultCode":0,
+ "ResultDesc":"The service request is processed successfully.",
+ "CallbackMetadata":{
+ "Item":[
+ { "Name":"Amount", "Value":Amount },
+ { "Name":"MpesaReceiptNumber", "Value":MpesaReceiptNumber },
+ { "Name":"Balance" },
+ { "Name":"TransactionDate", "Value":20170727154800 },
+ { "Name":"PhoneNumber", "Value":254721566839 }
+ ]
}
- ]
}
}
- }
}
def get_account_balance_callback_payload():
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
index 8d4b510..5f990cd 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_connector.py
@@ -20,7 +20,7 @@
client_id=self.settings.plaid_client_id,
secret=self.settings.get_password("plaid_secret"),
environment=self.settings.plaid_env,
- api_version="2019-05-29"
+ api_version="2020-09-14"
)
def get_access_token(self, public_token):
@@ -30,13 +30,10 @@
access_token = response["access_token"]
return access_token
- def get_link_token(self):
+ def get_token_request(self, update_mode=False):
country_codes = ["US", "CA", "FR", "IE", "NL", "ES", "GB"] if self.settings.enable_european_access else ["US", "CA"]
- token_request = {
+ args = {
"client_name": self.client_name,
- "client_id": self.settings.plaid_client_id,
- "secret": self.settings.plaid_secret,
- "products": self.products,
# only allow Plaid-supported languages and countries (LAST: Sep-19-2020)
"language": frappe.local.lang if frappe.local.lang in ["en", "fr", "es", "nl"] else "en",
"country_codes": country_codes,
@@ -45,6 +42,20 @@
}
}
+ if update_mode:
+ args["access_token"] = self.access_token
+ else:
+ args.update({
+ "client_id": self.settings.plaid_client_id,
+ "secret": self.settings.plaid_secret,
+ "products": self.products,
+ })
+
+ return args
+
+ def get_link_token(self, update_mode=False):
+ token_request = self.get_token_request(update_mode)
+
try:
response = self.client.LinkToken.create(token_request)
except InvalidRequestError:
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
index 22a4004..bbc2ca8 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.js
@@ -12,9 +12,25 @@
refresh: function (frm) {
if (frm.doc.enabled) {
- frm.add_custom_button('Link a new bank account', () => {
+ frm.add_custom_button(__('Link a new bank account'), () => {
new erpnext.integrations.plaidLink(frm);
});
+
+ frm.add_custom_button(__("Sync Now"), () => {
+ frappe.call({
+ method: "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.enqueue_synchronization",
+ freeze: true,
+ callback: () => {
+ let bank_transaction_link = '<a href="#List/Bank Transaction">Bank Transaction</a>';
+
+ frappe.msgprint({
+ title: __("Sync Started"),
+ message: __("The sync has started in the background, please check the {0} list for new records.", [bank_transaction_link]),
+ alert: 1
+ });
+ }
+ });
+ }).addClass("btn-primary");
}
}
});
@@ -30,10 +46,18 @@
this.product = ["auth", "transactions"];
this.plaid_env = this.frm.doc.plaid_env;
this.client_name = frappe.boot.sitename;
- this.token = await this.frm.call("get_link_token").then(resp => resp.message);
+ this.token = await this.get_link_token();
this.init_plaid();
}
+ async get_link_token() {
+ const token = await this.frm.call("get_link_token").then(resp => resp.message);
+ if (!token) {
+ frappe.throw(__('Cannot retrieve link token. Check Error Log for more information'));
+ }
+ return token;
+ }
+
init_plaid() {
const me = this;
me.loadScript(me.plaidUrl)
@@ -78,8 +102,8 @@
}
onScriptError(error) {
- frappe.msgprint("There was an issue connecting to Plaid's authentication server");
- frappe.msgprint(error);
+ frappe.msgprint(__("There was an issue connecting to Plaid's authentication server. Check browser console for more information"));
+ console.log(error);
}
plaid_success(token, response) {
@@ -107,4 +131,4 @@
});
}, __("Select a company"), __("Continue"));
}
-};
+};
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
index e535e81..21f6fee 100644
--- a/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
+++ b/erpnext/erpnext_integrations/doctype/plaid_settings/plaid_settings.py
@@ -166,7 +166,6 @@
related_bank = frappe.db.get_values("Bank Account", bank_account, ["bank", "integration_id"], as_dict=True)
access_token = frappe.db.get_value("Bank", related_bank[0].bank, "plaid_access_token")
account_id = related_bank[0].integration_id
-
else:
access_token = frappe.db.get_value("Bank", bank, "plaid_access_token")
account_id = None
@@ -205,8 +204,8 @@
"date": getdate(transaction["date"]),
"status": status,
"bank_account": bank_account,
- "debit": debit,
- "credit": credit,
+ "deposit": debit,
+ "withdrawal": credit,
"currency": transaction["iso_currency_code"],
"transaction_id": transaction["transaction_id"],
"reference_number": transaction["payment_meta"]["reference_number"],
@@ -228,13 +227,23 @@
def automatic_synchronization():
settings = frappe.get_doc("Plaid Settings", "Plaid Settings")
-
if settings.enabled == 1 and settings.automatic_sync == 1:
- plaid_accounts = frappe.get_all("Bank Account", filters={"integration_id": ["!=", ""]}, fields=["name", "bank"])
+ enqueue_synchronization()
- for plaid_account in plaid_accounts:
- frappe.enqueue(
- "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
- bank=plaid_account.bank,
- bank_account=plaid_account.name
- )
+@frappe.whitelist()
+def enqueue_synchronization():
+ plaid_accounts = frappe.get_all("Bank Account",
+ filters={"integration_id": ["!=", ""]},
+ fields=["name", "bank"])
+
+ for plaid_account in plaid_accounts:
+ frappe.enqueue(
+ "erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.sync_transactions",
+ bank=plaid_account.bank,
+ bank_account=plaid_account.name
+ )
+
+@frappe.whitelist()
+def get_link_token_for_update(access_token):
+ plaid = PlaidConnector(access_token)
+ return plaid.get_link_token(update_mode=True)
diff --git a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js
index fd16d1e..5482b9c 100644
--- a/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js
+++ b/erpnext/erpnext_integrations/doctype/tally_migration/tally_migration.js
@@ -23,10 +23,10 @@
frappe.msgprint({
message: __("An error has occurred during {0}. Check {1} for more details",
[
- repl("<a href='#Form/Tally Migration/%(tally_document)s' class='variant-click'>%(tally_document)s</a>", {
+ repl("<a href='/app/tally-migration/%(tally_document)s' class='variant-click'>%(tally_document)s</a>", {
tally_document: frm.docname
}),
- "<a href='#List/Error Log' class='variant-click'>Error Log</a>"
+ "<a href='/app/error-log' class='variant-click'>Error Log</a>"
]
),
title: __("Tally Migration Error"),
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
new file mode 100644
index 0000000..4a5e54e
--- /dev/null
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations/erpnext_integrations.json
@@ -0,0 +1,116 @@
+{
+ "category": "Modules",
+ "charts": [],
+ "creation": "2020-08-20 19:30:48.138801",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends": "Integrations",
+ "extends_another_page": 1,
+ "hide_custom": 1,
+ "idx": 0,
+ "is_standard": 1,
+ "label": "ERPNext Integrations",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Marketplace",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Woocommerce Settings",
+ "link_to": "Woocommerce Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Amazon MWS Settings",
+ "link_to": "Amazon MWS Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shopify Settings",
+ "link_to": "Shopify Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Payments",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "GoCardless Settings",
+ "link_to": "GoCardless Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "M-Pesa Settings",
+ "link_to": "Mpesa Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Plaid Settings",
+ "link_to": "Plaid Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Exotel Settings",
+ "link_to": "Exotel Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:35.846528",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "ERPNext Integrations",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": []
+}
\ No newline at end of file
diff --git a/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json
new file mode 100644
index 0000000..d258d57
--- /dev/null
+++ b/erpnext/erpnext_integrations/workspace/erpnext_integrations_settings/erpnext_integrations_settings.json
@@ -0,0 +1,82 @@
+{
+ "category": "Modules",
+ "charts": [],
+ "creation": "2020-07-31 10:38:54.021237",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends": "Settings",
+ "extends_another_page": 1,
+ "hide_custom": 0,
+ "idx": 0,
+ "is_standard": 1,
+ "label": "ERPNext Integrations Settings",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Integrations Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Woocommerce Settings",
+ "link_to": "Woocommerce Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shopify Settings",
+ "link_to": "Shopify Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Amazon MWS Settings",
+ "link_to": "Amazon MWS Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Plaid Settings",
+ "link_to": "Plaid Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Exotel Settings",
+ "link_to": "Exotel Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:34.732552",
+ "modified_by": "Administrator",
+ "module": "ERPNext Integrations",
+ "name": "ERPNext Integrations Settings",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": []
+}
\ No newline at end of file
diff --git a/erpnext/exceptions.py b/erpnext/exceptions.py
index d92af5d..04291cd 100644
--- a/erpnext/exceptions.py
+++ b/erpnext/exceptions.py
@@ -6,3 +6,5 @@
class InvalidAccountCurrency(frappe.ValidationError): pass
class InvalidCurrency(frappe.ValidationError): pass
class PartyDisabled(frappe.ValidationError):pass
+class InvalidAccountDimensionError(frappe.ValidationError): pass
+class MandatoryAccountDimensionError(frappe.ValidationError): pass
diff --git a/erpnext/healthcare/desk_page/healthcare/healthcare.json b/erpnext/healthcare/desk_page/healthcare/healthcare.json
index 81d6048..af601f3 100644
--- a/erpnext/healthcare/desk_page/healthcare/healthcare.json
+++ b/erpnext/healthcare/desk_page/healthcare/healthcare.json
@@ -32,13 +32,18 @@
},
{
"hidden": 0,
+ "label": "Inpatient",
+ "links": "[\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Medication Order\",\n\t\t\"label\": \"Inpatient Medication Order\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Medication Entry\",\n\t\t\"label\": \"Inpatient Medication Entry\"\n\t}\n]"
+ },
+ {
+ "hidden": 0,
"label": "Rehabilitation and Physiotherapy",
"links": "[\n {\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Exercise Type\",\n\t\t\"label\": \"Exercise Type\",\n\t\t\"onboard\": 1\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Therapy Type\",\n\t\t\"label\": \"Therapy Type\",\n\t\t\"onboard\": 1\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Therapy Plan\",\n\t\t\"label\": \"Therapy Plan\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Therapy Session\",\n\t\t\"label\": \"Therapy Session\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Assessment Template\",\n\t\t\"label\": \"Patient Assessment Template\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Assessment\",\n\t\t\"label\": \"Patient Assessment\"\n\t}\n]"
},
{
"hidden": 0,
"label": "Records and History",
- "links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient-progress\",\n\t\t\"label\": \"Patient Progress\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Inpatient Record\",\n\t\t\"label\": \"Inpatient Record\"\n\t}\n]"
+ "links": "[\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient_history\",\n\t\t\"label\": \"Patient History\"\n\t},\n\t{\n\t\t\"type\": \"page\",\n\t\t\"name\": \"patient-progress\",\n\t\t\"label\": \"Patient Progress\"\n\t},\n\t{\n\t\t\"type\": \"doctype\",\n\t\t\"name\": \"Patient Medical Record\",\n\t\t\"label\": \"Patient Medical Record\"\n\t}\n]"
},
{
"hidden": 0,
@@ -64,7 +69,7 @@
"idx": 0,
"is_standard": 1,
"label": "Healthcare",
- "modified": "2020-11-23 23:00:48.764377",
+ "modified": "2020-11-26 22:09:09.164584",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare",
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.js b/erpnext/healthcare/doctype/appointment_type/appointment_type.js
index 15916a5..861675a 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.js
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.js
@@ -2,4 +2,82 @@
// For license information, please see license.txt
frappe.ui.form.on('Appointment Type', {
+ refresh: function(frm) {
+ frm.set_query('price_list', function() {
+ return {
+ filters: {'selling': 1}
+ };
+ });
+
+ frm.set_query('medical_department', 'items', function(doc) {
+ let item_list = doc.items.map(({medical_department}) => medical_department);
+ return {
+ filters: [
+ ['Medical Department', 'name', 'not in', item_list]
+ ]
+ };
+ });
+
+ frm.set_query('op_consulting_charge_item', 'items', function() {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
+
+ frm.set_query('inpatient_visit_charge_item', 'items', function() {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
+ }
});
+
+frappe.ui.form.on('Appointment Type Service Item', {
+ op_consulting_charge_item: function(frm, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ if (frm.doc.price_list && d.op_consulting_charge_item) {
+ frappe.call({
+ 'method': 'frappe.client.get_value',
+ args: {
+ 'doctype': 'Item Price',
+ 'filters': {
+ 'item_code': d.op_consulting_charge_item,
+ 'price_list': frm.doc.price_list
+ },
+ 'fieldname': ['price_list_rate']
+ },
+ callback: function(data) {
+ if (data.message.price_list_rate) {
+ frappe.model.set_value(cdt, cdn, 'op_consulting_charge', data.message.price_list_rate);
+ }
+ }
+ });
+ }
+ },
+
+ inpatient_visit_charge_item: function(frm, cdt, cdn) {
+ let d = locals[cdt][cdn];
+ if (frm.doc.price_list && d.inpatient_visit_charge_item) {
+ frappe.call({
+ 'method': 'frappe.client.get_value',
+ args: {
+ 'doctype': 'Item Price',
+ 'filters': {
+ 'item_code': d.inpatient_visit_charge_item,
+ 'price_list': frm.doc.price_list
+ },
+ 'fieldname': ['price_list_rate']
+ },
+ callback: function (data) {
+ if (data.message.price_list_rate) {
+ frappe.model.set_value(cdt, cdn, 'inpatient_visit_charge', data.message.price_list_rate);
+ }
+ }
+ });
+ }
+ }
+});
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.json b/erpnext/healthcare/doctype/appointment_type/appointment_type.json
index 58753bb..3872318 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.json
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.json
@@ -12,7 +12,10 @@
"appointment_type",
"ip",
"default_duration",
- "color"
+ "color",
+ "billing_section",
+ "price_list",
+ "items"
],
"fields": [
{
@@ -52,10 +55,27 @@
"label": "Color",
"no_copy": 1,
"report_hide": 1
+ },
+ {
+ "fieldname": "billing_section",
+ "fieldtype": "Section Break",
+ "label": "Billing"
+ },
+ {
+ "fieldname": "price_list",
+ "fieldtype": "Link",
+ "label": "Price List",
+ "options": "Price List"
+ },
+ {
+ "fieldname": "items",
+ "fieldtype": "Table",
+ "label": "Appointment Type Service Items",
+ "options": "Appointment Type Service Item"
}
],
"links": [],
- "modified": "2020-02-03 21:06:05.833050",
+ "modified": "2021-01-22 09:41:05.010524",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Appointment Type",
diff --git a/erpnext/healthcare/doctype/appointment_type/appointment_type.py b/erpnext/healthcare/doctype/appointment_type/appointment_type.py
index 1dacffa..67a24f3 100644
--- a/erpnext/healthcare/doctype/appointment_type/appointment_type.py
+++ b/erpnext/healthcare/doctype/appointment_type/appointment_type.py
@@ -4,6 +4,53 @@
from __future__ import unicode_literals
from frappe.model.document import Document
+import frappe
class AppointmentType(Document):
- pass
+ def validate(self):
+ if self.items and self.price_list:
+ for item in self.items:
+ existing_op_item_price = frappe.db.exists('Item Price', {
+ 'item_code': item.op_consulting_charge_item,
+ 'price_list': self.price_list
+ })
+
+ if not existing_op_item_price and item.op_consulting_charge_item and item.op_consulting_charge:
+ make_item_price(self.price_list, item.op_consulting_charge_item, item.op_consulting_charge)
+
+ existing_ip_item_price = frappe.db.exists('Item Price', {
+ 'item_code': item.inpatient_visit_charge_item,
+ 'price_list': self.price_list
+ })
+
+ if not existing_ip_item_price and item.inpatient_visit_charge_item and item.inpatient_visit_charge:
+ make_item_price(self.price_list, item.inpatient_visit_charge_item, item.inpatient_visit_charge)
+
+@frappe.whitelist()
+def get_service_item_based_on_department(appointment_type, department):
+ item_list = frappe.db.get_value('Appointment Type Service Item',
+ filters = {'medical_department': department, 'parent': appointment_type},
+ fieldname = ['op_consulting_charge_item',
+ 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
+ as_dict = 1
+ )
+
+ # if department wise items are not set up
+ # use the generic items
+ if not item_list:
+ item_list = frappe.db.get_value('Appointment Type Service Item',
+ filters = {'parent': appointment_type},
+ fieldname = ['op_consulting_charge_item',
+ 'inpatient_visit_charge_item', 'op_consulting_charge', 'inpatient_visit_charge'],
+ as_dict = 1
+ )
+
+ return item_list
+
+def make_item_price(price_list, item, item_price):
+ frappe.get_doc({
+ 'doctype': 'Item Price',
+ 'price_list': price_list,
+ 'item_code': item,
+ 'price_list_rate': item_price
+ }).insert(ignore_permissions=True, ignore_mandatory=True)
diff --git a/erpnext/accounts/doctype/bank_statement_settings_item/__init__.py b/erpnext/healthcare/doctype/appointment_type_service_item/__init__.py
similarity index 100%
rename from erpnext/accounts/doctype/bank_statement_settings_item/__init__.py
rename to erpnext/healthcare/doctype/appointment_type_service_item/__init__.py
diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json
new file mode 100644
index 0000000..5ff68cd
--- /dev/null
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.json
@@ -0,0 +1,67 @@
+{
+ "actions": [],
+ "creation": "2021-01-22 09:34:53.373105",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "medical_department",
+ "op_consulting_charge_item",
+ "op_consulting_charge",
+ "column_break_4",
+ "inpatient_visit_charge_item",
+ "inpatient_visit_charge"
+ ],
+ "fields": [
+ {
+ "fieldname": "medical_department",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Medical Department",
+ "options": "Medical Department"
+ },
+ {
+ "fieldname": "op_consulting_charge_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Out Patient Consulting Charge Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "op_consulting_charge",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Out Patient Consulting Charge"
+ },
+ {
+ "fieldname": "column_break_4",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "inpatient_visit_charge_item",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Inpatient Visit Charge Item",
+ "options": "Item"
+ },
+ {
+ "fieldname": "inpatient_visit_charge",
+ "fieldtype": "Currency",
+ "in_list_view": 1,
+ "label": "Inpatient Visit Charge Item"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-01-22 09:35:26.503443",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Appointment Type Service Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
new file mode 100644
index 0000000..b2e0e82
--- /dev/null
+++ b/erpnext/healthcare/doctype/appointment_type_service_item/appointment_type_service_item.py
@@ -0,0 +1,10 @@
+# -*- 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.model.document import Document
+
+class AppointmentTypeServiceItem(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
index 1d4411d..ff51646 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.js
@@ -85,7 +85,7 @@
callback: function(r) {
if (r.message) {
frappe.show_alert({
- message: __('Stock Entry {0} created', ['<a class="bold" href="#Form/Stock Entry/'+ r.message + '">' + r.message + '</a>']),
+ message: __('Stock Entry {0} created', ['<a class="bold" href="/app/stock-entry/'+ r.message + '">' + r.message + '</a>']),
indicator: 'green'
});
}
diff --git a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
index e55a143..325c209 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
+++ b/erpnext/healthcare/doctype/clinical_procedure/clinical_procedure.py
@@ -100,7 +100,6 @@
allow_start = self.set_actual_qty()
if allow_start:
self.db_set('status', 'In Progress')
- insert_clinical_procedure_to_medical_record(self)
return 'success'
return 'insufficient stock'
@@ -122,6 +121,7 @@
stock_entry.stock_entry_type = 'Material Receipt'
stock_entry.to_warehouse = self.warehouse
+ stock_entry.company = self.company
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', self.company)
for item in self.items:
if item.qty > item.actual_qty:
@@ -247,21 +247,3 @@
}, target_doc, set_missing_values)
return doc
-
-
-def insert_clinical_procedure_to_medical_record(doc):
- subject = frappe.bold(_("Clinical Procedure conducted: ")) + cstr(doc.procedure_template) + "<br>"
- if doc.practitioner:
- subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner
- if subject and doc.notes:
- subject += '<br/>' + doc.notes
-
- medical_record = frappe.new_doc('Patient Medical Record')
- medical_record.patient = doc.patient
- medical_record.subject = subject
- medical_record.status = 'Open'
- medical_record.communication_date = doc.start_date
- medical_record.reference_doctype = 'Clinical Procedure'
- medical_record.reference_name = doc.name
- medical_record.reference_owner = doc.owner
- medical_record.save(ignore_permissions=True)
diff --git a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py
index 4ee5f6b..fb72073 100644
--- a/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py
+++ b/erpnext/healthcare/doctype/clinical_procedure/test_clinical_procedure.py
@@ -1,4 +1,4 @@
-# -*- coding: utf-8 -*-
+ # -*- coding: utf-8 -*-
# Copyright (c) 2017, ESS LLP and Contributors
# See license.txt
from __future__ import unicode_literals
@@ -60,6 +60,7 @@
procedure.practitioner = practitioner
procedure.consume_stock = procedure_template.allow_stock_consumption
procedure.items = procedure_template.items
- procedure.warehouse = frappe.db.get_single_value('Stock Settings', 'default_warehouse')
+ procedure.company = "_Test Company"
+ procedure.warehouse = "_Test Warehouse - _TC"
procedure.submit()
return procedure
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/exercise_type/exercise_type.js b/erpnext/healthcare/doctype/exercise_type/exercise_type.js
index 68db047..b49b00e 100644
--- a/erpnext/healthcare/doctype/exercise_type/exercise_type.js
+++ b/erpnext/healthcare/doctype/exercise_type/exercise_type.js
@@ -71,7 +71,7 @@
$('.btn-del').on('click', function() {
let id = $(this).attr('data-id');
- $('#card-'+id).addClass("zoomOutDelete");
+ $('#card-'+id).addClass("zoom-out");
setTimeout(() => {
// not using grid_rows[id].remove because
diff --git a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py
index cdf692e..7e7fd82 100644
--- a/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py
+++ b/erpnext/healthcare/doctype/fee_validity/test_fee_validity.py
@@ -7,6 +7,7 @@
import unittest
from frappe.utils import nowdate, add_days
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_appointment, create_healthcare_service_items
+from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
test_dependencies = ["Company"]
@@ -15,6 +16,7 @@
frappe.db.sql("""delete from `tabPatient Appointment`""")
frappe.db.sql("""delete from `tabFee Validity`""")
frappe.db.sql("""delete from `tabPatient`""")
+ make_pos_profile()
def test_fee_validity(self):
item = create_healthcare_service_items()
diff --git a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
index cb747f9..8162f03 100644
--- a/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
+++ b/erpnext/healthcare/doctype/healthcare_practitioner/healthcare_practitioner.json
@@ -159,6 +159,7 @@
"fieldname": "op_consulting_charge",
"fieldtype": "Currency",
"label": "Out Patient Consulting Charge",
+ "mandatory_depends_on": "op_consulting_charge_item",
"options": "Currency"
},
{
@@ -174,7 +175,8 @@
{
"fieldname": "inpatient_visit_charge",
"fieldtype": "Currency",
- "label": "Inpatient Visit Charge"
+ "label": "Inpatient Visit Charge",
+ "mandatory_depends_on": "inpatient_visit_charge_item"
},
{
"depends_on": "eval: !doc.__islocal",
@@ -280,7 +282,7 @@
],
"image_field": "image",
"links": [],
- "modified": "2020-04-06 13:44:24.759623",
+ "modified": "2021-01-22 10:14:43.187675",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Practitioner",
diff --git a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js
index a03b579..b75f271 100644
--- a/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js
+++ b/erpnext/healthcare/doctype/healthcare_service_unit/healthcare_service_unit_tree.js
@@ -12,20 +12,20 @@
get_tree_nodes: 'erpnext.healthcare.utils.get_children',
ignore_fields:["parent_healthcare_service_unit"],
onrender: function(node) {
- if (node.data.occupied_out_of_vacant!==undefined){
- $('<span class="balance-area pull-right text-muted small">'
+ if (node.data.occupied_out_of_vacant!==undefined) {
+ $('<span class="balance-area pull-right">'
+ " " + node.data.occupied_out_of_vacant
+ '</span>').insertBefore(node.$ul);
}
if (node.data && node.data.inpatient_occupancy!==undefined) {
- if (node.data.inpatient_occupancy == 1){
- if (node.data.occupancy_status == "Occupied"){
- $('<span class="balance-area pull-right small">'
+ if (node.data.inpatient_occupancy == 1) {
+ if (node.data.occupancy_status == "Occupied") {
+ $('<span class="balance-area pull-right">'
+ " " + node.data.occupancy_status
+ '</span>').insertBefore(node.$ul);
}
- if (node.data.occupancy_status == "Vacant"){
- $('<span class="balance-area pull-right text-muted small">'
+ if (node.data.occupancy_status == "Vacant") {
+ $('<span class="balance-area pull-right">'
+ " " + node.data.occupancy_status
+ '</span>').insertBefore(node.$ul);
}
diff --git a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json
index 0104386..ddf1bce 100644
--- a/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json
+++ b/erpnext/healthcare/doctype/healthcare_settings/healthcare_settings.json
@@ -17,6 +17,9 @@
"enable_free_follow_ups",
"max_visits",
"valid_days",
+ "inpatient_settings_section",
+ "allow_discharge_despite_unbilled_services",
+ "do_not_bill_inpatient_encounters",
"healthcare_service_items",
"inpatient_visit_charge_item",
"op_consulting_charge_item",
@@ -302,11 +305,28 @@
"fieldname": "enable_free_follow_ups",
"fieldtype": "Check",
"label": "Enable Free Follow-ups"
+ },
+ {
+ "fieldname": "inpatient_settings_section",
+ "fieldtype": "Section Break",
+ "label": "Inpatient Settings"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_discharge_despite_unbilled_services",
+ "fieldtype": "Check",
+ "label": "Allow Discharge Despite Unbilled Healthcare Services"
+ },
+ {
+ "default": "0",
+ "fieldname": "do_not_bill_inpatient_encounters",
+ "fieldtype": "Check",
+ "label": "Do Not Bill Patient Encounters for Inpatients"
}
],
"issingle": 1,
"links": [],
- "modified": "2020-07-08 15:17:21.543218",
+ "modified": "2021-01-13 09:04:35.877700",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Healthcare Settings",
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js
index f523cf2..a7b06b1 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.js
@@ -5,6 +5,7 @@
refresh: function(frm) {
// Ignore cancellation of doctype on cancel all
frm.ignore_doctypes_on_cancel_all = ['Stock Entry'];
+ frm.fields_dict['medication_orders'].grid.wrapper.find('.grid-add-row').hide();
frm.set_query('item_code', () => {
return {
@@ -29,6 +30,29 @@
}
};
});
+
+ if (frm.doc.__islocal || frm.doc.docstatus !== 0 || !frm.doc.update_stock)
+ return;
+
+ frm.add_custom_button(__('Make Stock Entry'), function() {
+ frappe.call({
+ method: 'erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry.make_difference_stock_entry',
+ args: { docname: frm.doc.name },
+ freeze: true,
+ callback: function(r) {
+ if (r.message) {
+ var doclist = frappe.model.sync(r.message);
+ frappe.set_route('Form', doclist[0].doctype, doclist[0].name);
+ } else {
+ frappe.msgprint({
+ title: __('No Drug Shortage'),
+ message: __('All the drugs are available with sufficient qty to process this Inpatient Medication Entry.'),
+ indicator: 'green'
+ });
+ }
+ }
+ });
+ });
},
patient: function(frm) {
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json
index dd4c423..b1a6ee4 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.json
@@ -139,7 +139,6 @@
"fieldtype": "Table",
"label": "Inpatient Medication Orders",
"options": "Inpatient Medication Entry Detail",
- "read_only": 1,
"reqd": 1
},
{
@@ -180,7 +179,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-03 13:22:37.820707",
+ "modified": "2021-01-11 12:37:46.749659",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Inpatient Medication Entry",
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
index 5dac23a..e731908 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/inpatient_medication_entry.py
@@ -15,8 +15,6 @@
self.validate_medication_orders()
def get_medication_orders(self):
- self.validate_datetime_filters()
-
# pull inpatient medication orders based on selected filters
orders = get_pending_medication_orders(self)
@@ -27,22 +25,6 @@
self.set('medication_orders', [])
frappe.msgprint(_('No pending medication orders found for selected criteria'))
- def validate_datetime_filters(self):
- if self.from_date and self.to_date:
- self.validate_from_to_dates('from_date', 'to_date')
-
- if self.from_date and getdate(self.from_date) > getdate():
- frappe.throw(_('From Date cannot be after the current date.'))
-
- if self.to_date and getdate(self.to_date) > getdate():
- frappe.throw(_('To Date cannot be after the current date.'))
-
- if self.from_time and self.from_time > nowtime():
- frappe.throw(_('From Time cannot be after the current time.'))
-
- if self.to_time and self.to_time > nowtime():
- frappe.throw(_('To Time cannot be after the current time.'))
-
def add_mo_to_table(self, orders):
# Add medication orders in the child table
self.set('medication_orders', [])
@@ -142,25 +124,32 @@
return orders, order_entry_map
def check_stock_qty(self):
- from erpnext.stock.stock_ledger import NegativeStockError
+ drug_shortage = get_drug_shortage_map(self.medication_orders, self.warehouse)
- drug_availability = dict()
- for d in self.medication_orders:
- if not drug_availability.get(d.drug_code):
- drug_availability[d.drug_code] = 0
- drug_availability[d.drug_code] += flt(d.dosage)
+ if drug_shortage:
+ message = _('Quantity not available for the following items in warehouse {0}. ').format(frappe.bold(self.warehouse))
+ message += _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.')
- for drug, dosage in drug_availability.items():
- available_qty = get_latest_stock_qty(drug, self.warehouse)
+ formatted_item_rows = ''
- # validate qty
- if flt(available_qty) < flt(dosage):
- frappe.throw(_('Quantity not available for {0} in warehouse {1}').format(
- frappe.bold(drug), frappe.bold(self.warehouse))
- + '<br><br>' + _('Available quantity is {0}, you need {1}').format(
- frappe.bold(available_qty), frappe.bold(dosage))
- + '<br><br>' + _('Please enable Allow Negative Stock in Stock Settings or create Stock Entry to proceed.'),
- NegativeStockError, title=_('Insufficient Stock'))
+ for drug, shortage_qty in drug_shortage.items():
+ item_link = get_link_to_form('Item', drug)
+ formatted_item_rows += """
+ <td>{0}</td>
+ <td>{1}</td>
+ </tr>""".format(item_link, frappe.bold(shortage_qty))
+
+ message += """
+ <table class='table'>
+ <thead>
+ <th>{0}</th>
+ <th>{1}</th>
+ </thead>
+ {2}
+ </table>
+ """.format(_('Drug Code'), _('Shortage Qty'), formatted_item_rows)
+
+ frappe.throw(message, title=_('Insufficient Stock'), is_minimizable=True, wide=True)
def make_stock_entry(self):
stock_entry = frappe.new_doc('Stock Entry')
@@ -223,7 +212,8 @@
for doc in data:
inpatient_record = doc.inpatient_record
- doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record)
+ if inpatient_record:
+ doc['service_unit'] = get_current_healthcare_service_unit(inpatient_record)
if entry.service_unit and doc.service_unit != entry.service_unit:
to_remove.append(doc)
@@ -274,6 +264,57 @@
def get_current_healthcare_service_unit(inpatient_record):
ip_record = frappe.get_doc('Inpatient Record', inpatient_record)
- if ip_record.inpatient_occupancies:
+ if ip_record.status in ['Admitted', 'Discharge Scheduled'] and ip_record.inpatient_occupancies:
return ip_record.inpatient_occupancies[-1].service_unit
- return
\ No newline at end of file
+ return
+
+
+def get_drug_shortage_map(medication_orders, warehouse):
+ """
+ Returns a dict like { drug_code: shortage_qty }
+ """
+ drug_requirement = dict()
+ for d in medication_orders:
+ if not drug_requirement.get(d.drug_code):
+ drug_requirement[d.drug_code] = 0
+ drug_requirement[d.drug_code] += flt(d.dosage)
+
+ drug_shortage = dict()
+ for drug, required_qty in drug_requirement.items():
+ available_qty = get_latest_stock_qty(drug, warehouse)
+ if flt(required_qty) > flt(available_qty):
+ drug_shortage[drug] = flt(flt(required_qty) - flt(available_qty))
+
+ return drug_shortage
+
+
+@frappe.whitelist()
+def make_difference_stock_entry(docname):
+ doc = frappe.get_doc('Inpatient Medication Entry', docname)
+ drug_shortage = get_drug_shortage_map(doc.medication_orders, doc.warehouse)
+
+ if not drug_shortage:
+ return None
+
+ stock_entry = frappe.new_doc('Stock Entry')
+ stock_entry.purpose = 'Material Transfer'
+ stock_entry.set_stock_entry_type()
+ stock_entry.to_warehouse = doc.warehouse
+ stock_entry.company = doc.company
+ cost_center = frappe.get_cached_value('Company', doc.company, 'cost_center')
+ expense_account = get_account(None, 'expense_account', 'Healthcare Settings', doc.company)
+
+ for drug, shortage_qty in drug_shortage.items():
+ se_child = stock_entry.append('items')
+ se_child.item_code = drug
+ se_child.item_name = frappe.db.get_value('Item', drug, 'stock_uom')
+ se_child.uom = frappe.db.get_value('Item', drug, 'stock_uom')
+ se_child.stock_uom = se_child.uom
+ se_child.qty = flt(shortage_qty)
+ se_child.t_warehouse = doc.warehouse
+ # in stock uom
+ se_child.conversion_factor = 1
+ se_child.cost_center = cost_center
+ se_child.expense_account = expense_account
+
+ return stock_entry
diff --git a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py
index 2f1bb6b..7cb5a48 100644
--- a/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py
+++ b/erpnext/healthcare/doctype/inpatient_medication_entry/test_inpatient_medication_entry.py
@@ -9,6 +9,7 @@
from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import create_patient, create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
from erpnext.healthcare.doctype.inpatient_medication_order.test_inpatient_medication_order import create_ipmo, create_ipme
+from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_drug_shortage_map, make_difference_stock_entry
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_account
class TestInpatientMedicationEntry(unittest.TestCase):
@@ -82,6 +83,39 @@
self.assertEqual(stock_entry.items[0].patient, self.patient)
self.assertEqual(stock_entry.items[0].inpatient_medication_entry_child, ipme.medication_orders[0].name)
+ def test_drug_shortage_stock_entry(self):
+ ipmo = create_ipmo(self.patient)
+ ipmo.submit()
+ ipmo.reload()
+
+ date = add_days(getdate(), -1)
+ filters = frappe._dict(
+ from_date=date,
+ to_date=date,
+ from_time='',
+ to_time='',
+ item_code='Dextromethorphan',
+ patient=self.patient
+ )
+
+ # check drug shortage
+ ipme = create_ipme(filters, update_stock=1)
+ ipme.warehouse = 'Finished Goods - _TC'
+ ipme.save()
+ drug_shortage = get_drug_shortage_map(ipme.medication_orders, ipme.warehouse)
+ self.assertEqual(drug_shortage.get('Dextromethorphan'), 3)
+
+ # check material transfer for drug shortage
+ make_stock_entry()
+ stock_entry = make_difference_stock_entry(ipme.name)
+ self.assertEqual(stock_entry.items[0].item_code, 'Dextromethorphan')
+ self.assertEqual(stock_entry.items[0].qty, 3)
+ stock_entry.from_warehouse = 'Stores - _TC'
+ stock_entry.submit()
+
+ ipme.reload()
+ ipme.submit()
+
def tearDown(self):
# cleanup - Discharge
schedule_discharge(frappe.as_json({'patient': self.patient}))
@@ -94,15 +128,12 @@
for entry in frappe.get_all('Inpatient Medication Entry'):
doc = frappe.get_doc('Inpatient Medication Entry', entry.name)
doc.cancel()
- frappe.db.delete('Stock Entry', {'inpatient_medication_entry': doc.name})
- doc.delete()
for entry in frappe.get_all('Inpatient Medication Order'):
doc = frappe.get_doc('Inpatient Medication Order', entry.name)
doc.cancel()
- doc.delete()
-def make_stock_entry():
+def make_stock_entry(warehouse=None):
frappe.db.set_value('Company', '_Test Company', {
'stock_adjustment_account': 'Stock Adjustment - _TC',
'default_inventory_account': 'Stock In Hand - _TC'
@@ -110,7 +141,7 @@
stock_entry = frappe.new_doc('Stock Entry')
stock_entry.stock_entry_type = 'Material Receipt'
stock_entry.company = '_Test Company'
- stock_entry.to_warehouse = 'Stores - _TC'
+ stock_entry.to_warehouse = warehouse or 'Stores - _TC'
expense_account = get_account(None, 'expense_account', 'Healthcare Settings', '_Test Company')
se_child = stock_entry.append('items')
se_child.item_code = 'Dextromethorphan'
diff --git a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
index bc76970..88d7f0b 100644
--- a/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
+++ b/erpnext/healthcare/doctype/inpatient_record/inpatient_record.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe, json
from frappe import _
-from frappe.utils import today, now_datetime, getdate, get_datetime
+from frappe.utils import today, now_datetime, getdate, get_datetime, get_link_to_form
from frappe.model.document import Document
from frappe.desk.reportview import get_match_cond
@@ -50,7 +50,7 @@
if ip_record:
msg = _(("Already {0} Patient {1} with Inpatient Record ").format(ip_record[0].status, self.patient) \
- + """ <b><a href="#Form/Inpatient Record/{0}">{0}</a></b>""".format(ip_record[0].name))
+ + """ <b><a href="/app/Form/Inpatient Record/{0}">{0}</a></b>""".format(ip_record[0].name))
frappe.throw(msg)
def admit(self, service_unit, check_in, expected_discharge=None):
@@ -113,6 +113,7 @@
inpatient_record.status = 'Admission Scheduled'
inpatient_record.save(ignore_permissions = True)
+
@frappe.whitelist()
def schedule_discharge(args):
discharge_order = json.loads(args)
@@ -126,16 +127,19 @@
frappe.db.set_value('Patient', discharge_order['patient'], 'inpatient_status', inpatient_record.status)
frappe.db.set_value('Patient Encounter', inpatient_record.discharge_encounter, 'inpatient_status', inpatient_record.status)
+
def set_details_from_ip_order(inpatient_record, ip_order):
for key in ip_order:
inpatient_record.set(key, ip_order[key])
+
def set_ip_child_records(inpatient_record, inpatient_record_child, encounter_child):
for item in encounter_child:
table = inpatient_record.append(inpatient_record_child)
for df in table.meta.get('fields'):
table.set(df.fieldname, item.get(df.fieldname))
+
def check_out_inpatient(inpatient_record):
if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@@ -144,54 +148,88 @@
inpatient_occupancy.check_out = now_datetime()
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
+
def discharge_patient(inpatient_record):
- validate_invoiced_inpatient(inpatient_record)
+ validate_inpatient_invoicing(inpatient_record)
inpatient_record.discharge_date = today()
inpatient_record.status = "Discharged"
inpatient_record.save(ignore_permissions = True)
-def validate_invoiced_inpatient(inpatient_record):
- pending_invoices = []
+
+def validate_inpatient_invoicing(inpatient_record):
+ if frappe.db.get_single_value("Healthcare Settings", "allow_discharge_despite_unbilled_services"):
+ return
+
+ pending_invoices = get_pending_invoices(inpatient_record)
+
+ if pending_invoices:
+ message = _("Cannot mark Inpatient Record as Discharged since there are unbilled services. ")
+
+ formatted_doc_rows = ''
+
+ for doctype, docnames in pending_invoices.items():
+ formatted_doc_rows += """
+ <td>{0}</td>
+ <td>{1}</td>
+ </tr>""".format(doctype, docnames)
+
+ message += """
+ <table class='table'>
+ <thead>
+ <th>{0}</th>
+ <th>{1}</th>
+ </thead>
+ {2}
+ </table>
+ """.format(_("Healthcare Service"), _("Documents"), formatted_doc_rows)
+
+ frappe.throw(message, title=_("Unbilled Services"), is_minimizable=True, wide=True)
+
+
+def get_pending_invoices(inpatient_record):
+ pending_invoices = {}
if inpatient_record.inpatient_occupancies:
service_unit_names = False
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
- if inpatient_occupancy.invoiced != 1:
+ if not inpatient_occupancy.invoiced:
if service_unit_names:
service_unit_names += ", " + inpatient_occupancy.service_unit
else:
service_unit_names = inpatient_occupancy.service_unit
if service_unit_names:
- pending_invoices.append("Inpatient Occupancy (" + service_unit_names + ")")
+ pending_invoices["Inpatient Occupancy"] = service_unit_names
docs = ["Patient Appointment", "Patient Encounter", "Lab Test", "Clinical Procedure"]
for doc in docs:
- doc_name_list = get_inpatient_docs_not_invoiced(doc, inpatient_record)
+ doc_name_list = get_unbilled_inpatient_docs(doc, inpatient_record)
if doc_name_list:
pending_invoices = get_pending_doc(doc, doc_name_list, pending_invoices)
- if pending_invoices:
- frappe.throw(_("Can not mark Inpatient Record Discharged, there are Unbilled Invoices {0}").format(", "
- .join(pending_invoices)), title=_('Unbilled Invoices'))
+ return pending_invoices
+
def get_pending_doc(doc, doc_name_list, pending_invoices):
if doc_name_list:
doc_ids = False
for doc_name in doc_name_list:
+ doc_link = get_link_to_form(doc, doc_name.name)
if doc_ids:
- doc_ids += ", "+doc_name.name
+ doc_ids += ", " + doc_link
else:
- doc_ids = doc_name.name
+ doc_ids = doc_link
if doc_ids:
- pending_invoices.append(doc + " (" + doc_ids + ")")
+ pending_invoices[doc] = doc_ids
return pending_invoices
-def get_inpatient_docs_not_invoiced(doc, inpatient_record):
+
+def get_unbilled_inpatient_docs(doc, inpatient_record):
return frappe.db.get_list(doc, filters = {'patient': inpatient_record.patient,
'inpatient_record': inpatient_record.name, 'docstatus': 1, 'invoiced': 0})
+
def admit_patient(inpatient_record, service_unit, check_in, expected_discharge=None):
inpatient_record.admitted_datetime = check_in
inpatient_record.status = 'Admitted'
@@ -203,6 +241,7 @@
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_status', 'Admitted')
frappe.db.set_value('Patient', inpatient_record.patient, 'inpatient_record', inpatient_record.name)
+
def transfer_patient(inpatient_record, service_unit, check_in):
item_line = inpatient_record.append('inpatient_occupancies', {})
item_line.service_unit = service_unit
@@ -212,6 +251,7 @@
frappe.db.set_value("Healthcare Service Unit", service_unit, "occupancy_status", "Occupied")
+
def patient_leave_service_unit(inpatient_record, check_out, leave_from):
if inpatient_record.inpatient_occupancies:
for inpatient_occupancy in inpatient_record.inpatient_occupancies:
@@ -221,6 +261,7 @@
frappe.db.set_value("Healthcare Service Unit", inpatient_occupancy.service_unit, "occupancy_status", "Vacant")
inpatient_record.save(ignore_permissions = True)
+
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_leave_from(doctype, txt, searchfield, start, page_len, filters):
diff --git a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
index 70706ad..a8c7720 100644
--- a/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
+++ b/erpnext/healthcare/doctype/inpatient_record/test_inpatient_record.py
@@ -8,6 +8,8 @@
from frappe.utils import now_datetime, today
from frappe.utils.make_random import get_random
from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
+from erpnext.healthcare.doctype.lab_test.test_lab_test import create_patient_encounter
+from erpnext.healthcare.utils import get_encounters_to_invoice
class TestInpatientRecord(unittest.TestCase):
def test_admit_and_discharge(self):
@@ -40,6 +42,60 @@
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
+ def test_allow_discharge_despite_unbilled_services(self):
+ frappe.db.sql("""delete from `tabInpatient Record`""")
+ setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=1)
+ patient = create_patient()
+ # Schedule Admission
+ ip_record = create_inpatient(patient)
+ ip_record.expected_length_of_stay = 0
+ ip_record.save(ignore_permissions = True)
+
+ # Admit
+ service_unit = get_healthcare_service_unit()
+ admit_patient(ip_record, service_unit, now_datetime())
+
+ # Discharge
+ schedule_discharge(frappe.as_json({"patient": patient}))
+ self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
+
+ ip_record = frappe.get_doc("Inpatient Record", ip_record.name)
+ # Should not validate Pending Invoices
+ ip_record.discharge()
+
+ self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_record"))
+ self.assertEqual(None, frappe.db.get_value("Patient", patient, "inpatient_status"))
+
+ setup_inpatient_settings(key="allow_discharge_despite_unbilled_services", value=0)
+
+ def test_do_not_bill_patient_encounters_for_inpatients(self):
+ frappe.db.sql("""delete from `tabInpatient Record`""")
+ setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=1)
+ patient = create_patient()
+ # Schedule Admission
+ ip_record = create_inpatient(patient)
+ ip_record.expected_length_of_stay = 0
+ ip_record.save(ignore_permissions = True)
+
+ # Admit
+ service_unit = get_healthcare_service_unit()
+ admit_patient(ip_record, service_unit, now_datetime())
+
+ # Patient Encounter
+ patient_encounter = create_patient_encounter()
+ encounters = get_encounters_to_invoice(patient, "_Test Company")
+ encounter_ids = [entry.reference_name for entry in encounters]
+ self.assertFalse(patient_encounter.name in encounter_ids)
+
+ # Discharge
+ schedule_discharge(frappe.as_json({"patient": patient}))
+ self.assertEqual("Vacant", frappe.db.get_value("Healthcare Service Unit", service_unit, "occupancy_status"))
+
+ ip_record = frappe.get_doc("Inpatient Record", ip_record.name)
+ mark_invoiced_inpatient_occupancy(ip_record)
+ discharge_patient(ip_record)
+ setup_inpatient_settings(key="do_not_bill_inpatient_encounters", value=0)
+
def test_validate_overlap_admission(self):
frappe.db.sql("""delete from `tabInpatient Record`""")
patient = create_patient()
@@ -63,6 +119,13 @@
inpatient_occupancy.invoiced = 1
ip_record.save(ignore_permissions = True)
+
+def setup_inpatient_settings(key, value):
+ settings = frappe.get_single("Healthcare Settings")
+ settings.set(key, value)
+ settings.save()
+
+
def create_inpatient(patient):
patient_obj = frappe.get_doc('Patient', patient)
inpatient_record = frappe.new_doc('Inpatient Record')
@@ -76,13 +139,19 @@
inpatient_record.phone = patient_obj.phone
inpatient_record.inpatient = "Scheduled"
inpatient_record.scheduled_date = today()
+ inpatient_record.company = "_Test Company"
return inpatient_record
-def get_healthcare_service_unit():
- service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1})
+
+def get_healthcare_service_unit(unit_name=None):
+ if not unit_name:
+ service_unit = get_random("Healthcare Service Unit", filters={"inpatient_occupancy": 1, "company": "_Test Company"})
+ else:
+ service_unit = frappe.db.exists("Healthcare Service Unit", {"healthcare_service_unit_name": unit_name})
+
if not service_unit:
service_unit = frappe.new_doc("Healthcare Service Unit")
- service_unit.healthcare_service_unit_name = "Test Service Unit Ip Occupancy"
+ service_unit.healthcare_service_unit_name = unit_name or "Test Service Unit Ip Occupancy"
service_unit.company = "_Test Company"
service_unit.service_unit_type = get_service_unit_type()
service_unit.inpatient_occupancy = 1
@@ -105,6 +174,7 @@
return service_unit.name
return service_unit
+
def get_service_unit_type():
service_unit_type = get_random("Healthcare Service Unit Type", filters={"inpatient_occupancy": 1})
@@ -116,6 +186,7 @@
return service_unit_type.name
return service_unit_type
+
def create_patient():
patient = frappe.db.exists('Patient', '_Test IPD Patient')
if not patient:
diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.json b/erpnext/healthcare/doctype/lab_test/lab_test.json
index edf1d91..ac61fea 100644
--- a/erpnext/healthcare/doctype/lab_test/lab_test.json
+++ b/erpnext/healthcare/doctype/lab_test/lab_test.json
@@ -359,6 +359,7 @@
{
"fieldname": "normal_test_items",
"fieldtype": "Table",
+ "label": "Normal Test Result",
"options": "Normal Test Result",
"print_hide": 1
},
@@ -380,6 +381,7 @@
{
"fieldname": "sensitivity_test_items",
"fieldtype": "Table",
+ "label": "Sensitivity Test Result",
"options": "Sensitivity Test Result",
"print_hide": 1,
"report_hide": 1
@@ -529,6 +531,7 @@
{
"fieldname": "descriptive_test_items",
"fieldtype": "Table",
+ "label": "Descriptive Test Result",
"options": "Descriptive Test Result",
"print_hide": 1,
"report_hide": 1
@@ -549,13 +552,14 @@
{
"fieldname": "organism_test_items",
"fieldtype": "Table",
+ "label": "Organism Test Result",
"options": "Organism Test Result",
"print_hide": 1
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-07-30 18:18:38.516215",
+ "modified": "2020-11-30 11:04:17.195848",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Lab Test",
diff --git a/erpnext/healthcare/doctype/lab_test/lab_test.py b/erpnext/healthcare/doctype/lab_test/lab_test.py
index 2db7743..4b57cd0 100644
--- a/erpnext/healthcare/doctype/lab_test/lab_test.py
+++ b/erpnext/healthcare/doctype/lab_test/lab_test.py
@@ -17,11 +17,9 @@
self.validate_result_values()
self.db_set('submitted_date', getdate())
self.db_set('status', 'Completed')
- insert_lab_test_to_medical_record(self)
def on_cancel(self):
self.db_set('status', 'Cancelled')
- delete_lab_test_from_medical_record(self)
self.reload()
def on_update(self):
@@ -330,60 +328,6 @@
return frappe.get_doc('Employee', emp_id)
return None
-def insert_lab_test_to_medical_record(doc):
- table_row = False
- subject = cstr(doc.lab_test_name)
- if doc.practitioner:
- subject += frappe.bold(_('Healthcare Practitioner: '))+ doc.practitioner + '<br>'
- if doc.normal_test_items:
- item = doc.normal_test_items[0]
- comment = ''
- if item.lab_test_comment:
- comment = str(item.lab_test_comment)
- table_row = frappe.bold(_('Lab Test Conducted: ')) + item.lab_test_name
-
- if item.lab_test_event:
- table_row += frappe.bold(_('Lab Test Event: ')) + item.lab_test_event
-
- if item.result_value:
- table_row += ' ' + frappe.bold(_('Lab Test Result: ')) + item.result_value
-
- if item.normal_range:
- table_row += ' ' + _('Normal Range: ') + item.normal_range
- table_row += ' ' + comment
-
- elif doc.descriptive_test_items:
- item = doc.descriptive_test_items[0]
-
- if item.lab_test_particulars and item.result_value:
- table_row = item.lab_test_particulars + ' ' + item.result_value
-
- elif doc.sensitivity_test_items:
- item = doc.sensitivity_test_items[0]
-
- if item.antibiotic and item.antibiotic_sensitivity:
- table_row = item.antibiotic + ' ' + item.antibiotic_sensitivity
-
- if table_row:
- subject += '<br>' + table_row
- if doc.lab_test_comment:
- subject += '<br>' + cstr(doc.lab_test_comment)
-
- medical_record = frappe.new_doc('Patient Medical Record')
- medical_record.patient = doc.patient
- medical_record.subject = subject
- medical_record.status = 'Open'
- medical_record.communication_date = doc.result_date
- medical_record.reference_doctype = 'Lab Test'
- medical_record.reference_name = doc.name
- medical_record.reference_owner = doc.owner
- medical_record.save(ignore_permissions = True)
-
-def delete_lab_test_from_medical_record(self):
- medical_record_id = frappe.db.sql('select name from `tabPatient Medical Record` where reference_name=%s', (self.name))
-
- if medical_record_id and medical_record_id[0][0]:
- frappe.delete_doc('Patient Medical Record', medical_record_id[0][0])
@frappe.whitelist()
def get_lab_test_prescribed(patient):
diff --git a/erpnext/healthcare/doctype/patient/patient_dashboard.py b/erpnext/healthcare/doctype/patient/patient_dashboard.py
index e3def72..39603f7 100644
--- a/erpnext/healthcare/doctype/patient/patient_dashboard.py
+++ b/erpnext/healthcare/doctype/patient/patient_dashboard.py
@@ -18,6 +18,10 @@
{
'label': _('Billing'),
'items': ['Sales Invoice']
+ },
+ {
+ 'label': _('Orders'),
+ 'items': ['Inpatient Medication Order']
}
]
}
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
index 2d6b645..0354733 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.js
@@ -22,23 +22,37 @@
filters: {'status': 'Active'}
};
});
+
frm.set_query('practitioner', function() {
+ if (frm.doc.department) {
+ return {
+ filters: {
+ 'department': frm.doc.department
+ }
+ };
+ }
+ });
+
+ frm.set_query('service_unit', function() {
return {
+ query: 'erpnext.controllers.queries.get_healthcare_service_units',
filters: {
- 'department': frm.doc.department
+ company: frm.doc.company,
+ inpatient_record: frm.doc.inpatient_record
}
};
});
- frm.set_query('service_unit', function(){
+
+ frm.set_query('therapy_plan', function() {
return {
filters: {
- 'is_group': false,
- 'allow_appointments': true,
- 'company': frm.doc.company
+ 'patient': frm.doc.patient
}
};
});
+ frm.trigger('set_therapy_type_filter');
+
if (frm.is_new()) {
frm.page.set_primary_action(__('Check Availability'), function() {
if (!frm.doc.patient) {
@@ -128,6 +142,20 @@
patient: function(frm) {
if (frm.doc.patient) {
frm.trigger('toggle_payment_fields');
+ frappe.call({
+ method: 'frappe.client.get',
+ args: {
+ doctype: 'Patient',
+ name: frm.doc.patient
+ },
+ callback: function (data) {
+ let age = null;
+ if (data.message.dob) {
+ age = calculate_age(data.message.dob);
+ }
+ frappe.model.set_value(frm.doctype, frm.docname, 'patient_age', age);
+ }
+ });
} else {
frm.set_value('patient_name', '');
frm.set_value('patient_sex', '');
@@ -136,6 +164,55 @@
}
},
+ practitioner: function(frm) {
+ if (frm.doc.practitioner ) {
+ frm.events.set_payment_details(frm);
+ }
+ },
+
+ appointment_type: function(frm) {
+ if (frm.doc.appointment_type) {
+ frm.events.set_payment_details(frm);
+ }
+ },
+
+ set_payment_details: function(frm) {
+ frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing').then(val => {
+ if (val) {
+ frappe.call({
+ method: 'erpnext.healthcare.utils.get_service_item_and_practitioner_charge',
+ args: {
+ doc: frm.doc
+ },
+ callback: function(data) {
+ if (data.message) {
+ frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.practitioner_charge);
+ frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.service_item);
+ }
+ }
+ });
+ }
+ });
+ },
+
+ therapy_plan: function(frm) {
+ frm.trigger('set_therapy_type_filter');
+ },
+
+ set_therapy_type_filter: function(frm) {
+ if (frm.doc.therapy_plan) {
+ frm.call('get_therapy_types').then(r => {
+ frm.set_query('therapy_type', function() {
+ return {
+ filters: {
+ 'name': ['in', r.message]
+ }
+ };
+ });
+ });
+ }
+ },
+
therapy_type: function(frm) {
if (frm.doc.therapy_type) {
frappe.db.get_value('Therapy Type', frm.doc.therapy_type, 'default_duration', (r) => {
@@ -160,14 +237,18 @@
// show payment fields as non-mandatory
frm.toggle_display('mode_of_payment', 0);
frm.toggle_display('paid_amount', 0);
+ frm.toggle_display('billing_item', 0);
frm.toggle_reqd('mode_of_payment', 0);
frm.toggle_reqd('paid_amount', 0);
+ frm.toggle_reqd('billing_item', 0);
} else {
// if automated appointment invoicing is disabled, hide fields
frm.toggle_display('mode_of_payment', data.message ? 1 : 0);
frm.toggle_display('paid_amount', data.message ? 1 : 0);
+ frm.toggle_display('billing_item', data.message ? 1 : 0);
frm.toggle_reqd('mode_of_payment', data.message ? 1 : 0);
frm.toggle_reqd('paid_amount', data.message ? 1 :0);
+ frm.toggle_reqd('billing_item', data.message ? 1 : 0);
}
}
});
@@ -510,57 +591,6 @@
);
};
-frappe.ui.form.on('Patient Appointment', 'practitioner', function(frm) {
- if (frm.doc.practitioner) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Healthcare Practitioner',
- name: frm.doc.practitioner
- },
- callback: function (data) {
- frappe.model.set_value(frm.doctype, frm.docname, 'department', data.message.department);
- frappe.model.set_value(frm.doctype, frm.docname, 'paid_amount', data.message.op_consulting_charge);
- frappe.model.set_value(frm.doctype, frm.docname, 'billing_item', data.message.op_consulting_charge_item);
- }
- });
- }
-});
-
-frappe.ui.form.on('Patient Appointment', 'patient', function(frm) {
- if (frm.doc.patient) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Patient',
- name: frm.doc.patient
- },
- callback: function (data) {
- let age = null;
- if (data.message.dob) {
- age = calculate_age(data.message.dob);
- }
- frappe.model.set_value(frm.doctype,frm.docname, 'patient_age', age);
- }
- });
- }
-});
-
-frappe.ui.form.on('Patient Appointment', 'appointment_type', function(frm) {
- if (frm.doc.appointment_type) {
- frappe.call({
- method: 'frappe.client.get',
- args: {
- doctype: 'Appointment Type',
- name: frm.doc.appointment_type
- },
- callback: function(data) {
- frappe.model.set_value(frm.doctype,frm.docname, 'duration',data.message.default_duration);
- }
- });
- }
-});
-
let calculate_age = function(birth) {
let ageMS = Date.parse(Date()) - Date.parse(birth);
let age = new Date();
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
index ac35acc..83c92af 100644
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.json
@@ -19,19 +19,19 @@
"inpatient_record",
"column_break_1",
"company",
- "service_unit",
- "procedure_template",
- "get_procedure_from_encounter",
- "procedure_prescription",
- "therapy_type",
- "get_prescribed_therapies",
- "therapy_plan",
"practitioner",
"practitioner_name",
"department",
+ "service_unit",
"section_break_12",
"appointment_type",
"duration",
+ "procedure_template",
+ "get_procedure_from_encounter",
+ "procedure_prescription",
+ "therapy_plan",
+ "therapy_type",
+ "get_prescribed_therapies",
"column_break_17",
"appointment_date",
"appointment_time",
@@ -79,6 +79,7 @@
"set_only_once": 1
},
{
+ "fetch_from": "appointment_type.default_duration",
"fieldname": "duration",
"fieldtype": "Int",
"in_filter": 1,
@@ -144,7 +145,6 @@
"in_standard_filter": 1,
"label": "Healthcare Practitioner",
"options": "Healthcare Practitioner",
- "read_only": 1,
"reqd": 1,
"search_index": 1,
"set_only_once": 1
@@ -158,7 +158,6 @@
"in_standard_filter": 1,
"label": "Department",
"options": "Medical Department",
- "read_only": 1,
"search_index": 1,
"set_only_once": 1
},
@@ -227,12 +226,14 @@
"fieldname": "mode_of_payment",
"fieldtype": "Link",
"label": "Mode of Payment",
- "options": "Mode of Payment"
+ "options": "Mode of Payment",
+ "read_only_depends_on": "invoiced"
},
{
"fieldname": "paid_amount",
"fieldtype": "Currency",
- "label": "Paid Amount"
+ "label": "Paid Amount",
+ "read_only_depends_on": "invoiced"
},
{
"fieldname": "column_break_2",
@@ -284,7 +285,7 @@
"report_hide": 1
},
{
- "depends_on": "eval:doc.patient;",
+ "depends_on": "eval:doc.patient && doc.therapy_plan;",
"fieldname": "therapy_type",
"fieldtype": "Link",
"label": "Therapy",
@@ -292,18 +293,18 @@
"set_only_once": 1
},
{
- "depends_on": "eval:doc.patient && doc.__islocal;",
+ "depends_on": "eval:doc.patient && doc.therapy_plan && doc.__islocal;",
"fieldname": "get_prescribed_therapies",
"fieldtype": "Button",
"label": "Get Prescribed Therapies"
},
{
- "depends_on": "eval: doc.patient && doc.therapy_type",
+ "depends_on": "eval: doc.patient;",
"fieldname": "therapy_plan",
"fieldtype": "Link",
"label": "Therapy Plan",
- "mandatory_depends_on": "eval: doc.patient && doc.therapy_type",
- "options": "Therapy Plan"
+ "options": "Therapy Plan",
+ "set_only_once": 1
},
{
"fieldname": "ref_sales_invoice",
@@ -348,7 +349,7 @@
}
],
"links": [],
- "modified": "2020-05-21 03:04:21.400893",
+ "modified": "2021-02-08 13:13:15.116833",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Appointment",
diff --git a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
index e685b20..1f76cd6 100755
--- a/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/patient_appointment.py
@@ -18,6 +18,7 @@
class PatientAppointment(Document):
def validate(self):
self.validate_overlaps()
+ self.validate_service_unit()
self.set_appointment_datetime()
self.validate_customer_created()
self.set_status()
@@ -25,6 +26,7 @@
def after_insert(self):
self.update_prescription_details()
+ self.set_payment_details()
invoice_appointment(self)
self.update_fee_validity()
send_confirmation_msg(self)
@@ -63,19 +65,39 @@
if overlaps:
overlapping_details = _('Appointment overlaps with ')
- overlapping_details += "<b><a href='#Form/Patient Appointment/{0}'>{0}</a></b><br>".format(overlaps[0][0])
+ overlapping_details += "<b><a href='/app/Form/Patient Appointment/{0}'>{0}</a></b><br>".format(overlaps[0][0])
overlapping_details += _('{0} has appointment scheduled with {1} at {2} having {3} minute(s) duration.').format(
overlaps[0][1], overlaps[0][2], overlaps[0][3], overlaps[0][4])
frappe.throw(overlapping_details, title=_('Appointments Overlapping'))
+ def validate_service_unit(self):
+ if self.inpatient_record and self.service_unit:
+ from erpnext.healthcare.doctype.inpatient_medication_entry.inpatient_medication_entry import get_current_healthcare_service_unit
+
+ is_inpatient_occupancy_unit = frappe.db.get_value('Healthcare Service Unit', self.service_unit,
+ 'inpatient_occupancy')
+ service_unit = get_current_healthcare_service_unit(self.inpatient_record)
+ if is_inpatient_occupancy_unit and service_unit != self.service_unit:
+ msg = _('Patient {0} is not admitted in the service unit {1}').format(frappe.bold(self.patient), frappe.bold(self.service_unit)) + '<br>'
+ msg += _('Appointment for service units with Inpatient Occupancy can only be created against the unit where patient has been admitted.')
+ frappe.throw(msg, title=_('Invalid Healthcare Service Unit'))
+
+
def set_appointment_datetime(self):
self.appointment_datetime = "%s %s" % (self.appointment_date, self.appointment_time or "00:00:00")
+ def set_payment_details(self):
+ if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
+ details = get_service_item_and_practitioner_charge(self)
+ self.db_set('billing_item', details.get('service_item'))
+ if not self.paid_amount:
+ self.db_set('paid_amount', details.get('practitioner_charge'))
+
def validate_customer_created(self):
if frappe.db.get_single_value('Healthcare Settings', 'automate_appointment_invoicing'):
if not frappe.db.get_value('Patient', self.patient, 'customer'):
msg = _("Please set a Customer linked to the Patient")
- msg += " <b><a href='#Form/Patient/{0}'>{0}</a></b>".format(self.patient)
+ msg += " <b><a href='/app/Form/Patient/{0}'>{0}</a></b>".format(self.patient)
frappe.throw(msg, title=_('Customer Not Found'))
def update_prescription_details(self):
@@ -91,6 +113,17 @@
if fee_validity:
frappe.msgprint(_('{0} has fee validity till {1}').format(self.patient, fee_validity.valid_till))
+ def get_therapy_types(self):
+ if not self.therapy_plan:
+ return
+
+ therapy_types = []
+ doc = frappe.get_doc('Therapy Plan', self.therapy_plan)
+ for entry in doc.therapy_plan_details:
+ therapy_types.append(entry.therapy_type)
+
+ return therapy_types
+
@frappe.whitelist()
def check_payment_fields_reqd(patient):
@@ -123,31 +156,37 @@
fee_validity = None
if automate_invoicing and not appointment_invoiced and not fee_validity:
- sales_invoice = frappe.new_doc('Sales Invoice')
- sales_invoice.patient = appointment_doc.patient
- sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
- sales_invoice.appointment = appointment_doc.name
- sales_invoice.due_date = getdate()
- sales_invoice.company = appointment_doc.company
- sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
+ create_sales_invoice(appointment_doc)
- item = sales_invoice.append('items', {})
- item = get_appointment_item(appointment_doc, item)
- # Add payments if payment details are supplied else proceed to create invoice as Unpaid
- if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
- sales_invoice.is_pos = 1
- payment = sales_invoice.append('payments', {})
- payment.mode_of_payment = appointment_doc.mode_of_payment
- payment.amount = appointment_doc.paid_amount
+def create_sales_invoice(appointment_doc):
+ sales_invoice = frappe.new_doc('Sales Invoice')
+ sales_invoice.patient = appointment_doc.patient
+ sales_invoice.customer = frappe.get_value('Patient', appointment_doc.patient, 'customer')
+ sales_invoice.appointment = appointment_doc.name
+ sales_invoice.due_date = getdate()
+ sales_invoice.company = appointment_doc.company
+ sales_invoice.debit_to = get_receivable_account(appointment_doc.company)
- sales_invoice.set_missing_values(for_validate=True)
- sales_invoice.flags.ignore_mandatory = True
- sales_invoice.save(ignore_permissions=True)
- sales_invoice.submit()
- frappe.msgprint(_('Sales Invoice {0} created'.format(sales_invoice.name)), alert=True)
- frappe.db.set_value('Patient Appointment', appointment_doc.name, 'invoiced', 1)
- frappe.db.set_value('Patient Appointment', appointment_doc.name, 'ref_sales_invoice', sales_invoice.name)
+ item = sales_invoice.append('items', {})
+ item = get_appointment_item(appointment_doc, item)
+
+ # Add payments if payment details are supplied else proceed to create invoice as Unpaid
+ if appointment_doc.mode_of_payment and appointment_doc.paid_amount:
+ sales_invoice.is_pos = 1
+ payment = sales_invoice.append('payments', {})
+ payment.mode_of_payment = appointment_doc.mode_of_payment
+ payment.amount = appointment_doc.paid_amount
+
+ sales_invoice.set_missing_values(for_validate=True)
+ sales_invoice.flags.ignore_mandatory = True
+ sales_invoice.save(ignore_permissions=True)
+ sales_invoice.submit()
+ frappe.msgprint(_('Sales Invoice {0} created').format(sales_invoice.name), alert=True)
+ frappe.db.set_value('Patient Appointment', appointment_doc.name, {
+ 'invoiced': 1,
+ 'ref_sales_invoice': sales_invoice.name
+ })
def check_is_new_patient(patient, name=None):
@@ -162,13 +201,14 @@
def get_appointment_item(appointment_doc, item):
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment_doc)
- item.item_code = service_item
+ details = get_service_item_and_practitioner_charge(appointment_doc)
+ charge = appointment_doc.paid_amount or details.get('practitioner_charge')
+ item.item_code = details.get('service_item')
item.description = _('Consulting Charges: {0}').format(appointment_doc.practitioner)
item.income_account = get_income_account(appointment_doc.practitioner, appointment_doc.company)
item.cost_center = frappe.get_cached_value('Company', appointment_doc.company, 'cost_center')
- item.rate = practitioner_charge
- item.amount = practitioner_charge
+ item.rate = charge
+ item.amount = charge
item.qty = 1
item.reference_dt = 'Patient Appointment'
item.reference_dn = appointment_doc.name
diff --git a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
index eeed157..2bb8a53 100644
--- a/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
+++ b/erpnext/healthcare/doctype/patient_appointment/test_patient_appointment.py
@@ -5,14 +5,16 @@
import unittest
import frappe
from erpnext.healthcare.doctype.patient_appointment.patient_appointment import update_status, make_encounter
-from frappe.utils import nowdate, add_days
+from frappe.utils import nowdate, add_days, now_datetime
from frappe.utils.make_random import get_random
+from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPatientAppointment(unittest.TestCase):
def setUp(self):
frappe.db.sql("""delete from `tabPatient Appointment`""")
frappe.db.sql("""delete from `tabFee Validity`""")
frappe.db.sql("""delete from `tabPatient Encounter`""")
+ make_pos_profile()
def test_status(self):
patient, medical_department, practitioner = create_healthcare_docs()
@@ -21,14 +23,17 @@
self.assertEquals(appointment.status, 'Open')
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2))
self.assertEquals(appointment.status, 'Scheduled')
- create_encounter(appointment)
+ encounter = create_encounter(appointment)
self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
+ encounter.cancel()
+ self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
def test_start_encounter(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
appointment = create_appointment(patient, practitioner, add_days(nowdate(), 4), invoice = 1)
- self.assertEqual(frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'), 1)
+ appointment.reload()
+ self.assertEqual(appointment.invoiced, 1)
encounter = make_encounter(appointment.name)
self.assertTrue(encounter)
self.assertEqual(encounter.company, appointment.company)
@@ -37,7 +42,7 @@
# invoiced flag mapped from appointment
self.assertEqual(encounter.invoiced, frappe.db.get_value('Patient Appointment', appointment.name, 'invoiced'))
- def test_invoicing(self):
+ def test_auto_invoicing(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 0)
@@ -53,6 +58,50 @@
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'patient'), appointment.patient)
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
+ def test_auto_invoicing_based_on_department(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ appointment_type = create_appointment_type()
+
+ appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
+ invoice=1, appointment_type=appointment_type.name, department='_Test Medical Department')
+ appointment.reload()
+
+ self.assertEqual(appointment.invoiced, 1)
+ self.assertEqual(appointment.billing_item, 'HLC-SI-001')
+ self.assertEqual(appointment.paid_amount, 200)
+
+ sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
+ self.assertTrue(sales_invoice_name)
+ self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'paid_amount'), appointment.paid_amount)
+
+ def test_auto_invoicing_according_to_appointment_type_charge(self):
+ patient, medical_department, practitioner = create_healthcare_docs()
+ frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
+ frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+
+ item = create_healthcare_service_items()
+ items = [{
+ 'op_consulting_charge_item': item,
+ 'op_consulting_charge': 300
+ }]
+ appointment_type = create_appointment_type(args={
+ 'name': 'Generic Appointment Type charge',
+ 'items': items
+ })
+
+ appointment = create_appointment(patient, practitioner, add_days(nowdate(), 2),
+ invoice=1, appointment_type=appointment_type.name)
+ appointment.reload()
+
+ self.assertEqual(appointment.invoiced, 1)
+ self.assertEqual(appointment.billing_item, item)
+ self.assertEqual(appointment.paid_amount, 300)
+
+ sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
+ self.assertTrue(sales_invoice_name)
+
def test_appointment_cancel(self):
patient, medical_department, practitioner = create_healthcare_docs()
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 1)
@@ -74,6 +123,59 @@
sales_invoice_name = frappe.db.get_value('Sales Invoice Item', {'reference_dn': appointment.name}, 'parent')
self.assertEqual(frappe.db.get_value('Sales Invoice', sales_invoice_name, 'status'), 'Cancelled')
+ def test_appointment_booking_for_admission_service_unit(self):
+ from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
+ from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \
+ create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
+
+ frappe.db.sql("""delete from `tabInpatient Record`""")
+ patient, medical_department, practitioner = create_healthcare_docs()
+ patient = create_patient()
+ # Schedule Admission
+ ip_record = create_inpatient(patient)
+ ip_record.expected_length_of_stay = 0
+ ip_record.save(ignore_permissions = True)
+
+ # Admit
+ service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
+ admit_patient(ip_record, service_unit, now_datetime())
+
+ appointment = create_appointment(patient, practitioner, nowdate(), service_unit=service_unit)
+ self.assertEqual(appointment.service_unit, service_unit)
+
+ # Discharge
+ schedule_discharge(frappe.as_json({'patient': patient}))
+ ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name)
+ mark_invoiced_inpatient_occupancy(ip_record1)
+ discharge_patient(ip_record1)
+
+ def test_invalid_healthcare_service_unit_validation(self):
+ from erpnext.healthcare.doctype.inpatient_record.inpatient_record import admit_patient, discharge_patient, schedule_discharge
+ from erpnext.healthcare.doctype.inpatient_record.test_inpatient_record import \
+ create_inpatient, get_healthcare_service_unit, mark_invoiced_inpatient_occupancy
+
+ frappe.db.sql("""delete from `tabInpatient Record`""")
+ patient, medical_department, practitioner = create_healthcare_docs()
+ patient = create_patient()
+ # Schedule Admission
+ ip_record = create_inpatient(patient)
+ ip_record.expected_length_of_stay = 0
+ ip_record.save(ignore_permissions = True)
+
+ # Admit
+ service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
+ admit_patient(ip_record, service_unit, now_datetime())
+
+ appointment_service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy for Appointment')
+ appointment = create_appointment(patient, practitioner, nowdate(), service_unit=appointment_service_unit, save=0)
+ self.assertRaises(frappe.exceptions.ValidationError, appointment.save)
+
+ # Discharge
+ schedule_discharge(frappe.as_json({'patient': patient}))
+ ip_record1 = frappe.get_doc("Inpatient Record", ip_record.name)
+ mark_invoiced_inpatient_occupancy(ip_record1)
+ discharge_patient(ip_record1)
+
def create_healthcare_docs():
patient = create_patient()
@@ -121,23 +223,28 @@
encounter.submit()
return encounter
-def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0):
+def create_appointment(patient, practitioner, appointment_date, invoice=0, procedure_template=0,
+ service_unit=None, appointment_type=None, save=1, department=None):
item = create_healthcare_service_items()
frappe.db.set_value('Healthcare Settings', None, 'inpatient_visit_charge_item', item)
frappe.db.set_value('Healthcare Settings', None, 'op_consulting_charge_item', item)
appointment = frappe.new_doc('Patient Appointment')
appointment.patient = patient
appointment.practitioner = practitioner
- appointment.department = '_Test Medical Department'
+ appointment.department = department or '_Test Medical Department'
appointment.appointment_date = appointment_date
appointment.company = '_Test Company'
appointment.duration = 15
+ if service_unit:
+ appointment.service_unit = service_unit
if invoice:
appointment.mode_of_payment = 'Cash'
- appointment.paid_amount = 500
+ if appointment_type:
+ appointment.appointment_type = appointment_type
if procedure_template:
appointment.procedure_template = create_clinical_procedure_template().get('name')
- appointment.save(ignore_permissions=True)
+ if save:
+ appointment.save(ignore_permissions=True)
return appointment
def create_healthcare_service_items():
@@ -148,6 +255,7 @@
item.item_name = 'Consulting Charges'
item.item_group = 'Services'
item.is_stock_item = 0
+ item.stock_uom = 'Nos'
item.save()
return item.name
@@ -162,4 +270,29 @@
template.description = 'Knee Surgery and Rehab'
template.rate = 50000
template.save()
- return template
\ No newline at end of file
+ return template
+
+def create_appointment_type(args=None):
+ if not args:
+ args = frappe.local.form_dict
+
+ name = args.get('name') or 'Test Appointment Type wise Charge'
+
+ if frappe.db.exists('Appointment Type', name):
+ return frappe.get_doc('Appointment Type', name)
+
+ else:
+ item = create_healthcare_service_items()
+ items = [{
+ 'medical_department': '_Test Medical Department',
+ 'op_consulting_charge_item': item,
+ 'op_consulting_charge': 200
+ }]
+ return frappe.get_doc({
+ 'doctype': 'Appointment Type',
+ 'appointment_type': args.get('name') or 'Test Appointment Type wise Charge',
+ 'default_duration': args.get('default_duration') or 20,
+ 'color': args.get('color') or '#7575ff',
+ 'price_list': args.get('price_list') or frappe.db.get_value("Price List", {"selling": 1}),
+ 'items': args.get('items') or items
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json
index 15675f4..b646ff9 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.json
@@ -210,7 +210,7 @@
{
"fieldname": "drug_prescription",
"fieldtype": "Table",
- "label": "Items",
+ "label": "Drug Prescription",
"options": "Drug Prescription"
},
{
@@ -328,7 +328,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-05-16 21:00:08.644531",
+ "modified": "2020-11-30 10:39:00.783119",
"modified_by": "Administrator",
"module": "Healthcare",
"name": "Patient Encounter",
diff --git a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py
index 87f4249..cc21417 100644
--- a/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py
+++ b/erpnext/healthcare/doctype/patient_encounter/patient_encounter.py
@@ -17,10 +17,6 @@
def on_update(self):
if self.appointment:
frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed')
- update_encounter_medical_record(self)
-
- def after_insert(self):
- insert_encounter_to_medical_record(self)
def on_submit(self):
if self.therapies:
@@ -33,8 +29,6 @@
if self.inpatient_record and self.drug_prescription:
delete_ip_medication_order(self)
- delete_medical_record(self)
-
def set_title(self):
self.title = _('{0} with {1}').format(self.patient_name or self.patient,
self.practitioner_name or self.practitioner)[:100]
@@ -102,61 +96,7 @@
frappe.msgprint(_('Therapy Plan {0} created successfully.').format(frappe.bold(doc.name)), alert=True)
-def insert_encounter_to_medical_record(doc):
- subject = set_subject_field(doc)
- medical_record = frappe.new_doc('Patient Medical Record')
- medical_record.patient = doc.patient
- medical_record.subject = subject
- medical_record.status = 'Open'
- medical_record.communication_date = doc.encounter_date
- medical_record.reference_doctype = 'Patient Encounter'
- medical_record.reference_name = doc.name
- medical_record.reference_owner = doc.owner
- medical_record.save(ignore_permissions=True)
-
-
-def update_encounter_medical_record(encounter):
- medical_record_id = frappe.db.exists('Patient Medical Record', {'reference_name': encounter.name})
-
- if medical_record_id and medical_record_id[0][0]:
- subject = set_subject_field(encounter)
- frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject)
- else:
- insert_encounter_to_medical_record(encounter)
-
-
-def delete_medical_record(encounter):
- record = frappe.db.exists('Patient Medical Record', {'reference_name', encounter.name})
- if record:
- frappe.delete_doc('Patient Medical Record', record, force=1)
-
def delete_ip_medication_order(encounter):
record = frappe.db.exists('Inpatient Medication Order', {'patient_encounter': encounter.name})
if record:
- frappe.delete_doc('Inpatient Medication Order', record, force=1)
-
-
-def set_subject_field(encounter):
- subject = frappe.bold(_('Healthcare Practitioner: ')) + encounter.practitioner + '<br>'
- if encounter.symptoms:
- subject += frappe.bold(_('Symptoms: ')) + '<br>'
- for entry in encounter.symptoms:
- subject += cstr(entry.complaint) + '<br>'
- else:
- subject += frappe.bold(_('No Symptoms')) + '<br>'
-
- if encounter.diagnosis:
- subject += frappe.bold(_('Diagnosis: ')) + '<br>'
- for entry in encounter.diagnosis:
- subject += cstr(entry.diagnosis) + '<br>'
- else:
- subject += frappe.bold(_('No Diagnosis')) + '<br>'
-
- if encounter.drug_prescription:
- subject += '<br>' + _('Drug(s) Prescribed.')
- if encounter.lab_test_prescription:
- subject += '<br>' + _('Test(s) Prescribed.')
- if encounter.procedure_prescription:
- subject += '<br>' + _('Procedure(s) Prescribed.')
-
- return subject
+ frappe.delete_doc('Inpatient Medication Order', record, force=1)
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/healthcare/doctype/patient_history_custom_document_type/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/healthcare/doctype/patient_history_custom_document_type/__init__.py
diff --git a/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json
new file mode 100644
index 0000000..3025c7b
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "creation": "2020-11-25 13:40:23.054469",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "date_fieldname",
+ "add_edit_fields",
+ "selected_fields"
+ ],
+ "fields": [
+ {
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Document Type",
+ "options": "DocType",
+ "reqd": 1
+ },
+ {
+ "fieldname": "selected_fields",
+ "fieldtype": "Code",
+ "label": "Selected Fields",
+ "read_only": 1
+ },
+ {
+ "fieldname": "add_edit_fields",
+ "fieldtype": "Button",
+ "in_list_view": 1,
+ "label": "Add / Edit Fields"
+ },
+ {
+ "fieldname": "date_fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Date Fieldname",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-11-30 13:54:37.474671",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Patient History Custom Document Type",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.py b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.py
new file mode 100644
index 0000000..f0a1f92
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_history_custom_document_type/patient_history_custom_document_type.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class PatientHistoryCustomDocumentType(Document):
+ pass
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/healthcare/doctype/patient_history_settings/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/healthcare/doctype/patient_history_settings/__init__.py
diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js
new file mode 100644
index 0000000..453da6a
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.js
@@ -0,0 +1,133 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Patient History Settings', {
+ refresh: function(frm) {
+ frm.set_query('document_type', 'custom_doctypes', () => {
+ return {
+ filters: {
+ custom: 1,
+ is_submittable: 1,
+ module: 'Healthcare',
+ }
+ };
+ });
+ },
+
+ field_selector: function(frm, doc, standard=1) {
+ let document_fields = [];
+ if (doc.selected_fields)
+ document_fields = (JSON.parse(doc.selected_fields)).map(f => f.fieldname);
+
+ frm.call({
+ method: 'get_doctype_fields',
+ doc: frm.doc,
+ args: {
+ document_type: doc.document_type,
+ fields: document_fields
+ },
+ freeze: true,
+ callback: function(r) {
+ if (r.message) {
+ let doctype = 'Patient History Custom Document Type';
+ if (standard)
+ doctype = 'Patient History Standard Document Type';
+
+ frm.events.show_field_selector_dialog(frm, doc, doctype, r.message);
+ }
+ }
+ });
+ },
+
+ show_field_selector_dialog: function(frm, doc, doctype, doc_fields) {
+ let d = new frappe.ui.Dialog({
+ title: __('{0} Fields', [__(doc.document_type)]),
+ fields: [
+ {
+ label: __('Select Fields'),
+ fieldtype: 'MultiCheck',
+ fieldname: 'fields',
+ options: doc_fields,
+ columns: 2
+ }
+ ]
+ });
+
+ d.$body.prepend(`
+ <div class="columns-search">
+ <input type="text" placeholder="${__('Search')}" data-element="search" class="form-control input-xs">
+ </div>`
+ );
+
+ frappe.utils.setup_search(d.$body, '.unit-checkbox', '.label-area');
+
+ d.set_primary_action(__('Save'), () => {
+ let values = d.get_values().fields;
+
+ let selected_fields = [];
+
+ frappe.model.with_doctype(doc.document_type, function() {
+ for (let idx in values) {
+ let value = values[idx];
+
+ let field = frappe.get_meta(doc.document_type).fields.filter((df) => df.fieldname == value)[0];
+ if (field) {
+ selected_fields.push({
+ label: field.label,
+ fieldname: field.fieldname,
+ fieldtype: field.fieldtype
+ });
+ }
+ }
+
+ d.refresh();
+ frappe.model.set_value(doctype, doc.name, 'selected_fields', JSON.stringify(selected_fields));
+ });
+
+ d.hide();
+ });
+
+ d.show();
+ },
+
+ get_date_field_for_dt: function(frm, row) {
+ frm.call({
+ method: 'get_date_field_for_dt',
+ doc: frm.doc,
+ args: {
+ document_type: row.document_type
+ },
+ callback: function(data) {
+ if (data.message) {
+ frappe.model.set_value('Patient History Custom Document Type',
+ row.name, 'date_fieldname', data.message);
+ }
+ }
+ });
+ }
+});
+
+frappe.ui.form.on('Patient History Custom Document Type', {
+ document_type: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ if (row.document_type) {
+ frm.events.get_date_field_for_dt(frm, row);
+ }
+ },
+
+ add_edit_fields: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ if (row.document_type) {
+ frm.events.field_selector(frm, row, 0);
+ }
+ }
+});
+
+frappe.ui.form.on('Patient History Standard Document Type', {
+ add_edit_fields: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ if (row.document_type) {
+ frm.events.field_selector(frm, row);
+ }
+ }
+});
diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json
new file mode 100644
index 0000000..143e2c9
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.json
@@ -0,0 +1,55 @@
+{
+ "actions": [],
+ "creation": "2020-11-25 13:41:37.675518",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "standard_doctypes",
+ "section_break_2",
+ "custom_doctypes"
+ ],
+ "fields": [
+ {
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "custom_doctypes",
+ "fieldtype": "Table",
+ "label": "Custom Document Types",
+ "options": "Patient History Custom Document Type"
+ },
+ {
+ "fieldname": "standard_doctypes",
+ "fieldtype": "Table",
+ "label": "Standard Document Types",
+ "options": "Patient History Standard Document Type",
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2020-11-25 13:43:38.511771",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Patient History Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py
new file mode 100644
index 0000000..2e8c994
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_history_settings/patient_history_settings.py
@@ -0,0 +1,188 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from frappe import _
+from frappe.utils import cstr, cint
+from frappe.model.document import Document
+from erpnext.healthcare.page.patient_history.patient_history import get_patient_history_doctypes
+
+class PatientHistorySettings(Document):
+ def validate(self):
+ self.validate_submittable_doctypes()
+ self.validate_date_fieldnames()
+
+ def validate_submittable_doctypes(self):
+ for entry in self.custom_doctypes:
+ if not cint(frappe.db.get_value('DocType', entry.document_type, 'is_submittable')):
+ msg = _('Row #{0}: Document Type {1} is not submittable. ').format(
+ entry.idx, frappe.bold(entry.document_type))
+ msg += _('Patient Medical Record can only be created for submittable document types.')
+ frappe.throw(msg)
+
+ def validate_date_fieldnames(self):
+ for entry in self.custom_doctypes:
+ field = frappe.get_meta(entry.document_type).get_field(entry.date_fieldname)
+ if not field:
+ frappe.throw(_('Row #{0}: No such Field named {1} found in the Document Type {2}.').format(
+ entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
+
+ if field.fieldtype not in ['Date', 'Datetime']:
+ frappe.throw(_('Row #{0}: Field {1} in Document Type {2} is not a Date / Datetime field.').format(
+ entry.idx, frappe.bold(entry.date_fieldname), frappe.bold(entry.document_type)))
+
+ def get_doctype_fields(self, document_type, fields):
+ multicheck_fields = []
+ doc_fields = frappe.get_meta(document_type).fields
+
+ for field in doc_fields:
+ if field.fieldtype not in frappe.model.no_value_fields or \
+ field.fieldtype in frappe.model.table_fields and not field.hidden:
+ multicheck_fields.append({
+ 'label': field.label,
+ 'value': field.fieldname,
+ 'checked': 1 if field.fieldname in fields else 0
+ })
+
+ return multicheck_fields
+
+ def get_date_field_for_dt(self, document_type):
+ meta = frappe.get_meta(document_type)
+ date_fields = meta.get('fields', {
+ 'fieldtype': ['in', ['Date', 'Datetime']]
+ })
+
+ if date_fields:
+ return date_fields[0].get('fieldname')
+
+def create_medical_record(doc, method=None):
+ medical_record_required = validate_medical_record_required(doc)
+ if not medical_record_required:
+ return
+
+ if frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name }):
+ return
+
+ subject = set_subject_field(doc)
+ date_field = get_date_field(doc.doctype)
+ medical_record = frappe.new_doc('Patient Medical Record')
+ medical_record.patient = doc.patient
+ medical_record.subject = subject
+ medical_record.status = 'Open'
+ medical_record.communication_date = doc.get(date_field)
+ medical_record.reference_doctype = doc.doctype
+ medical_record.reference_name = doc.name
+ medical_record.reference_owner = doc.owner
+ medical_record.save(ignore_permissions=True)
+
+
+def update_medical_record(doc, method=None):
+ medical_record_required = validate_medical_record_required(doc)
+ if not medical_record_required:
+ return
+
+ medical_record_id = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name })
+
+ if medical_record_id:
+ subject = set_subject_field(doc)
+ frappe.db.set_value('Patient Medical Record', medical_record_id[0][0], 'subject', subject)
+ else:
+ create_medical_record(doc)
+
+
+def delete_medical_record(doc, method=None):
+ medical_record_required = validate_medical_record_required(doc)
+ if not medical_record_required:
+ return
+
+ record = frappe.db.exists('Patient Medical Record', { 'reference_name': doc.name })
+ if record:
+ frappe.delete_doc('Patient Medical Record', record, force=1)
+
+
+def set_subject_field(doc):
+ from frappe.utils.formatters import format_value
+
+ meta = frappe.get_meta(doc.doctype)
+ subject = ''
+ patient_history_fields = get_patient_history_fields(doc)
+
+ for entry in patient_history_fields:
+ fieldname = entry.get('fieldname')
+ if entry.get('fieldtype') == 'Table' and doc.get(fieldname):
+ formatted_value = get_formatted_value_for_table_field(doc.get(fieldname), meta.get_field(fieldname))
+ subject += frappe.bold(_(entry.get('label')) + ': ') + '<br>' + cstr(formatted_value) + '<br>'
+
+ else:
+ if doc.get(fieldname):
+ formatted_value = format_value(doc.get(fieldname), meta.get_field(fieldname), doc)
+ subject += frappe.bold(_(entry.get('label')) + ': ') + cstr(formatted_value) + '<br>'
+
+ return subject
+
+
+def get_date_field(doctype):
+ dt = get_patient_history_config_dt(doctype)
+
+ return frappe.db.get_value(dt, { 'document_type': doctype }, 'date_fieldname')
+
+
+def get_patient_history_fields(doc):
+ dt = get_patient_history_config_dt(doc.doctype)
+ patient_history_fields = frappe.db.get_value(dt, { 'document_type': doc.doctype }, 'selected_fields')
+
+ if patient_history_fields:
+ return json.loads(patient_history_fields)
+
+
+def get_formatted_value_for_table_field(items, df):
+ child_meta = frappe.get_meta(df.options)
+
+ table_head = ''
+ table_row = ''
+ html = ''
+ create_head = True
+ for item in items:
+ table_row += '<tr>'
+ for cdf in child_meta.fields:
+ if cdf.in_list_view:
+ if create_head:
+ table_head += '<td>' + cdf.label + '</td>'
+ if item.get(cdf.fieldname):
+ table_row += '<td>' + str(item.get(cdf.fieldname)) + '</td>'
+ else:
+ table_row += '<td></td>'
+ create_head = False
+ table_row += '</tr>'
+
+ html += "<table class='table table-condensed table-bordered'>" + table_head + table_row + "</table>"
+
+ return html
+
+
+def get_patient_history_config_dt(doctype):
+ if frappe.db.get_value('DocType', doctype, 'custom'):
+ return 'Patient History Custom Document Type'
+ else:
+ return 'Patient History Standard Document Type'
+
+
+def validate_medical_record_required(doc):
+ if frappe.flags.in_patch or frappe.flags.in_install or frappe.flags.in_setup_wizard \
+ or get_module(doc) != 'Healthcare':
+ return False
+
+ if doc.doctype not in get_patient_history_doctypes():
+ return False
+
+ return True
+
+def get_module(doc):
+ module = doc.meta.module
+ if not module:
+ module = frappe.db.get_value('DocType', doc.doctype, 'module')
+
+ return module
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
new file mode 100644
index 0000000..c93b788
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_history_settings/test_patient_history_settings.py
@@ -0,0 +1,104 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+import frappe
+import unittest
+import json
+from frappe.utils import getdate
+from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_patient
+
+class TestPatientHistorySettings(unittest.TestCase):
+ def setUp(self):
+ dt = create_custom_doctype()
+ settings = frappe.get_single("Patient History Settings")
+ settings.append("custom_doctypes", {
+ "document_type": dt.name,
+ "date_fieldname": "date",
+ "selected_fields": json.dumps([{
+ "label": "Date",
+ "fieldname": "date",
+ "fieldtype": "Date"
+ },
+ {
+ "label": "Rating",
+ "fieldname": "rating",
+ "fieldtype": "Rating"
+ },
+ {
+ "label": "Feedback",
+ "fieldname": "feedback",
+ "fieldtype": "Small Text"
+ }])
+ })
+ settings.save()
+
+ def test_custom_doctype_medical_record(self):
+ # tests for medical record creation of standard doctypes in test_patient_medical_record.py
+ patient = create_patient()
+ doc = create_doc(patient)
+
+ # check for medical record
+ medical_rec = frappe.db.exists("Patient Medical Record", {"status": "Open", "reference_name": doc.name})
+ self.assertTrue(medical_rec)
+
+ medical_rec = frappe.get_doc("Patient Medical Record", medical_rec)
+ expected_subject = "<b>Date: </b>{0}<br><b>Rating: </b>3<br><b>Feedback: </b>Test Patient History Settings<br>".format(
+ frappe.utils.format_date(getdate()))
+ self.assertEqual(medical_rec.subject, expected_subject)
+ self.assertEqual(medical_rec.patient, patient)
+ self.assertEqual(medical_rec.communication_date, getdate())
+
+
+def create_custom_doctype():
+ if not frappe.db.exists("DocType", "Test Patient Feedback"):
+ doc = frappe.get_doc({
+ "doctype": "DocType",
+ "module": "Healthcare",
+ "custom": 1,
+ "is_submittable": 1,
+ "fields": [{
+ "label": "Date",
+ "fieldname": "date",
+ "fieldtype": "Date"
+ },
+ {
+ "label": "Patient",
+ "fieldname": "patient",
+ "fieldtype": "Link",
+ "options": "Patient"
+ },
+ {
+ "label": "Rating",
+ "fieldname": "rating",
+ "fieldtype": "Rating"
+ },
+ {
+ "label": "Feedback",
+ "fieldname": "feedback",
+ "fieldtype": "Small Text"
+ }],
+ "permissions": [{
+ "role": "System Manager",
+ "read": 1
+ }],
+ "name": "Test Patient Feedback",
+ })
+ doc.insert()
+ return doc
+ else:
+ return frappe.get_doc("DocType", "Test Patient Feedback")
+
+
+def create_doc(patient):
+ doc = frappe.get_doc({
+ "doctype": "Test Patient Feedback",
+ "patient": patient,
+ "date": getdate(),
+ "rating": 3,
+ "feedback": "Test Patient History Settings"
+ }).insert()
+ doc.submit()
+
+ return doc
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/healthcare/doctype/patient_history_standard_document_type/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/healthcare/doctype/patient_history_standard_document_type/__init__.py
diff --git a/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json
new file mode 100644
index 0000000..b43099c
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.json
@@ -0,0 +1,57 @@
+{
+ "actions": [],
+ "creation": "2020-11-25 13:39:36.014814",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "document_type",
+ "date_fieldname",
+ "add_edit_fields",
+ "selected_fields"
+ ],
+ "fields": [
+ {
+ "fieldname": "document_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Document Type",
+ "options": "DocType",
+ "read_only": 1,
+ "reqd": 1
+ },
+ {
+ "fieldname": "selected_fields",
+ "fieldtype": "Code",
+ "label": "Selected Fields",
+ "read_only": 1
+ },
+ {
+ "fieldname": "add_edit_fields",
+ "fieldtype": "Button",
+ "in_list_view": 1,
+ "label": "Add / Edit Fields"
+ },
+ {
+ "fieldname": "date_fieldname",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Date Fieldname",
+ "read_only": 1,
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-11-30 13:54:56.773325",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Patient History Standard Document Type",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.py b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.py
new file mode 100644
index 0000000..2d94911
--- /dev/null
+++ b/erpnext/healthcare/doctype/patient_history_standard_document_type/patient_history_standard_document_type.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class PatientHistoryStandardDocumentType(Document):
+ pass
diff --git a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
index aa85a23..c1d9872 100644
--- a/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
+++ b/erpnext/healthcare/doctype/patient_medical_record/test_patient_medical_record.py
@@ -6,16 +6,19 @@
import frappe
from frappe.utils import nowdate
from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_encounter, create_healthcare_docs, create_appointment
+from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile
class TestPatientMedicalRecord(unittest.TestCase):
def setUp(self):
frappe.db.set_value('Healthcare Settings', None, 'enable_free_follow_ups', 0)
frappe.db.set_value('Healthcare Settings', None, 'automate_appointment_invoicing', 1)
+ make_pos_profile()
def test_medical_record(self):
patient, medical_department, practitioner = create_healthcare_docs()
appointment = create_appointment(patient, practitioner, nowdate(), invoice=1)
encounter = create_encounter(appointment)
+
# check for encounter
medical_rec = frappe.db.exists('Patient Medical Record', {'status': 'Open', 'reference_name': encounter.name})
self.assertTrue(medical_rec)
diff --git a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
index a061c66..7fb159d 100644
--- a/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/test_therapy_plan.py
@@ -5,10 +5,10 @@
import frappe
import unittest
-from frappe.utils import getdate, flt
+from frappe.utils import getdate, flt, nowdate
from erpnext.healthcare.doctype.therapy_type.test_therapy_type import create_therapy_type
from erpnext.healthcare.doctype.therapy_plan.therapy_plan import make_therapy_session, make_sales_invoice
-from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient
+from erpnext.healthcare.doctype.patient_appointment.test_patient_appointment import create_healthcare_docs, create_patient, create_appointment
class TestTherapyPlan(unittest.TestCase):
def test_creation_on_encounter_submission(self):
@@ -28,6 +28,15 @@
frappe.get_doc(session).submit()
self.assertEquals(frappe.db.get_value('Therapy Plan', plan.name, 'status'), 'Completed')
+ patient, medical_department, practitioner = create_healthcare_docs()
+ appointment = create_appointment(patient, practitioner, nowdate())
+ session = make_therapy_session(plan.name, plan.patient, 'Basic Rehab', '_Test Company', appointment.name)
+ session = frappe.get_doc(session)
+ session.submit()
+ self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Closed')
+ session.cancel()
+ self.assertEquals(frappe.db.get_value('Patient Appointment', appointment.name, 'status'), 'Open')
+
def test_therapy_plan_from_template(self):
patient = create_patient()
template = create_therapy_plan_template()
diff --git a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
index bc0ff1a..ac01c60 100644
--- a/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
+++ b/erpnext/healthcare/doctype/therapy_plan/therapy_plan.py
@@ -47,7 +47,7 @@
@frappe.whitelist()
-def make_therapy_session(therapy_plan, patient, therapy_type, company):
+def make_therapy_session(therapy_plan, patient, therapy_type, company, appointment=None):
therapy_type = frappe.get_doc('Therapy Type', therapy_type)
therapy_session = frappe.new_doc('Therapy Session')
@@ -58,6 +58,7 @@
therapy_session.duration = therapy_type.default_duration
therapy_session.rate = therapy_type.rate
therapy_session.exercises = therapy_type.exercises
+ therapy_session.appointment = appointment
if frappe.flags.in_test:
therapy_session.start_date = today()
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.js b/erpnext/healthcare/doctype/therapy_session/therapy_session.js
index a2b01c9..fd20003 100644
--- a/erpnext/healthcare/doctype/therapy_session/therapy_session.js
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.js
@@ -19,6 +19,15 @@
}
};
});
+
+ frm.set_query('appointment', function() {
+
+ return {
+ filters: {
+ 'status': ['in', ['Open', 'Scheduled']]
+ }
+ };
+ });
},
refresh: function(frm) {
diff --git a/erpnext/healthcare/doctype/therapy_session/therapy_session.py b/erpnext/healthcare/doctype/therapy_session/therapy_session.py
index 85d0970..51f267f 100644
--- a/erpnext/healthcare/doctype/therapy_session/therapy_session.py
+++ b/erpnext/healthcare/doctype/therapy_session/therapy_session.py
@@ -41,9 +41,15 @@
def on_submit(self):
self.update_sessions_count_in_therapy_plan()
- insert_session_medical_record(self)
+
+ def on_update(self):
+ if self.appointment:
+ frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Closed')
def on_cancel(self):
+ if self.appointment:
+ frappe.db.set_value('Patient Appointment', self.appointment, 'status', 'Open')
+
self.update_sessions_count_in_therapy_plan(on_cancel=True)
def update_sessions_count_in_therapy_plan(self, on_cancel=False):
@@ -135,23 +141,3 @@
item.reference_dt = 'Therapy Session'
item.reference_dn = therapy.name
return item
-
-
-def insert_session_medical_record(doc):
- subject = frappe.bold(_('Therapy: ')) + cstr(doc.therapy_type) + '<br>'
- if doc.therapy_plan:
- subject += frappe.bold(_('Therapy Plan: ')) + cstr(doc.therapy_plan) + '<br>'
- if doc.practitioner:
- subject += frappe.bold(_('Healthcare Practitioner: ')) + doc.practitioner
- subject += frappe.bold(_('Total Counts Targeted: ')) + cstr(doc.total_counts_targeted) + '<br>'
- subject += frappe.bold(_('Total Counts Completed: ')) + cstr(doc.total_counts_completed) + '<br>'
-
- medical_record = frappe.new_doc('Patient Medical Record')
- medical_record.patient = doc.patient
- medical_record.subject = subject
- medical_record.status = 'Open'
- medical_record.communication_date = doc.start_date
- medical_record.reference_doctype = 'Therapy Session'
- medical_record.reference_name = doc.name
- medical_record.reference_owner = doc.owner
- medical_record.save(ignore_permissions=True)
\ No newline at end of file
diff --git a/erpnext/healthcare/doctype/vital_signs/vital_signs.py b/erpnext/healthcare/doctype/vital_signs/vital_signs.py
index 69d81ff..35c823d 100644
--- a/erpnext/healthcare/doctype/vital_signs/vital_signs.py
+++ b/erpnext/healthcare/doctype/vital_signs/vital_signs.py
@@ -12,47 +12,7 @@
def validate(self):
self.set_title()
- def on_submit(self):
- insert_vital_signs_to_medical_record(self)
-
- def on_cancel(self):
- delete_vital_signs_from_medical_record(self)
-
def set_title(self):
self.title = _('{0} on {1}').format(self.patient_name or self.patient,
frappe.utils.format_date(self.signs_date))[:100]
-def insert_vital_signs_to_medical_record(doc):
- subject = set_subject_field(doc)
- medical_record = frappe.new_doc('Patient Medical Record')
- medical_record.patient = doc.patient
- medical_record.subject = subject
- medical_record.status = 'Open'
- medical_record.communication_date = doc.signs_date
- medical_record.reference_doctype = 'Vital Signs'
- medical_record.reference_name = doc.name
- medical_record.reference_owner = doc.owner
- medical_record.flags.ignore_mandatory = True
- medical_record.save(ignore_permissions=True)
-
-def delete_vital_signs_from_medical_record(doc):
- medical_record = frappe.db.get_value('Patient Medical Record', {'reference_name': doc.name})
- if medical_record:
- frappe.delete_doc('Patient Medical Record', medical_record)
-
-def set_subject_field(doc):
- subject = ''
- if doc.temperature:
- subject += frappe.bold(_('Temperature: ')) + cstr(doc.temperature) + '<br>'
- if doc.pulse:
- subject += frappe.bold(_('Pulse: ')) + cstr(doc.pulse) + '<br>'
- if doc.respiratory_rate:
- subject += frappe.bold(_('Respiratory Rate: ')) + cstr(doc.respiratory_rate) + '<br>'
- if doc.bp:
- subject += frappe.bold(_('BP: ')) + cstr(doc.bp) + '<br>'
- if doc.bmi:
- subject += frappe.bold(_('BMI: ')) + cstr(doc.bmi) + '<br>'
- if doc.nutrition_note:
- subject += frappe.bold(_('Note: ')) + cstr(doc.nutrition_note) + '<br>'
-
- return subject
diff --git a/erpnext/healthcare/page/patient_history/patient_history.css b/erpnext/healthcare/page/patient_history/patient_history.css
index 865d6ab..1bb5891 100644
--- a/erpnext/healthcare/page/patient_history/patient_history.css
+++ b/erpnext/healthcare/page/patient_history/patient_history.css
@@ -109,6 +109,11 @@
padding-right: 0px;
}
+.patient-history-filter {
+ margin-left: 35px;
+ width: 25%;
+}
+
#page-medical_record .plot-wrapper {
padding: 20px 15px;
border-bottom: 1px solid #d1d8dd;
diff --git a/erpnext/healthcare/page/patient_history/patient_history.html b/erpnext/healthcare/page/patient_history/patient_history.html
index 7a9446d..be486c6 100644
--- a/erpnext/healthcare/page/patient_history/patient_history.html
+++ b/erpnext/healthcare/page/patient_history/patient_history.html
@@ -1,6 +1,5 @@
<div class="col-sm-12">
<div class="col-sm-3">
- <p class="text-center">{%= __("Select Patient") %}</p>
<p class="patient" style="margin: auto; max-width: 300px; margin-bottom: 20px;"></p>
<div class="patient_details" style="z-index=0"></div>
</div>
@@ -11,6 +10,13 @@
<div id="chart" class="col-sm-12 patient_vital_charts">
</div>
</div>
+ <div class="header-separator col-sm-12 d-flex border-bottom py-3" style="display:none"></div>
+ <div class="row">
+ <div class="col-sm-12 d-flex">
+ <div class="patient-history-filter doctype-filter"></div>
+ <div class="patient-history-filter date-filter"></div>
+ </div>
+ </div>
<div class="col-sm-12 patient_documents_list">
</div>
<div class="col-sm-12 text-center py-3">
diff --git a/erpnext/healthcare/page/patient_history/patient_history.js b/erpnext/healthcare/page/patient_history/patient_history.js
index fe5b7bc..54343aa 100644
--- a/erpnext/healthcare/page/patient_history/patient_history.js
+++ b/erpnext/healthcare/page/patient_history/patient_history.js
@@ -1,141 +1,225 @@
-frappe.provide("frappe.patient_history");
+frappe.provide('frappe.patient_history');
frappe.pages['patient_history'].on_page_load = function(wrapper) {
- var me = this;
- var page = frappe.ui.make_app_page({
+ let me = this;
+ let page = frappe.ui.make_app_page({
parent: wrapper,
title: 'Patient History',
single_column: true
});
- frappe.breadcrumbs.add("Healthcare");
+ frappe.breadcrumbs.add('Healthcare');
let pid = '';
- page.main.html(frappe.render_template("patient_history", {}));
- var patient = frappe.ui.form.make_control({
- parent: page.main.find(".patient"),
+ page.main.html(frappe.render_template('patient_history', {}));
+ page.main.find('.header-separator').hide();
+
+ let patient = frappe.ui.form.make_control({
+ parent: page.main.find('.patient'),
df: {
- fieldtype: "Link",
- options: "Patient",
- fieldname: "patient",
- change: function(){
- if(pid != patient.get_value() && patient.get_value()){
+ fieldtype: 'Link',
+ options: 'Patient',
+ fieldname: 'patient',
+ placeholder: __('Select Patient'),
+ only_select: true,
+ change: function() {
+ let patient_id = patient.get_value();
+ if (pid != patient_id && patient_id) {
me.start = 0;
- me.page.main.find(".patient_documents_list").html("");
- get_documents(patient.get_value(), me);
- show_patient_info(patient.get_value(), me);
- show_patient_vital_charts(patient.get_value(), me, "bp", "mmHg", "Blood Pressure");
+ me.page.main.find('.patient_documents_list').html('');
+ setup_filters(patient_id, me);
+ get_documents(patient_id, me);
+ show_patient_info(patient_id, me);
+ show_patient_vital_charts(patient_id, me, 'bp', 'mmHg', 'Blood Pressure');
}
- pid = patient.get_value();
+ pid = patient_id;
}
},
- only_input: true,
});
patient.refresh();
- if (frappe.route_options){
+ if (frappe.route_options) {
patient.set_value(frappe.route_options.patient);
}
- this.page.main.on("click", ".btn-show-chart", function() {
- var btn_show_id = $(this).attr("data-show-chart-id"), pts = $(this).attr("data-pts");
- var title = $(this).attr("data-title");
+ this.page.main.on('click', '.btn-show-chart', function() {
+ let btn_show_id = $(this).attr('data-show-chart-id'), pts = $(this).attr('data-pts');
+ let title = $(this).attr('data-title');
show_patient_vital_charts(patient.get_value(), me, btn_show_id, pts, title);
});
- this.page.main.on("click", ".btn-more", function() {
- var doctype = $(this).attr("data-doctype"), docname = $(this).attr("data-docname");
- if(me.page.main.find("."+docname).parent().find('.document-html').attr('data-fetched') == "1"){
- me.page.main.find("."+docname).hide();
- me.page.main.find("."+docname).parent().find('.document-html').show();
- }else{
- if(doctype && docname){
- let exclude = ["patient", "patient_name", 'patient_sex', "encounter_date"];
+ this.page.main.on('click', '.btn-more', function() {
+ let doctype = $(this).attr('data-doctype'), docname = $(this).attr('data-docname');
+ if (me.page.main.find('.'+docname).parent().find('.document-html').attr('data-fetched') == '1') {
+ me.page.main.find('.'+docname).hide();
+ me.page.main.find('.'+docname).parent().find('.document-html').show();
+ } else {
+ if (doctype && docname) {
+ let exclude = ['patient', 'patient_name', 'patient_sex', 'encounter_date'];
frappe.call({
- method: "erpnext.healthcare.utils.render_doc_as_html",
+ method: 'erpnext.healthcare.utils.render_doc_as_html',
args:{
doctype: doctype,
docname: docname,
exclude_fields: exclude
},
+ freeze: true,
callback: function(r) {
- if (r.message){
- me.page.main.find("."+docname).hide();
- me.page.main.find("."+docname).parent().find('.document-html').html(r.message.html+"\
- <div align='center'><a class='btn octicon octicon-chevron-up btn-default btn-xs\
- btn-less' data-doctype='"+doctype+"' data-docname='"+docname+"'></a></div>");
- me.page.main.find("."+docname).parent().find('.document-html').show();
- me.page.main.find("."+docname).parent().find('.document-html').attr('data-fetched', "1");
+ if (r.message) {
+ me.page.main.find('.' + docname).hide();
+
+ me.page.main.find('.' + docname).parent().find('.document-html').html(
+ `${r.message.html}
+ <div align='center'>
+ <a class='btn octicon octicon-chevron-up btn-default btn-xs btn-less'
+ data-doctype='${doctype}'
+ data-docname='${docname}'>
+ </a>
+ </div>
+ `);
+
+ me.page.main.find('.' + docname).parent().find('.document-html').show();
+ me.page.main.find('.' + docname).parent().find('.document-html').attr('data-fetched', '1');
}
- },
- freeze: true
+ }
});
}
}
});
- this.page.main.on("click", ".btn-less", function() {
- var docname = $(this).attr("data-docname");
- me.page.main.find("."+docname).parent().find('.document-id').show();
- me.page.main.find("."+docname).parent().find('.document-html').hide();
+ this.page.main.on('click', '.btn-less', function() {
+ let docname = $(this).attr('data-docname');
+ me.page.main.find('.' + docname).parent().find('.document-id').show();
+ me.page.main.find('.' + docname).parent().find('.document-html').hide();
});
me.start = 0;
- me.page.main.on("click", ".btn-get-records", function(){
+ me.page.main.on('click', '.btn-get-records', function() {
get_documents(patient.get_value(), me);
});
};
-var get_documents = function(patient, me){
+let setup_filters = function(patient, me) {
+ $('.doctype-filter').empty();
+ frappe.xcall(
+ 'erpnext.healthcare.page.patient_history.patient_history.get_patient_history_doctypes'
+ ).then(document_types => {
+ let doctype_filter = frappe.ui.form.make_control({
+ parent: $('.doctype-filter'),
+ df: {
+ fieldtype: 'MultiSelectList',
+ fieldname: 'document_type',
+ placeholder: __('Select Document Type'),
+ input_class: 'input-xs',
+ change: () => {
+ me.start = 0;
+ me.page.main.find('.patient_documents_list').html('');
+ get_documents(patient, me, doctype_filter.get_value(), date_range_field.get_value());
+ },
+ get_data: () => {
+ return document_types.map(document_type => {
+ return {
+ description: document_type,
+ value: document_type
+ };
+ });
+ },
+ }
+ });
+ doctype_filter.refresh();
+
+ $('.date-filter').empty();
+ let date_range_field = frappe.ui.form.make_control({
+ df: {
+ fieldtype: 'DateRange',
+ fieldname: 'date_range',
+ placeholder: __('Date Range'),
+ input_class: 'input-xs',
+ change: () => {
+ let selected_date_range = date_range_field.get_value();
+ if (selected_date_range && selected_date_range.length === 2) {
+ me.start = 0;
+ me.page.main.find('.patient_documents_list').html('');
+ get_documents(patient, me, doctype_filter.get_value(), selected_date_range);
+ }
+ }
+ },
+ parent: $('.date-filter')
+ });
+ date_range_field.refresh();
+ });
+};
+
+let get_documents = function(patient, me, document_types="", selected_date_range="") {
+ let filters = {
+ name: patient,
+ start: me.start,
+ page_length: 20
+ };
+ if (document_types)
+ filters['document_types'] = document_types;
+ if (selected_date_range)
+ filters['date_range'] = selected_date_range;
+
frappe.call({
- "method": "erpnext.healthcare.page.patient_history.patient_history.get_feed",
- args: {
- name: patient,
- start: me.start,
- page_length: 20
- },
- callback: function (r) {
- var data = r.message;
- if(data.length){
+ 'method': 'erpnext.healthcare.page.patient_history.patient_history.get_feed',
+ args: filters,
+ callback: function(r) {
+ let data = r.message;
+ if (data.length) {
add_to_records(me, data);
- }else{
- me.page.main.find(".patient_documents_list").append("<div class='text-muted' align='center'><br><br>No more records..<br><br></div>");
- me.page.main.find(".btn-get-records").hide();
+ } else {
+ me.page.main.find('.patient_documents_list').append(`
+ <div class='text-muted' align='center'>
+ <br><br>${__('No more records..')}<br><br>
+ </div>`);
+ me.page.main.find('.btn-get-records').hide();
}
}
});
};
-var add_to_records = function(me, data){
- var details = "<ul class='nav nav-pills nav-stacked'>";
- var i;
- for(i=0; i<data.length; i++){
- if(data[i].reference_doctype){
+let add_to_records = function(me, data) {
+ let details = "<ul class='nav nav-pills nav-stacked'>";
+ let i;
+ for (i=0; i<data.length; i++) {
+ if (data[i].reference_doctype) {
let label = '';
- if(data[i].subject){
- label += "<br/>"+data[i].subject;
+ if (data[i].subject) {
+ label += "<br/>" + data[i].subject;
}
data[i] = add_date_separator(data[i]);
- if(frappe.user_info(data[i].owner).image){
+
+ if (frappe.user_info(data[i].owner).image) {
data[i].imgsrc = frappe.utils.get_file_link(frappe.user_info(data[i].owner).image);
- }
- else{
+ } else {
data[i].imgsrc = false;
}
- var time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``;
- time_line_heading += data[i].reference_doctype + " - "+ data[i].reference_name;
- details += `<li data-toggle='pill' class='patient_doc_menu'
- data-doctype='${data[i].reference_doctype}' data-docname='${data[i].reference_name}'>
- <div class='col-sm-12 d-flex border-bottom py-3'>`;
- if (data[i].imgsrc){
- details += `<span class='mr-3'>
- <img class='avtar' src='${data[i].imgsrc}' width='32' height='32'>
- </img>
- </span>`;
- }else{
- details += `<span class='mr-3 avatar avatar-small' style='width:32px; height:32px;'><div align='center' class='standard-image'
- style='background-color: #fafbfc;'>${data[i].practitioner ? data[i].practitioner.charAt(0) : "U"}</div></span>`;
+
+ let time_line_heading = data[i].practitioner ? `${data[i].practitioner} ` : ``;
+ time_line_heading += data[i].reference_doctype + " - " +
+ `<a onclick="frappe.set_route('Form', '${data[i].reference_doctype}', '${data[i].reference_name}');">
+ ${data[i].reference_name}
+ </a>`;
+
+ details += `
+ <li data-toggle='pill' class='patient_doc_menu'
+ data-doctype='${data[i].reference_doctype}' data-docname='${data[i].reference_name}'>
+ <div class='col-sm-12 d-flex border-bottom py-3'>`;
+
+ if (data[i].imgsrc) {
+ details += `
+ <span class='mr-3'>
+ <img class='avtar' src='${data[i].imgsrc}' width='32' height='32'></img>
+ </span>`;
+ } else {
+ details += `<span class='mr-3 avatar avatar-small' style='width:32px; height:32px;'>
+ <div align='center' class='standard-image' style='background-color: #fafbfc;'>
+ ${data[i].practitioner ? data[i].practitioner.charAt(0) : 'U'}
+ </div>
+ </span>`;
}
+
details += `<div class='d-flex flex-column width-full'>
<div>
- `+time_line_heading+` on
+ `+time_line_heading+`
<span>
${data[i].date_sep}
</span>
@@ -156,133 +240,150 @@
</li>`;
}
}
- details += "</ul>";
- me.page.main.find(".patient_documents_list").append(details);
+
+ details += '</ul>';
+ me.page.main.find('.patient_documents_list').append(details);
me.start += data.length;
- if(data.length===20){
+
+ if (data.length === 20) {
me.page.main.find(".btn-get-records").show();
- }else{
+ } else {
me.page.main.find(".btn-get-records").hide();
- me.page.main.find(".patient_documents_list").append("<div class='text-muted' align='center'><br><br>No more records..<br><br></div>");
+ me.page.main.find(".patient_documents_list").append(`
+ <div class='text-muted' align='center'>
+ <br><br>${__('No more records..')}<br><br>
+ </div>`);
}
};
-var add_date_separator = function(data) {
- var date = frappe.datetime.str_to_obj(data.creation);
+let add_date_separator = function(data) {
+ let date = frappe.datetime.str_to_obj(data.communication_date);
+ let pdate = '';
+ let diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date));
- var diff = frappe.datetime.get_day_diff(frappe.datetime.get_today(), frappe.datetime.obj_to_str(date));
- if(diff < 1) {
- var pdate = 'Today';
- } else if(diff < 2) {
- pdate = 'Yesterday';
+ if (diff < 1) {
+ pdate = __('Today');
+ } else if (diff < 2) {
+ pdate = __('Yesterday');
} else {
- pdate = frappe.datetime.global_date_format(date);
+ pdate = __('on ') + frappe.datetime.global_date_format(date);
}
data.date_sep = pdate;
return data;
};
-var show_patient_info = function(patient, me){
+let show_patient_info = function(patient, me) {
frappe.call({
- "method": "erpnext.healthcare.doctype.patient.patient.get_patient_detail",
+ 'method': 'erpnext.healthcare.doctype.patient.patient.get_patient_detail',
args: {
patient: patient
},
- callback: function (r) {
- var data = r.message;
- var details = "";
- if(data.image){
- details += "<div><img class='thumbnail' width=75% src='"+data.image+"'></div>";
+ callback: function(r) {
+ let data = r.message;
+ let details = '';
+ if (data.image) {
+ details += `<div><img class='thumbnail' width=75% src='${data.image}'></div>`;
}
- details += "<b>" + data.patient_name +"</b><br>" + data.sex;
- if(data.email) details += "<br>" + data.email;
- if(data.mobile) details += "<br>" + data.mobile;
- if(data.occupation) details += "<br><br><b>Occupation :</b> " + data.occupation;
- if(data.blood_group) details += "<br><b>Blood group : </b> " + data.blood_group;
- if(data.allergies) details += "<br><br><b>Allergies : </b> "+ data.allergies.replace("\n", "<br>");
- if(data.medication) details += "<br><b>Medication : </b> "+ data.medication.replace("\n", "<br>");
- if(data.alcohol_current_use) details += "<br><br><b>Alcohol use : </b> "+ data.alcohol_current_use;
- if(data.alcohol_past_use) details += "<br><b>Alcohol past use : </b> "+ data.alcohol_past_use;
- if(data.tobacco_current_use) details += "<br><b>Tobacco use : </b> "+ data.tobacco_current_use;
- if(data.tobacco_past_use) details += "<br><b>Tobacco past use : </b> "+ data.tobacco_past_use;
- if(data.medical_history) details += "<br><br><b>Medical history : </b> "+ data.medical_history.replace("\n", "<br>");
- if(data.surgical_history) details += "<br><b>Surgical history : </b> "+ data.surgical_history.replace("\n", "<br>");
- if(data.surrounding_factors) details += "<br><br><b>Occupational hazards : </b> "+ data.surrounding_factors.replace("\n", "<br>");
- if(data.other_risk_factors) details += "<br><b>Other risk factors : </b> " + data.other_risk_factors.replace("\n", "<br>");
- if(data.patient_details) details += "<br><br><b>More info : </b> " + data.patient_details.replace("\n", "<br>");
- if(details){
- details = "<div style='padding-left:10px; font-size:13px;' align='center'>" + details + "</div>";
+ details += `<b> ${data.patient_name} </b><br> ${data.sex}`;
+ if (data.email) details += `<br> ${data.email}`;
+ if (data.mobile) details += `<br> ${data.mobile}`;
+ if (data.occupation) details += `<br><br><b> ${__('Occupation')} : </b> ${data.occupation}`;
+ if (data.blood_group) details += `<br><b> ${__('Blood Group')} : </b> ${data.blood_group}`;
+ if (data.allergies) details += `<br><br><b> ${__('Allerigies')} : </b> ${data.allergies.replace("\n", ", ")}`;
+ if (data.medication) details += `<br><b> ${__('Medication')} : </b> ${data.medication.replace("\n", ", ")}`;
+ if (data.alcohol_current_use) details += `<br><br><b> ${__('Alcohol use')} : </b> ${data.alcohol_current_use}`;
+ if (data.alcohol_past_use) details += `<br><b> ${__('Alcohol past use')} : </b> ${data.alcohol_past_use}`;
+ if (data.tobacco_current_use) details += `<br><b> ${__('Tobacco use')} : </b> ${data.tobacco_current_use}`;
+ if (data.tobacco_past_use) details += `<br><b> ${__('Tobacco past use')} : </b> ${data.tobacco_past_use}`;
+ if (data.medical_history) details += `<br><br><b> ${__('Medical history')} : </b> ${data.medical_history.replace("\n", ", ")}`;
+ if (data.surgical_history) details += `<br><b> ${__('Surgical history')} : </b> ${data.surgical_history.replace("\n", ", ")}`;
+ if (data.surrounding_factors) details += `<br><br><b> ${__('Occupational hazards')} : </b> ${data.surrounding_factors.replace("\n", ", ")}`;
+ if (data.other_risk_factors) details += `<br><b> ${__('Other risk factors')} : </b> ${data.other_risk_factors.replace("\n", ", ")}`;
+ if (data.patient_details) details += `<br><br><b> ${__('More info')} : </b> ${data.patient_details.replace("\n", ", ")}`;
+
+ if (details) {
+ details = `<div style='padding-left:10px; font-size:13px;' align='left'>` + details + `</div>`;
}
- me.page.main.find(".patient_details").html(details);
+ me.page.main.find('.patient_details').html(details);
}
});
};
-var show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) {
+let show_patient_vital_charts = function(patient, me, btn_show_id, pts, title) {
frappe.call({
- method: "erpnext.healthcare.utils.get_patient_vitals",
+ method: 'erpnext.healthcare.utils.get_patient_vitals',
args:{
patient: patient
},
callback: function(r) {
- if (r.message){
- var show_chart_btns_html = "<div style='padding-top:5px;'><a class='btn btn-default btn-xs btn-show-chart' \
- data-show-chart-id='bp' data-pts='mmHg' data-title='Blood Pressure'>Blood Pressure</a>\
- <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='pulse_rate' \
- data-pts='per Minutes' data-title='Respiratory/Pulse Rate'>Respiratory/Pulse Rate</a>\
- <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='temperature' \
- data-pts='°C or °F' data-title='Temperature'>Temperature</a>\
- <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bmi' \
- data-pts='' data-title='BMI'>BMI</a></div>";
- me.page.main.find(".show_chart_btns").html(show_chart_btns_html);
- var data = r.message;
+ if (r.message) {
+ let show_chart_btns_html = `
+ <div style='padding-top:10px;'>
+ <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bp' data-pts='mmHg' data-title='Blood Pressure'>
+ ${__('Blood Pressure')}
+ </a>
+ <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='pulse_rate' data-pts='per Minutes' data-title='Respiratory/Pulse Rate'>
+ ${__('Respiratory/Pulse Rate')}
+ </a>
+ <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='temperature' data-pts='°C or °F' data-title='Temperature'>
+ ${__('Temperature')}
+ </a>
+ <a class='btn btn-default btn-xs btn-show-chart' data-show-chart-id='bmi' data-pts='' data-title='BMI'>
+ ${__('BMI')}
+ </a>
+ </div>`;
+
+ me.page.main.find('.show_chart_btns').html(show_chart_btns_html);
+ let data = r.message;
let labels = [], datasets = [];
let bp_systolic = [], bp_diastolic = [], temperature = [];
let pulse = [], respiratory_rate = [], bmi = [], height = [], weight = [];
- for(var i=0; i<data.length; i++){
- labels.push(data[i].signs_date+"||"+data[i].signs_time);
- if(btn_show_id=="bp"){
+
+ for (let i=0; i<data.length; i++) {
+ labels.push(data[i].signs_date+'||'+data[i].signs_time);
+
+ if (btn_show_id === 'bp') {
bp_systolic.push(data[i].bp_systolic);
bp_diastolic.push(data[i].bp_diastolic);
}
- if(btn_show_id=="temperature"){
+ if (btn_show_id === 'temperature') {
temperature.push(data[i].temperature);
}
- if(btn_show_id=="pulse_rate"){
+ if (btn_show_id === 'pulse_rate') {
pulse.push(data[i].pulse);
respiratory_rate.push(data[i].respiratory_rate);
}
- if(btn_show_id=="bmi"){
+ if (btn_show_id === 'bmi') {
bmi.push(data[i].bmi);
height.push(data[i].height);
weight.push(data[i].weight);
}
}
- if(btn_show_id=="temperature"){
- datasets.push({name: "Temperature", values: temperature, chartType:'line'});
+ if (btn_show_id === 'temperature') {
+ datasets.push({name: 'Temperature', values: temperature, chartType: 'line'});
}
- if(btn_show_id=="bmi"){
- datasets.push({name: "BMI", values: bmi, chartType:'line'});
- datasets.push({name: "Height", values: height, chartType:'line'});
- datasets.push({name: "Weight", values: weight, chartType:'line'});
+ if (btn_show_id === 'bmi') {
+ datasets.push({name: 'BMI', values: bmi, chartType: 'line'});
+ datasets.push({name: 'Height', values: height, chartType: 'line'});
+ datasets.push({name: 'Weight', values: weight, chartType: 'line'});
}
- if(btn_show_id=="bp"){
- datasets.push({name: "BP Systolic", values: bp_systolic, chartType:'line'});
- datasets.push({name: "BP Diastolic", values: bp_diastolic, chartType:'line'});
+ if (btn_show_id === 'bp') {
+ datasets.push({name: 'BP Systolic', values: bp_systolic, chartType: 'line'});
+ datasets.push({name: 'BP Diastolic', values: bp_diastolic, chartType: 'line'});
}
- if(btn_show_id=="pulse_rate"){
- datasets.push({name: "Heart Rate / Pulse", values: pulse, chartType:'line'});
- datasets.push({name: "Respiratory Rate", values: respiratory_rate, chartType:'line'});
+ if (btn_show_id === 'pulse_rate') {
+ datasets.push({name: 'Heart Rate / Pulse', values: pulse, chartType: 'line'});
+ datasets.push({name: 'Respiratory Rate', values: respiratory_rate, chartType: 'line'});
}
- new frappe.Chart( ".patient_vital_charts", {
+ new frappe.Chart('.patient_vital_charts', {
data: {
labels: labels,
datasets: datasets
},
title: title,
- type: 'axis-mixed', // 'axis-mixed', 'bar', 'line', 'pie', 'percentage'
+ type: 'axis-mixed',
height: 200,
colors: ['purple', '#ffa3ef', 'light-blue'],
@@ -291,9 +392,11 @@
formatTooltipY: d => d + ' ' + pts,
}
});
- }else{
- me.page.main.find(".patient_vital_charts").html("");
- me.page.main.find(".show_chart_btns").html("");
+ me.page.main.find('.header-separator').show();
+ } else {
+ me.page.main.find('.patient_vital_charts').html('');
+ me.page.main.find('.show_chart_btns').html('');
+ me.page.main.find('.header-separator').hide();
}
}
});
diff --git a/erpnext/healthcare/page/patient_history/patient_history.py b/erpnext/healthcare/page/patient_history/patient_history.py
index 772aa4e..4cdfd64 100644
--- a/erpnext/healthcare/page/patient_history/patient_history.py
+++ b/erpnext/healthcare/page/patient_history/patient_history.py
@@ -4,36 +4,70 @@
from __future__ import unicode_literals
import frappe
+import json
from frappe.utils import cint
from erpnext.healthcare.utils import render_docs_as_html
@frappe.whitelist()
-def get_feed(name, start=0, page_length=20):
+def get_feed(name, document_types=None, date_range=None, start=0, page_length=20):
"""get feed"""
- result = frappe.db.sql("""select name, owner, creation,
- reference_doctype, reference_name, subject
- from `tabPatient Medical Record`
- where patient=%(patient)s
- order by creation desc
- limit %(start)s, %(page_length)s""",
- {
- "patient": name,
- "start": cint(start),
- "page_length": cint(page_length)
- }, as_dict=True)
+ filters = get_filters(name, document_types, date_range)
+
+ result = frappe.db.get_all('Patient Medical Record',
+ fields=['name', 'owner', 'communication_date',
+ 'reference_doctype', 'reference_name', 'subject'],
+ filters=filters,
+ order_by='communication_date DESC',
+ limit=cint(page_length),
+ start=cint(start)
+ )
+
return result
+
+def get_filters(name, document_types=None, date_range=None):
+ filters = {'patient': name}
+ if document_types:
+ document_types = json.loads(document_types)
+ if len(document_types):
+ filters['reference_doctype'] = ['IN', document_types]
+
+ if date_range:
+ try:
+ date_range = json.loads(date_range)
+ if date_range:
+ filters['communication_date'] = ['between', [date_range[0], date_range[1]]]
+ except json.decoder.JSONDecodeError:
+ pass
+
+ return filters
+
+
@frappe.whitelist()
def get_feed_for_dt(doctype, docname):
"""get feed"""
- result = frappe.db.sql("""select name, owner, modified, creation,
- reference_doctype, reference_name, subject
- from `tabPatient Medical Record`
- where reference_name=%(docname)s and reference_doctype=%(doctype)s
- order by creation desc""",
- {
- "docname": docname,
- "doctype": doctype
- }, as_dict=True)
+ result = frappe.db.get_all('Patient Medical Record',
+ fields=['name', 'owner', 'communication_date',
+ 'reference_doctype', 'reference_name', 'subject'],
+ filters={
+ 'reference_doctype': doctype,
+ 'reference_name': docname
+ },
+ order_by='communication_date DESC'
+ )
return result
+
+
+@frappe.whitelist()
+def get_patient_history_doctypes():
+ document_types = []
+ settings = frappe.get_single("Patient History Settings")
+
+ for entry in settings.standard_doctypes:
+ document_types.append(entry.document_type)
+
+ for entry in settings.custom_doctypes:
+ document_types.append(entry.document_type)
+
+ return document_types
diff --git a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py
index 0d3f45f..4b461f1 100644
--- a/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py
+++ b/erpnext/healthcare/report/inpatient_medication_orders/test_inpatient_medication_orders.py
@@ -119,7 +119,7 @@
ip_record.expected_length_of_stay = 0
ip_record.save()
ip_record.reload()
- service_unit = get_healthcare_service_unit()
+ service_unit = get_healthcare_service_unit('Test Service Unit Ip Occupancy')
admit_patient(ip_record, service_unit, now_datetime())
ipmo = create_ipmo(patient)
diff --git a/erpnext/healthcare/setup.py b/erpnext/healthcare/setup.py
index 0684080..bf4df7e 100644
--- a/erpnext/healthcare/setup.py
+++ b/erpnext/healthcare/setup.py
@@ -16,6 +16,7 @@
create_healthcare_item_groups()
create_sensitivity()
add_healthcare_service_unit_tree_root()
+ setup_patient_history_settings()
def create_medical_departments():
departments = [
@@ -213,3 +214,82 @@
if company:
return company[0].name
return None
+
+def setup_patient_history_settings():
+ import json
+
+ settings = frappe.get_single('Patient History Settings')
+ configuration = get_patient_history_config()
+ for dt, config in configuration.items():
+ settings.append("standard_doctypes", {
+ "document_type": dt,
+ "date_fieldname": config[0],
+ "selected_fields": json.dumps(config[1])
+ })
+ settings.save()
+
+def get_patient_history_config():
+ return {
+ "Patient Encounter": ("encounter_date", [
+ {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
+ {"label": "Symptoms", "fieldname": "symptoms", "fieldtype": "Table Multiselect"},
+ {"label": "Diagnosis", "fieldname": "diagnosis", "fieldtype": "Table Multiselect"},
+ {"label": "Drug Prescription", "fieldname": "drug_prescription", "fieldtype": "Table"},
+ {"label": "Lab Tests", "fieldname": "lab_test_prescription", "fieldtype": "Table"},
+ {"label": "Clinical Procedures", "fieldname": "procedure_prescription", "fieldtype": "Table"},
+ {"label": "Therapies", "fieldname": "therapies", "fieldtype": "Table"},
+ {"label": "Review Details", "fieldname": "encounter_comment", "fieldtype": "Small Text"}
+ ]),
+ "Clinical Procedure": ("start_date", [
+ {"label": "Procedure Template", "fieldname": "procedure_template", "fieldtype": "Link"},
+ {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
+ {"label": "Notes", "fieldname": "notes", "fieldtype": "Small Text"},
+ {"label": "Service Unit", "fieldname": "service_unit", "fieldtype": "Healthcare Service Unit"},
+ {"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"},
+ {"label": "Sample", "fieldname": "sample", "fieldtype": "Link"}
+ ]),
+ "Lab Test": ("result_date", [
+ {"label": "Test Template", "fieldname": "template", "fieldtype": "Link"},
+ {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
+ {"label": "Test Name", "fieldname": "lab_test_name", "fieldtype": "Data"},
+ {"label": "Lab Technician Name", "fieldname": "employee_name", "fieldtype": "Data"},
+ {"label": "Sample ID", "fieldname": "sample", "fieldtype": "Link"},
+ {"label": "Normal Test Result", "fieldname": "normal_test_items", "fieldtype": "Table"},
+ {"label": "Descriptive Test Result", "fieldname": "descriptive_test_items", "fieldtype": "Table"},
+ {"label": "Organism Test Result", "fieldname": "organism_test_items", "fieldtype": "Table"},
+ {"label": "Sensitivity Test Result", "fieldname": "sensitivity_test_items", "fieldtype": "Table"},
+ {"label": "Comments", "fieldname": "lab_test_comment", "fieldtype": "Table"}
+ ]),
+ "Therapy Session": ("start_date", [
+ {"label": "Therapy Type", "fieldname": "therapy_type", "fieldtype": "Link"},
+ {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
+ {"label": "Therapy Plan", "fieldname": "therapy_plan", "fieldtype": "Link"},
+ {"label": "Duration", "fieldname": "duration", "fieldtype": "Int"},
+ {"label": "Location", "fieldname": "location", "fieldtype": "Link"},
+ {"label": "Healthcare Service Unit", "fieldname": "service_unit", "fieldtype": "Link"},
+ {"label": "Start Time", "fieldname": "start_time", "fieldtype": "Time"},
+ {"label": "Exercises", "fieldname": "exercises", "fieldtype": "Table"},
+ {"label": "Total Counts Targeted", "fieldname": "total_counts_targeted", "fieldtype": "Int"},
+ {"label": "Total Counts Completed", "fieldname": "total_counts_completed", "fieldtype": "Int"}
+ ]),
+ "Vital Signs": ("signs_date", [
+ {"label": "Body Temperature", "fieldname": "temperature", "fieldtype": "Data"},
+ {"label": "Heart Rate / Pulse", "fieldname": "pulse", "fieldtype": "Data"},
+ {"label": "Respiratory rate", "fieldname": "respiratory_rate", "fieldtype": "Data"},
+ {"label": "Tongue", "fieldname": "tongue", "fieldtype": "Select"},
+ {"label": "Abdomen", "fieldname": "abdomen", "fieldtype": "Select"},
+ {"label": "Reflexes", "fieldname": "reflexes", "fieldtype": "Select"},
+ {"label": "Blood Pressure", "fieldname": "bp", "fieldtype": "Data"},
+ {"label": "Notes", "fieldname": "vital_signs_note", "fieldtype": "Small Text"},
+ {"label": "Height (In Meter)", "fieldname": "height", "fieldtype": "Float"},
+ {"label": "Weight (In Kilogram)", "fieldname": "weight", "fieldtype": "Float"},
+ {"label": "BMI", "fieldname": "bmi", "fieldtype": "Float"}
+ ]),
+ "Inpatient Medication Order": ("start_date", [
+ {"label": "Healthcare Practitioner", "fieldname": "practitioner", "fieldtype": "Link"},
+ {"label": "Start Date", "fieldname": "start_date", "fieldtype": "Date"},
+ {"label": "End Date", "fieldname": "end_date", "fieldtype": "Date"},
+ {"label": "Medication Orders", "fieldname": "medication_orders", "fieldtype": "Table"},
+ {"label": "Total Orders", "fieldname": "total_orders", "fieldtype": "Float"}
+ ])
+ }
\ No newline at end of file
diff --git a/erpnext/healthcare/utils.py b/erpnext/healthcare/utils.py
index 96282f5..d3d22c8 100644
--- a/erpnext/healthcare/utils.py
+++ b/erpnext/healthcare/utils.py
@@ -5,8 +5,11 @@
from __future__ import unicode_literals
import math
import frappe
+import json
from frappe import _
+from frappe.utils.formatters import format_value
from frappe.utils import time_diff_in_hours, rounded
+from six import string_types
from erpnext.healthcare.doctype.healthcare_settings.healthcare_settings import get_income_account
from erpnext.healthcare.doctype.fee_validity.fee_validity import create_fee_validity
from erpnext.healthcare.doctype.lab_test.lab_test import create_multiple
@@ -32,7 +35,7 @@
def validate_customer_created(patient):
if not frappe.db.get_value('Patient', patient.name, 'customer'):
msg = _("Please set a Customer linked to the Patient")
- msg += " <b><a href='#Form/Patient/{0}'>{0}</a></b>".format(patient.name)
+ msg += " <b><a href='/app/Form/Patient/{0}'>{0}</a></b>".format(patient.name)
frappe.throw(msg, title=_('Customer Not Found'))
@@ -63,7 +66,9 @@
income_account = None
service_item = None
if appointment.practitioner:
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(appointment)
+ details = get_service_item_and_practitioner_charge(appointment)
+ service_item = details.get('service_item')
+ practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(appointment.practitioner, appointment.company)
appointments_to_invoice.append({
'reference_type': 'Patient Appointment',
@@ -77,11 +82,13 @@
def get_encounters_to_invoice(patient, company):
+ if not isinstance(patient, str):
+ patient = patient.name
encounters_to_invoice = []
encounters = frappe.get_list(
'Patient Encounter',
fields=['*'],
- filters={'patient': patient.name, 'company': company, 'invoiced': False, 'docstatus': 1}
+ filters={'patient': patient, 'company': company, 'invoiced': False, 'docstatus': 1}
)
if encounters:
for encounter in encounters:
@@ -90,7 +97,13 @@
income_account = None
service_item = None
if encounter.practitioner:
- service_item, practitioner_charge = get_service_item_and_practitioner_charge(encounter)
+ if encounter.inpatient_record and \
+ frappe.db.get_single_value('Healthcare Settings', 'do_not_bill_inpatient_encounters'):
+ continue
+
+ details = get_service_item_and_practitioner_charge(encounter)
+ service_item = details.get('service_item')
+ practitioner_charge = details.get('practitioner_charge')
income_account = get_income_account(encounter.practitioner, encounter.company)
encounters_to_invoice.append({
@@ -166,10 +179,10 @@
if procedure.invoice_separately_as_consumables and procedure.consume_stock \
and procedure.status == 'Completed' and not procedure.consumption_invoiced:
- service_item = get_healthcare_service_item('clinical_procedure_consumable_item')
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
if not service_item:
msg = _('Please Configure Clinical Procedure Consumable Item in ')
- msg += '''<b><a href='#Form/Healthcare Settings'>Healthcare Settings</a></b>'''
+ msg += '''<b><a href='/app/Form/Healthcare Settings'>Healthcare Settings</a></b>'''
frappe.throw(msg, title=_('Missing Configuration'))
clinical_procedures_to_invoice.append({
@@ -297,24 +310,50 @@
return therapy_sessions_to_invoice
-
+@frappe.whitelist()
def get_service_item_and_practitioner_charge(doc):
+ if isinstance(doc, string_types):
+ doc = json.loads(doc)
+ doc = frappe.get_doc(doc)
+
+ service_item = None
+ practitioner_charge = None
+ department = doc.medical_department if doc.doctype == 'Patient Encounter' else doc.department
+
is_inpatient = doc.inpatient_record
- if is_inpatient:
- service_item = get_practitioner_service_item(doc.practitioner, 'inpatient_visit_charge_item')
+
+ if doc.get('appointment_type'):
+ service_item, practitioner_charge = get_appointment_type_service_item(doc.appointment_type, department, is_inpatient)
+
+ if not service_item and not practitioner_charge:
+ service_item, practitioner_charge = get_practitioner_service_item(doc.practitioner, is_inpatient)
if not service_item:
- service_item = get_healthcare_service_item('inpatient_visit_charge_item')
- else:
- service_item = get_practitioner_service_item(doc.practitioner, 'op_consulting_charge_item')
- if not service_item:
- service_item = get_healthcare_service_item('op_consulting_charge_item')
+ service_item = get_healthcare_service_item(is_inpatient)
+
if not service_item:
throw_config_service_item(is_inpatient)
- practitioner_charge = get_practitioner_charge(doc.practitioner, is_inpatient)
if not practitioner_charge:
throw_config_practitioner_charge(is_inpatient, doc.practitioner)
+ return {'service_item': service_item, 'practitioner_charge': practitioner_charge}
+
+
+def get_appointment_type_service_item(appointment_type, department, is_inpatient):
+ from erpnext.healthcare.doctype.appointment_type.appointment_type import get_service_item_based_on_department
+
+ item_list = get_service_item_based_on_department(appointment_type, department)
+ service_item = None
+ practitioner_charge = None
+
+ if item_list:
+ if is_inpatient:
+ service_item = item_list.get('inpatient_visit_charge_item')
+ practitioner_charge = item_list.get('inpatient_visit_charge')
+ else:
+ service_item = item_list.get('op_consulting_charge_item')
+ practitioner_charge = item_list.get('op_consulting_charge')
+
return service_item, practitioner_charge
@@ -324,7 +363,7 @@
service_item_label = _('Inpatient Visit Charge Item')
msg = _(('Please Configure {0} in ').format(service_item_label) \
- + '''<b><a href='#Form/Healthcare Settings'>Healthcare Settings</a></b>''')
+ + '''<b><a href='/app/Form/Healthcare Settings'>Healthcare Settings</a></b>''')
frappe.throw(msg, title=_('Missing Configuration'))
@@ -334,16 +373,31 @@
charge_name = _('Inpatient Visit Charge')
msg = _(('Please Configure {0} for Healthcare Practitioner').format(charge_name) \
- + ''' <b><a href='#Form/Healthcare Practitioner/{0}'>{0}</a></b>'''.format(practitioner))
+ + ''' <b><a href='/app/Form/Healthcare Practitioner/{0}'>{0}</a></b>'''.format(practitioner))
frappe.throw(msg, title=_('Missing Configuration'))
-def get_practitioner_service_item(practitioner, service_item_field):
- return frappe.db.get_value('Healthcare Practitioner', practitioner, service_item_field)
+def get_practitioner_service_item(practitioner, is_inpatient):
+ service_item = None
+ practitioner_charge = None
+
+ if is_inpatient:
+ service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['inpatient_visit_charge_item', 'inpatient_visit_charge'])
+ else:
+ service_item, practitioner_charge = frappe.db.get_value('Healthcare Practitioner', practitioner, ['op_consulting_charge_item', 'op_consulting_charge'])
+
+ return service_item, practitioner_charge
-def get_healthcare_service_item(service_item_field):
- return frappe.db.get_single_value('Healthcare Settings', service_item_field)
+def get_healthcare_service_item(is_inpatient):
+ service_item = None
+
+ if is_inpatient:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'inpatient_visit_charge_item')
+ else:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'op_consulting_charge_item')
+
+ return service_item
def get_practitioner_charge(practitioner, is_inpatient):
@@ -374,7 +428,8 @@
invoiced = True
if item.reference_dt == 'Clinical Procedure':
- if get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code:
+ service_item = frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item')
+ if service_item == item.item_code:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'consumption_invoiced', invoiced)
else:
frappe.db.set_value(item.reference_dt, item.reference_dn, 'invoiced', invoiced)
@@ -396,7 +451,8 @@
def validate_invoiced_on_submit(item):
- if item.reference_dt == 'Clinical Procedure' and get_healthcare_service_item('clinical_procedure_consumable_item') == item.item_code:
+ if item.reference_dt == 'Clinical Procedure' and \
+ frappe.db.get_single_value('Healthcare Settings', 'clinical_procedure_consumable_item') == item.item_code:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'consumption_invoiced')
else:
is_invoiced = frappe.db.get_value(item.reference_dt, item.reference_dn, 'invoiced')
@@ -642,11 +698,15 @@
html += "<table class='table table-condensed table-bordered'>" \
+ table_head + table_row + "</table>"
continue
+
#on other field types add label and value to html
if not df.hidden and not df.print_hide and doc.get(df.fieldname) and df.fieldname not in exclude_fields:
- html += '<br>{0} : {1}'.format(df.label or df.fieldname, \
- doc.get(df.fieldname))
+ if doc.get(df.fieldname):
+ formatted_value = format_value(doc.get(df.fieldname), meta.get_field(df.fieldname), doc)
+ html += '<br>{0} : {1}'.format(df.label or df.fieldname, formatted_value)
+
if not has_data : has_data = True
+
if sec_on and col_on and has_data:
doc_html += section_html + html + '</div></div>'
elif sec_on and not col_on and has_data:
@@ -654,6 +714,6 @@
><div class='col-md-12 col-sm-12'>" \
+ section_html + html +'</div></div>'
if doc_html:
- doc_html = "<div class='small'><div class='col-md-12 text-right'><a class='btn btn-default btn-xs' href='#Form/%s/%s'></a></div>" %(doctype, docname) + doc_html + '</div>'
+ doc_html = "<div class='small'><div class='col-md-12 text-right'><a class='btn btn-default btn-xs' href='/app/Form/%s/%s'></a></div>" %(doctype, docname) + doc_html + '</div>'
return {'html': doc_html}
diff --git a/erpnext/healthcare/workspace/healthcare/healthcare.json b/erpnext/healthcare/workspace/healthcare/healthcare.json
new file mode 100644
index 0000000..b93dda2
--- /dev/null
+++ b/erpnext/healthcare/workspace/healthcare/healthcare.json
@@ -0,0 +1,536 @@
+{
+ "category": "Domains",
+ "charts": [
+ {
+ "chart_name": "Patient Appointments",
+ "label": "Patient Appointments"
+ }
+ ],
+ "charts_label": "",
+ "creation": "2020-03-02 17:23:17.919682",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "healthcare",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Healthcare",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Masters",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient",
+ "link_to": "Patient",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Healthcare Practitioner",
+ "link_to": "Healthcare Practitioner",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Practitioner Schedule",
+ "link_to": "Practitioner Schedule",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Medical Department",
+ "link_to": "Medical Department",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Healthcare Service Unit Type",
+ "link_to": "Healthcare Service Unit Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Healthcare Service Unit",
+ "link_to": "Healthcare Service Unit",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Medical Code Standard",
+ "link_to": "Medical Code Standard",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Medical Code",
+ "link_to": "Medical Code",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Consultation Setup",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Appointment Type",
+ "link_to": "Appointment Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Clinical Procedure Template",
+ "link_to": "Clinical Procedure Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Prescription Dosage",
+ "link_to": "Prescription Dosage",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Prescription Duration",
+ "link_to": "Prescription Duration",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Antibiotic",
+ "link_to": "Antibiotic",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Consultation",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient Appointment",
+ "link_to": "Patient Appointment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Clinical Procedure",
+ "link_to": "Clinical Procedure",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient Encounter",
+ "link_to": "Patient Encounter",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Vital Signs",
+ "link_to": "Vital Signs",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Complaint",
+ "link_to": "Complaint",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Diagnosis",
+ "link_to": "Diagnosis",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fee Validity",
+ "link_to": "Fee Validity",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Healthcare Settings",
+ "link_to": "Healthcare Settings",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Laboratory Setup",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Lab Test Template",
+ "link_to": "Lab Test Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Lab Test Sample",
+ "link_to": "Lab Test Sample",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Lab Test UOM",
+ "link_to": "Lab Test UOM",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sensitivity",
+ "link_to": "Sensitivity",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Laboratory",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Lab Test",
+ "link_to": "Lab Test",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sample Collection",
+ "link_to": "Sample Collection",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Dosage Form",
+ "link_to": "Dosage Form",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Rehabilitation and Physiotherapy",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Exercise Type",
+ "link_to": "Exercise Type",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Therapy Type",
+ "link_to": "Therapy Type",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Therapy Plan",
+ "link_to": "Therapy Plan",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Therapy Session",
+ "link_to": "Therapy Session",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient Assessment Template",
+ "link_to": "Patient Assessment Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient Assessment",
+ "link_to": "Patient Assessment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Records and History",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient History",
+ "link_to": "patient_history",
+ "link_type": "Page",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient Progress",
+ "link_to": "patient-progress",
+ "link_type": "Page",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient Medical Record",
+ "link_to": "Patient Medical Record",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Inpatient Record",
+ "link_to": "Inpatient Record",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Patient Appointment Analytics",
+ "link_to": "Patient Appointment Analytics",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Lab Test Report",
+ "link_to": "Lab Test Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:34.841396",
+ "modified_by": "Administrator",
+ "module": "Healthcare",
+ "name": "Healthcare",
+ "onboarding": "Healthcare",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "restrict_to_domain": "Healthcare",
+ "shortcuts": [
+ {
+ "color": "Orange",
+ "format": "{} Open",
+ "label": "Patient Appointment",
+ "link_to": "Patient Appointment",
+ "stats_filter": "{\n \"status\": \"Open\",\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%']\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Orange",
+ "format": "{} Active",
+ "label": "Patient",
+ "link_to": "Patient",
+ "stats_filter": "{\n \"status\": \"Active\"\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Green",
+ "format": "{} Vacant",
+ "label": "Healthcare Service Unit",
+ "link_to": "Healthcare Service Unit",
+ "stats_filter": "{\n \"occupancy_status\": \"Vacant\",\n \"is_group\": 0,\n \"company\": [\"like\", \"%\" + frappe.defaults.get_global_default(\"company\") + \"%\"]\n}",
+ "type": "DocType"
+ },
+ {
+ "label": "Healthcare Practitioner",
+ "link_to": "Healthcare Practitioner",
+ "type": "DocType"
+ },
+ {
+ "label": "Patient History",
+ "link_to": "patient_history",
+ "type": "Page"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "Healthcare",
+ "type": "Dashboard"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/hooks.py b/erpnext/hooks.py
index 3ef93fc..59639ff 100644
--- a/erpnext/hooks.py
+++ b/erpnext/hooks.py
@@ -10,7 +10,7 @@
app_email = "info@erpnext.com"
app_license = "GNU General Public License (v3)"
source_link = "https://github.com/frappe/erpnext"
-app_logo_url = '/assets/erpnext/images/erp-icon.svg'
+app_logo_url = "/assets/erpnext/images/erpnext-logo.svg"
develop_version = '13.x.x-develop'
@@ -46,6 +46,7 @@
get_help_messages = "erpnext.utilities.activation.get_help_messages"
leaderboards = "erpnext.startup.leaderboard.get_leaderboards"
filters_config = "erpnext.startup.filters.get_filters_config"
+additional_print_settings = "erpnext.controllers.print_settings.get_print_settings"
on_session_creation = [
"erpnext.portal.utils.create_customer_or_supplier",
@@ -77,8 +78,8 @@
"Job Opening", "Student Admission"]
website_context = {
- "favicon": "/assets/erpnext/images/favicon.png",
- "splash_image": "/assets/erpnext/images/erp-icon.svg"
+ "favicon": "/assets/erpnext/images/erpnext-favicon.svg",
+ "splash_image": "/assets/erpnext/images/erpnext-logo.svg"
}
website_route_rules = [
@@ -221,6 +222,11 @@
}
doc_events = {
+ "*": {
+ "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"
+ },
"Stock Entry": {
"on_submit": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty",
"on_cancel": "erpnext.stock.doctype.material_request.material_request.update_completed_and_requested_qty"
@@ -266,17 +272,17 @@
'Address': {
'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category']
},
+ 'Supplier': {
+ 'validate': 'erpnext.regional.india.utils.validate_pan_for_india'
+ },
('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): {
'validate': ['erpnext.regional.india.utils.set_place_of_supply']
},
"Contact": {
"on_trash": "erpnext.support.doctype.issue.issue.update_issue",
- "after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information",
+ "after_insert": "erpnext.telephony.doctype.call_log.call_log.link_existing_conversations",
"validate": "erpnext.crm.utils.update_lead_phone_numbers"
},
- "Lead": {
- "after_insert": "erpnext.telephony.doctype.call_log.call_log.set_caller_information"
- },
"Email Unsubscribe": {
"after_insert": "erpnext.crm.doctype.email_campaign.email_campaign.unsubscribe_recipient"
},
@@ -341,7 +347,8 @@
"erpnext.selling.doctype.quotation.quotation.set_expired_status",
"erpnext.healthcare.doctype.patient_appointment.patient_appointment.update_appointment_status",
"erpnext.buying.doctype.supplier_quotation.supplier_quotation.set_expired_status",
- "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email"
+ "erpnext.accounts.doctype.process_statement_of_accounts.process_statement_of_accounts.send_auto_email",
+ "erpnext.non_profit.doctype.membership.membership.set_expired_status"
],
"daily_long": [
"erpnext.setup.doctype.email_digest.email_digest.send",
@@ -404,9 +411,11 @@
'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_header': 'erpnext.regional.india.utils.get_itemised_tax_breakup_header',
'erpnext.controllers.taxes_and_totals.get_itemised_tax_breakup_data': 'erpnext.regional.india.utils.get_itemised_tax_breakup_data',
'erpnext.accounts.party.get_regional_address_details': 'erpnext.regional.india.utils.get_regional_address_details',
+ '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.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'
},
'United Arab Emirates': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.united_arab_emirates.utils.update_itemised_tax_data',
@@ -418,9 +427,6 @@
'Italy': {
'erpnext.controllers.taxes_and_totals.update_itemised_tax_data': 'erpnext.regional.italy.utils.update_itemised_tax_data',
'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.italy.utils.sales_invoice_validate',
- },
- 'Germany': {
- 'erpnext.controllers.accounts_controller.validate_regional': 'erpnext.regional.germany.accounts_controller.validate_regional',
}
}
user_privacy_documents = [
@@ -450,42 +456,43 @@
{"doctype": "Sales Order", "index": 8},
{"doctype": "Quotation", "index": 9},
{"doctype": "Work Order", "index": 10},
- {"doctype": "Purchase Receipt", "index": 11},
- {"doctype": "Purchase Invoice", "index": 12},
- {"doctype": "Delivery Note", "index": 13},
- {"doctype": "Stock Entry", "index": 14},
- {"doctype": "Material Request", "index": 15},
- {"doctype": "Delivery Trip", "index": 16},
- {"doctype": "Pick List", "index": 17},
- {"doctype": "Salary Slip", "index": 18},
- {"doctype": "Leave Application", "index": 19},
- {"doctype": "Expense Claim", "index": 20},
- {"doctype": "Payment Entry", "index": 21},
- {"doctype": "Lead", "index": 22},
- {"doctype": "Opportunity", "index": 23},
- {"doctype": "Item Price", "index": 24},
- {"doctype": "Purchase Taxes and Charges Template", "index": 25},
- {"doctype": "Sales Taxes and Charges", "index": 26},
- {"doctype": "Asset", "index": 27},
- {"doctype": "Project", "index": 28},
- {"doctype": "Task", "index": 29},
- {"doctype": "Timesheet", "index": 30},
- {"doctype": "Issue", "index": 31},
- {"doctype": "Serial No", "index": 32},
- {"doctype": "Batch", "index": 33},
- {"doctype": "Branch", "index": 34},
- {"doctype": "Department", "index": 35},
- {"doctype": "Employee Grade", "index": 36},
- {"doctype": "Designation", "index": 37},
- {"doctype": "Job Opening", "index": 38},
- {"doctype": "Job Applicant", "index": 39},
- {"doctype": "Job Offer", "index": 40},
- {"doctype": "Salary Structure Assignment", "index": 41},
- {"doctype": "Appraisal", "index": 42},
- {"doctype": "Loan", "index": 43},
- {"doctype": "Maintenance Schedule", "index": 44},
- {"doctype": "Maintenance Visit", "index": 45},
- {"doctype": "Warranty Claim", "index": 46},
+ {"doctype": "Purchase Order", "index": 11},
+ {"doctype": "Purchase Receipt", "index": 12},
+ {"doctype": "Purchase Invoice", "index": 13},
+ {"doctype": "Delivery Note", "index": 14},
+ {"doctype": "Stock Entry", "index": 15},
+ {"doctype": "Material Request", "index": 16},
+ {"doctype": "Delivery Trip", "index": 17},
+ {"doctype": "Pick List", "index": 18},
+ {"doctype": "Salary Slip", "index": 19},
+ {"doctype": "Leave Application", "index": 20},
+ {"doctype": "Expense Claim", "index": 21},
+ {"doctype": "Payment Entry", "index": 22},
+ {"doctype": "Lead", "index": 23},
+ {"doctype": "Opportunity", "index": 24},
+ {"doctype": "Item Price", "index": 25},
+ {"doctype": "Purchase Taxes and Charges Template", "index": 26},
+ {"doctype": "Sales Taxes and Charges", "index": 27},
+ {"doctype": "Asset", "index": 28},
+ {"doctype": "Project", "index": 29},
+ {"doctype": "Task", "index": 30},
+ {"doctype": "Timesheet", "index": 31},
+ {"doctype": "Issue", "index": 32},
+ {"doctype": "Serial No", "index": 33},
+ {"doctype": "Batch", "index": 34},
+ {"doctype": "Branch", "index": 35},
+ {"doctype": "Department", "index": 36},
+ {"doctype": "Employee Grade", "index": 37},
+ {"doctype": "Designation", "index": 38},
+ {"doctype": "Job Opening", "index": 39},
+ {"doctype": "Job Applicant", "index": 40},
+ {"doctype": "Job Offer", "index": 41},
+ {"doctype": "Salary Structure Assignment", "index": 42},
+ {"doctype": "Appraisal", "index": 43},
+ {"doctype": "Loan", "index": 44},
+ {"doctype": "Maintenance Schedule", "index": 45},
+ {"doctype": "Maintenance Visit", "index": 46},
+ {"doctype": "Warranty Claim", "index": 47},
],
"Healthcare": [
{'doctype': 'Patient', 'index': 1},
@@ -588,3 +595,7 @@
{'doctype': 'Hotel Room Type', 'index': 4}
]
}
+
+additional_timeline_content = {
+ '*': ['erpnext.telephony.doctype.call_log.call_log.get_linked_call_logs']
+}
diff --git a/erpnext/hr/desk_page/hr/hr.json b/erpnext/hr/desk_page/hr/hr.json
deleted file mode 100644
index 895cf72..0000000
--- a/erpnext/hr/desk_page/hr/hr.json
+++ /dev/null
@@ -1,130 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Employee",
- "links": "[\n {\n \"label\": \"Employee\",\n \"name\": \"Employee\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employment Type\",\n \"name\": \"Employment Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Branch\",\n \"name\": \"Branch\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Department\",\n \"name\": \"Department\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Designation\",\n \"name\": \"Designation\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Grade\",\n \"name\": \"Employee Grade\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Group\",\n \"name\": \"Employee Group\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Health Insurance\",\n \"name\": \"Employee Health Insurance\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Employee Lifecycle",
- "links": "[\n {\n \"dependencies\": [\n \"Job Applicant\"\n ],\n \"label\": \"Employee Onboarding\",\n \"name\": \"Employee Onboarding\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Skill Map\",\n \"name\": \"Employee Skill Map\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Promotion\",\n \"name\": \"Employee Promotion\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Transfer\",\n \"name\": \"Employee Transfer\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Separation\",\n \"name\": \"Employee Separation\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Onboarding Template\",\n \"name\": \"Employee Onboarding Template\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Separation Template\",\n \"name\": \"Employee Separation Template\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Shift Management",
- "links": "[\n {\n \"label\": \"Shift Type\",\n \"name\": \"Shift Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shift Request\",\n \"name\": \"Shift Request\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shift Assignment\",\n \"name\": \"Shift Assignment\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Leaves",
- "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Application\",\n \"name\": \"Leave Application\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Allocation\",\n \"name\": \"Leave Allocation\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Type\"\n ],\n \"label\": \"Leave Policy\",\n \"name\": \"Leave Policy\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Period\",\n \"name\": \"Leave Period\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Type\",\n \"name\": \"Leave Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Holiday List\",\n \"name\": \"Holiday List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Compensatory Leave Request\",\n \"name\": \"Compensatory Leave Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Leave Encashment\",\n \"name\": \"Leave Encashment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Leave Block List\",\n \"name\": \"Leave Block List\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Leave Application\"\n ],\n \"doctype\": \"Leave Application\",\n \"is_query_report\": true,\n \"label\": \"Employee Leave Balance\",\n \"name\": \"Employee Leave Balance\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Attendance",
- "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"hide_count\": true,\n \"label\": \"Employee Attendance Tool\",\n \"name\": \"Employee Attendance Tool\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Attendance\",\n \"name\": \"Attendance\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Attendance Request\",\n \"name\": \"Attendance Request\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"hide_count\": true,\n \"label\": \"Upload Attendance\",\n \"name\": \"Upload Attendance\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"hide_count\": true,\n \"label\": \"Employee Checkin\",\n \"name\": \"Employee Checkin\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Attendance\"\n ],\n \"doctype\": \"Attendance\",\n \"is_query_report\": true,\n \"label\": \"Monthly Attendance Sheet\",\n \"name\": \"Monthly Attendance Sheet\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Expense Claims",
- "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Expense Claim\",\n \"name\": \"Expense Claim\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"label\": \"Employee Advance\",\n \"name\": \"Employee Advance\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Settings",
- "links": "[\n {\n \"label\": \"HR Settings\",\n \"name\": \"HR Settings\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Daily Work Summary Group\",\n \"name\": \"Daily Work Summary Group\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Team Updates\",\n \"name\": \"team-updates\",\n \"type\": \"page\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Fleet Management",
- "links": "[\n {\n \"label\": \"Vehicle\",\n \"name\": \"Vehicle\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Vehicle Log\",\n \"name\": \"Vehicle Log\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Vehicle\"\n ],\n \"doctype\": \"Vehicle\",\n \"is_query_report\": true,\n \"label\": \"Vehicle Expenses\",\n \"name\": \"Vehicle Expenses\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Recruitment",
- "links": "[\n {\n \"label\": \"Job Opening\",\n \"name\": \"Job Opening\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Job Applicant\",\n \"name\": \"Job Applicant\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Job Offer\",\n \"name\": \"Job Offer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Staffing Plan\",\n \"name\": \"Staffing Plan\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Training",
- "links": "[\n {\n \"label\": \"Training Program\",\n \"name\": \"Training Program\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Training Event\",\n \"name\": \"Training Event\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Training Result\",\n \"name\": \"Training Result\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Training Feedback\",\n \"name\": \"Training Feedback\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"doctype\": \"Employee\",\n \"is_query_report\": true,\n \"label\": \"Employee Birthday\",\n \"name\": \"Employee Birthday\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"doctype\": \"Employee\",\n \"is_query_report\": true,\n \"label\": \"Employees working on a holiday\",\n \"name\": \"Employees working on a holiday\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"doctype\": \"Employee\",\n \"is_query_report\": true,\n \"label\": \"Department Analytics\",\n \"name\": \"Department Analytics\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Performance",
- "links": "[\n {\n \"label\": \"Appraisal\",\n \"name\": \"Appraisal\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Appraisal Template\",\n \"name\": \"Appraisal Template\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Energy Point Rule\",\n \"name\": \"Energy Point Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Energy Point Log\",\n \"name\": \"Energy Point Log\",\n \"type\": \"doctype\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [
- {
- "chart_name": "Attendance Count",
- "label": "Attendance Count"
- }
- ],
- "creation": "2020-03-02 15:48:58.322521",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "HR",
- "modified": "2020-08-11 17:04:38.655417",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "HR",
- "onboarding": "Human Resource",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "color": "#cef6d1",
- "format": "{} Active",
- "label": "Employee",
- "link_to": "Employee",
- "stats_filter": "{\"status\":\"Active\"}",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} Open",
- "label": "Leave Application",
- "link_to": "Leave Application",
- "stats_filter": "{\"status\":\"Open\"}",
- "type": "DocType"
- },
- {
- "label": "Attendance",
- "link_to": "Attendance",
- "stats_filter": "",
- "type": "DocType"
- },
- {
- "label": "Job Applicant",
- "link_to": "Job Applicant",
- "type": "DocType"
- },
- {
- "label": "Monthly Attendance Sheet",
- "link_to": "Monthly Attendance Sheet",
- "type": "Report"
- },
- {
- "format": "{} Open",
- "label": "Dashboard",
- "link_to": "Human Resource",
- "stats_filter": "{\n \"status\": \"Open\"\n}",
- "type": "Dashboard"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/employee/employee.json b/erpnext/hr/doctype/employee/employee.json
index 4f1c04f..dc2aaa4 100644
--- a/erpnext/hr/doctype/employee/employee.json
+++ b/erpnext/hr/doctype/employee/employee.json
@@ -813,7 +813,7 @@
"idx": 24,
"image_field": "image",
"links": [],
- "modified": "2020-10-16 15:02:04.283657",
+ "modified": "2021-01-01 16:54:33.477439",
"modified_by": "Administrator",
"module": "HR",
"name": "Employee",
@@ -855,7 +855,6 @@
"write": 1
}
],
- "quick_entry": 1,
"search_fields": "employee_name",
"show_name_in_global_search": 1,
"sort_field": "modified",
diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py
index dfc600c..d0e7d05 100755
--- a/erpnext/hr/doctype/employee/employee.py
+++ b/erpnext/hr/doctype/employee/employee.py
@@ -135,7 +135,7 @@
try:
frappe.get_doc({
"doctype": "File",
- "file_name": self.image,
+ "file_url": self.image,
"attached_to_doctype": "User",
"attached_to_name": self.user_id
}).insert()
@@ -278,63 +278,89 @@
if int(frappe.db.get_single_value("HR Settings", "stop_birthday_reminders") or 0):
return
- birthdays = get_employees_who_are_born_today()
+ employees_born_today = get_employees_who_are_born_today()
- if birthdays:
- employee_list = frappe.get_all('Employee',
- fields=['name','employee_name'],
- filters={'status': 'Active',
- 'company': birthdays[0]['company']
- }
- )
- employee_emails = get_employee_emails(employee_list)
- birthday_names = [name["employee_name"] for name in birthdays]
- birthday_emails = [email["user_id"] or email["personal_email"] or email["company_email"] for email in birthdays]
+ for company, birthday_persons in employees_born_today.items():
+ employee_emails = get_all_employee_emails(company)
+ birthday_person_emails = [get_employee_email(doc) for doc in birthday_persons]
+ recipients = list(set(employee_emails) - set(birthday_person_emails))
- birthdays.append({'company_email': '','employee_name': '','personal_email': '','user_id': ''})
+ reminder_text, message = get_birthday_reminder_text_and_message(birthday_persons)
+ send_birthday_reminder(recipients, reminder_text, birthday_persons, message)
- for e in birthdays:
- if e['company_email'] or e['personal_email'] or e['user_id']:
- if len(birthday_names) == 1:
- continue
- recipients = e['company_email'] or e['personal_email'] or e['user_id']
+ if len(birthday_persons) > 1:
+ # special email for people sharing birthdays
+ for person in birthday_persons:
+ person_email = person["user_id"] or person["personal_email"] or person["company_email"]
+ others = [d for d in birthday_persons if d != person]
+ reminder_text, message = get_birthday_reminder_text_and_message(others)
+ send_birthday_reminder(person_email, reminder_text, others, message)
+def get_employee_email(employee_doc):
+ return employee_doc["user_id"] or employee_doc["personal_email"] or employee_doc["company_email"]
- else:
- recipients = list(set(employee_emails) - set(birthday_emails))
-
- frappe.sendmail(recipients=recipients,
- subject=_("Birthday Reminder"),
- message=get_birthday_reminder_message(e, birthday_names),
- header=['Birthday Reminder', 'green'],
- )
-
-def get_birthday_reminder_message(employee, employee_names):
- """Get employee birthday reminder message"""
- pattern = "</Li><Br><Li>"
- message = pattern.join(filter(lambda u: u not in (employee['employee_name']), employee_names))
- message = message.title()
-
- if pattern not in message:
- message = "Today is {0}'s birthday \U0001F603".format(message)
-
+def get_birthday_reminder_text_and_message(birthday_persons):
+ if len(birthday_persons) == 1:
+ birthday_person_text = birthday_persons[0]['name']
else:
- message = "Today your colleagues are celebrating their birthdays \U0001F382<br><ul><strong><li> " + message +"</li></strong></ul>"
+ # converts ["Jim", "Rim", "Dim"] to Jim, Rim & Dim
+ person_names = [d['name'] for d in birthday_persons]
+ last_person = person_names[-1]
+ birthday_person_text = ", ".join(person_names[:-1])
+ birthday_person_text = _("{} & {}").format(birthday_person_text, last_person)
- return message
+ reminder_text = _("Today is {0}'s birthday 🎉").format(birthday_person_text)
+ message = _("A friendly reminder of an important date for our team.")
+ message += "<br>"
+ message += _("Everyone, let’s congratulate {0} on their birthday.").format(birthday_person_text)
+ return reminder_text, message
-def get_employees_who_are_born_today():
- """Get Employee properties whose birthday is today."""
- return frappe.db.get_values("Employee",
- fieldname=["name", "personal_email", "company", "company_email", "user_id", "employee_name"],
- filters={
- "date_of_birth": ("like", "%{}".format(format_datetime(getdate(), "-MM-dd"))),
- "status": "Active",
- },
- as_dict=True
+def send_birthday_reminder(recipients, reminder_text, birthday_persons, message):
+ frappe.sendmail(
+ recipients=recipients,
+ subject=_("Birthday Reminder"),
+ template="birthday_reminder",
+ args=dict(
+ reminder_text=reminder_text,
+ birthday_persons=birthday_persons,
+ message=message,
+ ),
+ header=_("Birthday Reminder 🎂")
)
+def get_employees_who_are_born_today():
+ """Get all employee born today & group them based on their company"""
+ from collections import defaultdict
+ employees_born_today = frappe.db.multisql({
+ "mariadb": """
+ SELECT `personal_email`, `company`, `company_email`, `user_id`, `employee_name` AS 'name', `image`
+ FROM `tabEmployee`
+ WHERE
+ DAY(date_of_birth) = DAY(%(today)s)
+ AND
+ MONTH(date_of_birth) = MONTH(%(today)s)
+ AND
+ `status` = 'Active'
+ """,
+ "postgres": """
+ SELECT "personal_email", "company", "company_email", "user_id", "employee_name" AS 'name', "image"
+ FROM "tabEmployee"
+ WHERE
+ DATE_PART('day', "date_of_birth") = date_part('day', %(today)s)
+ AND
+ DATE_PART('month', "date_of_birth") = date_part('month', %(today)s)
+ AND
+ "status" = 'Active'
+ """,
+ }, dict(today=today()), as_dict=1)
+
+ grouped_employees = defaultdict(lambda: [])
+
+ for employee_doc in employees_born_today:
+ grouped_employees[employee_doc.get('company')].append(employee_doc)
+
+ return grouped_employees
def get_holiday_list_for_employee(employee, raise_exception=True):
if employee:
@@ -404,6 +430,26 @@
user.insert()
return user.name
+def get_all_employee_emails(company):
+ '''Returns list of employee emails either based on user_id or company_email'''
+ employee_list = frappe.get_all('Employee',
+ fields=['name','employee_name'],
+ filters={
+ 'status': 'Active',
+ 'company': company
+ }
+ )
+ employee_emails = []
+ for employee in employee_list:
+ if not employee:
+ continue
+ user, company_email, personal_email = frappe.db.get_value('Employee',
+ employee, ['user_id', 'company_email', 'personal_email'])
+ email = user or company_email or personal_email
+ if email:
+ employee_emails.append(email)
+ return employee_emails
+
def get_employee_emails(employee_list):
'''Returns list of employee emails either based on user_id or company_email'''
employee_emails = []
diff --git a/erpnext/hr/doctype/employee/employee_list.js b/erpnext/hr/doctype/employee/employee_list.js
index 7a66d12..4483703 100644
--- a/erpnext/hr/doctype/employee/employee_list.js
+++ b/erpnext/hr/doctype/employee/employee_list.js
@@ -3,7 +3,7 @@
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": "darkgrey"}[doc.status];
+ indicator[1] = {"Active": "green", "Temporary Leave": "red", "Left": "gray"}[doc.status];
return indicator;
}
};
diff --git a/erpnext/hr/doctype/employee/test_employee.py b/erpnext/hr/doctype/employee/test_employee.py
index f4b214a..c0e614a 100644
--- a/erpnext/hr/doctype/employee/test_employee.py
+++ b/erpnext/hr/doctype/employee/test_employee.py
@@ -16,11 +16,13 @@
employee = frappe.get_doc("Employee", frappe.db.sql_list("select name from tabEmployee limit 1")[0])
employee.date_of_birth = "1992" + frappe.utils.nowdate()[4:]
employee.company_email = "test@example.com"
+ employee.company = "_Test Company"
employee.save()
from erpnext.hr.doctype.employee.employee import get_employees_who_are_born_today, send_birthday_reminders
- self.assertTrue(employee.name in [e.name for e in get_employees_who_are_born_today()])
+ employees_born_today = get_employees_who_are_born_today()
+ self.assertTrue(employees_born_today.get("_Test Company"))
frappe.db.sql("delete from `tabEmail Queue`")
diff --git a/erpnext/hr/doctype/employee_advance/employee_advance.js b/erpnext/hr/doctype/employee_advance/employee_advance.js
index 7056adf..5037ceb 100644
--- a/erpnext/hr/doctype/employee_advance/employee_advance.js
+++ b/erpnext/hr/doctype/employee_advance/employee_advance.js
@@ -18,13 +18,18 @@
if (!frm.doc.employee) {
frappe.msgprint(__("Please select employee first"));
}
- var company_currency = erpnext.get_currency(frm.doc.company);
+ let company_currency = erpnext.get_currency(frm.doc.company);
+ let currencies = [company_currency];
+ if (frm.doc.currency && (frm.doc.currency != company_currency)) {
+ currencies.push(frm.doc.currency);
+ }
+
return {
filters: {
"root_type": "Asset",
"is_group": 0,
"company": frm.doc.company,
- "account_currency": ["in", [frm.doc.currency, company_currency]],
+ "account_currency": ["in", currencies],
}
};
});
@@ -181,21 +186,23 @@
},
currency: function(frm) {
- var from_currency = frm.doc.currency;
- var company_currency;
- if (!frm.doc.company) {
- company_currency = erpnext.get_currency(frappe.defaults.get_default("Company"));
- } else {
- company_currency = erpnext.get_currency(frm.doc.company);
+ if (frm.doc.currency) {
+ var from_currency = frm.doc.currency;
+ var company_currency;
+ if (!frm.doc.company) {
+ company_currency = erpnext.get_currency(frappe.defaults.get_default("Company"));
+ } else {
+ company_currency = erpnext.get_currency(frm.doc.company);
+ }
+ if (from_currency != company_currency) {
+ frm.events.set_exchange_rate(frm, from_currency, company_currency);
+ } else {
+ frm.set_value("exchange_rate", 1.0);
+ frm.set_df_property('exchange_rate', 'hidden', 1);
+ frm.set_df_property("exchange_rate", "description", "" );
+ }
+ frm.refresh_fields();
}
- if (from_currency != company_currency) {
- frm.events.set_exchange_rate(frm, from_currency, company_currency);
- } else {
- frm.set_value("exchange_rate", 1.0);
- frm.set_df_property('exchange_rate', 'hidden', 1);
- frm.set_df_property("exchange_rate", "description", "" );
- }
- frm.refresh_fields();
},
set_exchange_rate: function(frm, from_currency, company_currency) {
diff --git a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
index 4e9ee3b..336e13c 100644
--- a/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
+++ b/erpnext/hr/doctype/employee_onboarding/test_employee_onboarding.py
@@ -38,7 +38,8 @@
onboarding.insert()
onboarding.submit()
- self.assertEqual(onboarding.project, 'Employee Onboarding : Test Researcher - test@researcher.com')
+ project_name = frappe.db.get_value("Project", onboarding.project, "project_name")
+ self.assertEqual(project_name, 'Employee Onboarding : Test Researcher - test@researcher.com')
# don't allow making employee if onboarding is not complete
self.assertRaises(IncompleteTaskError, make_employee, onboarding.name)
diff --git a/erpnext/hr/doctype/employee_transfer/employee_transfer.py b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
index c730e02..3539970 100644
--- a/erpnext/hr/doctype/employee_transfer/employee_transfer.py
+++ b/erpnext/hr/doctype/employee_transfer/employee_transfer.py
@@ -50,8 +50,9 @@
employee = frappe.get_doc("Employee", self.employee)
if self.create_new_employee_id:
if self.new_employee_id:
- frappe.throw(_("Please delete the Employee <a href='#Form/Employee/{0}'>{0}</a>\
- to cancel this document").format(self.new_employee_id))
+ frappe.throw(_("Please delete the Employee {0} to cancel this document").format(
+ "<a href='/app/Form/Employee/{0}'>{0}</a>".format(self.new_employee_id)
+ ))
#mark the employee as active
employee.status = "Active"
employee.relieving_date = ''
diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js
index 221300b..629341f 100644
--- a/erpnext/hr/doctype/expense_claim/expense_claim.js
+++ b/erpnext/hr/doctype/expense_claim/expense_claim.js
@@ -2,11 +2,21 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.hr");
+frappe.provide("erpnext.accounts.dimensions");
-erpnext.hr.ExpenseClaimController = frappe.ui.form.Controller.extend({
- expense_type: function(doc, cdt, cdn) {
+frappe.ui.form.on('Expense Claim', {
+ onload: function(frm) {
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
+ },
+});
+
+frappe.ui.form.on('Expense Claim Detail', {
+ expense_type: function(frm, cdt, cdn) {
var d = locals[cdt][cdn];
- if(!doc.company) {
+ if (!frm.doc.company) {
d.expense_type = "";
frappe.msgprint(__("Please set the Company"));
this.frm.refresh_fields();
@@ -20,7 +30,7 @@
method: "erpnext.hr.doctype.expense_claim.expense_claim.get_expense_claim_account_and_cost_center",
args: {
"expense_claim_type": d.expense_type,
- "company": doc.company
+ "company": frm.doc.company
},
callback: function(r) {
if (r.message) {
@@ -32,8 +42,6 @@
}
});
-$.extend(cur_frm.cscript, new erpnext.hr.ExpenseClaimController({frm: cur_frm}));
-
cur_frm.add_fetch('employee', 'company', 'company');
cur_frm.add_fetch('employee','employee_name','employee_name');
cur_frm.add_fetch('expense_type','description','description');
@@ -167,15 +175,6 @@
};
});
- frm.set_query("cost_center", "expenses", function() {
- return {
- filters: {
- "company": frm.doc.company,
- "is_group": 0
- }
- };
- });
-
frm.set_query("payable_account", function() {
return {
filters: {
diff --git a/erpnext/hr/doctype/expense_claim/test_expense_claim.py b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
index 4a0908d..f9e3a44 100644
--- a/erpnext/hr/doctype/expense_claim/test_expense_claim.py
+++ b/erpnext/hr/doctype/expense_claim/test_expense_claim.py
@@ -20,35 +20,36 @@
frappe.db.sql("""delete from `tabProject` where name = "_Test Project 1" """)
frappe.db.sql("update `tabExpense Claim` set project = '', task = ''")
- frappe.get_doc({
+ project = frappe.get_doc({
"project_name": "_Test Project 1",
"doctype": "Project"
- }).save()
+ })
+ project.save()
task = frappe.get_doc(dict(
doctype = 'Task',
subject = '_Test Project Task 1',
status = 'Open',
- project = '_Test Project 1'
+ project = project.name
)).insert()
task_name = task.name
payable_account = get_payable_account(company_name)
- make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", "_Test Project 1", task_name)
+ make_expense_claim(payable_account, 300, 200, company_name, "Travel Expenses - _TC4", project.name, task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
- self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200)
- expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4","_Test Project 1", task_name)
+ expense_claim2 = make_expense_claim(payable_account, 600, 500, company_name, "Travel Expenses - _TC4", project.name, task_name)
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 700)
- self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 700)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 700)
expense_claim2.cancel()
self.assertEqual(frappe.db.get_value("Task", task_name, "total_expense_claim"), 200)
- self.assertEqual(frappe.db.get_value("Project", "_Test Project 1", "total_expense_claim"), 200)
+ self.assertEqual(frappe.db.get_value("Project", project.name, "total_expense_claim"), 200)
def test_expense_claim_status(self):
payable_account = get_payable_account(company_name)
diff --git a/erpnext/hr/doctype/job_applicant/job_applicant.json b/erpnext/hr/doctype/job_applicant/job_applicant.json
index c13548a..1360fd1 100644
--- a/erpnext/hr/doctype/job_applicant/job_applicant.json
+++ b/erpnext/hr/doctype/job_applicant/job_applicant.json
@@ -11,15 +11,24 @@
"field_order": [
"applicant_name",
"email_id",
+ "phone_number",
+ "country",
"status",
"column_break_3",
"job_title",
"source",
"source_name",
+ "applicant_rating",
"section_break_6",
"notes",
"cover_letter",
- "resume_attachment"
+ "resume_attachment",
+ "resume_link",
+ "section_break_16",
+ "currency",
+ "column_break_18",
+ "lower_range",
+ "upper_range"
],
"fields": [
{
@@ -91,12 +100,65 @@
"fieldtype": "Data",
"label": "Notes",
"read_only": 1
+ },
+ {
+ "fieldname": "phone_number",
+ "fieldtype": "Data",
+ "label": "Phone Number",
+ "options": "Phone"
+ },
+ {
+ "fieldname": "country",
+ "fieldtype": "Link",
+ "label": "Country",
+ "options": "Country"
+ },
+ {
+ "fieldname": "resume_link",
+ "fieldtype": "Data",
+ "label": "Resume Link"
+ },
+ {
+ "fieldname": "applicant_rating",
+ "fieldtype": "Rating",
+ "in_list_view": 1,
+ "label": "Applicant Rating"
+ },
+ {
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break",
+ "label": "Salary Expectation"
+ },
+ {
+ "fieldname": "lower_range",
+ "fieldtype": "Currency",
+ "label": "Lower Range",
+ "options": "currency",
+ "precision": "0"
+ },
+ {
+ "fieldname": "upper_range",
+ "fieldtype": "Currency",
+ "label": "Upper Range",
+ "options": "currency",
+ "precision": "0"
+ },
+ {
+ "fieldname": "column_break_18",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Currency",
+ "options": "Currency"
}
],
"icon": "fa fa-user",
"idx": 1,
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-01-13 16:19:39.113330",
+ "modified": "2020-09-18 12:39:02.557563",
"modified_by": "Administrator",
"module": "HR",
"name": "Job Applicant",
diff --git a/erpnext/hr/doctype/job_offer/job_offer.py b/erpnext/hr/doctype/job_offer/job_offer.py
index c397a3f..7e650f7 100644
--- a/erpnext/hr/doctype/job_offer/job_offer.py
+++ b/erpnext/hr/doctype/job_offer/job_offer.py
@@ -16,7 +16,7 @@
def validate(self):
self.validate_vacancies()
- job_offer = frappe.db.exists("Job Offer",{"job_applicant": self.job_applicant})
+ job_offer = frappe.db.exists("Job Offer",{"job_applicant": self.job_applicant, "docstatus": ["!=", 2]})
if job_offer and job_offer != self.name:
frappe.throw(_("Job Offer: {0} is already for Job Applicant: {1}").format(frappe.bold(job_offer), frappe.bold(self.job_applicant)))
diff --git a/erpnext/hr/doctype/job_opening/job_opening.json b/erpnext/hr/doctype/job_opening/job_opening.json
index 4437e02..b8f6df6 100644
--- a/erpnext/hr/doctype/job_opening/job_opening.json
+++ b/erpnext/hr/doctype/job_opening/job_opening.json
@@ -1,456 +1,188 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "autoname": "field:route",
- "beta": 0,
- "creation": "2013-01-15 16:13:36",
- "custom": 0,
- "description": "Description of a Job Opening",
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "Document",
- "editable_grid": 0,
- "engine": "InnoDB",
+ "actions": [],
+ "autoname": "field:route",
+ "creation": "2013-01-15 16:13:36",
+ "description": "Description of a Job Opening",
+ "doctype": "DocType",
+ "document_type": "Document",
+ "engine": "InnoDB",
+ "field_order": [
+ "job_title",
+ "company",
+ "status",
+ "column_break_5",
+ "designation",
+ "department",
+ "staffing_plan",
+ "planned_vacancies",
+ "section_break_6",
+ "publish",
+ "route",
+ "column_break_12",
+ "job_application_route",
+ "section_break_14",
+ "description",
+ "section_break_16",
+ "currency",
+ "lower_range",
+ "upper_range",
+ "column_break_20",
+ "publish_salary_range"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "job_title",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Job Title",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "job_title",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Job Title",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "company",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Company",
- "length": 0,
- "no_copy": 0,
- "options": "Company",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "status",
- "fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 1,
- "label": "Status",
- "length": 0,
- "no_copy": 0,
- "options": "Open\nClosed",
- "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": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "options": "Open\nClosed"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_5",
- "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_5",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "designation",
- "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": "Designation",
- "length": 0,
- "no_copy": 0,
- "options": "Designation",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "designation",
+ "fieldtype": "Link",
+ "label": "Designation",
+ "options": "Designation",
+ "reqd": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "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": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "department",
+ "fieldtype": "Link",
+ "label": "Department",
+ "options": "Department"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "staffing_plan",
- "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": "Staffing Plan",
- "length": 0,
- "no_copy": 0,
- "options": "Staffing Plan",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "staffing_plan",
+ "fieldtype": "Link",
+ "label": "Staffing Plan",
+ "options": "Staffing Plan",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "staffing_plan",
- "fieldname": "planned_vacancies",
- "fieldtype": "Int",
- "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": "Planned number of Positions",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "depends_on": "staffing_plan",
+ "fieldname": "planned_vacancies",
+ "fieldtype": "Int",
+ "label": "Planned number of Positions",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_6",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "section_break_6",
+ "fieldtype": "Section Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "publish",
- "fieldtype": "Check",
- "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": "Publish on website",
- "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
- },
+ "default": "0",
+ "fieldname": "publish",
+ "fieldtype": "Check",
+ "label": "Publish on website"
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "publish",
- "fieldname": "route",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Route",
- "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,
+ "depends_on": "publish",
+ "fieldname": "route",
+ "fieldtype": "Data",
+ "label": "Route",
"unique": 1
- },
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "Job profile, qualifications required etc.",
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "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": "Description",
- "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
+ "description": "Job profile, qualifications required etc.",
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "in_list_view": 1,
+ "label": "Description"
+ },
+ {
+ "fieldname": "column_break_12",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_14",
+ "fieldtype": "Section Break"
+ },
+ {
+ "collapsible": 1,
+ "fieldname": "section_break_16",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "label": "Currency",
+ "options": "Currency"
+ },
+ {
+ "fieldname": "lower_range",
+ "fieldtype": "Currency",
+ "label": "Lower Range",
+ "options": "currency",
+ "precision": "0"
+ },
+ {
+ "fieldname": "upper_range",
+ "fieldtype": "Currency",
+ "label": "Upper Range",
+ "options": "currency",
+ "precision": "0"
+ },
+ {
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "publish",
+ "description": "Route to the custom Job Application Webform",
+ "fieldname": "job_application_route",
+ "fieldtype": "Data",
+ "label": "Job Application Route"
+ },
+ {
+ "default": "0",
+ "fieldname": "publish_salary_range",
+ "fieldtype": "Check",
+ "label": "Publish Salary Range"
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "icon": "fa fa-bookmark",
- "idx": 1,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-05-20 15:38:44.705823",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "Job Opening",
- "owner": "Administrator",
+ ],
+ "icon": "fa fa-bookmark",
+ "idx": 1,
+ "links": [],
+ "modified": "2020-09-18 11:23:29.488923",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "Job Opening",
+ "owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "HR User",
- "set_user_permissions": 0,
- "share": 1,
- "submit": 0,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "HR User",
+ "share": 1,
"write": 1
- },
+ },
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
- "print": 0,
- "read": 1,
- "report": 0,
- "role": "Guest",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "read": 1,
+ "role": "Guest"
}
- ],
- "quick_entry": 0,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_order": "ASC",
- "track_changes": 0,
- "track_seen": 0
+ ],
+ "sort_field": "modified",
+ "sort_order": "ASC"
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/job_opening/job_opening.py b/erpnext/hr/doctype/job_opening/job_opening.py
index 00883d7..1e89767 100644
--- a/erpnext/hr/doctype/job_opening/job_opening.py
+++ b/erpnext/hr/doctype/job_opening/job_opening.py
@@ -43,9 +43,8 @@
current_count = designation_counts['employee_count'] + designation_counts['job_openings']
if self.planned_vacancies <= current_count:
- frappe.throw(_("Job Openings for designation {0} already open \
- or hiring completed as per Staffing Plan {1}"
- .format(self.designation, self.staffing_plan)))
+ frappe.throw(_("Job Openings for designation {0} already open or hiring completed as per Staffing Plan {1}").format(
+ self.designation, self.staffing_plan))
def get_context(self, context):
context.parents = [{'route': 'jobs', 'title': _('All Jobs') }]
@@ -56,7 +55,8 @@
context.get_list = get_job_openings
def get_job_openings(doctype, txt=None, filters=None, limit_start=0, limit_page_length=20, order_by=None):
- fields = ['name', 'status', 'job_title', 'description']
+ fields = ['name', 'status', 'job_title', 'description', 'publish_salary_range',
+ 'lower_range', 'upper_range', 'currency', 'job_application_route']
filters = filters or {}
filters.update({
diff --git a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html
index 5da8cc8..c015101 100644
--- a/erpnext/hr/doctype/job_opening/templates/job_opening_row.html
+++ b/erpnext/hr/doctype/job_opening/templates/job_opening_row.html
@@ -1,9 +1,18 @@
<div class="my-5">
<h3>{{ doc.job_title }}</h3>
<p>{{ doc.description }}</p>
+ {%- if doc.publish_salary_range -%}
+ <p><b>{{_("Salary range per month")}}: </b>{{ frappe.format_value(frappe.utils.flt(doc.lower_range), currency=doc.currency) }} - {{ frappe.format_value(frappe.utils.flt(doc.upper_range), currency=doc.currency) }}</p>
+ {% endif %}
<div>
- <a class="btn btn-primary"
- href="/job_application?new=1&job_title={{ doc.name }}">
+ {%- if doc.job_application_route -%}
+ <a class='btn btn-primary'
+ href='/{{doc.job_application_route}}?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
+ {% else %}
+ <a class='btn btn-primary'
+ href='/job_application?new=1&job_title={{ doc.name }}'>
+ {{ _("Apply Now") }}</a>
+ {% endif %}
</div>
</div>
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.json b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
index 4b31501..3a300c0 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.json
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.json
@@ -11,6 +11,7 @@
"employee",
"employee_name",
"department",
+ "company",
"column_break1",
"leave_type",
"from_date",
@@ -219,6 +220,15 @@
"label": "Leave Policy Assignment",
"options": "Leave Policy Assignment",
"read_only": 1
+ },
+ {
+ "fetch_from": "employee.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1,
+ "reqd": 1
}
],
"icon": "fa fa-ok",
@@ -226,7 +236,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-08-20 14:25:10.314323",
+ "modified": "2021-01-04 18:46:13.184104",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Allocation",
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
index a09cd2e..5e3822e 100755
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py
@@ -92,7 +92,7 @@
frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} to {3}")
.format(self.leave_type, self.employee, formatdate(self.from_date), formatdate(self.to_date)))
- frappe.throw(_('Reference') + ': <a href="#Form/Leave Allocation/{0}">{0}</a>'
+ frappe.throw(_('Reference') + ': <a href="/app/Form/Leave Allocation/{0}">{0}</a>'
.format(leave_allocation[0][0]), OverlapError)
def validate_back_dated_allocation(self):
diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation_list.js b/erpnext/hr/doctype/leave_allocation/leave_allocation_list.js
index 93f7b83..3ab176f 100644
--- a/erpnext/hr/doctype/leave_allocation/leave_allocation_list.js
+++ b/erpnext/hr/doctype/leave_allocation/leave_allocation_list.js
@@ -5,7 +5,7 @@
frappe.listview_settings['Leave Allocation'] = {
get_indicator: function(doc) {
if(doc.status==="Expired") {
- return [__("Expired"), "darkgrey", "expired, =, 1"];
+ return [__("Expired"), "gray", "expired, =, 1"];
}
},
};
diff --git a/erpnext/hr/doctype/leave_application/leave_application.js b/erpnext/hr/doctype/leave_application/leave_application.js
index d62e418..9ccb915 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.js
+++ b/erpnext/hr/doctype/leave_application/leave_application.js
@@ -75,7 +75,8 @@
frm.dashboard.add_section(
frappe.render_template('leave_application_dashboard', {
data: leave_details
- })
+ }),
+ __("Allocated Leaves")
);
frm.dashboard.show();
let allowed_leave_types = Object.keys(leave_details);
diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py
index 4f3e462..132c3bd 100755
--- a/erpnext/hr/doctype/leave_application/leave_application.py
+++ b/erpnext/hr/doctype/leave_application/leave_application.py
@@ -245,7 +245,7 @@
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'])) \
- + """ <b><a href="#Form/Leave Application/{0}">{0}</a></b>""".format(d["name"])
+ + """ <b><a href="/app/Form/Leave Application/{0}">{0}</a></b>""".format(d["name"])
frappe.throw(msg, OverlapError)
def get_total_leaves_on_half_day(self):
diff --git a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
index d30e3b9..9f667a6 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
+++ b/erpnext/hr/doctype/leave_application/leave_application_dashboard.html
@@ -1,15 +1,14 @@
{% if not jQuery.isEmptyObject(data) %}
-<h5 style="margin-top: 20px;"> {{ __("Allocated Leaves") }} </h5>
<table class="table table-bordered small">
<thead>
<tr>
<th style="width: 16%">{{ __("Leave Type") }}</th>
- <th style="width: 16%" class="text-right">{{ __("Total Allocated Leaves") }}</th>
- <th style="width: 16%" class="text-right">{{ __("Expired Leaves") }}</th>
- <th style="width: 16%" class="text-right">{{ __("Used Leaves") }}</th>
- <th style="width: 16%" class="text-right">{{ __("Pending Leaves") }}</th>
- <th style="width: 16%" class="text-right">{{ __("Available Leaves") }}</th>
+ <th style="width: 16%" class="text-right">{{ __("Total Allocated Leave") }}</th>
+ <th style="width: 16%" class="text-right">{{ __("Expired Leave") }}</th>
+ <th style="width: 16%" class="text-right">{{ __("Used Leave") }}</th>
+ <th style="width: 16%" class="text-right">{{ __("Pending Leave") }}</th>
+ <th style="width: 16%" class="text-right">{{ __("Available Leave") }}</th>
</tr>
</thead>
<tbody>
@@ -26,5 +25,5 @@
</tbody>
</table>
{% else %}
-<p style="margin-top: 30px;"> No Leaves have been allocated. </p>
-{% endif %}
\ No newline at end of file
+<p style="margin-top: 30px;"> No Leave has been allocated. </p>
+{% endif %}
diff --git a/erpnext/hr/doctype/leave_application/leave_application_list.js b/erpnext/hr/doctype/leave_application/leave_application_list.js
index cbb4b73..a3c03b1 100644
--- a/erpnext/hr/doctype/leave_application/leave_application_list.js
+++ b/erpnext/hr/doctype/leave_application/leave_application_list.js
@@ -1,5 +1,6 @@
frappe.listview_settings['Leave Application'] = {
add_fields: ["leave_type", "employee", "employee_name", "total_leave_days", "from_date", "to_date"],
+ has_indicator_for_draft: 1,
get_indicator: function (doc) {
if (doc.status === "Approved") {
return [__("Approved"), "green", "status,=,Approved"];
diff --git a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
index 4abba5f..d74760a 100644
--- a/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
+++ b/erpnext/hr/doctype/leave_ledger_entry/leave_ledger_entry.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2019-05-09 15:47:39.760406",
"doctype": "DocType",
"engine": "InnoDB",
@@ -8,6 +9,7 @@
"leave_type",
"transaction_type",
"transaction_name",
+ "company",
"leaves",
"column_break_7",
"from_date",
@@ -106,12 +108,22 @@
"fieldtype": "Link",
"label": "Holiday List",
"options": "Holiday List"
+ },
+ {
+ "fetch_from": "employee.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company",
+ "read_only": 1,
+ "reqd": 1
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2020-09-04 12:16:36.569066",
+ "links": [],
+ "modified": "2021-01-04 18:47:45.146652",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Ledger Entry",
diff --git a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
index ff5dc2f..e0ec4be 100644
--- a/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
+++ b/erpnext/hr/doctype/leave_policy/leave_policy_dashboard.py
@@ -4,22 +4,10 @@
def get_data():
return {
'fieldname': 'leave_policy',
- 'non_standard_fieldnames': {
- 'Employee Grade': 'default_leave_policy'
- },
'transactions': [
{
- 'label': _('Employees'),
- 'items': ['Employee', 'Employee Grade']
- },
- {
'label': _('Leaves'),
'items': ['Leave Allocation']
},
]
- }
-
-
-
-
-
\ No newline at end of file
+ }
\ No newline at end of file
diff --git a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
index ecebb3b..a0327bd 100644
--- a/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
+++ b/erpnext/hr/doctype/leave_policy_assignment/leave_policy_assignment.json
@@ -111,13 +111,14 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-10-15 15:18:15.227848",
+ "modified": "2020-12-31 16:43:30.695206",
"modified_by": "Administrator",
"module": "HR",
"name": "Leave Policy Assignment",
"owner": "Administrator",
"permissions": [
{
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -127,9 +128,11 @@
"report": 1,
"role": "HR Manager",
"share": 1,
+ "submit": 1,
"write": 1
},
{
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -139,9 +142,11 @@
"report": 1,
"role": "HR User",
"share": 1,
+ "submit": 1,
"write": 1
},
{
+ "cancel": 1,
"create": 1,
"delete": 1,
"email": 1,
@@ -151,6 +156,7 @@
"report": 1,
"role": "System Manager",
"share": 1,
+ "submit": 1,
"write": 1
}
],
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment.py b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
index 2c385e8..ab65260 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment.py
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment.py
@@ -88,7 +88,7 @@
def add_assignments(events, start, end, conditions=None):
query = """select name, start_date, end_date, employee_name,
- employee, docstatus
+ employee, docstatus, shift_type
from `tabShift Assignment` where
start_date >= %(start_date)s
or end_date <= %(end_date)s
@@ -97,18 +97,40 @@
if conditions:
query += conditions
- for d in frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True):
- e = {
- "name": d.name,
- "doctype": "Shift Assignment",
- "start_date": d.start_date,
- "end_date": d.end_date if d.end_date else nowdate(),
- "title": cstr(d.employee_name) + ": "+ \
- cstr(d.shift_type),
- "docstatus": d.docstatus
- }
- if e not in events:
- events.append(e)
+ records = frappe.db.sql(query, {"start_date":start, "end_date":end}, as_dict=True)
+ shift_timing_map = get_shift_type_timing([d.shift_type for d in records])
+
+ for d in records:
+ daily_event_start = d.start_date
+ daily_event_end = d.end_date if d.end_date else getdate()
+ delta = timedelta(days=1)
+ while daily_event_start <= daily_event_end:
+ start_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['start_time']
+ end_timing = frappe.utils.get_datetime(daily_event_start)+ shift_timing_map[d.shift_type]['end_time']
+ daily_event_start += delta
+ e = {
+ "name": d.name,
+ "doctype": "Shift Assignment",
+ "start_date": start_timing,
+ "end_date": end_timing,
+ "title": cstr(d.employee_name) + ": "+ \
+ cstr(d.shift_type),
+ "docstatus": d.docstatus,
+ "allDay": 0
+ }
+ if e not in events:
+ events.append(e)
+
+ return events
+
+def get_shift_type_timing(shift_types):
+ shift_timing_map = {}
+ data = frappe.get_all("Shift Type", filters = {"name": ("IN", shift_types)}, fields = ['name', 'start_time', 'end_time'])
+
+ for d in data:
+ shift_timing_map[d.name] = d
+
+ return shift_timing_map
def get_employee_shift(employee, for_date=nowdate(), consider_default_shift=False, next_shift_direction=None):
diff --git a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js
index 17a986d..bb692e1 100644
--- a/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js
+++ b/erpnext/hr/doctype/shift_assignment/shift_assignment_calendar.js
@@ -6,14 +6,8 @@
"start": "start_date",
"end": "end_date",
"id": "name",
- "docstatus": 1
- },
- options: {
- header: {
- left: 'prev,next today',
- center: 'title',
- right: 'month'
- }
+ "docstatus": 1,
+ "allDay": "allDay",
},
get_events_method: "erpnext.hr.doctype.shift_assignment.shift_assignment.get_events"
}
\ No newline at end of file
diff --git a/erpnext/hr/doctype/shift_request/shift_request.py b/erpnext/hr/doctype/shift_request/shift_request.py
index 1c2801b..473193d 100644
--- a/erpnext/hr/doctype/shift_request/shift_request.py
+++ b/erpnext/hr/doctype/shift_request/shift_request.py
@@ -87,5 +87,5 @@
def throw_overlap_error(self, d):
msg = _("Employee {0} has already applied for {1} between {2} and {3} : ").format(self.employee,
d['shift_type'], formatdate(d['from_date']), formatdate(d['to_date'])) \
- + """ <b><a href="#Form/Shift Request/{0}">{0}</a></b>""".format(d["name"])
+ + """ <b><a href="/app/Form/Shift Request/{0}">{0}</a></b>""".format(d["name"])
frappe.throw(msg, OverlapError)
\ No newline at end of file
diff --git a/erpnext/hr/page/team_updates/team_updates.js b/erpnext/hr/page/team_updates/team_updates.js
index da1f531..13d0074 100644
--- a/erpnext/hr/page/team_updates/team_updates.js
+++ b/erpnext/hr/page/team_updates/team_updates.js
@@ -41,7 +41,7 @@
me.add_row(d);
});
} else {
- frappe.show_alert({message:__('No more updates'), indicator:'darkgrey'});
+ frappe.show_alert({message: __('No more updates'), indicator: 'gray'});
me.more.parent().addClass('hidden');
}
}
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 1b92358..06f9160 100644
--- a/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
+++ b/erpnext/hr/report/employee_leave_balance/employee_leave_balance.py
@@ -40,17 +40,17 @@
'fieldname': 'opening_balance',
'width': 130,
}, {
- 'label': _('Leaves Allocated'),
+ 'label': _('Leave Allocated'),
'fieldtype': 'float',
'fieldname': 'leaves_allocated',
'width': 130,
}, {
- 'label': _('Leaves Taken'),
+ 'label': _('Leave Taken'),
'fieldtype': 'float',
'fieldname': 'leaves_taken',
'width': 130,
}, {
- 'label': _('Leaves Expired'),
+ 'label': _('Leave Expired'),
'fieldtype': 'float',
'fieldname': 'leaves_expired',
'width': 130,
diff --git a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
index 4608212..c5929c6 100644
--- a/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
+++ b/erpnext/hr/report/monthly_attendance_sheet/monthly_attendance_sheet.py
@@ -36,6 +36,8 @@
conditions, filters = get_conditions(filters)
columns, days = get_columns(filters)
att_map = get_attendance_list(conditions, filters)
+ if not att_map:
+ return columns, [], None, None
if filters.group_by:
emp_map, group_by_parameters = get_employee_details(filters.group_by, filters.company)
@@ -65,10 +67,14 @@
if filters.group_by:
emp_att_map = {}
for parameter in group_by_parameters:
- data.append([ "<b>"+ parameter + "</b>"])
- record, aaa = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list)
- emp_att_map.update(aaa)
- data += record
+ emp_map_set = set([key for key in emp_map[parameter].keys()])
+ att_map_set = set([key for key in att_map.keys()])
+ if (att_map_set & emp_map_set):
+ parameter_row = ["<b>"+ parameter + "</b>"] + ['' for day in range(filters["total_days_in_month"] + 2)]
+ data.append(parameter_row)
+ record, emp_att_data = add_data(emp_map[parameter], att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list)
+ emp_att_map.update(emp_att_data)
+ data += record
else:
record, emp_att_map = add_data(emp_map, att_map, filters, holiday_map, conditions, default_holiday_list, leave_list=leave_list)
data += record
@@ -237,6 +243,9 @@
status from tabAttendance where docstatus = 1 %s order by employee, attendance_date""" %
conditions, filters, as_dict=1)
+ if not attendance_list:
+ msgprint(_("No attendance record found"), alert=True, indicator="orange")
+
att_map = {}
for d in attendance_list:
att_map.setdefault(d.employee, frappe._dict()).setdefault(d.day_of_month, "")
diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py
index d700e7f..e2aa7a4 100644
--- a/erpnext/hr/utils.py
+++ b/erpnext/hr/utils.py
@@ -211,7 +211,7 @@
def throw_overlap_error(doc, exists_for, overlap_doc, from_date, to_date):
msg = _("A {0} exists between {1} and {2} (").format(doc.doctype,
formatdate(from_date), formatdate(to_date)) \
- + """ <b><a href="#Form/{0}/{1}">{1}</a></b>""".format(doc.doctype, overlap_doc) \
+ + """ <b><a href="/app/Form/{0}/{1}">{1}</a></b>""".format(doc.doctype, overlap_doc) \
+ _(") for {0}").format(exists_for)
frappe.throw(msg)
diff --git a/erpnext/hr/web_form/job_application/job_application.json b/erpnext/hr/web_form/job_application/job_application.json
index f630570..512ba5c 100644
--- a/erpnext/hr/web_form/job_application/job_application.json
+++ b/erpnext/hr/web_form/job_application/job_application.json
@@ -1,86 +1,200 @@
{
- "accept_payment": 0,
- "allow_comments": 1,
- "allow_delete": 0,
- "allow_edit": 1,
- "allow_incomplete": 0,
- "allow_multiple": 1,
- "allow_print": 0,
- "amount": 0.0,
- "amount_based_on_field": 0,
- "creation": "2016-09-10 02:53:16.598314",
- "doc_type": "Job Applicant",
- "docstatus": 0,
- "doctype": "Web Form",
- "idx": 0,
- "introduction_text": "",
- "is_standard": 1,
- "login_required": 0,
- "max_attachment_size": 0,
- "modified": "2016-12-20 00:21:44.081622",
- "modified_by": "Administrator",
- "module": "HR",
- "name": "job-application",
- "owner": "Administrator",
- "published": 1,
- "route": "job_application",
- "show_sidebar": 1,
- "sidebar_items": [],
- "success_message": "Thank you for applying.",
- "success_url": "/jobs",
- "title": "Job Application",
+ "accept_payment": 0,
+ "allow_comments": 1,
+ "allow_delete": 0,
+ "allow_edit": 1,
+ "allow_incomplete": 0,
+ "allow_multiple": 1,
+ "allow_print": 0,
+ "amount": 0.0,
+ "amount_based_on_field": 0,
+ "apply_document_permissions": 0,
+ "client_script": "frappe.web_form.on('resume_link', (field, value) => {\n if (!frappe.utils.is_url(value)) {\n frappe.msgprint(__('Resume link not valid'));\n }\n});\n",
+ "creation": "2016-09-10 02:53:16.598314",
+ "doc_type": "Job Applicant",
+ "docstatus": 0,
+ "doctype": "Web Form",
+ "idx": 0,
+ "introduction_text": "",
+ "is_standard": 1,
+ "login_required": 0,
+ "max_attachment_size": 0,
+ "modified": "2020-10-07 19:27:17.143355",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "job-application",
+ "owner": "Administrator",
+ "published": 1,
+ "route": "job_application",
+ "route_to_success_link": 0,
+ "show_attachments": 0,
+ "show_in_grid": 0,
+ "show_sidebar": 1,
+ "sidebar_items": [],
+ "success_message": "Thank you for applying.",
+ "success_url": "/jobs",
+ "title": "Job Application",
"web_form_fields": [
{
- "fieldname": "job_title",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Job Opening",
- "max_length": 0,
- "max_value": 0,
- "options": "",
- "read_only": 1,
- "reqd": 0
- },
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "job_title",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Job Opening",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "",
+ "read_only": 1,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
{
- "fieldname": "applicant_name",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Applicant Name",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 1
- },
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "applicant_name",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Applicant Name",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 1,
+ "show_in_filter": 0
+ },
{
- "fieldname": "email_id",
- "fieldtype": "Data",
- "hidden": 0,
- "label": "Email Address",
- "max_length": 0,
- "max_value": 0,
- "options": "Email",
- "read_only": 0,
- "reqd": 1
- },
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "email_id",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Email Address",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "Email",
+ "read_only": 0,
+ "reqd": 1,
+ "show_in_filter": 0
+ },
{
- "fieldname": "cover_letter",
- "fieldtype": "Text",
- "hidden": 0,
- "label": "Cover Letter",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0
- },
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "phone_number",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Phone Number",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "Phone",
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
{
- "fieldname": "resume_attachment",
- "fieldtype": "Attach",
- "hidden": 0,
- "label": "Resume Attachment",
- "max_length": 0,
- "max_value": 0,
- "read_only": 0,
- "reqd": 0
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "country",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "label": "Country of Residence",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "Country",
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "cover_letter",
+ "fieldtype": "Text",
+ "hidden": 0,
+ "label": "Cover Letter",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "resume_link",
+ "fieldtype": "Data",
+ "hidden": 0,
+ "label": "Resume Link",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "",
+ "fieldtype": "Section Break",
+ "hidden": 0,
+ "label": "Expected Salary Range per month",
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 1,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "currency",
+ "fieldtype": "Link",
+ "hidden": 0,
+ "label": "Currency",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "Currency",
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "lower_range",
+ "fieldtype": "Currency",
+ "hidden": 0,
+ "label": "Lower Range",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "currency",
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "",
+ "fieldtype": "Column Break",
+ "hidden": 0,
+ "max_length": 0,
+ "max_value": 0,
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
+ },
+ {
+ "allow_read_on_all_link_options": 0,
+ "fieldname": "upper_range",
+ "fieldtype": "Currency",
+ "hidden": 0,
+ "label": "Upper Range",
+ "max_length": 0,
+ "max_value": 0,
+ "options": "currency",
+ "read_only": 0,
+ "reqd": 0,
+ "show_in_filter": 0
}
]
}
\ No newline at end of file
diff --git a/erpnext/hr/workspace/hr/hr.json b/erpnext/hr/workspace/hr/hr.json
new file mode 100644
index 0000000..f650b24
--- /dev/null
+++ b/erpnext/hr/workspace/hr/hr.json
@@ -0,0 +1,829 @@
+{
+ "category": "Modules",
+ "charts": [
+ {
+ "chart_name": "Outgoing Salary",
+ "label": "Outgoing Salary"
+ }
+ ],
+ "creation": "2020-03-02 15:48:58.322521",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "hr",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "HR",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee",
+ "link_to": "Employee",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employment Type",
+ "link_to": "Employment Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Branch",
+ "link_to": "Branch",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Department",
+ "link_to": "Department",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Designation",
+ "link_to": "Designation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Grade",
+ "link_to": "Employee Grade",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Group",
+ "link_to": "Employee Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Health Insurance",
+ "link_to": "Employee Health Insurance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Lifecycle",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Job Applicant",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Onboarding",
+ "link_to": "Employee Onboarding",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Skill Map",
+ "link_to": "Employee Skill Map",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Promotion",
+ "link_to": "Employee Promotion",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Transfer",
+ "link_to": "Employee Transfer",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Separation",
+ "link_to": "Employee Separation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Onboarding Template",
+ "link_to": "Employee Onboarding Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Separation Template",
+ "link_to": "Employee Separation Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shift Management",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shift Type",
+ "link_to": "Shift Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shift Request",
+ "link_to": "Shift Request",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shift Assignment",
+ "link_to": "Shift Assignment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leaves",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Application",
+ "link_to": "Leave Application",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Allocation",
+ "link_to": "Leave Allocation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Leave Type",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Policy",
+ "link_to": "Leave Policy",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Period",
+ "link_to": "Leave Period",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Type",
+ "link_to": "Leave Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Holiday List",
+ "link_to": "Holiday List",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Compensatory Leave Request",
+ "link_to": "Compensatory Leave Request",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Encashment",
+ "link_to": "Leave Encashment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Leave Block List",
+ "link_to": "Leave Block List",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Leave Application",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employee Leave Balance",
+ "link_to": "Employee Leave Balance",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Attendance",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Attendance Tool",
+ "link_to": "Employee Attendance Tool",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Attendance",
+ "link_to": "Attendance",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Attendance Request",
+ "link_to": "Attendance Request",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Upload Attendance",
+ "link_to": "Upload Attendance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Checkin",
+ "link_to": "Employee Checkin",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Attendance",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Monthly Attendance Sheet",
+ "link_to": "Monthly Attendance Sheet",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Expense Claims",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Expense Claim",
+ "link_to": "Expense Claim",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Advance",
+ "link_to": "Employee Advance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "HR Settings",
+ "link_to": "HR Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Daily Work Summary Group",
+ "link_to": "Daily Work Summary Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Team Updates",
+ "link_to": "team-updates",
+ "link_type": "Page",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fleet Management",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Vehicle",
+ "link_to": "Vehicle",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Vehicle Log",
+ "link_to": "Vehicle Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Vehicle",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Vehicle Expenses",
+ "link_to": "Vehicle Expenses",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Recruitment",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Job Opening",
+ "link_to": "Job Opening",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Job Applicant",
+ "link_to": "Job Applicant",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Job Offer",
+ "link_to": "Job Offer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Staffing Plan",
+ "link_to": "Staffing Plan",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loans",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Application",
+ "link_to": "Loan Application",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan",
+ "link_to": "Loan",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Type",
+ "link_to": "Loan Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Training",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Training Program",
+ "link_to": "Training Program",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Training Event",
+ "link_to": "Training Event",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Training Result",
+ "link_to": "Training Result",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Training Feedback",
+ "link_to": "Training Feedback",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employee Birthday",
+ "link_to": "Employee Birthday",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Employees working on a holiday",
+ "link_to": "Employees working on a holiday",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Performance",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Appraisal",
+ "link_to": "Appraisal",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Appraisal Template",
+ "link_to": "Appraisal Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Energy Point Rule",
+ "link_to": "Energy Point Rule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Energy Point Log",
+ "link_to": "Energy Point Log",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Tax and Benefits",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Tax Exemption Declaration",
+ "link_to": "Employee Tax Exemption Declaration",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Tax Exemption Proof Submission",
+ "link_to": "Employee Tax Exemption Proof Submission",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee, Payroll Period",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Other Income",
+ "link_to": "Employee Other Income",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Benefit Application",
+ "link_to": "Employee Benefit Application",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Benefit Claim",
+ "link_to": "Employee Benefit Claim",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Tax Exemption Category",
+ "link_to": "Employee Tax Exemption Category",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Tax Exemption Sub Category",
+ "link_to": "Employee Tax Exemption Sub Category",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2021-01-21 13:38:38.941001",
+ "modified_by": "Administrator",
+ "module": "HR",
+ "name": "HR",
+ "onboarding": "Human Resource",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "color": "Green",
+ "format": "{} Active",
+ "label": "Employee",
+ "link_to": "Employee",
+ "stats_filter": "{\"status\":\"Active\"}",
+ "type": "DocType"
+ },
+ {
+ "color": "Yellow",
+ "format": "{} Open",
+ "label": "Leave Application",
+ "link_to": "Leave Application",
+ "stats_filter": "{\"status\":\"Open\"}",
+ "type": "DocType"
+ },
+ {
+ "label": "Attendance",
+ "link_to": "Attendance",
+ "stats_filter": "",
+ "type": "DocType"
+ },
+ {
+ "label": "Job Applicant",
+ "link_to": "Job Applicant",
+ "type": "DocType"
+ },
+ {
+ "label": "Monthly Attendance Sheet",
+ "link_to": "Monthly Attendance Sheet",
+ "type": "Report"
+ },
+ {
+ "format": "{} Open",
+ "label": "Dashboard",
+ "link_to": "Human Resource",
+ "stats_filter": "{\n \"status\": \"Open\"\n}",
+ "type": "Dashboard"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json b/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json
new file mode 100644
index 0000000..b8abf21
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/loan_disbursements/loan_disbursements.json
@@ -0,0 +1,29 @@
+{
+ "based_on": "disbursement_date",
+ "chart_name": "Loan Disbursements",
+ "chart_type": "Sum",
+ "creation": "2021-02-06 18:40:36.148470",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Loan Disbursement",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Disbursement\",\"docstatus\",\"=\",\"1\",false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "modified": "2021-02-06 18:40:49.308663",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Disbursements",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "",
+ "time_interval": "Daily",
+ "timeseries": 1,
+ "timespan": "Last Month",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "disbursed_amount",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json
new file mode 100644
index 0000000..aa0f78a
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/loan_interest_accrual/loan_interest_accrual.json
@@ -0,0 +1,31 @@
+{
+ "based_on": "posting_date",
+ "chart_name": "Loan Interest Accrual",
+ "chart_type": "Sum",
+ "color": "#39E4A5",
+ "creation": "2021-02-18 20:07:04.843876",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Loan Interest Accrual",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Interest Accrual\",\"docstatus\",\"=\",\"1\",false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2021-02-21 21:01:26.022634",
+ "modified": "2021-02-21 21:01:44.930712",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Interest Accrual",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "",
+ "time_interval": "Monthly",
+ "timeseries": 1,
+ "timespan": "Last Year",
+ "type": "Line",
+ "use_report_chart": 0,
+ "value_based_on": "interest_amount",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json
new file mode 100644
index 0000000..35bd43b
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/new_loans/new_loans.json
@@ -0,0 +1,31 @@
+{
+ "based_on": "creation",
+ "chart_name": "New Loans",
+ "chart_type": "Count",
+ "color": "#449CF0",
+ "creation": "2021-02-06 16:59:27.509170",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false]]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2021-02-21 20:55:33.515025",
+ "modified": "2021-02-21 21:00:33.900821",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "New Loans",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "",
+ "time_interval": "Daily",
+ "timeseries": 1,
+ "timespan": "Last Month",
+ "type": "Bar",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json b/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
new file mode 100644
index 0000000..76c27b0
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
@@ -0,0 +1,31 @@
+{
+ "based_on": "",
+ "chart_name": "Top 10 Pledged Loan Securities",
+ "chart_type": "Custom",
+ "color": "#EC864B",
+ "creation": "2021-02-06 22:02:46.284479",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart",
+ "document_type": "",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[]",
+ "group_by_type": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "last_synced_on": "2021-02-21 21:00:57.043034",
+ "modified": "2021-02-21 21:01:10.048623",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Top 10 Pledged Loan Securities",
+ "number_of_groups": 0,
+ "owner": "Administrator",
+ "source": "Top 10 Pledged Loan Securities",
+ "time_interval": "Yearly",
+ "timeseries": 0,
+ "timespan": "Last Year",
+ "type": "Bar",
+ "use_report_chart": 0,
+ "value_based_on": "",
+ "y_axis": []
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/loan_management/dashboard_chart_source/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/loan_management/dashboard_chart_source/__init__.py
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/__init__.py
diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js
new file mode 100644
index 0000000..cf75cc8
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.js
@@ -0,0 +1,14 @@
+frappe.provide('frappe.dashboards.chart_sources');
+
+frappe.dashboards.chart_sources["Top 10 Pledged Loan Securities"] = {
+ method: "erpnext.loan_management.dashboard_chart_source.top_10_pledged_loan_securities.top_10_pledged_loan_securities.get_data",
+ filters: [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company")
+ }
+ ]
+};
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
new file mode 100644
index 0000000..42c9b1c
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.json
@@ -0,0 +1,13 @@
+{
+ "creation": "2021-02-06 22:01:01.332628",
+ "docstatus": 0,
+ "doctype": "Dashboard Chart Source",
+ "idx": 0,
+ "modified": "2021-02-06 22:01:01.332628",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Top 10 Pledged Loan Securities",
+ "owner": "Administrator",
+ "source_name": "Top 10 Pledged Loan Securities ",
+ "timeseries": 0
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py
new file mode 100644
index 0000000..6bb0440
--- /dev/null
+++ b/erpnext/loan_management/dashboard_chart_source/top_10_pledged_loan_securities/top_10_pledged_loan_securities.py
@@ -0,0 +1,76 @@
+# 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
+from frappe.utils.dashboard import cache_source
+from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \
+ import get_loan_security_details
+from six import iteritems
+
+@frappe.whitelist()
+@cache_source
+def get_data(chart_name = None, chart = None, no_cache = None, filters = None, from_date = None,
+ to_date = None, timespan = None, time_interval = None, heatmap_year = None):
+ if chart_name:
+ chart = frappe.get_doc('Dashboard Chart', chart_name)
+ else:
+ chart = frappe._dict(frappe.parse_json(chart))
+
+ filters = {}
+ current_pledges = {}
+
+ if filters:
+ filters = frappe.parse_json(filters)[0]
+
+ conditions = ""
+ labels = []
+ values = []
+
+ if filters.get('company'):
+ conditions = "AND company = %(company)s"
+
+ loan_security_details = get_loan_security_details()
+
+ unpledges = frappe._dict(frappe.db.sql("""
+ SELECT u.loan_security, sum(u.qty) as qty
+ FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
+ WHERE u.parent = up.name
+ AND up.status = 'Approved'
+ {conditions}
+ GROUP BY u.loan_security
+ """.format(conditions=conditions), filters, as_list=1))
+
+ pledges = frappe._dict(frappe.db.sql("""
+ SELECT p.loan_security, sum(p.qty) as qty
+ FROM `tabLoan Security Pledge` lp, `tabPledge`p
+ WHERE p.parent = lp.name
+ AND lp.status = 'Pledged'
+ {conditions}
+ GROUP BY p.loan_security
+ """.format(conditions=conditions), filters, as_list=1))
+
+ for security, qty in iteritems(pledges):
+ current_pledges.setdefault(security, qty)
+ current_pledges[security] -= unpledges.get(security, 0.0)
+
+ sorted_pledges = dict(sorted(current_pledges.items(), key=lambda item: item[1], reverse=True))
+
+ count = 0
+ for security, qty in iteritems(sorted_pledges):
+ values.append(qty * loan_security_details.get(security, {}).get('latest_price', 0))
+ labels.append(security)
+ count +=1
+
+ ## Just need top 10 securities
+ if count == 10:
+ break
+
+ return {
+ 'labels': labels,
+ 'datasets': [{
+ 'name': 'Top 10 Securities',
+ 'chartType': 'bar',
+ 'values': values
+ }]
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/desk_page/loan/loan.json b/erpnext/loan_management/desk_page/loan/loan.json
deleted file mode 100644
index fc59c19..0000000
--- a/erpnext/loan_management/desk_page/loan/loan.json
+++ /dev/null
@@ -1,63 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Loan",
- "links": "[\n {\n \"description\": \"Loan Type for interest and penalty rates\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Applications from customers and employees.\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loans provided to customers and employees.\",\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Loan Processes",
- "links": "[\n {\n \"label\": \"Process Loan Security Shortfall\",\n \"name\": \"Process Loan Security Shortfall\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Process Loan Interest Accrual\",\n \"name\": \"Process Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Disbursement and Repayment",
- "links": "[\n {\n \"label\": \"Loan Disbursement\",\n \"name\": \"Loan Disbursement\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Repayment\",\n \"name\": \"Loan Repayment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Write Off\",\n \"name\": \"Loan Write Off\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Interest Accrual\",\n \"name\": \"Loan Interest Accrual\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Loan Security",
- "links": "[\n {\n \"label\": \"Loan Security Type\",\n \"name\": \"Loan Security Type\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Security Price\",\n \"name\": \"Loan Security Price\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Security\",\n \"name\": \"Loan Security\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Security Pledge\",\n \"name\": \"Loan Security Pledge\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Security Unpledge\",\n \"name\": \"Loan Security Unpledge\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan Security Shortfall\",\n \"name\": \"Loan Security Shortfall\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Reports",
- "links": "[\n {\n \"doctype\": \"Loan Repayment\",\n \"is_query_report\": true,\n \"label\": \"Loan Repayment and Closure\",\n \"name\": \"Loan Repayment and Closure\",\n \"route\": \"#query-report/Loan Repayment and Closure\",\n \"type\": \"report\"\n },\n {\n \"doctype\": \"Loan Security Pledge\",\n \"is_query_report\": true,\n \"label\": \"Loan Security Status\",\n \"name\": \"Loan Security Status\",\n \"route\": \"#query-report/Loan Security Status\",\n \"type\": \"report\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [],
- "creation": "2020-03-12 16:35:55.299820",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Loan",
- "modified": "2020-10-17 12:59:50.336085",
- "modified_by": "Administrator",
- "module": "Loan Management",
- "name": "Loan",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "color": "#ffe8cd",
- "format": "{} Open",
- "label": "Loan Application",
- "link_to": "Loan Application",
- "stats_filter": "{ \"status\": \"Open\" }",
- "type": "DocType"
- },
- {
- "label": "Loan",
- "link_to": "Loan",
- "type": "DocType"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/loan.py b/erpnext/loan_management/doctype/loan/loan.py
index cd40a66..83a813f 100644
--- a/erpnext/loan_management/doctype/loan/loan.py
+++ b/erpnext/loan_management/doctype/loan/loan.py
@@ -6,6 +6,7 @@
import frappe, math, json
import erpnext
from frappe import _
+from six import string_types
from frappe.utils import flt, rounded, add_months, nowdate, getdate, now_datetime
from erpnext.loan_management.doctype.loan_security_unpledge.loan_security_unpledge import get_pledged_security_qty
from erpnext.controllers.accounts_controller import AccountsController
@@ -200,8 +201,12 @@
write_off_limit = frappe.get_value('Loan Type', loan_type, 'write_off_amount')
# checking greater than 0 as there may be some minor precision error
- if pending_amount < write_off_limit:
- # update status as loan closure requested
+ if not pending_amount:
+ frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
+ elif pending_amount < write_off_limit:
+ # Auto create loan write off and update status as loan closure requested
+ write_off = make_loan_write_off(loan)
+ write_off.submit()
frappe.db.set_value('Loan', loan, 'status', 'Loan Closure Requested')
else:
frappe.throw(_("Cannot close loan as there is an outstanding of {0}").format(pending_amount))
@@ -280,10 +285,13 @@
return write_off
@frappe.whitelist()
-def unpledge_security(loan=None, loan_security_pledge=None, as_dict=0, save=0, submit=0, approve=0):
- # if loan is passed it will be considered as full unpledge
+def unpledge_security(loan=None, loan_security_pledge=None, security_map=None, as_dict=0, save=0, submit=0, approve=0):
+ # if no security_map is passed it will be considered as full unpledge
+ if security_map and isinstance(security_map, string_types):
+ security_map = json.loads(security_map)
+
if loan:
- pledge_qty_map = get_pledged_security_qty(loan)
+ pledge_qty_map = security_map or get_pledged_security_qty(loan)
loan_doc = frappe.get_doc('Loan', loan)
unpledge_request = create_loan_security_unpledge(pledge_qty_map, loan_doc.name, loan_doc.company,
loan_doc.applicant_type, loan_doc.applicant)
@@ -332,13 +340,23 @@
return unpledge_request
def validate_employee_currency_with_company_currency(applicant, company):
- from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency
- if not applicant:
- frappe.throw(_("Please select Applicant"))
- if not company:
- frappe.throw(_("Please select Company"))
- employee_currency = get_employee_currency(applicant)
- company_currency = erpnext.get_company_currency(company)
- if employee_currency != company_currency:
- frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
- .format(applicant, employee_currency))
+ from erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment import get_employee_currency
+ if not applicant:
+ frappe.throw(_("Please select Applicant"))
+ if not company:
+ frappe.throw(_("Please select Company"))
+ employee_currency = get_employee_currency(applicant)
+ company_currency = erpnext.get_company_currency(company)
+ if employee_currency != company_currency:
+ frappe.throw(_("Loan cannot be repayed from salary for Employee {0} because salary is processed in currency {1}")
+ .format(applicant, employee_currency))
+
+@frappe.whitelist()
+def get_shortfall_applicants():
+ loans = frappe.get_all('Loan Security Shortfall', {'status': 'Pending'}, pluck='loan')
+ applicants = set(frappe.get_all('Loan', {'name': ('in', loans)}, pluck='name'))
+
+ return {
+ "value": len(applicants),
+ "fieldtype": "Int"
+ }
\ No newline at end of file
diff --git a/erpnext/loan_management/doctype/loan/test_loan.py b/erpnext/loan_management/doctype/loan/test_loan.py
index a63d065..13a2094 100644
--- a/erpnext/loan_management/doctype/loan/test_loan.py
+++ b/erpnext/loan_management/doctype/loan/test_loan.py
@@ -45,7 +45,7 @@
create_loan_security_price("Test Security 2", 250, "Nos", get_datetime() , get_datetime(add_to_date(nowdate(), hours=24)))
self.applicant1 = make_employee("robert_loan@loan.com")
- make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR')
+ make_salary_structure("Test Salary Structure Loan", "Monthly", employee=self.applicant1, currency='INR', company="_Test Company")
if not frappe.db.exists("Customer", "_Test Loan Customer"):
frappe.get_doc(get_customer_dict('_Test Loan Customer')).insert(ignore_permissions=True)
@@ -321,10 +321,68 @@
self.assertEquals(sum(pledged_qty.values()), 0)
amounts = amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- self.assertTrue(amounts['pending_principal_amount'] < 0)
+ self.assertEqual(amounts['pending_principal_amount'], 0)
self.assertEquals(amounts['payable_principal_amount'], 0.0)
self.assertEqual(amounts['interest_amount'], 0)
+ def test_partial_loan_security_unpledge(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 2000.00
+ },
+ {
+ "loan_security": "Test Security 2",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan.submit()
+
+ self.assertEquals(loan.loan_amount, 1000000)
+
+ first_date = '2019-10-01'
+ last_date = '2019-10-30'
+
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date = last_date)
+
+ repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), 600000)
+ repayment_entry.submit()
+
+ unpledge_map = {'Test Security 2': 2000}
+
+ unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1)
+ unpledge_request.submit()
+ unpledge_request.status = 'Approved'
+ unpledge_request.save()
+ unpledge_request.submit()
+ unpledge_request.load_from_db()
+ self.assertEqual(unpledge_request.docstatus, 1)
+
+ def test_santined_loan_security_unpledge(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+
+ loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01')
+ loan.submit()
+
+ self.assertEquals(loan.loan_amount, 1000000)
+
+ unpledge_map = {'Test Security 1': 4000}
+ unpledge_request = unpledge_security(loan=loan.name, security_map = unpledge_map, save=1)
+ unpledge_request.submit()
+ unpledge_request.status = 'Approved'
+ unpledge_request.save()
+ unpledge_request.submit()
+
def test_disbursal_check_with_shortfall(self):
pledges = [{
"loan_security": "Test Security 2",
@@ -415,7 +473,7 @@
self.assertEquals(loan.status, "Loan Closure Requested")
amounts = calculate_amounts(loan.name, add_days(last_date, 5))
- self.assertTrue(amounts['pending_principal_amount'] < 0.0)
+ self.assertEqual(amounts['pending_principal_amount'], 0.0)
def test_partial_unaccrued_interest_payment(self):
pledge = [{
@@ -489,7 +547,7 @@
# 30 days - grace period
penalty_days = 30 - 4
- penalty_applicable_amount = flt(amounts['interest_amount']/2, 2)
+ penalty_applicable_amount = flt(amounts['interest_amount']/2)
penalty_amount = flt((((penalty_applicable_amount * 25) / 100) * penalty_days), 2)
process = process_loan_interest_accrual_for_demand_loans(posting_date = '2019-11-30')
diff --git a/erpnext/loan_management/doctype/loan_application/loan_application.py b/erpnext/loan_management/doctype/loan_application/loan_application.py
index e59db4c..9c0147e 100644
--- a/erpnext/loan_management/doctype/loan_application/loan_application.py
+++ b/erpnext/loan_management/doctype/loan_application/loan_application.py
@@ -197,7 +197,7 @@
security.qty = cint(security.amount/security.loan_security_price)
security.amount = security.qty * security.loan_security_price
- security.post_haircut_amount = security.amount - (security.amount * security.haircut/100)
+ security.post_haircut_amount = cint(security.amount - (security.amount * security.haircut/100))
maximum_loan_amount += security.post_haircut_amount
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
index f157f0d..185bf7a 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.json
@@ -22,6 +22,7 @@
"paid_principal_amount",
"column_break_14",
"interest_amount",
+ "total_pending_interest_amount",
"paid_interest_amount",
"penalty_amount",
"section_break_15",
@@ -172,13 +173,19 @@
"hidden": 1,
"label": "Last Accrual Date",
"read_only": 1
+ },
+ {
+ "fieldname": "total_pending_interest_amount",
+ "fieldtype": "Currency",
+ "label": "Total Pending Interest Amount",
+ "options": "Company:company:default_currency"
}
],
"in_create": 1,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-07 05:49:25.448875",
+ "modified": "2021-01-10 00:15:21.544140",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Interest Accrual",
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
index d17f5af..7978350 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/loan_interest_accrual.py
@@ -100,6 +100,8 @@
interest_per_day = get_per_day_interest(pending_principal_amount, loan.rate_of_interest, posting_date)
payable_interest = interest_per_day * no_of_days
+ pending_amounts = calculate_amounts(loan.name, posting_date, payment_type='Loan Closure')
+
args = frappe._dict({
'loan': loan.name,
'applicant_type': loan.applicant_type,
@@ -108,7 +110,8 @@
'loan_account': loan.loan_account,
'pending_principal_amount': pending_principal_amount,
'interest_amount': payable_interest,
- 'penalty_amount': calculate_amounts(loan.name, posting_date)['penalty_amount'],
+ 'total_pending_interest_amount': pending_amounts['interest_amount'],
+ 'penalty_amount': pending_amounts['penalty_amount'],
'process_loan_interest': process_loan_interest,
'posting_date': posting_date,
'accrual_type': accrual_type
@@ -202,6 +205,7 @@
loan_interest_accrual.loan_account = args.loan_account
loan_interest_accrual.pending_principal_amount = flt(args.pending_principal_amount, precision)
loan_interest_accrual.interest_amount = flt(args.interest_amount, precision)
+ loan_interest_accrual.total_pending_interest_amount = flt(args.total_pending_interest_amount, precision)
loan_interest_accrual.penalty_amount = flt(args.penalty_amount, precision)
loan_interest_accrual.posting_date = args.posting_date or nowdate()
loan_interest_accrual.process_loan_interest_accrual = args.process_loan_interest
@@ -242,7 +246,5 @@
if not posting_date:
posting_date = getdate()
- precision = cint(frappe.db.get_default("currency_precision")) or 2
-
- return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100), precision)
+ return flt((principal_amount * rate_of_interest) / (days_in_year(get_datetime(posting_date).year) * 100))
diff --git a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
index 46a6440..85e008a 100644
--- a/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
+++ b/erpnext/loan_management/doctype/loan_interest_accrual/test_loan_interest_accrual.py
@@ -37,10 +37,8 @@
loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
create_pledge(loan_application)
-
loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
posting_date=get_first_day(nowdate()))
-
loan.submit()
first_date = '2019-10-01'
@@ -50,11 +48,46 @@
accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
/ (days_in_year(get_datetime(first_date).year) * 100)
-
make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
-
process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
-
loan_interest_accural = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
self.assertEquals(flt(loan_interest_accural.interest_amount, 0), flt(accrued_interest_amount, 0))
+
+ def test_accumulated_amounts(self):
+ pledge = [{
+ "loan_security": "Test Security 1",
+ "qty": 4000.00
+ }]
+
+ loan_application = create_loan_application('_Test Company', self.applicant, 'Demand Loan', pledge)
+ create_pledge(loan_application)
+ loan = create_demand_loan(self.applicant, "Demand Loan", loan_application,
+ posting_date=get_first_day(nowdate()))
+ loan.submit()
+
+ first_date = '2019-10-01'
+ last_date = '2019-10-30'
+
+ no_of_days = date_diff(last_date, first_date) + 1
+ accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
+ / (days_in_year(get_datetime(first_date).year) * 100)
+ make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date)
+ process_loan_interest_accrual_for_demand_loans(posting_date=last_date)
+ loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name})
+
+ self.assertEquals(flt(loan_interest_accrual.interest_amount, 0), flt(accrued_interest_amount, 0))
+
+ next_start_date = '2019-10-31'
+ next_end_date = '2019-11-29'
+
+ no_of_days = date_diff(next_end_date, next_start_date) + 1
+ process = process_loan_interest_accrual_for_demand_loans(posting_date=next_end_date)
+ new_accrued_interest_amount = (loan.loan_amount * loan.rate_of_interest * no_of_days) \
+ / (days_in_year(get_datetime(first_date).year) * 100)
+
+ total_pending_interest_amount = flt(accrued_interest_amount + new_accrued_interest_amount, 0)
+
+ loan_interest_accrual = frappe.get_doc("Loan Interest Accrual", {'loan': loan.name,
+ 'process_loan_interest_accrual': process})
+ self.assertEquals(flt(loan_interest_accrual.total_pending_interest_amount, 0), total_pending_interest_amount)
diff --git a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
index 415ba99..bac06c4 100644
--- a/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
+++ b/erpnext/loan_management/doctype/loan_repayment/loan_repayment.py
@@ -81,8 +81,8 @@
last_accrual_date = get_last_accrual_date(self.against_loan)
# get posting date upto which interest has to be accrued
- per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
- self.rate_of_interest, self.posting_date), 2)
+ per_day_interest = get_per_day_interest(self.pending_principal_amount,
+ self.rate_of_interest, self.posting_date)
no_of_days = flt(flt(self.total_interest_paid - self.interest_payable,
precision)/per_day_interest, 0) - 1
@@ -105,8 +105,6 @@
})
def update_paid_amount(self):
- precision = cint(frappe.db.get_default("currency_precision")) or 2
-
loan = frappe.get_doc("Loan", self.against_loan)
for payment in self.repayment_details:
@@ -114,7 +112,7 @@
SET paid_principal_amount = `paid_principal_amount` + %s,
paid_interest_amount = `paid_interest_amount` + %s
WHERE name = %s""",
- (flt(payment.paid_principal_amount, precision), flt(payment.paid_interest_amount, precision), payment.loan_interest_accrual))
+ (flt(payment.paid_principal_amount), flt(payment.paid_interest_amount), payment.loan_interest_accrual))
frappe.db.sql(""" UPDATE `tabLoan` SET total_amount_paid = %s, total_principal_paid = %s
WHERE name = %s """, (loan.total_amount_paid + self.amount_paid,
@@ -148,8 +146,6 @@
frappe.db.set_value("Loan", self.against_loan, "status", "Disbursed")
def allocate_amounts(self, repayment_details):
- precision = cint(frappe.db.get_default("currency_precision")) or 2
-
self.set('repayment_details', [])
self.principal_amount_paid = 0
total_interest_paid = 0
@@ -185,21 +181,18 @@
# no of days for which to accrue interest
# Interest can only be accrued for an entire day and not partial
if interest_paid > repayment_details['unaccrued_interest']:
- per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
- self.rate_of_interest, self.posting_date), precision)
interest_paid -= repayment_details['unaccrued_interest']
total_interest_paid += repayment_details['unaccrued_interest']
else:
# get no of days for which interest can be paid
- per_day_interest = flt(get_per_day_interest(self.pending_principal_amount,
- self.rate_of_interest, self.posting_date), precision)
+ per_day_interest = get_per_day_interest(self.pending_principal_amount,
+ self.rate_of_interest, self.posting_date)
no_of_days = cint(interest_paid/per_day_interest)
total_interest_paid += no_of_days * per_day_interest
interest_paid -= no_of_days * per_day_interest
self.total_interest_paid = total_interest_paid
-
if interest_paid:
self.principal_amount_paid += interest_paid
@@ -369,7 +362,7 @@
if pending_days > 0:
principal_amount = flt(pending_principal_amount, precision)
per_day_interest = get_per_day_interest(principal_amount, loan_type_details.rate_of_interest, posting_date)
- unaccrued_interest += (pending_days * flt(per_day_interest, precision))
+ unaccrued_interest += (pending_days * per_day_interest)
amounts["pending_principal_amount"] = flt(pending_principal_amount, precision)
amounts["payable_principal_amount"] = flt(payable_principal_amount, precision)
@@ -377,7 +370,7 @@
amounts["penalty_amount"] = flt(penalty_amount, precision)
amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision)
amounts["pending_accrual_entries"] = pending_accrual_entries
- amounts["unaccrued_interest"] = unaccrued_interest
+ amounts["unaccrued_interest"] = flt(unaccrued_interest, precision)
if final_due_date:
amounts["due_date"] = final_due_date
diff --git a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json
index a55b482..b6e8763 100644
--- a/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json
+++ b/erpnext/loan_management/doctype/loan_security_price/loan_security_price.json
@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
+ "loan_security_name",
"loan_security_type",
"column_break_2",
"uom",
@@ -79,10 +80,18 @@
"label": "Loan Security Type",
"options": "Loan Security Type",
"read_only": 1
+ },
+ {
+ "fetch_from": "loan_security.loan_security_name",
+ "fieldname": "loan_security_name",
+ "fieldtype": "Data",
+ "label": "Loan Security Name",
+ "read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-06-11 03:41:33.900340",
+ "modified": "2021-01-17 07:41:49.598086",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Security Price",
diff --git a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
index 8ec0bfb..6469806 100644
--- a/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
+++ b/erpnext/loan_management/doctype/loan_security_shortfall/loan_security_shortfall.py
@@ -81,7 +81,6 @@
process_loan_security_shortfall)
def create_loan_security_shortfall(loan, loan_amount, security_value, shortfall_amount, process_loan_security_shortfall):
-
existing_shortfall = frappe.db.get_value("Loan Security Shortfall", {"loan": loan, "status": "Pending"}, "name")
if existing_shortfall:
diff --git a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
index c29f325..c4c2d68 100644
--- a/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
+++ b/erpnext/loan_management/doctype/loan_security_unpledge/loan_security_unpledge.py
@@ -30,6 +30,8 @@
d.idx, frappe.bold(d.loan_security)))
def validate_unpledge_qty(self):
+ from erpnext.loan_management.doctype.loan_security_shortfall.loan_security_shortfall import get_ltv_ratio
+
pledge_qty_map = get_pledged_security_qty(self.loan)
ltv_ratio_map = frappe._dict(frappe.get_all("Loan Security Type",
@@ -42,11 +44,19 @@
"valid_upto": (">=", get_datetime())
}, as_list=1))
- total_payment, principal_paid, interest_payable, written_off_amount = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
- 'total_interest_payable', 'written_off_amount'])
+ loan_details = frappe.get_value("Loan", self.loan, ['total_payment', 'total_principal_paid',
+ 'total_interest_payable', 'written_off_amount', 'disbursed_amount', 'status'], as_dict=1)
- pending_principal_amount = flt(total_payment) - flt(interest_payable) - flt(principal_paid) - flt(written_off_amount)
+ if loan_details.status == 'Disbursed':
+ pending_principal_amount = flt(loan_details.total_payment) - flt(loan_details.total_interest_payable) \
+ - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
+ else:
+ pending_principal_amount = flt(loan_details.disbursed_amount) - flt(loan_details.total_interest_payable) \
+ - flt(loan_details.total_principal_paid) - flt(loan_details.written_off_amount)
+
security_value = 0
+ unpledge_qty_map = {}
+ ltv_ratio = 0
for security in self.securities:
pledged_qty = pledge_qty_map.get(security.loan_security, 0)
@@ -57,13 +67,15 @@
msg += _("You are trying to unpledge more.")
frappe.throw(msg, title=_("Loan Security Unpledge Error"))
- qty_after_unpledge = pledged_qty - security.qty
- ltv_ratio = ltv_ratio_map.get(security.loan_security_type)
+ unpledge_qty_map.setdefault(security.loan_security, 0)
+ unpledge_qty_map[security.loan_security] += security.qty
- current_price = loan_security_price_map.get(security.loan_security)
- if not current_price:
- frappe.throw(_("No valid Loan Security Price found for {0}").format(frappe.bold(security.loan_security)))
+ for security in pledge_qty_map:
+ if not ltv_ratio:
+ ltv_ratio = get_ltv_ratio(security)
+ qty_after_unpledge = pledge_qty_map.get(security, 0) - unpledge_qty_map.get(security, 0)
+ current_price = loan_security_price_map.get(security)
security_value += qty_after_unpledge * current_price
if not security_value and flt(pending_principal_amount, 2) > 0:
diff --git a/erpnext/loan_management/doctype/loan_type/loan_type.json b/erpnext/loan_management/doctype/loan_type/loan_type.json
index 18a9731..3ef5304 100644
--- a/erpnext/loan_management/doctype/loan_type/loan_type.json
+++ b/erpnext/loan_management/doctype/loan_type/loan_type.json
@@ -144,17 +144,17 @@
},
{
"allow_on_submit": 1,
- "description": "Pending amount that will be automatically ignored on loan closure request ",
+ "description": "Loan Write Off will be automatically created on loan closure request if pending amount is below this limit",
"fieldname": "write_off_amount",
"fieldtype": "Currency",
- "label": "Write Off Amount ",
+ "label": "Auto Write Off Amount ",
"options": "Company:company:default_currency"
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-26 07:13:55.029811",
+ "modified": "2021-01-17 06:51:26.082879",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Loan Type",
diff --git a/erpnext/loan_management/doctype/pledge/pledge.json b/erpnext/loan_management/doctype/pledge/pledge.json
index 801e3a3..c23479c 100644
--- a/erpnext/loan_management/doctype/pledge/pledge.json
+++ b/erpnext/loan_management/doctype/pledge/pledge.json
@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
+ "loan_security_name",
"loan_security_type",
"loan_security_code",
"uom",
@@ -85,11 +86,18 @@
"label": "Post Haircut Amount",
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fetch_from": "loan_security.loan_security_name",
+ "fieldname": "loan_security_name",
+ "fieldtype": "Data",
+ "label": "Loan Security Name",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-11-05 10:07:15.424937",
+ "modified": "2021-01-17 07:41:12.452514",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Pledge",
diff --git a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json
index ffc3671..3feb305 100644
--- a/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json
+++ b/erpnext/loan_management/doctype/process_loan_security_shortfall/process_loan_security_shortfall.json
@@ -30,7 +30,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-02-01 08:14:05.845161",
+ "modified": "2021-01-17 03:59:14.494557",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Process Loan Security Shortfall",
@@ -45,7 +45,9 @@
"read": 1,
"report": 1,
"role": "System Manager",
+ "select": 1,
"share": 1,
+ "submit": 1,
"write": 1
},
{
@@ -57,7 +59,9 @@
"read": 1,
"report": 1,
"role": "Loan Manager",
+ "select": 1,
"share": 1,
+ "submit": 1,
"write": 1
}
],
diff --git a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
index 3e7e778..a0b3a79 100644
--- a/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
+++ b/erpnext/loan_management/doctype/proposed_pledge/proposed_pledge.json
@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
+ "loan_security_name",
"qty",
"loan_security_price",
"amount",
@@ -56,12 +57,19 @@
"label": "Post Haircut Amount",
"options": "Company:company:default_currency",
"read_only": 1
+ },
+ {
+ "fetch_from": "loan_security.loan_security_name",
+ "fieldname": "loan_security_name",
+ "fieldtype": "Data",
+ "label": "Loan Security Name",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-05 10:07:37.542344",
+ "modified": "2021-01-17 07:29:01.671722",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Proposed Pledge",
diff --git a/erpnext/loan_management/doctype/unpledge/unpledge.json b/erpnext/loan_management/doctype/unpledge/unpledge.json
index 0035668..0091e6c 100644
--- a/erpnext/loan_management/doctype/unpledge/unpledge.json
+++ b/erpnext/loan_management/doctype/unpledge/unpledge.json
@@ -6,6 +6,7 @@
"engine": "InnoDB",
"field_order": [
"loan_security",
+ "loan_security_name",
"loan_security_type",
"loan_security_code",
"haircut",
@@ -61,12 +62,19 @@
"fieldtype": "Percent",
"label": "Haircut",
"read_only": 1
+ },
+ {
+ "fetch_from": "loan_security.loan_security_name",
+ "fieldname": "loan_security_name",
+ "fieldtype": "Data",
+ "label": "Loan Security Name",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-05 10:07:28.106961",
+ "modified": "2021-01-17 07:36:20.212342",
"modified_by": "Administrator",
"module": "Loan Management",
"name": "Unpledge",
diff --git a/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json
new file mode 100644
index 0000000..e060253
--- /dev/null
+++ b/erpnext/loan_management/loan_management_dashboard/loan_dashboard/loan_dashboard.json
@@ -0,0 +1,70 @@
+{
+ "cards": [
+ {
+ "card": "New Loans"
+ },
+ {
+ "card": "Active Loans"
+ },
+ {
+ "card": "Closed Loans"
+ },
+ {
+ "card": "Total Disbursed"
+ },
+ {
+ "card": "Open Loan Applications"
+ },
+ {
+ "card": "New Loan Applications"
+ },
+ {
+ "card": "Total Sanctioned Amount"
+ },
+ {
+ "card": "Active Securities"
+ },
+ {
+ "card": "Applicants With Unpaid Shortfall"
+ },
+ {
+ "card": "Total Shortfall Amount"
+ },
+ {
+ "card": "Total Repayment"
+ },
+ {
+ "card": "Total Write Off"
+ }
+ ],
+ "charts": [
+ {
+ "chart": "New Loans",
+ "width": "Half"
+ },
+ {
+ "chart": "Loan Disbursements",
+ "width": "Half"
+ },
+ {
+ "chart": "Top 10 Pledged Loan Securities",
+ "width": "Half"
+ },
+ {
+ "chart": "Loan Interest Accrual",
+ "width": "Half"
+ }
+ ],
+ "creation": "2021-02-06 16:52:43.484752",
+ "dashboard_name": "Loan Dashboard",
+ "docstatus": 0,
+ "doctype": "Dashboard",
+ "idx": 0,
+ "is_default": 0,
+ "is_standard": 1,
+ "modified": "2021-02-21 20:53:47.531699",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Dashboard",
+ "owner": "Administrator"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/active_loans/active_loans.json b/erpnext/loan_management/number_card/active_loans/active_loans.json
new file mode 100644
index 0000000..7e0db47
--- /dev/null
+++ b/erpnext/loan_management/number_card/active_loans/active_loans.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:10:26.132493",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"in\",[\"Disbursed\",\"Partially Disbursed\",null],false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Active Loans",
+ "modified": "2021-02-06 17:29:20.304087",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Active Loans",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/active_securities/active_securities.json b/erpnext/loan_management/number_card/active_securities/active_securities.json
new file mode 100644
index 0000000..298e410
--- /dev/null
+++ b/erpnext/loan_management/number_card/active_securities/active_securities.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 19:07:21.344199",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Security",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Security\",\"disabled\",\"=\",0,false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Active Securities",
+ "modified": "2021-02-06 19:07:26.671516",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Active Securities",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json b/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json
new file mode 100644
index 0000000..3b9eba1
--- /dev/null
+++ b/erpnext/loan_management/number_card/applicants_with_unpaid_shortfall/applicants_with_unpaid_shortfall.json
@@ -0,0 +1,21 @@
+{
+ "creation": "2021-02-07 18:55:12.632616",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "filters_json": "null",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Applicants With Unpaid Shortfall",
+ "method": "erpnext.loan_management.doctype.loan.loan.get_shortfall_applicants",
+ "modified": "2021-02-07 21:46:27.369795",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Applicants With Unpaid Shortfall",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Custom"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/closed_loans/closed_loans.json b/erpnext/loan_management/number_card/closed_loans/closed_loans.json
new file mode 100644
index 0000000..c2f2244
--- /dev/null
+++ b/erpnext/loan_management/number_card/closed_loans/closed_loans.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-21 19:51:49.261813",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"=\",\"Closed\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Closed Loans",
+ "modified": "2021-02-21 19:51:54.087903",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Closed Loans",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json b/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json
new file mode 100644
index 0000000..65c8ce6
--- /dev/null
+++ b/erpnext/loan_management/number_card/last_interest_accrual/last_interest_accrual.json
@@ -0,0 +1,21 @@
+{
+ "creation": "2021-02-07 21:57:14.758007",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "filters_json": "null",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Last Interest Accrual",
+ "method": "erpnext.loan_management.doctype.loan.loan.get_last_accrual_date",
+ "modified": "2021-02-07 21:59:47.525197",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Last Interest Accrual",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Custom"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json b/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json
new file mode 100644
index 0000000..7e655ff
--- /dev/null
+++ b/erpnext/loan_management/number_card/new_loan_applications/new_loan_applications.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:59:10.051269",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Application",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Application\",\"docstatus\",\"=\",\"1\",false],[\"Loan Application\",\"creation\",\"Timespan\",\"today\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "New Loan Applications",
+ "modified": "2021-02-06 17:59:21.880979",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "New Loan Applications",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/new_loans/new_loans.json b/erpnext/loan_management/number_card/new_loans/new_loans.json
new file mode 100644
index 0000000..424f0f1
--- /dev/null
+++ b/erpnext/loan_management/number_card/new_loans/new_loans.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:56:34.624031",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"creation\",\"Timespan\",\"today\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "New Loans",
+ "modified": "2021-02-06 17:58:20.209166",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "New Loans",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json b/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json
new file mode 100644
index 0000000..1d5e84e
--- /dev/null
+++ b/erpnext/loan_management/number_card/open_loan_applications/open_loan_applications.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "",
+ "creation": "2021-02-06 17:23:32.509899",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Application",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Application\",\"docstatus\",\"=\",\"1\",false],[\"Loan Application\",\"status\",\"=\",\"Open\",false]]",
+ "function": "Count",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Open Loan Applications",
+ "modified": "2021-02-06 17:29:09.761011",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Open Loan Applications",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json b/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json
new file mode 100644
index 0000000..4a3f869
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_disbursed/total_disbursed.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "disbursed_amount",
+ "creation": "2021-02-06 16:52:19.505462",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Disbursement",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Disbursement\",\"docstatus\",\"=\",\"1\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Disbursed Amount",
+ "modified": "2021-02-06 17:29:38.453870",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Disbursed",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_repayment/total_repayment.json b/erpnext/loan_management/number_card/total_repayment/total_repayment.json
new file mode 100644
index 0000000..38de42b
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_repayment/total_repayment.json
@@ -0,0 +1,24 @@
+{
+ "aggregate_function_based_on": "amount_paid",
+ "color": "#29CD42",
+ "creation": "2021-02-21 19:27:45.989222",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Repayment",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Repayment\",\"docstatus\",\"=\",\"1\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Repayment",
+ "modified": "2021-02-21 19:34:59.656546",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Repayment",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json b/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json
new file mode 100644
index 0000000..dfb9d24
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_sanctioned_amount/total_sanctioned_amount.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "loan_amount",
+ "creation": "2021-02-06 17:05:04.704162",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan\",\"docstatus\",\"=\",\"1\",false],[\"Loan\",\"status\",\"=\",\"Sanctioned\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Sanctioned Amount",
+ "modified": "2021-02-06 17:29:29.930557",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Sanctioned Amount",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Monthly",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json b/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json
new file mode 100644
index 0000000..aa6b093
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_shortfall_amount/total_shortfall_amount.json
@@ -0,0 +1,23 @@
+{
+ "aggregate_function_based_on": "shortfall_amount",
+ "creation": "2021-02-09 08:07:20.096995",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Security Shortfall",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Unpaid Shortfall Amount",
+ "modified": "2021-02-09 08:09:00.355547",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Shortfall Amount",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/number_card/total_write_off/total_write_off.json b/erpnext/loan_management/number_card/total_write_off/total_write_off.json
new file mode 100644
index 0000000..c85169a
--- /dev/null
+++ b/erpnext/loan_management/number_card/total_write_off/total_write_off.json
@@ -0,0 +1,24 @@
+{
+ "aggregate_function_based_on": "write_off_amount",
+ "color": "#CB2929",
+ "creation": "2021-02-21 19:48:29.004429",
+ "docstatus": 0,
+ "doctype": "Number Card",
+ "document_type": "Loan Write Off",
+ "dynamic_filters_json": "[]",
+ "filters_json": "[[\"Loan Write Off\",\"docstatus\",\"=\",\"1\",false]]",
+ "function": "Sum",
+ "idx": 0,
+ "is_public": 0,
+ "is_standard": 1,
+ "label": "Total Write Off",
+ "modified": "2021-02-21 19:48:58.604159",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Total Write Off",
+ "owner": "Administrator",
+ "report_function": "Sum",
+ "show_percentage_stats": 1,
+ "stats_time_interval": "Daily",
+ "type": "Document Type"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/loan_management/report/applicant_wise_loan_security_exposure/__init__.py
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js
new file mode 100644
index 0000000..73d60c4
--- /dev/null
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Applicant-Wise Loan Security Exposure"] = {
+ "filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
+ }
+ ]
+};
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json
new file mode 100644
index 0000000..a778cd7
--- /dev/null
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-01-15 23:48:38.913514",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-01-15 23:48:38.913514",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Applicant-Wise Loan Security Exposure",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Loan Security",
+ "report_name": "Applicant-Wise Loan Security Exposure",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Loan Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
new file mode 100644
index 0000000..0ccd149
--- /dev/null
+++ b/erpnext/loan_management/report/applicant_wise_loan_security_exposure/applicant_wise_loan_security_exposure.py
@@ -0,0 +1,139 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import erpnext
+from frappe import _
+from frappe.utils import get_datetime, flt
+from six import iteritems
+
+def execute(filters=None):
+ columns = get_columns(filters)
+ data = get_data(filters)
+ return columns, data
+
+
+def get_columns(filters):
+ columns = [
+ {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100},
+ {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150},
+ {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160},
+ {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100},
+ {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150},
+ {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100},
+ {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120},
+ {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
+ {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
+ {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
+ {"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100},
+ {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
+ {"label": _("% Of Applicant Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
+ {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
+ ]
+
+ return columns
+
+def get_data(filters):
+ data = []
+ loan_security_details = get_loan_security_details()
+ pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters,
+ loan_security_details)
+
+ currency = erpnext.get_company_currency(filters.get('company'))
+
+ for key, qty in iteritems(pledge_values):
+ if qty:
+ row = {}
+ current_value = flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
+ valid_upto = loan_security_details.get(key[1], {}).get('valid_upto')
+
+ row.update(loan_security_details.get(key[1]))
+ row.update({
+ 'applicant_type': applicant_type_map.get(key[0]),
+ 'applicant_name': key[0],
+ 'total_qty': qty,
+ 'current_value': current_value,
+ 'price_valid_upto': valid_upto,
+ 'portfolio_percent': flt(current_value * 100 / total_value_map.get(key[0]), 2) if total_value_map.get(key[0]) \
+ else 0.0,
+ 'currency': currency
+ })
+
+ data.append(row)
+
+ return data
+
+def get_loan_security_details():
+ security_detail_map = {}
+ loan_security_price_map = {}
+ lsp_validity_map = {}
+
+ loan_security_prices = frappe.db.sql("""
+ SELECT loan_security, loan_security_price, valid_upto
+ FROM `tabLoan Security Price` t1
+ WHERE valid_from >= (SELECT MAX(valid_from) FROM `tabLoan Security Price` t2
+ WHERE t1.loan_security = t2.loan_security)
+ """, as_dict=1)
+
+ for security in loan_security_prices:
+ loan_security_price_map.setdefault(security.loan_security, security.loan_security_price)
+ lsp_validity_map.setdefault(security.loan_security, security.valid_upto)
+
+ loan_security_details = frappe.get_all('Loan Security', fields=['name as loan_security',
+ 'loan_security_code', 'loan_security_name', 'haircut', 'loan_security_type',
+ 'disabled'])
+
+ for security in loan_security_details:
+ security.update({
+ 'latest_price': flt(loan_security_price_map.get(security.loan_security)),
+ 'valid_upto': lsp_validity_map.get(security.loan_security)
+ })
+
+ security_detail_map.setdefault(security.loan_security, security)
+
+ return security_detail_map
+
+def get_applicant_wise_total_loan_security_qty(filters, loan_security_details):
+ current_pledges = {}
+ total_value_map = {}
+ applicant_type_map = {}
+ applicant_wise_unpledges = {}
+ conditions = ""
+
+ if filters.get('company'):
+ conditions = "AND company = %(company)s"
+
+ unpledges = frappe.db.sql("""
+ SELECT up.applicant, u.loan_security, sum(u.qty) as qty
+ FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
+ WHERE u.parent = up.name
+ AND up.status = 'Approved'
+ {conditions}
+ GROUP BY up.applicant, u.loan_security
+ """.format(conditions=conditions), filters, as_dict=1)
+
+ for unpledge in unpledges:
+ applicant_wise_unpledges.setdefault((unpledge.applicant, unpledge.loan_security), unpledge.qty)
+
+ pledges = frappe.db.sql("""
+ SELECT lp.applicant_type, lp.applicant, p.loan_security, sum(p.qty) as qty
+ FROM `tabLoan Security Pledge` lp, `tabPledge`p
+ WHERE p.parent = lp.name
+ AND lp.status = 'Pledged'
+ {conditions}
+ GROUP BY lp.applicant, p.loan_security
+ """.format(conditions=conditions), filters, as_dict=1)
+
+ for security in pledges:
+ current_pledges.setdefault((security.applicant, security.loan_security), security.qty)
+ total_value_map.setdefault(security.applicant, 0.0)
+ applicant_type_map.setdefault(security.applicant, security.applicant_type)
+
+ current_pledges[(security.applicant, security.loan_security)] -= \
+ applicant_wise_unpledges.get((security.applicant, security.loan_security), 0.0)
+
+ total_value_map[security.applicant] += current_pledges.get((security.applicant, security.loan_security)) \
+ * loan_security_details.get(security.loan_security, {}).get('latest_price', 0)
+
+ return current_pledges, total_value_map, applicant_type_map
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/loan_management/report/loan_interest_report/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/loan_management/report/loan_interest_report/__init__.py
diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js
new file mode 100644
index 0000000..a227b6d
--- /dev/null
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Loan Interest Report"] = {
+ "filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
+ }
+ ]
+};
diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json
new file mode 100644
index 0000000..321d606
--- /dev/null
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2021-01-10 02:03:26.742693",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-01-10 02:03:26.742693",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Interest Report",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Loan Interest Accrual",
+ "report_name": "Loan Interest Report",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Loan Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
new file mode 100644
index 0000000..0f72c3c
--- /dev/null
+++ b/erpnext/loan_management/report/loan_interest_report/loan_interest_report.py
@@ -0,0 +1,183 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import erpnext
+from frappe import _
+from frappe.utils import flt, getdate, add_days
+from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \
+ import get_loan_security_details
+
+
+def execute(filters=None):
+ columns = get_columns(filters)
+ data = get_active_loan_details(filters)
+ return columns, data
+
+def get_columns(filters):
+ columns = [
+ {"label": _("Loan"), "fieldname": "loan", "fieldtype": "Link", "options": "Loan", "width": 160},
+ {"label": _("Status"), "fieldname": "status", "fieldtype": "Data", "width": 160},
+ {"label": _("Applicant Type"), "fieldname": "applicant_type", "options": "DocType", "width": 100},
+ {"label": _("Applicant Name"), "fieldname": "applicant_name", "fieldtype": "Dynamic Link", "options": "applicant_type", "width": 150},
+ {"label": _("Loan Type"), "fieldname": "loan_type", "fieldtype": "Link", "options": "Loan Type", "width": 100},
+ {"label": _("Sanctioned Amount"), "fieldname": "sanctioned_amount", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Disbursed Amount"), "fieldname": "disbursed_amount", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Penalty Amount"), "fieldname": "penalty", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Accrued Interest"), "fieldname": "accrued_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Total Repayment"), "fieldname": "total_repayment", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Principal Outstanding"), "fieldname": "principal_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Interest Outstanding"), "fieldname": "interest_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Total Outstanding"), "fieldname": "total_outstanding", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Undue Booked Interest"), "fieldname": "undue_interest", "fieldtype": "Currency", "options": "currency", "width": 120},
+ {"label": _("Interest %"), "fieldname": "rate_of_interest", "fieldtype": "Percent", "width": 100},
+ {"label": _("Penalty Interest %"), "fieldname": "penalty_interest", "fieldtype": "Percent", "width": 100},
+ {"label": _("Loan To Value Ratio"), "fieldname": "loan_to_value", "fieldtype": "Percent", "width": 100},
+ {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
+ ]
+
+ return columns
+
+def get_active_loan_details(filters):
+
+ filter_obj = {"status": ("!=", "Closed")}
+ if filters.get('company'):
+ filter_obj.update({'company': filters.get('company')})
+
+ loan_details = frappe.get_all("Loan",
+ fields=["name as loan", "applicant_type", "applicant as applicant_name", "loan_type",
+ "disbursed_amount", "rate_of_interest", "total_payment", "total_principal_paid",
+ "total_interest_payable", "written_off_amount", "status"],
+ filters=filter_obj)
+
+ loan_list = [d.loan for d in loan_details]
+
+ current_pledges = get_loan_wise_pledges(filters)
+ loan_wise_security_value = get_loan_wise_security_value(filters, current_pledges)
+
+ sanctioned_amount_map = get_sanctioned_amount_map()
+ penal_interest_rate_map = get_penal_interest_rate_map()
+ payments = get_payments(loan_list)
+ accrual_map = get_interest_accruals(loan_list)
+ currency = erpnext.get_company_currency(filters.get('company'))
+
+ for loan in loan_details:
+ loan.update({
+ "sanctioned_amount": flt(sanctioned_amount_map.get(loan.applicant_name)),
+ "principal_outstanding": flt(loan.total_payment) - flt(loan.total_principal_paid) \
+ - flt(loan.total_interest_payable) - flt(loan.written_off_amount),
+ "total_repayment": flt(payments.get(loan.loan)),
+ "accrued_interest": flt(accrual_map.get(loan.loan, {}).get("accrued_interest")),
+ "interest_outstanding": flt(accrual_map.get(loan.loan, {}).get("interest_outstanding")),
+ "penalty": flt(accrual_map.get(loan.loan, {}).get("penalty")),
+ "penalty_interest": penal_interest_rate_map.get(loan.loan_type),
+ "undue_interest": flt(accrual_map.get(loan.loan, {}).get("undue_interest")),
+ "loan_to_value": 0.0,
+ "currency": currency
+ })
+
+ loan['total_outstanding'] = loan['principal_outstanding'] + loan['interest_outstanding'] \
+ + loan['penalty']
+
+ if loan_wise_security_value.get(loan.loan):
+ loan['loan_to_value'] = flt((loan['principal_outstanding'] * 100) / loan_wise_security_value.get(loan.loan))
+
+ return loan_details
+
+def get_sanctioned_amount_map():
+ return frappe._dict(frappe.get_all("Sanctioned Loan Amount", fields=["applicant", "sanctioned_amount_limit"],
+ as_list=1))
+
+def get_payments(loans):
+ return frappe._dict(frappe.get_all("Loan Repayment", fields=["against_loan", "sum(amount_paid)"],
+ filters={"against_loan": ("in", loans)}, group_by="against_loan", as_list=1))
+
+def get_interest_accruals(loans):
+ accrual_map = {}
+
+ interest_accruals = frappe.get_all("Loan Interest Accrual",
+ fields=["loan", "interest_amount", "posting_date", "penalty_amount",
+ "paid_interest_amount", "accrual_type"], filters={"loan": ("in", loans)}, order_by="posting_date desc")
+
+ for entry in interest_accruals:
+ accrual_map.setdefault(entry.loan, {
+ "accrued_interest": 0.0,
+ "undue_interest": 0.0,
+ "interest_outstanding": 0.0,
+ "last_accrual_date": '',
+ "due_date": ''
+ })
+
+ if entry.accrual_type == 'Regular':
+ if not accrual_map[entry.loan]['due_date']:
+ accrual_map[entry.loan]['due_date'] = add_days(entry.posting_date, 1)
+ if not accrual_map[entry.loan]['last_accrual_date']:
+ accrual_map[entry.loan]['last_accrual_date'] = entry.posting_date
+
+ due_date = accrual_map[entry.loan]['due_date']
+ last_accrual_date = accrual_map[entry.loan]['last_accrual_date']
+
+ if due_date and getdate(entry.posting_date) < getdate(due_date):
+ accrual_map[entry.loan]["interest_outstanding"] += entry.interest_amount - entry.paid_interest_amount
+ else:
+ accrual_map[entry.loan]['undue_interest'] += entry.interest_amount - entry.paid_interest_amount
+
+ accrual_map[entry.loan]["accrued_interest"] += entry.interest_amount
+
+ if last_accrual_date and getdate(entry.posting_date) == last_accrual_date:
+ accrual_map[entry.loan]["penalty"] = entry.penalty_amount
+
+ return accrual_map
+
+def get_penal_interest_rate_map():
+ return frappe._dict(frappe.get_all("Loan Type", fields=["name", "penalty_interest_rate"], as_list=1))
+
+def get_loan_wise_pledges(filters):
+ loan_wise_unpledges = {}
+ current_pledges = {}
+
+ conditions = ""
+
+ if filters.get('company'):
+ conditions = "AND company = %(company)s"
+
+ unpledges = frappe.db.sql("""
+ SELECT up.loan, u.loan_security, sum(u.qty) as qty
+ FROM `tabLoan Security Unpledge` up, `tabUnpledge` u
+ WHERE u.parent = up.name
+ AND up.status = 'Approved'
+ {conditions}
+ GROUP BY up.loan, u.loan_security
+ """.format(conditions=conditions), filters, as_dict=1)
+
+ for unpledge in unpledges:
+ loan_wise_unpledges.setdefault((unpledge.loan, unpledge.loan_security), unpledge.qty)
+
+ pledges = frappe.db.sql("""
+ SELECT lp.loan, p.loan_security, sum(p.qty) as qty
+ FROM `tabLoan Security Pledge` lp, `tabPledge`p
+ WHERE p.parent = lp.name
+ AND lp.status = 'Pledged'
+ {conditions}
+ GROUP BY lp.loan, p.loan_security
+ """.format(conditions=conditions), filters, as_dict=1)
+
+ for security in pledges:
+ current_pledges.setdefault((security.loan, security.loan_security), security.qty)
+ current_pledges[(security.loan, security.loan_security)] -= \
+ loan_wise_unpledges.get((security.loan, security.loan_security), 0.0)
+
+ return current_pledges
+
+def get_loan_wise_security_value(filters, current_pledges):
+ loan_security_details = get_loan_security_details()
+ loan_wise_security_value = {}
+
+ for key in current_pledges:
+ qty = current_pledges.get(key)
+ loan_wise_security_value.setdefault(key[0], 0.0)
+ loan_wise_security_value[key[0]] += \
+ flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
+
+ return loan_wise_security_value
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
index b63cc8e..c6f6b99 100644
--- a/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
+++ b/erpnext/loan_management/report/loan_repayment_and_closure/loan_repayment_and_closure.py
@@ -103,7 +103,7 @@
loan_repayments = frappe.get_all("Loan Repayment",
filters = query_filters,
- fields=["posting_date", "applicant", "name", "against_loan", "payment_type", "payable_amount",
+ fields=["posting_date", "applicant", "name", "against_loan", "payable_amount",
"pending_principal_amount", "interest_payable", "penalty_amount", "amount_paid"]
)
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/loan_management/report/loan_security_exposure/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/loan_management/report/loan_security_exposure/__init__.py
diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js
new file mode 100644
index 0000000..777f296
--- /dev/null
+++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.js
@@ -0,0 +1,16 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Loan Security Exposure"] = {
+ "filters": [
+ {
+ "fieldname":"company",
+ "label": __("Company"),
+ "fieldtype": "Link",
+ "options": "Company",
+ "default": frappe.defaults.get_user_default("Company"),
+ "reqd": 1
+ }
+ ]
+};
diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json
new file mode 100644
index 0000000..d4dca08
--- /dev/null
+++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.json
@@ -0,0 +1,29 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2021-01-16 08:08:01.694583",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2021-01-16 08:08:01.694583",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Security Exposure",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Loan Security",
+ "report_name": "Loan Security Exposure",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "System Manager"
+ },
+ {
+ "role": "Loan Manager"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
new file mode 100644
index 0000000..887a86a
--- /dev/null
+++ b/erpnext/loan_management/report/loan_security_exposure/loan_security_exposure.py
@@ -0,0 +1,84 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import erpnext
+from frappe import _
+from frappe.utils import flt
+from six import iteritems
+from erpnext.loan_management.report.applicant_wise_loan_security_exposure.applicant_wise_loan_security_exposure \
+ import get_loan_security_details, get_applicant_wise_total_loan_security_qty
+
+def execute(filters=None):
+ columns = get_columns(filters)
+ data = get_data(filters)
+ return columns, data
+
+def get_columns(filters):
+ columns = [
+ {"label": _("Loan Security"), "fieldname": "loan_security", "fieldtype": "Link", "options": "Loan Security", "width": 160},
+ {"label": _("Loan Security Code"), "fieldname": "loan_security_code", "fieldtype": "Data", "width": 100},
+ {"label": _("Loan Security Name"), "fieldname": "loan_security_name", "fieldtype": "Data", "width": 150},
+ {"label": _("Haircut"), "fieldname": "haircut", "fieldtype": "Percent", "width": 100},
+ {"label": _("Loan Security Type"), "fieldname": "loan_security_type", "fieldtype": "Link", "options": "Loan Security Type", "width": 120},
+ {"label": _("Disabled"), "fieldname": "disabled", "fieldtype": "Check", "width": 80},
+ {"label": _("Total Qty"), "fieldname": "total_qty", "fieldtype": "Float", "width": 100},
+ {"label": _("Latest Price"), "fieldname": "latest_price", "fieldtype": "Currency", "options": "currency", "width": 100},
+ {"label": _("Price Valid Upto"), "fieldname": "price_valid_upto", "fieldtype": "Datetime", "width": 100},
+ {"label": _("Current Value"), "fieldname": "current_value", "fieldtype": "Currency", "options": "currency", "width": 100},
+ {"label": _("% Of Total Portfolio"), "fieldname": "portfolio_percent", "fieldtype": "Percentage", "width": 100},
+ {"label": _("Pledged Applicant Count"), "fieldname": "pledged_applicant_count", "fieldtype": "Percentage", "width": 100},
+ {"label": _("Currency"), "fieldname": "currency", "fieldtype": "Currency", "options": "Currency", "hidden": 1, "width": 100},
+ ]
+
+ return columns
+
+def get_data(filters):
+ data = []
+ loan_security_details = get_loan_security_details()
+ current_pledges, total_portfolio_value = get_company_wise_loan_security_details(filters, loan_security_details)
+ currency = erpnext.get_company_currency(filters.get('company'))
+
+ for security, value in iteritems(current_pledges):
+ if value.get('qty'):
+ row = {}
+ current_value = flt(value.get('qty', 0) * loan_security_details.get(security, {}).get('latest_price', 0))
+ valid_upto = loan_security_details.get(security, {}).get('valid_upto')
+
+ row.update(loan_security_details.get(security))
+ row.update({
+ 'total_qty': value.get('qty'),
+ 'current_value': current_value,
+ 'price_valid_upto': valid_upto,
+ 'portfolio_percent': flt(current_value * 100 / total_portfolio_value, 2),
+ 'pledged_applicant_count': value.get('applicant_count'),
+ 'currency': currency
+ })
+
+ data.append(row)
+
+ return data
+
+
+def get_company_wise_loan_security_details(filters, loan_security_details):
+ pledge_values, total_value_map, applicant_type_map = get_applicant_wise_total_loan_security_qty(filters,
+ loan_security_details)
+
+ total_portfolio_value = 0
+ security_wise_map = {}
+ for key, qty in iteritems(pledge_values):
+ security_wise_map.setdefault(key[1], {
+ 'qty': 0.0,
+ 'applicant_count': 0.0
+ })
+
+ security_wise_map[key[1]]['qty'] += qty
+ if qty:
+ security_wise_map[key[1]]['applicant_count'] += 1
+
+ total_portfolio_value += flt(qty * loan_security_details.get(key[1], {}).get('latest_price', 0))
+
+ return security_wise_map, total_portfolio_value
+
+
+
diff --git a/erpnext/loan_management/workspace/loan_management/loan_management.json b/erpnext/loan_management/workspace/loan_management/loan_management.json
new file mode 100644
index 0000000..18559dc
--- /dev/null
+++ b/erpnext/loan_management/workspace/loan_management/loan_management.json
@@ -0,0 +1,251 @@
+{
+ "category": "Modules",
+ "charts": [],
+ "creation": "2020-03-12 16:35:55.299820",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "loan",
+ "idx": 0,
+ "is_default": 0,
+ "is_standard": 1,
+ "label": "Loan Management",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Type",
+ "link_to": "Loan Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Application",
+ "link_to": "Loan Application",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan",
+ "link_to": "Loan",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Processes",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Process Loan Security Shortfall",
+ "link_to": "Process Loan Security Shortfall",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Process Loan Interest Accrual",
+ "link_to": "Process Loan Interest Accrual",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Disbursement and Repayment",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Disbursement",
+ "link_to": "Loan Disbursement",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Repayment",
+ "link_to": "Loan Repayment",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Write Off",
+ "link_to": "Loan Write Off",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Interest Accrual",
+ "link_to": "Loan Interest Accrual",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Security",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Security Type",
+ "link_to": "Loan Security Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Security Price",
+ "link_to": "Loan Security Price",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Security",
+ "link_to": "Loan Security",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Security Pledge",
+ "link_to": "Loan Security Pledge",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Security Unpledge",
+ "link_to": "Loan Security Unpledge",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Security Shortfall",
+ "link_to": "Loan Security Shortfall",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Loan Repayment and Closure",
+ "link_to": "Loan Repayment and Closure",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Loan Security Status",
+ "link_to": "Loan Security Status",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2021-02-18 17:31:53.586508",
+ "modified_by": "Administrator",
+ "module": "Loan Management",
+ "name": "Loan Management",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "color": "Green",
+ "format": "{} Open",
+ "label": "Loan Application",
+ "link_to": "Loan Application",
+ "stats_filter": "{ \"status\": \"Open\" }",
+ "type": "DocType"
+ },
+ {
+ "label": "Loan",
+ "link_to": "Loan",
+ "type": "DocType"
+ },
+ {
+ "doc_view": "",
+ "label": "Dashboard",
+ "link_to": "Loan Dashboard",
+ "type": "Dashboard"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json b/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json
deleted file mode 100644
index 8d11294..0000000
--- a/erpnext/manufacturing/desk_page/manufacturing/manufacturing.json
+++ /dev/null
@@ -1,124 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Production",
- "links": "[\n {\n \"dependencies\": [\n \"Item\",\n \"BOM\"\n ],\n \"description\": \"Orders released for production.\",\n \"label\": \"Work Order\",\n \"name\": \"Work Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"BOM\"\n ],\n \"description\": \"Generate Material Requests (MRP) and Work Orders.\",\n \"label\": \"Production Plan\",\n \"name\": \"Production Plan\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Job Card\",\n \"name\": \"Job Card\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Downtime Entry\",\n \"name\": \"Downtime Entry\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Bill of Materials",
- "links": "[\n {\n \"description\": \"All Products or Services.\",\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Bill of Materials (BOM)\",\n \"label\": \"Bill of Materials\",\n \"name\": \"BOM\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Where manufacturing operations are carried.\",\n \"label\": \"Workstation\",\n \"name\": \"Workstation\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Details of the operations carried out.\",\n \"label\": \"Operation\",\n \"name\": \"Operation\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Routing\",\n \"name\": \"Routing\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Reports",
- "links": "[{\n\t\"dependencies\": [\"Work Order\"],\n\t\"name\": \"Production Planning Report\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Work Order\",\n\t\"label\": \"Production Planning Report\"\n}, {\n\t\"dependencies\": [\"Work Order\"],\n\t\"name\": \"Work Order Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Work Order\",\n\t\"label\": \"Work Order Summary\"\n}, {\n\t\"dependencies\": [\"Quality Inspection\"],\n\t\"name\": \"Quality Inspection Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Quality Inspection\",\n\t\"label\": \"Quality Inspection Summary\"\n}, {\n\t\"dependencies\": [\"Downtime Entry\"],\n\t\"name\": \"Downtime Analysis\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Downtime Entry\",\n\t\"label\": \"Downtime Analysis\"\n}, {\n\t\"dependencies\": [\"Job Card\"],\n\t\"name\": \"Job Card Summary\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Job Card\",\n\t\"label\": \"Job Card Summary\"\n}, {\n\t\"dependencies\": [\"BOM\"],\n\t\"name\": \"BOM Search\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"BOM\",\n\t\"label\": \"BOM Search\"\n}, {\n\t\"dependencies\": [\"BOM\"],\n\t\"name\": \"BOM Stock Report\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"BOM\",\n\t\"label\": \"BOM Stock Report\"\n}, {\n\t\"dependencies\": [\"Work Order\"],\n\t\"name\": \"Production Analytics\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"Work Order\",\n\t\"label\": \"Production Analytics\"\n}, {\n\t\"dependencies\": [\"BOM\"],\n\t\"name\": \"BOM Operations Time\",\n\t\"is_query_report\": true,\n\t\"type\": \"report\",\n\t\"doctype\": \"BOM\",\n\t\"label\": \"BOM Operations Time\"\n}]"
- },
- {
- "hidden": 0,
- "label": "Tools",
- "links": "[\n {\n \"description\": \"Replace BOM and update latest price in all BOMs\",\n \"label\": \"BOM Update Tool\",\n \"name\": \"BOM Update Tool\",\n \"type\": \"doctype\"\n },\n {\n \"data_doctype\": \"BOM\",\n \"description\": \"Compare BOMs for changes in Raw Materials and Operations\",\n \"label\": \"BOM Comparison Tool\",\n \"name\": \"bom-comparison-tool\",\n \"type\": \"page\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Settings",
- "links": "[\n {\n \"description\": \"Global settings for all manufacturing processes.\",\n \"label\": \"Manufacturing Settings\",\n \"name\": \"Manufacturing Settings\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Help",
- "links": "[\n {\n \"label\": \"Work Order\",\n \"name\": \"Work Order\",\n \"type\": \"help\",\n \"youtube_id\": \"ZotgLyp2YFY\"\n }\n]"
- }
- ],
- "category": "Domains",
- "charts": [
- {
- "chart_name": "Produced Quantity"
- }
- ],
- "creation": "2020-03-02 17:11:37.032604",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Manufacturing",
- "modified": "2020-05-28 13:54:02.048419",
- "modified_by": "Administrator",
- "module": "Manufacturing",
- "name": "Manufacturing",
- "onboarding": "Manufacturing",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "restrict_to_domain": "Manufacturing",
- "shortcuts": [
- {
- "color": "#cef6d1",
- "format": "{} Active",
- "label": "Item",
- "link_to": "Item",
- "restrict_to_domain": "Manufacturing",
- "stats_filter": "{\n \"disabled\": 0\n}",
- "type": "DocType"
- },
- {
- "color": "#cef6d1",
- "format": "{} Active",
- "label": "BOM",
- "link_to": "BOM",
- "restrict_to_domain": "Manufacturing",
- "stats_filter": "{\n \"is_active\": 1\n}",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} Open",
- "label": "Work Order",
- "link_to": "Work Order",
- "restrict_to_domain": "Manufacturing",
- "stats_filter": "{ \n \"status\": [\"in\", \n [\"Draft\", \"Not Started\", \"In Process\"]\n ]\n}",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} Open",
- "label": "Production Plan",
- "link_to": "Production Plan",
- "restrict_to_domain": "Manufacturing",
- "stats_filter": "{ \n \"status\": [\"not in\", [\"Completed\"]]\n}",
- "type": "DocType"
- },
- {
- "label": "Forecasting",
- "link_to": "Exponential Smoothing Forecasting",
- "type": "Report"
- },
- {
- "label": "Work Order Summary",
- "link_to": "Work Order Summary",
- "restrict_to_domain": "Manufacturing",
- "type": "Report"
- },
- {
- "label": "BOM Stock Report",
- "link_to": "BOM Stock Report",
- "type": "Report"
- },
- {
- "label": "Production Planning Report",
- "link_to": "Production Planning Report",
- "type": "Report"
- },
- {
- "label": "Dashboard",
- "link_to": "Manufacturing",
- "restrict_to_domain": "Manufacturing",
- "type": "Dashboard"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js
index 1c4b7a1..fbfd801 100644
--- a/erpnext/manufacturing/doctype/bom/bom.js
+++ b/erpnext/manufacturing/doctype/bom/bom.js
@@ -134,7 +134,7 @@
frm.set_intro(__('This is a Template BOM and will be used to make the work order for {0} of the item {1}',
[
`<a class="variants-intro">variants</a>`,
- `<a href="#Form/Item/${frm.doc.item}">${frm.doc.item}</a>`,
+ `<a href="/app/item/${frm.doc.item}">${frm.doc.item}</a>`,
]), true);
frm.$wrapper.find(".variants-intro").on("click", () => {
@@ -411,7 +411,7 @@
cur_frm.cscript.time_in_mins = cur_frm.cscript.hour_rate;
-cur_frm.cscript.bom_no = function(doc, cdt, cdn) {
+cur_frm.cscript.bom_no = function(doc, cdt, cdn) {
get_bom_material_detail(doc, cdt, cdn, false);
};
@@ -419,17 +419,22 @@
if (doc.is_default) cur_frm.set_value("is_active", 1);
};
-var get_bom_material_detail= function(doc, cdt, cdn, scrap_items) {
+var get_bom_material_detail = function(doc, cdt, cdn, scrap_items) {
+ if (!doc.company) {
+ frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")});
+ }
+
var d = locals[cdt][cdn];
if (d.item_code) {
return frappe.call({
doc: doc,
method: "get_bom_material_detail",
args: {
- 'item_code': d.item_code,
- 'bom_no': d.bom_no != null ? d.bom_no: '',
+ "company": doc.company,
+ "item_code": d.item_code,
+ "bom_no": d.bom_no != null ? d.bom_no: '',
"scrap_items": scrap_items,
- 'qty': d.qty,
+ "qty": d.qty,
"stock_qty": d.stock_qty,
"include_item_in_manufacturing": d.include_item_in_manufacturing,
"uom": d.uom,
@@ -468,7 +473,7 @@
}
if (d.bom_no) {
- frappe.msgprint(__("You can not change rate if BOM mentioned agianst any item"));
+ frappe.msgprint(__("You cannot change the rate if BOM is mentioned against any Item."));
get_bom_material_detail(doc, cdt, cdn, scrap_items);
} else {
erpnext.bom.calculate_rm_cost(doc);
diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py
index 6363242..03beedb 100644
--- a/erpnext/manufacturing/doctype/bom/bom.py
+++ b/erpnext/manufacturing/doctype/bom/bom.py
@@ -65,6 +65,10 @@
def validate(self):
self.route = frappe.scrub(self.name).replace('_', '-')
+
+ if not self.company:
+ frappe.throw(_("Please select a Company first."), title=_("Mandatory"))
+
self.clear_operations()
self.validate_main_item()
self.validate_currency()
@@ -125,6 +129,7 @@
self.validate_bom_currecny(item)
ret = self.get_bom_material_detail({
+ "company": self.company,
"item_code": item.item_code,
"item_name": item.item_name,
"bom_no": item.bom_no,
@@ -213,6 +218,7 @@
for d in self.get("items"):
rate = self.get_rm_rate({
+ "company": self.company,
"item_code": d.item_code,
"bom_no": d.bom_no,
"qty": d.qty,
@@ -611,10 +617,20 @@
""" Get weighted average of valuation rate from all warehouses """
total_qty, total_value, valuation_rate = 0.0, 0.0, 0.0
- for d in frappe.db.sql("""select actual_qty, stock_value from `tabBin`
- where item_code=%s""", args['item_code'], as_dict=1):
- total_qty += flt(d.actual_qty)
- total_value += flt(d.stock_value)
+ item_bins = frappe.db.sql("""
+ select
+ bin.actual_qty, bin.stock_value
+ from
+ `tabBin` bin, `tabWarehouse` warehouse
+ where
+ bin.item_code=%(item)s
+ and bin.warehouse = warehouse.name
+ and warehouse.company=%(company)s""",
+ {"item": args['item_code'], "company": args['company']}, as_dict=1)
+
+ for d in item_bins:
+ total_qty += flt(d.actual_qty)
+ total_value += flt(d.stock_value)
if total_qty:
valuation_rate = total_value / total_qty
diff --git a/erpnext/manufacturing/doctype/bom/bom_item_preview.html b/erpnext/manufacturing/doctype/bom/bom_item_preview.html
index c782f7b..6cd5f8c 100644
--- a/erpnext/manufacturing/doctype/bom/bom_item_preview.html
+++ b/erpnext/manufacturing/doctype/bom/bom_item_preview.html
@@ -12,11 +12,11 @@
<hr style="margin: 15px -15px;">
<p>
{% if data.value %}
- <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="#Form/BOM/{{ data.value }}">
+ <a style="margin-right: 7px; margin-bottom: 7px" class="btn btn-default btn-xs" href="/app/Form/BOM/{{ data.value }}">
{{ __("Open BOM {0}", [data.value.bold()]) }}</a>
{% endif %}
{% if data.item_code %}
- <a class="btn btn-default btn-xs" href="#Form/Item/{{ data.item_code }}">
+ <a class="btn btn-default btn-xs" href="/app/Form/Item/{{ data.item_code }}">
{{ __("Open Item {0}", [data.item_code.bold()]) }}</a>
{% endif %}
</p>
diff --git a/erpnext/manufacturing/doctype/bom/bom_list.js b/erpnext/manufacturing/doctype/bom/bom_list.js
index 94cb466..4b5887f 100644
--- a/erpnext/manufacturing/doctype/bom/bom_list.js
+++ b/erpnext/manufacturing/doctype/bom/bom_list.js
@@ -8,7 +8,7 @@
} else if(doc.is_active) {
return [__("Active"), "blue", "is_active,=,Yes"];
} else if(!doc.is_active) {
- return [__("Not active"), "darkgrey", "is_active,=,No"];
+ return [__("Not active"), "gray", "is_active,=,No"];
}
}
};
diff --git a/erpnext/manufacturing/doctype/job_card/job_card.py b/erpnext/manufacturing/doctype/job_card/job_card.py
index d15d81e..662a06b 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card.py
+++ b/erpnext/manufacturing/doctype/job_card/job_card.py
@@ -17,6 +17,7 @@
class OperationMismatchError(frappe.ValidationError): pass
class OperationSequenceError(frappe.ValidationError): pass
+class JobCardCancelError(frappe.ValidationError): pass
class JobCard(Document):
def validate(self):
@@ -217,39 +218,66 @@
field = "operation_id"
data = self.get_current_operation_data()
if data and len(data) > 0:
- for_quantity = data[0].completed_qty
- time_in_mins = data[0].time_in_mins
+ for_quantity = flt(data[0].completed_qty)
+ time_in_mins = flt(data[0].time_in_mins)
- if self.get(field):
- time_data = frappe.db.sql("""
+ wo = frappe.get_doc('Work Order', self.work_order)
+ if self.operation_id:
+ self.validate_produced_quantity(for_quantity, wo)
+ self.update_work_order_data(for_quantity, time_in_mins, wo)
+
+ def validate_produced_quantity(self, for_quantity, wo):
+ if self.docstatus < 2: return
+
+ if wo.produced_qty > for_quantity:
+ first_part_msg = (_("The {0} {1} is used to calculate the valuation cost for the finished good {2}.")
+ .format(frappe.bold(_("Job Card")), frappe.bold(self.name), frappe.bold(self.production_item)))
+
+ second_part_msg = (_("Kindly cancel the Manufacturing Entries first against the work order {0}.")
+ .format(frappe.bold(get_link_to_form("Work Order", self.work_order))))
+
+ frappe.throw(_("{0} {1}").format(first_part_msg, second_part_msg),
+ JobCardCancelError, title = _("Error"))
+
+ def update_work_order_data(self, for_quantity, time_in_mins, wo):
+ time_data = frappe.db.sql("""
SELECT
min(from_time) as start_time, max(to_time) as end_time
FROM `tabJob Card` jc, `tabJob Card Time Log` jctl
WHERE
jctl.parent = jc.name and jc.work_order = %s
- and jc.{0} = %s and jc.docstatus = 1
- """.format(field), (self.work_order, self.get(field)), as_dict=1)
+ and jc.operation_id = %s and jc.docstatus = 1
+ """, (self.work_order, self.operation_id), as_dict=1)
- wo = frappe.get_doc('Work Order', self.work_order)
+ for data in wo.operations:
+ if data.get("name") == self.operation_id:
+ data.completed_qty = for_quantity
+ data.actual_operation_time = time_in_mins
+ data.actual_start_time = time_data[0].start_time if time_data else None
+ data.actual_end_time = time_data[0].end_time if time_data else None
- for data in wo.operations:
- if data.get("name") == self.get(field):
- data.completed_qty = for_quantity
- data.actual_operation_time = time_in_mins
- data.actual_start_time = time_data[0].start_time if time_data else None
- data.actual_end_time = time_data[0].end_time if time_data else None
-
- wo.flags.ignore_validate_update_after_submit = True
- wo.update_operation_status()
- wo.calculate_operating_cost()
- wo.set_actual_dates()
- wo.save()
+ wo.flags.ignore_validate_update_after_submit = True
+ wo.update_operation_status()
+ wo.calculate_operating_cost()
+ wo.set_actual_dates()
+ wo.save()
def get_current_operation_data(self):
return frappe.get_all('Job Card',
fields = ["sum(total_time_in_mins) as time_in_mins", "sum(total_completed_qty) as completed_qty"],
filters = {"docstatus": 1, "work_order": self.work_order, "operation_id": self.operation_id})
+ def set_transferred_qty_in_job_card(self, ste_doc):
+ for row in ste_doc.items:
+ if not row.job_card_item: continue
+
+ qty = frappe.db.sql(""" SELECT SUM(qty) from `tabStock Entry Detail` sed, `tabStock Entry` se
+ WHERE sed.job_card_item = %s and se.docstatus = 1 and sed.parent = se.name and
+ se.purpose = 'Material Transfer for Manufacture'
+ """, (row.job_card_item))[0][0]
+
+ frappe.db.set_value('Job Card Item', row.job_card_item, 'transferred_qty', flt(qty))
+
def set_transferred_qty(self, update_status=False):
if not self.items:
self.transferred_qty = self.for_quantity if self.docstatus == 1 else 0
@@ -262,7 +290,8 @@
self.transferred_qty = frappe.db.get_value('Stock Entry', {
'job_card': self.name,
'work_order': self.work_order,
- 'docstatus': 1
+ 'docstatus': 1,
+ 'purpose': 'Material Transfer for Manufacture'
}, 'sum(fg_completed_qty)') or 0
self.db_set("transferred_qty", self.transferred_qty)
@@ -403,6 +432,7 @@
target.purpose = "Material Transfer for Manufacture"
target.from_bom = 1
target.fg_completed_qty = source.get('for_quantity', 0) - source.get('transferred_qty', 0)
+ target.set_transfer_qty()
target.calculate_rate_and_amount()
target.set_missing_values()
target.set_stock_entry_type()
@@ -420,9 +450,10 @@
"field_map": {
"source_warehouse": "s_warehouse",
"required_qty": "qty",
- "uom": "stock_uom"
+ "name": "job_card_item"
},
"postprocess": update_item,
+ "condition": lambda doc: doc.required_qty > 0
}
}, target_doc, set_missing_values)
diff --git a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
index cf07698..f4877fd 100644
--- a/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
+++ b/erpnext/manufacturing/doctype/job_card/job_card_calendar.js
@@ -8,7 +8,17 @@
"allDay": "allDay",
"progress": "progress"
},
- gantt: true,
+ gantt: {
+ field_map: {
+ "start": "started_time",
+ "end": "started_time",
+ "id": "name",
+ "title": "subject",
+ "color": "color",
+ "allDay": "allDay",
+ "progress": "progress"
+ }
+ },
filters: [
{
"fieldtype": "Link",
diff --git a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
index bc9fe10..100ef4c 100644
--- a/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
+++ b/erpnext/manufacturing/doctype/job_card_item/job_card_item.json
@@ -1,363 +1,120 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2018-07-09 17:20:44.737289",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2018-07-09 17:20:44.737289",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "item_code",
+ "source_warehouse",
+ "uom",
+ "item_group",
+ "column_break_3",
+ "stock_uom",
+ "item_name",
+ "description",
+ "qty_section",
+ "required_qty",
+ "column_break_9",
+ "transferred_qty",
+ "allow_alternative_item"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item_code",
- "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": "Item Code",
- "length": 0,
- "no_copy": 0,
- "options": "Item",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Item Code",
+ "options": "Item",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "source_warehouse",
- "fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 1,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Source Warehouse",
- "length": 0,
- "no_copy": 0,
- "options": "Warehouse",
- "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": "source_warehouse",
+ "fieldtype": "Link",
+ "ignore_user_permissions": 1,
+ "in_list_view": 1,
+ "label": "Source Warehouse",
+ "options": "Warehouse"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "uom",
- "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": "UOM",
- "length": 0,
- "no_copy": 0,
- "options": "UOM",
- "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": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "options": "UOM"
+ },
{
- "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": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "item_name",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Item Name",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text",
- "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": "Description",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "description",
+ "fieldtype": "Text",
+ "label": "Description",
+ "read_only": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "qty_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": "Qty",
- "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": "qty_section",
+ "fieldtype": "Section Break",
+ "label": "Qty"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "required_qty",
- "fieldtype": "Float",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Required Qty",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
+ "fieldname": "required_qty",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Required Qty",
+ "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_9",
- "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_9",
+ "fieldtype": "Column Break"
+ },
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "allow_alternative_item",
- "fieldtype": "Check",
- "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": "Allow Alternative Item",
- "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
+ "default": "0",
+ "fieldname": "allow_alternative_item",
+ "fieldtype": "Check",
+ "label": "Allow Alternative Item"
+ },
+ {
+ "fetch_from": "item_code.item_group",
+ "fieldname": "item_group",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM"
+ },
+ {
+ "fieldname": "transferred_qty",
+ "fieldtype": "Float",
+ "label": "Transferred Qty",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 1,
- "max_attachments": 0,
- "modified": "2018-08-28 15:23:48.099459",
- "modified_by": "Administrator",
- "module": "Manufacturing",
- "name": "Job Card Item",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2021-02-11 13:50:13.804108",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Job Card Item",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py
index 3833e86..8f9dd05 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan.py
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py
@@ -319,7 +319,7 @@
frappe.flags.mute_messages = False
if wo_list:
- wo_list = ["""<a href="#Form/Work Order/%s" target="_blank">%s</a>""" % \
+ wo_list = ["""<a href="/app/Form/Work Order/%s" target="_blank">%s</a>""" % \
(p, p) for p in wo_list]
msgprint(_("{0} created").format(comma_and(wo_list)))
else :
@@ -423,7 +423,7 @@
frappe.flags.mute_messages = False
if material_request_list:
- material_request_list = ["""<a href="#Form/Material Request/{0}">{1}</a>""".format(m.name, m.name) \
+ material_request_list = ["""<a href="/app/Form/Material Request/{0}">{1}</a>""".format(m.name, m.name) \
for m in material_request_list]
msgprint(_("{0} created").format(comma_and(material_request_list)))
else :
diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan_list.js b/erpnext/manufacturing/doctype/production_plan/production_plan_list.js
index 165b66f..c2e3e6d 100644
--- a/erpnext/manufacturing/doctype/production_plan/production_plan_list.js
+++ b/erpnext/manufacturing/doctype/production_plan/production_plan_list.js
@@ -1,16 +1,16 @@
frappe.listview_settings['Production Plan'] = {
add_fields: ["status"],
filters: [["status", "!=", "Closed"]],
- get_indicator: function(doc) {
- if(doc.status==="Submitted") {
+ get_indicator: function (doc) {
+ if (doc.status === "Submitted") {
return [__("Not Started"), "orange", "status,=,Submitted"];
} else {
return [__(doc.status), {
"Draft": "red",
"In Process": "orange",
"Completed": "green",
- "Material Requested": "darkgrey",
- "Cancelled": "darkgrey",
+ "Material Requested": "yellow",
+ "Cancelled": "gray",
"Closed": "grey"
}[doc.status], "status,=," + doc.status];
}
diff --git a/erpnext/manufacturing/doctype/work_order/test_work_order.py b/erpnext/manufacturing/doctype/work_order/test_work_order.py
index e539279..00e8c54 100644
--- a/erpnext/manufacturing/doctype/work_order/test_work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/test_work_order.py
@@ -5,8 +5,7 @@
from __future__ import unicode_literals
import unittest
import frappe
-from frappe.utils import flt, time_diff_in_hours, now, add_months, cint, today
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
+from frappe.utils import flt, now, add_months, cint, today, add_to_date
from erpnext.manufacturing.doctype.work_order.work_order import (make_stock_entry,
ItemHasVariantError, stop_unstop, StockOverProductionError, OverProductionError, CapacityError)
from erpnext.stock.doctype.stock_entry import test_stock_entry
@@ -15,10 +14,10 @@
from erpnext.stock.doctype.item.test_item import make_item
from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+from erpnext.manufacturing.doctype.job_card.job_card import JobCardCancelError
class TestWorkOrder(unittest.TestCase):
def setUp(self):
- set_perpetual_inventory(0)
self.warehouse = '_Test Warehouse 2 - _TC'
self.item = '_Test Item'
@@ -95,11 +94,11 @@
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=2,
source_warehouse=warehouse, skip_transfer=1)
- bin1_on_submit = get_bin(item, warehouse)
+ reserved_qty_on_submission = cint(get_bin(item, warehouse).reserved_qty_for_production)
# reserved qty for production is updated
- self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2,
- cint(bin1_on_submit.reserved_qty_for_production))
+ self.assertEqual(cint(bin1_at_start.reserved_qty_for_production) + 2, reserved_qty_on_submission)
+
test_stock_entry.make_stock_entry(item_code="_Test Item",
target=warehouse, qty=100, basic_rate=100)
@@ -110,9 +109,9 @@
s.submit()
bin1_at_completion = get_bin(item, warehouse)
-
+
self.assertEqual(cint(bin1_at_completion.reserved_qty_for_production),
- cint(bin1_on_submit.reserved_qty_for_production) - 1)
+ reserved_qty_on_submission - 1)
def test_production_item(self):
wo_order = make_wo_order_test_record(item="_Test FG Item", qty=1, do_not_save=True)
@@ -371,21 +370,49 @@
self.assertEqual(ste.total_additional_costs, 1000)
def test_job_card(self):
+ stock_entries = []
data = frappe.get_cached_value('BOM',
{'docstatus': 1, 'with_operations': 1, 'company': '_Test Company'}, ['name', 'item'])
- if data:
- frappe.db.set_value("Manufacturing Settings",
- None, "disable_capacity_planning", 0)
+ bom, bom_item = data
- bom, bom_item = data
+ bom_doc = frappe.get_doc('BOM', bom)
+ work_order = make_wo_order_test_record(item=bom_item, qty=1,
+ bom_no=bom, source_warehouse="_Test Warehouse - _TC")
- bom_doc = frappe.get_doc('BOM', bom)
- work_order = make_wo_order_test_record(item=bom_item, qty=1, bom_no=bom)
- self.assertTrue(work_order.planned_end_date)
+ for row in work_order.required_items:
+ stock_entry_doc = test_stock_entry.make_stock_entry(item_code=row.item_code,
+ target="_Test Warehouse - _TC", qty=row.required_qty, basic_rate=100)
+ stock_entries.append(stock_entry_doc)
- job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
- self.assertEqual(len(job_cards), len(bom_doc.operations))
+ ste = frappe.get_doc(make_stock_entry(work_order.name, "Material Transfer for Manufacture", 1))
+ ste.submit()
+ stock_entries.append(ste)
+
+ job_cards = frappe.get_all('Job Card', filters = {'work_order': work_order.name})
+ self.assertEqual(len(job_cards), len(bom_doc.operations))
+
+ for i, job_card in enumerate(job_cards):
+ doc = frappe.get_doc("Job Card", job_card)
+ doc.append("time_logs", {
+ "from_time": now(),
+ "hours": i,
+ "to_time": add_to_date(now(), i),
+ "completed_qty": doc.for_quantity
+ })
+ doc.submit()
+
+ ste1 = frappe.get_doc(make_stock_entry(work_order.name, "Manufacture", 1))
+ ste1.submit()
+ stock_entries.append(ste1)
+
+ for job_card in job_cards:
+ doc = frappe.get_doc("Job Card", job_card)
+ self.assertRaises(JobCardCancelError, doc.cancel)
+
+ stock_entries.reverse()
+ for stock_entry in stock_entries:
+ stock_entry.cancel()
def test_capcity_planning(self):
frappe.db.set_value("Manufacturing Settings", None, {
@@ -491,6 +518,80 @@
work_order1.save()
self.assertEqual(work_order1.operations[0].time_in_mins, 40.0)
+ def test_partial_material_consumption(self):
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 1)
+ wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)
+
+ ste_cancel_list = []
+ ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
+ target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0)
+ ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
+ target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0)
+
+ ste_cancel_list.extend([ste1, ste2])
+
+ s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4))
+ s.submit()
+ ste_cancel_list.append(s)
+
+ ste1 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2))
+ ste1.submit()
+ ste_cancel_list.append(ste1)
+
+ ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Material Consumption for Manufacture", 2))
+ self.assertEquals(ste3.fg_completed_qty, 2)
+
+ expected_qty = {"_Test Item": 2, "_Test Item Home Desktop 100": 4}
+ for row in ste3.items:
+ self.assertEquals(row.qty, expected_qty.get(row.item_code))
+ ste_cancel_list.reverse()
+ for ste_doc in ste_cancel_list:
+ ste_doc.cancel()
+
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0)
+
+ def test_extra_material_transfer(self):
+ frappe.db.set_value("Manufacturing Settings", None, "material_consumption", 0)
+ frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on",
+ "Material Transferred for Manufacture")
+
+ wo_order = make_wo_order_test_record(planned_start_date=now(), qty=4)
+
+ ste_cancel_list = []
+ ste1 = test_stock_entry.make_stock_entry(item_code="_Test Item",
+ target="_Test Warehouse - _TC", qty=20, basic_rate=5000.0)
+ ste2 = test_stock_entry.make_stock_entry(item_code="_Test Item Home Desktop 100",
+ target="_Test Warehouse - _TC", qty=20, basic_rate=1000.0)
+
+ ste_cancel_list.extend([ste1, ste2])
+
+ itemwise_qty = {}
+ s = frappe.get_doc(make_stock_entry(wo_order.name, "Material Transfer for Manufacture", 4))
+ for row in s.items:
+ row.qty = row.qty + 2
+ itemwise_qty.setdefault(row.item_code, row.qty)
+
+ s.submit()
+ ste_cancel_list.append(s)
+
+ ste3 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2))
+ for ste_row in ste3.items:
+ if itemwise_qty.get(ste_row.item_code) and ste_row.s_warehouse:
+ self.assertEquals(ste_row.qty, itemwise_qty.get(ste_row.item_code) / 2)
+
+ ste3.submit()
+ ste_cancel_list.append(ste3)
+
+ ste2 = frappe.get_doc(make_stock_entry(wo_order.name, "Manufacture", 2))
+ for ste_row in ste2.items:
+ if itemwise_qty.get(ste_row.item_code) and ste_row.s_warehouse:
+ self.assertEquals(ste_row.qty, itemwise_qty.get(ste_row.item_code) / 2)
+ ste_cancel_list.reverse()
+ for ste_doc in ste_cancel_list:
+ ste_doc.cancel()
+
+ frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM")
+
def get_scrap_item_details(bom_no):
scrap_items = {}
for item in frappe.db.sql("""select item_code, stock_qty from `tabBOM Scrap Item`
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js
index 9ce465c..a6086fb 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order.js
@@ -545,7 +545,8 @@
var tbl = frm.doc.required_items || [];
var tbl_lenght = tbl.length;
for (var i = 0, len = tbl_lenght; i < len; i++) {
- if (flt(frm.doc.required_items[i].required_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
+ let wo_item_qty = frm.doc.required_items[i].transferred_qty || frm.doc.required_items[i].required_qty;
+ if (flt(wo_item_qty) > flt(frm.doc.required_items[i].consumed_qty)) {
counter += 1;
}
}
diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py
index cc93bf9..ca530bb 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order.py
+++ b/erpnext/manufacturing/doctype/work_order/work_order.py
@@ -456,10 +456,10 @@
if data and len(data):
dates = [d.posting_datetime for d in data]
- self.actual_start_date = min(dates)
+ self.db_set('actual_start_date', min(dates))
if self.status == "Completed":
- self.actual_end_date = max(dates)
+ self.db_set('actual_end_date', max(dates))
self.set_lead_time()
@@ -725,6 +725,7 @@
args.update(item_data)
args["rate"] = get_bom_item_rate({
+ "company": wo_doc.company,
"item_code": args.get("item_code"),
"qty": args.get("required_qty"),
"uom": args.get("stock_uom"),
diff --git a/erpnext/manufacturing/doctype/work_order/work_order_list.js b/erpnext/manufacturing/doctype/work_order/work_order_list.js
index 8d18395..81c23bb 100644
--- a/erpnext/manufacturing/doctype/work_order/work_order_list.js
+++ b/erpnext/manufacturing/doctype/work_order/work_order_list.js
@@ -12,7 +12,7 @@
"Not Started": "red",
"In Process": "orange",
"Completed": "green",
- "Cancelled": "darkgrey"
+ "Cancelled": "gray"
}[doc.status], "status,=," + doc.status];
}
}
diff --git a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
index f7b407b..ffd9242 100644
--- a/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
+++ b/erpnext/manufacturing/report/bom_stock_calculated/bom_stock_calculated.py
@@ -88,11 +88,11 @@
GROUP BY bom_item.item_code""".format(qty_field=qty_field, table=table, conditions=conditions, bom=bom), as_dict=1)
def get_manufacturer_records():
- details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no, parent"])
+ details = frappe.get_list('Item Manufacturer', fields = ["manufacturer", "manufacturer_part_no", "parent"])
manufacture_details = frappe._dict()
for detail in details:
dic = manufacture_details.setdefault(detail.get('parent'), {})
dic.setdefault('manufacturer', []).append(detail.get('manufacturer'))
dic.setdefault('manufacturer_part', []).append(detail.get('manufacturer_part_no'))
- return manufacture_details
\ No newline at end of file
+ return manufacture_details
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
index 8cd0164..7beecac 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.js
@@ -27,9 +27,9 @@
value = default_formatter(value, row, column, data);
if (column.id == "item") {
if (data["enough_parts_to_build"] > 0) {
- value = `<a style='color:green' href="#Form/Item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
+ value = `<a style='color:green' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
} else {
- value = `<a style='color:red' href="#Form/Item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
+ value = `<a style='color:red' href="/app/item/${data['item']}" data-doctype="Item">${data['item']}</a>`;
}
}
return value
diff --git a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
index 75ebcbc..1c6758e 100644
--- a/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
+++ b/erpnext/manufacturing/report/bom_stock_report/bom_stock_report.py
@@ -20,6 +20,7 @@
_("Item") + ":Link/Item:150",
_("Description") + "::300",
_("BOM Qty") + ":Float:160",
+ _("BOM UoM") + "::160",
_("Required Qty") + ":Float:120",
_("In Stock Qty") + ":Float:120",
_("Enough Parts to Build") + ":Float:200",
@@ -32,7 +33,7 @@
bom = filters.get("bom")
table = "`tabBOM Item`"
- qty_field = "qty"
+ qty_field = "stock_qty"
qty_to_produce = filters.get("qty_to_produce", 1)
if int(qty_to_produce) <= 0:
@@ -40,7 +41,6 @@
if filters.get("show_exploded_view"):
table = "`tabBOM Explosion Item`"
- qty_field = "stock_qty"
if filters.get("warehouse"):
warehouse_details = frappe.db.get_value("Warehouse", filters.get("warehouse"), ["lft", "rgt"], as_dict=1)
@@ -59,6 +59,7 @@
bom_item.item_code,
bom_item.description ,
bom_item.{qty_field},
+ bom_item.stock_uom,
bom_item.{qty_field} * {qty_to_produce} / bom.quantity,
sum(ledger.actual_qty) as actual_qty,
sum(FLOOR(ledger.actual_qty / (bom_item.{qty_field} * {qty_to_produce} / bom.quantity)))
diff --git a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
index ebc01c6..806d268 100644
--- a/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
+++ b/erpnext/manufacturing/report/production_planning_report/production_planning_report.py
@@ -124,7 +124,7 @@
if self.filters.include_subassembly_raw_materials else "(bom_item.qty / bom.quantity)")
raw_materials = frappe.db.sql(""" SELECT bom_item.parent, bom_item.item_code,
- bom_item.item_name as raw_material_name, {0} as required_qty
+ bom_item.item_name as raw_material_name, {0} as required_qty_per_unit
FROM
`tabBOM` as bom, `tab{1}` as bom_item
WHERE
@@ -208,7 +208,7 @@
warehouses = self.mrp_warehouses or []
for d in self.raw_materials_dict.get(key):
if self.filters.based_on != "Work Order":
- d.required_qty = d.required_qty * data.qty_to_manufacture
+ d.required_qty = d.required_qty_per_unit * data.qty_to_manufacture
if not warehouses:
warehouses = [data.warehouse]
diff --git a/erpnext/manufacturing/workspace/manufacturing/manufacturing.json b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
new file mode 100644
index 0000000..a355203
--- /dev/null
+++ b/erpnext/manufacturing/workspace/manufacturing/manufacturing.json
@@ -0,0 +1,350 @@
+{
+ "category": "Domains",
+ "charts": [
+ {
+ "chart_name": "Produced Quantity"
+ }
+ ],
+ "creation": "2020-03-02 17:11:37.032604",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "organization",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Manufacturing",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Production",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Item, BOM",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Work Order",
+ "link_to": "Work Order",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, BOM",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Production Plan",
+ "link_to": "Production Plan",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock Entry",
+ "link_to": "Stock Entry",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Job Card",
+ "link_to": "Job Card",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Downtime Entry",
+ "link_to": "Downtime Entry",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bill of Materials",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item",
+ "link_to": "Item",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Bill of Materials",
+ "link_to": "BOM",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Workstation",
+ "link_to": "Workstation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Operation",
+ "link_to": "Operation",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Routing",
+ "link_to": "Routing",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Production Planning Report",
+ "link_to": "Production Planning Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Work Order Summary",
+ "link_to": "Work Order Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Quality Inspection",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Quality Inspection Summary",
+ "link_to": "Quality Inspection Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Downtime Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Downtime Analysis",
+ "link_to": "Downtime Analysis",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Job Card",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Job Card Summary",
+ "link_to": "Job Card Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Search",
+ "link_to": "BOM Search",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Stock Report",
+ "link_to": "BOM Stock Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Work Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Production Analytics",
+ "link_to": "Production Analytics",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "BOM",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "BOM Operations Time",
+ "link_to": "BOM Operations Time",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tools",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "BOM Update Tool",
+ "link_to": "BOM Update Tool",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "BOM Comparison Tool",
+ "link_to": "bom-comparison-tool",
+ "link_type": "Page",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Manufacturing Settings",
+ "link_to": "Manufacturing Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:39.365928",
+ "modified_by": "Administrator",
+ "module": "Manufacturing",
+ "name": "Manufacturing",
+ "onboarding": "Manufacturing",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "restrict_to_domain": "Manufacturing",
+ "shortcuts": [
+ {
+ "color": "Green",
+ "format": "{} Active",
+ "label": "Item",
+ "link_to": "Item",
+ "restrict_to_domain": "Manufacturing",
+ "stats_filter": "{\n \"disabled\": 0\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Green",
+ "format": "{} Active",
+ "label": "BOM",
+ "link_to": "BOM",
+ "restrict_to_domain": "Manufacturing",
+ "stats_filter": "{\n \"is_active\": 1\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Yellow",
+ "format": "{} Open",
+ "label": "Work Order",
+ "link_to": "Work Order",
+ "restrict_to_domain": "Manufacturing",
+ "stats_filter": "{ \n \"status\": [\"in\", \n [\"Draft\", \"Not Started\", \"In Process\"]\n ]\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Yellow",
+ "format": "{} Open",
+ "label": "Production Plan",
+ "link_to": "Production Plan",
+ "restrict_to_domain": "Manufacturing",
+ "stats_filter": "{ \n \"status\": [\"not in\", [\"Completed\"]]\n}",
+ "type": "DocType"
+ },
+ {
+ "label": "Forecasting",
+ "link_to": "Exponential Smoothing Forecasting",
+ "type": "Report"
+ },
+ {
+ "label": "Work Order Summary",
+ "link_to": "Work Order Summary",
+ "restrict_to_domain": "Manufacturing",
+ "type": "Report"
+ },
+ {
+ "label": "BOM Stock Report",
+ "link_to": "BOM Stock Report",
+ "type": "Report"
+ },
+ {
+ "label": "Production Planning Report",
+ "link_to": "Production Planning Report",
+ "type": "Report"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "Manufacturing",
+ "restrict_to_domain": "Manufacturing",
+ "type": "Dashboard"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/non_profit/desk_page/non_profit/non_profit.json b/erpnext/non_profit/desk_page/non_profit/non_profit.json
deleted file mode 100644
index ebe6194..0000000
--- a/erpnext/non_profit/desk_page/non_profit/non_profit.json
+++ /dev/null
@@ -1,80 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Loan Management",
- "links": "[\n {\n \"description\": \"Define various loan types\",\n \"label\": \"Loan Type\",\n \"name\": \"Loan Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Loan Application\",\n \"label\": \"Loan Application\",\n \"name\": \"Loan Application\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Loan\",\n \"name\": \"Loan\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Grant Application",
- "links": "[\n {\n \"description\": \"Grant information.\",\n \"label\": \"Grant Application\",\n \"name\": \"Grant Application\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Membership",
- "links": "[\n {\n \"description\": \"Member information.\",\n \"label\": \"Member\",\n \"name\": \"Member\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Membership Details\",\n \"label\": \"Membership\",\n \"name\": \"Membership\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Membership Type Details\",\n \"label\": \"Membership Type\",\n \"name\": \"Membership Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Billing and Gateway Settings\",\n \"label\": \"Membership Settings\",\n \"name\": \"Membership Settings\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Volunteer",
- "links": "[\n {\n \"description\": \"Volunteer information.\",\n \"label\": \"Volunteer\",\n \"name\": \"Volunteer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Volunteer Type information.\",\n \"label\": \"Volunteer Type\",\n \"name\": \"Volunteer Type\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Chapter",
- "links": "[\n {\n \"description\": \"Chapter information.\",\n \"label\": \"Chapter\",\n \"name\": \"Chapter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Donor",
- "links": "[\n {\n \"description\": \"Donor information.\",\n \"label\": \"Donor\",\n \"name\": \"Donor\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Donor Type information.\",\n \"label\": \"Donor Type\",\n \"name\": \"Donor Type\",\n \"type\": \"doctype\"\n }\n]"
- }
- ],
- "category": "Domains",
- "charts": [],
- "creation": "2020-03-02 17:23:47.811421",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Non Profit",
- "modified": "2020-04-13 13:41:52.373705",
- "modified_by": "Administrator",
- "module": "Non Profit",
- "name": "Non Profit",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "restrict_to_domain": "Non Profit",
- "shortcuts": [
- {
- "label": "Member",
- "link_to": "Member",
- "type": "DocType"
- },
- {
- "label": "Membership Settings",
- "link_to": "Membership Settings",
- "type": "DocType"
- },
- {
- "label": "Membership",
- "link_to": "Membership",
- "type": "DocType"
- },
- {
- "label": "Chapter",
- "link_to": "Chapter",
- "type": "DocType"
- },
- {
- "label": "Chapter Member",
- "link_to": "Chapter Member",
- "type": "DocType"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/member/member.json b/erpnext/non_profit/doctype/member/member.json
index 992ef16..f190cfa 100644
--- a/erpnext/non_profit/doctype/member/member.json
+++ b/erpnext/non_profit/doctype/member/member.json
@@ -12,7 +12,6 @@
"membership_expiry_date",
"column_break_5",
"membership_type",
- "email",
"email_id",
"image",
"customer_section",
@@ -65,13 +64,6 @@
"reqd": 1
},
{
- "fieldname": "email",
- "fieldtype": "Link",
- "in_list_view": 1,
- "label": "User",
- "options": "User"
- },
- {
"fieldname": "image",
"fieldtype": "Attach Image",
"hidden": 1,
@@ -178,7 +170,7 @@
],
"image_field": "image",
"links": [],
- "modified": "2020-09-16 23:44:13.596948",
+ "modified": "2020-11-09 12:12:10.174647",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Member",
diff --git a/erpnext/non_profit/doctype/member/member.py b/erpnext/non_profit/doctype/member/member.py
index 44b975e..04b99f9 100644
--- a/erpnext/non_profit/doctype/member/member.py
+++ b/erpnext/non_profit/doctype/member/member.py
@@ -18,8 +18,6 @@
def validate(self):
- if self.email:
- self.validate_email_type(self.email)
if self.email_id:
self.validate_email_type(self.email_id)
@@ -57,14 +55,16 @@
def make_customer_and_link(self):
if self.customer:
frappe.msgprint(_("A customer is already linked to this Member"))
- cust = create_customer(frappe._dict({
+
+ customer = create_customer(frappe._dict({
'fullname': self.member_name,
- 'email': self.email_id or self.user,
+ 'email': self.email_id,
'phone': None
}))
- self.customer = cust
+ self.customer = customer
self.save()
+ frappe.msgprint(_("Customer {0} has been created succesfully.").format(self.customer))
def get_or_create_member(user_details):
@@ -177,4 +177,4 @@
mobile=mobile
))
- return member.name
\ No newline at end of file
+ return member.name
diff --git a/erpnext/non_profit/doctype/membership/membership.js b/erpnext/non_profit/doctype/membership/membership.js
index ee8a8c0..573ac33 100644
--- a/erpnext/non_profit/doctype/membership/membership.js
+++ b/erpnext/non_profit/doctype/membership/membership.js
@@ -4,16 +4,25 @@
frappe.ui.form.on('Membership', {
setup: function(frm) {
frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
- if (val) frm.set_df_property('razorpay_details_section', 'hidden', false);
+ if (val) frm.set_df_property("razorpay_details_section", "hidden", false);
})
},
refresh: function(frm) {
+ if (frm.doc.__islocal)
+ return;
+
!frm.doc.invoice && frm.add_custom_button("Generate Invoice", () => {
- frm.call("generate_invoice", {
- save: true
- }).then(() => {
- frm.reload_doc();
+ frm.call({
+ doc: frm.doc,
+ method: "generate_invoice",
+ args: {save: true},
+ freeze: true,
+ freeze_message: __("Creating Membership Invoice"),
+ callback: function(r) {
+ if (r.invoice)
+ frm.reload_doc();
+ }
});
});
@@ -27,6 +36,6 @@
},
onload: function(frm) {
- frm.add_fetch('membership_type', 'amount', 'amount');
+ frm.add_fetch("membership_type", "amount", "amount");
}
});
diff --git a/erpnext/non_profit/doctype/membership/membership.json b/erpnext/non_profit/doctype/membership/membership.json
index 7f21896..6da053f 100644
--- a/erpnext/non_profit/doctype/membership/membership.json
+++ b/erpnext/non_profit/doctype/membership/membership.json
@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"member",
+ "member_name",
"membership_type",
"column_break_3",
"membership_status",
@@ -46,6 +47,8 @@
{
"fieldname": "membership_status",
"fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
"label": "Membership Status",
"options": "New\nCurrent\nExpired\nPending\nCancelled"
},
@@ -122,11 +125,18 @@
"fieldtype": "Link",
"label": "Invoice",
"options": "Sales Invoice"
+ },
+ {
+ "fetch_from": "member.member_name",
+ "fieldname": "member_name",
+ "fieldtype": "Data",
+ "label": "Member Name",
+ "read_only": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-09-19 14:28:11.532696",
+ "modified": "2021-01-21 16:31:20.032656",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership",
@@ -158,7 +168,9 @@
}
],
"restrict_to_domain": "Non Profit",
+ "search_fields": "member, member_name",
"sort_field": "modified",
"sort_order": "DESC",
+ "title_field": "member_name",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership/membership.py b/erpnext/non_profit/doctype/membership/membership.py
index 7d15aba..c113b80 100644
--- a/erpnext/non_profit/doctype/membership/membership.py
+++ b/erpnext/non_profit/doctype/membership/membership.py
@@ -14,33 +14,43 @@
from frappe import _
import erpnext
-
class Membership(Document):
def validate(self):
if not self.member or not frappe.db.exists("Member", self.member):
- member_name = frappe.get_value('Member', dict(email=frappe.session.user))
+ # for web forms
+ user_type = frappe.db.get_value("User", frappe.session.user, "user_type")
+ if user_type == "Website User":
+ self.create_member_from_website_user()
+ else:
+ frappe.throw(_("Please select a Member"))
- if not member_name:
- user = frappe.get_doc('User', frappe.session.user)
- member = frappe.get_doc(dict(
- doctype='Member',
- email=frappe.session.user,
- membership_type=self.membership_type,
- member_name=user.get_fullname()
- )).insert(ignore_permissions=True)
- member_name = member.name
+ self.validate_membership_period()
- if self.get("__islocal"):
- self.member = member_name
+ def create_member_from_website_user(self):
+ member_name = frappe.get_value("Member", dict(email_id=frappe.session.user))
+ if not member_name:
+ user = frappe.get_doc("User", frappe.session.user)
+ member = frappe.get_doc(dict(
+ doctype="Member",
+ email_id=frappe.session.user,
+ membership_type=self.membership_type,
+ member_name=user.get_fullname()
+ )).insert(ignore_permissions=True)
+ member_name = member.name
+
+ if self.get("__islocal"):
+ self.member = member_name
+
+ def validate_membership_period(self):
# get last membership (if active)
- last_membership = erpnext.get_last_membership()
+ last_membership = erpnext.get_last_membership(self.member)
# if person applied for offline membership
if last_membership and not frappe.session.user == "Administrator":
# if last membership does not expire in 30 days, then do not allow to renew
if getdate(add_days(last_membership.to_date, -30)) > getdate(nowdate()) :
- frappe.throw(_('You can only renew if your membership expires within 30 days'))
+ frappe.throw(_("You can only renew if your membership expires within 30 days"))
self.from_date = add_days(last_membership.to_date, 1)
elif frappe.session.user == "Administrator":
@@ -54,11 +64,16 @@
self.to_date = add_months(self.from_date, 1)
def on_payment_authorized(self, status_changed_to=None):
- if status_changed_to in ("Completed", "Authorized"):
- self.load_from_db()
- self.db_set('paid', 1)
+ if status_changed_to not in ("Completed", "Authorized"):
+ return
+ self.load_from_db()
+ self.db_set("paid", 1)
+ settings = frappe.get_doc("Membership Settings")
+ if settings.enable_invoicing and settings.create_for_web_forms:
+ self.generate_invoice(with_payment_entry=settings.make_payment_entry, save=True)
- def generate_invoice(self, save=True):
+
+ def generate_invoice(self, save=True, with_payment_entry=False):
if not (self.paid or self.currency or self.amount):
frappe.throw(_("The payment for this membership is not paid. To generate invoice fill the payment details"))
@@ -66,34 +81,64 @@
frappe.throw(_("An invoice is already linked to this document"))
member = frappe.get_doc("Member", self.member)
- plan = frappe.get_doc("Membership Type", self.membership_type)
- settings = frappe.get_doc("Membership Settings")
-
if not member.customer:
frappe.throw(_("No customer linked to member {0}").format(frappe.bold(self.member)))
- if not settings.debit_account:
- frappe.throw(_("You need to set <b>Debit Account</b> in Membership Settings"))
-
- if not settings.company:
- frappe.throw(_("You need to set <b>Default Company</b> for invoicing in Membership Settings"))
+ plan = frappe.get_doc("Membership Type", self.membership_type)
+ settings = frappe.get_doc("Membership Settings")
+ self.validate_membership_type_and_settings(plan, settings)
invoice = make_invoice(self, member, plan, settings)
self.invoice = invoice.name
+ if with_payment_entry:
+ self.make_payment_entry(settings, invoice)
+
if save:
self.save()
return invoice
+ def validate_membership_type_and_settings(self, plan, settings):
+ settings_link = get_link_to_form("Membership Type", self.membership_type)
+
+ if not settings.debit_account:
+ frappe.throw(_("You need to set <b>Debit Account</b> in {0}").format(settings_link))
+
+ if not settings.company:
+ frappe.throw(_("You need to set <b>Default Company</b> for invoicing in {0}").format(settings_link))
+
+ if not plan.linked_item:
+ frappe.throw(_("Please set a Linked Item for the Membership Type {0}").format(
+ get_link_to_form("Membership Type", self.membership_type)))
+
+ def make_payment_entry(self, settings, invoice):
+ if not settings.payment_account:
+ frappe.throw(_("You need to set <b>Payment Account</b> in {0}").format(
+ get_link_to_form("Membership Type", self.membership_type)))
+
+ from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry
+ frappe.flags.ignore_account_permission = True
+ pe = get_payment_entry(dt="Sales Invoice", dn=invoice.name, bank_amount=invoice.grand_total)
+ frappe.flags.ignore_account_permission=False
+ pe.paid_to = settings.payment_account
+ pe.reference_no = self.name
+ pe.reference_date = getdate()
+ pe.save(ignore_permissions=True)
+ pe.submit()
+
def send_acknowlement(self):
settings = frappe.get_doc("Membership Settings")
if not settings.send_email:
- frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in Membership Settings"))
+ frappe.throw(_("You need to enable <b>Send Acknowledge Email</b> in {0}").format(
+ get_link_to_form("Membership Settings", "Membership Settings")))
member = frappe.get_doc("Member", self.member)
+ if not member.email_id:
+ frappe.throw(_("Email address of member {0} is missing").format(frappe.utils.get_link_to_form("Member", self.member)))
+
plan = frappe.get_doc("Membership Type", self.membership_type)
- email = member.email_id if member.email_id else member.email
+ email = member.email_id
attachments = [frappe.attach_print("Membership", self.name, print_format=settings.membership_print_format)]
if self.invoice and settings.send_invoice:
@@ -112,48 +157,56 @@
}
if not frappe.flags.in_test:
- frappe.enqueue(method=frappe.sendmail, queue='short', timeout=300, is_async=True, **email_args)
+ frappe.enqueue(method=frappe.sendmail, queue="short", timeout=300, is_async=True, **email_args)
else:
frappe.sendmail(**email_args)
def generate_and_send_invoice(self):
- invoice = self.generate_invoice(False)
+ self.generate_invoice(save=False)
self.send_acknowlement()
+
def make_invoice(membership, member, plan, settings):
invoice = frappe.get_doc({
- 'doctype': 'Sales Invoice',
- 'customer': member.customer,
- 'debit_to': settings.debit_account,
- 'currency': membership.currency,
- 'is_pos': 0,
- 'items': [
+ "doctype": "Sales Invoice",
+ "customer": member.customer,
+ "debit_to": settings.debit_account,
+ "currency": membership.currency,
+ "company": settings.company,
+ "is_pos": 0,
+ "items": [
{
- 'item_code': plan.linked_item,
- 'rate': membership.amount,
- 'qty': 1
+ "item_code": plan.linked_item,
+ "rate": membership.amount,
+ "qty": 1
}
]
})
-
+ invoice.set_missing_values()
invoice.insert(ignore_permissions=True)
invoice.submit()
+ frappe.msgprint(_("Sales Invoice created successfully"))
+
return invoice
+
def get_member_based_on_subscription(subscription_id, email):
members = frappe.get_all("Member", filters={
- 'subscription_id': subscription_id,
- 'email_id': email
+ "subscription_id": subscription_id,
+ "email_id": email
}, order_by="creation desc")
try:
- return frappe.get_doc("Member", members[0]['name'])
+ return frappe.get_doc("Member", members[0]["name"])
except:
return None
+
def verify_signature(data):
- signature = frappe.request.headers.get('X-Razorpay-Signature')
+ if frappe.flags.in_test:
+ return True
+ signature = frappe.request.headers.get("X-Razorpay-Signature")
settings = frappe.get_doc("Membership Settings")
key = settings.get_webhook_secret()
@@ -162,6 +215,7 @@
controller.verify_signature(data, signature, key)
+
@frappe.whitelist(allow_guest=True)
def trigger_razorpay_subscription(*args, **kwargs):
data = frappe.request.get_data(as_text=True)
@@ -170,16 +224,16 @@
except Exception as e:
log = frappe.log_error(e, "Webhook Verification Error")
notify_failure(log)
- return { 'status': 'Failed', 'reason': e}
+ return { "status": "Failed", "reason": e}
if isinstance(data, six.string_types):
data = json.loads(data)
data = frappe._dict(data)
- subscription = data.payload.get("subscription", {}).get('entity', {})
+ subscription = data.payload.get("subscription", {}).get("entity", {})
subscription = frappe._dict(subscription)
- payment = data.payload.get("payment", {}).get('entity', {})
+ payment = data.payload.get("payment", {}).get("entity", {})
payment = frappe._dict(payment)
try:
@@ -189,15 +243,15 @@
member = get_member_based_on_subscription(subscription.id, payment.email)
if not member:
member = create_member(frappe._dict({
- 'fullname': payment.email,
- 'email': payment.email,
- 'plan_id': get_plan_from_razorpay_id(subscription.plan_id)
+ "fullname": payment.email,
+ "email": payment.email,
+ "plan_id": get_plan_from_razorpay_id(subscription.plan_id)
}))
member.subscription_id = subscription.id
member.customer_id = payment.customer_id
if subscription.notes and type(subscription.notes) == dict:
- notes = '\n'.join("{}: {}".format(k, v) for k, v in subscription.notes.items())
+ notes = "\n".join("{}: {}".format(k, v) for k, v in subscription.notes.items())
member.add_comment("Comment", notes)
elif subscription.notes and type(subscription.notes) == str:
member.add_comment("Comment", subscription.notes)
@@ -227,28 +281,39 @@
message = "{0}\n\n{1}\n\n{2}: {3}".format(e, frappe.get_traceback(), __("Payment ID"), payment.id)
log = frappe.log_error(message, _("Error creating membership entry for {0}").format(member.name))
notify_failure(log)
- return { 'status': 'Failed', 'reason': e}
+ return { "status": "Failed", "reason": e}
- return { 'status': 'Success' }
+ return { "status": "Success" }
def notify_failure(log):
try:
- content = """Dear System Manager,
-Razorpay webhook for creating renewing membership subscription failed due to some reason. Please check the following error log linked below
+ content = """
+ Dear System Manager,
+ Razorpay webhook for creating renewing membership subscription failed due to some reason.
+ Please check the following error log linked below
+ Error Log: {0}
+ Regards, Administrator
+ """.format(get_link_to_form("Error Log", log.name))
-Error Log: {0}
-
-Regards,
-Administrator""".format(get_link_to_form("Error Log", log.name))
sendmail_to_system_managers("[Important] [ERPNext] Razorpay membership webhook failed , please check.", content)
except:
pass
+
def get_plan_from_razorpay_id(plan_id):
- plan = frappe.get_all("Membership Type", filters={'razorpay_plan_id': plan_id}, order_by="creation desc")
+ plan = frappe.get_all("Membership Type", filters={"razorpay_plan_id": plan_id}, order_by="creation desc")
try:
- return plan[0]['name']
+ return plan[0]["name"]
except:
return None
+
+
+def set_expired_status():
+ frappe.db.sql("""
+ UPDATE
+ `tabMembership` SET `status` = 'Expired'
+ WHERE
+ `status` not in ('Cancelled') AND `to_date` < %s
+ """, (nowdate()))
\ No newline at end of file
diff --git a/erpnext/non_profit/doctype/membership/membership_list.js b/erpnext/non_profit/doctype/membership/membership_list.js
new file mode 100644
index 0000000..a959159
--- /dev/null
+++ b/erpnext/non_profit/doctype/membership/membership_list.js
@@ -0,0 +1,15 @@
+frappe.listview_settings['Membership'] = {
+ get_indicator: function(doc) {
+ if (doc.membership_status == 'New') {
+ return [__('New'), 'blue', 'membership_status,=,New'];
+ } else if (doc.membership_status === 'Current') {
+ return [__('Current'), 'green', 'membership_status,=,Current'];
+ } else if (doc.membership_status === 'Pending') {
+ return [__('Pending'), 'yellow', 'membership_status,=,Pending'];
+ } else if (doc.membership_status === 'Expired') {
+ return [__('Expired'), 'grey', 'membership_status,=,Expired'];
+ } else {
+ return [__('Cancelled'), 'red', 'membership_status,=,Cancelled'];
+ }
+ }
+};
diff --git a/erpnext/non_profit/doctype/membership/test_membership.py b/erpnext/non_profit/doctype/membership/test_membership.py
index b23f406..ff7e6c4 100644
--- a/erpnext/non_profit/doctype/membership/test_membership.py
+++ b/erpnext/non_profit/doctype/membership/test_membership.py
@@ -2,8 +2,110 @@
# Copyright (c) 2017, Frappe Technologies Pvt. Ltd. and Contributors
# See license.txt
from __future__ import unicode_literals
-
import unittest
+import frappe
+import erpnext
+from erpnext.non_profit.doctype.member.member import create_member
+from frappe.utils import nowdate, add_months
class TestMembership(unittest.TestCase):
- pass
+ def setUp(self):
+ # Get default company
+ company = frappe.get_doc("Company", erpnext.get_default_company())
+
+ # update membership settings
+ settings = frappe.get_doc("Membership Settings")
+ # Enable razorpay
+ settings.enable_razorpay = 1
+ settings.billing_cycle = "Monthly"
+ settings.billing_frequency = 24
+ # Enable invoicing
+ settings.enable_invoicing = 1
+ settings.make_payment_entry = 1
+ settings.company = company.name
+ settings.payment_account = company.default_cash_account
+ settings.debit_account = company.default_receivable_account
+ settings.save()
+
+ # make test plan
+ if not frappe.db.exists("Membership Type", "_rzpy_test_milythm"):
+ plan = frappe.new_doc("Membership Type")
+ plan.membership_type = "_rzpy_test_milythm"
+ plan.amount = 100
+ plan.razorpay_plan_id = "_rzpy_test_milythm"
+ plan.linked_item = create_item("_Test Item for Non Profit Membership").name
+ plan.insert()
+ else:
+ plan = frappe.get_doc("Membership Type", "_rzpy_test_milythm")
+
+ # make test member
+ self.member_doc = create_member(frappe._dict({
+ 'fullname': "_Test_Member",
+ 'email': "_test_member_erpnext@example.com",
+ 'plan_id': plan.name
+ }))
+ self.member_doc.make_customer_and_link()
+ self.member = self.member_doc.name
+
+ def test_auto_generate_invoice_and_payment_entry(self):
+ entry = make_membership(self.member)
+
+ # Naive test to see if at all invoice was generated and attached to member
+ # In any case if details were missing, the invoicing would throw an error
+ invoice = entry.generate_invoice(save=True)
+ self.assertEqual(invoice.name, entry.invoice)
+
+ def test_renew_within_30_days(self):
+ # create a membership for two months
+ # Should work fine
+ make_membership(self.member, { "from_date": nowdate() })
+ make_membership(self.member, { "from_date": add_months(nowdate(), 1) })
+
+ from frappe.utils.user import add_role
+ add_role("test@example.com", "Non Profit Manager")
+ frappe.set_user("test@example.com")
+
+ # create next membership with expiry not within 30 days
+ self.assertRaises(frappe.ValidationError, make_membership, self.member, {
+ "from_date": add_months(nowdate(), 2),
+ })
+
+ frappe.set_user("Administrator")
+ # create the same membership but as administrator
+ make_membership(self.member, {
+ "from_date": add_months(nowdate(), 2),
+ "to_date": add_months(nowdate(), 3),
+ })
+
+def set_config(key, value):
+ frappe.db.set_value("Membership Settings", None, key, value)
+
+def make_membership(member, payload={}):
+ data = {
+ "doctype": "Membership",
+ "member": member,
+ "membership_status": "Current",
+ "membership_type": "_rzpy_test_milythm",
+ "currency": "INR",
+ "paid": 1,
+ "from_date": nowdate(),
+ "amount": 100
+ }
+ data.update(payload)
+ membership = frappe.get_doc(data)
+ membership.insert(ignore_permissions=True, ignore_if_duplicate=True)
+ return membership
+
+def create_item(item_code):
+ if not frappe.db.exists("Item", item_code):
+ item = frappe.new_doc("Item")
+ item.item_code = item_code
+ item.item_name = item_code
+ item.stock_uom = "Nos"
+ item.description = item_code
+ item.item_group = "All Item Groups"
+ item.is_stock_item = 0
+ item.save()
+ else:
+ item = frappe.get_doc("Item", item_code)
+ return item
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.js b/erpnext/non_profit/doctype/membership_settings/membership_settings.js
index 1d89402..c95aab2 100644
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.js
+++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.js
@@ -11,7 +11,7 @@
});
}
- frm.set_query('inv_print_format', function(doc) {
+ frm.set_query("inv_print_format", function() {
return {
filters: {
"doc_type": "Sales Invoice"
@@ -19,7 +19,7 @@
};
});
- frm.set_query('membership_print_format', function(doc) {
+ frm.set_query("membership_print_format", function() {
return {
filters: {
"doc_type": "Membership"
@@ -27,12 +27,23 @@
};
});
- frm.set_query('debit_account', function(doc) {
+ frm.set_query("debit_account", function() {
return {
filters: {
- 'account_type': 'Receivable',
- 'is_group': 0,
- 'company': frm.doc.company
+ "account_type": "Receivable",
+ "is_group": 0,
+ "company": frm.doc.company
+ }
+ };
+ });
+
+ frm.set_query("payment_account", function () {
+ var account_types = ["Bank", "Cash"];
+ return {
+ filters: {
+ "account_type": ["in", account_types],
+ "is_group": 0,
+ "company": frm.doc.company
}
};
});
diff --git a/erpnext/non_profit/doctype/membership_settings/membership_settings.json b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
index 5b6bab5..3887b0a 100644
--- a/erpnext/non_profit/doctype/membership_settings/membership_settings.json
+++ b/erpnext/non_profit/doctype/membership_settings/membership_settings.json
@@ -11,9 +11,12 @@
"billing_frequency",
"webhook_secret",
"column_break_6",
- "enable_auto_invoicing",
+ "enable_invoicing",
+ "create_for_web_forms",
+ "make_payment_entry",
"company",
"debit_account",
+ "payment_account",
"column_break_9",
"send_email",
"send_invoice",
@@ -58,14 +61,7 @@
"label": "Invoicing"
},
{
- "default": "0",
- "fieldname": "enable_auto_invoicing",
- "fieldtype": "Check",
- "label": "Enable Auto Invoicing",
- "mandatory_depends_on": "eval:doc.send_invoice"
- },
- {
- "depends_on": "eval:doc.enable_auto_invoicing",
+ "depends_on": "eval:doc.enable_invoicing",
"fieldname": "debit_account",
"fieldtype": "Link",
"label": "Debit Account",
@@ -77,7 +73,7 @@
"fieldtype": "Column Break"
},
{
- "depends_on": "eval:doc.enable_auto_invoicing",
+ "depends_on": "eval:doc.enable_invoicing",
"fieldname": "company",
"fieldtype": "Link",
"label": "Company",
@@ -86,7 +82,7 @@
},
{
"default": "0",
- "depends_on": "eval:doc.enable_auto_invoicing && doc.send_email",
+ "depends_on": "eval:doc.enable_invoicing && doc.send_email",
"fieldname": "send_invoice",
"fieldtype": "Check",
"label": "Send Invoice with Email"
@@ -119,11 +115,43 @@
"label": "Email Template",
"mandatory_depends_on": "eval:doc.send_email",
"options": "Email Template"
+ },
+ {
+ "default": "0",
+ "fieldname": "enable_invoicing",
+ "fieldtype": "Check",
+ "label": "Enable Invoicing",
+ "mandatory_depends_on": "eval:doc.send_invoice || doc.make_payment_entry"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.enable_invoicing",
+ "description": "Auto creates Payment Entry for Sales Invoices created for Membership from web forms.",
+ "fieldname": "make_payment_entry",
+ "fieldtype": "Check",
+ "label": "Make Payment Entry"
+ },
+ {
+ "depends_on": "eval:doc.make_payment_entry",
+ "fieldname": "payment_account",
+ "fieldtype": "Link",
+ "label": "Payment To",
+ "mandatory_depends_on": "eval:doc.make_payment_entry",
+ "options": "Account"
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:doc.enable_invoicing",
+ "description": "Automatically create an invoice when payment is authorized from a web form entry",
+ "fieldname": "create_for_web_forms",
+ "fieldtype": "Check",
+ "label": "Auto Create Invoice for Web Forms"
}
],
+ "index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-08-05 17:26:37.287395",
+ "modified": "2021-01-21 19:57:53.213286",
"modified_by": "Administrator",
"module": "Non Profit",
"name": "Membership Settings",
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.js b/erpnext/non_profit/doctype/membership_type/membership_type.js
index 43311a2..91a5cb7 100644
--- a/erpnext/non_profit/doctype/membership_type/membership_type.js
+++ b/erpnext/non_profit/doctype/membership_type/membership_type.js
@@ -2,13 +2,21 @@
// For license information, please see license.txt
frappe.ui.form.on('Membership Type', {
- refresh: function(frm) {
- frappe.db.get_single_value("Membership Settings", "enable_razorpay").then(val => {
+ refresh: function (frm) {
+ frappe.db.get_single_value('Membership Settings', 'enable_razorpay').then(val => {
if (val) frm.set_df_property('razorpay_plan_id', 'hidden', false);
});
- frappe.db.get_single_value("Membership Settings", "enable_auto_invoicing").then(val => {
+ frappe.db.get_single_value('Membership Settings', 'enable_invoicing').then(val => {
if (val) frm.set_df_property('linked_item', 'hidden', false);
});
+
+ frm.set_query('linked_item', () => {
+ return {
+ filters: {
+ is_stock_item: 0
+ }
+ };
+ });
}
});
diff --git a/erpnext/non_profit/doctype/membership_type/membership_type.py b/erpnext/non_profit/doctype/membership_type/membership_type.py
index b95b043..022829b 100644
--- a/erpnext/non_profit/doctype/membership_type/membership_type.py
+++ b/erpnext/non_profit/doctype/membership_type/membership_type.py
@@ -5,9 +5,14 @@
from __future__ import unicode_literals
from frappe.model.document import Document
import frappe
+from frappe import _
class MembershipType(Document):
- pass
+ def validate(self):
+ if self.linked_item:
+ is_stock_item = frappe.db.get_value("Item", self.linked_item, "is_stock_item")
+ if is_stock_item:
+ frappe.throw(_("The Linked Item should be a service item"))
def get_membership_type(razorpay_id):
return frappe.db.exists("Membership Type", {"razorpay_plan_id": razorpay_id})
\ No newline at end of file
diff --git a/erpnext/non_profit/workspace/non_profit/non_profit.json b/erpnext/non_profit/workspace/non_profit/non_profit.json
new file mode 100644
index 0000000..da2a514
--- /dev/null
+++ b/erpnext/non_profit/workspace/non_profit/non_profit.json
@@ -0,0 +1,224 @@
+{
+ "category": "Domains",
+ "charts": [],
+ "creation": "2020-03-02 17:23:47.811421",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "non-profit",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Non Profit",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Management",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Type",
+ "link_to": "Loan Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan Application",
+ "link_to": "Loan Application",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loan",
+ "link_to": "Loan",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Grant Application",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Grant Application",
+ "link_to": "Grant Application",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Membership",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Member",
+ "link_to": "Member",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Membership",
+ "link_to": "Membership",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Membership Type",
+ "link_to": "Membership Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Membership Settings",
+ "link_to": "Membership Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Volunteer",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Volunteer",
+ "link_to": "Volunteer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Volunteer Type",
+ "link_to": "Volunteer Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Chapter",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Chapter",
+ "link_to": "Chapter",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Donor",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Donor",
+ "link_to": "Donor",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Donor Type",
+ "link_to": "Donor Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:38.351409",
+ "modified_by": "Administrator",
+ "module": "Non Profit",
+ "name": "Non Profit",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "restrict_to_domain": "Non Profit",
+ "shortcuts": [
+ {
+ "label": "Member",
+ "link_to": "Member",
+ "type": "DocType"
+ },
+ {
+ "label": "Membership Settings",
+ "link_to": "Membership Settings",
+ "type": "DocType"
+ },
+ {
+ "label": "Membership",
+ "link_to": "Membership",
+ "type": "DocType"
+ },
+ {
+ "label": "Chapter",
+ "link_to": "Chapter",
+ "type": "DocType"
+ },
+ {
+ "label": "Chapter Member",
+ "link_to": "Chapter Member",
+ "type": "DocType"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/patches.txt b/erpnext/patches.txt
index 61aa2ee..ba31fee 100644
--- a/erpnext/patches.txt
+++ b/erpnext/patches.txt
@@ -450,7 +450,6 @@
erpnext.patches.v9_0.add_user_to_child_table_in_pos_profile
erpnext.patches.v9_0.set_schedule_date_for_material_request_and_purchase_order
erpnext.patches.v9_0.student_admission_childtable_migrate
-erpnext.patches.v9_0.fix_subscription_next_date #2017-10-23
erpnext.patches.v9_0.add_healthcare_domain
erpnext.patches.v9_0.set_variant_item_description
erpnext.patches.v9_0.set_uoms_in_variant_field
@@ -678,7 +677,7 @@
erpnext.patches.v12_0.fix_quotation_expired_status
erpnext.patches.v12_0.update_appointment_reminder_scheduler_entry
erpnext.patches.v12_0.rename_pos_closing_doctype
-erpnext.patches.v13_0.replace_pos_payment_mode_table
+erpnext.patches.v13_0.replace_pos_payment_mode_table #2020-12-29
erpnext.patches.v12_0.remove_duplicate_leave_ledger_entries #2020-05-22
erpnext.patches.v13_0.patch_to_fix_reverse_linking_in_additional_salary_encashment_and_incentive
execute:frappe.reload_doc("HR", "doctype", "Employee Advance")
@@ -691,6 +690,7 @@
erpnext.patches.v12_0.set_serial_no_status #2020-05-21
erpnext.patches.v12_0.update_price_list_currency_in_bom
execute:frappe.reload_doctype('Dashboard')
+execute:frappe.reload_doc('desk', 'doctype', 'number_card_link')
execute:frappe.delete_doc_if_exists('Dashboard', 'Accounts')
erpnext.patches.v13_0.update_actual_start_and_end_date_in_wo
erpnext.patches.v13_0.set_company_field_in_healthcare_doctypes #2020-05-25
@@ -711,6 +711,7 @@
execute:frappe.delete_doc_if_exists("DocType", "Bank Reconciliation")
erpnext.patches.v13_0.move_doctype_reports_and_notification_from_hr_to_payroll #22-06-2020
erpnext.patches.v13_0.move_payroll_setting_separately_from_hr_settings #22-06-2020
+execute:frappe.reload_doc("regional", "doctype", "e_invoice_settings")
erpnext.patches.v13_0.check_is_income_tax_component #22-06-2020
erpnext.patches.v13_0.loyalty_points_entry_for_pos_invoice #22-07-2020
erpnext.patches.v12_0.add_taxjar_integration_field
@@ -729,12 +730,29 @@
erpnext.patches.v13_0.rename_issue_doctype_fields
erpnext.patches.v13_0.change_default_pos_print_format
erpnext.patches.v13_0.set_youtube_video_id
+erpnext.patches.v13_0.set_app_name
erpnext.patches.v13_0.print_uom_after_quantity_patch
erpnext.patches.v13_0.set_payment_channel_in_payment_gateway_account
erpnext.patches.v13_0.create_healthcare_custom_fields_in_stock_entry_detail
+erpnext.patches.v12_0.setup_einvoice_fields #2020-12-02
erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.update_reason_for_resignation_in_employee
-erpnext.patches.v13_0.update_custom_fields_for_shopify
execute:frappe.delete_doc("Report", "Quoted Item Comparison")
+erpnext.patches.v13_0.update_member_email_address
+erpnext.patches.v13_0.update_custom_fields_for_shopify
erpnext.patches.v13_0.updates_for_multi_currency_payroll
erpnext.patches.v13_0.create_leave_policy_assignment_based_on_employee_current_leave_policy
+erpnext.patches.v13_0.update_pos_closing_entry_in_merge_log
+erpnext.patches.v13_0.add_po_to_global_search
+erpnext.patches.v13_0.update_returned_qty_in_pr_dn
+execute:frappe.rename_doc("Workspace", "Loan", "Loan Management", ignore_if_exists=True, force=True)
+erpnext.patches.v13_0.create_uae_pos_invoice_fields
+erpnext.patches.v13_0.update_project_template_tasks
+erpnext.patches.v13_0.set_company_in_leave_ledger_entry
+erpnext.patches.v13_0.convert_qi_parameter_to_link_field
+erpnext.patches.v13_0.setup_patient_history_settings_for_standard_doctypes
+erpnext.patches.v13_0.add_naming_series_to_old_projects # 1-02-2021
+erpnext.patches.v12_0.add_state_code_for_ladakh
+erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl
+erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes
+erpnext.patches.v13_0.update_vehicle_no_reqd_condition
diff --git a/erpnext/patches/v11_0/refactor_autoname_naming.py b/erpnext/patches/v11_0/refactor_autoname_naming.py
index 5dc5d3b..b997ba2 100644
--- a/erpnext/patches/v11_0/refactor_autoname_naming.py
+++ b/erpnext/patches/v11_0/refactor_autoname_naming.py
@@ -20,7 +20,7 @@
'Certified Consultant': 'NPO-CONS-.YYYY.-.#####',
'Chat Room': 'CHAT-ROOM-.#####',
'Compensatory Leave Request': 'HR-CMP-.YY.-.MM.-.#####',
- 'Custom Script': 'SYS-SCR-.#####',
+ 'Client Script': 'SYS-SCR-.#####',
'Employee Benefit Application': 'HR-BEN-APP-.YY.-.MM.-.#####',
'Employee Benefit Application Detail': '',
'Employee Benefit Claim': 'HR-BEN-CLM-.YY.-.MM.-.#####',
diff --git a/erpnext/patches/v11_1/update_bank_transaction_status.py b/erpnext/patches/v11_1/update_bank_transaction_status.py
index 1acdfcc..544bc5e 100644
--- a/erpnext/patches/v11_1/update_bank_transaction_status.py
+++ b/erpnext/patches/v11_1/update_bank_transaction_status.py
@@ -7,9 +7,20 @@
def execute():
frappe.reload_doc("accounts", "doctype", "bank_transaction")
- frappe.db.sql(""" UPDATE `tabBank Transaction`
- SET status = 'Reconciled'
- WHERE
- status = 'Settled' and (debit = allocated_amount or credit = allocated_amount)
- and ifnull(allocated_amount, 0) > 0
- """)
\ No newline at end of file
+ bank_transaction_fields = frappe.get_meta("Bank Transaction").get_valid_columns()
+
+ if 'debit' in bank_transaction_fields:
+ frappe.db.sql(""" UPDATE `tabBank Transaction`
+ SET status = 'Reconciled'
+ WHERE
+ status = 'Settled' and (debit = allocated_amount or credit = allocated_amount)
+ and ifnull(allocated_amount, 0) > 0
+ """)
+
+ elif 'deposit' in bank_transaction_fields:
+ frappe.db.sql(""" UPDATE `tabBank Transaction`
+ SET status = 'Reconciled'
+ WHERE
+ status = 'Settled' and (deposit = allocated_amount or withdrawal = allocated_amount)
+ and ifnull(allocated_amount, 0) > 0
+ """)
\ No newline at end of file
diff --git a/erpnext/patches/v12_0/add_state_code_for_ladakh.py b/erpnext/patches/v12_0/add_state_code_for_ladakh.py
new file mode 100644
index 0000000..d41101c
--- /dev/null
+++ b/erpnext/patches/v12_0/add_state_code_for_ladakh.py
@@ -0,0 +1,16 @@
+import frappe
+from erpnext.regional.india import states
+
+def execute():
+
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ custom_fields = ['Address-gst_state', 'Tax Category-gst_state']
+
+ # Update options in gst_state custom fields
+ for field in custom_fields:
+ gst_state_field = frappe.get_doc('Custom Field', field)
+ gst_state_field.options = '\n'.join(states)
+ gst_state_field.save()
diff --git a/erpnext/patches/v12_0/setup_einvoice_fields.py b/erpnext/patches/v12_0/setup_einvoice_fields.py
new file mode 100644
index 0000000..2474bc3
--- /dev/null
+++ b/erpnext/patches/v12_0/setup_einvoice_fields.py
@@ -0,0 +1,56 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
+from erpnext.regional.india.setup import add_permissions, add_print_formats
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ frappe.reload_doc("custom", "doctype", "custom_field")
+ frappe.reload_doc("regional", "doctype", "e_invoice_settings")
+ custom_fields = {
+ 'Sales Invoice': [
+ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
+ depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
+
+ dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
+
+ dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
+
+ dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
+ depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
+
+ dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
+ depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
+
+ dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
+ ]
+ }
+ create_custom_fields(custom_fields, update=True)
+ add_permissions()
+ add_print_formats()
+
+ einvoice_cond = 'in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category)'
+ t = {
+ 'mode_of_transport': [{'default': None}],
+ 'distance': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.transporter'}],
+ 'gst_vehicle_type': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
+ 'lr_date': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
+ 'lr_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && in_list(["Air", "Ship", "Rail"], doc.mode_of_transport)'}],
+ 'vehicle_no': [{'mandatory_depends_on': f'eval:{einvoice_cond} && doc.mode_of_transport == "Road"'}],
+ 'ewaybill': [
+ {'read_only_depends_on': 'eval:doc.irn && doc.ewaybill'},
+ {'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)'}
+ ]
+ }
+
+ for field, conditions in t.items():
+ for c in conditions:
+ [(prop, value)] = c.items()
+ frappe.db.set_value('Custom Field', { 'fieldname': field }, prop, value)
diff --git a/erpnext/patches/v13_0/add_naming_series_to_old_projects.py b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py
new file mode 100644
index 0000000..5ed9040
--- /dev/null
+++ b/erpnext/patches/v13_0/add_naming_series_to_old_projects.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+import frappe
+from frappe.custom.doctype.property_setter.property_setter import make_property_setter, delete_property_setter
+
+def execute():
+ frappe.reload_doc("projects", "doctype", "project")
+
+ frappe.db.sql("""UPDATE `tabProject`
+ SET
+ naming_series = 'PROJ-.####'
+ WHERE
+ naming_series is NULL""")
+
diff --git a/erpnext/patches/v13_0/add_po_to_global_search.py b/erpnext/patches/v13_0/add_po_to_global_search.py
new file mode 100644
index 0000000..1c60b18
--- /dev/null
+++ b/erpnext/patches/v13_0/add_po_to_global_search.py
@@ -0,0 +1,17 @@
+from __future__ import unicode_literals
+import frappe
+
+
+def execute():
+ global_search_settings = frappe.get_single("Global Search Settings")
+
+ if "Purchase Order" in (
+ dt.document_type for dt in global_search_settings.allowed_in_global_search
+ ):
+ return
+
+ global_search_settings.append(
+ "allowed_in_global_search", {"document_type": "Purchase Order"}
+ )
+
+ global_search_settings.save(ignore_permissions=True)
diff --git a/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
new file mode 100644
index 0000000..289b6a7
--- /dev/null
+++ b/erpnext/patches/v13_0/convert_qi_parameter_to_link_field.py
@@ -0,0 +1,23 @@
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc('stock', 'doctype', 'quality_inspection_parameter')
+
+ # get all distinct parameters from QI readigs table
+ reading_params = frappe.db.get_all("Quality Inspection Reading", fields=["distinct specification"])
+ reading_params = [d.specification for d in reading_params]
+
+ # get all distinct parameters from QI Template as some may be unused in QI
+ template_params = frappe.db.get_all("Item Quality Inspection Parameter", fields=["distinct specification"])
+ template_params = [d.specification for d in template_params]
+
+ params = list(set(reading_params + template_params))
+
+ for parameter in params:
+ if not frappe.db.exists("Quality Inspection Parameter", parameter):
+ frappe.get_doc({
+ "doctype": "Quality Inspection Parameter",
+ "parameter": parameter,
+ "description": parameter
+ }).insert(ignore_permissions=True)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
index 80c9137..90dc0e2 100644
--- a/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
+++ b/erpnext/patches/v13_0/create_leave_policy_assignment_based_on_employee_current_leave_policy.py
@@ -52,6 +52,8 @@
if leave_period:
filters["leave_period"] = leave_period
+ frappe.reload_doc('hr', 'doctype', 'leave_policy_assignment')
+
if not frappe.db.exists("Leave Policy Assignment" , filters):
lpa = frappe.new_doc("Leave Policy Assignment")
lpa.employee = employee
diff --git a/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py
new file mode 100644
index 0000000..48d5cb4
--- /dev/null
+++ b/erpnext/patches/v13_0/create_uae_pos_invoice_fields.py
@@ -0,0 +1,14 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from erpnext.regional.united_arab_emirates.setup import make_custom_fields
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': ['in', ['Saudi Arabia', 'United Arab Emirates']]})
+ if not company:
+ return
+
+ make_custom_fields()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py
new file mode 100644
index 0000000..af1f6e7
--- /dev/null
+++ b/erpnext/patches/v13_0/delete_old_bank_reconciliation_doctypes.py
@@ -0,0 +1,26 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ doctypes = [
+ "Bank Statement Settings",
+ "Bank Statement Settings Item",
+ "Bank Statement Transaction Entry",
+ "Bank Statement Transaction Invoice Item",
+ "Bank Statement Transaction Payment Item",
+ "Bank Statement Transaction Settings Item",
+ "Bank Statement Transaction Settings",
+ ]
+
+ for doctype in doctypes:
+ frappe.delete_doc("DocType", doctype, force=1)
+
+ frappe.delete_doc("Page", "bank-reconciliation", force=1)
+
+ rename_field("Bank Transaction", "debit", "deposit")
+ rename_field("Bank Transaction", "credit", "withdrawal")
diff --git a/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
new file mode 100644
index 0000000..ca04e8a
--- /dev/null
+++ b/erpnext/patches/v13_0/item_reposting_for_incorrect_sl_and_gl.py
@@ -0,0 +1,50 @@
+import frappe
+from frappe import _
+from frappe.utils import getdate, get_time
+from erpnext.stock.stock_ledger import update_entries_after
+from erpnext.accounts.utils import update_gl_entries_after
+
+def execute():
+ frappe.reload_doc('stock', 'doctype', 'repost_item_valuation')
+
+ reposting_project_deployed_on = frappe.db.get_value("DocType", "Repost Item Valuation", "creation")
+
+ data = frappe.db.sql('''
+ SELECT
+ name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time
+ FROM
+ `tabStock Ledger Entry`
+ WHERE
+ creation > %s
+ and is_cancelled = 0
+ ORDER BY timestamp(posting_date, posting_time) asc, creation asc
+ ''', reposting_project_deployed_on, as_dict=1)
+
+ frappe.db.auto_commit_on_many_writes = 1
+ print("Reposting Stock Ledger Entries...")
+ total_sle = len(data)
+ i = 0
+ for d in data:
+ update_entries_after({
+ "item_code": d.item_code,
+ "warehouse": d.warehouse,
+ "posting_date": d.posting_date,
+ "posting_time": d.posting_time,
+ "voucher_type": d.voucher_type,
+ "voucher_no": d.voucher_no,
+ "sle_id": d.name
+ }, allow_negative_stock=True)
+
+ i += 1
+ if i%100 == 0:
+ print(i, "/", total_sle)
+
+
+ print("Reposting General Ledger Entries...")
+ posting_date = getdate(reposting_project_deployed_on)
+ posting_time = get_time(reposting_project_deployed_on)
+
+ for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}):
+ update_gl_entries_after(posting_date, posting_time, company=row.name)
+
+ frappe.db.auto_commit_on_many_writes = 0
diff --git a/erpnext/patches/v13_0/rename_issue_doctype_fields.py b/erpnext/patches/v13_0/rename_issue_doctype_fields.py
index 96a6362..fa1dfed 100644
--- a/erpnext/patches/v13_0/rename_issue_doctype_fields.py
+++ b/erpnext/patches/v13_0/rename_issue_doctype_fields.py
@@ -29,7 +29,7 @@
'response_by_variance': response_by_variance,
'resolution_by_variance': resolution_by_variance,
'first_response_time': mins_to_first_response
- })
+ }, update_modified=False)
# commit after every 100 updates
count += 1
if count%100 == 0:
@@ -44,7 +44,7 @@
count = 0
for entry in opportunities:
mins_to_first_response = convert_to_seconds(entry.mins_to_first_response, 'Minutes')
- frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response)
+ frappe.db.set_value('Opportunity', entry.name, 'first_response_time', mins_to_first_response, update_modified=False)
# commit after every 100 updates
count += 1
if count%100 == 0:
diff --git a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py
index 1ca211b..7cb2648 100644
--- a/erpnext/patches/v13_0/replace_pos_payment_mode_table.py
+++ b/erpnext/patches/v13_0/replace_pos_payment_mode_table.py
@@ -6,12 +6,10 @@
import frappe
def execute():
- frappe.reload_doc("accounts", "doctype", "POS Payment Method")
+ frappe.reload_doc("accounts", "doctype", "pos_payment_method")
pos_profiles = frappe.get_all("POS Profile")
for pos_profile in pos_profiles:
- if not pos_profile.get("payments"): return
-
payments = frappe.db.sql("""
select idx, parentfield, parenttype, parent, mode_of_payment, `default` from `tabSales Invoice Payment` where parent=%s
""", pos_profile.name, as_dict=1)
diff --git a/erpnext/patches/v13_0/set_app_name.py b/erpnext/patches/v13_0/set_app_name.py
new file mode 100644
index 0000000..3f886f1
--- /dev/null
+++ b/erpnext/patches/v13_0/set_app_name.py
@@ -0,0 +1,7 @@
+import frappe
+from frappe import _
+
+def execute():
+ frappe.reload_doctype("System Settings")
+ settings = frappe.get_doc("System Settings")
+ settings.db_set("app_name", "ERPNext", commit=True)
diff --git a/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py
new file mode 100644
index 0000000..66857c4
--- /dev/null
+++ b/erpnext/patches/v13_0/set_company_in_leave_ledger_entry.py
@@ -0,0 +1,7 @@
+import frappe
+
+def execute():
+ frappe.reload_doc('HR', 'doctype', 'Leave Allocation')
+ frappe.reload_doc('HR', 'doctype', 'Leave Ledger Entry')
+ frappe.db.sql("""update `tabLeave Ledger Entry` as lle set company = (select company from `tabEmployee` where employee = lle.employee)""")
+ frappe.db.sql("""update `tabLeave Allocation` as la set company = (select company from `tabEmployee` where employee = la.employee)""")
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
new file mode 100644
index 0000000..de08aa2
--- /dev/null
+++ b/erpnext/patches/v13_0/setup_patient_history_settings_for_standard_doctypes.py
@@ -0,0 +1,13 @@
+from __future__ import unicode_literals
+import frappe
+from erpnext.healthcare.setup import setup_patient_history_settings
+
+def execute():
+ if "Healthcare" not in frappe.get_active_domains():
+ return
+
+ frappe.reload_doc("healthcare", "doctype", "Patient History Settings")
+ frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type")
+ frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type")
+
+ setup_patient_history_settings()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_member_email_address.py b/erpnext/patches/v13_0/update_member_email_address.py
new file mode 100644
index 0000000..4056f84
--- /dev/null
+++ b/erpnext/patches/v13_0/update_member_email_address.py
@@ -0,0 +1,23 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe.model.utils.rename_field import rename_field
+
+def execute():
+ """add value to email_id column from email"""
+
+ if frappe.db.has_column("Member", "email"):
+ # Get all members
+ for member in frappe.db.get_all("Member", pluck="name"):
+ # Check if email_id already exists
+ if not frappe.db.get_value("Member", member, "email_id"):
+ # fetch email id from the user linked field email
+ email = frappe.db.get_value("Member", member, "email")
+
+ # Set the value for it
+ frappe.db.set_value("Member", member, "email_id", email)
+
+ if frappe.db.exists("DocType", "Membership Settings"):
+ rename_field("Membership Settings", "enable_auto_invoicing", "enable_invoicing")
diff --git a/erpnext/patches/v13_0/update_old_loans.py b/erpnext/patches/v13_0/update_old_loans.py
index 7723942..8cf09aa 100644
--- a/erpnext/patches/v13_0/update_old_loans.py
+++ b/erpnext/patches/v13_0/update_old_loans.py
@@ -1,10 +1,12 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import nowdate
+from frappe.utils import nowdate, flt
from erpnext.accounts.doctype.account.test_account import create_account
from erpnext.loan_management.doctype.process_loan_interest_accrual.process_loan_interest_accrual import process_loan_interest_accrual_for_term_loans
from erpnext.loan_management.doctype.loan.loan import make_repayment_entry
+from erpnext.loan_management.doctype.loan_repayment.loan_repayment import get_accrued_interest_entries
+from frappe.model.naming import make_autoname
def execute():
@@ -18,15 +20,29 @@
frappe.reload_doc('loan_management', 'doctype', 'loan_repayment_detail')
frappe.reload_doc('loan_management', 'doctype', 'loan_interest_accrual')
frappe.reload_doc('accounts', 'doctype', 'gl_entry')
+ frappe.reload_doc('accounts', 'doctype', 'journal_entry_account')
updated_loan_types = []
+ loans_to_close = []
+
+ # Update old loan status as closed
+ if frappe.db.has_column('Repayment Schedule', 'paid'):
+ loans_list = frappe.db.sql("""SELECT distinct parent from `tabRepayment Schedule`
+ where paid = 0 and docstatus = 1""", as_dict=1)
+
+ loans_to_close = [d.parent for d in loans_list]
+
+ if loans_to_close:
+ frappe.db.sql("UPDATE `tabLoan` set status = 'Closed' where name not in (%s)" % (', '.join(['%s'] * len(loans_to_close))), tuple(loans_to_close))
loans = frappe.get_all('Loan', fields=['name', 'loan_type', 'company', 'status', 'mode_of_payment',
- 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'])
+ 'applicant_type', 'applicant', 'loan_account', 'payment_account', 'interest_income_account'],
+ filters={'docstatus': 1, 'status': ('!=', 'Closed')})
for loan in loans:
# Update details in Loan Types and Loan
loan_type_company = frappe.db.get_value('Loan Type', loan.loan_type, 'company')
+ loan_type = loan.loan_type
group_income_account = frappe.get_value('Account', {'company': loan.company,
'is_group': 1, 'root_type': 'Income', 'account_name': _('Indirect Income')})
@@ -38,7 +54,26 @@
penalty_account = create_account(company=loan.company, account_type='Income Account',
account_name='Penalty Account', parent_account=group_income_account)
- if not loan_type_company:
+ # Same loan type used for multiple companies
+ if loan_type_company and loan_type_company != loan.company:
+ # get loan type for appropriate company
+ loan_type_name = frappe.get_value('Loan Type', {'company': loan.company,
+ 'mode_of_payment': loan.mode_of_payment, 'loan_account': loan.loan_account,
+ 'payment_account': loan.payment_account, 'interest_income_account': loan.interest_income_account,
+ 'penalty_income_account': loan.penalty_income_account}, 'name')
+
+ if not loan_type_name:
+ loan_type_name = create_loan_type(loan, loan_type_name, penalty_account)
+
+ # update loan type in loan
+ frappe.db.sql("UPDATE `tabLoan` set loan_type = %s where name = %s", (loan_type_name,
+ loan.name))
+
+ loan_type = loan_type_name
+ if loan_type_name not in updated_loan_types:
+ updated_loan_types.append(loan_type_name)
+
+ elif not loan_type_company:
loan_type_doc = frappe.get_doc('Loan Type', loan.loan_type)
loan_type_doc.is_term_loan = 1
loan_type_doc.company = loan.company
@@ -49,8 +84,9 @@
loan_type_doc.penalty_income_account = penalty_account
loan_type_doc.submit()
updated_loan_types.append(loan.loan_type)
+ loan_type = loan.loan_type
- if loan.loan_type in updated_loan_types:
+ if loan_type in updated_loan_types:
if loan.status == 'Fully Disbursed':
status = 'Disbursed'
elif loan.status == 'Repaid/Closed':
@@ -64,25 +100,48 @@
'status': status
})
- process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan.loan_type,
+ process_loan_interest_accrual_for_term_loans(posting_date=nowdate(), loan_type=loan_type,
loan=loan.name)
- payments = frappe.db.sql(''' SELECT j.name, a.debit, a.debit_in_account_currency, j.posting_date
- FROM `tabJournal Entry` j, `tabJournal Entry Account` a
- WHERE a.parent = j.name and a.reference_type='Loan' and a.reference_name = %s
- and account = %s
- ''', (loan.name, loan.loan_account), as_dict=1)
- for payment in payments:
- repayment_entry = make_repayment_entry(loan.name, loan.loan_applicant_type, loan.applicant,
- loan.loan_type, loan.company)
+ if frappe.db.has_column('Repayment Schedule', 'paid'):
+ total_principal, total_interest = frappe.db.get_value('Repayment Schedule', {'paid': 1, 'parent': loan.name},
+ ['sum(principal_amount) as total_principal', 'sum(interest_amount) as total_interest'])
- repayment_entry.amount_paid = payment.debit_in_account_currency
- repayment_entry.posting_date = payment.posting_date
- repayment_entry.save()
- repayment_entry.submit()
+ accrued_entries = get_accrued_interest_entries(loan.name)
+ for entry in accrued_entries:
+ interest_paid = 0
+ principal_paid = 0
- jv = frappe.get_doc('Journal Entry', payment.name)
- jv.flags.ignore_links = True
- jv.cancel()
+ if flt(total_interest) > flt(entry.interest_amount):
+ interest_paid = flt(entry.interest_amount)
+ else:
+ interest_paid = flt(total_interest)
+ if flt(total_principal) > flt(entry.payable_principal_amount):
+ principal_paid = flt(entry.payable_principal_amount)
+ else:
+ principal_paid = flt(total_principal)
+
+ frappe.db.sql(""" UPDATE `tabLoan Interest Accrual`
+ SET paid_principal_amount = `paid_principal_amount` + %s,
+ paid_interest_amount = `paid_interest_amount` + %s
+ WHERE name = %s""",
+ (principal_paid, interest_paid, entry.name))
+
+ total_principal = flt(total_principal) - principal_paid
+ total_interest = flt(total_interest) - interest_paid
+
+def create_loan_type(loan, loan_type_name, penalty_account):
+ loan_type_doc = frappe.new_doc('Loan Type')
+ loan_type_doc.loan_name = make_autoname("Loan Type-.####")
+ loan_type_doc.is_term_loan = 1
+ loan_type_doc.company = loan.company
+ loan_type_doc.mode_of_payment = loan.mode_of_payment
+ loan_type_doc.payment_account = loan.payment_account
+ loan_type_doc.loan_account = loan.loan_account
+ loan_type_doc.interest_income_account = loan.interest_income_account
+ loan_type_doc.penalty_income_account = penalty_account
+ loan_type_doc.submit()
+
+ return loan_type_doc.name
diff --git a/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py b/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py
new file mode 100644
index 0000000..262e38d
--- /dev/null
+++ b/erpnext/patches/v13_0/update_pos_closing_entry_in_merge_log.py
@@ -0,0 +1,25 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# MIT License. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc("accounts", "doctype", "POS Invoice Merge Log")
+ frappe.reload_doc("accounts", "doctype", "POS Closing Entry")
+ if frappe.db.count('POS Invoice Merge Log'):
+ frappe.db.sql('''
+ UPDATE
+ `tabPOS Invoice Merge Log` log, `tabPOS Invoice Reference` log_ref
+ SET
+ log.pos_closing_entry = (
+ SELECT clo_ref.parent FROM `tabPOS Invoice Reference` clo_ref
+ WHERE clo_ref.pos_invoice = log_ref.pos_invoice
+ AND clo_ref.parenttype = 'POS Closing Entry' LIMIT 1
+ )
+ WHERE
+ log_ref.parent = log.name
+ ''')
+
+ frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Submitted' where docstatus = 1''')
+ frappe.db.sql('''UPDATE `tabPOS Closing Entry` SET status = 'Cancelled' where docstatus = 2''')
diff --git a/erpnext/patches/v13_0/update_project_template_tasks.py b/erpnext/patches/v13_0/update_project_template_tasks.py
new file mode 100644
index 0000000..8cc27d2
--- /dev/null
+++ b/erpnext/patches/v13_0/update_project_template_tasks.py
@@ -0,0 +1,47 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc("projects", "doctype", "project_template")
+ frappe.reload_doc("projects", "doctype", "project_template_task")
+ frappe.reload_doc("projects", "doctype", "task")
+
+ # Update property setter status if any
+ property_setter = frappe.db.get_value('Property Setter', {'doc_type': 'Task',
+ 'field_name': 'status', 'property': 'options'})
+
+ if property_setter:
+ property_setter_doc = frappe.get_doc('Property Setter', {'doc_type': 'Task',
+ 'field_name': 'status', 'property': 'options'})
+ property_setter_doc.value += "\nTemplate"
+ property_setter_doc.save()
+
+ for template_name in frappe.get_all('Project Template'):
+ template = frappe.get_doc("Project Template", template_name.name)
+ replace_tasks = False
+ new_tasks = []
+ for task in template.tasks:
+ if task.subject:
+ replace_tasks = True
+ new_task = frappe.get_doc(dict(
+ doctype = "Task",
+ subject = task.subject,
+ start = task.start,
+ duration = task.duration,
+ task_weight = task.task_weight,
+ description = task.description,
+ is_template = 1
+ )).insert()
+ new_tasks.append(new_task)
+
+ if replace_tasks:
+ template.tasks = []
+ for tsk in new_tasks:
+ template.append("tasks", {
+ "task": tsk.name,
+ "subject": tsk.subject
+ })
+ template.save()
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py
new file mode 100644
index 0000000..7f42cd9
--- /dev/null
+++ b/erpnext/patches/v13_0/update_returned_qty_in_pr_dn.py
@@ -0,0 +1,27 @@
+# Copyright (c) 2019, Frappe and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+
+def execute():
+ frappe.reload_doc('stock', 'doctype', 'purchase_receipt')
+ frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item')
+ frappe.reload_doc('stock', 'doctype', 'delivery_note')
+ frappe.reload_doc('stock', 'doctype', 'delivery_note_item')
+
+ def update_from_return_docs(doctype):
+ for return_doc in frappe.get_all(doctype, filters={'is_return' : 1, 'docstatus' : 1}):
+ # Update original receipt/delivery document from return
+ return_doc = frappe.get_cached_doc(doctype, return_doc.name)
+ return_doc.update_prevdoc_status()
+ return_against = frappe.get_doc(doctype, return_doc.return_against)
+ return_against.update_billing_status()
+
+ # Set received qty in stock uom in PR, as returned qty is checked against it
+ frappe.db.sql(""" update `tabPurchase Receipt Item`
+ set received_stock_qty = received_qty * conversion_factor
+ where docstatus = 1 """)
+
+ for doctype in ('Purchase Receipt', 'Delivery Note'):
+ update_from_return_docs(doctype)
\ No newline at end of file
diff --git a/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
new file mode 100644
index 0000000..c26cddb
--- /dev/null
+++ b/erpnext/patches/v13_0/update_vehicle_no_reqd_condition.py
@@ -0,0 +1,9 @@
+import frappe
+
+def execute():
+ company = frappe.get_all('Company', filters = {'country': 'India'})
+ if not company:
+ return
+
+ if frappe.db.exists('Custom Field', { 'fieldname': 'vehicle_no' }):
+ frappe.db.set_value('Custom Field', { 'fieldname': 'vehicle_no' }, 'mandatory_depends_on', '')
diff --git a/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py b/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py
index ad043dd..97e217a 100644
--- a/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py
+++ b/erpnext/patches/v4_0/map_charge_to_taxes_and_charges.py
@@ -5,11 +5,11 @@
import frappe
def execute():
- # udpate sales cycle
+ # update sales cycle
for d in ['Sales Invoice', 'Sales Order', 'Quotation', 'Delivery Note']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=charge""" % d)
- # udpate purchase cycle
+ # update purchase cycle
for d in ['Purchase Invoice', 'Purchase Order', 'Supplier Quotation', 'Purchase Receipt']:
frappe.db.sql("""update `tab%s` set taxes_and_charges=purchase_other_charges""" % d)
diff --git a/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py b/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py
index ef3f1d6..c564f8b 100644
--- a/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py
+++ b/erpnext/patches/v5_0/replace_renamed_fields_in_custom_scripts_and_print_formats.py
@@ -9,7 +9,7 @@
# NOTE: sequence is important
renamed_fields = get_all_renamed_fields()
- for dt, script_field, ref_dt_field in (("Custom Script", "script", "dt"), ("Print Format", "html", "doc_type")):
+ for dt, script_field, ref_dt_field in (("Client Script", "script", "dt"), ("Print Format", "html", "doc_type")):
cond1 = " or ".join("""{0} like "%%{1}%%" """.format(script_field, d[0].replace("_", "\\_")) for d in renamed_fields)
cond2 = " and standard = 'No'" if dt == "Print Format" else ""
diff --git a/erpnext/patches/v7_0/po_status_issue_for_pr_return.py b/erpnext/patches/v7_0/po_status_issue_for_pr_return.py
index 6e92ffb..910814f 100644
--- a/erpnext/patches/v7_0/po_status_issue_for_pr_return.py
+++ b/erpnext/patches/v7_0/po_status_issue_for_pr_return.py
@@ -7,19 +7,23 @@
def execute():
parent_list = []
count = 0
- for data in frappe.db.sql("""
- select
+
+ frappe.reload_doc('stock', 'doctype', 'purchase_receipt')
+ frappe.reload_doc('stock', 'doctype', 'purchase_receipt_item')
+
+ for data in frappe.db.sql("""
+ select
`tabPurchase Receipt Item`.purchase_order, `tabPurchase Receipt Item`.name,
`tabPurchase Receipt Item`.item_code, `tabPurchase Receipt Item`.idx,
`tabPurchase Receipt Item`.parent
- from
+ from
`tabPurchase Receipt Item`, `tabPurchase Receipt`
where
`tabPurchase Receipt Item`.parent = `tabPurchase Receipt`.name and
`tabPurchase Receipt Item`.purchase_order_item is null and
`tabPurchase Receipt Item`.purchase_order is not null and
`tabPurchase Receipt`.is_return = 1""", as_dict=1):
- name = frappe.db.get_value('Purchase Order Item',
+ name = frappe.db.get_value('Purchase Order Item',
{'item_code': data.item_code, 'parent': data.purchase_order, 'idx': data.idx}, 'name')
if name:
diff --git a/erpnext/patches/v7_0/remove_doctypes_and_reports.py b/erpnext/patches/v7_0/remove_doctypes_and_reports.py
index 746cae0..2356e2f 100644
--- a/erpnext/patches/v7_0/remove_doctypes_and_reports.py
+++ b/erpnext/patches/v7_0/remove_doctypes_and_reports.py
@@ -7,7 +7,7 @@
where name in('Time Log Batch', 'Time Log Batch Detail', 'Time Log')""")
frappe.db.sql("""delete from `tabDocField` where parent in ('Time Log', 'Time Log Batch')""")
- frappe.db.sql("""update `tabCustom Script` set dt = 'Timesheet' where dt = 'Time Log'""")
+ frappe.db.sql("""update `tabClient Script` set dt = 'Timesheet' where dt = 'Time Log'""")
for data in frappe.db.sql(""" select label, fieldname from `tabCustom Field` where dt = 'Time Log'""", as_dict=1):
custom_field = frappe.get_doc({
diff --git a/erpnext/patches/v9_0/fix_subscription_next_date.py b/erpnext/patches/v9_0/fix_subscription_next_date.py
deleted file mode 100644
index 4595c8d..0000000
--- a/erpnext/patches/v9_0/fix_subscription_next_date.py
+++ /dev/null
@@ -1,48 +0,0 @@
-# Copyright (c) 2017, Frappe and Contributors
-# License: GNU General Public License v3. See license.txt
-
-from __future__ import unicode_literals
-import frappe
-from frappe.utils import getdate
-from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date
-
-def execute():
- frappe.reload_doc('accounts', 'doctype', 'subscription')
- fields = ["name", "reference_doctype", "reference_document",
- "start_date", "frequency", "repeat_on_day"]
-
- for d in fields:
- if not frappe.db.has_column('Subscription', d):
- return
-
- doctypes = ('Purchase Order', 'Sales Order', 'Purchase Invoice', 'Sales Invoice')
- for data in frappe.get_all('Subscription',
- fields = fields,
- filters = {'reference_doctype': ('in', doctypes), 'docstatus': 1}):
-
- recurring_id = frappe.db.get_value(data.reference_doctype, data.reference_document, "recurring_id")
- if recurring_id:
- frappe.db.sql("update `tab{0}` set subscription=%s where recurring_id=%s"
- .format(data.reference_doctype), (data.name, recurring_id))
-
- date_field = 'transaction_date'
- if data.reference_doctype in ['Sales Invoice', 'Purchase Invoice']:
- date_field = 'posting_date'
-
- start_date = frappe.db.get_value(data.reference_doctype, data.reference_document, date_field)
-
- if start_date and getdate(start_date) != getdate(data.start_date):
- last_ref_date = frappe.db.sql("""
- select {0}
- from `tab{1}`
- where subscription=%s and docstatus < 2
- order by creation desc
- limit 1
- """.format(date_field, data.reference_doctype), data.name)[0][0]
-
- next_schedule_date = get_next_schedule_date(last_ref_date, data.frequency, data.repeat_on_day)
-
- frappe.db.set_value("Subscription", data.name, {
- "start_date": start_date,
- "next_schedule_date": next_schedule_date
- }, None)
\ No newline at end of file
diff --git a/erpnext/payroll/desk_page/payroll/payroll.json b/erpnext/payroll/desk_page/payroll/payroll.json
deleted file mode 100644
index 285e3b3..0000000
--- a/erpnext/payroll/desk_page/payroll/payroll.json
+++ /dev/null
@@ -1,84 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Payroll",
- "links": "[\n {\n \"label\": \"Salary Component\",\n \"name\": \"Salary Component\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Salary Structure\",\n \"name\": \"Salary Structure\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Structure Assignment\",\n \"name\": \"Salary Structure Assignment\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Payroll Entry\",\n \"name\": \"Payroll Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Slip\",\n \"name\": \"Salary Slip\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Taxation",
- "links": "[\n {\n \"label\": \"Payroll Period\",\n \"name\": \"Payroll Period\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Income Tax Slab\",\n \"name\": \"Income Tax Slab\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Other Income\",\n \"name\": \"Employee Other Income\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Declaration\",\n \"name\": \"Employee Tax Exemption Declaration\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Proof Submission\",\n \"name\": \"Employee Tax Exemption Proof Submission\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Category\",\n \"name\": \"Employee Tax Exemption Category\",\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Employee Tax Exemption Sub Category\",\n \"name\": \"Employee Tax Exemption Sub Category\",\n \"type\": \"doctype\"\n \n }\n]"
- },
- {
- "hidden": 0,
- "label": "Compensations",
- "links": "[\n {\n \"label\": \"Additional Salary\",\n \"name\": \"Additional Salary\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n \n },\n {\n \"label\": \"Retention Bonus\",\n \"name\": \"Retention Bonus\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Incentive\",\n \"name\": \"Employee Incentive\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Benefit Application\",\n \"name\": \"Employee Benefit Application\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Employee Benefit Claim\",\n \"name\": \"Employee Benefit Claim\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"is_query_report\": true,\n \"label\": \"Salary Register\",\n \"name\": \"Salary Register\",\n \"type\": \"report\"\n \n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"label\": \"Salary Payments Based On Payment Mode\",\n \"is_query_report\": true,\n \"name\": \"Salary Payments Based On Payment Mode\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"label\": \"Salary Payments via ECS\",\n \"is_query_report\": true,\n \"name\": \"Salary Payments via ECS\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"label\": \"Income Tax Deductions\",\n \"is_query_report\": true,\n \"name\": \"Income Tax Deductions\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"label\": \"Professional Tax Deductions\",\n \"is_query_report\": true,\n \"name\": \"Professional Tax Deductions\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Salary Slip\"\n ],\n \"doctype\": \"Salary Slip\",\n \"label\": \"Provident Fund Deductions\",\n \"is_query_report\": true,\n \"name\": \"Provident Fund Deductions\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Payroll Entry\"\n ],\n \"doctype\": \"Payroll Entry\",\n \"is_query_report\": true,\n \"label\": \"Bank Remittance\",\n \"name\": \"Bank Remittance\",\n \"type\": \"report\"\n \n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [
- {
- "chart_name": "Outgoing Salary",
- "label": "Outgoing Salary"
- }
- ],
- "creation": "2020-05-27 19:54:23.405607",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Payroll",
- "modified": "2020-08-10 19:38:45.976209",
- "modified_by": "Administrator",
- "module": "Payroll",
- "name": "Payroll",
- "onboarding": "Payroll",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "label": "Salary Structure",
- "link_to": "Salary Structure",
- "type": "DocType"
- },
- {
- "label": "Payroll Entry",
- "link_to": "Payroll Entry",
- "type": "DocType"
- },
- {
- "color": "",
- "format": "{} Pending",
- "label": "Salary Slip",
- "link_to": "Salary Slip",
- "stats_filter": "{\"status\": \"Draft\"}",
- "type": "DocType"
- },
- {
- "label": "Income Tax Slab",
- "link_to": "Income Tax Slab",
- "type": "DocType"
- },
- {
- "label": "Salary Register",
- "link_to": "Salary Register",
- "type": "Report"
- },
- {
- "label": "Dashboard",
- "link_to": "Payroll",
- "type": "Dashboard"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/additional_salary/additional_salary.js b/erpnext/payroll/doctype/additional_salary/additional_salary.js
index 0784de9..d1ed91f 100644
--- a/erpnext/payroll/doctype/additional_salary/additional_salary.js
+++ b/erpnext/payroll/doctype/additional_salary/additional_salary.js
@@ -13,13 +13,7 @@
};
});
- if (!frm.doc.currency) return;
- frm.set_query("salary_component", function() {
- return {
- query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
- filters: {currency: frm.doc.currency, company: frm.doc.company}
- };
- });
+ frm.trigger('set_earning_component');
},
employee: function(frm) {
@@ -51,6 +45,19 @@
});
},
+ company: function(frm) {
+ frm.trigger('set_earning_component');
+ },
+
+ set_earning_component: function(frm) {
+ if (!frm.doc.company) return;
+ frm.set_query("salary_component", function() {
+ return {
+ filters: {type: ["in", ["earning", "deduction"]], company: frm.doc.company}
+ };
+ });
+ },
+
get_employee_currency: function(frm) {
frappe.call({
method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
diff --git a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
index 9a5a463..4c45580 100644
--- a/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
+++ b/erpnext/payroll/doctype/employee_benefit_application/employee_benefit_application.json
@@ -23,6 +23,7 @@
"employee_benefits",
"totals",
"total_amount",
+ "column_break",
"pro_rata_dispensed_amount"
],
"fields": [
@@ -139,11 +140,15 @@
"label": "Company",
"options": "Company",
"reqd": 1
+ },
+ {
+ "fieldname": "column_break",
+ "fieldtype": "Column Break"
}
],
"is_submittable": 1,
"links": [],
- "modified": "2020-11-25 11:49:05.095101",
+ "modified": "2020-12-14 15:52:08.566418",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Employee Benefit Application",
diff --git a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
index 85d1c54..b2809b1 100644
--- a/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
+++ b/erpnext/payroll/doctype/employee_incentive/employee_incentive.js
@@ -10,15 +10,7 @@
}
};
});
-
- if (!frm.doc.currency) return;
- frm.set_query("salary_component", function() {
- return {
- query: "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
- filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company}
- };
- });
-
+ frm.trigger('set_earning_component');
},
employee: function(frm) {
@@ -45,11 +37,21 @@
callback: function(data) {
if (data.message) {
frm.set_value("company", data.message.company);
+ frm.trigger('set_earning_component');
}
}
});
},
+ set_earning_component: function(frm) {
+ if (!frm.doc.company) return;
+ frm.set_query("salary_component", function() {
+ return {
+ filters: {type: "earning", company: frm.doc.company}
+ };
+ });
+ },
+
get_employee_currency: function(frm) {
frappe.call({
method: "erpnext.payroll.doctype.salary_structure_assignment.salary_structure_assignment.get_employee_currency",
diff --git a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
index 0609d19..311f352 100644
--- a/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
+++ b/erpnext/payroll/doctype/employee_tax_exemption_declaration/test_employee_tax_exemption_declaration.py
@@ -86,19 +86,21 @@
self.assertEqual(declaration.total_exemption_amount, 100000)
-def create_payroll_period():
- if not frappe.db.exists("Payroll Period", "_Test Payroll Period"):
+def create_payroll_period(**args):
+ args = frappe._dict(args)
+ name = args.name or "_Test Payroll Period"
+ if not frappe.db.exists("Payroll Period", name):
from datetime import date
payroll_period = frappe.get_doc(dict(
doctype = 'Payroll Period',
- name = "_Test Payroll Period",
- company = erpnext.get_default_company(),
- start_date = date(date.today().year, 1, 1),
- end_date = date(date.today().year, 12, 31)
+ name = name,
+ company = args.company or erpnext.get_default_company(),
+ start_date = args.start_date or date(date.today().year, 1, 1),
+ end_date = args.end_date or date(date.today().year, 12, 31)
)).insert()
return payroll_period
else:
- return frappe.get_doc("Payroll Period", "_Test Payroll Period")
+ return frappe.get_doc("Payroll Period", name)
def create_exemption_category():
if not frappe.db.exists("Employee Tax Exemption Category", "_Test Category"):
diff --git a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py
index 253f023..81e3647 100644
--- a/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py
+++ b/erpnext/payroll/doctype/income_tax_slab/income_tax_slab.py
@@ -3,8 +3,11 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-# import frappe
+#import frappe
+import erpnext
from frappe.model.document import Document
class IncomeTaxSlab(Document):
- pass
+ def validate(self):
+ if self.company:
+ self.currency = erpnext.get_company_currency(self.company)
diff --git a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json
index 8a55224..09c7eb9 100644
--- a/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json
+++ b/erpnext/payroll/doctype/payroll_employee_detail/payroll_employee_detail.json
@@ -17,8 +17,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"label": "Employee",
- "options": "Employee",
- "read_only": 1
+ "options": "Employee"
},
{
"fetch_from": "employee.employee_name",
@@ -52,7 +51,7 @@
],
"istable": 1,
"links": [],
- "modified": "2020-09-30 12:40:07.999878",
+ "modified": "2020-12-17 15:43:29.542977",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Employee Detail",
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
index cb48abb..395e56f 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.js
@@ -3,6 +3,8 @@
var in_progress = false;
+frappe.provide("erpnext.accounts.dimensions");
+
frappe.ui.form.on('Payroll Entry', {
onload: function (frm) {
if (!frm.doc.posting_date) {
@@ -10,15 +12,23 @@
}
frm.toggle_reqd(['payroll_frequency'], !frm.doc.salary_slip_based_on_timesheet);
- frm.set_query("department", function() {
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ frm.events.department_filters(frm);
+ frm.events.payroll_payable_account_filters(frm);
+ },
+
+ department_filters: function (frm) {
+ frm.set_query("department", function () {
return {
"filters": {
"company": frm.doc.company,
}
};
});
+ },
- frm.set_query("payroll_payable_account", function() {
+ payroll_payable_account_filters: function (frm) {
+ frm.set_query("payroll_payable_account", function () {
return {
filters: {
"company": frm.doc.company,
@@ -29,20 +39,20 @@
});
},
- refresh: function(frm) {
+ refresh: function (frm) {
if (frm.doc.docstatus == 0) {
- if(!frm.is_new()) {
+ if (!frm.is_new()) {
frm.page.clear_primary_action();
frm.add_custom_button(__("Get Employees"),
- function() {
+ function () {
frm.events.get_employee_details(frm);
}
).toggleClass('btn-primary', !(frm.doc.employees || []).length);
}
- if ((frm.doc.employees || []).length) {
+ if ((frm.doc.employees || []).length && !frappe.model.has_workflow(frm.doctype)) {
frm.page.clear_primary_action();
frm.page.set_primary_action(__('Create Salary Slips'), () => {
- frm.save('Submit').then(()=>{
+ frm.save('Submit').then(() => {
frm.page.clear_primary_action();
frm.refresh();
frm.events.refresh(frm);
@@ -61,48 +71,48 @@
doc: frm.doc,
method: 'fill_employee_details',
}).then(r => {
- if (r.docs && r.docs[0].employees){
+ if (r.docs && r.docs[0].employees) {
frm.employees = r.docs[0].employees;
frm.dirty();
frm.save();
frm.refresh();
- if(r.docs[0].validate_attendance){
+ if (r.docs[0].validate_attendance) {
render_employee_attendance(frm, r.message);
}
}
- })
+ });
},
- create_salary_slips: function(frm) {
+ create_salary_slips: function (frm) {
frm.call({
doc: frm.doc,
method: "create_salary_slips",
- callback: function(r) {
+ callback: function () {
frm.refresh();
frm.toolbar.refresh();
}
- })
+ });
},
- add_context_buttons: function(frm) {
- if(frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
+ add_context_buttons: function (frm) {
+ if (frm.doc.salary_slips_submitted || (frm.doc.__onload && frm.doc.__onload.submitted_ss)) {
frm.events.add_bank_entry_button(frm);
- } else if(frm.doc.salary_slips_created) {
- frm.add_custom_button(__("Submit Salary Slip"), function() {
+ } else if (frm.doc.salary_slips_created) {
+ frm.add_custom_button(__("Submit Salary Slip"), function () {
submit_salary_slip(frm);
}).addClass("btn-primary");
}
},
- add_bank_entry_button: function(frm) {
+ add_bank_entry_button: function (frm) {
frappe.call({
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.payroll_entry_has_bank_entries',
args: {
'name': frm.doc.name
},
- callback: function(r) {
+ callback: function (r) {
if (r.message && !r.message.submitted) {
- frm.add_custom_button("Make Bank Entry", function() {
+ frm.add_custom_button("Make Bank Entry", function () {
make_bank_entry(frm);
}).addClass("btn-primary");
}
@@ -122,31 +132,46 @@
"company": frm.doc.company
}
};
- }),
- frm.set_query("cost_center", function () {
+ });
+ },
+
+ payroll_frequency: function (frm) {
+ frm.trigger("set_start_end_dates").then( ()=> {
+ frm.events.clear_employee_table(frm);
+ frm.events.get_employee_with_salary_slip_and_set_query(frm);
+ });
+ },
+
+ employee_filters: function (frm, emp_list) {
+ frm.set_query('employee', 'employees', () => {
return {
filters: {
- "is_group": 0,
- company: frm.doc.company
- }
- };
- }),
- frm.set_query("project", function () {
- return {
- filters: {
- company: frm.doc.company
+ name: ["not in", emp_list]
}
};
});
},
- payroll_frequency: function (frm) {
- frm.trigger("set_start_end_dates");
- frm.events.clear_employee_table(frm);
+ get_employee_with_salary_slip_and_set_query: function (frm) {
+ frappe.db.get_list('Salary Slip', {
+ filters: {
+ start_date: frm.doc.start_date,
+ end_date: frm.doc.end_date,
+ docstatus: 1,
+ },
+ fields: ['employee']
+ }).then((emp) => {
+ var emp_list = [];
+ emp.forEach((employee_data) => {
+ emp_list.push(Object.values(employee_data)[0]);
+ });
+ frm.events.employee_filters(frm, emp_list);
+ });
},
company: function (frm) {
frm.events.clear_employee_table(frm);
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
currency: function (frm) {
@@ -164,17 +189,17 @@
from_currency: frm.doc.currency,
to_currency: company_currency,
},
- callback: function(r) {
+ callback: function (r) {
frm.set_value("exchange_rate", flt(r.message));
frm.set_df_property('exchange_rate', 'hidden', 0);
- frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency
- + " = [?] " + company_currency);
+ frm.set_df_property("exchange_rate", "description", "1 " + frm.doc.currency +
+ " = [?] " + company_currency);
}
});
} else {
frm.set_value("exchange_rate", 1.0);
frm.set_df_property('exchange_rate', 'hidden', 1);
- frm.set_df_property("exchange_rate", "description", "" );
+ frm.set_df_property("exchange_rate", "description", "");
}
}
},
@@ -192,9 +217,9 @@
},
start_date: function (frm) {
- if(!in_progress && frm.doc.start_date){
+ if (!in_progress && frm.doc.start_date) {
frm.trigger("set_end_date");
- }else{
+ } else {
// reset flag
in_progress = false;
}
@@ -228,7 +253,7 @@
}
},
- set_end_date: function(frm){
+ set_end_date: function (frm) {
frappe.call({
method: 'erpnext.payroll.doctype.payroll_entry.payroll_entry.get_end_date',
args: {
@@ -243,19 +268,19 @@
});
},
- validate_attendance: function(frm){
- if(frm.doc.validate_attendance && frm.doc.employees){
+ validate_attendance: function (frm) {
+ if (frm.doc.validate_attendance && frm.doc.employees) {
frappe.call({
method: 'validate_employee_attendance',
args: {},
- callback: function(r) {
+ callback: function (r) {
render_employee_attendance(frm, r.message);
},
doc: frm.doc,
freeze: true,
freeze_message: __('Validating Employee Attendance...')
});
- }else{
+ } else {
frm.fields_dict.attendance_detail_html.html("");
}
},
@@ -270,18 +295,20 @@
const submit_salary_slip = function (frm) {
frappe.confirm(__('This will submit Salary Slips and create accrual Journal Entry. Do you want to proceed?'),
- function() {
+ function () {
frappe.call({
method: 'submit_salary_slips',
args: {},
- callback: function() {frm.events.refresh(frm);},
+ callback: function () {
+ frm.events.refresh(frm);
+ },
doc: frm.doc,
freeze: true,
freeze_message: __('Submitting Salary Slips and creating Journal Entry...')
});
},
- function() {
- if(frappe.dom.freeze_count) {
+ function () {
+ if (frappe.dom.freeze_count) {
frappe.dom.unfreeze();
frm.events.refresh(frm);
}
@@ -295,9 +322,11 @@
return frappe.call({
doc: cur_frm.doc,
method: "make_payment_entry",
- callback: function() {
+ callback: function () {
frappe.set_route(
- 'List', 'Journal Entry', {"Journal Entry Account.reference_name": frm.doc.name}
+ 'List', 'Journal Entry', {
+ "Journal Entry Account.reference_name": frm.doc.name
+ }
);
},
freeze: true,
@@ -309,11 +338,18 @@
}
};
-
-let render_employee_attendance = function(frm, data) {
+let render_employee_attendance = function (frm, data) {
frm.fields_dict.attendance_detail_html.html(
frappe.render_template('employees_to_mark_attendance', {
data: data
})
);
-}
+};
+
+frappe.ui.form.on('Payroll Employee Detail', {
+ employee: function(frm) {
+ if (!frm.doc.payroll_frequency) {
+ frappe.throw(__("Please set a Payroll Frequency"));
+ }
+ }
+});
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
index 7a48dd1..0444134 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.json
@@ -129,8 +129,7 @@
"fieldname": "employees",
"fieldtype": "Table",
"label": "Employee Details",
- "options": "Payroll Employee Detail",
- "read_only": 1
+ "options": "Payroll Employee Detail"
},
{
"fieldname": "section_break_13",
@@ -290,7 +289,7 @@
"icon": "fa fa-cog",
"is_submittable": 1,
"links": [],
- "modified": "2020-10-23 13:00:33.753228",
+ "modified": "2020-12-17 15:13:17.766210",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Payroll Entry",
diff --git a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
index 67ee231..6bcd4e0 100644
--- a/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/payroll_entry.py
@@ -6,7 +6,7 @@
import frappe, erpnext
from frappe.model.document import Document
from dateutil.relativedelta import relativedelta
-from frappe.utils import cint, flt, nowdate, add_days, getdate, fmt_money, add_to_date, DATE_FORMAT, date_diff
+from frappe.utils import cint, flt, add_days, getdate, add_to_date, DATE_FORMAT, date_diff, comma_and
from frappe import _
from erpnext.accounts.utils import get_fiscal_year
from erpnext.hr.doctype.employee.employee import get_holiday_list_for_employee
@@ -19,16 +19,29 @@
# check if salary slips were manually submitted
entries = frappe.db.count("Salary Slip", {'payroll_entry': self.name, 'docstatus': 1}, ['name'])
if cint(entries) == len(self.employees):
- self.set_onload("submitted_ss", True)
+ self.set_onload("submitted_ss", True)
+
+ def validate(self):
+ self.number_of_employees = len(self.employees)
def on_submit(self):
self.create_salary_slips()
def before_submit(self):
+ self.validate_employee_details()
if self.validate_attendance:
if self.validate_employee_attendance():
frappe.throw(_("Cannot Submit, Employees left to mark attendance"))
+ def validate_employee_details(self):
+ emp_with_sal_slip = []
+ for employee_details in self.employees:
+ if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}):
+ emp_with_sal_slip.append(employee_details.employee)
+
+ if len(emp_with_sal_slip):
+ frappe.throw(_("Salary Slip already exists for {0} ").format(comma_and(emp_with_sal_slip)))
+
def on_cancel(self):
frappe.delete_doc("Salary Slip", frappe.db.sql_list("""select name from `tabSalary Slip`
where payroll_entry=%s """, (self.name)))
@@ -71,8 +84,17 @@
and t2.docstatus = 1
%s order by t2.from_date desc
""" % cond, {"sal_struct": tuple(sal_struct), "from_date": self.end_date, "payroll_payable_account": self.payroll_payable_account}, as_dict=True)
+
+ emp_list = self.remove_payrolled_employees(emp_list)
return emp_list
+ def remove_payrolled_employees(self, emp_list):
+ for employee_details in emp_list:
+ if frappe.db.exists("Salary Slip", {"employee": employee_details.employee, "start_date": self.start_date, "end_date": self.end_date, "docstatus": 1}):
+ emp_list.remove(employee_details)
+
+ return emp_list
+
def fill_employee_details(self):
self.set('employees', [])
employees = self.get_emp_list()
@@ -94,7 +116,7 @@
for d in employees:
self.append('employees', d)
- self.number_of_employees = len(employees)
+ self.number_of_employees = len(self.employees)
if self.validate_attendance:
return self.validate_employee_attendance()
@@ -126,8 +148,8 @@
"""
self.check_permission('write')
self.created = 1
- emp_list = [d.employee for d in self.get_emp_list()]
- if emp_list:
+ employees = [emp.employee for emp in self.employees]
+ if employees:
args = frappe._dict({
"salary_slip_based_on_timesheet": self.salary_slip_based_on_timesheet,
"payroll_frequency": self.payroll_frequency,
@@ -141,10 +163,10 @@
"exchange_rate": self.exchange_rate,
"currency": self.currency
})
- if len(emp_list) > 30:
- frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=emp_list, args=args)
+ if len(employees) > 30:
+ frappe.enqueue(create_salary_slips_for_employees, timeout=600, employees=employees, args=args)
else:
- create_salary_slips_for_employees(emp_list, args, publish_progress=False)
+ create_salary_slips_for_employees(employees, args, publish_progress=False)
# since this method is called via frm.call this doc needs to be updated manually
self.reload()
@@ -289,7 +311,9 @@
jv_name = journal_entry.name
self.update_salary_slip_status(jv_name = jv_name)
except Exception as e:
- frappe.msgprint(e)
+ if type(e) in (str, list, tuple):
+ frappe.msgprint(e)
+ raise
return jv_name
@@ -540,7 +564,7 @@
title = _("Creating Salary Slips..."))
else:
salary_slip_name = frappe.db.sql(
- '''SELECT
+ '''SELECT
name
FROM `tabSalary Slip`
WHERE company=%s
diff --git a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
index 54106c8..e098ec7 100644
--- a/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
+++ b/erpnext/payroll/doctype/payroll_entry/test_payroll_entry.py
@@ -22,7 +22,7 @@
frappe.db.sql("delete from `tab%s`" % dt)
make_earning_salary_component(setup=True, company_list=["_Test Company"])
- make_deduction_salary_component(setup=True, company_list=["_Test Company"])
+ make_deduction_salary_component(setup=True, test_tax=False, company_list=["_Test Company"])
frappe.db.set_value("Payroll Settings", None, "email_salary_slip_to_employee", 0)
@@ -107,9 +107,9 @@
frappe.db.get_value("Company", "_Test Company", "default_payroll_payable_account") != "_Test Payroll Payable - _TC":
frappe.db.set_value("Company", "_Test Company", "default_payroll_payable_account",
"_Test Payroll Payable - _TC")
-
- make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency"))
- make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=frappe.db.get_value("Company", "_Test Company", "default_currency"))
+ currency=frappe.db.get_value("Company", "_Test Company", "default_currency")
+ make_salary_structure("_Test Salary Structure 1", "Monthly", employee1, company="_Test Company", currency=currency, test_tax=False)
+ make_salary_structure("_Test Salary Structure 2", "Monthly", employee2, company="_Test Company", currency=currency, test_tax=False)
dates = get_start_end_dates('Monthly', nowdate())
if not frappe.db.get_value("Salary Slip", {"start_date": dates.start_date, "end_date": dates.end_date}):
diff --git a/erpnext/payroll/doctype/payroll_period/payroll_period.py b/erpnext/payroll/doctype/payroll_period/payroll_period.py
index d7893d0..ef3a6cc 100644
--- a/erpnext/payroll/doctype/payroll_period/payroll_period.py
+++ b/erpnext/payroll/doctype/payroll_period/payroll_period.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt
+from frappe.utils import date_diff, getdate, formatdate, cint, month_diff, flt, add_months
from frappe.model.document import Document
from erpnext.hr.utils import get_holidays_for_employee
@@ -41,7 +41,7 @@
if overlap_doc:
msg = _("A {0} exists between {1} and {2} (").format(self.doctype,
formatdate(self.start_date), formatdate(self.end_date)) \
- + """ <b><a href="#Form/{0}/{1}">{1}</a></b>""".format(self.doctype, overlap_doc[0].name) \
+ + """ <b><a href="/app/Form/{0}/{1}">{1}</a></b>""".format(self.doctype, overlap_doc[0].name) \
+ _(") for {0}").format(self.company)
frappe.throw(msg)
@@ -88,6 +88,8 @@
period_start = joining_date
if relieving_date and getdate(relieving_date) < getdate(period_end):
period_end = relieving_date
+ if month_diff(period_end, start_date) > 1:
+ start_date = add_months(start_date, - (month_diff(period_end, start_date)+1))
total_sub_periods, remaining_sub_periods = 0.0, 0.0
diff --git a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js
index 6fe8cca..f8bb40a 100644
--- a/erpnext/payroll/doctype/retention_bonus/retention_bonus.js
+++ b/erpnext/payroll/doctype/retention_bonus/retention_bonus.js
@@ -4,9 +4,13 @@
frappe.ui.form.on('Retention Bonus', {
setup: function(frm) {
frm.set_query("employee", function() {
+ if (!frm.doc.company) {
+ frappe.msgprint(__("Please Select Company First"));
+ }
return {
filters: {
- "status": "Active"
+ "status": "Active",
+ "company": frm.doc.company
}
};
});
diff --git a/erpnext/payroll/doctype/salary_detail/salary_detail.json b/erpnext/payroll/doctype/salary_detail/salary_detail.json
index 5c1eb61..393f647 100644
--- a/erpnext/payroll/doctype/salary_detail/salary_detail.json
+++ b/erpnext/payroll/doctype/salary_detail/salary_detail.json
@@ -9,6 +9,7 @@
"abbr",
"column_break_3",
"amount",
+ "year_to_date",
"section_break_5",
"additional_salary",
"statistical_component",
@@ -226,11 +227,19 @@
{
"fieldname": "column_break_24",
"fieldtype": "Column Break"
+ },
+ {
+ "description": "Total salary booked against this component for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.",
+ "fieldname": "year_to_date",
+ "fieldtype": "Currency",
+ "label": "Year To Date",
+ "options": "currency",
+ "read_only": 1
}
],
"istable": 1,
"links": [],
- "modified": "2020-11-25 13:12:41.081106",
+ "modified": "2021-01-14 13:39:15.847158",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Detail",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.js b/erpnext/payroll/doctype/salary_slip/salary_slip.js
index f7e22c6..7460c75 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.js
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.js
@@ -116,7 +116,7 @@
},
exchange_rate: function(frm) {
- calculate_totals(frm);
+ set_totals(frm);
},
hide_loan_section: function(frm) {
@@ -125,24 +125,24 @@
change_form_labels: function(frm, company_currency) {
frm.set_currency_labels(["base_hour_rate", "base_gross_pay", "base_total_deduction",
- "base_net_pay", "base_rounded_total", "base_total_in_words"],
+ "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"],
company_currency);
- frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words"],
+ frm.set_currency_labels(["hour_rate", "gross_pay", "total_deduction", "net_pay", "rounded_total", "total_in_words", "year_to_date", "month_to_date"],
frm.doc.currency);
// toggle fields
frm.toggle_display(["exchange_rate", "base_hour_rate", "base_gross_pay", "base_total_deduction",
- "base_net_pay", "base_rounded_total", "base_total_in_words"],
+ "base_net_pay", "base_rounded_total", "base_total_in_words", "base_year_to_date", "base_month_to_date"],
frm.doc.currency != company_currency);
},
change_grid_labels: function(frm) {
- frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit",
- "tax_on_additional_salary"], frm.doc.currency, "earnings");
+ let fields = ["amount", "year_to_date", "default_amount", "additional_amount", "tax_on_flexible_benefit",
+ "tax_on_additional_salary"];
- frm.set_currency_labels(["amount", "default_amount", "additional_amount", "tax_on_flexible_benefit",
- "tax_on_additional_salary"], frm.doc.currency, "deductions");
+ frm.set_currency_labels(fields, frm.doc.currency, "earnings");
+ frm.set_currency_labels(fields, frm.doc.currency, "deductions");
},
refresh: function(frm) {
@@ -151,7 +151,6 @@
var salary_detail_fields = ["formula", "abbr", "statistical_component", "variable_based_on_taxable_salary"];
frm.fields_dict['earnings'].grid.set_column_disp(salary_detail_fields, false);
frm.fields_dict['deductions'].grid.set_column_disp(salary_detail_fields, false);
- calculate_totals(frm);
frm.trigger("set_dynamic_labels");
},
@@ -206,36 +205,38 @@
frappe.ui.form.on('Salary Slip Timesheet', {
time_sheet: function(frm) {
- calculate_totals(frm);
+ set_totals(frm);
},
timesheets_remove: function(frm) {
- calculate_totals(frm);
+ set_totals(frm);
}
});
-var calculate_totals = function(frm) {
- if (frm.doc.earnings || frm.doc.deductions) {
- frappe.call({
- method: "set_totals",
- doc: frm.doc,
- callback: function() {
- frm.refresh_fields();
- }
- });
+var set_totals = function(frm) {
+ if (frm.doc.docstatus === 0) {
+ if (frm.doc.earnings || frm.doc.deductions) {
+ frappe.call({
+ method: "set_totals",
+ doc: frm.doc,
+ callback: function() {
+ frm.refresh_fields();
+ }
+ });
+ }
}
};
frappe.ui.form.on('Salary Detail', {
amount: function(frm) {
- calculate_totals(frm);
+ set_totals(frm);
},
earnings_remove: function(frm) {
- calculate_totals(frm);
+ set_totals(frm);
},
deductions_remove: function(frm) {
- calculate_totals(frm);
+ set_totals(frm);
},
salary_component: function(frm, cdt, cdn) {
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.json b/erpnext/payroll/doctype/salary_slip/salary_slip.json
index 386618c..9f9691b 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.json
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.json
@@ -69,9 +69,13 @@
"net_pay_info",
"net_pay",
"base_net_pay",
+ "year_to_date",
+ "base_year_to_date",
"column_break_53",
"rounded_total",
"base_rounded_total",
+ "month_to_date",
+ "base_month_to_date",
"section_break_55",
"total_in_words",
"column_break_69",
@@ -578,13 +582,43 @@
{
"fieldname": "column_break_69",
"fieldtype": "Column Break"
+ },
+ {
+ "description": "Total salary booked for this employee from the beginning of the year (payroll period or fiscal year) up to the current salary slip's end date.",
+ "fieldname": "year_to_date",
+ "fieldtype": "Currency",
+ "label": "Year To Date",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "description": "Total salary booked for this employee from the beginning of the month up to the current salary slip's end date.",
+ "fieldname": "month_to_date",
+ "fieldtype": "Currency",
+ "label": "Month To Date",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_year_to_date",
+ "fieldtype": "Currency",
+ "label": "Year To Date(Company Currency)",
+ "options": "Company:company:default_currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "base_month_to_date",
+ "fieldtype": "Currency",
+ "label": "Month To Date(Company Currency)",
+ "options": "Company:company:default_currency",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 9,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-21 23:02:59.400249",
+ "modified": "2021-01-14 13:37:38.180920",
"modified_by": "Administrator",
"module": "Payroll",
"name": "Salary Slip",
diff --git a/erpnext/payroll/doctype/salary_slip/salary_slip.py b/erpnext/payroll/doctype/salary_slip/salary_slip.py
index 20365b1..60aff02 100644
--- a/erpnext/payroll/doctype/salary_slip/salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/salary_slip.py
@@ -5,7 +5,7 @@
import frappe, erpnext
import datetime, math
-from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate
+from frappe.utils import add_days, cint, cstr, flt, getdate, rounded, date_diff, money_in_words, formatdate, get_first_day
from frappe.model.naming import make_autoname
from frappe import msgprint, _
@@ -18,6 +18,7 @@
from erpnext.payroll.doctype.employee_benefit_application.employee_benefit_application import get_benefit_component_amount
from erpnext.payroll.doctype.employee_benefit_claim.employee_benefit_claim import get_benefit_claim_amount, get_last_payroll_period_benefits
from erpnext.loan_management.doctype.loan_repayment.loan_repayment import calculate_amounts, create_repayment_entry
+from erpnext.accounts.utils import get_fiscal_year
class SalarySlip(TransactionBase):
def __init__(self, *args, **kwargs):
@@ -49,6 +50,9 @@
self.get_working_days_details(lwp = self.leave_without_pay)
self.calculate_net_pay()
+ self.compute_year_to_date()
+ self.compute_month_to_date()
+ self.compute_component_wise_year_to_date()
if frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet"):
max_working_hours = frappe.db.get_single_value("Payroll Settings", "max_working_hours_against_timesheet")
@@ -140,8 +144,8 @@
self.salary_slip_based_on_timesheet = self._salary_structure_doc.salary_slip_based_on_timesheet or 0
self.set_time_sheet()
self.pull_sal_struct()
- payroll_based_on, consider_unmarked_attendance_as = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"])
- return [payroll_based_on, consider_unmarked_attendance_as]
+ ps = frappe.db.get_value("Payroll Settings", None, ["payroll_based_on","consider_unmarked_attendance_as"], as_dict=1)
+ return [ps.payroll_based_on, ps.consider_unmarked_attendance_as]
def set_time_sheet(self):
if self.salary_slip_based_on_timesheet:
@@ -421,16 +425,19 @@
def calculate_net_pay(self):
if self.salary_structure:
self.calculate_component_amounts("earnings")
- self.gross_pay = self.get_component_totals("earnings")
+ self.gross_pay = self.get_component_totals("earnings", depends_on_payment_days=1)
self.base_gross_pay = flt(flt(self.gross_pay) * flt(self.exchange_rate), self.precision('base_gross_pay'))
if self.salary_structure:
self.calculate_component_amounts("deductions")
- self.total_deduction = self.get_component_totals("deductions")
- self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
self.set_loan_repayment()
+ self.set_component_amounts_based_on_payment_days()
+ self.set_net_pay()
+ def set_net_pay(self):
+ self.total_deduction = self.get_component_totals("deductions")
+ self.base_total_deduction = flt(flt(self.total_deduction) * flt(self.exchange_rate), self.precision('base_total_deduction'))
self.net_pay = flt(self.gross_pay) - (flt(self.total_deduction) + flt(self.total_loan_repayment))
self.rounded_total = rounded(self.net_pay)
self.base_net_pay = flt(flt(self.net_pay) * flt(self.exchange_rate), self.precision('base_net_pay'))
@@ -452,8 +459,6 @@
else:
self.add_tax_components(payroll_period)
- self.set_component_amounts_based_on_payment_days(component_type)
-
def add_structure_components(self, component_type):
data = self.get_data_for_eval()
for struct_row in self._salary_structure_doc.get(component_type):
@@ -573,7 +578,7 @@
'default_amount': amount if not struct_row.get("is_additional_component") else 0,
'depends_on_payment_days' : struct_row.depends_on_payment_days,
'salary_component' : struct_row.salary_component,
- 'abbr' : struct_row.abbr,
+ 'abbr' : struct_row.abbr or struct_row.get("salary_component_abbr"),
'additional_salary': additional_salary,
'do_not_include_in_total' : struct_row.do_not_include_in_total,
'is_tax_applicable': struct_row.is_tax_applicable,
@@ -810,7 +815,7 @@
cint(row.depends_on_payment_days) and cint(self.total_working_days) and
(not self.salary_slip_based_on_timesheet or
getdate(self.start_date) < joining_date or
- getdate(self.end_date) > relieving_date
+ (relieving_date and getdate(self.end_date) > relieving_date)
)):
additional_amount = flt((flt(row.additional_amount) * flt(self.payment_days)
/ cint(self.total_working_days)), row.precision("additional_amount"))
@@ -943,15 +948,21 @@
struct_row['variable_based_on_taxable_salary'] = component.variable_based_on_taxable_salary
return struct_row
- def get_component_totals(self, component_type):
+ def get_component_totals(self, component_type, depends_on_payment_days=0):
+ joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
+ ["date_of_joining", "relieving_date"])
+
total = 0.0
for d in self.get(component_type):
if not d.do_not_include_in_total:
- d.amount = flt(d.amount, d.precision("amount"))
- total += d.amount
+ if depends_on_payment_days:
+ amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
+ else:
+ amount = flt(d.amount, d.precision("amount"))
+ total += amount
return total
- def set_component_amounts_based_on_payment_days(self, component_type):
+ def set_component_amounts_based_on_payment_days(self):
joining_date, relieving_date = frappe.get_cached_value("Employee", self.employee,
["date_of_joining", "relieving_date"])
@@ -961,8 +972,9 @@
if not joining_date:
frappe.throw(_("Please set the Date Of Joining for employee {0}").format(frappe.bold(self.employee_name)))
- for d in self.get(component_type):
- d.amount = self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0]
+ for component_type in ("earnings", "deductions"):
+ for d in self.get(component_type):
+ d.amount = flt(self.get_amount_based_on_payment_days(d, joining_date, relieving_date)[0], d.precision("amount"))
def set_loan_repayment(self):
self.total_loan_repayment = 0
@@ -1086,17 +1098,17 @@
self.calculate_net_pay()
def set_totals(self):
- self.gross_pay = 0
+ self.gross_pay = 0.0
if self.salary_slip_based_on_timesheet == 1:
self.calculate_total_for_salary_slip_based_on_timesheet()
else:
- self.total_deduction = 0
- if self.earnings:
+ self.total_deduction = 0.0
+ if hasattr(self, "earnings"):
for earning in self.earnings:
- self.gross_pay += flt(earning.amount)
- if self.deductions:
+ self.gross_pay += flt(earning.amount, earning.precision("amount"))
+ if hasattr(self, "deductions"):
for deduction in self.deductions:
- self.total_deduction += flt(deduction.amount)
+ self.total_deduction += flt(deduction.amount, deduction.precision("amount"))
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction) - flt(self.total_loan_repayment)
self.set_base_totals()
@@ -1125,6 +1137,82 @@
self.gross_pay += self.earnings[i].amount
self.net_pay = flt(self.gross_pay) - flt(self.total_deduction)
+ def compute_year_to_date(self):
+ year_to_date = 0
+ period_start_date, period_end_date = self.get_year_to_date_period()
+
+ salary_slip_sum = frappe.get_list('Salary Slip',
+ fields = ['sum(net_pay) as sum'],
+ filters = {'employee_name' : self.employee_name,
+ 'start_date' : ['>=', period_start_date],
+ 'end_date' : ['<', period_end_date],
+ 'name': ['!=', self.name],
+ 'docstatus': 1
+ })
+
+ year_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
+
+ year_to_date += self.net_pay
+ self.year_to_date = year_to_date
+
+ def compute_month_to_date(self):
+ month_to_date = 0
+ first_day_of_the_month = get_first_day(self.start_date)
+ salary_slip_sum = frappe.get_list('Salary Slip',
+ fields = ['sum(net_pay) as sum'],
+ filters = {'employee_name' : self.employee_name,
+ 'start_date' : ['>=', first_day_of_the_month],
+ 'end_date' : ['<', self.start_date],
+ 'name': ['!=', self.name],
+ 'docstatus': 1
+ })
+
+ month_to_date = flt(salary_slip_sum[0].sum) if salary_slip_sum else 0.0
+
+ month_to_date += self.net_pay
+ self.month_to_date = month_to_date
+
+ def compute_component_wise_year_to_date(self):
+ period_start_date, period_end_date = self.get_year_to_date_period()
+
+ for key in ('earnings', 'deductions'):
+ for component in self.get(key):
+ year_to_date = 0
+ component_sum = frappe.db.sql("""
+ SELECT sum(detail.amount) as sum
+ FROM `tabSalary Detail` as detail
+ INNER JOIN `tabSalary Slip` as salary_slip
+ ON detail.parent = salary_slip.name
+ WHERE
+ salary_slip.employee_name = %(employee_name)s
+ AND detail.salary_component = %(component)s
+ AND salary_slip.start_date >= %(period_start_date)s
+ AND salary_slip.end_date < %(period_end_date)s
+ AND salary_slip.name != %(docname)s
+ AND salary_slip.docstatus = 1""",
+ {'employee_name': self.employee_name, 'component': component.salary_component, 'period_start_date': period_start_date,
+ 'period_end_date': period_end_date, 'docname': self.name}
+ )
+
+ year_to_date = flt(component_sum[0][0]) if component_sum else 0.0
+ year_to_date += component.amount
+ component.year_to_date = year_to_date
+
+ def get_year_to_date_period(self):
+ payroll_period = get_payroll_period(self.start_date, self.end_date, self.company)
+
+ if payroll_period:
+ period_start_date = payroll_period.start_date
+ period_end_date = payroll_period.end_date
+ else:
+ # get dates based on fiscal year if no payroll period exists
+ fiscal_year = get_fiscal_year(date=self.start_date, company=self.company, as_dict=1)
+ period_start_date = fiscal_year.year_start_date
+ period_end_date = fiscal_year.year_end_date
+
+ return period_start_date, period_end_date
+
+
def unlink_ref_doc_from_salary_slip(ref_no):
linked_ss = frappe.db.sql_list("""select name from `tabSalary Slip`
where journal_entry=%s and docstatus < 2""", (ref_no))
@@ -1135,4 +1223,4 @@
def generate_password_for_pdf(policy_template, employee):
employee = frappe.get_doc("Employee", employee)
- return policy_template.format(**employee.as_dict())
+ return policy_template.format(**employee.as_dict())
\ No newline at end of file
diff --git a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
index 5daf1d4..f58a8e5 100644
--- a/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
+++ b/erpnext/payroll/doctype/salary_slip/test_salary_slip.py
@@ -9,7 +9,7 @@
import random
from erpnext.accounts.utils import get_fiscal_year
from frappe.utils.make_random import get_random
-from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day
+from frappe.utils import getdate, nowdate, add_days, add_months, flt, get_first_day, get_last_day, cstr
from erpnext.payroll.doctype.salary_structure.salary_structure import make_salary_slip
from erpnext.payroll.doctype.payroll_entry.payroll_entry import get_month_details
from erpnext.hr.doctype.employee.test_employee import make_employee
@@ -240,7 +240,11 @@
interest_income_account='Interest Income Account - _TC',
penalty_income_account='Penalty Income Account - _TC')
- make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR')
+ payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
+
+ make_salary_structure("Test Loan Repayment Salary Structure", "Monthly", employee=applicant, currency='INR',
+ payroll_period=payroll_period)
+
frappe.db.sql("""delete from `tabLoan""")
loan = create_loan(applicant, "Car Loan", 11000, "Repay Over Number of Periods", 20, posting_date=add_months(nowdate(), -1))
loan.repay_from_salary = 1
@@ -290,6 +294,65 @@
self.assertEqual(salary_slip.gross_pay, 78000)
self.assertEqual(salary_slip.base_gross_pay, 78000*70)
+ def test_year_to_date_computation(self):
+ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+
+ applicant = make_employee("test_ytd@salary.com", company="_Test Company")
+
+ payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
+
+ create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"),
+ company="_Test Company")
+
+ salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD",
+ "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period)
+
+ # clear salary slip for this employee
+ frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'")
+
+ create_salary_slips_for_payroll_period(applicant, salary_structure.name,
+ payroll_period, deduct_random=False)
+
+ salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name':
+ 'test_ytd@salary.com'}, order_by = 'posting_date')
+
+ year_to_date = 0
+ for slip in salary_slips:
+ year_to_date += flt(slip.net_pay)
+ self.assertEqual(slip.year_to_date, year_to_date)
+
+ def test_component_wise_year_to_date_computation(self):
+ from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure
+
+ applicant = make_employee("test_ytd@salary.com", company="_Test Company")
+
+ payroll_period = create_payroll_period(name="_Test Payroll Period 1", company="_Test Company")
+
+ create_tax_slab(payroll_period, allow_tax_exemption=True, currency="INR", effective_date=getdate("2019-04-01"),
+ company="_Test Company")
+
+ salary_structure = make_salary_structure("Monthly Salary Structure Test for Salary Slip YTD",
+ "Monthly", employee=applicant, company="_Test Company", currency="INR", payroll_period=payroll_period)
+
+ # clear salary slip for this employee
+ frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'")
+
+ create_salary_slips_for_payroll_period(applicant, salary_structure.name,
+ payroll_period, deduct_random=False, num=3)
+
+ salary_slips = frappe.get_all("Salary Slip", fields=["name"], filters={"employee_name":
+ "test_ytd@salary.com"}, order_by = "posting_date")
+
+ year_to_date = dict()
+ for slip in salary_slips:
+ doc = frappe.get_doc("Salary Slip", slip.name)
+ for entry in doc.get("earnings"):
+ if not year_to_date.get(entry.salary_component):
+ year_to_date[entry.salary_component] = 0
+
+ year_to_date[entry.salary_component] += entry.amount
+ self.assertEqual(year_to_date[entry.salary_component], entry.year_to_date)
+
def test_tax_for_payroll_period(self):
data = {}
# test the impact of tax exemption declaration, tax exemption proof submission
@@ -410,10 +473,7 @@
salary_structure = payroll_frequency + " Salary Structure Test for Salary Slip"
employee = frappe.db.get_value("Employee", {"user_id": user})
- if not frappe.db.exists('Salary Structure', salary_structure):
- salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee)
- else:
- salary_structure_doc = frappe.get_doc('Salary Structure', salary_structure)
+ salary_structure_doc = make_salary_structure(salary_structure, payroll_frequency, employee=employee)
salary_slip_name = frappe.db.get_value("Salary Slip", {"employee": frappe.db.get_value("Employee", {"user_id": user})})
if not salary_slip_name:
@@ -557,14 +617,6 @@
"amount": 200,
"exempted_from_income_tax": 1
- },
- {
- "salary_component": 'TDS',
- "abbr":'T',
- "type": "Deduction",
- "depends_on_payment_days": 0,
- "variable_based_on_taxable_salary": 1,
- "round_to_the_nearest_integer": 1
}
]
if not test_tax:
@@ -575,6 +627,15 @@
"type": "Deduction",
"round_to_the_nearest_integer": 1
})
+ else:
+ data.append({
+ "salary_component": 'TDS',
+ "abbr":'T',
+ "type": "Deduction",
+ "depends_on_payment_days": 0,
+ "variable_based_on_taxable_salary": 1,
+ "round_to_the_nearest_integer": 1
+ })
if setup or test_tax:
make_salary_component(data, test_tax, company_list)
@@ -631,8 +692,13 @@
}).submit()
return claim_date
-def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=erpnext.get_default_currency()):
- frappe.db.sql("""delete from `tabIncome Tax Slab`""")
+def create_tax_slab(payroll_period, effective_date = None, allow_tax_exemption = False, dont_submit = False, currency=None,
+ company=None):
+ if not currency:
+ currency = erpnext.get_default_currency()
+
+ if company:
+ currency = erpnext.get_company_currency(company)
slabs = [
{
@@ -652,31 +718,38 @@
}
]
- income_tax_slab = frappe.new_doc("Income Tax Slab")
- income_tax_slab.name = "Tax Slab: " + payroll_period.name
- income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2)
- income_tax_slab.currency = currency
+ income_tax_slab_name = frappe.db.get_value("Income Tax Slab", {"currency": currency})
+ if not income_tax_slab_name:
+ income_tax_slab = frappe.new_doc("Income Tax Slab")
+ income_tax_slab.name = "Tax Slab: " + payroll_period.name + " " + cstr(currency)
+ income_tax_slab.effective_from = effective_date or add_days(payroll_period.start_date, -2)
+ income_tax_slab.company = company or ''
+ income_tax_slab.currency = currency
- if allow_tax_exemption:
- income_tax_slab.allow_tax_exemption = 1
- income_tax_slab.standard_tax_exemption_amount = 50000
+ if allow_tax_exemption:
+ income_tax_slab.allow_tax_exemption = 1
+ income_tax_slab.standard_tax_exemption_amount = 50000
- for item in slabs:
- income_tax_slab.append("slabs", item)
+ for item in slabs:
+ income_tax_slab.append("slabs", item)
- income_tax_slab.append("other_taxes_and_charges", {
- "description": "cess",
- "percent": 4
- })
+ income_tax_slab.append("other_taxes_and_charges", {
+ "description": "cess",
+ "percent": 4
+ })
- income_tax_slab.save()
- if not dont_submit:
- income_tax_slab.submit()
+ income_tax_slab.save()
+ if not dont_submit:
+ income_tax_slab.submit()
-def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True):
+ return income_tax_slab.name
+ else:
+ return income_tax_slab_name
+
+def create_salary_slips_for_payroll_period(employee, salary_structure, payroll_period, deduct_random=True, num=12):
deducted_dates = []
i = 0
- while i < 12:
+ while i < num:
slip = frappe.get_doc({"doctype": "Salary Slip", "employee": employee,
"salary_structure": salary_structure, "frequency": "Monthly"})
if i == 0:
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.js b/erpnext/payroll/doctype/salary_structure/salary_structure.js
index 7daae49..1378bf0 100755
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.js
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.js
@@ -55,26 +55,26 @@
},
set_earning_deduction_component: function(frm) {
- if(!frm.doc.currency && !frm.doc.company) return;
+ if(!frm.doc.company) return;
frm.set_query("salary_component", "earnings", function() {
return {
- query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
- filters: {type: "earning", currency: frm.doc.currency, company: frm.doc.company}
+ filters: {type: "earning", company: frm.doc.company}
};
});
frm.set_query("salary_component", "deductions", function() {
return {
- query : "erpnext.payroll.doctype.salary_structure.salary_structure.get_earning_deduction_components",
- filters: {type: "deduction", currency: frm.doc.currency, company: frm.doc.company}
+ filters: {type: "deduction", company: frm.doc.company}
};
});
},
+ company: function(frm) {
+ frm.trigger('set_earning_deduction_component');
+ },
currency: function(frm) {
calculate_totals(frm.doc);
frm.trigger("set_dynamic_labels")
- frm.trigger('set_earning_deduction_component');
frm.refresh()
},
@@ -118,6 +118,7 @@
fields_read_only.forEach(function(field) {
frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1;
});
+ frm.trigger('set_earning_deduction_component');
},
assign_to_employees:function (frm) {
diff --git a/erpnext/payroll/doctype/salary_structure/salary_structure.py b/erpnext/payroll/doctype/salary_structure/salary_structure.py
index 877e41d..1712081 100644
--- a/erpnext/payroll/doctype/salary_structure/salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/salary_structure.py
@@ -207,17 +207,3 @@
return list(set([d.employee for d in employees]))
-@frappe.whitelist()
-@frappe.validate_and_sanitize_search_inputs
-def get_earning_deduction_components(doctype, txt, searchfield, start, page_len, filters):
- if len(filters) < 3:
- return {}
-
- return frappe.db.sql("""
- select t1.salary_component
- from `tabSalary Component` t1, `tabSalary Component Account` t2
- where t1.salary_component = t2.parent
- and t1.type = %s
- and t2.company = %s
- order by salary_component
- """, (filters['type'], filters['company']) )
diff --git a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
index abb6697..f2fb558 100644
--- a/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
+++ b/erpnext/payroll/doctype/salary_structure/test_salary_structure.py
@@ -114,7 +114,7 @@
self.assertEqual(sal_struct.currency, 'USD')
def make_salary_structure(salary_structure, payroll_frequency, employee=None, dont_submit=False, other_details=None,
- test_tax=False, company=None, currency=erpnext.get_default_currency()):
+ test_tax=False, company=None, currency=erpnext.get_default_currency(), payroll_period=None):
if test_tax:
frappe.db.sql("""delete from `tabSalary Structure` where name=%s""",(salary_structure))
@@ -141,16 +141,24 @@
if employee and not frappe.db.get_value("Salary Structure Assignment",
{'employee':employee, 'docstatus': 1}) and salary_structure_doc.docstatus==1:
- create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency)
+ create_salary_structure_assignment(employee, salary_structure, company=company, currency=currency,
+ payroll_period=payroll_period)
return salary_structure_doc
-def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency()):
+def create_salary_structure_assignment(employee, salary_structure, from_date=None, company=None, currency=erpnext.get_default_currency(),
+ payroll_period=None):
+
if frappe.db.exists("Salary Structure Assignment", {"employee": employee}):
frappe.db.sql("""delete from `tabSalary Structure Assignment` where employee=%s""",(employee))
- payroll_period = create_payroll_period()
- create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency)
+ if not payroll_period:
+ payroll_period = create_payroll_period()
+
+ income_tax_slab = frappe.db.get_value("Income Tax Slab", {"currency": currency})
+
+ if not income_tax_slab:
+ income_tax_slab = create_tax_slab(payroll_period, allow_tax_exemption=True, currency=currency)
salary_structure_assignment = frappe.new_doc("Salary Structure Assignment")
salary_structure_assignment.employee = employee
@@ -162,7 +170,7 @@
salary_structure_assignment.payroll_payable_account = get_payable_account(company)
salary_structure_assignment.company = company or erpnext.get_default_company()
salary_structure_assignment.save(ignore_permissions=True)
- salary_structure_assignment.income_tax_slab = "Tax Slab: _Test Payroll Period"
+ salary_structure_assignment.income_tax_slab = income_tax_slab
salary_structure_assignment.submit()
return salary_structure_assignment
diff --git a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
index dccb5df..a0c3013 100644
--- a/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
+++ b/erpnext/payroll/doctype/salary_structure_assignment/salary_structure_assignment.py
@@ -43,7 +43,7 @@
def set_payroll_payable_account(self):
if not self.payroll_payable_account:
- payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payable_account')
+ payroll_payable_account = frappe.db.get_value('Company', self.company, 'default_payroll_payable_account')
if not payroll_payable_account:
payroll_payable_account = frappe.db.get_value(
"Account", {
diff --git a/erpnext/config/__init__.py b/erpnext/payroll/print_format/__init__.py
similarity index 100%
rename from erpnext/config/__init__.py
rename to erpnext/payroll/print_format/__init__.py
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/payroll/print_format/salary_slip_with_year_to_date/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/payroll/print_format/salary_slip_with_year_to_date/__init__.py
diff --git a/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json b/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json
new file mode 100644
index 0000000..71ba37f
--- /dev/null
+++ b/erpnext/payroll/print_format/salary_slip_with_year_to_date/salary_slip_with_year_to_date.json
@@ -0,0 +1,25 @@
+{
+ "absolute_value": 0,
+ "align_labels_right": 0,
+ "creation": "2021-01-14 09:56:42.393623",
+ "custom_format": 0,
+ "default_print_language": "en",
+ "disabled": 0,
+ "doc_type": "Salary Slip",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \" <h3 style=\\\"text-align: right;\\\"><span style=\\\"line-height: 1.42857;\\\">{{doc.name}}</span></h3>\\n<div>\\n <hr style=\\\"text-align: center;\\\">\\n</div> \"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"employee\", \"print_hide\": 0, \"label\": \"Employee\"}, {\"fieldname\": \"company\", \"print_hide\": 0, \"label\": \"Company\"}, {\"fieldname\": \"employee_name\", \"print_hide\": 0, \"label\": \"Employee Name\"}, {\"fieldname\": \"department\", \"print_hide\": 0, \"label\": \"Department\"}, {\"fieldname\": \"designation\", \"print_hide\": 0, \"label\": \"Designation\"}, {\"fieldname\": \"branch\", \"print_hide\": 0, \"label\": \"Branch\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"start_date\", \"print_hide\": 0, \"label\": \"Start Date\"}, {\"fieldname\": \"end_date\", \"print_hide\": 0, \"label\": \"End Date\"}, {\"fieldname\": \"total_working_days\", \"print_hide\": 0, \"label\": \"Working Days\"}, {\"fieldname\": \"leave_without_pay\", \"print_hide\": 0, \"label\": \"Leave Without Pay\"}, {\"fieldname\": \"payment_days\", \"print_hide\": 0, \"label\": \"Payment Days\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"earnings\", \"print_hide\": 0, \"label\": \"Earnings\", \"visible_columns\": [{\"fieldname\": \"salary_component\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"year_to_date\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"deductions\", \"print_hide\": 0, \"label\": \"Deductions\", \"visible_columns\": [{\"fieldname\": \"salary_component\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"amount\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"year_to_date\", \"print_width\": \"\", \"print_hide\": 0}, {\"fieldname\": \"depends_on_payment_days\", \"print_width\": \"\", \"print_hide\": 0}]}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"gross_pay\", \"print_hide\": 0, \"label\": \"Gross Pay\"}, {\"fieldname\": \"total_deduction\", \"print_hide\": 0, \"label\": \"Total Deduction\"}, {\"fieldname\": \"net_pay\", \"print_hide\": 0, \"label\": \"Net Pay\"}, {\"fieldname\": \"rounded_total\", \"print_hide\": 0, \"label\": \"Rounded Total\"}, {\"fieldname\": \"total_in_words\", \"print_hide\": 0, \"label\": \"Total in words\"}, {\"fieldtype\": \"Section Break\", \"label\": \"net pay info\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldname\": \"year_to_date\", \"print_hide\": 0, \"label\": \"Year To Date\"}, {\"fieldname\": \"month_to_date\", \"print_hide\": 0, \"label\": \"Month To Date\"}]",
+ "idx": 0,
+ "line_breaks": 0,
+ "modified": "2021-01-14 10:03:45.283725",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Salary Slip with Year to Date",
+ "owner": "Administrator",
+ "print_format_builder": 0,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "Yes"
+}
\ No newline at end of file
diff --git a/erpnext/payroll/workspace/payroll/payroll.json b/erpnext/payroll/workspace/payroll/payroll.json
new file mode 100644
index 0000000..8149730
--- /dev/null
+++ b/erpnext/payroll/workspace/payroll/payroll.json
@@ -0,0 +1,333 @@
+{
+ "category": "Modules",
+ "charts": [
+ {
+ "chart_name": "Outgoing Salary",
+ "label": "Outgoing Salary"
+ }
+ ],
+ "creation": "2020-05-27 19:54:23.405607",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "money-coins-1",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Payroll",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Payroll",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Salary Component",
+ "link_to": "Salary Component",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Salary Structure",
+ "link_to": "Salary Structure",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Salary Structure Assignment",
+ "link_to": "Salary Structure Assignment",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Payroll Entry",
+ "link_to": "Payroll Entry",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Salary Slip",
+ "link_to": "Salary Slip",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Taxation",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Payroll Period",
+ "link_to": "Payroll Period",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Income Tax Slab",
+ "link_to": "Income Tax Slab",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Other Income",
+ "link_to": "Employee Other Income",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Tax Exemption Declaration",
+ "link_to": "Employee Tax Exemption Declaration",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Tax Exemption Proof Submission",
+ "link_to": "Employee Tax Exemption Proof Submission",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Tax Exemption Category",
+ "link_to": "Employee Tax Exemption Category",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Tax Exemption Sub Category",
+ "link_to": "Employee Tax Exemption Sub Category",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Compensations",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Additional Salary",
+ "link_to": "Additional Salary",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Retention Bonus",
+ "link_to": "Retention Bonus",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Incentive",
+ "link_to": "Employee Incentive",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Benefit Application",
+ "link_to": "Employee Benefit Application",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Benefit Claim",
+ "link_to": "Employee Benefit Claim",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Salary Slip",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Salary Register",
+ "link_to": "Salary Register",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Salary Slip",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Salary Payments Based On Payment Mode",
+ "link_to": "Salary Payments Based On Payment Mode",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Salary Slip",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Salary Payments via ECS",
+ "link_to": "Salary Payments via ECS",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Salary Slip",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Income Tax Deductions",
+ "link_to": "Income Tax Deductions",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Salary Slip",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Professional Tax Deductions",
+ "link_to": "Professional Tax Deductions",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Salary Slip",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Provident Fund Deductions",
+ "link_to": "Provident Fund Deductions",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Payroll Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Bank Remittance",
+ "link_to": "Bank Remittance",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:37.205628",
+ "modified_by": "Administrator",
+ "module": "Payroll",
+ "name": "Payroll",
+ "onboarding": "Payroll",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "label": "Salary Structure",
+ "link_to": "Salary Structure",
+ "type": "DocType"
+ },
+ {
+ "label": "Payroll Entry",
+ "link_to": "Payroll Entry",
+ "type": "DocType"
+ },
+ {
+ "color": "",
+ "format": "{} Pending",
+ "label": "Salary Slip",
+ "link_to": "Salary Slip",
+ "stats_filter": "{\"status\": \"Draft\"}",
+ "type": "DocType"
+ },
+ {
+ "label": "Income Tax Slab",
+ "link_to": "Income Tax Slab",
+ "type": "DocType"
+ },
+ {
+ "label": "Salary Register",
+ "link_to": "Salary Register",
+ "type": "Report"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "Payroll",
+ "type": "Dashboard"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/portal/doctype/products_settings/products_settings.py b/erpnext/portal/doctype/products_settings/products_settings.py
index ae7dc68..9a70892 100644
--- a/erpnext/portal/doctype/products_settings/products_settings.py
+++ b/erpnext/portal/doctype/products_settings/products_settings.py
@@ -17,6 +17,7 @@
self.validate_field_filters()
self.validate_attribute_filters()
+ frappe.clear_document_cache("Product Settings", "Product Settings")
def validate_field_filters(self):
if not (self.enable_field_filters and self.filter_fields): return
diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py
index 9ba4cdc..21fd7c2 100644
--- a/erpnext/portal/product_configurator/utils.py
+++ b/erpnext/portal/product_configurator/utils.py
@@ -1,6 +1,7 @@
import frappe
from frappe.utils import cint
from erpnext.portal.product_configurator.item_variants_cache import ItemVariantsCacheManager
+from erpnext.shopping_cart.product_info import get_product_info_for_website
def get_field_filter_data():
product_settings = get_product_settings()
@@ -356,10 +357,10 @@
results = frappe.db.sql('''
SELECT
- `tabItem`.`name`, `tabItem`.`item_name`,
+ `tabItem`.`name`, `tabItem`.`item_name`, `tabItem`.`item_code`,
`tabItem`.`website_image`, `tabItem`.`image`,
`tabItem`.`web_long_description`, `tabItem`.`description`,
- `tabItem`.`route`
+ `tabItem`.`route`, `tabItem`.`item_group`
FROM
`tabItem`
{left_join}
@@ -384,6 +385,9 @@
for r in results:
r.description = r.web_long_description or r.description
r.image = r.website_image or r.image
+ product_info = get_product_info_for_website(r.item_code, skip_quotation_creation=True).get('product_info')
+ if product_info:
+ r.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
return results
diff --git a/erpnext/projects/desk_page/projects/projects.json b/erpnext/projects/desk_page/projects/projects.json
deleted file mode 100644
index e24cf30..0000000
--- a/erpnext/projects/desk_page/projects/projects.json
+++ /dev/null
@@ -1,76 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Projects",
- "links": "[\n {\n \"description\": \"Project master.\",\n \"label\": \"Project\",\n \"name\": \"Project\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Project activity / task.\",\n \"label\": \"Task\",\n \"name\": \"Task\",\n \"onboard\": 1,\n \"route\": \"#List/Task\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Make project from a template.\",\n \"label\": \"Project Template\",\n \"name\": \"Project Template\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Define Project type.\",\n \"label\": \"Project Type\",\n \"name\": \"Project Type\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Project\"\n ],\n \"description\": \"Project Update.\",\n \"label\": \"Project Update\",\n \"name\": \"Project Update\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Time Tracking",
- "links": "[\n {\n \"description\": \"Timesheet for tasks.\",\n \"label\": \"Timesheet\",\n \"name\": \"Timesheet\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Types of activities for Time Logs\",\n \"label\": \"Activity Type\",\n \"name\": \"Activity Type\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Activity Type\"\n ],\n \"description\": \"Cost of various activities\",\n \"label\": \"Activity Cost\",\n \"name\": \"Activity Cost\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Timesheet\"\n ],\n \"doctype\": \"Timesheet\",\n \"is_query_report\": true,\n \"label\": \"Daily Timesheet Summary\",\n \"name\": \"Daily Timesheet Summary\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Project\"\n ],\n \"doctype\": \"Project\",\n \"is_query_report\": true,\n \"label\": \"Project wise Stock Tracking\",\n \"name\": \"Project wise Stock Tracking\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Project\"\n ],\n \"doctype\": \"Project\",\n \"is_query_report\": true,\n \"label\": \"Project Billing Summary\",\n \"name\": \"Project Billing Summary\",\n \"type\": \"report\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [
- {
- "chart_name": "Project Summary",
- "label": "Open Projects"
- }
- ],
- "creation": "2020-03-02 15:46:04.874669",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Projects",
- "modified": "2020-05-28 13:38:19.934937",
- "modified_by": "Administrator",
- "module": "Projects",
- "name": "Projects",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "color": "#cef6d1",
- "format": "{} Assigned",
- "label": "Task",
- "link_to": "Task",
- "stats_filter": "{\n \"_assign\": [\"like\", '%' + frappe.session.user + '%'],\n \"status\": \"Open\"\n}",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} Open",
- "label": "Project",
- "link_to": "Project",
- "stats_filter": "{\n \"status\": \"Open\"\n}",
- "type": "DocType"
- },
- {
- "label": "Timesheet",
- "link_to": "Timesheet",
- "type": "DocType"
- },
- {
- "label": "Project Billing Summary",
- "link_to": "Project Billing Summary",
- "type": "Report"
- },
- {
- "label": "Dashboard",
- "link_to": "Project",
- "type": "Dashboard"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js
index 3570a0f..077011a 100644
--- a/erpnext/projects/doctype/project/project.js
+++ b/erpnext/projects/doctype/project/project.js
@@ -75,24 +75,27 @@
frm.add_custom_button(__('Cancelled'), () => {
frm.events.set_status(frm, 'Cancelled');
}, __('Set Status'));
- }
- if (frappe.model.can_read("Task")) {
- frm.add_custom_button(__("Gantt Chart"), function () {
- frappe.route_options = {
- "project": frm.doc.name
- };
- frappe.set_route("List", "Task", "Gantt");
- });
- frm.add_custom_button(__("Kanban Board"), () => {
- frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
- project: frm.doc.project_name
- }).then(() => {
- frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
+ if (frappe.model.can_read("Task")) {
+ frm.add_custom_button(__("Gantt Chart"), function () {
+ frappe.route_options = {
+ "project": frm.doc.name
+ };
+ frappe.set_route("List", "Task", "Gantt");
});
- });
+
+ frm.add_custom_button(__("Kanban Board"), () => {
+ frappe.call('erpnext.projects.doctype.project.project.create_kanban_board_if_not_exists', {
+ project: frm.doc.project_name
+ }).then(() => {
+ frappe.set_route('List', 'Task', 'Kanban', frm.doc.project_name);
+ });
+ });
+ }
}
+
+
},
create_duplicate: function(frm) {
@@ -135,4 +138,4 @@
frappe.ui.form.make_quick_entry(doctype, null, null, new_doc);
});
-}
\ No newline at end of file
+}
diff --git a/erpnext/projects/doctype/project/project.json b/erpnext/projects/doctype/project/project.json
index f3cecd9..3cdfcb2 100644
--- a/erpnext/projects/doctype/project/project.json
+++ b/erpnext/projects/doctype/project/project.json
@@ -2,12 +2,13 @@
"actions": [],
"allow_import": 1,
"allow_rename": 1,
- "autoname": "field:project_name",
+ "autoname": "naming_series:",
"creation": "2013-03-07 11:55:07",
"doctype": "DocType",
"document_type": "Setup",
"engine": "InnoDB",
"field_order": [
+ "naming_series",
"project_name",
"status",
"project_type",
@@ -440,13 +441,24 @@
"fieldtype": "Text",
"label": "Message",
"mandatory_depends_on": "collect_progress"
+ },
+ {
+ "fieldname": "naming_series",
+ "fieldtype": "Select",
+ "label": "Series",
+ "no_copy": 1,
+ "options": "PROJ-.####",
+ "print_hide": 1,
+ "reqd": 1,
+ "set_only_once": 1
}
],
"icon": "fa fa-puzzle-piece",
"idx": 29,
+ "index_web_pages_for_search": 1,
"links": [],
"max_attachments": 4,
- "modified": "2020-04-08 22:11:14.552615",
+ "modified": "2020-09-02 11:54:01.223620",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project",
@@ -488,5 +500,6 @@
"sort_field": "modified",
"sort_order": "DESC",
"timeline_field": "customer",
+ "title_field": "project_name",
"track_seen": 1
}
diff --git a/erpnext/projects/doctype/project/project.py b/erpnext/projects/doctype/project/project.py
index 5bbd29c..8ba0b6c 100644
--- a/erpnext/projects/doctype/project/project.py
+++ b/erpnext/projects/doctype/project/project.py
@@ -13,6 +13,7 @@
from erpnext.hr.doctype.daily_work_summary.daily_work_summary import get_users_email
from erpnext.hr.doctype.holiday_list.holiday_list import is_holiday
from frappe.model.document import Document
+from erpnext.education.doctype.student_attendance.student_attendance import get_holiday_list
class Project(Document):
def get_feed(self):
@@ -26,7 +27,7 @@
self.update_costing()
- def before_print(self):
+ def before_print(self, settings=None):
self.onload()
@@ -54,17 +55,64 @@
self.project_type = template.project_type
# create tasks from template
+ project_tasks = []
+ tmp_task_details = []
for task in template.tasks:
- frappe.get_doc(dict(
- doctype = 'Task',
- subject = task.subject,
- project = self.name,
- status = 'Open',
- exp_start_date = add_days(self.expected_start_date, task.start),
- exp_end_date = add_days(self.expected_start_date, task.start + task.duration),
- description = task.description,
- task_weight = task.task_weight
- )).insert()
+ template_task_details = frappe.get_doc("Task", task.task)
+ tmp_task_details.append(template_task_details)
+ task = self.create_task_from_template(template_task_details)
+ project_tasks.append(task)
+ self.dependency_mapping(tmp_task_details, project_tasks)
+
+ def create_task_from_template(self, task_details):
+ return frappe.get_doc(dict(
+ doctype = 'Task',
+ subject = task_details.subject,
+ project = self.name,
+ status = 'Open',
+ exp_start_date = self.calculate_start_date(task_details),
+ exp_end_date = self.calculate_end_date(task_details),
+ description = task_details.description,
+ task_weight = task_details.task_weight,
+ type = task_details.type,
+ issue = task_details.issue,
+ is_group = task_details.is_group
+ )).insert()
+
+ def calculate_start_date(self, task_details):
+ self.start_date = add_days(self.expected_start_date, task_details.start)
+ self.start_date = update_if_holiday(self.holiday_list, self.start_date)
+ return self.start_date
+
+ def calculate_end_date(self, task_details):
+ self.end_date = add_days(self.start_date, task_details.duration)
+ return update_if_holiday(self.holiday_list, self.end_date)
+
+ def dependency_mapping(self, template_tasks, project_tasks):
+ for template_task in template_tasks:
+ project_task = list(filter(lambda x: x.subject == template_task.subject, project_tasks))[0]
+ project_task = frappe.get_doc("Task", project_task.name)
+ self.check_depends_on_value(template_task, project_task, project_tasks)
+ self.check_for_parent_tasks(template_task, project_task, project_tasks)
+
+ def check_depends_on_value(self, template_task, project_task, project_tasks):
+ if template_task.get("depends_on") and not project_task.get("depends_on"):
+ for child_task in template_task.get("depends_on"):
+ child_task_subject = frappe.db.get_value("Task", child_task.task, "subject")
+ corresponding_project_task = list(filter(lambda x: x.subject == child_task_subject, project_tasks))
+ if len(corresponding_project_task):
+ project_task.append("depends_on",{
+ "task": corresponding_project_task[0].name
+ })
+ project_task.save()
+
+ def check_for_parent_tasks(self, template_task, project_task, project_tasks):
+ if template_task.get("parent_task") and not project_task.get("parent_task"):
+ parent_task_subject = frappe.db.get_value("Task", template_task.get("parent_task"), "subject")
+ corresponding_project_task = list(filter(lambda x: x.subject == parent_task_subject, project_tasks))
+ if len(corresponding_project_task):
+ project_task.parent_task = corresponding_project_task[0].name
+ project_task.save()
def is_row_updated(self, row, existing_task_data, fields):
if self.get("__islocal") or not existing_task_data: return True
@@ -493,3 +541,9 @@
project.status = status
project.save()
+
+def update_if_holiday(holiday_list, date):
+ holiday_list = holiday_list or get_holiday_list()
+ while is_holiday(holiday_list, date):
+ date = add_days(date, 1)
+ return date
diff --git a/erpnext/projects/doctype/project/test_project.py b/erpnext/projects/doctype/project/test_project.py
index 0c4f6f1..6290538 100644
--- a/erpnext/projects/doctype/project/test_project.py
+++ b/erpnext/projects/doctype/project/test_project.py
@@ -7,60 +7,131 @@
test_records = frappe.get_test_records('Project')
test_ignore = ["Sales Order"]
-from erpnext.projects.doctype.project_template.test_project_template import get_project_template, make_project_template
-from erpnext.projects.doctype.project.project import set_project_status
-
-from frappe.utils import getdate
+from erpnext.projects.doctype.project_template.test_project_template import make_project_template
+from erpnext.projects.doctype.project.project import update_if_holiday
+from erpnext.projects.doctype.task.test_task import create_task
+from frappe.utils import getdate, nowdate, add_days
class TestProject(unittest.TestCase):
- def test_project_with_template(self):
- frappe.db.sql('delete from tabTask where project = "Test Project with Template"')
- frappe.delete_doc('Project', 'Test Project with Template')
+ def test_project_with_template_having_no_parent_and_depend_tasks(self):
+ project_name = "Test Project with Template - No Parent and Dependend Tasks"
+ frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+ frappe.delete_doc('Project', project_name)
- project = get_project('Test Project with Template')
+ task1 = task_exists("Test Template Task with No Parent and Dependency")
+ if not task1:
+ task1 = create_task(subject="Test Template Task with No Parent and Dependency", is_template=1, begin=5, duration=3)
- tasks = frappe.get_all('Task', '*', dict(project=project.name), order_by='creation asc')
+ template = make_project_template("Test Project Template - No Parent and Dependend Tasks", [task1])
+ project = get_project(project_name, template)
+ tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks'], dict(project=project.name), order_by='creation asc')
- task1 = tasks[0]
- self.assertEqual(task1.subject, 'Task 1')
- self.assertEqual(task1.description, 'Task 1 description')
- self.assertEqual(getdate(task1.exp_start_date), getdate('2019-01-01'))
- self.assertEqual(getdate(task1.exp_end_date), getdate('2019-01-04'))
+ self.assertEqual(tasks[0].subject, 'Test Template Task with No Parent and Dependency')
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 5, 3))
+ self.assertEqual(len(tasks), 1)
- self.assertEqual(len(tasks), 4)
- task4 = tasks[3]
- self.assertEqual(task4.subject, 'Task 4')
- self.assertEqual(getdate(task4.exp_end_date), getdate('2019-01-06'))
+ def test_project_template_having_parent_child_tasks(self):
+ project_name = "Test Project with Template - Tasks with Parent-Child Relation"
+ frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+ frappe.delete_doc('Project', project_name)
-def get_project(name):
- template = get_project_template()
+ task1 = task_exists("Test Template Task Parent")
+ if not task1:
+ task1 = create_task(subject="Test Template Task Parent", is_group=1, is_template=1, begin=1, duration=4)
+
+ task2 = task_exists("Test Template Task Child 1")
+ if not task2:
+ task2 = create_task(subject="Test Template Task Child 1", parent_task=task1.name, is_template=1, begin=1, duration=3)
+
+ task3 = task_exists("Test Template Task Child 2")
+ if not task3:
+ task3 = create_task(subject="Test Template Task Child 2", parent_task=task1.name, is_template=1, begin=2, duration=3)
+
+ template = make_project_template("Test Project Template - Tasks with Parent-Child Relation", [task1, task2, task3])
+ project = get_project(project_name, template)
+ tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name', 'parent_task'], dict(project=project.name), order_by='creation asc')
+
+ self.assertEqual(tasks[0].subject, 'Test Template Task Parent')
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 1, 4))
+
+ self.assertEqual(tasks[1].subject, 'Test Template Task Child 1')
+ self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 1, 3))
+ self.assertEqual(tasks[1].parent_task, tasks[0].name)
+
+ self.assertEqual(tasks[2].subject, 'Test Template Task Child 2')
+ self.assertEqual(getdate(tasks[2].exp_end_date), calculate_end_date(project, 2, 3))
+ self.assertEqual(tasks[2].parent_task, tasks[0].name)
+
+ self.assertEqual(len(tasks), 3)
+
+ def test_project_template_having_dependent_tasks(self):
+ project_name = "Test Project with Template - Dependent Tasks"
+ frappe.db.sql(""" delete from tabTask where project = %s """, project_name)
+ frappe.delete_doc('Project', project_name)
+
+ task1 = task_exists("Test Template Task for Dependency")
+ if not task1:
+ task1 = create_task(subject="Test Template Task for Dependency", is_template=1, begin=3, duration=1)
+
+ task2 = task_exists("Test Template Task with Dependency")
+ if not task2:
+ task2 = create_task(subject="Test Template Task with Dependency", depends_on=task1.name, is_template=1, begin=2, duration=2)
+
+ template = make_project_template("Test Project with Template - Dependent Tasks", [task1, task2])
+ project = get_project(project_name, template)
+ tasks = frappe.get_all('Task', ['subject','exp_end_date','depends_on_tasks', 'name'], dict(project=project.name), order_by='creation asc')
+
+ self.assertEqual(tasks[1].subject, 'Test Template Task with Dependency')
+ self.assertEqual(getdate(tasks[1].exp_end_date), calculate_end_date(project, 2, 2))
+ self.assertTrue(tasks[1].depends_on_tasks.find(tasks[0].name) >= 0 )
+
+ self.assertEqual(tasks[0].subject, 'Test Template Task for Dependency')
+ self.assertEqual(getdate(tasks[0].exp_end_date), calculate_end_date(project, 3, 1) )
+
+ self.assertEqual(len(tasks), 2)
+
+def get_project(name, template):
project = frappe.get_doc(dict(
doctype = 'Project',
project_name = name,
status = 'Open',
project_template = template.name,
- expected_start_date = '2019-01-01'
+ expected_start_date = nowdate()
)).insert()
return project
def make_project(args):
args = frappe._dict(args)
- if args.project_template_name:
- template = make_project_template(args.project_template_name)
- else:
- template = get_project_template()
+
+ if args.project_name and frappe.db.exists("Project", {"project_name": args.project_name}):
+ return frappe.get_doc("Project", {"project_name": args.project_name})
project = frappe.get_doc(dict(
doctype = 'Project',
project_name = args.project_name,
status = 'Open',
- project_template = template.name,
expected_start_date = args.start_date
))
- if not frappe.db.exists("Project", args.project_name):
- project.insert()
+ if args.project_template_name:
+ template = make_project_template(args.project_template_name)
+ project.project_template = template.name
- return project
\ No newline at end of file
+ project.insert()
+
+ return project
+
+def task_exists(subject):
+ result = frappe.db.get_list("Task", filters={"subject": subject},fields=["name"])
+ if not len(result):
+ return False
+ return frappe.get_doc("Task", result[0].name)
+
+def calculate_end_date(project, start, duration):
+ start = add_days(project.expected_start_date, start)
+ start = update_if_holiday(project.holiday_list, start)
+ end = add_days(start, duration)
+ end = update_if_holiday(project.holiday_list, end)
+ return getdate(end)
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project_template/project_template.js b/erpnext/projects/doctype/project_template/project_template.js
index d7a876d..3d3c15c 100644
--- a/erpnext/projects/doctype/project_template/project_template.js
+++ b/erpnext/projects/doctype/project_template/project_template.js
@@ -5,4 +5,23 @@
// refresh: function(frm) {
// }
+ setup: function (frm) {
+ frm.set_query("task", "tasks", function () {
+ return {
+ filters: {
+ "is_template": 1
+ }
+ };
+ });
+ }
+});
+
+frappe.ui.form.on('Project Template Task', {
+ task: function (frm, cdt, cdn) {
+ var row = locals[cdt][cdn];
+ frappe.db.get_value("Task", row.task, "subject", (value) => {
+ row.subject = value.subject;
+ refresh_field("tasks");
+ });
+ }
});
diff --git a/erpnext/projects/doctype/project_template/project_template.py b/erpnext/projects/doctype/project_template/project_template.py
index ac78135..aace402 100644
--- a/erpnext/projects/doctype/project_template/project_template.py
+++ b/erpnext/projects/doctype/project_template/project_template.py
@@ -3,8 +3,28 @@
# For license information, please see license.txt
from __future__ import unicode_literals
-# import frappe
+import frappe
from frappe.model.document import Document
+from frappe import _
+from frappe.utils import get_link_to_form
class ProjectTemplate(Document):
- pass
+
+ def validate(self):
+ self.validate_dependencies()
+
+ def validate_dependencies(self):
+ for task in self.tasks:
+ task_details = frappe.get_doc("Task", task.task)
+ if task_details.depends_on:
+ for dependency_task in task_details.depends_on:
+ if not self.check_dependent_task_presence(dependency_task.task):
+ task_details_format = get_link_to_form("Task",task_details.name)
+ dependency_task_format = get_link_to_form("Task", dependency_task.task)
+ frappe.throw(_("Task {0} depends on Task {1}. Please add Task {1} to the Tasks list.").format(frappe.bold(task_details_format), frappe.bold(dependency_task_format)))
+
+ def check_dependent_task_presence(self, task):
+ for task_details in self.tasks:
+ if task_details.task == task:
+ return True
+ return False
diff --git a/erpnext/projects/doctype/project_template/test_project_template.py b/erpnext/projects/doctype/project_template/test_project_template.py
index 2c5831a..95663cd 100644
--- a/erpnext/projects/doctype/project_template/test_project_template.py
+++ b/erpnext/projects/doctype/project_template/test_project_template.py
@@ -5,44 +5,25 @@
import frappe
import unittest
+from erpnext.projects.doctype.task.test_task import create_task
class TestProjectTemplate(unittest.TestCase):
pass
-def get_project_template():
- if not frappe.db.exists('Project Template', 'Test Project Template'):
- frappe.get_doc(dict(
- doctype = 'Project Template',
- name = 'Test Project Template',
- tasks = [
- dict(subject='Task 1', description='Task 1 description',
- start=0, duration=3),
- dict(subject='Task 2', description='Task 2 description',
- start=0, duration=2),
- dict(subject='Task 3', description='Task 3 description',
- start=2, duration=4),
- dict(subject='Task 4', description='Task 4 description',
- start=3, duration=2),
- ]
- )).insert()
-
- return frappe.get_doc('Project Template', 'Test Project Template')
-
def make_project_template(project_template_name, project_tasks=[]):
if not frappe.db.exists('Project Template', project_template_name):
- frappe.get_doc(dict(
- doctype = 'Project Template',
- name = project_template_name,
- tasks = project_tasks or [
- dict(subject='Task 1', description='Task 1 description',
- start=0, duration=3),
- dict(subject='Task 2', description='Task 2 description',
- start=0, duration=2),
- dict(subject='Task 3', description='Task 3 description',
- start=2, duration=4),
- dict(subject='Task 4', description='Task 4 description',
- start=3, duration=2),
+ project_tasks = project_tasks or [
+ create_task(subject="_Test Template Task 1", is_template=1, begin=0, duration=3),
+ create_task(subject="_Test Template Task 2", is_template=1, begin=0, duration=2),
]
- )).insert()
+ doc = frappe.get_doc(dict(
+ doctype = 'Project Template',
+ name = project_template_name
+ ))
+ for task in project_tasks:
+ doc.append("tasks",{
+ "task": task.name
+ })
+ doc.insert()
return frappe.get_doc('Project Template', project_template_name)
\ No newline at end of file
diff --git a/erpnext/projects/doctype/project_template_task/project_template_task.json b/erpnext/projects/doctype/project_template_task/project_template_task.json
index 8644d89..69530b1 100644
--- a/erpnext/projects/doctype/project_template_task/project_template_task.json
+++ b/erpnext/projects/doctype/project_template_task/project_template_task.json
@@ -1,203 +1,41 @@
{
- "allow_copy": 0,
- "allow_events_in_timeline": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
+ "actions": [],
"creation": "2019-02-18 17:24:41.830096",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
- "document_type": "",
"editable_grid": 1,
"engine": "InnoDB",
+ "field_order": [
+ "task",
+ "subject"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
+ "columns": 2,
+ "fieldname": "task",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Task",
+ "options": "Task",
+ "reqd": 1
+ },
+ {
+ "columns": 6,
"fieldname": "subject",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
+ "fieldtype": "Read Only",
"in_list_view": 1,
- "in_standard_filter": 0,
- "label": "Subject",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "start",
- "fieldtype": "Int",
- "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": "Begin On (Days)",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "duration",
- "fieldtype": "Int",
- "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": "Duration (Days)",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "task_weight",
- "fieldtype": "Float",
- "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": "Task Weight",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
- },
- {
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "description",
- "fieldtype": "Text Editor",
- "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": "Description",
- "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
+ "label": "Subject"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
"istable": 1,
- "max_attachments": 0,
- "modified": "2019-02-18 18:30:22.688966",
+ "links": [],
+ "modified": "2021-01-07 15:13:40.995071",
"modified_by": "Administrator",
"module": "Projects",
"name": "Project Template Task",
- "name_case": "",
"owner": "Administrator",
"permissions": [],
"quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
"sort_field": "modified",
"sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0,
- "track_views": 0
+ "track_changes": 1
}
\ No newline at end of file
diff --git a/erpnext/projects/doctype/task/task.json b/erpnext/projects/doctype/task/task.json
index 27f1a71..160cc58 100644
--- a/erpnext/projects/doctype/task/task.json
+++ b/erpnext/projects/doctype/task/task.json
@@ -12,6 +12,7 @@
"issue",
"type",
"is_group",
+ "is_template",
"column_break0",
"status",
"priority",
@@ -22,9 +23,11 @@
"sb_timeline",
"exp_start_date",
"expected_time",
+ "start",
"column_break_11",
"exp_end_date",
"progress",
+ "duration",
"is_milestone",
"sb_details",
"description",
@@ -112,7 +115,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "Open\nWorking\nPending Review\nOverdue\nCompleted\nCancelled"
+ "options": "Open\nWorking\nPending Review\nOverdue\nTemplate\nCompleted\nCancelled"
},
{
"fieldname": "priority",
@@ -360,6 +363,24 @@
"label": "Completed By",
"no_copy": 1,
"options": "User"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_template",
+ "fieldtype": "Check",
+ "label": "Is Template"
+ },
+ {
+ "depends_on": "is_template",
+ "fieldname": "start",
+ "fieldtype": "Int",
+ "label": "Begin On (Days)"
+ },
+ {
+ "depends_on": "is_template",
+ "fieldname": "duration",
+ "fieldtype": "Int",
+ "label": "Duration (Days)"
}
],
"icon": "fa fa-check",
@@ -367,7 +388,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 5,
- "modified": "2020-07-03 12:36:04.960457",
+ "modified": "2020-12-28 11:32:58.714991",
"modified_by": "Administrator",
"module": "Projects",
"name": "Task",
diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py
index fb84094..7116348 100755
--- a/erpnext/projects/doctype/task/task.py
+++ b/erpnext/projects/doctype/task/task.py
@@ -17,291 +17,319 @@
class EndDateCannotBeGreaterThanProjectEndDateError(frappe.ValidationError): pass
class Task(NestedSet):
- nsm_parent_field = 'parent_task'
+ nsm_parent_field = 'parent_task'
- def get_feed(self):
- return '{0}: {1}'.format(_(self.status), self.subject)
+ def get_feed(self):
+ return '{0}: {1}'.format(_(self.status), self.subject)
- def get_customer_details(self):
- cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
- if cust:
- ret = {'customer_name': cust and cust[0][0] or ''}
- return ret
+ def get_customer_details(self):
+ cust = frappe.db.sql("select customer_name from `tabCustomer` where name=%s", self.customer)
+ if cust:
+ ret = {'customer_name': cust and cust[0][0] or ''}
+ return ret
- def validate(self):
- self.validate_dates()
- self.validate_parent_project_dates()
- self.validate_progress()
- self.validate_status()
- self.update_depends_on()
+ def validate(self):
+ self.validate_dates()
+ self.validate_parent_expected_end_date()
+ self.validate_parent_project_dates()
+ self.validate_progress()
+ self.validate_status()
+ self.update_depends_on()
+ self.validate_dependencies_for_template_task()
- def validate_dates(self):
- if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
- frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
- frappe.bold("Expected End Date")))
+ def validate_dates(self):
+ if self.exp_start_date and self.exp_end_date and getdate(self.exp_start_date) > getdate(self.exp_end_date):
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Expected Start Date"), \
+ frappe.bold("Expected End Date")))
- if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
- frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
- frappe.bold("Actual End Date")))
+ if self.act_start_date and self.act_end_date and getdate(self.act_start_date) > getdate(self.act_end_date):
+ frappe.throw(_("{0} can not be greater than {1}").format(frappe.bold("Actual Start Date"), \
+ frappe.bold("Actual End Date")))
- def validate_parent_project_dates(self):
- if not self.project or frappe.flags.in_test:
- return
+ def validate_parent_expected_end_date(self):
+ if self.parent_task:
+ parent_exp_end_date = frappe.db.get_value("Task", self.parent_task, "exp_end_date")
+ if parent_exp_end_date and getdate(self.get("exp_end_date")) > getdate(parent_exp_end_date):
+ frappe.throw(_("Expected End Date should be less than or equal to parent task's Expected End Date {0}.").format(getdate(parent_exp_end_date)))
- expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
+ def validate_parent_project_dates(self):
+ if not self.project or frappe.flags.in_test:
+ return
- if expected_end_date:
- validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
- validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
+ expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date")
- def validate_status(self):
- if self.status!=self.get_db_value("status") and self.status == "Completed":
- for d in self.depends_on:
- if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
- frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
+ if expected_end_date:
+ validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected")
+ validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual")
- close_all_assignments(self.doctype, self.name)
+ def validate_status(self):
+ if self.is_template and self.status != "Template":
+ self.status = "Template"
+ if self.status!=self.get_db_value("status") and self.status == "Completed":
+ for d in self.depends_on:
+ if frappe.db.get_value("Task", d.task, "status") not in ("Completed", "Cancelled"):
+ frappe.throw(_("Cannot complete task {0} as its dependant task {1} are not ccompleted / cancelled.").format(frappe.bold(self.name), frappe.bold(d.task)))
- def validate_progress(self):
- if flt(self.progress or 0) > 100:
- frappe.throw(_("Progress % for a task cannot be more than 100."))
+ close_all_assignments(self.doctype, self.name)
- if flt(self.progress) == 100:
- self.status = 'Completed'
+ def validate_progress(self):
+ if flt(self.progress or 0) > 100:
+ frappe.throw(_("Progress % for a task cannot be more than 100."))
- if self.status == 'Completed':
- self.progress = 100
+ if flt(self.progress) == 100:
+ self.status = 'Completed'
- def update_depends_on(self):
- depends_on_tasks = self.depends_on_tasks or ""
- for d in self.depends_on:
- if d.task and not d.task in depends_on_tasks:
- depends_on_tasks += d.task + ","
- self.depends_on_tasks = depends_on_tasks
+ if self.status == 'Completed':
+ self.progress = 100
- def update_nsm_model(self):
- frappe.utils.nestedset.update_nsm(self)
+ def validate_dependencies_for_template_task(self):
+ if self.is_template:
+ self.validate_parent_template_task()
+ self.validate_depends_on_tasks()
+
+ def validate_parent_template_task(self):
+ if self.parent_task:
+ if not frappe.db.get_value("Task", self.parent_task, "is_template"):
+ parent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(self.parent_task)
+ frappe.throw(_("Parent Task {0} is not a Template Task").format(parent_task_format))
+
+ def validate_depends_on_tasks(self):
+ if self.depends_on:
+ for task in self.depends_on:
+ if not frappe.db.get_value("Task", task.task, "is_template"):
+ dependent_task_format = """<a href="#Form/Task/{0}">{0}</a>""".format(task.task)
+ frappe.throw(_("Dependent Task {0} is not a Template Task").format(dependent_task_format))
- def on_update(self):
- self.update_nsm_model()
- self.check_recursion()
- self.reschedule_dependent_tasks()
- self.update_project()
- self.unassign_todo()
- self.populate_depends_on()
+ def update_depends_on(self):
+ depends_on_tasks = self.depends_on_tasks or ""
+ for d in self.depends_on:
+ if d.task and d.task not in depends_on_tasks:
+ depends_on_tasks += d.task + ","
+ self.depends_on_tasks = depends_on_tasks
- def unassign_todo(self):
- if self.status == "Completed":
- close_all_assignments(self.doctype, self.name)
- if self.status == "Cancelled":
- clear(self.doctype, self.name)
+ def update_nsm_model(self):
+ frappe.utils.nestedset.update_nsm(self)
- def update_total_expense_claim(self):
- self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
- where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
+ def on_update(self):
+ self.update_nsm_model()
+ self.check_recursion()
+ self.reschedule_dependent_tasks()
+ self.update_project()
+ self.unassign_todo()
+ self.populate_depends_on()
- def update_time_and_costing(self):
- tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
- sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
- sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
- ,self.name, as_dict=1)[0]
- if self.status == "Open":
- self.status = "Working"
- self.total_costing_amount= tl.total_costing_amount
- self.total_billing_amount= tl.total_billing_amount
- self.actual_time= tl.time
- self.act_start_date= tl.start_date
- self.act_end_date= tl.end_date
+ def unassign_todo(self):
+ if self.status == "Completed":
+ close_all_assignments(self.doctype, self.name)
+ if self.status == "Cancelled":
+ clear(self.doctype, self.name)
- def update_project(self):
- if self.project and not self.flags.from_project:
- frappe.get_cached_doc("Project", self.project).update_project()
+ def update_total_expense_claim(self):
+ self.total_expense_claim = frappe.db.sql("""select sum(total_sanctioned_amount) from `tabExpense Claim`
+ where project = %s and task = %s and docstatus=1""",(self.project, self.name))[0][0]
- def check_recursion(self):
- if self.flags.ignore_recursion_check: return
- check_list = [['task', 'parent'], ['parent', 'task']]
- for d in check_list:
- task_list, count = [self.name], 0
- while (len(task_list) > count ):
- tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
- (d[0], d[1], '%s'), cstr(task_list[count]))
- count = count + 1
- for b in tasks:
- if b[0] == self.name:
- frappe.throw(_("Circular Reference Error"), CircularReferenceError)
- if b[0]:
- task_list.append(b[0])
+ def update_time_and_costing(self):
+ tl = frappe.db.sql("""select min(from_time) as start_date, max(to_time) as end_date,
+ sum(billing_amount) as total_billing_amount, sum(costing_amount) as total_costing_amount,
+ sum(hours) as time from `tabTimesheet Detail` where task = %s and docstatus=1"""
+ ,self.name, as_dict=1)[0]
+ if self.status == "Open":
+ self.status = "Working"
+ self.total_costing_amount= tl.total_costing_amount
+ self.total_billing_amount= tl.total_billing_amount
+ self.actual_time= tl.time
+ self.act_start_date= tl.start_date
+ self.act_end_date= tl.end_date
- if count == 15:
- break
+ def update_project(self):
+ if self.project and not self.flags.from_project:
+ frappe.get_cached_doc("Project", self.project).update_project()
- def reschedule_dependent_tasks(self):
- end_date = self.exp_end_date or self.act_end_date
- if end_date:
- for task_name in frappe.db.sql("""
- select name from `tabTask` as parent
- where parent.project = %(project)s
- and parent.name in (
- select parent from `tabTask Depends On` as child
- where child.task = %(task)s and child.project = %(project)s)
- """, {'project': self.project, 'task':self.name }, as_dict=1):
- task = frappe.get_doc("Task", task_name.name)
- if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
- task_duration = date_diff(task.exp_end_date, task.exp_start_date)
- task.exp_start_date = add_days(end_date, 1)
- task.exp_end_date = add_days(task.exp_start_date, task_duration)
- task.flags.ignore_recursion_check = True
- task.save()
+ def check_recursion(self):
+ if self.flags.ignore_recursion_check: return
+ check_list = [['task', 'parent'], ['parent', 'task']]
+ for d in check_list:
+ task_list, count = [self.name], 0
+ while (len(task_list) > count ):
+ tasks = frappe.db.sql(" select %s from `tabTask Depends On` where %s = %s " %
+ (d[0], d[1], '%s'), cstr(task_list[count]))
+ count = count + 1
+ for b in tasks:
+ if b[0] == self.name:
+ frappe.throw(_("Circular Reference Error"), CircularReferenceError)
+ if b[0]:
+ task_list.append(b[0])
- def has_webform_permission(self):
- project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
- if project_user:
- return True
+ if count == 15:
+ break
- def populate_depends_on(self):
- if self.parent_task:
- parent = frappe.get_doc('Task', self.parent_task)
- if not self.name in [row.task for row in parent.depends_on]:
- parent.append("depends_on", {
- "doctype": "Task Depends On",
- "task": self.name,
- "subject": self.subject
- })
- parent.save()
+ def reschedule_dependent_tasks(self):
+ end_date = self.exp_end_date or self.act_end_date
+ if end_date:
+ for task_name in frappe.db.sql("""
+ select name from `tabTask` as parent
+ where parent.project = %(project)s
+ and parent.name in (
+ select parent from `tabTask Depends On` as child
+ where child.task = %(task)s and child.project = %(project)s)
+ """, {'project': self.project, 'task':self.name }, as_dict=1):
+ task = frappe.get_doc("Task", task_name.name)
+ if task.exp_start_date and task.exp_end_date and task.exp_start_date < getdate(end_date) and task.status == "Open":
+ task_duration = date_diff(task.exp_end_date, task.exp_start_date)
+ task.exp_start_date = add_days(end_date, 1)
+ task.exp_end_date = add_days(task.exp_start_date, task_duration)
+ task.flags.ignore_recursion_check = True
+ task.save()
- def on_trash(self):
- if check_if_child_exists(self.name):
- throw(_("Child Task exists for this Task. You can not delete this Task."))
+ def has_webform_permission(self):
+ project_user = frappe.db.get_value("Project User", {"parent": self.project, "user":frappe.session.user} , "user")
+ if project_user:
+ return True
- self.update_nsm_model()
+ def populate_depends_on(self):
+ if self.parent_task:
+ parent = frappe.get_doc('Task', self.parent_task)
+ if self.name not in [row.task for row in parent.depends_on]:
+ parent.append("depends_on", {
+ "doctype": "Task Depends On",
+ "task": self.name,
+ "subject": self.subject
+ })
+ parent.save()
- def after_delete(self):
- self.update_project()
+ def on_trash(self):
+ if check_if_child_exists(self.name):
+ throw(_("Child Task exists for this Task. You can not delete this Task."))
- def update_status(self):
- if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
- from datetime import datetime
- if self.exp_end_date < datetime.now().date():
- self.db_set('status', 'Overdue', update_modified=False)
- self.update_project()
+ self.update_nsm_model()
+
+ def after_delete(self):
+ self.update_project()
+
+ def update_status(self):
+ if self.status not in ('Cancelled', 'Completed') and self.exp_end_date:
+ from datetime import datetime
+ if self.exp_end_date < datetime.now().date():
+ self.db_set('status', 'Overdue', update_modified=False)
+ self.update_project()
@frappe.whitelist()
def check_if_child_exists(name):
- child_tasks = frappe.get_all("Task", filters={"parent_task": name})
- child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
- return child_tasks
+ child_tasks = frappe.get_all("Task", filters={"parent_task": name})
+ child_tasks = [get_link_to_form("Task", task.name) for task in child_tasks]
+ return child_tasks
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_project(doctype, txt, searchfield, start, page_len, filters):
- from erpnext.controllers.queries import get_match_cond
- return frappe.db.sql(""" select name from `tabProject`
- where %(key)s like %(txt)s
- %(mcond)s
- order by name
- limit %(start)s, %(page_len)s""" % {
- 'key': searchfield,
- 'txt': frappe.db.escape('%' + txt + '%'),
- 'mcond':get_match_cond(doctype),
- 'start': start,
- 'page_len': page_len
- })
+ from erpnext.controllers.queries import get_match_cond
+ return frappe.db.sql(""" select name from `tabProject`
+ where %(key)s like %(txt)s
+ %(mcond)s
+ order by name
+ limit %(start)s, %(page_len)s""" % {
+ 'key': searchfield,
+ 'txt': frappe.db.escape('%' + txt + '%'),
+ 'mcond':get_match_cond(doctype),
+ 'start': start,
+ 'page_len': page_len
+ })
@frappe.whitelist()
def set_multiple_status(names, status):
- names = json.loads(names)
- for name in names:
- task = frappe.get_doc("Task", name)
- task.status = status
- task.save()
+ names = json.loads(names)
+ for name in names:
+ task = frappe.get_doc("Task", name)
+ task.status = status
+ task.save()
def set_tasks_as_overdue():
- tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
- for task in tasks:
- if task.status == "Pending Review":
- if getdate(task.review_date) > getdate(today()):
- continue
- frappe.get_doc("Task", task.name).update_status()
+ tasks = frappe.get_all("Task", filters={"status": ["not in", ["Cancelled", "Completed"]]}, fields=["name", "status", "review_date"])
+ for task in tasks:
+ if task.status == "Pending Review":
+ if getdate(task.review_date) > getdate(today()):
+ continue
+ frappe.get_doc("Task", task.name).update_status()
@frappe.whitelist()
def make_timesheet(source_name, target_doc=None, ignore_permissions=False):
- def set_missing_values(source, target):
- target.append("time_logs", {
- "hours": source.actual_time,
- "completed": source.status == "Completed",
- "project": source.project,
- "task": source.name
- })
+ def set_missing_values(source, target):
+ target.append("time_logs", {
+ "hours": source.actual_time,
+ "completed": source.status == "Completed",
+ "project": source.project,
+ "task": source.name
+ })
- doclist = get_mapped_doc("Task", source_name, {
- "Task": {
- "doctype": "Timesheet"
- }
- }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
+ doclist = get_mapped_doc("Task", source_name, {
+ "Task": {
+ "doctype": "Timesheet"
+ }
+ }, target_doc, postprocess=set_missing_values, ignore_permissions=ignore_permissions)
- return doclist
+ return doclist
@frappe.whitelist()
def get_children(doctype, parent, task=None, project=None, is_root=False):
- filters = [['docstatus', '<', '2']]
+ filters = [['docstatus', '<', '2']]
- if task:
- filters.append(['parent_task', '=', task])
- elif parent and not is_root:
- # via expand child
- filters.append(['parent_task', '=', parent])
- else:
- filters.append(['ifnull(`parent_task`, "")', '=', ''])
+ if task:
+ filters.append(['parent_task', '=', task])
+ elif parent and not is_root:
+ # via expand child
+ filters.append(['parent_task', '=', parent])
+ else:
+ filters.append(['ifnull(`parent_task`, "")', '=', ''])
- if project:
- filters.append(['project', '=', project])
+ if project:
+ filters.append(['project', '=', project])
- tasks = frappe.get_list(doctype, fields=[
- 'name as value',
- 'subject as title',
- 'is_group as expandable'
- ], filters=filters, order_by='name')
+ tasks = frappe.get_list(doctype, fields=[
+ 'name as value',
+ 'subject as title',
+ 'is_group as expandable'
+ ], filters=filters, order_by='name')
- # return tasks
- return tasks
+ # return tasks
+ return tasks
@frappe.whitelist()
def add_node():
- from frappe.desk.treeview import make_tree_args
- args = frappe.form_dict
- args.update({
- "name_field": "subject"
- })
- args = make_tree_args(**args)
+ from frappe.desk.treeview import make_tree_args
+ args = frappe.form_dict
+ args.update({
+ "name_field": "subject"
+ })
+ args = make_tree_args(**args)
- if args.parent_task == 'All Tasks' or args.parent_task == args.project:
- args.parent_task = None
+ if args.parent_task == 'All Tasks' or args.parent_task == args.project:
+ args.parent_task = None
- frappe.get_doc(args).insert()
+ frappe.get_doc(args).insert()
@frappe.whitelist()
def add_multiple_tasks(data, parent):
- data = json.loads(data)
- new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
- new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
+ data = json.loads(data)
+ new_doc = {'doctype': 'Task', 'parent_task': parent if parent!="All Tasks" else ""}
+ new_doc['project'] = frappe.db.get_value('Task', {"name": parent}, 'project') or ""
- for d in data:
- if not d.get("subject"): continue
- new_doc['subject'] = d.get("subject")
- new_task = frappe.get_doc(new_doc)
- new_task.insert()
+ for d in data:
+ if not d.get("subject"): continue
+ new_doc['subject'] = d.get("subject")
+ new_task = frappe.get_doc(new_doc)
+ new_task.insert()
def on_doctype_update():
- frappe.db.add_index("Task", ["lft", "rgt"])
+ frappe.db.add_index("Task", ["lft", "rgt"])
def validate_project_dates(project_end_date, task, task_start, task_end, actual_or_expected_date):
- if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
- frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
+ if task.get(task_start) and date_diff(project_end_date, getdate(task.get(task_start))) < 0:
+ frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date))
- if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
- frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
+ if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0:
+ frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date))
diff --git a/erpnext/projects/doctype/task/task_list.js b/erpnext/projects/doctype/task/task_list.js
index 941fe97..98d2bbc 100644
--- a/erpnext/projects/doctype/task/task_list.js
+++ b/erpnext/projects/doctype/task/task_list.js
@@ -20,13 +20,14 @@
"Pending Review": "orange",
"Working": "orange",
"Completed": "green",
- "Cancelled": "dark grey"
+ "Cancelled": "dark grey",
+ "Template": "blue"
}
return [__(doc.status), colors[doc.status], "status,=," + doc.status];
},
gantt_custom_popup_html: function(ganttobj, task) {
var html = `<h5><a style="text-decoration:underline"\
- href="#Form/Task/${ganttobj.id}""> ${ganttobj.name} </a></h5>`;
+ href="/app/task/${ganttobj.id}""> ${ganttobj.name} </a></h5>`;
if(task.project) html += `<p>Project: ${task.project}</p>`;
html += `<p>Progress: ${ganttobj.progress}</p>`;
diff --git a/erpnext/projects/doctype/task/test_task.py b/erpnext/projects/doctype/task/test_task.py
index 47a28fd..0fad5e8 100644
--- a/erpnext/projects/doctype/task/test_task.py
+++ b/erpnext/projects/doctype/task/test_task.py
@@ -30,14 +30,16 @@
})
def test_reschedule_dependent_task(self):
+ project = frappe.get_value("Project", {"project_name": "_Test Project"})
+
task1 = create_task("_Test Task 1", nowdate(), add_days(nowdate(), 10))
task2 = create_task("_Test Task 2", add_days(nowdate(), 11), add_days(nowdate(), 15), task1.name)
- task2.get("depends_on")[0].project = "_Test Project"
+ task2.get("depends_on")[0].project = project
task2.save()
task3 = create_task("_Test Task 3", add_days(nowdate(), 11), add_days(nowdate(), 15), task2.name)
- task3.get("depends_on")[0].project = "_Test Project"
+ task3.get("depends_on")[0].project = project
task3.save()
task1.update({
@@ -97,14 +99,19 @@
self.assertEqual(frappe.db.get_value("Task", task.name, "status"), "Overdue")
-def create_task(subject, start=None, end=None, depends_on=None, project=None, save=True):
+def create_task(subject, start=None, end=None, depends_on=None, project=None, parent_task=None, is_group=0, is_template=0, begin=0, duration=0, save=True):
if not frappe.db.exists("Task", subject):
task = frappe.new_doc('Task')
task.status = "Open"
task.subject = subject
task.exp_start_date = start or nowdate()
task.exp_end_date = end or nowdate()
- task.project = project or "_Test Project"
+ task.project = project or None if is_template else frappe.get_value("Project", {"project_name": "_Test Project"})
+ task.is_template = is_template
+ task.start = begin
+ task.duration = duration
+ task.is_group = is_group
+ task.parent_task = parent_task
if save:
task.save()
else:
@@ -116,5 +123,4 @@
})
if save:
task.save()
-
return task
diff --git a/erpnext/projects/doctype/timesheet/test_timesheet.py b/erpnext/projects/doctype/timesheet/test_timesheet.py
index a5ce44d..4cb3804 100644
--- a/erpnext/projects/doctype/timesheet/test_timesheet.py
+++ b/erpnext/projects/doctype/timesheet/test_timesheet.py
@@ -89,10 +89,11 @@
def test_timesheet_billing_based_on_project(self):
emp = make_employee("test_employee_6@salary.com")
+ project = frappe.get_value("Project", {"project_name": "_Test Project"})
- timesheet = make_timesheet(emp, simulate=True, billable=1, project = '_Test Project', company='_Test Company')
+ timesheet = make_timesheet(emp, simulate=True, billable=1, project=project, company='_Test Company')
sales_invoice = create_sales_invoice(do_not_save=True)
- sales_invoice.project = '_Test Project'
+ sales_invoice.project = project
sales_invoice.submit()
ts = frappe.get_doc('Timesheet', timesheet.name)
diff --git a/erpnext/projects/doctype/timesheet/timesheet.js b/erpnext/projects/doctype/timesheet/timesheet.js
index b068245..b123af5 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.js
+++ b/erpnext/projects/doctype/timesheet/timesheet.js
@@ -134,7 +134,7 @@
});
},
- project: function(frm) {
+ parent_project: function(frm) {
set_project_in_timelog(frm);
},
@@ -168,8 +168,8 @@
},
time_logs_add: function(frm, cdt, cdn) {
- if(frm.doc.project) {
- frappe.model.set_value(cdt, cdn, 'project', frm.doc.project);
+ if(frm.doc.parent_project) {
+ frappe.model.set_value(cdt, cdn, 'project', frm.doc.parent_project);
}
var $trigger_again = $('.form-grid').find('.grid-row').find('.btn-open-row');
@@ -308,7 +308,9 @@
};
function set_project_in_timelog(frm) {
- if(frm.doc.project){
- erpnext.utils.copy_value_in_all_rows(frm.doc, frm.doc.doctype, frm.doc.name, "time_logs", "project");
+ if(frm.doc.parent_project) {
+ $.each(frm.doc.time_logs || [], function(i, item) {
+ frappe.model.set_value(item.doctype, item.name, "project", frm.doc.parent_project);
+ });
}
}
\ No newline at end of file
diff --git a/erpnext/projects/doctype/timesheet/timesheet.json b/erpnext/projects/doctype/timesheet/timesheet.json
index 4c2edf4..b286821 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.json
+++ b/erpnext/projects/doctype/timesheet/timesheet.json
@@ -15,7 +15,7 @@
"column_break_3",
"salary_slip",
"status",
- "project",
+ "parent_project",
"employee_detail",
"employee",
"employee_name",
@@ -261,7 +261,7 @@
"read_only": 1
},
{
- "fieldname": "project",
+ "fieldname": "parent_project",
"fieldtype": "Link",
"label": "Project",
"options": "Project"
@@ -271,7 +271,7 @@
"idx": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-29 07:50:35.938231",
+ "modified": "2021-01-08 20:51:14.590080",
"modified_by": "Administrator",
"module": "Projects",
"name": "Timesheet",
diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py
index 9e807f7..ea81b3e 100644
--- a/erpnext/projects/doctype/timesheet/timesheet.py
+++ b/erpnext/projects/doctype/timesheet/timesheet.py
@@ -288,7 +288,7 @@
def make_salary_slip(source_name, target_doc=None):
target = frappe.new_doc("Salary Slip")
set_missing_values(source_name, target)
- target.run_method("get_emp_and_leave_details")
+ target.run_method("get_emp_and_working_day_details")
return target
diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json
new file mode 100644
index 0000000..dbbd7e1
--- /dev/null
+++ b/erpnext/projects/workspace/projects/projects.json
@@ -0,0 +1,193 @@
+{
+ "category": "Modules",
+ "charts": [
+ {
+ "chart_name": "Project Summary",
+ "label": "Open Projects"
+ }
+ ],
+ "creation": "2020-03-02 15:46:04.874669",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "project",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Projects",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Projects",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Project",
+ "link_to": "Project",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Task",
+ "link_to": "Task",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Project Template",
+ "link_to": "Project Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Project Type",
+ "link_to": "Project Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Project",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Project Update",
+ "link_to": "Project Update",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Time Tracking",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Timesheet",
+ "link_to": "Timesheet",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Activity Type",
+ "link_to": "Activity Type",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Activity Type",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Activity Cost",
+ "link_to": "Activity Cost",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Timesheet",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Daily Timesheet Summary",
+ "link_to": "Daily Timesheet Summary",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Project",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Project wise Stock Tracking",
+ "link_to": "Project wise Stock Tracking",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Project",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Project Billing Summary",
+ "link_to": "Project Billing Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:37.856224",
+ "modified_by": "Administrator",
+ "module": "Projects",
+ "name": "Projects",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "color": "Blue",
+ "format": "{} Assigned",
+ "label": "Task",
+ "link_to": "Task",
+ "stats_filter": "{\n \"_assign\": [\"like\", '%' + frappe.session.user + '%'],\n \"status\": \"Open\"\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Blue",
+ "format": "{} Open",
+ "label": "Project",
+ "link_to": "Project",
+ "stats_filter": "{\n \"status\": \"Open\"\n}",
+ "type": "DocType"
+ },
+ {
+ "label": "Timesheet",
+ "link_to": "Timesheet",
+ "type": "DocType"
+ },
+ {
+ "label": "Project Billing Summary",
+ "link_to": "Project Billing Summary",
+ "type": "Report"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "Project",
+ "type": "Dashboard"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/public/build.json b/erpnext/public/build.json
index 2f15cbc..7a3cb83 100644
--- a/erpnext/public/build.json
+++ b/erpnext/public/build.json
@@ -2,7 +2,8 @@
"css/erpnext.css": [
"public/less/erpnext.less",
"public/less/hub.less",
- "public/less/call_popup.less"
+ "public/scss/call_popup.scss",
+ "public/scss/point-of-sale.scss"
],
"css/marketplace.css": [
"public/less/hub.less"
@@ -12,7 +13,8 @@
"public/js/shopping_cart.js"
],
"css/erpnext-web.css": [
- "public/scss/website.scss"
+ "public/scss/website.scss",
+ "public/scss/shopping_cart.scss"
],
"js/marketplace.min.js": [
"public/js/hub/marketplace.js"
@@ -27,16 +29,6 @@
"public/js/payment/payments.js",
"public/js/controllers/taxes_and_totals.js",
"public/js/controllers/transaction.js",
- "public/js/pos/pos.html",
- "public/js/pos/pos_bill_item.html",
- "public/js/pos/pos_bill_item_new.html",
- "public/js/pos/pos_selected_item.html",
- "public/js/pos/pos_item.html",
- "public/js/pos/pos_tax_row.html",
- "public/js/pos/customer_toolbar.html",
- "public/js/pos/pos_invoice_list.html",
- "public/js/payment/pos_payment.html",
- "public/js/payment/payment_details.html",
"public/js/templates/item_selector.html",
"public/js/templates/employees_to_mark_attendance.html",
"public/js/utils/item_selector.js",
@@ -50,11 +42,29 @@
"public/js/hub/hub_factory.js",
"public/js/call_popup/call_popup.js",
"public/js/utils/dimension_tree_filter.js",
- "public/js/telephony.js"
+ "public/js/telephony.js",
+ "public/js/templates/call_link.html"
],
"js/item-dashboard.min.js": [
"stock/dashboard/item_dashboard.html",
"stock/dashboard/item_dashboard_list.html",
- "stock/dashboard/item_dashboard.js"
+ "stock/dashboard/item_dashboard.js",
+ "stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html",
+ "stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html"
+ ],
+ "js/point-of-sale.min.js": [
+ "selling/page/point_of_sale/pos_item_selector.js",
+ "selling/page/point_of_sale/pos_item_cart.js",
+ "selling/page/point_of_sale/pos_item_details.js",
+ "selling/page/point_of_sale/pos_number_pad.js",
+ "selling/page/point_of_sale/pos_payment.js",
+ "selling/page/point_of_sale/pos_past_order_list.js",
+ "selling/page/point_of_sale/pos_past_order_summary.js",
+ "selling/page/point_of_sale/pos_controller.js"
+ ],
+ "js/bank-reconciliation-tool.min.js": [
+ "public/js/bank_reconciliation_tool/data_table_manager.js",
+ "public/js/bank_reconciliation_tool/number_card.js",
+ "public/js/bank_reconciliation_tool/dialog_manager.js"
]
}
diff --git a/erpnext/public/css/pos.css b/erpnext/public/css/pos.css
deleted file mode 100644
index 47f5771..0000000
--- a/erpnext/public/css/pos.css
+++ /dev/null
@@ -1,217 +0,0 @@
-[data-route="point-of-sale"] .layout-main-section { border: none; font-size: 12px; }
-[data-route="point-of-sale"] .layout-main-section-wrapper { margin-bottom: 0; }
-[data-route="point-of-sale"] .pos-items-wrapper { max-height: calc(100vh - 210px); }
-:root { --border-color: #d1d8dd; --text-color: #8d99a6; --primary: #5e64ff; }
-[data-route="point-of-sale"] .flex { display: flex; }
-[data-route="point-of-sale"] .grid { display: grid; }
-[data-route="point-of-sale"] .absolute { position: absolute; }
-[data-route="point-of-sale"] .relative { position: relative; }
-[data-route="point-of-sale"] .abs-center { top: 50%; left: 50%; transform: translate(-50%, -50%); }
-[data-route="point-of-sale"] .inline { display: inline; }
-[data-route="point-of-sale"] .float-right { float: right; }
-[data-route="point-of-sale"] .grid-cols-1 { grid-template-columns: repeat(1, minmax(0, 1fr)); }
-[data-route="point-of-sale"] .grid-cols-2 { grid-template-columns: repeat(2, minmax(0, 1fr)); }
-[data-route="point-of-sale"] .grid-cols-3 { grid-template-columns: repeat(3, minmax(0, 1fr)); }
-[data-route="point-of-sale"] .grid-cols-4 { grid-template-columns: repeat(4, minmax(0, 1fr)); }
-[data-route="point-of-sale"] .grid-cols-5 { grid-template-columns: repeat(5, minmax(0, 1fr)); }
-[data-route="point-of-sale"] .grid-cols-10 { grid-template-columns: repeat(10, minmax(0, 1fr)); }
-[data-route="point-of-sale"] .gap-2 { grid-gap: 0.5rem; gap: 0.5rem; }
-[data-route="point-of-sale"] .gap-4 { grid-gap: 1rem; gap: 1rem; }
-[data-route="point-of-sale"] .gap-6 { grid-gap: 1.25rem; gap: 1.25rem; }
-[data-route="point-of-sale"] .gap-8 { grid-gap: 1.5rem; gap: 1.5rem; }
-[data-route="point-of-sale"] .row-gap-2 { grid-row-gap: 0.5rem; row-gap: 0.5rem; }
-[data-route="point-of-sale"] .col-gap-4 { grid-column-gap: 1rem; column-gap: 1rem; }
-[data-route="point-of-sale"] .col-span-2 { grid-column: span 2 / span 2; }
-[data-route="point-of-sale"] .col-span-3 { grid-column: span 3 / span 3; }
-[data-route="point-of-sale"] .col-span-4 { grid-column: span 4 / span 4; }
-[data-route="point-of-sale"] .col-span-6 { grid-column: span 6 / span 6; }
-[data-route="point-of-sale"] .col-span-10 { grid-column: span 10 / span 10; }
-[data-route="point-of-sale"] .row-span-2 { grid-row: span 2 / span 2; }
-[data-route="point-of-sale"] .grid-auto-row { grid-auto-rows: 5.5rem; }
-[data-route="point-of-sale"] .d-none { display: none; }
-[data-route="point-of-sale"] .flex-wrap { flex-wrap: wrap; }
-[data-route="point-of-sale"] .flex-row { flex-direction: row; }
-[data-route="point-of-sale"] .flex-col { flex-direction: column; }
-[data-route="point-of-sale"] .flex-row-rev { flex-direction: row-reverse; }
-[data-route="point-of-sale"] .flex-col-rev { flex-direction: column-reverse; }
-[data-route="point-of-sale"] .flex-1 { flex: 1 1 0%; }
-[data-route="point-of-sale"] .items-center { align-items: center; }
-[data-route="point-of-sale"] .items-end { align-items: flex-end; }
-[data-route="point-of-sale"] .f-grow-1 { flex-grow: 1; }
-[data-route="point-of-sale"] .f-grow-2 { flex-grow: 2; }
-[data-route="point-of-sale"] .f-grow-3 { flex-grow: 3; }
-[data-route="point-of-sale"] .f-grow-4 { flex-grow: 4; }
-[data-route="point-of-sale"] .f-shrink-0 { flex-shrink: 0; }
-[data-route="point-of-sale"] .f-shrink-1 { flex-shrink: 1; }
-[data-route="point-of-sale"] .f-shrink-2 { flex-shrink: 2; }
-[data-route="point-of-sale"] .f-shrink-3 { flex-shrink: 3; }
-[data-route="point-of-sale"] .shadow { box-shadow: 0 0px 3px 0 rgba(0, 0, 0, 0.2), 0 1px 2px 0 rgba(0, 0, 0, 0.06); }
-[data-route="point-of-sale"] .shadow-sm { box-shadow: 0 0.5px 3px 0 rgba(0, 0, 0, 0.125); }
-[data-route="point-of-sale"] .shadow-inner { box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.1); }
-[data-route="point-of-sale"] .rounded { border-radius: 0.3rem; }
-[data-route="point-of-sale"] .rounded-b { border-bottom-left-radius: 0.3rem; border-bottom-right-radius: 0.3rem; }
-[data-route="point-of-sale"] .p-8 { padding: 2rem; }
-[data-route="point-of-sale"] .p-16 { padding: 4rem; }
-[data-route="point-of-sale"] .p-32 { padding: 8rem; }
-[data-route="point-of-sale"] .p-6 { padding: 1.5rem; }
-[data-route="point-of-sale"] .p-4 { padding: 1rem; }
-[data-route="point-of-sale"] .p-3 { padding: 0.75rem; }
-[data-route="point-of-sale"] .p-2 { padding: 0.5rem; }
-[data-route="point-of-sale"] .m-8 { margin: 2rem; }
-[data-route="point-of-sale"] .p-1 { padding: 0.25rem; }
-[data-route="point-of-sale"] .pr-0 { padding-right: 0rem; }
-[data-route="point-of-sale"] .pl-0 { padding-left: 0rem; }
-[data-route="point-of-sale"] .pt-0 { padding-top: 0rem; }
-[data-route="point-of-sale"] .pb-0 { padding-bottom: 0rem; }
-[data-route="point-of-sale"] .mr-0 { margin-right: 0rem; }
-[data-route="point-of-sale"] .ml-0 { margin-left: 0rem; }
-[data-route="point-of-sale"] .mt-0 { margin-top: 0rem; }
-[data-route="point-of-sale"] .mb-0 { margin-bottom: 0rem; }
-[data-route="point-of-sale"] .pr-2 { padding-right: 0.5rem; }
-[data-route="point-of-sale"] .pl-2 { padding-left: 0.5rem; }
-[data-route="point-of-sale"] .pt-2 { padding-top: 0.5rem; }
-[data-route="point-of-sale"] .pb-2 { padding-bottom: 0.5rem; }
-[data-route="point-of-sale"] .pr-3 { padding-right: 0.75rem; }
-[data-route="point-of-sale"] .pl-3 { padding-left: 0.75rem; }
-[data-route="point-of-sale"] .pt-3 { padding-top: 0.75rem; }
-[data-route="point-of-sale"] .pb-3 { padding-bottom: 0.75rem; }
-[data-route="point-of-sale"] .pr-4 { padding-right: 1rem; }
-[data-route="point-of-sale"] .pl-4 { padding-left: 1rem; }
-[data-route="point-of-sale"] .pt-4 { padding-top: 1rem; }
-[data-route="point-of-sale"] .pb-4 { padding-bottom: 1rem; }
-[data-route="point-of-sale"] .mr-4 { margin-right: 1rem; }
-[data-route="point-of-sale"] .ml-4 { margin-left: 1rem; }
-[data-route="point-of-sale"] .mt-4 { margin-top: 1rem; }
-[data-route="point-of-sale"] .mb-4 { margin-bottom: 1rem; }
-[data-route="point-of-sale"] .mr-2 { margin-right: 0.5rem; }
-[data-route="point-of-sale"] .ml-2 { margin-left: 0.5rem; }
-[data-route="point-of-sale"] .mt-2 { margin-top: 0.5rem; }
-[data-route="point-of-sale"] .mb-2 { margin-bottom: 0.5rem; }
-[data-route="point-of-sale"] .mr-1 { margin-right: 0.25rem; }
-[data-route="point-of-sale"] .ml-1 { margin-left: 0.25rem; }
-[data-route="point-of-sale"] .mt-1 { margin-top: 0.25rem; }
-[data-route="point-of-sale"] .mb-1 { margin-bottom: 0.25rem; }
-[data-route="point-of-sale"] .mr-auto { margin-right: auto; }
-[data-route="point-of-sale"] .ml-auto { margin-left: auto; }
-[data-route="point-of-sale"] .mt-auto { margin-top: auto; }
-[data-route="point-of-sale"] .mb-auto { margin-bottom: auto; }
-[data-route="point-of-sale"] .pr-6 { padding-right: 1.5rem; }
-[data-route="point-of-sale"] .pl-6 { padding-left: 1.5rem; }
-[data-route="point-of-sale"] .pt-6 { padding-top: 1.5rem; }
-[data-route="point-of-sale"] .pb-6 { padding-bottom: 1.5rem; }
-[data-route="point-of-sale"] .mr-6 { margin-right: 1.5rem; }
-[data-route="point-of-sale"] .ml-6 { margin-left: 1.5rem; }
-[data-route="point-of-sale"] .mt-6 { margin-top: 1.5rem; }
-[data-route="point-of-sale"] .mb-6 { margin-bottom: 1.5rem; }
-[data-route="point-of-sale"] .mr-8 { margin-right: 2rem; }
-[data-route="point-of-sale"] .ml-8 { margin-left: 2rem; }
-[data-route="point-of-sale"] .mt-8 { margin-top: 2rem; }
-[data-route="point-of-sale"] .mb-8 { margin-bottom: 2rem; }
-[data-route="point-of-sale"] .pr-8 { padding-right: 2rem; }
-[data-route="point-of-sale"] .pl-8 { padding-left: 2rem; }
-[data-route="point-of-sale"] .pt-8 { padding-top: 2rem; }
-[data-route="point-of-sale"] .pb-8 { padding-bottom: 2rem; }
-[data-route="point-of-sale"] .pr-16 { padding-right: 4rem; }
-[data-route="point-of-sale"] .pl-16 { padding-left: 4rem; }
-[data-route="point-of-sale"] .pt-16 { padding-top: 4rem; }
-[data-route="point-of-sale"] .pb-16 { padding-bottom: 4rem; }
-[data-route="point-of-sale"] .w-full { width: 100%; }
-[data-route="point-of-sale"] .h-full { height: 100%; }
-[data-route="point-of-sale"] .w-quarter { width: 25%; }
-[data-route="point-of-sale"] .w-half { width: 50%; }
-[data-route="point-of-sale"] .w-66 { width: 66.66%; }
-[data-route="point-of-sale"] .w-33 { width: 33.33%; }
-[data-route="point-of-sale"] .w-60 { width: 60%; }
-[data-route="point-of-sale"] .w-40 { width: 40%; }
-[data-route="point-of-sale"] .w-fit { width: fit-content; }
-[data-route="point-of-sale"] .w-6 { width: 2rem; }
-[data-route="point-of-sale"] .h-6 { min-height: 2rem; height: 2rem; }
-[data-route="point-of-sale"] .w-8 { width: 2.5rem; }
-[data-route="point-of-sale"] .h-8 { min-height: 2.5rem; height: 2.5rem; }
-[data-route="point-of-sale"] .w-10 { width: 3rem; }
-[data-route="point-of-sale"] .h-10 { min-height:3rem; height: 3rem; }
-[data-route="point-of-sale"] .h-12 { min-height: 3.3rem; height: 3.3rem; }
-[data-route="point-of-sale"] .w-12 { width: 3.3rem; }
-[data-route="point-of-sale"] .h-14 { min-height: 4.2rem; height: 4.2rem; }
-[data-route="point-of-sale"] .h-16 { min-height: 4.6rem; height: 4.6rem; }
-[data-route="point-of-sale"] .h-18 { min-height: 5rem; height: 5rem; }
-[data-route="point-of-sale"] .w-18 { width: 5.4rem; }
-[data-route="point-of-sale"] .w-24 { width: 7.2rem; }
-[data-route="point-of-sale"] .w-26 { width: 8.4rem; }
-[data-route="point-of-sale"] .h-24 { min-height: 7.2rem; height: 7.2rem; }
-[data-route="point-of-sale"] .h-32 { min-height: 9.6rem; height: 9.6rem; }
-[data-route="point-of-sale"] .w-46 { width: 15rem; }
-[data-route="point-of-sale"] .h-46 { min-height:15rem; height: 15rem; }
-[data-route="point-of-sale"] .h-100 { height: 100vh; }
-[data-route="point-of-sale"] .mx-h-70 { max-height: 67rem; }
-[data-route="point-of-sale"] .border-grey-300 { border-color: #e2e8f0; }
-[data-route="point-of-sale"] .border-grey { border: 1px solid #d1d8dd; }
-[data-route="point-of-sale"] .border-white { border: 1px solid #fff; }
-[data-route="point-of-sale"] .border-b-grey { border-bottom: 1px solid #d1d8dd; }
-[data-route="point-of-sale"] .border-t-grey { border-top: 1px solid #d1d8dd; }
-[data-route="point-of-sale"] .border-r-grey { border-right: 1px solid #d1d8dd; }
-[data-route="point-of-sale"] .text-dark-grey { color: #5f5f5f; }
-[data-route="point-of-sale"] .text-grey { color: #8d99a6; }
-[data-route="point-of-sale"] .text-grey-100 { color: #d1d8dd; }
-[data-route="point-of-sale"] .text-grey-200 { color: #a0aec0; }
-[data-route="point-of-sale"] .bg-green-200 { background-color: #c6f6d5; }
-[data-route="point-of-sale"] .text-bold { font-weight: bold; }
-[data-route="point-of-sale"] .italic { font-style: italic; }
-[data-route="point-of-sale"] .font-weight-450 { font-weight: 450; }
-[data-route="point-of-sale"] .justify-around { justify-content: space-around; }
-[data-route="point-of-sale"] .justify-between { justify-content: space-between; }
-[data-route="point-of-sale"] .justify-center { justify-content: center; }
-[data-route="point-of-sale"] .justify-end { justify-content: flex-end; }
-[data-route="point-of-sale"] .bg-white { background-color: white; }
-[data-route="point-of-sale"] .bg-light-grey { background-color: #f0f4f7; }
-[data-route="point-of-sale"] .bg-grey-100 { background-color: #f7fafc; }
-[data-route="point-of-sale"] .bg-grey-200 { background-color: #edf2f7; }
-[data-route="point-of-sale"] .bg-grey { background-color: #f4f5f6; }
-[data-route="point-of-sale"] .text-center { text-align: center; }
-[data-route="point-of-sale"] .text-right { text-align: right; }
-[data-route="point-of-sale"] .text-sm { font-size: 1rem; }
-[data-route="point-of-sale"] .text-md-0 { font-size: 1.25rem; }
-[data-route="point-of-sale"] .text-md { font-size: 1.4rem; }
-[data-route="point-of-sale"] .text-lg { font-size: 1.6rem; }
-[data-route="point-of-sale"] .text-xl { font-size: 2.2rem; }
-[data-route="point-of-sale"] .text-2xl { font-size: 2.8rem; }
-[data-route="point-of-sale"] .text-2-5xl { font-size: 3rem; }
-[data-route="point-of-sale"] .text-3xl { font-size: 3.8rem; }
-[data-route="point-of-sale"] .text-6xl { font-size: 4.8rem; }
-[data-route="point-of-sale"] .line-through { text-decoration: line-through; }
-[data-route="point-of-sale"] .text-primary { color: #5e64ff; }
-[data-route="point-of-sale"] .text-white { color: #fff; }
-[data-route="point-of-sale"] .text-green-500 { color: #48bb78; }
-[data-route="point-of-sale"] .bg-primary { background-color: #5e64ff; }
-[data-route="point-of-sale"] .border-primary { border-color: #5e64ff; }
-[data-route="point-of-sale"] .text-danger { color: #e53e3e; }
-[data-route="point-of-sale"] .scroll-x { overflow-x: scroll;overflow-y: hidden; }
-[data-route="point-of-sale"] .scroll-y { overflow-y: scroll;overflow-x: hidden; }
-[data-route="point-of-sale"] .overflow-hidden { overflow: hidden; }
-[data-route="point-of-sale"] .whitespace-nowrap { white-space: nowrap; }
-[data-route="point-of-sale"] .sticky { position: sticky; top: -1px; }
-[data-route="point-of-sale"] .bg-white { background-color: #fff; }
-[data-route="point-of-sale"] .bg-selected { background-color: #fffdf4; }
-[data-route="point-of-sale"] .border-dashed { border-width:1px; border-style: dashed; }
-[data-route="point-of-sale"] .z-100 { z-index: 100; }
-
-[data-route="point-of-sale"] .frappe-control { margin: 0 !important; width: 100%; }
-[data-route="point-of-sale"] .form-control { font-size: 12px; }
-[data-route="point-of-sale"] .form-group { margin: 0 !important; }
-[data-route="point-of-sale"] .pointer { cursor: pointer; }
-[data-route="point-of-sale"] .no-select { user-select: none; }
-[data-route="point-of-sale"] .item-wrapper:hover { transform: scale(1.02, 1.02); }
-[data-route="point-of-sale"] .hover-underline:hover { text-decoration: underline; }
-[data-route="point-of-sale"] .item-wrapper { transition: scale 0.2s ease-in-out; }
-[data-route="point-of-sale"] .cart-items-section .cart-item-wrapper:not(:first-child) { border-top: none; }
-[data-route="point-of-sale"] .customer-transactions .invoice-wrapper:not(:first-child) { border-top: none; }
-
-[data-route="point-of-sale"] .payment-summary-wrapper:last-child { border-bottom: none; }
-[data-route="point-of-sale"] .item-summary-wrapper:last-child { border-bottom: none; }
-[data-route="point-of-sale"] .total-summary-wrapper:last-child { border-bottom: none; }
-[data-route="point-of-sale"] .invoices-container .invoice-wrapper:last-child { border-bottom: none; }
-[data-route="point-of-sale"] .new-btn { background-color: #5e64ff; color: white; border: none;}
-[data-route="point-of-sale"] .summary-btns:last-child { margin-right: 0px; }
-[data-route="point-of-sale"] ::-webkit-scrollbar { width: 1px }
-
-[data-route="point-of-sale"] .indicator.grey::before { background-color: #8d99a6; }
\ No newline at end of file
diff --git a/erpnext/public/images/erp-icon.svg b/erpnext/public/images/erp-icon.svg
deleted file mode 100644
index 6bec40c..0000000
--- a/erpnext/public/images/erp-icon.svg
+++ /dev/null
@@ -1,21 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="88px" height="88px" viewBox="0 0 88 88" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch -->
- <title>erpnext-logo</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <g id="erpnext-logo" transform="translate(-2.000000, -2.000000)" fill-rule="nonzero">
- <g id="g1422-7-2" transform="translate(0.025630, 0.428785)" fill="#5E64FF">
- <g id="g1418-4-6" transform="translate(0.268998, 0.867736)">
- <g id="g1416-4-9" transform="translate(0.749997, 0.000000)">
- <path d="M14.1845844,0.703479866 L75.0387175,0.703479866 C82.3677094,0.703479866 88.2679029,6.60367875 88.2679029,13.9326374 L88.2679029,74.7868158 C88.2679029,82.1157744 82.3677094,88.0159833 75.0387175,88.0159833 L14.1845844,88.0159833 C6.85569246,88.0159833 0.955398949,82.1157744 0.955398949,74.7868158 L0.955398949,13.9326374 C0.955398949,6.60367875 6.85569246,0.703479866 14.1845844,0.703479866 L14.1845844,0.703479866 Z" id="path1414-3-4"></path>
- </g>
- </g>
- </g>
- <g id="g1444-6-7" transform="translate(27.708247, 23.320960)" fill="#FFFFFF">
- <path d="M4.06942472,0.507006595 C3.79457554,0.507006595 3.52673783,0.534925429 3.26792241,0.587619847 C3.00908052,0.640314265 2.75926093,0.717948309 2.52171801,0.818098395 C2.40292009,0.868173438 2.28745592,0.924056085 2.17495509,0.985013441 C1.94997987,1.10692286 1.73828674,1.24983755 1.54244215,1.41134187 C0.661062132,2.13811791 0.100674618,3.23899362 0.100674618,4.4757567 L0.100674618,4.71760174 L0.100674618,39.9531653 L0.100674618,40.1945182 C0.100674618,42.3932057 1.87073716,44.1632683 4.06942472,44.1632683 L31.8263867,44.1632683 C34.0250742,44.1632683 35.7951368,42.3932057 35.7951368,40.1945182 L35.7951368,39.9531653 C35.7951368,37.7544777 34.0250742,35.9844152 31.8263867,35.9844152 L8.28000399,35.9844152 L8.28000399,26.0992376 L25.7874571,26.0992376 C27.9861447,26.0992376 29.7562072,24.3291751 29.7562072,22.1304875 L29.7562072,21.8891611 C29.7562072,19.6904735 27.9861447,17.920411 25.7874571,17.920411 L8.28000399,17.920411 L8.28000399,8.68635184 L31.8263867,8.68635184 C34.0250742,8.68635184 35.7951368,6.9162893 35.7951368,4.71760174 L35.7951368,4.4757567 C35.7951368,2.27706914 34.0250742,0.507006595 31.8263867,0.507006595 L4.06942472,0.507006595 Z" id="rect1436-8-4"></path>
- </g>
- </g>
- </g>
-</svg>
\ No newline at end of file
diff --git a/erpnext/public/images/erpnext-12.svg b/erpnext/public/images/erpnext-12.svg
deleted file mode 100644
index fcc8e46..0000000
--- a/erpnext/public/images/erpnext-12.svg
+++ /dev/null
@@ -1,26 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<svg width="165px" height="88px" viewBox="0 0 165 88" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink">
- <!-- Generator: Sketch 44.1 (41455) - http://www.bohemiancoding.com/sketch -->
- <title>version-12</title>
- <desc>Created with Sketch.</desc>
- <defs></defs>
- <g id="Page-1" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
- <g id="version-12" transform="translate(-2.000000, -2.000000)">
- <g id="erp-icon" fill-rule="nonzero">
- <g id="g1422-7-2" transform="translate(0.025630, 0.428785)" fill="#5E64FF">
- <g id="g1418-4-6" transform="translate(0.268998, 0.867736)">
- <g id="g1416-4-9" transform="translate(0.749997, 0.000000)">
- <path d="M14.1845844,0.703479866 L75.0387175,0.703479866 C82.3677094,0.703479866 88.2679029,6.60367875 88.2679029,13.9326374 L88.2679029,74.7868158 C88.2679029,82.1157744 82.3677094,88.0159833 75.0387175,88.0159833 L14.1845844,88.0159833 C6.85569246,88.0159833 0.955398949,82.1157744 0.955398949,74.7868158 L0.955398949,13.9326374 C0.955398949,6.60367875 6.85569246,0.703479866 14.1845844,0.703479866 L14.1845844,0.703479866 Z" id="path1414-3-4"></path>
- </g>
- </g>
- </g>
- <g id="g1444-6-7" transform="translate(27.708247, 23.320960)" fill="#FFFFFF">
- <path d="M4.06942472,0.507006595 C3.79457554,0.507006595 3.52673783,0.534925429 3.26792241,0.587619847 C3.00908052,0.640314265 2.75926093,0.717948309 2.52171801,0.818098395 C2.40292009,0.868173438 2.28745592,0.924056085 2.17495509,0.985013441 C1.94997987,1.10692286 1.73828674,1.24983755 1.54244215,1.41134187 C0.661062132,2.13811791 0.100674618,3.23899362 0.100674618,4.4757567 L0.100674618,4.71760174 L0.100674618,39.9531653 L0.100674618,40.1945182 C0.100674618,42.3932057 1.87073716,44.1632683 4.06942472,44.1632683 L31.8263867,44.1632683 C34.0250742,44.1632683 35.7951368,42.3932057 35.7951368,40.1945182 L35.7951368,39.9531653 C35.7951368,37.7544777 34.0250742,35.9844152 31.8263867,35.9844152 L8.28000399,35.9844152 L8.28000399,26.0992376 L25.7874571,26.0992376 C27.9861447,26.0992376 29.7562072,24.3291751 29.7562072,22.1304875 L29.7562072,21.8891611 C29.7562072,19.6904735 27.9861447,17.920411 25.7874571,17.920411 L8.28000399,17.920411 L8.28000399,8.68635184 L31.8263867,8.68635184 C34.0250742,8.68635184 35.7951368,6.9162893 35.7951368,4.71760174 L35.7951368,4.4757567 C35.7951368,2.27706914 34.0250742,0.507006595 31.8263867,0.507006595 L4.06942472,0.507006595 Z" id="rect1436-8-4"></path>
- </g>
- </g>
- <text id="12" font-family="SourceSansPro-Regular, Source Sans Pro" font-size="72" font-weight="normal" letter-spacing="-0.386831313" fill="#D1D8DD">
- <tspan x="99" y="71">12</tspan>
- </text>
- </g>
- </g>
-</svg>
\ No newline at end of file
diff --git a/erpnext/public/images/erpnext-favicon.svg b/erpnext/public/images/erpnext-favicon.svg
new file mode 100644
index 0000000..a3ac3bb
--- /dev/null
+++ b/erpnext/public/images/erpnext-favicon.svg
@@ -0,0 +1,5 @@
+<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 12C0 5.37258 5.37258 0 12 0H88C94.6274 0 100 5.37258 100 12V88C100 94.6274 94.6274 100 88 100H12C5.37258 100 0 94.6274 0 88V12Z" fill="#0089FF"/>
+<path d="M65.7097 32.9462H67.3871V24H33V32.9462H43.9032H65.7097Z" fill="white"/>
+<path d="M43.9032 66.2151V53.914H65.7097V44.9677H43.9032H33V75.1613H67.6667V66.2151H43.9032Z" fill="white"/>
+</svg>
\ No newline at end of file
diff --git a/erpnext/public/images/erpnext-footer.png b/erpnext/public/images/erpnext-footer.png
deleted file mode 100644
index ffff775..0000000
--- a/erpnext/public/images/erpnext-footer.png
+++ /dev/null
Binary files differ
diff --git a/erpnext/public/images/erpnext-logo.png b/erpnext/public/images/erpnext-logo.png
index 115faaa..3090727 100644
--- a/erpnext/public/images/erpnext-logo.png
+++ b/erpnext/public/images/erpnext-logo.png
Binary files differ
diff --git a/erpnext/public/images/erpnext-logo.svg b/erpnext/public/images/erpnext-logo.svg
new file mode 100644
index 0000000..a3ac3bb
--- /dev/null
+++ b/erpnext/public/images/erpnext-logo.svg
@@ -0,0 +1,5 @@
+<svg width="100" height="100" viewBox="0 0 100 100" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M0 12C0 5.37258 5.37258 0 12 0H88C94.6274 0 100 5.37258 100 12V88C100 94.6274 94.6274 100 88 100H12C5.37258 100 0 94.6274 0 88V12Z" fill="#0089FF"/>
+<path d="M65.7097 32.9462H67.3871V24H33V32.9462H43.9032H65.7097Z" fill="white"/>
+<path d="M43.9032 66.2151V53.914H65.7097V44.9677H43.9032H33V75.1613H67.6667V66.2151H43.9032Z" fill="white"/>
+</svg>
\ No newline at end of file
diff --git a/erpnext/public/images/favicon.png b/erpnext/public/images/favicon.png
deleted file mode 100644
index b694885..0000000
--- a/erpnext/public/images/favicon.png
+++ /dev/null
Binary files differ
diff --git a/erpnext/public/images/splash.png b/erpnext/public/images/splash.png
deleted file mode 100644
index 8e5d055..0000000
--- a/erpnext/public/images/splash.png
+++ /dev/null
Binary files differ
diff --git a/erpnext/public/images/ui-states/cart-empty-state.png b/erpnext/public/images/ui-states/cart-empty-state.png
new file mode 100644
index 0000000..e1ead0e
--- /dev/null
+++ b/erpnext/public/images/ui-states/cart-empty-state.png
Binary files differ
diff --git a/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
new file mode 100644
index 0000000..5bb58fa
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool/data_table_manager.js
@@ -0,0 +1,220 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.DataTableManager = class DataTableManager {
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.dialog_manager = new erpnext.accounts.bank_reconciliation.DialogManager(
+ this.company,
+ this.bank_account
+ );
+ this.make_dt();
+ }
+
+ make_dt() {
+ var me = this;
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_bank_transactions",
+ args: {
+ bank_account: this.bank_account,
+ },
+ callback: function (response) {
+ me.format_data(response.message);
+ me.get_dt_columns();
+ me.get_datatable();
+ me.set_listeners();
+ },
+ });
+ }
+
+ get_dt_columns() {
+ this.columns = [
+ {
+ name: "Date",
+ editable: false,
+ width: 100,
+ },
+
+ {
+ name: "Party Type",
+ editable: false,
+ width: 95,
+ },
+ {
+ name: "Party",
+ editable: false,
+ width: 100,
+ },
+ {
+ name: "Description",
+ editable: false,
+ width: 350,
+ },
+ {
+ name: "Deposit",
+ editable: false,
+ width: 100,
+ format: (value) =>
+ "<span style='color:green;'>" +
+ format_currency(value, this.currency) +
+ "</span>",
+ },
+ {
+ name: "Withdrawal",
+ editable: false,
+ width: 100,
+ format: (value) =>
+ "<span style='color:red;'>" +
+ format_currency(value, this.currency) +
+ "</span>",
+ },
+ {
+ name: "Unallocated Amount",
+ editable: false,
+ width: 100,
+ format: (value) =>
+ "<span style='color:blue;'>" +
+ format_currency(value, this.currency) +
+ "</span>",
+ },
+ {
+ name: "Reference Number",
+ editable: false,
+ width: 140,
+ },
+ {
+ name: "Actions",
+ editable: false,
+ sortable: false,
+ focusable: false,
+ dropdown: false,
+ width: 80,
+ },
+ ];
+ }
+
+ format_data(transactions) {
+ this.transactions = [];
+ if (transactions[0]) {
+ this.currency = transactions[0]["currency"];
+ }
+ this.transaction_dt_map = {};
+ let length;
+ transactions.forEach((row) => {
+ length = this.transactions.push(this.format_row(row));
+ this.transaction_dt_map[row["name"]] = length - 1;
+ });
+ }
+
+ format_row(row) {
+ return [
+ row["date"],
+ row["party_type"],
+ row["party"],
+ row["description"],
+ row["deposit"],
+ row["withdrawal"],
+ row["unallocated_amount"],
+ row["reference_number"],
+ `
+ <Button class="btn btn-primary btn-xs center" data-name = ${row["name"]} >
+ Actions
+ </a>
+ `,
+ ];
+ }
+
+ get_datatable() {
+ const datatable_options = {
+ columns: this.columns,
+ data: this.transactions,
+ dynamicRowHeight: true,
+ checkboxColumn: false,
+ inlineFilters: true,
+ };
+ this.datatable = new frappe.DataTable(
+ this.$reconciliation_tool_dt.get(0),
+ datatable_options
+ );
+ $(`.${this.datatable.style.scopeClass} .dt-scrollable`).css(
+ "max-height",
+ "calc(100vh - 400px)"
+ );
+
+ if (this.transactions.length > 0) {
+ this.$reconciliation_tool_dt.show();
+ this.$no_bank_transactions.hide();
+ } else {
+ this.$reconciliation_tool_dt.hide();
+ this.$no_bank_transactions.show();
+ }
+ }
+
+ set_listeners() {
+ var me = this;
+ $(`.${this.datatable.style.scopeClass} .dt-scrollable`).on(
+ "click",
+ `.btn`,
+ function () {
+ me.dialog_manager.show_dialog(
+ $(this).attr("data-name"),
+ (bank_transaction) => me.update_dt_cards(bank_transaction)
+ );
+ return true;
+ }
+ );
+ }
+
+ update_dt_cards(bank_transaction) {
+ const transaction_index = this.transaction_dt_map[
+ bank_transaction.name
+ ];
+ if (bank_transaction.unallocated_amount > 0) {
+ this.transactions[transaction_index] = this.format_row(
+ bank_transaction
+ );
+ } else {
+ this.transactions.splice(transaction_index, 1);
+ }
+ this.datatable.refresh(this.transactions, this.columns);
+
+ if (this.transactions.length == 0) {
+ this.$reconciliation_tool_dt.hide();
+ this.$no_bank_transactions.show();
+ }
+
+ // this.make_dt();
+ this.get_cleared_balance().then(() => {
+ this.cards_manager.$cards[1].set_value(
+ format_currency(this.cleared_balance),
+ this.currency
+ );
+ this.cards_manager.$cards[2].set_value(
+ format_currency(
+ this.bank_statement_closing_balance - this.cleared_balance
+ ),
+ this.currency
+ );
+ this.cards_manager.$cards[2].set_value_color(
+ this.bank_statement_closing_balance - this.cleared_balance == 0
+ ? "text-success"
+ : "text-danger"
+ );
+ });
+ }
+
+ get_cleared_balance() {
+ if (this.bank_account && this.bank_statement_to_date) {
+ return frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_account_balance",
+ args: {
+ bank_account: this.bank_account,
+ till_date: this.bank_statement_to_date,
+ },
+ callback: (response) =>
+ (this.cleared_balance = response.message),
+ });
+ }
+ }
+};
diff --git a/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
new file mode 100644
index 0000000..142fe79
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool/dialog_manager.js
@@ -0,0 +1,594 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.DialogManager = class DialogManager {
+ constructor(company, bank_account) {
+ this.bank_account = bank_account;
+ this.company = company;
+ this.make_dialog();
+ }
+
+ show_dialog(bank_transaction_name, update_dt_cards) {
+ this.bank_transaction_name = bank_transaction_name;
+ this.update_dt_cards = update_dt_cards;
+ frappe.call({
+ method: "frappe.client.get_value",
+ args: {
+ doctype: "Bank Transaction",
+ filters: { name: this.bank_transaction_name },
+ fieldname: [
+ "date",
+ "deposit",
+ "withdrawal",
+ "currency",
+ "description",
+ "name",
+ "bank_account",
+ "company",
+ "reference_number",
+ "party_type",
+ "party",
+ "unallocated_amount",
+ "allocated_amount",
+ ],
+ },
+ callback: (r) => {
+ if (r.message) {
+ this.bank_transaction = r.message;
+ r.message.payment_entry = 1;
+ this.dialog.set_values(r.message);
+ this.dialog.show();
+ }
+ },
+ });
+ }
+
+ get_linked_vouchers(document_types) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.get_linked_payments",
+ args: {
+ bank_transaction_name: this.bank_transaction_name,
+ document_types: document_types,
+ },
+
+ callback: (result) => {
+ const data = result.message;
+
+
+ if (data && data.length > 0) {
+ const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper;
+ proposals_wrapper.show();
+ this.dialog.fields_dict.no_matching_vouchers.$wrapper.hide();
+ this.data = [];
+ data.forEach((row) => {
+ const reference_date = row[5] ? row[5] : row[8];
+ this.data.push([
+ row[1],
+ row[2],
+ reference_date,
+ format_currency(row[3], row[9]),
+ row[6],
+ row[4],
+ ]);
+ });
+ this.get_dt_columns();
+ this.get_datatable(proposals_wrapper);
+ } else {
+ const proposals_wrapper = this.dialog.fields_dict.payment_proposals.$wrapper;
+ proposals_wrapper.hide();
+ this.dialog.fields_dict.no_matching_vouchers.$wrapper.show();
+
+ }
+ this.dialog.show();
+ },
+ });
+ }
+
+ get_dt_columns() {
+ this.columns = [
+ {
+ name: "Document Type",
+ editable: false,
+ width: 125,
+ },
+ {
+ name: "Document Name",
+ editable: false,
+ width: 150,
+ },
+ {
+ name: "Reference Date",
+ editable: false,
+ width: 120,
+ },
+ {
+ name: "Amount",
+ editable: false,
+ width: 100,
+ },
+ {
+ name: "Party",
+ editable: false,
+ width: 120,
+ },
+
+ {
+ name: "Reference Number",
+ editable: false,
+ width: 140,
+ },
+ ];
+ }
+
+ get_datatable(proposals_wrapper) {
+ if (!this.datatable) {
+ const datatable_options = {
+ columns: this.columns,
+ data: this.data,
+ dynamicRowHeight: true,
+ checkboxColumn: true,
+ inlineFilters: true,
+ };
+ this.datatable = new frappe.DataTable(
+ proposals_wrapper.get(0),
+ datatable_options
+ );
+ } else {
+ this.datatable.refresh(this.data, this.columns);
+ this.datatable.rowmanager.checkMap = [];
+ }
+ }
+
+ make_dialog() {
+ const me = this;
+ me.selected_payment = null;
+
+ const fields = [
+ {
+ label: __("Action"),
+ fieldname: "action",
+ fieldtype: "Select",
+ options: `Match Against Voucher\nCreate Voucher\nUpdate Bank Transaction`,
+ default: "Match Against Voucher",
+ },
+ {
+ fieldname: "column_break_4",
+ fieldtype: "Column Break",
+ },
+ {
+ label: __("Document Type"),
+ fieldname: "document_type",
+ fieldtype: "Select",
+ options: `Payment Entry\nJournal Entry`,
+ default: "Payment Entry",
+ depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "section_break_1",
+ label: __("Filters"),
+ depends_on: "eval:doc.action=='Match Against Voucher'",
+ },
+ {
+ fieldtype: "Check",
+ label: "Payment Entry",
+ fieldname: "payment_entry",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldtype: "Check",
+ label: "Journal Entry",
+ fieldname: "journal_entry",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldname: "column_break_5",
+ fieldtype: "Column Break",
+ },
+ {
+ fieldtype: "Check",
+ label: "Sales Invoice",
+ fieldname: "sales_invoice",
+ onchange: () => this.update_options(),
+ },
+
+ {
+ fieldtype: "Check",
+ label: "Purchase Invoice",
+ fieldname: "purchase_invoice",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldname: "column_break_5",
+ fieldtype: "Column Break",
+ },
+ {
+ fieldtype: "Check",
+ label: "Expense Claim",
+ fieldname: "expense_claim",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldtype: "Check",
+ label: "Show Only Exact Amount",
+ fieldname: "exact_match",
+ onchange: () => this.update_options(),
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "section_break_1",
+ label: __("Select Vouchers to Match"),
+ depends_on: "eval:doc.action=='Match Against Voucher'",
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: "payment_proposals",
+ },
+ {
+ fieldtype: "HTML",
+ fieldname: "no_matching_vouchers",
+ options: "<div class='text-muted text-center'>No Matching Vouchers Found</div>"
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "details",
+ label: "Details",
+ depends_on: "eval:doc.action!='Match Against Voucher'",
+ },
+ {
+ fieldname: "reference_number",
+ fieldtype: "Data",
+ label: "Reference Number",
+ mandatory_depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ default: "Today",
+ fieldname: "posting_date",
+ fieldtype: "Date",
+ label: "Posting Date",
+ reqd: 1,
+ depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldname: "reference_date",
+ fieldtype: "Date",
+ label: "Cheque/Reference Date",
+ mandatory_depends_on: "eval:doc.action=='Create Voucher'",
+ depends_on: "eval:doc.action=='Create Voucher'",
+ reqd: 1,
+ },
+ {
+ fieldname: "mode_of_payment",
+ fieldtype: "Link",
+ label: "Mode of Payment",
+ options: "Mode of Payment",
+ depends_on: "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldname: "edit_in_full_page",
+ fieldtype: "Button",
+ label: "Edit in Full Page",
+ click: () => {
+ this.edit_in_full_page();
+ },
+ depends_on:
+ "eval:doc.action=='Create Voucher'",
+ },
+ {
+ fieldname: "column_break_7",
+ fieldtype: "Column Break",
+ },
+ {
+ default: "Journal Entry Type",
+ fieldname: "journal_entry_type",
+ fieldtype: "Select",
+ label: "Journal Entry Type",
+ options:
+ "Journal Entry\nInter Company Journal Entry\nBank Entry\nCash Entry\nCredit Card Entry\nDebit Note\nCredit Note\nContra Entry\nExcise Entry\nWrite Off Entry\nOpening Entry\nDepreciation Entry\nExchange Rate Revaluation\nDeferred Revenue\nDeferred Expense",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ },
+ {
+ fieldname: "second_account",
+ fieldtype: "Link",
+ label: "Account",
+ options: "Account",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Journal Entry'",
+ get_query: () => {
+ return {
+ filters: {
+ is_group: 0,
+ company: this.company,
+ },
+ };
+ },
+ },
+ {
+ fieldname: "party_type",
+ fieldtype: "Link",
+ label: "Party Type",
+ options: "DocType",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ get_query: function () {
+ return {
+ filters: {
+ name: [
+ "in",
+ Object.keys(frappe.boot.party_account_types),
+ ],
+ },
+ };
+ },
+ },
+ {
+ fieldname: "party",
+ fieldtype: "Dynamic Link",
+ label: "Party",
+ options: "party_type",
+ mandatory_depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ },
+ {
+ fieldname: "project",
+ fieldtype: "Link",
+ label: "Project",
+ options: "Project",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ },
+ {
+ fieldname: "cost_center",
+ fieldtype: "Link",
+ label: "Cost Center",
+ options: "Cost Center",
+ depends_on:
+ "eval:doc.action=='Create Voucher' && doc.document_type=='Payment Entry'",
+ },
+ {
+ fieldtype: "Section Break",
+ fieldname: "details_section",
+ label: "Transaction Details",
+ collapsible: 1,
+ },
+ {
+ fieldname: "deposit",
+ fieldtype: "Currency",
+ label: "Deposit",
+ read_only: 1,
+ },
+ {
+ fieldname: "withdrawal",
+ fieldtype: "Currency",
+ label: "Withdrawal",
+ read_only: 1,
+ },
+ {
+ fieldname: "description",
+ fieldtype: "Small Text",
+ label: "Description",
+ read_only: 1,
+ },
+ {
+ fieldname: "column_break_17",
+ fieldtype: "Column Break",
+ read_only: 1,
+ },
+ {
+ fieldname: "allocated_amount",
+ fieldtype: "Currency",
+ label: "Allocated Amount",
+ read_only: 1,
+ },
+
+ {
+ fieldname: "unallocated_amount",
+ fieldtype: "Currency",
+ label: "Unallocated Amount",
+ read_only: 1,
+ },
+ ];
+
+ me.dialog = new frappe.ui.Dialog({
+ title: __("Reconcile the Bank Transaction"),
+ fields: fields,
+ size: "large",
+ primary_action: (values) =>
+ this.reconciliation_dialog_primary_action(values),
+ });
+ }
+
+ get_selected_attributes() {
+ let selected_attributes = [];
+ this.dialog.$wrapper.find(".checkbox input").each((i, col) => {
+ if ($(col).is(":checked")) {
+ selected_attributes.push($(col).attr("data-fieldname"));
+ }
+ });
+
+ return selected_attributes;
+ }
+
+ update_options() {
+ let selected_attributes = this.get_selected_attributes();
+ this.get_linked_vouchers(selected_attributes);
+ }
+
+ reconciliation_dialog_primary_action(values) {
+ if (values.action == "Match Against Voucher") this.match(values);
+ if (
+ values.action == "Create Voucher" &&
+ values.document_type == "Payment Entry"
+ )
+ this.add_payment_entry(values);
+ if (
+ values.action == "Create Voucher" &&
+ values.document_type == "Journal Entry"
+ )
+ this.add_journal_entry(values);
+ else if (values.action == "Update Bank Transaction")
+ this.update_transaction(values);
+ }
+
+ match() {
+ var selected_map = this.datatable.rowmanager.checkMap;
+ let rows = [];
+ selected_map.forEach((val, index) => {
+ if (val == 1) rows.push(this.datatable.datamanager.rows[index]);
+ });
+ let vouchers = [];
+ rows.forEach((x) => {
+ vouchers.push({
+ payment_doctype: x[2].content,
+ payment_name: x[3].content,
+ amount: x[5].content,
+ });
+ });
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.reconcile_vouchers",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ vouchers: vouchers,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " Matched";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ add_payment_entry(values) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ project: values.project,
+ cost_center: values.cost_center,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " added as Payment Entry";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ add_journal_entry(values) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ entry_type: values.journal_entry_type,
+ second_account: values.second_account,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " added as Journal Entry";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ update_transaction(values) {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.update_bank_transaction",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ party_type: values.party_type,
+ party: values.party,
+ },
+ callback: (response) => {
+ const alert_string =
+ "Bank Transaction " +
+ this.bank_transaction.name +
+ " updated";
+ frappe.show_alert(alert_string);
+ this.update_dt_cards(response.message);
+ this.dialog.hide();
+ },
+ });
+ }
+
+ edit_in_full_page() {
+ const values = this.dialog.get_values(true);
+ if (values.document_type == "Payment Entry") {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_payment_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ project: values.project,
+ cost_center: values.cost_center,
+ allow_edit: true
+ },
+ callback: (r) => {
+ const doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ },
+ });
+ } else {
+ frappe.call({
+ method:
+ "erpnext.accounts.doctype.bank_reconciliation_tool.bank_reconciliation_tool.create_journal_entry_bts",
+ args: {
+ bank_transaction_name: this.bank_transaction.name,
+ reference_number: values.reference_number,
+ reference_date: values.reference_date,
+ party_type: values.party_type,
+ party: values.party,
+ posting_date: values.posting_date,
+ mode_of_payment: values.mode_of_payment,
+ entry_type: values.journal_entry_type,
+ second_account: values.second_account,
+ allow_edit: true
+ },
+ callback: (r) => {
+ var doc = frappe.model.sync(r.message);
+ frappe.set_route("Form", doc[0].doctype, doc[0].name);
+ },
+ });
+ }
+ }
+
+};
diff --git a/erpnext/public/js/bank_reconciliation_tool/number_card.js b/erpnext/public/js/bank_reconciliation_tool/number_card.js
new file mode 100644
index 0000000..e10d109
--- /dev/null
+++ b/erpnext/public/js/bank_reconciliation_tool/number_card.js
@@ -0,0 +1,75 @@
+frappe.provide("erpnext.accounts.bank_reconciliation");
+
+erpnext.accounts.bank_reconciliation.NumberCardManager = class NumberCardManager {
+ constructor(opts) {
+ Object.assign(this, opts);
+ this.make_cards();
+ }
+
+ make_cards() {
+ this.$reconciliation_tool_cards.empty();
+ this.$cards = [];
+ this.$summary = $(`<div class="report-summary"></div>`)
+ .hide()
+ .appendTo(this.$reconciliation_tool_cards);
+ var chart_data = [
+ {
+ value: this.bank_statement_closing_balance,
+ label: "Closing Balance as per Bank Statement",
+ datatype: "Currency",
+ currency: this.currency,
+ },
+ {
+ value: this.cleared_balance,
+ label: "Closing Balance as per ERP",
+ datatype: "Currency",
+ currency: this.currency,
+ },
+ {
+ value:
+ this.bank_statement_closing_balance - this.cleared_balance,
+ label: "Difference",
+ datatype: "Currency",
+ currency: this.currency,
+ },
+ ];
+
+ chart_data.forEach((summary) => {
+ let number_card = new erpnext.accounts.NumberCard(summary);
+ this.$cards.push(number_card);
+
+ number_card.$card.appendTo(this.$summary);
+ });
+ this.$cards[2].set_value_color(
+ this.bank_statement_closing_balance - this.cleared_balance == 0
+ ? "text-success"
+ : "text-danger"
+ );
+ this.$summary.css({"border-bottom": "0px", "margin-left": "0px", "margin-right": "0px"});
+ this.$summary.show();
+ }
+};
+
+erpnext.accounts.NumberCard = class NumberCard {
+ constructor(options) {
+ this.$card = frappe.utils.build_summary_item(options);
+ }
+
+ set_value(value) {
+ this.$card.find("div").text(value);
+ }
+
+ set_value_color(color) {
+ this.$card
+ .find("div")
+ .removeClass("text-danger text-success")
+ .addClass(`${color}`);
+ }
+
+ set_indicator(color) {
+ this.$card
+ .find("span")
+ .removeClass("indicator red green")
+ .addClass(`indicator ${color}`);
+ }
+};
diff --git a/erpnext/public/js/call_popup/call_popup.js b/erpnext/public/js/call_popup/call_popup.js
index aeb3b38..c954f12 100644
--- a/erpnext/public/js/call_popup/call_popup.js
+++ b/erpnext/public/js/call_popup/call_popup.js
@@ -7,103 +7,25 @@
}
make() {
+ frappe.utils.play_sound('incoming-call');
this.dialog = new frappe.ui.Dialog({
'static': true,
- 'minimizable': true,
- 'fields': [{
- 'fieldname': 'name',
- 'label': 'Name',
- 'default': this.get_caller_name() || __('Unknown Caller'),
- 'fieldtype': 'Data',
- 'read_only': 1
- }, {
- 'fieldtype': 'Button',
- 'label': __('Open Contact'),
- 'click': () => frappe.set_route('Form', 'Contact', this.call_log.contact),
- 'depends_on': () => this.call_log.contact
- }, {
- 'fieldtype': 'Button',
- 'label': __('Open Lead'),
- 'click': () => frappe.set_route('Form', 'Lead', this.call_log.lead),
- 'depends_on': () => this.call_log.lead
- }, {
- 'fieldtype': 'Button',
- 'label': __('Create New Contact'),
- 'click': () => frappe.new_doc('Contact', { 'mobile_no': this.caller_number }),
- 'depends_on': () => !this.get_caller_name()
- }, {
- 'fieldtype': 'Button',
- 'label': __('Create New Lead'),
- 'click': () => frappe.new_doc('Lead', { 'mobile_no': this.caller_number }),
- 'depends_on': () => !this.get_caller_name()
- }, {
- 'fieldtype': 'Column Break',
- }, {
- 'fieldname': 'number',
- 'label': 'Phone Number',
- 'fieldtype': 'Data',
- 'default': this.caller_number,
- 'read_only': 1
- }, {
- 'fielname': 'last_interaction',
- 'fieldtype': 'Section Break',
- 'label': __('Activity'),
- 'depends_on': () => this.get_caller_name()
- }, {
- 'fieldtype': 'Small Text',
- 'label': __('Last Issue'),
- 'fieldname': 'last_issue',
- 'read_only': true,
- 'depends_on': () => this.call_log.contact,
- 'default': `<i class="text-muted">${__('No issue has been raised by the caller.')}<i>`
- }, {
- 'fieldtype': 'Small Text',
- 'label': __('Last Communication'),
- 'fieldname': 'last_communication',
- 'read_only': true,
- 'default': `<i class="text-muted">${__('No communication found.')}<i>`
- }, {
- 'fieldtype': 'Section Break',
- }, {
- 'fieldtype': 'Small Text',
- 'label': __('Call Summary'),
- 'fieldname': 'call_summary',
- }, {
- 'fieldtype': 'Button',
- 'label': __('Save'),
- 'click': () => {
- const call_summary = this.dialog.get_value('call_summary');
- if (!call_summary) return;
- frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', {
- 'call_log': this.call_log.name,
- 'summary': call_summary,
- }).then(() => {
- this.close_modal();
- frappe.show_alert({
- message: `
- ${__('Call Summary Saved')}
- <br>
- <a
- class="text-small text-muted"
- href="#Form/Call Log/${this.call_log.name}">
- ${__('View call log')}
- </a>
- `,
- indicator: 'green'
- });
- });
- }
- }],
+ 'minimizable': true
});
- this.set_call_status();
this.dialog.get_close_btn().show();
- this.make_last_interaction_section();
- this.dialog.$body.addClass('call-popup');
- this.dialog.set_secondary_action(this.close_modal.bind(this));
- frappe.utils.play_sound('incoming-call');
+ this.setup_dialog();
+ this.set_call_status();
+ frappe.utils.bind_actions_with_object(this.dialog.$body, this);
+ this.dialog.$wrapper.addClass('call-popup');
+ this.dialog.get_close_btn().unbind('click').click(this.close_modal.bind(this));
this.dialog.show();
}
+ setup_dialog() {
+ this.setup_call_details();
+ this.dialog.$body.empty().append(this.caller_info);
+ }
+
set_indicator(color, blink=false) {
let classes = `indicator ${color} ${blink ? 'blink': ''}`;
this.dialog.header.find('.indicator').attr('class', classes);
@@ -117,13 +39,13 @@
this.set_indicator('blue', true);
} else if (call_status === 'In Progress') {
title = __('Call Connected');
+ this.set_indicator('green');
+ } else if (['No Answer', 'Missed'].includes(call_status)) {
this.set_indicator('yellow');
- } else if (call_status === 'Missed') {
- this.set_indicator('red');
title = __('Call Missed');
- } else if (['Completed', 'Disconnected'].includes(call_status)) {
+ } else if (['Completed', 'Busy', 'Failed'].includes(call_status)) {
this.set_indicator('red');
- title = __('Call Disconnected');
+ title = __('Call Ended');
} else {
this.set_indicator('blue');
title = call_status;
@@ -131,9 +53,9 @@
this.dialog.set_title(title);
}
- update_call_log(call_log) {
+ update_call_log(call_log, missed) {
this.call_log = call_log;
- this.set_call_status();
+ this.set_call_status(missed ? 'Missed': null);
}
close_modal() {
@@ -141,61 +63,149 @@
delete erpnext.call_popup;
}
- call_disconnected(call_log) {
+ call_ended(call_log, missed) {
frappe.utils.play_sound('call-disconnect');
- this.update_call_log(call_log);
+ this.update_call_log(call_log, missed);
setTimeout(() => {
if (!this.dialog.get_value('call_summary')) {
this.close_modal();
}
- }, 30000);
- }
-
- make_last_interaction_section() {
- frappe.xcall('erpnext.crm.doctype.utils.get_last_interaction', {
- 'contact': this.call_log.contact,
- 'lead': this.call_log.lead
- }).then(data => {
- const comm_field = this.dialog.get_field('last_communication');
- if (data.last_communication) {
- const comm = data.last_communication;
- comm_field.set_value(comm.content);
- }
-
- if (data.last_issue) {
- const issue = data.last_issue;
- const issue_field = this.dialog.get_field("last_issue");
- issue_field.set_value(issue.subject);
- issue_field.$wrapper.append(`
- <a class="text-medium" href="#List/Issue?customer=${issue.customer}">
- ${__('View all issues from {0}', [issue.customer])}
- </a>
- `);
- }
- });
+ }, 60000);
+ this.clear_listeners();
}
get_caller_name() {
+ const contact_link = this.get_contact_link();
+ return contact_link.link_title || contact_link.link_name;
+ }
+
+ get_contact_link() {
let log = this.call_log;
- return log.contact_name || log.lead_name;
+ let contact_link = log.links.find(d => d.link_doctype === 'Contact');
+ return contact_link || {};
}
setup_listener() {
- frappe.realtime.on(`call_${this.call_log.id}_disconnected`, call_log => {
- this.call_disconnected(call_log);
- // Remove call disconnect listener after the call is disconnected
- frappe.realtime.off(`call_${this.call_log.id}_disconnected`);
+ frappe.realtime.on(`call_${this.call_log.id}_ended`, call_log => {
+ this.call_ended(call_log);
});
+
+ frappe.realtime.on(`call_${this.call_log.id}_missed`, call_log => {
+ this.call_ended(call_log, true);
+ });
+ }
+
+ clear_listeners() {
+ frappe.realtime.off(`call_${this.call_log.id}_ended`);
+ frappe.realtime.off(`call_${this.call_log.id}_missed`);
+ }
+
+ setup_call_details() {
+ this.caller_info = $(`<div></div>`);
+ this.call_details = new frappe.ui.FieldGroup({
+ fields: [{
+ 'fieldname': 'name',
+ 'label': 'Name',
+ 'default': this.get_caller_name() || __('Unknown Caller'),
+ 'fieldtype': 'Data',
+ 'read_only': 1
+ }, {
+ 'fieldtype': 'Button',
+ 'label': __('Open Contact'),
+ 'click': () => frappe.set_route('Form', 'Contact', this.get_contact_link().link_name),
+ 'depends_on': () => this.get_caller_name()
+ }, {
+ 'fieldtype': 'Button',
+ 'label': __('Create New Contact'),
+ 'click': this.create_new_contact.bind(this),
+ 'depends_on': () => !this.get_caller_name()
+ }, {
+ 'fieldtype': 'Button',
+ 'label': __('Create New Customer'),
+ 'click': this.create_new_customer.bind(this),
+ 'depends_on': () => !this.get_caller_name()
+ }, {
+ 'fieldtype': 'Button',
+ 'label': __('Create New Lead'),
+ 'click': () => frappe.new_doc('Lead', { 'mobile_no': this.caller_number }),
+ 'depends_on': () => !this.get_caller_name()
+ }, {
+ 'fieldtype': 'Column Break',
+ }, {
+ 'fieldname': 'number',
+ 'label': 'Phone Number',
+ 'fieldtype': 'Data',
+ 'default': this.caller_number,
+ 'read_only': 1
+ }, {
+ 'fieldtype': 'Section Break',
+ 'hide_border': 1,
+ }, {
+ 'fieldtype': 'Small Text',
+ 'label': __('Call Summary'),
+ 'fieldname': 'call_summary',
+ }, {
+ 'fieldtype': 'Button',
+ 'label': __('Save'),
+ 'click': () => {
+ const call_summary = this.call_details.get_value('call_summary');
+ if (!call_summary) return;
+ frappe.xcall('erpnext.telephony.doctype.call_log.call_log.add_call_summary', {
+ 'call_log': this.call_log.name,
+ 'summary': call_summary,
+ }).then(() => {
+ this.close_modal();
+ frappe.show_alert({
+ message: `
+ ${__('Call Summary Saved')}
+ <br>
+ <a
+ class="text-small text-muted"
+ href="/app/call-log/${this.call_log.name}">
+ ${__('View call log')}
+ </a>
+ `,
+ indicator: 'green'
+ });
+ });
+ }
+ }],
+ body: this.caller_info
+ });
+ this.call_details.make();
+ }
+
+ is_known_caller() {
+ return Boolean(this.get_caller_name());
+ }
+
+ create_new_customer() {
+ // to avoid quick entry form
+ const new_customer = frappe.model.get_new_doc('Customer');
+ new_customer.mobile_no = this.caller_number;
+ frappe.set_route('Form', new_customer.doctype, new_customer.name);
+ }
+
+ create_new_contact() {
+ // TODO: fix new_doc, it should accept child table values
+ const new_contact = frappe.model.get_new_doc('Contact');
+ const phone_no = frappe.model.add_child(new_contact, 'Contact Phone', 'phone_nos');
+ phone_no.phone = this.caller_number;
+ phone_no.is_primary_mobile_no = 1;
+ frappe.set_route('Form', new_contact.doctype, new_contact.name);
}
}
$(document).on('app_ready', function () {
frappe.realtime.on('show_call_popup', call_log => {
- if (!erpnext.call_popup) {
- erpnext.call_popup = new CallPopup(call_log);
+ let call_popup = erpnext.call_popup;
+ if (call_popup && call_log.name === call_popup.call_log.name) {
+ call_popup.update_call_log(call_log);
+ call_popup.dialog.show();
} else {
- erpnext.call_popup.update_call_log(call_log);
- erpnext.call_popup.dialog.show();
+ erpnext.call_popup = new CallPopup(call_log);
}
});
});
+
+window.CallPopup = CallPopup;
diff --git a/erpnext/public/js/communication.js b/erpnext/public/js/communication.js
index 26e5ab8..7ce8b09 100644
--- a/erpnext/public/js/communication.js
+++ b/erpnext/public/js/communication.js
@@ -84,7 +84,7 @@
frm.reload_doc();
frappe.show_alert({
message: __("Opportunity {0} created",
- ['<a href="#Form/Opportunity/'+r.message+'">' + r.message + '</a>']),
+ ['<a href="/app/opportunity/'+r.message+'">' + r.message + '</a>']),
indicator: 'green'
});
}
diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js
index 29f3595..649eb45 100644
--- a/erpnext/public/js/controllers/accounts.js
+++ b/erpnext/public/js/controllers/accounts.js
@@ -31,15 +31,6 @@
}
}
});
-
- frm.set_query("cost_center", "taxes", function(doc) {
- return {
- filters: {
- 'company': doc.company,
- "is_group": 0
- }
- }
- });
}
},
validate: function(frm) {
diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js
index 3f5652a..c963866 100644
--- a/erpnext/public/js/controllers/buying.js
+++ b/erpnext/public/js/controllers/buying.js
@@ -189,8 +189,12 @@
frappe.model.round_floats_in(item, ["qty", "received_qty"]);
item.rejected_qty = flt(item.received_qty - item.qty, precision("rejected_qty", item));
+ item.received_stock_qty = flt(item.conversion_factor, precision("conversion_factor", item)) * flt(item.received_qty);
}
+ this._super(doc, cdt, cdn);
+ },
+ batch_no: function(doc, cdt, cdn) {
this._super(doc, cdt, cdn);
},
@@ -293,69 +297,6 @@
this.get_terms();
},
- link_to_mrs: function() {
- var my_items = [];
- for (var i in cur_frm.doc.items) {
- if(!cur_frm.doc.items[i].material_request){
- my_items.push(cur_frm.doc.items[i].item_code);
- }
- }
- frappe.call({
- method: "erpnext.buying.utils.get_linked_material_requests",
- args:{
- items: my_items
- },
- callback: function(r) {
- if(!r.message || r.message.length == 0) {
- frappe.throw(__("No pending Material Requests found to link for the given items."))
- }
- else {
- var i = 0;
- var item_length = cur_frm.doc.items.length;
- while (i < item_length) {
- var qty = cur_frm.doc.items[i].qty;
- (r.message[0] || []).forEach(function(d) {
- if (d.qty > 0 && qty > 0 && cur_frm.doc.items[i].item_code == d.item_code && !cur_frm.doc.items[i].material_request_item)
- {
- cur_frm.doc.items[i].material_request = d.mr_name;
- cur_frm.doc.items[i].material_request_item = d.mr_item;
- var my_qty = Math.min(qty, d.qty);
- qty = qty - my_qty;
- d.qty = d.qty - my_qty;
- cur_frm.doc.items[i].stock_qty = my_qty*cur_frm.doc.items[i].conversion_factor;
- cur_frm.doc.items[i].qty = my_qty;
-
- frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + cur_frm.doc.items[i].idx + ")");
- if (qty > 0)
- {
- frappe.msgprint("Splitting " + qty + " units of " + d.item_code);
- var newrow = frappe.model.add_child(cur_frm.doc, cur_frm.doc.items[i].doctype, "items");
- item_length++;
-
- for (var key in cur_frm.doc.items[i])
- {
- newrow[key] = cur_frm.doc.items[i][key];
- }
-
- newrow.idx = item_length;
- newrow["stock_qty"] = newrow.conversion_factor*qty;
- newrow["qty"] = qty;
-
- newrow["material_request"] = "";
- newrow["material_request_item"] = "";
-
- }
- }
- });
- i++;
- }
- refresh_field("items");
- //cur_frm.save();
- }
- }
- });
- },
-
update_auto_repeat_reference: function(doc) {
if (doc.auto_repeat) {
frappe.call({
@@ -421,6 +362,62 @@
cur_frm.add_fetch('project', 'cost_center', 'cost_center');
+erpnext.buying.link_to_mrs = function(frm) {
+ frappe.call({
+ method: "erpnext.buying.utils.get_linked_material_requests",
+ args:{
+ items: frm.doc.items.map((item) => item.item_code)
+ },
+ callback: function(r) {
+ if (!r.message || r.message.length == 0) {
+ frappe.throw({
+ message: __("No pending Material Requests found to link for the given items."),
+ title: __("Note")
+ });
+ }
+
+ var item_length = frm.doc.items.length;
+ for (let item of frm.doc.items) {
+ var qty = item.qty;
+ (r.message[0] || []).forEach(function(d) {
+ if (d.qty > 0 && qty > 0 && item.item_code == d.item_code && !item.material_request_item)
+ {
+ item.material_request = d.mr_name;
+ item.material_request_item = d.mr_item;
+ var my_qty = Math.min(qty, d.qty);
+ qty = qty - my_qty;
+ d.qty = d.qty - my_qty;
+ item.stock_qty = my_qty*item.conversion_factor;
+ item.qty = my_qty;
+
+ frappe.msgprint("Assigning " + d.mr_name + " to " + d.item_code + " (row " + item.idx + ")");
+ if (qty > 0)
+ {
+ frappe.msgprint("Splitting " + qty + " units of " + d.item_code);
+ var newrow = frappe.model.add_child(frm.doc, item.doctype, "items");
+ item_length++;
+
+ for (var key in item)
+ {
+ newrow[key] = item[key];
+ }
+
+ newrow.idx = item_length;
+ newrow["stock_qty"] = newrow.conversion_factor*qty;
+ newrow["qty"] = qty;
+
+ newrow["material_request"] = "";
+ newrow["material_request_item"] = "";
+
+ }
+ }
+ });
+ }
+ refresh_field("items");
+ }
+ });
+}
+
erpnext.buying.get_default_bom = function(frm) {
$.each(frm.doc["items"] || [], function(i, d) {
if (d.item_code && d.bom === "") {
@@ -522,4 +519,4 @@
});
dialog.show();
-}
+}
\ No newline at end of file
diff --git a/erpnext/public/js/controllers/taxes_and_totals.js b/erpnext/public/js/controllers/taxes_and_totals.js
index 99f3995..d81321b 100644
--- a/erpnext/public/js/controllers/taxes_and_totals.js
+++ b/erpnext/public/js/controllers/taxes_and_totals.js
@@ -2,9 +2,11 @@
// License: GNU General Public License v3. See license.txt
erpnext.taxes_and_totals = erpnext.payments.extend({
- setup: function() {},
+ setup: function() {
+ this.fetch_round_off_accounts();
+ },
- apply_pricing_rule_on_item: function(item){
+ apply_pricing_rule_on_item: function(item) {
let effective_item_rate = item.price_list_rate;
let item_rate = item.rate;
if (in_list(["Sales Order", "Quotation"], item.parenttype) && item.blanket_order_rate) {
@@ -26,6 +28,7 @@
if (item.discount_amount) {
item_rate = flt((item.rate_with_margin) - (item.discount_amount), precision('rate', item));
+ item.discount_percentage = 100 * flt(item.discount_amount) / flt(item.rate_with_margin);
}
frappe.model.set_value(item.doctype, item.name, "rate", item_rate);
@@ -151,6 +154,22 @@
});
},
+ fetch_round_off_accounts: function() {
+ let me = this;
+ frappe.flags.round_off_applicable_accounts = [];
+
+ return frappe.call({
+ "method": "erpnext.controllers.taxes_and_totals.get_round_off_applicable_accounts",
+ "args": {
+ "company": me.frm.doc.company,
+ "account_list": frappe.flags.round_off_applicable_accounts
+ },
+ callback: function(r) {
+ frappe.flags.round_off_applicable_accounts.push(...r.message);
+ }
+ });
+ },
+
determine_exclusive_rate: function() {
var me = this;
@@ -371,11 +390,21 @@
} else if (tax.charge_type == "On Item Quantity") {
current_tax_amount = tax_rate * item.qty;
}
+
+ current_tax_amount = this.get_final_tax_amount(tax, current_tax_amount);
this.set_item_wise_tax(item, tax, tax_rate, current_tax_amount);
return current_tax_amount;
},
+ get_final_tax_amount: function(tax, current_tax_amount) {
+ if (frappe.flags.round_off_applicable_accounts.includes(tax.account_head)) {
+ current_tax_amount = Math.round(current_tax_amount);
+ }
+
+ return current_tax_amount;
+ },
+
set_item_wise_tax: function(item, tax, tax_rate, current_tax_amount) {
// store tax breakup for each item
let tax_detail = tax.item_wise_tax_detail;
@@ -609,6 +638,15 @@
this.calculate_outstanding_amount(update_paid_amount);
},
+ is_internal_invoice: function() {
+ if (['Sales Invoice', 'Purchase Invoice'].includes(this.frm.doc.doctype)) {
+ if (this.frm.doc.company === this.frm.doc.represents_company) {
+ return true;
+ }
+ }
+ return false;
+ },
+
calculate_outstanding_amount: function(update_paid_amount) {
// NOTE:
// paid_amount and write_off_amount is only for POS/Loyalty Point Redemption Invoice
@@ -617,7 +655,7 @@
this.calculate_paid_amount();
}
- if(this.frm.doc.is_return || this.frm.doc.docstatus > 0) return;
+ if (this.frm.doc.is_return || (this.frm.doc.docstatus > 0) || this.is_internal_invoice()) return;
frappe.model.round_floats_in(this.frm.doc, ["grand_total", "total_advance", "write_off_amount"]);
diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js
index 7f08cd1..e5f9049 100644
--- a/erpnext/public/js/controllers/transaction.js
+++ b/erpnext/public/js/controllers/transaction.js
@@ -1,6 +1,8 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
+frappe.provide('erpnext.accounts.dimensions');
+
erpnext.TransactionController = erpnext.taxes_and_totals.extend({
setup: function() {
this._super();
@@ -38,7 +40,7 @@
cur_frm.cscript.set_gross_profit(item);
cur_frm.cscript.calculate_taxes_and_totals();
-
+ cur_frm.cscript.calculate_stock_uom_rate(frm, cdt, cdn);
});
@@ -103,9 +105,19 @@
frappe.ui.form.on(this.frm.doctype + " Item", {
items_add: function(frm, cdt, cdn) {
var item = frappe.get_doc(cdt, cdn);
- if(!item.warehouse && frm.doc.set_warehouse) {
+ if (!item.warehouse && frm.doc.set_warehouse) {
item.warehouse = frm.doc.set_warehouse;
}
+
+ if (!item.target_warehouse && frm.doc.set_target_warehouse) {
+ item.target_warehouse = frm.doc.set_target_warehouse;
+ }
+
+ if (!item.from_warehouse && frm.doc.set_from_warehouse) {
+ item.from_warehouse = frm.doc.set_from_warehouse;
+ }
+
+ erpnext.accounts.dimensions.copy_dimension_from_first_row(frm, cdt, cdn, 'items');
}
});
@@ -159,16 +171,6 @@
};
});
}
- if (this.frm.fields_dict["items"].grid.get_field("cost_center")) {
- this.frm.set_query("cost_center", "items", function(doc) {
- return {
- filters: {
- "company": doc.company,
- "is_group": 0
- }
- };
- });
- }
if (this.frm.fields_dict["items"].grid.get_field("expense_account")) {
this.frm.set_query("expense_account", "items", function(doc) {
@@ -233,6 +235,8 @@
}
};
+ this.frm.trigger('set_default_internal_warehouse');
+
return frappe.run_serially([
() => set_value('currency', currency),
() => set_value('price_list_currency', currency),
@@ -408,7 +412,7 @@
show_description(row_to_modify.idx, row_to_modify.item_code);
- this.frm.from_barcode = true;
+ this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
frappe.model.set_value(row_to_modify.doctype, row_to_modify.name, {
item_code: data.item_code,
qty: (row_to_modify.qty || 0) + 1
@@ -446,9 +450,10 @@
method: "erpnext.controllers.accounts_controller.get_default_taxes_and_charges",
args: {
"master_doctype": taxes_and_charges_field.options,
- "tax_template": me.frm.doc.taxes_and_charges,
+ "tax_template": me.frm.doc.taxes_and_charges || "",
"company": me.frm.doc.company
},
+ debounce: 2000,
callback: function(r) {
if(!r.exc && r.message) {
frappe.run_serially([
@@ -492,7 +497,7 @@
d.item_code = "";
}
- this.frm.from_barcode = true;
+ this.frm.from_barcode = this.frm.from_barcode ? this.frm.from_barcode + 1 : 1;
this.item_code(doc, cdt, cdn);
},
@@ -509,11 +514,12 @@
show_batch_dialog = 1;
}
// clear barcode if setting item (else barcode will take priority)
- if(!this.frm.from_barcode) {
+ if (this.frm.from_barcode == 0) {
item.barcode = null;
}
+ this.frm.from_barcode = this.frm.from_barcode - 1 >= 0 ? this.frm.from_barcode - 1 : 0;
- this.frm.from_barcode = false;
+
if(item.item_code || item.barcode || item.serial_no) {
if(!this.validate_company_and_party()) {
this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove();
@@ -542,6 +548,7 @@
company: me.frm.doc.company,
order_type: me.frm.doc.order_type,
is_pos: cint(me.frm.doc.is_pos),
+ is_return: cint(me.frm.doc.is_return),
is_subcontracted: me.frm.doc.is_subcontracted,
transaction_date: me.frm.doc.transaction_date || me.frm.doc.posting_date,
ignore_pricing_rule: me.frm.doc.ignore_pricing_rule,
@@ -593,12 +600,22 @@
return frappe.db.get_value("Item", item.item_code, ["has_batch_no", "has_serial_no"])
.then((r) => {
if (r.message &&
- (r.message.has_batch_no || r.message.has_serial_no)) {
+ (r.message.has_batch_no || r.message.has_serial_no)) {
frappe.flags.hide_serial_batch_dialog = false;
}
});
},
() => {
+ // check if batch serial selector is disabled or not
+ if (show_batch_dialog && !frappe.flags.hide_serial_batch_dialog)
+ return frappe.db.get_single_value('Stock Settings', 'disable_serial_no_and_batch_selector')
+ .then((value) => {
+ if (value) {
+ frappe.flags.hide_serial_batch_dialog = true;
+ }
+ });
+ },
+ () => {
if(show_batch_dialog && !frappe.flags.hide_serial_batch_dialog) {
var d = locals[cdt][cdn];
$.each(r.message, function(k, v) {
@@ -652,7 +669,7 @@
args: item_args
},
callback: function(r) {
- frappe.model.set_value(item.doctype, item.name, 'rate', r.message);
+ frappe.model.set_value(item.doctype, item.name, 'rate', r.message * item.conversion_factor);
}
});
},
@@ -718,6 +735,31 @@
this.calculate_taxes_and_totals(false);
},
+ update_stock: function() {
+ this.frm.trigger('set_default_internal_warehouse');
+ },
+
+ set_default_internal_warehouse: function() {
+ let me = this;
+ if ((this.frm.doc.doctype === 'Sales Invoice' && me.frm.doc.update_stock)
+ || this.frm.doc.doctype == 'Delivery Note') {
+ if (this.frm.doc.is_internal_customer && this.frm.doc.company === this.frm.doc.represents_company) {
+ frappe.db.get_value('Company', this.frm.doc.company, 'default_in_transit_warehouse', function(value) {
+ me.frm.set_value('set_target_warehouse', value.default_in_transit_warehouse);
+ });
+ }
+ }
+
+ if ((this.frm.doc.doctype === 'Purchase Invoice' && me.frm.doc.update_stock)
+ || this.frm.doc.doctype == 'Purchase Receipt') {
+ if (this.frm.doc.is_internal_supplier && this.frm.doc.company === this.frm.doc.represents_company) {
+ frappe.db.get_value('Company', this.frm.doc.company, 'default_in_transit_warehouse', function(value) {
+ me.frm.set_value('set_from_warehouse', value.default_in_transit_warehouse);
+ });
+ }
+ }
+ },
+
company: function() {
var me = this;
var set_pricing = function() {
@@ -804,7 +846,7 @@
in_list(['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'], this.frm.doctype)) {
erpnext.utils.get_shipping_address(this.frm, function(){
set_party_account(set_pricing);
- })
+ });
// Get default company billing address in Purchase Invoice, Order and Receipt
frappe.call({
@@ -1080,6 +1122,7 @@
}
});
}
+ me.calculate_stock_uom_rate(doc, cdt, cdn);
},
conversion_factor: function(doc, cdt, cdn, dont_fetch_price_list_rate) {
@@ -1100,9 +1143,15 @@
frappe.meta.has_field(doc.doctype, "price_list_currency")) {
this.apply_price_list(item, true);
}
+ this.calculate_stock_uom_rate(doc, cdt, cdn);
}
},
+ batch_no: function(doc, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ this.apply_price_list(item, true);
+ },
+
toggle_conversion_factor: function(item) {
// toggle read only property for conversion factor field if the uom and stock uom are same
if(this.frm.get_field('items').grid.fields_map.conversion_factor) {
@@ -1115,9 +1164,15 @@
qty: function(doc, cdt, cdn) {
let item = frappe.get_doc(cdt, cdn);
this.conversion_factor(doc, cdt, cdn, true);
+ this.calculate_stock_uom_rate(doc, cdt, cdn);
this.apply_pricing_rule(item, true);
},
+ calculate_stock_uom_rate: function(doc, cdt, cdn) {
+ let item = frappe.get_doc(cdt, cdn);
+ item.stock_uom_rate = flt(item.rate)/flt(item.conversion_factor);
+ refresh_field("stock_uom_rate", item.name, item.parentfield);
+ },
service_stop_date: function(frm, cdt, cdn) {
var child = locals[cdt][cdn];
@@ -1228,7 +1283,7 @@
this.frm.set_currency_labels(["base_rate", "base_net_rate", "base_price_list_rate", "base_amount", "base_net_amount"],
company_currency, "items");
- this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount"],
+ this.frm.set_currency_labels(["rate", "net_rate", "price_list_rate", "amount", "net_amount", "stock_uom_rate"],
this.frm.doc.currency, "items");
if(this.frm.fields_dict["operations"]) {
@@ -1407,6 +1462,7 @@
"pricing_rules": d.pricing_rules,
"warehouse": d.warehouse,
"serial_no": d.serial_no,
+ "batch_no": d.batch_no,
"price_list_rate": d.price_list_rate,
"conversion_factor": d.conversion_factor || 1.0
});
@@ -1965,6 +2021,14 @@
this.autofill_warehouse(this.frm.doc.items, "warehouse", this.frm.doc.set_warehouse);
},
+ set_target_warehouse: function() {
+ this.autofill_warehouse(this.frm.doc.items, "target_warehouse", this.frm.doc.set_target_warehouse);
+ },
+
+ set_from_warehouse: function() {
+ this.autofill_warehouse(this.frm.doc.items, "from_warehouse", this.frm.doc.set_from_warehouse);
+ },
+
autofill_warehouse : function (child_table, warehouse_field, warehouse) {
if (warehouse && child_table && child_table.length) {
let doctype = child_table[0].doctype;
@@ -2029,3 +2093,35 @@
}, show_dialog);
});
}
+
+erpnext.apply_putaway_rule = (frm, purpose=null) => {
+ if (!frm.doc.company) {
+ frappe.throw({message: __("Please select a Company first."), title: __("Mandatory")});
+ }
+ if (!frm.doc.items.length) return;
+
+ frappe.call({
+ method: "erpnext.stock.doctype.putaway_rule.putaway_rule.apply_putaway_rule",
+ args: {
+ doctype: frm.doctype,
+ items: frm.doc.items,
+ company: frm.doc.company,
+ sync: true,
+ purpose: purpose
+ },
+ callback: (result) => {
+ if (!result.exc && result.message) {
+ frm.clear_table("items");
+
+ let items = result.message;
+ items.forEach((row) => {
+ delete row["name"]; // dont overwrite name from server side
+ let child = frm.add_child("items");
+ Object.assign(child, row);
+ frm.script_manager.trigger("qty", child.doctype, child.name);
+ });
+ frm.get_field("items").grid.refresh();
+ }
+ }
+ });
+};
\ No newline at end of file
diff --git a/erpnext/public/js/education/assessment_result_tool.html b/erpnext/public/js/education/assessment_result_tool.html
index 9fc17f7..b591010 100644
--- a/erpnext/public/js/education/assessment_result_tool.html
+++ b/erpnext/public/js/education/assessment_result_tool.html
@@ -19,7 +19,7 @@
</thead>
<tbody>
{% for s in students %}
- <tr
+ <tr
{% if(s.assessment_details && s.docstatus && s.docstatus == 1) { %} class="text-muted" {% } %}
data-student="{{s.student}}">
@@ -29,7 +29,7 @@
<td>
<span data-student="{{s.student}}" data-criteria="{{c.assessment_criteria}}" class="student-result-grade badge" >
{% if(s.assessment_details) { %}
- {{s.assessment_details[c.assessment_criteria][1]}}
+ {{s.assessment_details[c.assessment_criteria][1]}}
{% } %}
</span>
<input type="number" class="student-result-data" style="width:70%; float:right;"
@@ -61,7 +61,7 @@
{% } %}
</span>
<span data-student="{{s.student}}" class="total-result-link" style="width: 10%; display:{% if(!s.assessment_details) { %}None{% } %}; float:right;">
- <a class="btn-open no-decoration" title="Open Link" href="#Form/Assessment Result/{% if(s.assessment_details) { %}{{s.name}}{% } %}">
+ <a class="btn-open no-decoration" title="Open Link" href="/app/Form/Assessment Result/{% if(s.assessment_details) { %}{{s.name}}{% } %}">
<i class="octicon octicon-arrow-right"></i>
</a>
</span>
diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js
index 459c01b..b2f7afe 100644
--- a/erpnext/public/js/financial_statements.js
+++ b/erpnext/public/js/financial_statements.js
@@ -57,18 +57,22 @@
});
});
- report.page.add_inner_button(__("Balance Sheet"), function() {
+ const views_menu = report.page.add_custom_button_group(__('Financial Statements'));
+
+ report.page.add_custom_menu_item(views_menu, __("Balance Sheet"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Balance Sheet', {company: filters.company});
- }, __('Financial Statements'));
- report.page.add_inner_button(__("Profit and Loss"), function() {
+ });
+
+ report.page.add_custom_menu_item(views_menu, __("Profit and Loss"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Profit and Loss Statement', {company: filters.company});
- }, __('Financial Statements'));
- report.page.add_inner_button(__("Cash Flow Statement"), function() {
+ });
+
+ report.page.add_custom_menu_item(views_menu, __("Cash Flow Statement"), function() {
var filters = report.get_values();
frappe.set_route('query-report', 'Cash Flow', {company: filters.company});
- }, __('Financial Statements'));
+ });
}
};
diff --git a/erpnext/public/js/help_links.js b/erpnext/public/js/help_links.js
index 66ff464..472c537 100644
--- a/erpnext/public/js/help_links.js
+++ b/erpnext/public/js/help_links.js
@@ -2,13 +2,13 @@
const docsUrl = 'https://erpnext.com/docs/';
-frappe.help.help_links['Form/Rename Tool'] = [
+frappe.help.help_links['rename tool'] = [
{ label: 'Bulk Rename', url: docsUrl + 'user/manual/en/setting-up/data/bulk-rename' },
]
//Setup
-frappe.help.help_links['List/User'] = [
+frappe.help.help_links['user'] = [
{ label: 'New User', url: docsUrl + 'user/manual/en/setting-up/users-and-permissions/adding-users' },
{ label: 'Rename User', url: docsUrl + 'user/manual/en/setting-up/articles/rename-user' },
]
@@ -21,7 +21,7 @@
{ label: 'Password', url: docsUrl + 'user/manual/en/setting-up/articles/change-password' },
]
-frappe.help.help_links['Form/System Settings'] = [
+frappe.help.help_links['system-settings'] = [
{ label: 'Naming Series', url: docsUrl + 'user/manual/en/setting-up/settings/system-settings' },
]
@@ -30,64 +30,60 @@
{ label: 'Overwriting Data from Data Import Tool', url: docsUrl + 'user/manual/en/setting-up/articles/overwriting-data-from-data-import-tool' },
]
-frappe.help.help_links['module_setup'] = [
- { label: 'Role Permissions Manager', url: docsUrl + 'user/manual/en/setting-up/users-and-permissions/role-based-permissions' },
-]
-
-frappe.help.help_links['Form/Naming Series'] = [
+frappe.help.help_links['naming-series'] = [
{ label: 'Naming Series', url: docsUrl + 'user/manual/en/setting-up/settings/naming-series' },
{ label: 'Setting the Current Value for Naming Series', url: docsUrl + 'user/manual/en/setting-up/articles/naming-series-current-value' },
]
-frappe.help.help_links['Form/Global Defaults'] = [
+frappe.help.help_links['global-defaults'] = [
{ label: 'Global Settings', url: docsUrl + 'user/manual/en/setting-up/settings/global-defaults' },
]
-frappe.help.help_links['Form/Email Digest'] = [
+frappe.help.help_links['email-digest'] = [
{ label: 'Email Digest', url: docsUrl + 'user/manual/en/setting-up/email/email-digest' },
]
-frappe.help.help_links['List/Print Heading'] = [
+frappe.help.help_links['print-heading'] = [
{ label: 'Print Heading', url: docsUrl + 'user/manual/en/setting-up/print/print-headings' },
]
-frappe.help.help_links['List/Letter Head'] = [
+frappe.help.help_links['letter-head'] = [
{ label: 'Letter Head', url: docsUrl + 'user/manual/en/setting-up/print/letter-head' },
]
-frappe.help.help_links['List/Address Template'] = [
+frappe.help.help_links['address-template'] = [
{ label: 'Address Template', url: docsUrl + 'user/manual/en/setting-up/print/address-template' },
]
-frappe.help.help_links['List/Terms and Conditions'] = [
+frappe.help.help_links['terms-and-conditions'] = [
{ label: 'Terms and Conditions', url: docsUrl + 'user/manual/en/setting-up/print/terms-and-conditions' },
]
-frappe.help.help_links['List/Cheque Print Template'] = [
+frappe.help.help_links['cheque-print-template'] = [
{ label: 'Cheque Print Template', url: docsUrl + 'user/manual/en/setting-up/print/cheque-print-template' },
]
-frappe.help.help_links['List/Email Account'] = [
+frappe.help.help_links['email-account'] = [
{ label: 'Email Account', url: docsUrl + 'user/manual/en/setting-up/email/email-account' },
]
-frappe.help.help_links['List/Notification'] = [
+frappe.help.help_links['notification'] = [
{ label: 'Notification', url: docsUrl + 'user/manual/en/setting-up/email/notifications' },
]
-frappe.help.help_links['Form/Notification'] = [
+frappe.help.help_links['notification'] = [
{ label: 'Notification', url: docsUrl + 'user/manual/en/setting-up/email/notifications' },
]
-frappe.help.help_links['List/Email Digest'] = [
+frappe.help.help_links['email-digest'] = [
{ label: 'Email Digest', url: docsUrl + 'user/manual/en/setting-up/email/email-digest' },
]
-frappe.help.help_links['List/Auto Email Report'] = [
+frappe.help.help_links['auto-email-report'] = [
{ label: 'Auto Email Reports', url: docsUrl + 'user/manual/en/setting-up/email/email-reports' },
]
-frappe.help.help_links['Form/Print Settings'] = [
+frappe.help.help_links['print-settings'] = [
{ label: 'Print Settings', url: docsUrl + 'user/manual/en/setting-up/print/print-settings' },
]
@@ -95,66 +91,60 @@
{ label: 'Print Format Builder', url: docsUrl + 'user/manual/en/setting-up/print/print-settings' },
]
-frappe.help.help_links['List/Print Heading'] = [
+frappe.help.help_links['print-heading'] = [
{ label: 'Print Heading', url: docsUrl + 'user/manual/en/setting-up/print/print-headings' },
]
//setup-integrations
-frappe.help.help_links['Form/PayPal Settings'] = [
+frappe.help.help_links['paypal-settings'] = [
{ label: 'PayPal Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/paypal-integration' },
]
-frappe.help.help_links['Form/Razorpay Settings'] = [
+frappe.help.help_links['razorpay-settings'] = [
{ label: 'Razorpay Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/razorpay-integration' },
]
-frappe.help.help_links['Form/Dropbox Settings'] = [
+frappe.help.help_links['dropbox-settings'] = [
{ label: 'Dropbox Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/dropbox-backup' },
]
-frappe.help.help_links['Form/LDAP Settings'] = [
+frappe.help.help_links['ldap-settings'] = [
{ label: 'LDAP Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/ldap-integration' },
]
-frappe.help.help_links['Form/Stripe Settings'] = [
+frappe.help.help_links['stripe-settings'] = [
{ label: 'Stripe Settings', url: docsUrl + 'user/manual/en/setting-up/integrations/stripe-integration' },
]
//Sales
-frappe.help.help_links['Form/Quotation'] = [
+frappe.help.help_links['quotation'] = [
{ label: 'Quotation', url: docsUrl + 'user/manual/en/selling/quotation' },
{ label: 'Applying Discount', url: docsUrl + 'user/manual/en/selling/articles/applying-discount' },
{ label: 'Sales Person', url: docsUrl + 'user/manual/en/selling/articles/sales-persons-in-the-sales-transactions' },
{ label: 'Applying Margin', url: docsUrl + 'user/manual/en/selling/articles/adding-margin' },
]
-frappe.help.help_links['List/Customer'] = [
+frappe.help.help_links['customer'] = [
{ label: 'Customer', url: docsUrl + 'user/manual/en/CRM/customer' },
{ label: 'Credit Limit', url: docsUrl + 'user/manual/en/accounts/credit-limit' },
]
-frappe.help.help_links['Form/Customer'] = [
+frappe.help.help_links['customer'] = [
{ label: 'Customer', url: docsUrl + 'user/manual/en/CRM/customer' },
{ label: 'Credit Limit', url: docsUrl + 'user/manual/en/accounts/credit-limit' },
]
-frappe.help.help_links['List/Sales Taxes and Charges Template'] = [
+frappe.help.help_links['sales-taxes-and-charges-template'] = [
{ label: 'Setting Up Taxes', url: docsUrl + 'user/manual/en/setting-up/setting-up-taxes' },
]
-frappe.help.help_links['Form/Sales Taxes and Charges Template'] = [
+frappe.help.help_links['sales-taxes-and-charges-template'] = [
{ label: 'Setting Up Taxes', url: docsUrl + 'user/manual/en/setting-up/setting-up-taxes' },
]
-frappe.help.help_links['List/Sales Order'] = [
- { label: 'Sales Order', url: docsUrl + 'user/manual/en/selling/sales-order' },
- { label: 'Recurring Sales Order', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' },
- { label: 'Applying Discount', url: docsUrl + 'user/manual/en/selling/articles/applying-discount' },
-]
-
-frappe.help.help_links['Form/Sales Order'] = [
+frappe.help.help_links['sales-order'] = [
{ label: 'Sales Order', url: docsUrl + 'user/manual/en/selling/sales-order' },
{ label: 'Recurring Sales Order', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' },
{ label: 'Applying Discount', url: docsUrl + 'user/manual/en/selling/articles/applying-discount' },
@@ -164,43 +154,34 @@
{ label: 'Applying Margin', url: docsUrl + 'user/manual/en/selling/articles/adding-margin' },
]
-frappe.help.help_links['Form/Product Bundle'] = [
+frappe.help.help_links['product-bundle'] = [
{ label: 'Product Bundle', url: docsUrl + 'user/manual/en/selling/setup/product-bundle' },
]
-frappe.help.help_links['Form/Selling Settings'] = [
+frappe.help.help_links['selling-settings'] = [
{ label: 'Selling Settings', url: docsUrl + 'user/manual/en/selling/setup/selling-settings' },
]
//Buying
-frappe.help.help_links['List/Supplier'] = [
+frappe.help.help_links['supplier'] = [
{ label: 'Supplier', url: docsUrl + 'user/manual/en/buying/supplier' },
]
-frappe.help.help_links['Form/Supplier'] = [
- { label: 'Supplier', url: docsUrl + 'user/manual/en/buying/supplier' },
-]
-
-frappe.help.help_links['Form/Request for Quotation'] = [
+frappe.help.help_links['request-for-quotation'] = [
{ label: 'Request for Quotation', url: docsUrl + 'user/manual/en/buying/request-for-quotation' },
{ label: 'RFQ Video', url: docsUrl + 'user/videos/learn/request-for-quotation.html' },
]
-frappe.help.help_links['Form/Supplier Quotation'] = [
+frappe.help.help_links['supplier-quotation'] = [
{ label: 'Supplier Quotation', url: docsUrl + 'user/manual/en/buying/supplier-quotation' },
]
-frappe.help.help_links['Form/Buying Settings'] = [
+frappe.help.help_links['buying-settings'] = [
{ label: 'Buying Settings', url: docsUrl + 'user/manual/en/buying/setup/buying-settings' },
]
-frappe.help.help_links['List/Purchase Order'] = [
- { label: 'Purchase Order', url: docsUrl + 'user/manual/en/buying/purchase-order' },
- { label: 'Recurring Purchase Order', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' },
-]
-
-frappe.help.help_links['Form/Purchase Order'] = [
+frappe.help.help_links['purchase-order'] = [
{ label: 'Purchase Order', url: docsUrl + 'user/manual/en/buying/purchase-order' },
{ label: 'Item UoM', url: docsUrl + 'user/manual/en/buying/articles/purchasing-in-different-unit' },
{ label: 'Supplier Item Code', url: docsUrl + 'user/manual/en/buying/articles/maintaining-suppliers-part-no-in-item' },
@@ -208,44 +189,44 @@
{ label: 'Subcontracting', url: docsUrl + 'user/manual/en/manufacturing/subcontracting' },
]
-frappe.help.help_links['List/Purchase Taxes and Charges Template'] = [
+frappe.help.help_links['purchase-taxes-and-charges-template'] = [
{ label: 'Setting Up Taxes', url: docsUrl + 'user/manual/en/setting-up/setting-up-taxes' },
]
-frappe.help.help_links['List/POS Profile'] = [
+frappe.help.help_links['pos-profile'] = [
{ label: 'POS Profile', url: docsUrl + 'user/manual/en/setting-up/pos-setting' },
]
-frappe.help.help_links['List/Price List'] = [
+frappe.help.help_links['price-list'] = [
{ label: 'Price List', url: docsUrl + 'user/manual/en/setting-up/price-lists' },
]
-frappe.help.help_links['List/Authorization Rule'] = [
+frappe.help.help_links['authorization-rule'] = [
{ label: 'Authorization Rule', url: docsUrl + 'user/manual/en/setting-up/authorization-rule' },
]
-frappe.help.help_links['Form/SMS Settings'] = [
+frappe.help.help_links['sms-settings'] = [
{ label: 'SMS Settings', url: docsUrl + 'user/manual/en/setting-up/sms-setting' },
]
-frappe.help.help_links['List/Stock Reconciliation'] = [
+frappe.help.help_links['stock-reconciliation'] = [
{ label: 'Stock Reconciliation', url: docsUrl + 'user/manual/en/setting-up/stock-reconciliation-for-non-serialized-item' },
]
-frappe.help.help_links['Tree/Territory'] = [
+frappe.help.help_links['territory/view/tree'] = [
{ label: 'Territory', url: docsUrl + 'user/manual/en/setting-up/territory' },
]
-frappe.help.help_links['Form/Dropbox Backup'] = [
+frappe.help.help_links['dropbox-backup'] = [
{ label: 'Dropbox Backup', url: docsUrl + 'user/manual/en/setting-up/third-party-backups' },
{ label: 'Setting Up Dropbox Backup', url: docsUrl + 'user/manual/en/setting-up/articles/setting-up-dropbox-backups' },
]
-frappe.help.help_links['List/Workflow'] = [
+frappe.help.help_links['workflow'] = [
{ label: 'Workflow', url: docsUrl + 'user/manual/en/setting-up/workflows' },
]
-frappe.help.help_links['List/Company'] = [
+frappe.help.help_links['company'] = [
{ label: 'Company', url: docsUrl + 'user/manual/en/setting-up/company-setup' },
{ label: 'Managing Multiple Companies', url: docsUrl + 'user/manual/en/setting-up/articles/managing-multiple-companies' },
{ label: 'Delete All Related Transactions for a Company', url: docsUrl + 'user/manual/en/setting-up/articles/delete-a-company-and-all-related-transactions' },
@@ -253,25 +234,25 @@
//Accounts
-frappe.help.help_links['modules/Accounts'] = [
+frappe.help.help_links['accounts'] = [
{ label: 'Introduction to Accounts', url: docsUrl + 'user/manual/en/accounts/' },
{ label: 'Chart of Accounts', url: docsUrl + 'user/manual/en/accounts/chart-of-accounts.html' },
{ label: 'Multi Currency Accounting', url: docsUrl + 'user/manual/en/accounts/multi-currency-accounting' },
]
-frappe.help.help_links['Tree/Account'] = [
+frappe.help.help_links['account/view/tree'] = [
{ label: 'Chart of Accounts', url: docsUrl + 'user/manual/en/accounts/chart-of-accounts' },
{ label: 'Managing Tree Mastes', url: docsUrl + 'user/manual/en/setting-up/articles/managing-tree-structure-masters' },
]
-frappe.help.help_links['Form/Sales Invoice'] = [
+frappe.help.help_links['sales-invoice'] = [
{ label: 'Sales Invoice', url: docsUrl + 'user/manual/en/accounts/sales-invoice' },
{ label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' },
{ label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' },
{ label: 'Recurring Sales Invoice', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' },
]
-frappe.help.help_links['List/Sales Invoice'] = [
+frappe.help.help_links['sales-invoice'] = [
{ label: 'Sales Invoice', url: docsUrl + 'user/manual/en/accounts/sales-invoice' },
{ label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' },
{ label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' },
@@ -282,43 +263,43 @@
{ label: 'Point of Sale Invoice', url: docsUrl + 'user/manual/en/accounts/point-of-sale-pos-invoice' },
]
-frappe.help.help_links['List/POS Profile'] = [
+frappe.help.help_links['pos-profile'] = [
{ label: 'Point of Sale Profile', url: docsUrl + 'user/manual/en/setting-up/pos-setting' },
]
-frappe.help.help_links['List/Purchase Invoice'] = [
+frappe.help.help_links['purchase-invoice'] = [
{ label: 'Purchase Invoice', url: docsUrl + 'user/manual/en/accounts/purchase-invoice' },
{ label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' },
{ label: 'Recurring Purchase Invoice', url: docsUrl + 'user/manual/en/accounts/recurring-orders-and-invoices' },
]
-frappe.help.help_links['List/Journal Entry'] = [
+frappe.help.help_links['journal-entry'] = [
{ label: 'Journal Entry', url: docsUrl + 'user/manual/en/accounts/journal-entry' },
{ label: 'Advance Payment Entry', url: docsUrl + 'user/manual/en/accounts/advance-payment-entry' },
{ label: 'Accounts Opening Balance', url: docsUrl + 'user/manual/en/accounts/opening-accounts' },
]
-frappe.help.help_links['List/Payment Entry'] = [
+frappe.help.help_links['payment-entry'] = [
{ label: 'Payment Entry', url: docsUrl + 'user/manual/en/accounts/payment-entry' },
]
-frappe.help.help_links['List/Payment Request'] = [
+frappe.help.help_links['payment-request'] = [
{ label: 'Payment Request', url: docsUrl + 'user/manual/en/accounts/payment-request' },
]
-frappe.help.help_links['List/Asset'] = [
+frappe.help.help_links['asset'] = [
{ label: 'Managing Fixed Assets', url: docsUrl + 'user/manual/en/accounts/managing-fixed-assets' },
]
-frappe.help.help_links['List/Asset Category'] = [
+frappe.help.help_links['asset-category'] = [
{ label: 'Asset Category', url: docsUrl + 'user/manual/en/accounts/managing-fixed-assets' },
]
-frappe.help.help_links['Tree/Cost Center'] = [
+frappe.help.help_links['cost-center/view/tree'] = [
{ label: 'Budgeting', url: docsUrl + 'user/manual/en/accounts/budgeting' },
]
-frappe.help.help_links['List/Item'] = [
+frappe.help.help_links['item'] = [
{ label: 'Item', url: docsUrl + 'user/manual/en/stock/item' },
{ label: 'Item Price', url: docsUrl + 'user/manual/en/stock/item/item-price' },
{ label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' },
@@ -329,61 +310,42 @@
{ label: 'Item Valuation', url: docsUrl + 'user/manual/en/stock/item/item-valuation-fifo-and-moving-average' },
]
-frappe.help.help_links['Form/Item'] = [
- { label: 'Item', url: docsUrl + 'user/manual/en/stock/item' },
- { label: 'Item Price', url: docsUrl + 'user/manual/en/stock/item/item-price' },
- { label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' },
- { label: 'Item Wise Taxation', url: docsUrl + 'user/manual/en/accounts/item-wise-taxation' },
- { label: 'Managing Fixed Assets', url: docsUrl + 'user/manual/en/accounts/managing-fixed-assets' },
- { label: 'Item Codification', url: docsUrl + 'user/manual/en/stock/item/item-codification' },
- { label: 'Item Variants', url: docsUrl + 'user/manual/en/stock/item/item-variants' },
- { label: 'Item Valuation', url: docsUrl + 'user/manual/en/stock/item/item-valuation-fifo-and-moving-average' },
-]
-
-frappe.help.help_links['List/Purchase Receipt'] = [
+frappe.help.help_links['purchase-receipt'] = [
{ label: 'Purchase Receipt', url: docsUrl + 'user/manual/en/stock/purchase-receipt' },
{ label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' },
]
-frappe.help.help_links['List/Delivery Note'] = [
+frappe.help.help_links['delivery-note'] = [
{ label: 'Delivery Note', url: docsUrl + 'user/manual/en/stock/delivery-note' },
{ label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' },
{ label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' },
]
-frappe.help.help_links['Form/Delivery Note'] = [
+frappe.help.help_links['delivery-note'] = [
{ label: 'Delivery Note', url: docsUrl + 'user/manual/en/stock/delivery-note' },
{ label: 'Sales Return', url: docsUrl + 'user/manual/en/stock/sales-return' },
{ label: 'Barcode', url: docsUrl + 'user/manual/en/stock/articles/track-items-using-barcode' },
{ label: 'Subcontracting', url: docsUrl + 'user/manual/en/manufacturing/subcontracting' },
]
-frappe.help.help_links['List/Installation Note'] = [
+frappe.help.help_links['installation-note'] = [
{ label: 'Installation Note', url: docsUrl + 'user/manual/en/stock/installation-note' },
]
-frappe.help.help_links['Tree'] = [
- { label: 'Managing Tree Structure Masters', url: docsUrl + 'user/manual/en/setting-up/articles/managing-tree-structure-masters' },
-]
-frappe.help.help_links['List/Budget'] = [
+frappe.help.help_links['budget'] = [
{ label: 'Budgeting', url: docsUrl + 'user/manual/en/accounts/budgeting' },
]
//Stock
-frappe.help.help_links['List/Material Request'] = [
+frappe.help.help_links['material-request'] = [
{ label: 'Material Request', url: docsUrl + 'user/manual/en/stock/material-request' },
{ label: 'Auto-creation of Material Request', url: docsUrl + 'user/manual/en/stock/articles/auto-creation-of-material-request' },
]
-frappe.help.help_links['Form/Material Request'] = [
- { label: 'Material Request', url: docsUrl + 'user/manual/en/stock/material-request' },
- { label: 'Auto-creation of Material Request', url: docsUrl + 'user/manual/en/stock/articles/auto-creation-of-material-request' },
-]
-
-frappe.help.help_links['Form/Stock Entry'] = [
+frappe.help.help_links['stock-entry'] = [
{ label: 'Stock Entry', url: docsUrl + 'user/manual/en/stock/stock-entry' },
{ label: 'Stock Entry Types', url: docsUrl + 'user/manual/en/stock/articles/stock-entry-purpose' },
{ label: 'Repack Entry', url: docsUrl + 'user/manual/en/stock/articles/repack-entry' },
@@ -391,136 +353,114 @@
{ label: 'Subcontracting', url: docsUrl + 'user/manual/en/manufacturing/subcontracting' },
]
-frappe.help.help_links['List/Stock Entry'] = [
- { label: 'Stock Entry', url: docsUrl + 'user/manual/en/stock/stock-entry' },
-]
-
-frappe.help.help_links['Tree/Warehouse'] = [
+frappe.help.help_links['warehouse/view/tree'] = [
{ label: 'Warehouse', url: docsUrl + 'user/manual/en/stock/warehouse' },
]
-frappe.help.help_links['List/Serial No'] = [
+frappe.help.help_links['serial-no'] = [
{ label: 'Serial No', url: docsUrl + 'user/manual/en/stock/serial-no' },
]
-frappe.help.help_links['Form/Serial No'] = [
- { label: 'Serial No', url: docsUrl + 'user/manual/en/stock/serial-no' },
-]
-
-frappe.help.help_links['Form/Batch'] = [
+frappe.help.help_links['batch'] = [
{ label: 'Batch', url: docsUrl + 'user/manual/en/stock/batch' },
]
-frappe.help.help_links['Form/Packing Slip'] = [
+frappe.help.help_links['packing-slip'] = [
{ label: 'Packing Slip', url: docsUrl + 'user/manual/en/stock/tools/packing-slip' },
]
-frappe.help.help_links['Form/Quality Inspection'] = [
+frappe.help.help_links['quality-inspection'] = [
{ label: 'Quality Inspection', url: docsUrl + 'user/manual/en/stock/tools/quality-inspection' },
]
-frappe.help.help_links['Form/Landed Cost Voucher'] = [
+frappe.help.help_links['landed-cost-voucher'] = [
{ label: 'Landed Cost Voucher', url: docsUrl + 'user/manual/en/stock/tools/landed-cost-voucher' },
]
-frappe.help.help_links['Tree/Item Group'] = [
+frappe.help.help_links['item-group/view/tree'] = [
{ label: 'Item Group', url: docsUrl + 'user/manual/en/stock/setup/item-group' },
]
-frappe.help.help_links['Form/Item Attribute'] = [
+frappe.help.help_links['item-attribute'] = [
{ label: 'Item Attribute', url: docsUrl + 'user/manual/en/stock/setup/item-attribute' },
]
-frappe.help.help_links['Form/UOM'] = [
+frappe.help.help_links['uom'] = [
{ label: 'Fractions in UOM', url: docsUrl + 'user/manual/en/stock/articles/managing-fractions-in-uom' },
]
-frappe.help.help_links['Form/Stock Reconciliation'] = [
+frappe.help.help_links['stock-reconciliation'] = [
{ label: 'Opening Stock Entry', url: docsUrl + 'user/manual/en/stock/opening-stock' },
]
//CRM
-frappe.help.help_links['Form/Lead'] = [
+frappe.help.help_links['lead'] = [
{ label: 'Lead', url: docsUrl + 'user/manual/en/CRM/lead' },
]
-frappe.help.help_links['Form/Opportunity'] = [
+frappe.help.help_links['opportunity'] = [
{ label: 'Opportunity', url: docsUrl + 'user/manual/en/CRM/opportunity' },
]
-frappe.help.help_links['Form/Address'] = [
+frappe.help.help_links['address'] = [
{ label: 'Address', url: docsUrl + 'user/manual/en/CRM/address' },
]
-frappe.help.help_links['Form/Contact'] = [
+frappe.help.help_links['contact'] = [
{ label: 'Contact', url: docsUrl + 'user/manual/en/CRM/contact' },
]
-frappe.help.help_links['Form/Newsletter'] = [
+frappe.help.help_links['newsletter'] = [
{ label: 'Newsletter', url: docsUrl + 'user/manual/en/CRM/newsletter' },
]
-frappe.help.help_links['Form/Campaign'] = [
+frappe.help.help_links['campaign'] = [
{ label: 'Campaign', url: docsUrl + 'user/manual/en/CRM/setup/campaign' },
]
-frappe.help.help_links['Tree/Sales Person'] = [
+frappe.help.help_links['sales-person/view/tree'] = [
{ label: 'Sales Person', url: docsUrl + 'user/manual/en/CRM/setup/sales-person' },
]
-frappe.help.help_links['Form/Sales Person'] = [
+frappe.help.help_links['sales-person'] = [
{ label: 'Sales Person Target', url: docsUrl + 'user/manual/en/selling/setup/sales-person-target-allocation' },
]
-//Support
-
-frappe.help.help_links['List/Feedback Trigger'] = [
- { label: 'Feedback Trigger', url: docsUrl + 'user/manual/en/setting-up/feedback/setting-up-feedback' },
-]
-
-frappe.help.help_links['List/Feedback Request'] = [
- { label: 'Feedback Request', url: docsUrl + 'user/manual/en/setting-up/feedback/submit-feedback' },
-]
-
-frappe.help.help_links['List/Feedback Request'] = [
- { label: 'Feedback Request', url: docsUrl + 'user/manual/en/setting-up/feedback/submit-feedback' },
-]
-
//Manufacturing
-frappe.help.help_links['Form/BOM'] = [
+frappe.help.help_links['bom'] = [
{ label: 'Bill of Material', url: docsUrl + 'user/manual/en/manufacturing/bill-of-materials' },
{ label: 'Nested BOM Structure', url: docsUrl + 'user/manual/en/manufacturing/articles/nested-bom-structure' },
]
-frappe.help.help_links['Form/Work Order'] = [
+frappe.help.help_links['work-order'] = [
{ label: 'Work Order', url: docsUrl + 'user/manual/en/manufacturing/work-order' },
]
-frappe.help.help_links['Form/Workstation'] = [
+frappe.help.help_links['workstation'] = [
{ label: 'Workstation', url: docsUrl + 'user/manual/en/manufacturing/workstation' },
]
-frappe.help.help_links['Form/Operation'] = [
+frappe.help.help_links['operation'] = [
{ label: 'Operation', url: docsUrl + 'user/manual/en/manufacturing/operation' },
]
-frappe.help.help_links['Form/BOM Update Tool'] = [
+frappe.help.help_links['bom-update-tool'] = [
{ label: 'BOM Update Tool', url: docsUrl + 'user/manual/en/manufacturing/tools/bom-update-tool' },
]
//Customize
-frappe.help.help_links['Form/Customize Form'] = [
+frappe.help.help_links['customize-form'] = [
{ label: 'Custom Field', url: docsUrl + 'user/manual/en/customize-erpnext/custom-field' },
{ label: 'Customize Field', url: docsUrl + 'user/manual/en/customize-erpnext/customize-form' },
]
-frappe.help.help_links['Form/Custom Field'] = [
+frappe.help.help_links['custom-field'] = [
{ label: 'Custom Field', url: docsUrl + 'user/manual/en/customize-erpnext/custom-field' },
]
-frappe.help.help_links['Form/Custom Field'] = [
+frappe.help.help_links['custom-field'] = [
{ label: 'Custom Field', url: docsUrl + 'user/manual/en/customize-erpnext/custom-field' },
]
diff --git a/erpnext/public/js/payment/payment_details.html b/erpnext/public/js/payment/payment_details.html
deleted file mode 100644
index 3e63944..0000000
--- a/erpnext/public/js/payment/payment_details.html
+++ /dev/null
@@ -1,11 +0,0 @@
-<div class="row pos-payment-row" type="{{type}}" idx={{idx}}>
- <div class="col-xs-6" style="padding:20px">{{mode_of_payment}}</div>
- <div class="col-xs-6">
- <div class="input-group">
- <input disabled class="form-control text-right amount" idx="{{idx}}" type="text" value="{%= format_currency(amount, currency) %}">
- <span class="input-group-btn">
- <button type="button" class="btn btn-default clr" idx="{{idx}}" style="border:1px solid #d1d8dd">C</button>
- </span>
- </div>
- </div>
-</div>
\ No newline at end of file
diff --git a/erpnext/public/js/payment/pos_payment.html b/erpnext/public/js/payment/pos_payment.html
deleted file mode 100644
index cb6971b..0000000
--- a/erpnext/public/js/payment/pos_payment.html
+++ /dev/null
@@ -1,42 +0,0 @@
-<div class="pos_payment row">
- <div class="row" style="padding: 0px 30px;">
- <h3>{{ __("Total Amount") }}: <span class="label label-default" style="font-size:20px;padding:5px">{%= format_currency(grand_total, currency) %}</span></h3>
- </div>
- <div class="row amount-row">
- <div class="col-xs-6 col-sm-3 text-center">
- <p class="amount-label"> {{ __("Paid") }} <h3 class="paid_amount">{%= format_currency(paid_amount, currency) %}</h3></p>
- </div>
- <div class="col-xs-6 col-sm-3 text-center">
- <p class="amount-label"> {{ __("Outstanding") }} <h3 class="outstanding_amount">{%= format_currency(outstanding_amount, currency) %} </h3></p>
- </div>
- <div class="col-xs-6 col-sm-3 text-center">
- <p class="amount-label"> {{ __("Change") }} <input class="form-control text-right change_amount bold" type="text" idx="change_amount" value="{{format_number(change_amount, null, 2)}}">
- </p>
- </div>
- <div class="col-xs-6 col-sm-3 text-center">
- <p class="amount-label"> {{ __("Write off") }} <input class="form-control text-right write_off_amount bold" type="text" idx="write_off_amount" value="{{format_number(write_off_amount, null, 2)}}">
- </p>
- </div>
- </div>
- <hr>
- <div class="row">
- <div class="col-sm-6 ">
- <div class ="row multimode-payments" style = "margin-right:10px">
- </div>
- </div>
- <div class="col-sm-6 payment-toolbar">
- {% for(var i=0; i<3; i++) { %}
- <div class="row">
- {% for(var j=i*3; j<(i+1)*3; j++) { %}
- <button type="button" class="btn btn-default pos-keyboard-key">{{j+1}}</button>
- {% } %}
- </div>
- {% } %}
- <div class="row">
- <button type="button" class="btn btn-default delete-btn">{{ __("Del") }}</button>
- <button type="button" class="btn btn-default pos-keyboard-key">0</button>
- <button type="button" class="btn btn-default pos-keyboard-key">.</button>
- </div>
- </div>
- </div>
-</div>
diff --git a/erpnext/public/js/pos/clusterize.js b/erpnext/public/js/pos/clusterize.js
deleted file mode 100644
index 075c9ca..0000000
--- a/erpnext/public/js/pos/clusterize.js
+++ /dev/null
@@ -1,330 +0,0 @@
-/* eslint-disable */
-/*! Clusterize.js - v0.17.6 - 2017-03-05
-* http://NeXTs.github.com/Clusterize.js/
-* Copyright (c) 2015 Denis Lukov; Licensed GPLv3 */
-
-;(function(name, definition) {
- if (typeof module != 'undefined') module.exports = definition();
- else if (typeof define == 'function' && typeof define.amd == 'object') define(definition);
- else this[name] = definition();
-}('Clusterize', function() {
- "use strict"
-
- // detect ie9 and lower
- // https://gist.github.com/padolsey/527683#comment-786682
- var ie = (function(){
- for( var v = 3,
- el = document.createElement('b'),
- all = el.all || [];
- el.innerHTML = '<!--[if gt IE ' + (++v) + ']><i><![endif]-->',
- all[0];
- ){}
- return v > 4 ? v : document.documentMode;
- }()),
- is_mac = navigator.platform.toLowerCase().indexOf('mac') + 1;
- var Clusterize = function(data) {
- if( ! (this instanceof Clusterize))
- return new Clusterize(data);
- var self = this;
-
- var defaults = {
- rows_in_block: 50,
- blocks_in_cluster: 4,
- tag: null,
- show_no_data_row: true,
- no_data_class: 'clusterize-no-data',
- no_data_text: 'No data',
- keep_parity: true,
- callbacks: {}
- }
-
- // public parameters
- self.options = {};
- var options = ['rows_in_block', 'blocks_in_cluster', 'show_no_data_row', 'no_data_class', 'no_data_text', 'keep_parity', 'tag', 'callbacks'];
- for(var i = 0, option; option = options[i]; i++) {
- self.options[option] = typeof data[option] != 'undefined' && data[option] != null
- ? data[option]
- : defaults[option];
- }
-
- var elems = ['scroll', 'content'];
- for(var i = 0, elem; elem = elems[i]; i++) {
- self[elem + '_elem'] = data[elem + 'Id']
- ? document.getElementById(data[elem + 'Id'])
- : data[elem + 'Elem'];
- if( ! self[elem + '_elem'])
- throw new Error("Error! Could not find " + elem + " element");
- }
-
- // tabindex forces the browser to keep focus on the scrolling list, fixes #11
- if( ! self.content_elem.hasAttribute('tabindex'))
- self.content_elem.setAttribute('tabindex', 0);
-
- // private parameters
- var rows = isArray(data.rows)
- ? data.rows
- : self.fetchMarkup(),
- cache = {},
- scroll_top = self.scroll_elem.scrollTop;
-
- // append initial data
- self.insertToDOM(rows, cache);
-
- // restore the scroll position
- self.scroll_elem.scrollTop = scroll_top;
-
- // adding scroll handler
- var last_cluster = false,
- scroll_debounce = 0,
- pointer_events_set = false,
- scrollEv = function() {
- // fixes scrolling issue on Mac #3
- if (is_mac) {
- if( ! pointer_events_set) self.content_elem.style.pointerEvents = 'none';
- pointer_events_set = true;
- clearTimeout(scroll_debounce);
- scroll_debounce = setTimeout(function () {
- self.content_elem.style.pointerEvents = 'auto';
- pointer_events_set = false;
- }, 50);
- }
- if (last_cluster != (last_cluster = self.getClusterNum()))
- self.insertToDOM(rows, cache);
- if (self.options.callbacks.scrollingProgress)
- self.options.callbacks.scrollingProgress(self.getScrollProgress());
- },
- resize_debounce = 0,
- resizeEv = function() {
- clearTimeout(resize_debounce);
- resize_debounce = setTimeout(self.refresh, 100);
- }
- on('scroll', self.scroll_elem, scrollEv);
- on('resize', window, resizeEv);
-
- // public methods
- self.destroy = function(clean) {
- off('scroll', self.scroll_elem, scrollEv);
- off('resize', window, resizeEv);
- self.html((clean ? self.generateEmptyRow() : rows).join(''));
- }
- self.refresh = function(force) {
- if(self.getRowsHeight(rows) || force) self.update(rows);
- }
- self.update = function(new_rows) {
- rows = isArray(new_rows)
- ? new_rows
- : [];
- var scroll_top = self.scroll_elem.scrollTop;
- // fixes #39
- if(rows.length * self.options.item_height < scroll_top) {
- self.scroll_elem.scrollTop = 0;
- last_cluster = 0;
- }
- self.insertToDOM(rows, cache);
- self.scroll_elem.scrollTop = scroll_top;
- }
- self.clear = function() {
- self.update([]);
- }
- self.getRowsAmount = function() {
- return rows.length;
- }
- self.getScrollProgress = function() {
- return this.options.scroll_top / (rows.length * this.options.item_height) * 100 || 0;
- }
-
- var add = function(where, _new_rows) {
- var new_rows = isArray(_new_rows)
- ? _new_rows
- : [];
- if( ! new_rows.length) return;
- rows = where == 'append'
- ? rows.concat(new_rows)
- : new_rows.concat(rows);
- self.insertToDOM(rows, cache);
- }
- self.append = function(rows) {
- add('append', rows);
- }
- self.prepend = function(rows) {
- add('prepend', rows);
- }
- }
-
- Clusterize.prototype = {
- constructor: Clusterize,
- // fetch existing markup
- fetchMarkup: function() {
- var rows = [], rows_nodes = this.getChildNodes(this.content_elem);
- while (rows_nodes.length) {
- rows.push(rows_nodes.shift().outerHTML);
- }
- return rows;
- },
- // get tag name, content tag name, tag height, calc cluster height
- exploreEnvironment: function(rows, cache) {
- var opts = this.options;
- opts.content_tag = this.content_elem.tagName.toLowerCase();
- if( ! rows.length) return;
- if(ie && ie <= 9 && ! opts.tag) opts.tag = rows[0].match(/<([^>\s/]*)/)[1].toLowerCase();
- if(this.content_elem.children.length <= 1) cache.data = this.html(rows[0] + rows[0] + rows[0]);
- if( ! opts.tag) opts.tag = this.content_elem.children[0].tagName.toLowerCase();
- this.getRowsHeight(rows);
- },
- getRowsHeight: function(rows) {
- var opts = this.options,
- prev_item_height = opts.item_height;
- opts.cluster_height = 0;
- if( ! rows.length) return;
- var nodes = this.content_elem.children;
- var node = nodes[Math.floor(nodes.length / 2)];
- opts.item_height = node.offsetHeight;
- // consider table's border-spacing
- if(opts.tag == 'tr' && getStyle('borderCollapse', this.content_elem) != 'collapse')
- opts.item_height += parseInt(getStyle('borderSpacing', this.content_elem), 10) || 0;
- // consider margins (and margins collapsing)
- if(opts.tag != 'tr') {
- var marginTop = parseInt(getStyle('marginTop', node), 10) || 0;
- var marginBottom = parseInt(getStyle('marginBottom', node), 10) || 0;
- opts.item_height += Math.max(marginTop, marginBottom);
- }
- opts.block_height = opts.item_height * opts.rows_in_block;
- opts.rows_in_cluster = opts.blocks_in_cluster * opts.rows_in_block;
- opts.cluster_height = opts.blocks_in_cluster * opts.block_height;
- return prev_item_height != opts.item_height;
- },
- // get current cluster number
- getClusterNum: function () {
- this.options.scroll_top = this.scroll_elem.scrollTop;
- return Math.floor(this.options.scroll_top / (this.options.cluster_height - this.options.block_height)) || 0;
- },
- // generate empty row if no data provided
- generateEmptyRow: function() {
- var opts = this.options;
- if( ! opts.tag || ! opts.show_no_data_row) return [];
- var empty_row = document.createElement(opts.tag),
- no_data_content = document.createTextNode(opts.no_data_text), td;
- empty_row.className = opts.no_data_class;
- if(opts.tag == 'tr') {
- td = document.createElement('td');
- // fixes #53
- td.colSpan = 100;
- td.appendChild(no_data_content);
- }
- empty_row.appendChild(td || no_data_content);
- return [empty_row.outerHTML];
- },
- // generate cluster for current scroll position
- generate: function (rows, cluster_num) {
- var opts = this.options,
- rows_len = rows.length;
- if (rows_len < opts.rows_in_block) {
- return {
- top_offset: 0,
- bottom_offset: 0,
- rows_above: 0,
- rows: rows_len ? rows : this.generateEmptyRow()
- }
- }
- var items_start = Math.max((opts.rows_in_cluster - opts.rows_in_block) * cluster_num, 0),
- items_end = items_start + opts.rows_in_cluster,
- top_offset = Math.max(items_start * opts.item_height, 0),
- bottom_offset = Math.max((rows_len - items_end) * opts.item_height, 0),
- this_cluster_rows = [],
- rows_above = items_start;
- if(top_offset < 1) {
- rows_above++;
- }
- for (var i = items_start; i < items_end; i++) {
- rows[i] && this_cluster_rows.push(rows[i]);
- }
- return {
- top_offset: top_offset,
- bottom_offset: bottom_offset,
- rows_above: rows_above,
- rows: this_cluster_rows
- }
- },
- renderExtraTag: function(class_name, height) {
- var tag = document.createElement(this.options.tag),
- clusterize_prefix = 'clusterize-';
- tag.className = [clusterize_prefix + 'extra-row', clusterize_prefix + class_name].join(' ');
- height && (tag.style.height = height + 'px');
- return tag.outerHTML;
- },
- // if necessary verify data changed and insert to DOM
- insertToDOM: function(rows, cache) {
- // explore row's height
- if( ! this.options.cluster_height) {
- this.exploreEnvironment(rows, cache);
- }
- var data = this.generate(rows, this.getClusterNum()),
- this_cluster_rows = data.rows.join(''),
- this_cluster_content_changed = this.checkChanges('data', this_cluster_rows, cache),
- top_offset_changed = this.checkChanges('top', data.top_offset, cache),
- only_bottom_offset_changed = this.checkChanges('bottom', data.bottom_offset, cache),
- callbacks = this.options.callbacks,
- layout = [];
-
- if(this_cluster_content_changed || top_offset_changed) {
- if(data.top_offset) {
- this.options.keep_parity && layout.push(this.renderExtraTag('keep-parity'));
- layout.push(this.renderExtraTag('top-space', data.top_offset));
- }
- layout.push(this_cluster_rows);
- data.bottom_offset && layout.push(this.renderExtraTag('bottom-space', data.bottom_offset));
- callbacks.clusterWillChange && callbacks.clusterWillChange();
- this.html(layout.join(''));
- this.options.content_tag == 'ol' && this.content_elem.setAttribute('start', data.rows_above);
- callbacks.clusterChanged && callbacks.clusterChanged();
- } else if(only_bottom_offset_changed) {
- this.content_elem.lastChild.style.height = data.bottom_offset + 'px';
- }
- },
- // unfortunately ie <= 9 does not allow to use innerHTML for table elements, so make a workaround
- html: function(data) {
- var content_elem = this.content_elem;
- if(ie && ie <= 9 && this.options.tag == 'tr') {
- var div = document.createElement('div'), last;
- div.innerHTML = '<table><tbody>' + data + '</tbody></table>';
- while((last = content_elem.lastChild)) {
- content_elem.removeChild(last);
- }
- var rows_nodes = this.getChildNodes(div.firstChild.firstChild);
- while (rows_nodes.length) {
- content_elem.appendChild(rows_nodes.shift());
- }
- } else {
- content_elem.innerHTML = data;
- }
- },
- getChildNodes: function(tag) {
- var child_nodes = tag.children, nodes = [];
- for (var i = 0, ii = child_nodes.length; i < ii; i++) {
- nodes.push(child_nodes[i]);
- }
- return nodes;
- },
- checkChanges: function(type, value, cache) {
- var changed = value != cache[type];
- cache[type] = value;
- return changed;
- }
- }
-
- // support functions
- function on(evt, element, fnc) {
- return element.addEventListener ? element.addEventListener(evt, fnc, false) : element.attachEvent("on" + evt, fnc);
- }
- function off(evt, element, fnc) {
- return element.removeEventListener ? element.removeEventListener(evt, fnc, false) : element.detachEvent("on" + evt, fnc);
- }
- function isArray(arr) {
- return Object.prototype.toString.call(arr) === '[object Array]';
- }
- function getStyle(prop, elem) {
- return window.getComputedStyle ? window.getComputedStyle(elem)[prop] : elem.currentStyle[prop];
- }
-
- return Clusterize;
-}));
\ No newline at end of file
diff --git a/erpnext/public/js/pos/customer_toolbar.html b/erpnext/public/js/pos/customer_toolbar.html
deleted file mode 100644
index 3ba5ccb..0000000
--- a/erpnext/public/js/pos/customer_toolbar.html
+++ /dev/null
@@ -1,16 +0,0 @@
-<div class="pos-bill-toolbar col-xs-9" style="display: flex; width: 70%;">
- <div class="party-area" style="flex: 1;">
- <span class="edit-customer-btn text-muted" style="display: inline;">
- <a class="btn-open no-decoration" title="Edit Customer">
- <i class="octicon octicon-pencil"></i>
- </a>
- </span>
- </div>
- <button class="btn btn-default list-customers-btn" style="margin-left: 12px">
- <i class="octicon octicon-organization"></i>
- </button>
- </button> {% if (allow_delete) { %}
- <button class="btn btn-default btn-danger" style="margin: 0 5px 0 5px">
- <i class="octicon octicon-trashcan"></i>
- </button> {% } %}
-</div>
\ No newline at end of file
diff --git a/erpnext/public/js/pos/pos.html b/erpnext/public/js/pos/pos.html
deleted file mode 100644
index 89e2940..0000000
--- a/erpnext/public/js/pos/pos.html
+++ /dev/null
@@ -1,136 +0,0 @@
-<div class="pos">
- <div class="row">
- <div class="col-sm-5 pos-bill-wrapper">
- <div class="col-sm-12"><h6 class="form-section-heading uppercase">{{ __("Item Cart") }}</h6></div>
- <div class="pos-bill">
- <div class="item-cart">
- <div class="pos-list-row pos-bill-header text-muted h6">
- <span class="cell subject">
- <!--<input class="list-select-all" type="checkbox" title="{%= __("Select All") %}">-->
- {{ __("Item Name")}}
- </span>
- <span class="cell text-right">{{ __("Quantity") }}</span>
- <span class="cell text-right">{{ __("Discount") }}</span>
- <span class="cell text-right">{{ __("Rate") }}</span>
- </div>
- <div class="item-cart-items">
- <div class="no-items-message text-extra-muted">
- <span class="text-center">
- <i class="fa fa-2x fa-shopping-cart"></i>
- <p>{{ __("Tap items to add them here") }}</p>
- </span>
- </div>
- <div class="items">
- </div>
- </div>
- </div>
- </div>
- <div class="totals-area">
- <div class="pos-list-row net-total-area">
- <div class="cell"></div>
- <div class="cell text-right">{%= __("Net Total") %}</div>
- <div class="cell price-cell bold net-total text-right"></div>
- </div>
- <div class="pos-list-row tax-area">
- <div class="cell"></div>
- <div class="cell text-right">{%= __("Taxes") %}</div>
- <div class="cell price-cell text-right tax-table">
- </div>
- </div>
- {% if(allow_user_to_edit_discount) { %}
- <div class="pos-list-row discount-amount-area">
- <div class="cell"></div>
- <div class="cell text-right">{%= __("Discount") %}</div>
- <div class="cell price-cell discount-field-col">
- <div class="input-group input-group-sm">
- <span class="input-group-addon">%</span>
- <input type="text" class="form-control discount-percentage text-right">
- </div>
- <div class="input-group input-group-sm">
- <span class="input-group-addon">{%= get_currency_symbol(currency) %}</span>
- <input type="text" class="form-control discount-amount text-right" placeholder="{%= 0.00 %}">
- </div>
- </div>
- </div>
- {% } %}
- <div class="pos-list-row grand-total-area collapse-btn" style="border-bottom:1px solid #d1d8dd;">
- <div class="cell">
- <a class="">
- <i class="octicon octicon-chevron-down"></i>
- </a>
- </div>
- <div class="cell text-right bold">{%= __("Grand Total") %}</div>
- <div class="cell price-cell grand-total text-right lead"></div>
- </div>
- <div class="pos-list-row qty-total-area collapse-btn" style="border-bottom:1px solid #d1d8dd;">
- <div class="cell">
- <a class="">
- <i class="octicon octicon-chevron-down"></i>
- </a>
- </div>
- <div class="cell text-right bold">{%= __("Qty Total") %}</div>
- <div class="cell price-cell qty-total text-right lead"></div>
- </div>
- </div>
- <div class="row" style="margin-top: 30px">
- <div class="col-sm-6 selected-item">
-
- </div>
- <div class="col-xs-6 numeric_keypad hidden-xs" style="display:none">
- {% var chartData = ["Qty", "Disc", "Price"] %} {% for(var i=0; i
- <3; i++) { %} <div class="row text-right">
- {% for(var j=i*3; j
- <(i+1)*3; j++) { %} <button type="button" class="btn btn-default numeric-keypad" val="{{j+1}}">{{j+1}}</button>
- {% } %}
- <button type="button" {% if((!allow_user_to_edit_rate && __(chartData[i]) == __("Price")) || (!allow_user_to_edit_discount && __(chartData[i]) == __("Disc"))) { %} disabled {% } %} id="pos-item-{{ chartData[i].toLowerCase() }}" class="btn text-center btn-default numeric-keypad pos-operation">{{ __(chartData[i]) }}</button>
- </div>
- {% } %}
- <div class="row text-right">
- <button type="button" class="btn btn-default numeric-keypad numeric-del">{{ __("Del") }}</button>
- <button type="button" class="btn btn-default numeric-keypad" val="0">0</button>
- <button type="button" class="btn btn-default numeric-keypad" val=".">.</button>
- <button type="button" class="btn btn-primary numeric-keypad pos-pay">{{ __("Pay") }}</button>
- </div>
- </div>
- </div>
- </div>
- <div class="col-sm-5 list-customers">
- <div class="col-sm-12"><h6 class="form-section-heading uppercase">{{ __("Customers in Queue") }}</h6></div>
- <div class="pos-list-row pos-bill-header">
- <div class="cell subject"><input class="list-select-all" type="checkbox">{{ __("Customer") }}</div>
- <div class="cell text-left">{{ __("Status") }}</div>
- <div class="cell text-right">{{ __("Amount") }}</div>
- <div class="cell text-right">{{ __("Grand Total") }}</div>
- </div>
- <div class="list-customers-table border-left border-right border-bottom">
- <div class="no-items-message text-extra-muted">
- <span class="text-center">
- <i class="fa fa-2x fa-user"></i>
- <p>{{ __("No Customers yet!") }}</p>
- </span>
- </div>
- </div>
- </div>
- <div class="col-sm-7 pos-items-section">
- <div class="col-sm-12"><h6 class="form-section-heading uppercase">{{ __("Stock Items") }}</h6></div>
- <div class="row pos-item-area">
-
- </div>
- <span id="customer-results" style="color:#68a;"></span>
- <div class="item-list-area">
- <div class="pos-list-row pos-bill-header text-muted h6">
- <div class="cell subject search-item-group">
- <div class="dropdown">
- <a class="text-muted dropdown-toggle" data-toggle="dropdown"><span class="dropdown-text">{{ __("All Item Groups") }}</span><i class="caret"></i></a>
- <ul class="dropdown-menu">
- </ul>
- </div>
- </div>
- <div class="cell search-item"></div>
- </div>
- <div class="app-listing item-list image-view-container">
-
- </div>
- </div>
- </div>
-</div>
diff --git a/erpnext/public/js/pos/pos_bill_item.html b/erpnext/public/js/pos/pos_bill_item.html
deleted file mode 100644
index 21868a6..0000000
--- a/erpnext/public/js/pos/pos_bill_item.html
+++ /dev/null
@@ -1,34 +0,0 @@
-<div class="row pos-bill-row pos-bill-item" data-item-code="{%= item_code %}">
- <div class="col-xs-4"><h6>{%= item_code || "" %}{%= __(item_name) || "" %}</h6></div>
- <div class="col-xs-3">
- <div class="row pos-qty-row">
- <div class="col-xs-2 text-center pos-qty-btn" data-action="decrease-qty"><i class="fa fa-minus text-muted" style="font-size:12px"></i></div>
- <div class="col-xs-8">
- <div>
- <input type="tel" value="{%= qty %}" class="form-control pos-item-qty text-right">
- </div>
- {% if(actual_qty != null) { %}
- <div style="margin-top: 5px;" class="text-muted small text-right">
- {%= __("In Stock: ") %} <span>{%= actual_qty || 0.0 %}</span>
- </div>
- {% } %}
- </div>
- <div class="col-xs-2 text-center pos-qty-btn" data-action="increase-qty"><i class="fa fa-plus text-muted" style="font-size:12px"></i></div>
- </div>
- </div>
- <div class="col-xs-2 text-right">
- <div class="row input-sm">
- <input type="tel" value="{%= discount_percentage %}" class="form-control text-right pos-item-disc">
- </div>
- </div>
- <div class="col-xs-3 text-right">
- <div class="text-muted" style="margin-top: 5px;">
- {% if(enabled) { %}
- <input type="tel" value="{%= rate %}" class="form-control input-sm pos-item-price text-right">
- {% } else { %}
- <h6>{%= format_currency(rate) %}</h6>
- {% } %}
- </div>
- <p><h6>{%= amount %}</h6></p>
- </div>
-</div>
diff --git a/erpnext/public/js/pos/pos_bill_item_new.html b/erpnext/public/js/pos/pos_bill_item_new.html
deleted file mode 100644
index cb626ce..0000000
--- a/erpnext/public/js/pos/pos_bill_item_new.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<div class="pos-list-row pos-bill-item {{ selected_class }}" data-item-code="{{ item_code }}">
- <div class="cell subject">
- <!--<input class="list-row-checkbox" type="checkbox" data-name="{{item_code}}">-->
- <a class="grey list-id" title="{{ item_name }}">{{ strip_html(__(item_name)) || item_code }}</a>
- </div>
- <div class="cell text-right">{%= qty %}</div>
- <div class="cell text-right">{%= discount_percentage %}</div>
- <div class="cell text-right">{%= format_currency(rate) %}</div>
-</div>
diff --git a/erpnext/public/js/pos/pos_invoice_list.html b/erpnext/public/js/pos/pos_invoice_list.html
deleted file mode 100644
index 13aa520..0000000
--- a/erpnext/public/js/pos/pos_invoice_list.html
+++ /dev/null
@@ -1,9 +0,0 @@
-<div class="pos-list-row" invoice-name = "{{name}}">
- <div class="list-column cell subject" invoice-name = "{{name}}">
- <input class="list-delete text-left" type="checkbox" style = "margin-right:5px">
- <a class="grey list-id text-left customer-row" title="{{ customer }}">{%= customer %}</a>
- </div>
- <div class="list-column cell text-left customer-row"><span class="indicator {{data.indicator}}">{{ data.status }}</span></div>
- <div class="list-column cell text-right customer-row">{%= paid_amount %}</div>
- <div class="list-column cell text-right customer-row">{%= grand_total %}</div>
-</div>
diff --git a/erpnext/public/js/pos/pos_item.html b/erpnext/public/js/pos/pos_item.html
deleted file mode 100755
index 52f3cf6..0000000
--- a/erpnext/public/js/pos/pos_item.html
+++ /dev/null
@@ -1,32 +0,0 @@
-<div class="pos-item-wrapper image-view-item" data-item-code="{{item_code}}">
- <div class="image-view-header doclist-row">
- <div class="list-value">
- <a class="grey list-id" data-name="{{item_code}}" title="{{ item_name || item_code}}">{{item_name || item_code}}<br>({{ __(item_stock) }})</a>
- </div>
- </div>
- <div class="image-view-body">
- <a data-item-code="{{ item_code }}"
- title="{{ item_name || item_code }}"
- >
- <div class="image-field"
- style="
- {% if (!item_image) { %}
- background-color: #fafbfc;
- {% } %}
- border: 0px;"
- >
- {% if (!item_image) { %}
- <span class="placeholder-text">
- {%= frappe.get_abbr(item_name || item_code) %}
- </span>
- {% } %}
- {% if (item_image) { %}
- <img src="{{ item_image }}" alt="{{item_name || item_code}}">
- {% } %}
- </div>
- <span class="price-info">
- {{item_price}} / {{item_uom}}
- </span>
- </a>
- </div>
-</div>
\ No newline at end of file
diff --git a/erpnext/public/js/pos/pos_selected_item.html b/erpnext/public/js/pos/pos_selected_item.html
deleted file mode 100644
index 03c7341..0000000
--- a/erpnext/public/js/pos/pos_selected_item.html
+++ /dev/null
@@ -1,22 +0,0 @@
-<div class="pos-selected-item-action" data-item-code="{%= item_code %}" data-idx="{%= idx %}">
- <div class="pos-list-row">
- <div class="cell">{{ __("Quantity") }}:</div>
- <input type="tel" class="form-control cell pos-item-qty" value="{%= qty %}"/>
- </div>
- <div class="pos-list-row">
- <div class="cell">{{ __("Price List Rate") }}:</div>
- <input type="tel" class="form-control cell" disabled value="{%= price_list_rate %}"/>
- </div>
- <div class="pos-list-row">
- <div class="cell">{{ __("Discount") }}: %</div>
- <input type="tel" class="form-control cell pos-item-disc" {% if !allow_user_to_edit_discount %} disabled {% endif %} value="{%= discount_percentage %}">
- </div>
- <div class="pos-list-row">
- <div class="cell">{{ __("Price") }}:</div>
- <input type="tel" class="form-control cell pos-item-price" {% if !allow_user_to_edit_rate %} disabled {% endif %} value="{%= rate %}"/>
- </div>
- <div class="pos-list-row">
- <div class="cell">{{ __("Amount") }}:</div>
- <input type="tel" class="form-control cell pos-amount" disabled value="{%= amount %}"/>
- </div>
-</div>
\ No newline at end of file
diff --git a/erpnext/public/js/pos/pos_tax_row.html b/erpnext/public/js/pos/pos_tax_row.html
deleted file mode 100644
index 3752a89..0000000
--- a/erpnext/public/js/pos/pos_tax_row.html
+++ /dev/null
@@ -1,4 +0,0 @@
-<div class="pos-list-row" style="padding-right: 0;">
- <div class="cell">{%= description %}</div>
- <div class="cell text-right bold">{%= tax_amount %}</div>
-</div>
diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js
index 560a561..b635adc 100644
--- a/erpnext/public/js/queries.js
+++ b/erpnext/public/js/queries.js
@@ -115,7 +115,26 @@
["Warehouse", "is_group", "=",0]
]
- }
+ };
+ },
+
+ get_filtered_dimensions: function(doc, child_fields, dimension, company) {
+ let account = '';
+
+ child_fields.forEach((field) => {
+ if (!account) {
+ account = doc[field];
+ }
+ });
+
+ return {
+ query: "erpnext.controllers.queries.get_filtered_dimensions",
+ filters: {
+ 'dimension': dimension,
+ 'account': account,
+ 'company': company
+ }
+ };
}
});
diff --git a/erpnext/public/js/setup_wizard.js b/erpnext/public/js/setup_wizard.js
index 092f839..ef03b01 100644
--- a/erpnext/public/js/setup_wizard.js
+++ b/erpnext/public/js/setup_wizard.js
@@ -127,11 +127,9 @@
options: "", fieldtype: 'Select'
},
{ fieldname: 'view_coa', label: __('View Chart of Accounts'), fieldtype: 'Button' },
-
- { fieldtype: "Section Break", label: __('Financial Year') },
- { fieldname: 'fy_start_date', label: __('Start Date'), fieldtype: 'Date', reqd: 1 },
- { fieldtype: "Column Break" },
- { fieldname: 'fy_end_date', label: __('End Date'), fieldtype: 'Date', reqd: 1 },
+ { fieldname: 'fy_start_date', label: __('Financial Year Begins On'), fieldtype: 'Date', reqd: 1 },
+ // end date should be hidden (auto calculated)
+ { fieldname: 'fy_end_date', label: __('End Date'), fieldtype: 'Date', reqd: 1, hidden: 1 },
],
onload: function (slide) {
diff --git a/erpnext/public/js/telephony.js b/erpnext/public/js/telephony.js
index bd7f890..b66126c 100644
--- a/erpnext/public/js/telephony.js
+++ b/erpnext/public/js/telephony.js
@@ -1,17 +1,20 @@
frappe.ui.form.ControlData = frappe.ui.form.ControlData.extend( {
make_input() {
- this._super();
+ if (!this.df.read_only) {
+ this._super();
+ }
if (this.df.options == 'Phone') {
this.setup_phone();
}
},
setup_phone() {
if (frappe.phone_call.handler) {
- this.$wrapper.find('.control-input')
+ let control = this.df.read_only ? '.control-value' : '.control-input';
+ this.$wrapper.find(control)
.append(`
<span class="phone-btn">
<a class="btn-open no-decoration" title="${__('Make a call')}">
- <i class="fa fa-phone"></i></a>
+ ${frappe.utils.icon('call')}
</span>
`)
.find('.phone-btn')
@@ -20,4 +23,4 @@
});
}
}
-});
\ No newline at end of file
+});
diff --git a/erpnext/public/js/templates/call_link.html b/erpnext/public/js/templates/call_link.html
new file mode 100644
index 0000000..071078c
--- /dev/null
+++ b/erpnext/public/js/templates/call_link.html
@@ -0,0 +1,42 @@
+<div class="call-detail-wrapper">
+ <div class="head flex justify-between">
+ <div>
+ <span class="bold"> {{ type }} Call</span>
+ {% if (duration) %}
+ <span class="text-muted"> • {{ frappe.format(duration, { fieldtype: "Duration" }) }}</span>
+ {% endif %}
+ <span class="text-muted"> • {{ comment_when(creation) }}</span>
+ </div>
+ <span>
+ <a class="action-btn" href="/app/call-log/{{ name }}" title="{{ __("Open Call Log") }}">
+ <svg class="icon icon-sm">
+ <use href="#icon-link-url" class="like-icon"></use>
+ </svg>
+ </a>
+ </span>
+ </div>
+
+
+ <div class="body pt-3">
+ {% if (type === "Incoming") { %}
+ <span> Incoming call from {{ from }}, received by {{ to }}</span>
+ {% } else { %}
+ <span> Outgoing Call made by {{ from }} to {{ to }}</span>
+ {% } %}
+ <div class="summary pt-3">
+ {% if (summary) { %}
+ <i>{{ summary }}</i>
+ {% } else { %}
+ <i class="text-muted">{{ __("No Summary") }}</i>
+ {% } %}
+ </div>
+ {% if (recording_url) { %}
+ <div class="margin-top">
+ <audio
+ controls
+ src="{{ recording_url }}">
+ </audio>
+ </div>
+ {% } %}
+ </div>
+</div>
diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js
index 891bbe5..e5bd4d7 100755
--- a/erpnext/public/js/utils.js
+++ b/erpnext/public/js/utils.js
@@ -194,15 +194,21 @@
add_dimensions: function(report_name, index) {
let filters = frappe.query_reports[report_name].filters;
- erpnext.dimension_filters.forEach((dimension) => {
- let found = filters.some(el => el.fieldname === dimension['fieldname']);
+ frappe.call({
+ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
+ callback: function(r) {
+ let accounting_dimensions = r.message[0];
+ accounting_dimensions.forEach((dimension) => {
+ let found = filters.some(el => el.fieldname === dimension['fieldname']);
- if (!found) {
- filters.splice(index, 0 ,{
- "fieldname": dimension["fieldname"],
- "label": __(dimension["label"]),
- "fieldtype": "Link",
- "options": dimension["document_type"]
+ if (!found) {
+ filters.splice(index, 0, {
+ "fieldname": dimension["fieldname"],
+ "label": __(dimension["label"]),
+ "fieldtype": "Link",
+ "options": dimension["document_type"]
+ });
+ }
});
}
});
@@ -507,6 +513,7 @@
}, {
fieldtype:'Currency',
fieldname:"rate",
+ options: "currency",
default: 0,
read_only: 0,
in_list_view: 1,
diff --git a/erpnext/public/js/utils/dimension_tree_filter.js b/erpnext/public/js/utils/dimension_tree_filter.js
index b6720c0..96e1817 100644
--- a/erpnext/public/js/utils/dimension_tree_filter.js
+++ b/erpnext/public/js/utils/dimension_tree_filter.js
@@ -1,54 +1,83 @@
-frappe.provide('frappe.ui.form');
+frappe.provide('erpnext.accounts');
-let default_dimensions = {};
+erpnext.accounts.dimensions = {
+ setup_dimension_filters(frm, doctype) {
+ this.accounting_dimensions = [];
+ this.default_dimensions = {};
+ this.fetch_custom_dimensions(frm, doctype);
+ },
-let doctypes_with_dimensions = ["GL Entry", "Sales Invoice", "Purchase Invoice", "Payment Entry", "Asset",
- "Expense Claim", "Stock Entry", "Budget", "Payroll Entry", "Delivery Note", "Shipping Rule", "Loyalty Program",
- "Fee Schedule", "Fee Structure", "Stock Reconciliation", "Travel Request", "Fees", "POS Profile", "Opening Invoice Creation Tool",
- "Subscription", "Purchase Order", "Journal Entry", "Material Request", "Purchase Receipt", "Landed Cost Item", "Asset"];
+ fetch_custom_dimensions(frm, doctype) {
+ let me = this;
+ frappe.call({
+ method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimensions",
+ args: {
+ 'with_cost_center_and_project': true
+ },
+ callback: function(r) {
+ me.accounting_dimensions = r.message[0];
+ me.default_dimensions = r.message[1];
+ me.setup_filters(frm, doctype);
+ }
+ });
+ },
-let child_docs = ["Sales Invoice Item", "Purchase Invoice Item", "Purchase Order Item", "Journal Entry Account",
- "Material Request Item", "Delivery Note Item", "Purchase Receipt Item", "Stock Entry Detail", "Payment Entry Deduction",
- "Landed Cost Item", "Asset Value Adjustment", "Opening Invoice Creation Tool Item", "Subscription Plan"];
-
-frappe.call({
- method: "erpnext.accounts.doctype.accounting_dimension.accounting_dimension.get_dimension_filters",
- callback: function(r) {
- erpnext.dimension_filters = r.message[0];
- default_dimensions = r.message[1];
- }
-});
-
-doctypes_with_dimensions.forEach((doctype) => {
- frappe.ui.form.on(doctype, {
- onload: function(frm) {
- erpnext.dimension_filters.forEach((dimension) => {
+ setup_filters(frm, doctype) {
+ if (this.accounting_dimensions) {
+ this.accounting_dimensions.forEach((dimension) => {
frappe.model.with_doctype(dimension['document_type'], () => {
- if(frappe.meta.has_field(dimension['document_type'], 'is_group')) {
- frm.set_query(dimension['fieldname'], {
- "is_group": 0
- });
- }
+ let parent_fields = [];
+ frappe.meta.get_docfields(doctype).forEach((df) => {
+ if (df.fieldtype === 'Link' && df.options === 'Account') {
+ parent_fields.push(df.fieldname);
+ } else if (df.fieldtype === 'Table') {
+ this.setup_child_filters(frm, df.options, df.fieldname, dimension['fieldname']);
+ }
+
+ if (frappe.meta.has_field(doctype, dimension['fieldname'])) {
+ this.setup_account_filters(frm, dimension['fieldname'], parent_fields);
+ }
+ });
});
});
- },
+ }
+ },
- company: function(frm) {
- if(frm.doc.company && (Object.keys(default_dimensions || {}).length > 0)
- && default_dimensions[frm.doc.company]) {
- frm.trigger('update_dimension');
- }
- },
+ setup_child_filters(frm, doctype, parentfield, dimension) {
+ let fields = [];
- update_dimension: function(frm) {
- erpnext.dimension_filters.forEach((dimension) => {
- if(frm.is_new()) {
- if(frm.doc.company && Object.keys(default_dimensions || {}).length > 0
- && default_dimensions[frm.doc.company]) {
+ if (frappe.meta.has_field(doctype, dimension)) {
+ frappe.model.with_doctype(doctype, () => {
+ frappe.meta.get_docfields(doctype).forEach((df) => {
+ if (df.fieldtype === 'Link' && df.options === 'Account') {
+ fields.push(df.fieldname);
+ }
+ });
- let default_dimension = default_dimensions[frm.doc.company][dimension['fieldname']];
+ frm.set_query(dimension, parentfield, function(doc, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ return erpnext.queries.get_filtered_dimensions(row, fields, dimension, doc.company);
+ });
+ });
+ }
+ },
- if(default_dimension) {
+ setup_account_filters(frm, dimension, fields) {
+ frm.set_query(dimension, function(doc) {
+ return erpnext.queries.get_filtered_dimensions(doc, fields, dimension, doc.company);
+ });
+ },
+
+ update_dimension(frm, doctype) {
+ if (this.accounting_dimensions) {
+ this.accounting_dimensions.forEach((dimension) => {
+ if (frm.is_new()) {
+ if (frm.doc.company && Object.keys(this.default_dimensions || {}).length > 0
+ && this.default_dimensions[frm.doc.company]) {
+
+ let default_dimension = this.default_dimensions[frm.doc.company][dimension['fieldname']];
+
+ if (default_dimension) {
if (frappe.meta.has_field(doctype, dimension['fieldname'])) {
frm.set_value(dimension['fieldname'], default_dimension);
}
@@ -61,23 +90,14 @@
}
});
}
- });
-});
+ },
-child_docs.forEach((doctype) => {
- frappe.ui.form.on(doctype, {
- items_add: function(frm, cdt, cdn) {
- erpnext.dimension_filters.forEach((dimension) => {
- var row = frappe.get_doc(cdt, cdn);
- frm.script_manager.copy_from_first_row("items", row, [dimension['fieldname']]);
- });
- },
-
- accounts_add: function(frm, cdt, cdn) {
- erpnext.dimension_filters.forEach((dimension) => {
- var row = frappe.get_doc(cdt, cdn);
- frm.script_manager.copy_from_first_row("accounts", row, [dimension['fieldname']]);
+ copy_dimension_from_first_row(frm, cdt, cdn, fieldname) {
+ if (frappe.meta.has_field(frm.doctype, fieldname) && this.accounting_dimensions) {
+ this.accounting_dimensions.forEach((dimension) => {
+ let row = frappe.get_doc(cdt, cdn);
+ frm.script_manager.copy_from_first_row(fieldname, row, [dimension['fieldname']]);
});
}
- });
-});
\ No newline at end of file
+ }
+};
\ No newline at end of file
diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js
index 770704e..808dd5a 100644
--- a/erpnext/public/js/utils/party.js
+++ b/erpnext/public/js/utils/party.js
@@ -276,6 +276,12 @@
erpnext.utils.get_shipping_address = function(frm, callback){
if (frm.doc.company) {
+ if (!(frm.doc.inter_com_order_reference || frm.doc.internal_invoice_reference ||
+ frm.doc.internal_order_reference)) {
+ if (callback) {
+ return callback();
+ }
+ }
frappe.call({
method: "erpnext.accounts.custom.address.get_shipping_address",
args: {
diff --git a/erpnext/public/js/utils/serial_no_batch_selector.js b/erpnext/public/js/utils/serial_no_batch_selector.js
index 2623c3c..d49a813 100644
--- a/erpnext/public/js/utils/serial_no_batch_selector.js
+++ b/erpnext/public/js/utils/serial_no_batch_selector.js
@@ -140,6 +140,7 @@
() => me.update_batch_serial_no_items(),
() => {
refresh_field("items");
+ refresh_field("packed_items");
if (me.callback) {
return me.callback(me.item);
}
@@ -154,7 +155,7 @@
if (this.item.serial_no) {
this.dialog.fields_dict.serial_no.set_value(this.item.serial_no);
}
-
+
if (this.has_batch && !this.has_serial_no && d.batch_no) {
this.frm.doc.items.forEach(data => {
if(data.item_code == d.item_code) {
@@ -231,7 +232,7 @@
this.map_row_values(row, batch, 'batch_no',
'selected_qty', this.values.warehouse);
});
- }
+ }
},
update_serial_no_item() {
@@ -250,7 +251,7 @@
filters: { 'name': ["in", selected_serial_nos]},
fields: ["batch_no", "name"]
}).then((data) => {
- // data = [{batch_no: 'batch-1', name: "SR-001"},
+ // data = [{batch_no: 'batch-1', name: "SR-001"},
// {batch_no: 'batch-2', name: "SR-003"}, {batch_no: 'batch-2', name: "SR-004"}]
const batch_serial_map = data.reduce((acc, d) => {
if (!acc[d['batch_no']]) acc[d['batch_no']] = [];
@@ -298,6 +299,8 @@
} else {
row.warehouse = values.warehouse || warehouse;
}
+
+ this.frm.dirty();
},
update_total_qty: function() {
diff --git a/erpnext/public/less/call_popup.less b/erpnext/public/less/call_popup.less
deleted file mode 100644
index 32e85ce..0000000
--- a/erpnext/public/less/call_popup.less
+++ /dev/null
@@ -1,9 +0,0 @@
-.call-popup {
- a:hover {
- text-decoration: underline;
- }
- .for-description {
- max-height: 250px;
- overflow: scroll;
- }
-}
\ No newline at end of file
diff --git a/erpnext/public/less/erpnext.less b/erpnext/public/less/erpnext.less
index 8685837..4076ebe 100644
--- a/erpnext/public/less/erpnext.less
+++ b/erpnext/public/less/erpnext.less
@@ -39,8 +39,9 @@
.dashboard-list-item {
background-color: inherit;
- padding: 5px 0px;
- border-bottom: 1px solid @border-color;
+ border-bottom: 1px solid var(--border-color);
+ font-size: var(--text-md);
+ color: var(--text-color);
}
#page-stock-balance .dashboard-list-item {
@@ -446,20 +447,6 @@
}
-// Leaderboard
-
-.leaderboard {
- .result {
- border-top: 1px solid #d1d8dd;
- }
- .list-item {
- padding-left: 45px;
- }
- .list-item_content {
- padding-right: 45px;
- }
-}
-
// Healthcare
.exercise-card {
diff --git a/erpnext/public/less/hub.less b/erpnext/public/less/hub.less
index 8cb7a9c..29deada 100644
--- a/erpnext/public/less/hub.less
+++ b/erpnext/public/less/hub.less
@@ -32,7 +32,12 @@
}
.hub-image-loading, .hub-image-broken {
- .img-background();
+ content: " ";
+ position: absolute;
+ left: 0;
+ height: 100%;
+ width: 100%;
+ background-color: var(--bg-light-gray);
display: flex;
align-items: center;
justify-content: center;
diff --git a/erpnext/public/scss/call_popup.scss b/erpnext/public/scss/call_popup.scss
new file mode 100644
index 0000000..95e3182
--- /dev/null
+++ b/erpnext/public/scss/call_popup.scss
@@ -0,0 +1,21 @@
+.call-popup {
+ a:hover {
+ text-decoration: underline;
+ }
+ .for-description {
+ max-height: 250px;
+ overflow: scroll;
+ }
+}
+
+audio {
+ height: 40px;
+ width: 100%;
+ max-width: 500px;
+ background-color: var(--control-bg);
+ border-radius: var(--border-radius-sm);
+ &-webkit-media-controls-panel {
+ background: var(--control-bg);
+ }
+ outline: none;
+}
diff --git a/erpnext/public/scss/point-of-sale.scss b/erpnext/public/scss/point-of-sale.scss
new file mode 100644
index 0000000..0bb8e68
--- /dev/null
+++ b/erpnext/public/scss/point-of-sale.scss
@@ -0,0 +1,1110 @@
+.point-of-sale-app {
+ display: grid;
+ grid-template-columns: repeat(10, minmax(0, 1fr));
+ gap: var(--margin-md);
+
+ section {
+ min-height: 45rem;
+ height: calc(100vh - 200px);
+ max-height: calc(100vh - 200px);
+ }
+
+ .frappe-control {
+ margin: 0 !important;
+ width: 100%;
+ }
+
+ .form-group {
+ margin-bottom: 0px !important;
+ }
+
+ .pointer-no-select {
+ cursor: pointer;
+ user-select: none;
+ }
+
+ .nowrap {
+ overflow: hidden;
+ white-space: nowrap;
+ }
+
+ .image {
+ height: 100% !important;
+ object-fit: cover;
+ }
+
+ .abbr {
+ background-color: var(--gray-50);
+ font-size: var(--text-3xl);
+ }
+
+ .label {
+ display: flex;
+ align-items: center;
+ font-weight: 700;
+ font-size: var(--text-lg);
+ }
+
+ .pos-card {
+ background-color: var(--fg-color);
+ box-shadow: var(--shadow-base);
+ border-radius: var(--border-radius-md);
+ }
+
+ .seperator {
+ margin-left: var(--margin-sm);
+ margin-right: var(--margin-sm);
+ border-bottom: 1px solid var(--gray-300);
+ }
+
+ .primary-action {
+ @extend .pointer-no-select;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--padding-sm);
+ margin-top: var(--margin-sm);
+ border-radius: var(--border-radius-md);
+ font-size: var(--text-lg);
+ font-weight: 700;
+ }
+
+ .highlighted-numpad-btn {
+ box-shadow: inset 0 0px 4px 0px rgba(0, 0, 0, 0.15) !important;
+ font-weight: 700;
+ background-color: var(--gray-50);
+ }
+
+ > .items-selector {
+ @extend .pos-card;
+ grid-column: span 6 / span 6;
+ display: flex;
+ flex-direction: column;
+ overflow: hidden;
+
+ > .filter-section {
+ display: grid;
+ grid-template-columns: repeat(12, minmax(0, 1fr));
+ background-color: var(--fg-color);
+ padding: var(--padding-lg);
+ padding-bottom: var(--padding-sm);
+ align-items: center;
+
+ > .label {
+ @extend .label;
+ grid-column: span 4 / span 4;
+ padding-bottom: var(--padding-xs);
+ }
+
+ > .search-field {
+ grid-column: span 5 / span 5;
+ display: flex;
+ align-items: center;
+ margin: 0px var(--margin-sm);
+ }
+
+ > .item-group-field {
+ grid-column: span 3 / span 3;
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ > .items-container {
+ display: grid;
+ grid-template-columns: repeat(4, minmax(0, 1fr));
+ gap: var(--margin-lg);
+ padding: var(--padding-lg);
+ padding-top: var(--padding-xs);
+ overflow-y: scroll;
+ overflow-x: hidden;
+
+ &:after {
+ content: "";
+ display: block;
+ height: 1px;
+ }
+
+ > .item-wrapper {
+ @extend .pointer-no-select;
+ border-radius: var(--border-radius-md);
+ box-shadow: var(--shadow-base);
+
+ &:hover {
+ transform: scale(1.02, 1.02);
+ }
+
+ .item-display {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--border-radius-md);
+ margin: var(--margin-sm);
+ margin-bottom: 0px;
+ min-height: 8rem;
+ height: 8rem;
+ color: var(--gray-500);
+
+ > img {
+ @extend .image;
+ }
+ }
+
+ > .item-detail {
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ min-height: 3.5rem;
+ height: 3.5rem;
+ padding-left: var(--padding-sm);
+ padding-right: var(--padding-sm);
+
+ > .item-name {
+ @extend .nowrap;
+ display: flex;
+ align-items: center;
+ font-size: var(--text-md);
+ }
+
+ > .item-rate {
+ font-weight: 700;
+ }
+ }
+
+ }
+ }
+ }
+
+ > .customer-cart-container {
+ grid-column: span 4 / span 4;
+ display: flex;
+ flex-direction: column;
+
+ > .customer-section {
+ @extend .pos-card;
+ display: flex;
+ flex-direction: column;
+ padding: var(--padding-md) var(--padding-lg);
+ overflow: visible;
+
+ > .customer-field {
+ display: flex;
+ align-items: center;
+ padding-top: var(--padding-xs);
+ }
+
+ > .customer-details {
+ display: flex;
+ flex-direction: column;
+ background-color: var(--fg-color);
+
+ > .header {
+ display: flex;
+ margin-bottom: var(--margin-md);
+ justify-content: space-between;
+ padding-top: var(--padding-md);
+
+ > .label {
+ @extend .label;
+ }
+
+ > .close-details-btn {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ }
+ }
+
+ > .customer-display {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+
+ > .customer-image {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 3rem;
+ height: 3rem;
+ border-radius: 50%;
+ color: var(--gray-500);
+ margin-right: var(--margin-md);
+
+ > img {
+ @extend .image;
+ border-radius: 50%;
+ }
+ }
+
+ > .customer-abbr {
+ @extend .abbr;
+ font-size: var(--text-2xl);
+ }
+
+ > .customer-name-desc {
+ @extend .nowrap;
+ display: flex;
+ flex-direction: column;
+ margin-right: auto;
+
+ >.customer-name {
+ font-weight: 700;
+ font-size: var(--text-lg);
+ }
+
+ >.customer-desc {
+ color: var(--gray-600);
+ font-weight: 500;
+ font-size: var(--text-sm);
+ }
+ }
+
+ > .reset-customer-btn {
+ display: flex;
+ align-items: center;
+ cursor: pointer;
+ }
+
+ }
+
+ > .customer-fields-container {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ margin-top: var(--margin-md);
+ column-gap: var(--padding-sm);
+ row-gap: var(--padding-xs);
+ }
+
+ > .transactions-label {
+ @extend .label;
+ margin-top: var(--margin-md);
+ margin-bottom: var(--margin-sm);
+ }
+ }
+
+ > .customer-transactions {
+ height: 100%;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ margin-right: -12px;
+ padding-right: 12px;
+ margin-left: -10px;
+
+ > .no-transactions-placeholder {
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--gray-50);
+ border-radius: var(--border-radius-md);
+ }
+ }
+ }
+
+ > .cart-container {
+ @extend .pos-card;
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ margin-top: var(--margin-md);
+ position: relative;
+ height: 100%;
+
+ > .abs-cart-container {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ padding: var(--padding-lg);
+ width: 100%;
+ height: 100%;
+
+ > .cart-label {
+ @extend .label;
+ padding-bottom: var(--padding-md);
+ }
+
+ > .cart-header {
+ display: flex;
+ width: 100%;
+ font-size: var(--text-md);
+ padding-bottom: var(--padding-md);
+
+ > .name-header {
+ flex: 1 1 0%;
+ }
+
+ > .qty-header {
+ margin-right: var(--margin-lg);
+ text-align: center;
+ }
+
+ > .rate-amount-header {
+ text-align: right;
+ margin-right: var(--margin-sm);
+ }
+ }
+
+ .no-item-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background-color: var(--gray-50);
+ border-radius: var(--border-radius-md);
+ font-size: var(--text-md);
+ font-weight: 500;
+ width: 100%;
+ height: 100%;
+ }
+
+ > .cart-items-section {
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 0%;
+ overflow-y: scroll;
+
+ > .cart-item-wrapper {
+ @extend .pointer-no-select;
+ display: flex;
+ align-items: center;
+ padding: var(--padding-sm);
+ border-radius: var(--border-radius-md);
+
+ &:hover {
+ background-color: var(--gray-50);
+ }
+
+ > .item-image {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 2rem;
+ height: 2rem;
+ border-radius: var(--border-radius-md);
+ color: var(--gray-500);
+ margin-right: var(--margin-md);
+
+ > img {
+ @extend .image;
+ }
+ }
+
+ > .item-abbr {
+ @extend .abbr;
+ font-size: var(--text-lg);
+ }
+
+
+ > .item-name-desc {
+ @extend .nowrap;
+ display: flex;
+ flex-direction: column;
+ flex: 1 1 0%;
+ flex-shrink: 1;
+
+ > .item-name {
+ font-weight: 700;
+ }
+
+ > .item-desc {
+ font-size: var(--text-sm);
+ color: var(--gray-600);
+ font-weight: 500;
+ }
+ }
+
+ > .item-qty-rate {
+ display: flex;
+ flex-shrink: 0;
+ text-align: right;
+ margin-left: var(--margin-md);
+
+ > .item-qty {
+ display: flex;
+ align-items: center;
+ margin-right: var(--margin-lg);
+ font-weight: 700;
+ }
+
+ > .item-rate-amount {
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ text-align: right;
+
+ > .item-rate {
+ font-weight: 700;
+ }
+
+ > .item-amount {
+ font-size: var(--text-md);
+ font-weight: 600;
+ }
+ }
+ }
+
+ }
+ }
+
+ > .cart-totals-section {
+ display: flex;
+ flex-direction: column;
+ flex-shrink: 0;
+ width: 100%;
+ margin-top: var(--margin-md);
+
+ > .add-discount-wrapper {
+ @extend .pointer-no-select;
+ display: none;
+ align-items: center;
+ border-radius: var(--border-radius-md);
+ border: 1px dashed var(--gray-500);
+ padding: var(--padding-sm) var(--padding-md);
+ margin-bottom: var(--margin-sm);
+
+ > .add-discount-field {
+ width: 100%;
+ }
+
+ .discount-icon {
+ margin-right: var(--margin-sm);
+ }
+
+ .edit-discount-btn {
+ display: flex;
+ align-items: center;
+ font-weight: 500;
+ color: var(--dark-green-500);
+ }
+ }
+
+ > .net-total-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--padding-sm) 0px;
+ font-weight: 500;
+ font-size: var(--text-md);
+ }
+
+ > .taxes-container {
+ display: none;
+ flex-direction: column;
+ font-weight: 500;
+ font-size: var(--text-md);
+
+ > .tax-row {
+ display: flex;
+ justify-content: space-between;
+ line-height: var(--text-3xl);
+ }
+ }
+
+ > .grand-total-container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--padding-sm) 0px;
+ font-weight: 700;
+ font-size: var(--text-lg);
+ }
+
+ > .checkout-btn {
+ @extend .primary-action;
+ background-color: var(--blue-200);
+ color: white;
+ }
+
+ > .edit-cart-btn {
+ @extend .primary-action;
+ display: none;
+ background-color: var(--gray-300);
+ font-weight: 500;
+ transition: all 0.15s ease-in-out;
+
+ &:hover {
+ background-color: var(--gray-600);
+ color: white;
+ font-weight: 700;
+ }
+ }
+ }
+
+ > .numpad-section {
+ display: none;
+ flex-direction: column;
+ flex-shrink: 0;
+ margin-top: var(--margin-sm);
+ padding: var(--padding-sm);
+ padding-bottom: 0px;
+ width: 100%;
+
+ > .numpad-totals {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: var(--margin-md);
+ font-size: var(--text-md);
+ font-weight: 700;
+ }
+
+ > .numpad-container {
+ display: grid;
+ grid-template-columns: repeat(5, minmax(0, 1fr));
+ gap: var(--margin-md);
+ margin-bottom: var(--margin-md);
+
+ > .numpad-btn {
+ @extend .pointer-no-select;
+ border-radius: var(--border-radius-md);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--padding-md);
+ box-shadow: var(--shadow-sm);
+ }
+
+ > .col-span-2 {
+ grid-column: span 2 / span 2;
+ }
+
+ > .remove-btn {
+ font-weight: 700;
+ color: var(--red-500);
+ }
+ }
+
+ > .checkout-btn {
+ @extend .primary-action;
+ margin: 0px;
+ margin-bottom: var(--margin-sm);
+ background-color: var(--blue-200);
+ color: white;
+ }
+ }
+ }
+ }
+ }
+
+ .invoice-wrapper {
+ @extend .pointer-no-select;
+ display: flex;
+ justify-content: space-between;
+ border-radius: var(--border-radius-md);
+ padding: var(--padding-sm);
+
+ &:hover {
+ background-color: var(--gray-50);
+ }
+
+ > .invoice-name-date {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-around;
+
+ > .invoice-name {
+ @extend .nowrap;
+ font-size: var(--text-md);
+ font-weight: 700;
+ }
+
+ > .invoice-date {
+ @extend .nowrap;
+ font-size: var(--text-sm);
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ > .invoice-total-status {
+ display: flex;
+ flex-direction: column;
+ font-weight: 500;
+ font-size: var(--text-sm);
+ margin-left: var(--margin-md);
+
+ > .invoice-total {
+ margin-bottom: var(--margin-xs);
+ font-size: var(--text-base);
+ font-weight: 700;
+ text-align: right;
+ }
+
+ > .invoice-status {
+ display: flex;
+ align-items: center;
+ justify-content: right;
+ }
+ }
+ }
+
+ > .item-details-container {
+ @extend .pos-card;
+ grid-column: span 4 / span 4;
+ display: none;
+ flex-direction: column;
+ padding: var(--padding-lg);
+ padding-top: var(--padding-md);
+
+ > .item-details-header {
+ display: flex;
+ justify-content: space-between;
+ margin-bottom: var(--margin-md);
+
+ > .close-btn {
+ @extend .pointer-no-select;
+ }
+ }
+
+ > .item-display {
+ display: flex;
+
+ > .item-name-desc-price {
+ flex: 1 1 0%;
+ display: flex;
+ flex-direction: column;
+ justify-content: flex-end;
+ margin-right: var(--margin-md);
+
+ > .item-name {
+ font-size: var(--text-3xl);
+ font-weight: 600;
+ }
+
+ > .item-desc {
+ font-size: var(--text-md);
+ font-weight: 500;
+ }
+
+ > .item-price {
+ font-size: var(--text-3xl);
+ font-weight: 700;
+ }
+ }
+
+ > .item-image {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 11rem;
+ height: 11rem;
+ border-radius: var(--border-radius-md);
+ margin-left: var(--margin-md);
+ color: var(--gray-500);
+
+ > img {
+ @extend .image;
+ }
+
+ > .item-abbr {
+ @extend .abbr;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ border-radius: var(--border-radius-md);
+ font-size: var(--text-3xl);
+ width: 100%;
+ height: 100%;
+ }
+ }
+ }
+
+ > .discount-section {
+ display: flex;
+ align-items: center;
+ margin-bottom: var(--margin-sm);
+
+ > .item-rate {
+ font-weight: 500;
+ margin-right: var(--margin-sm);
+ text-decoration: line-through;
+ }
+
+ > .item-discount {
+ padding: 3px var(--padding-sm);
+ border-radius: var(--border-radius-sm);
+ background-color: var(--green-100);
+ color: var(--dark-green-500);
+ font-size: var(--text-sm);
+ font-weight: 700;
+ }
+ }
+
+ > .form-container {
+ display: grid;
+ grid-template-columns: repeat(2, minmax(0, 1fr));
+ column-gap: var(--padding-md);
+
+ > .auto-fetch-btn {
+ @extend .pointer-no-select;
+ margin: var(--margin-xs);
+ }
+ }
+ }
+
+ > .payment-container {
+ @extend .pos-card;
+ grid-column: span 6 / span 6;
+ display: none;
+ flex-direction: column;
+ padding: var(--padding-lg);
+
+ .border-primary {
+ border: 1px solid var(--blue-500);
+ }
+
+ .submit-order-btn {
+ @extend .primary-action;
+ background-color: var(--blue-500);
+ color: white;
+ }
+
+ .section-label {
+ @extend .label;
+ @extend .pointer-no-select;
+ margin-bottom: var(--margin-md);
+ }
+
+ > .payment-modes {
+ display: flex;
+ padding-bottom: var(--padding-sm);
+ margin-bottom: var(--margin-xs);
+ overflow-x: scroll;
+ overflow-y: hidden;
+
+ > .payment-mode-wrapper {
+ min-width: 40%;
+ padding: var(--padding-xs);
+
+ > .mode-of-payment {
+ @extend .pos-card;
+ @extend .pointer-no-select;
+ padding: var(--padding-md) var(--padding-lg);
+
+ > .pay-amount {
+ display: inline;
+ float: right;
+ font-weight: 700;
+ }
+
+ > .mode-of-payment-control {
+ display: none;
+ align-items: center;
+ margin-top: var(--margin-sm);
+ margin-bottom: var(--margin-xs);
+ }
+
+ > .loyalty-amount-name {
+ display: none;
+ float: right;
+ font-weight: 700;
+ }
+
+ > .cash-shortcuts {
+ display: none;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: var(--margin-sm);
+ font-size: var(--text-sm);
+ text-align: center;
+
+ > .shortcut {
+ @extend .pointer-no-select;
+ border-radius: var(--border-radius-sm);
+ background-color: var(--gray-100);
+ font-weight: 500;
+ padding: var(--padding-xs) var(--padding-sm);
+ transition: all 0.15s ease-in-out;
+
+ &:hover {
+ background-color: var(--gray-300);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ > .fields-numpad-container {
+ display: flex;
+ flex: 1;
+
+ > .fields-section {
+ flex: 1;
+ }
+
+ > .number-pad {
+ flex: 1;
+ display: flex;
+ justify-content: flex-end;
+ align-items: flex-end;
+
+ .numpad-container {
+ display: grid;
+ grid-template-columns: repeat(3, minmax(0, 1fr));
+ gap: var(--margin-md);
+ margin-bottom: var(--margin-md);
+
+ > .numpad-btn {
+ @extend .pointer-no-select;
+ border-radius: var(--border-radius-md);
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ padding: var(--padding-md);
+ box-shadow: var(--shadow-sm);
+ }
+ }
+ }
+ }
+
+ > .totals-section {
+ display: flex;
+ margin-top: auto;
+ margin-bottom: var(--margin-sm);
+ justify-content: center;
+ flex-direction: column;
+
+ > .totals {
+ display: flex;
+ background-color: var(--gray-100);
+ justify-content: center;
+ padding: var(--padding-md);
+ border-radius: var(--border-radius-md);
+
+ > .col {
+ flex-grow: 1;
+ text-align: center;
+
+ > .total-label {
+ font-size: var(--text-md);
+ font-weight: 500;
+ color: var(--gray-600);
+ }
+
+ > .value {
+ font-size: var(--text-2xl);
+ font-weight: 700;
+ }
+ }
+
+ > .seperator-y {
+ margin-left: var(--margin-sm);
+ margin-right: var(--margin-sm);
+ border-right: 1px solid var(--gray-300);
+ }
+ }
+
+ > .number-pad {
+ display: none;
+ }
+ }
+ }
+
+ > .past-order-list {
+ @extend .pos-card;
+ grid-column: span 4 / span 4;
+ display: none;
+ flex-direction: column;
+ overflow: hidden;
+
+ > .filter-section {
+ display: flex;
+ flex-direction: column;
+ background-color: var(--fg-color);
+ padding: var(--padding-lg);
+
+ > .search-field {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ margin-top: var(--margin-md);
+ margin-bottom: var(--margin-xs);
+ }
+
+ > .status-field {
+ width: 100%;
+ display: flex;
+ align-items: center;
+ }
+ }
+
+ > .invoices-container {
+ padding: var(--padding-lg);
+ padding-top: 0px;
+ overflow-x: hidden;
+ overflow-y: scroll;
+ }
+ }
+
+ > .past-order-summary {
+ display: none;
+ grid-column: span 6 / span 6;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+
+ > .no-summary-placeholder {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 100%;
+ height: 100%;
+ background-color: var(--gray-50);
+ font-weight: 500;
+ border-radius: var(--border-radius-md);
+ }
+
+ > .invoice-summary-wrapper {
+ @extend .pos-card;
+ display: none;
+ position: relative;
+ width: 31rem;
+ height: 100%;
+
+ > .abs-container {
+ position: absolute;
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ height: 100%;
+ padding: var(--padding-lg);
+
+ > .upper-section {
+ display: flex;
+ justify-content: space-between;
+ width: 100%;
+ margin-bottom: var(--margin-md);
+
+ > .left-section {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+ justify-content: flex-end;
+ padding-right: var(--padding-sm);
+
+ > .customer-name {
+ font-size: var(--text-2xl);
+ font-weight: 700;
+ }
+
+ > .customer-email {
+ font-size: var(--text-md);
+ font-weight: 500;
+ color: var(--gray-600);
+ }
+
+ > .cashier {
+ font-size: var(--text-md);
+ font-weight: 500;
+ color: var(--gray-600);
+ margin-top: auto;
+ }
+ }
+
+ > .right-section {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ justify-content: space-between;
+
+ > .paid-amount {
+ font-size: var(--text-2xl);
+ font-weight: 700;
+ }
+
+ > .invoice-name {
+ font-size: var(--text-md);
+ font-weight: 500;
+ color: var(--gray-600);
+ margin-bottom: var(--margin-sm);
+ }
+ }
+ }
+
+ > .summary-container {
+ display: flex;
+ flex-direction: column;
+ border-radius: var(--border-radius-md);
+ background-color: var(--gray-50);
+ margin: var(--margin-md) 0px;
+
+ > .summary-row-wrapper {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ padding: var(--padding-sm) var(--padding-md);
+ }
+
+ > .taxes-wrapper {
+ display: flex;
+ flex-direction: column;
+ padding: 0px var(--padding-md);
+
+ > .tax-row {
+ display: flex;
+ justify-content: space-between;
+ font-size: var(--text-md);
+ line-height: var(--text-3xl);
+ }
+ }
+
+ > .item-row-wrapper {
+ display: flex;
+ align-items: center;
+ padding: var(--padding-sm) var(--padding-md);
+
+ > .item-name {
+ @extend .nowrap;
+ font-weight: 500;
+ margin-right: var(--margin-md);
+ }
+
+ > .item-qty {
+ font-weight: 500;
+ margin-left: auto;
+ }
+
+ > .item-rate-disc {
+ display: flex;
+ text-align: right;
+ margin-left: var(--margin-md);
+ justify-content: flex-end;
+
+ > .item-disc {
+ color: var(--dark-green-500);
+ }
+
+ > .item-rate {
+ font-weight: 500;
+ margin-left: var(--margin-md);
+ }
+ }
+ }
+
+ > .grand-total {
+ font-weight: 700;
+ }
+
+ > .payments {
+ font-weight: 700;
+ }
+ }
+
+
+ > .summary-btns {
+ display: flex;
+ justify-content: space-between;
+
+ > .summary-btn {
+ flex: 1;
+ margin: 0px var(--margin-xs);
+ }
+
+ > .new-btn {
+ background-color: var(--blue-500);
+ color:white;
+ font-weight: 500;
+ }
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/erpnext/public/scss/shopping_cart.scss b/erpnext/public/scss/shopping_cart.scss
new file mode 100644
index 0000000..159a8a4
--- /dev/null
+++ b/erpnext/public/scss/shopping_cart.scss
@@ -0,0 +1,494 @@
+@import "frappe/public/scss/desk/variables";
+@import "frappe/public/scss/common/mixins";
+
+body.product-page {
+ background: var(--gray-50);
+}
+
+
+.item-breadcrumbs {
+ .breadcrumb-container {
+ ol.breadcrumb {
+ background-color: var(--gray-50) !important;
+ }
+
+ a {
+ color: var(--gray-900);
+ }
+ }
+}
+
+.carousel-control {
+ height: 42px;
+ width: 42px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ background: white;
+ box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.08), 0px 1px 2px 1px rgba(0, 0, 0, 0.06);
+ border-radius: 100px;
+}
+
+.carousel-control-prev,
+.carousel-control-next {
+ opacity: 1;
+}
+
+.carousel-body {
+ position: absolute;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+}
+
+.carousel-content {
+ max-width: 400px;
+}
+
+.card {
+ border: none;
+}
+
+.product-category-section {
+ .card:hover {
+ box-shadow: 0px 16px 45px 6px rgba(0, 0, 0, 0.08), 0px 8px 10px -10px rgba(0, 0, 0, 0.04);
+ }
+
+ .card-grid {
+ display: grid;
+ grid-gap: 15px;
+ grid-template-columns: repeat(auto-fit, minmax(120px, 1fr));
+ }
+}
+
+.item-card-group-section {
+ .card {
+ height: 360px;
+ align-items: center;
+ justify-content: center;
+
+ &:hover {
+ box-shadow: 0px 16px 60px rgba(0, 0, 0, 0.08), 0px 8px 30px -20px rgba(0, 0, 0, 0.04);
+ transition: box-shadow 400ms;
+ }
+ }
+
+ // .card-body {
+ // text-align: center;
+ // }
+
+ // .featured-item {
+ // .card-body {
+ // text-align: left;
+ // }
+ // }
+ .card-img-container {
+ height: 210px;
+ width: 100%;
+ }
+
+ .card-img {
+ max-height: 210px;
+ object-fit: contain;
+ margin-top: 1.25rem;
+ }
+
+ .no-image {
+ @include flex(flex, center, center, null);
+ height: 200px;
+ margin: 0 auto;
+ margin-top: var(--margin-xl);
+ background: var(--gray-100);
+ width: 80%;
+ border-radius: var(--border-radius);
+ font-size: 2rem;
+ color: var(--gray-500);
+ }
+
+ .product-title {
+ font-size: 14px;
+ color: var(--gray-800);
+ font-weight: 500;
+ }
+
+ .product-description {
+ font-size: 12px;
+ color: var(--text-color);
+ margin: 20px 0;
+ display: -webkit-box;
+ -webkit-line-clamp: 6;
+ -webkit-box-orient: vertical;
+
+ p {
+ margin-bottom: 0.5rem;
+ }
+ }
+
+ .product-category {
+ font-size: 13px;
+ color: var(--text-muted);
+ margin: var(--margin-sm) 0;
+ }
+
+ .product-price {
+ font-size: 18px;
+ font-weight: 600;
+ color: var(--text-color);
+ margin: var(--margin-sm) 0;
+ }
+
+ .item-card {
+ padding: var(--padding-sm);
+ }
+}
+
+[data-doctype="Item Group"],
+#page-all-products {
+ .page-header {
+ font-size: 20px;
+ font-weight: 700;
+ color: var(--text-color);
+ }
+
+ .filters-section {
+ .title-section {
+ border-bottom: 1px solid var(--table-border-color);
+ }
+
+ .filter-title {
+ font-weight: 500;
+ }
+
+ .clear-filters {
+ font-size: 13px;
+ }
+
+ .filter-label {
+ font-size: 11px;
+ font-weight: 600;
+ color: var(--gray-700);
+ text-transform: uppercase;
+ }
+
+ .filter-block {
+ border-bottom: 1px solid var(--table-border-color);
+ }
+
+ .checkbox {
+ .label-area {
+ font-size: 13px;
+ color: var(--gray-800);
+ }
+ }
+ }
+}
+
+.product-container {
+ @include card($padding: var(--padding-md));
+ min-height: 70vh;
+
+ .product-details {
+ max-width: 40%;
+ margin-left: -30px;
+
+ .btn-add-to-cart {
+ font-size: var(--text-base);
+ }
+ }
+
+ .product-title {
+ font-size: 24px;
+ font-weight: 600;
+ color: var(--text-color);
+ }
+
+ .product-code {
+ color: var(--text-muted);
+ font-size: 13px;
+ }
+
+ .product-description {
+ font-size: 13px;
+ color: var(--gray-800);
+ }
+
+ .product-image {
+ border-color: var(--table-border-color) !important;
+ padding: 15px;
+
+ @include media-breakpoint-between(xs, md) {
+ height: 300px;
+ width: 300px;
+ }
+
+ @include media-breakpoint-up(lg) {
+ height: 350px;
+ width: 350px;
+ }
+
+ img {
+ object-fit: contain;
+ }
+ }
+
+ .item-slideshow {
+ @include media-breakpoint-between(xs, md) {
+ max-height: 320px;
+ }
+
+ @include media-breakpoint-up(lg) {
+ max-height: 430px;
+ }
+
+ overflow: scroll;
+ }
+
+ .item-slideshow-image {
+ height: 4rem;
+ width: 6rem;
+ object-fit: contain;
+ padding: 0.5rem;
+ border: 1px solid var(--table-border-color);
+ border-radius: 4px;
+ cursor: pointer;
+
+ &:hover, &.active {
+ border-color: $primary;
+ }
+ }
+
+ .item-cart {
+ .product-price {
+ font-size: 20px;
+ color: var(--text-color);
+ font-weight: 600;
+
+ .formatted-price {
+ color: var(--text-muted);
+ font-size: var(--text-base);
+ }
+ }
+
+ .no-stock {
+ font-size: var(--text-base);
+ }
+ }
+}
+
+.item-configurator-dialog {
+ .modal-header {
+ padding: var(--padding-md) var(--padding-xl);
+ }
+
+ .modal-body {
+ padding: 0 var(--padding-xl);
+ padding-bottom: var(--padding-xl);
+
+ .status-area {
+ .alert {
+ padding: var(--padding-xs) var(--padding-sm);
+ font-size: var(--text-sm);
+ }
+ }
+
+ .form-layout {
+ max-height: 50vh;
+ overflow-y: auto;
+ }
+
+ .section-body {
+ .form-column {
+ .form-group {
+ .control-label {
+ font-size: var(--text-md);
+ color: var(--gray-700);
+ }
+
+ .help-box {
+ margin-top: 2px;
+ font-size: var(--text-sm);
+ }
+ }
+ }
+ }
+ }
+}
+
+.item-group-slideshow {
+ .item-group-description {
+ // max-width: 900px;
+ }
+
+ .carousel-inner.rounded-carousel {
+ border-radius: $card-border-radius;
+ }
+}
+
+.cart-icon {
+ .cart-badge {
+ position: relative;
+ top: -10px;
+ left: -12px;
+ background: var(--red-600);
+ width: 16px;
+ align-items: center;
+ height: 16px;
+ font-size: 10px;
+ border-radius: 50%;
+ }
+}
+
+
+#page-cart {
+ .shopping-cart-header {
+ font-weight: bold;
+ }
+
+ .cart-container {
+ color: var(--text-color);
+
+ .frappe-card {
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ }
+
+ .cart-items-header {
+ font-weight: 600;
+ }
+
+ .cart-table {
+ th, tr, td {
+ border-color: var(--border-color);
+ border-width: 1px;
+ }
+
+ th {
+ font-weight: normal;
+ font-size: 13px;
+ color: var(--text-muted);
+ padding: var(--padding-sm) 0;
+ }
+
+ td {
+ padding: var(--padding-sm) 0;
+ color: var(--text-color);
+ }
+
+ .cart-items {
+ .item-title {
+ font-size: var(--text-base);
+ font-weight: 500;
+ color: var(--text-color);
+ }
+
+ .item-subtitle {
+ color: var(--text-muted);
+ font-size: var(--text-md);
+ }
+
+ .item-subtotal {
+ font-size: var(--text-base);
+ font-weight: 500;
+ }
+
+ .item-rate {
+ font-size: var(--text-md);
+ color: var(--text-muted);
+ }
+
+ textarea {
+ width: 40%;
+ }
+ }
+
+ .cart-tax-items {
+ .item-grand-total {
+ font-size: 16px;
+ font-weight: 600;
+ color: var(--text-color);
+ }
+ }
+ }
+
+ .cart-addresses {
+ hr {
+ border-color: var(--border-color);
+ }
+ }
+
+ .number-spinner {
+ width: 75%;
+ .cart-btn {
+ border: none;
+ background: var(--gray-100);
+ box-shadow: none;
+ height: 28px;
+ align-items: center;
+ display: flex;
+ }
+
+ .cart-qty {
+ height: 28px;
+ font-size: var(--text-md);
+ }
+ }
+
+ .place-order-container {
+ .btn-place-order {
+ width: 62%;
+ }
+ }
+ }
+}
+
+.cart-empty.frappe-card {
+ min-height: 76vh;
+ @include flex(flex, center, center, column);
+
+ .cart-empty-message {
+ font-size: 18px;
+ color: var(--text-color);
+ font-weight: bold;
+ }
+}
+
+.address-card {
+ .card-title {
+ font-size: var(--text-base);
+ font-weight: 500;
+ }
+
+ .card-body {
+ max-width: 80%;
+ }
+
+ .card-text {
+ font-size: var(--text-md);
+ color: var(--gray-700);
+ }
+
+ .card-link {
+ font-size: var(--text-md);
+
+ svg use {
+ stroke: var(--blue-500);
+ }
+ }
+
+ .btn-change-address {
+ color: var(--blue-500);
+ box-shadow: none;
+ border: 1px solid var(--blue-500);
+ }
+}
+
+.modal .address-card {
+ .card-body {
+ padding: var(--padding-sm);
+ border-radius: var(--border-radius);
+ border: 1px solid var(--dark-border-color);
+ }
+}
+
diff --git a/erpnext/public/scss/website.scss b/erpnext/public/scss/website.scss
index 617e916..56b717c 100644
--- a/erpnext/public/scss/website.scss
+++ b/erpnext/public/scss/website.scss
@@ -1,29 +1,10 @@
-@import "frappe/public/scss/variables";
-
-.product-image img {
- min-height: 20rem;
- max-height: 30rem;
-}
+@import "frappe/public/scss/website/variables";
.filter-options {
max-height: 300px;
overflow: auto;
}
-.item-slideshow-image {
- height: 3rem;
- width: 3rem;
- object-fit: contain;
- padding: 0.5rem;
- border: 1px solid $border-color;
- border-radius: 4px;
- cursor: pointer;
-
- &:hover, &.active {
- border-color: $primary;
- }
-}
-
.address-card {
cursor: pointer;
position: relative;
@@ -43,10 +24,10 @@
.check {
display: inline-flex;
- padding: 0.25rem;
- background: $primary;
- color: white;
- border-radius: 50%;
+ padding: 0.25rem;
+ background: $primary;
+ color: white;
+ border-radius: 50%;
font-size: 12px;
width: 24px;
height: 24px;
diff --git a/erpnext/quality_management/desk_page/quality/quality.json b/erpnext/quality_management/desk_page/quality/quality.json
deleted file mode 100644
index 7a049b2..0000000
--- a/erpnext/quality_management/desk_page/quality/quality.json
+++ /dev/null
@@ -1,88 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Goal and Procedure",
- "links": "[\n {\n \"description\": \"Quality Goal.\",\n \"label\": \"Quality Goal\",\n \"name\": \"Quality Goal\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Quality Procedure.\",\n \"label\": \"Quality Procedure\",\n \"name\": \"Quality Procedure\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tree of Quality Procedures.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Tree of Procedures\",\n \"name\": \"Quality Procedure\",\n \"route\": \"#Tree/Quality Procedure\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Feedback",
- "links": "[\n {\n \"description\": \"Quality Feedback\",\n \"label\": \"Quality Feedback\",\n \"name\": \"Quality Feedback\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Quality Feedback Template\",\n \"label\": \"Quality Feedback Template\",\n \"name\": \"Quality Feedback Template\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Meeting",
- "links": "[\n {\n \"description\": \"Quality Meeting\",\n \"label\": \"Quality Meeting\",\n \"name\": \"Quality Meeting\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Review and Action",
- "links": "[\n {\n \"description\": \"Non Conformance\",\n \"label\": \"Non Conformance\",\n \"name\": \"Non Conformance\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Quality Review\",\n \"label\": \"Quality Review\",\n \"name\": \"Quality Review\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Quality Action\",\n \"label\": \"Quality Action\",\n \"name\": \"Quality Action\",\n \"type\": \"doctype\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [],
- "creation": "2020-03-02 15:49:28.632014",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Quality",
- "modified": "2020-10-27 16:28:54.138055",
- "modified_by": "Administrator",
- "module": "Quality Management",
- "name": "Quality",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "label": "Quality Goal",
- "link_to": "Quality Goal",
- "type": "DocType"
- },
- {
- "doc_view": "Tree",
- "label": "Quality Procedure",
- "link_to": "Quality Procedure",
- "type": "DocType"
- },
- {
- "label": "Quality Inspection",
- "link_to": "Quality Inspection",
- "type": "DocType"
- },
- {
- "color": "#ff8989",
- "doc_view": "",
- "format": "{} Open",
- "label": "Quality Review",
- "link_to": "Quality Review",
- "stats_filter": "{\"status\": \"Open\"}",
- "type": "DocType"
- },
- {
- "color": "#ff8989",
- "doc_view": "",
- "format": "{} Open",
- "label": "Quality Action",
- "link_to": "Quality Action",
- "stats_filter": "{\"status\": \"Open\"}",
- "type": "DocType"
- },
- {
- "color": "#ff8989",
- "doc_view": "",
- "format": "{} Open",
- "label": "Non Conformance",
- "link_to": "Non Conformance",
- "stats_filter": "{\"status\": \"Open\"}",
- "type": "DocType"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/quality_management/workspace/quality/quality.json b/erpnext/quality_management/workspace/quality/quality.json
new file mode 100644
index 0000000..e5fef43
--- /dev/null
+++ b/erpnext/quality_management/workspace/quality/quality.json
@@ -0,0 +1,190 @@
+{
+ "category": "Modules",
+ "charts": [],
+ "creation": "2020-03-02 15:49:28.632014",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "quality",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Quality",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Goal and Procedure",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quality Goal",
+ "link_to": "Quality Goal",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quality Procedure",
+ "link_to": "Quality Procedure",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tree of Procedures",
+ "link_to": "Quality Procedure",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Feedback",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quality Feedback",
+ "link_to": "Quality Feedback",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quality Feedback Template",
+ "link_to": "Quality Feedback Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Meeting",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quality Meeting",
+ "link_to": "Quality Meeting",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Review and Action",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Non Conformance",
+ "link_to": "Non Conformance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quality Review",
+ "link_to": "Quality Review",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quality Action",
+ "link_to": "Quality Action",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:35.120213",
+ "modified_by": "Administrator",
+ "module": "Quality Management",
+ "name": "Quality",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "color": "Grey",
+ "label": "Quality Goal",
+ "link_to": "Quality Goal",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "Tree",
+ "label": "Quality Procedure",
+ "link_to": "Quality Procedure",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "label": "Quality Inspection",
+ "link_to": "Quality Inspection",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "",
+ "format": "{} Open",
+ "label": "Quality Review",
+ "link_to": "Quality Review",
+ "stats_filter": "{\"status\": \"Open\"}",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "",
+ "format": "{} Open",
+ "label": "Quality Action",
+ "link_to": "Quality Action",
+ "stats_filter": "{\"status\": \"Open\"}",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "doc_view": "",
+ "format": "{} Open",
+ "label": "Non Conformance",
+ "link_to": "Non Conformance",
+ "stats_filter": "{\"status\": \"Open\"}",
+ "type": "DocType"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/datev_settings/datev_settings.json b/erpnext/regional/doctype/datev_settings/datev_settings.json
index 713e8e3..f60de4c 100644
--- a/erpnext/regional/doctype/datev_settings/datev_settings.json
+++ b/erpnext/regional/doctype/datev_settings/datev_settings.json
@@ -7,13 +7,14 @@
"engine": "InnoDB",
"field_order": [
"client",
- "account_number_length",
- "column_break_2",
"client_number",
- "section_break_4",
+ "column_break_2",
+ "consultant_number",
"consultant",
+ "section_break_4",
+ "account_number_length",
"column_break_6",
- "consultant_number"
+ "temporary_against_account_number"
],
"fields": [
{
@@ -66,10 +67,17 @@
"fieldtype": "Int",
"label": "Account Number Length",
"reqd": 1
+ },
+ {
+ "allow_in_quick_entry": 1,
+ "fieldname": "temporary_against_account_number",
+ "fieldtype": "Data",
+ "label": "Temporary Against Account Number",
+ "reqd": 1
}
],
"links": [],
- "modified": "2020-11-05 17:52:11.674329",
+ "modified": "2020-11-19 19:00:09.088816",
"modified_by": "Administrator",
"module": "Regional",
"name": "DATEV Settings",
diff --git a/erpnext/config/__init__.py b/erpnext/regional/doctype/e_invoice_request_log/__init__.py
similarity index 100%
copy from erpnext/config/__init__.py
copy to erpnext/regional/doctype/e_invoice_request_log/__init__.py
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js
new file mode 100644
index 0000000..7b7ba96
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('E Invoice Request Log', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
new file mode 100644
index 0000000..3034370
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.json
@@ -0,0 +1,102 @@
+{
+ "actions": [],
+ "autoname": "EINV-REQ-.#####",
+ "creation": "2020-12-08 12:54:08.175992",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "url",
+ "headers",
+ "response",
+ "column_break_7",
+ "timestamp",
+ "reference_invoice",
+ "data"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "label": "User",
+ "options": "User"
+ },
+ {
+ "fieldname": "reference_invoice",
+ "fieldtype": "Data",
+ "label": "Reference Invoice"
+ },
+ {
+ "fieldname": "headers",
+ "fieldtype": "Code",
+ "label": "Headers",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "data",
+ "fieldtype": "Code",
+ "label": "Data",
+ "options": "JSON"
+ },
+ {
+ "default": "Now",
+ "fieldname": "timestamp",
+ "fieldtype": "Datetime",
+ "label": "Timestamp"
+ },
+ {
+ "fieldname": "response",
+ "fieldtype": "Code",
+ "label": "Response",
+ "options": "JSON"
+ },
+ {
+ "fieldname": "url",
+ "fieldtype": "Data",
+ "label": "URL"
+ },
+ {
+ "fieldname": "column_break_7",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-01-13 12:06:57.253111",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "E Invoice Request Log",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts Manager",
+ "share": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py
new file mode 100644
index 0000000..9150bdd
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/e_invoice_request_log.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class EInvoiceRequestLog(Document):
+ pass
diff --git a/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py
new file mode 100644
index 0000000..c84e9a2
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_request_log/test_e_invoice_request_log.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestEInvoiceRequestLog(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/regional/doctype/e_invoice_settings/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/regional/doctype/e_invoice_settings/__init__.py
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
new file mode 100644
index 0000000..cc2d9f0
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.js
@@ -0,0 +1,11 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('E Invoice Settings', {
+ refresh(frm) {
+ const docs_link = 'https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing';
+ frm.dashboard.set_headline(
+ __("Read {0} for more information on E Invoicing features.", [`<a href='${docs_link}'>documentation</a>`])
+ );
+ }
+});
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
new file mode 100644
index 0000000..db8bda7
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.json
@@ -0,0 +1,65 @@
+{
+ "actions": [],
+ "creation": "2020-09-24 16:23:16.235722",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "enable",
+ "section_break_2",
+ "sandbox_mode",
+ "credentials",
+ "auth_token",
+ "token_expiry"
+ ],
+ "fields": [
+ {
+ "default": "0",
+ "fieldname": "enable",
+ "fieldtype": "Check",
+ "label": "Enable"
+ },
+ {
+ "depends_on": "enable",
+ "fieldname": "section_break_2",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "auth_token",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "token_expiry",
+ "fieldtype": "Datetime",
+ "hidden": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "credentials",
+ "fieldtype": "Table",
+ "label": "Credentials",
+ "mandatory_depends_on": "enable",
+ "options": "E Invoice User"
+ },
+ {
+ "default": "0",
+ "fieldname": "sandbox_mode",
+ "fieldtype": "Check",
+ "label": "Sandbox Mode"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-01-13 12:04:49.449199",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "E Invoice Settings",
+ "owner": "Administrator",
+ "permissions": [],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py
new file mode 100644
index 0000000..c24ad88
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/e_invoice_settings.py
@@ -0,0 +1,14 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, 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 EInvoiceSettings(Document):
+ def validate(self):
+ if self.enable and not self.credentials:
+ frappe.throw(_('You must add atleast one credentials to be able to use E Invoicing.'))
+
diff --git a/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py
new file mode 100644
index 0000000..a11ce63
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_settings/test_e_invoice_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestEInvoiceSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/page/bank_reconciliation/__init__.py b/erpnext/regional/doctype/e_invoice_user/__init__.py
similarity index 100%
copy from erpnext/accounts/page/bank_reconciliation/__init__.py
copy to erpnext/regional/doctype/e_invoice_user/__init__.py
diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
new file mode 100644
index 0000000..dd9d997
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.json
@@ -0,0 +1,48 @@
+{
+ "actions": [],
+ "creation": "2020-12-22 15:02:46.229474",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "gstin",
+ "username",
+ "password"
+ ],
+ "fields": [
+ {
+ "fieldname": "gstin",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "GSTIN",
+ "reqd": 1
+ },
+ {
+ "fieldname": "username",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Username",
+ "reqd": 1
+ },
+ {
+ "fieldname": "password",
+ "fieldtype": "Password",
+ "in_list_view": 1,
+ "label": "Password",
+ "reqd": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "istable": 1,
+ "links": [],
+ "modified": "2020-12-22 15:10:53.466205",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "E Invoice User",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py
new file mode 100644
index 0000000..056c54f
--- /dev/null
+++ b/erpnext/regional/doctype/e_invoice_user/e_invoice_user.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class EInvoiceUser(Document):
+ pass
diff --git a/erpnext/regional/doctype/gst_settings/gst_settings.json b/erpnext/regional/doctype/gst_settings/gst_settings.json
index 98c33ad..95b930c 100644
--- a/erpnext/regional/doctype/gst_settings/gst_settings.json
+++ b/erpnext/regional/doctype/gst_settings/gst_settings.json
@@ -1,222 +1,86 @@
{
- "allow_copy": 0,
- "allow_guest_to_view": 0,
- "allow_import": 0,
- "allow_rename": 0,
- "beta": 0,
- "creation": "2017-06-27 15:09:01.318003",
- "custom": 0,
- "docstatus": 0,
- "doctype": "DocType",
- "document_type": "",
- "editable_grid": 1,
- "engine": "InnoDB",
+ "actions": [],
+ "creation": "2017-06-27 15:09:01.318003",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "gst_summary",
+ "column_break_2",
+ "round_off_gst_values",
+ "gstin_email_sent_on",
+ "section_break_4",
+ "gst_accounts",
+ "b2c_limit"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gst_summary",
- "fieldtype": "HTML",
- "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": "GST Summary",
- "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,
- "unique": 0
- },
+ "fieldname": "gst_summary",
+ "fieldtype": "HTML",
+ "label": "GST Summary",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "column_break_2",
- "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,
- "unique": 0
- },
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gstin_email_sent_on",
- "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": "GSTIN Email Sent On",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "gstin_email_sent_on",
+ "fieldtype": "Date",
+ "label": "GSTIN Email Sent On",
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "section_break_4",
- "fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "section_break_4",
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fieldname": "gst_accounts",
- "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": "GST Accounts",
- "length": 0,
- "no_copy": 0,
- "options": "GST Account",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
- },
+ "fieldname": "gst_accounts",
+ "fieldtype": "Table",
+ "label": "GST Accounts",
+ "options": "GST Account",
+ "show_days": 1,
+ "show_seconds": 1
+ },
{
- "allow_bulk_edit": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "250000",
- "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.",
- "fieldname": "b2c_limit",
- "fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 1,
- "in_standard_filter": 0,
- "label": "B2C Limit",
- "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": 1,
- "search_index": 0,
- "set_only_once": 0,
- "unique": 0
+ "default": "250000",
+ "description": "Set Invoice Value for B2C. B2CL and B2CS calculated based on this invoice value.",
+ "fieldname": "b2c_limit",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "B2C Limit",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "0",
+ "description": "Enabling this option will round off individual GST components in all the Invoices",
+ "fieldname": "round_off_gst_values",
+ "fieldtype": "Check",
+ "label": "Round Off GST Values",
+ "show_days": 1,
+ "show_seconds": 1
}
- ],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
- "idx": 0,
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 1,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2018-02-14 08:14:15.375181",
- "modified_by": "Administrator",
- "module": "Regional",
- "name": "GST Settings",
- "name_case": "",
- "owner": "Administrator",
- "permissions": [],
- "quick_entry": 1,
- "read_only": 0,
- "read_only_onload": 0,
- "show_name_in_global_search": 0,
- "sort_field": "modified",
- "sort_order": "DESC",
- "track_changes": 1,
- "track_seen": 0
-}
\ No newline at end of file
+ ],
+ "index_web_pages_for_search": 1,
+ "issingle": 1,
+ "links": [],
+ "modified": "2021-01-28 17:19:47.969260",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "GST Settings",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+ }
\ No newline at end of file
diff --git a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
index 787d557..68c8a0d 100644
--- a/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/gstr_3b_report.py
@@ -192,19 +192,20 @@
for d in self.report_dict["itc_elg"]["itc_avl"]:
itc_type = itc_type_map.get(d["ty"])
- gst_category = ["Registered Regular"]
if d["ty"] == 'ISRC':
- reverse_charge = "Y"
+ reverse_charge = ["Y"]
itc_type = 'All Other ITC'
gst_category = ['Unregistered', 'Overseas']
else:
- reverse_charge = "N"
+ gst_category = ['Unregistered', 'Overseas', 'Registered Regular']
+ reverse_charge = ["N", "Y"]
for account_head in self.account_heads:
for category in gst_category:
- for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
- d[key[0]] += flt(itc_details.get((category, itc_type, reverse_charge, account_head.get(key[1])), {}).get("amount"), 2)
+ for charge_type in reverse_charge:
+ for key in [['iamt', 'igst_account'], ['camt', 'cgst_account'], ['samt', 'sgst_account'], ['csamt', 'cess_account']]:
+ d[key[0]] += flt(itc_details.get((category, itc_type, charge_type, account_head.get(key[1])), {}).get("amount"), 2)
for key in ['iamt', 'camt', 'samt', 'csamt']:
net_itc[key] += flt(d[key], 2)
@@ -264,7 +265,8 @@
def get_itc_details(self):
itc_amount = frappe.db.sql("""
- select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head, s.eligibility_for_itc, s.reverse_charge
+ select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount,
+ t.account_head, s.eligibility_for_itc, s.reverse_charge
from `tabPurchase Invoice` s , `tabPurchase Taxes and Charges` t
where s.docstatus = 1 and t.parent = s.name
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
@@ -387,7 +389,7 @@
tax_template = 'Purchase Taxes and Charges'
tax_amounts = frappe.db.sql("""
- select s.gst_category, sum(t.tax_amount_after_discount_amount) as tax_amount, t.account_head
+ select s.gst_category, sum(t.base_tax_amount_after_discount_amount) as tax_amount, t.account_head
from `tab{doctype}` s , `tab{template}` t
where s.docstatus = 1 and t.parent = s.name and s.reverse_charge = %s
and month(s.posting_date) = %s and year(s.posting_date) = %s and s.company = %s
diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
index 8174da2..023b4ed 100644
--- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
+++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py
@@ -14,8 +14,20 @@
test_dependencies = ["Territory", "Customer Group", "Supplier Group", "Item"]
class TestGSTR3BReport(unittest.TestCase):
- def test_gstr_3b_report(self):
+ def setUp(self):
+ frappe.set_user("Administrator")
+ frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'")
+ frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'")
+ frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'")
+
+ make_company()
+ make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000})
+ set_account_heads()
+ make_customers()
+ make_suppliers()
+
+ def test_gstr_3b_report(self):
month_number_mapping = {
1: "January",
2: "February",
@@ -31,17 +43,6 @@
12: "December"
}
- frappe.set_user("Administrator")
-
- frappe.db.sql("delete from `tabSales Invoice` where company='_Test Company GST'")
- frappe.db.sql("delete from `tabPurchase Invoice` where company='_Test Company GST'")
- frappe.db.sql("delete from `tabGSTR 3B Report` where company='_Test Company GST'")
-
- make_company()
- make_item("Milk", properties = {"is_nil_exempt": 1, "standard_rate": 0.000000})
- set_account_heads()
- make_customers()
- make_suppliers()
make_sales_invoice()
create_purchase_invoices()
@@ -67,6 +68,42 @@
self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50)
self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50)
+ def test_gst_rounding(self):
+ gst_settings = frappe.get_doc('GST Settings')
+ gst_settings.round_off_gst_values = 1
+ gst_settings.save()
+
+ current_country = frappe.flags.country
+ frappe.flags.country = 'India'
+
+ si = create_sales_invoice(company="_Test Company GST",
+ customer = '_Test GST Customer',
+ currency = 'INR',
+ warehouse = 'Finished Goods - _GST',
+ debit_to = 'Debtors - _GST',
+ income_account = 'Sales - _GST',
+ expense_account = 'Cost of Goods Sold - _GST',
+ cost_center = 'Main - _GST',
+ rate=216,
+ do_not_save=1
+ )
+
+ si.append("taxes", {
+ "charge_type": "On Net Total",
+ "account_head": "IGST - _GST",
+ "cost_center": "Main - _GST",
+ "description": "IGST @ 18.0",
+ "rate": 18
+ })
+
+ si.save()
+ # Check for 39 instead of 38.88
+ self.assertEqual(si.taxes[0].base_tax_amount_after_discount_amount, 39)
+
+ frappe.flags.country = current_country
+ gst_settings.round_off_gst_values = 1
+ gst_settings.save()
+
def make_sales_invoice():
si = create_sales_invoice(company="_Test Company GST",
customer = '_Test GST Customer',
@@ -145,7 +182,6 @@
si3.submit()
def create_purchase_invoices():
-
pi = make_purchase_invoice(
company="_Test Company GST",
supplier = '_Test Registered Supplier',
@@ -193,7 +229,6 @@
pi1.submit()
def make_suppliers():
-
if not frappe.db.exists("Supplier", "_Test Registered Supplier"):
frappe.get_doc({
"supplier_group": "_Test Supplier Group",
@@ -257,7 +292,6 @@
address.save()
def make_customers():
-
if not frappe.db.exists("Customer", "_Test GST Customer"):
frappe.get_doc({
"customer_group": "_Test Customer Group",
@@ -354,9 +388,9 @@
address.save()
def make_company():
-
if frappe.db.exists("Company", "_Test Company GST"):
return
+
company = frappe.new_doc("Company")
company.company_name = "_Test Company GST"
company.abbr = "_GST"
@@ -388,7 +422,6 @@
address.save()
def set_account_heads():
-
gst_settings = frappe.get_doc("GST Settings")
gst_account = frappe.get_all(
diff --git a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
index e8a8ed8..ad60db0 100644
--- a/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
+++ b/erpnext/regional/doctype/lower_deduction_certificate/lower_deduction_certificate.py
@@ -5,12 +5,16 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import getdate
+from frappe.utils import getdate, get_link_to_form
from frappe.model.document import Document
from erpnext.accounts.utils import get_fiscal_year
class LowerDeductionCertificate(Document):
def validate(self):
+ self.validate_dates()
+ self.validate_supplier_against_section_code()
+
+ def validate_dates(self):
if getdate(self.valid_upto) < getdate(self.valid_from):
frappe.throw(_("Valid Upto date cannot be before Valid From date"))
@@ -24,3 +28,20 @@
<= fiscal_year.year_end_date):
frappe.throw(_("Valid Upto date not in Fiscal Year {0}").format(frappe.bold(self.fiscal_year)))
+ def validate_supplier_against_section_code(self):
+ duplicate_certificate = frappe.db.get_value('Lower Deduction Certificate', {'supplier': self.supplier, 'section_code': self.section_code}, ['name', 'valid_from', 'valid_upto'], as_dict=True)
+ if duplicate_certificate and self.are_dates_overlapping(duplicate_certificate):
+ certificate_link = get_link_to_form('Lower Deduction Certificate', duplicate_certificate.name)
+ frappe.throw(_("There is already a valid Lower Deduction Certificate {0} for Supplier {1} against Section Code {2} for this time period.")
+ .format(certificate_link, frappe.bold(self.supplier), frappe.bold(self.section_code)))
+
+ def are_dates_overlapping(self,duplicate_certificate):
+ valid_from = duplicate_certificate.valid_from
+ valid_upto = duplicate_certificate.valid_upto
+ if valid_from <= getdate(self.valid_from) <= valid_upto:
+ return True
+ elif valid_from <= getdate(self.valid_upto) <= valid_upto:
+ return True
+ elif getdate(self.valid_from) <= valid_from and valid_upto <= getdate(self.valid_upto):
+ return True
+ return False
\ No newline at end of file
diff --git a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json
index ce2c1d4..1ff5680 100644
--- a/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json
+++ b/erpnext/regional/doctype/uae_vat_settings/uae_vat_settings.json
@@ -29,25 +29,12 @@
],
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-09-30 20:08:18.764798",
+ "modified": "2020-12-25 20:20:22.342426",
"modified_by": "Administrator",
"module": "Regional",
"name": "UAE VAT Settings",
"owner": "Administrator",
- "permissions": [
- {
- "create": 1,
- "delete": 1,
- "email": 1,
- "export": 1,
- "print": 1,
- "read": 1,
- "report": 1,
- "role": "System Manager",
- "share": 1,
- "write": 1
- }
- ],
+ "permissions": [],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
diff --git a/erpnext/regional/germany/accounts_controller.py b/erpnext/regional/germany/accounts_controller.py
deleted file mode 100644
index 5b2b31f..0000000
--- a/erpnext/regional/germany/accounts_controller.py
+++ /dev/null
@@ -1,56 +0,0 @@
-import frappe
-from frappe import _
-from frappe import msgprint
-
-
-REQUIRED_FIELDS = {
- "Sales Invoice": [
- {
- "field_name": "company_address",
- "regulation": "§ 14 Abs. 4 Nr. 1 UStG"
- },
- {
- "field_name": "company_tax_id",
- "regulation": "§ 14 Abs. 4 Nr. 2 UStG"
- },
- {
- "field_name": "taxes",
- "regulation": "§ 14 Abs. 4 Nr. 8 UStG"
- },
- {
- "field_name": "customer_address",
- "regulation": "§ 14 Abs. 4 Nr. 1 UStG",
- "condition": "base_grand_total > 250"
- }
- ]
-}
-
-
-def validate_regional(doc):
- """Check if required fields for this document are present."""
- required_fields = REQUIRED_FIELDS.get(doc.doctype)
- if not required_fields:
- return
-
- meta = frappe.get_meta(doc.doctype)
- field_map = {field.fieldname: field.label for field in meta.fields}
-
- for field in required_fields:
- condition = field.get("condition")
- if condition and not frappe.safe_eval(condition, doc.as_dict()):
- continue
-
- field_name = field.get("field_name")
- regulation = field.get("regulation")
- if field_name and not doc.get(field_name):
- missing(field_map.get(field_name), regulation)
-
-
-def missing(field_label, regulation):
- """Notify the user that a required field is missing."""
- context = 'Specific for Germany. Example: Remember to set Company Tax ID. It is required by § 14 Abs. 4 Nr. 2 UStG.'
- msgprint(_('Remember to set {field_label}. It is required by {regulation}.', context=context).format(
- field_label=frappe.bold(_(field_label)),
- regulation=regulation
- )
- )
diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py
index d6221a8..378b735 100644
--- a/erpnext/regional/india/__init__.py
+++ b/erpnext/regional/india/__init__.py
@@ -20,6 +20,7 @@
'Jharkhand',
'Karnataka',
'Kerala',
+ 'Ladakh',
'Lakshadweep Islands',
'Madhya Pradesh',
'Maharashtra',
@@ -59,6 +60,7 @@
"Jharkhand": "20",
"Karnataka": "29",
"Kerala": "32",
+ "Ladakh": "38",
"Lakshadweep Islands": "31",
"Madhya Pradesh": "23",
"Maharashtra": "27",
@@ -80,4 +82,4 @@
"West Bengal": "19",
}
-number_state_mapping = {v: k for k, v in iteritems(state_numbers)}
\ No newline at end of file
+number_state_mapping = {v: k for k, v in iteritems(state_numbers)}
diff --git a/erpnext/config/__init__.py b/erpnext/regional/india/e_invoice/__init__.py
similarity index 100%
copy from erpnext/config/__init__.py
copy to erpnext/regional/india/e_invoice/__init__.py
diff --git a/erpnext/regional/india/e_invoice/einv_item_template.json b/erpnext/regional/india/e_invoice/einv_item_template.json
new file mode 100644
index 0000000..78e5651
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einv_item_template.json
@@ -0,0 +1,31 @@
+{{
+ "SlNo": "{item.sr_no}",
+ "PrdDesc": "{item.description}",
+ "IsServc": "{item.is_service_item}",
+ "HsnCd": "{item.gst_hsn_code}",
+ "Barcde": "{item.barcode}",
+ "Unit": "{item.uom}",
+ "Qty": "{item.qty}",
+ "FreeQty": "{item.free_qty}",
+ "UnitPrice": "{item.unit_rate}",
+ "TotAmt": "{item.gross_amount}",
+ "Discount": "{item.discount_amount}",
+ "AssAmt": "{item.taxable_value}",
+ "PrdSlNo": "{item.serial_no}",
+ "GstRt": "{item.tax_rate}",
+ "IgstAmt": "{item.igst_amount}",
+ "CgstAmt": "{item.cgst_amount}",
+ "SgstAmt": "{item.sgst_amount}",
+ "CesRt": "{item.cess_rate}",
+ "CesAmt": "{item.cess_amount}",
+ "CesNonAdvlAmt": "{item.cess_nadv_amount}",
+ "StateCesRt": "{item.state_cess_rate}",
+ "StateCesAmt": "{item.state_cess_amount}",
+ "StateCesNonAdvlAmt": "{item.state_cess_nadv_amount}",
+ "OthChrg": "{item.other_charges}",
+ "TotItemVal": "{item.total_value}",
+ "BchDtls": {{
+ "Nm": "{item.batch_no}",
+ "ExpDt": "{item.batch_expiry_date}"
+ }}
+}}
\ No newline at end of file
diff --git a/erpnext/regional/india/e_invoice/einv_template.json b/erpnext/regional/india/e_invoice/einv_template.json
new file mode 100644
index 0000000..60f490d
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einv_template.json
@@ -0,0 +1,110 @@
+{{
+ "Version": "1.1",
+ "TranDtls": {{
+ "TaxSch": "{transaction_details.tax_scheme}",
+ "SupTyp": "{transaction_details.supply_type}",
+ "RegRev": "{transaction_details.reverse_charge}",
+ "EcmGstin": "{transaction_details.ecom_gstin}",
+ "IgstOnIntra": "{transaction_details.igst_on_intra}"
+ }},
+ "DocDtls": {{
+ "Typ": "{doc_details.invoice_type}",
+ "No": "{doc_details.invoice_name}",
+ "Dt": "{doc_details.invoice_date}"
+ }},
+ "SellerDtls": {{
+ "Gstin": "{seller_details.gstin}",
+ "LglNm": "{seller_details.legal_name}",
+ "TrdNm": "{seller_details.trade_name}",
+ "Loc": "{seller_details.location}",
+ "Pin": "{seller_details.pincode}",
+ "Stcd": "{seller_details.state_code}",
+ "Addr1": "{seller_details.address_line1}",
+ "Addr2": "{seller_details.address_line2}",
+ "Ph": "{seller_details.phone}",
+ "Em": "{seller_details.email}"
+ }},
+ "BuyerDtls": {{
+ "Gstin": "{buyer_details.gstin}",
+ "LglNm": "{buyer_details.legal_name}",
+ "TrdNm": "{buyer_details.trade_name}",
+ "Addr1": "{buyer_details.address_line1}",
+ "Addr2": "{buyer_details.address_line2}",
+ "Loc": "{buyer_details.location}",
+ "Pin": "{buyer_details.pincode}",
+ "Stcd": "{buyer_details.state_code}",
+ "Ph": "{buyer_details.phone}",
+ "Em": "{buyer_details.email}",
+ "Pos": "{buyer_details.place_of_supply}"
+ }},
+ "DispDtls": {{
+ "Nm": "{dispatch_details.company_name}",
+ "Addr1": "{dispatch_details.address_line1}",
+ "Addr2": "{dispatch_details.address_line2}",
+ "Loc": "{dispatch_details.location}",
+ "Pin": "{dispatch_details.pincode}",
+ "Stcd": "{dispatch_details.state_code}"
+ }},
+ "ShipDtls": {{
+ "Gstin": "{shipping_details.gstin}",
+ "LglNm": "{shipping_details.legal_name}",
+ "TrdNm": "{shipping_details.trader_name}",
+ "Addr1": "{shipping_details.address_line1}",
+ "Addr2": "{shipping_details.address_line2}",
+ "Loc": "{shipping_details.location}",
+ "Pin": "{shipping_details.pincode}",
+ "Stcd": "{shipping_details.state_code}"
+ }},
+ "ItemList": [
+ {item_list}
+ ],
+ "ValDtls": {{
+ "AssVal": "{invoice_value_details.base_total}",
+ "CgstVal": "{invoice_value_details.total_cgst_amt}",
+ "SgstVal": "{invoice_value_details.total_sgst_amt}",
+ "IgstVal": "{invoice_value_details.total_igst_amt}",
+ "CesVal": "{invoice_value_details.total_cess_amt}",
+ "Discount": "{invoice_value_details.invoice_discount_amt}",
+ "RndOffAmt": "{invoice_value_details.round_off}",
+ "OthChrg": "{invoice_value_details.total_other_charges}",
+ "TotInvVal": "{invoice_value_details.base_grand_total}",
+ "TotInvValFc": "{invoice_value_details.grand_total}"
+ }},
+ "PayDtls": {{
+ "Nm": "{payment_details.payee_name}",
+ "AccDet": "{payment_details.account_no}",
+ "Mode": "{payment_details.mode_of_payment}",
+ "FinInsBr": "{payment_details.ifsc_code}",
+ "PayTerm": "{payment_details.terms}",
+ "PaidAmt": "{payment_details.paid_amount}",
+ "PaymtDue": "{payment_details.outstanding_amount}"
+ }},
+ "RefDtls": {{
+ "DocPerdDtls": {{
+ "InvStDt": "{period_details.start_date}",
+ "InvEndDt": "{period_details.end_date}"
+ }},
+ "PrecDocDtls": [{{
+ "InvNo": "{prev_doc_details.invoice_name}",
+ "InvDt": "{prev_doc_details.invoice_date}"
+ }}]
+ }},
+ "ExpDtls": {{
+ "ShipBNo": "{export_details.bill_no}",
+ "ShipBDt": "{export_details.bill_date}",
+ "Port": "{export_details.port}",
+ "ForCur": "{export_details.foreign_curr_code}",
+ "CntCode": "{export_details.country_code}",
+ "ExpDuty": "{export_details.export_duty}"
+ }},
+ "EwbDtls": {{
+ "TransId": "{eway_bill_details.gstin}",
+ "TransName": "{eway_bill_details.name}",
+ "TransMode": "{eway_bill_details.mode_of_transport}",
+ "Distance": "{eway_bill_details.distance}",
+ "TransDocNo": "{eway_bill_details.document_name}",
+ "TransDocDt": "{eway_bill_details.document_date}",
+ "VehNo": "{eway_bill_details.vehicle_no}",
+ "VehType": "{eway_bill_details.vehicle_type}"
+ }}
+}}
\ No newline at end of file
diff --git a/erpnext/regional/india/e_invoice/einv_validation.json b/erpnext/regional/india/e_invoice/einv_validation.json
new file mode 100644
index 0000000..86290cf
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einv_validation.json
@@ -0,0 +1,956 @@
+{
+ "Version": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 6,
+ "description": "Version of the schema"
+ },
+ "Irn": {
+ "type": "string",
+ "minLength": 64,
+ "maxLength": 64,
+ "description": "Invoice Reference Number"
+ },
+ "TranDtls": {
+ "type": "object",
+ "properties": {
+ "TaxSch": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 10,
+ "enum": ["GST"],
+ "description": "GST- Goods and Services Tax Scheme"
+ },
+ "SupTyp": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 10,
+ "enum": ["B2B", "SEZWP", "SEZWOP", "EXPWP", "EXPWOP", "DEXP"],
+ "description": "Type of Supply: B2B-Business to Business, SEZWP - SEZ with payment, SEZWOP - SEZ without payment, EXPWP - Export with Payment, EXPWOP - Export without payment,DEXP - Deemed Export"
+ },
+ "RegRev": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["Y", "N"],
+ "description": "Y- whether the tax liability is payable under reverse charge"
+ },
+ "EcmGstin": {
+ "type": "string",
+ "minLength": 15,
+ "maxLength": 15,
+ "pattern": "([0-9]{2}[0-9A-Z]{13})",
+ "description": "E-Commerce GSTIN",
+ "validationMsg": "E-Commerce GSTIN is invalid"
+ },
+ "IgstOnIntra": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["Y", "N"],
+ "description": "Y- indicates the supply is intra state but chargeable to IGST"
+ }
+ },
+ "required": ["TaxSch", "SupTyp"]
+ },
+ "DocDtls": {
+ "type": "object",
+ "properties": {
+ "Typ": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 3,
+ "enum": ["INV", "CRN", "DBN"],
+ "description": "Document Type"
+ },
+ "No": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 16,
+ "pattern": "^([A-Z1-9]{1}[A-Z0-9/-]{0,15})$",
+ "description": "Document Number",
+ "validationMsg": "Document Number should not be starting with 0, / and -"
+ },
+ "Dt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Document Date"
+ }
+ },
+ "required": ["Typ", "No", "Dt"]
+ },
+ "SellerDtls": {
+ "type": "object",
+ "properties": {
+ "Gstin": {
+ "type": "string",
+ "minLength": 15,
+ "maxLength": 15,
+ "pattern": "([0-9]{2}[0-9A-Z]{13})",
+ "description": "Supplier GSTIN",
+ "validationMsg": "Company GSTIN is invalid"
+ },
+ "LglNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Legal Name"
+ },
+ "TrdNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Tradename"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 50,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "Supplier State Code"
+ },
+ "Ph": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 12,
+ "description": "Phone"
+ },
+ "Em": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 100,
+ "description": "Email-Id"
+ }
+ },
+ "required": ["Gstin", "LglNm", "Addr1", "Loc", "Pin", "Stcd"]
+ },
+ "BuyerDtls": {
+ "type": "object",
+ "properties": {
+ "Gstin": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 15,
+ "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
+ "description": "Buyer GSTIN",
+ "validationMsg": "Customer GSTIN is invalid"
+ },
+ "LglNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Legal Name"
+ },
+ "TrdNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Trade Name"
+ },
+ "Pos": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "Place of Supply State code"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "Buyer State Code"
+ },
+ "Ph": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 12,
+ "description": "Phone"
+ },
+ "Em": {
+ "type": "string",
+ "minLength": 6,
+ "maxLength": 100,
+ "description": "Email-Id"
+ }
+ },
+ "required": ["Gstin", "LglNm", "Pos", "Addr1", "Loc", "Stcd"]
+ },
+ "DispDtls": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Dispatch Address Name"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "State Code"
+ }
+ },
+ "required": ["Nm", "Addr1", "Loc", "Pin", "Stcd"]
+ },
+ "ShipDtls": {
+ "type": "object",
+ "properties": {
+ "Gstin": {
+ "type": "string",
+ "maxLength": 15,
+ "minLength": 3,
+ "pattern": "^(([0-9]{2}[0-9A-Z]{13})|URP)$",
+ "description": "Shipping Address GSTIN",
+ "validationMsg": "Shipping Address GSTIN is invalid"
+ },
+ "LglNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Legal Name"
+ },
+ "TrdNm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Trade Name"
+ },
+ "Addr1": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Address Line 1"
+ },
+ "Addr2": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Address Line 2"
+ },
+ "Loc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Location"
+ },
+ "Pin": {
+ "type": "number",
+ "minimum": 100000,
+ "maximum": 999999,
+ "description": "Pincode"
+ },
+ "Stcd": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 2,
+ "description": "State Code"
+ }
+ },
+ "required": ["LglNm", "Addr1", "Loc", "Pin", "Stcd"]
+ },
+ "ItemList": {
+ "type": "Array",
+ "properties": {
+ "SlNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 6,
+ "description": "Serial No. of Item"
+ },
+ "PrdDesc": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 300,
+ "description": "Item Name"
+ },
+ "IsServc": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["Y", "N"],
+ "description": "Is Service Item"
+ },
+ "HsnCd": {
+ "type": "string",
+ "minLength": 4,
+ "maxLength": 8,
+ "description": "HSN Code"
+ },
+ "Barcde": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 30,
+ "description": "Barcode"
+ },
+ "Qty": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 9999999999.999,
+ "description": "Quantity"
+ },
+ "FreeQty": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 9999999999.999,
+ "description": "Free Quantity"
+ },
+ "Unit": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 8,
+ "description": "UOM"
+ },
+ "UnitPrice": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.999,
+ "description": "Rate"
+ },
+ "TotAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Gross Amount"
+ },
+ "Discount": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Discount"
+ },
+ "PreTaxVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Pre tax value"
+ },
+ "AssAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Taxable Value"
+ },
+ "GstRt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999.999,
+ "description": "GST Rate"
+ },
+ "IgstAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "IGST Amount"
+ },
+ "CgstAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "CGST Amount"
+ },
+ "SgstAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "SGST Amount"
+ },
+ "CesRt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999.999,
+ "description": "Cess Rate"
+ },
+ "CesAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Cess Amount (Advalorem)"
+ },
+ "CesNonAdvlAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Cess Amount (Non-Advalorem)"
+ },
+ "StateCesRt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999.999,
+ "description": "State CESS Rate"
+ },
+ "StateCesAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "State CESS Amount"
+ },
+ "StateCesNonAdvlAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "State CESS Amount (Non Advalorem)"
+ },
+ "OthChrg": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Other Charges"
+ },
+ "TotItemVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Total Item Value"
+ },
+ "OrdLineRef": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 50,
+ "description": "Order line reference"
+ },
+ "OrgCntry": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 2,
+ "description": "Origin Country"
+ },
+ "PrdSlNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "description": "Serial number"
+ },
+ "BchDtls": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 20,
+ "description": "Batch number"
+ },
+ "ExpDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Batch Expiry Date"
+ },
+ "WrDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Warranty Date"
+ }
+ },
+ "required": ["Nm"]
+ },
+ "AttribDtls": {
+ "type": "Array",
+ "Attribute": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Attribute name of the item"
+ },
+ "Val": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Attribute value of the item"
+ }
+ }
+ }
+ }
+ },
+ "required": [
+ "SlNo",
+ "IsServc",
+ "HsnCd",
+ "UnitPrice",
+ "TotAmt",
+ "AssAmt",
+ "GstRt",
+ "TotItemVal"
+ ]
+ },
+ "ValDtls": {
+ "type": "object",
+ "properties": {
+ "AssVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total Assessable value of all items"
+ },
+ "CgstVal": {
+ "type": "number",
+ "maximum": 99999999999999.99,
+ "minimum": 0,
+ "description": "Total CGST value of all items"
+ },
+ "SgstVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total SGST value of all items"
+ },
+ "IgstVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total IGST value of all items"
+ },
+ "CesVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total CESS value of all items"
+ },
+ "StCesVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Total State CESS value of all items"
+ },
+ "Discount": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Invoice Discount"
+ },
+ "OthChrg": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Other Charges"
+ },
+ "RndOffAmt": {
+ "type": "number",
+ "minimum": -99.99,
+ "maximum": 99.99,
+ "description": "Rounded off Amount"
+ },
+ "TotInvVal": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Final Invoice Value "
+ },
+ "TotInvValFc": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Final Invoice value in Foreign Currency"
+ }
+ },
+ "required": ["AssVal", "TotInvVal"]
+ },
+ "PayDtls": {
+ "type": "object",
+ "properties": {
+ "Nm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Payee Name"
+ },
+ "AccDet": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 18,
+ "description": "Bank Account Number of Payee"
+ },
+ "Mode": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 18,
+ "description": "Mode of Payment"
+ },
+ "FinInsBr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 11,
+ "description": "Branch or IFSC code"
+ },
+ "PayTerm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Terms of Payment"
+ },
+ "PayInstr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Payment Instruction"
+ },
+ "CrTrn": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Credit Transfer"
+ },
+ "DirDr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 100,
+ "description": "Direct Debit"
+ },
+ "CrDay": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 9999,
+ "description": "Credit Days"
+ },
+ "PaidAmt": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Advance Amount"
+ },
+ "PaymtDue": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 99999999999999.99,
+ "description": "Outstanding Amount"
+ }
+ }
+ },
+ "RefDtls": {
+ "type": "object",
+ "properties": {
+ "InvRm": {
+ "type": "string",
+ "maxLength": 100,
+ "minLength": 3,
+ "pattern": "^[0-9A-Za-z/-]{3,100}$",
+ "description": "Remarks/Note"
+ },
+ "DocPerdDtls": {
+ "type": "object",
+ "properties": {
+ "InvStDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Invoice Period Start Date"
+ },
+ "InvEndDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Invoice Period End Date"
+ }
+ },
+ "required": ["InvStDt ", "InvEndDt "]
+ },
+ "PrecDocDtls": {
+ "type": "object",
+ "properties": {
+ "InvNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 16,
+ "pattern": "^[1-9A-Z]{1}[0-9A-Z/-]{1,15}$",
+ "description": "Reference of Original Invoice"
+ },
+ "InvDt": {
+ "type": "string",
+ "maxLength": 10,
+ "minLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Date of Orginal Invoice"
+ },
+ "OthRefNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "description": "Other Reference"
+ }
+ }
+ },
+ "required": ["InvNo", "InvDt"],
+ "ContrDtls": {
+ "type": "object",
+ "properties": {
+ "RecAdvRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Receipt Advice No."
+ },
+ "RecAdvDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Date of receipt advice"
+ },
+ "TendRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Lot/Batch Reference No."
+ },
+ "ContrRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Contract Reference Number"
+ },
+ "ExtRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Any other reference"
+ },
+ "ProjRefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "pattern": "^([0-9A-Za-z/-]){1,20}$",
+ "description": "Project Reference Number"
+ },
+ "PORefr": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 16,
+ "pattern": "^([0-9A-Za-z/-]){1,16}$",
+ "description": "PO Reference Number"
+ },
+ "PORefDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "PO Reference date"
+ }
+ }
+ }
+ }
+ },
+ "AddlDocDtls": {
+ "type": "Array",
+ "properties": {
+ "Url": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Supporting document URL"
+ },
+ "Docs": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 1000,
+ "description": "Supporting document in Base64 Format"
+ },
+ "Info": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 1000,
+ "description": "Any additional information"
+ }
+ }
+ },
+
+ "ExpDtls": {
+ "type": "object",
+ "properties": {
+ "ShipBNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 20,
+ "description": "Shipping Bill No."
+ },
+ "ShipBDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Shipping Bill Date"
+ },
+ "Port": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 10,
+ "pattern": "^[0-9A-Za-z]{2,10}$",
+ "description": "Port Code. Refer the master"
+ },
+ "RefClm": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "description": "Claiming Refund. Y/N"
+ },
+ "ForCur": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 16,
+ "description": "Additional Currency Code. Refer the master"
+ },
+ "CntCode": {
+ "type": "string",
+ "minLength": 2,
+ "maxLength": 2,
+ "description": "Country Code. Refer the master"
+ },
+ "ExpDuty": {
+ "type": "number",
+ "minimum": 0,
+ "maximum": 999999999999.99,
+ "description": "Export Duty"
+ }
+ }
+ },
+ "EwbDtls": {
+ "type": "object",
+ "properties": {
+ "TransId": {
+ "type": "string",
+ "minLength": 15,
+ "maxLength": 15,
+ "description": "Transporter GSTIN"
+ },
+ "TransName": {
+ "type": "string",
+ "minLength": 3,
+ "maxLength": 100,
+ "description": "Transporter Name"
+ },
+ "TransMode": {
+ "type": "string",
+ "maxLength": 1,
+ "minLength": 1,
+ "enum": ["1", "2", "3", "4"],
+ "description": "Mode of Transport"
+ },
+ "Distance": {
+ "type": "number",
+ "minimum": 1,
+ "maximum": 9999,
+ "description": "Distance"
+ },
+ "TransDocNo": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 15,
+ "pattern": "^([0-9A-Z/-]){1,15}$",
+ "description": "Tranport Document Number"
+ },
+ "TransDocDt": {
+ "type": "string",
+ "minLength": 10,
+ "maxLength": 10,
+ "pattern": "[0-3][0-9]/[0-1][0-9]/[2][0][1-2][0-9]",
+ "description": "Transport Document Date"
+ },
+ "VehNo": {
+ "type": "string",
+ "minLength": 4,
+ "maxLength": 20,
+ "description": "Vehicle Number"
+ },
+ "VehType": {
+ "type": "string",
+ "minLength": 1,
+ "maxLength": 1,
+ "enum": ["O", "R"],
+ "description": "Vehicle Type"
+ }
+ },
+ "required": ["Distance"]
+ },
+ "required": [
+ "Version",
+ "TranDtls",
+ "DocDtls",
+ "SellerDtls",
+ "BuyerDtls",
+ "ItemList",
+ "ValDtls"
+ ]
+}
diff --git a/erpnext/regional/india/e_invoice/einvoice.js b/erpnext/regional/india/e_invoice/einvoice.js
new file mode 100644
index 0000000..a756b57
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/einvoice.js
@@ -0,0 +1,307 @@
+erpnext.setup_einvoice_actions = (doctype) => {
+ frappe.ui.form.on(doctype, {
+ refresh(frm) {
+ const einvoicing_enabled = frappe.db.get_value("E Invoice Settings", "E Invoice Settings", "enable");
+ const supply_type = frm.doc.gst_category;
+ const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type);
+ const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin;
+
+ if (!einvoicing_enabled || !valid_supply_type || company_transaction) return;
+
+ const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc;
+
+ const add_custom_button = (label, action) => {
+ if (!frm.custom_buttons[label]) {
+ frm.add_custom_button(label, action, __('E Invoicing'));
+ }
+ };
+
+ if (!irn && !__unsaved) {
+ const action = () => {
+ if (frm.doc.__unsaved) {
+ frappe.throw(__('Please save the document to generate IRN.'));
+ }
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.get_einvoice',
+ args: { doctype, docname: name },
+ freeze: true,
+ callback: (res) => {
+ const einvoice = res.message;
+ show_einvoice_preview(frm, einvoice);
+ }
+ });
+ };
+
+ add_custom_button(__("Generate IRN"), action);
+ }
+
+ if (irn && !irn_cancelled && !ewaybill) {
+ const fields = [
+ {
+ "label": "Reason",
+ "fieldname": "reason",
+ "fieldtype": "Select",
+ "reqd": 1,
+ "default": "1-Duplicate",
+ "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
+ },
+ {
+ "label": "Remark",
+ "fieldname": "remark",
+ "fieldtype": "Data",
+ "reqd": 1
+ }
+ ];
+ const action = () => {
+ const d = new frappe.ui.Dialog({
+ title: __("Cancel IRN"),
+ fields: fields,
+ primary_action: function() {
+ const data = d.get_values();
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.cancel_irn',
+ args: {
+ doctype,
+ docname: name,
+ irn: irn,
+ reason: data.reason.split('-')[0],
+ remark: data.remark
+ },
+ freeze: true,
+ callback: () => frm.reload_doc() || d.hide(),
+ error: () => d.hide()
+ });
+ },
+ primary_action_label: __('Submit')
+ });
+ d.show();
+ };
+ add_custom_button(__("Cancel IRN"), action);
+ }
+
+ if (irn && !irn_cancelled && !ewaybill) {
+ const action = () => {
+ const d = new frappe.ui.Dialog({
+ title: __('Generate E-Way Bill'),
+ wide: 1,
+ fields: get_ewaybill_fields(frm),
+ primary_action: function() {
+ const data = d.get_values();
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.generate_eway_bill',
+ args: {
+ doctype,
+ docname: name,
+ irn,
+ ...data
+ },
+ freeze: true,
+ callback: () => frm.reload_doc() || d.hide(),
+ error: () => d.hide()
+ });
+ },
+ primary_action_label: __('Submit')
+ });
+ d.show();
+ };
+
+ add_custom_button(__("Generate E-Way Bill"), action);
+ }
+
+ if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) {
+ const fields = [
+ {
+ "label": "Reason",
+ "fieldname": "reason",
+ "fieldtype": "Select",
+ "reqd": 1,
+ "default": "1-Duplicate",
+ "options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"]
+ },
+ {
+ "label": "Remark",
+ "fieldname": "remark",
+ "fieldtype": "Data",
+ "reqd": 1
+ }
+ ];
+ const action = () => {
+ const d = new frappe.ui.Dialog({
+ title: __('Cancel E-Way Bill'),
+ fields: fields,
+ primary_action: function() {
+ const data = d.get_values();
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill',
+ args: {
+ doctype,
+ docname: name,
+ eway_bill: ewaybill,
+ reason: data.reason.split('-')[0],
+ remark: data.remark
+ },
+ freeze: true,
+ callback: () => frm.reload_doc() || d.hide(),
+ error: () => d.hide()
+ });
+ },
+ primary_action_label: __('Submit')
+ });
+ d.show();
+ };
+ add_custom_button(__("Cancel E-Way Bill"), action);
+ }
+ }
+ });
+};
+
+const get_ewaybill_fields = (frm) => {
+ return [
+ {
+ 'fieldname': 'transporter',
+ 'label': 'Transporter',
+ 'fieldtype': 'Link',
+ 'options': 'Supplier',
+ 'default': frm.doc.transporter
+ },
+ {
+ 'fieldname': 'gst_transporter_id',
+ 'label': 'GST Transporter ID',
+ 'fieldtype': 'Data',
+ 'fetch_from': 'transporter.gst_transporter_id',
+ 'default': frm.doc.gst_transporter_id
+ },
+ {
+ 'fieldname': 'driver',
+ 'label': 'Driver',
+ 'fieldtype': 'Link',
+ 'options': 'Driver',
+ 'default': frm.doc.driver
+ },
+ {
+ 'fieldname': 'lr_no',
+ 'label': 'Transport Receipt No',
+ 'fieldtype': 'Data',
+ 'default': frm.doc.lr_no
+ },
+ {
+ 'fieldname': 'vehicle_no',
+ 'label': 'Vehicle No',
+ 'fieldtype': 'Data',
+ 'default': frm.doc.vehicle_no
+ },
+ {
+ 'fieldname': 'distance',
+ 'label': 'Distance (in km)',
+ 'fieldtype': 'Float',
+ 'default': frm.doc.distance
+ },
+ {
+ 'fieldname': 'transporter_col_break',
+ 'fieldtype': 'Column Break',
+ },
+ {
+ 'fieldname': 'transporter_name',
+ 'label': 'Transporter Name',
+ 'fieldtype': 'Data',
+ 'fetch_from': 'transporter.name',
+ 'read_only': 1,
+ 'default': frm.doc.transporter_name
+ },
+ {
+ 'fieldname': 'mode_of_transport',
+ 'label': 'Mode of Transport',
+ 'fieldtype': 'Select',
+ 'options': `\nRoad\nAir\nRail\nShip`,
+ 'default': frm.doc.mode_of_transport
+ },
+ {
+ 'fieldname': 'driver_name',
+ 'label': 'Driver Name',
+ 'fieldtype': 'Data',
+ 'fetch_from': 'driver.full_name',
+ 'read_only': 1,
+ 'default': frm.doc.driver_name
+ },
+ {
+ 'fieldname': 'lr_date',
+ 'label': 'Transport Receipt Date',
+ 'fieldtype': 'Date',
+ 'default': frm.doc.lr_date
+ },
+ {
+ 'fieldname': 'gst_vehicle_type',
+ 'label': 'GST Vehicle Type',
+ 'fieldtype': 'Select',
+ 'options': `Regular\nOver Dimensional Cargo (ODC)`,
+ 'depends_on': 'eval:(doc.mode_of_transport === "Road")',
+ 'default': frm.doc.gst_vehicle_type
+ }
+ ];
+};
+
+const request_irn_generation = (frm) => {
+ frappe.call({
+ method: 'erpnext.regional.india.e_invoice.utils.generate_irn',
+ args: { doctype: frm.doc.doctype, docname: frm.doc.name },
+ freeze: true,
+ callback: () => frm.reload_doc()
+ });
+};
+
+const get_preview_dialog = (frm, action) => {
+ const dialog = new frappe.ui.Dialog({
+ title: __("Preview"),
+ wide: 1,
+ fields: [
+ {
+ "label": "Preview",
+ "fieldname": "preview_html",
+ "fieldtype": "HTML"
+ }
+ ],
+ primary_action: () => action(frm) || dialog.hide(),
+ primary_action_label: __('Generate IRN')
+ });
+ return dialog;
+};
+
+const show_einvoice_preview = (frm, einvoice) => {
+ const preview_dialog = get_preview_dialog(frm, request_irn_generation);
+
+ // initialize e-invoice fields
+ einvoice["Irn"] = einvoice["AckNo"] = ''; einvoice["AckDt"] = frappe.datetime.nowdate();
+ frm.doc.signed_einvoice = JSON.stringify(einvoice);
+
+ // initialize preview wrapper
+ const $preview_wrapper = preview_dialog.get_field("preview_html").$wrapper;
+ $preview_wrapper.html(
+ `<div>
+ <div class="print-preview">
+ <div class="print-format"></div>
+ </div>
+ <div class="page-break-message text-muted text-center text-medium margin-top"></div>
+ </div>`
+ );
+
+ frappe.call({
+ method: "frappe.www.printview.get_html_and_style",
+ args: {
+ doc: frm.doc,
+ print_format: "GST E-Invoice",
+ no_letterhead: 1
+ },
+ callback: function (r) {
+ if (!r.exc) {
+ $preview_wrapper.find(".print-format").html(r.message.html);
+ const style = `
+ .print-format { box-shadow: 0px 0px 5px rgba(0,0,0,0.2); padding: 0.30in; min-height: 80vh; }
+ .print-preview { min-height: 0px; }
+ .modal-dialog { width: 720px; }`;
+
+ frappe.dom.set_style(style, "custom-print-style");
+ preview_dialog.show();
+ }
+ }
+ });
+};
\ No newline at end of file
diff --git a/erpnext/regional/india/e_invoice/utils.py b/erpnext/regional/india/e_invoice/utils.py
new file mode 100644
index 0000000..eea85cd
--- /dev/null
+++ b/erpnext/regional/india/e_invoice/utils.py
@@ -0,0 +1,849 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import os
+import re
+import jwt
+import sys
+import json
+import base64
+import frappe
+import six
+import traceback
+import io
+from frappe import _, bold
+from pyqrcode import create as qrcreate
+from frappe.integrations.utils import make_post_request, make_get_request
+from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply
+from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form
+
+def validate_einvoice_fields(doc):
+ einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable'))
+ invalid_doctype = doc.doctype != 'Sales Invoice'
+ invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export']
+ company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin')
+ no_taxes_applied = not doc.get('taxes')
+
+ if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied:
+ return
+
+ if doc.docstatus == 0 and doc._action == 'save':
+ if doc.irn:
+ frappe.throw(_('You cannot edit the invoice after generating IRN'), title=_('Edit Not Allowed'))
+ if len(doc.name) > 16:
+ raise_document_name_too_long_error()
+
+ elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn:
+ frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN'))
+
+ elif doc.irn and doc.docstatus == 2 and doc._action == 'cancel' and not doc.irn_cancelled:
+ frappe.throw(_('You must cancel IRN before cancelling the document.'), title=_('Cancel Not Allowed'))
+
+def raise_document_name_too_long_error():
+ title = _('Document ID Too Long')
+ msg = _('As you have E-Invoicing enabled, to be able to generate IRN for this invoice, ')
+ msg += _('document id {} exceed 16 letters. ').format(bold(_('should not')))
+ msg += '<br><br>'
+ msg += _('You must {} your {} in order to have document id of {} length 16. ').format(
+ bold(_('modify')), bold(_('naming series')), bold(_('maximum'))
+ )
+ msg += _('Please account for ammended documents too. ')
+ frappe.throw(msg, title=title)
+
+def read_json(name):
+ file_path = os.path.join(os.path.dirname(__file__), '{name}.json'.format(name=name))
+ with open(file_path, 'r') as f:
+ return cstr(f.read())
+
+def get_transaction_details(invoice):
+ supply_type = ''
+ if invoice.gst_category == 'Registered Regular': supply_type = 'B2B'
+ elif invoice.gst_category == 'SEZ': supply_type = 'SEZWOP'
+ elif invoice.gst_category == 'Overseas': supply_type = 'EXPWOP'
+ elif invoice.gst_category == 'Deemed Export': supply_type = 'DEXP'
+
+ if not supply_type:
+ rr, sez, overseas, export = bold('Registered Regular'), bold('SEZ'), bold('Overseas'), bold('Deemed Export')
+ frappe.throw(_('GST category should be one of {}, {}, {}, {}').format(rr, sez, overseas, export),
+ title=_('Invalid Supply Type'))
+
+ return frappe._dict(dict(
+ tax_scheme='GST',
+ supply_type=supply_type,
+ reverse_charge=invoice.reverse_charge
+ ))
+
+def get_doc_details(invoice):
+ invoice_type = 'CRN' if invoice.is_return else 'INV'
+
+ invoice_name = invoice.name
+ invoice_date = format_date(invoice.posting_date, 'dd/mm/yyyy')
+
+ return frappe._dict(dict(
+ invoice_type=invoice_type,
+ invoice_name=invoice_name,
+ invoice_date=invoice_date
+ ))
+
+def get_party_details(address_name):
+ d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0]
+
+ if (not d.gstin
+ or not d.city
+ or not d.pincode
+ or not d.address_title
+ or not d.address_line1
+ or not d.gst_state_number):
+
+ frappe.throw(
+ msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format(
+ get_link_to_form('Address', address_name)
+ ),
+ title=_('Missing Address Fields')
+ )
+
+ if d.gst_state_number == 97:
+ # according to einvoice standard
+ pincode = 999999
+
+ return frappe._dict(dict(
+ gstin=d.gstin,
+ legal_name=sanitize_for_json(d.address_title),
+ location=sanitize_for_json(d.city),
+ pincode=d.pincode,
+ state_code=d.gst_state_number,
+ address_line1=sanitize_for_json(d.address_line1),
+ address_line2=sanitize_for_json(d.address_line2)
+ ))
+
+def get_gstin_details(gstin):
+ if not hasattr(frappe.local, 'gstin_cache'):
+ frappe.local.gstin_cache = {}
+
+ key = gstin
+ details = frappe.local.gstin_cache.get(key)
+ if details:
+ return details
+
+ details = frappe.cache().hget('gstin_cache', key)
+ if details:
+ frappe.local.gstin_cache[key] = details
+ return details
+
+ if not details:
+ return GSPConnector.get_gstin_details(gstin)
+
+def get_overseas_address_details(address_name):
+ address_title, address_line1, address_line2, city = frappe.db.get_value(
+ 'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city']
+ )
+
+ if not address_title or not address_line1 or not city:
+ frappe.throw(
+ msg=_('Address lines and city is mandatory for address {}. Please set them and try again.').format(
+ get_link_to_form('Address', address_name)
+ ),
+ title=_('Missing Address Fields')
+ )
+
+ return frappe._dict(dict(
+ gstin='URP',
+ legal_name=sanitize_for_json(address_title),
+ location=city,
+ address_line1=sanitize_for_json(address_line1),
+ address_line2=sanitize_for_json(address_line2),
+ pincode=999999, state_code=96, place_of_supply=96
+ ))
+
+def get_item_list(invoice):
+ item_list = []
+
+ for d in invoice.items:
+ einvoice_item_schema = read_json('einv_item_template')
+ item = frappe._dict({})
+ item.update(d.as_dict())
+
+ item.sr_no = d.idx
+ item.description = sanitize_for_json(d.item_name)
+
+ item.qty = abs(item.qty)
+ item.discount_amount = 0
+ item.unit_rate = abs(item.base_net_amount / item.qty)
+ item.gross_amount = abs(item.base_net_amount)
+ item.taxable_value = abs(item.base_net_amount)
+
+ item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None
+ item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None
+ item.is_service_item = 'N' if frappe.db.get_value('Item', d.item_code, 'is_stock_item') else 'Y'
+ item.serial_no = ""
+
+ item = update_item_taxes(invoice, item)
+
+ item.total_value = abs(
+ item.taxable_value + item.igst_amount + item.sgst_amount +
+ item.cgst_amount + item.cess_amount + item.cess_nadv_amount + item.other_charges
+ )
+ einv_item = einvoice_item_schema.format(item=item)
+ item_list.append(einv_item)
+
+ return ', '.join(item_list)
+
+def update_item_taxes(invoice, item):
+ gst_accounts = get_gst_accounts(invoice.company)
+ gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
+
+ for attr in [
+ 'tax_rate', 'cess_rate', 'cess_nadv_amount',
+ 'cgst_amount', 'sgst_amount', 'igst_amount',
+ 'cess_amount', 'cess_nadv_amount', 'other_charges'
+ ]:
+ item[attr] = 0
+
+ for t in invoice.taxes:
+ # this contains item wise tax rate & tax amount (incl. discount)
+ item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code)
+ if t.account_head in gst_accounts_list:
+ item_tax_rate = item_tax_detail[0]
+ # item tax amount excluding discount amount
+ item_tax_amount = (item_tax_rate / 100) * item.base_net_amount
+
+ if t.account_head in gst_accounts.cess_account:
+ item_tax_amount_after_discount = item_tax_detail[1]
+ if t.charge_type == 'On Item Quantity':
+ item.cess_nadv_amount += abs(item_tax_amount_after_discount)
+ else:
+ item.cess_rate += item_tax_rate
+ item.cess_amount += abs(item_tax_amount_after_discount)
+
+ for tax_type in ['igst', 'cgst', 'sgst']:
+ if t.account_head in gst_accounts[f'{tax_type}_account']:
+ item.tax_rate += item_tax_rate
+ item[f'{tax_type}_amount'] += abs(item_tax_amount)
+
+ return item
+
+def get_invoice_value_details(invoice):
+ invoice_value_details = frappe._dict(dict())
+
+ if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount:
+ invoice_value_details.base_total = abs(invoice.base_total)
+ invoice_value_details.invoice_discount_amt = invoice.base_discount_amount
+ else:
+ invoice_value_details.base_total = abs(invoice.base_net_total)
+ # since tax already considers discount amount
+ invoice_value_details.invoice_discount_amt = 0
+
+ invoice_value_details.round_off = invoice.base_rounding_adjustment
+ invoice_value_details.base_grand_total = abs(invoice.base_rounded_total) or abs(invoice.base_grand_total)
+ invoice_value_details.grand_total = abs(invoice.rounded_total) or abs(invoice.grand_total)
+
+ invoice_value_details = update_invoice_taxes(invoice, invoice_value_details)
+
+ return invoice_value_details
+
+def update_invoice_taxes(invoice, invoice_value_details):
+ gst_accounts = get_gst_accounts(invoice.company)
+ gst_accounts_list = [d for accounts in gst_accounts.values() for d in accounts if d]
+
+ invoice_value_details.total_cgst_amt = 0
+ invoice_value_details.total_sgst_amt = 0
+ invoice_value_details.total_igst_amt = 0
+ invoice_value_details.total_cess_amt = 0
+ invoice_value_details.total_other_charges = 0
+ for t in invoice.taxes:
+ if t.account_head in gst_accounts_list:
+ if t.account_head in gst_accounts.cess_account:
+ # using after discount amt since item also uses after discount amt for cess calc
+ invoice_value_details.total_cess_amt += abs(t.base_tax_amount_after_discount_amount)
+
+ for tax_type in ['igst', 'cgst', 'sgst']:
+ if t.account_head in gst_accounts[f'{tax_type}_account']:
+ invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount)
+ else:
+ invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount)
+
+ return invoice_value_details
+
+def get_payment_details(invoice):
+ payee_name = invoice.company
+ mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments])
+ paid_amount = invoice.base_paid_amount
+ outstanding_amount = invoice.outstanding_amount
+
+ return frappe._dict(dict(
+ payee_name=payee_name, mode_of_payment=mode_of_payment,
+ paid_amount=paid_amount, outstanding_amount=outstanding_amount
+ ))
+
+def get_return_doc_reference(invoice):
+ invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date')
+ return frappe._dict(dict(
+ invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy')
+ ))
+
+def get_eway_bill_details(invoice):
+ if invoice.is_return:
+ frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed'))
+
+ mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' }
+ vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' }
+
+ return frappe._dict(dict(
+ gstin=invoice.gst_transporter_id,
+ name=invoice.transporter_name,
+ mode_of_transport=mode_of_transport[invoice.mode_of_transport],
+ distance=invoice.distance or 0,
+ document_name=invoice.lr_no,
+ document_date=format_date(invoice.lr_date, 'dd/mm/yyyy'),
+ vehicle_no=invoice.vehicle_no,
+ vehicle_type=vehicle_type[invoice.gst_vehicle_type]
+ ))
+
+def validate_mandatory_fields(invoice):
+ if not invoice.company_address:
+ frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields'))
+ if not invoice.customer_address:
+ frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields'))
+ if not frappe.db.get_value('Address', invoice.company_address, 'gstin'):
+ frappe.throw(
+ _('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'),
+ title=_('Missing Fields')
+ )
+ if invoice.gst_category != 'Overseas' and not frappe.db.get_value('Address', invoice.customer_address, 'gstin'):
+ frappe.throw(
+ _('GSTIN is mandatory to fetch customer GSTIN details. Please enter GSTIN in selected customer address.'),
+ title=_('Missing Fields')
+ )
+
+def make_einvoice(invoice):
+ validate_mandatory_fields(invoice)
+
+ schema = read_json('einv_template')
+
+ transaction_details = get_transaction_details(invoice)
+ item_list = get_item_list(invoice)
+ doc_details = get_doc_details(invoice)
+ invoice_value_details = get_invoice_value_details(invoice)
+ seller_details = get_party_details(invoice.company_address)
+
+ if invoice.gst_category == 'Overseas':
+ buyer_details = get_overseas_address_details(invoice.customer_address)
+ else:
+ buyer_details = get_party_details(invoice.customer_address)
+ place_of_supply = get_place_of_supply(invoice, invoice.doctype) or sanitize_for_json(invoice.billing_address_gstin)
+ place_of_supply = place_of_supply[:2]
+ buyer_details.update(dict(place_of_supply=place_of_supply))
+
+ shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({})
+ if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name:
+ if invoice.gst_category == 'Overseas':
+ shipping_details = get_overseas_address_details(invoice.shipping_address_name)
+ else:
+ shipping_details = get_party_details(invoice.shipping_address_name)
+
+ if invoice.is_pos and invoice.base_paid_amount:
+ payment_details = get_payment_details(invoice)
+
+ if invoice.is_return and invoice.return_against:
+ prev_doc_details = get_return_doc_reference(invoice)
+
+ if invoice.transporter:
+ eway_bill_details = get_eway_bill_details(invoice)
+
+ # not yet implemented
+ dispatch_details = period_details = export_details = frappe._dict({})
+
+ einvoice = schema.format(
+ transaction_details=transaction_details, doc_details=doc_details, dispatch_details=dispatch_details,
+ seller_details=seller_details, buyer_details=buyer_details, shipping_details=shipping_details,
+ item_list=item_list, invoice_value_details=invoice_value_details, payment_details=payment_details,
+ period_details=period_details, prev_doc_details=prev_doc_details,
+ export_details=export_details, eway_bill_details=eway_bill_details
+ )
+ einvoice = safe_json_load(einvoice)
+
+ validations = json.loads(read_json('einv_validation'))
+ errors = validate_einvoice(validations, einvoice)
+ if errors:
+ message = "\n".join([
+ "E Invoice: ", json.dumps(einvoice, indent=4),
+ "-" * 50,
+ "Errors: ", json.dumps(errors, indent=4)
+ ])
+ frappe.log_error(title="E Invoice Validation Failed", message=message)
+ frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1)
+
+ return einvoice
+
+def safe_json_load(json_string):
+ JSONDecodeError = ValueError if six.PY2 else json.JSONDecodeError
+
+ try:
+ return json.loads(json_string)
+ except JSONDecodeError as e:
+ # print a snippet of 40 characters around the location where error occured
+ pos = e.pos
+ start, end = max(0, pos-20), min(len(json_string)-1, pos+20)
+ snippet = json_string[start:end]
+ frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet))
+
+def validate_einvoice(validations, einvoice, errors=[]):
+ for fieldname, field_validation in validations.items():
+ value = einvoice.get(fieldname, None)
+ if not value or value == "None":
+ # remove keys with empty values
+ einvoice.pop(fieldname, None)
+ continue
+
+ value_type = field_validation.get("type").lower()
+ if value_type in ['object', 'array']:
+ child_validations = field_validation.get('properties')
+
+ if isinstance(value, list):
+ for d in value:
+ validate_einvoice(child_validations, d, errors)
+ if not d:
+ # remove empty dicts
+ einvoice.pop(fieldname, None)
+ else:
+ validate_einvoice(child_validations, value, errors)
+ if not value:
+ # remove empty dicts
+ einvoice.pop(fieldname, None)
+ continue
+
+ # convert to int or str
+ if value_type == 'string':
+ einvoice[fieldname] = str(value)
+ elif value_type == 'number':
+ is_integer = '.' not in str(field_validation.get('maximum'))
+ precision = 3 if '.999' in str(field_validation.get('maximum')) else 2
+ einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value)
+ value = einvoice[fieldname]
+
+ max_length = field_validation.get('maxLength')
+ minimum = flt(field_validation.get('minimum'))
+ maximum = flt(field_validation.get('maximum'))
+ pattern_str = field_validation.get('pattern')
+ pattern = re.compile(pattern_str or '')
+
+ label = field_validation.get('description') or fieldname
+
+ if value_type == 'string' and len(value) > max_length:
+ errors.append(_('{} should not exceed {} characters').format(label, max_length))
+ if value_type == 'number' and (value > maximum or value < minimum):
+ errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum))
+ if pattern_str and not pattern.match(value):
+ errors.append(field_validation.get('validationMsg'))
+
+ return errors
+
+class RequestFailed(Exception): pass
+
+class GSPConnector():
+ def __init__(self, doctype=None, docname=None):
+ self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings')
+ sandbox_mode = self.e_invoice_settings.sandbox_mode
+
+ self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None
+ self.credentials = self.get_credentials()
+
+ # authenticate url is same for sandbox & live
+ self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token'
+ self.base_url = 'https://gsp.adaequare.com' if not sandbox_mode else 'https://gsp.adaequare.com/test'
+
+ self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel'
+ self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn'
+ self.generate_irn_url = self.base_url + '/enriched/ei/api/invoice'
+ self.gstin_details_url = self.base_url + '/enriched/ei/api/master/gstin'
+ self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB'
+ self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill'
+
+ def get_credentials(self):
+ if self.invoice:
+ gstin = self.get_seller_gstin()
+ if not self.e_invoice_settings.enable:
+ frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings")))
+ credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin)
+ else:
+ credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None
+ return credentials
+
+ def get_seller_gstin(self):
+ gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin')
+ if not gstin:
+ frappe.throw(_('Cannot retrieve Company GSTIN. Please select company address with valid GSTIN.'))
+ return gstin
+
+ def get_auth_token(self):
+ if time_diff_in_seconds(self.e_invoice_settings.token_expiry, now_datetime()) < 150.0:
+ self.fetch_auth_token()
+
+ return self.e_invoice_settings.auth_token
+
+ def make_request(self, request_type, url, headers=None, data=None):
+ if request_type == 'post':
+ res = make_post_request(url, headers=headers, data=data)
+ else:
+ res = make_get_request(url, headers=headers, data=data)
+
+ self.log_request(url, headers, data, res)
+ return res
+
+ def log_request(self, url, headers, data, res):
+ headers.update({ 'password': self.credentials.password })
+ request_log = frappe.get_doc({
+ "doctype": "E Invoice Request Log",
+ "user": frappe.session.user,
+ "reference_invoice": self.invoice.name if self.invoice else None,
+ "url": url,
+ "headers": json.dumps(headers, indent=4) if headers else None,
+ "data": json.dumps(data, indent=4) if isinstance(data, dict) else data,
+ "response": json.dumps(res, indent=4) if res else None
+ })
+ request_log.save(ignore_permissions=True)
+ frappe.db.commit()
+
+ def fetch_auth_token(self):
+ headers = {
+ 'gspappid': frappe.conf.einvoice_client_id,
+ 'gspappsecret': frappe.conf.einvoice_client_secret
+ }
+ res = {}
+ try:
+ res = self.make_request('post', self.authenticate_url, headers)
+ self.e_invoice_settings.auth_token = "{} {}".format(res.get('token_type'), res.get('access_token'))
+ self.e_invoice_settings.token_expiry = add_to_date(None, seconds=res.get('expires_in'))
+ self.e_invoice_settings.save(ignore_permissions=True)
+ self.e_invoice_settings.reload()
+
+ except Exception:
+ self.log_error(res)
+ self.raise_error(True)
+
+ def get_headers(self):
+ return {
+ 'content-type': 'application/json',
+ 'user_name': self.credentials.username,
+ 'password': self.credentials.get_password(),
+ 'gstin': self.credentials.gstin,
+ 'authorization': self.get_auth_token(),
+ 'requestid': str(base64.b64encode(os.urandom(18))),
+ }
+
+ def fetch_gstin_details(self, gstin):
+ headers = self.get_headers()
+
+ try:
+ params = '?gstin={gstin}'.format(gstin=gstin)
+ res = self.make_request('get', self.gstin_details_url + params, headers)
+ if res.get('success'):
+ return res.get('result')
+ else:
+ self.log_error(res)
+ raise RequestFailed
+
+ except RequestFailed:
+ self.raise_error()
+
+ except Exception:
+ self.log_error()
+ self.raise_error(True)
+
+ @staticmethod
+ def get_gstin_details(gstin):
+ '''fetch and cache GSTIN details'''
+ if not hasattr(frappe.local, 'gstin_cache'):
+ frappe.local.gstin_cache = {}
+
+ key = gstin
+ gsp_connector = GSPConnector()
+ details = gsp_connector.fetch_gstin_details(gstin)
+
+ frappe.local.gstin_cache[key] = details
+ frappe.cache().hset('gstin_cache', key, details)
+ return details
+
+ def generate_irn(self):
+ headers = self.get_headers()
+ einvoice = make_einvoice(self.invoice)
+ data = json.dumps(einvoice, indent=4)
+
+ try:
+ res = self.make_request('post', self.generate_irn_url, headers, data)
+ if res.get('success'):
+ self.set_einvoice_data(res.get('result'))
+
+ elif '2150' in res.get('message'):
+ # IRN already generated but not updated in invoice
+ # Extract the IRN from the response description and fetch irn details
+ irn = res.get('result')[0].get('Desc').get('Irn')
+ irn_details = self.get_irn_details(irn)
+ if irn_details:
+ self.set_einvoice_data(irn_details)
+ else:
+ raise RequestFailed('IRN has already been generated for the invoice but cannot fetch details for the it. \
+ Contact ERPNext support to resolve the issue.')
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ self.log_error(data)
+ self.raise_error(True)
+
+ def get_irn_details(self, irn):
+ headers = self.get_headers()
+
+ try:
+ params = '?irn={irn}'.format(irn=irn)
+ res = self.make_request('get', self.irn_details_url + params, headers)
+ if res.get('success'):
+ return res.get('result')
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ self.log_error()
+ self.raise_error(True)
+
+ def cancel_irn(self, irn, reason, remark):
+ headers = self.get_headers()
+ data = json.dumps({
+ 'Irn': irn,
+ 'Cnlrsn': reason,
+ 'Cnlrem': remark
+ }, indent=4)
+
+ try:
+ res = self.make_request('post', self.cancel_irn_url, headers, data)
+ if res.get('success'):
+ self.invoice.irn_cancelled = 1
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('IRN Cancelled - {}').format(remark)
+ }
+ self.update_invoice()
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ self.log_error(data)
+ self.raise_error(True)
+
+ def generate_eway_bill(self, **kwargs):
+ args = frappe._dict(kwargs)
+
+ headers = self.get_headers()
+ eway_bill_details = get_eway_bill_details(args)
+ data = json.dumps({
+ 'Irn': args.irn,
+ 'Distance': cint(eway_bill_details.distance),
+ 'TransMode': eway_bill_details.mode_of_transport,
+ 'TransId': eway_bill_details.gstin,
+ 'TransName': eway_bill_details.transporter,
+ 'TrnDocDt': eway_bill_details.document_date,
+ 'TrnDocNo': eway_bill_details.document_name,
+ 'VehNo': eway_bill_details.vehicle_no,
+ 'VehType': eway_bill_details.vehicle_type
+ }, indent=4)
+
+ try:
+ res = self.make_request('post', self.generate_ewaybill_url, headers, data)
+ if res.get('success'):
+ self.invoice.ewaybill = res.get('result').get('EwbNo')
+ self.invoice.eway_bill_cancelled = 0
+ self.invoice.update(args)
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('E-Way Bill Generated')
+ }
+ self.update_invoice()
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ self.log_error(data)
+ self.raise_error(True)
+
+ def cancel_eway_bill(self, eway_bill, reason, remark):
+ headers = self.get_headers()
+ data = json.dumps({
+ 'ewbNo': eway_bill,
+ 'cancelRsnCode': reason,
+ 'cancelRmrk': remark
+ }, indent=4)
+ headers["username"] = headers["user_name"]
+ del headers["user_name"]
+ try:
+ res = self.make_request('post', self.cancel_ewaybill_url, headers, data)
+ if res.get('success'):
+ self.invoice.ewaybill = ''
+ self.invoice.eway_bill_cancelled = 1
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('E-Way Bill Cancelled - {}').format(remark)
+ }
+ self.update_invoice()
+
+ else:
+ raise RequestFailed
+
+ except RequestFailed:
+ errors = self.sanitize_error_message(res.get('message'))
+ self.raise_error(errors=errors)
+
+ except Exception:
+ self.log_error(data)
+ self.raise_error(True)
+
+ def sanitize_error_message(self, message):
+ '''
+ On validation errors, response message looks something like this:
+ message = '2174 : For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable,
+ 3095 : Supplier GSTIN is inactive'
+ we search for string between ':' to extract the error messages
+ errors = [
+ ': For inter-state transaction, CGST and SGST amounts are not applicable; only IGST amount is applicable, 3095 ',
+ ': Test'
+ ]
+ then we trim down the message by looping over errors
+ '''
+ errors = re.findall(': [^:]+', message)
+ for idx, e in enumerate(errors):
+ # remove colons
+ errors[idx] = errors[idx].replace(':', '').strip()
+ # if not last
+ if idx != len(errors) - 1:
+ # remove last 7 chars eg: ', 3095 '
+ errors[idx] = errors[idx][:-6]
+
+ return errors
+
+ def log_error(self, data={}):
+ if not isinstance(data, dict):
+ data = json.loads(data)
+
+ seperator = "--" * 50
+ err_tb = traceback.format_exc()
+ err_msg = str(sys.exc_info()[1])
+ data = json.dumps(data, indent=4)
+
+ message = "\n".join([
+ "Error", err_msg, seperator,
+ "Data:", data, seperator,
+ "Exception:", err_tb
+ ])
+ frappe.log_error(title=_('E Invoice Request Failed'), message=message)
+
+ def raise_error(self, raise_exception=False, errors=[]):
+ title = _('E Invoice Request Failed')
+ if errors:
+ frappe.throw(errors, title=title, as_list=1)
+ else:
+ link_to_error_list = '<a href="desk#List/Error Log/List?method=E Invoice Request Failed">Error Log</a>'
+ frappe.msgprint(
+ _('An error occurred while making e-invoicing request. Please check {} for more information.').format(link_to_error_list),
+ title=title,
+ raise_exception=raise_exception,
+ indicator='red'
+ )
+
+ def set_einvoice_data(self, res):
+ enc_signed_invoice = res.get('SignedInvoice')
+ dec_signed_invoice = jwt.decode(enc_signed_invoice, verify=False)['data']
+
+ self.invoice.irn = res.get('Irn')
+ self.invoice.ewaybill = res.get('EwbNo')
+ self.invoice.signed_einvoice = dec_signed_invoice
+ self.invoice.signed_qr_code = res.get('SignedQRCode')
+
+ self.attach_qrcode_image()
+
+ self.invoice.flags.updater_reference = {
+ 'doctype': self.invoice.doctype,
+ 'docname': self.invoice.name,
+ 'label': _('IRN Generated')
+ }
+ self.update_invoice()
+
+ def attach_qrcode_image(self):
+ qrcode = self.invoice.signed_qr_code
+ doctype = self.invoice.doctype
+ docname = self.invoice.name
+ filename = 'QRCode_{}.png'.format(docname).replace(os.path.sep, "__")
+
+ qr_image = io.BytesIO()
+ url = qrcreate(qrcode, error='L')
+ url.png(qr_image, scale=2, quiet_zone=1)
+ _file = frappe.get_doc({
+ "doctype": "File",
+ "file_name": filename,
+ "attached_to_doctype": doctype,
+ "attached_to_name": docname,
+ "attached_to_field": "qrcode_image",
+ "is_private": 1,
+ "content": qr_image.getvalue()})
+ _file.save()
+ frappe.db.commit()
+ self.invoice.qrcode_image = _file.file_url
+
+ def update_invoice(self):
+ self.invoice.flags.ignore_validate_update_after_submit = True
+ self.invoice.flags.ignore_validate = True
+ self.invoice.save()
+
+
+def sanitize_for_json(string):
+ """Escape JSON specific characters from a string."""
+
+ # json.dumps adds double-quotes to the string. Indexing to remove them.
+ return json.dumps(string)[1:-1]
+
+@frappe.whitelist()
+def get_einvoice(doctype, docname):
+ invoice = frappe.get_doc(doctype, docname)
+ return make_einvoice(invoice)
+
+@frappe.whitelist()
+def generate_irn(doctype, docname):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.generate_irn()
+
+@frappe.whitelist()
+def cancel_irn(doctype, docname, irn, reason, remark):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.cancel_irn(irn, reason, remark)
+
+@frappe.whitelist()
+def generate_eway_bill(doctype, docname, **kwargs):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.generate_eway_bill(**kwargs)
+
+@frappe.whitelist()
+def cancel_eway_bill(doctype, docname, eway_bill, reason, remark):
+ gsp_connector = GSPConnector(doctype, docname)
+ gsp_connector.cancel_eway_bill(eway_bill, reason, remark)
diff --git a/erpnext/regional/india/gst_state_code_data.json b/erpnext/regional/india/gst_state_code_data.json
index ff88e0f..8481c27 100644
--- a/erpnext/regional/india/gst_state_code_data.json
+++ b/erpnext/regional/india/gst_state_code_data.json
@@ -168,5 +168,10 @@
"state_number": "37",
"state_code": "AD",
"state_name": "Andhra Pradesh (New)"
+ },
+ {
+ "state_number": "38",
+ "state_code": "LA",
+ "state_name": "Ladakh"
}
]
diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py
index cbcd6e3..5261984 100644
--- a/erpnext/regional/india/setup.py
+++ b/erpnext/regional/india/setup.py
@@ -7,7 +7,7 @@
from frappe.custom.doctype.custom_field.custom_field import create_custom_fields
from frappe.permissions import add_permission, update_permission_property
from erpnext.regional.india import states
-from erpnext.accounts.utils import get_fiscal_year
+from erpnext.accounts.utils import get_fiscal_year, FiscalYearError
from frappe.utils import today
def setup(company=None, patch=True):
@@ -87,7 +87,7 @@
)).insert()
def add_permissions():
- for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate'):
+ for doctype in ('GST HSN Code', 'GST Settings', 'GSTR 3B Report', 'Lower Deduction Certificate', 'E Invoice Settings'):
add_permission(doctype, 'All', 0)
for role in ('Accounts Manager', 'Accounts User', 'System Manager'):
add_permission(doctype, role, 0)
@@ -103,9 +103,10 @@
def add_print_formats():
frappe.reload_doc("regional", "print_format", "gst_tax_invoice")
frappe.reload_doc("accounts", "print_format", "gst_pos_invoice")
+ frappe.reload_doc("accounts", "print_format", "GST E-Invoice")
frappe.db.sql(""" update `tabPrint Format` set disabled = 0 where
- name in('GST POS Invoice', 'GST Tax Invoice') """)
+ name in('GST POS Invoice', 'GST Tax Invoice', 'GST E-Invoice') """)
def make_custom_fields(update=True):
hsn_sac_field = dict(fieldname='gst_hsn_code', label='HSN/SAC',
@@ -351,7 +352,6 @@
'label': 'Mode of Transport',
'fieldtype': 'Select',
'options': '\nRoad\nAir\nRail\nShip',
- 'default': 'Road',
'insert_after': 'transporter_name',
'print_hide': 1,
'translatable': 0
@@ -388,13 +388,34 @@
'fieldname': 'ewaybill',
'label': 'E-Way Bill No.',
'fieldtype': 'Data',
- 'depends_on': 'eval:(doc.docstatus === 1)',
+ 'depends_on': 'eval:((doc.docstatus === 1 || doc.ewaybill) && doc.eway_bill_cancelled === 0)',
'allow_on_submit': 1,
'insert_after': 'tax_id',
'translatable': 0
}
]
+ si_einvoice_fields = [
+ dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1,
+ depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'),
+
+ dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1),
+
+ dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1),
+
+ dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
+ depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
+
+ dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1,
+ depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'),
+
+ dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1),
+
+ dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1)
+ ]
+
custom_fields = {
'Address': [
dict(fieldname='gstin', label='Party GSTIN', fieldtype='Data',
@@ -407,7 +428,7 @@
'Purchase Invoice': purchase_invoice_gst_category + invoice_gst_fields + purchase_invoice_itc_fields + purchase_invoice_gst_fields,
'Purchase Order': purchase_invoice_gst_fields,
'Purchase Receipt': purchase_invoice_gst_fields,
- 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields,
+ 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields,
'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields,
'Sales Order': sales_invoice_gst_fields,
'Tax Category': inter_state_gst_field,
@@ -608,15 +629,20 @@
def set_tax_withholding_category(company):
accounts = []
+ fiscal_year = None
abbr = frappe.get_value("Company", company, "abbr")
tds_account = frappe.get_value("Account", 'TDS Payable - {0}'.format(abbr), 'name')
if company and tds_account:
accounts = [dict(company=company, account=tds_account)]
- fiscal_year = get_fiscal_year(today(), company=company)[0]
- docs = get_tds_details(accounts, fiscal_year)
+ try:
+ fiscal_year = get_fiscal_year(today(), verbose=0, company=company)[0]
+ except FiscalYearError:
+ pass
+ docs = get_tds_details(accounts, fiscal_year)
+
for d in docs:
try:
doc = frappe.get_doc(d)
@@ -629,11 +655,14 @@
if accounts:
doc.append("accounts", accounts[0])
- # if fiscal year don't match with any of the already entered data, append rate row
- fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
- if not fy_exist:
- doc.append("rates", d.get('rates')[0])
-
+ if fiscal_year:
+ # if fiscal year don't match with any of the already entered data, append rate row
+ fy_exist = [k for k in doc.get('rates') if k.get('fiscal_year')==fiscal_year]
+ if not fy_exist:
+ doc.append("rates", d.get('rates')[0])
+
+ doc.flags.ignore_permissions = True
+ doc.flags.ignore_mandatory = True
doc.save()
def set_tds_account(docs, company):
diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js
index 3c15647..d3b7ea3 100644
--- a/erpnext/regional/india/taxes.js
+++ b/erpnext/regional/india/taxes.js
@@ -12,6 +12,9 @@
tax_category: function(frm) {
frm.trigger('get_tax_template');
},
+ customer_address: function(frm) {
+ frm.trigger('get_tax_template');
+ },
get_tax_template: function(frm) {
if (!frm.doc.company) return;
@@ -19,6 +22,7 @@
'shipping_address': frm.doc.shipping_address || '',
'shipping_address_name': frm.doc.shipping_address_name || '',
'customer_address': frm.doc.customer_address || '',
+ 'supplier_address': frm.doc.supplier_address,
'customer': frm.doc.customer,
'supplier': frm.doc.supplier,
'supplier_gstin': frm.doc.supplier_gstin,
@@ -33,17 +37,16 @@
doctype: frm.doc.doctype,
company: frm.doc.company
},
+ debounce: 2000,
callback: function(r) {
if(r.message) {
frm.set_value('taxes_and_charges', r.message.taxes_and_charges);
+ frm.set_value('taxes', r.message.taxes);
frm.set_value('place_of_supply', r.message.place_of_supply);
- } else if (frm.doc.is_internal_supplier || frm.doc.is_internal_customer) {
- frm.set_value('taxes_and_charges', '');
- frm.set_value('taxes', []);
}
}
});
}
});
-};
+}
diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py
index 62487ba..cb30605 100644
--- a/erpnext/regional/india/utils.py
+++ b/erpnext/regional/india/utils.py
@@ -12,6 +12,7 @@
from six import string_types
from erpnext.accounts.general_ledger import make_gl_entries
from erpnext.accounts.utils import get_account_currency
+from frappe.model.utils import get_fetch_values
def validate_gstin_for_india(doc, method):
if hasattr(doc, 'gst_state') and doc.gst_state:
@@ -47,12 +48,23 @@
validate_gstin_check_digit(doc.gstin)
set_gst_state_and_state_number(doc)
+ if not doc.gst_state:
+ frappe.throw(_("Please Enter GST state"))
+
if doc.gst_state_number != doc.gstin[:2]:
frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.")
.format(doc.gst_state_number))
+def validate_pan_for_india(doc, method):
+ if doc.get('country') != 'India' or not doc.pan:
+ return
+
+ p = re.compile("[A-Z]{5}[0-9]{4}[A-Z]{1}")
+ if not p.match(doc.pan):
+ frappe.throw(_("Invalid PAN No. The input you've entered doesn't match the format of PAN."))
+
def validate_tax_category(doc, method):
- if doc.get('gst_state') and frappe.db.get_value('Tax category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
+ if doc.get('gst_state') and frappe.db.get_value('Tax Category', {'gst_state': doc.gst_state, 'is_inter_state': doc.is_inter_state}):
if doc.is_inter_state:
frappe.throw(_("Inter State tax category for GST State {0} already exists").format(doc.gst_state))
else:
@@ -161,11 +173,13 @@
party_details = json.loads(party_details)
party_details = frappe._dict(party_details)
+ update_party_details(party_details, doctype)
+
party_details.place_of_supply = get_place_of_supply(party_details, doctype)
if is_internal_transfer(party_details, doctype):
party_details.taxes_and_charges = ''
- party_details.taxes = ''
+ party_details.taxes = []
return party_details
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
@@ -209,6 +223,11 @@
return party_details
+def update_party_details(party_details, doctype):
+ for address_field in ['shipping_address', 'company_address', 'supplier_address', 'shipping_address_name', 'customer_address']:
+ if party_details.get(address_field):
+ party_details.update(get_fetch_values(doctype, address_field, party_details.get(address_field)))
+
def is_internal_transfer(party_details, doctype):
if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"):
destination_gstin = party_details.company_gstin
@@ -761,3 +780,24 @@
)
return gl_entries
+
+@frappe.whitelist()
+def get_regional_round_off_accounts(company, account_list):
+ country = frappe.get_cached_value('Company', company, 'country')
+
+ if country != 'India':
+ return
+
+ if isinstance(account_list, string_types):
+ account_list = json.loads(account_list)
+
+ if not frappe.db.get_single_value('GST Settings', 'round_off_gst_values'):
+ return
+
+ gst_accounts = get_gst_accounts(company)
+ gst_account_list = gst_accounts.get('cgst_account') + gst_accounts.get('sgst_account') \
+ + gst_accounts.get('igst_account')
+
+ account_list.extend(gst_account_list)
+
+ return account_list
\ No newline at end of file
diff --git a/erpnext/regional/italy/setup.py b/erpnext/regional/italy/setup.py
index 6ab7341..217d623 100644
--- a/erpnext/regional/italy/setup.py
+++ b/erpnext/regional/italy/setup.py
@@ -127,7 +127,7 @@
options="\n".join(map(lambda x: frappe.safe_decode(x, encoding='utf-8'), vat_collectability_options)),
fetch_from="company.vat_collectability"),
dict(fieldname='sb_e_invoicing_reference', label='E-Invoicing',
- fieldtype='Section Break', insert_after='pos_total_qty', print_hide=1),
+ fieldtype='Section Break', insert_after='against_income_account', print_hide=1),
dict(fieldname='company_tax_id', label='Company Tax ID',
fieldtype='Data', insert_after='sb_e_invoicing_reference', print_hide=1, read_only=1,
fetch_from="company.tax_id"),
diff --git a/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json b/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json
index ce8c44a..e59700f 100644
--- a/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json
+++ b/erpnext/regional/print_format/irs_1099_form/irs_1099_form.json
@@ -1,23 +1,26 @@
-[
- {
- "align_labels_right": 0,
- "css": "",
- "custom_format": 1,
- "default_print_language": "en",
- "disabled": 0,
- "doc_type": "Supplier",
- "docstatus": 0,
- "doctype": "Print Format",
- "font": "Default",
- "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div class=\\\"print-heading\\\">\\t\\t\\t\\t<h2>TAX Invoice<br><small>{{ doc.name }}</small>\\t\\t\\t\\t</h2></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"customer_name\", \"label\": \"Customer Name\"}, {\"print_hide\": 0, \"fieldname\": \"customer_name_in_arabic\", \"label\": \"Customer Name in Arabic\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"posting_date\", \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Address\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"company\", \"label\": \"Company\"}, {\"print_hide\": 0, \"fieldname\": \"company_trn\", \"label\": \"Company TRN\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"company_address_display\", \"label\": \"Company Address\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"visible_columns\": [{\"print_hide\": 0, \"fieldname\": \"item_code\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"description\", \"print_width\": \"200px\"}, {\"print_hide\": 0, \"fieldname\": \"uom\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_code\", \"print_width\": \"\"}], \"print_hide\": 0, \"fieldname\": \"items\", \"label\": \"Items\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"total\", \"label\": \"Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"visible_columns\": [{\"print_hide\": 0, \"fieldname\": \"charge_type\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"row_id\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"account_head\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"cost_center\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"description\", \"print_width\": \"300px\"}, {\"print_hide\": 0, \"fieldname\": \"rate\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"total\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_amount_after_discount_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_tax_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_total\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_tax_amount_after_discount_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"item_wise_tax_detail\", \"print_width\": \"\"}], \"print_hide\": 0, \"fieldname\": \"taxes\", \"label\": \"Sales Taxes and Charges\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"grand_total\", \"label\": \"Grand Total\"}, {\"print_hide\": 0, \"fieldname\": \"rounded_total\", \"label\": \"Rounded Total\"}, {\"print_hide\": 0, \"fieldname\": \"in_words\", \"align\": \"left\", \"label\": \"In Words\"}]",
- "html": "<div id=\"copy_a\" style=\"position: relative; top:0cm; width:17cm;height:28.0cm;\">\n <table>\n <tbody>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:86mm\" colspan=\"4\"; rowspan=\"3\">PAYER'S name, street address, city or town, state or province, country, ZIP<br>or foreign postal code, and telephone no.<br>\n\t{{company if company else \"\"}}<br>\n\t{{payer_street_address if payer_street_address else \"\"}}\n</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:35mm\">1 Rents</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:25mm\" rowspan=\"2\">OMB No. 1545-0115<br><yone>20</yone><ytwo>18</ytwo><br>Form 1099-MISC</td>\n <td class=\"lbs bbs\" style=\"width:38mm\" colspan=\"2\" rowspan=\"2\">Miscellaneous Income</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:35mm\">2 Royalties</td>\n </tr>\n <tr style=\"height:9mm\">\n <td class=\"tbs rbs lbs bbs\" >3 Other Income<br>\n\t{{payments if payments else \"\"}}\n\t</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">4 Federal Income tax withheld</td>\n <td class=\"tbs lbs bbs\" style=\"width:29mm\" rowspan=\"2\">Copy A<br>For<br>Internal Revenue<br>Service Center<br><br>File with Form 1096</td>\n </tr>\n <tr style=\"height:16mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:43mm\">PAYER'S TIN<br>\n\t{{company_tin if company_tin else \"\"}}\n\t</td>\n \n <td class=\"tbs rbs lbs bbs\" colspan=\"3\">RECIPIENT'S TIN<br><br>\n {{tax_id if tax_id else \"None\"}}\n</td>\n <td class=\"tbs rbs lbs bbs\" >Fishing boat proceeds</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">6 Medical and health care payments</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\">RECIPIENT'S name <br>\n {{supplier if supplier else \"\"}}\n </td>\n <td class=\"tbs rbs lbs bbs\" >7 Nonemployee compensation<br>\n\t</td> \n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">Substitute payments in lieu of dividends or interest</td>\n <td class=\"tbs lbs bbs\" rowspan=\"6\">For Privacy Act<br>and Paperwork<br>Reduction Act<br>Notice, see the<br>2018 General<br>Instructions for<br>Certain<br>Information<br>Returns.</td>\n </tr>\n <tr style=\"height:6mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\" rowspan=\"2\">Street address (including apt. no.)<br>\n\t{{recipient_street_address if recipient_street_address else \"\"}}\n\t</td>\n <td class=\"tbs rbs lbs bbs\" >$___________</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">$___________</td>\n </tr>\n <tr style=\"height:7mm\">\n <td class=\"tbs rbs lbs bbs\" rowspan=\"2\">9 Payer made direct sales of<br>$5,000 or more of consumer products<br>to a buyer<br>(recipient) for resale</td>\n <td class=\"tbs rbs lbs\" colspan=\"2\">10 Crop insurance proceeds</td>\n </tr>\n <tr style=\"height:5mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\" rowspan=\"2\">City or town, state or province, country, and ZIP or foreign postal code<br>\n\t{{recipient_city_state if recipient_city_state else \"\"}}\n</td>\n <td style=\"vertical-align:bottom\" class=\" rbs lbs bbs\" colspan=\"2\">$___________</td>\n </tr>\n <tr style=\"height:9mm\">\n <td class=\"tbs rbs lbs bbs\" >11</td>\n <td class=\"tbs rbs lbs bbs\" colspan=2>12</td>\n </tr>\n <tr style=\"height:13mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">Account number (see instructions)</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:16mm\">FACTA filing<br>requirement</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:14mm\">2nd TIN not.</td>\n <td class=\"tbs rbs lbs bbs\" >13 Excess golden parachute payments<br>$___________</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">14 Gross proceeds paid to an<br>attorney<br>$___________</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs \" >15a Section 409A deferrals</td>\n <td class=\"tbs rbs lbs \" colspan=\"3\">15b Section 409 income</td>\n <td class=\"tbs rbs lbs \" >16 State tax withheld</td>\n <td class=\"tbs rbs lbs \" colspan=\"2\">17 State/Payer's state no.</td>\n <td class=\"tbs lbs\" >18 State income</td>\n </tr>\n <tr>\n <td class=\"lbs rbs bbs\">$</td>\n <td class=\"lbs rbs bbs\" colspan=\"3\">$</td>\n <td class=\"lbs rbs bbs tbd\">$</td>\n <td class=\"lbs rbs bbs tbd\" colspan=\"2\"></td>\n <td class=\"lbs bbs tbd\">$</td>\n </tr>\n\n <tr style=\"height:8mm\">\n <td class=\"tbs\" colspan=\"8\">Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the Treasury - Internal Revenue Service</td>\n </tr>\n\n </tbody>\n</table>\n</div>\n<div id=\"copy_1\" style=\"position: relative; top:0cm; width:17cm;height:28.0cm;\">\n <table>\n <tbody>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:86mm\" colspan=\"4\"; rowspan=\"3\">PAYER'S name, street address, city or town, state or province, country, ZIP<br>or foreign postal code, and telephone no.<br>\n {{company if company else \"\"}}<br>\n \t{{payer_street_address if payer_street_address else \"\"}}</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:35mm\">1 Rents</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:25mm\" rowspan=\"2\">OMB No. 1545-0115<br><yone>20</yone><ytwo>18</ytwo><br>Form 1099-MISC</td>\n <td class=\"lbs bbs\" style=\"width:38mm\" colspan=\"2\" rowspan=\"2\">Miscellaneous Income</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:35mm\">2 Royalties</td>\n </tr>\n <tr style=\"height:9mm\">\n <td class=\"tbs rbs lbs bbs\" >3 Other Income<br>\n\t{{payments if payments else \"\"}}\n\t</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">4 Federal Income tax withheld</td>\n <td class=\"tbs lbs bbs\" style=\"width:29mm\" rowspan=\"2\">Copy 1<br>For State Tax<br>Department</td>\n </tr>\n <tr style=\"height:16mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:43mm\">PAYER'S TIN<br>\n\t{{company_tin if company_tin else \"\"}}\n\t</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"3\">RECIPIENT'S TIN<br>\n\t{{tax_id if tax_id else \"\"}}\n\t</td>\n <td class=\"tbs rbs lbs bbs\" >Fishing boat proceeds</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">6 Medical and health care payments</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\">RECIPIENT'S name</td>\n {{supplier if supplier else \"\"}}\n <td class=\"tbs rbs lbs bbs\" >7 Nonemployee compensation<br>\n\t</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">Substitute payments in lieu of dividends or interest</td>\n <td class=\"tbs lbs bbs\" rowspan=\"6\"></td>\n </tr>\n <tr style=\"height:6mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\" rowspan=\"2\">Street address (including apt. no.)<br>\n\t{{recipient_street_address if recipient_street_address else \"\"}}\n\t</td>\n <td class=\"tbs rbs lbs bbs\" >$___________</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">$___________</td>\n </tr>\n <tr style=\"height:7mm\">\n <td class=\"tbs rbs lbs bbs\" rowspan=\"2\">9 Payer made direct sales of<br>$5,000 or more of consumer products<br>to a buyer<br>(recipient) for resale</td>\n <td class=\"tbs rbs lbs\" colspan=\"2\">10 Crop insurance proceeds</td>\n </tr>\n <tr style=\"height:5mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\" rowspan=\"2\">City or town, state or province, country, and ZIP or foreign postal code<br>\n\t{{recipient_city_state if recipient_city_state else \"\"}}\n\t</td>\n <td style=\"vertical-align:bottom\" class=\" rbs lbs bbs\" colspan=\"2\">$___________</td>\n </tr>\n <tr style=\"height:9mm\">\n <td class=\"tbs rbs lbs bbs\" >11</td>\n <td class=\"tbs rbs lbs bbs\" colspan=2>12</td>\n </tr>\n <tr style=\"height:13mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">Account number (see instructions)</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:16mm\">FACTA filing<br>requirement</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:14mm\">2nd TIN not.</td>\n <td class=\"tbs rbs lbs bbs\" >13 Excess golden parachute payments<br>$___________</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">14 Gross proceeds paid to an<br>attorney<br>$___________</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs \" >15a Section 409A deferrals</td>\n <td class=\"tbs rbs lbs \" colspan=\"3\">15b Section 409 income</td>\n <td class=\"tbs rbs lbs \" >16 State tax withheld</td>\n <td class=\"tbs rbs lbs \" colspan=\"2\">17 State/Payer's state no.</td>\n <td class=\"tbs lbs\" >18 State income</td>\n </tr>\n <tr>\n <td class=\"lbs rbs bbs\">$</td>\n <td class=\"lbs rbs bbs\" colspan=\"3\">$</td>\n <td class=\"lbs rbs bbs tbd\">$</td>\n <td class=\"lbs rbs bbs tbd\" colspan=\"2\"></td>\n <td class=\"lbs bbs tbd\">$</td>\n </tr>\n\n <tr style=\"height:8mm\">\n <td class=\"tbs\" colspan=\"8\">Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the Treasury - Internal Revenue Service</td>\n </tr>\n\n </tbody>\n</table>\n</div>\n<style>\nbody {\n font-family: 'Helvetica', sans-serif;\n font-size: 5.66pt;\n}\nyone {\n font-family: 'Helvetica', sans-serif;\n font-size: 14pt;\n color: black;\n -webkit-text-fill-color: white; /* Will override color (regardless of order) */\n -webkit-text-stroke-width: 1px;\n -webkit-text-stroke-color: black;\n}\nytwo {\n font-family: 'Helvetica', sans-serif;\n font-size: 14pt;\n color: black;\n -webkit-text-stroke-width: 1px;\n -webkit-text-stroke-color: black;\n}\n\ntable, th, td {\n font-family: 'Helvetica', sans-serif;\n font-size: 5.66pt;\n border: none;\n}\n\n.tbs {\n border-top: 1px solid black;\n}\n\n.bbs {\n border-bottom: 1px solid black;\n}\n.lbs {\n border-left: 1px solid black;\n}\n.rbs {\n border-right: 1px solid black;\n}\n.allBorder {\n border-top: 1px solid black;\n border-right: 1px solid black;\n border-left: 1px solid black;\n borter-bottom: 1px solid black;\n}\n.bottomBorderOnlyDashed {\n\tborder-bottom: 1px dashed black;\n}\n.tbd {\n\tborder-top: 1px dashed black;\n}\n.address {\n\tvertical-align: bottom;\n}\n</style>",
- "line_breaks": 0,
- "modified": "2018-10-08 14:56:56.912851",
- "module": "Regional",
- "name": "IRS 1099 Form",
- "print_format_builder": 1,
- "print_format_type": "Server",
- "show_section_headings": 0,
- "standard": "No"
- }
-]
+{
+ "align_labels_right": 0,
+ "creation": "2020-11-09 16:01:26.096002",
+ "css": "",
+ "custom_format": 1,
+ "default_print_language": "en",
+ "disabled": 0,
+ "doc_type": "Supplier",
+ "docstatus": 0,
+ "doctype": "Print Format",
+ "font": "Default",
+ "format_data": "[{\"fieldname\": \"print_heading_template\", \"fieldtype\": \"Custom HTML\", \"options\": \"<div class=\\\"print-heading\\\">\\t\\t\\t\\t<h2>TAX Invoice<br><small>{{ doc.name }}</small>\\t\\t\\t\\t</h2></div>\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"customer_name\", \"label\": \"Customer Name\"}, {\"print_hide\": 0, \"fieldname\": \"customer_name_in_arabic\", \"label\": \"Customer Name in Arabic\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"posting_date\", \"label\": \"Date\"}, {\"fieldtype\": \"Section Break\", \"label\": \"Address\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"company\", \"label\": \"Company\"}, {\"print_hide\": 0, \"fieldname\": \"company_trn\", \"label\": \"Company TRN\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"company_address_display\", \"label\": \"Company Address\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"visible_columns\": [{\"print_hide\": 0, \"fieldname\": \"item_code\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"description\", \"print_width\": \"200px\"}, {\"print_hide\": 0, \"fieldname\": \"uom\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_code\", \"print_width\": \"\"}], \"print_hide\": 0, \"fieldname\": \"items\", \"label\": \"Items\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"total\", \"label\": \"Total\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"visible_columns\": [{\"print_hide\": 0, \"fieldname\": \"charge_type\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"row_id\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"account_head\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"cost_center\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"description\", \"print_width\": \"300px\"}, {\"print_hide\": 0, \"fieldname\": \"rate\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"total\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"tax_amount_after_discount_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_tax_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_total\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"base_tax_amount_after_discount_amount\", \"print_width\": \"\"}, {\"print_hide\": 0, \"fieldname\": \"item_wise_tax_detail\", \"print_width\": \"\"}], \"print_hide\": 0, \"fieldname\": \"taxes\", \"label\": \"Sales Taxes and Charges\"}, {\"fieldtype\": \"Section Break\", \"label\": \"\"}, {\"fieldtype\": \"Column Break\"}, {\"fieldtype\": \"Column Break\"}, {\"print_hide\": 0, \"fieldname\": \"grand_total\", \"label\": \"Grand Total\"}, {\"print_hide\": 0, \"fieldname\": \"rounded_total\", \"label\": \"Rounded Total\"}, {\"print_hide\": 0, \"fieldname\": \"in_words\", \"align\": \"left\", \"label\": \"In Words\"}]",
+ "html": "<div id=\"copy_a\" style=\"position: relative; top:0cm; width:17cm;height:28.0cm;\">\n <table>\n <tbody>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:86mm\" colspan=\"4\" ; rowspan=\"3\">PAYER'S name, street address,\n city or town, state or province, country, ZIP<br>or foreign postal code, and telephone no.<br>\n {{ company or \"\" }}<br>\n {{ payer_street_address or \"\" }}\n </td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:35mm\">1 Rents</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:25mm\" rowspan=\"2\">OMB No. 1545-0115<br>\n <yone>{{ fiscal_year[:2] }}</yone>\n <ytwo>{{ fiscal_year[-2:] }}</ytwo><br>Form 1099-MISC\n </td>\n <td class=\"lbs bbs\" style=\"width:38mm\" colspan=\"2\" rowspan=\"2\">Miscellaneous Income</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:35mm\">2 Royalties</td>\n </tr>\n <tr style=\"height:9mm\">\n <td class=\"tbs rbs lbs bbs\">3 Other Income<br>{{ payments or \"\" }}</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">4 Federal Income tax withheld</td>\n <td class=\"tbs lbs bbs\" style=\"width:29mm\" rowspan=\"2\">Copy A<br>For<br>Internal Revenue<br>Service\n Center<br><br>File with Form 1096</td>\n </tr>\n <tr style=\"height:16mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:43mm\">PAYER'S TIN<br>{{ company_tin or \"\" }}</td>\n\n <td class=\"tbs rbs lbs bbs\" colspan=\"3\">RECIPIENT'S TIN<br><br>{{ tax_id or \"None\" }}</td>\n <td class=\"tbs rbs lbs bbs\">Fishing boat proceeds</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">6 Medical and health care payments</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\">RECIPIENT'S name <br>{{ supplier or \"\" }}</td>\n <td class=\"tbs rbs lbs bbs\">7 Nonemployee compensation<br>\n </td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">Substitute payments in lieu of dividends or interest</td>\n <td class=\"tbs lbs bbs\" rowspan=\"6\">For Privacy Act<br>and Paperwork<br>Reduction Act<br>Notice, see\n the<br>2018 General<br>Instructions for<br>Certain<br>Information<br>Returns.</td>\n </tr>\n <tr style=\"height:6mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\" rowspan=\"2\">Street address (including apt. no.)<br>\n {{ recipient_street_address or \"\" }}\n </td>\n <td class=\"tbs rbs lbs bbs\">$___________</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">$___________</td>\n </tr>\n <tr style=\"height:7mm\">\n <td class=\"tbs rbs lbs bbs\" rowspan=\"2\">9 Payer made direct sales of<br>$5,000 or more of consumer\n products<br>to a buyer<br>(recipient) for resale</td>\n <td class=\"tbs rbs lbs\" colspan=\"2\">10 Crop insurance proceeds</td>\n </tr>\n <tr style=\"height:5mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\" rowspan=\"2\">City or town, state or province, country, and ZIP or\n foreign postal code<br>\n {{ recipient_city_state or \"\" }}\n </td>\n <td style=\"vertical-align:bottom\" class=\" rbs lbs bbs\" colspan=\"2\">$___________</td>\n </tr>\n <tr style=\"height:9mm\">\n <td class=\"tbs rbs lbs bbs\">11</td>\n <td class=\"tbs rbs lbs bbs\" colspan=2>12</td>\n </tr>\n <tr style=\"height:13mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">Account number (see instructions)</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:16mm\">FACTA filing<br>requirement</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:14mm\">2nd TIN not.</td>\n <td class=\"tbs rbs lbs bbs\">13 Excess golden parachute payments<br>$___________</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">14 Gross proceeds paid to an<br>attorney<br>$___________</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs \">15a Section 409A deferrals</td>\n <td class=\"tbs rbs lbs \" colspan=\"3\">15b Section 409 income</td>\n <td class=\"tbs rbs lbs \">16 State tax withheld</td>\n <td class=\"tbs rbs lbs \" colspan=\"2\">17 State/Payer's state no.</td>\n <td class=\"tbs lbs\">18 State income</td>\n </tr>\n <tr>\n <td class=\"lbs rbs bbs\">$</td>\n <td class=\"lbs rbs bbs\" colspan=\"3\">$</td>\n <td class=\"lbs rbs bbs tbd\">$</td>\n <td class=\"lbs rbs bbs tbd\" colspan=\"2\"></td>\n <td class=\"lbs bbs tbd\">$</td>\n </tr>\n\n <tr style=\"height:8mm\">\n <td class=\"tbs\" colspan=\"8\">Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the\n Treasury - Internal Revenue Service</td>\n </tr>\n\n </tbody>\n </table>\n</div>\n<div id=\"copy_1\" style=\"position: relative; top:0cm; width:17cm;height:28.0cm;\">\n <table>\n <tbody>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:86mm\" colspan=\"4\" ; rowspan=\"3\">PAYER'S name, street address,\n city or town, state or province, country, ZIP<br>or foreign postal code, and telephone no.<br>\n {{ company or \"\"}}<b r>\n {{ payer_street_address or \"\" }}\n </td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:35mm\">1 Rents</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:25mm\" rowspan=\"2\">OMB No. 1545-0115<br>\n <yone>{{ fiscal_year[:2] }}</yone>\n <ytwo>{{ fiscal_year[-2:] }}</ytwo><br>Form 1099-MISC\n </td>\n <td class=\"lbs bbs\" style=\"width:38mm\" colspan=\"2\" rowspan=\"2\">Miscellaneous Income</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:35mm\">2 Royalties</td>\n </tr>\n <tr style=\"height:9mm\">\n <td class=\"tbs rbs lbs bbs\">3 Other Income<br>\n {{ payments or \"\" }}\n </td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">4 Federal Income tax withheld</td>\n <td class=\"tbs lbs bbs\" style=\"width:29mm\" rowspan=\"2\">Copy 1<br>For State Tax<br>Department</td>\n </tr>\n <tr style=\"height:16mm\">\n <td class=\"tbs rbs lbs bbs\" style=\"width:43mm\">PAYER'S TIN<br>\n {{ company_tin or \"\" }}\n </td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"3\">RECIPIENT'S TIN<br>\n {{ tax_id or \"\" }}\n </td>\n <td class=\"tbs rbs lbs bbs\">Fishing boat proceeds</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">6 Medical and health care payments</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\">RECIPIENT'S name</td>\n {{ supplier or \"\" }}\n <td class=\"tbs rbs lbs bbs\">7 Nonemployee compensation<br>\n </td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">Substitute payments in lieu of dividends or interest</td>\n <td class=\"tbs lbs bbs\" rowspan=\"6\"></td>\n </tr>\n <tr style=\"height:6mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\" rowspan=\"2\">Street address (including apt. no.)<br>\n {{ recipient_street_address or \"\" }}\n </td>\n <td class=\"tbs rbs lbs bbs\">$___________</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">$___________</td>\n </tr>\n <tr style=\"height:7mm\">\n <td class=\"tbs rbs lbs bbs\" rowspan=\"2\">9 Payer made direct sales of<br>$5,000 or more of consumer\n products<br>to a buyer<br>(recipient) for resale</td>\n <td class=\"tbs rbs lbs\" colspan=\"2\">10 Crop insurance proceeds</td>\n </tr>\n <tr style=\"height:5mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"4\" rowspan=\"2\">City or town, state or province, country, and ZIP or\n foreign postal code<br>\n {{ recipient_city_state or \"\" }}\n </td>\n <td style=\"vertical-align:bottom\" class=\" rbs lbs bbs\" colspan=\"2\">$___________</td>\n </tr>\n <tr style=\"height:9mm\">\n <td class=\"tbs rbs lbs bbs\">11</td>\n <td class=\"tbs rbs lbs bbs\" colspan=2>12</td>\n </tr>\n <tr style=\"height:13mm\">\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">Account number (see instructions)</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:16mm\">FACTA filing<br>requirement</td>\n <td class=\"tbs rbs lbs bbs\" style=\"width:14mm\">2nd TIN not.</td>\n <td class=\"tbs rbs lbs bbs\">13 Excess golden parachute payments<br>$___________</td>\n <td class=\"tbs rbs lbs bbs\" colspan=\"2\">14 Gross proceeds paid to an<br>attorney<br>$___________</td>\n </tr>\n <tr style=\"height:12mm\">\n <td class=\"tbs rbs lbs \">15a Section 409A deferrals</td>\n <td class=\"tbs rbs lbs \" colspan=\"3\">15b Section 409 income</td>\n <td class=\"tbs rbs lbs \">16 State tax withheld</td>\n <td class=\"tbs rbs lbs \" colspan=\"2\">17 State/Payer's state no.</td>\n <td class=\"tbs lbs\">18 State income</td>\n </tr>\n <tr>\n <td class=\"lbs rbs bbs\">$</td>\n <td class=\"lbs rbs bbs\" colspan=\"3\">$</td>\n <td class=\"lbs rbs bbs tbd\">$</td>\n <td class=\"lbs rbs bbs tbd\" colspan=\"2\"></td>\n <td class=\"lbs bbs tbd\">$</td>\n </tr>\n\n <tr style=\"height:8mm\">\n <td class=\"tbs\" colspan=\"8\">Form 1099-MISC Cat. No. 14425J www.irs.gov/Form1099MISC Department of the\n Treasury - Internal Revenue Service</td>\n </tr>\n\n </tbody>\n </table>\n</div>\n<style>\n body {\n font-family: 'Helvetica', sans-serif;\n font-size: 5.66pt;\n }\n\n yone {\n font-family: 'Helvetica', sans-serif;\n font-size: 14pt;\n color: black;\n -webkit-text-fill-color: white;\n /* Will override color (regardless of order) */\n -webkit-text-stroke-width: 1px;\n -webkit-text-stroke-color: black;\n }\n\n ytwo {\n font-family: 'Helvetica', sans-serif;\n font-size: 14pt;\n color: black;\n -webkit-text-stroke-width: 1px;\n -webkit-text-stroke-color: black;\n }\n\n table,\n th,\n td {\n font-family: 'Helvetica', sans-serif;\n font-size: 5.66pt;\n border: none;\n }\n\n .tbs {\n border-top: 1px solid black;\n }\n\n .bbs {\n border-bottom: 1px solid black;\n }\n\n .lbs {\n border-left: 1px solid black;\n }\n\n .rbs {\n border-right: 1px solid black;\n }\n\n .allBorder {\n border-top: 1px solid black;\n border-right: 1px solid black;\n border-left: 1px solid black;\n border-bottom: 1px solid black;\n }\n\n .bottomBorderOnlyDashed {\n border-bottom: 1px dashed black;\n }\n\n .tbd {\n border-top: 1px dashed black;\n }\n\n .address {\n vertical-align: bottom;\n }\n</style>",
+ "idx": 0,
+ "line_breaks": 0,
+ "modified": "2021-01-19 07:25:16.333666",
+ "modified_by": "Administrator",
+ "module": "Regional",
+ "name": "IRS 1099 Form",
+ "owner": "Administrator",
+ "print_format_builder": 1,
+ "print_format_type": "Jinja",
+ "raw_printing": 0,
+ "show_section_headings": 0,
+ "standard": "No"
+}
\ No newline at end of file
diff --git a/erpnext/regional/report/datev/datev.py b/erpnext/regional/report/datev/datev.py
index 1e39c57..cbc9478 100644
--- a/erpnext/regional/report/datev/datev.py
+++ b/erpnext/regional/report/datev/datev.py
@@ -96,6 +96,8 @@
"""Entry point for frappe."""
data = []
if filters and validate(filters):
+ fn = 'temporary_against_account_number'
+ filters[fn] = frappe.get_value('DATEV Settings', filters.get('company'), fn)
data = get_transactions(filters, as_dict=0)
return COLUMNS, data
@@ -156,11 +158,11 @@
case gl.debit when 0 then 'H' else 'S' end as 'Soll/Haben-Kennzeichen',
/* account number or, if empty, party account number */
- coalesce(acc.account_number, acc_pa.account_number) as 'Konto',
+ acc.account_number as 'Konto',
/* against number or, if empty, party against number */
- coalesce(acc_against.account_number, acc_against_pa.account_number) as 'Gegenkonto (ohne BU-Schlüssel)',
-
+ %(temporary_against_account_number)s as 'Gegenkonto (ohne BU-Schlüssel)',
+
gl.posting_date as 'Belegdatum',
gl.voucher_no as 'Belegfeld 1',
LEFT(gl.remarks, 60) as 'Buchungstext',
@@ -171,27 +173,10 @@
FROM `tabGL Entry` gl
- /* Statistisches Konto (Debitoren/Kreditoren) */
- left join `tabParty Account` pa
- on gl.against = pa.parent
- and gl.company = pa.company
-
/* Kontonummer */
left join `tabAccount` acc
on gl.account = acc.name
- /* Gegenkonto-Nummer */
- left join `tabAccount` acc_against
- on gl.against = acc_against.name
-
- /* Statistische Kontonummer */
- left join `tabAccount` acc_pa
- on pa.account = acc_pa.name
-
- /* Statistische Gegenkonto-Nummer */
- left join `tabAccount` acc_against_pa
- on pa.account = acc_against_pa.name
-
WHERE gl.company = %(company)s
AND DATE(gl.posting_date) >= %(from_date)s
AND DATE(gl.posting_date) <= %(to_date)s
@@ -347,7 +332,9 @@
coa = frappe.get_value('Company', company, 'chart_of_accounts')
filters['skr'] = '04' if 'SKR04' in coa else ('03' if 'SKR03' in coa else '')
- filters['account_number_length'] = frappe.get_value('DATEV Settings', company, 'account_number_length')
+ datev_settings = frappe.get_doc('DATEV Settings', company)
+ filters['account_number_length'] = datev_settings.account_number_length
+ filters['temporary_against_account_number'] = datev_settings.temporary_against_account_number
transactions = get_transactions(filters)
account_names = get_account_names(filters)
diff --git a/erpnext/regional/report/datev/test_datev.py b/erpnext/regional/report/datev/test_datev.py
index 9529923..59b878e 100644
--- a/erpnext/regional/report/datev/test_datev.py
+++ b/erpnext/regional/report/datev/test_datev.py
@@ -126,7 +126,8 @@
"doctype": "DATEV Settings",
"client": company.name,
"client_number": "12345",
- "consultant_number": "67890"
+ "consultant_number": "67890",
+ "temporary_against_account_number": "9999"
}).insert()
@@ -137,7 +138,8 @@
self.filters = {
"company": self.company.name,
"from_date": today(),
- "to_date": today()
+ "to_date": today(),
+ "temporary_against_account_number": "9999"
}
make_datev_settings(self.company)
diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py
index 8379297..09b04ff 100644
--- a/erpnext/regional/report/gstr_1/gstr_1.py
+++ b/erpnext/regional/report/gstr_1/gstr_1.py
@@ -151,6 +151,7 @@
{select_columns}
from `tab{doctype}`
where docstatus = 1 {where_conditions}
+ and is_opening = 'No'
order by posting_date desc
""".format(select_columns=self.select_columns, doctype=self.doctype,
where_conditions=conditions), self.filters, as_dict=1)
@@ -235,6 +236,7 @@
self.cgst_sgst_invoices = []
unidentified_gst_accounts = []
+ unidentified_gst_accounts_invoice = []
for parent, account, item_wise_tax_detail, tax_amount in self.tax_details:
if account in self.gst_accounts.cess_account:
self.invoice_cess.setdefault(parent, tax_amount)
@@ -250,19 +252,21 @@
if not (cgst_or_sgst or account in self.gst_accounts.igst_account):
if "gst" in account.lower() and account not in unidentified_gst_accounts:
unidentified_gst_accounts.append(account)
+ unidentified_gst_accounts_invoice.append(parent)
continue
for item_code, tax_amounts in item_wise_tax_detail.items():
tax_rate = tax_amounts[0]
- if cgst_or_sgst:
- tax_rate *= 2
- if parent not in self.cgst_sgst_invoices:
- self.cgst_sgst_invoices.append(parent)
+ if tax_rate:
+ if cgst_or_sgst:
+ tax_rate *= 2
+ if parent not in self.cgst_sgst_invoices:
+ self.cgst_sgst_invoices.append(parent)
- rate_based_dict = self.items_based_on_tax_rate\
- .setdefault(parent, {}).setdefault(tax_rate, [])
- if item_code not in rate_based_dict:
- rate_based_dict.append(item_code)
+ rate_based_dict = self.items_based_on_tax_rate\
+ .setdefault(parent, {}).setdefault(tax_rate, [])
+ if item_code not in rate_based_dict:
+ rate_based_dict.append(item_code)
except ValueError:
continue
if unidentified_gst_accounts:
@@ -271,7 +275,7 @@
# Build itemised tax for export invoices where tax table is blank
for invoice, items in iteritems(self.invoice_items):
- if invoice not in self.items_based_on_tax_rate \
+ if invoice not in self.items_based_on_tax_rate and invoice not in unidentified_gst_accounts_invoice \
and frappe.db.get_value(self.doctype, invoice, "export_type") == "Without Payment of Tax":
self.items_based_on_tax_rate.setdefault(invoice, {}).setdefault(0, items.keys())
diff --git a/erpnext/regional/report/irs_1099/irs_1099.js b/erpnext/regional/report/irs_1099/irs_1099.js
index 2d74652..070ff43 100644
--- a/erpnext/regional/report/irs_1099/irs_1099.js
+++ b/erpnext/regional/report/irs_1099/irs_1099.js
@@ -4,7 +4,7 @@
frappe.query_reports["IRS 1099"] = {
"filters": [
{
- "fieldname":"company",
+ "fieldname": "company",
"label": __("Company"),
"fieldtype": "Link",
"options": "Company",
@@ -13,7 +13,7 @@
"width": 80,
},
{
- "fieldname":"fiscal_year",
+ "fieldname": "fiscal_year",
"label": __("Fiscal Year"),
"fieldtype": "Link",
"options": "Fiscal Year",
@@ -22,7 +22,7 @@
"width": 80,
},
{
- "fieldname":"supplier_group",
+ "fieldname": "supplier_group",
"label": __("Supplier Group"),
"fieldtype": "Link",
"options": "Supplier Group",
@@ -32,16 +32,16 @@
},
],
- onload: function(query_report) {
+ onload: function (query_report) {
query_report.page.add_inner_button(__("Print IRS 1099 Forms"), () => {
build_1099_print(query_report);
});
}
};
-function build_1099_print(query_report){
+function build_1099_print(query_report) {
let filters = JSON.stringify(query_report.get_values());
let w = window.open('/api/method/erpnext.regional.report.irs_1099.irs_1099.irs_1099_print?' +
- '&filters=' + encodeURIComponent(filters));
+ '&filters=' + encodeURIComponent(filters));
// w.print();
}
diff --git a/erpnext/regional/report/irs_1099/irs_1099.py b/erpnext/regional/report/irs_1099/irs_1099.py
index d3509e5..4e57ff7 100644
--- a/erpnext/regional/report/irs_1099/irs_1099.py
+++ b/erpnext/regional/report/irs_1099/irs_1099.py
@@ -1,32 +1,41 @@
# Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
# For license information, please see license.txt
-from __future__ import unicode_literals
-import frappe
import json
-from frappe import _, _dict
-from frappe.utils import nowdate
-from frappe.utils.data import fmt_money
-from erpnext.accounts.utils import get_fiscal_year
+
from PyPDF2 import PdfFileWriter
+
+import frappe
+from erpnext.accounts.utils import get_fiscal_year
+from frappe import _
+from frappe.utils import cstr, nowdate
+from frappe.utils.data import fmt_money
+from frappe.utils.jinja import render_template
from frappe.utils.pdf import get_pdf
from frappe.utils.print_format import read_multi_pdf
-from frappe.utils.jinja import render_template
+
+IRS_1099_FORMS_FILE_EXTENSION = ".pdf"
def execute(filters=None):
- filters = filters if isinstance(filters, _dict) else _dict(filters)
-
+ filters = filters if isinstance(filters, frappe._dict) else frappe._dict(filters)
if not filters:
filters.setdefault('fiscal_year', get_fiscal_year(nowdate())[0])
filters.setdefault('company', frappe.db.get_default("company"))
- region = frappe.db.get_value("Company", fieldname = ["country"], filters = { "name": filters.company })
+ region = frappe.db.get_value("Company",
+ filters={"name": filters.company},
+ fieldname=["country"])
+
if region != 'United States':
- return [],[]
+ return [], []
data = []
columns = get_columns()
+ conditions = ""
+ if filters.supplier_group:
+ conditions += "AND s.supplier_group = %s" %frappe.db.escape(filters.get("supplier_group"))
+
data = frappe.db.sql("""
SELECT
s.supplier_group as "supplier_group",
@@ -34,20 +43,25 @@
s.tax_id as "tax_id",
SUM(gl.debit_in_account_currency) AS "payments"
FROM
- `tabGL Entry` gl INNER JOIN `tabSupplier` s
+ `tabGL Entry` gl
+ INNER JOIN `tabSupplier` s
WHERE
s.name = gl.party
- AND s.irs_1099 = 1
- AND gl.fiscal_year = %(fiscal_year)s
- AND gl.party_type = "Supplier"
-
+ AND s.irs_1099 = 1
+ AND gl.fiscal_year = %(fiscal_year)s
+ AND gl.party_type = "Supplier"
+ AND gl.company = %(company)s
+ {conditions}
+
GROUP BY
gl.party
ORDER BY
- gl.party DESC""", {"fiscal_year": filters.fiscal_year,
- "supplier_group": filters.supplier_group,
- "company": filters.company}, as_dict=True)
+ gl.party DESC""".format(conditions=conditions), {
+ "fiscal_year": filters.fiscal_year,
+ "company": filters.company
+ }, as_dict=True)
+
return columns, data
@@ -71,14 +85,13 @@
"fieldname": "tax_id",
"label": _("Tax ID"),
"fieldtype": "Data",
- "width": 120
+ "width": 200
},
{
-
"fieldname": "payments",
"label": _("Total Payments"),
"fieldtype": "Currency",
- "width": 120
+ "width": 200
}
]
@@ -88,23 +101,32 @@
if not filters:
frappe._dict({
"company": frappe.db.get_default("Company"),
- "fiscal_year": frappe.db.get_default("fiscal_year")})
+ "fiscal_year": frappe.db.get_default("Fiscal Year")
+ })
else:
filters = frappe._dict(json.loads(filters))
+
+ fiscal_year_doc = get_fiscal_year(fiscal_year=filters.fiscal_year, as_dict=True)
+ fiscal_year = cstr(fiscal_year_doc.year_start_date.year)
+
company_address = get_payer_address_html(filters.company)
company_tin = frappe.db.get_value("Company", filters.company, "tax_id")
+
columns, data = execute(filters)
template = frappe.get_doc("Print Format", "IRS 1099 Form").html
output = PdfFileWriter()
+
for row in data:
+ row["fiscal_year"] = fiscal_year
row["company"] = filters.company
row["company_tin"] = company_tin
row["payer_street_address"] = company_address
- row["recipient_street_address"], row["recipient_city_state"] = get_street_address_html("Supplier", row.supplier)
+ row["recipient_street_address"], row["recipient_city_state"] = get_street_address_html(
+ "Supplier", row.supplier)
row["payments"] = fmt_money(row["payments"], precision=0, currency="USD")
- frappe._dict(row)
pdf = get_pdf(render_template(template, row), output=output if output else None)
- frappe.local.response.filename = filters.fiscal_year + " " + filters.company + " IRS 1099 Forms"
+
+ frappe.local.response.filename = f"{filters.fiscal_year} {filters.company} IRS 1099 Forms{IRS_1099_FORMS_FILE_EXTENSION}"
frappe.local.response.filecontent = read_multi_pdf(output)
frappe.local.response.type = "download"
@@ -120,36 +142,45 @@
ORDER BY
address_type="Postal" DESC, address_type="Billing" DESC
LIMIT 1
- """, {"company": company}, as_dict=True)
+ """, {"company": company}, as_dict=True)
+
+ address_display = ""
if address_list:
company_address = address_list[0]["name"]
- return frappe.get_doc("Address", company_address).get_display()
- else:
- return ""
+ address_display = frappe.get_doc("Address", company_address).get_display()
+
+ return address_display
def get_street_address_html(party_type, party):
address_list = frappe.db.sql("""
SELECT
link.parent
- FROM `tabDynamic Link` link, `tabAddress` address
- WHERE link.parenttype = "Address"
- AND link.link_name = %(party)s
- ORDER BY address.address_type="Postal" DESC,
+ FROM
+ `tabDynamic Link` link,
+ `tabAddress` address
+ WHERE
+ link.parenttype = "Address"
+ AND link.link_name = %(party)s
+ ORDER BY
+ address.address_type="Postal" DESC,
address.address_type="Billing" DESC
LIMIT 1
- """, {"party": party}, as_dict=True)
+ """, {"party": party}, as_dict=True)
+
+ street_address = city_state = ""
if address_list:
supplier_address = address_list[0]["parent"]
doc = frappe.get_doc("Address", supplier_address)
+
if doc.address_line2:
- street = doc.address_line1 + "<br>\n" + doc.address_line2 + "<br>\n"
+ street_address = doc.address_line1 + "<br>\n" + doc.address_line2 + "<br>\n"
else:
- street = doc.address_line1 + "<br>\n"
- city = doc.city + ", " if doc.city else ""
- city = city + doc.state + " " if doc.state else city
- city = city + doc.pincode if doc.pincode else city
- city += "<br>\n"
- return street, city
- else:
- return "", ""
+ street_address = doc.address_line1 + "<br>\n"
+
+ city_state = doc.city + ", " if doc.city else ""
+ city_state = city_state + doc.state + " " if doc.state else city_state
+ city_state = city_state + doc.pincode if doc.pincode else city_state
+ city_state += "<br>\n"
+
+ return street_address, city_state
diff --git a/erpnext/regional/united_arab_emirates/setup.py b/erpnext/regional/united_arab_emirates/setup.py
index 013ae5c..776a82c 100644
--- a/erpnext/regional/united_arab_emirates/setup.py
+++ b/erpnext/regional/united_arab_emirates/setup.py
@@ -110,9 +110,11 @@
'Purchase Order': purchase_invoice_fields + invoice_fields,
'Purchase Receipt': purchase_invoice_fields + invoice_fields,
'Sales Invoice': sales_invoice_fields + invoice_fields,
+ 'POS Invoice': sales_invoice_fields + invoice_fields,
'Sales Order': sales_invoice_fields + invoice_fields,
'Delivery Note': sales_invoice_fields + invoice_fields,
'Sales Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt],
+ 'POS Invoice Item': invoice_item_fields + delivery_date_field + [is_zero_rated, is_exempt],
'Purchase Invoice Item': invoice_item_fields,
'Sales Order Item': invoice_item_fields,
'Delivery Note Item': invoice_item_fields,
diff --git a/erpnext/regional/united_states/test_united_states.py b/erpnext/regional/united_states/test_united_states.py
index ad95010..513570e 100644
--- a/erpnext/regional/united_states/test_united_states.py
+++ b/erpnext/regional/united_states/test_united_states.py
@@ -26,7 +26,6 @@
make_payment_entry_to_irs_1099_supplier()
filters = frappe._dict({"fiscal_year": "_Test Fiscal Year 2016", "company": "_Test Company 1"})
columns, data = execute_1099_report(filters)
- print(columns, data)
expected_row = {'supplier': '_US 1099 Test Supplier',
'supplier_group': 'Services',
'payments': 100.0,
diff --git a/erpnext/selling/desk_page/retail/retail.json b/erpnext/selling/desk_page/retail/retail.json
deleted file mode 100644
index c4ddf26..0000000
--- a/erpnext/selling/desk_page/retail/retail.json
+++ /dev/null
@@ -1,47 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Settings & Configurations",
- "links": "[\n {\n \"description\": \"Setup default values for POS Invoices\",\n \"label\": \"Point-of-Sale Profile\",\n \"name\": \"POS Profile\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"POS Settings\",\n \"name\": \"POS Settings\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Loyalty Program",
- "links": "[\n {\n \"description\": \"To make Customer based incentive schemes.\",\n \"label\": \"Loyalty Program\",\n \"name\": \"Loyalty Program\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"To view logs of Loyalty Points assigned to a Customer.\",\n \"label\": \"Loyalty Point Entry\",\n \"name\": \"Loyalty Point Entry\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Opening & Closing",
- "links": "[\n {\n \"label\": \"POS Opening Entry\",\n \"name\": \"POS Opening Entry\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"POS Closing Entry\",\n \"name\": \"POS Closing Entry\",\n \"type\": \"doctype\"\n }\n]"
- }
- ],
- "category": "Domains",
- "charts": [],
- "creation": "2020-03-02 17:18:32.505616",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Retail",
- "modified": "2020-09-09 11:46:28.297435",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "Retail",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "restrict_to_domain": "Retail",
- "shortcuts": [
- {
- "doc_view": "",
- "label": "Point Of Sale",
- "link_to": "point-of-sale",
- "type": "Page"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/selling/desk_page/selling/selling.json b/erpnext/selling/desk_page/selling/selling.json
deleted file mode 100644
index b15df98..0000000
--- a/erpnext/selling/desk_page/selling/selling.json
+++ /dev/null
@@ -1,92 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Selling",
- "links": "[\n {\n \"description\": \"Customer Database.\",\n \"label\": \"Customer\",\n \"name\": \"Customer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Quotes to Leads or Customers.\",\n \"label\": \"Quotation\",\n \"name\": \"Quotation\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Confirmed orders from Customers.\",\n \"label\": \"Sales Order\",\n \"name\": \"Sales Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Sales Invoice\",\n \"name\": \"Sales Invoice\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Blanket Orders from Costumers.\",\n \"label\": \"Blanket Order\",\n \"name\": \"Blanket Order\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Manage Sales Partners.\",\n \"label\": \"Sales Partner\",\n \"name\": \"Sales Partner\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"description\": \"Manage Sales Person Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Sales Person\",\n \"link\": \"Tree/Sales Person\",\n \"name\": \"Sales Person\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Items and Pricing",
- "links": "[\n {\n \"description\": \"All Products or Services.\",\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Price List\"\n ],\n \"description\": \"Multiple Item prices.\",\n \"label\": \"Item Price\",\n \"name\": \"Item Price\",\n \"onboard\": 1,\n \"route\": \"#Report/Item Price\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Price List master.\",\n \"label\": \"Price List\",\n \"name\": \"Price List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tree of Item Groups.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Item Group\",\n \"link\": \"Tree/Item Group\",\n \"name\": \"Item Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Bundle items at time of sale.\",\n \"label\": \"Product Bundle\",\n \"name\": \"Product Bundle\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Rules for applying different promotional schemes.\",\n \"label\": \"Promotional Scheme\",\n \"name\": \"Promotional Scheme\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"description\": \"Rules for applying pricing and discount.\",\n \"label\": \"Pricing Rule\",\n \"name\": \"Pricing Rule\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Rules for adding shipping costs.\",\n \"label\": \"Shipping Rule\",\n \"name\": \"Shipping Rule\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Define coupon codes.\",\n \"label\": \"Coupon Code\",\n \"name\": \"Coupon Code\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Settings",
- "links": "[\n {\n \"description\": \"Default settings for selling transactions.\",\n \"label\": \"Selling Settings\",\n \"name\": \"Selling Settings\",\n \"settings\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Template of terms or contract.\",\n \"label\": \"Terms and Conditions Template\",\n \"name\": \"Terms and Conditions\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tax template for selling transactions.\",\n \"label\": \"Sales Taxes and Charges Template\",\n \"name\": \"Sales Taxes and Charges Template\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Track Leads by Lead Source.\",\n \"label\": \"Lead Source\",\n \"name\": \"Lead Source\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Customer Group Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Customer Group\",\n \"link\": \"Tree/Customer Group\",\n \"name\": \"Customer Group\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Contacts.\",\n \"label\": \"Contact\",\n \"name\": \"Contact\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"All Addresses.\",\n \"label\": \"Address\",\n \"name\": \"Address\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Territory Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Territory\",\n \"link\": \"Tree/Territory\",\n \"name\": \"Territory\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Sales campaigns.\",\n \"label\": \"Campaign\",\n \"name\": \"Campaign\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Key Reports",
- "links": "[\n {\n \n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Analytics\",\n \"name\": \"Sales Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"icon\": \"fa fa-bar-chart\",\n \"label\": \"Sales Funnel\",\n \"name\": \"sales-funnel\",\n \"onboard\": 1,\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Trends\",\n \"name\": \"Sales Order Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Quotation\"\n ],\n \"doctype\": \"Quotation\",\n \"is_query_report\": true,\n \"label\": \"Quotation Trends\",\n \"name\": \"Quotation Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"icon\": \"fa fa-bar-chart\",\n \"is_query_report\": true,\n \"label\": \"Customer Acquisition and Loyalty\",\n \"name\": \"Customer Acquisition and Loyalty\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Inactive Customers\",\n \"name\": \"Inactive Customers\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person-wise Transaction Summary\",\n \"name\": \"Sales Person-wise Transaction Summary\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item-wise Sales History\",\n \"name\": \"Item-wise Sales History\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Other Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Lead\"\n ],\n \"doctype\": \"Lead\",\n \"is_query_report\": true,\n \"label\": \"Lead Details\",\n \"name\": \"Lead Details\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Address\"\n ],\n \"doctype\": \"Address\",\n \"is_query_report\": true,\n \"label\": \"Customer Addresses And Contacts\",\n \"name\": \"Address And Contacts\",\n \"route_options\": {\n \"party_type\": \"Customer\"\n },\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Available Stock for Packing Items\",\n \"name\": \"Available Stock for Packing Items\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Pending SO Items For Purchase Request\",\n \"name\": \"Pending SO Items For Purchase Request\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Invoice\"\n ],\n \"doctype\": \"Sales Invoice\",\n \"is_query_report\": true,\n \"label\": \"Sales Invoice Trends\",\n \"name\": \"Sales Invoice Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customer Credit Balance\",\n \"name\": \"Customer Credit Balance\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Customers Without Any Sales Transactions\",\n \"name\": \"Customers Without Any Sales Transactions\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Customer\"\n ],\n \"doctype\": \"Customer\",\n \"is_query_report\": true,\n \"label\": \"Sales Partners Commission\",\n \"name\": \"Sales Partners Commission\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Territory Target Variance Based On Item Group\",\n \"name\": \"Territory Target Variance Based On Item Group\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Person Target Variance Based On Item Group\",\n \"name\": \"Sales Person Target Variance Based On Item Group\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Partner Target Variance Based On Item Group\",\n \"name\": \"Sales Partner Target Variance based on Item Group\",\n \"type\": \"report\"\n }\n \n]"
- }
- ],
- "category": "Modules",
- "charts": [
- {
- "chart_name": "Sales Order Trends",
- "label": "Sales Order Trends"
- }
- ],
- "charts_label": "Selling ",
- "creation": "2020-01-28 11:49:12.092882",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 1,
- "idx": 0,
- "is_standard": 1,
- "label": "Selling",
- "modified": "2020-10-08 10:23:09.984377",
- "modified_by": "Administrator",
- "module": "Selling",
- "name": "Selling",
- "onboarding": "Selling",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "color": "#cef6d1",
- "format": "{} Available",
- "label": "Item",
- "link_to": "Item",
- "stats_filter": "{\n \"disabled\":0\n}",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} To Deliver",
- "label": "Sales Order",
- "link_to": "Sales Order",
- "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\":[\"in\", [\"To Deliver\", \"To Deliver and Bill\"]]\n}",
- "type": "DocType"
- },
- {
- "color": "#cef6d1",
- "format": "{} Open",
- "label": "Sales Analytics",
- "link_to": "Sales Analytics",
- "stats_filter": "{ \"Status\": \"Open\" }",
- "type": "Report"
- },
- {
- "label": "Sales Order Analysis",
- "link_to": "Sales Order Analysis",
- "type": "Report"
- },
- {
- "label": "Dashboard",
- "link_to": "Selling",
- "type": "Dashboard"
- }
- ],
- "shortcuts_label": "Quick Access"
-}
\ No newline at end of file
diff --git a/erpnext/selling/doctype/customer/customer.json b/erpnext/selling/doctype/customer/customer.json
index 557c715..a048928 100644
--- a/erpnext/selling/doctype/customer/customer.json
+++ b/erpnext/selling/doctype/customer/customer.json
@@ -16,6 +16,8 @@
"customer_name",
"gender",
"customer_type",
+ "pan",
+ "tax_withholding_category",
"default_bank_account",
"lead_name",
"image",
@@ -34,9 +36,8 @@
"companies",
"currency_and_price_list",
"default_currency",
- "default_price_list",
"column_break_14",
- "language",
+ "default_price_list",
"address_contacts",
"address_html",
"website",
@@ -59,6 +60,7 @@
"column_break_45",
"market_segment",
"industry",
+ "language",
"is_frozen",
"column_break_38",
"loyalty_program",
@@ -210,7 +212,8 @@
"fieldtype": "Link",
"ignore_user_permissions": 1,
"label": "Represents Company",
- "options": "Company"
+ "options": "Company",
+ "unique": 1
},
{
"depends_on": "represents_company",
@@ -479,13 +482,25 @@
"fieldname": "dn_required",
"fieldtype": "Check",
"label": "Allow Sales Invoice Creation Without Delivery Note"
+ },
+ {
+ "fieldname": "pan",
+ "fieldtype": "Data",
+ "label": "PAN"
+ },
+ {
+ "fieldname": "tax_withholding_category",
+ "fieldtype": "Link",
+ "label": "Tax Withholding Category",
+ "options": "Tax Withholding Category"
}
],
"icon": "fa fa-user",
"idx": 363,
"image_field": "image",
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-03-17 11:03:42.706907",
+ "modified": "2021-01-27 12:54:57.258959",
"modified_by": "Administrator",
"module": "Selling",
"name": "Customer",
diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py
index 0172d9c..c452594 100644
--- a/erpnext/selling/doctype/customer/customer.py
+++ b/erpnext/selling/doctype/customer/customer.py
@@ -58,6 +58,7 @@
self.set_loyalty_program()
self.check_customer_group_change()
self.validate_default_bank_account()
+ self.validate_internal_customer()
# set loyalty program tier
if frappe.db.exists('Customer', self.name):
@@ -82,6 +83,14 @@
if not is_company_account:
frappe.throw(_("{0} is not a company bank account").format(frappe.bold(self.default_bank_account)))
+ def validate_internal_customer(self):
+ internal_customer = frappe.db.get_value("Customer",
+ {"is_internal_customer": 1, "represents_company": self.represents_company, "name": ("!=", self.name)}, "name")
+
+ if internal_customer:
+ frappe.throw(_("Internal Customer for company {0} already exists").format(
+ frappe.bold(self.represents_company)))
+
def on_update(self):
self.validate_name_with_customer_group()
self.create_primary_contact()
@@ -117,7 +126,9 @@
'''If Customer created from Lead, update lead status to "Converted"
update Customer link in Quotation, Opportunity'''
if self.lead_name:
- frappe.db.set_value('Lead', self.lead_name, 'status', 'Converted', update_modified=False)
+ lead = frappe.get_doc('Lead', self.lead_name)
+ lead.status = 'Converted'
+ lead.save()
def create_lead_address_contact(self):
if self.lead_name:
@@ -398,7 +409,7 @@
# form a list of emails and names to show to the user
credit_controller_users_formatted = [get_formatted_email(user).replace("<", "(").replace(">", ")") for user in credit_controller_users]
if not credit_controller_users_formatted:
- frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.".format(customer)))
+ frappe.throw(_("Please contact your administrator to extend the credit limits for {0}.").format(customer))
message = """Please contact any of the following users to extend the credit limits for {0}:
<br><br><ul><li>{1}</li></ul>""".format(customer, '<li>'.join(credit_controller_users_formatted))
diff --git a/erpnext/selling/doctype/quotation/quotation.js b/erpnext/selling/doctype/quotation/quotation.js
index 661e107..5a0d9c9 100644
--- a/erpnext/selling/doctype/quotation/quotation.js
+++ b/erpnext/selling/doctype/quotation/quotation.js
@@ -7,7 +7,7 @@
frappe.ui.form.on('Quotation', {
setup: function(frm) {
frm.custom_make_buttons = {
- 'Sales Order': 'Make Sales Order'
+ 'Sales Order': 'Sales Order'
},
frm.set_query("quotation_to", function() {
diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py
index 20ae19f..5da248c 100644
--- a/erpnext/selling/doctype/quotation/quotation.py
+++ b/erpnext/selling/doctype/quotation/quotation.py
@@ -19,13 +19,12 @@
self.indicator_color = 'blue'
self.indicator_title = 'Submitted'
if self.valid_till and getdate(self.valid_till) < getdate(nowdate()):
- self.indicator_color = 'darkgrey'
+ self.indicator_color = 'gray'
self.indicator_title = 'Expired'
def validate(self):
super(Quotation, self).validate()
self.set_status()
- self.update_opportunity()
self.validate_uom_is_integer("stock_uom", "qty")
self.validate_valid_till()
self.set_customer_name()
@@ -50,21 +49,20 @@
lead_name, company_name = frappe.db.get_value("Lead", self.party_name, ["lead_name", "company_name"])
self.customer_name = company_name or lead_name
- def update_opportunity(self):
+ def update_opportunity(self, status):
for opportunity in list(set([d.prevdoc_docname for d in self.get("items")])):
if opportunity:
- self.update_opportunity_status(opportunity)
+ self.update_opportunity_status(status, opportunity)
if self.opportunity:
- self.update_opportunity_status()
+ self.update_opportunity_status(status)
- def update_opportunity_status(self, opportunity=None):
+ def update_opportunity_status(self, status, opportunity=None):
if not opportunity:
opportunity = self.opportunity
opp = frappe.get_doc("Opportunity", opportunity)
- opp.status = None
- opp.set_status(update=True)
+ opp.set_status(status=status, update=True)
def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None):
if not self.has_sales_order():
@@ -82,7 +80,7 @@
else:
frappe.throw(_("Invalid lost reason {0}, please create a new lost reason").format(frappe.bold(reason.get('lost_reason'))))
- self.update_opportunity()
+ self.update_opportunity('Lost')
self.update_lead()
self.save()
@@ -95,7 +93,7 @@
self.company, self.base_grand_total, self)
#update enquiry status
- self.update_opportunity()
+ self.update_opportunity('Quotation')
self.update_lead()
def on_cancel(self):
@@ -105,7 +103,7 @@
#update enquiry status
self.set_status(update=True)
- self.update_opportunity()
+ self.update_opportunity('Open')
self.update_lead()
def print_other_charges(self,docname):
diff --git a/erpnext/selling/doctype/quotation/quotation_list.js b/erpnext/selling/doctype/quotation/quotation_list.js
index f425acf..b631685 100644
--- a/erpnext/selling/doctype/quotation/quotation_list.js
+++ b/erpnext/selling/doctype/quotation/quotation_list.js
@@ -20,9 +20,9 @@
} else if(doc.status==="Ordered") {
return [__("Ordered"), "green", "status,=,Ordered"];
} else if(doc.status==="Lost") {
- return [__("Lost"), "darkgrey", "status,=,Lost"];
+ return [__("Lost"), "gray", "status,=,Lost"];
} else if(doc.status==="Expired") {
- return [__("Expired"), "darkgrey", "status,=,Expired"];
+ return [__("Expired"), "gray", "status,=,Expired"];
}
}
};
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.json b/erpnext/selling/doctype/quotation_item/quotation_item.json
index 59ae7b2..a6785f7 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.json
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.json
@@ -47,6 +47,7 @@
"base_amount",
"base_net_amount",
"pricing_rules",
+ "stock_uom_rate",
"is_free_item",
"section_break_43",
"valuation_rate",
@@ -634,12 +635,20 @@
"print_hide": 1,
"read_only": 1,
"report_hide": 1
+ },
+ {
+ "depends_on": "eval: doc.uom != doc.stock_uom",
+ "fieldname": "stock_uom_rate",
+ "fieldtype": "Currency",
+ "label": "Rate of Stock UOM",
+ "options": "currency",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-19 20:48:43.222229",
+ "modified": "2021-01-30 21:39:40.174551",
"modified_by": "Administrator",
"module": "Selling",
"name": "Quotation Item",
diff --git a/erpnext/selling/doctype/quotation_item/quotation_item.py b/erpnext/selling/doctype/quotation_item/quotation_item.py
index 966b542..7384871 100644
--- a/erpnext/selling/doctype/quotation_item/quotation_item.py
+++ b/erpnext/selling/doctype/quotation_item/quotation_item.py
@@ -5,8 +5,6 @@
import frappe
from frappe.model.document import Document
-from erpnext.controllers.print_settings import print_settings_for_item_table
class QuotationItem(Document):
- def __setup__(self):
- print_settings_for_item_table(self)
+ pass
diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js
index 1d890bb..e3b41e6 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.js
+++ b/erpnext/selling/doctype/sales_order/sales_order.js
@@ -8,7 +8,7 @@
frm.custom_make_buttons = {
'Delivery Note': 'Delivery Note',
'Pick List': 'Pick List',
- 'Sales Invoice': 'Invoice',
+ 'Sales Invoice': 'Sales Invoice',
'Material Request': 'Material Request',
'Purchase Order': 'Purchase Order',
'Project': 'Project',
@@ -171,8 +171,10 @@
this.frm.add_custom_button(__('Request for Raw Materials'), () => this.make_raw_material_request(), __('Create'));
}
- // make purchase order
+ // Make Purchase Order
+ if (!this.frm.doc.is_internal_customer) {
this.frm.add_custom_button(__('Purchase Order'), () => this.make_purchase_order(), __('Create'));
+ }
// maintenance
if(flt(doc.per_delivered, 2) < 100 && (order_is_maintenance || order_is_a_custom_sale)) {
@@ -193,16 +195,15 @@
if (doc.docstatus === 1 && !doc.inter_company_order_reference) {
let me = this;
- frappe.model.with_doc("Customer", me.frm.doc.customer, () => {
- let customer = frappe.model.get_doc("Customer", me.frm.doc.customer);
- let internal = customer.is_internal_customer;
- let disabled = customer.disabled;
- if (internal === 1 && disabled === 0) {
- me.frm.add_custom_button("Inter Company Order", function() {
- me.make_inter_company_order();
- }, __('Create'));
- }
- });
+ let internal = me.frm.doc.is_internal_customer;
+ if (internal) {
+ let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Order" :
+ "Inter Company Purchase Order";
+
+ me.frm.add_custom_button(button_label, function() {
+ me.make_inter_company_order();
+ }, __('Create'));
+ }
}
}
// payment request
@@ -327,7 +328,7 @@
if(r.message) {
frappe.msgprint({
message: __('Work Orders Created: {0}', [r.message.map(function(d) {
- return repl('<a href="#Form/Work Order/%(name)s">%(name)s</a>', {name:d})
+ return repl('<a href="/app/work-order/%(name)s">%(name)s</a>', {name:d})
}).join(', ')]),
indicator: 'green'
})
@@ -436,7 +437,7 @@
callback: function(r) {
if(r.message) {
frappe.msgprint(__('Material Request {0} submitted.',
- ['<a href="#Form/Material Request/'+r.message.name+'">' + r.message.name+ '</a>']));
+ ['<a href="/app/material-request/'+r.message.name+'">' + r.message.name+ '</a>']));
}
d.hide();
me.frm.reload_doc();
@@ -513,7 +514,7 @@
make_delivery_note: function() {
frappe.model.open_mapped_doc({
method: "erpnext.selling.doctype.sales_order.sales_order.make_delivery_note",
- frm: me.frm
+ frm: this.frm
})
},
diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json
index 3d64ac3..0a5c665 100644
--- a/erpnext/selling/doctype/sales_order/sales_order.json
+++ b/erpnext/selling/doctype/sales_order/sales_order.json
@@ -107,6 +107,8 @@
"tc_name",
"terms",
"more_info",
+ "is_internal_customer",
+ "represents_company",
"inter_company_order_reference",
"project",
"party_account_currency",
@@ -1103,7 +1105,8 @@
"hide_days": 1,
"hide_seconds": 1,
"label": "Inter Company Order Reference",
- "options": "Purchase Order"
+ "options": "Purchase Order",
+ "read_only": 1
},
{
"description": "Track this Sales Order against any Project",
@@ -1455,13 +1458,29 @@
"hide_seconds": 1,
"label": "Skip Delivery Note",
"print_hide": 1
+ },
+ {
+ "default": "0",
+ "fetch_from": "customer.is_internal_customer",
+ "fieldname": "is_internal_customer",
+ "fieldtype": "Check",
+ "label": "Is Internal Customer",
+ "read_only": 1
+ },
+ {
+ "fetch_from": "customer.represents_company",
+ "fieldname": "represents_company",
+ "fieldtype": "Link",
+ "label": "Represents Company",
+ "options": "Company",
+ "read_only": 1
}
],
"icon": "fa fa-file-text",
"idx": 105,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 13:59:18.628077",
+ "modified": "2021-01-20 23:40:39.929296",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order",
diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py
index 04d85e5..e561291 100755
--- a/erpnext/selling/doctype/sales_order/sales_order.py
+++ b/erpnext/selling/doctype/sales_order/sales_order.py
@@ -14,7 +14,6 @@
from frappe.desk.notifications import clear_doctype_notifications
from frappe.contacts.doctype.address.address import get_company_address
from erpnext.controllers.selling_controller import SellingController
-from frappe.automation.doctype.auto_repeat.auto_repeat import get_next_schedule_date
from erpnext.selling.doctype.customer.customer import check_credit_limit
from erpnext.stock.doctype.item.item import get_item_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
@@ -159,7 +158,6 @@
frappe.throw(_("Quotation {0} is cancelled").format(quotation))
doc.set_status(update=True)
- doc.update_opportunity()
def validate_drop_ship(self):
for d in self.get('items'):
@@ -182,6 +180,7 @@
update_coupon_code_count(self.coupon_code,'used')
def on_cancel(self):
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
super(SalesOrder, self).on_cancel()
# Cannot cancel closed SO
@@ -418,8 +417,7 @@
def on_recurring(self, reference_doc, auto_repeat_doc):
def _get_delivery_date(ref_doc_delivery_date, red_doc_transaction_date, transaction_date):
- delivery_date = get_next_schedule_date(ref_doc_delivery_date,
- auto_repeat_doc.frequency, auto_repeat_doc.start_date, cint(auto_repeat_doc.repeat_on_day))
+ delivery_date = auto_repeat_doc.get_next_schedule_date(schedule_date=ref_doc_delivery_date)
if delivery_date <= transaction_date:
delivery_date_diff = frappe.utils.date_diff(ref_doc_delivery_date, red_doc_transaction_date)
@@ -832,56 +830,49 @@
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
for supplier in suppliers:
- po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
- if len(po) == 0:
- doc = get_mapped_doc("Sales Order", source_name, {
- "Sales Order": {
- "doctype": "Purchase Order",
- "field_no_map": [
- "address_display",
- "contact_display",
- "contact_mobile",
- "contact_email",
- "contact_person",
- "taxes_and_charges",
- "shipping_address",
- "terms"
- ],
- "validation": {
- "docstatus": ["=", 1]
- }
- },
- "Sales Order Item": {
- "doctype": "Purchase Order Item",
- "field_map": [
- ["name", "sales_order_item"],
- ["parent", "sales_order"],
- ["stock_uom", "stock_uom"],
- ["uom", "uom"],
- ["conversion_factor", "conversion_factor"],
- ["delivery_date", "schedule_date"]
- ],
- "field_no_map": [
- "rate",
- "price_list_rate",
- "item_tax_template",
- "discount_percentage",
- "discount_amount",
- "pricing_rules"
- ],
- "postprocess": update_item,
- "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
+ doc = get_mapped_doc("Sales Order", source_name, {
+ "Sales Order": {
+ "doctype": "Purchase Order",
+ "field_no_map": [
+ "address_display",
+ "contact_display",
+ "contact_mobile",
+ "contact_email",
+ "contact_person",
+ "taxes_and_charges",
+ "shipping_address",
+ "terms"
+ ],
+ "validation": {
+ "docstatus": ["=", 1]
}
- }, target_doc, set_missing_values)
+ },
+ "Sales Order Item": {
+ "doctype": "Purchase Order Item",
+ "field_map": [
+ ["name", "sales_order_item"],
+ ["parent", "sales_order"],
+ ["stock_uom", "stock_uom"],
+ ["uom", "uom"],
+ ["conversion_factor", "conversion_factor"],
+ ["delivery_date", "schedule_date"]
+ ],
+ "field_no_map": [
+ "rate",
+ "price_list_rate",
+ "item_tax_template",
+ "discount_percentage",
+ "discount_amount",
+ "pricing_rules"
+ ],
+ "postprocess": update_item,
+ "condition": lambda doc: doc.ordered_qty < doc.stock_qty and doc.supplier == supplier and doc.item_code in items_to_map
+ }
+ }, target_doc, set_missing_values)
- doc.insert()
- else:
- suppliers =[]
- if suppliers:
+ doc.insert()
frappe.db.commit()
return doc
- else:
- frappe.msgprint(_("Purchase Order already created for all Sales Order items"))
@frappe.whitelist()
def make_purchase_order(source_name, selected_items=None, target_doc=None):
@@ -1096,4 +1087,4 @@
if not total_produced_qty and frappe.flags.in_patch: return
- frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)
\ No newline at end of file
+ frappe.db.set_value('Sales Order Item', sales_order_item, 'produced_qty', total_produced_qty)
diff --git a/erpnext/selling/doctype/sales_order/test_sales_order.py b/erpnext/selling/doctype/sales_order/test_sales_order.py
index 643e7cf..52a0174 100644
--- a/erpnext/selling/doctype/sales_order/test_sales_order.py
+++ b/erpnext/selling/doctype/sales_order/test_sales_order.py
@@ -17,6 +17,18 @@
from erpnext.stock.doctype.item.test_item import make_item
class TestSalesOrder(unittest.TestCase):
+
+ @classmethod
+ def setUpClass(cls):
+ cls.unlink_setting = int(frappe.db.get_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order"))
+
+ @classmethod
+ def tearDownClass(cls) -> None:
+ # reset config to previous state
+ frappe.db.set_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order", cls.unlink_setting)
+
def tearDown(self):
frappe.set_user("Administrator")
@@ -325,6 +337,9 @@
create_dn_against_so(so.name, 4)
make_sales_invoice(so.name)
+ prev_total = so.get("base_total")
+ prev_total_in_words = so.get("base_in_words")
+
first_item_of_so = so.get("items")[0]
trans_item = json.dumps([
{'item_code' : first_item_of_so.item_code, 'rate' : first_item_of_so.rate, \
@@ -340,6 +355,12 @@
self.assertEqual(so.get("items")[-1].amount, 1400)
self.assertEqual(so.status, 'To Deliver and Bill')
+ updated_total = so.get("base_total")
+ updated_total_in_words = so.get("base_in_words")
+
+ self.assertEqual(updated_total, prev_total+1400)
+ self.assertNotEqual(updated_total_in_words, prev_total_in_words)
+
def test_update_child_removing_item(self):
so = make_sales_order(**{
"item_list": [{
@@ -772,6 +793,59 @@
so.load_from_db()
so.cancel()
+ def test_drop_shipping_partial_order(self):
+ from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order_for_default_supplier, \
+ update_status as so_update_status
+
+ # make items
+ po_item1 = make_item("_Test Item for Drop Shipping 1", {"is_stock_item": 1, "delivered_by_supplier": 1})
+ po_item2 = make_item("_Test Item for Drop Shipping 2", {"is_stock_item": 1, "delivered_by_supplier": 1})
+
+ so_items = [
+ {
+ "item_code": po_item1.item_code,
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ },
+ {
+ "item_code": po_item2.item_code,
+ "warehouse": "",
+ "qty": 2,
+ "rate": 400,
+ "delivered_by_supplier": 1,
+ "supplier": '_Test Supplier'
+ }
+ ]
+
+ # create so and po
+ so = make_sales_order(item_list=so_items, do_not_submit=True)
+ so.submit()
+
+ # create po for only one item
+ po1 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[0]])
+ po1.submit()
+
+ self.assertEqual(so.customer, po1.customer)
+ self.assertEqual(po1.items[0].sales_order, so.name)
+ self.assertEqual(po1.items[0].item_code, po_item1.item_code)
+ #test po item length
+ self.assertEqual(len(po1.items), 1)
+
+ # create po for remaining item
+ po2 = make_purchase_order_for_default_supplier(so.name, selected_items=[so_items[1]])
+ po2.submit()
+
+ # teardown
+ so_update_status("Draft", so.name)
+
+ po1.cancel()
+ po2.cancel()
+ so.load_from_db()
+ so.cancel()
+
def test_reserved_qty_for_closing_so(self):
bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"},
fields=["reserved_qty"])
@@ -987,6 +1061,38 @@
self.assertRaises(frappe.LinkExistsError, so_doc.cancel)
+ def test_cancel_sales_order_after_cancel_payment_entry(self):
+ from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry
+ # make a sales order
+ so = make_sales_order()
+
+ # disable unlinking of payment entry
+ frappe.db.set_value("Accounts Settings", "Accounts Settings",
+ "unlink_advance_payment_on_cancelation_of_order", 0)
+
+ # create a payment entry against sales order
+ pe = get_payment_entry("Sales Order", so.name, bank_account="_Test Bank - _TC")
+ pe.reference_no = "1"
+ pe.reference_date = nowdate()
+ pe.paid_from_account_currency = so.currency
+ pe.paid_to_account_currency = so.currency
+ pe.source_exchange_rate = 1
+ pe.target_exchange_rate = 1
+ pe.paid_amount = so.grand_total
+ pe.save(ignore_permissions=True)
+ pe.submit()
+
+ # Cancel payment entry
+ po_doc = frappe.get_doc("Payment Entry", pe.name)
+ po_doc.cancel()
+
+ # Cancel sales order
+ try:
+ so_doc = frappe.get_doc('Sales Order', so.name)
+ so_doc.cancel()
+ except Exception:
+ self.fail("Can not cancel sales order with linked cancelled payment entry")
+
def test_request_for_raw_materials(self):
item = make_item("_Test Finished Item", {"is_stock_item": 1,
"maintain_stock": 1,
@@ -1145,4 +1251,4 @@
))
workflow.insert(ignore_permissions=True)
- return workflow
\ No newline at end of file
+ return workflow
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
index eff17f8..37e47a9 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json
@@ -46,6 +46,7 @@
"base_rate",
"base_amount",
"pricing_rules",
+ "stock_uom_rate",
"is_free_item",
"section_break_24",
"net_rate",
@@ -214,7 +215,6 @@
"fieldtype": "Link",
"label": "UOM",
"options": "UOM",
- "print_hide": 0,
"reqd": 1
},
{
@@ -780,12 +780,20 @@
"fieldname": "manufacturing_section_section",
"fieldtype": "Section Break",
"label": "Manufacturing Section"
+ },
+ {
+ "depends_on": "eval: doc.uom != doc.stock_uom",
+ "fieldname": "stock_uom_rate",
+ "fieldtype": "Currency",
+ "label": "Rate of Stock UOM",
+ "options": "currency",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-05-29 20:54:32.309460",
+ "modified": "2021-01-30 21:35:07.617320",
"modified_by": "Administrator",
"module": "Selling",
"name": "Sales Order Item",
diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.py b/erpnext/selling/doctype/sales_order_item/sales_order_item.py
index 4a87a0c..27f303d 100644
--- a/erpnext/selling/doctype/sales_order_item/sales_order_item.py
+++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.py
@@ -5,11 +5,9 @@
import frappe
from frappe.model.document import Document
-from erpnext.controllers.print_settings import print_settings_for_item_table
class SalesOrderItem(Document):
- def __setup__(self):
- print_settings_for_item_table(self)
+ pass
def on_doctype_update():
frappe.db.add_index("Sales Order Item", ["item_code", "warehouse"])
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/onscan.js b/erpnext/selling/page/point_of_sale/onscan.js
deleted file mode 100644
index 428dc75..0000000
--- a/erpnext/selling/page/point_of_sale/onscan.js
+++ /dev/null
@@ -1 +0,0 @@
-!function(e,t){"object"==typeof exports&&"undefined"!=typeof module?module.exports=t():"function"==typeof define&&define.amd?define(t()):e.onScan=t()}(this,function(){var d={attachTo:function(e,t){if(void 0!==e.scannerDetectionData)throw new Error("onScan.js is already initialized for DOM element "+e);var n={onScan:function(e,t){},onScanError:function(e){},onKeyProcess:function(e,t){},onKeyDetect:function(e,t){},onPaste:function(e,t){},keyCodeMapper:function(e){return d.decodeKeyEvent(e)},onScanButtonLongPress:function(){},scanButtonKeyCode:!1,scanButtonLongPressTime:500,timeBeforeScanTest:100,avgTimeByChar:30,minLength:6,suffixKeyCodes:[9,13],prefixKeyCodes:[],ignoreIfFocusOn:!1,stopPropagation:!1,preventDefault:!1,captureEvents:!1,reactToKeydown:!0,reactToPaste:!1,singleScanQty:1};return t=this._mergeOptions(n,t),e.scannerDetectionData={options:t,vars:{firstCharTime:0,lastCharTime:0,accumulatedString:"",testTimer:!1,longPressTimeStart:0,longPressed:!1}},!0===t.reactToPaste&&e.addEventListener("paste",this._handlePaste,t.captureEvents),!1!==t.scanButtonKeyCode&&e.addEventListener("keyup",this._handleKeyUp,t.captureEvents),!0!==t.reactToKeydown&&!1===t.scanButtonKeyCode||e.addEventListener("keydown",this._handleKeyDown,t.captureEvents),this},detachFrom:function(e){e.scannerDetectionData.options.reactToPaste&&e.removeEventListener("paste",this._handlePaste),!1!==e.scannerDetectionData.options.scanButtonKeyCode&&e.removeEventListener("keyup",this._handleKeyUp),e.removeEventListener("keydown",this._handleKeyDown),e.scannerDetectionData=void 0},getOptions:function(e){return e.scannerDetectionData.options},setOptions:function(e,t){switch(e.scannerDetectionData.options.reactToPaste){case!0:!1===t.reactToPaste&&e.removeEventListener("paste",this._handlePaste);break;case!1:!0===t.reactToPaste&&e.addEventListener("paste",this._handlePaste)}switch(e.scannerDetectionData.options.scanButtonKeyCode){case!1:!1!==t.scanButtonKeyCode&&e.addEventListener("keyup",this._handleKeyUp);break;default:!1===t.scanButtonKeyCode&&e.removeEventListener("keyup",this._handleKeyUp)}return e.scannerDetectionData.options=this._mergeOptions(e.scannerDetectionData.options,t),this._reinitialize(e),this},decodeKeyEvent:function(e){var t=this._getNormalizedKeyNum(e);switch(!0){case 48<=t&&t<=90:case 106<=t&&t<=111:if(void 0!==e.key&&""!==e.key)return e.key;var n=String.fromCharCode(t);switch(e.shiftKey){case!1:n=n.toLowerCase();break;case!0:n=n.toUpperCase()}return n;case 96<=t&&t<=105:return t-96}return""},simulate:function(e,t){return this._reinitialize(e),Array.isArray(t)?t.forEach(function(e){var t={};"object"!=typeof e&&"function"!=typeof e||null===e?t.keyCode=parseInt(e):t=e;var n=new KeyboardEvent("keydown",t);document.dispatchEvent(n)}):this._validateScanCode(e,t),this},_reinitialize:function(e){var t=e.scannerDetectionData.vars;t.firstCharTime=0,t.lastCharTime=0,t.accumulatedString=""},_isFocusOnIgnoredElement:function(e){var t=e.scannerDetectionData.options.ignoreIfFocusOn;if(!t)return!1;var n=document.activeElement;if(Array.isArray(t)){for(var a=0;a<t.length;a++)if(!0===n.matches(t[a]))return!0}else if(n.matches(t))return!0;return!1},_validateScanCode:function(e,t){var n,a=e.scannerDetectionData,i=a.options,o=a.options.singleScanQty,r=a.vars.firstCharTime,s=a.vars.lastCharTime,c={};switch(!0){case t.length<i.minLength:c={message:"Receieved code is shorter then minimal length"};break;case s-r>t.length*i.avgTimeByChar:c={message:"Receieved code was not entered in time"};break;default:return i.onScan.call(e,t,o),n=new CustomEvent("scan",{detail:{scanCode:t,qty:o}}),e.dispatchEvent(n),d._reinitialize(e),!0}return c.scanCode=t,c.scanDuration=s-r,c.avgTimeByChar=i.avgTimeByChar,c.minLength=i.minLength,i.onScanError.call(e,c),n=new CustomEvent("scanError",{detail:c}),e.dispatchEvent(n),d._reinitialize(e),!1},_mergeOptions:function(e,t){var n,a={};for(n in e)Object.prototype.hasOwnProperty.call(e,n)&&(a[n]=e[n]);for(n in t)Object.prototype.hasOwnProperty.call(t,n)&&(a[n]=t[n]);return a},_getNormalizedKeyNum:function(e){return e.which||e.keyCode},_handleKeyDown:function(e){var t=d._getNormalizedKeyNum(e),n=this.scannerDetectionData.options,a=this.scannerDetectionData.vars,i=!1;if(!1!==n.onKeyDetect.call(this,t,e)&&!d._isFocusOnIgnoredElement(this))if(!1===n.scanButtonKeyCode||t!=n.scanButtonKeyCode){switch(!0){case a.firstCharTime&&-1!==n.suffixKeyCodes.indexOf(t):e.preventDefault(),e.stopImmediatePropagation(),i=!0;break;case!a.firstCharTime&&-1!==n.prefixKeyCodes.indexOf(t):e.preventDefault(),e.stopImmediatePropagation(),i=!1;break;default:var o=n.keyCodeMapper.call(this,e);if(null===o)return;a.accumulatedString+=o,n.preventDefault&&e.preventDefault(),n.stopPropagation&&e.stopImmediatePropagation(),i=!1}a.firstCharTime||(a.firstCharTime=Date.now()),a.lastCharTime=Date.now(),a.testTimer&&clearTimeout(a.testTimer),i?(d._validateScanCode(this,a.accumulatedString),a.testTimer=!1):a.testTimer=setTimeout(d._validateScanCode,n.timeBeforeScanTest,this,a.accumulatedString),n.onKeyProcess.call(this,o,e)}else a.longPressed||(a.longPressTimer=setTimeout(n.onScanButtonLongPress,n.scanButtonLongPressTime,this),a.longPressed=!0)},_handlePaste:function(e){if(!d._isFocusOnIgnoredElement(this)){e.preventDefault(),oOptions.stopPropagation&&e.stopImmediatePropagation();var t=(event.clipboardData||window.clipboardData).getData("text");this.scannerDetectionData.options.onPaste.call(this,t,event);var n=this.scannerDetectionData.vars;n.firstCharTime=0,n.lastCharTime=0,d._validateScanCode(this,t)}},_handleKeyUp:function(e){d._isFocusOnIgnoredElement(this)||d._getNormalizedKeyNum(e)==this.scannerDetectionData.options.scanButtonKeyCode&&(clearTimeout(this.scannerDetectionData.vars.longPressTimer),this.scannerDetectionData.vars.longPressed=!1)},isScanInProgressFor:function(e){return 0<e.scannerDetectionData.vars.firstCharTime}};return d});
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js
index 9d44a9f..e3405e0 100644
--- a/erpnext/selling/page/point_of_sale/point_of_sale.js
+++ b/erpnext/selling/page/point_of_sale/point_of_sale.js
@@ -1,7 +1,4 @@
-/* global Clusterize */
frappe.provide('erpnext.PointOfSale');
-{% include "erpnext/selling/page/point_of_sale/pos_controller.js" %}
-frappe.provide('erpnext.queries');
frappe.pages['point-of-sale'].on_page_load = function(wrapper) {
frappe.ui.make_app_page({
@@ -10,8 +7,10 @@
single_column: true
});
- wrapper.pos = new erpnext.PointOfSale.Controller(wrapper);
- window.cur_pos = wrapper.pos;
+ frappe.require('assets/js/point-of-sale.min.js', function() {
+ wrapper.pos = new erpnext.PointOfSale.Controller(wrapper);
+ window.cur_pos = wrapper.pos;
+ });
};
frappe.pages['point-of-sale'].refresh = function(wrapper) {
@@ -20,4 +19,4 @@
wrapper.pos.wrapper.html("");
wrapper.pos.check_opening_entry();
}
-}
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_controller.js b/erpnext/selling/page/point_of_sale/pos_controller.js
index ad1633e..338a3cc 100644
--- a/erpnext/selling/page/point_of_sale/pos_controller.js
+++ b/erpnext/selling/page/point_of_sale/pos_controller.js
@@ -1,23 +1,9 @@
-{% include "erpnext/selling/page/point_of_sale/onscan.js" %}
-{% include "erpnext/selling/page/point_of_sale/pos_item_selector.js" %}
-{% include "erpnext/selling/page/point_of_sale/pos_item_cart.js" %}
-{% include "erpnext/selling/page/point_of_sale/pos_item_details.js" %}
-{% include "erpnext/selling/page/point_of_sale/pos_payment.js" %}
-{% include "erpnext/selling/page/point_of_sale/pos_number_pad.js" %}
-{% include "erpnext/selling/page/point_of_sale/pos_past_order_list.js" %}
-{% include "erpnext/selling/page/point_of_sale/pos_past_order_summary.js" %}
-
erpnext.PointOfSale.Controller = class {
constructor(wrapper) {
this.wrapper = $(wrapper).find('.layout-main-section');
this.page = wrapper.page;
- this.load_assets();
- }
-
- load_assets() {
- // after loading assets first check if opening entry has been made
- frappe.require(['assets/erpnext/css/pos.css'], this.check_opening_entry.bind(this));
+ this.check_opening_entry();
}
fetch_opening_entry() {
@@ -36,6 +22,7 @@
}
create_opening_voucher() {
+ const me = this;
const table_fields = [
{
fieldname: "mode_of_payment", fieldtype: "Link",
@@ -45,7 +32,7 @@
{
fieldname: "opening_amount", fieldtype: "Currency",
in_list_view: 1, label: "Opening Amount",
- options: "company:company_currency",
+ options: "company:company_currency",
change: function () {
dialog.fields_dict.balance_details.df.data.some(d => {
if (d.idx == this.doc.idx) {
@@ -69,6 +56,10 @@
dialog.fields_dict.balance_details.grid.refresh();
});
}
+ const pos_profile_query = {
+ query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query',
+ filters: { company: frappe.defaults.get_default('company') }
+ }
const dialog = new frappe.ui.Dialog({
title: __('Create POS Opening Entry'),
static: true,
@@ -80,6 +71,7 @@
{
fieldtype: 'Link', label: __('POS Profile'),
options: 'POS Profile', fieldname: 'pos_profile', reqd: 1,
+ get_query: () => pos_profile_query,
onchange: () => fetch_pos_payment_methods()
},
{
@@ -93,7 +85,7 @@
fields: table_fields
}
],
- primary_action: async ({ company, pos_profile, balance_details }) => {
+ primary_action: async function({ company, pos_profile, balance_details }) {
if (!balance_details.length) {
frappe.show_alert({
message: __("Please add Mode of payments and opening balance details."),
@@ -103,7 +95,7 @@
}
const method = "erpnext.selling.page.point_of_sale.point_of_sale.create_opening_voucher";
const res = await frappe.call({ method, args: { pos_profile, company, balance_details }, freeze:true });
- !res.exc && this.prepare_app_defaults(res.message);
+ !res.exc && me.prepare_app_defaults(res.message);
dialog.hide();
},
primary_action_label: __('Submit')
@@ -111,24 +103,23 @@
dialog.show();
}
- prepare_app_defaults(data) {
+ async prepare_app_defaults(data) {
this.pos_opening = data.name;
this.company = data.company;
this.pos_profile = data.pos_profile;
this.pos_opening_time = data.period_start_date;
+ this.item_stock_map = {};
+ this.settings = {};
frappe.db.get_value('Stock Settings', undefined, 'allow_negative_stock').then(({ message }) => {
this.allow_negative_stock = flt(message.allow_negative_stock) || false;
});
frappe.db.get_doc("POS Profile", this.pos_profile).then((profile) => {
- this.customer_groups = profile.customer_groups.map(group => group.customer_group);
- this.cart.make_customer_selector();
+ Object.assign(this.settings, profile);
+ this.settings.customer_groups = profile.customer_groups.map(group => group.customer_group);
+ this.make_app();
});
-
- this.item_stock_map = {};
-
- this.make_app();
}
set_opening_entry_status() {
@@ -141,26 +132,18 @@
}
make_app() {
- return frappe.run_serially([
- () => frappe.dom.freeze(),
- () => {
- this.set_opening_entry_status();
- this.prepare_dom();
- this.prepare_components();
- this.prepare_menu();
- },
- () => this.make_new_invoice(),
- () => frappe.dom.unfreeze(),
- () => this.page.set_title(__('Point of Sale')),
- ]);
+ this.prepare_dom();
+ this.prepare_components();
+ this.prepare_menu();
+ this.make_new_invoice();
}
prepare_dom() {
this.wrapper.append(
- `<div class="app grid grid-cols-10 pt-8 gap-6"></div>`
+ `<div class="point-of-sale-app"></div>`
);
- this.$components_wrapper = this.wrapper.find('.app');
+ this.$components_wrapper = this.wrapper.find('.point-of-sale-app');
}
prepare_components() {
@@ -190,7 +173,7 @@
}
toggle_recent_order() {
- const show = this.recent_order_list.$component.hasClass('d-none');
+ const show = this.recent_order_list.$component.is(':hidden');
this.toggle_recent_order_list(show);
}
@@ -199,7 +182,7 @@
if (this.frm.doc.items.length == 0) {
frappe.show_alert({
- message:__("You must add atleast one item to save it as draft."),
+ message: __("You must add atleast one item to save it as draft."),
indicator:'red'
});
frappe.utils.play_sound("error");
@@ -208,8 +191,8 @@
this.frm.save(undefined, undefined, undefined, () => {
frappe.show_alert({
- message:__("There was an error saving the document."),
- indicator:'red'
+ message: __("There was an error saving the document."),
+ indicator: 'red'
});
frappe.utils.play_sound("error");
}).then(() => {
@@ -218,7 +201,7 @@
() => this.make_new_invoice(),
() => frappe.dom.unfreeze(),
]);
- })
+ });
}
close_pos() {
@@ -238,12 +221,11 @@
this.item_selector = new erpnext.PointOfSale.ItemSelector({
wrapper: this.$components_wrapper,
pos_profile: this.pos_profile,
+ settings: this.settings,
events: {
item_selected: args => this.on_cart_update(args),
- get_frm: () => this.frm || {},
-
- get_allowed_item_group: () => this.item_groups
+ get_frm: () => this.frm || {}
}
})
}
@@ -251,15 +233,14 @@
init_item_cart() {
this.cart = new erpnext.PointOfSale.ItemCart({
wrapper: this.$components_wrapper,
+ settings: this.settings,
events: {
get_frm: () => this.frm,
cart_item_clicked: (item_code, batch_no, uom) => {
- const item_row = this.frm.doc.items.find(
- i => i.item_code === item_code
- && i.uom === uom
- && (!batch_no || (batch_no && i.batch_no === batch_no))
- );
+ const search_field = batch_no ? 'batch_no' : 'item_code';
+ const search_value = batch_no || item_code;
+ const item_row = this.frm.doc.items.find(i => i[search_field] === search_value && i.uom === uom);
this.item_details.toggle_item_details_section(item_row);
},
@@ -273,9 +254,7 @@
this.customer_details = details;
// will add/remove LP payment method
this.payment.render_loyalty_points_payment_mode();
- },
-
- get_allowed_customer_group: () => this.customer_groups
+ }
}
})
}
@@ -283,6 +262,7 @@
init_item_details() {
this.item_details = new erpnext.PointOfSale.ItemDetails({
wrapper: this.$components_wrapper,
+ settings: this.settings,
events: {
get_frm: () => this.frm,
@@ -356,10 +336,10 @@
toggle_other_sections: (show) => {
if (show) {
- this.item_details.$component.hasClass('d-none') ? '' : this.item_details.$component.addClass('d-none');
- this.item_selector.$component.addClass('d-none');
+ this.item_details.$component.is(':visible') ? this.item_details.$component.css('display', 'none') : '';
+ this.item_selector.$component.css('display', 'none');
} else {
- this.item_selector.$component.removeClass('d-none');
+ this.item_selector.$component.css('display', 'flex');
}
},
@@ -388,7 +368,7 @@
this.order_summary.load_summary_of(doc);
});
},
- reset_summary: () => this.order_summary.show_summary_placeholder()
+ reset_summary: () => this.order_summary.toggle_summary_placeholder(true)
}
})
}
@@ -417,6 +397,11 @@
() => this.item_selector.toggle_component(true)
]);
},
+ delete_order: (name) => {
+ frappe.model.delete_doc(this.frm.doc.doctype, name, () => {
+ this.recent_order_list.refresh_list();
+ });
+ },
new_order: () => {
frappe.run_serially([
() => frappe.dom.freeze(),
@@ -429,8 +414,6 @@
})
}
-
-
toggle_recent_order_list(show) {
this.toggle_components(!show);
this.recent_order_list.toggle_component(show);
@@ -447,10 +430,12 @@
make_new_invoice() {
return frappe.run_serially([
+ () => frappe.dom.freeze(),
() => this.make_sales_invoice_frm(),
() => this.set_pos_profile_data(),
() => this.set_pos_profile_status(),
() => this.cart.load_invoice(),
+ () => frappe.dom.unfreeze()
]);
}
@@ -507,16 +492,6 @@
return this.frm.trigger("set_pos_data");
}
- raise_exception_for_pos_profile() {
- setTimeout(() => frappe.set_route('List', 'POS Profile'), 2000);
- frappe.throw(__("POS Profile is required to use Point-of-Sale"));
- }
-
- set_invoice_status() {
- const [status, indicator] = frappe.listview_settings["POS Invoice"].get_indicator(this.frm.doc);
- this.page.set_indicator(status, indicator);
- }
-
set_pos_profile_status() {
this.page.set_indicator(this.pos_profile, "blue");
}
@@ -539,7 +514,7 @@
const qty_needed = field === 'qty' ? value * item_row.conversion_factor : item_row.qty * value;
await this.check_stock_availability(item_row, qty_needed, this.frm.doc.set_warehouse);
}
-
+
if (this.is_current_item_being_edited(item_row) || item_selected_from_selector) {
await frappe.model.set_value(item_row.doctype, item_row.name, field, value);
this.update_cart_html(item_row);
@@ -577,7 +552,7 @@
this.check_serial_batch_selection_needed(item_row) && this.edit_item_details_of(item_row);
this.update_cart_html(item_row);
- }
+ }
} catch (error) {
console.log(error);
} finally {
@@ -588,7 +563,7 @@
get_item_from_frm(item_code, batch_no, uom) {
const has_batch_no = batch_no;
return this.frm.doc.items.find(
- i => i.item_code === item_code
+ i => i.item_code === item_code
&& (!has_batch_no || (has_batch_no && i.batch_no === batch_no))
&& (i.uom === uom)
);
@@ -617,7 +592,7 @@
const no_serial_selected = !item_row.serial_no;
const no_batch_selected = !item_row.batch_no;
- if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
+ if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
(serialized && batched && (no_batch_selected || no_serial_selected))) {
return true;
}
@@ -698,14 +673,14 @@
frappe.dom.freeze();
const { doctype, name, current_item } = this.item_details;
- frappe.model.set_value(doctype, name, 'qty', 0);
-
- this.frm.script_manager.trigger('qty', doctype, name).then(() => {
- frappe.model.clear_doc(doctype, name);
- this.update_cart_html(current_item, true);
- this.item_details.toggle_item_details_section(undefined);
- frappe.dom.unfreeze();
- })
+ frappe.model.set_value(doctype, name, 'qty', 0)
+ .then(() => {
+ frappe.model.clear_doc(doctype, name);
+ this.update_cart_html(current_item, true);
+ this.item_details.toggle_item_details_section(undefined);
+ frappe.dom.unfreeze();
+ })
+ .catch(e => console.log(e));
}
-}
+};
diff --git a/erpnext/selling/page/point_of_sale/pos_item_cart.js b/erpnext/selling/page/point_of_sale/pos_item_cart.js
index 7799dac..044e803 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_cart.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_cart.js
@@ -1,12 +1,16 @@
erpnext.PointOfSale.ItemCart = class {
- constructor({ wrapper, events }) {
+ constructor({ wrapper, events, settings }) {
this.wrapper = wrapper;
this.events = events;
this.customer_info = undefined;
-
+ this.hide_images = settings.hide_images;
+ this.allowed_customer_groups = settings.customer_groups;
+ this.allow_rate_change = settings.allow_rate_change;
+ this.allow_discount_change = settings.allow_discount_change;
+
this.init_component();
}
-
+
init_component() {
this.prepare_dom();
this.init_child_components();
@@ -16,10 +20,10 @@
prepare_dom() {
this.wrapper.append(
- `<section class="col-span-4 flex flex-col shadow rounded item-cart bg-white mx-h-70 h-100"></section>`
+ `<section class="customer-cart-container"></section>`
)
- this.$component = this.wrapper.find('.item-cart');
+ this.$component = this.wrapper.find('.customer-cart-container');
}
init_child_components() {
@@ -29,32 +33,33 @@
init_customer_selector() {
this.$component.append(
- `<div class="customer-section rounded flex flex-col m-8 mb-0"></div>`
+ `<div class="customer-section"></div>`
)
this.$customer_section = this.$component.find('.customer-section');
+ this.make_customer_selector();
}
-
+
reset_customer_selector() {
const frm = this.events.get_frm();
frm.set_value('customer', '');
- this.$customer_section.removeClass('border pr-4 pl-4');
this.make_customer_selector();
this.customer_field.set_focus();
}
-
+
init_cart_components() {
this.$component.append(
- `<div class="cart-container flex flex-col items-center rounded flex-1 relative">
- <div class="absolute flex flex-col p-8 pt-0 w-full h-full">
- <div class="flex text-grey cart-header pt-2 pb-2 p-4 mt-2 mb-2 w-full f-shrink-0">
- <div class="flex-1">Item</div>
- <div class="mr-4">Qty</div>
- <div class="rate-list-header mr-1 text-right">Amount</div>
+ `<div class="cart-container">
+ <div class="abs-cart-container">
+ <div class="cart-label">Item Cart</div>
+ <div class="cart-header">
+ <div class="name-header">Item</div>
+ <div class="qty-header">Qty</div>
+ <div class="rate-amount-header">Amount</div>
</div>
- <div class="cart-items-section flex flex-col flex-1 scroll-y rounded w-full"></div>
- <div class="cart-totals-section flex flex-col w-full mt-4 f-shrink-0"></div>
- <div class="numpad-section flex flex-col mt-4 d-none w-full p-8 pt-0 pb-0 f-shrink-0"></div>
- </div>
+ <div class="cart-items-section"></div>
+ <div class="cart-totals-section"></div>
+ <div class="numpad-section"></div>
+ </div>
</div>`
);
this.$cart_container = this.$component.find('.cart-container');
@@ -70,54 +75,48 @@
this.make_no_items_placeholder();
}
-
+
make_no_items_placeholder() {
- this.$cart_header.addClass('d-none');
+ this.$cart_header.css('display', 'none');
this.$cart_items_wrapper.html(
- `<div class="no-item-wrapper flex items-center h-18">
- <div class="flex-1 text-center text-grey">No items in cart</div>
- </div>`
- )
- this.$cart_items_wrapper.addClass('mt-4 border-grey border-dashed');
+ `<div class="no-item-wrapper">No items in cart</div>`
+ );
+ }
+
+ get_discount_icon() {
+ return (
+ `<svg class="discount-icon" width="24" height="24" viewBox="0 0 24 24" stroke="currentColor" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M19 15.6213C19 15.2235 19.158 14.842 19.4393 14.5607L20.9393 13.0607C21.5251 12.4749 21.5251 11.5251 20.9393 10.9393L19.4393 9.43934C19.158 9.15804 19 8.7765 19 8.37868V6.5C19 5.67157 18.3284 5 17.5 5H15.6213C15.2235 5 14.842 4.84196 14.5607 4.56066L13.0607 3.06066C12.4749 2.47487 11.5251 2.47487 10.9393 3.06066L9.43934 4.56066C9.15804 4.84196 8.7765 5 8.37868 5H6.5C5.67157 5 5 5.67157 5 6.5V8.37868C5 8.7765 4.84196 9.15804 4.56066 9.43934L3.06066 10.9393C2.47487 11.5251 2.47487 12.4749 3.06066 13.0607L4.56066 14.5607C4.84196 14.842 5 15.2235 5 15.6213V17.5C5 18.3284 5.67157 19 6.5 19H8.37868C8.7765 19 9.15804 19.158 9.43934 19.4393L10.9393 20.9393C11.5251 21.5251 12.4749 21.5251 13.0607 20.9393L14.5607 19.4393C14.842 19.158 15.2235 19 15.6213 19H17.5C18.3284 19 19 18.3284 19 17.5V15.6213Z" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M15 9L9 15" stroke-miterlimit="10" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M10.5 9.5C10.5 10.0523 10.0523 10.5 9.5 10.5C8.94772 10.5 8.5 10.0523 8.5 9.5C8.5 8.94772 8.94772 8.5 9.5 8.5C10.0523 8.5 10.5 8.94772 10.5 9.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
+ <path d="M15.5 14.5C15.5 15.0523 15.0523 15.5 14.5 15.5C13.9477 15.5 13.5 15.0523 13.5 14.5C13.5 13.9477 13.9477 13.5 14.5 13.5C15.0523 13.5 15.5 13.9477 15.5 14.5Z" fill="white" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>`
+ );
}
make_cart_totals_section() {
this.$totals_section = this.$component.find('.cart-totals-section');
this.$totals_section.append(
- `<div class="add-discount flex items-center pt-4 pb-4 pr-4 pl-4 text-grey pointer no-select d-none">
- + Add Discount
+ `<div class="add-discount-wrapper">
+ ${this.get_discount_icon()} Add Discount
</div>
- <div class="border border-grey rounded">
- <div class="net-total flex justify-between items-center h-16 pr-8 pl-8 border-b-grey">
- <div class="flex flex-col">
- <div class="text-md text-dark-grey text-bold">Net Total</div>
- </div>
- <div class="flex flex-col text-right">
- <div class="text-md text-dark-grey text-bold">0.00</div>
- </div>
- </div>
- <div class="taxes"></div>
- <div class="grand-total flex justify-between items-center h-16 pr-8 pl-8 border-b-grey">
- <div class="flex flex-col">
- <div class="text-md text-dark-grey text-bold">Grand Total</div>
- </div>
- <div class="flex flex-col text-right">
- <div class="text-md text-dark-grey text-bold">0.00</div>
- </div>
- </div>
- <div class="checkout-btn flex items-center justify-center h-16 pr-8 pl-8 text-center text-grey no-select pointer rounded-b text-md text-bold">
- Checkout
- </div>
- <div class="edit-cart-btn flex items-center justify-center h-16 pr-8 pl-8 text-center text-grey no-select pointer d-none text-md text-bold">
- Edit Cart
- </div>
- </div>`
+ <div class="net-total-container">
+ <div class="net-total-label">Net Total</div>
+ <div class="net-total-value">0.00</div>
+ </div>
+ <div class="taxes-container"></div>
+ <div class="grand-total-container">
+ <div>Grand Total</div>
+ <div>0.00</div>
+ </div>
+ <div class="checkout-btn">Checkout</div>
+ <div class="edit-cart-btn">Edit Cart</div>`
)
- this.$add_discount_elem = this.$component.find(".add-discount");
+ this.$add_discount_elem = this.$component.find(".add-discount-wrapper");
}
-
+
make_cart_numpad() {
this.$numpad_section = this.$component.find('.numpad-section');
@@ -137,39 +136,37 @@
[ '', '', '', 'col-span-2' ],
[ '', '', '', 'col-span-2' ],
[ '', '', '', 'col-span-2' ],
- [ '', '', '', 'col-span-2 text-bold text-danger' ]
+ [ '', '', '', 'col-span-2 remove-btn' ]
],
fieldnames_map: { 'Quantity': 'qty', 'Discount': 'discount_percentage' }
})
this.$numpad_section.prepend(
- `<div class="flex mb-2 justify-between">
+ `<div class="numpad-totals">
<span class="numpad-net-total"></span>
<span class="numpad-grand-total"></span>
</div>`
)
this.$numpad_section.append(
- `<div class="numpad-btn checkout-btn flex items-center justify-center h-16 pr-8 pl-8 bg-primary
- text-center text-white no-select pointer rounded text-md text-bold mt-4" data-button-value="checkout">
- Checkout
- </div>`
+ `<div class="numpad-btn checkout-btn" data-button-value="checkout">Checkout</div>`
)
}
-
+
bind_events() {
const me = this;
- this.$customer_section.on('click', '.add-remove-customer', function (e) {
- const customer_info_is_visible = me.$cart_container.hasClass('d-none');
- customer_info_is_visible ?
- me.toggle_customer_info(false) : me.reset_customer_selector();
+ this.$customer_section.on('click', '.reset-customer-btn', function () {
+ me.reset_customer_selector();
});
- this.$customer_section.on('click', '.customer-header', function(e) {
- // don't triggger the event if .add-remove-customer btn is clicked which is under .customer-header
- if ($(e.target).closest('.add-remove-customer').length) return;
+ this.$customer_section.on('click', '.close-details-btn', function () {
+ me.toggle_customer_info(false);
+ });
- const show = !me.$cart_container.hasClass('d-none');
+ this.$customer_section.on('click', '.customer-display', function(e) {
+ if ($(e.target).closest('.reset-customer-btn').length) return;
+
+ const show = me.$cart_container.is(':visible');
me.toggle_customer_info(show);
});
@@ -178,7 +175,7 @@
me.toggle_item_highlight(this);
- const payment_section_hidden = me.$totals_section.find('.edit-cart-btn').hasClass('d-none');
+ const payment_section_hidden = !me.$totals_section.find('.edit-cart-btn').is(':visible');
if (!payment_section_hidden) {
// payment section is visible
// edit cart first and then open item details section
@@ -193,23 +190,21 @@
});
this.$component.on('click', '.checkout-btn', function() {
- if (!$(this).hasClass('bg-primary')) return;
-
+ if ($(this).attr('style').indexOf('--blue-500') == -1) return;
+
me.events.checkout();
me.toggle_checkout_btn(false);
- me.$add_discount_elem.removeClass("d-none");
+ me.allow_discount_change && me.$add_discount_elem.removeClass("d-none");
});
this.$totals_section.on('click', '.edit-cart-btn', () => {
this.events.edit_cart();
this.toggle_checkout_btn(true);
-
- this.$add_discount_elem.addClass("d-none");
});
- this.$component.on('click', '.add-discount', () => {
- const can_edit_discount = this.$add_discount_elem.find('.edit-discount').length;
+ this.$component.on('click', '.add-discount-wrapper', () => {
+ const can_edit_discount = this.$add_discount_elem.find('.edit-discount-btn').length;
if(!this.discount_field || can_edit_discount) this.show_discount_control();
});
@@ -231,7 +226,7 @@
if (btn === '.') shortcut_key = 'ctrl+>';
// to account for fieldname map
- const fieldname = this.number_pad.fieldnames[btn] ? this.number_pad.fieldnames[btn] :
+ const fieldname = this.number_pad.fieldnames[btn] ? this.number_pad.fieldnames[btn] :
typeof btn === 'string' ? frappe.scrub(btn) : btn;
let shortcut_label = shortcut_key.split('+').map(frappe.utils.to_title_case).join('+');
@@ -242,7 +237,7 @@
const cart_is_visible = this.$component.is(":visible");
if (cart_is_visible && this.item_is_selected && this.$numpad_section.is(":visible")) {
this.$numpad_section.find(`.numpad-btn[data-button-value="${fieldname}"]`).click();
- }
+ }
})
}
}
@@ -251,7 +246,7 @@
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+enter",
action: () => this.$component.find(".checkout-btn").click(),
- condition: () => this.$component.is(":visible") && this.$totals_section.find('.edit-cart-btn').hasClass('d-none'),
+ condition: () => this.$component.is(":visible") && !this.$totals_section.find('.edit-cart-btn').is(':visible'),
description: __("Checkout Order / Submit Order / New Order"),
ignore_inputs: true,
page: cur_page.page.page
@@ -259,14 +254,15 @@
this.$component.find(".edit-cart-btn").attr("title", `${ctrl_label}+E`);
frappe.ui.keys.on("ctrl+e", () => {
const item_cart_visible = this.$component.is(":visible");
- if (item_cart_visible && this.$totals_section.find('.checkout-btn').hasClass('d-none')) {
- this.$component.find(".edit-cart-btn").click()
+ const checkout_btn_invisible = !this.$totals_section.find('.checkout-btn').is('visible');
+ if (item_cart_visible && checkout_btn_invisible) {
+ this.$component.find(".edit-cart-btn").click();
}
});
- this.$component.find(".add-discount").attr("title", `${ctrl_label}+D`);
+ this.$component.find(".add-discount-wrapper").attr("title", `${ctrl_label}+D`);
frappe.ui.keys.add_shortcut({
shortcut: "ctrl+d",
- action: () => this.$component.find(".add-discount").click(),
+ action: () => this.$component.find(".add-discount-wrapper").click(),
condition: () => this.$add_discount_elem.is(":visible"),
description: __("Add Order Discount"),
ignore_inputs: true,
@@ -279,30 +275,28 @@
}
});
}
-
+
toggle_item_highlight(item) {
const $cart_item = $(item);
- const item_is_highlighted = $cart_item.hasClass("shadow");
+ const item_is_highlighted = $cart_item.attr("style") == "background-color:var(--gray-50);";
if (!item || item_is_highlighted) {
this.item_is_selected = false;
- this.$cart_container.find('.cart-item-wrapper').removeClass("shadow").css("opacity", "1");
+ this.$cart_container.find('.cart-item-wrapper').css("background-color", "");
} else {
- $cart_item.addClass("shadow");
+ $cart_item.css("background-color", "var(--gray-50)");
this.item_is_selected = true;
- this.$cart_container.find('.cart-item-wrapper').css("opacity", "1");
- this.$cart_container.find('.cart-item-wrapper').not(item).removeClass("shadow").css("opacity", "0.65");
+ this.$cart_container.find('.cart-item-wrapper').not(item).css("background-color", "");
}
- // highlight with inner shadow
- // $cart_item.addClass("shadow-inner bg-selected");
- // me.$cart_container.find('.cart-item-wrapper').not(this).removeClass("shadow-inner bg-selected");
}
make_customer_selector() {
- this.$customer_section.html(`<div class="customer-search-field flex flex-1 items-center"></div>`);
+ this.$customer_section.html(`
+ <div class="customer-field"></div>
+ `);
const me = this;
const query = { query: 'erpnext.controllers.queries.customer_query' };
- const allowed_customer_group = this.events.get_allowed_customer_group() || [];
+ const allowed_customer_group = this.allowed_customer_groups || [];
if (allowed_customer_group.length) {
query.filters = {
customer_group: ['in', allowed_customer_group]
@@ -332,12 +326,12 @@
}
},
},
- parent: this.$customer_section.find('.customer-search-field'),
+ parent: this.$customer_section.find('.customer-field'),
render_input: true,
});
this.customer_field.toggle_label(false);
}
-
+
fetch_customer_details(customer) {
if (customer) {
return new Promise((resolve) => {
@@ -371,9 +365,9 @@
}
show_discount_control() {
- this.$add_discount_elem.removeClass("pr-4 pl-4");
+ this.$add_discount_elem.css({ 'padding': '0px', 'border': 'none' });
this.$add_discount_elem.html(
- `<div class="add-discount-field flex flex-1 items-center"></div>`
+ `<div class="add-discount-field"></div>`
);
const me = this;
@@ -382,14 +376,19 @@
label: __('Discount'),
fieldtype: 'Data',
placeholder: __('Enter discount percentage.'),
+ input_class: 'input-xs',
onchange: function() {
const frm = me.events.get_frm();
- if (this.value.length || this.value === 0) {
+ if (flt(this.value) != 0) {
frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', flt(this.value));
me.hide_discount_control(this.value);
} else {
frappe.model.set_value(frm.doc.doctype, frm.doc.name, 'additional_discount_percentage', 0);
- me.$add_discount_elem.html(`+ Add Discount`);
+ me.$add_discount_elem.css({
+ 'border': '1px dashed var(--gray-500)',
+ 'padding': 'var(--padding-sm) var(--padding-md)'
+ });
+ me.$add_discount_elem.html(`${me.get_discount_icon()} Add Discount`);
me.discount_field = undefined;
}
},
@@ -403,38 +402,37 @@
hide_discount_control(discount) {
if (!discount) {
- this.$add_discount_elem.removeClass("pr-4 pl-4");
+ this.$add_discount_elem.css({ 'padding': '0px', 'border': 'none' });
this.$add_discount_elem.html(
- `<div class="add-discount-field flex flex-1 items-center"></div>`
+ `<div class="add-discount-field"></div>`
);
} else {
- this.$add_discount_elem.addClass('pr-4 pl-4');
+ this.$add_discount_elem.css({
+ 'border': '1px dashed var(--dark-green-500)',
+ 'padding': 'var(--padding-sm) var(--padding-md)'
+ });
this.$add_discount_elem.html(
- `<svg class="mr-2" width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1"
- stroke-linecap="round" stroke-linejoin="round">
- <path d="M12 20h9"/><path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"/>
- </svg>
- <div class="edit-discount p-1 pr-3 pl-3 text-dark-grey rounded w-fit bg-green-200 mb-2">
- ${String(discount).bold()}% off
- </div>
- `
+ `<div class="edit-discount-btn">
+ ${this.get_discount_icon()} Additional ${String(discount).bold()}% discount applied
+ </div>`
);
}
}
-
+
update_customer_section() {
+ const me = this;
const { customer, email_id='', mobile_no='', image } = this.customer_info || {};
if (customer) {
- this.$customer_section.addClass('border pr-4 pl-4').html(
- `<div class="customer-details flex flex-col">
- <div class="customer-header flex items-center rounded h-18 pointer">
- ${get_customer_image()}
- <div class="customer-name flex flex-col flex-1 f-shrink-1 overflow-hidden whitespace-nowrap">
- <div class="text-md text-dark-grey text-bold">${customer}</div>
+ this.$customer_section.html(
+ `<div class="customer-details">
+ <div class="customer-display">
+ ${this.get_customer_image()}
+ <div class="customer-name-desc">
+ <div class="customer-name">${customer}</div>
${get_customer_description()}
</div>
- <div class="f-shrink-0 add-remove-customer flex items-center pointer" data-customer="${escape(customer)}">
+ <div class="reset-customer-btn" data-customer="${escape(customer)}">
<svg width="32" height="32" viewBox="0 0 14 14" fill="none">
<path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
</svg>
@@ -449,158 +447,143 @@
function get_customer_description() {
if (!email_id && !mobile_no) {
- return `<div class="text-grey-200 italic">Click to add email / phone</div>`
+ return `<div class="customer-desc">Click to add email / phone</div>`;
} else if (email_id && !mobile_no) {
- return `<div class="text-grey">${email_id}</div>`
+ return `<div class="customer-desc">${email_id}</div>`;
} else if (mobile_no && !email_id) {
- return `<div class="text-grey">${mobile_no}</div>`
+ return `<div class="customer-desc">${mobile_no}</div>`;
} else {
- return `<div class="text-grey">${email_id} | ${mobile_no}</div>`
+ return `<div class="customer-desc">${email_id} - ${mobile_no}</div>`;
}
}
- function get_customer_image() {
- if (image) {
- return `<div class="icon flex items-center justify-center w-12 h-12 rounded bg-light-grey mr-4 text-grey-200">
- <img class="h-full" src="${image}" alt="${image}" style="object-fit: cover;">
- </div>`
- } else {
- return `<div class="icon flex items-center justify-center w-12 h-12 rounded bg-light-grey mr-4 text-grey-200 text-md">
- ${frappe.get_abbr(customer)}
- </div>`
- }
+ }
+
+ get_customer_image() {
+ const { customer, image } = this.customer_info || {};
+ if (image) {
+ return `<div class="customer-image"><img src="${image}" alt="${image}""></div>`;
+ } else {
+ return `<div class="customer-image customer-abbr">${frappe.get_abbr(customer)}</div>`;
}
}
-
+
update_totals_section(frm) {
if (!frm) frm = this.events.get_frm();
- this.render_net_total(frm.doc.base_net_total);
- this.render_grand_total(frm.doc.base_grand_total);
+ this.render_net_total(frm.doc.net_total);
+ this.render_grand_total(frm.doc.grand_total);
- const taxes = frm.doc.taxes.map(t => { return { description: t.description, rate: t.rate }})
- this.render_taxes(frm.doc.base_total_taxes_and_charges, taxes);
+ const taxes = frm.doc.taxes.map(t => {
+ return {
+ description: t.description, rate: t.rate
+ };
+ });
+ this.render_taxes(frm.doc.total_taxes_and_charges, taxes);
}
-
+
render_net_total(value) {
const currency = this.events.get_frm().doc.currency;
- this.$totals_section.find('.net-total').html(
- `<div class="flex flex-col">
- <div class="text-md text-dark-grey text-bold">Net Total</div>
- </div>
- <div class="flex flex-col text-right">
- <div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
- </div>`
+ this.$totals_section.find('.net-total-container').html(
+ `<div>Net Total</div><div>${format_currency(value, currency)}</div>`
)
- this.$numpad_section.find('.numpad-net-total').html(`Net Total: <span class="text-bold">${format_currency(value, currency)}</span>`)
+ this.$numpad_section.find('.numpad-net-total').html(
+ `<div>Net Total: <span>${format_currency(value, currency)}</span></div>`
+ );
}
-
+
render_grand_total(value) {
const currency = this.events.get_frm().doc.currency;
- this.$totals_section.find('.grand-total').html(
- `<div class="flex flex-col">
- <div class="text-md text-dark-grey text-bold">Grand Total</div>
- </div>
- <div class="flex flex-col text-right">
- <div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
- </div>`
+ this.$totals_section.find('.grand-total-container').html(
+ `<div>Grand Total</div><div>${format_currency(value, currency)}</div>`
)
- this.$numpad_section.find('.numpad-grand-total').html(`Grand Total: <span class="text-bold">${format_currency(value, currency)}</span>`)
+ this.$numpad_section.find('.numpad-grand-total').html(
+ `<div>Grand Total: <span>${format_currency(value, currency)}</span></div>`
+ );
}
render_taxes(value, taxes) {
if (taxes.length) {
const currency = this.events.get_frm().doc.currency;
- this.$totals_section.find('.taxes').html(
- `<div class="flex items-center justify-between h-16 pr-8 pl-8 border-b-grey">
- <div class="flex overflow-hidden whitespace-nowrap">
- <div class="text-md text-dark-grey text-bold w-fit">Tax Charges</div>
- <div class="flex ml-4 text-dark-grey">
- ${
- taxes.map((t, i) => {
- let margin_left = '';
- if (i !== 0) margin_left = 'ml-2';
- const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`;
- return `<span class="border-grey p-1 pl-2 pr-2 rounded ${margin_left}">${description}</span>`
- }).join('')
- }
- </div>
- </div>
- <div class="flex flex-col text-right f-shrink-0 ml-4">
- <div class="text-md text-dark-grey text-bold">${format_currency(value, currency)}</div>
- </div>
- </div>`
- )
+ const taxes_html = taxes.map(t => {
+ const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`;
+ return `<div class="tax-row">
+ <div class="tax-label">${description}</div>
+ <div class="tax-value">${format_currency(value, currency)}</div>
+ </div>`;
+ }).join('');
+ this.$totals_section.find('.taxes-container').css('display', 'flex').html(taxes_html);
} else {
- this.$totals_section.find('.taxes').html('')
+ this.$totals_section.find('.taxes-container').css('display', 'none').html('');
}
}
get_cart_item({ item_code, batch_no, uom }) {
const batch_attr = `[data-batch-no="${escape(batch_no)}"]`;
const item_code_attr = `[data-item-code="${escape(item_code)}"]`;
- const uom_attr = `[data-uom=${escape(uom)}]`;
+ const uom_attr = `[data-uom="${escape(uom)}"]`;
- const item_selector = batch_no ?
+ const item_selector = batch_no ?
`.cart-item-wrapper${batch_attr}${uom_attr}` : `.cart-item-wrapper${item_code_attr}${uom_attr}`;
-
+
return this.$cart_items_wrapper.find(item_selector);
}
-
+
update_item_html(item, remove_item) {
const $item = this.get_cart_item(item);
if (remove_item) {
- $item && $item.remove();
+ $item && $item.next().remove() && $item.remove();
} else {
const { item_code, batch_no, uom } = item;
const search_field = batch_no ? 'batch_no' : 'item_code';
const search_value = batch_no || item_code;
const item_row = this.events.get_frm().doc.items.find(i => i[search_field] === search_value && i.uom === uom);
-
+
this.render_cart_item(item_row, $item);
}
- const no_of_cart_items = this.$cart_items_wrapper.children().length;
- no_of_cart_items > 0 && this.highlight_checkout_btn(no_of_cart_items > 0);
-
+ const no_of_cart_items = this.$cart_items_wrapper.find('.cart-item-wrapper').length;
+ this.highlight_checkout_btn(no_of_cart_items > 0);
+
this.update_empty_cart_section(no_of_cart_items);
}
-
+
render_cart_item(item_data, $item_to_update) {
const currency = this.events.get_frm().doc.currency;
const me = this;
-
+
if (!$item_to_update.length) {
this.$cart_items_wrapper.append(
- `<div class="cart-item-wrapper flex items-center h-18 pr-4 pl-4 rounded border-grey pointer no-select"
+ `<div class="cart-item-wrapper"
data-item-code="${escape(item_data.item_code)}" data-uom="${escape(item_data.uom)}"
data-batch-no="${escape(item_data.batch_no || '')}">
- </div>`
+ </div>
+ <div class="seperator"></div>`
)
$item_to_update = this.get_cart_item(item_data);
}
$item_to_update.html(
- `<div class="flex flex-col flex-1 f-shrink-1 overflow-hidden whitespace-nowrap">
- <div class="text-md text-dark-grey text-bold">
+ `${get_item_image_html()}
+ <div class="item-name-desc">
+ <div class="item-name">
${item_data.item_name}
</div>
${get_description_html()}
</div>
- ${get_rate_discount_html()}
- </div>`
+ ${get_rate_discount_html()}`
)
set_dynamic_rate_header_width();
this.scroll_to_item($item_to_update);
function set_dynamic_rate_header_width() {
- const rate_cols = Array.from(me.$cart_items_wrapper.find(".rate-col"));
- me.$cart_header.find(".rate-list-header").css("width", "");
- me.$cart_items_wrapper.find(".rate-col").css("width", "");
+ const rate_cols = Array.from(me.$cart_items_wrapper.find(".item-rate-amount"));
+ me.$cart_header.find(".rate-amount-header").css("width", "");
+ me.$cart_items_wrapper.find(".item-rate-amount").css("width", "");
let max_width = rate_cols.reduce((max_width, elm) => {
if ($(elm).width() > max_width)
max_width = $(elm).width();
@@ -610,30 +593,26 @@
max_width += 1;
if (max_width == 1) max_width = "";
- me.$cart_header.find(".rate-list-header").css("width", max_width);
- me.$cart_items_wrapper.find(".rate-col").css("width", max_width);
+ me.$cart_header.find(".rate-amount-header").css("width", max_width);
+ me.$cart_items_wrapper.find(".item-rate-amount").css("width", max_width);
}
-
+
function get_rate_discount_html() {
if (item_data.rate && item_data.amount && item_data.rate !== item_data.amount) {
return `
- <div class="flex f-shrink-0 ml-4 items-center">
- <div class="flex w-8 h-8 rounded bg-light-grey mr-4 items-center justify-center font-bold f-shrink-0">
- <span>${item_data.qty || 0}</span>
- </div>
- <div class="rate-col flex flex-col f-shrink-0 text-right">
- <div class="text-md text-dark-grey text-bold">${format_currency(item_data.amount, currency)}</div>
- <div class="text-md-0 text-dark-grey">${format_currency(item_data.rate, currency)}</div>
+ <div class="item-qty-rate">
+ <div class="item-qty"><span>${item_data.qty || 0}</span></div>
+ <div class="item-rate-amount">
+ <div class="item-rate">${format_currency(item_data.amount, currency)}</div>
+ <div class="item-amount">${format_currency(item_data.rate, currency)}</div>
</div>
</div>`
} else {
return `
- <div class="flex f-shrink-0 ml-4 text-right">
- <div class="flex w-8 h-8 rounded bg-light-grey mr-4 items-center justify-center font-bold f-shrink-0">
- <span>${item_data.qty || 0}</span>
- </div>
- <div class="rate-col flex flex-col f-shrink-0 text-right">
- <div class="text-md text-dark-grey text-bold">${format_currency(item_data.rate, currency)}</div>
+ <div class="item-qty-rate">
+ <div class="item-qty"><span>${item_data.qty || 0}</span></div>
+ <div class="item-rate-amount">
+ <div class="item-rate">${format_currency(item_data.rate, currency)}</div>
</div>
</div>`
}
@@ -649,10 +628,19 @@
}
}
item_data.description = frappe.ellipsis(item_data.description, 45);
- return `<div class="text-grey">${item_data.description}</div>`
+ return `<div class="item-desc">${item_data.description}</div>`;
}
return ``;
}
+
+ function get_item_image_html() {
+ const { image, item_name } = item_data;
+ if (image) {
+ return `<div class="item-image"><img src="${image}" alt="${image}""></div>`;
+ } else {
+ return `<div class="item-image item-abbr">${frappe.get_abbr(item_name)}</div>`;
+ }
+ }
}
scroll_to_item($item) {
@@ -660,52 +648,68 @@
const scrollTop = $item.offset().top - this.$cart_items_wrapper.offset().top + this.$cart_items_wrapper.scrollTop();
this.$cart_items_wrapper.animate({ scrollTop });
}
-
+
update_selector_value_in_cart_item(selector, value, item) {
const $item_to_update = this.get_cart_item(item);
- $item_to_update.attr(`data-${selector}`, value);
+ $item_to_update.attr(`data-${selector}`, escape(value));
}
toggle_checkout_btn(show_checkout) {
if (show_checkout) {
- this.$totals_section.find('.checkout-btn').removeClass('d-none');
- this.$totals_section.find('.edit-cart-btn').addClass('d-none');
+ this.$totals_section.find('.checkout-btn').css('display', 'flex');
+ this.$totals_section.find('.edit-cart-btn').css('display', 'none');
} else {
- this.$totals_section.find('.checkout-btn').addClass('d-none');
- this.$totals_section.find('.edit-cart-btn').removeClass('d-none');
+ this.$totals_section.find('.checkout-btn').css('display', 'none');
+ this.$totals_section.find('.edit-cart-btn').css('display', 'flex');
}
}
highlight_checkout_btn(toggle) {
- const has_primary_class = this.$totals_section.find('.checkout-btn').hasClass('bg-primary');
- if (toggle && !has_primary_class) {
- this.$totals_section.find('.checkout-btn').addClass('bg-primary text-white text-lg');
- } else if (!toggle && has_primary_class) {
- this.$totals_section.find('.checkout-btn').removeClass('bg-primary text-white text-lg');
+ if (toggle) {
+ this.$add_discount_elem.css('display', 'flex');
+ this.$cart_container.find('.checkout-btn').css({
+ 'background-color': 'var(--blue-500)'
+ });
+ } else {
+ this.$add_discount_elem.css('display', 'none');
+ this.$cart_container.find('.checkout-btn').css({
+ 'background-color': 'var(--blue-200)'
+ });
}
}
-
+
update_empty_cart_section(no_of_cart_items) {
const $no_item_element = this.$cart_items_wrapper.find('.no-item-wrapper');
// if cart has items and no item is present
- no_of_cart_items > 0 && $no_item_element && $no_item_element.remove()
- && this.$cart_items_wrapper.removeClass('mt-4 border-grey border-dashed') && this.$cart_header.removeClass('d-none');
+ no_of_cart_items > 0 && $no_item_element && $no_item_element.remove() && this.$cart_header.css('display', 'flex');
no_of_cart_items === 0 && !$no_item_element.length && this.make_no_items_placeholder();
}
-
+
on_numpad_event($btn) {
const current_action = $btn.attr('data-button-value');
const action_is_field_edit = ['qty', 'discount_percentage', 'rate'].includes(current_action);
-
- this.highlight_numpad_btn($btn, current_action);
+ const action_is_allowed = action_is_field_edit ? (
+ (current_action == 'rate' && this.allow_rate_change) ||
+ (current_action == 'discount_percentage' && this.allow_discount_change) ||
+ (current_action == 'qty')) : true;
const action_is_pressed_twice = this.prev_action === current_action;
const first_click_event = !this.prev_action;
const field_to_edit_changed = this.prev_action && this.prev_action != current_action;
if (action_is_field_edit) {
+ if (!action_is_allowed) {
+ const label = current_action == 'rate' ? 'Rate'.bold() : 'Discount'.bold();
+ const message = __('Editing {0} is not allowed as per POS Profile settings', [label]);
+ frappe.show_alert({
+ indicator: 'red',
+ message: message
+ });
+ frappe.utils.play_sound("error");
+ return;
+ }
if (first_click_event || field_to_edit_changed) {
this.prev_action = current_action;
@@ -713,7 +717,7 @@
this.prev_action = undefined;
}
this.numpad_value = '';
-
+
} else if (current_action === 'checkout') {
this.prev_action = undefined;
this.toggle_item_highlight();
@@ -739,7 +743,7 @@
frappe.utils.play_sound("error");
return;
}
-
+
if (flt(this.numpad_value) > 100 && this.prev_action === 'discount_percentage') {
frappe.show_alert({
message: __('Discount cannot be greater than 100%'),
@@ -749,40 +753,41 @@
this.numpad_value = current_action;
}
+ this.highlight_numpad_btn($btn, current_action);
this.events.numpad_event(this.numpad_value, this.prev_action);
}
-
+
highlight_numpad_btn($btn, curr_action) {
- const curr_action_is_highlighted = $btn.hasClass('shadow-inner');
+ const curr_action_is_highlighted = $btn.hasClass('highlighted-numpad-btn');
const curr_action_is_action = ['qty', 'discount_percentage', 'rate', 'done'].includes(curr_action);
if (!curr_action_is_highlighted) {
- $btn.addClass('shadow-inner bg-selected');
+ $btn.addClass('highlighted-numpad-btn');
}
if (this.prev_action === curr_action && curr_action_is_highlighted) {
// if Qty is pressed twice
- $btn.removeClass('shadow-inner bg-selected');
+ $btn.removeClass('highlighted-numpad-btn');
}
if (this.prev_action && this.prev_action !== curr_action && curr_action_is_action) {
// Order: Qty -> Rate then remove Qty highlight
const prev_btn = $(`[data-button-value='${this.prev_action}']`);
- prev_btn.removeClass('shadow-inner bg-selected');
+ prev_btn.removeClass('highlighted-numpad-btn');
}
if (!curr_action_is_action || curr_action === 'done') {
// if numbers are clicked
setTimeout(() => {
- $btn.removeClass('shadow-inner bg-selected');
- }, 100);
+ $btn.removeClass('highlighted-numpad-btn');
+ }, 200);
}
}
toggle_numpad(show) {
if (show) {
- this.$totals_section.addClass('d-none');
- this.$numpad_section.removeClass('d-none');
+ this.$totals_section.css('display', 'none');
+ this.$numpad_section.css('display', 'flex');
} else {
- this.$totals_section.removeClass('d-none');
- this.$numpad_section.addClass('d-none');
+ this.$totals_section.css('display', 'flex');
+ this.$numpad_section.css('display', 'none');
}
this.reset_numpad();
}
@@ -790,7 +795,7 @@
reset_numpad() {
this.numpad_value = '';
this.prev_action = undefined;
- this.$numpad_section.find('.shadow-inner').removeClass('shadow-inner bg-selected');
+ this.$numpad_section.find('.highlighted-numpad-btn').removeClass('highlighted-numpad-btn');
}
toggle_numpad_field_edit(fieldname) {
@@ -801,48 +806,56 @@
toggle_customer_info(show) {
if (show) {
- this.$cart_container.addClass('d-none')
- this.$customer_section.addClass('flex-1 scroll-y').removeClass('mb-0 border pr-4 pl-4')
- this.$customer_section.find('.icon').addClass('w-24 h-24 text-2xl').removeClass('w-12 h-12 text-md')
- this.$customer_section.find('.customer-header').removeClass('h-18');
- this.$customer_section.find('.customer-details').addClass('sticky z-100 bg-white');
+ const { customer } = this.customer_info || {};
- this.$customer_section.find('.customer-name').html(
- `<div class="text-md text-dark-grey text-bold">${this.customer_info.customer}</div>
- <div class="last-transacted-on text-grey-200"></div>`
- )
-
- this.$customer_section.find('.customer-details').append(
- `<div class="customer-form">
- <div class="text-grey mt-4 mb-6">CONTACT DETAILS</div>
- <div class="grid grid-cols-2 gap-4">
- <div class="email_id-field"></div>
- <div class="mobile_no-field"></div>
- <div class="loyalty_program-field"></div>
- <div class="loyalty_points-field"></div>
+ this.$cart_container.css('display', 'none');
+ this.$customer_section.css({
+ 'height': '100%',
+ 'padding-top': '0px'
+ });
+ this.$customer_section.find('.customer-details').html(
+ `<div class="header">
+ <div class="label">Contact Details</div>
+ <div class="close-details-btn">
+ <svg width="32" height="32" viewBox="0 0 14 14" fill="none">
+ <path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
+ </svg>
</div>
- <div class="text-grey mt-4 mb-6">RECENT TRANSACTIONS</div>
- </div>`
- )
+ </div>
+ <div class="customer-display">
+ ${this.get_customer_image()}
+ <div class="customer-name-desc">
+ <div class="customer-name">${customer}</div>
+ <div class="customer-desc"></div>
+ </div>
+ </div>
+ <div class="customer-fields-container">
+ <div class="email_id-field"></div>
+ <div class="mobile_no-field"></div>
+ <div class="loyalty_program-field"></div>
+ <div class="loyalty_points-field"></div>
+ </div>
+ <div class="transactions-label">Recent Transactions</div>`
+ );
// transactions need to be in diff div from sticky elem for scrolling
- this.$customer_section.append(`<div class="customer-transactions flex-1 rounded"></div>`)
+ this.$customer_section.append(`<div class="customer-transactions"></div>`);
- this.render_customer_info_form();
+ this.render_customer_fields();
this.fetch_customer_transactions();
} else {
- this.$cart_container.removeClass('d-none');
- this.$customer_section.removeClass('flex-1 scroll-y').addClass('mb-0 border pr-4 pl-4');
- this.$customer_section.find('.icon').addClass('w-12 h-12 text-md').removeClass('w-24 h-24 text-2xl');
- this.$customer_section.find('.customer-header').addClass('h-18')
- this.$customer_section.find('.customer-details').removeClass('sticky z-100 bg-white');
+ this.$cart_container.css('display', 'flex');
+ this.$customer_section.css({
+ 'height': '',
+ 'padding-top': ''
+ });
this.update_customer_section();
}
}
- render_customer_info_form() {
- const $customer_form = this.$customer_section.find('.customer-form');
+ render_customer_fields() {
+ const $customer_form = this.$customer_section.find('.customer-fields-container');
const dfs = [{
fieldname: 'email_id',
@@ -864,7 +877,7 @@
},{
fieldname: 'loyalty_points',
label: __('Loyalty Points'),
- fieldtype: 'Int',
+ fieldtype: 'Data',
read_only: 1
}];
@@ -908,7 +921,7 @@
}
fetch_customer_transactions() {
- frappe.db.get_list('POS Invoice', {
+ frappe.db.get_list('POS Invoice', {
filters: { customer: this.customer_info.customer, docstatus: 1 },
fields: ['name', 'grand_total', 'status', 'posting_date', 'posting_time', 'currency'],
limit: 20
@@ -916,41 +929,45 @@
const transaction_container = this.$customer_section.find('.customer-transactions');
if (!res.length) {
- transaction_container.removeClass('flex-1 border rounded').html(
- `<div class="text-grey text-center">No recent transactions found</div>`
+ transaction_container.html(
+ `<div class="no-transactions-placeholder">No recent transactions found</div>`
)
return;
};
const elapsed_time = moment(res[0].posting_date+" "+res[0].posting_time).fromNow();
- this.$customer_section.find('.last-transacted-on').html(`Last transacted ${elapsed_time}`);
+ this.$customer_section.find('.customer-desc').html(`Last transacted ${elapsed_time}`);
res.forEach(invoice => {
const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma");
- let indicator_color = '';
-
- if (in_list(['Paid', 'Consolidated'], invoice.status)) (indicator_color = 'green');
- if (invoice.status === 'Draft') (indicator_color = 'red');
- if (invoice.status === 'Return') (indicator_color = 'grey');
+ let indicator_color = {
+ 'Paid': 'green',
+ 'Draft': 'red',
+ 'Return': 'gray',
+ 'Consolidated': 'blue'
+ };
transaction_container.append(
- `<div class="invoice-wrapper flex p-3 justify-between border-grey rounded pointer no-select" data-invoice-name="${escape(invoice.name)}">
- <div class="flex flex-col justify-end">
- <div class="text-dark-grey text-bold overflow-hidden whitespace-nowrap mb-2">${invoice.name}</div>
- <div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap">
- ${posting_datetime}
- </div>
+ `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
+ <div class="invoice-name-date">
+ <div class="invoice-name">${invoice.name}</div>
+ <div class="invoice-date">${posting_datetime}</div>
</div>
- <div class="flex flex-col text-right">
- <div class="f-shrink-0 text-md text-dark-grey text-bold ml-4">
+ <div class="invoice-total-status">
+ <div class="invoice-total">
${format_currency(invoice.grand_total, invoice.currency, 0) || 0}
</div>
- <div class="f-shrink-0 text-grey ml-4 text-bold indicator ${indicator_color}">${invoice.status}</div>
+ <div class="invoice-status">
+ <span class="indicator-pill whitespace-nowrap ${indicator_color[invoice.status]}">
+ <span>${invoice.status}</span>
+ </span>
+ </div>
</div>
- </div>`
+ </div>
+ <div class="seperator"></div>`
)
});
- })
+ });
}
load_invoice() {
@@ -958,8 +975,8 @@
this.fetch_customer_details(frm.doc.customer).then(() => {
this.events.customer_details_updated(this.customer_info);
this.update_customer_section();
- })
-
+ });
+
this.$cart_items_wrapper.html('');
if (frm.doc.items.length) {
frm.doc.items.forEach(item => {
@@ -973,20 +990,18 @@
this.update_totals_section(frm);
if(frm.doc.docstatus === 1) {
- this.$totals_section.find('.checkout-btn').addClass('d-none');
- this.$totals_section.find('.edit-cart-btn').addClass('d-none');
- this.$totals_section.find('.grand-total').removeClass('border-b-grey');
+ this.$totals_section.find('.checkout-btn').css('display', 'none');
+ this.$totals_section.find('.edit-cart-btn').css('display', 'none');
} else {
- this.$totals_section.find('.checkout-btn').removeClass('d-none');
- this.$totals_section.find('.edit-cart-btn').addClass('d-none');
- this.$totals_section.find('.grand-total').addClass('border-b-grey');
+ this.$totals_section.find('.checkout-btn').css('display', 'flex');
+ this.$totals_section.find('.edit-cart-btn').css('display', 'none');
}
this.toggle_component(true);
}
toggle_component(show) {
- show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
+ show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none');
}
-
+
}
diff --git a/erpnext/selling/page/point_of_sale/pos_item_details.js b/erpnext/selling/page/point_of_sale/pos_item_details.js
index a4de9f1..cb0a010 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_details.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_details.js
@@ -1,7 +1,9 @@
erpnext.PointOfSale.ItemDetails = class {
- constructor({ wrapper, events }) {
+ constructor({ wrapper, events, settings }) {
this.wrapper = wrapper;
this.events = events;
+ this.allow_rate_change = settings.allow_rate_change;
+ this.allow_discount_change = settings.allow_discount_change;
this.current_item = {};
this.init_component();
@@ -16,35 +18,36 @@
prepare_dom() {
this.wrapper.append(
- `<section class="col-span-4 flex shadow rounded item-details bg-white mx-h-70 h-100 d-none"></section>`
+ `<section class="item-details-container"></section>`
)
- this.$component = this.wrapper.find('.item-details');
+ this.$component = this.wrapper.find('.item-details-container');
}
init_child_components() {
this.$component.html(
- `<div class="details-container flex flex-col p-8 rounded w-full">
- <div class="flex justify-between mb-2">
- <div class="text-grey">ITEM DETAILS</div>
- <div class="close-btn text-grey hover-underline pointer no-select">Close</div>
+ `<div class="item-details-header">
+ <div class="label">Item Details</div>
+ <div class="close-btn">
+ <svg width="32" height="32" viewBox="0 0 14 14" fill="none">
+ <path d="M4.93764 4.93759L7.00003 6.99998M9.06243 9.06238L7.00003 6.99998M7.00003 6.99998L4.93764 9.06238L9.06243 4.93759" stroke="#8D99A6"/>
+ </svg>
</div>
- <div class="item-defaults flex">
- <div class="flex-1 flex flex-col justify-end mr-4 mb-2">
- <div class="item-name text-xl font-weight-450"></div>
- <div class="item-description text-md-0 text-grey-200"></div>
- <div class="item-price text-xl font-bold"></div>
- </div>
- <div class="item-image flex items-center justify-center w-46 h-46 bg-light-grey rounded ml-4 text-6xl text-grey-100"></div>
+ </div>
+ <div class="item-display">
+ <div class="item-name-desc-price">
+ <div class="item-name"></div>
+ <div class="item-desc"></div>
+ <div class="item-price"></div>
</div>
- <div class="discount-section flex items-center"></div>
- <div class="text-grey mt-4 mb-6">STOCK DETAILS</div>
- <div class="form-container grid grid-cols-2 row-gap-2 col-gap-4 grid-auto-row"></div>
- </div>`
+ <div class="item-image"></div>
+ </div>
+ <div class="discount-section"></div>
+ <div class="form-container"></div>`
)
this.$item_name = this.$component.find('.item-name');
- this.$item_description = this.$component.find('.item-description');
+ this.$item_description = this.$component.find('.item-desc');
this.$item_price = this.$component.find('.item-price');
this.$item_image = this.$component.find('.item-image');
this.$form_container = this.$component.find('.form-container');
@@ -52,7 +55,7 @@
}
toggle_item_details_section(item) {
- const { item_code, batch_no, uom } = this.current_item;
+ const { item_code, batch_no, uom } = this.current_item;
const item_code_is_same = item && item_code === item.item_code;
const batch_is_same = item && batch_no == item.batch_no;
const uom_is_same = item && uom === item.uom;
@@ -61,16 +64,16 @@
this.events.toggle_item_selector(this.item_has_changed);
this.toggle_component(this.item_has_changed);
-
+
if (this.item_has_changed) {
this.doctype = item.doctype;
this.item_meta = frappe.get_meta(this.doctype);
this.name = item.name;
this.item_row = item;
this.currency = this.events.get_frm().doc.currency;
-
+
this.current_item = { item_code: item.item_code, batch_no: item.batch_no, uom: item.uom };
-
+
this.render_dom(item);
this.render_discount_dom(item);
this.render_form(item);
@@ -79,7 +82,7 @@
this.current_item = {};
}
}
-
+
validate_serial_batch_item() {
const doc = this.events.get_frm().doc;
const item_row = doc.items.find(item => item.name === this.name);
@@ -91,7 +94,7 @@
const no_serial_selected = !item_row.serial_no;
const no_batch_selected = !item_row.batch_no;
- if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
+ if ((serialized && no_serial_selected) || (batched && no_batch_selected) ||
(serialized && batched && (no_batch_selected || no_serial_selected))) {
frappe.show_alert({
@@ -102,40 +105,34 @@
this.events.remove_item_from_cart();
}
}
-
+
render_dom(item) {
- let { item_code ,item_name, description, image, price_list_rate } = item;
+ let { item_name, description, image, price_list_rate } = item;
function get_description_html() {
if (description) {
- description = description.indexOf('...') === -1 && description.length > 75 ? description.substr(0, 73) + '...' : description;
+ description = description.indexOf('...') === -1 && description.length > 140 ? description.substr(0, 139) + '...' : description;
return description;
}
return ``;
}
-
+
this.$item_name.html(item_name);
this.$item_description.html(get_description_html());
this.$item_price.html(format_currency(price_list_rate, this.currency));
if (image) {
- this.$item_image.html(
- `<img class="h-full" src="${image}" alt="${image}" style="object-fit: cover;">`
- );
+ this.$item_image.html(`<img src="${image}" alt="${image}">`);
} else {
- this.$item_image.html(frappe.get_abbr(item_code));
+ this.$item_image.html(`<div class="item-abbr">${frappe.get_abbr(item_name)}</div>`);
}
}
-
+
render_discount_dom(item) {
if (item.discount_percentage) {
this.$dicount_section.html(
- `<div class="text-grey line-through mr-4 text-md mb-2">
- ${format_currency(item.price_list_rate, this.currency)}
- </div>
- <div class="p-1 pr-3 pl-3 rounded w-fit text-bold bg-green-200 mb-2">
- ${item.discount_percentage}% off
- </div>`
+ `<div class="item-rate">${format_currency(item.price_list_rate, this.currency)}</div>
+ <div class="item-discount">${item.discount_percentage}% off</div>`
)
this.$item_price.html(format_currency(item.rate, this.currency));
} else {
@@ -149,18 +146,16 @@
fields_to_display.forEach((fieldname, idx) => {
this.$form_container.append(
- `<div class="">
- <div class="item_detail_field ${fieldname}-control" data-fieldname="${fieldname}"></div>
- </div>`
+ `<div class="${fieldname}-control" data-fieldname="${fieldname}"></div>`
)
const field_meta = this.item_meta.fields.find(df => df.fieldname === fieldname);
fieldname === 'discount_percentage' ? (field_meta.label = __('Discount (%)')) : '';
const me = this;
-
+
this[`${fieldname}_control`] = frappe.ui.form.make_control({
- df: {
- ...field_meta,
+ df: {
+ ...field_meta,
onchange: function() {
me.events.form_updated(me.doctype, me.name, fieldname, this.value);
}
@@ -185,39 +180,42 @@
make_auto_serial_selection_btn(item) {
if (item.has_serial_no) {
- this.$form_container.append(
- `<div class="grid-filler no-select"></div>`
- )
if (!item.has_batch_no) {
this.$form_container.append(
`<div class="grid-filler no-select"></div>`
- )
+ );
}
this.$form_container.append(
- `<div class="auto-fetch-btn bg-grey-100 border border-grey text-bold rounded pt-3 pb-3 pl-6 pr-8 text-grey pointer no-select mt-2"
- style="height: 3.3rem">
- Auto Fetch Serial Numbers
- </div>`
- )
- this.$form_container.find('.serial_no-control').find('textarea').css('height', '9rem');
- this.$form_container.find('.serial_no-control').parent().addClass('row-span-2');
+ `<div class="btn btn-sm btn-secondary auto-fetch-btn">Auto Fetch Serial Numbers</div>`
+ );
+ this.$form_container.find('.serial_no-control').find('textarea').css('height', '6rem');
}
}
-
+
bind_custom_control_change_event() {
const me = this;
if (this.rate_control) {
- this.rate_control.df.onchange = function() {
- if (this.value || flt(this.value) === 0) {
- me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
- const item_row = frappe.get_doc(me.doctype, me.name);
- const doc = me.events.get_frm().doc;
+ if (this.allow_rate_change) {
+ this.rate_control.df.onchange = function() {
+ if (this.value || flt(this.value) === 0) {
+ me.events.form_updated(me.doctype, me.name, 'rate', this.value).then(() => {
+ const item_row = frappe.get_doc(me.doctype, me.name);
+ const doc = me.events.get_frm().doc;
- me.$item_price.html(format_currency(item_row.rate, doc.currency));
- me.render_discount_dom(item_row);
- });
- }
+ me.$item_price.html(format_currency(item_row.rate, doc.currency));
+ me.render_discount_dom(item_row);
+ });
+ }
+ };
+ } else {
+ this.rate_control.df.read_only = 1;
}
+ this.rate_control.refresh();
+ }
+
+ if (this.discount_percentage_control && !this.allow_discount_change) {
+ this.discount_percentage_control.df.read_only = 1;
+ this.discount_percentage_control.refresh();
}
if (this.warehouse_control) {
@@ -286,7 +284,7 @@
me.events.set_value_in_current_cart_item('uom', this.value);
me.events.form_updated(me.doctype, me.name, 'uom', this.value);
me.current_item.uom = this.value;
-
+
const item_row = frappe.get_doc(me.doctype, me.name);
me.conversion_factor_control.df.read_only = (item_row.stock_uom == this.value);
me.conversion_factor_control.refresh();
@@ -294,20 +292,26 @@
}
frappe.model.on("POS Invoice Item", "*", (fieldname, value, item_row) => {
- const field_control = me[`${fieldname}_control`];
- if (field_control) {
+ const field_control = this[`${fieldname}_control`];
+ const { item_code, batch_no, uom } = this.current_item;
+ const item_code_is_same = item_code === item_row.item_code;
+ const batch_is_same = batch_no == item_row.batch_no;
+ const uom_is_same = uom === item_row.uom;
+ const item_is_same = item_code_is_same && batch_is_same && uom_is_same ? true : false;
+
+ if (item_is_same && field_control && field_control.get_value() !== value) {
field_control.set_value(value);
cur_pos.update_cart_html(item_row);
}
});
}
-
+
async auto_update_batch_no() {
if (this.serial_no_control && this.batch_no_control) {
const selected_serial_nos = this.serial_no_control.get_value().split(`\n`).filter(s => s);
if (!selected_serial_nos.length) return;
- // find batch nos of the selected serial no
+ // find batch nos of the selected serial no
const serials_with_batch_no = await frappe.db.get_list("Serial No", {
filters: { 'name': ["in", selected_serial_nos]},
fields: ["batch_no", "name"]
@@ -322,7 +326,7 @@
const batch_serial_nos = batch_serial_map[batch_no].join(`\n`);
// eg. 10 selected serial no. -> 5 belongs to first batch other 5 belongs to second batch
const serial_nos_belongs_to_other_batch = selected_serial_nos.length !== batch_serial_map[batch_no].length;
-
+
const current_batch_no = this.batch_no_control.get_value();
current_batch_no != batch_no && await this.batch_no_control.set_value(batch_no);
@@ -337,7 +341,7 @@
this.events.clone_new_batch_item_in_frm(batch_serial_map, this.current_item);
}
}
-
+
bind_events() {
this.bind_auto_serial_fetch_event();
this.bind_fields_to_numpad_fields();
@@ -367,7 +371,7 @@
}
});
}
-
+
bind_auto_serial_fetch_event() {
this.$form_container.on('click', '.auto-fetch-btn', () => {
this.batch_no_control && this.batch_no_control.set_value('');
@@ -409,6 +413,6 @@
}
toggle_component(show) {
- show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
+ show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none');
}
}
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_item_selector.js b/erpnext/selling/page/point_of_sale/pos_item_selector.js
index 49d4281..7c116e9 100644
--- a/erpnext/selling/page/point_of_sale/pos_item_selector.js
+++ b/erpnext/selling/page/point_of_sale/pos_item_selector.js
@@ -1,12 +1,17 @@
+import onScan from 'onscan.js';
+
erpnext.PointOfSale.ItemSelector = class {
- constructor({ frm, wrapper, events, pos_profile }) {
+ // eslint-disable-next-line no-unused-vars
+ constructor({ frm, wrapper, events, pos_profile, settings }) {
this.wrapper = wrapper;
this.events = events;
this.pos_profile = pos_profile;
-
+ this.hide_images = settings.hide_images;
+ this.auto_add_item = settings.auto_add_item_to_cart;
+
this.inti_component();
}
-
+
inti_component() {
this.prepare_dom();
this.make_search_bar();
@@ -17,29 +22,25 @@
prepare_dom() {
this.wrapper.append(
- `<section class="col-span-6 flex shadow rounded items-selector bg-white mx-h-70 h-100">
- <div class="flex flex-col rounded w-full scroll-y">
- <div class="filter-section flex p-8 pb-2 bg-white sticky z-100">
- <div class="search-field flex f-grow-3 mr-8 items-center text-grey"></div>
- <div class="item-group-field flex f-grow-1 items-center text-grey text-bold"></div>
- </div>
- <div class="flex flex-1 flex-col p-8 pt-2">
- <div class="text-grey mb-6">ALL ITEMS</div>
- <div class="items-container grid grid-cols-4 gap-8">
- </div>
- </div>
+ `<section class="items-selector">
+ <div class="filter-section">
+ <div class="label">All Items</div>
+ <div class="search-field"></div>
+ <div class="item-group-field"></div>
</div>
+ <div class="items-container"></div>
</section>`
);
-
+
this.$component = this.wrapper.find('.items-selector');
+ this.$items_container = this.$component.find('.items-container');
}
async load_items_data() {
if (!this.item_group) {
const res = await frappe.db.get_value("Item Group", {lft: 1, is_group: 1}, "name");
this.parent_item_group = res.message.name;
- };
+ }
if (!this.price_list) {
const res = await frappe.db.get_value("POS Profile", this.pos_profile, "selling_price_list");
this.price_list = res.message.selling_price_list;
@@ -51,11 +52,12 @@
}
get_items({start = 0, page_length = 40, search_value=''}) {
- const price_list = this.events.get_frm().doc?.selling_price_list || this.price_list;
+ const doc = this.events.get_frm().doc;
+ const price_list = (doc && doc.selling_price_list) || this.price_list;
let { item_group, pos_profile } = this;
!item_group && (item_group = this.parent_item_group);
-
+
return frappe.call({
method: "erpnext.selling.page.point_of_sale.point_of_sale.get_items",
freeze: true,
@@ -65,49 +67,52 @@
render_item_list(items) {
- this.$items_container = this.$component.find('.items-container');
this.$items_container.html('');
items.forEach(item => {
const item_html = this.get_item_html(item);
this.$items_container.append(item_html);
- })
+ });
}
get_item_html(item) {
+ const me = this;
+ // eslint-disable-next-line no-unused-vars
const { item_image, serial_no, batch_no, barcode, actual_qty, stock_uom } = item;
const indicator_color = actual_qty > 10 ? "green" : actual_qty <= 0 ? "red" : "orange";
function get_item_image_html() {
- if (item_image) {
+ if (!me.hide_images && item_image) {
return `<div class="flex items-center justify-center h-32 border-b-grey text-6xl text-grey-100">
<img class="h-full" src="${item_image}" alt="${frappe.get_abbr(item.item_name)}" style="object-fit: cover;">
- </div>`
+ </div>`;
} else {
- return `<div class="flex items-center justify-center h-32 bg-light-grey text-6xl text-grey-100">
- ${frappe.get_abbr(item.item_name)}
- </div>`
+ return `<div class="item-display abbr">${frappe.get_abbr(item.item_name)}</div>`;
}
}
return (
- `<div class="item-wrapper rounded shadow pointer no-select" data-item-code="${escape(item.item_code)}"
- data-serial-no="${escape(serial_no)}" data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
+ `<div class="item-wrapper"
+ data-item-code="${escape(item.item_code)}" data-serial-no="${escape(serial_no)}"
+ data-batch-no="${escape(batch_no)}" data-uom="${escape(stock_uom)}"
title="Avaiable Qty: ${actual_qty}">
+
${get_item_image_html()}
- <div class="flex items-center pr-4 pl-4 h-10 justify-between">
- <div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap">
+
+ <div class="item-detail">
+ <div class="item-name">
<span class="indicator ${indicator_color}"></span>
${frappe.ellipsis(item.item_name, 18)}
</div>
- <div class="f-shrink-0 text-dark-grey text-bold ml-4">${format_currency(item.price_list_rate, item.currency, 0) || 0}</div>
+ <div class="item-rate">${format_currency(item.price_list_rate, item.currency, 0) || 0}</div>
</div>
</div>`
- )
+ );
}
make_search_bar() {
const me = this;
+ const doc = me.events.get_frm().doc;
this.$component.find('.search-field').html('');
this.$component.find('.item-group-field').html('');
@@ -115,7 +120,7 @@
df: {
label: __('Search'),
fieldtype: 'Data',
- placeholder: __('Search by item code, serial number, batch no or barcode')
+ placeholder: __('Search by item code, serial number or barcode')
},
parent: this.$component.find('.search-field'),
render_input: true,
@@ -135,9 +140,9 @@
return {
query: 'erpnext.selling.page.point_of_sale.point_of_sale.item_group_query',
filters: {
- pos_profile: me.events.get_frm().doc?.pos_profile
+ pos_profile: doc ? doc.pos_profile : ''
}
- }
+ };
},
},
parent: this.$component.find('.item-group-field'),
@@ -149,6 +154,7 @@
bind_events() {
const me = this;
+ window.onScan = onScan;
onScan.attachTo(document, {
onScan: (sScancode) => {
if (this.search_field && this.$component.is(':visible')) {
@@ -165,14 +171,14 @@
let batch_no = unescape($item.attr('data-batch-no'));
let serial_no = unescape($item.attr('data-serial-no'));
let uom = unescape($item.attr('data-uom'));
-
+
// escape(undefined) returns "undefined" then unescape returns "undefined"
batch_no = batch_no === "undefined" ? undefined : batch_no;
serial_no = serial_no === "undefined" ? undefined : serial_no;
uom = uom === "undefined" ? undefined : uom;
me.events.item_selected({ field: 'qty', value: "+1", item: { item_code, batch_no, serial_no, uom }});
- })
+ });
this.search_field.$input.on('input', (e) => {
clearTimeout(this.last_search);
@@ -203,6 +209,7 @@
ignore_inputs: true,
page: cur_page.page.page
});
+
// for selecting the last filtered item on search
frappe.ui.keys.on("enter", () => {
const selector_is_visible = this.$component.is(':visible');
@@ -224,7 +231,7 @@
}
});
}
-
+
filter_items({ search_term='' }={}) {
if (search_term) {
search_term = search_term.toLowerCase();
@@ -235,40 +242,47 @@
const items = this.search_index[search_term];
this.items = items;
this.render_item_list(items);
+ this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart();
return;
}
}
this.get_items({ search_value: search_term })
.then(({ message }) => {
+ // eslint-disable-next-line no-unused-vars
const { items, serial_no, batch_no, barcode } = message;
if (search_term && !barcode) {
this.search_index[search_term] = items;
}
this.items = items;
this.render_item_list(items);
+ this.auto_add_item && this.items.length == 1 && this.add_filtered_item_to_cart();
});
}
-
+
+ add_filtered_item_to_cart() {
+ this.$items_container.find(".item-wrapper").click();
+ }
+
resize_selector(minimize) {
- minimize ?
- this.$component.find('.search-field').removeClass('mr-8') :
- this.$component.find('.search-field').addClass('mr-8');
-
- minimize ?
- this.$component.find('.filter-section').addClass('flex-col') :
- this.$component.find('.filter-section').removeClass('flex-col');
+ minimize ?
+ this.$component.find('.filter-section').css('grid-template-columns', 'repeat(1, minmax(0, 1fr))') :
+ this.$component.find('.filter-section').css('grid-template-columns', 'repeat(12, minmax(0, 1fr))');
minimize ?
- this.$component.removeClass('col-span-6').addClass('col-span-2') :
- this.$component.removeClass('col-span-2').addClass('col-span-6')
+ this.$component.find('.search-field').css('margin', 'var(--margin-sm) 0px') :
+ this.$component.find('.search-field').css('margin', '0px var(--margin-sm)');
minimize ?
- this.$items_container.removeClass('grid-cols-4').addClass('grid-cols-1') :
- this.$items_container.removeClass('grid-cols-1').addClass('grid-cols-4')
+ this.$component.css('grid-column', 'span 2 / span 2') :
+ this.$component.css('grid-column', 'span 6 / span 6');
+
+ minimize ?
+ this.$items_container.css('grid-template-columns', 'repeat(1, minmax(0, 1fr))') :
+ this.$items_container.css('grid-template-columns', 'repeat(4, minmax(0, 1fr))');
}
toggle_component(show) {
- show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
+ show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none');
}
-}
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_number_pad.js b/erpnext/selling/page/point_of_sale/pos_number_pad.js
index 4b8e841..962bcaf 100644
--- a/erpnext/selling/page/point_of_sale/pos_number_pad.js
+++ b/erpnext/selling/page/point_of_sale/pos_number_pad.js
@@ -22,17 +22,16 @@
return keys.reduce((a, row, i) => {
return a + row.reduce((a2, number, j) => {
const class_to_append = css_classes && css_classes[i] ? css_classes[i][j] : '';
- const fieldname = fieldnames && fieldnames[number] ?
+ const fieldname = fieldnames && fieldnames[number] ?
fieldnames[number] : typeof number === 'string' ? frappe.scrub(number) : number;
- return a2 + `<div class="numpad-btn pointer no-select rounded ${class_to_append}
- flex items-center justify-center h-16 text-md border-grey border" data-button-value="${fieldname}">${number}</div>`
- }, '')
+ return a2 + `<div class="numpad-btn ${class_to_append}" data-button-value="${fieldname}">${number}</div>`;
+ }, '');
}, '');
}
this.wrapper.html(
- `<div class="grid grid-cols-${cols} gap-4">
+ `<div class="numpad-container">
${get_keys()}
</div>`
)
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_list.js b/erpnext/selling/page/point_of_sale/pos_past_order_list.js
index b256247..ec39231 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_list.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_list.js
@@ -14,17 +14,13 @@
prepare_dom() {
this.wrapper.append(
- `<section class="col-span-4 flex flex-col shadow rounded past-order-list bg-white mx-h-70 h-100 d-none">
- <div class="flex flex-col rounded w-full scroll-y">
- <div class="filter-section flex flex-col p-8 pb-2 bg-white sticky z-100">
- <div class="search-field flex items-center text-grey"></div>
- <div class="status-field flex items-center text-grey text-bold"></div>
- </div>
- <div class="flex flex-1 flex-col p-8 pt-2">
- <div class="text-grey mb-6">RECENT ORDERS</div>
- <div class="invoices-container rounded border grid grid-cols-1"></div>
- </div>
+ `<section class="past-order-list">
+ <div class="filter-section">
+ <div class="label">Recent Orders</div>
+ <div class="search-field"></div>
+ <div class="status-field"></div>
</div>
+ <div class="invoices-container"></div>
</section>`
);
@@ -66,7 +62,7 @@
options: `Draft\nPaid\nConsolidated\nReturn`,
placeholder: __('Filter by invoice status'),
onchange: function() {
- me.refresh_list(me.search_field.get_value(), this.value);
+ if (me.$component.is(':visible')) me.refresh_list();
}
},
parent: this.$component.find('.status-field'),
@@ -77,10 +73,6 @@
this.status_field.set_value('Draft');
}
- toggle_component(show) {
- show ? this.$component.removeClass('d-none') && this.refresh_list() : this.$component.addClass('d-none');
- }
-
refresh_list() {
frappe.dom.freeze();
this.events.reset_summary();
@@ -106,23 +98,26 @@
get_invoice_html(invoice) {
const posting_datetime = moment(invoice.posting_date+" "+invoice.posting_time).format("Do MMMM, h:mma");
return (
- `<div class="invoice-wrapper flex p-4 justify-between border-b-grey pointer no-select" data-invoice-name="${escape(invoice.name)}">
- <div class="flex flex-col justify-end">
- <div class="text-dark-grey text-bold overflow-hidden whitespace-nowrap mb-2">${invoice.name}</div>
- <div class="flex items-center">
- <div class="flex items-center f-shrink-1 text-dark-grey overflow-hidden whitespace-nowrap">
- <svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
- <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
- </svg>
- ${invoice.customer}
- </div>
+ `<div class="invoice-wrapper" data-invoice-name="${escape(invoice.name)}">
+ <div class="invoice-name-date">
+ <div class="invoice-name">${invoice.name}</div>
+ <div class="invoice-date">
+ <svg class="mr-2" width="12" height="12" viewBox="0 0 24 24" stroke="currentColor" stroke-width="1" stroke-linecap="round" stroke-linejoin="round">
+ <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>
+ </svg>
+ ${invoice.customer}
</div>
</div>
- <div class="flex flex-col text-right">
- <div class="f-shrink-0 text-lg text-dark-grey text-bold ml-4">${format_currency(invoice.grand_total, invoice.currency, 0) || 0}</div>
- <div class="f-shrink-0 text-grey ml-4">${posting_datetime}</div>
+ <div class="invoice-total-status">
+ <div class="invoice-total">${format_currency(invoice.grand_total, invoice.currency, 0) || 0}</div>
+ <div class="invoice-date">${posting_datetime}</div>
</div>
- </div>`
+ </div>
+ <div class="seperator"></div>`
);
}
+
+ toggle_component(show) {
+ show ? this.$component.css('display', 'flex') && this.refresh_list() : this.$component.css('display', 'none');
+ }
};
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
index 6fd4c26..be2b769 100644
--- a/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
+++ b/erpnext/selling/page/point_of_sale/pos_past_order_summary.js
@@ -8,85 +8,39 @@
init_component() {
this.prepare_dom();
- this.init_child_components();
+ this.init_email_print_dialog();
this.bind_events();
this.attach_shortcuts();
}
prepare_dom() {
this.wrapper.append(
- `<section class="col-span-6 flex flex-col items-center shadow rounded past-order-summary bg-white mx-h-70 h-100 d-none">
- <div class="no-summary-placeholder flex flex-1 items-center justify-center p-16">
- <div class="no-item-wrapper flex items-center h-18 pr-4 pl-4">
- <div class="flex-1 text-center text-grey">Select an invoice to load summary data</div>
- </div>
+ `<section class="past-order-summary">
+ <div class="no-summary-placeholder">
+ Select an invoice to load summary data
</div>
- <div class="summary-wrapper d-none flex-1 w-66 text-dark-grey relative">
- <div class="summary-container absolute flex flex-col pt-16 pb-16 pr-8 pl-8 w-full h-full"></div>
+ <div class="invoice-summary-wrapper">
+ <div class="abs-container">
+ <div class="upper-section"></div>
+ <div class="label">Items</div>
+ <div class="items-container summary-container"></div>
+ <div class="label">Totals</div>
+ <div class="totals-container summary-container"></div>
+ <div class="label">Payments</div>
+ <div class="payments-container summary-container"></div>
+ <div class="summary-btns"></div>
+ </div>
</div>
</section>`
);
this.$component = this.wrapper.find('.past-order-summary');
- this.$summary_wrapper = this.$component.find('.summary-wrapper');
- this.$summary_container = this.$component.find('.summary-container');
- }
-
- init_child_components() {
- this.init_upper_section();
- this.init_items_summary();
- this.init_totals_summary();
- this.init_payments_summary();
- this.init_summary_buttons();
- this.init_email_print_dialog();
- }
-
- init_upper_section() {
- this.$summary_container.append(
- `<div class="flex upper-section justify-between w-full h-24"></div>`
- );
-
+ this.$summary_wrapper = this.$component.find('.invoice-summary-wrapper');
+ this.$summary_container = this.$component.find('.abs-container');
this.$upper_section = this.$summary_container.find('.upper-section');
- }
-
- init_items_summary() {
- this.$summary_container.append(
- `<div class="flex flex-col flex-1 mt-6 w-full scroll-y">
- <div class="text-grey mb-4 sticky bg-white">ITEMS</div>
- <div class="items-summary-container border rounded flex flex-col w-full"></div>
- </div>`
- );
-
- this.$items_summary_container = this.$summary_container.find('.items-summary-container');
- }
-
- init_totals_summary() {
- this.$summary_container.append(
- `<div class="flex flex-col mt-6 w-full f-shrink-0">
- <div class="text-grey mb-4">TOTALS</div>
- <div class="summary-totals-container border rounded flex flex-col w-full"></div>
- </div>`
- );
-
- this.$totals_summary_container = this.$summary_container.find('.summary-totals-container');
- }
-
- init_payments_summary() {
- this.$summary_container.append(
- `<div class="flex flex-col mt-6 w-full f-shrink-0">
- <div class="text-grey mb-4">PAYMENTS</div>
- <div class="payments-summary-container border rounded flex flex-col w-full mb-4"></div>
- </div>`
- );
-
- this.$payment_summary_container = this.$summary_container.find('.payments-summary-container');
- }
-
- init_summary_buttons() {
- this.$summary_container.append(
- `<div class="summary-btns flex summary-btns justify-between w-full f-shrink-0"></div>`
- );
-
+ this.$items_container = this.$summary_container.find('.items-container');
+ this.$totals_container = this.$summary_container.find('.totals-container');
+ this.$payment_container = this.$summary_container.find('.payments-container');
this.$summary_btns = this.$summary_container.find('.summary-btns');
}
@@ -94,7 +48,7 @@
const email_dialog = new frappe.ui.Dialog({
title: 'Email Receipt',
fields: [
- {fieldname:'email_id', fieldtype:'Data', options: 'Email', label:'Email ID'},
+ {fieldname: 'email_id', fieldtype: 'Data', options: 'Email', label: 'Email ID'},
// {fieldname:'remarks', fieldtype:'Text', label:'Remarks (if any)'}
],
primary_action: () => {
@@ -107,7 +61,7 @@
const print_dialog = new frappe.ui.Dialog({
title: 'Print Receipt',
fields: [
- {fieldname:'print', fieldtype:'Data', label:'Print Preview'}
+ {fieldname: 'print', fieldtype: 'Data', label: 'Print Preview'}
],
primary_action: () => {
const frm = this.events.get_frm();
@@ -121,132 +75,87 @@
}
get_upper_section_html(doc) {
- const { status } = doc; let indicator_color = '';
+ const { status } = doc;
+ let indicator_color = '';
in_list(['Paid', 'Consolidated'], status) && (indicator_color = 'green');
status === 'Draft' && (indicator_color = 'red');
status === 'Return' && (indicator_color = 'grey');
- return `<div class="flex flex-col items-start justify-end pr-4">
- <div class="text-lg text-bold pt-2">${doc.customer}</div>
- <div class="text-grey">${this.customer_email}</div>
- <div class="text-grey mt-auto">Sold by: ${doc.owner}</div>
+ return `<div class="left-section">
+ <div class="customer-name">${doc.customer}</div>
+ <div class="customer-email">${this.customer_email}</div>
+ <div class="cashier">Sold by: ${doc.owner}</div>
</div>
- <div class="flex flex-col flex-1 items-end justify-between">
- <div class="text-2-5xl text-bold">${format_currency(doc.paid_amount, doc.currency)}</div>
- <div class="flex justify-between">
- <div class="text-grey mr-4">${doc.name}</div>
- <div class="text-grey text-bold indicator ${indicator_color}">${doc.status}</div>
- </div>
+ <div class="right-section">
+ <div class="paid-amount">${format_currency(doc.paid_amount, doc.currency)}</div>
+ <div class="invoice-name">${doc.name}</div>
+ <span class="indicator-pill whitespace-nowrap ${indicator_color}"><span>${doc.status}</span></span>
</div>`;
}
+ get_item_html(doc, item_data) {
+ return `<div class="item-row-wrapper">
+ <div class="item-name">${item_data.item_name}</div>
+ <div class="item-qty">${item_data.qty || 0}</div>
+ <div class="item-rate-disc">${get_rate_discount_html()}</div>
+ </div>`;
+
+ function get_rate_discount_html() {
+ if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) {
+ return `<span class="item-disc">(${item_data.discount_percentage}% off)</span>
+ <div class="item-rate">${format_currency(item_data.rate, doc.currency)}</div>`;
+ } else {
+ return `<div class="item-rate">${format_currency(item_data.price_list_rate || item_data.rate, doc.currency)}</div>`;
+ }
+ }
+ }
+
get_discount_html(doc) {
if (doc.discount_amount) {
- return `<div class="total-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
- <div class="flex f-shrink-1 items-center">
- <div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap mr-2">
- Discount
- </div>
- <span class="text-grey">(${doc.additional_discount_percentage} %)</span>
- </div>
- <div class="flex flex-col f-shrink-0 ml-auto text-right">
- <div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.discount_amount, doc.currency)}</div>
- </div>
- </div>`;
+ return `<div class="summary-row-wrapper">
+ <div>Discount (${doc.additional_discount_percentage} %)</div>
+ <div>${format_currency(doc.discount_amount, doc.currency)}</div>
+ </div>`;
} else {
return ``;
}
}
get_net_total_html(doc) {
- return `<div class="total-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
- <div class="flex f-shrink-1 items-center">
- <div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap">
- Net Total
- </div>
- </div>
- <div class="flex flex-col f-shrink-0 ml-auto text-right">
- <div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.net_total, doc.currency)}</div>
- </div>
+ return `<div class="summary-row-wrapper">
+ <div>Net Total</div>
+ <div>${format_currency(doc.net_total, doc.currency)}</div>
</div>`;
}
get_taxes_html(doc) {
- const taxes = doc.taxes.map((t, i) => {
- let margin_left = '';
- if (i !== 0) margin_left = 'ml-2';
- return `<span class="pl-2 pr-2 ${margin_left}">${t.description} @${t.rate}%</span>`;
+ if (!doc.taxes.length) return '';
+
+ let taxes_html = doc.taxes.map(t => {
+ const description = /[0-9]+/.test(t.description) ? t.description : `${t.description} @ ${t.rate}%`;
+ return `
+ <div class="tax-row">
+ <div class="tax-label">${description}</div>
+ <div class="tax-value">${format_currency(t.tax_amount_after_discount_amount, doc.currency)}</div>
+ </div>
+ `;
}).join('');
- return `
- <div class="total-summary-wrapper flex items-center justify-between h-12 pr-4 pl-4 border-b-grey">
- <div class="flex">
- <div class="text-md-0 text-dark-grey text-bold w-fit">Tax Charges</div>
- <div class="flex ml-6 text-dark-grey">${taxes}</div>
- </div>
- <div class="flex flex-col text-right">
- <div class="text-md-0 text-dark-grey text-bold">
- ${format_currency(doc.base_total_taxes_and_charges, doc.currency)}
- </div>
- </div>
- </div>`;
+ return `<div class="taxes-wrapper">${taxes_html}</div>`;
}
get_grand_total_html(doc) {
- return `<div class="total-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
- <div class="flex f-shrink-1 items-center">
- <div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap">
- Grand Total
- </div>
- </div>
- <div class="flex flex-col f-shrink-0 ml-auto text-right">
- <div class="text-md-0 text-dark-grey text-bold">${format_currency(doc.grand_total, doc.currency)}</div>
- </div>
+ return `<div class="summary-row-wrapper grand-total">
+ <div>Grand Total</div>
+ <div>${format_currency(doc.grand_total, doc.currency)}</div>
</div>`;
}
- get_item_html(doc, item_data) {
- return `<div class="item-summary-wrapper flex items-center h-12 pr-4 pl-4 border-b-grey pointer no-select">
- <div class="flex w-6 h-6 rounded bg-light-grey mr-4 items-center justify-center font-bold f-shrink-0">
- <span>${item_data.qty || 0}</span>
- </div>
- <div class="flex flex-col f-shrink-1">
- <div class="text-md text-dark-grey text-bold overflow-hidden whitespace-nowrap">
- ${item_data.item_name}
- </div>
- </div>
- <div class="flex f-shrink-0 ml-auto text-right">
- ${get_rate_discount_html()}
- </div>
- </div>`;
-
- function get_rate_discount_html() {
- if (item_data.rate && item_data.price_list_rate && item_data.rate !== item_data.price_list_rate) {
- return `<span class="text-grey mr-2">
- (${item_data.discount_percentage}% off)
- </span>
- <div class="text-md-0 text-dark-grey text-bold">
- ${format_currency(item_data.rate, doc.currency)}
- </div>`;
- } else {
- return `<div class="text-md-0 text-dark-grey text-bold">
- ${format_currency(item_data.price_list_rate || item_data.rate, doc.currency)}
- </div>`;
- }
- }
- }
-
get_payment_html(doc, payment) {
- return `<div class="payment-summary-wrapper flex items-center h-12 pr-4 pl-4 pointer border-b-grey no-select">
- <div class="flex f-shrink-1 items-center">
- <div class="text-md-0 text-dark-grey text-bold overflow-hidden whitespace-nowrap">
- ${payment.mode_of_payment}
- </div>
- </div>
- <div class="flex flex-col f-shrink-0 ml-auto text-right">
- <div class="text-md-0 text-dark-grey text-bold">${format_currency(payment.amount, doc.currency)}</div>
- </div>
+ return `<div class="summary-row-wrapper payments">
+ <div>${payment.mode_of_payment}</div>
+ <div>${format_currency(payment.amount, doc.currency)}</div>
</div>`;
}
@@ -254,22 +163,27 @@
this.$summary_container.on('click', '.return-btn', () => {
this.events.process_return(this.doc.name);
this.toggle_component(false);
- this.$component.find('.no-summary-placeholder').removeClass('d-none');
- this.$summary_wrapper.addClass('d-none');
+ this.$component.find('.no-summary-placeholder').css('display', 'flex');
+ this.$summary_wrapper.css('display', 'none');
});
this.$summary_container.on('click', '.edit-btn', () => {
this.events.edit_order(this.doc.name);
this.toggle_component(false);
- this.$component.find('.no-summary-placeholder').removeClass('d-none');
- this.$summary_wrapper.addClass('d-none');
+ this.$component.find('.no-summary-placeholder').css('display', 'flex');
+ this.$summary_wrapper.css('display', 'none');
+ });
+
+ this.$summary_container.on('click', '.delete-btn', () => {
+ this.events.delete_order(this.doc.name);
+ this.show_summary_placeholder();
});
this.$summary_container.on('click', '.new-btn', () => {
this.events.new_order();
this.toggle_component(false);
- this.$component.find('.no-summary-placeholder').removeClass('d-none');
- this.$summary_wrapper.addClass('d-none');
+ this.$component.find('.no-summary-placeholder').css('display', 'flex');
+ this.$summary_wrapper.css('display', 'none');
});
this.$summary_container.on('click', '.email-btn', () => {
@@ -312,10 +226,6 @@
});
}
- toggle_component(show) {
- show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
- }
-
send_email() {
const frm = this.events.get_frm();
const recipients = this.email_dialog.get_values().recipients;
@@ -323,7 +233,7 @@
const print_format = frm.pos_print_format;
frappe.call({
- method:"frappe.core.doctype.communication.email.make",
+ method: "frappe.core.doctype.communication.email.make",
args: {
recipients: recipients,
subject: __(frm.meta.name) + ': ' + doc.name,
@@ -332,14 +242,16 @@
send_email: 1,
print_format,
sender_full_name: frappe.user.full_name(),
- _lang : doc.language
+ _lang: doc.language
},
callback: r => {
- if(!r.exc) {
+ if (!r.exc) {
frappe.utils.play_sound("email");
- if(r.message["emails_not_sent_to"]) {
- frappe.msgprint(__("Email not sent to {0} (unsubscribed / disabled)",
- [ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]) );
+ if (r.message["emails_not_sent_to"]) {
+ frappe.msgprint(__(
+ "Email not sent to {0} (unsubscribed / disabled)",
+ [ frappe.utils.escape_html(r.message["emails_not_sent_to"]) ]
+ ));
} else {
frappe.show_alert({
message: __('Email sent successfully.'),
@@ -361,9 +273,7 @@
m.visible_btns.forEach(b => {
const class_name = b.split(' ')[0].toLowerCase();
this.$summary_btns.append(
- `<div class="${class_name}-btn border rounded h-14 flex flex-1 items-center mr-4 justify-center text-md text-bold no-select pointer">
- ${b}
- </div>`
+ `<div class="summary-btn btn btn-default ${class_name}-btn">${b}</div>`
);
});
}
@@ -371,29 +281,14 @@
this.$summary_btns.children().last().removeClass('mr-4');
}
- show_summary_placeholder() {
- this.$summary_wrapper.addClass("d-none");
- this.$component.find('.no-summary-placeholder').removeClass('d-none');
- }
-
- switch_to_post_submit_summary() {
- // switch to full width view
- this.$component.removeClass('col-span-6').addClass('col-span-10');
- this.$summary_wrapper.removeClass('w-66').addClass('w-40');
-
- // switch place holder with summary container
- this.$component.find('.no-summary-placeholder').addClass('d-none');
- this.$summary_wrapper.removeClass('d-none');
- }
-
- switch_to_recent_invoice_summary() {
- // switch full width view with 60% view
- this.$component.removeClass('col-span-10').addClass('col-span-6');
- this.$summary_wrapper.removeClass('w-40').addClass('w-66');
-
- // switch place holder with summary container
- this.$component.find('.no-summary-placeholder').addClass('d-none');
- this.$summary_wrapper.removeClass('d-none');
+ toggle_summary_placeholder(show) {
+ if (show) {
+ this.$summary_wrapper.css('display', 'none');
+ this.$component.find('.no-summary-placeholder').css('display', 'flex');
+ } else {
+ this.$summary_wrapper.css('display', 'flex');
+ this.$component.find('.no-summary-placeholder').css('display', 'none');
+ }
}
get_condition_btn_map(after_submission) {
@@ -401,21 +296,22 @@
return [{ condition: true, visible_btns: ['Print Receipt', 'Email Receipt', 'New Order'] }];
return [
- { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order'] },
+ { condition: this.doc.docstatus === 0, visible_btns: ['Edit Order', 'Delete Order'] },
{ condition: !this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt', 'Return']},
{ condition: this.doc.is_return && this.doc.docstatus === 1, visible_btns: ['Print Receipt', 'Email Receipt']}
];
}
load_summary_of(doc, after_submission=false) {
- this.$summary_wrapper.removeClass("d-none");
-
after_submission ?
- this.switch_to_post_submit_summary() : this.switch_to_recent_invoice_summary();
+ this.$component.css('grid-column', 'span 10 / span 10') :
+ this.$component.css('grid-column', 'span 6 / span 6');
+
+ this.toggle_summary_placeholder(false);
this.doc = doc;
- this.attach_basic_info(doc);
+ this.attach_document_info(doc);
this.attach_items_info(doc);
@@ -428,7 +324,7 @@
this.add_summary_btns(condition_btns_map);
}
- attach_basic_info(doc) {
+ attach_document_info(doc) {
frappe.db.get_value('Customer', this.doc.customer, 'email_id').then(({ message }) => {
this.customer_email = message.email_id || '';
const upper_section_dom = this.get_upper_section_html(doc);
@@ -437,19 +333,35 @@
}
attach_items_info(doc) {
- this.$items_summary_container.html('');
+ this.$items_container.html('');
doc.items.forEach(item => {
const item_dom = this.get_item_html(doc, item);
- this.$items_summary_container.append(item_dom);
+ this.$items_container.append(item_dom);
+ this.set_dynamic_rate_header_width();
});
}
+ set_dynamic_rate_header_width() {
+ const rate_cols = Array.from(this.$items_container.find(".item-rate-disc"));
+ this.$items_container.find(".item-rate-disc").css("width", "");
+ let max_width = rate_cols.reduce((max_width, elm) => {
+ if ($(elm).width() > max_width)
+ max_width = $(elm).width();
+ return max_width;
+ }, 0);
+
+ max_width += 1;
+ if (max_width == 1) max_width = "";
+
+ this.$items_container.find(".item-rate-disc").css("width", max_width);
+ }
+
attach_payments_info(doc) {
- this.$payment_summary_container.html('');
+ this.$payment_container.html('');
doc.payments.forEach(p => {
if (p.amount) {
const payment_dom = this.get_payment_html(doc, p);
- this.$payment_summary_container.append(payment_dom);
+ this.$payment_container.append(payment_dom);
}
});
if (doc.redeem_loyalty_points && doc.loyalty_amount) {
@@ -457,20 +369,24 @@
mode_of_payment: 'Loyalty Points',
amount: doc.loyalty_amount,
});
- this.$payment_summary_container.append(payment_dom);
+ this.$payment_container.append(payment_dom);
}
}
attach_totals_info(doc) {
- this.$totals_summary_container.html('');
+ this.$totals_container.html('');
- const discount_dom = this.get_discount_html(doc);
const net_total_dom = this.get_net_total_html(doc);
const taxes_dom = this.get_taxes_html(doc);
+ const discount_dom = this.get_discount_html(doc);
const grand_total_dom = this.get_grand_total_html(doc);
- this.$totals_summary_container.append(discount_dom);
- this.$totals_summary_container.append(net_total_dom);
- this.$totals_summary_container.append(taxes_dom);
- this.$totals_summary_container.append(grand_total_dom);
+ this.$totals_container.append(net_total_dom);
+ this.$totals_container.append(taxes_dom);
+ this.$totals_container.append(discount_dom);
+ this.$totals_container.append(grand_total_dom);
+ }
+
+ toggle_component(show) {
+ show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none');
}
};
\ No newline at end of file
diff --git a/erpnext/selling/page/point_of_sale/pos_payment.js b/erpnext/selling/page/point_of_sale/pos_payment.js
index e4d8965..bcbac3b 100644
--- a/erpnext/selling/page/point_of_sale/pos_payment.js
+++ b/erpnext/selling/page/point_of_sale/pos_payment.js
@@ -1,5 +1,4 @@
-{% include "erpnext/selling/page/point_of_sale/pos_number_pad.js" %}
-
+/* eslint-disable no-unused-vars */
erpnext.PointOfSale.Payment = class {
constructor({ events, wrapper }) {
this.wrapper = wrapper;
@@ -13,43 +12,33 @@
this.initialize_numpad();
this.bind_events();
this.attach_shortcuts();
-
+
}
prepare_dom() {
this.wrapper.append(
- `<section class="col-span-6 flex shadow rounded payment-section bg-white mx-h-70 h-100 d-none">
- <div class="flex flex-col p-16 pt-8 pb-8 w-full">
- <div class="text-grey mb-6 payment-section no-select pointer">
- PAYMENT METHOD<span class="octicon octicon-chevron-down collapse-indicator"></span>
+ `<section class="payment-container">
+ <div class="section-label payment-section">Payment Method</div>
+ <div class="payment-modes"></div>
+ <div class="fields-numpad-container">
+ <div class="fields-section">
+ <div class="section-label">Additional Information</div>
+ <div class="invoice-fields"></div>
</div>
- <div class="payment-modes flex flex-wrap"></div>
- <div class="invoice-details-section"></div>
- <div class="flex mt-auto justify-center w-full">
- <div class="flex flex-col justify-center flex-1 ml-4">
- <div class="flex w-full">
- <div class="totals-remarks items-end justify-end flex flex-1">
- <div class="remarks text-md-0 text-grey mr-auto"></div>
- <div class="totals flex justify-end pt-4"></div>
- </div>
- <div class="number-pad w-40 mb-4 ml-8 d-none"></div>
- </div>
- <div class="flex items-center justify-center mt-4 submit-order h-16 w-full rounded bg-primary text-md text-white no-select pointer text-bold">
- Complete Order
- </div>
- <div class="order-time flex items-center justify-end mt-2 pt-2 pb-2 w-full text-md-0 text-grey no-select pointer d-none"></div>
- </div>
- </div>
+ <div class="number-pad"></div>
</div>
+ <div class="totals-section">
+ <div class="totals"></div>
+ </div>
+ <div class="submit-order-btn">Complete Order</div>
</section>`
- )
- this.$component = this.wrapper.find('.payment-section');
+ );
+ this.$component = this.wrapper.find('.payment-container');
this.$payment_modes = this.$component.find('.payment-modes');
- this.$totals_remarks = this.$component.find('.totals-remarks');
+ this.$totals_section = this.$component.find('.totals-section');
this.$totals = this.$component.find('.totals');
- this.$remarks = this.$component.find('.remarks');
this.$numpad = this.$component.find('.number-pad');
- this.$invoice_details_section = this.$component.find('.invoice-details-section');
+ this.$invoice_fields_section = this.$component.find('.fields-section');
}
make_invoice_fields_control() {
@@ -57,13 +46,8 @@
const fields = doc.invoice_fields;
if (!fields.length) return;
- this.$invoice_details_section.html(
- `<div class="text-grey pb-6 mt-2 pointer no-select">
- ADDITIONAL INFORMATION<span class="octicon octicon-chevron-down collapse-indicator"></span>
- </div>
- <div class="invoice-fields grid grid-cols-2 gap-4 mb-6 d-none"></div>`
- );
- this.$invoice_fields = this.$invoice_details_section.find('.invoice-fields');
+ this.$invoice_fields = this.$invoice_fields_section.find('.invoice-fields');
+ this.$invoice_fields.html('');
const frm = this.events.get_frm();
fields.forEach(df => {
@@ -71,8 +55,10 @@
`<div class="invoice_detail_field ${df.fieldname}-field" data-fieldname="${df.fieldname}"></div>`
);
let df_events = {
- onchange: function() { frm.set_value(this.df.fieldname, this.value); }
- }
+ onchange: function() {
+ frm.set_value(this.df.fieldname, this.value);
+ }
+ };
if (df.fieldtype == "Button") {
df_events = {
click: function() {
@@ -80,11 +66,11 @@
frm.script_manager.trigger(df.fieldname, frm.doc.doctype, frm.doc.docname);
}
}
- }
+ };
}
this[`${df.fieldname}_field`] = frappe.ui.form.make_control({
- df: {
+ df: {
...df,
...df_events
},
@@ -92,7 +78,7 @@
render_input: true,
});
this[`${df.fieldname}_field`].set_value(frm.doc[df.fieldname]);
- })
+ });
});
}
@@ -112,13 +98,12 @@
[ 7, 8, 9 ],
[ '.', 0, 'Delete' ]
],
- })
+ });
this.numpad_value = '';
}
on_numpad_clicked($btn) {
- const me = this;
const button_value = $btn.attr('data-button-value');
highlight_numpad_btn($btn);
@@ -127,9 +112,9 @@
this.selected_mode.set_value(this.numpad_value);
function highlight_numpad_btn($btn) {
- $btn.addClass('shadow-inner bg-selected');
+ $btn.addClass('shadow-base-inner bg-selected');
setTimeout(() => {
- $btn.removeClass('shadow-inner bg-selected');
+ $btn.removeClass('shadow-base-inner bg-selected');
}, 100);
}
}
@@ -142,13 +127,16 @@
// if clicked element doesn't have .mode-of-payment class then return
if (!$(e.target).is(mode_clicked)) return;
+ const scrollLeft = mode_clicked.offset().left - me.$payment_modes.offset().left + me.$payment_modes.scrollLeft();
+ me.$payment_modes.animate({ scrollLeft });
+
const mode = mode_clicked.attr('data-mode');
// hide all control fields and shortcuts
- $(`.mode-of-payment-control`).addClass('d-none');
- $(`.cash-shortcuts`).addClass('d-none');
- me.$payment_modes.find(`.pay-amount`).removeClass('d-none');
- me.$payment_modes.find(`.loyalty-amount-name`).addClass('d-none');
+ $(`.mode-of-payment-control`).css('display', 'none');
+ $(`.cash-shortcuts`).css('display', 'none');
+ me.$payment_modes.find(`.pay-amount`).css('display', 'inline');
+ me.$payment_modes.find(`.loyalty-amount-name`).css('display', 'none');
// remove highlight from all mode-of-payments
$('.mode-of-payment').removeClass('border-primary');
@@ -157,70 +145,60 @@
// clicked one is selected then unselect it
mode_clicked.removeClass('border-primary');
me.selected_mode = '';
- me.toggle_numpad(false);
} else {
// clicked one is not selected then select it
mode_clicked.addClass('border-primary');
- mode_clicked.find('.mode-of-payment-control').removeClass('d-none');
- mode_clicked.find('.cash-shortcuts').removeClass('d-none');
- me.$payment_modes.find(`.${mode}-amount`).addClass('d-none');
- me.$payment_modes.find(`.${mode}-name`).removeClass('d-none');
- me.toggle_numpad(true);
+ mode_clicked.find('.mode-of-payment-control').css('display', 'flex');
+ mode_clicked.find('.cash-shortcuts').css('display', 'grid');
+ me.$payment_modes.find(`.${mode}-amount`).css('display', 'none');
+ me.$payment_modes.find(`.${mode}-name`).css('display', 'inline');
me.selected_mode = me[`${mode}_control`];
- const doc = me.events.get_frm().doc;
- me.selected_mode?.$input?.get(0).focus();
- const current_value = me.selected_mode?.get_value()
- !current_value && doc.grand_total > doc.paid_amount ? me.selected_mode?.set_value(doc.grand_total - doc.paid_amount) : '';
+ me.selected_mode && me.selected_mode.$input.get(0).focus();
+ me.auto_set_remaining_amount();
}
- })
-
- frappe.realtime.on("process_phone_payment", function(data) {
- frappe.dom.unfreeze();
- cur_frm.reload_doc();
- let message = data["ResultDesc"];
- let title = __("Payment Failed");
-
- if (data["ResultCode"] == 0) {
- title = __("Payment Received");
- $('.btn.btn-xs.btn-default[data-fieldname=request_for_payment]').html(`Payment Received`)
- me.events.submit_invoice();
- }
-
- frappe.msgprint({
- "message": message,
- "title": title
- });
});
- this.$payment_modes.on('click', '.shortcut', function(e) {
+ frappe.ui.form.on('POS Invoice', 'contact_mobile', (frm) => {
+ const contact = frm.doc.contact_mobile;
+ const request_button = $(this.request_for_payment_field.$input[0]);
+ if (contact) {
+ request_button.removeClass('btn-default').addClass('btn-primary');
+ } else {
+ request_button.removeClass('btn-primary').addClass('btn-default');
+ }
+ });
+
+ this.setup_listener_for_payments();
+
+ this.$payment_modes.on('click', '.shortcut', () => {
const value = $(this).attr('data-value');
me.selected_mode.set_value(value);
- })
+ });
- this.$component.on('click', '.submit-order', () => {
+ this.$component.on('click', '.submit-order-btn', () => {
const doc = this.events.get_frm().doc;
const paid_amount = doc.paid_amount;
const items = doc.items;
if (paid_amount == 0 || !items.length) {
- const message = items.length ? __("You cannot submit the order without payment.") : __("You cannot submit empty order.")
+ const message = items.length ? __("You cannot submit the order without payment.") : __("You cannot submit empty order.");
frappe.show_alert({ message, indicator: "orange" });
frappe.utils.play_sound("error");
return;
}
this.events.submit_invoice();
- })
+ });
frappe.ui.form.on('POS Invoice', 'paid_amount', (frm) => {
this.update_totals_section(frm.doc);
// need to re calculate cash shortcuts after discount is applied
- const is_cash_shortcuts_invisible = this.$payment_modes.find('.cash-shortcuts').hasClass('d-none');
+ const is_cash_shortcuts_invisible = !this.$payment_modes.find('.cash-shortcuts').is(':visible');
this.attach_cash_shortcuts(frm.doc);
- !is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').removeClass('d-none');
- })
+ !is_cash_shortcuts_invisible && this.$payment_modes.find('.cash-shortcuts').css('display', 'grid');
+ });
frappe.ui.form.on('POS Invoice', 'loyalty_amount', (frm) => {
const formatted_currency = format_currency(frm.doc.loyalty_amount, frm.doc.currency);
@@ -235,29 +213,51 @@
this[`${mode}_control`].set_value(default_mop.amount);
}
});
+ }
- this.$component.on('click', '.invoice-details-section', function(e) {
- if ($(e.target).closest('.invoice-fields').length) return;
+ setup_listener_for_payments() {
+ frappe.realtime.on("process_phone_payment", (data) => {
+ const doc = this.events.get_frm().doc;
+ const { response, amount, success, failure_message } = data;
+ let message, title;
- me.$payment_modes.addClass('d-none');
- me.$invoice_fields.toggleClass("d-none");
- me.toggle_numpad(false);
+ if (success) {
+ title = __("Payment Received");
+ if (amount >= doc.grand_total) {
+ frappe.dom.unfreeze();
+ message = __("Payment of {0} received successfully.", [format_currency(amount, doc.currency, 0)]);
+ this.events.submit_invoice();
+ cur_frm.reload_doc();
+
+ } else {
+ message = __("Payment of {0} received successfully. Waiting for other requests to complete...", [format_currency(amount, doc.currency, 0)]);
+ }
+ } else if (failure_message) {
+ message = failure_message;
+ title = __("Payment Failed");
+ }
+
+ frappe.msgprint({ "message": message, "title": title });
});
- this.$component.on('click', '.payment-section', () => {
- this.$invoice_fields.addClass("d-none");
- this.$payment_modes.toggleClass('d-none');
- this.toggle_numpad(true);
- })
+ }
+
+ auto_set_remaining_amount() {
+ const doc = this.events.get_frm().doc;
+ const remaining_amount = doc.grand_total - doc.paid_amount;
+ const current_value = this.selected_mode ? this.selected_mode.get_value() : undefined;
+ if (!current_value && remaining_amount > 0 && this.selected_mode) {
+ this.selected_mode.set_value(remaining_amount);
+ }
}
attach_shortcuts() {
const ctrl_label = frappe.utils.is_mac() ? '⌘' : 'Ctrl';
- this.$component.find('.submit-order').attr("title", `${ctrl_label}+Enter`);
+ this.$component.find('.submit-order-btn').attr("title", `${ctrl_label}+Enter`);
frappe.ui.keys.on("ctrl+enter", () => {
const payment_is_visible = this.$component.is(":visible");
const active_mode = this.$payment_modes.find(".border-primary");
if (payment_is_visible && active_mode.length) {
- this.$component.find('.submit-order').click();
+ this.$component.find('.submit-order-btn').click();
}
});
@@ -267,14 +267,14 @@
const payment_is_visible = this.$component.is(":visible");
let active_mode = this.$payment_modes.find(".border-primary");
active_mode = active_mode.length ? active_mode.attr("data-mode") : undefined;
-
+
if (!active_mode) return;
-
+
const mode_of_payments = Array.from(this.$payment_modes.find(".mode-of-payment")).map(m => $(m).attr("data-mode"));
const mode_index = mode_of_payments.indexOf(active_mode);
const next_mode_index = (mode_index + 1) % mode_of_payments.length;
const next_mode_to_be_clicked = this.$payment_modes.find(`.mode-of-payment[data-mode="${mode_of_payments[next_mode_index]}"]`);
-
+
if (payment_is_visible && mode_index != next_mode_index) {
next_mode_to_be_clicked.click();
}
@@ -286,16 +286,8 @@
});
}
- toggle_numpad(show) {
- if (show) {
- this.$numpad.removeClass('d-none');
- this.$remarks.addClass('d-none');
- this.$totals_remarks.addClass('w-60 justify-center').removeClass('justify-end w-full');
- } else {
- this.$numpad.addClass('d-none');
- this.$remarks.removeClass('d-none');
- this.$totals_remarks.removeClass('w-60 justify-center').addClass('justify-end w-full');
- }
+ toggle_numpad() {
+ // pass
}
render_payment_section() {
@@ -327,7 +319,7 @@
fieldtype: 'Data',
onchange: function() {}
},
- parent: this.$totals_remarks.find(`.remarks`),
+ parent: this.$totals_section.find(`.remarks`),
render_input: true,
});
this[`remark_control`].set_value('');
@@ -339,27 +331,24 @@
const payments = doc.payments;
const currency = doc.currency;
- this.$payment_modes.html(
- `${
- payments.map((p, i) => {
+ this.$payment_modes.html(`${
+ payments.map((p, i) => {
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
const payment_type = p.type;
const margin = i % 2 === 0 ? 'pr-2' : 'pl-2';
const amount = p.amount > 0 ? format_currency(p.amount, currency) : '';
- return (
- `<div class="w-half ${margin} bg-white">
- <div class="mode-of-payment rounded border border-grey text-grey text-md
- mb-4 p-8 pt-4 pb-4 no-select pointer" data-mode="${mode}" data-payment-type="${payment_type}">
+ return (`
+ <div class="payment-mode-wrapper">
+ <div class="mode-of-payment" data-mode="${mode}" data-payment-type="${payment_type}">
${p.mode_of_payment}
- <div class="${mode}-amount pay-amount inline float-right text-bold">${amount}</div>
- <div class="${mode} mode-of-payment-control mt-4 flex flex-1 items-center d-none"></div>
+ <div class="${mode}-amount pay-amount">${amount}</div>
+ <div class="${mode} mode-of-payment-control"></div>
</div>
- </div>`
- )
- }).join('')
- }`
- )
+ </div>
+ `);
+ }).join('')
+ }`);
payments.forEach(p => {
const mode = p.mode_of_payment.replace(/ +/g, "_").toLowerCase();
@@ -370,9 +359,11 @@
fieldtype: 'Currency',
placeholder: __('Enter {0} amount.', [p.mode_of_payment]),
onchange: function() {
- if (this.value || this.value == 0) {
- frappe.model.set_value(p.doctype, p.name, 'amount', flt(this.value))
- .then(() => me.update_totals_section());
+ const current_value = frappe.model.get_value(p.doctype, p.name, 'amount');
+ if (current_value != this.value) {
+ frappe.model
+ .set_value(p.doctype, p.name, 'amount', flt(this.value))
+ .then(() => me.update_totals_section())
const formatted_currency = format_currency(this.value, currency);
me.$payment_modes.find(`.${mode}-amount`).html(formatted_currency);
@@ -390,10 +381,10 @@
this.$payment_modes.find(`.${mode}.mode-of-payment-control`).parent().click();
}, 500);
}
- })
+ });
this.render_loyalty_points_payment_mode();
-
+
this.attach_cash_shortcuts(doc);
}
@@ -404,17 +395,12 @@
const shortcuts = this.get_cash_shortcuts(flt(grand_total));
this.$payment_modes.find('.cash-shortcuts').remove();
- this.$payment_modes.find('[data-payment-type="Cash"]').find('.mode-of-payment-control').after(
- `<div class="cash-shortcuts grid grid-cols-3 gap-2 flex-1 text-center text-md-0 mb-2 d-none">
- ${
- shortcuts.map(s => {
- return `<div class="shortcut rounded bg-light-grey text-dark-grey pt-2 pb-2 no-select pointer" data-value="${s}">
- ${format_currency(s, currency, 0)}
- </div>`
- }).join('')
- }
- </div>`
- )
+ let shortcuts_html = shortcuts.map(s => {
+ return `<div class="shortcut" data-value="${s}">${format_currency(s, currency, 0)}</div>`;
+ }).join('');
+
+ this.$payment_modes.find('[data-payment-type="Cash"]').find('.mode-of-payment-control')
+ .after(`<div class="cash-shortcuts">${shortcuts_html}</div>`);
}
get_cash_shortcuts(grand_total) {
@@ -426,13 +412,13 @@
const get_nearest = (amount, x) => {
let nearest_x = Math.ceil((amount / x)) * x;
return nearest_x === amount ? nearest_x + x : nearest_x;
- }
+ };
return steps.reduce((finalArr, x) => {
let nearest_x = get_nearest(grand_total, x);
nearest_x = finalArr.indexOf(nearest_x) != -1 ? nearest_x + x : nearest_x;
return [...finalArr, nearest_x];
- }, []);
+ }, []);
}
render_loyalty_points_payment_mode() {
@@ -441,7 +427,7 @@
const { loyalty_program, loyalty_points, conversion_factor } = this.events.get_customer_details();
this.$payment_modes.find(`.mode-of-payment[data-mode="loyalty-amount"]`).parent().remove();
-
+
if (!loyalty_program) return;
let description, read_only, max_redeemable_amount;
@@ -449,7 +435,7 @@
description = __("You don't have enough points to redeem.");
read_only = true;
} else {
- max_redeemable_amount = flt(flt(loyalty_points) * flt(conversion_factor), precision("loyalty_amount", doc))
+ max_redeemable_amount = flt(flt(loyalty_points) * flt(conversion_factor), precision("loyalty_amount", doc));
description = __("You can redeem upto {0}.", [format_currency(max_redeemable_amount)]);
read_only = false;
}
@@ -457,16 +443,15 @@
const margin = this.$payment_modes.children().length % 2 === 0 ? 'pr-2' : 'pl-2';
const amount = doc.loyalty_amount > 0 ? format_currency(doc.loyalty_amount, doc.currency) : '';
this.$payment_modes.append(
- `<div class="w-half ${margin} bg-white">
- <div class="mode-of-payment rounded border border-grey text-grey text-md
- mb-4 p-8 pt-4 pb-4 no-select pointer" data-mode="loyalty-amount" data-payment-type="loyalty-amount">
+ `<div class="payment-mode-wrapper">
+ <div class="mode-of-payment" data-mode="loyalty-amount" data-payment-type="loyalty-amount">
Redeem Loyalty Points
- <div class="loyalty-amount-amount pay-amount inline float-right text-bold">${amount}</div>
- <div class="loyalty-amount-name inline float-right text-bold text-md-0 d-none">${loyalty_program}</div>
- <div class="loyalty-amount mode-of-payment-control mt-4 flex flex-1 items-center d-none"></div>
+ <div class="loyalty-amount-amount pay-amount">${amount}</div>
+ <div class="loyalty-amount-name">${loyalty_program}</div>
+ <div class="loyalty-amount mode-of-payment-control"></div>
</div>
</div>`
- )
+ );
this['loyalty-amount_control'] = frappe.ui.form.make_control({
df: {
@@ -508,7 +493,7 @@
`<div class="w-full pr-2">
<div class="add-mode-of-payment w-half text-grey mb-4 no-select pointer">+ Add Payment Method</div>
</div>`
- )
+ );
}
update_totals_section(doc) {
@@ -516,22 +501,28 @@
const paid_amount = doc.paid_amount;
const remaining = doc.grand_total - doc.paid_amount;
const change = doc.change_amount || remaining <= 0 ? -1 * remaining : undefined;
- const currency = doc.currency
+ const currency = doc.currency;
const label = change ? __('Change') : __('To Be Paid');
this.$totals.html(
- `<div>
- <div class="pr-8 border-r-grey">Paid Amount</div>
- <div class="pr-8 border-r-grey text-bold text-2xl">${format_currency(paid_amount, currency)}</div>
+ `<div class="col">
+ <div class="total-label">Grand Total</div>
+ <div class="value">${format_currency(doc.grand_total, currency)}</div>
</div>
- <div>
- <div class="pl-8">${label}</div>
- <div class="pl-8 text-green-400 text-bold text-2xl">${format_currency(change || remaining, currency)}</div>
+ <div class="seperator-y"></div>
+ <div class="col">
+ <div class="total-label">Paid Amount</div>
+ <div class="value">${format_currency(paid_amount, currency)}</div>
+ </div>
+ <div class="seperator-y"></div>
+ <div class="col">
+ <div class="total-label">${label}</div>
+ <div class="value">${format_currency(change || remaining, currency)}</div>
</div>`
- )
+ );
}
toggle_component(show) {
- show ? this.$component.removeClass('d-none') : this.$component.addClass('d-none');
+ show ? this.$component.css('display', 'flex') : this.$component.css('display', 'none');
}
- }
\ No newline at end of file
+};
\ No newline at end of file
diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
index c716aa9..8473276 100644
--- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
+++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py
@@ -10,8 +10,8 @@
def execute(filters=None):
filters = frappe._dict(filters or {})
if filters.from_date > filters.to_date:
- frappe.throw(_('From Date cannot be greater than To Date'))
-
+ frappe.throw(_("From Date cannot be greater than To Date"))
+
columns = get_columns(filters)
data = get_data(filters)
@@ -148,14 +148,16 @@
company_list.append(filters.get("company"))
customer_details = get_customer_details()
+ item_details = get_item_details()
sales_order_records = get_sales_order_details(company_list, filters)
for record in sales_order_records:
customer_record = customer_details.get(record.customer)
+ item_record = item_details.get(record.item_code)
row = {
"item_code": record.item_code,
- "item_name": record.item_name,
- "item_group": record.item_group,
+ "item_name": item_record.item_name,
+ "item_group": item_record.item_group,
"description": record.description,
"quantity": record.qty,
"uom": record.uom,
@@ -196,8 +198,8 @@
return conditions
def get_customer_details():
- details = frappe.get_all('Customer',
- fields=['name', 'customer_name', "customer_group"])
+ details = frappe.get_all("Customer",
+ fields=["name", "customer_name", "customer_group"])
customer_details = {}
for d in details:
customer_details.setdefault(d.name, frappe._dict({
@@ -206,15 +208,25 @@
}))
return customer_details
+def get_item_details():
+ details = frappe.db.get_all("Item",
+ fields=["item_code", "item_name", "item_group"])
+ item_details = {}
+ for d in details:
+ item_details.setdefault(d.item_code, frappe._dict({
+ "item_name": d.item_name,
+ "item_group": d.item_group
+ }))
+ return item_details
+
def get_sales_order_details(company_list, filters):
conditions = get_conditions(filters)
return frappe.db.sql("""
SELECT
- so_item.item_code, so_item.item_name, so_item.item_group,
- so_item.description, so_item.qty, so_item.uom,
- so_item.base_rate, so_item.base_amount, so.name,
- so.transaction_date, so.customer, so.territory,
+ so_item.item_code, so_item.description, so_item.qty,
+ so_item.uom, so_item.base_rate, so_item.base_amount,
+ so.name, so.transaction_date, so.customer,so.territory,
so.project, so_item.delivered_qty,
so_item.billed_amt, so.company
FROM
diff --git a/erpnext/selling/report/sales_analytics/sales_analytics.js b/erpnext/selling/report/sales_analytics/sales_analytics.js
index 0e565a3..9089b53 100644
--- a/erpnext/selling/report/sales_analytics/sales_analytics.js
+++ b/erpnext/selling/report/sales_analytics/sales_analytics.js
@@ -74,67 +74,71 @@
return Object.assign(options, {
checkboxColumn: true,
events: {
- onCheckRow: function(data) {
+ onCheckRow: function (data) {
+ if (!data) return;
+ const data_doctype = $(
+ data[2].html
+ )[0].attributes.getNamedItem("data-doctype").value;
+ const tree_type = frappe.query_report.filters[0].value;
+ if (data_doctype != tree_type) return;
+
row_name = data[2].content;
length = data.length;
- var tree_type = frappe.query_report.filters[0].value;
-
- if(tree_type == "Customer") {
- row_values = data.slice(4,length-1).map(function (column) {
- return column.content;
- })
+ if (tree_type == "Customer") {
+ row_values = data
+ .slice(4, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
} else if (tree_type == "Item") {
- row_values = data.slice(5,length-1).map(function (column) {
- return column.content;
- })
- }
- else {
- row_values = data.slice(3,length-1).map(function (column) {
- return column.content;
- })
+ row_values = data
+ .slice(5, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
+ } else {
+ row_values = data
+ .slice(3, length - 1)
+ .map(function (column) {
+ return column.content;
+ });
}
entry = {
- 'name':row_name,
- 'values':row_values
- }
+ name: row_name,
+ values: row_values,
+ };
let raw_data = frappe.query_report.chart.data;
let new_datasets = raw_data.datasets;
- var found = false;
-
- for(var i=0; i < new_datasets.length;i++){
- if(new_datasets[i].name == row_name){
- found = true;
- new_datasets.splice(i,1);
- break;
+ let element_found = new_datasets.some((element, index, array)=>{
+ if(element.name == row_name){
+ array.splice(index, 1)
+ return true
}
- }
+ return false
+ })
- if(!found){
+ if (!element_found) {
new_datasets.push(entry);
}
let new_data = {
labels: raw_data.labels,
- datasets: new_datasets
- }
-
- setTimeout(() => {
- frappe.query_report.chart.update(new_data)
- }, 500)
-
-
- setTimeout(() => {
- frappe.query_report.chart.draw(true);
- }, 1000)
+ datasets: new_datasets,
+ };
+ chart_options = {
+ data: new_data,
+ type: "line",
+ };
+ frappe.query_report.render_chart(chart_options);
frappe.query_report.raw_chart_data = new_data;
},
- }
- })
+ },
+ });
},
}
diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js
index 7f00fca..ce08464 100644
--- a/erpnext/selling/sales_common.js
+++ b/erpnext/selling/sales_common.js
@@ -399,6 +399,10 @@
}
},
+ batch_no: function(doc, cdt, cdn) {
+ this._super(doc, cdt, cdn);
+ },
+
qty: function(doc, cdt, cdn) {
this._super(doc, cdt, cdn);
diff --git a/erpnext/selling/workspace/retail/retail.json b/erpnext/selling/workspace/retail/retail.json
new file mode 100644
index 0000000..e20f834
--- /dev/null
+++ b/erpnext/selling/workspace/retail/retail.json
@@ -0,0 +1,114 @@
+{
+ "category": "Domains",
+ "charts": [],
+ "creation": "2020-03-02 17:18:32.505616",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "retail",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Retail",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings & Configurations",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Point-of-Sale Profile",
+ "link_to": "POS Profile",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "POS Settings",
+ "link_to": "POS Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loyalty Program",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loyalty Program",
+ "link_to": "Loyalty Program",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Loyalty Point Entry",
+ "link_to": "Loyalty Point Entry",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Opening & Closing",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "POS Opening Entry",
+ "link_to": "POS Opening Entry",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "POS Closing Entry",
+ "link_to": "POS Closing Entry",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:36.758038",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Retail",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "restrict_to_domain": "Retail",
+ "shortcuts": [
+ {
+ "doc_view": "",
+ "label": "Point Of Sale",
+ "link_to": "point-of-sale",
+ "type": "Page"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/selling/workspace/selling/selling.json b/erpnext/selling/workspace/selling/selling.json
new file mode 100644
index 0000000..879034a
--- /dev/null
+++ b/erpnext/selling/workspace/selling/selling.json
@@ -0,0 +1,563 @@
+{
+ "category": "Modules",
+ "charts": [
+ {
+ "chart_name": "Sales Order Trends",
+ "label": "Sales Order Trends"
+ }
+ ],
+ "charts_label": "Selling ",
+ "creation": "2020-01-28 11:49:12.092882",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 1,
+ "icon": "sell",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Selling",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Selling",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Customer",
+ "link_to": "Customer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Customer",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quotation",
+ "link_to": "Quotation",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Customer",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Order",
+ "link_to": "Sales Order",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Customer",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Invoice",
+ "link_to": "Sales Invoice",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Customer",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Blanket Order",
+ "link_to": "Blanket Order",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Partner",
+ "link_to": "Sales Partner",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Customer",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Person",
+ "link_to": "Sales Person",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Items and Pricing",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item",
+ "link_to": "Item",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Price List",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Price",
+ "link_to": "Item Price",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Price List",
+ "link_to": "Price List",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Group",
+ "link_to": "Item Group",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Product Bundle",
+ "link_to": "Product Bundle",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Promotional Scheme",
+ "link_to": "Promotional Scheme",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Pricing Rule",
+ "link_to": "Pricing Rule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shipping Rule",
+ "link_to": "Shipping Rule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Coupon Code",
+ "link_to": "Coupon Code",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Selling Settings",
+ "link_to": "Selling Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Terms and Conditions Template",
+ "link_to": "Terms and Conditions",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Taxes and Charges Template",
+ "link_to": "Sales Taxes and Charges Template",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Lead Source",
+ "link_to": "Lead Source",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Customer Group",
+ "link_to": "Customer Group",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Contact",
+ "link_to": "Contact",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Address",
+ "link_to": "Address",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Territory",
+ "link_to": "Territory",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Campaign",
+ "link_to": "Campaign",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Key Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Analytics",
+ "link_to": "Sales Analytics",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Order Analysis",
+ "link_to": "Sales Order Analysis",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Sales Funnel",
+ "link_to": "sales-funnel",
+ "link_type": "Page",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Order Trends",
+ "link_to": "Sales Order Trends",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Quotation",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Quotation Trends",
+ "link_to": "Quotation Trends",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Customer",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Customer Acquisition and Loyalty",
+ "link_to": "Customer Acquisition and Loyalty",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Inactive Customers",
+ "link_to": "Inactive Customers",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Person-wise Transaction Summary",
+ "link_to": "Sales Person-wise Transaction Summary",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Item-wise Sales History",
+ "link_to": "Item-wise Sales History",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Other Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Lead",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Lead Details",
+ "link_to": "Lead Details",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Address",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Customer Addresses And Contacts",
+ "link_to": "Address And Contacts",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Available Stock for Packing Items",
+ "link_to": "Available Stock for Packing Items",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Pending SO Items For Purchase Request",
+ "link_to": "Pending SO Items For Purchase Request",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Delivery Note",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Delivery Note Trends",
+ "link_to": "Delivery Note Trends",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Invoice",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Invoice Trends",
+ "link_to": "Sales Invoice Trends",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Customer",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Customer Credit Balance",
+ "link_to": "Customer Credit Balance",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Customer",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Customers Without Any Sales Transactions",
+ "link_to": "Customers Without Any Sales Transactions",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Customer",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Partners Commission",
+ "link_to": "Sales Partners Commission",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Territory Target Variance Based On Item Group",
+ "link_to": "Territory Target Variance Based On Item Group",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Person Target Variance Based On Item Group",
+ "link_to": "Sales Person Target Variance Based On Item Group",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Partner Target Variance Based On Item Group",
+ "link_to": "Sales Partner Target Variance based on Item Group",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:35.971277",
+ "modified_by": "Administrator",
+ "module": "Selling",
+ "name": "Selling",
+ "onboarding": "Selling",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "color": "Grey",
+ "format": "{} Available",
+ "label": "Item",
+ "link_to": "Item",
+ "stats_filter": "{\n \"disabled\":0\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Yellow",
+ "format": "{} To Deliver",
+ "label": "Sales Order",
+ "link_to": "Sales Order",
+ "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\":[\"in\", [\"To Deliver\", \"To Deliver and Bill\"]]\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Grey",
+ "format": "{} Open",
+ "label": "Sales Analytics",
+ "link_to": "Sales Analytics",
+ "stats_filter": "{ \"Status\": \"Open\" }",
+ "type": "Report"
+ },
+ {
+ "label": "Sales Order Analysis",
+ "link_to": "Sales Order Analysis",
+ "type": "Report"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "Selling",
+ "type": "Dashboard"
+ }
+ ],
+ "shortcuts_label": "Quick Access"
+}
\ No newline at end of file
diff --git a/erpnext/setup/desk_page/home/home.json b/erpnext/setup/desk_page/home/home.json
deleted file mode 100644
index 9cf9f41..0000000
--- a/erpnext/setup/desk_page/home/home.json
+++ /dev/null
@@ -1,99 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Healthcare",
- "links": "[\n {\n \"label\": \"Patient\",\n \"name\": \"Patient\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Diagnosis\",\n \"name\": \"Diagnosis\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Agriculture",
- "links": "[\n {\n \"label\": \"Crop\",\n \"name\": \"Crop\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Crop Cycle\",\n \"name\": \"Crop Cycle\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Location\",\n \"name\": \"Location\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Fertilizer\",\n \"name\": \"Fertilizer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Education",
- "links": "[\n {\n \"label\": \"Student\",\n \"name\": \"Student\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Course\",\n \"name\": \"Course\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Instructor\",\n \"name\": \"Instructor\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Room\",\n \"name\": \"Room\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Non Profit",
- "links": "[\n {\n \"description\": \"Member information.\",\n \"label\": \"Member\",\n \"name\": \"Member\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Volunteer information.\",\n \"label\": \"Volunteer\",\n \"name\": \"Volunteer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Chapter information.\",\n \"label\": \"Chapter\",\n \"name\": \"Chapter\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Donor information.\",\n \"label\": \"Donor\",\n \"name\": \"Donor\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Stock",
- "links": "[\n {\n \"label\": \"Warehouse\",\n \"name\": \"Warehouse\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Brand\",\n \"name\": \"Brand\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Unit of Measure (UOM)\",\n \"name\": \"UOM\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Stock Reconciliation\",\n \"name\": \"Stock Reconciliation\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Human Resources",
- "links": "[\n {\n \"label\": \"Employee\",\n \"name\": \"Employee\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Employee\"\n ],\n \"hide_count\": true,\n \"label\": \"Employee Attendance Tool\",\n \"name\": \"Employee Attendance Tool\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Salary Structure\",\n \"name\": \"Salary Structure\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "CRM",
- "links": "[\n {\n \"description\": \"Database of potential customers.\",\n \"label\": \"Lead\",\n \"name\": \"Lead\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Customer Group Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Customer Group\",\n \"link\": \"Tree/Customer Group\",\n \"name\": \"Customer Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Manage Territory Tree.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Territory\",\n \"link\": \"Tree/Territory\",\n \"name\": \"Territory\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Accounting",
- "links": "[\n {\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Customer database.\",\n \"label\": \"Customer\",\n \"name\": \"Customer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Supplier database.\",\n \"label\": \"Supplier\",\n \"name\": \"Supplier\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Company (not Customer or Supplier) master.\",\n \"label\": \"Company\",\n \"name\": \"Company\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Tree of financial accounts.\",\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Chart of Accounts\",\n \"name\": \"Account\",\n \"onboard\": 1,\n \"route\": \"#Tree/Account\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Create Opening Sales and Purchase Invoices\",\n \"label\": \"Opening Invoice Creation Tool\",\n \"name\": \"Opening Invoice Creation Tool\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Data Import and Settings",
- "links": "[\n {\n \"description\": \"Import Data from CSV / Excel files.\",\n \"icon\": \"octicon octicon-cloud-upload\",\n \"label\": \"Import Data\",\n \"name\": \"Data Import\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Import Chart of Accounts from CSV / Excel files\",\n \"label\": \"Chart of Accounts Importer\",\n \"label\": \"Chart of Accounts Importer\",\n \"name\": \"Chart of Accounts Importer\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Letter Heads for print templates.\",\n \"label\": \"Letter Head\",\n \"name\": \"Letter Head\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Add / Manage Email Accounts.\",\n \"label\": \"Email Account\",\n \"name\": \"Email Account\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [],
- "creation": "2020-01-23 13:46:38.833076",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Home",
- "modified": "2020-05-11 10:20:37.358701",
- "modified_by": "Administrator",
- "module": "Setup",
- "name": "Home",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 1,
- "shortcuts": [
- {
- "label": "Item",
- "link_to": "Item",
- "type": "DocType"
- },
- {
- "label": "Customer",
- "link_to": "Customer",
- "type": "DocType"
- },
- {
- "label": "Supplier",
- "link_to": "Supplier",
- "type": "DocType"
- },
- {
- "label": "Sales Invoice",
- "link_to": "Sales Invoice",
- "type": "DocType"
- },
- {
- "label": "Dashboard",
- "link_to": "dashboard",
- "type": "Page"
- },
- {
- "label": "Leaderboard",
- "link_to": "leaderboard",
- "type": "Page"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js
index cbf67b4..36033d9 100644
--- a/erpnext/setup/doctype/company/company.js
+++ b/erpnext/setup/doctype/company/company.js
@@ -274,7 +274,8 @@
["default_employee_advance_account", {"root_type": "Asset"}],
["expenses_included_in_asset_valuation", {"account_type": "Expenses Included In Asset Valuation"}],
["capital_work_in_progress_account", {"account_type": "Capital Work in Progress"}],
- ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}]
+ ["asset_received_but_not_billed", {"account_type": "Asset Received But Not Billed"}],
+ ["unrealized_profit_loss_account", {"root_type": "Liability"}]
], function(i, v) {
erpnext.company.set_custom_query(frm, v);
});
diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json
index 40938ea..d49ae7c 100644
--- a/erpnext/setup/doctype/company/company.json
+++ b/erpnext/setup/doctype/company/company.json
@@ -46,10 +46,9 @@
"round_off_account",
"round_off_cost_center",
"write_off_account",
- "discount_allowed_account",
- "discount_received_account",
"exchange_gain_loss_account",
"unrealized_exchange_gain_loss_account",
+ "unrealized_profit_loss_account",
"column_break0",
"allow_account_creation_against_child_company",
"default_payable_account",
@@ -261,14 +260,14 @@
{
"fieldname": "create_chart_of_accounts_based_on",
"fieldtype": "Select",
- "label": "Create Chart of Accounts Based on",
+ "label": "Create Chart Of Accounts Based On",
"options": "\nStandard Template\nExisting Company"
},
{
"depends_on": "eval:doc.create_chart_of_accounts_based_on===\"Standard Template\"",
"fieldname": "chart_of_accounts",
"fieldtype": "Select",
- "label": "Chart of Accounts Template",
+ "label": "Chart Of Accounts Template",
"no_copy": 1
},
{
@@ -346,18 +345,6 @@
"options": "Account"
},
{
- "fieldname": "discount_allowed_account",
- "fieldtype": "Link",
- "label": "Discount Allowed Account",
- "options": "Account"
- },
- {
- "fieldname": "discount_received_account",
- "fieldtype": "Link",
- "label": "Discount Received Account",
- "options": "Account"
- },
- {
"fieldname": "exchange_gain_loss_account",
"fieldtype": "Link",
"label": "Exchange Gain / Loss Account",
@@ -740,6 +727,12 @@
"fieldtype": "Link",
"label": "Default In Transit Warehouse",
"options": "Warehouse"
+ },
+ {
+ "fieldname": "unrealized_profit_loss_account",
+ "fieldtype": "Link",
+ "label": "Unrealized Profit / Loss Account",
+ "options": "Account"
}
],
"icon": "fa fa-building",
@@ -747,7 +740,7 @@
"image_field": "company_logo",
"is_tree": 1,
"links": [],
- "modified": "2020-08-06 00:38:08.311216",
+ "modified": "2020-12-03 12:27:27.085094",
"modified_by": "Administrator",
"module": "Setup",
"name": "Company",
@@ -808,4 +801,4 @@
"sort_field": "modified",
"sort_order": "ASC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py
index 4a438f7..819ba78 100644
--- a/erpnext/setup/doctype/company/company.py
+++ b/erpnext/setup/doctype/company/company.py
@@ -75,7 +75,7 @@
def validate_default_accounts(self):
accounts = [
- ["Default Bank Account", "default_bank_account"], ["Default Cash Account", "default_cash_account"],
+ ["Default Bank Account", "default_bank_account"], ["Default Cash Account", "default_cash_account"],
["Default Receivable Account", "default_receivable_account"], ["Default Payable Account", "default_payable_account"],
["Default Expense Account", "default_expense_account"], ["Default Income Account", "default_income_account"],
["Stock Received But Not Billed Account", "stock_received_but_not_billed"], ["Stock Adjustment Account", "stock_adjustment_account"],
@@ -89,8 +89,9 @@
frappe.throw(_("Account {0} does not belong to company: {1}").format(self.get(account[1]), self.name))
if get_account_currency(self.get(account[1])) != self.default_currency:
- frappe.throw(_("""{0} currency must be same as company's default currency.
- Please select another account""").format(frappe.bold(account[0])))
+ error_message = _("{0} currency must be same as company's default currency. Please select another account.") \
+ .format(frappe.bold(account[0]))
+ frappe.throw(error_message)
def validate_currency(self):
if self.is_new():
diff --git a/erpnext/setup/doctype/company/delete_company_transactions.py b/erpnext/setup/doctype/company/delete_company_transactions.py
index 566f20c..0df4c87 100644
--- a/erpnext/setup/doctype/company/delete_company_transactions.py
+++ b/erpnext/setup/doctype/company/delete_company_transactions.py
@@ -28,7 +28,7 @@
"Party Account", "Employee", "Sales Taxes and Charges Template",
"Purchase Taxes and Charges Template", "POS Profile", "BOM",
"Company", "Bank Account", "Item Tax Template", "Mode Of Payment",
- "Item Default"):
+ "Item Default", "Customer", "Supplier", "GST Account"):
delete_for_doctype(doctype, company_name)
# reset company values
diff --git a/erpnext/setup/doctype/company/test_records.json b/erpnext/setup/doctype/company/test_records.json
index 2130241..9e55702 100644
--- a/erpnext/setup/doctype/company/test_records.json
+++ b/erpnext/setup/doctype/company/test_records.json
@@ -7,7 +7,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC1",
@@ -17,7 +18,8 @@
"doctype": "Company",
"domain": "Retail",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC2",
@@ -27,7 +29,8 @@
"doctype": "Company",
"domain": "Retail",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC3",
@@ -38,7 +41,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC4",
@@ -50,7 +54,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "_TC5",
@@ -61,7 +66,8 @@
"doctype": "Company",
"domain": "Manufacturing",
"chart_of_accounts": "Standard",
- "default_holiday_list": "_Test Holiday List"
+ "default_holiday_list": "_Test Holiday List",
+ "enable_perpetual_inventory": 0
},
{
"abbr": "TCP1",
diff --git a/erpnext/setup/doctype/customer_group/customer_group.json b/erpnext/setup/doctype/customer_group/customer_group.json
index 10f9bd0..0e2ed9e 100644
--- a/erpnext/setup/doctype/customer_group/customer_group.json
+++ b/erpnext/setup/doctype/customer_group/customer_group.json
@@ -139,7 +139,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2020-03-18 18:10:13.048492",
+ "modified": "2021-02-08 17:01:52.162202",
"modified_by": "Administrator",
"module": "Setup",
"name": "Customer Group",
@@ -189,6 +189,15 @@
"permlevel": 1,
"read": 1,
"role": "Sales Manager"
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "report": 1,
+ "role": "Customer",
+ "select": 1,
+ "share": 1
}
],
"search_fields": "parent_customer_group",
diff --git a/erpnext/setup/doctype/item_group/item_group.js b/erpnext/setup/doctype/item_group/item_group.js
index 9892dc3..1413cb2 100644
--- a/erpnext/setup/doctype/item_group/item_group.js
+++ b/erpnext/setup/doctype/item_group/item_group.js
@@ -61,6 +61,19 @@
frappe.set_route("List", "Item", {"item_group": frm.doc.name});
});
}
+
+ frappe.model.with_doctype('Item', () => {
+ const item_meta = frappe.get_meta('Item');
+
+ const valid_fields = item_meta.fields.filter(
+ df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden
+ ).map(df => ({ label: df.label, value: df.fieldname }));
+
+ const field = frappe.meta.get_docfield("Website Filter Field", "fieldname", frm.docname);
+ field.fieldtype = 'Select';
+ field.options = valid_fields;
+ frm.fields_dict.filter_fields.grid.refresh();
+ });
},
set_root_readonly: function(frm) {
diff --git a/erpnext/setup/doctype/item_group/item_group.json b/erpnext/setup/doctype/item_group/item_group.json
index 004421d..e835214 100644
--- a/erpnext/setup/doctype/item_group/item_group.json
+++ b/erpnext/setup/doctype/item_group/item_group.json
@@ -24,8 +24,12 @@
"route",
"weightage",
"slideshow",
+ "website_title",
"description",
"website_specifications",
+ "website_filters_section",
+ "filter_fields",
+ "filter_attributes",
"lft",
"rgt",
"old_parent"
@@ -180,6 +184,28 @@
"options": "Item Group",
"print_hide": 1,
"report_hide": 1
+ },
+ {
+ "fieldname": "website_filters_section",
+ "fieldtype": "Section Break",
+ "label": "Website Filters"
+ },
+ {
+ "fieldname": "filter_fields",
+ "fieldtype": "Table",
+ "label": "Item Fields",
+ "options": "Website Filter Field"
+ },
+ {
+ "fieldname": "filter_attributes",
+ "fieldtype": "Table",
+ "label": "Attributes",
+ "options": "Website Attribute"
+ },
+ {
+ "fieldname": "website_title",
+ "fieldtype": "Data",
+ "label": "Title"
}
],
"icon": "fa fa-sitemap",
@@ -188,7 +214,7 @@
"is_tree": 1,
"links": [],
"max_attachments": 3,
- "modified": "2020-03-18 18:10:34.383363",
+ "modified": "2021-02-08 17:02:44.951572",
"modified_by": "Administrator",
"module": "Setup",
"name": "Item Group",
@@ -245,6 +271,15 @@
"read": 1,
"report": 1,
"role": "Accounts User"
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "report": 1,
+ "role": "Customer",
+ "select": 1,
+ "share": 1
}
],
"search_fields": "parent_item_group",
diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py
index 4377840..bff806d 100644
--- a/erpnext/setup/doctype/item_group/item_group.py
+++ b/erpnext/setup/doctype/item_group/item_group.py
@@ -13,13 +13,16 @@
from erpnext.shopping_cart.product_info import set_product_info_for_website
from erpnext.utilities.product import get_qty_in_stock
from six.moves.urllib.parse import quote
+from erpnext.shopping_cart.product_query import ProductQuery
+from erpnext.shopping_cart.filters import ProductFiltersBuilder
class ItemGroup(NestedSet, WebsiteGenerator):
nsm_parent_field = 'parent_item_group'
website = frappe._dict(
condition_field = "show_in_website",
template = "templates/generators/item_group.html",
- no_cache = 1
+ no_cache = 1,
+ no_breadcrumbs = 1
)
def autoname(self):
@@ -70,18 +73,58 @@
context.page_length = cint(frappe.db.get_single_value('Products Settings', 'products_per_page')) or 6
context.search_link = '/product_search'
- start = int(frappe.form_dict.start or 0)
- if start < 0:
+ if frappe.form_dict:
+ search = frappe.form_dict.search
+ field_filters = frappe.parse_json(frappe.form_dict.field_filters)
+ attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
+ start = frappe.parse_json(frappe.form_dict.start)
+ else:
+ search = None
+ attribute_filters = None
+ field_filters = {}
start = 0
+
+ if not field_filters:
+ field_filters = {}
+
+ # Ensure the query remains within current item group
+ field_filters['item_group'] = self.name
+
+ engine = ProductQuery()
+ context.items = engine.query(attribute_filters, field_filters, search, start)
+
+ filter_engine = ProductFiltersBuilder(self.name)
+
+ context.field_filters = filter_engine.get_field_filters()
+ context.attribute_filters = filter_engine.get_attribute_fitlers()
+
context.update({
- "items": get_product_list_for_group(product_group = self.name, start=start,
- limit=context.page_length + 1, search=frappe.form_dict.get("search")),
"parents": get_parent_item_groups(self.parent_item_group),
"title": self.name
})
if self.slideshow:
- context.update(get_slideshow(self))
+ values = {
+ 'show_indicators': 1,
+ 'show_controls': 0,
+ 'rounded': 1,
+ 'slider_name': self.slideshow
+ }
+ slideshow = frappe.get_doc("Website Slideshow", self.slideshow)
+ slides = slideshow.get({"doctype":"Website Slideshow Item"})
+ for index, slide in enumerate(slides):
+ values[f"slide_{index + 1}_image"] = slide.image
+ values[f"slide_{index + 1}_title"] = slide.heading
+ values[f"slide_{index + 1}_subtitle"] = slide.description
+ values[f"slide_{index + 1}_theme"] = slide.theme or "Light"
+ values[f"slide_{index + 1}_content_align"] = slide.content_align or "Centre"
+ values[f"slide_{index + 1}_primary_action_label"] = slide.label
+ values[f"slide_{index + 1}_primary_action"] = slide.url
+
+ context.slideshow = values
+
+ context.breadcrumbs = 0
+ context.title = self.website_title or self.name
return context
diff --git a/erpnext/setup/doctype/territory/territory.json b/erpnext/setup/doctype/territory/territory.json
index aa8e048..a25bda0 100644
--- a/erpnext/setup/doctype/territory/territory.json
+++ b/erpnext/setup/doctype/territory/territory.json
@@ -123,7 +123,7 @@
"idx": 1,
"is_tree": 1,
"links": [],
- "modified": "2020-03-18 18:11:36.623555",
+ "modified": "2021-02-08 17:10:03.767426",
"modified_by": "Administrator",
"module": "Setup",
"name": "Territory",
@@ -166,6 +166,15 @@
{
"read": 1,
"role": "Maintenance User"
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "report": 1,
+ "role": "Customer",
+ "select": 1,
+ "share": 1
}
],
"search_fields": "parent_territory,territory_manager",
diff --git a/erpnext/setup/install.py b/erpnext/setup/install.py
index 2225fe1..0bb480b 100644
--- a/erpnext/setup/install.py
+++ b/erpnext/setup/install.py
@@ -28,6 +28,7 @@
create_default_energy_point_rules()
add_company_to_session_defaults()
add_standard_navbar_items()
+ add_app_name()
frappe.db.commit()
@@ -158,3 +159,7 @@
})
navbar_settings.save()
+
+def add_app_name():
+ settings = frappe.get_doc("System Settings")
+ settings.app_name = _("ERPNext")
\ No newline at end of file
diff --git a/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.html b/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.html
index 5808ce7..7166ba3 100644
--- a/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.html
+++ b/erpnext/setup/page/welcome_to_erpnext/welcome_to_erpnext.html
@@ -21,7 +21,6 @@
<h3>{%= __("Next Steps") %}</h3>
<ul class="list-unstyled">
<li><a class="text-muted" href="#">{%= __("Go to the Desktop and start using ERPNext") %}</a></li>
- <li><a class="text-muted" href="#modules/Learn">{%= __("View a list of all the help videos") %}</a></li>
<li><a class="text-muted" href="https://erpnext.com/docs/user" target="_blank">{%= __("Read the ERPNext Manual") %}</a></li>
<li><a class="text-muted" href="https://discuss.erpnext.com" target="_blank">{%= __("Community Forum") %}</a></li>
</ul>
diff --git a/erpnext/setup/desk_page/erpnext_settings/erpnext_settings.json b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
similarity index 77%
rename from erpnext/setup/desk_page/erpnext_settings/erpnext_settings.json
rename to erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
index 253d711..014f409 100644
--- a/erpnext/setup/desk_page/erpnext_settings/erpnext_settings.json
+++ b/erpnext/setup/workspace/erpnext_settings/erpnext_settings.json
@@ -1,18 +1,20 @@
{
- "cards": [],
"category": "Modules",
"charts": [],
"creation": "2020-03-12 14:47:51.166455",
"developer_mode_only": 0,
"disable_user_customization": 0,
"docstatus": 0,
- "doctype": "Desk Page",
+ "doctype": "Workspace",
"extends": "Settings",
"extends_another_page": 1,
+ "hide_custom": 0,
+ "icon": "settings",
"idx": 0,
"is_standard": 1,
"label": "ERPNext Settings",
- "modified": "2020-04-01 11:28:51.400851",
+ "links": [],
+ "modified": "2020-12-01 13:38:37.759596",
"modified_by": "Administrator",
"module": "Setup",
"name": "ERPNext Settings",
@@ -21,89 +23,89 @@
"pin_to_top": 0,
"shortcuts": [
{
- "icon": "octicon octicon-rocket",
+ "icon": "project",
"label": "Projects Settings",
"link_to": "Projects Settings",
"type": "DocType"
},
{
- "icon": "octicon octicon-repo",
+ "icon": "accounting",
"label": "Accounts Settings",
"link_to": "Accounts Settings",
"type": "DocType"
},
{
- "icon": "octicon octicon-package",
+ "icon": "stock",
"label": "Stock Settings",
"link_to": "Stock Settings",
"type": "DocType"
},
{
- "icon": "octicon octicon-organization",
+ "icon": "hr",
"label": "HR Settings",
"link_to": "HR Settings",
"type": "DocType"
},
{
- "icon": "octicon octicon-tag",
+ "icon": "sell",
"label": "Selling Settings",
"link_to": "Selling Settings",
"type": "DocType"
},
{
- "icon": "octicon octicon-briefcase",
+ "icon": "buying",
"label": "Buying Settings",
"link_to": "Buying Settings",
"type": "DocType"
},
{
- "icon": "fa fa-life-ring",
+ "icon": "support",
"label": "Support Settings",
"link_to": "Support Settings",
"type": "DocType"
},
{
- "icon": "fa fa-shopping-cart",
+ "icon": "retail",
"label": "Shopping Cart Settings",
"link_to": "Shopping Cart Settings",
"type": "DocType"
},
{
- "icon": "fa fa-globe",
+ "icon": "website",
"label": "Portal Settings",
"link_to": "Portal Settings",
"type": "DocType"
},
{
- "icon": "octicon octicon-tools",
+ "icon": "organization",
"label": "Manufacturing Settings",
"link_to": "Manufacturing Settings",
"restrict_to_domain": "Manufacturing",
"type": "DocType"
},
{
- "icon": "octicon octicon-mortar-board",
+ "icon": "education",
"label": "Education Settings",
"link_to": "Education Settings",
"restrict_to_domain": "Education",
"type": "DocType"
},
{
- "icon": "fa fa-bed",
+ "icon": "organization",
"label": "Hotel Settings",
"link_to": "Hotel Settings",
"restrict_to_domain": "Hospitality",
"type": "DocType"
},
{
- "icon": "fa fa-heartbeat",
+ "icon": "non-profit",
"label": "Healthcare Settings",
"link_to": "Healthcare Settings",
"restrict_to_domain": "Healthcare",
"type": "DocType"
},
{
- "icon": "fa fa-cog",
+ "icon": "setting",
"label": "Domain Settings",
"link_to": "Domain Settings",
"type": "DocType"
diff --git a/erpnext/setup/workspace/home/home.json b/erpnext/setup/workspace/home/home.json
new file mode 100644
index 0000000..69ca7cf
--- /dev/null
+++ b/erpnext/setup/workspace/home/home.json
@@ -0,0 +1,454 @@
+{
+ "category": "Modules",
+ "charts": [],
+ "creation": "2020-01-23 13:46:38.833076",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "getting-started",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Home",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Healthcare",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Patient",
+ "link_to": "Patient",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Diagnosis",
+ "link_to": "Diagnosis",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Agriculture",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Crop",
+ "link_to": "Crop",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Crop Cycle",
+ "link_to": "Crop Cycle",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Location",
+ "link_to": "Location",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Fertilizer",
+ "link_to": "Fertilizer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Education",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Student",
+ "link_to": "Student",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Course",
+ "link_to": "Course",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Instructor",
+ "link_to": "Instructor",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Room",
+ "link_to": "Room",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Non Profit",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Member",
+ "link_to": "Member",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Volunteer",
+ "link_to": "Volunteer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Chapter",
+ "link_to": "Chapter",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Donor",
+ "link_to": "Donor",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Warehouse",
+ "link_to": "Warehouse",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Brand",
+ "link_to": "Brand",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Unit of Measure (UOM)",
+ "link_to": "UOM",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock Reconciliation",
+ "link_to": "Stock Reconciliation",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Human Resources",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee",
+ "link_to": "Employee",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Employee",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Employee Attendance Tool",
+ "link_to": "Employee Attendance Tool",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Salary Structure",
+ "link_to": "Salary Structure",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "CRM",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Lead",
+ "link_to": "Lead",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Customer Group",
+ "link_to": "Customer Group",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Territory",
+ "link_to": "Territory",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Accounting",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item",
+ "link_to": "Item",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Customer",
+ "link_to": "Customer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Supplier",
+ "link_to": "Supplier",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Company",
+ "link_to": "Company",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Chart of Accounts",
+ "link_to": "Account",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Opening Invoice Creation Tool",
+ "link_to": "Opening Invoice Creation Tool",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Data Import and Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Import Data",
+ "link_to": "Data Import",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Chart of Accounts Importer",
+ "link_to": "Chart of Accounts Importer",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Letter Head",
+ "link_to": "Letter Head",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Email Account",
+ "link_to": "Email Account",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ }
+ ],
+ "modified": "2021-01-01 12:13:16.055668",
+ "modified_by": "Administrator",
+ "module": "Setup",
+ "name": "Home",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 1,
+ "shortcuts": [
+ {
+ "label": "Item",
+ "link_to": "Item",
+ "type": "DocType"
+ },
+ {
+ "label": "Customer",
+ "link_to": "Customer",
+ "type": "DocType"
+ },
+ {
+ "label": "Supplier",
+ "link_to": "Supplier",
+ "type": "DocType"
+ },
+ {
+ "label": "Sales Invoice",
+ "link_to": "Sales Invoice",
+ "type": "DocType"
+ },
+ {
+ "label": "Leaderboard",
+ "link_to": "leaderboard",
+ "type": "Page"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py
index c2549fe..681d161 100644
--- a/erpnext/shopping_cart/cart.py
+++ b/erpnext/shopping_cart/cart.py
@@ -42,15 +42,31 @@
return {
"doc": decorate_quotation_doc(doc),
- "shipping_addresses": [{"name": address.name, "title": address.address_title, "display": address.display}
- for address in addresses if address.address_type == "Shipping"],
- "billing_addresses": [{"name": address.name, "title": address.address_title, "display": address.display}
- for address in addresses if address.address_type == "Billing"],
+ "shipping_addresses": get_shipping_addresses(party),
+ "billing_addresses": get_billing_addresses(party),
"shipping_rules": get_applicable_shipping_rules(party),
"cart_settings": frappe.get_cached_doc("Shopping Cart Settings")
}
@frappe.whitelist()
+def get_shipping_addresses(party=None):
+ if not party:
+ party = get_party()
+ addresses = get_address_docs(party=party)
+ return [{"name": address.name, "title": address.address_title, "display": address.display}
+ for address in addresses if address.address_type == "Shipping"
+ ]
+
+@frappe.whitelist()
+def get_billing_addresses(party=None):
+ if not party:
+ party = get_party()
+ addresses = get_address_docs(party=party)
+ return [{"name": address.name, "title": address.address_title, "display": address.display}
+ for address in addresses if address.address_type == "Billing"
+ ]
+
+@frappe.whitelist()
def place_order():
quotation = _get_cart_quotation()
cart_settings = frappe.db.get_value("Shopping Cart Settings", None,
@@ -180,6 +196,13 @@
lead_doc.update(lead)
lead_doc.set('lead_owner', '')
+ if not frappe.db.exists('Lead Source', 'Product Inquiry'):
+ frappe.get_doc({
+ 'doctype': 'Lead Source',
+ 'source_name' : 'Product Inquiry'
+ }).insert(ignore_permissions=True)
+ lead_doc.set('source', 'Product Inquiry')
+
try:
lead_doc.save(ignore_permissions=True)
except frappe.exceptions.DuplicateEntryError:
@@ -203,27 +226,33 @@
@frappe.whitelist()
def update_cart_address(address_type, address_name):
quotation = _get_cart_quotation()
- address_display = get_address_display(frappe.get_doc("Address", address_name).as_dict())
+ address_doc = frappe.get_doc("Address", address_name).as_dict()
+ address_display = get_address_display(address_doc)
if address_type.lower() == "billing":
quotation.customer_address = address_name
quotation.address_display = address_display
quotation.shipping_address_name == quotation.shipping_address_name or address_name
+ address_doc = next((doc for doc in get_billing_addresses() if doc["name"] == address_name), None)
elif address_type.lower() == "shipping":
quotation.shipping_address_name = address_name
quotation.shipping_address = address_display
quotation.customer_address == quotation.customer_address or address_name
-
+ address_doc = next((doc for doc in get_shipping_addresses() if doc["name"] == address_name), None)
apply_cart_settings(quotation=quotation)
quotation.flags.ignore_permissions = True
quotation.save()
context = get_cart_quotation(quotation)
+ context['address'] = address_doc
+
return {
"taxes": frappe.render_template("templates/includes/order/order_taxes.html",
context),
- }
+ "address": frappe.render_template("templates/includes/cart/address_card.html",
+ context)
+ }
def guess_territory():
territory = None
@@ -433,6 +462,9 @@
return customer
def get_debtors_account(cart_settings):
+ if not cart_settings.payment_gateway_account:
+ frappe.throw(_("Payment Gateway Account not set"), _("Mandatory"))
+
payment_gateway_account_currency = \
frappe.get_doc("Payment Gateway Account", cart_settings.payment_gateway_account).currency
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js
index 20c6342..b38828e 100644
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js
+++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.js
@@ -12,6 +12,18 @@
return { 'filters': { 'payment_channel': "Email" } };
});
},
+ refresh: function(frm) {
+ if (frm.doc.enabled) {
+ frm.get_field('store_page_docs').$wrapper.removeClass('hide-control').html(
+ `<div>${__("Follow these steps to create a landing page for your store")}:
+ <a href="https://docs.erpnext.com/docs/user/manual/en/website/store-landing-page"
+ style="color: var(--gray-600)">
+ docs/store-landing-page
+ </a>
+ </div>`
+ );
+ }
+ },
enabled: function(frm) {
if (frm.doc.enabled === 1) {
frm.set_value('enable_variants', 1);
diff --git a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
index 9d61e7d..3691721 100644
--- a/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
+++ b/erpnext/shopping_cart/doctype/shopping_cart_settings/shopping_cart_settings.json
@@ -7,6 +7,7 @@
"engine": "InnoDB",
"field_order": [
"enabled",
+ "store_page_docs",
"display_settings",
"show_attachments",
"show_price",
@@ -25,10 +26,10 @@
"quotation_series",
"section_break_8",
"enable_checkout",
- "payment_success_url",
- "column_break_11",
"save_quotations_as_draft",
- "payment_gateway_account"
+ "column_break_11",
+ "payment_gateway_account",
+ "payment_success_url"
],
"fields": [
{
@@ -142,10 +143,12 @@
},
{
"default": "Orders",
+ "depends_on": "enable_checkout",
"description": "After payment completion redirect user to selected page.",
"fieldname": "payment_success_url",
"fieldtype": "Select",
"label": "Payment Success Url",
+ "mandatory_depends_on": "enable_checkout",
"options": "\nOrders\nInvoices\nMy Account"
},
{
@@ -153,9 +156,11 @@
"fieldtype": "Column Break"
},
{
+ "depends_on": "enable_checkout",
"fieldname": "payment_gateway_account",
"fieldtype": "Link",
"label": "Payment Gateway Account",
+ "mandatory_depends_on": "enable_checkout",
"options": "Payment Gateway Account"
},
{
@@ -174,13 +179,18 @@
"fieldname": "save_quotations_as_draft",
"fieldtype": "Check",
"label": "Save Quotations as Draft"
+ },
+ {
+ "depends_on": "doc.enabled",
+ "fieldname": "store_page_docs",
+ "fieldtype": "HTML"
}
],
"icon": "fa fa-shopping-cart",
"idx": 1,
"issingle": 1,
"links": [],
- "modified": "2020-09-24 16:28:07.192525",
+ "modified": "2021-02-11 18:48:30.433058",
"modified_by": "Administrator",
"module": "Shopping Cart",
"name": "Shopping Cart Settings",
diff --git a/erpnext/shopping_cart/filters.py b/erpnext/shopping_cart/filters.py
new file mode 100644
index 0000000..6c63d87
--- /dev/null
+++ b/erpnext/shopping_cart/filters.py
@@ -0,0 +1,82 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+from __future__ import unicode_literals
+import frappe
+from frappe import _dict
+
+class ProductFiltersBuilder:
+ def __init__(self, item_group=None):
+ if not item_group or item_group == "Products Settings":
+ self.doc = frappe.get_doc("Products Settings")
+ else:
+ self.doc = frappe.get_doc("Item Group", item_group)
+
+ self.item_group = item_group
+
+ def get_field_filters(self):
+ filter_fields = [row.fieldname for row in self.doc.filter_fields]
+
+ meta = frappe.get_meta('Item')
+ fields = [df for df in meta.fields if df.fieldname in filter_fields]
+
+ filter_data = []
+ for df in fields:
+ filters = {}
+ if df.fieldtype == "Link":
+ if self.item_group:
+ filters['item_group'] = self.item_group
+
+ values = frappe.get_all("Item", fields=[df.fieldname], filters=filters, distinct="True", pluck=df.fieldname)
+ else:
+ doctype = df.get_link_doctype()
+
+ # apply enable/disable/show_in_website filter
+ meta = frappe.get_meta(doctype)
+
+ if meta.has_field('enabled'):
+ filters['enabled'] = 1
+ if meta.has_field('disabled'):
+ filters['disabled'] = 0
+ if meta.has_field('show_in_website'):
+ filters['show_in_website'] = 1
+
+ values = [d.name for d in frappe.get_all(doctype, filters)]
+
+ # Remove None
+ values = values.remove(None) if None in values else values
+ if values:
+ filter_data.append([df, values])
+
+ return filter_data
+
+ def get_attribute_fitlers(self):
+ attributes = [row.attribute for row in self.doc.filter_attributes]
+ attribute_docs = [
+ frappe.get_doc('Item Attribute', attribute) for attribute in attributes
+ ]
+
+ valid_attributes = []
+
+ for attr_doc in attribute_docs:
+ selected_attributes = []
+ for attr in attr_doc.item_attribute_values:
+ filters= [
+ ["Item Variant Attribute", "attribute", "=", attr.parent],
+ ["Item Variant Attribute", "attribute_value", "=", attr.attribute_value]
+ ]
+ if self.item_group:
+ filters.append(["item_group", "=", self.item_group])
+
+ if frappe.db.get_all("Item", filters, limit=1):
+ selected_attributes.append(attr)
+
+ if selected_attributes:
+ valid_attributes.append(
+ _dict(
+ item_attribute_values=selected_attributes,
+ name=attr_doc.name
+ )
+ )
+
+ return valid_attributes
diff --git a/erpnext/shopping_cart/product_query.py b/erpnext/shopping_cart/product_query.py
new file mode 100644
index 0000000..36d446e
--- /dev/null
+++ b/erpnext/shopping_cart/product_query.py
@@ -0,0 +1,123 @@
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# License: GNU General Public License v3. See license.txt
+
+import frappe
+from erpnext.shopping_cart.product_info import get_product_info_for_website
+
+class ProductQuery:
+ """Query engine for product listing
+
+ Attributes:
+ cart_settings (Document): Settings for Cart
+ fields (list): Fields to fetch in query
+ filters (TYPE): Description
+ or_filters (list): Description
+ page_length (Int): Length of page for the query
+ settings (Document): Products Settings DocType
+ filters (list)
+ or_filters (list)
+ """
+
+ def __init__(self):
+ self.settings = frappe.get_doc("Products Settings")
+ self.cart_settings = frappe.get_doc("Shopping Cart Settings")
+ self.page_length = self.settings.products_per_page or 20
+ self.fields = ['name', 'item_name', 'item_code', 'website_image', 'variant_of', 'has_variants', 'item_group', 'image', 'web_long_description', 'description', 'route']
+ self.filters = []
+ self.or_filters = [['show_in_website', '=', 1]]
+ if not self.settings.get('hide_variants'):
+ self.or_filters.append(['show_variant_in_website', '=', 1])
+
+ def query(self, attributes=None, fields=None, search_term=None, start=0):
+ """Summary
+
+ Args:
+ attributes (dict, optional): Item Attribute filters
+ fields (dict, optional): Field level filters
+ search_term (str, optional): Search term to lookup
+ start (int, optional): Page start
+
+ Returns:
+ list: List of results with set fields
+ """
+ if fields: self.build_fields_filters(fields)
+ if search_term: self.build_search_filters(search_term)
+
+ result = []
+
+ if attributes:
+ all_items = []
+ for attribute, values in attributes.items():
+ if not isinstance(values, list):
+ values = [values]
+
+ items = frappe.get_all(
+ "Item",
+ fields=self.fields,
+ filters=[
+ *self.filters,
+ ["Item Variant Attribute", "attribute", "=", attribute],
+ ["Item Variant Attribute", "attribute_value", "in", values],
+ ],
+ or_filters=self.or_filters,
+ start=start,
+ limit=self.page_length
+ )
+
+ items_dict = {item.name: item for item in items}
+ # TODO: Replace Variants by their parent templates
+
+ all_items.append(set(items_dict.keys()))
+
+ result = [items_dict.get(item) for item in list(set.intersection(*all_items))]
+ else:
+ result = frappe.get_all("Item", fields=self.fields, filters=self.filters, or_filters=self.or_filters, start=start, limit=self.page_length)
+
+ for item in result:
+ product_info = get_product_info_for_website(item.item_code, skip_quotation_creation=True).get('product_info')
+ if product_info:
+ item.formatted_price = product_info['price'].get('formatted_price') if product_info['price'] else None
+
+ return result
+
+ def build_fields_filters(self, filters):
+ """Build filters for field values
+
+ Args:
+ filters (dict): Filters
+ """
+ for field, values in filters.items():
+ if not values:
+ continue
+
+ if isinstance(values, list):
+ # If value is a list use `IN` query
+ self.filters.append([field, 'IN', values])
+ else:
+ # `=` will be faster than `IN` for most cases
+ self.filters.append([field, '=', values])
+
+ def build_search_filters(self, search_term):
+ """Query search term in specified fields
+
+ Args:
+ search_term (str): Search candidate
+ """
+ # Default fields to search from
+ default_fields = {'name', 'item_name', 'description', 'item_group'}
+
+ # Get meta search fields
+ meta = frappe.get_meta("Item")
+ meta_fields = set(meta.get_search_fields())
+
+ # Join the meta fields and default fields set
+ search_fields = default_fields.union(meta_fields)
+ try:
+ if frappe.db.count('Item', cache=True) > 50000:
+ search_fields.remove('description')
+ except KeyError:
+ pass
+
+ # Build or filters for query
+ search = '%{}%'.format(search_term)
+ self.or_filters += [[field, 'like', search] for field in search_fields]
diff --git a/erpnext/shopping_cart/search.py b/erpnext/shopping_cart/search.py
new file mode 100644
index 0000000..63e9fe1
--- /dev/null
+++ b/erpnext/shopping_cart/search.py
@@ -0,0 +1,126 @@
+import frappe
+from frappe.search.full_text_search import FullTextSearch
+from whoosh.fields import TEXT, ID, KEYWORD, Schema
+from frappe.utils import strip_html_tags
+from whoosh.qparser import MultifieldParser, FieldsPlugin, WildcardPlugin
+from whoosh.analysis import StemmingAnalyzer
+from whoosh.query import Prefix
+
+INDEX_NAME = "products"
+
+class ProductSearch(FullTextSearch):
+ """ Wrapper for WebsiteSearch """
+
+ def get_schema(self):
+ return Schema(
+ title=TEXT(stored=True, field_boost=1.5),
+ name=ID(stored=True),
+ path=ID(stored=True),
+ content=TEXT(stored=True, analyzer=StemmingAnalyzer()),
+ keywords=KEYWORD(stored=True, scorable=True, commas=True),
+ )
+
+ def get_id(self):
+ return "name"
+
+ def get_items_to_index(self):
+ """Get all routes to be indexed, this includes the static pages
+ in www/ and routes from published documents
+
+ Returns:
+ self (object): FullTextSearch Instance
+ """
+ items = get_all_published_items()
+ documents = [self.get_document_to_index(item) for item in items]
+ return documents
+
+ def get_document_to_index(self, item):
+ try:
+ item = frappe.get_doc("Item", item)
+ title = item.item_name
+ keywords = [item.item_group]
+
+ if item.brand:
+ keywords.append(item.brand)
+
+ if item.website_image_alt:
+ keywords.append(item.website_image_alt)
+
+ if item.has_variants and item.variant_based_on == "Item Attribute":
+ keywords = keywords + [attr.attribute for attr in item.attributes]
+
+ if item.web_long_description:
+ content = strip_html_tags(item.web_long_description)
+ elif item.description:
+ content = strip_html_tags(item.description)
+
+ return frappe._dict(
+ title=title,
+ name=item.name,
+ path=item.route,
+ content=content,
+ keywords=", ".join(keywords),
+ )
+ except Exception:
+ pass
+
+ def search(self, text, scope=None, limit=20):
+ """Search from the current index
+
+ Args:
+ text (str): String to search for
+ scope (str, optional): Scope to limit the search. Defaults to None.
+ limit (int, optional): Limit number of search results. Defaults to 20.
+
+ Returns:
+ [List(_dict)]: Search results
+ """
+ ix = self.get_index()
+
+ results = None
+ out = []
+
+ with ix.searcher() as searcher:
+ parser = MultifieldParser(["title", "content", "keywords"], ix.schema)
+ parser.remove_plugin_class(FieldsPlugin)
+ parser.remove_plugin_class(WildcardPlugin)
+ query = parser.parse(text)
+
+ filter_scoped = None
+ if scope:
+ filter_scoped = Prefix(self.id, scope)
+ results = searcher.search(query, limit=limit, filter=filter_scoped)
+
+ for r in results:
+ out.append(self.parse_result(r))
+
+ return out
+
+ def parse_result(self, result):
+ title_highlights = result.highlights("title")
+ content_highlights = result.highlights("content")
+ keyword_highlights = result.highlights("keywords")
+
+ return frappe._dict(
+ title=result["title"],
+ path=result["path"],
+ keywords=result["keywords"],
+ title_highlights=title_highlights,
+ content_highlights=content_highlights,
+ keyword_highlights=keyword_highlights,
+ )
+
+def get_all_published_items():
+ return frappe.get_all("Item", filters={"variant_of": "", "show_in_website": 1},pluck="name")
+
+def update_index_for_path(path):
+ search = ProductSearch(INDEX_NAME)
+ return search.update_index_by_name(path)
+
+def remove_document_from_index(path):
+ search = ProductSearch(INDEX_NAME)
+ return search.remove_document_from_index(path)
+
+def build_index_for_all_routes():
+ search = ProductSearch(INDEX_NAME)
+ return search.build()
\ No newline at end of file
diff --git a/erpnext/config/__init__.py b/erpnext/shopping_cart/web_template/__init__.py
similarity index 100%
copy from erpnext/config/__init__.py
copy to erpnext/shopping_cart/web_template/__init__.py
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/shopping_cart/web_template/hero_slider/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/shopping_cart/web_template/hero_slider/__init__.py
diff --git a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html b/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html
new file mode 100644
index 0000000..1b39534
--- /dev/null
+++ b/erpnext/shopping_cart/web_template/hero_slider/hero_slider.html
@@ -0,0 +1,85 @@
+{%- macro slide(image, title, subtitle, action, label, index, align="Left", theme="Dark") -%}
+{%- set align_class = resolve_class({
+ 'text-right': align == 'Right',
+ 'text-centre': align == 'Center',
+ 'text-left': align == 'Left',
+}) -%}
+
+{%- set heading_class = resolve_class({
+ 'text-white': theme == 'Dark',
+ '': theme == 'Light',
+}) -%}
+<div class="carousel-item {{ 'active' if index=='1' else ''}}" style="height: 450px;">
+ <img class="d-block h-100 w-100" style="object-fit: cover;" src="{{ image }}" alt="{{ title }}">
+ {%- if title or subtitle -%}
+ <div class="carousel-body container d-flex {{ align_class }}">
+ <div class="carousel-content align-self-center">
+ {%- if title -%}<h1 class="{{ heading_class }}">{{ title }}</h1>{%- endif -%}
+ {%- if subtitle -%}<p class="text-muted mt-2">{{ subtitle }}</p>{%- endif -%}
+ {%- if action -%}
+ <a href="{{ action }}" class="btn btn-primary mt-3">
+ {{ label }}
+ </a>
+ {%- endif -%}
+ </div>
+ </div>
+ {%- endif -%}
+</div>
+{%- endmacro -%}
+
+<div id="{{ slider_name }}" class="section-carousel carousel slide" data-ride="carousel">
+ {%- if show_indicators -%}
+ <ol class="carousel-indicators">
+ {%- for index in ['1', '2', '3', '4', '5'] -%}
+ {%- if values['slide_' + index + '_image'] -%}
+ <li data-target="#{{ slider_name }}" data-slide-to="{{ frappe.utils.cint(index) - 1 }}" class="{{ 'active' if index=='1' else ''}}"></li>
+ {%- endif -%}
+ {%- endfor -%}
+ </ol>
+ {%- endif -%}
+ <div class="carousel-inner {{ resolve_class({'rounded-carousel': rounded }) }}">
+ {%- for index in ['1', '2', '3', '4', '5'] -%}
+ {%- set image = values['slide_' + index + '_image'] -%}
+ {%- set title = values['slide_' + index + '_title'] -%}
+ {%- set subtitle = values['slide_' + index + '_subtitle'] -%}
+ {%- set primary_action = values['slide_' + index + '_primary_action'] -%}
+ {%- set primary_action_label = values['slide_' + index + '_primary_action_label'] -%}
+ {%- set align = values['slide_' + index + '_content_align'] -%}
+ {%- set theme = values['slide_' + index + '_theme'] -%}
+
+ {%- if image -%}
+ {{ slide(image, title, subtitle, primary_action, primary_action_label, index, align, theme) }}
+ {%- endif -%}
+
+ {%- endfor -%}
+ </div>
+ {%- if show_controls -%}
+ <a class="carousel-control-prev" href="#{{ slider_name }}" role="button" data-slide="prev">
+ <div class="carousel-control">
+ <svg class="mr-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M11.625 3.75L6.375 9L11.625 14.25" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ </div>
+ <span class="sr-only">Previous</span>
+ </a>
+ <a class="carousel-control-next" href="#{{ slider_name }}" role="button" data-slide="next">
+ <div class="carousel-control">
+ <svg class="ml-1" width="20" height="20" viewBox="0 0 18 18" fill="none" xmlns="http://www.w3.org/2000/svg">
+ <path d="M6.375 14.25L11.625 9L6.375 3.75" stroke="#4C5A67" stroke-linecap="round" stroke-linejoin="round"/>
+ </svg>
+ </div>
+ <span class="sr-only">Next</span>
+ </a>
+ {%- endif -%}
+</div>
+
+<script type="text/javascript">
+ $('.carousel').carousel({
+ interval: false,
+ pause: "hover",
+ wrap: true
+ })
+</script>
+
+<style>
+</style>
\ No newline at end of file
diff --git a/erpnext/shopping_cart/web_template/hero_slider/hero_slider.json b/erpnext/shopping_cart/web_template/hero_slider/hero_slider.json
new file mode 100644
index 0000000..04fb1d2
--- /dev/null
+++ b/erpnext/shopping_cart/web_template/hero_slider/hero_slider.json
@@ -0,0 +1,284 @@
+{
+ "creation": "2020-11-17 15:21:51.207221",
+ "docstatus": 0,
+ "doctype": "Web Template",
+ "fields": [
+ {
+ "fieldname": "slider_name",
+ "fieldtype": "Data",
+ "label": "Slider Name",
+ "reqd": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "show_indicators",
+ "fieldtype": "Check",
+ "label": "Show Indicators",
+ "reqd": 0
+ },
+ {
+ "default": "1",
+ "fieldname": "show_controls",
+ "fieldtype": "Check",
+ "label": "Show Controls",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_1",
+ "fieldtype": "Section Break",
+ "label": "Slide 1",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_1_image",
+ "fieldtype": "Attach Image",
+ "label": "Image",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_1_title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_1_subtitle",
+ "fieldtype": "Small Text",
+ "label": "Subtitle",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_1_primary_action_label",
+ "fieldtype": "Data",
+ "label": "Primary Action Label",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_1_primary_action",
+ "fieldtype": "Data",
+ "label": "Primary Action",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_1_content_align",
+ "fieldtype": "Select",
+ "label": "Content Align",
+ "options": "Left\nCentre\nRight",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_1_theme",
+ "fieldtype": "Select",
+ "label": "Slide Theme",
+ "options": "Dark\nLight",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_2",
+ "fieldtype": "Section Break",
+ "label": "Slide 2",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_2_image",
+ "fieldtype": "Attach Image",
+ "label": "Image ",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_2_title",
+ "fieldtype": "Data",
+ "label": "Title ",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_2_subtitle",
+ "fieldtype": "Small Text",
+ "label": "Subtitle ",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_2_primary_action_label",
+ "fieldtype": "Data",
+ "label": "Primary Action Label ",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_2_primary_action",
+ "fieldtype": "Data",
+ "label": "Primary Action ",
+ "reqd": 0
+ },
+ {
+ "default": "Left",
+ "fieldname": "slide_2_content_align",
+ "fieldtype": "Select",
+ "label": "Content Align",
+ "options": "Left\nCentre\nRight",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_2_theme",
+ "fieldtype": "Select",
+ "label": "Slide Theme",
+ "options": "Dark\nLight",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_3",
+ "fieldtype": "Section Break",
+ "label": "Slide 3",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_3_image",
+ "fieldtype": "Attach Image",
+ "label": "Image",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_3_title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_3_subtitle",
+ "fieldtype": "Small Text",
+ "label": "Subtitle",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_3_primary_action_label",
+ "fieldtype": "Data",
+ "label": "Primary Action Label",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_3_primary_action",
+ "fieldtype": "Data",
+ "label": "Primary Action",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_3_content_align",
+ "fieldtype": "Select",
+ "label": "Content Align",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_3_theme",
+ "fieldtype": "Select",
+ "label": "Slide Theme",
+ "options": "Dark\nLight",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_4",
+ "fieldtype": "Section Break",
+ "label": "Slide 4",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_4_image",
+ "fieldtype": "Attach Image",
+ "label": "Image",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_4_title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_4_subtitle",
+ "fieldtype": "Small Text",
+ "label": "Subtitle",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_4_primary_action_label",
+ "fieldtype": "Data",
+ "label": "Primary Action Label",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_4_primary_action",
+ "fieldtype": "Data",
+ "label": "Primary Action",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_4_content_align",
+ "fieldtype": "Select",
+ "label": "Content Align",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_4_theme",
+ "fieldtype": "Select",
+ "label": "Slide Theme",
+ "options": "Dark\nLight",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_5",
+ "fieldtype": "Section Break",
+ "label": "Slide 5",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_5_image",
+ "fieldtype": "Attach Image",
+ "label": "Image",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_5_title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_5_subtitle",
+ "fieldtype": "Small Text",
+ "label": "Subtitle",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_5_primary_action_label",
+ "fieldtype": "Data",
+ "label": "Primary Action Label",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_5_primary_action",
+ "fieldtype": "Data",
+ "label": "Primary Action",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_5_content_align",
+ "fieldtype": "Select",
+ "label": "Content Align",
+ "reqd": 0
+ },
+ {
+ "fieldname": "slide_5_theme",
+ "fieldtype": "Select",
+ "label": "Slide Theme",
+ "options": "Dark\nLight",
+ "reqd": 0
+ }
+ ],
+ "idx": 2,
+ "modified": "2020-12-29 12:30:02.794994",
+ "modified_by": "Administrator",
+ "module": "Shopping Cart",
+ "name": "Hero Slider",
+ "owner": "Administrator",
+ "standard": 1,
+ "template": "",
+ "type": "Section"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/shopping_cart/web_template/item_card_group/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/shopping_cart/web_template/item_card_group/__init__.py
diff --git a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html b/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html
new file mode 100644
index 0000000..890ae50
--- /dev/null
+++ b/erpnext/shopping_cart/web_template/item_card_group/item_card_group.html
@@ -0,0 +1,38 @@
+{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
+
+<div class="section-with-cards item-card-group-section">
+ <div class="item-group-header d-flex justify-content-between">
+ <div class="title-section">
+ {%- if title -%}
+ <h2 class="section-title">{{ title }}</h2>
+ {%- endif -%}
+ {%- if subtitle -%}
+ <p class="section-description">{{ subtitle }}</p>
+ {%- endif -%}
+ </div>
+ <div class="primary-action-section">
+ {%- if primary_action -%}
+ <a href="{{ action }}" class="btn btn-primary pull-right">
+ {{ primary_action_label }}
+ </a>
+ {%- endif -%}
+ </div>
+ </div>
+
+ <div class="row">
+ {%- for index in ['1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12'] -%}
+ {%- set item = values['card_' + index + '_item'] -%}
+ {%- if item -%}
+ {%- set item = frappe.get_doc("Item", item) -%}
+ {{ item_card(
+ item.item_name, item.image, item.route, item.description,
+ None, item.item_group, values['card_' + index + '_featured'],
+ True, "Center"
+ ) }}
+ {%- endif -%}
+ {%- endfor -%}
+ </div>
+</div>
+
+<style>
+</style>
\ No newline at end of file
diff --git a/erpnext/shopping_cart/web_template/item_card_group/item_card_group.json b/erpnext/shopping_cart/web_template/item_card_group/item_card_group.json
new file mode 100644
index 0000000..ad087b0
--- /dev/null
+++ b/erpnext/shopping_cart/web_template/item_card_group/item_card_group.json
@@ -0,0 +1,273 @@
+{
+ "__unsaved": 1,
+ "creation": "2020-11-17 15:35:05.285322",
+ "docstatus": 0,
+ "doctype": "Web Template",
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "fieldname": "subtitle",
+ "fieldtype": "Data",
+ "label": "Subtitle",
+ "reqd": 0
+ },
+ {
+ "__unsaved": 1,
+ "fieldname": "primary_action_label",
+ "fieldtype": "Data",
+ "label": "Primary Action Label",
+ "reqd": 0
+ },
+ {
+ "__islocal": 1,
+ "__unsaved": 1,
+ "fieldname": "primary_action",
+ "fieldtype": "Data",
+ "label": "Primary Action",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_1",
+ "fieldtype": "Section Break",
+ "label": "Card 1",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_1_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_1_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_2",
+ "fieldtype": "Section Break",
+ "label": "Card 2",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_2_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_2_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_3",
+ "fieldtype": "Section Break",
+ "label": "Card 3",
+ "options": "",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_3_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_3_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_4",
+ "fieldtype": "Section Break",
+ "label": "Card 4",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_4_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_4_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_5",
+ "fieldtype": "Section Break",
+ "label": "Card 5",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_5_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_5_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_6",
+ "fieldtype": "Section Break",
+ "label": "Card 6",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_6_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_6_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_7",
+ "fieldtype": "Section Break",
+ "label": "Card 7",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_7_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_7_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_8",
+ "fieldtype": "Section Break",
+ "label": "Card 8",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_8_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_8_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_9",
+ "fieldtype": "Section Break",
+ "label": "Card 9",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_9_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_9_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_10",
+ "fieldtype": "Section Break",
+ "label": "Card 10",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_10_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_10_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_11",
+ "fieldtype": "Section Break",
+ "label": "Card 11",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_11_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_11_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_12",
+ "fieldtype": "Section Break",
+ "label": "Card 12",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_12_item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "fieldname": "card_12_featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "reqd": 0
+ }
+ ],
+ "idx": 0,
+ "modified": "2020-11-19 18:48:52.633045",
+ "modified_by": "Administrator",
+ "module": "Shopping Cart",
+ "name": "Item Card Group",
+ "owner": "Administrator",
+ "standard": 1,
+ "template": "",
+ "type": "Section"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/shopping_cart/web_template/product_card/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/shopping_cart/web_template/product_card/__init__.py
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/shopping_cart/web_template/product_card/product_card.html
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/shopping_cart/web_template/product_card/product_card.html
diff --git a/erpnext/shopping_cart/web_template/product_card/product_card.json b/erpnext/shopping_cart/web_template/product_card/product_card.json
new file mode 100644
index 0000000..1059c1b
--- /dev/null
+++ b/erpnext/shopping_cart/web_template/product_card/product_card.json
@@ -0,0 +1,33 @@
+{
+ "__unsaved": 1,
+ "creation": "2020-11-17 15:28:47.809342",
+ "docstatus": 0,
+ "doctype": "Web Template",
+ "fields": [
+ {
+ "__unsaved": 1,
+ "fieldname": "item",
+ "fieldtype": "Link",
+ "label": "Item",
+ "options": "Item",
+ "reqd": 0
+ },
+ {
+ "__unsaved": 1,
+ "fieldname": "featured",
+ "fieldtype": "Check",
+ "label": "Featured",
+ "options": "",
+ "reqd": 0
+ }
+ ],
+ "idx": 0,
+ "modified": "2020-11-17 15:33:34.982515",
+ "modified_by": "Administrator",
+ "module": "Shopping Cart",
+ "name": "Product Card",
+ "owner": "Administrator",
+ "standard": 1,
+ "template": "",
+ "type": "Component"
+}
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/shopping_cart/web_template/product_category_cards/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/shopping_cart/web_template/product_category_cards/__init__.py
diff --git a/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.html b/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.html
new file mode 100644
index 0000000..06b76af
--- /dev/null
+++ b/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.html
@@ -0,0 +1,40 @@
+{%- macro card(title, image, url, text_primary=False) -%}
+{%- set align_class = resolve_class({
+ 'text-right': text_primary,
+ 'text-centre': align == 'Center',
+ 'text-left': align == 'Left',
+}) -%}
+<div class="card h-100">
+ {% if image %}
+ <img class="card-img-top" src="{{ image }}" alt="{{ title }}">
+ {% endif %}
+ <div class="card-body text-center text-muted small">
+ {{ title or '' }}
+ </div>
+ <a href="{{ url or '#' }}" class="stretched-link"></a>
+</div>
+{%- endmacro -%}
+
+<div class="section-with-cards product-category-section">
+ {%- if title -%}
+ <h2 class="section-title">{{ title }}</h2>
+ {%- endif -%}
+ {%- if subtitle -%}
+ <p class="section-description">{{ subtitle }}</p>
+ {%- endif -%}
+ <!-- {%- set card_size = card_size or 'Small' -%} -->
+ <div class="{{ resolve_class({'mt-6': title}) }}">
+ <div class="card-grid">
+ {%- for index in ['1', '2', '3', '4', '5', '6', '7', '8'] -%}
+ {%- set category = values['category_' + index] -%}
+ {%- if category -%}
+ {%- set category = frappe.get_doc("Item Group", category) -%}
+ {{ card(category.name, category.image, category.route) }}
+ {%- endif -%}
+ {%- endfor -%}
+ </div>
+ </div>
+</div>
+
+<style>
+</style>
diff --git a/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.json b/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.json
new file mode 100644
index 0000000..ba5f63b
--- /dev/null
+++ b/erpnext/shopping_cart/web_template/product_category_cards/product_category_cards.json
@@ -0,0 +1,85 @@
+{
+ "__unsaved": 1,
+ "creation": "2020-11-17 15:25:50.855934",
+ "docstatus": 0,
+ "doctype": "Web Template",
+ "fields": [
+ {
+ "fieldname": "title",
+ "fieldtype": "Data",
+ "label": "Title",
+ "reqd": 1
+ },
+ {
+ "fieldname": "subtitle",
+ "fieldtype": "Data",
+ "label": "Subtitle",
+ "reqd": 0
+ },
+ {
+ "fieldname": "category_1",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 0
+ },
+ {
+ "fieldname": "category_2",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 0
+ },
+ {
+ "fieldname": "category_3",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 0
+ },
+ {
+ "fieldname": "category_4",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 0
+ },
+ {
+ "fieldname": "category_5",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 0
+ },
+ {
+ "fieldname": "category_6",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 0
+ },
+ {
+ "fieldname": "category_7",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 0
+ },
+ {
+ "fieldname": "category_8",
+ "fieldtype": "Link",
+ "label": "Item Group",
+ "options": "Item Group",
+ "reqd": 0
+ }
+ ],
+ "idx": 0,
+ "modified": "2020-11-18 17:26:28.726260",
+ "modified_by": "Administrator",
+ "module": "Shopping Cart",
+ "name": "Product Category Cards",
+ "owner": "Administrator",
+ "standard": 1,
+ "template": "",
+ "type": "Section"
+}
\ No newline at end of file
diff --git a/erpnext/startup/filters.py b/erpnext/startup/filters.py
index a99e49b..ec07329 100644
--- a/erpnext/startup/filters.py
+++ b/erpnext/startup/filters.py
@@ -2,13 +2,13 @@
import frappe
def get_filters_config():
- filters_config = {
+ filters_config = {
"fiscal year": {
"label": "Fiscal Year",
"get_field": "erpnext.accounts.utils.get_fiscal_year_filter_field",
"valid_for_fieldtypes": ["Date", "Datetime", "DateRange"],
"depends_on": "company",
}
- }
+ }
- return filters_config
\ No newline at end of file
+ return filters_config
\ No newline at end of file
diff --git a/erpnext/startup/leaderboard.py b/erpnext/startup/leaderboard.py
index ef238f1..8819a55 100644
--- a/erpnext/startup/leaderboard.py
+++ b/erpnext/startup/leaderboard.py
@@ -12,6 +12,7 @@
{'fieldname': 'outstanding_amount', 'fieldtype': 'Currency'}
],
"method": "erpnext.startup.leaderboard.get_all_customers",
+ "icon": "customer"
},
"Item": {
"fields": [
@@ -23,6 +24,7 @@
{'fieldname': 'available_stock_value', 'fieldtype': 'Currency'}
],
"method": "erpnext.startup.leaderboard.get_all_items",
+ "icon": "stock"
},
"Supplier": {
"fields": [
@@ -31,6 +33,7 @@
{'fieldname': 'outstanding_amount', 'fieldtype': 'Currency'}
],
"method": "erpnext.startup.leaderboard.get_all_suppliers",
+ "icon": "buying"
},
"Sales Partner": {
"fields": [
@@ -38,12 +41,14 @@
{'fieldname': 'total_commission', 'fieldtype': 'Currency'}
],
"method": "erpnext.startup.leaderboard.get_all_sales_partner",
+ "icon": "hr"
},
"Sales Person": {
"fields": [
{'fieldname': 'total_sales_amount', 'fieldtype': 'Currency'}
],
"method": "erpnext.startup.leaderboard.get_all_sales_person",
+ "icon": "customer"
}
}
diff --git a/erpnext/stock/__init__.py b/erpnext/stock/__init__.py
index 8d64efe..9e240cc 100644
--- a/erpnext/stock/__init__.py
+++ b/erpnext/stock/__init__.py
@@ -64,10 +64,10 @@
if not account and warehouse.company:
account = get_company_default_inventory_account(warehouse.company)
- if not account and warehouse.company:
+ if not account and warehouse.company and not warehouse.is_group:
frappe.throw(_("Please set Account in Warehouse {0} or Default Inventory Account in Company {1}")
.format(warehouse.name, warehouse.company))
return account
def get_company_default_inventory_account(company):
- return frappe.get_cached_value('Company', company, 'default_inventory_account')
+ return frappe.get_cached_value('Company', company, 'default_inventory_account')
diff --git a/erpnext/stock/dashboard/item_dashboard.js b/erpnext/stock/dashboard/item_dashboard.js
index 9bd03d4..95cb92b 100644
--- a/erpnext/stock/dashboard/item_dashboard.js
+++ b/erpnext/stock/dashboard/item_dashboard.js
@@ -24,6 +24,16 @@
handle_move_add($(this), "Add")
});
+ this.content.on('click', '.btn-edit', function() {
+ let item = unescape($(this).attr('data-item'));
+ let warehouse = unescape($(this).attr('data-warehouse'));
+ let company = unescape($(this).attr('data-company'));
+ frappe.db.get_value('Putaway Rule',
+ {'item_code': item, 'warehouse': warehouse, 'company': company}, 'name', (r) => {
+ frappe.set_route("Form", "Putaway Rule", r.name);
+ });
+ });
+
function handle_move_add(element, action) {
let item = unescape(element.attr('data-item'));
let warehouse = unescape(element.attr('data-warehouse'));
@@ -59,7 +69,7 @@
// more
this.content.find('.btn-more').on('click', function() {
- me.start += 20;
+ me.start += me.page_length;
me.refresh();
});
@@ -69,33 +79,43 @@
this.before_refresh();
}
+ let args = {
+ item_code: this.item_code,
+ warehouse: this.warehouse,
+ parent_warehouse: this.parent_warehouse,
+ item_group: this.item_group,
+ company: this.company,
+ start: this.start,
+ sort_by: this.sort_by,
+ sort_order: this.sort_order
+ };
+
var me = this;
frappe.call({
- method: 'erpnext.stock.dashboard.item_dashboard.get_data',
- args: {
- item_code: this.item_code,
- warehouse: this.warehouse,
- item_group: this.item_group,
- start: this.start,
- sort_by: this.sort_by,
- sort_order: this.sort_order,
- },
+ method: this.method,
+ args: args,
callback: function(r) {
me.render(r.message);
}
});
},
render: function(data) {
- if(this.start===0) {
+ if (this.start===0) {
this.max_count = 0;
this.result.empty();
}
- var context = this.get_item_dashboard_data(data, this.max_count, true);
+ let context = "";
+ if (this.page_name === "warehouse-capacity-summary") {
+ context = this.get_capacity_dashboard_data(data);
+ } else {
+ context = this.get_item_dashboard_data(data, this.max_count, true);
+ }
+
this.max_count = this.max_count;
// show more button
- if(data && data.length===21) {
+ if (data && data.length===(this.page_length + 1)) {
this.content.find('.more').removeClass('hidden');
// remove the last element
@@ -106,12 +126,17 @@
// If not any stock in any warehouses provide a message to end user
if (context.data.length > 0) {
- $(frappe.render_template('item_dashboard_list', context)).appendTo(this.result);
+ this.content.find('.result').css('text-align', 'unset');
+ $(frappe.render_template(this.template, context)).appendTo(this.result);
} else {
- var message = __("Currently no stock available in any warehouse");
- $(`<span class='text-muted small'> ${message} </span>`).appendTo(this.result);
+ var message = __("No Stock Available Currently");
+ this.content.find('.result').css('text-align', 'center');
+
+ $(`<div class='text-muted' style='margin: 20px 5px;'>
+ ${message} </div>`).appendTo(this.result);
}
},
+
get_item_dashboard_data: function(data, max_count, show_item) {
if(!max_count) max_count = 0;
if(!data) data = [];
@@ -128,8 +153,8 @@
d.total_reserved, max_count);
});
- var can_write = 0;
- if(frappe.boot.user.can_write.indexOf("Stock Entry")>=0){
+ let can_write = 0;
+ if (frappe.boot.user.can_write.indexOf("Stock Entry") >= 0) {
can_write = 1;
}
@@ -138,9 +163,27 @@
max_count: max_count,
can_write:can_write,
show_item: show_item || false
+ };
+ },
+
+ get_capacity_dashboard_data: function(data) {
+ if (!data) data = [];
+
+ data.forEach(function(d) {
+ d.color = d.percent_occupied >=80 ? "#f8814f" : "#2490ef";
+ });
+
+ let can_write = 0;
+ if (frappe.boot.user.can_write.indexOf("Putaway Rule") >= 0) {
+ can_write = 1;
}
+
+ return {
+ data: data,
+ can_write: can_write,
+ };
}
-})
+});
erpnext.stock.move_item = function(item, source, target, actual_qty, rate, callback) {
var dialog = new frappe.ui.Dialog({
@@ -198,7 +241,7 @@
freeze: true,
callback: function(r) {
frappe.show_alert(__('Stock Entry {0} created',
- ['<a href="#Form/Stock Entry/'+r.message.name+'">' + r.message.name+ '</a>']));
+ ['<a href="/app/stock-entry/'+r.message.name+'">' + r.message.name+ '</a>']));
dialog.hide();
callback(r);
},
diff --git a/erpnext/stock/dashboard/item_dashboard_list.html b/erpnext/stock/dashboard/item_dashboard_list.html
index e1914ed..0c10be4 100644
--- a/erpnext/stock/dashboard/item_dashboard_list.html
+++ b/erpnext/stock/dashboard/item_dashboard_list.html
@@ -1,10 +1,10 @@
{% for d in data %}
<div class="dashboard-list-item">
<div class="row">
- <div class="col-sm-3 small" style="margin-top: 8px;">
+ <div class="col-sm-3" style="margin-top: 8px;">
<a data-type="warehouse" data-name="{{ d.warehouse }}">{{ d.warehouse }}</a>
</div>
- <div class="col-sm-3 small" style="margin-top: 8px;">
+ <div class="col-sm-3" style="margin-top: 8px;">
{% if show_item %}
<a data-type="item"
data-name="{{ d.item_code }}">{{ d.item_code }}
@@ -12,7 +12,7 @@
</a>
{% endif %}
</div>
- <div class="col-sm-4 small">
+ <div class="col-sm-4">
<span class="inline-graph">
<span class="inline-graph-half" title="{{ __("Reserved Qty") }}">
<span class="inline-graph-count">{{ d.total_reserved }}</span>
@@ -40,7 +40,7 @@
</span>
</div>
{% if can_write %}
- <div class="col-sm-2 text-right" style="margin-top: 8px;">
+ <div class="col-sm-2 text-right" style="margin: var(--margin-sm) 0;">
{% if d.actual_qty %}
<button class="btn btn-default btn-xs btn-move"
data-disable_quick_entry="{{ d.disable_quick_entry }}"
diff --git a/erpnext/stock/dashboard/warehouse_capacity_dashboard.py b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py
new file mode 100644
index 0000000..ab573e5
--- /dev/null
+++ b/erpnext/stock/dashboard/warehouse_capacity_dashboard.py
@@ -0,0 +1,69 @@
+from __future__ import unicode_literals
+
+import frappe
+from frappe.model.db_query import DatabaseQuery
+from frappe.utils import nowdate
+from frappe.utils import flt
+from erpnext.stock.utils import get_stock_balance
+
+@frappe.whitelist()
+def get_data(item_code=None, warehouse=None, parent_warehouse=None,
+ company=None, start=0, sort_by="stock_capacity", sort_order="desc"):
+ """Return data to render the warehouse capacity dashboard."""
+ filters = get_filters(item_code, warehouse, parent_warehouse, company)
+
+ no_permission, filters = get_warehouse_filter_based_on_permissions(filters)
+ if no_permission:
+ return []
+
+ capacity_data = get_warehouse_capacity_data(filters, start)
+
+ asc_desc = -1 if sort_order == "desc" else 1
+ capacity_data = sorted(capacity_data, key = lambda i: (i[sort_by] * asc_desc))
+
+ return capacity_data
+
+def get_filters(item_code=None, warehouse=None, parent_warehouse=None,
+ company=None):
+ filters = [['disable', '=', 0]]
+ if item_code:
+ filters.append(['item_code', '=', item_code])
+ if warehouse:
+ filters.append(['warehouse', '=', warehouse])
+ if company:
+ filters.append(['company', '=', company])
+ if parent_warehouse:
+ lft, rgt = frappe.db.get_value("Warehouse", parent_warehouse, ["lft", "rgt"])
+ warehouses = frappe.db.sql_list("""
+ select name from `tabWarehouse`
+ where lft >=%s and rgt<=%s
+ """, (lft, rgt))
+ filters.append(['warehouse', 'in', warehouses])
+ return filters
+
+def get_warehouse_filter_based_on_permissions(filters):
+ try:
+ # check if user has any restrictions based on user permissions on warehouse
+ if DatabaseQuery('Warehouse', user=frappe.session.user).build_match_conditions():
+ filters.append(['warehouse', 'in', [w.name for w in frappe.get_list('Warehouse')]])
+ return False, filters
+ except frappe.PermissionError:
+ # user does not have access on warehouse
+ return True, []
+
+def get_warehouse_capacity_data(filters, start):
+ capacity_data = frappe.db.get_all('Putaway Rule',
+ fields=['item_code', 'warehouse','stock_capacity', 'company'],
+ filters=filters,
+ limit_start=start,
+ limit_page_length='11'
+ )
+
+ for entry in capacity_data:
+ balance_qty = get_stock_balance(entry.item_code, entry.warehouse, nowdate()) or 0
+ entry.update({
+ 'actual_qty': balance_qty,
+ 'percent_occupied': flt((flt(balance_qty) / flt(entry.stock_capacity)) * 100, 0)
+ })
+
+ return capacity_data
\ No newline at end of file
diff --git a/erpnext/stock/desk_page/stock/stock.json b/erpnext/stock/desk_page/stock/stock.json
deleted file mode 100644
index 9068e33..0000000
--- a/erpnext/stock/desk_page/stock/stock.json
+++ /dev/null
@@ -1,124 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Items and Pricing",
- "links": "[\n {\n \"label\": \"Item\",\n \"name\": \"Item\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"icon\": \"fa fa-sitemap\",\n \"label\": \"Item Group\",\n \"link\": \"Tree/Item Group\",\n \"name\": \"Item Group\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Product Bundle\",\n \"name\": \"Product Bundle\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Price List\",\n \"name\": \"Price List\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Price\",\n \"name\": \"Item Price\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipping Rule\",\n \"name\": \"Shipping Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Pricing Rule\",\n \"name\": \"Pricing Rule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Alternative\",\n \"name\": \"Item Alternative\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Manufacturer\",\n \"name\": \"Item Manufacturer\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Customs Tariff Number\",\n \"name\": \"Customs Tariff Number\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Stock Transactions",
- "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Material Request\",\n \"name\": \"Material Request\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Entry\",\n \"name\": \"Stock Entry\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Customer\"\n ],\n \"label\": \"Delivery Note\",\n \"name\": \"Delivery Note\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\",\n \"Supplier\"\n ],\n \"label\": \"Purchase Receipt\",\n \"name\": \"Purchase Receipt\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Pick List\",\n \"name\": \"Pick List\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Shipment\",\n \"name\": \"Shipment\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Delivery Trip\",\n \"name\": \"Delivery Trip\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Stock Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Ledger\",\n \"name\": \"Stock Ledger\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Balance\",\n \"name\": \"Stock Balance\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Projected Qty\",\n \"name\": \"Stock Projected Qty\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Stock Summary\",\n \"name\": \"stock-balance\",\n \"type\": \"page\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Ageing\",\n \"name\": \"Stock Ageing\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Price Stock\",\n \"name\": \"Item Price Stock\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Settings",
- "links": "[\n {\n \"label\": \"Stock Settings\",\n \"name\": \"Stock Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Warehouse\",\n \"name\": \"Warehouse\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Unit of Measure (UOM)\",\n \"name\": \"UOM\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Variant Settings\",\n \"name\": \"Item Variant Settings\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Brand\",\n \"name\": \"Brand\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Item Attribute\",\n \"name\": \"Item Attribute\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"UOM Conversion Factor\",\n \"name\": \"UOM Conversion Factor\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Serial No and Batch",
- "links": "[\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Serial No\",\n \"name\": \"Serial No\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Batch\",\n \"name\": \"Batch\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"label\": \"Installation Note\",\n \"name\": \"Installation Note\",\n \"type\": \"doctype\"\n },\n {\n \"dependencies\": [\n \"Serial No\"\n ],\n \"doctype\": \"Serial No\",\n \"label\": \"Serial No Service Contract Expiry\",\n \"name\": \"Serial No Service Contract Expiry\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Serial No\"\n ],\n \"doctype\": \"Serial No\",\n \"label\": \"Serial No Status\",\n \"name\": \"Serial No Status\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Serial No\"\n ],\n \"doctype\": \"Serial No\",\n \"label\": \"Serial No Warranty Expiry\",\n \"name\": \"Serial No Warranty Expiry\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Tools",
- "links": "[\n {\n \"label\": \"Stock Reconciliation\",\n \"name\": \"Stock Reconciliation\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Landed Cost Voucher\",\n \"name\": \"Landed Cost Voucher\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Packing Slip\",\n \"name\": \"Packing Slip\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Quality Inspection\",\n \"name\": \"Quality Inspection\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Quality Inspection Template\",\n \"name\": \"Quality Inspection Template\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Quick Stock Balance\",\n \"name\": \"Quick Stock Balance\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Key Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Item Price\"\n ],\n \"doctype\": \"Item Price\",\n \"is_query_report\": false,\n \"label\": \"Item-wise Price List Rate\",\n \"name\": \"Item-wise Price List Rate\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Entry\"\n ],\n \"doctype\": \"Stock Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock Analytics\",\n \"name\": \"Stock Analytics\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Stock Qty vs Serial No Count\",\n \"name\": \"Stock Qty vs Serial No Count\",\n \"onboard\": 1,\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Delivery Note\"\n ],\n \"doctype\": \"Delivery Note\",\n \"is_query_report\": true,\n \"label\": \"Delivery Note Trends\",\n \"name\": \"Delivery Note Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Receipt\"\n ],\n \"doctype\": \"Purchase Receipt\",\n \"is_query_report\": true,\n \"label\": \"Purchase Receipt Trends\",\n \"name\": \"Purchase Receipt Trends\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Sales Order\"\n ],\n \"doctype\": \"Sales Order\",\n \"is_query_report\": true,\n \"label\": \"Sales Order Analysis\",\n \"name\": \"Sales Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Purchase Order Analysis\",\n \"name\": \"Purchase Order Analysis\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Bin\"\n ],\n \"doctype\": \"Bin\",\n \"is_query_report\": true,\n \"label\": \"Item Shortage Report\",\n \"name\": \"Item Shortage Report\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Batch\"\n ],\n \"doctype\": \"Batch\",\n \"is_query_report\": true,\n \"label\": \"Batch-Wise Balance History\",\n \"name\": \"Batch-Wise Balance History\",\n \"type\": \"report\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Other Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Material Request\"\n ],\n \"doctype\": \"Material Request\",\n \"is_query_report\": true,\n \"label\": \"Requested Items To Be Transferred\",\n \"name\": \"Requested Items To Be Transferred\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Ledger Entry\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Batch Item Expiry Status\",\n \"name\": \"Batch Item Expiry Status\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Price List\"\n ],\n \"doctype\": \"Price List\",\n \"is_query_report\": true,\n \"label\": \"Item Prices\",\n \"name\": \"Item Prices\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Itemwise Recommended Reorder Level\",\n \"name\": \"Itemwise Recommended Reorder Level\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Item\"\n ],\n \"doctype\": \"Item\",\n \"is_query_report\": true,\n \"label\": \"Item Variant Details\",\n \"name\": \"Item Variant Details\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Subcontracted Raw Materials To Be Transferred\",\n \"name\": \"Subcontracted Raw Materials To Be Transferred\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Purchase Order\"\n ],\n \"doctype\": \"Purchase Order\",\n \"is_query_report\": true,\n \"label\": \"Subcontracted Item To Be Received\",\n \"name\": \"Subcontracted Item To Be Received\",\n \"type\": \"report\"\n },\n {\n \"dependencies\": [\n \"Stock Ledger Entry\"\n ],\n \"doctype\": \"Stock Ledger Entry\",\n \"is_query_report\": true,\n \"label\": \"Stock and Account Value Comparison\",\n \"name\": \"Stock and Account Value Comparison\",\n \"type\": \"report\"\n }\n]"
- }
- ],
- "cards_label": "Masters & Reports",
- "category": "Modules",
- "charts": [
- {
- "chart_name": "Warehouse wise Stock Value"
- }
- ],
- "creation": "2020-03-02 15:43:10.096528",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Stock",
- "modified": "2020-12-02 15:47:41.532942",
- "modified_by": "Administrator",
- "module": "Stock",
- "name": "Stock",
- "onboarding": "Stock",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "color": "#cef6d1",
- "format": "{} Available",
- "label": "Item",
- "link_to": "Item",
- "stats_filter": "{\n \"disabled\" : 0\n}",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} Pending",
- "label": "Material Request",
- "link_to": "Material Request",
- "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"Pending\"\n}",
- "type": "DocType"
- },
- {
- "label": "Stock Entry",
- "link_to": "Stock Entry",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} To Bill",
- "label": "Purchase Receipt",
- "link_to": "Purchase Receipt",
- "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"To Bill\"\n}",
- "type": "DocType"
- },
- {
- "color": "#ffe8cd",
- "format": "{} To Bill",
- "label": "Delivery Note",
- "link_to": "Delivery Note",
- "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"To Bill\"\n}",
- "type": "DocType"
- },
- {
- "label": "Stock Ledger",
- "link_to": "Stock Ledger",
- "type": "Report"
- },
- {
- "label": "Stock Balance",
- "link_to": "Stock Balance",
- "type": "Report"
- },
- {
- "label": "Dashboard",
- "link_to": "Stock",
- "type": "Dashboard"
- }
- ],
- "shortcuts_label": "Quick Access"
-}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/batch/batch.js b/erpnext/stock/doctype/batch/batch.js
index e2ea7f9..3b07e4e 100644
--- a/erpnext/stock/doctype/batch/batch.js
+++ b/erpnext/stock/doctype/batch/batch.js
@@ -47,8 +47,7 @@
return;
}
- var section = frm.dashboard.add_section(`<h5 style="margin-top: 0px;">
- ${ __("Stock Levels") }</a></h5>`);
+ const section = frm.dashboard.add_section('', __("Stock Levels"));
// sort by qty
r.message.sort(function(a, b) { a.qty > b.qty ? 1 : -1 });
@@ -103,7 +102,7 @@
},
callback: (r) => {
frappe.show_alert(__('Stock Entry {0} created',
- ['<a href="#Form/Stock Entry/'+r.message.name+'">' + r.message.name+ '</a>']));
+ ['<a href="/app/stock-entry/'+r.message.name+'">' + r.message.name+ '</a>']));
frm.refresh();
},
});
diff --git a/erpnext/stock/doctype/batch/batch_list.js b/erpnext/stock/doctype/batch/batch_list.js
index d4f74c3..0de9fd0 100644
--- a/erpnext/stock/doctype/batch/batch_list.js
+++ b/erpnext/stock/doctype/batch/batch_list.js
@@ -2,9 +2,9 @@
add_fields: ["item", "expiry_date", "batch_qty", "disabled"],
get_indicator: (doc) => {
if (doc.disabled) {
- return [__("Disabled"), "darkgrey", "disabled,=,1"];
+ return [__("Disabled"), "gray", "disabled,=,1"];
} else if (!doc.batch_qty) {
- return [__("Empty"), "darkgrey", "batch_qty,=,0|disabled,=,0"];
+ return [__("Empty"), "gray", "batch_qty,=,0|disabled,=,0"];
} else if (doc.expiry_date && frappe.datetime.get_diff(doc.expiry_date, frappe.datetime.nowdate()) <= 0) {
return [__("Expired"), "red", "expiry_date,not in,|expiry_date,<=,Today|batch_qty,>,0|disabled,=,0"]
} else {
diff --git a/erpnext/stock/doctype/batch/test_batch.py b/erpnext/stock/doctype/batch/test_batch.py
index c2a3d3c..cbd272d 100644
--- a/erpnext/stock/doctype/batch/test_batch.py
+++ b/erpnext/stock/doctype/batch/test_batch.py
@@ -8,13 +8,10 @@
from erpnext.stock.doctype.batch.batch import get_batch_qty, UnableToSelectBatchError, get_batch_no
from frappe.utils import cint, flt
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
+from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
+from erpnext.stock.get_item_details import get_item_details
class TestBatch(unittest.TestCase):
-
- def setUp(self):
- set_perpetual_inventory(0)
-
def test_item_has_batch_enabled(self):
self.assertRaises(ValidationError, frappe.get_doc({
"doctype": "Batch",
@@ -187,7 +184,7 @@
stock_entry.cancel()
current_batch_qty = flt(frappe.db.get_value("Batch", "B100", "batch_qty"))
self.assertEqual(current_batch_qty, existing_batch_qty)
-
+
@classmethod
def make_new_batch_and_entry(cls, item_name, batch_name, warehouse):
'''Make a new stock entry for given target warehouse and batch name of item'''
@@ -257,6 +254,72 @@
return batch
+ def test_batch_wise_item_price(self):
+ if not frappe.db.get_value('Item', '_Test Batch Price Item'):
+ frappe.get_doc({
+ 'doctype': 'Item',
+ 'is_stock_item': 1,
+ 'item_code': '_Test Batch Price Item',
+ 'item_group': 'Products',
+ 'has_batch_no': 1,
+ 'create_new_batch': 1
+ }).insert(ignore_permissions=True)
+
+ batch1 = create_batch('_Test Batch Price Item', 200, 1)
+ batch2 = create_batch('_Test Batch Price Item', 300, 1)
+ batch3 = create_batch('_Test Batch Price Item', 400, 0)
+
+ args = frappe._dict({
+ "item_code": "_Test Batch Price Item",
+ "company": "_Test Company with perpetual inventory",
+ "price_list": "_Test Price List",
+ "currency": "_Test Currency",
+ "doctype": "Sales Invoice",
+ "conversion_rate": 1,
+ "price_list_currency": "_Test Currency",
+ "plc_conversion_rate": 1,
+ "customer": "_Test Customer",
+ "name": None
+ })
+
+ #test price for batch1
+ args.update({'batch_no': batch1})
+ details = get_item_details(args)
+ self.assertEqual(details.get('price_list_rate'), 200)
+
+ #test price for batch2
+ args.update({'batch_no': batch2})
+ details = get_item_details(args)
+ self.assertEqual(details.get('price_list_rate'), 300)
+
+ #test price for batch3
+ args.update({'batch_no': batch3})
+ details = get_item_details(args)
+ self.assertEqual(details.get('price_list_rate'), 400)
+
+def create_batch(item_code, rate, create_item_price_for_batch):
+ pi = make_purchase_invoice(company="_Test Company",
+ warehouse= "Stores - _TC", cost_center = "Main - _TC", update_stock=1,
+ expense_account ="_Test Account Cost for Goods Sold - _TC", item_code=item_code)
+
+ batch = frappe.db.get_value('Batch', {'item': item_code, 'reference_name': pi.name})
+
+ if not create_item_price_for_batch:
+ create_price_list_for_batch(item_code, None, rate)
+ else:
+ create_price_list_for_batch(item_code, batch, rate)
+
+ return batch
+
+def create_price_list_for_batch(item_code, batch, rate):
+ frappe.get_doc({
+ 'doctype': 'Item Price',
+ 'item_code': '_Test Batch Price Item',
+ 'price_list': '_Test Price List',
+ 'batch_no': batch,
+ 'price_list_rate': rate
+ }).insert()
+
def make_new_batch(**args):
args = frappe._dict(args)
diff --git a/erpnext/stock/doctype/bin/bin.py b/erpnext/stock/doctype/bin/bin.py
index 7acdec7..0514bd2 100644
--- a/erpnext/stock/doctype/bin/bin.py
+++ b/erpnext/stock/doctype/bin/bin.py
@@ -18,20 +18,31 @@
self.update_qty(args)
if args.get("actual_qty") or args.get("voucher_type") == "Stock Reconciliation":
- from erpnext.stock.stock_ledger import update_entries_after
+ from erpnext.stock.stock_ledger import update_entries_after, update_qty_in_future_sle
if not args.get("posting_date"):
args["posting_date"] = nowdate()
+ if args.get("is_cancelled") and via_landed_cost_voucher:
+ return
+
+ # Reposts only current voucher SL Entries
+ # Updates valuation rate, stock value, stock queue for current transaction
update_entries_after({
"item_code": self.item_code,
"warehouse": self.warehouse,
"posting_date": args.get("posting_date"),
"posting_time": args.get("posting_time"),
+ "voucher_type": args.get("voucher_type"),
"voucher_no": args.get("voucher_no"),
- "sle_id": args.sle_id
+ "sle_id": args.name,
+ "creation": args.creation
}, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+ # update qty in future ale and Validate negative qty
+ update_qty_in_future_sle(args, allow_negative_stock)
+
+
def update_qty(self, args):
# update the stock values (for current quantities)
if args.get("voucher_type")=="Stock Reconciliation":
@@ -43,7 +54,7 @@
self.reserved_qty = flt(self.reserved_qty) + flt(args.get("reserved_qty"))
self.indented_qty = flt(self.indented_qty) + flt(args.get("indented_qty"))
self.planned_qty = flt(self.planned_qty) + flt(args.get("planned_qty"))
-
+
self.set_projected_qty()
self.db_update()
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.js b/erpnext/stock/doctype/delivery_note/delivery_note.js
index 03921c5..334bdea 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.js
@@ -7,14 +7,16 @@
frappe.provide("erpnext.stock");
frappe.provide("erpnext.stock.delivery_note");
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Delivery Note", {
setup: function(frm) {
frm.custom_make_buttons = {
'Packing Slip': 'Packing Slip',
'Installation Note': 'Installation Note',
- 'Sales Invoice': 'Invoice',
+ 'Sales Invoice': 'Sales Invoice',
'Stock Entry': 'Return',
+ 'Shipment': 'Shipment'
},
frm.set_indicator_formatter('item_code',
function(doc) {
@@ -75,7 +77,7 @@
}
});
-
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
print_without_amount: function(frm) {
@@ -93,13 +95,19 @@
frm.page.set_inner_btn_group_as_primary(__('Create'));
}
- if (frm.doc.docstatus === 1 && frm.doc.is_internal_customer && !frm.doc.inter_company_reference) {
- frm.add_custom_button(__('Purchase Receipt'), function() {
- frappe.model.open_mapped_doc({
- method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt',
- frm: frm,
- })
- }, __('Create'));
+ if (frm.doc.docstatus == 1 && !frm.doc.inter_company_reference) {
+ let internal = me.frm.doc.is_internal_customer;
+ if (internal) {
+ let button_label = (me.frm.doc.company === me.frm.doc.represents_company) ? "Internal Purchase Receipt" :
+ "Inter Company Purchase Receipt";
+
+ me.frm.add_custom_button(button_label, function() {
+ frappe.model.open_mapped_doc({
+ method: 'erpnext.stock.doctype.delivery_note.delivery_note.make_inter_company_purchase_receipt',
+ frm: frm,
+ });
+ }, __('Create'));
+ }
}
}
});
@@ -295,15 +303,6 @@
}
})
},
-
- to_warehouse: function() {
- let packed_items_table = this.frm.doc["packed_items"];
- this.autofill_warehouse(this.frm.doc["items"], "target_warehouse", this.frm.doc.to_warehouse);
- if (packed_items_table && packed_items_table.length) {
- this.autofill_warehouse(packed_items_table, "target_warehouse", this.frm.doc.to_warehouse);
- }
- }
-
});
$.extend(cur_frm.cscript, new erpnext.stock.DeliveryNoteController({frm: cur_frm}));
@@ -317,6 +316,7 @@
company: function(frm) {
frm.trigger("unhide_account_head");
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
unhide_account_head: function(frm) {
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.json b/erpnext/stock/doctype/delivery_note/delivery_note.json
index 7393c8a..f595aad 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.json
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.json
@@ -53,7 +53,7 @@
"sec_warehouse",
"set_warehouse",
"col_break_warehouse",
- "to_warehouse",
+ "set_target_warehouse",
"items_section",
"scan_barcode",
"items",
@@ -117,6 +117,7 @@
"source",
"column_break5",
"is_internal_customer",
+ "represents_company",
"inter_company_reference",
"per_billed",
"customer_group",
@@ -133,6 +134,7 @@
"per_installed",
"installation_status",
"column_break_89",
+ "per_returned",
"excise_page",
"instructions",
"subscription_section",
@@ -502,18 +504,6 @@
"fieldtype": "Column Break"
},
{
- "description": "Required only for sample item.",
- "fieldname": "to_warehouse",
- "fieldtype": "Link",
- "in_standard_filter": 1,
- "label": "To Warehouse",
- "no_copy": 1,
- "oldfieldname": "to_warehouse",
- "oldfieldtype": "Link",
- "options": "Warehouse",
- "print_hide": 1
- },
- {
"fieldname": "items_section",
"fieldtype": "Section Break",
"oldfieldtype": "Section Break",
@@ -1099,7 +1089,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "\nDraft\nTo Bill\nCompleted\nCancelled\nClosed",
+ "options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
@@ -1251,13 +1241,43 @@
"fieldtype": "Link",
"label": "Inter Company Reference",
"options": "Purchase Receipt"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "per_returned",
+ "fieldtype": "Percent",
+ "label": "% Returned",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.is_internal_customer",
+ "fieldname": "set_target_warehouse",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Set Target Warehouse",
+ "no_copy": 1,
+ "oldfieldname": "to_warehouse",
+ "oldfieldtype": "Link",
+ "options": "Warehouse",
+ "print_hide": 1
+ },
+ {
+ "description": "Company which internal customer represents.",
+ "fetch_from": "customer.represents_company",
+ "fieldname": "represents_company",
+ "fieldtype": "Link",
+ "label": "Represents Company",
+ "options": "Company",
+ "read_only": 1
}
],
"icon": "fa fa-truck",
"idx": 146,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-11 14:57:16.388139",
+ "modified": "2020-12-26 17:07:59.194403",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note",
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py
index 979e83d..3544390 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note.py
@@ -55,7 +55,7 @@
'no_allowance': 1
}]
if cint(self.is_return):
- self.status_updater.append({
+ self.status_updater.extend([{
'source_dt': 'Delivery Note Item',
'target_dt': 'Sales Order Item',
'join_field': 'so_detail',
@@ -69,9 +69,21 @@
where name=`tabDelivery Note Item`.parent and is_return=1)""",
'second_source_extra_cond': """ and exists (select name from `tabSales Invoice`
where name=`tabSales Invoice Item`.parent and is_return=1 and update_stock=1)"""
- })
+ },
+ {
+ 'source_dt': 'Delivery Note Item',
+ 'target_dt': 'Delivery Note Item',
+ 'join_field': 'dn_detail',
+ 'target_field': 'returned_qty',
+ 'target_parent_dt': 'Delivery Note',
+ 'target_parent_field': 'per_returned',
+ 'target_ref_field': 'stock_qty',
+ 'source_field': '-1 * stock_qty',
+ 'percent_join_field_parent': 'return_against'
+ }
+ ])
- def before_print(self):
+ def before_print(self, settings=None):
def toggle_print_hide(meta, fieldname):
df = meta.get_field(fieldname)
if self.get("print_without_amount"):
@@ -205,6 +217,7 @@
# because updating reserved qty in bin depends upon updated delivered qty in SO
self.update_stock_ledger()
self.make_gl_entries()
+ self.repost_future_sle_and_gle()
def on_cancel(self):
super(DeliveryNote, self).on_cancel()
@@ -222,7 +235,8 @@
self.cancel_packing_slips()
self.make_gl_entries_on_cancel()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.repost_future_sle_and_gle()
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
def check_credit_limit(self):
from erpnext.selling.doctype.customer.customer import check_credit_limit
@@ -584,6 +598,9 @@
pickup_contact_display += '<br>' + user.mobile_no
target.pickup_contact = pickup_contact_display
+ # As we are using session user details in the pickup_contact then pickup_contact_person will be session user
+ target.pickup_contact_person = frappe.session.user
+
contact = frappe.db.get_value("Contact", source.contact_person, ['email_id', 'phone', 'mobile_no'], as_dict=1)
delivery_contact_display = '{}'.format(source.contact_display)
if contact:
@@ -595,6 +612,13 @@
delivery_contact_display += '<br>' + contact.mobile_no
target.delivery_contact = delivery_contact_display
+ if source.shipping_address_name:
+ target.delivery_address_name = source.shipping_address_name
+ target.delivery_address = source.shipping_address
+ elif source.customer_address:
+ target.delivery_address_name = source.customer_address
+ target.delivery_address = source.address_display
+
doclist = get_mapped_doc("Delivery Note", source_name, {
"Delivery Note": {
"doctype": "Shipment",
@@ -603,9 +627,7 @@
"company": "pickup_company",
"company_address": "pickup_address_name",
"company_address_display": "pickup_address",
- "address_display": "delivery_address",
"customer": "delivery_customer",
- "shipping_address_name": "delivery_address_name",
"contact_person": "delivery_contact_name",
"contact_email": "delivery_contact_email"
},
@@ -623,7 +645,7 @@
}
}
}, target_doc, postprocess)
-
+
return doclist
@frappe.whitelist()
@@ -642,7 +664,8 @@
return make_inter_company_transaction("Delivery Note", source_name, target_doc)
def make_inter_company_transaction(doctype, source_name, target_doc=None):
- from erpnext.accounts.doctype.sales_invoice.sales_invoice import validate_inter_company_transaction, get_inter_company_details
+ from erpnext.accounts.doctype.sales_invoice.sales_invoice import (validate_inter_company_transaction,
+ get_inter_company_details, update_address, update_taxes, set_purchase_references)
if doctype == 'Delivery Note':
source_doc = frappe.get_doc(doctype, source_name)
@@ -660,6 +683,7 @@
def set_missing_values(source, target):
target.run_method("set_missing_values")
+ set_purchase_references(target)
if target.doctype == 'Purchase Receipt':
master_doctype = 'Purchase Taxes and Charges Template'
@@ -675,21 +699,35 @@
if target_doc.doctype == 'Purchase Receipt':
target_doc.company = details.get("company")
target_doc.supplier = details.get("party")
- target_doc.supplier_address = source_doc.company_address
- target_doc.shipping_address = source_doc.shipping_address_name or source_doc.customer_address
target_doc.buying_price_list = source_doc.selling_price_list
target_doc.is_internal_supplier = 1
target_doc.inter_company_reference = source_doc.name
+
+ # Invert the address on target doc creation
+ update_address(target_doc, 'supplier_address', 'address_display', source_doc.company_address)
+ update_address(target_doc, 'shipping_address', 'shipping_address_display', source_doc.customer_address)
+
+ update_taxes(target_doc, party=target_doc.supplier, party_type='Supplier', company=target_doc.company,
+ doctype=target_doc.doctype, party_address=target_doc.supplier_address,
+ company_address=target_doc.shipping_address)
else:
target_doc.company = details.get("company")
target_doc.customer = details.get("party")
target_doc.company_address = source_doc.supplier_address
- target_doc.shipping_address_name = source_doc.shipping_address
target_doc.selling_price_list = source_doc.buying_price_list
target_doc.is_internal_customer = 1
target_doc.inter_company_reference = source_doc.name
- doclist = get_mapped_doc(doctype, source_name, {
+ # Invert the address on target doc creation
+ update_address(target_doc, 'company_address', 'company_address_display', source_doc.supplier_address)
+ update_address(target_doc, 'shipping_address_name', 'shipping_address', source_doc.shipping_address)
+ update_address(target_doc, 'customer_address', 'address_display', source_doc.shipping_address)
+
+ update_taxes(target_doc, party=target_doc.customer, party_type='Customer', company=target_doc.company,
+ doctype=target_doc.doctype, party_address=target_doc.customer_address,
+ company_address=target_doc.company_address, shipping_address_name=target_doc.shipping_address_name)
+
+ doclist = get_mapped_doc(doctype, source_name, {
doctype: {
"doctype": target_doctype,
"postprocess": update_details,
@@ -700,7 +738,10 @@
doctype +" Item": {
"doctype": target_doctype + " Item",
"field_map": {
- source_document_warehouse_field: target_document_warehouse_field
+ source_document_warehouse_field: target_document_warehouse_field,
+ 'name': 'delivery_note_item',
+ 'batch_no': 'batch_no',
+ 'serial_no': 'serial_no'
},
"field_no_map": [
"warehouse"
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
index beeb9eb..47684d5 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_dashboard.py
@@ -19,7 +19,7 @@
},
{
'label': _('Reference'),
- 'items': ['Sales Order', 'Quality Inspection']
+ 'items': ['Sales Order', 'Shipment', 'Quality Inspection']
},
{
'label': _('Returns'),
diff --git a/erpnext/stock/doctype/delivery_note/delivery_note_list.js b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
index 0ae7c37..f08125b 100644
--- a/erpnext/stock/doctype/delivery_note/delivery_note_list.js
+++ b/erpnext/stock/doctype/delivery_note/delivery_note_list.js
@@ -3,12 +3,14 @@
"transporter_name", "grand_total", "is_return", "status", "currency"],
get_indicator: function(doc) {
if(cint(doc.is_return)==1) {
- return [__("Return"), "darkgrey", "is_return,=,Yes"];
+ return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
+ } else if (flt(doc.per_returned, 2) === 100) {
+ return [__("Return Issued"), "grey", "per_returned,=,100"];
} else if (flt(doc.per_billed, 2) < 100) {
return [__("To Bill"), "orange", "per_billed,<,100"];
- } else if (flt(doc.per_billed, 2) == 100) {
+ } else if (flt(doc.per_billed, 2) === 100) {
return [__("Completed"), "green", "per_billed,=,100"];
}
},
diff --git a/erpnext/stock/doctype/delivery_note/test_delivery_note.py b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
index 9566af7..d39b229 100644
--- a/erpnext/stock/doctype/delivery_note/test_delivery_note.py
+++ b/erpnext/stock/doctype/delivery_note/test_delivery_note.py
@@ -10,8 +10,7 @@
from frappe.utils import cint, nowdate, nowtime, cstr, add_days, flt, today
from erpnext.stock.stock_ledger import get_previous_sle
from erpnext.accounts.utils import get_balance_on
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
- import get_gl_entries, set_perpetual_inventory
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import get_gl_entries
from erpnext.stock.doctype.delivery_note.delivery_note import make_sales_invoice, make_delivery_trip
from erpnext.stock.doctype.stock_entry.test_stock_entry \
import make_stock_entry, make_serialized_item, get_qty_after_transaction
@@ -24,9 +23,6 @@
from erpnext.stock.doctype.item.test_item import create_item
class TestDeliveryNote(unittest.TestCase):
- def setUp(self):
- set_perpetual_inventory(0)
-
def test_over_billing_against_dn(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -43,7 +39,6 @@
def test_delivery_note_no_gl_entry(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
- set_perpetual_inventory(0, company)
make_stock_entry(target="_Test Warehouse - _TC", qty=5, basic_rate=100)
stock_queue = json.loads(get_previous_sle({
@@ -206,7 +201,7 @@
for field, value in field_values.items():
self.assertEqual(cstr(serial_no.get(field)), value)
- def test_sales_return_for_non_bundled_items(self):
+ def test_sales_return_for_non_bundled_items_partial(self):
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
make_stock_entry(item_code="_Test Item", target="Stores - TCP1", qty=50, basic_rate=100)
@@ -225,7 +220,10 @@
# return entry
dn1 = create_delivery_note(is_return=1, return_against=dn.name, qty=-2, rate=500,
- company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1")
+ company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1", do_not_submit=1)
+ dn1.items[0].dn_detail = dn.items[0].name
+ dn1.submit()
actual_qty_2 = get_qty_after_transaction(warehouse="Stores - TCP1")
@@ -243,6 +241,70 @@
self.assertEqual(gle_warehouse_amount, stock_value_difference)
+ # hack because new_doc isn't considering is_return portion of status_updater
+ returned = frappe.get_doc("Delivery Note", dn1.name)
+ returned.update_prevdoc_status()
+ dn.load_from_db()
+
+ # Check if Original DN updated
+ self.assertEqual(dn.items[0].returned_qty, 2)
+ self.assertEqual(dn.per_returned, 40)
+
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ return_dn_2 = make_return_doc("Delivery Note", dn.name)
+
+ # Check if unreturned amount is mapped in 2nd return
+ self.assertEqual(return_dn_2.items[0].qty, -3)
+
+ si = make_sales_invoice(dn.name)
+ si.submit()
+
+ self.assertEqual(si.items[0].qty, 3)
+
+ dn.load_from_db()
+ # DN should be completed on billing all unreturned amount
+ self.assertEqual(dn.items[0].billed_amt, 1500)
+ self.assertEqual(dn.per_billed, 100)
+ self.assertEqual(dn.status, 'Completed')
+
+ si.load_from_db()
+ si.cancel()
+
+ dn.load_from_db()
+ self.assertEqual(dn.per_billed, 0)
+
+ dn1.cancel()
+ dn.cancel()
+
+ def test_sales_return_for_non_bundled_items_full(self):
+ from erpnext.stock.doctype.item.test_item import make_item
+
+ company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
+
+ make_item("Box", {'is_stock_item': 1})
+
+ make_stock_entry(item_code="Box", target="Stores - TCP1", qty=10, basic_rate=100)
+
+ dn = create_delivery_note(item_code="Box", qty=5, rate=500, warehouse="Stores - TCP1", company=company,
+ expense_account="Cost of Goods Sold - TCP1", cost_center="Main - TCP1")
+
+ #return entry
+ dn1 = create_delivery_note(item_code="Box", is_return=1, return_against=dn.name, qty=-5, rate=500,
+ company=company, warehouse="Stores - TCP1", expense_account="Cost of Goods Sold - TCP1",
+ cost_center="Main - TCP1", do_not_submit=1)
+ dn1.items[0].dn_detail = dn.items[0].name
+ dn1.submit()
+
+ # hack because new_doc isn't considering is_return portion of status_updater
+ returned = frappe.get_doc("Delivery Note", dn1.name)
+ returned.update_prevdoc_status()
+ dn.load_from_db()
+
+ # Check if Original DN updated
+ self.assertEqual(dn.items[0].returned_qty, 5)
+ self.assertEqual(dn.per_returned, 100)
+ self.assertEqual(dn.status, 'Return Issued')
+
def test_return_single_item_from_bundled_items(self):
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
@@ -427,7 +489,10 @@
def test_closed_delivery_note(self):
from erpnext.stock.doctype.delivery_note.delivery_note import update_delivery_note_status
- dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
+ make_stock_entry(target="Stores - TCP1", qty=5, basic_rate=100)
+
+ dn = create_delivery_note(company='_Test Company with perpetual inventory', warehouse='Stores - TCP1',
+ cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1", do_not_submit=True)
dn.submit()
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
index 3d57f47..1799624 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "hash",
"creation": "2013-04-22 13:15:44",
"doctype": "DocType",
@@ -24,7 +25,10 @@
"col_break2",
"uom",
"conversion_factor",
+ "stock_qty_sec_break",
"stock_qty",
+ "stock_qty_col_break",
+ "returned_qty",
"section_break_17",
"price_list_rate",
"base_price_list_rate",
@@ -43,6 +47,7 @@
"base_rate",
"base_amount",
"pricing_rules",
+ "stock_uom_rate",
"is_free_item",
"section_break_25",
"net_rate",
@@ -52,6 +57,7 @@
"base_net_rate",
"base_net_amount",
"billed_amt",
+ "incoming_rate",
"item_weight_details",
"weight_per_unit",
"total_weight",
@@ -211,7 +217,7 @@
{
"fieldname": "stock_qty",
"fieldtype": "Float",
- "label": "Qty as per Stock UOM",
+ "label": "Qty in Stock UOM",
"no_copy": 1,
"print_hide": 1,
"read_only": 1
@@ -453,7 +459,7 @@
"fieldname": "warehouse",
"fieldtype": "Link",
"in_list_view": 1,
- "label": "From Warehouse",
+ "label": "Warehouse",
"oldfieldname": "warehouse",
"oldfieldtype": "Link",
"options": "Warehouse",
@@ -462,11 +468,12 @@
"width": "100px"
},
{
+ "depends_on": "eval:parent.is_internal_customer",
"fieldname": "target_warehouse",
"fieldtype": "Link",
"hidden": 1,
"ignore_user_permissions": 1,
- "label": "Customer Warehouse (Optional)",
+ "label": "Target Warehouse",
"no_copy": 1,
"options": "Warehouse",
"print_hide": 1
@@ -715,12 +722,43 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "stock_qty_sec_break",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "stock_qty_col_break",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "returned_qty",
+ "fieldname": "returned_qty",
+ "fieldtype": "Float",
+ "label": "Returned Qty in Stock UOM"
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Currency",
+ "label": "Incoming Rate",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.uom != doc.stock_uom",
+ "fieldname": "stock_uom_rate",
+ "fieldtype": "Currency",
+ "label": "Rate of Stock UOM",
+ "options": "currency",
+ "read_only": 1
}
],
"idx": 1,
+ "index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-07-20 12:25:06.177894",
+ "modified": "2021-01-30 21:42:03.767968",
"modified_by": "Administrator",
"module": "Stock",
"name": "Delivery Note Item",
diff --git a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py
index aaca802..5030595 100644
--- a/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py
+++ b/erpnext/stock/doctype/delivery_note_item/delivery_note_item.py
@@ -5,8 +5,6 @@
import frappe
from frappe.model.document import Document
-from erpnext.controllers.print_settings import print_settings_for_item_table
class DeliveryNoteItem(Document):
- def __setup__(self):
- print_settings_for_item_table(self)
+ pass
\ No newline at end of file
diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js
index faeeb57..f851aaf 100644
--- a/erpnext/stock/doctype/item/item.js
+++ b/erpnext/stock/doctype/item/item.js
@@ -81,11 +81,11 @@
}, __('Create'));
}
- frm.page.set_inner_btn_group_as_primary(__('Create'));
+ // frm.page.set_inner_btn_group_as_primary(__('Create'));
}
if (frm.doc.variant_of) {
frm.set_intro(__('This Item is a Variant of {0} (Template).',
- [`<a href="#Form/Item/${frm.doc.variant_of}">${frm.doc.variant_of}</a>`]), true);
+ [`<a href="/app/item/${frm.doc.variant_of}">${frm.doc.variant_of}</a>`]), true);
}
if (frappe.defaults.get_default("item_naming_by")!="Naming Series" || frm.doc.variant_of) {
@@ -380,11 +380,13 @@
// Show Stock Levels only if is_stock_item
if (frm.doc.is_stock_item) {
frappe.require('assets/js/item-dashboard.min.js', function() {
- var section = frm.dashboard.add_section('<h5 style="margin-top: 0px;">\
- <a href="#stock-balance">' + __("Stock Levels") + '</a></h5>');
+ const section = frm.dashboard.add_section('', __("Stock Levels"));
erpnext.item.item_dashboard = new erpnext.stock.ItemDashboard({
parent: section,
- item_code: frm.doc.name
+ item_code: frm.doc.name,
+ page_length: 20,
+ method: 'erpnext.stock.dashboard.item_dashboard.get_data',
+ template: 'item_dashboard_list'
});
erpnext.item.item_dashboard.refresh();
});
@@ -650,7 +652,7 @@
if (r.message) {
var variant = r.message;
frappe.msgprint_dialog = frappe.msgprint(__("Item Variant {0} already exists with same attributes",
- [repl('<a href="#Form/Item/%(item_encoded)s" class="strong variant-click">%(item)s</a>', {
+ [repl('<a href="/app/item/%(item_encoded)s" class="strong variant-click">%(item)s</a>', {
item_encoded: encodeURIComponent(variant),
item: variant
})]
diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json
index d07b3dc..fcf7c26 100644
--- a/erpnext/stock/doctype/item/item.json
+++ b/erpnext/stock/doctype/item/item.json
@@ -106,9 +106,9 @@
"item_tax_section_break",
"taxes",
"inspection_criteria",
+ "quality_inspection_template",
"inspection_required_before_purchase",
"inspection_required_before_delivery",
- "quality_inspection_template",
"manufacturing",
"default_bom",
"is_sub_contracted_item",
@@ -814,7 +814,6 @@
"label": "Inspection Required before Delivery"
},
{
- "depends_on": "eval:(doc.inspection_required_before_purchase || doc.inspection_required_before_delivery)",
"fieldname": "quality_inspection_template",
"fieldtype": "Link",
"label": "Quality Inspection Template",
@@ -1069,7 +1068,7 @@
"index_web_pages_for_search": 1,
"links": [],
"max_attachments": 1,
- "modified": "2020-08-07 14:24:58.384992",
+ "modified": "2021-01-25 20:49:50.222976",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item",
@@ -1131,4 +1130,4 @@
"sort_order": "DESC",
"title_field": "item_name",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py
index be845d9..7b7d2da 100644
--- a/erpnext/stock/doctype/item/item.py
+++ b/erpnext/stock/doctype/item/item.py
@@ -177,7 +177,7 @@
if not self.valuation_rate and self.standard_rate:
self.valuation_rate = self.standard_rate
- if not self.valuation_rate:
+ if not self.valuation_rate and not self.is_customer_provided_item:
frappe.throw(_("Valuation Rate is mandatory if Opening Stock entered"))
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
@@ -317,6 +317,7 @@
context.search_link = '/product_search'
context.parents = get_parent_item_groups(self.item_group)
+ context.body_class = "product-page"
self.set_variant_context(context)
self.set_attribute_context(context)
@@ -672,13 +673,14 @@
if not records: return
document = _("Stock Reconciliation") if len(records) == 1 else _("Stock Reconciliations")
- msg = _("The items {0} and {1} are present in the following {2} : <br>"
- .format(frappe.bold(old_name), frappe.bold(new_name), document))
+ msg = _("The items {0} and {1} are present in the following {2} : ").format(
+ frappe.bold(old_name), frappe.bold(new_name), document)
+ msg += '<br>'
msg += ', '.join([get_link_to_form("Stock Reconciliation", d.parent) for d in records]) + "<br><br>"
- msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}"
- .format(frappe.bold(old_name)))
+ msg += _("Note: To merge the items, create a separate Stock Reconciliation for the old item {0}").format(
+ frappe.bold(old_name))
frappe.throw(_(msg), title=_("Merge not allowed"))
@@ -860,7 +862,7 @@
rows = ''
for docname, attr_list in not_included.items():
- link = "<a href='#Form/Item/{0}'>{0}</a>".format(frappe.bold(_(docname)))
+ link = "<a href='/app/Form/Item/{0}'>{0}</a>".format(frappe.bold(_(docname)))
rows += table_row(link, body(attr_list))
error_description = _('The following deleted attributes exist in Variants but not in the Template. You can either delete the Variants or keep the attribute(s) in template.')
@@ -971,7 +973,7 @@
frappe.throw(_("As there are existing transactions against item {0}, you can not change the value of {1}").format(self.name, frappe.bold(self.meta.get_label(field))))
def check_if_linked_document_exists(self, field):
- linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "Purchase Receipt Item",
+ linked_doctypes = ["Delivery Note Item", "Sales Invoice Item", "POS Invoice Item", "Purchase Receipt Item",
"Purchase Invoice Item", "Stock Entry Detail", "Stock Reconciliation Item"]
# For "Is Stock Item", following doctypes is important
diff --git a/erpnext/stock/doctype/item/item_dashboard.py b/erpnext/stock/doctype/item/item_dashboard.py
index dd4676a..b3e4796 100644
--- a/erpnext/stock/doctype/item/item_dashboard.py
+++ b/erpnext/stock/doctype/item/item_dashboard.py
@@ -32,16 +32,16 @@
'Purchase Order', 'Purchase Receipt', 'Purchase Invoice']
},
{
+ 'label': _('Manufacture'),
+ 'items': ['Production Plan', 'Work Order', 'Item Manufacturer']
+ },
+ {
'label': _('Traceability'),
'items': ['Serial No', 'Batch']
},
{
'label': _('Move'),
'items': ['Stock Entry']
- },
- {
- 'label': _('Manufacture'),
- 'items': ['Production Plan', 'Work Order', 'Item Manufacturer']
}
]
}
diff --git a/erpnext/stock/doctype/item/test_records.json b/erpnext/stock/doctype/item/test_records.json
index 9ca887c..8f437b1 100644
--- a/erpnext/stock/doctype/item/test_records.json
+++ b/erpnext/stock/doctype/item/test_records.json
@@ -458,5 +458,15 @@
"item_tax_template": "_Test Item Tax Template 1"
}
]
+ },
+ {
+ "description": "_Test",
+ "doctype": "Item",
+ "is_stock_item": 1,
+ "item_code": "138-CMS Shoe",
+ "item_group": "_Test Item Group",
+ "item_name": "138-CMS Shoe",
+ "stock_uom": "_Test UOM",
+ "gst_hsn_code": "999800"
}
]
diff --git a/erpnext/stock/doctype/item_alternative/test_item_alternative.py b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
index f045e4f..d5700fe 100644
--- a/erpnext/stock/doctype/item_alternative/test_item_alternative.py
+++ b/erpnext/stock/doctype/item_alternative/test_item_alternative.py
@@ -12,11 +12,9 @@
from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record
from erpnext.buying.doctype.purchase_order.purchase_order import make_purchase_receipt, make_rm_stock_entry
import unittest
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
class TestItemAlternative(unittest.TestCase):
def setUp(self):
- set_perpetual_inventory(0)
make_items()
def test_alternative_item_for_subcontract_rm(self):
diff --git a/erpnext/stock/doctype/item_price/item_price.js b/erpnext/stock/doctype/item_price/item_price.js
index 2729f4b..12cf6cf 100644
--- a/erpnext/stock/doctype/item_price/item_price.js
+++ b/erpnext/stock/doctype/item_price/item_price.js
@@ -14,6 +14,14 @@
frm.add_fetch("item_code", "stock_uom", "uom");
frm.set_df_property("bulk_import_help", "options",
- '<a href="#data-import-tool/Item Price">' + __("Import in Bulk") + '</a>');
+ '<a href="/app/data-import-tool/Item Price">' + __("Import in Bulk") + '</a>');
+
+ frm.set_query('batch_no', function() {
+ return {
+ filters: {
+ 'item': frm.doc.item_code
+ }
+ };
+ });
}
});
diff --git a/erpnext/stock/doctype/item_price/item_price.json b/erpnext/stock/doctype/item_price/item_price.json
index 5f62381..83177b3 100644
--- a/erpnext/stock/doctype/item_price/item_price.json
+++ b/erpnext/stock/doctype/item_price/item_price.json
@@ -18,6 +18,7 @@
"price_list",
"customer",
"supplier",
+ "batch_no",
"column_break_3",
"buying",
"selling",
@@ -47,31 +48,41 @@
"oldfieldtype": "Select",
"options": "Item",
"reqd": 1,
- "search_index": 1
+ "search_index": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
- "options": "UOM"
+ "options": "UOM",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"description": "Quantity that must be bought or sold per UOM",
"fieldname": "packing_unit",
"fieldtype": "Int",
- "label": "Packing Unit"
+ "label": "Packing Unit",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_17",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "item_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Item Name",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fetch_from": "item_code.brand",
@@ -79,19 +90,25 @@
"fieldtype": "Read Only",
"in_list_view": 1,
"label": "Brand",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "item_description",
"fieldtype": "Text",
"label": "Item Description",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "price_list_details",
"fieldtype": "Section Break",
"label": "Price List",
- "options": "fa fa-tags"
+ "options": "fa fa-tags",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "price_list",
@@ -100,7 +117,9 @@
"in_standard_filter": 1,
"label": "Price List",
"options": "Price List",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"bold": 1,
@@ -108,37 +127,49 @@
"fieldname": "customer",
"fieldtype": "Link",
"label": "Customer",
- "options": "Customer"
+ "options": "Customer",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"depends_on": "eval:doc.buying == 1",
"fieldname": "supplier",
"fieldtype": "Link",
"label": "Supplier",
- "options": "Supplier"
+ "options": "Supplier",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_3",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"fieldname": "buying",
"fieldtype": "Check",
"label": "Buying",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"fieldname": "selling",
"fieldtype": "Check",
"label": "Selling",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "item_details",
"fieldtype": "Section Break",
- "options": "fa fa-tag"
+ "options": "fa fa-tag",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"bold": 1,
@@ -146,11 +177,15 @@
"fieldtype": "Link",
"label": "Currency",
"options": "Currency",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "col_br_1",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "price_list_rate",
@@ -162,53 +197,80 @@
"oldfieldname": "ref_rate",
"oldfieldtype": "Currency",
"options": "currency",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "section_break_15",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "Today",
"fieldname": "valid_from",
"fieldtype": "Date",
- "label": "Valid From"
+ "label": "Valid From",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
"fieldname": "lead_time_days",
"fieldtype": "Int",
- "label": "Lead Time in days"
+ "label": "Lead Time in days",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "column_break_18",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "valid_upto",
"fieldtype": "Date",
- "label": "Valid Upto"
+ "label": "Valid Upto",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "section_break_24",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "note",
"fieldtype": "Text",
- "label": "Note"
+ "label": "Note",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "reference",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Reference"
+ "label": "Reference",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "batch_no",
+ "fieldtype": "Link",
+ "label": "Batch No",
+ "options": "Batch",
+ "show_days": 1,
+ "show_seconds": 1
}
],
"icon": "fa fa-flag",
"idx": 1,
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-07-06 22:31:32.943475",
+ "modified": "2020-12-08 18:12:15.395772",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Price",
diff --git a/erpnext/stock/doctype/item_price/item_price.py b/erpnext/stock/doctype/item_price/item_price.py
index bed5ea9..e82a19b 100644
--- a/erpnext/stock/doctype/item_price/item_price.py
+++ b/erpnext/stock/doctype/item_price/item_price.py
@@ -54,7 +54,8 @@
"valid_upto",
"packing_unit",
"customer",
- "supplier",]:
+ "supplier",
+ "batch_no"]:
if self.get(field):
conditions += " and {0} = %({0})s ".format(field)
else:
@@ -68,7 +69,7 @@
self.as_dict(),)
if price_list_rate:
- frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, UOM, Qty, and Dates."), ItemPriceDuplicateItem,)
+ frappe.throw(_("Item Price appears multiple times based on Price List, Supplier/Customer, Currency, Item, Batch, UOM, Qty, and Dates."), ItemPriceDuplicateItem,)
def before_save(self):
if self.selling:
diff --git a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
index 888bc2d..9b1a47e 100644
--- a/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
+++ b/erpnext/stock/doctype/item_quality_inspection_parameter/item_quality_inspection_parameter.json
@@ -7,27 +7,34 @@
"engine": "InnoDB",
"field_order": [
"specification",
+ "parameter_group",
"value",
+ "numeric",
"column_break_3",
+ "min_value",
+ "max_value",
+ "formula_based_criteria",
"acceptance_formula"
],
"fields": [
{
"fieldname": "specification",
- "fieldtype": "Data",
+ "fieldtype": "Link",
"in_list_view": 1,
"label": "Parameter",
"oldfieldname": "specification",
"oldfieldtype": "Data",
+ "options": "Quality Inspection Parameter",
"print_width": "200px",
"reqd": 1,
- "width": "200px"
+ "width": "100px"
},
{
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.numeric)",
"fieldname": "value",
"fieldtype": "Data",
"in_list_view": 1,
- "label": "Acceptance Criteria",
+ "label": "Acceptance Criteria Value",
"oldfieldname": "value",
"oldfieldtype": "Data"
},
@@ -36,17 +43,53 @@
"fieldtype": "Column Break"
},
{
- "description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 < 10</b>",
+ "depends_on": "formula_based_criteria",
+ "description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nNumeric eg. 2: <b>mean > 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>",
"fieldname": "acceptance_formula",
"fieldtype": "Code",
- "in_list_view": 1,
"label": "Acceptance Criteria Formula"
+ },
+ {
+ "default": "0",
+ "fieldname": "formula_based_criteria",
+ "fieldtype": "Check",
+ "label": "Formula Based Criteria"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && doc.numeric)",
+ "fieldname": "min_value",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Minimum Value"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && doc.numeric)",
+ "fieldname": "max_value",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Maximum Value"
+ },
+ {
+ "default": "1",
+ "fieldname": "numeric",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Numeric",
+ "width": "80px"
+ },
+ {
+ "fetch_from": "specification.parameter_group",
+ "fieldname": "parameter_group",
+ "fieldtype": "Link",
+ "label": "Parameter Group",
+ "options": "Quality Inspection Parameter Group",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-16 16:33:42.421842",
+ "modified": "2021-02-04 18:50:02.056173",
"modified_by": "Administrator",
"module": "Stock",
"name": "Item Quality Inspection Parameter",
diff --git a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json
index b24d621..c77b993 100644
--- a/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json
+++ b/erpnext/stock/doctype/landed_cost_item/landed_cost_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2013-02-22 01:28:02",
"doctype": "DocType",
"document_type": "Document",
@@ -29,6 +30,8 @@
"options": "Item",
"read_only": 1,
"reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1,
"width": "100px"
},
{
@@ -41,6 +44,8 @@
"print_width": "300px",
"read_only": 1,
"reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1,
"width": "120px"
},
{
@@ -50,7 +55,9 @@
"no_copy": 1,
"options": "Purchase Invoice\nPurchase Receipt",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "receipt_document",
@@ -59,25 +66,33 @@
"no_copy": 1,
"options": "receipt_document_type",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "col_break2",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "qty",
"fieldtype": "Float",
"in_list_view": 1,
"label": "Qty",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "rate",
"fieldtype": "Currency",
"label": "Rate",
"options": "Company:company:default_currency",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "amount",
@@ -88,14 +103,19 @@
"oldfieldtype": "Currency",
"options": "Company:company:default_currency",
"read_only": 1,
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "applicable_charges",
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Applicable Charges",
- "options": "Company:company:default_currency"
+ "options": "Company:company:default_currency",
+ "read_only_depends_on": "eval:parent.distribute_charges_based_on != 'Distribute Manually'",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "purchase_receipt_item",
@@ -104,22 +124,30 @@
"label": "Purchase Receipt Item",
"no_copy": 1,
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "cost_center",
"fieldtype": "Link",
"label": "Cost Center",
- "options": "Cost Center"
+ "options": "Cost Center",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
- "label": "Accounting Dimensions"
+ "label": "Accounting Dimensions",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "dimension_col_break",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"default": "0",
@@ -128,12 +156,15 @@
"fieldtype": "Check",
"hidden": 1,
"label": "Is Fixed Asset",
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
}
],
"idx": 1,
"istable": 1,
- "modified": "2020-09-18 17:26:09.703215",
+ "links": [],
+ "modified": "2021-01-25 23:09:23.322282",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Item",
diff --git a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
index 0cc243d..4fcdb4c 100644
--- a/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
+++ b/erpnext/stock/doctype/landed_cost_taxes_and_charges/landed_cost_taxes_and_charges.json
@@ -1,12 +1,16 @@
{
+ "actions": [],
"creation": "2014-07-11 11:51:00.453717",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"expense_account",
+ "account_currency",
+ "exchange_rate",
"description",
"col_break3",
+ "base_amount",
"amount"
],
"fields": [
@@ -27,20 +31,43 @@
"fieldtype": "Currency",
"in_list_view": 1,
"label": "Amount",
- "options": "Company:company:default_currency",
+ "options": "account_currency",
"reqd": 1
},
{
+ "depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"fieldname": "expense_account",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Expense Account",
- "options": "Account",
- "reqd": 1
+ "mandatory_depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
+ "options": "Account"
+ },
+ {
+ "fieldname": "account_currency",
+ "fieldtype": "Link",
+ "label": "Account Currency",
+ "options": "Currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "exchange_rate",
+ "fieldtype": "Float",
+ "label": "Exchange Rate",
+ "precision": "9"
+ },
+ {
+ "fieldname": "base_amount",
+ "fieldtype": "Currency",
+ "label": "Base Amount",
+ "options": "Company:company:default_currency",
+ "read_only": 1
}
],
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-09-30 18:28:32.070655",
+ "links": [],
+ "modified": "2020-12-26 01:07:23.233604",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Taxes and Charges",
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js
index 5de1352..1abbc35 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.js
@@ -1,6 +1,7 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
// License: GNU General Public License v3. See license.txt
+{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %};
frappe.provide("erpnext.stock");
@@ -29,20 +30,9 @@
this.frm.add_fetch("receipt_document", "supplier", "supplier");
this.frm.add_fetch("receipt_document", "posting_date", "posting_date");
this.frm.add_fetch("receipt_document", "base_grand_total", "grand_total");
-
- this.frm.set_query("expense_account", "taxes", function() {
- return {
- query: "erpnext.controllers.queries.tax_account_query",
- filters: {
- "account_type": ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"],
- "company": me.frm.doc.company
- }
- };
- });
-
},
- refresh: function(frm) {
+ refresh: function() {
var help_content =
`<br><br>
<table class="table table-bordered" style="background-color: #f9f9f9;">
@@ -72,6 +62,11 @@
</table>`;
set_field_options("landed_cost_help", help_content);
+
+ if (this.frm.doc.company) {
+ let company_currency = frappe.get_doc(":Company", this.frm.doc.company).default_currency;
+ this.frm.set_currency_labels(["total_taxes_and_charges"], company_currency);
+ }
},
get_items_from_purchase_receipts: function() {
@@ -97,34 +92,36 @@
set_total_taxes_and_charges: function() {
var total_taxes_and_charges = 0.0;
$.each(this.frm.doc.taxes || [], function(i, d) {
- total_taxes_and_charges += flt(d.amount)
+ total_taxes_and_charges += flt(d.base_amount);
});
- cur_frm.set_value("total_taxes_and_charges", total_taxes_and_charges);
+ this.frm.set_value("total_taxes_and_charges", total_taxes_and_charges);
},
set_applicable_charges_for_item: function() {
var me = this;
if(this.frm.doc.taxes.length) {
-
var total_item_cost = 0.0;
var based_on = this.frm.doc.distribute_charges_based_on.toLowerCase();
- $.each(this.frm.doc.items || [], function(i, d) {
- total_item_cost += flt(d[based_on])
- });
- var total_charges = 0.0;
- $.each(this.frm.doc.items || [], function(i, item) {
- item.applicable_charges = flt(item[based_on]) * flt(me.frm.doc.total_taxes_and_charges) / flt(total_item_cost)
- item.applicable_charges = flt(item.applicable_charges, precision("applicable_charges", item))
- total_charges += item.applicable_charges
- });
+ if (based_on != 'distribute manually') {
+ $.each(this.frm.doc.items || [], function(i, d) {
+ total_item_cost += flt(d[based_on])
+ });
- if (total_charges != this.frm.doc.total_taxes_and_charges){
- var diff = this.frm.doc.total_taxes_and_charges - flt(total_charges)
- this.frm.doc.items.slice(-1)[0].applicable_charges += diff
+ var total_charges = 0.0;
+ $.each(this.frm.doc.items || [], function(i, item) {
+ item.applicable_charges = flt(item[based_on]) * flt(me.frm.doc.total_taxes_and_charges) / flt(total_item_cost)
+ item.applicable_charges = flt(item.applicable_charges, precision("applicable_charges", item))
+ total_charges += item.applicable_charges
+ });
+
+ if (total_charges != this.frm.doc.total_taxes_and_charges){
+ var diff = this.frm.doc.total_taxes_and_charges - flt(total_charges)
+ this.frm.doc.items.slice(-1)[0].applicable_charges += diff
+ }
+ refresh_field("items");
}
- refresh_field("items");
}
},
distribute_charges_based_on: function (frm) {
@@ -134,7 +131,16 @@
items_remove: () => {
this.trigger('set_applicable_charges_for_item');
}
-
});
cur_frm.script_manager.make(erpnext.stock.LandedCostVoucher);
+
+frappe.ui.form.on('Landed Cost Taxes and Charges', {
+ expense_account: function(frm, cdt, cdn) {
+ frm.events.set_account_currency(frm, cdt, cdn);
+ },
+
+ amount: function(frm, cdt, cdn) {
+ frm.events.set_base_amount(frm, cdt, cdn);
+ }
+});
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json
index 0149280..059f925 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "naming_series:",
"creation": "2014-07-11 11:33:42.547339",
"doctype": "DocType",
@@ -7,6 +8,9 @@
"field_order": [
"naming_series",
"company",
+ "column_break_2",
+ "posting_date",
+ "section_break_5",
"purchase_receipts",
"purchase_receipt_items",
"get_items_from_purchase_receipts",
@@ -30,7 +34,9 @@
"options": "MAT-LCV-.YYYY.-",
"print_hide": 1,
"reqd": 1,
- "set_only_once": 1
+ "set_only_once": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "company",
@@ -40,24 +46,32 @@
"label": "Company",
"options": "Company",
"remember_last_selected_value": 1,
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "purchase_receipts",
"fieldtype": "Table",
"label": "Purchase Receipts",
"options": "Landed Cost Purchase Receipt",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "purchase_receipt_items",
"fieldtype": "Section Break",
- "label": "Purchase Receipt Items"
+ "label": "Purchase Receipt Items",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "get_items_from_purchase_receipts",
"fieldtype": "Button",
- "label": "Get Items From Purchase Receipts"
+ "label": "Get Items From Purchase Receipts",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "items",
@@ -65,42 +79,56 @@
"label": "Purchase Receipt Items",
"no_copy": 1,
"options": "Landed Cost Item",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "sec_break1",
"fieldtype": "Section Break",
- "label": "Applicable Charges"
+ "label": "Applicable Charges",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "taxes",
"fieldtype": "Table",
"label": "Taxes and Charges",
"options": "Landed Cost Taxes and Charges",
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "section_break_9",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "total_taxes_and_charges",
"fieldtype": "Currency",
- "label": "Total Taxes and Charges",
+ "label": "Total Taxes and Charges (Company Currency)",
"options": "Company:company:default_currency",
"read_only": 1,
- "reqd": 1
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "col_break1",
- "fieldtype": "Column Break"
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "distribute_charges_based_on",
"fieldtype": "Select",
"label": "Distribute Charges Based On",
- "options": "Qty\nAmount",
- "reqd": 1
+ "options": "Qty\nAmount\nDistribute Manually",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "amended_from",
@@ -109,21 +137,51 @@
"no_copy": 1,
"options": "Landed Cost Voucher",
"print_hide": 1,
- "read_only": 1
+ "read_only": 1,
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "sec_break2",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "show_days": 1,
+ "show_seconds": 1
},
{
"fieldname": "landed_cost_help",
"fieldtype": "HTML",
- "label": "Landed Cost Help"
+ "label": "Landed Cost Help",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "column_break_2",
+ "fieldtype": "Column Break",
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "default": "Today",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date",
+ "reqd": 1,
+ "show_days": 1,
+ "show_seconds": 1
+ },
+ {
+ "fieldname": "section_break_5",
+ "fieldtype": "Section Break",
+ "hide_border": 1,
+ "show_days": 1,
+ "show_seconds": 1
}
],
"icon": "icon-usd",
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
- "modified": "2019-11-21 15:34:10.846093",
+ "links": [],
+ "modified": "2021-01-25 23:07:30.468423",
"modified_by": "Administrator",
"module": "Stock",
"name": "Landed Cost Voucher",
diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
index bc3d326..69a8bf1 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py
@@ -9,6 +9,7 @@
from frappe.model.document import Document
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.accounts.doctype.account.account import get_account_currency
+from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
class LandedCostVoucher(Document):
def get_items_from_purchase_receipts(self):
@@ -39,13 +40,15 @@
def validate(self):
self.check_mandatory()
+ self.validate_purchase_receipts()
+ init_landed_taxes_and_totals(self)
+ self.set_total_taxes_and_charges()
if not self.get("items"):
self.get_items_from_purchase_receipts()
- else:
- self.validate_applicable_charges_for_item()
- self.validate_purchase_receipts()
- self.validate_expense_accounts()
- self.set_total_taxes_and_charges()
+
+ self.set_applicable_charges_on_item()
+ self.validate_applicable_charges_for_item()
+
def check_mandatory(self):
if not self.get("purchase_receipts"):
@@ -73,21 +76,37 @@
frappe.throw(_("Row {0}: Cost center is required for an item {1}")
.format(item.idx, item.item_code))
- def validate_expense_accounts(self):
- company_currency = erpnext.get_company_currency(self.company)
- for account in self.taxes:
- if get_account_currency(account.expense_account) != company_currency:
- frappe.throw(msg=_(""" Row {0}: Expense account currency should be same as company's default currency.
- Please select expense account with account currency as {1}""")
- .format(account.idx, frappe.bold(company_currency)), title=_("Invalid Account Currency"))
-
def set_total_taxes_and_charges(self):
- self.total_taxes_and_charges = sum([flt(d.amount) for d in self.get("taxes")])
+ self.total_taxes_and_charges = sum([flt(d.base_amount) for d in self.get("taxes")])
+
+ def set_applicable_charges_on_item(self):
+ if self.get('taxes') and self.distribute_charges_based_on != 'Distribute Manually':
+ total_item_cost = 0.0
+ total_charges = 0.0
+ item_count = 0
+ based_on_field = frappe.scrub(self.distribute_charges_based_on)
+
+ for item in self.get('items'):
+ total_item_cost += item.get(based_on_field)
+
+ for item in self.get('items'):
+ item.applicable_charges = flt(flt(item.get(based_on_field)) * (flt(self.total_taxes_and_charges) / flt(total_item_cost)),
+ item.precision('applicable_charges'))
+ total_charges += item.applicable_charges
+ item_count += 1
+
+ if total_charges != self.total_taxes_and_charges:
+ diff = self.total_taxes_and_charges - total_charges
+ self.get('items')[item_count - 1].applicable_charges += diff
def validate_applicable_charges_for_item(self):
based_on = self.distribute_charges_based_on.lower()
- total = sum([flt(d.get(based_on)) for d in self.get("items")])
+ if based_on != 'distribute manually':
+ total = sum([flt(d.get(based_on)) for d in self.get("items")])
+ else:
+ # consider for proportion while distributing manually
+ total = sum([flt(d.get('applicable_charges')) for d in self.get("items")])
if not total:
frappe.throw(_("Total {0} for all items is zero, may be you should change 'Distribute Charges Based On'").format(based_on))
@@ -121,7 +140,7 @@
doc.set_landed_cost_voucher_amount()
# set valuation amount in pr item
- doc.update_valuation_rate("items")
+ doc.update_valuation_rate(reset_outgoing_rate=False)
# db_update will update and save landed_cost_voucher_amount and voucher_amount in PR
for item in doc.get("items"):
@@ -143,6 +162,7 @@
doc.docstatus = 1
doc.update_stock_ledger(allow_negative_stock=True, via_landed_cost_voucher=True)
doc.make_gl_entries()
+ doc.repost_future_sle_and_gle()
def validate_asset_qty_and_status(self, receipt_document_type, receipt_document):
for item in self.get('items'):
@@ -152,13 +172,12 @@
docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document,
'item_code': item.item_code }, fields=['name', 'docstatus'])
if not docs or len(docs) != item.qty:
- frappe.throw(_('There are not enough asset created or linked to {0}. \
- Please create or link {1} Assets with respective document.').format(item.receipt_document, item.qty))
+ frappe.throw(_('There are not enough asset created or linked to {0}. Please create or link {1} Assets with respective document.').format(
+ item.receipt_document, item.qty))
if docs:
for d in docs:
if d.docstatus == 1:
- frappe.throw(_('{2} <b>{0}</b> has submitted Assets.\
- Remove Item <b>{1}</b> from table to continue.').format(
+ frappe.throw(_('{2} <b>{0}</b> has submitted Assets. Remove Item <b>{1}</b> from table to continue.').format(
item.receipt_document, item.item_code, item.receipt_document_type))
def update_rate_in_serial_no_for_non_asset_items(self, receipt_document):
diff --git a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
index 3f2c5da..984ae46 100644
--- a/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
+++ b/erpnext/stock/doctype/landed_cost_voucher/test_landed_cost_voucher.py
@@ -7,9 +7,10 @@
import frappe
from frappe.utils import flt
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt \
- import set_perpetual_inventory, get_gl_entries, test_records as pr_test_records, make_purchase_receipt
+ import get_gl_entries, test_records as pr_test_records, make_purchase_receipt
from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice
from erpnext.accounts.doctype.account.test_account import get_inventory_account
+from erpnext.accounts.doctype.account.test_account import create_account
class TestLandedCostVoucher(unittest.TestCase):
def test_landed_cost_voucher(self):
@@ -27,7 +28,7 @@
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
- submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
pr_lc_value = frappe.db.get_value("Purchase Receipt Item", {"parent": pr.name}, "landed_cost_voucher_amount")
self.assertEqual(pr_lc_value, 25.0)
@@ -89,7 +90,7 @@
},
fieldname=["qty_after_transaction", "stock_value"], as_dict=1)
- submit_landed_cost_voucher("Purchase Invoice", pi.name, pi.company)
+ create_landed_cost_voucher("Purchase Invoice", pi.name, pi.company)
pi_lc_value = frappe.db.get_value("Purchase Invoice Item", {"parent": pi.name},
"landed_cost_voucher_amount")
@@ -137,7 +138,7 @@
serial_no_rate = frappe.db.get_value("Serial No", "SN001", "purchase_rate")
- submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
serial_no = frappe.db.get_value("Serial No", "SN001",
["warehouse", "purchase_rate"], as_dict=1)
@@ -147,7 +148,6 @@
def test_landed_cost_voucher_for_odd_numbers (self):
-
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True)
pr.items[0].cost_center = "Main - TCP1"
for x in range(2):
@@ -160,10 +160,10 @@
})
pr.submit()
- lcv = submit_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22)
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, 123.22)
- self.assertEqual(lcv.items[0].applicable_charges, 41.07)
- self.assertEqual(lcv.items[2].applicable_charges, 41.08)
+ self.assertEqual(flt(lcv.items[0].applicable_charges, 2), 41.07)
+ self.assertEqual(flt(lcv.items[2].applicable_charges, 2), 41.08)
def test_multiple_landed_cost_voucher_against_pr(self):
pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
@@ -206,6 +206,50 @@
self.assertEqual(pr.items[0].landed_cost_voucher_amount, 100)
self.assertEqual(pr.items[1].landed_cost_voucher_amount, 100)
+ def test_multi_currency_lcv(self):
+ from erpnext.setup.doctype.currency_exchange.test_currency_exchange import test_records, save_new_records
+
+ save_new_records(test_records)
+
+ ## Create USD Shipping charges_account
+ usd_shipping = create_account(account_name="Shipping Charges USD",
+ parent_account="Duties and Taxes - TCP1", company="_Test Company with perpetual inventory",
+ account_currency="USD")
+
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
+ supplier_warehouse = "Stores - TCP1")
+ pr.submit()
+
+ lcv = make_landed_cost_voucher(company = pr.company, receipt_document_type = "Purchase Receipt",
+ receipt_document=pr.name, charges=100, do_not_save=True)
+
+ lcv.append("taxes", {
+ "description": "Shipping Charges",
+ "expense_account": usd_shipping,
+ "amount": 10
+ })
+
+ lcv.save()
+ lcv.submit()
+ pr.load_from_db()
+
+ # Considering exchange rate from USD to INR as 62.9
+ self.assertEqual(lcv.total_taxes_and_charges, 729)
+ self.assertEqual(pr.items[0].landed_cost_voucher_amount, 729)
+
+ gl_entries = frappe.get_all("GL Entry", fields=["account", "credit", "credit_in_account_currency"],
+ filters={"voucher_no": pr.name, "account": ("in", ["Shipping Charges USD - TCP1", "Expenses Included In Valuation - TCP1"])})
+
+ expected_gl_entries = {
+ "Shipping Charges USD - TCP1": [629, 10],
+ "Expenses Included In Valuation - TCP1": [100, 100]
+ }
+
+ for entry in gl_entries:
+ amounts = expected_gl_entries.get(entry.account)
+ self.assertEqual(entry.credit, amounts[0])
+ self.assertEqual(entry.credit_in_account_currency, amounts[1])
+
def make_landed_cost_voucher(** args):
args = frappe._dict(args)
ref_doc = frappe.get_doc(args.receipt_document_type, args.receipt_document)
@@ -236,7 +280,7 @@
return lcv
-def submit_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50):
+def create_landed_cost_voucher(receipt_document_type, receipt_document, company, charges=50):
ref_doc = frappe.get_doc(receipt_document_type, receipt_document)
lcv = frappe.new_doc("Landed Cost Voucher")
diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js
index 01edd99..527b0d3 100644
--- a/erpnext/stock/doctype/material_request/material_request.js
+++ b/erpnext/stock/doctype/material_request/material_request.js
@@ -2,6 +2,7 @@
// License: GNU General Public License v3. See license.txt
// eslint-disable-next-line
+frappe.provide("erpnext.accounts.dimensions");
{% include 'erpnext/public/js/controllers/buying.js' %};
frappe.ui.form.on('Material Request', {
@@ -66,6 +67,12 @@
filters: {'company': doc.company}
};
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
onload_post_render: function(frm) {
diff --git a/erpnext/stock/doctype/material_request/material_request_list.js b/erpnext/stock/doctype/material_request/material_request_list.js
index 0d70958..de7a3d0 100644
--- a/erpnext/stock/doctype/material_request/material_request_list.js
+++ b/erpnext/stock/doctype/material_request/material_request_list.js
@@ -1,9 +1,10 @@
frappe.listview_settings['Material Request'] = {
add_fields: ["material_request_type", "status", "per_ordered", "per_received", "transfer_status"],
get_indicator: function(doc) {
- if(doc.status=="Stopped") {
+ var precision = frappe.defaults.get_default("float_precision");
+ if (doc.status=="Stopped") {
return [__("Stopped"), "red", "status,=,Stopped"];
- } else if(doc.transfer_status && doc.docstatus != 2) {
+ } else if (doc.transfer_status && doc.docstatus != 2) {
if (doc.transfer_status == "Not Started") {
return [__("Not Started"), "orange"];
} else if (doc.transfer_status == "In Transit") {
@@ -11,14 +12,14 @@
} else if (doc.transfer_status == "Completed") {
return [__("Completed"), "green"];
}
- } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 0) {
+ } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 0) {
return [__("Pending"), "orange", "per_ordered,=,0"];
- } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) < 100) {
+ } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) < 100) {
return [__("Partially ordered"), "yellow", "per_ordered,<,100"];
- } else if(doc.docstatus==1 && flt(doc.per_ordered, 2) == 100) {
- if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) < 100 && flt(doc.per_received, 2) > 0) {
+ } else if (doc.docstatus==1 && flt(doc.per_ordered, precision) == 100) {
+ if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) < 100 && flt(doc.per_received, precision) > 0) {
return [__("Partially Received"), "yellow", "per_received,<,100"];
- } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, 2) == 100) {
+ } else if (doc.material_request_type == "Purchase" && flt(doc.per_received, precision) == 100) {
return [__("Received"), "green", "per_received,=,100"];
} else if (doc.material_request_type == "Purchase") {
return [__("Ordered"), "green", "per_ordered,=,100"];
diff --git a/erpnext/stock/doctype/material_request/test_material_request.py b/erpnext/stock/doctype/material_request/test_material_request.py
index 19924b1..72a3a5e 100644
--- a/erpnext/stock/doctype/material_request/test_material_request.py
+++ b/erpnext/stock/doctype/material_request/test_material_request.py
@@ -12,9 +12,6 @@
from erpnext.stock.doctype.item.test_item import create_item
class TestMaterialRequest(unittest.TestCase):
- def setUp(self):
- erpnext.set_perpetual_inventory(0)
-
def test_make_purchase_order(self):
mr = frappe.copy_doc(test_records[0]).insert()
@@ -427,6 +424,7 @@
"basic_rate": 1.0
})
se_doc.get("items")[1].update({
+ "item_code": "_Test Item Home Desktop 100",
"qty": 3.0,
"transfer_qty": 3.0,
"s_warehouse": "_Test Warehouse 1 - _TC",
@@ -537,7 +535,7 @@
mr = make_material_request(item_code='_Test FG Item', material_request_type='Manufacture',
uom="_Test UOM 1", conversion_factor=12)
-
+
requested_qty = self._get_requested_qty('_Test FG Item', '_Test Warehouse - _TC')
self.assertEqual(requested_qty, existing_requested_qty + 120)
diff --git a/erpnext/stock/doctype/material_request_item/material_request_item.py b/erpnext/stock/doctype/material_request_item/material_request_item.py
index 6c6ecfe..16f007f 100644
--- a/erpnext/stock/doctype/material_request_item/material_request_item.py
+++ b/erpnext/stock/doctype/material_request_item/material_request_item.py
@@ -6,12 +6,10 @@
from __future__ import unicode_literals
import frappe
-from erpnext.controllers.print_settings import print_settings_for_item_table
from frappe.model.document import Document
class MaterialRequestItem(Document):
- def __setup__(self):
- print_settings_for_item_table(self)
+ pass
def on_doctype_update():
frappe.db.add_index("Material Request Item", ["item_code", "warehouse"])
\ No newline at end of file
diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json
index 2ac5c42..f1d7f8c 100644
--- a/erpnext/stock/doctype/packed_item/packed_item.json
+++ b/erpnext/stock/doctype/packed_item/packed_item.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"creation": "2013-02-22 01:28:00",
"doctype": "DocType",
"editable_grid": 1,
@@ -14,6 +15,7 @@
"target_warehouse",
"column_break_9",
"qty",
+ "uom",
"section_break_9",
"serial_no",
"column_break_11",
@@ -23,7 +25,7 @@
"actual_qty",
"projected_qty",
"column_break_16",
- "uom",
+ "incoming_rate",
"page_break",
"prevdoc_doctype",
"parent_detail_docname"
@@ -199,11 +201,21 @@
"no_copy": 1,
"print_hide": 1,
"read_only": 1
+ },
+ {
+ "fieldname": "incoming_rate",
+ "fieldtype": "Currency",
+ "label": "Incoming Rate",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
+ "index_web_pages_for_search": 1,
"istable": 1,
- "modified": "2019-11-26 20:09:59.400960",
+ "links": [],
+ "modified": "2020-09-24 09:25:13.050151",
"modified_by": "Administrator",
"module": "Stock",
"name": "Packed Item",
@@ -212,4 +224,4 @@
"sort_field": "modified",
"sort_order": "DESC",
"track_changes": 1
-}
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
index bc1d81d..57cc350 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.js
@@ -46,6 +46,8 @@
erpnext.queries.setup_queries(frm, "Warehouse", function() {
return erpnext.queries.warehouse(frm.doc);
});
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
refresh: function(frm) {
@@ -75,6 +77,7 @@
company: function(frm) {
frm.trigger("toggle_display_account_head");
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
toggle_display_account_head: function(frm) {
@@ -213,6 +216,10 @@
});
},
+ apply_putaway_rule: function() {
+ if (this.frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(this.frm);
+ }
+
});
// for backward compatibility: combine new and previous states
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
index 13c8ceb..32d349f 100755
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.json
@@ -21,6 +21,7 @@
"posting_date",
"posting_time",
"set_posting_time",
+ "apply_putaway_rule",
"is_return",
"return_against",
"section_addresses",
@@ -47,6 +48,7 @@
"set_warehouse",
"rejected_warehouse",
"col_break_warehouse",
+ "set_from_warehouse",
"is_subcontracted",
"supplier_warehouse",
"items_section",
@@ -111,8 +113,10 @@
"range",
"column_break4",
"per_billed",
+ "per_returned",
"is_internal_supplier",
"inter_company_reference",
+ "represents_company",
"subscription_detail",
"auto_repeat",
"printing_settings",
@@ -895,7 +899,7 @@
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "\nDraft\nTo Bill\nCompleted\nCancelled\nClosed",
+ "options": "\nDraft\nTo Bill\nCompleted\nReturn Issued\nCancelled\nClosed",
"print_hide": 1,
"print_width": "150px",
"read_only": 1,
@@ -1085,7 +1089,9 @@
"fieldname": "inter_company_reference",
"fieldtype": "Link",
"label": "Inter Company Reference",
+ "no_copy": 1,
"options": "Delivery Note",
+ "print_hide": 1,
"read_only": 1
},
{
@@ -1104,13 +1110,44 @@
"fieldtype": "Small Text",
"label": "Billing Address",
"read_only": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "apply_putaway_rule",
+ "fieldtype": "Check",
+ "label": "Apply Putaway Rule"
+ },
+ {
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "per_returned",
+ "fieldtype": "Percent",
+ "label": "% Returned",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "depends_on": "eval: doc.is_internal_supplier",
+ "description": "Sets 'From Warehouse' in each row of the items table.",
+ "fieldname": "set_from_warehouse",
+ "fieldtype": "Link",
+ "label": "Set From Warehouse",
+ "options": "Warehouse"
+ },
+ {
+ "fetch_from": "supplier.represents_company",
+ "fieldname": "represents_company",
+ "fieldtype": "Link",
+ "label": "Represents Company",
+ "options": "Company",
+ "read_only": 1
}
],
"icon": "fa fa-truck",
"idx": 261,
"is_submittable": 1,
"links": [],
- "modified": "2020-10-30 14:00:08.347534",
+ "modified": "2020-12-26 20:49:39.106049",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt",
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
index 2cc4679..d721014 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py
@@ -55,20 +55,39 @@
'percent_join_field': 'material_request'
}]
if cint(self.is_return):
- self.status_updater.append({
- 'source_dt': 'Purchase Receipt Item',
- 'target_dt': 'Purchase Order Item',
- 'join_field': 'purchase_order_item',
- 'target_field': 'returned_qty',
- 'source_field': '-1 * qty',
- 'second_source_dt': 'Purchase Invoice Item',
- 'second_source_field': '-1 * qty',
- 'second_join_field': 'po_detail',
- 'extra_cond': """ and exists (select name from `tabPurchase Receipt`
- where name=`tabPurchase Receipt Item`.parent and is_return=1)""",
- 'second_source_extra_cond': """ and exists (select name from `tabPurchase Invoice`
- where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)"""
- })
+ self.status_updater.extend([
+ {
+ 'source_dt': 'Purchase Receipt Item',
+ 'target_dt': 'Purchase Order Item',
+ 'join_field': 'purchase_order_item',
+ 'target_field': 'returned_qty',
+ 'source_field': '-1 * qty',
+ 'second_source_dt': 'Purchase Invoice Item',
+ 'second_source_field': '-1 * qty',
+ 'second_join_field': 'po_detail',
+ 'extra_cond': """ and exists (select name from `tabPurchase Receipt`
+ where name=`tabPurchase Receipt Item`.parent and is_return=1)""",
+ 'second_source_extra_cond': """ and exists (select name from `tabPurchase Invoice`
+ where name=`tabPurchase Invoice Item`.parent and is_return=1 and update_stock=1)"""
+ },
+ {
+ 'source_dt': 'Purchase Receipt Item',
+ 'target_dt': 'Purchase Receipt Item',
+ 'join_field': 'purchase_receipt_item',
+ 'target_field': 'returned_qty',
+ 'target_parent_dt': 'Purchase Receipt',
+ 'target_parent_field': 'per_returned',
+ 'target_ref_field': 'received_stock_qty',
+ 'source_field': '-1 * received_stock_qty',
+ 'percent_join_field_parent': 'return_against'
+ }
+ ])
+
+ def before_validate(self):
+ from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule
+
+ if self.get("items") and self.apply_putaway_rule and not self.get("is_return"):
+ apply_putaway_rule(self.doctype, self.get("items"), self.company)
def validate(self):
self.validate_posting_time()
@@ -90,6 +109,7 @@
if getdate(self.posting_date) > getdate(nowdate()):
throw(_("Posting Date cannot be future date"))
+
def validate_cwip_accounts(self):
for item in self.get('items'):
if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category):
@@ -168,6 +188,7 @@
update_serial_nos_after_submit(self, "items")
self.make_gl_entries()
+ self.repost_future_sle_and_gle()
def check_next_docstatus(self):
submit_rv = frappe.db.sql("""select t1.name
@@ -196,7 +217,8 @@
# because updating ordered qty in bin depends upon updated ordered qty in PO
self.update_stock_ledger()
self.make_gl_entries_on_cancel()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.repost_future_sle_and_gle()
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.delete_auto_created_batches()
def get_current_stock(self):
@@ -266,12 +288,16 @@
# Amount added through landed-cost-voucher
if d.landed_cost_voucher_amount and landed_cost_entries:
for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]):
+ account_currency = get_account_currency(account)
gl_entries.append(self.get_gl_dict({
"account": account,
+ "account_currency": account_currency,
"against": warehouse_account[d.warehouse]["account"],
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": flt(amount),
+ "credit": (flt(amount["base_amount"]) if (amount["base_amount"] or
+ account_currency!=self.company_currency) else flt(amount["amount"])),
+ "credit_in_account_currency": flt(amount["amount"]),
"project": d.project
}, item=d))
@@ -310,7 +336,7 @@
elif d.warehouse not in warehouse_with_no_account or \
d.rejected_warehouse not in warehouse_with_no_account:
warehouse_with_no_account.append(d.warehouse)
- elif d.item_code not in stock_items and flt(d.qty) and auto_accounting_for_non_stock_items:
+ elif d.item_code not in stock_items and not d.is_fixed_asset and flt(d.qty) and auto_accounting_for_non_stock_items:
service_received_but_not_billed_account = self.get_company_default("service_received_but_not_billed")
credit_currency = get_account_currency(service_received_but_not_billed_account)
@@ -478,7 +504,7 @@
frappe.db.set_value("Asset", asset.name, "purchase_receipt_amount", flt(valuation_rate))
def update_status(self, status):
- self.set_status(update=True, status = status)
+ self.set_status(update=True, status=status)
self.notify_update()
clear_doctype_notifications(self)
@@ -490,7 +516,7 @@
for pr in set(updated_pr):
pr_doc = self if (pr == self.name) else frappe.get_doc("Purchase Receipt", pr)
- pr_doc.update_billing_percentage(update_modified=update_modified)
+ update_billing_percentage(pr_doc, update_modified=update_modified)
self.load_from_db()
@@ -500,7 +526,7 @@
where po_detail=%s and (pr_detail is null or pr_detail = '') and docstatus=1""", po_detail)
billed_against_po = billed_against_po and billed_against_po[0][0] or 0
- # Get all Delivery Note Item rows against the Sales Order Item row
+ # Get all Purchase Receipt Item rows against the Purchase Order Item row
pr_details = frappe.db.sql("""select pr_item.name, pr_item.amount, pr_item.parent
from `tabPurchase Receipt Item` pr_item, `tabPurchase Receipt` pr
where pr.name=pr_item.parent and pr_item.purchase_order_item=%s
@@ -530,6 +556,39 @@
return updated_pr
+def update_billing_percentage(pr_doc, update_modified=True):
+ # Reload as billed amount was set in db directly
+ pr_doc.load_from_db()
+
+ # Update Billing % based on pending accepted qty
+ total_amount, total_billed_amount = 0, 0
+ for item in pr_doc.items:
+ return_data = frappe.db.get_list("Purchase Receipt",
+ fields = [
+ "sum(abs(`tabPurchase Receipt Item`.qty)) as qty"
+ ],
+ filters = [
+ ["Purchase Receipt", "docstatus", "=", 1],
+ ["Purchase Receipt", "is_return", "=", 1],
+ ["Purchase Receipt Item", "purchase_receipt_item", "=", item.name]
+ ])
+
+ returned_qty = return_data[0].qty if return_data else 0
+ returned_amount = flt(returned_qty) * flt(item.rate)
+ pending_amount = flt(item.amount) - returned_amount
+ total_billable_amount = pending_amount if item.billed_amt <= pending_amount else item.billed_amt
+
+ total_amount += total_billable_amount
+ total_billed_amount += flt(item.billed_amt)
+
+ percent_billed = round(100 * (total_billed_amount / (total_amount or 1)), 6)
+ pr_doc.db_set("per_billed", percent_billed)
+ pr_doc.load_from_db()
+
+ if update_modified:
+ pr_doc.set_status(update=True)
+ pr_doc.notify_update()
+
@frappe.whitelist()
def make_purchase_invoice(source_name, target_doc=None):
from frappe.model.mapper import get_mapped_doc
@@ -552,6 +611,7 @@
def update_item(source_doc, target_doc, source_parent):
target_doc.qty, returned_qty = get_pending_qty(source_doc)
+ target_doc.stock_qty = flt(target_doc.qty) * flt(target_doc.conversion_factor, target_doc.precision("conversion_factor"))
returned_qty_map[source_doc.name] = returned_qty
def get_pending_qty(item_row):
@@ -672,7 +732,13 @@
for lcv in landed_cost_vouchers:
landed_cost_voucher_doc = frappe.get_doc("Landed Cost Voucher", lcv.parent)
- based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
+
+ #Use amount field for total item cost for manually cost distributed LCVs
+ if landed_cost_voucher_doc.distribute_charges_based_on == 'Distribute Manually':
+ based_on_field = 'amount'
+ else:
+ based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on)
+
total_item_cost = 0
for item in landed_cost_voucher_doc.items:
@@ -682,9 +748,16 @@
if item.receipt_document == purchase_document:
for account in landed_cost_voucher_doc.taxes:
item_account_wise_cost.setdefault((item.item_code, item.purchase_receipt_item), {})
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, 0.0)
- item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account] += \
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)].setdefault(account.expense_account, {
+ "amount": 0.0,
+ "base_amount": 0.0
+ })
+
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["amount"] += \
account.amount * item.get(based_on_field) / total_item_cost
+ item_account_wise_cost[(item.item_code, item.purchase_receipt_item)][account.expense_account]["base_amount"] += \
+ account.base_amount * item.get(based_on_field) / total_item_cost
+
return item_account_wise_cost
diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js
index e81f323..77711de 100644
--- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js
+++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt_list.js
@@ -3,12 +3,14 @@
"transporter_name", "is_return", "status", "per_billed", "currency"],
get_indicator: function(doc) {
if(cint(doc.is_return)==1) {
- return [__("Return"), "darkgrey", "is_return,=,Yes"];
+ return [__("Return"), "gray", "is_return,=,Yes"];
} else if (doc.status === "Closed") {
return [__("Closed"), "green", "status,=,Closed"];
+ } else if (flt(doc.per_returned, 2) === 100) {
+ return [__("Return Issued"), "grey", "per_returned,=,100"];
} else if (flt(doc.grand_total) !== 0 && flt(doc.per_billed, 2) < 100) {
return [__("To Bill"), "orange", "per_billed,<,100"];
- } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) == 100) {
+ } else if (flt(doc.grand_total) === 0 || flt(doc.per_billed, 2) === 100) {
return [__("Completed"), "green", "per_billed,=,100"];
}
}
diff --git a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
index 253edb0..7741ee7 100644
--- a/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
+++ b/erpnext/stock/doctype/purchase_receipt/test_purchase_receipt.py
@@ -9,14 +9,15 @@
from frappe.utils import cint, flt, cstr, today, random_string, add_days
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext import set_perpetual_inventory
from erpnext.stock.doctype.serial_no.serial_no import SerialNoDuplicateError
from erpnext.accounts.doctype.account.test_account import get_inventory_account
from erpnext.stock.doctype.item.test_item import make_item
from six import iteritems
+from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+
+
class TestPurchaseReceipt(unittest.TestCase):
def setUp(self):
- set_perpetual_inventory(0)
frappe.db.set_value("Buying Settings", None, "allow_multiple_items", 1)
def test_reverse_purchase_receipt_sle(self):
@@ -93,10 +94,15 @@
frappe.get_doc('Payment Terms Template', '_Test Payment Terms Template For Purchase Invoice').delete()
def test_purchase_receipt_no_gl_entry(self):
+ from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
- existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
- "warehouse": "_Test Warehouse - _TC"}, "stock_value")
+ existing_bin_qty, existing_bin_stock_value = frappe.db.get_value("Bin", {"item_code": "_Test Item",
+ "warehouse": "_Test Warehouse - _TC"}, ["actual_qty", "stock_value"])
+
+ if existing_bin_qty < 0:
+ make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=abs(existing_bin_qty))
pr = make_purchase_receipt()
@@ -112,6 +118,8 @@
self.assertFalse(get_gl_entries("Purchase Receipt", pr.name))
+ pr.cancel()
+
def test_batched_serial_no_purchase(self):
item = frappe.db.exists("Item", {'item_name': 'Batched Serialized Item'})
if not item:
@@ -137,7 +145,10 @@
self.assertFalse(frappe.db.get_all('Serial No', {'batch_no': batch_no}))
def test_purchase_receipt_gl_entry(self):
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", get_multiple_items = True, get_taxes_and_charges = True)
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1",
+ get_multiple_items = True, get_taxes_and_charges = True)
+
self.assertEqual(cint(erpnext.is_perpetual_inventory_enabled(pr.company)), 1)
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -180,22 +191,30 @@
rm_supp_cost = sum([d.amount for d in pr.get("supplied_items")])
self.assertEqual(pr.get("items")[0].rm_supp_cost, flt(rm_supp_cost, 2))
+
+ pr.cancel()
def test_subcontracting_gle_fg_item_rate_zero(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- set_perpetual_inventory()
frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
- make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1", qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
- make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
+
+ se1 = make_stock_entry(item_code="_Test Item", target="Work In Progress - TCP1",
qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
+
+ se2 = make_stock_entry(item_code="_Test Item Home Desktop 100", target="Work In Progress - TCP1",
+ qty=100, basic_rate=100, company="_Test Company with perpetual inventory")
+
pr = make_purchase_receipt(item_code="_Test FG Item", qty=10, rate=0, is_subcontracted="Yes",
- company="_Test Company with perpetual inventory", warehouse='Stores - TCP1', supplier_warehouse='Work In Progress - TCP1')
+ company="_Test Company with perpetual inventory", warehouse='Stores - TCP1',
+ supplier_warehouse='Work In Progress - TCP1')
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
self.assertFalse(gl_entries)
- set_perpetual_inventory(0)
+ pr.cancel()
+ se1.cancel()
+ se2.cancel()
def test_subcontracting_over_receipt(self):
"""
@@ -213,13 +232,13 @@
item_code = "_Test Subcontracted FG Item 1"
make_subcontracted_item(item_code=item_code)
- po = create_purchase_order(item_code=item_code, qty=1,
+ po = create_purchase_order(item_code=item_code, qty=1, include_exploded_items=0,
is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC")
#stock raw materials in a warehouse before transfer
- make_stock_entry(target="_Test Warehouse - _TC",
- item_code = "Test Extra Item 1", qty=1, basic_rate=100)
- make_stock_entry(target="_Test Warehouse - _TC",
+ se1 = make_stock_entry(target="_Test Warehouse - _TC",
+ item_code = "Test Extra Item 1", qty=10, basic_rate=100)
+ se2 = make_stock_entry(target="_Test Warehouse - _TC",
item_code = "_Test FG Item", qty=1, basic_rate=100)
rm_items = [
{
@@ -251,6 +270,13 @@
pr1.submit()
self.assertRaises(frappe.ValidationError, pr2.submit)
+ pr1.cancel()
+ se.cancel()
+ se1.cancel()
+ se2.cancel()
+ po.reload()
+ po.cancel()
+
def test_serial_no_supplier(self):
pr = make_purchase_receipt(item_code="_Test Serialized Item With Series", qty=1)
self.assertEqual(frappe.db.get_value("Serial No", pr.get("items")[0].serial_no, "supplier"),
@@ -281,11 +307,17 @@
self.assertEqual(frappe.db.get_value("Serial No", serial_no, "warehouse"),
pr.get("items")[0].rejected_warehouse)
- def test_purchase_return(self):
+ pr.cancel()
- pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
+ def test_purchase_return_partial(self):
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
- return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-2)
+ return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1",
+ is_return=1, return_against=pr.name, qty=-2, do_not_submit=1)
+ return_pr.items[0].purchase_receipt_item = pr.items[0].name
+ return_pr.submit()
# check sle
outgoing_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
@@ -309,6 +341,63 @@
self.assertEqual(expected_values[gle.account][0], gle.debit)
self.assertEqual(expected_values[gle.account][1], gle.credit)
+ # hack because new_doc isn't considering is_return portion of status_updater
+ returned = frappe.get_doc("Purchase Receipt", return_pr.name)
+ returned.update_prevdoc_status()
+ pr.load_from_db()
+
+ # Check if Original PR updated
+ self.assertEqual(pr.items[0].returned_qty, 2)
+ self.assertEqual(pr.per_returned, 40)
+
+ from erpnext.controllers.sales_and_purchase_return import make_return_doc
+ return_pr_2 = make_return_doc("Purchase Receipt", pr.name)
+
+ # Check if unreturned amount is mapped in 2nd return
+ self.assertEqual(return_pr_2.items[0].qty, -3)
+
+ # Make PI against unreturned amount
+ pi = make_purchase_invoice(pr.name)
+ pi.submit()
+
+ self.assertEqual(pi.items[0].qty, 3)
+
+ pr.load_from_db()
+ # PR should be completed on billing all unreturned amount
+ self.assertEqual(pr.items[0].billed_amt, 150)
+ self.assertEqual(pr.per_billed, 100)
+ self.assertEqual(pr.status, 'Completed')
+
+ pi.load_from_db()
+ pi.cancel()
+
+ pr.load_from_db()
+ self.assertEqual(pr.per_billed, 0)
+
+ return_pr.cancel()
+ pr.cancel()
+
+ def test_purchase_return_full(self):
+ pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1")
+
+ return_pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1",
+ supplier_warehouse = "Work in Progress - TCP1", is_return=1, return_against=pr.name, qty=-5, do_not_submit=1)
+ return_pr.items[0].purchase_receipt_item = pr.items[0].name
+ return_pr.submit()
+
+ # hack because new_doc isn't considering is_return portion of status_updater
+ returned = frappe.get_doc("Purchase Receipt", return_pr.name)
+ returned.update_prevdoc_status()
+ pr.load_from_db()
+
+ # Check if Original PR updated
+ self.assertEqual(pr.items[0].returned_qty, 5)
+ self.assertEqual(pr.per_returned, 100)
+ self.assertEqual(pr.status, 'Return Issued')
+
+ return_pr.cancel()
+ pr.cancel()
def test_purchase_return_for_rejected_qty(self):
from erpnext.stock.doctype.warehouse.test_warehouse import get_warehouse
@@ -327,6 +416,9 @@
self.assertEqual(actual_qty, -2)
+ return_pr.cancel()
+ pr.cancel()
+
def test_purchase_return_for_serialized_items(self):
def _check_serial_no_values(serial_no, field_values):
@@ -354,6 +446,10 @@
"delivery_document_no": return_pr.name
})
+ return_pr.cancel()
+ pr.reload()
+ pr.cancel()
+
def test_purchase_return_for_multi_uom(self):
item_code = "_Test Purchase Return For Multi-UOM"
if not frappe.db.exists('Item', item_code):
@@ -370,6 +466,9 @@
self.assertEqual(abs(return_pr.items[0].stock_qty), 1.0)
+ return_pr.cancel()
+ pr.cancel()
+
def test_closed_purchase_receipt(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import update_purchase_receipt_status
@@ -379,6 +478,9 @@
update_purchase_receipt_status(pr.name, "Closed")
self.assertEqual(frappe.db.get_value("Purchase Receipt", pr.name, "status"), "Closed")
+ pr.reload()
+ pr.cancel()
+
def test_pr_billing_status(self):
# PO -> PR1 -> PI and PO -> PI and PO -> PR2
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order
@@ -416,10 +518,21 @@
self.assertEqual(pr1.per_billed, 100)
self.assertEqual(pr1.status, "Completed")
+ pr2.load_from_db()
self.assertEqual(pr2.get("items")[0].billed_amt, 2000)
self.assertEqual(pr2.per_billed, 80)
self.assertEqual(pr2.status, "To Bill")
+ pr2.cancel()
+ pi2.reload()
+ pi2.cancel()
+ pi1.reload()
+ pi1.cancel()
+ pr1.reload()
+ pr1.cancel()
+ po.reload()
+ po.cancel()
+
def test_serial_no_against_purchase_receipt(self):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
@@ -447,6 +560,8 @@
self.assertEqual(serial_no, frappe.db.get_value("Serial No",
{"purchase_document_type": "Purchase Receipt", "purchase_document_no": new_pr_doc.name}, "name"))
+ new_pr_doc.cancel()
+
def test_not_accept_duplicate_serial_no(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
@@ -457,15 +572,18 @@
item_code = item.name
serial_no = random_string(5)
- make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
- create_delivery_note(item_code=item_code, qty=1, serial_no=serial_no)
+ pr1 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no)
+ dn = create_delivery_note(item_code=item_code, qty=1, serial_no=serial_no)
- pr = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True)
- self.assertRaises(SerialNoDuplicateError, pr.submit)
+ pr2 = make_purchase_receipt(item_code=item_code, qty=1, serial_no=serial_no, do_not_submit=True)
+ self.assertRaises(SerialNoDuplicateError, pr2.submit)
se = make_stock_entry(item_code=item_code, target="_Test Warehouse - _TC", qty=1,
serial_no=serial_no, basic_rate=100, do_not_submit=True)
- self.assertRaises(SerialNoDuplicateError, se.submit)
+ se.submit()
+
+ dn.cancel()
+ pr1.cancel()
def test_auto_asset_creation(self):
asset_item = "Test Asset Item"
@@ -487,7 +605,7 @@
'company_name': '_Test Company',
'fixed_asset_account': '_Test Fixed Asset - _TC',
'accumulated_depreciation_account': '_Test Accumulated Depreciations - _TC',
- 'depreciation_expense_account': '_Test Depreciation - _TC'
+ 'depreciation_expense_account': '_Test Depreciations - _TC'
}]
}).insert()
@@ -506,6 +624,8 @@
location = frappe.db.get_value('Asset', assets[0].name, 'location')
self.assertEquals(location, "Test Location")
+ pr.cancel()
+
def test_purchase_return_with_submitted_asset(self):
from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_return
@@ -532,6 +652,9 @@
pr_return.submit()
+ pr_return.cancel()
+ pr.cancel()
+
def test_purchase_receipt_cost_center(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
cost_center = "_Test Cost Center for BS Account - TCP1"
@@ -543,7 +666,8 @@
'location_name': 'Test Location'
}).insert()
- pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
+ pr = make_purchase_receipt(cost_center=cost_center, company="_Test Company with perpetual inventory",
+ warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1")
stock_in_hand_account = get_inventory_account(pr.company, pr.get("items")[0].warehouse)
gl_entries = get_gl_entries("Purchase Receipt", pr.name)
@@ -561,6 +685,8 @@
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
+ pr.cancel()
+
def test_purchase_receipt_cost_center_with_balance_sheet_account(self):
if not frappe.db.exists('Location', 'Test Location'):
frappe.get_doc({
@@ -586,6 +712,8 @@
for i, gle in enumerate(gl_entries):
self.assertEqual(expected_values[gle.account]["cost_center"], gle.cost_center)
+ pr.cancel()
+
def test_make_purchase_invoice_from_pr_for_returned_qty(self):
from erpnext.buying.doctype.purchase_order.test_purchase_order import create_purchase_order, create_pr_against_po
@@ -601,6 +729,12 @@
pi = make_purchase_invoice(pr.name)
self.assertEquals(pi.items[0].qty, 3)
+ pr1.cancel()
+ pr.reload()
+ pr.cancel()
+ po.reload()
+ po.cancel()
+
def test_make_purchase_invoice_from_pr_with_returned_qty_duplicate_items(self):
pr1 = make_purchase_receipt(qty=8, do_not_submit=True)
pr1.append("items", {
@@ -627,8 +761,14 @@
self.assertEquals(pi2.items[0].qty, 2)
self.assertEquals(pi2.items[1].qty, 1)
+ pr2.cancel()
+ pi1.cancel()
+ pr1.reload()
+ pr1.cancel()
+
def test_stock_transfer_from_purchase_receipt(self):
- pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1', company="_Test Company with perpetual inventory")
+ pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1',
+ company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", do_not_save=1)
@@ -651,18 +791,20 @@
for sle in sl_entries:
self.assertEqual(expected_sle[sle.warehouse], sle.actual_qty)
- def test_stock_transfer_from_purchase_receipt_with_valuation(self):
- warehouse = frappe.get_doc('Warehouse', 'Work In Progress - TCP1')
- warehouse.account = '_Test Account Stock In Hand - TCP1'
- warehouse.save()
+ pr.cancel()
+ pr1.cancel()
- pr1 = make_purchase_receipt(warehouse = 'Work In Progress - TCP1',
+ def test_stock_transfer_from_purchase_receipt_with_valuation(self):
+ create_warehouse("_Test Warehouse for Valuation", company="_Test Company with perpetual inventory",
+ properties={"account": '_Test Account Stock In Hand - TCP1'})
+
+ pr1 = make_purchase_receipt(warehouse = '_Test Warehouse for Valuation - TCP1',
company="_Test Company with perpetual inventory")
pr = make_purchase_receipt(company="_Test Company with perpetual inventory",
warehouse = "Stores - TCP1", do_not_save=1)
- pr.items[0].from_warehouse = 'Work In Progress - TCP1'
+ pr.items[0].from_warehouse = '_Test Warehouse for Valuation - TCP1'
pr.supplier_warehouse = ''
@@ -687,7 +829,7 @@
]
expected_sle = {
- 'Work In Progress - TCP1': -5,
+ '_Test Warehouse for Valuation - TCP1': -5,
'Stores - TCP1': 5
}
@@ -699,60 +841,9 @@
self.assertEqual(gle.debit, expected_gle[i][1])
self.assertEqual(gle.credit, expected_gle[i][2])
- warehouse.account = ''
- warehouse.save()
+ pr.cancel()
+ pr1.cancel()
- def test_backdated_purchase_receipt(self):
- # make purchase receipt for default company
- make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4")
-
- # try to make another backdated PR
- posting_date = add_days(today(), -1)
- pr = make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.save()
-
- self.assertRaises(frappe.ValidationError, pr.submit)
-
- # make purchase receipt for other company backdated
- pr = make_purchase_receipt(company="_Test Company 5", warehouse="Stores - _TC5",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.submit()
-
- # Allowed to submit for other company's PR
- self.assertEqual(pr.docstatus, 1)
-
- def test_backdated_purchase_receipt_for_same_company_different_warehouse(self):
- # make purchase receipt for default company
- make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4")
-
- # try to make another backdated PR
- posting_date = add_days(today(), -1)
- pr = make_purchase_receipt(company="_Test Company 4", warehouse="Stores - _TC4",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.save()
-
- self.assertRaises(frappe.ValidationError, pr.submit)
-
- # make purchase receipt for other company backdated
- pr = make_purchase_receipt(company="_Test Company 4", warehouse="Finished Goods - _TC4",
- do_not_submit=True)
-
- pr.set_posting_time = 1
- pr.posting_date = posting_date
- pr.submit()
-
- # Allowed to submit for other company's PR
- self.assertEqual(pr.docstatus, 1)
def test_subcontracted_pr_for_multi_transfer_batches(self):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
@@ -815,6 +906,12 @@
update_backflush_based_on("BOM")
+ pr.delete()
+ se.cancel()
+ ste2.cancel()
+ ste1.cancel()
+ po.cancel()
+
def get_sl_entries(voucher_type, voucher_no):
return frappe.db.sql(""" select actual_qty, warehouse, stock_value_difference
from `tabStock Ledger Entry` where voucher_type=%s and voucher_no=%s
@@ -910,6 +1007,8 @@
pr.posting_date = args.posting_date or today()
if args.posting_time:
pr.posting_time = args.posting_time
+ if args.posting_date or args.posting_time:
+ pr.set_posting_time = 1
pr.company = args.company or "_Test Company"
pr.supplier = args.supplier or "_Test Supplier"
pr.is_subcontracted = args.is_subcontracted or "No"
@@ -917,6 +1016,7 @@
pr.currency = args.currency or "INR"
pr.is_return = args.is_return
pr.return_against = args.return_against
+ pr.apply_putaway_rule = args.apply_putaway_rule
qty = args.qty or 5
received_qty = args.received_qty or qty
rejected_qty = args.rejected_qty or flt(received_qty) - flt(qty)
@@ -932,6 +1032,7 @@
"rejected_warehouse": args.rejected_warehouse or "_Test Rejected Warehouse - _TC" if rejected_qty != 0 else "",
"rate": args.rate if args.rate != None else 50,
"conversion_factor": args.conversion_factor or 1.0,
+ "stock_qty": flt(qty) * (flt(args.conversion_factor) or 1.0),
"serial_no": args.serial_no,
"stock_uom": args.stock_uom or "_Test UOM",
"uom": uom,
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
index c1e1f90..8974ad9 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.json
@@ -28,9 +28,13 @@
"uom",
"stock_uom",
"conversion_factor",
- "stock_qty",
"retain_sample",
"sample_quantity",
+ "tracking_section",
+ "received_stock_qty",
+ "stock_qty",
+ "col_break_tracking_section",
+ "returned_qty",
"rate_and_amount",
"price_list_rate",
"discount_percentage",
@@ -44,6 +48,7 @@
"base_rate",
"base_amount",
"pricing_rules",
+ "stock_uom_rate",
"is_free_item",
"section_break_29",
"net_rate",
@@ -72,6 +77,8 @@
"purchase_order_item",
"material_request_item",
"purchase_receipt_item",
+ "delivery_note_item",
+ "putaway_rule",
"section_break_45",
"allow_zero_valuation_rate",
"bom",
@@ -526,7 +533,7 @@
{
"fieldname": "stock_qty",
"fieldtype": "Float",
- "label": "Accepted Qty as per Stock UOM",
+ "label": "Accepted Qty in Stock UOM",
"oldfieldname": "stock_qty",
"oldfieldtype": "Currency",
"print_hide": 1,
@@ -814,11 +821,12 @@
"read_only": 1
},
{
+ "depends_on": "eval:parent.is_internal_supplier",
"fieldname": "from_warehouse",
"fieldtype": "Link",
"hidden": 1,
"ignore_user_permissions": 1,
- "label": "Supplier Warehouse",
+ "label": "From Warehouse",
"options": "Warehouse"
},
{
@@ -834,12 +842,60 @@
"collapsible": 1,
"fieldname": "image_column",
"fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "putaway_rule",
+ "fieldtype": "Link",
+ "label": "Putaway Rule",
+ "no_copy": 1,
+ "options": "Putaway Rule",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "tracking_section",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "col_break_tracking_section",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "returned_qty",
+ "fieldname": "returned_qty",
+ "fieldtype": "Float",
+ "label": "Returned Qty in Stock UOM",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "received_stock_qty",
+ "fieldtype": "Float",
+ "label": "Received Qty in Stock UOM",
+ "print_hide": 1
+ },
+ {
+ "depends_on": "eval: doc.uom != doc.stock_uom",
+ "fieldname": "stock_uom_rate",
+ "fieldtype": "Currency",
+ "label": "Rate of Stock UOM",
+ "options": "currency",
+ "read_only": 1
+ },
+ {
+ "fieldname": "delivery_note_item",
+ "fieldtype": "Data",
+ "label": "Delivery Note Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-04-28 19:01:21.154963",
+ "modified": "2021-01-30 21:44:06.918515",
"modified_by": "Administrator",
"module": "Stock",
"name": "Purchase Receipt Item",
diff --git a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
index 679bd1e..b79bb5d 100644
--- a/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
+++ b/erpnext/stock/doctype/purchase_receipt_item/purchase_receipt_item.py
@@ -5,8 +5,6 @@
import frappe
from frappe.model.document import Document
-from erpnext.controllers.print_settings import print_settings_for_item_table
class PurchaseReceiptItem(Document):
- def __setup__(self):
- print_settings_for_item_table(self)
+ pass
diff --git a/erpnext/config/__init__.py b/erpnext/stock/doctype/putaway_rule/__init__.py
similarity index 100%
copy from erpnext/config/__init__.py
copy to erpnext/stock/doctype/putaway_rule/__init__.py
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.js b/erpnext/stock/doctype/putaway_rule/putaway_rule.js
new file mode 100644
index 0000000..e056920
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.js
@@ -0,0 +1,43 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Putaway Rule', {
+ setup: function(frm) {
+ frm.set_query("warehouse", function() {
+ return {
+ "filters": {
+ "company": frm.doc.company,
+ "is_group": 0
+ }
+ };
+ });
+ },
+
+ uom: function(frm) {
+ if (frm.doc.item_code && frm.doc.uom) {
+ return frm.call({
+ method: "erpnext.stock.get_item_details.get_conversion_factor",
+ args: {
+ item_code: frm.doc.item_code,
+ uom: frm.doc.uom
+ },
+ callback: function(r) {
+ if (!r.exc) {
+ let stock_capacity = flt(frm.doc.capacity) * flt(r.message.conversion_factor);
+ frm.set_value('conversion_factor', r.message.conversion_factor);
+ frm.set_value('stock_capacity', stock_capacity);
+ }
+ }
+ });
+ }
+ },
+
+ capacity: function(frm) {
+ let stock_capacity = flt(frm.doc.capacity) * flt(frm.doc.conversion_factor);
+ frm.set_value('stock_capacity', stock_capacity);
+ }
+
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.json b/erpnext/stock/doctype/putaway_rule/putaway_rule.json
new file mode 100644
index 0000000..a003f49
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.json
@@ -0,0 +1,160 @@
+{
+ "actions": [],
+ "autoname": "PUT-.####",
+ "creation": "2020-11-09 11:39:46.489501",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "disable",
+ "item_code",
+ "item_name",
+ "warehouse",
+ "priority",
+ "col_break_capacity",
+ "company",
+ "capacity",
+ "uom",
+ "conversion_factor",
+ "stock_uom",
+ "stock_capacity"
+ ],
+ "fields": [
+ {
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Item",
+ "options": "Item",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.item_name",
+ "fieldname": "item_name",
+ "fieldtype": "Data",
+ "label": "Item Name",
+ "read_only": 1
+ },
+ {
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Warehouse",
+ "options": "Warehouse",
+ "reqd": 1
+ },
+ {
+ "fieldname": "col_break_capacity",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "capacity",
+ "fieldtype": "Float",
+ "in_list_view": 1,
+ "label": "Capacity",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "item_code.stock_uom",
+ "fieldname": "stock_uom",
+ "fieldtype": "Link",
+ "label": "Stock UOM",
+ "options": "UOM",
+ "read_only": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "priority",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Priority"
+ },
+ {
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "in_standard_filter": 1,
+ "label": "Company",
+ "options": "Company",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:!doc.__islocal",
+ "fieldname": "disable",
+ "fieldtype": "Check",
+ "label": "Disable"
+ },
+ {
+ "fieldname": "uom",
+ "fieldtype": "Link",
+ "label": "UOM",
+ "no_copy": 1,
+ "options": "UOM"
+ },
+ {
+ "fieldname": "stock_capacity",
+ "fieldtype": "Float",
+ "label": "Capacity in Stock UOM",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "conversion_factor",
+ "fieldtype": "Float",
+ "label": "Conversion Factor",
+ "no_copy": 1,
+ "read_only": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-11-25 20:39:19.973437",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Putaway Rule",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "permlevel": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock Manager",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "title_field": "item_code",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule.py b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
new file mode 100644
index 0000000..ea26cac
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule.py
@@ -0,0 +1,235 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import copy
+import json
+from collections import defaultdict
+from six import string_types
+from frappe import _
+from frappe.utils import flt, floor, nowdate, cint
+from frappe.model.document import Document
+from erpnext.stock.utils import get_stock_balance
+from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
+
+class PutawayRule(Document):
+ def validate(self):
+ self.validate_duplicate_rule()
+ self.validate_warehouse_and_company()
+ self.validate_capacity()
+ self.validate_priority()
+ self.set_stock_capacity()
+
+ def validate_duplicate_rule(self):
+ existing_rule = frappe.db.exists("Putaway Rule", {"item_code": self.item_code, "warehouse": self.warehouse})
+ if existing_rule and existing_rule != self.name:
+ frappe.throw(_("Putaway Rule already exists for Item {0} in Warehouse {1}.")
+ .format(frappe.bold(self.item_code), frappe.bold(self.warehouse)),
+ title=_("Duplicate"))
+
+ def validate_priority(self):
+ if self.priority < 1:
+ frappe.throw(_("Priority cannot be lesser than 1."), title=_("Invalid Priority"))
+
+ def validate_warehouse_and_company(self):
+ company = frappe.db.get_value("Warehouse", self.warehouse, "company")
+ if company != self.company:
+ frappe.throw(_("Warehouse {0} does not belong to Company {1}.")
+ .format(frappe.bold(self.warehouse), frappe.bold(self.company)),
+ title=_("Invalid Warehouse"))
+
+ def validate_capacity(self):
+ stock_uom = frappe.db.get_value("Item", self.item_code, "stock_uom")
+ balance_qty = get_stock_balance(self.item_code, self.warehouse, nowdate())
+
+ if flt(self.stock_capacity) < flt(balance_qty):
+ frappe.throw(_("Warehouse Capacity for Item '{0}' must be greater than the existing stock level of {1} {2}.")
+ .format(self.item_code, frappe.bold(balance_qty), stock_uom),
+ title=_("Insufficient Capacity"))
+
+ if not self.capacity:
+ frappe.throw(_("Capacity must be greater than 0"), title=_("Invalid"))
+
+ def set_stock_capacity(self):
+ self.stock_capacity = (flt(self.conversion_factor) or 1) * flt(self.capacity)
+
+@frappe.whitelist()
+def get_available_putaway_capacity(rule):
+ stock_capacity, item_code, warehouse = frappe.db.get_value("Putaway Rule", rule,
+ ["stock_capacity", "item_code", "warehouse"])
+ balance_qty = get_stock_balance(item_code, warehouse, nowdate())
+ free_space = flt(stock_capacity) - flt(balance_qty)
+ return free_space if free_space > 0 else 0
+
+@frappe.whitelist()
+def apply_putaway_rule(doctype, items, company, sync=None, purpose=None):
+ """ Applies Putaway Rule on line items.
+
+ items: List of Purchase Receipt/Stock Entry Items
+ company: Company in the Purchase Receipt/Stock Entry
+ doctype: Doctype to apply rule on
+ purpose: Purpose of Stock Entry
+ sync (optional): Sync with client side only for client side calls
+ """
+ if isinstance(items, string_types):
+ items = json.loads(items)
+
+ items_not_accomodated, updated_table = [], []
+ item_wise_rules = defaultdict(list)
+
+ for item in items:
+ if isinstance(item, dict):
+ item = frappe._dict(item)
+
+ source_warehouse = item.get("s_warehouse")
+ serial_nos = get_serial_nos(item.get("serial_no"))
+ item.conversion_factor = flt(item.conversion_factor) or 1.0
+ pending_qty, item_code = flt(item.qty), item.item_code
+ pending_stock_qty = flt(item.transfer_qty) if doctype == "Stock Entry" else flt(item.stock_qty)
+ uom_must_be_whole_number = frappe.db.get_value('UOM', item.uom, 'must_be_whole_number')
+
+ if not pending_qty or not item_code:
+ updated_table = add_row(item, pending_qty, source_warehouse or item.warehouse, updated_table)
+ continue
+
+ at_capacity, rules = get_ordered_putaway_rules(item_code, company, source_warehouse=source_warehouse)
+
+ if not rules:
+ warehouse = source_warehouse or item.warehouse
+ if at_capacity:
+ # rules available, but no free space
+ items_not_accomodated.append([item_code, pending_qty])
+ else:
+ updated_table = add_row(item, pending_qty, warehouse, updated_table)
+ continue
+
+ # maintain item/item-warehouse wise rules, to handle if item is entered twice
+ # in the table, due to different price, etc.
+ key = item_code
+ if doctype == "Stock Entry" and purpose == "Material Transfer" and source_warehouse:
+ key = (item_code, source_warehouse)
+
+ if not item_wise_rules[key]:
+ item_wise_rules[key] = rules
+
+ for rule in item_wise_rules[key]:
+ if pending_stock_qty > 0 and rule.free_space:
+ stock_qty_to_allocate = flt(rule.free_space) if pending_stock_qty >= flt(rule.free_space) else pending_stock_qty
+ qty_to_allocate = stock_qty_to_allocate / item.conversion_factor
+
+ if uom_must_be_whole_number:
+ qty_to_allocate = floor(qty_to_allocate)
+ stock_qty_to_allocate = qty_to_allocate * item.conversion_factor
+
+ if not qty_to_allocate: break
+
+ updated_table = add_row(item, qty_to_allocate, rule.warehouse, updated_table,
+ rule.name, serial_nos=serial_nos)
+
+ pending_stock_qty -= stock_qty_to_allocate
+ pending_qty -= qty_to_allocate
+ rule["free_space"] -= stock_qty_to_allocate
+
+ if not pending_stock_qty > 0: break
+
+ # if pending qty after applying all rules, add row without warehouse
+ if pending_stock_qty > 0:
+ items_not_accomodated.append([item.item_code, pending_qty])
+
+ if items_not_accomodated:
+ show_unassigned_items_message(items_not_accomodated)
+
+ items[:] = updated_table if updated_table else items # modify items table
+
+ if sync and json.loads(sync): # sync with client side
+ return items
+
+def get_ordered_putaway_rules(item_code, company, source_warehouse=None):
+ """Returns an ordered list of putaway rules to apply on an item."""
+ filters = {
+ "item_code": item_code,
+ "company": company,
+ "disable": 0
+ }
+ if source_warehouse:
+ filters.update({"warehouse": ["!=", source_warehouse]})
+
+ rules = frappe.get_all("Putaway Rule",
+ fields=["name", "item_code", "stock_capacity", "priority", "warehouse"],
+ filters=filters,
+ order_by="priority asc, capacity desc")
+
+ if not rules:
+ return False, None
+
+ vacant_rules = []
+ for rule in rules:
+ balance_qty = get_stock_balance(rule.item_code, rule.warehouse, nowdate())
+ free_space = flt(rule.stock_capacity) - flt(balance_qty)
+ if free_space > 0:
+ rule["free_space"] = free_space
+ vacant_rules.append(rule)
+
+ if not vacant_rules:
+ # After iterating through rules, if no rules are left
+ # then there is not enough space left in any rule
+ return True, None
+
+ vacant_rules = sorted(vacant_rules, key = lambda i: (i['priority'], -i['free_space']))
+
+ return False, vacant_rules
+
+def add_row(item, to_allocate, warehouse, updated_table, rule=None, serial_nos=None):
+ new_updated_table_row = copy.deepcopy(item)
+ new_updated_table_row.idx = 1 if not updated_table else cint(updated_table[-1].idx) + 1
+ new_updated_table_row.name = None
+ new_updated_table_row.qty = to_allocate
+
+ if item.doctype == "Stock Entry Detail":
+ new_updated_table_row.t_warehouse = warehouse
+ new_updated_table_row.transfer_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor)
+ else:
+ new_updated_table_row.stock_qty = flt(to_allocate) * flt(new_updated_table_row.conversion_factor)
+ new_updated_table_row.warehouse = warehouse
+ new_updated_table_row.rejected_qty = 0
+ new_updated_table_row.received_qty = to_allocate
+
+ if rule:
+ new_updated_table_row.putaway_rule = rule
+ if serial_nos:
+ new_updated_table_row.serial_no = get_serial_nos_to_allocate(serial_nos, to_allocate)
+
+ updated_table.append(new_updated_table_row)
+ return updated_table
+
+def show_unassigned_items_message(items_not_accomodated):
+ msg = _("The following Items, having Putaway Rules, could not be accomodated:") + "<br><br>"
+ formatted_item_rows = ""
+
+ for entry in items_not_accomodated:
+ item_link = frappe.utils.get_link_to_form("Item", entry[0])
+ formatted_item_rows += """
+ <td>{0}</td>
+ <td>{1}</td>
+ </tr>""".format(item_link, frappe.bold(entry[1]))
+
+ msg += """
+ <table class="table">
+ <thead>
+ <td>{0}</td>
+ <td>{1}</td>
+ </thead>
+ {2}
+ </table>
+ """.format(_("Item"), _("Unassigned Qty"), formatted_item_rows)
+
+ frappe.msgprint(msg, title=_("Insufficient Capacity"), is_minimizable=True, wide=True)
+
+def get_serial_nos_to_allocate(serial_nos, to_allocate):
+ if serial_nos:
+ allocated_serial_nos = serial_nos[0: cint(to_allocate)]
+ serial_nos[:] = serial_nos[cint(to_allocate):] # pop out allocated serial nos and modify list
+ return "\n".join(allocated_serial_nos) if allocated_serial_nos else ""
+ else: return ""
\ No newline at end of file
diff --git a/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js
new file mode 100644
index 0000000..725e91e
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/putaway_rule_list.js
@@ -0,0 +1,18 @@
+frappe.listview_settings['Putaway Rule'] = {
+ add_fields: ["disable"],
+ get_indicator: (doc) => {
+ if (doc.disable) {
+ return [__("Disabled"), "darkgrey", "disable,=,1"];
+ } else {
+ return [__("Active"), "blue", "disable,=,0"];
+ }
+ },
+
+ reports: [
+ {
+ name: 'Warehouse Capacity Summary',
+ report_type: 'Page',
+ route: 'warehouse-capacity-summary'
+ }
+ ]
+};
diff --git a/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
new file mode 100644
index 0000000..86f7dc3
--- /dev/null
+++ b/erpnext/stock/doctype/putaway_rule/test_putaway_rule.py
@@ -0,0 +1,389 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+import frappe
+import unittest
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.get_item_details import get_conversion_factor
+from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
+from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
+from erpnext.stock.doctype.batch.test_batch import make_new_batch
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+
+class TestPutawayRule(unittest.TestCase):
+ def setUp(self):
+ if not frappe.db.exists("Item", "_Rice"):
+ make_item("_Rice", {
+ 'is_stock_item': 1,
+ 'has_batch_no' : 1,
+ 'create_new_batch': 1,
+ 'stock_uom': 'Kg'
+ })
+
+ if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 1"}):
+ create_warehouse("Rack 1")
+ if not frappe.db.exists("Warehouse", {"warehouse_name": "Rack 2"}):
+ create_warehouse("Rack 2")
+
+ self.warehouse_1 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 1"})
+ self.warehouse_2 = frappe.db.get_value("Warehouse", {"warehouse_name": "Rack 2"})
+
+ if not frappe.db.exists("UOM", "Bag"):
+ new_uom = frappe.new_doc("UOM")
+ new_uom.uom_name = "Bag"
+ new_uom.save()
+
+ def test_putaway_rules_priority(self):
+ """Test if rule is applied by priority, irrespective of free space."""
+ rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200,
+ uom="Kg")
+ rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=300,
+ uom="Kg", priority=2)
+
+ pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1,
+ do_not_submit=1)
+ self.assertEqual(len(pr.items), 2)
+ self.assertEqual(pr.items[0].qty, 200)
+ self.assertEqual(pr.items[0].warehouse, self.warehouse_1)
+ self.assertEqual(pr.items[1].qty, 100)
+ self.assertEqual(pr.items[1].warehouse, self.warehouse_2)
+
+ pr.delete()
+ rule_1.delete()
+ rule_2.delete()
+
+ def test_putaway_rules_with_same_priority(self):
+ """Test if rule with more free space is applied,
+ among two rules with same priority and capacity."""
+ rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=500,
+ uom="Kg")
+ rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500,
+ uom="Kg")
+
+ # out of 500 kg capacity, occupy 100 kg in warehouse_1
+ stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=100, basic_rate=50)
+
+ pr = make_purchase_receipt(item_code="_Rice", qty=700, apply_putaway_rule=1,
+ do_not_submit=1)
+ self.assertEqual(len(pr.items), 2)
+ self.assertEqual(pr.items[0].qty, 500)
+ # warehouse_2 has 500 kg free space, it is given priority
+ self.assertEqual(pr.items[0].warehouse, self.warehouse_2)
+ self.assertEqual(pr.items[1].qty, 200)
+ # warehouse_1 has 400 kg free space, it is given less priority
+ self.assertEqual(pr.items[1].warehouse, self.warehouse_1)
+
+ stock_receipt.cancel()
+ pr.delete()
+ rule_1.delete()
+ rule_2.delete()
+
+ def test_putaway_rules_with_insufficient_capacity(self):
+ """Test if qty exceeding capacity, is handled."""
+ rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=100,
+ uom="Kg")
+ rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=200,
+ uom="Kg")
+
+ pr = make_purchase_receipt(item_code="_Rice", qty=350, apply_putaway_rule=1,
+ do_not_submit=1)
+ self.assertEqual(len(pr.items), 2)
+ self.assertEqual(pr.items[0].qty, 200)
+ self.assertEqual(pr.items[0].warehouse, self.warehouse_2)
+ self.assertEqual(pr.items[1].qty, 100)
+ self.assertEqual(pr.items[1].warehouse, self.warehouse_1)
+ # total 300 assigned, 50 unassigned
+
+ pr.delete()
+ rule_1.delete()
+ rule_2.delete()
+
+ def test_putaway_rules_multi_uom(self):
+ """Test rules applied on uom other than stock uom."""
+ item = frappe.get_doc("Item", "_Rice")
+ if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}):
+ item.append("uoms", {
+ "uom": "Bag",
+ "conversion_factor": 1000
+ })
+ item.save()
+
+ rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=3,
+ uom="Bag")
+ self.assertEqual(rule_1.stock_capacity, 3000)
+ rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=4,
+ uom="Bag")
+ self.assertEqual(rule_2.stock_capacity, 4000)
+
+ # populate 'Rack 1' with 1 Bag, making the free space 2 Bags
+ stock_receipt = make_stock_entry(item_code="_Rice", target=self.warehouse_1, qty=1000, basic_rate=50)
+
+ pr = make_purchase_receipt(item_code="_Rice", qty=6, uom="Bag", stock_uom="Kg",
+ conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1)
+ self.assertEqual(len(pr.items), 2)
+ self.assertEqual(pr.items[0].qty, 4)
+ self.assertEqual(pr.items[0].warehouse, self.warehouse_2)
+ self.assertEqual(pr.items[1].qty, 2)
+ self.assertEqual(pr.items[1].warehouse, self.warehouse_1)
+
+ stock_receipt.cancel()
+ pr.delete()
+ rule_1.delete()
+ rule_2.delete()
+
+ def test_putaway_rules_multi_uom_whole_uom(self):
+ """Test if whole UOMs are handled."""
+ item = frappe.get_doc("Item", "_Rice")
+ if not frappe.db.get_value("UOM Conversion Detail", {"parent": "_Rice", "uom": "Bag"}):
+ item.append("uoms", {
+ "uom": "Bag",
+ "conversion_factor": 1000
+ })
+ item.save()
+
+ frappe.db.set_value("UOM", "Bag", "must_be_whole_number", 1)
+
+ # Putaway Rule in different UOM
+ rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=1,
+ uom="Bag")
+ self.assertEqual(rule_1.stock_capacity, 1000)
+ # Putaway Rule in Stock UOM
+ rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=500)
+ self.assertEqual(rule_2.stock_capacity, 500)
+ # total capacity is 1500 Kg
+
+ pr = make_purchase_receipt(item_code="_Rice", qty=2, uom="Bag", stock_uom="Kg",
+ conversion_factor=1000, apply_putaway_rule=1, do_not_submit=1)
+ self.assertEqual(len(pr.items), 1)
+ self.assertEqual(pr.items[0].qty, 1)
+ self.assertEqual(pr.items[0].warehouse, self.warehouse_1)
+ # leftover space was for 500 kg (0.5 Bag)
+ # Since Bag is a whole UOM, 1(out of 2) Bag will be unassigned
+
+ pr.delete()
+ rule_1.delete()
+ rule_2.delete()
+
+ def test_putaway_rules_with_reoccurring_item(self):
+ """Test rules on same item entered multiple times with different rate."""
+ rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200,
+ uom="Kg")
+ # total capacity is 200 Kg
+
+ pr = make_purchase_receipt(item_code="_Rice", qty=100, apply_putaway_rule=1,
+ do_not_submit=1)
+ pr.append("items", {
+ "item_code": "_Rice",
+ "warehouse": "_Test Warehouse - _TC",
+ "qty": 200,
+ "uom": "Kg",
+ "stock_uom": "Kg",
+ "stock_qty": 200,
+ "received_qty": 200,
+ "rate": 100,
+ "conversion_factor": 1.0,
+ }) # same item entered again in PR but with different rate
+ pr.save()
+ self.assertEqual(len(pr.items), 2)
+ self.assertEqual(pr.items[0].qty, 100)
+ self.assertEqual(pr.items[0].warehouse, self.warehouse_1)
+ self.assertEqual(pr.items[0].putaway_rule, rule_1.name)
+ # same rule applied to second item row
+ # with previous assignment considered
+ self.assertEqual(pr.items[1].qty, 100) # 100 unassigned in second row from 200
+ self.assertEqual(pr.items[1].warehouse, self.warehouse_1)
+ self.assertEqual(pr.items[1].putaway_rule, rule_1.name)
+
+ pr.delete()
+ rule_1.delete()
+
+ def test_validate_over_receipt_in_warehouse(self):
+ """Test if overreceipt is blocked in the presence of putaway rules."""
+ rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200,
+ uom="Kg")
+
+ pr = make_purchase_receipt(item_code="_Rice", qty=300, apply_putaway_rule=1,
+ do_not_submit=1)
+ self.assertEqual(len(pr.items), 1)
+ self.assertEqual(pr.items[0].qty, 200) # 100 is unassigned fro 300 Kg
+ self.assertEqual(pr.items[0].warehouse, self.warehouse_1)
+ self.assertEqual(pr.items[0].putaway_rule, rule_1.name)
+
+ # force overreceipt and disable apply putaway rule in PR
+ pr.items[0].qty = 300
+ pr.items[0].stock_qty = 300
+ pr.apply_putaway_rule = 0
+ self.assertRaises(frappe.ValidationError, pr.save)
+
+ pr.delete()
+ rule_1.delete()
+
+ def test_putaway_rule_on_stock_entry_material_transfer(self):
+ """Test if source warehouse is considered while applying rules."""
+ rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200,
+ uom="Kg") # higher priority
+ rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100,
+ uom="Kg", priority=2)
+
+ stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_1, qty=200,
+ target="_Test Warehouse - _TC", purpose="Material Transfer",
+ apply_putaway_rule=1, do_not_submit=1)
+
+ stock_entry_item = stock_entry.get("items")[0]
+
+ # since source warehouse is Rack 1, rule 1 (for Rack 1) will be avoided
+ # even though it has more free space and higher priority
+ self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_2)
+ self.assertEqual(stock_entry_item.qty, 100) # unassigned 100 out of 200 Kg
+ self.assertEqual(stock_entry_item.putaway_rule, rule_2.name)
+
+ stock_entry.delete()
+ rule_1.delete()
+ rule_2.delete()
+
+ def test_putaway_rule_on_stock_entry_material_transfer_reoccuring_item(self):
+ """Test if reoccuring item is correctly considered."""
+ rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=300,
+ uom="Kg")
+ rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=600,
+ uom="Kg", priority=2)
+
+ # create SE with first row having source warehouse as Rack 2
+ stock_entry = make_stock_entry(item_code="_Rice", source=self.warehouse_2, qty=200,
+ target="_Test Warehouse - _TC", purpose="Material Transfer",
+ apply_putaway_rule=1, do_not_submit=1)
+
+ # Add rows with source warehouse as Rack 1
+ stock_entry.extend("items", [
+ {
+ "item_code": "_Rice",
+ "s_warehouse": self.warehouse_1,
+ "t_warehouse": "_Test Warehouse - _TC",
+ "qty": 100,
+ "basic_rate": 50,
+ "conversion_factor": 1.0,
+ "transfer_qty": 100
+ },
+ {
+ "item_code": "_Rice",
+ "s_warehouse": self.warehouse_1,
+ "t_warehouse": "_Test Warehouse - _TC",
+ "qty": 200,
+ "basic_rate": 60,
+ "conversion_factor": 1.0,
+ "transfer_qty": 200
+ }
+ ])
+
+ stock_entry.save()
+
+ # since source warehouse was Rack 2, exclude rule_2
+ self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1)
+ self.assertEqual(stock_entry.items[0].qty, 200)
+ self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name)
+
+ # since source warehouse was Rack 1, exclude rule_1 even though it has
+ # higher priority
+ self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2)
+ self.assertEqual(stock_entry.items[1].qty, 100)
+ self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name)
+
+ self.assertEqual(stock_entry.items[2].t_warehouse, self.warehouse_2)
+ self.assertEqual(stock_entry.items[2].qty, 200)
+ self.assertEqual(stock_entry.items[2].putaway_rule, rule_2.name)
+
+ stock_entry.delete()
+ rule_1.delete()
+ rule_2.delete()
+
+ def test_putaway_rule_on_stock_entry_material_transfer_batch_serial_item(self):
+ """Test if batch and serial items are split correctly."""
+ if not frappe.db.exists("Item", "Water Bottle"):
+ make_item("Water Bottle", {
+ "is_stock_item": 1,
+ "has_batch_no" : 1,
+ "create_new_batch": 1,
+ "has_serial_no": 1,
+ "serial_no_series": "BOTTL-.####",
+ "stock_uom": "Nos"
+ })
+
+ rule_1 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_1, capacity=3,
+ uom="Nos")
+ rule_2 = create_putaway_rule(item_code="Water Bottle", warehouse=self.warehouse_2, capacity=2,
+ uom="Nos")
+
+ make_new_batch(batch_id="BOTTL-BATCH-1", item_code="Water Bottle")
+
+ pr = make_purchase_receipt(item_code="Water Bottle", qty=5, do_not_submit=1)
+ pr.items[0].batch_no = "BOTTL-BATCH-1"
+ pr.save()
+ pr.submit()
+
+ serial_nos = frappe.get_list("Serial No", filters={"purchase_document_no": pr.name, "status": "Active"})
+ serial_nos = [d.name for d in serial_nos]
+
+ stock_entry = make_stock_entry(item_code="Water Bottle", source="_Test Warehouse - _TC", qty=5,
+ target="Finished Goods - _TC", purpose="Material Transfer",
+ apply_putaway_rule=1, do_not_save=1)
+ stock_entry.items[0].batch_no = "BOTTL-BATCH-1"
+ stock_entry.items[0].serial_no = "\n".join(serial_nos)
+ stock_entry.save()
+
+ self.assertEqual(stock_entry.items[0].t_warehouse, self.warehouse_1)
+ self.assertEqual(stock_entry.items[0].qty, 3)
+ self.assertEqual(stock_entry.items[0].putaway_rule, rule_1.name)
+ self.assertEqual(stock_entry.items[0].serial_no, "\n".join(serial_nos[:3]))
+ self.assertEqual(stock_entry.items[0].batch_no, "BOTTL-BATCH-1")
+
+ self.assertEqual(stock_entry.items[1].t_warehouse, self.warehouse_2)
+ self.assertEqual(stock_entry.items[1].qty, 2)
+ self.assertEqual(stock_entry.items[1].putaway_rule, rule_2.name)
+ self.assertEqual(stock_entry.items[1].serial_no, "\n".join(serial_nos[3:]))
+ self.assertEqual(stock_entry.items[1].batch_no, "BOTTL-BATCH-1")
+
+ stock_entry.delete()
+ pr.cancel()
+ rule_1.delete()
+ rule_2.delete()
+
+ def test_putaway_rule_on_stock_entry_material_receipt(self):
+ """Test if rules are applied in Stock Entry of type Receipt."""
+ rule_1 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_1, capacity=200,
+ uom="Kg") # more capacity
+ rule_2 = create_putaway_rule(item_code="_Rice", warehouse=self.warehouse_2, capacity=100,
+ uom="Kg")
+
+ stock_entry = make_stock_entry(item_code="_Rice", qty=100,
+ target="_Test Warehouse - _TC", purpose="Material Receipt",
+ apply_putaway_rule=1, do_not_submit=1)
+
+ stock_entry_item = stock_entry.get("items")[0]
+
+ self.assertEqual(stock_entry_item.t_warehouse, self.warehouse_1)
+ self.assertEqual(stock_entry_item.qty, 100)
+ self.assertEqual(stock_entry_item.putaway_rule, rule_1.name)
+
+ stock_entry.delete()
+ rule_1.delete()
+ rule_2.delete()
+
+def create_putaway_rule(**args):
+ args = frappe._dict(args)
+ putaway = frappe.new_doc("Putaway Rule")
+
+ putaway.disable = args.disable or 0
+ putaway.company = args.company or "_Test Company"
+ putaway.item_code = args.item or args.item_code or "_Test Item"
+ putaway.warehouse = args.warehouse
+ putaway.priority = args.priority or 1
+ putaway.capacity = args.capacity or 1
+ putaway.stock_uom = frappe.db.get_value("Item", putaway.item_code, "stock_uom")
+ putaway.uom = args.uom or putaway.stock_uom
+ putaway.conversion_factor = get_conversion_factor(putaway.item_code, putaway.uom)['conversion_factor']
+
+ if not args.do_not_save:
+ putaway.save()
+
+ return putaway
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.js b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
index 376848a..f7565fd 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.js
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.js
@@ -4,6 +4,60 @@
cur_frm.cscript.refresh = cur_frm.cscript.inspection_type;
frappe.ui.form.on("Quality Inspection", {
+
+ setup: function(frm) {
+ frm.set_query("batch_no", function() {
+ return {
+ filters: {
+ "item": frm.doc.item_code
+ }
+ };
+ });
+
+ // Serial No based on item_code
+ frm.set_query("item_serial_no", function() {
+ let filters = {};
+ if (frm.doc.item_code) {
+ filters = {
+ 'item_code': frm.doc.item_code
+ };
+ }
+ return { filters: filters };
+ });
+
+ // item code based on GRN/DN
+ frm.set_query("item_code", function(doc) {
+ let doctype = doc.reference_type;
+
+ if (doc.reference_type !== "Job Card") {
+ doctype = (doc.reference_type == "Stock Entry") ?
+ "Stock Entry Detail" : doc.reference_type + " Item";
+ }
+
+ if (doc.reference_type && doc.reference_name) {
+ let filters = {
+ "from": doctype,
+ "inspection_type": doc.inspection_type
+ };
+
+ if (doc.reference_type == doctype)
+ filters["reference_name"] = doc.reference_name;
+ else
+ filters["parent"] = doc.reference_name;
+
+ return {
+ query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
+ filters: filters
+ };
+ }
+ });
+ },
+
+ refresh: function(frm) {
+ // Ignore cancellation of reference doctype on cancel all.
+ frm.ignore_doctypes_on_cancel_all = [frm.doc.reference_type];
+ },
+
item_code: function(frm) {
if (frm.doc.item_code) {
return frm.call({
@@ -26,55 +80,5 @@
}
});
}
- }
-})
-
-// item code based on GRN/DN
-cur_frm.fields_dict['item_code'].get_query = function(doc, cdt, cdn) {
- let doctype = doc.reference_type;
-
- if (doc.reference_type !== "Job Card") {
- doctype = (doc.reference_type == "Stock Entry") ?
- "Stock Entry Detail" : doc.reference_type + " Item";
- }
-
- if (doc.reference_type && doc.reference_name) {
- let filters = {
- "from": doctype,
- "inspection_type": doc.inspection_type
- };
-
- if (doc.reference_type == doctype)
- filters["reference_name"] = doc.reference_name;
- else
- filters["parent"] = doc.reference_name;
-
- return {
- query: "erpnext.stock.doctype.quality_inspection.quality_inspection.item_query",
- filters: filters
- };
- }
-},
-
-// Serial No based on item_code
-cur_frm.fields_dict['item_serial_no'].get_query = function(doc, cdt, cdn) {
- var filters = {};
- if (doc.item_code) {
- filters = {
- 'item_code': doc.item_code
- }
- }
- return { filters: filters }
-}
-
-cur_frm.set_query("batch_no", function(doc) {
- return {
- filters: {
- "item": doc.item_code
- }
- }
-})
-
-cur_frm.add_fetch('item_code', 'item_name', 'item_name');
-cur_frm.add_fetch('item_code', 'description', 'description');
-
+ },
+});
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.json b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
index f6d7619..edfe7e9 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.json
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.json
@@ -136,6 +136,7 @@
"width": "50%"
},
{
+ "fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"in_global_search": 1,
@@ -143,6 +144,7 @@
"read_only": 1
},
{
+ "fetch_from": "item_code.description",
"fieldname": "description",
"fieldtype": "Small Text",
"label": "Description",
@@ -236,7 +238,7 @@
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-11-19 17:06:05.409963",
+ "modified": "2020-12-18 19:59:55.710300",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection",
diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
index ae4eb9b..58b1eca 100644
--- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py
@@ -6,7 +6,7 @@
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe import _
-from frappe.utils import flt
+from frappe.utils import flt, cint
from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \
import get_template_details
@@ -16,7 +16,7 @@
self.get_item_specification_details()
if self.readings:
- self.set_status_based_on_acceptance_formula()
+ self.inspect_and_set_status()
def get_item_specification_details(self):
if not self.quality_inspection_template:
@@ -29,9 +29,7 @@
parameters = get_template_details(self.quality_inspection_template)
for d in parameters:
child = self.append('readings', {})
- child.specification = d.specification
- child.value = d.value
- child.acceptance_formula = d.acceptance_formula
+ child.update(d)
child.status = "Accepted"
def get_quality_inspection_template(self):
@@ -69,35 +67,98 @@
doctype = 'Stock Entry Detail'
if self.reference_type and self.reference_name:
+ conditions = ""
+ if self.batch_no and self.docstatus == 1:
+ conditions += " and t1.batch_no = '%s'"%(self.batch_no)
+
+ if self.docstatus == 2: # if cancel, then remove qi link wherever same name
+ conditions += " and t1.quality_inspection = '%s'"%(self.name)
+
frappe.db.sql("""
- UPDATE `tab{child_doc}` t1, `tab{parent_doc}` t2
- SET t1.quality_inspection = %s, t2.modified = %s
- WHERE t1.parent = %s and t1.item_code = %s and t1.parent = t2.name
- """.format(parent_doc=self.reference_type, child_doc=doctype),
+ UPDATE
+ `tab{child_doc}` t1, `tab{parent_doc}` t2
+ SET
+ t1.quality_inspection = %s, t2.modified = %s
+ WHERE
+ t1.parent = %s
+ and t1.item_code = %s
+ and t1.parent = t2.name
+ {conditions}
+ """.format(parent_doc=self.reference_type, child_doc=doctype, conditions=conditions),
(quality_inspection, self.modified, self.reference_name, self.item_code))
- def set_status_based_on_acceptance_formula(self):
+ def inspect_and_set_status(self):
for reading in self.readings:
- if not reading.acceptance_formula: continue
+ if not reading.manual_inspection: # dont auto set status if manual
+ if reading.formula_based_criteria:
+ self.set_status_based_on_acceptance_formula(reading)
+ else:
+ # if not formula based check acceptance values set
+ self.set_status_based_on_acceptance_values(reading)
- condition = reading.acceptance_formula
- data = {}
+ def set_status_based_on_acceptance_values(self, reading):
+ if not cint(reading.numeric):
+ result = reading.get("reading_value") == reading.get("value")
+ else:
+ # numeric readings
+ result = self.min_max_criteria_passed(reading)
+
+ reading.status = "Accepted" if result else "Rejected"
+
+ def min_max_criteria_passed(self, reading):
+ """Determine whether all readings fall in the acceptable range."""
+ for i in range(1, 11):
+ reading_value = reading.get("reading_" + str(i))
+ if reading_value is not None and reading_value.strip():
+ result = flt(reading.get("min_value")) <= flt(reading_value) <= flt(reading.get("max_value"))
+ if not result: return False
+ return True
+
+ def set_status_based_on_acceptance_formula(self, reading):
+ if not reading.acceptance_formula:
+ frappe.throw(_("Row #{0}: Acceptance Criteria Formula is required.").format(reading.idx),
+ title=_("Missing Formula"))
+
+ condition = reading.acceptance_formula
+ data = self.get_formula_evaluation_data(reading)
+
+ try:
+ result = frappe.safe_eval(condition, None, data)
+ reading.status = "Accepted" if result else "Rejected"
+ except NameError as e:
+ field = frappe.bold(e.args[0].split()[1])
+ frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
+ .format(reading.idx, field),
+ title=_("Invalid Formula"))
+ except Exception:
+ frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
+ title=_("Invalid Formula"))
+
+ def get_formula_evaluation_data(self, reading):
+ data = {}
+ if not cint(reading.numeric):
+ data = {"reading_value": reading.get("reading_value")}
+ else:
+ # numeric readings
for i in range(1, 11):
field = "reading_" + str(i)
- data[field] = flt(reading.get(field)) or 0
+ data[field] = flt(reading.get(field))
+ data["mean"] = self.calculate_mean(reading)
- try:
- result = frappe.safe_eval(condition, None, data)
- reading.status = "Accepted" if result else "Rejected"
- except SyntaxError:
- frappe.throw(_("Row #{0}: Acceptance Criteria Formula is incorrect.").format(reading.idx),
- title=_("Invalid Formula"))
- except NameError as e:
- field = frappe.bold(e.args[0].split()[1])
- frappe.throw(_("Row #{0}: {1} is not a valid reading field. Please refer to the field description.")
- .format(reading.idx, field),
- title=_("Invalid Formula"))
+ return data
+ def calculate_mean(self, reading):
+ """Calculate mean of all non-empty readings."""
+ from statistics import mean
+ readings_list = []
+
+ for i in range(1, 11):
+ reading_value = reading.get("reading_" + str(i))
+ if reading_value is not None and reading_value.strip():
+ readings_list.append(flt(reading_value))
+
+ actual_mean = mean(readings_list) if readings_list else 0
+ return actual_mean
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
diff --git a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
index 2c40009..a7dfc9e 100644
--- a/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
+++ b/erpnext/stock/doctype/quality_inspection/test_quality_inspection.py
@@ -44,24 +44,61 @@
qa.delete()
dn.delete()
+ def test_value_based_qi_readings(self):
+ # Test QI based on acceptance values (Non formula)
+ dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
+ readings = [{
+ "specification": "Iron Content", # numeric reading
+ "min_value": 0.1,
+ "max_value": 0.9,
+ "reading_1": "0.4"
+ },
+ {
+ "specification": "Particle Inspection Needed", # non-numeric reading
+ "numeric": 0,
+ "value": "Yes",
+ "reading_value": "Yes"
+ }]
+
+ qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
+ readings=readings, do_not_save=True)
+ qa.save()
+
+ # status must be auto set as per formula
+ self.assertEqual(qa.readings[0].status, "Accepted")
+ self.assertEqual(qa.readings[1].status, "Accepted")
+
+ qa.delete()
+ dn.delete()
+
def test_formula_based_qi_readings(self):
dn = create_delivery_note(item_code="_Test Item with QA", do_not_submit=True)
readings = [{
- "specification": "Iron Content",
+ "specification": "Iron Content", # numeric reading
+ "formula_based_criteria": 1,
"acceptance_formula": "reading_1 > 0.35 and reading_1 < 0.50",
- "reading_1": 0.4
+ "reading_1": "0.4"
},
{
- "specification": "Calcium Content",
+ "specification": "Calcium Content", # numeric reading
+ "formula_based_criteria": 1,
"acceptance_formula": "reading_1 > 0.20 and reading_1 < 0.50",
- "reading_1": 0.7
+ "reading_1": "0.7"
},
{
- "specification": "Mg Content",
- "acceptance_formula": "(reading_1 + reading_2 + reading_3) / 3 < 0.9",
- "reading_1": 0.5,
- "reading_2": 0.7,
+ "specification": "Mg Content", # numeric reading
+ "formula_based_criteria": 1,
+ "acceptance_formula": "mean < 0.9",
+ "reading_1": "0.5",
+ "reading_2": "0.7",
"reading_3": "random text" # check if random string input causes issues
+ },
+ {
+ "specification": "Calcium Content", # non-numeric reading
+ "formula_based_criteria": 1,
+ "numeric": 0,
+ "acceptance_formula": "reading_value in ('Grade A', 'Grade B', 'Grade C')",
+ "reading_value": "Grade B"
}]
qa = create_quality_inspection(reference_type="Delivery Note", reference_name=dn.name,
@@ -72,6 +109,7 @@
self.assertEqual(qa.readings[0].status, "Accepted")
self.assertEqual(qa.readings[1].status, "Rejected")
self.assertEqual(qa.readings[2].status, "Accepted")
+ self.assertEqual(qa.readings[3].status, "Accepted")
qa.delete()
dn.delete()
@@ -86,11 +124,20 @@
qa.item_code = args.item_code or "_Test Item with QA"
qa.sample_size = 1
qa.inspected_by = frappe.session.user
+ qa.status = args.status or "Accepted"
- readings = args.readings or {"specification": "Size", "status": args.status}
+ if not args.readings:
+ create_quality_inspection_parameter("Size")
+ readings = {"specification": "Size", "min_value": 0, "max_value": 10}
+ else:
+ readings = args.readings
+
+ if args.status == "Rejected":
+ readings["reading_1"] = "12" # status is auto set in child on save
if isinstance(readings, list):
for entry in readings:
+ create_quality_inspection_parameter(entry["specification"])
qa.append("readings", entry)
else:
qa.append("readings", readings)
@@ -101,3 +148,11 @@
qa.submit()
return qa
+
+def create_quality_inspection_parameter(parameter):
+ if not frappe.db.exists("Quality Inspection Parameter", parameter):
+ frappe.get_doc({
+ "doctype": "Quality Inspection Parameter",
+ "parameter": parameter,
+ "description": parameter
+ }).insert()
\ No newline at end of file
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/stock/doctype/quality_inspection_parameter/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/stock/doctype/quality_inspection_parameter/__init__.py
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js
new file mode 100644
index 0000000..47c7e11
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Quality Inspection Parameter', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
new file mode 100644
index 0000000..418b482
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.json
@@ -0,0 +1,96 @@
+{
+ "actions": [],
+ "autoname": "field:parameter",
+ "creation": "2020-12-28 17:06:00.254129",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "parameter",
+ "parameter_group",
+ "description"
+ ],
+ "fields": [
+ {
+ "fieldname": "parameter",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Parameter",
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "description",
+ "fieldtype": "Text Editor",
+ "label": "Description"
+ },
+ {
+ "fieldname": "parameter_group",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "Parameter Group",
+ "options": "Quality Inspection Parameter Group"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-02-19 20:33:30.657406",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Quality Inspection Parameter",
+ "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": "Stock User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Quality Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py
new file mode 100644
index 0000000..8678422
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/quality_inspection_parameter.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class QualityInspectionParameter(Document):
+ pass
diff --git a/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py b/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py
new file mode 100644
index 0000000..cefdc08
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter/test_quality_inspection_parameter.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestQualityInspectionParameter(unittest.TestCase):
+ pass
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/stock/doctype/quality_inspection_parameter_group/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/stock/doctype/quality_inspection_parameter_group/__init__.py
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.js
new file mode 100644
index 0000000..8716a29
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_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('Quality Inspection Parameter Group', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json
new file mode 100644
index 0000000..5726474
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.json
@@ -0,0 +1,82 @@
+{
+ "actions": [],
+ "autoname": "field:group_name",
+ "creation": "2021-02-04 18:44:12.223295",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "group_name"
+ ],
+ "fields": [
+ {
+ "fieldname": "group_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Parameter Group Name",
+ "reqd": 1,
+ "unique": 1
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2021-02-04 18:44:12.223295",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Quality Inspection Parameter 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": "Stock User",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Quality Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Manufacturing User",
+ "share": 1,
+ "write": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py
new file mode 100644
index 0000000..1a3b1a0
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/quality_inspection_parameter_group.py
@@ -0,0 +1,10 @@
+# -*- 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.model.document import Document
+
+class QualityInspectionParameterGroup(Document):
+ pass
diff --git a/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py b/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_group.py
new file mode 100644
index 0000000..212d4b8
--- /dev/null
+++ b/erpnext/stock/doctype/quality_inspection_parameter_group/test_quality_inspection_parameter_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 TestQualityInspectionParameterGroup(unittest.TestCase):
+ pass
diff --git a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
index c1976dd..0eff5a8 100644
--- a/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
+++ b/erpnext/stock/doctype/quality_inspection_reading/quality_inspection_reading.json
@@ -7,21 +7,29 @@
"engine": "InnoDB",
"field_order": [
"specification",
- "value",
+ "parameter_group",
"status",
+ "value",
+ "numeric",
+ "manual_inspection",
"column_break_4",
+ "min_value",
+ "max_value",
+ "formula_based_criteria",
"acceptance_formula",
"section_break_3",
+ "reading_value",
+ "section_break_14",
"reading_1",
"reading_2",
"reading_3",
- "column_break_10",
"reading_4",
+ "column_break_10",
"reading_5",
"reading_6",
- "column_break_14",
"reading_7",
"reading_8",
+ "column_break_14",
"reading_9",
"reading_10"
],
@@ -29,24 +37,25 @@
{
"columns": 3,
"fieldname": "specification",
- "fieldtype": "Data",
+ "fieldtype": "Link",
"in_list_view": 1,
"label": "Parameter",
"oldfieldname": "specification",
"oldfieldtype": "Data",
+ "options": "Quality Inspection Parameter",
"reqd": 1
},
{
"columns": 2,
+ "depends_on": "eval:(!doc.formula_based_criteria && !doc.numeric)",
"fieldname": "value",
"fieldtype": "Data",
- "in_list_view": 1,
- "label": "Acceptance Criteria",
+ "label": "Acceptance Criteria Value",
"oldfieldname": "value",
"oldfieldtype": "Data"
},
{
- "columns": 1,
+ "columns": 2,
"fieldname": "reading_1",
"fieldtype": "Data",
"in_list_view": 1,
@@ -58,7 +67,6 @@
"columns": 1,
"fieldname": "reading_2",
"fieldtype": "Data",
- "in_list_view": 1,
"label": "Reading 2",
"oldfieldname": "reading_2",
"oldfieldtype": "Data"
@@ -67,7 +75,6 @@
"columns": 1,
"fieldname": "reading_3",
"fieldtype": "Data",
- "in_list_view": 1,
"label": "Reading 3",
"oldfieldname": "reading_3",
"oldfieldtype": "Data"
@@ -130,18 +137,21 @@
"label": "Status",
"oldfieldname": "status",
"oldfieldtype": "Select",
- "options": "Accepted\nRejected"
+ "options": "\nAccepted\nRejected"
},
{
+ "depends_on": "eval:!doc.numeric",
"fieldname": "section_break_3",
- "fieldtype": "Section Break"
+ "fieldtype": "Section Break",
+ "label": "Value Based Inspection"
},
{
"fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
- "description": "Simple Python formula based on numeric Readings.<br> Example 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nExample 2: <b>(reading_1 + reading_2) / 2 < 10</b>",
+ "depends_on": "formula_based_criteria",
+ "description": "Simple Python formula applied on Reading fields.<br> Numeric eg. 1: <b>reading_1 > 0.2 and reading_1 < 0.5</b><br>\nNumeric eg. 2: <b>mean > 3.5</b> (mean of populated fields)<br>\nValue based eg.: <b>reading_value in (\"A\", \"B\", \"C\")</b>",
"fieldname": "acceptance_formula",
"fieldtype": "Code",
"label": "Acceptance Criteria Formula"
@@ -153,12 +163,68 @@
{
"fieldname": "column_break_14",
"fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "formula_based_criteria",
+ "fieldtype": "Check",
+ "label": "Formula Based Criteria"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && doc.numeric)",
+ "description": "Applied on each reading.",
+ "fieldname": "min_value",
+ "fieldtype": "Float",
+ "label": "Minimum Value"
+ },
+ {
+ "depends_on": "eval:(!doc.formula_based_criteria && doc.numeric)",
+ "description": "Applied on each reading.",
+ "fieldname": "max_value",
+ "fieldtype": "Float",
+ "label": "Maximum Value"
+ },
+ {
+ "columns": 2,
+ "depends_on": "eval:!doc.numeric",
+ "fieldname": "reading_value",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Reading Value"
+ },
+ {
+ "depends_on": "numeric",
+ "fieldname": "section_break_14",
+ "fieldtype": "Section Break",
+ "label": "Numeric Inspection"
+ },
+ {
+ "default": "0",
+ "description": "Set the status manually.",
+ "fieldname": "manual_inspection",
+ "fieldtype": "Check",
+ "label": "Manual Inspection"
+ },
+ {
+ "default": "1",
+ "fieldname": "numeric",
+ "fieldtype": "Check",
+ "in_list_view": 1,
+ "label": "Numeric"
+ },
+ {
+ "fetch_from": "specification.parameter_group",
+ "fieldname": "parameter_group",
+ "fieldtype": "Link",
+ "label": "Parameter Group",
+ "options": "Quality Inspection Parameter Group",
+ "read_only": 1
}
],
"idx": 1,
"istable": 1,
"links": [],
- "modified": "2020-11-16 16:34:29.947856",
+ "modified": "2021-02-04 19:15:37.991221",
"modified_by": "Administrator",
"module": "Stock",
"name": "Quality Inspection Reading",
diff --git a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
index e284846..01d2031 100644
--- a/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
+++ b/erpnext/stock/doctype/quality_inspection_template/quality_inspection_template.py
@@ -13,6 +13,7 @@
if not template: return []
return frappe.get_all('Item Quality Inspection Parameter',
- fields=["specification", "value", "acceptance_formula"],
+ fields=["specification", "value", "acceptance_formula",
+ "numeric", "formula_based_criteria", "min_value", "max_value"],
filters={'parenttype': 'Quality Inspection Template', 'parent': template},
order_by="idx")
\ No newline at end of file
diff --git a/erpnext/accounts/page/bank_reconciliation/__init__.py b/erpnext/stock/doctype/repost_item_valuation/__init__.py
similarity index 100%
copy from erpnext/accounts/page/bank_reconciliation/__init__.py
copy to erpnext/stock/doctype/repost_item_valuation/__init__.py
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js
new file mode 100644
index 0000000..b3e4286
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.js
@@ -0,0 +1,52 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Repost Item Valuation', {
+ setup: function(frm) {
+ frm.set_query("warehouse", () => {
+ let filters = {
+ 'is_group': 0
+ };
+ if (frm.doc.company) filters['company'] = frm.doc.company;
+ return {filters: filters};
+ });
+
+ frm.set_query("voucher_type", () => {
+ return {
+ filters: {
+ name: ['in', ['Purchase Receipt', 'Purchase Invoice', 'Delivery Note',
+ 'Sales Invoice', 'Stock Entry', 'Stock Reconciliation']]
+ }
+ };
+ });
+
+ if (frm.doc.company) {
+ frm.set_query("voucher_no", () => {
+ return {
+ filters: {
+ company: frm.doc.company
+ }
+ };
+ });
+ }
+ },
+ refresh: function(frm) {
+ if (frm.doc.status == "Failed" && frm.doc.docstatus==1) {
+ frm.add_custom_button(__('Restart'), function () {
+ frm.trigger("restart_reposting");
+ }).addClass("btn-primary");
+ }
+ },
+
+ restart_reposting: function(frm) {
+ frappe.call({
+ method: "restart_reposting",
+ doc: frm.doc,
+ callback: function(r) {
+ if (!r.exc) {
+ frm.refresh();
+ }
+ }
+ });
+ }
+});
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
new file mode 100644
index 0000000..071fc86
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.json
@@ -0,0 +1,215 @@
+{
+ "actions": [],
+ "autoname": "REPOST-ITEM-VAL-.######",
+ "creation": "2020-10-22 22:27:07.742161",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "based_on",
+ "voucher_type",
+ "voucher_no",
+ "item_code",
+ "warehouse",
+ "posting_date",
+ "posting_time",
+ "column_break_5",
+ "status",
+ "company",
+ "allow_negative_stock",
+ "via_landed_cost_voucher",
+ "allow_zero_rate",
+ "amended_from",
+ "error_section",
+ "error_log"
+ ],
+ "fields": [
+ {
+ "depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "fieldname": "item_code",
+ "fieldtype": "Link",
+ "label": "Item Code",
+ "mandatory_depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "options": "Item"
+ },
+ {
+ "depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "fieldname": "warehouse",
+ "fieldtype": "Link",
+ "label": "Warehouse",
+ "mandatory_depends_on": "eval:doc.based_on=='Item and Warehouse'",
+ "options": "Warehouse"
+ },
+ {
+ "fetch_from": "voucher_no.posting_date",
+ "fieldname": "posting_date",
+ "fieldtype": "Date",
+ "label": "Posting Date",
+ "reqd": 1
+ },
+ {
+ "fetch_from": "voucher_no.posting_time",
+ "fieldname": "posting_time",
+ "fieldtype": "Time",
+ "label": "Posting Time"
+ },
+ {
+ "default": "Queued",
+ "fieldname": "status",
+ "fieldtype": "Select",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Status",
+ "no_copy": 1,
+ "options": "Queued\nIn Progress\nCompleted\nFailed",
+ "read_only": 1
+ },
+ {
+ "fieldname": "amended_from",
+ "fieldtype": "Link",
+ "label": "Amended From",
+ "no_copy": 1,
+ "options": "Repost Item Valuation",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "column_break_5",
+ "fieldtype": "Column Break"
+ },
+ {
+ "depends_on": "eval:doc.status=='Failed'",
+ "fieldname": "error_section",
+ "fieldtype": "Section Break",
+ "label": "Error"
+ },
+ {
+ "fieldname": "error_log",
+ "fieldtype": "Long Text",
+ "label": "Error Log",
+ "no_copy": 1,
+ "read_only": 1
+ },
+ {
+ "fetch_from": "warehouse.company",
+ "fieldname": "company",
+ "fieldtype": "Link",
+ "label": "Company",
+ "options": "Company"
+ },
+ {
+ "depends_on": "eval:doc.based_on=='Transaction'",
+ "fieldname": "voucher_type",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Voucher Type",
+ "mandatory_depends_on": "eval:doc.based_on=='Transaction'",
+ "options": "DocType"
+ },
+ {
+ "depends_on": "eval:doc.based_on=='Transaction'",
+ "fieldname": "voucher_no",
+ "fieldtype": "Dynamic Link",
+ "in_list_view": 1,
+ "in_standard_filter": 1,
+ "label": "Voucher No",
+ "mandatory_depends_on": "eval:doc.based_on=='Transaction'",
+ "options": "voucher_type"
+ },
+ {
+ "default": "Transaction",
+ "fieldname": "based_on",
+ "fieldtype": "Select",
+ "label": "Based On",
+ "options": "Transaction\nItem and Warehouse",
+ "reqd": 1
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_negative_stock",
+ "fieldtype": "Check",
+ "label": "Allow Negative Stock"
+ },
+ {
+ "default": "0",
+ "fieldname": "via_landed_cost_voucher",
+ "fieldtype": "Check",
+ "label": "Via Landed Cost Voucher"
+ },
+ {
+ "default": "0",
+ "fieldname": "allow_zero_rate",
+ "fieldtype": "Check",
+ "label": "Allow Zero Rate"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "is_submittable": 1,
+ "links": [],
+ "modified": "2020-12-10 07:52:12.476589",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Repost Item Valuation",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Stock Manager",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ },
+ {
+ "cancel": 1,
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Accounts User",
+ "share": 1,
+ "submit": 1,
+ "write": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC"
+}
\ No newline at end of file
diff --git a/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
new file mode 100644
index 0000000..8436acb
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/repost_item_valuation.py
@@ -0,0 +1,115 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe, erpnext
+from frappe.model.document import Document
+from frappe.utils import cint, get_link_to_form
+from erpnext.stock.stock_ledger import repost_future_sle
+from erpnext.accounts.utils import update_gl_entries_after, check_if_stock_and_account_balance_synced
+from frappe.utils.user import get_users_with_role
+from frappe import _
+class RepostItemValuation(Document):
+ def validate(self):
+ self.set_status()
+ self.reset_field_values()
+ self.set_company()
+
+ def reset_field_values(self):
+ if self.based_on == 'Transaction':
+ self.item_code = None
+ self.warehouse = None
+ else:
+ self.voucher_type = None
+ self.voucher_no = None
+
+ def set_company(self):
+ if self.voucher_type and self.voucher_no:
+ self.company = frappe.get_cached_value(self.voucher_type, self.voucher_no, "company")
+ elif self.warehouse:
+ self.company = frappe.get_cached_value("Warehouse", self.warehouse, "company")
+
+ def set_status(self, status=None):
+ if not status:
+ status = 'Queued'
+ self.db_set('status', status)
+
+ def on_submit(self):
+ frappe.enqueue(repost, timeout=1800, queue='long',
+ job_name='repost_sle', now=frappe.flags.in_test, doc=self)
+
+ def restart_reposting(self):
+ self.set_status('Queued')
+ frappe.enqueue(repost, timeout=1800, queue='long',
+ job_name='repost_sle', now=True, doc=self)
+
+def repost(doc):
+ try:
+ if not frappe.db.exists("Repost Item Valuation", doc.name):
+ return
+
+ doc.set_status('In Progress')
+ frappe.db.commit()
+
+ repost_sl_entries(doc)
+ repost_gl_entries(doc)
+ check_if_stock_and_account_balance_synced(doc.posting_date, doc.company)
+
+ doc.set_status('Completed')
+ except Exception:
+ frappe.db.rollback()
+ traceback = frappe.get_traceback()
+ frappe.log_error(traceback)
+
+ message = frappe.message_log.pop()
+ if traceback:
+ message += "<br>" + "Traceback: <br>" + traceback
+ frappe.db.set_value(doc.doctype, doc.name, 'error_log', message)
+
+ notify_error_to_stock_managers(doc, message)
+ doc.set_status('Failed')
+ raise
+ finally:
+ frappe.db.commit()
+
+def repost_sl_entries(doc):
+ if doc.based_on == 'Transaction':
+ repost_future_sle(voucher_type=doc.voucher_type, voucher_no=doc.voucher_no,
+ allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher)
+ else:
+ repost_future_sle(args=[frappe._dict({
+ "item_code": doc.item_code,
+ "warehouse": doc.warehouse,
+ "posting_date": doc.posting_date,
+ "posting_time": doc.posting_time
+ })], allow_negative_stock=doc.allow_negative_stock, via_landed_cost_voucher=doc.via_landed_cost_voucher)
+
+def repost_gl_entries(doc):
+ if not cint(erpnext.is_perpetual_inventory_enabled(doc.company)):
+ return
+
+ if doc.based_on == 'Transaction':
+ ref_doc = frappe.get_doc(doc.voucher_type, doc.voucher_no)
+ items, warehouses = ref_doc.get_items_and_warehouses()
+ else:
+ items = [doc.item_code]
+ warehouses = [doc.warehouse]
+
+ update_gl_entries_after(doc.posting_date, doc.posting_time,
+ warehouses, items, company=doc.company)
+
+def notify_error_to_stock_managers(doc, traceback):
+ recipients = get_users_with_role("Stock Manager")
+ if not recipients:
+ get_users_with_role("System Manager")
+
+ subject = _("Error while reposting item valuation")
+ message = (_("Hi,") + "<br>"
+ + _("An error has been appeared while reposting item valuation via {0}")
+ .format(get_link_to_form(doc.doctype, doc.name)) + "<br>"
+ + _("Please check the error message and take necessary actions to fix the error and then restart the reposting again.")
+ )
+ frappe.sendmail(recipients=recipients, subject=subject, message=message)
+
+
diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
new file mode 100644
index 0000000..13ceb68
--- /dev/null
+++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestRepostItemValuation(unittest.TestCase):
+ pass
diff --git a/erpnext/stock/doctype/serial_no/serial_no.py b/erpnext/stock/doctype/serial_no/serial_no.py
index 295149e..6bacf1f 100644
--- a/erpnext/stock/doctype/serial_no/serial_no.py
+++ b/erpnext/stock/doctype/serial_no/serial_no.py
@@ -6,7 +6,7 @@
import json
from frappe.model.naming import make_autoname
-from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate
+from frappe.utils import cint, cstr, flt, add_days, nowdate, getdate, get_link_to_form
from erpnext.stock.get_item_details import get_reserved_qty_for_so
from frappe import _, ValidationError
@@ -134,17 +134,13 @@
sle_dict = self.get_stock_ledger_entries(serial_no)
if sle_dict:
if sle_dict.get("incoming", []):
- sle_list = [sle for sle in sle_dict["incoming"] if sle.is_cancelled == 0]
- if sle_list:
- entries["purchase_sle"] = sle_list[0]
+ entries["purchase_sle"] = sle_dict["incoming"][0]
if len(sle_dict.get("incoming", [])) - len(sle_dict.get("outgoing", [])) > 0:
entries["last_sle"] = sle_dict["incoming"][0]
else:
entries["last_sle"] = sle_dict["outgoing"][0]
- sle_list = [sle for sle in sle_dict["outgoing"] if sle.is_cancelled == 0]
- if sle_list:
- entries["delivery_sle"] = sle_list[0]
+ entries["delivery_sle"] = sle_dict["outgoing"][0]
return entries
@@ -155,11 +151,12 @@
for sle in frappe.db.sql("""
SELECT voucher_type, voucher_no,
- posting_date, posting_time, incoming_rate, actual_qty, serial_no, is_cancelled
+ posting_date, posting_time, incoming_rate, actual_qty, serial_no
FROM
`tabStock Ledger Entry`
WHERE
item_code=%s AND company = %s
+ AND is_cancelled = 0
AND (serial_no = %s
OR serial_no like %s
OR serial_no like %s
@@ -179,7 +176,7 @@
def on_trash(self):
sl_entries = frappe.db.sql("""select serial_no from `tabStock Ledger Entry`
- where serial_no like %s and item_code=%s""",
+ where serial_no like %s and item_code=%s and is_cancelled=0""",
("%%%s%%" % self.name, self.item_code), as_dict=True)
# Find the exact match
@@ -229,7 +226,7 @@
if serial_nos:
frappe.throw(_("Item {0} is not setup for Serial Nos. Column must be blank").format(sle.item_code),
SerialNoNotRequiredError)
- else:
+ elif not sle.is_cancelled:
if serial_nos:
if cint(sle.actual_qty) != flt(sle.actual_qty):
frappe.throw(_("Serial No {0} quantity {1} cannot be a fraction").format(sle.item_code, sle.actual_qty))
@@ -244,21 +241,18 @@
for serial_no in serial_nos:
if frappe.db.exists("Serial No", serial_no):
sr = frappe.db.get_value("Serial No", serial_no, ["name", "item_code", "batch_no", "sales_order",
- "delivery_document_no", "delivery_document_type", "warehouse",
+ "delivery_document_no", "delivery_document_type", "warehouse", "purchase_document_type",
"purchase_document_no", "company"], as_dict=1)
- if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse:
- frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}")
- .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse), SerialNoWarehouseError)
-
if sr.item_code!=sle.item_code:
if not allow_serial_nos_with_different_item(serial_no, sle):
frappe.throw(_("Serial No {0} does not belong to Item {1}").format(serial_no,
sle.item_code), SerialNoItemError)
- if cint(sle.actual_qty) > 0 and has_duplicate_serial_no(sr, sle):
- frappe.throw(_("Serial No {0} has already been received").format(serial_no),
- SerialNoDuplicateError)
+ if cint(sle.actual_qty) > 0 and has_serial_no_exists(sr, sle):
+ doc_name = frappe.bold(get_link_to_form(sr.purchase_document_type, sr.purchase_document_no))
+ frappe.throw(_("Serial No {0} has already been received in the {1} #{2}")
+ .format(frappe.bold(serial_no), sr.purchase_document_type, doc_name), SerialNoDuplicateError)
if (sr.delivery_document_no and sle.voucher_type not in ['Stock Entry', 'Stock Reconciliation']
and sle.voucher_type == sr.delivery_document_type):
@@ -277,7 +271,7 @@
frappe.throw(_("Serial No {0} does not belong to Batch {1}").format(serial_no,
sle.batch_no), SerialNoBatchError)
- if not sr.warehouse:
+ if not sle.is_cancelled and not sr.warehouse:
frappe.throw(_("Serial No {0} does not belong to any Warehouse")
.format(serial_no), SerialNoWarehouseError)
@@ -327,6 +321,12 @@
elif cint(sle.actual_qty) < 0 or not item_det.serial_no_series:
frappe.throw(_("Serial Nos Required for Serialized Item {0}").format(sle.item_code),
SerialNoRequiredError)
+ elif serial_nos:
+ for serial_no in serial_nos:
+ sr = frappe.db.get_value("Serial No", serial_no, ["name", "warehouse"], as_dict=1)
+ if sr and cint(sle.actual_qty) < 0 and sr.warehouse != sle.warehouse:
+ frappe.throw(_("Cannot cancel {0} {1} because Serial No {2} does not belong to the warehouse {3}")
+ .format(sle.voucher_type, sle.voucher_no, serial_no, sle.warehouse))
def validate_material_transfer_entry(sle_doc):
sle_doc.update({
@@ -334,7 +334,7 @@
"skip_serial_no_validaiton": False
})
- if (sle_doc.voucher_type == "Stock Entry" and
+ if (sle_doc.voucher_type == "Stock Entry" and not sle_doc.is_cancelled and
frappe.get_cached_value("Stock Entry", sle_doc.voucher_no, "purpose") == "Material Transfer"):
if sle_doc.actual_qty < 0:
sle_doc.skip_update_serial_no = True
@@ -349,7 +349,7 @@
frappe.throw(_("""{0} Serial No {1} cannot be delivered""")
.format(msg, sr.name))
-def has_duplicate_serial_no(sn, sle):
+def has_serial_no_exists(sn, sle):
if (sn.warehouse and not sle.skip_serial_no_validaiton
and sle.voucher_type != 'Stock Reconciliation'):
return True
@@ -359,12 +359,13 @@
status = False
if sn.purchase_document_no:
- if sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and \
- sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]:
+ if (sle.voucher_type in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"] and
+ sn.delivery_document_type not in ['Purchase Receipt', 'Stock Entry', "Purchase Invoice"]):
status = True
- if status and sle.voucher_type == 'Stock Entry' and \
- frappe.db.get_value('Stock Entry', sle.voucher_no, 'purpose') != 'Material Receipt':
+ # If status is receipt then system will allow to in-ward the delivered serial no
+ if (status and sle.voucher_type == "Stock Entry" and frappe.db.get_value("Stock Entry",
+ sle.voucher_no, "purpose") in ("Material Receipt", "Material Transfer")):
status = False
return status
@@ -379,7 +380,7 @@
stock_entry = frappe.get_cached_doc("Stock Entry", sle.voucher_no)
if stock_entry.purpose in ("Repack", "Manufacture"):
for d in stock_entry.get("items"):
- if d.serial_no and (d.s_warehouse or d.t_warehouse):
+ if d.serial_no and (d.s_warehouse if not sle.is_cancelled else d.t_warehouse):
serial_nos = get_serial_nos(d.serial_no)
if sle_serial_no in serial_nos:
allow_serial_nos = True
@@ -388,7 +389,7 @@
def update_serial_nos(sle, item_det):
if sle.skip_update_serial_no: return
- if not sle.serial_no and cint(sle.actual_qty) > 0 \
+ if not sle.is_cancelled and not sle.serial_no and cint(sle.actual_qty) > 0 \
and item_det.has_serial_no == 1 and item_det.serial_no_series:
serial_nos = get_auto_serial_nos(item_det.serial_no_series, sle.actual_qty)
frappe.db.set(sle, "serial_no", serial_nos)
@@ -420,7 +421,7 @@
if is_new:
created_numbers.append(sr.name)
- form_links = list(map(lambda d: frappe.utils.get_link_to_form('Serial No', d), created_numbers))
+ form_links = list(map(lambda d: get_link_to_form('Serial No', d), created_numbers))
# Setting up tranlated title field for all cases
singular_title = _("Serial Number Created")
diff --git a/erpnext/stock/doctype/serial_no/test_serial_no.py b/erpnext/stock/doctype/serial_no/test_serial_no.py
index ab06107..ed70790 100644
--- a/erpnext/stock/doctype/serial_no/test_serial_no.py
+++ b/erpnext/stock/doctype/serial_no/test_serial_no.py
@@ -12,7 +12,6 @@
from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
test_dependencies = ["Item"]
test_records = frappe.get_test_records('Serial No')
@@ -38,8 +37,6 @@
self.assertTrue(SerialNoCannotCannotChangeError, sr.save)
def test_inter_company_transfer(self):
- set_perpetual_inventory(0, "_Test Company 1")
- set_perpetual_inventory(0)
se = make_serialized_item(target_warehouse="_Test Warehouse - _TC")
serial_nos = get_serial_nos(se.get("items")[0].serial_no)
diff --git a/erpnext/stock/doctype/shipment/shipment.js b/erpnext/stock/doctype/shipment/shipment.js
index 5ccb7d2..7af16af 100644
--- a/erpnext/stock/doctype/shipment/shipment.js
+++ b/erpnext/stock/doctype/shipment/shipment.js
@@ -150,7 +150,9 @@
frm.set_value('pickup_contact_name', '');
frm.set_value('pickup_contact', '');
}
- frappe.throw(__("Email or Phone/Mobile of the Contact are mandatory to continue.") + "</br>" + __("Please set Email/Phone for the contact") + ` <a href='#Form/Contact/${contact_name}'>${contact_name}</a>`);
+ frappe.throw(__("Email or Phone/Mobile of the Contact are mandatory to continue.")
+ + "</br>" + __("Please set Email/Phone for the contact")
+ + ` <a href='/app/contact/${contact_name}'>${contact_name}</a>`);
}
let contact_display = r.message.contact_display;
if (r.message.contact_email) {
@@ -242,7 +244,9 @@
frm.set_value('pickup_company', '');
frm.set_value('pickup_contact', '');
}
- frappe.throw(__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + "</br>" + __("Please first set Last Name, Email and Phone for the user") + ` <a href="#Form/User/${frappe.session.user}">${frappe.session.user}</a>`);
+ frappe.throw(__("Last Name, Email or Phone/Mobile of the user are mandatory to continue.") + "</br>"
+ + __("Please first set Last Name, Email and Phone for the user")
+ + ` <a href="/app/user/${frappe.session.user}">${frappe.session.user}</a>`);
}
let contact_display = r.full_name;
if (r.email) {
diff --git a/erpnext/stock/doctype/shipment/shipment.json b/erpnext/stock/doctype/shipment/shipment.json
index 37a9cc6..76c331c 100644
--- a/erpnext/stock/doctype/shipment/shipment.json
+++ b/erpnext/stock/doctype/shipment/shipment.json
@@ -345,7 +345,8 @@
"label": "Status",
"no_copy": 1,
"options": "Draft\nSubmitted\nBooked\nCancelled\nCompleted",
- "print_hide": 1
+ "print_hide": 1,
+ "read_only": 1
},
{
"fieldname": "tracking_url",
@@ -430,7 +431,7 @@
],
"is_submittable": 1,
"links": [],
- "modified": "2020-12-02 15:43:44.607039",
+ "modified": "2020-12-25 15:02:34.891976",
"modified_by": "Administrator",
"module": "Stock",
"name": "Shipment",
diff --git a/erpnext/stock/doctype/shipment/shipment.py b/erpnext/stock/doctype/shipment/shipment.py
index de0c243..4697a7b 100644
--- a/erpnext/stock/doctype/shipment/shipment.py
+++ b/erpnext/stock/doctype/shipment/shipment.py
@@ -5,7 +5,7 @@
from __future__ import unicode_literals
import frappe
from frappe import _
-from frappe.utils import flt
+from frappe.utils import flt, get_time
from frappe.model.document import Document
from erpnext.accounts.party import get_party_shipping_address
from frappe.contacts.doctype.contact.contact import get_default_contact
@@ -13,6 +13,7 @@
class Shipment(Document):
def validate(self):
self.validate_weight()
+ self.validate_pickup_time()
self.set_value_of_goods()
if self.docstatus == 0:
self.status = 'Draft'
@@ -32,6 +33,10 @@
if flt(parcel.weight) <= 0:
frappe.throw(_('Parcel weight cannot be 0'))
+ def validate_pickup_time(self):
+ if self.pickup_from and self.pickup_to and get_time(self.pickup_to) < get_time(self.pickup_from):
+ frappe.throw(_("Pickup To time should be greater than Pickup From time"))
+
def set_value_of_goods(self):
value_of_goods = 0
for entry in self.get("shipment_delivery_note"):
diff --git a/erpnext/stock/doctype/shipment/test_shipment.py b/erpnext/stock/doctype/shipment/test_shipment.py
index e1fa207..9c3e22f 100644
--- a/erpnext/stock/doctype/shipment/test_shipment.py
+++ b/erpnext/stock/doctype/shipment/test_shipment.py
@@ -190,6 +190,7 @@
company.abbr = abbr
company.default_currency = 'EUR'
company.country = 'Germany'
+ company.enable_perpetual_inventory = 0
company.insert()
return company
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js
index 9121758..726118d 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.js
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.js
@@ -1,9 +1,20 @@
// Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors // License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.stock");
+frappe.provide("erpnext.accounts.dimensions");
+
+{% include 'erpnext/stock/landed_taxes_and_charges_common.js' %};
frappe.ui.form.on('Stock Entry', {
setup: function(frm) {
+ frm.set_indicator_formatter('item_code', function(doc) {
+ if (!doc.s_warehouse) {
+ return 'blue';
+ } else {
+ return (doc.qty<=doc.actual_qty) ? 'green' : 'orange';
+ }
+ });
+
frm.set_query('work_order', function() {
return {
filters: [
@@ -86,17 +97,9 @@
}
});
- frm.set_query("expense_account", "additional_costs", function() {
- return {
- query: "erpnext.controllers.queries.tax_account_query",
- filters: {
- "account_type": ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"],
- "company": frm.doc.company
- }
- };
- });
frm.add_fetch("bom_no", "inspection_required", "inspection_required");
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
},
setup_quality_inspection: function(frm) {
@@ -312,6 +315,8 @@
frm.set_value("letter_head", company_doc.default_letter_head);
}
frm.trigger("toggle_display_account_head");
+
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
}
},
@@ -510,22 +515,31 @@
calculate_amount: function(frm) {
frm.events.calculate_total_additional_costs(frm);
-
- const total_basic_amount = frappe.utils.sum(
- (frm.doc.items || []).map(function(i) { return i.t_warehouse ? flt(i.basic_amount) : 0; })
- );
+ let total_basic_amount = 0;
+ if (in_list(["Repack", "Manufacture"], frm.doc.purpose)) {
+ total_basic_amount = frappe.utils.sum(
+ (frm.doc.items || []).map(function(i) {
+ return i.is_finished_item ? flt(i.basic_amount) : 0;
+ })
+ );
+ } else {
+ total_basic_amount = frappe.utils.sum(
+ (frm.doc.items || []).map(function(i) {
+ return i.t_warehouse ? flt(i.basic_amount) : 0;
+ })
+ );
+ }
for (let i in frm.doc.items) {
let item = frm.doc.items[i];
- if (item.t_warehouse && total_basic_amount) {
+ if (((in_list(["Repack", "Manufacture"], frm.doc.purpose) && item.is_finished_item) || item.t_warehouse) && total_basic_amount) {
item.additional_cost = (flt(item.basic_amount) / total_basic_amount) * frm.doc.total_additional_costs;
} else {
item.additional_cost = 0;
}
- item.amount = flt(item.basic_amount + flt(item.additional_cost),
- precision("amount", item));
+ item.amount = flt(item.basic_amount + flt(item.additional_cost), precision("amount", item));
if (flt(item.transfer_qty)) {
item.valuation_rate = flt(flt(item.basic_rate) + (flt(item.additional_cost) / flt(item.transfer_qty)),
@@ -538,7 +552,7 @@
calculate_total_additional_costs: function(frm) {
const total_additional_costs = frappe.utils.sum(
- (frm.doc.additional_costs || []).map(function(c) { return flt(c.amount); })
+ (frm.doc.additional_costs || []).map(function(c) { return flt(c.base_amount); })
);
frm.set_value("total_additional_costs",
@@ -571,8 +585,12 @@
}
});
}
+ },
+
+ apply_putaway_rule: function (frm) {
+ if (frm.doc.apply_putaway_rule) erpnext.apply_putaway_rule(frm, frm.doc.purpose);
}
-})
+});
frappe.ui.form.on('Stock Entry Detail', {
qty: function(frm, cdt, cdn) {
@@ -666,7 +684,13 @@
});
refresh_field("items");
- if (!d.serial_no) {
+ let no_batch_serial_number_value = !d.serial_no;
+ if (d.has_batch_no && !d.has_serial_no) {
+ // check only batch_no for batched item
+ no_batch_serial_number_value = !d.batch_no;
+ }
+
+ if (no_batch_serial_number_value) {
erpnext.stock.select_batch_and_serial_no(frm, d);
}
}
@@ -707,8 +731,18 @@
};
frappe.ui.form.on('Landed Cost Taxes and Charges', {
- amount: function(frm) {
- frm.events.calculate_amount(frm);
+ amount: function(frm, cdt, cdn) {
+ frm.events.set_base_amount(frm, cdt, cdn);
+
+ // Adding this check because same table in used in LCV
+ // This causes an error if you try to post an LCV immediately after a Stock Entry
+ if (frm.doc.doctype == 'Stock Entry') {
+ frm.events.calculate_amount(frm);
+ }
+ },
+
+ expense_account: function(frm, cdt, cdn) {
+ frm.events.set_account_currency(frm, cdt, cdn);
}
});
@@ -756,15 +790,6 @@
}
}
- this.frm.set_indicator_formatter('item_code',
- function(doc) {
- if (!doc.s_warehouse) {
- return 'blue';
- } else {
- return (doc.qty<=doc.actual_qty) ? "green" : "orange"
- }
- })
-
this.frm.add_fetch("purchase_order", "supplier", "supplier");
frappe.dynamic_link = { doc: this.frm.doc, fieldname: 'supplier', doctype: 'Supplier' }
@@ -841,6 +866,10 @@
}
},
+ fg_completed_qty: function() {
+ this.get_items();
+ },
+
get_items: function() {
var me = this;
if(!this.frm.doc.fg_completed_qty || !this.frm.doc.bom_no)
@@ -850,6 +879,7 @@
// if work order / bom is mentioned, get items
return this.frm.call({
doc: me.frm.doc,
+ freeze: true,
method: "get_items",
callback: function(r) {
if(!r.exc) refresh_field("items");
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.json b/erpnext/stock/doctype/stock_entry/stock_entry.json
index 61e0df6..98c047a 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.json
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.json
@@ -27,6 +27,7 @@
"set_posting_time",
"inspection_required",
"from_bom",
+ "apply_putaway_rule",
"sb1",
"bom_no",
"fg_completed_qty",
@@ -640,13 +641,21 @@
"fieldtype": "Check",
"label": "Add to Transit",
"no_copy": 1
+ },
+ {
+ "default": "0",
+ "depends_on": "eval:in_list([\"Material Transfer\", \"Material Receipt\"], doc.purpose)",
+ "fieldname": "apply_putaway_rule",
+ "fieldtype": "Check",
+ "label": "Apply Putaway Rule"
}
],
"icon": "fa fa-file-text",
"idx": 1,
+ "index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
- "modified": "2020-08-11 19:10:07.954981",
+ "modified": "2020-12-09 14:58:13.267321",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry",
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py
index e3159b9..9cdc3cf 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry.py
@@ -18,7 +18,8 @@
from frappe.model.mapper import get_mapped_doc
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit, get_serial_nos
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError
-
+from erpnext.accounts.general_ledger import process_gl_map
+from erpnext.controllers.taxes_and_totals import init_landed_taxes_and_totals
import json
from six import string_types, itervalues, iteritems
@@ -42,6 +43,14 @@
for item in self.get("items"):
item.update(get_bin_details(item.item_code, item.s_warehouse))
+ def before_validate(self):
+ from erpnext.stock.doctype.putaway_rule.putaway_rule import apply_putaway_rule
+ apply_rule = self.apply_putaway_rule and (self.purpose in ["Material Transfer", "Material Receipt"])
+
+ if self.get("items") and apply_rule:
+ apply_putaway_rule(self.doctype, self.get("items"), self.company,
+ purpose=self.purpose)
+
def validate(self):
self.pro_doc = frappe._dict()
if self.work_order:
@@ -58,6 +67,7 @@
self.validate_warehouse()
self.validate_work_order()
self.validate_bom()
+ self.mark_finished_and_scrap_items()
self.validate_finished_goods()
self.validate_with_material_request()
self.validate_batch()
@@ -75,13 +85,12 @@
else:
set_batch_nos(self, 's_warehouse')
- self.set_incoming_rate()
self.validate_serialized_batch()
self.set_actual_qty()
- self.calculate_rate_and_amount(update_finished_item_rate=False)
+ self.calculate_rate_and_amount()
+ self.validate_putaway_capacity()
def on_submit(self):
-
self.update_stock_ledger()
update_serial_nos_after_submit(self, "items")
@@ -89,11 +98,15 @@
self.validate_purchase_order()
if self.purchase_order and self.purpose == "Send to Subcontractor":
self.update_purchase_order_supplied_items()
+
self.make_gl_entries()
+
+ self.repost_future_sle_and_gle()
self.update_cost_in_project()
self.validate_reserved_serial_no_consumption()
self.update_transferred_qty()
self.update_quality_inspection()
+
if self.work_order and self.purpose == "Manufacture":
self.update_so_in_serial_number()
@@ -113,13 +126,15 @@
self.update_work_order()
self.update_stock_ledger()
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.make_gl_entries_on_cancel()
+ self.repost_future_sle_and_gle()
self.update_cost_in_project()
self.update_transferred_qty()
self.update_quality_inspection()
self.delete_auto_created_batches()
+ self.delete_linked_stock_entry()
if self.purpose == 'Material Transfer' and self.add_to_transit:
self.set_material_request_transfer_status('Not Started')
@@ -148,10 +163,16 @@
if self.purpose not in valid_purposes:
frappe.throw(_("Purpose must be one of {0}").format(comma_or(valid_purposes)))
- if self.job_card and self.purpose != 'Material Transfer for Manufacture':
+ if self.job_card and self.purpose not in ['Material Transfer for Manufacture', 'Repack']:
frappe.throw(_("For job card {0}, you can only make the 'Material Transfer for Manufacture' type stock entry")
.format(self.job_card))
+ def delete_linked_stock_entry(self):
+ if self.purpose == "Send to Warehouse":
+ for d in frappe.get_all("Stock Entry", filters={"docstatus": 0,
+ "outgoing_stock_entry": self.name, "purpose": "Receive at Warehouse"}):
+ frappe.delete_doc("Stock Entry", d.name)
+
def set_transfer_qty(self):
for item in self.get("items"):
if not flt(item.qty):
@@ -175,7 +196,7 @@
and (sed.t_warehouse is null or sed.t_warehouse = '')""", self.project, as_list=1)
amount = amount[0][0] if amount else 0
- additional_costs = frappe.db.sql(""" select ifnull(sum(sed.amount), 0)
+ additional_costs = frappe.db.sql(""" select ifnull(sum(sed.base_amount), 0)
from
`tabStock Entry` se, `tabLanded Cost Taxes and Charges` sed
where
@@ -248,12 +269,16 @@
item_code.append(item.item_code)
def validate_fg_completed_qty(self):
+ item_wise_qty = {}
if self.purpose == "Manufacture" and self.work_order:
- production_item = frappe.get_value('Work Order', self.work_order, 'production_item')
- for item in self.items:
- if item.item_code == production_item and item.t_warehouse and item.qty != self.fg_completed_qty:
- frappe.throw(_("Finished product quantity <b>{0}</b> and For Quantity <b>{1}</b> cannot be different")
- .format(item.qty, self.fg_completed_qty))
+ for d in self.items:
+ if d.is_finished_item:
+ item_wise_qty.setdefault(d.item_code, []).append(d.qty)
+
+ for item_code, qty_list in iteritems(item_wise_qty):
+ if self.fg_completed_qty != sum(qty_list):
+ frappe.throw(_("The finished product {0} quantity {1} and For Quantity {2} cannot be different")
+ .format(frappe.bold(item_code), frappe.bold(sum(qty_list)), frappe.bold(self.fg_completed_qty)))
def validate_difference_account(self):
if not cint(erpnext.is_perpetual_inventory_enabled(self.company)):
@@ -309,7 +334,7 @@
if self.purpose == "Manufacture":
if validate_for_manufacture:
- if d.bom_no:
+ if d.is_finished_item or d.is_scrap_item:
d.s_warehouse = None
if not d.t_warehouse:
frappe.throw(_("Target warehouse is mandatory for row {0}").format(d.idx))
@@ -375,21 +400,6 @@
frappe.throw(_("Stock Entries already created for Work Order ")
+ self.work_order + ":" + ", ".join(other_ste), DuplicateEntryForWorkOrderError)
- def set_incoming_rate(self):
- if self.purpose == "Repack":
- self.set_basic_rate_for_finished_goods()
-
- for d in self.items:
- if d.s_warehouse:
- args = self.get_args_for_incoming_rate(d)
- d.basic_rate = get_incoming_rate(args)
- elif d.allow_zero_valuation_rate and not d.s_warehouse:
- d.basic_rate = 0.0
- elif d.t_warehouse and not d.basic_rate:
- d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
- self.doctype, self.name, d.allow_zero_valuation_rate,
- currency=erpnext.get_company_currency(self.company), company=self.company)
-
def set_actual_qty(self):
allow_negative_stock = cint(frappe.db.get_value("Stock Settings", None, "allow_negative_stock"))
@@ -425,57 +435,66 @@
d.serial_no = transferred_serial_no
def get_stock_and_rate(self):
+ """
+ Updates rate and availability of all the items.
+ Called from Update Rate and Availability button.
+ """
self.set_work_order_details()
self.set_transfer_qty()
self.set_actual_qty()
self.calculate_rate_and_amount()
- def calculate_rate_and_amount(self, force=False,
- update_finished_item_rate=True, raise_error_if_no_rate=True):
- self.set_basic_rate(force, update_finished_item_rate, raise_error_if_no_rate)
+ def calculate_rate_and_amount(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
+ self.set_basic_rate(reset_outgoing_rate, raise_error_if_no_rate)
+ init_landed_taxes_and_totals(self)
self.distribute_additional_costs()
self.update_valuation_rate()
self.set_total_incoming_outgoing_value()
self.set_total_amount()
- def set_basic_rate(self, force=False, update_finished_item_rate=True, raise_error_if_no_rate=True):
- """get stock and incoming rate on posting date"""
- raw_material_cost = 0.0
- scrap_material_cost = 0.0
- fg_basic_rate = 0.0
+ def set_basic_rate(self, reset_outgoing_rate=True, raise_error_if_no_rate=True):
+ """
+ Set rate for outgoing, scrapped and finished items
+ """
+ # Set rate for outgoing items
+ outgoing_items_cost = self.set_rate_for_outgoing_items(reset_outgoing_rate)
+ finished_item_qty = sum([d.transfer_qty for d in self.items if d.is_finished_item])
+ # Set basic rate for incoming items
for d in self.get('items'):
- if d.t_warehouse: fg_basic_rate = flt(d.basic_rate)
- args = self.get_args_for_incoming_rate(d)
+ if d.s_warehouse or d.set_basic_rate_manually: continue
- # get basic rate
- if not d.bom_no:
- if (not flt(d.basic_rate) and not d.allow_zero_valuation_rate) or d.s_warehouse or force:
- basic_rate = flt(get_incoming_rate(args, raise_error_if_no_rate), self.precision("basic_rate", d))
- if basic_rate > 0:
- d.basic_rate = basic_rate
+ if d.allow_zero_valuation_rate:
+ d.basic_rate = 0.0
+ elif d.is_finished_item:
+ if self.purpose == "Manufacture":
+ d.basic_rate = self.get_basic_rate_for_manufactured_item(finished_item_qty, outgoing_items_cost)
+ elif self.purpose == "Repack":
+ d.basic_rate = self.get_basic_rate_for_repacked_items(d.transfer_qty, outgoing_items_cost)
+
+ if not d.basic_rate and not d.allow_zero_valuation_rate:
+ d.basic_rate = get_valuation_rate(d.item_code, d.t_warehouse,
+ self.doctype, self.name, d.allow_zero_valuation_rate,
+ currency=erpnext.get_company_currency(self.company), company=self.company,
+ raise_error_if_no_rate=raise_error_if_no_rate)
+
+ d.basic_rate = flt(d.basic_rate, d.precision("basic_rate"))
+ d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
+
+ def set_rate_for_outgoing_items(self, reset_outgoing_rate=True):
+ outgoing_items_cost = 0.0
+ for d in self.get('items'):
+ if d.s_warehouse:
+ if reset_outgoing_rate:
+ args = self.get_args_for_incoming_rate(d)
+ rate = get_incoming_rate(args)
+ if rate > 0:
+ d.basic_rate = rate
d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
if not d.t_warehouse:
- raw_material_cost += flt(d.basic_amount)
-
- # get scrap items basic rate
- if d.bom_no:
- if not flt(d.basic_rate) and not d.allow_zero_valuation_rate and \
- getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse:
- basic_rate = flt(get_incoming_rate(args, raise_error_if_no_rate),
- self.precision("basic_rate", d))
- if basic_rate > 0:
- d.basic_rate = basic_rate
- d.basic_amount = flt(flt(d.transfer_qty) * flt(d.basic_rate), d.precision("basic_amount"))
-
- if getattr(self, "pro_doc", frappe._dict()).scrap_warehouse == d.t_warehouse:
-
- scrap_material_cost += flt(d.basic_amount)
-
- number_of_fg_items = len([t.t_warehouse for t in self.get("items") if t.t_warehouse])
- if (fg_basic_rate == 0.0 and number_of_fg_items == 1) or update_finished_item_rate:
- self.set_basic_rate_for_finished_goods(raw_material_cost, scrap_material_cost)
+ outgoing_items_cost += flt(d.basic_amount)
+ return outgoing_items_cost
def get_args_for_incoming_rate(self, item):
return frappe._dict({
@@ -491,44 +510,44 @@
"allow_zero_valuation": item.allow_zero_valuation_rate,
})
- def set_basic_rate_for_finished_goods(self, raw_material_cost=0, scrap_material_cost=0):
- total_fg_qty = 0
- if not raw_material_cost and self.get("items"):
- raw_material_cost = sum([flt(row.basic_amount) for row in self.items
- if row.s_warehouse and not row.t_warehouse])
+ def get_basic_rate_for_repacked_items(self, finished_item_qty, outgoing_items_cost):
+ finished_items = [d.item_code for d in self.get("items") if d.is_finished_item]
+ if len(finished_items) == 1:
+ return flt(outgoing_items_cost / finished_item_qty)
+ else:
+ unique_finished_items = set(finished_items)
+ if len(unique_finished_items) == 1:
+ total_fg_qty = sum([flt(d.transfer_qty) for d in self.items if d.is_finished_item])
+ return flt(outgoing_items_cost / total_fg_qty)
- total_fg_qty = sum([flt(row.qty) for row in self.items
- if row.t_warehouse and not row.s_warehouse])
+ def get_basic_rate_for_manufactured_item(self, finished_item_qty, outgoing_items_cost=0):
+ scrap_items_cost = sum([flt(d.basic_amount) for d in self.get("items") if d.is_scrap_item])
- if self.purpose in ["Manufacture", "Repack"]:
- for d in self.get("items"):
- if (d.transfer_qty and (d.bom_no or d.t_warehouse)
- and (getattr(self, "pro_doc", frappe._dict()).scrap_warehouse != d.t_warehouse)):
+ # Get raw materials cost from BOM if multiple material consumption entries
+ if frappe.db.get_single_value("Manufacturing Settings", "material_consumption"):
+ bom_items = self.get_bom_raw_materials(finished_item_qty)
+ outgoing_items_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
- if (self.work_order and self.purpose == "Manufacture"
- and frappe.db.get_single_value("Manufacturing Settings", "material_consumption")):
- bom_items = self.get_bom_raw_materials(d.transfer_qty)
- raw_material_cost = sum([flt(row.qty)*flt(row.rate) for row in bom_items.values()])
-
- if raw_material_cost and self.purpose == "Manufacture":
- d.basic_rate = flt((raw_material_cost - scrap_material_cost) / flt(d.transfer_qty), d.precision("basic_rate"))
- d.basic_amount = flt((raw_material_cost - scrap_material_cost), d.precision("basic_amount"))
- elif self.purpose == "Repack" and total_fg_qty and not d.set_basic_rate_manually:
- d.basic_rate = flt(raw_material_cost) / flt(total_fg_qty)
- d.basic_amount = d.basic_rate * flt(d.qty)
+ return flt((outgoing_items_cost - scrap_items_cost) / finished_item_qty)
def distribute_additional_costs(self):
- if self.purpose == "Material Issue":
+ # If no incoming items, set additional costs blank
+ if not any([d.item_code for d in self.items if d.t_warehouse]):
self.additional_costs = []
- self.total_additional_costs = sum([flt(t.amount) for t in self.get("additional_costs")])
- total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
+ self.total_additional_costs = sum([flt(t.base_amount) for t in self.get("additional_costs")])
- for d in self.get("items"):
- if d.t_warehouse and total_basic_amount:
- d.additional_cost = (flt(d.basic_amount) / total_basic_amount) * self.total_additional_costs
- else:
- d.additional_cost = 0
+ if self.purpose in ("Repack", "Manufacture"):
+ incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.is_finished_item])
+ else:
+ incoming_items_cost = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse])
+
+ if incoming_items_cost:
+ for d in self.get("items"):
+ if (self.purpose in ("Repack", "Manufacture") and d.is_finished_item) or d.t_warehouse:
+ d.additional_cost = (flt(d.basic_amount) / incoming_items_cost) * self.total_additional_costs
+ else:
+ d.additional_cost = 0
def update_valuation_rate(self):
for d in self.get("items"):
@@ -631,71 +650,115 @@
item_code = d.original_item or d.item_code
validate_bom_no(item_code, d.bom_no)
+ def mark_finished_and_scrap_items(self):
+ if self.purpose in ("Repack", "Manufacture"):
+ if any([d.item_code for d in self.items if (d.is_finished_item and d.t_warehouse)]):
+ return
+
+ finished_item = self.get_finished_item()
+
+ for d in self.items:
+ if d.t_warehouse and not d.s_warehouse:
+ if self.purpose=="Repack" or d.item_code == finished_item:
+ d.is_finished_item = 1
+ else:
+ d.is_scrap_item = 1
+ else:
+ d.is_finished_item = 0
+ d.is_scrap_item = 0
+
+ def get_finished_item(self):
+ finished_item = None
+ if self.work_order:
+ finished_item = frappe.db.get_value("Work Order", self.work_order, "production_item")
+ elif self.bom_no:
+ finished_item = frappe.db.get_value("BOM", self.bom_no, "item")
+
+ return finished_item
+
def validate_finished_goods(self):
"""validation: finished good quantity should be same as manufacturing quantity"""
if not self.work_order: return
- items_with_target_warehouse = []
- allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
- "overproduction_percentage_for_work_order"))
-
production_item, wo_qty = frappe.db.get_value("Work Order",
self.work_order, ["production_item", "qty"])
+ finished_items = []
for d in self.get('items'):
- if (self.purpose != "Send to Subcontractor" and d.bom_no
- and flt(d.transfer_qty) > flt(self.fg_completed_qty) and d.item_code == production_item):
- frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
- format(d.idx, d.transfer_qty, self.fg_completed_qty))
+ if d.is_finished_item:
+ if d.item_code != production_item:
+ frappe.throw(_("Finished Item {0} does not match with Work Order {1}")
+ .format(d.item_code, self.work_order))
+ elif flt(d.transfer_qty) > flt(self.fg_completed_qty):
+ frappe.throw(_("Quantity in row {0} ({1}) must be same as manufactured quantity {2}"). \
+ format(d.idx, d.transfer_qty, self.fg_completed_qty))
+ finished_items.append(d.item_code)
- if self.work_order and self.purpose == "Manufacture" and d.t_warehouse:
- items_with_target_warehouse.append(d.item_code)
+ if len(set(finished_items)) > 1:
+ frappe.throw(_("Multiple items cannot be marked as finished item"))
- if self.work_order and self.purpose == "Manufacture":
+ if self.purpose == "Manufacture":
+ allowance_percentage = flt(frappe.db.get_single_value("Manufacturing Settings",
+ "overproduction_percentage_for_work_order"))
+
allowed_qty = wo_qty + (allowance_percentage/100 * wo_qty)
if self.fg_completed_qty > allowed_qty:
frappe.throw(_("For quantity {0} should not be greater than work order quantity {1}")
.format(flt(self.fg_completed_qty), wo_qty))
- if production_item not in items_with_target_warehouse:
- frappe.throw(_("Finished Item {0} must be entered for Manufacture type entry")
- .format(production_item))
-
def update_stock_ledger(self):
sl_entries = []
+ finished_item_row = self.get_finished_item_row()
- # make sl entries for source warehouse first, then do for target warehouse
- for d in self.get('items'):
- if cstr(d.s_warehouse):
- sl_entries.append(self.get_sl_entries(d, {
- "warehouse": cstr(d.s_warehouse),
- "actual_qty": -flt(d.transfer_qty),
- "incoming_rate": 0
- }))
+ # make sl entries for source warehouse first
+ self.get_sle_for_source_warehouse(sl_entries, finished_item_row)
- for d in self.get('items'):
- if cstr(d.t_warehouse):
- sl_entries.append(self.get_sl_entries(d, {
- "warehouse": cstr(d.t_warehouse),
- "actual_qty": flt(d.transfer_qty),
- "incoming_rate": flt(d.valuation_rate)
- }))
+ # SLE for target warehouse
+ self.get_sle_for_target_warehouse(sl_entries, finished_item_row)
- # On cancellation, make stock ledger entry for
- # target warehouse first, to update serial no values properly
-
- # if cstr(d.s_warehouse) and self.docstatus == 2:
- # sl_entries.append(self.get_sl_entries(d, {
- # "warehouse": cstr(d.s_warehouse),
- # "actual_qty": -flt(d.transfer_qty),
- # "incoming_rate": 0
- # }))
-
+ # reverse sl entries if cancel
if self.docstatus == 2:
sl_entries.reverse()
self.make_sl_entries(sl_entries)
+ def get_finished_item_row(self):
+ finished_item_row = None
+ if self.purpose in ("Manufacture", "Repack"):
+ for d in self.get('items'):
+ if d.is_finished_item:
+ finished_item_row = d
+
+ return finished_item_row
+
+ def get_sle_for_source_warehouse(self, sl_entries, finished_item_row):
+ for d in self.get('items'):
+ if cstr(d.s_warehouse):
+ sle = self.get_sl_entries(d, {
+ "warehouse": cstr(d.s_warehouse),
+ "actual_qty": -flt(d.transfer_qty),
+ "incoming_rate": 0
+ })
+ if cstr(d.t_warehouse):
+ sle.dependant_sle_voucher_detail_no = d.name
+ elif finished_item_row and (finished_item_row.item_code != d.item_code or finished_item_row.t_warehouse != d.s_warehouse):
+ sle.dependant_sle_voucher_detail_no = finished_item_row.name
+
+ sl_entries.append(sle)
+
+ def get_sle_for_target_warehouse(self, sl_entries, finished_item_row):
+ for d in self.get('items'):
+ if cstr(d.t_warehouse):
+ sle = self.get_sl_entries(d, {
+ "warehouse": cstr(d.t_warehouse),
+ "actual_qty": flt(d.transfer_qty),
+ "incoming_rate": flt(d.valuation_rate)
+ })
+ if cstr(d.s_warehouse) or (finished_item_row and d.name == finished_item_row.name):
+ sle.recalculate_rate = 1
+
+ sl_entries.append(sle)
+
def get_gl_entries(self, warehouse_account):
gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account)
@@ -712,13 +775,19 @@
for d in self.get("items"):
if d.t_warehouse:
item_account_wise_additional_cost.setdefault((d.item_code, d.name), {})
- item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0)
+ item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, {
+ "amount": 0.0,
+ "base_amount": 0.0
+ })
multiply_based_on = d.basic_amount if total_basic_amount else d.qty
- item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \
+ item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["amount"] += \
flt(t.amount * multiply_based_on) / divide_based_on
+ item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account]["base_amount"] += \
+ flt(t.base_amount * multiply_based_on) / divide_based_on
+
if item_account_wise_additional_cost:
for d in self.get("items"):
for account, amount in iteritems(item_account_wise_additional_cost.get((d.item_code, d.name), {})):
@@ -729,7 +798,8 @@
"against": d.expense_account,
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": amount
+ "credit_in_account_currency": flt(amount["amount"]),
+ "credit": flt(amount["base_amount"])
}, item=d))
gl_entries.append(self.get_gl_dict({
@@ -737,10 +807,10 @@
"against": account,
"cost_center": d.cost_center,
"remarks": self.get("remarks") or _("Accounting Entry for Stock"),
- "credit": -1 * amount # put it as negative credit instead of debit purposefully
+ "credit": -1 * amount['base_amount'] # put it as negative credit instead of debit purposefully
}, item=d))
- return gl_entries
+ return process_gl_map(gl_entries)
def update_work_order(self):
def _validate_work_order(pro_doc):
@@ -753,6 +823,7 @@
if self.job_card:
job_doc = frappe.get_doc('Job Card', self.job_card)
job_doc.set_transferred_qty(update_status=True)
+ job_doc.set_transferred_qty_in_job_card(self)
if self.work_order:
pro_doc = frappe.get_doc("Work Order", self.work_order)
@@ -989,6 +1060,7 @@
"stock_uom": item.stock_uom,
"expense_account": item.get("expense_account"),
"cost_center": item.get("buying_cost_center"),
+ "is_finished_item": 1
}
}, bom_no = self.bom_no)
@@ -1027,32 +1099,29 @@
for item in itervalues(item_dict):
item.from_warehouse = ""
+ item.is_scrap_item = 1
return item_dict
def get_unconsumed_raw_materials(self):
wo = frappe.get_doc("Work Order", self.work_order)
wo_items = frappe.get_all('Work Order Item',
filters={'parent': self.work_order},
- fields=["item_code", "required_qty", "consumed_qty"]
+ fields=["item_code", "required_qty", "consumed_qty", "transferred_qty"]
)
+ work_order_qty = wo.material_transferred_for_manufacturing or wo.qty
for item in wo_items:
- qty = item.required_qty
-
item_account_details = get_item_defaults(item.item_code, self.company)
# Take into account consumption if there are any.
- if self.purpose == 'Manufacture':
- req_qty_each = flt(item.required_qty / wo.qty)
- if (flt(item.consumed_qty) != 0):
- remaining_qty = flt(item.consumed_qty) - (flt(wo.produced_qty) * req_qty_each)
- exhaust_qty = req_qty_each * wo.produced_qty
- if remaining_qty > exhaust_qty :
- if (remaining_qty/(req_qty_each * flt(self.fg_completed_qty))) >= 1:
- qty =0
- else:
- qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
- else:
- qty = req_qty_each * flt(self.fg_completed_qty)
+
+ wo_item_qty = item.transferred_qty or item.required_qty
+
+ req_qty_each = (
+ (flt(wo_item_qty) - flt(item.consumed_qty)) /
+ (flt(work_order_qty) - flt(wo.produced_qty))
+ )
+
+ qty = req_qty_each * flt(self.fg_completed_qty)
if qty > 0:
self.add_to_stock_entry_detail({
@@ -1132,8 +1201,10 @@
else:
qty = (req_qty_each * flt(self.fg_completed_qty)) - remaining_qty
else:
- qty = req_qty_each * flt(self.fg_completed_qty)
-
+ if self.flags.backflush_based_on == "Material Transferred for Manufacture":
+ qty = (item.qty/trans_qty) * flt(self.fg_completed_qty)
+ else:
+ qty = req_qty_each * flt(self.fg_completed_qty)
elif backflushed_materials.get(item.item_code):
for d in backflushed_materials.get(item.item_code):
@@ -1141,6 +1212,10 @@
if (qty > req_qty):
qty = (qty/trans_qty) * flt(self.fg_completed_qty)
+ if consumed_qty and frappe.db.get_single_value("Manufacturing Settings",
+ "material_consumption"):
+ qty -= consumed_qty
+
if cint(frappe.get_cached_value('UOM', item.stock_uom, 'must_be_whole_number')):
qty = frappe.utils.ceil(qty)
@@ -1241,6 +1316,8 @@
se_child.subcontracted_item = item_dict[d].get("main_item_code")
se_child.cost_center = (item_dict[d].get("cost_center") or
get_default_cost_center(item_dict[d], company = self.company))
+ se_child.is_finished_item = item_dict[d].get("is_finished_item", 0)
+ se_child.is_scrap_item = item_dict[d].get("is_scrap_item", 0)
for field in ["idx", "po_detail", "original_item",
"expense_account", "description", "item_name"]:
@@ -1279,9 +1356,6 @@
frappe.MappingMismatchError)
elif self.purpose == "Material Transfer" and self.add_to_transit:
continue
- elif mreq_item.warehouse != (item.s_warehouse if self.purpose == "Material Issue" else item.t_warehouse):
- frappe.throw(_("Warehouse for row {0} does not match Material Request").format(item.idx),
- frappe.MappingMismatchError)
def validate_batch(self):
if self.purpose in ["Material Transfer for Manufacture", "Manufacture", "Repack", "Send to Subcontractor"]:
diff --git a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
index b78c6be..b12a854 100644
--- a/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
+++ b/erpnext/stock/doctype/stock_entry/stock_entry_utils.py
@@ -53,6 +53,8 @@
args.target = args.to_warehouse
if args.item_code:
args.item = args.item_code
+ if args.apply_putaway_rule:
+ s.apply_putaway_rule = args.apply_putaway_rule
if isinstance(args.qty, string_types):
if '.' in args.qty:
@@ -118,7 +120,8 @@
"t_warehouse": args.target,
"qty": args.qty,
"basic_rate": args.rate or args.basic_rate,
- "conversion_factor": 1.0,
+ "conversion_factor": args.conversion_factor or 1.0,
+ "transfer_qty": flt(args.qty) * (flt(args.conversion_factor) or 1.0),
"serial_no": args.serial_no,
'batch_no': args.batch_no,
'cost_center': args.cost_center,
diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
index 9b6744c..123f0c8 100644
--- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py
+++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py
@@ -6,7 +6,6 @@
import frappe.defaults
from frappe.utils import flt, nowdate, nowtime
from erpnext.stock.doctype.serial_no.serial_no import *
-from erpnext import set_perpetual_inventory
from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import StockFreezeError
from erpnext.stock.stock_ledger import get_previous_sle
from frappe.permissions import add_user_permission, remove_user_permission
@@ -32,7 +31,6 @@
class TestStockEntry(unittest.TestCase):
def tearDown(self):
frappe.set_user("Administrator")
- set_perpetual_inventory(0)
def test_fifo(self):
frappe.db.set_value("Stock Settings", None, "allow_negative_stock", 1)
@@ -181,22 +179,20 @@
def test_material_transfer_gl_entry(self):
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
- create_stock_reconciliation(qty=100, rate=100)
-
mtn = make_stock_entry(item_code="_Test Item", source="Stores - TCP1",
- target="Finished Goods - TCP1", qty=45)
+ target="Finished Goods - TCP1", qty=45, company=company)
self.check_stock_ledger_entries("Stock Entry", mtn.name,
[["_Test Item", "Stores - TCP1", -45.0], ["_Test Item", "Finished Goods - TCP1", 45.0]])
- stock_in_hand_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse)
+ source_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].s_warehouse)
- fixed_asset_account = get_inventory_account(mtn.company, mtn.get("items")[0].t_warehouse)
+ target_warehouse_account = get_inventory_account(mtn.company, mtn.get("items")[0].t_warehouse)
- if stock_in_hand_account == fixed_asset_account:
+ if source_warehouse_account == target_warehouse_account:
# no gl entry as both source and target warehouse has linked to same account.
self.assertFalse(frappe.db.sql("""select * from `tabGL Entry`
- where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name))
+ where voucher_type='Stock Entry' and voucher_no=%s""", mtn.name, as_dict=1))
else:
stock_value_diff = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Stock Entry",
@@ -204,8 +200,8 @@
self.check_gl_entries("Stock Entry", mtn.name,
sorted([
- [stock_in_hand_account, 0.0, stock_value_diff],
- [fixed_asset_account, stock_value_diff, 0.0],
+ [source_warehouse_account, 0.0, stock_value_diff],
+ [target_warehouse_account, stock_value_diff, 0.0],
])
)
@@ -213,7 +209,6 @@
def test_repack_no_change_in_valuation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse - _TC', 'company')
- set_perpetual_inventory(0, company)
make_stock_entry(item_code="_Test Item", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
make_stock_entry(item_code="_Test Item Home Desktop 100", target="_Test Warehouse - _TC",
@@ -235,8 +230,6 @@
order by account desc""", repack.name, as_dict=1)
self.assertFalse(gl_entries)
- set_perpetual_inventory(0, repack.company)
-
def test_repack_with_additional_costs(self):
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
@@ -474,7 +467,6 @@
def test_warehouse_company_validation(self):
company = frappe.db.get_value('Warehouse', '_Test Warehouse 2 - _TC1', 'company')
- set_perpetual_inventory(0, company)
frappe.get_doc("User", "test2@example.com")\
.add_roles("Sales User", "Sales Manager", "Stock User", "Stock Manager")
frappe.set_user("test2@example.com")
@@ -500,7 +492,7 @@
st1 = frappe.copy_doc(test_records[0])
st1.company = "_Test Company 1"
- set_perpetual_inventory(0, st1.company)
+
frappe.set_user("test@example.com")
st1.get("items")[0].t_warehouse="_Test Warehouse 2 - _TC1"
self.assertRaises(frappe.PermissionError, st1.insert)
@@ -698,47 +690,54 @@
repack.insert()
self.assertRaises(frappe.ValidationError, repack.submit)
- def test_material_consumption(self):
- from erpnext.manufacturing.doctype.work_order.work_order \
- import make_stock_entry as _make_stock_entry
- bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
- "is_default": 1, "docstatus": 1})
+ # def test_material_consumption(self):
+ # frappe.db.set_value("Manufacturing Settings", None, "backflush_raw_materials_based_on", "BOM")
+ # frappe.db.set_value("Manufacturing Settings", None, "material_consumption", "0")
- work_order = frappe.new_doc("Work Order")
- work_order.update({
- "company": "_Test Company",
- "fg_warehouse": "_Test Warehouse 1 - _TC",
- "production_item": "_Test FG Item 2",
- "bom_no": bom_no,
- "qty": 4.0,
- "stock_uom": "_Test UOM",
- "wip_warehouse": "_Test Warehouse - _TC",
- "additional_operating_cost": 1000
- })
- work_order.insert()
- work_order.submit()
+ # from erpnext.manufacturing.doctype.work_order.work_order \
+ # import make_stock_entry as _make_stock_entry
+ # bom_no = frappe.db.get_value("BOM", {"item": "_Test FG Item 2",
+ # "is_default": 1, "docstatus": 1})
- make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
- make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
+ # work_order = frappe.new_doc("Work Order")
+ # work_order.update({
+ # "company": "_Test Company",
+ # "fg_warehouse": "_Test Warehouse 1 - _TC",
+ # "production_item": "_Test FG Item 2",
+ # "bom_no": bom_no,
+ # "qty": 4.0,
+ # "stock_uom": "_Test UOM",
+ # "wip_warehouse": "_Test Warehouse - _TC",
+ # "additional_operating_cost": 1000,
+ # "use_multi_level_bom": 1
+ # })
+ # work_order.insert()
+ # work_order.submit()
- item_quantity = {
- '_Test Item': 10.0,
- '_Test Item 2': 12.0,
- '_Test Serialized Item With Series': 6.0
- }
+ # make_stock_entry(item_code="_Test Serialized Item With Series", target="_Test Warehouse - _TC", qty=50, basic_rate=100)
+ # make_stock_entry(item_code="_Test Item 2", target="_Test Warehouse - _TC", qty=50, basic_rate=20)
- stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2))
- for d in stock_entry.get('items'):
- self.assertEqual(item_quantity.get(d.item_code), d.qty)
+ # item_quantity = {
+ # '_Test Item': 2.0,
+ # '_Test Item 2': 12.0,
+ # '_Test Serialized Item With Series': 6.0
+ # }
+
+ # stock_entry = frappe.get_doc(_make_stock_entry(work_order.name, "Material Consumption for Manufacture", 2))
+ # for d in stock_entry.get('items'):
+ # self.assertEqual(item_quantity.get(d.item_code), d.qty)
def test_customer_provided_parts_se(self):
create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0)
- se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt', qty=4, to_warehouse = "_Test Warehouse - _TC")
+ se = make_stock_entry(item_code='CUST-0987', purpose = 'Material Receipt',
+ qty=4, to_warehouse = "_Test Warehouse - _TC")
self.assertEqual(se.get("items")[0].allow_zero_valuation_rate, 1)
self.assertEqual(se.get("items")[0].amount, 0)
def test_gle_for_opening_stock_entry(self):
- mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1", company="_Test Company with perpetual inventory",qty=50, basic_rate=100, expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True)
+ mr = make_stock_entry(item_code="_Test Item", target="Stores - TCP1",
+ company="_Test Company with perpetual inventory", qty=50, basic_rate=100,
+ expense_account="Stock Adjustment - TCP1", is_opening="Yes", do_not_save=True)
self.assertRaises(OpeningEntryAccountError, mr.save)
@@ -753,37 +752,37 @@
def test_total_basic_amount_zero(self):
se = frappe.get_doc({"doctype":"Stock Entry",
- "purpose":"Material Receipt",
- "stock_entry_type":"Material Receipt",
- "posting_date": nowdate(),
- "company":"_Test Company with perpetual inventory",
- "items":[
- {
- "item_code":"Basil Leaves",
- "description":"Basil Leaves",
- "qty": 1,
- "basic_rate": 0,
- "uom":"Nos",
- "t_warehouse": "Stores - TCP1",
- "allow_zero_valuation_rate": 1,
- "cost_center": "Main - TCP1"
- },
- {
- "item_code":"Basil Leaves",
- "description":"Basil Leaves",
- "qty": 2,
- "basic_rate": 0,
- "uom":"Nos",
- "t_warehouse": "Stores - TCP1",
- "allow_zero_valuation_rate": 1,
- "cost_center": "Main - TCP1"
- },
- ],
- "additional_costs":[
- {"expense_account":"Miscellaneous Expenses - TCP1",
- "amount":100,
- "description": "miscellanous"}
- ]
+ "purpose":"Material Receipt",
+ "stock_entry_type":"Material Receipt",
+ "posting_date": nowdate(),
+ "company":"_Test Company with perpetual inventory",
+ "items":[
+ {
+ "item_code":"_Test Item",
+ "description":"_Test Item",
+ "qty": 1,
+ "basic_rate": 0,
+ "uom":"Nos",
+ "t_warehouse": "Stores - TCP1",
+ "allow_zero_valuation_rate": 1,
+ "cost_center": "Main - TCP1"
+ },
+ {
+ "item_code":"_Test Item",
+ "description":"_Test Item",
+ "qty": 2,
+ "basic_rate": 0,
+ "uom":"Nos",
+ "t_warehouse": "Stores - TCP1",
+ "allow_zero_valuation_rate": 1,
+ "cost_center": "Main - TCP1"
+ },
+ ],
+ "additional_costs":[
+ {"expense_account":"Miscellaneous Expenses - TCP1",
+ "amount":100,
+ "description": "miscellanous"
+ }]
})
se.insert()
se.submit()
diff --git a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
index 79e8f9a..864ff48 100644
--- a/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
+++ b/erpnext/stock/doctype/stock_entry_detail/stock_entry_detail.json
@@ -1,4 +1,5 @@
{
+ "actions": [],
"autoname": "hash",
"creation": "2013-03-29 18:22:12",
"doctype": "DocType",
@@ -13,8 +14,10 @@
"t_warehouse",
"sec_break1",
"item_code",
- "col_break2",
"item_name",
+ "col_break2",
+ "is_finished_item",
+ "is_scrap_item",
"subcontracted_item",
"section_break_8",
"description",
@@ -22,35 +25,37 @@
"item_group",
"image",
"image_view",
- "quantity_and_rate",
- "set_basic_rate_manually",
+ "quantity_section",
"qty",
- "basic_rate",
- "basic_amount",
- "additional_cost",
- "amount",
- "valuation_rate",
- "col_break3",
- "uom",
- "conversion_factor",
- "stock_uom",
"transfer_qty",
"retain_sample",
+ "column_break_20",
+ "uom",
+ "stock_uom",
+ "conversion_factor",
"sample_quantity",
+ "rates_section",
+ "basic_rate",
+ "additional_cost",
+ "valuation_rate",
+ "allow_zero_valuation_rate",
+ "col_break3",
+ "set_basic_rate_manually",
+ "basic_amount",
+ "amount",
"serial_no_batch",
"serial_no",
"col_break4",
"batch_no",
- "quality_inspection",
"accounting",
"expense_account",
- "col_break5",
"accounting_dimensions_section",
"cost_center",
+ "project",
"dimension_col_break",
"more_info",
- "allow_zero_valuation_rate",
"actual_qty",
+ "transferred_qty",
"bom_no",
"allow_alternative_item",
"col_break6",
@@ -61,10 +66,11 @@
"against_stock_entry",
"ste_detail",
"po_detail",
+ "putaway_rule",
"column_break_51",
- "transferred_qty",
"reference_purchase_receipt",
- "project"
+ "quality_inspection",
+ "job_card_item"
],
"fields": [
{
@@ -160,11 +166,6 @@
"print_hide": 1
},
{
- "fieldname": "quantity_and_rate",
- "fieldtype": "Section Break",
- "label": "Quantity and Rate"
- },
- {
"bold": 1,
"fieldname": "qty",
"fieldtype": "Float",
@@ -322,10 +323,6 @@
"print_hide": 1
},
{
- "fieldname": "col_break5",
- "fieldtype": "Column Break"
- },
- {
"default": ":Company",
"depends_on": "eval:cint(erpnext.is_perpetual_inventory_enabled(parent.company))",
"fieldname": "cost_center",
@@ -335,6 +332,7 @@
"print_hide": 1
},
{
+ "collapsible": 1,
"fieldname": "more_info",
"fieldtype": "Section Break",
"label": "More Information"
@@ -456,6 +454,7 @@
"read_only": 1
},
{
+ "collapsible": 1,
"fieldname": "accounting_dimensions_section",
"fieldtype": "Section Break",
"label": "Accounting Dimensions"
@@ -498,13 +497,58 @@
"fieldname": "set_basic_rate_manually",
"fieldtype": "Check",
"label": "Set Basic Rate Manually"
+ },
+ {
+ "depends_on": "eval:in_list([\"Material Transfer\", \"Material Receipt\"], parent.purpose)",
+ "fieldname": "putaway_rule",
+ "fieldtype": "Link",
+ "label": "Putaway Rule",
+ "no_copy": 1,
+ "options": "Putaway Rule",
+ "print_hide": 1,
+ "read_only": 1
+ },
+ {
+ "fieldname": "quantity_section",
+ "fieldtype": "Section Break",
+ "label": "Quantity"
+ },
+ {
+ "fieldname": "column_break_20",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "rates_section",
+ "fieldtype": "Section Break",
+ "label": "Rates"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_scrap_item",
+ "fieldtype": "Check",
+ "label": "Is Scrap Item"
+ },
+ {
+ "default": "0",
+ "fieldname": "is_finished_item",
+ "fieldtype": "Check",
+ "label": "Is Finished Item"
+ },
+ {
+ "fieldname": "job_card_item",
+ "fieldtype": "Data",
+ "hidden": 1,
+ "label": "Job Card Item",
+ "no_copy": 1,
+ "print_hide": 1,
+ "read_only": 1
}
],
"idx": 1,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
- "modified": "2020-09-23 17:55:03.384138",
+ "modified": "2021-02-11 13:47:50.158754",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Entry Detail",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
index fda17e0..2463a21 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json
@@ -8,26 +8,33 @@
"engine": "InnoDB",
"field_order": [
"item_code",
- "serial_no",
- "batch_no",
"warehouse",
"posting_date",
"posting_time",
+ "column_break_6",
"voucher_type",
"voucher_no",
"voucher_detail_no",
+ "dependant_sle_voucher_detail_no",
+ "recalculate_rate",
+ "section_break_11",
"actual_qty",
+ "qty_after_transaction",
"incoming_rate",
"outgoing_rate",
- "stock_uom",
- "qty_after_transaction",
+ "column_break_17",
"valuation_rate",
"stock_value",
"stock_value_difference",
"stock_queue",
- "project",
+ "section_break_21",
"company",
+ "stock_uom",
+ "project",
+ "batch_no",
+ "column_break_26",
"fiscal_year",
+ "serial_no",
"is_cancelled",
"to_rename"
],
@@ -50,7 +57,6 @@
{
"fieldname": "serial_no",
"fieldtype": "Long Text",
- "in_list_view": 1,
"label": "Serial No",
"print_width": "100px",
"read_only": 1,
@@ -59,7 +65,6 @@
{
"fieldname": "batch_no",
"fieldtype": "Data",
- "in_list_view": 1,
"label": "Batch No",
"oldfieldname": "batch_no",
"oldfieldtype": "Data",
@@ -119,6 +124,7 @@
"fieldname": "voucher_no",
"fieldtype": "Dynamic Link",
"in_filter": 1,
+ "in_list_view": 1,
"in_standard_filter": 1,
"label": "Voucher No",
"oldfieldname": "voucher_no",
@@ -142,6 +148,7 @@
"fieldname": "actual_qty",
"fieldtype": "Float",
"in_filter": 1,
+ "in_list_view": 1,
"label": "Actual Quantity",
"oldfieldname": "actual_qty",
"oldfieldtype": "Currency",
@@ -152,6 +159,7 @@
{
"fieldname": "incoming_rate",
"fieldtype": "Currency",
+ "in_list_view": 1,
"label": "Incoming Rate",
"oldfieldname": "incoming_rate",
"oldfieldtype": "Currency",
@@ -217,13 +225,11 @@
{
"fieldname": "stock_queue",
"fieldtype": "Text",
- "hidden": 1,
"label": "Stock Queue (FIFO)",
"oldfieldname": "fcfs_stack",
"oldfieldtype": "Text",
"print_hide": 1,
- "read_only": 1,
- "report_hide": 1
+ "read_only": 1
},
{
"fieldname": "project",
@@ -269,14 +275,48 @@
"hidden": 1,
"label": "To Rename",
"search_index": 1
+ },
+ {
+ "fieldname": "dependant_sle_voucher_detail_no",
+ "fieldtype": "Data",
+ "label": "Dependant SLE Voucher Detail No"
+ },
+ {
+ "fieldname": "column_break_6",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_17",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "section_break_21",
+ "fieldtype": "Section Break"
+ },
+ {
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break"
+ },
+ {
+ "default": "0",
+ "fieldname": "recalculate_rate",
+ "fieldtype": "Check",
+ "label": "Recalculate Incoming/Outgoing Rate",
+ "no_copy": 1,
+ "read_only": 1
}
],
"hide_toolbar": 1,
"icon": "fa fa-list",
"idx": 1,
"in_create": 1,
+ "index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-04-23 05:57:03.985520",
+ "modified": "2020-09-07 11:10:35.318872",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Ledger Entry",
diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
index bb356f6..b0e7440 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.py
@@ -10,8 +10,10 @@
from datetime import date
from erpnext.controllers.item_variant import ItemTemplateCannotHaveStock
from erpnext.accounts.utils import get_fiscal_year
+from frappe.core.doctype.role.role import get_users
class StockFreezeError(frappe.ValidationError): pass
+class BackDatedStockTransaction(frappe.ValidationError): pass
exclude_from_linked_with = True
@@ -25,16 +27,17 @@
def validate(self):
self.flags.ignore_submit_comment = True
- from erpnext.stock.utils import validate_warehouse_company
+ from erpnext.stock.utils import validate_warehouse_company, validate_disabled_warehouse
self.validate_mandatory()
self.validate_item()
self.validate_batch()
+ validate_disabled_warehouse(self.warehouse)
validate_warehouse_company(self.warehouse, self.company)
self.scrub_posting_time()
self.validate_and_set_fiscal_year()
self.block_transactions_against_group_warehouse()
self.validate_with_last_transaction_posting_time()
- self.validate_future_posting()
+
def on_submit(self):
self.check_stock_frozen_date()
@@ -48,7 +51,7 @@
def calculate_batch_qty(self):
if self.batch_no:
batch_qty = frappe.db.get_value("Stock Ledger Entry",
- {"docstatus": 1, "batch_no": self.batch_no},
+ {"docstatus": 1, "batch_no": self.batch_no, "is_cancelled": 0},
"sum(actual_qty)") or 0
frappe.db.set_value("Batch", self.batch_no, "batch_qty", batch_qty)
@@ -88,14 +91,14 @@
# check if batch number is required
if self.voucher_type != 'Stock Reconciliation':
- if item_det.has_batch_no ==1:
+ if item_det.has_batch_no == 1:
batch_item = self.item_code if self.item_code == item_det.item_name else self.item_code + ":" + item_det.item_name
if not self.batch_no:
frappe.throw(_("Batch number is mandatory for Item {0}").format(batch_item))
elif not frappe.db.get_value("Batch",{"item": self.item_code, "name": self.batch_no}):
frappe.throw(_("{0} is not a valid Batch Number for Item {1}").format(self.batch_no, batch_item))
- elif item_det.has_batch_no ==0 and self.batch_no:
+ elif item_det.has_batch_no == 0 and self.batch_no and self.is_cancelled == 0:
frappe.throw(_("The Item {0} cannot have Batch").format(self.item_code))
if item_det.has_variants:
@@ -142,28 +145,28 @@
is_group_warehouse(self.warehouse)
def validate_with_last_transaction_posting_time(self):
- last_transaction_time = frappe.db.sql("""
- select MAX(timestamp(posting_date, posting_time)) as posting_time
- from `tabStock Ledger Entry`
- where docstatus = 1 and item_code = %s
- and warehouse = %s""", (self.item_code, self.warehouse))[0][0]
+ authorized_role = frappe.db.get_single_value("Stock Settings", "role_allowed_to_create_edit_back_dated_transactions")
+ if authorized_role:
+ authorized_users = get_users(authorized_role)
+ if authorized_users and frappe.session.user not in authorized_users:
+ last_transaction_time = frappe.db.sql("""
+ select MAX(timestamp(posting_date, posting_time)) as posting_time
+ from `tabStock Ledger Entry`
+ where docstatus = 1 and item_code = %s
+ and warehouse = %s""", (self.item_code, self.warehouse))[0][0]
- cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00")
+ cur_doc_posting_datetime = "%s %s" % (self.posting_date, self.get("posting_time") or "00:00:00")
- if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time):
- msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code),
- frappe.bold(self.warehouse), frappe.bold(last_transaction_time))
+ if last_transaction_time and get_datetime(cur_doc_posting_datetime) < get_datetime(last_transaction_time):
+ msg = _("Last Stock Transaction for item {0} under warehouse {1} was on {2}.").format(frappe.bold(self.item_code),
+ frappe.bold(self.warehouse), frappe.bold(last_transaction_time))
- msg += "<br><br>" + _("Stock Transactions for Item {0} under warehouse {1} cannot be posted before this time.").format(
- frappe.bold(self.item_code), frappe.bold(self.warehouse))
+ msg += "<br><br>" + _("You are not authorized to make/edit Stock Transactions for Item {0} under warehouse {1} before this time.").format(
+ frappe.bold(self.item_code), frappe.bold(self.warehouse))
- msg += "<br><br>" + _("Please remove this item and try to submit again or update the posting time.")
- frappe.throw(msg, title=_("Backdated Stock Entry"))
-
- def validate_future_posting(self):
- if date_diff(self.posting_date, getdate()) > 0:
- msg = _("Posting future stock transactions are not allowed due to Immutable Ledger")
- frappe.throw(msg, title=_("Future Posting Not Allowed"))
+ msg += "<br><br>" + _("Please contact any of the following users to {} this transaction.")
+ msg += "<br>" + "<br>".join(authorized_users)
+ frappe.throw(msg, BackDatedStockTransaction, title=_("Backdated Stock Entry"))
def on_doctype_update():
if not frappe.db.has_index('tabStock Ledger Entry', 'posting_sort_index'):
diff --git a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
index 04dae83..59f1f39 100644
--- a/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
+++ b/erpnext/stock/doctype/stock_ledger_entry/test_stock_ledger_entry.py
@@ -5,8 +5,397 @@
import frappe
import unittest
-
-# test_records = frappe.get_test_records('Stock Ledger Entry')
+from frappe.utils import today, add_days
+from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
+from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation \
+ import create_stock_reconciliation
+from erpnext.stock.doctype.item.test_item import make_item
+from erpnext.stock.stock_ledger import get_previous_sle
+from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
+from erpnext.stock.doctype.landed_cost_voucher.test_landed_cost_voucher import create_landed_cost_voucher
+from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note
+from erpnext.stock.doctype.stock_ledger_entry.stock_ledger_entry import BackDatedStockTransaction
class TestStockLedgerEntry(unittest.TestCase):
- pass
+ def setUp(self):
+ items = create_items()
+
+ # delete SLE and BINs for all items
+ frappe.db.sql("delete from `tabStock Ledger Entry` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
+ frappe.db.sql("delete from `tabBin` where item_code in (%s)" % (', '.join(['%s']*len(items))), items)
+
+ def test_item_cost_reposting(self):
+ company = "_Test Company"
+
+ # _Test Item for Reposting at Stores warehouse on 10-04-2020: Qty = 50, Rate = 100
+ create_stock_reconciliation(
+ item_code="_Test Item for Reposting",
+ warehouse="Stores - _TC",
+ qty=50,
+ rate=100,
+ company=company,
+ expense_account = "Stock Adjustment - _TC",
+ posting_date='2020-04-10',
+ posting_time='14:00'
+ )
+
+ # _Test Item for Reposting at FG warehouse on 20-04-2020: Qty = 10, Rate = 200
+ create_stock_reconciliation(
+ item_code="_Test Item for Reposting",
+ warehouse="Finished Goods - _TC",
+ qty=10,
+ rate=200,
+ company=company,
+ expense_account = "Stock Adjustment - _TC",
+ posting_date='2020-04-20',
+ posting_time='14:00'
+ )
+
+ # _Test Item for Reposting transferred from Stores to FG warehouse on 30-04-2020
+ make_stock_entry(
+ item_code="_Test Item for Reposting",
+ source="Stores - _TC",
+ target="Finished Goods - _TC",
+ company=company,
+ qty=10,
+ expense_account="Stock Adjustment - _TC",
+ posting_date='2020-04-30',
+ posting_time='14:00'
+ )
+ target_wh_sle = get_previous_sle({
+ "item_code": "_Test Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-04-30',
+ "posting_time": '14:00'
+ })
+
+ self.assertEqual(target_wh_sle.get("valuation_rate"), 150)
+
+ # Repack entry on 5-5-2020
+ repack = create_repack_entry(company=company, posting_date='2020-05-05', posting_time='14:00')
+
+ finished_item_sle = get_previous_sle({
+ "item_code": "_Test Finished Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-05-05',
+ "posting_time": '14:00'
+ })
+ self.assertEqual(finished_item_sle.get("incoming_rate"), 540)
+ self.assertEqual(finished_item_sle.get("valuation_rate"), 540)
+
+ # Reconciliation for _Test Item for Reposting at Stores on 12-04-2020: Qty = 50, Rate = 150
+ create_stock_reconciliation(
+ item_code="_Test Item for Reposting",
+ warehouse="Stores - _TC",
+ qty=50,
+ rate=150,
+ company=company,
+ expense_account = "Stock Adjustment - _TC",
+ posting_date='2020-04-12',
+ posting_time='14:00'
+ )
+
+
+ # Check valuation rate of finished goods warehouse after back-dated entry at Stores
+ target_wh_sle = get_previous_sle({
+ "item_code": "_Test Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-04-30',
+ "posting_time": '14:00'
+ })
+ self.assertEqual(target_wh_sle.get("incoming_rate"), 150)
+ self.assertEqual(target_wh_sle.get("valuation_rate"), 175)
+
+ # Check valuation rate of repacked item after back-dated entry at Stores
+ finished_item_sle = get_previous_sle({
+ "item_code": "_Test Finished Item for Reposting",
+ "warehouse": "Finished Goods - _TC",
+ "posting_date": '2020-05-05',
+ "posting_time": '14:00'
+ })
+ self.assertEqual(finished_item_sle.get("incoming_rate"), 790)
+ self.assertEqual(finished_item_sle.get("valuation_rate"), 790)
+
+ # Check updated rate in Repack entry
+ repack.reload()
+ self.assertEqual(repack.items[0].get("basic_rate"), 150)
+ self.assertEqual(repack.items[1].get("basic_rate"), 750)
+
+ def test_purchase_return_valuation_reposting(self):
+ pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code="_Test Item for Reposting", qty=5, rate=100)
+
+ return_pr = make_purchase_receipt(company="_Test Company", posting_date='2020-04-15',
+ warehouse="Stores - _TC", item_code="_Test Item for Reposting", is_return=1, return_against=pr.name, qty=-2)
+
+ # check sle
+ outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
+ "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"])
+
+ self.assertEqual(outgoing_rate, 100)
+ self.assertEqual(stock_value_difference, -200)
+
+ create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ outgoing_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
+ "voucher_no": return_pr.name}, ["outgoing_rate", "stock_value_difference"])
+
+ self.assertEqual(outgoing_rate, 110)
+ self.assertEqual(stock_value_difference, -220)
+
+ def test_sales_return_valuation_reposting(self):
+ company = "_Test Company"
+ item_code="_Test Item for Reposting"
+
+ # Purchase Return: Qty = 5, Rate = 100
+ pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code=item_code, qty=5, rate=100)
+
+ #Delivery Note: Qty = 5, Rate = 150
+ dn = create_delivery_note(item_code=item_code, qty=5, rate=150, warehouse="Stores - _TC",
+ company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check outgoing_rate for DN
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 5)
+
+ self.assertEqual(dn.items[0].incoming_rate, 100)
+ self.assertEqual(outgoing_rate, 100)
+
+ # Return Entry: Qty = -2, Rate = 150
+ return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=item_code, qty=-2, rate=150,
+ company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check incoming rate for Return entry
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(return_dn.items[0].incoming_rate, 100)
+ self.assertEqual(incoming_rate, 100)
+ self.assertEqual(stock_value_difference, 200)
+
+ #-------------------------------
+
+ # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ # check outgoing_rate for DN after reposting
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 5)
+ self.assertEqual(outgoing_rate, 110)
+
+ dn.reload()
+ self.assertEqual(dn.items[0].incoming_rate, 110)
+
+ # check incoming rate for Return entry after reposting
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(incoming_rate, 110)
+ self.assertEqual(stock_value_difference, 220)
+
+ return_dn.reload()
+ self.assertEqual(return_dn.items[0].incoming_rate, 110)
+
+ # Cleanup data
+ return_dn.cancel()
+ dn.cancel()
+ lcv.cancel()
+ pr.cancel()
+
+ def test_reposting_of_sales_return_for_packed_item(self):
+ company = "_Test Company"
+ packed_item_code="_Test Item for Reposting"
+ bundled_item = "_Test Bundled Item for Reposting"
+ create_product_bundle_item(bundled_item, [[packed_item_code, 4]])
+
+ # Purchase Return: Qty = 50, Rate = 100
+ pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code=packed_item_code, qty=50, rate=100)
+
+ #Delivery Note: Qty = 5, Rate = 150
+ dn = create_delivery_note(item_code=bundled_item, qty=5, rate=150, warehouse="Stores - _TC",
+ company=company, expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check outgoing_rate for DN
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 20)
+
+ self.assertEqual(dn.packed_items[0].incoming_rate, 100)
+ self.assertEqual(outgoing_rate, 100)
+
+ # Return Entry: Qty = -2, Rate = 150
+ return_dn = create_delivery_note(is_return=1, return_against=dn.name, item_code=bundled_item, qty=-2, rate=150,
+ company=company, warehouse="Stores - _TC", expense_account="Cost of Goods Sold - _TC", cost_center="Main - _TC")
+
+ # check incoming rate for Return entry
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(return_dn.packed_items[0].incoming_rate, 100)
+ self.assertEqual(incoming_rate, 100)
+ self.assertEqual(stock_value_difference, 800)
+
+ #-------------------------------
+
+ # Landed Cost Voucher to update the rate of incoming Purchase Return: Additional cost = 50
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ # check outgoing_rate for DN after reposting
+ outgoing_rate = abs(frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Delivery Note",
+ "voucher_no": dn.name}, "stock_value_difference") / 20)
+ self.assertEqual(outgoing_rate, 101)
+
+ dn.reload()
+ self.assertEqual(dn.packed_items[0].incoming_rate, 101)
+
+ # check incoming rate for Return entry after reposting
+ incoming_rate, stock_value_difference = frappe.db.get_value("Stock Ledger Entry",
+ {"voucher_type": "Delivery Note", "voucher_no": return_dn.name},
+ ["incoming_rate", "stock_value_difference"])
+
+ self.assertEqual(incoming_rate, 101)
+ self.assertEqual(stock_value_difference, 808)
+
+ return_dn.reload()
+ self.assertEqual(return_dn.packed_items[0].incoming_rate, 101)
+
+ # Cleanup data
+ return_dn.cancel()
+ dn.cancel()
+ lcv.cancel()
+ pr.cancel()
+
+ def test_sub_contracted_item_costing(self):
+ from erpnext.manufacturing.doctype.production_plan.test_production_plan import make_bom
+
+ company = "_Test Company"
+ rm_item_code="_Test Item for Reposting"
+ subcontracted_item = "_Test Subcontracted Item for Reposting"
+
+ frappe.db.set_value("Buying Settings", None, "backflush_raw_materials_of_subcontract_based_on", "BOM")
+ make_bom(item = subcontracted_item, raw_materials =[rm_item_code], currency="INR")
+
+ # Purchase raw materials on supplier warehouse: Qty = 50, Rate = 100
+ pr = make_purchase_receipt(company=company, posting_date='2020-04-10',
+ warehouse="Stores - _TC", item_code=rm_item_code, qty=10, rate=100)
+
+ # Purchase Receipt for subcontracted item
+ pr1 = make_purchase_receipt(company=company, posting_date='2020-04-20',
+ warehouse="Finished Goods - _TC", supplier_warehouse="Stores - _TC",
+ item_code=subcontracted_item, qty=10, rate=20, is_subcontracted="Yes")
+
+ self.assertEqual(pr1.items[0].valuation_rate, 120)
+
+ # Update raw material's valuation via LCV, Additional cost = 50
+ lcv = create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company)
+
+ pr1.reload()
+ self.assertEqual(pr1.items[0].valuation_rate, 125)
+
+ # check outgoing_rate for DN after reposting
+ incoming_rate = frappe.db.get_value("Stock Ledger Entry", {"voucher_type": "Purchase Receipt",
+ "voucher_no": pr1.name, "item_code": subcontracted_item}, "incoming_rate")
+ self.assertEqual(incoming_rate, 125)
+
+ # cleanup data
+ pr1.cancel()
+ lcv.cancel()
+ pr.cancel()
+
+ def test_back_dated_entry_not_allowed(self):
+ # Back dated stock transactions are only allowed to stock managers
+ frappe.db.set_value("Stock Settings", None,
+ "role_allowed_to_create_edit_back_dated_transactions", "Stock Manager")
+
+ # Set User with Stock User role but not Stock Manager
+ frappe.set_user("test@example.com")
+ user = frappe.get_doc("User", "test@example.com")
+ user.add_roles("Stock User")
+ user.remove_roles("Stock Manager")
+
+ stock_entry_on_today = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100)
+ back_dated_se_1 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
+ posting_date=add_days(today(), -1), do_not_submit=True)
+
+ # Block back-dated entry
+ self.assertRaises(BackDatedStockTransaction, back_dated_se_1.submit)
+
+ user.add_roles("Stock Manager")
+
+ # Back dated entry allowed to Stock Manager
+ back_dated_se_2 = make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100,
+ posting_date=add_days(today(), -1))
+
+ back_dated_se_2.cancel()
+ stock_entry_on_today.cancel()
+
+ frappe.db.set_value("Stock Settings", None, "role_allowed_to_create_edit_back_dated_transactions", None)
+ frappe.set_user("Administrator")
+
+
+def create_repack_entry(**args):
+ args = frappe._dict(args)
+ repack = frappe.new_doc("Stock Entry")
+ repack.stock_entry_type = "Repack"
+ repack.company = args.company or "_Test Company"
+ repack.posting_date = args.posting_date
+ repack.set_posting_time = 1
+ repack.append("items", {
+ "item_code": "_Test Item for Reposting",
+ "s_warehouse": "Stores - _TC",
+ "qty": 5,
+ "conversion_factor": 1,
+ "expense_account": "Stock Adjustment - _TC",
+ "cost_center": "Main - _TC"
+ })
+
+ repack.append("items", {
+ "item_code": "_Test Finished Item for Reposting",
+ "t_warehouse": "Finished Goods - _TC",
+ "qty": 1,
+ "conversion_factor": 1,
+ "expense_account": "Stock Adjustment - _TC",
+ "cost_center": "Main - _TC"
+ })
+
+ repack.append("additional_costs", {
+ "expense_account": "Freight and Forwarding Charges - _TC",
+ "description": "transport cost",
+ "amount": 40
+ })
+
+ repack.save()
+ repack.submit()
+
+ return repack
+
+def create_product_bundle_item(new_item_code, packed_items):
+ if not frappe.db.exists("Product Bundle", new_item_code):
+ item = frappe.new_doc("Product Bundle")
+ item.new_item_code = new_item_code
+
+ for d in packed_items:
+ item.append("items", {
+ "item_code": d[0],
+ "qty": d[1]
+ })
+
+ item.save()
+
+def create_items():
+ items = ["_Test Item for Reposting", "_Test Finished Item for Reposting",
+ "_Test Subcontracted Item for Reposting", "_Test Bundled Item for Reposting"]
+ for d in items:
+ properties = {"valuation_method": "FIFO"}
+ if d == "_Test Bundled Item for Reposting":
+ properties.update({"is_stock_item": 0})
+ elif d == "_Test Subcontracted Item for Reposting":
+ properties.update({"is_sub_contracted_item": 1})
+
+ make_item(d, properties=properties)
+
+ return items
\ No newline at end of file
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
index e2121fc..ac4ed5e 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.js
@@ -2,6 +2,7 @@
// License: GNU General Public License v3. See license.txt
frappe.provide("erpnext.stock");
+frappe.provide("erpnext.accounts.dimensions");
frappe.ui.form.on("Stock Reconciliation", {
onload: function(frm) {
@@ -26,6 +27,12 @@
if (!frm.doc.expense_account) {
frm.trigger("set_expense_account");
}
+
+ erpnext.accounts.dimensions.setup_dimension_filters(frm, frm.doctype);
+ },
+
+ company: function(frm) {
+ erpnext.accounts.dimensions.update_dimension(frm, frm.doctype);
},
refresh: function(frm) {
diff --git a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
index 00b8f69..f0a90f9 100644
--- a/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/stock_reconciliation.py
@@ -30,6 +30,7 @@
self.validate_data()
self.validate_expense_account()
self.set_total_qty_and_amount()
+ self.validate_putaway_capacity()
if self._action=="submit":
self.make_batches('warehouse')
@@ -37,14 +38,16 @@
def on_submit(self):
self.update_stock_ledger()
self.make_gl_entries()
+ self.repost_future_sle_and_gle()
from erpnext.stock.doctype.serial_no.serial_no import update_serial_nos_after_submit
update_serial_nos_after_submit(self, "items")
def on_cancel(self):
- self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry')
+ self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry', 'Repost Item Valuation')
self.make_sle_on_cancel()
self.make_gl_entries_on_cancel()
+ self.repost_future_sle_and_gle()
def remove_items_with_no_change(self):
"""Remove items if qty or rate is not changed"""
diff --git a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
index 23d48d4..088456f 100644
--- a/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
+++ b/erpnext/stock/doctype/stock_reconciliation/test_stock_reconciliation.py
@@ -8,12 +8,11 @@
import frappe, unittest
from frappe.utils import flt, nowdate, nowtime
from erpnext.accounts.utils import get_stock_and_account_balance
-from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import set_perpetual_inventory
from erpnext.stock.stock_ledger import get_previous_sle, update_entries_after
from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import EmptyStockReconciliationItemsError, get_items
from erpnext.stock.doctype.warehouse.test_warehouse import create_warehouse
from erpnext.stock.doctype.item.test_item import create_item
-from erpnext.stock.utils import get_stock_balance, get_incoming_rate, get_available_serial_nos, get_stock_value_on
+from erpnext.stock.utils import get_incoming_rate, get_stock_value_on, get_valuation_method
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
class TestStockReconciliation(unittest.TestCase):
@@ -29,16 +28,17 @@
self._test_reco_sle_gle("Moving Average")
def _test_reco_sle_gle(self, valuation_method):
- insert_existing_sle(warehouse='Stores - TCP1')
+ se1, se2, se3 = insert_existing_sle(warehouse='Stores - TCP1')
company = frappe.db.get_value('Warehouse', 'Stores - TCP1', 'company')
# [[qty, valuation_rate, posting_date,
# posting_time, expected_stock_value, bin_qty, bin_valuation]]
+
input_data = [
- [50, 1000],
- [25, 900],
- ["", 1000],
- [20, ""],
- [0, ""]
+ [50, 1000, "2012-12-26", "12:00"],
+ [25, 900, "2012-12-26", "12:00"],
+ ["", 1000, "2012-12-20", "12:05"],
+ [20, "", "2012-12-26", "12:05"],
+ [0, "", "2012-12-31", "12:10"]
]
for d in input_data:
@@ -47,13 +47,13 @@
last_sle = get_previous_sle({
"item_code": "_Test Item",
"warehouse": "Stores - TCP1",
- "posting_date": nowdate(),
- "posting_time": nowtime()
+ "posting_date": d[2],
+ "posting_time": d[3]
})
# submit stock reconciliation
stock_reco = create_stock_reconciliation(qty=d[0], rate=d[1],
- posting_date=nowdate(), posting_time=nowtime(), warehouse="Stores - TCP1",
+ posting_date=d[2], posting_time=d[3], warehouse="Stores - TCP1",
company=company, expense_account = "Stock Adjustment - TCP1")
# check stock value
@@ -81,10 +81,15 @@
stock_reco.cancel()
+ se3.cancel()
+ se2.cancel()
+ se1.cancel()
+
def test_get_items(self):
- create_warehouse("_Test Warehouse Group 1", {"is_group": 1})
+ create_warehouse("_Test Warehouse Group 1",
+ {"is_group": 1, "company": "_Test Company", "parent_warehouse": "All Warehouses - _TC"})
create_warehouse("_Test Warehouse Ledger 1",
- {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC"})
+ {"is_group": 0, "parent_warehouse": "_Test Warehouse Group 1 - _TC", "company": "_Test Company"})
create_item("_Test Stock Reco Item", is_stock_item=1, valuation_rate=100,
warehouse="_Test Warehouse Ledger 1 - _TC", opening_stock=100)
@@ -95,8 +100,6 @@
[items[0]["item_code"], items[0]["warehouse"], items[0]["qty"]])
def test_stock_reco_for_serialized_item(self):
- set_perpetual_inventory()
-
to_delete_records = []
to_delete_serial_nos = []
@@ -148,8 +151,6 @@
stock_doc.cancel()
def test_stock_reco_for_batch_item(self):
- set_perpetual_inventory()
-
to_delete_records = []
to_delete_serial_nos = []
@@ -196,15 +197,17 @@
def insert_existing_sle(warehouse):
from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry
- make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item",
+ se1 = make_stock_entry(posting_date="2012-12-15", posting_time="02:00", item_code="_Test Item",
target=warehouse, qty=10, basic_rate=700)
- make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item",
+ se2 = make_stock_entry(posting_date="2012-12-25", posting_time="03:00", item_code="_Test Item",
source=warehouse, qty=15)
- make_stock_entry(posting_date=nowdate(), posting_time=nowtime(), item_code="_Test Item",
+ se3 = make_stock_entry(posting_date="2013-01-05", posting_time="07:00", item_code="_Test Item",
target=warehouse, qty=15, basic_rate=1200)
+ return se1, se2, se3
+
def create_batch_or_serial_no_items():
create_warehouse("_Test Warehouse for Stock Reco1",
{"is_group": 0, "parent_warehouse": "_Test Warehouse Group - _TC"})
@@ -256,6 +259,10 @@
return sr
def set_valuation_method(item_code, valuation_method):
+ existing_valuation_method = get_valuation_method(item_code)
+ if valuation_method == existing_valuation_method:
+ return
+
frappe.db.set_value("Item", item_code, "valuation_method", valuation_method)
for warehouse in frappe.get_all("Warehouse", filters={"company": "_Test Company"}, fields=["name", "is_group"]):
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.json b/erpnext/stock/doctype/stock_settings/stock_settings.json
index a166657..84af57b 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.json
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.json
@@ -16,6 +16,7 @@
"action_if_quality_inspection_is_not_submitted",
"show_barcode_field",
"clean_description_html",
+ "disable_serial_no_and_batch_selector",
"section_break_7",
"auto_insert_price_list_rate_if_missing",
"allow_negative_stock",
@@ -28,7 +29,9 @@
"inter_warehouse_transfer_settings_section",
"allow_from_dn",
"allow_from_pr",
- "freeze_stock_entries",
+ "control_historical_stock_transactions_section",
+ "role_allowed_to_create_edit_back_dated_transactions",
+ "column_break_26",
"stock_frozen_upto",
"stock_frozen_upto_days",
"stock_auth_role",
@@ -156,21 +159,20 @@
"label": "Notify by Email on Creation of Automatic Material Request"
},
{
- "fieldname": "freeze_stock_entries",
- "fieldtype": "Section Break",
- "label": "Freeze Stock Entries"
- },
- {
+ "description": "No stock transactions can be created or modified before this date.",
"fieldname": "stock_frozen_upto",
"fieldtype": "Date",
"label": "Stock Frozen Upto"
},
{
+ "description": "Stock transactions that are older than the mentioned days cannot be modified.",
"fieldname": "stock_frozen_upto_days",
"fieldtype": "Int",
"label": "Freeze Stocks Older Than (Days)"
},
{
+ "depends_on": "eval:(doc.stock_frozen_upto || doc.stock_frozen_upto_days)",
+ "description": "The users with this Role are allowed to create/modify a stock transaction, even though the transaction is frozen.",
"fieldname": "stock_auth_role",
"fieldtype": "Link",
"label": "Role Allowed to Edit Frozen Stock",
@@ -210,6 +212,28 @@
"fieldname": "allow_from_pr",
"fieldtype": "Check",
"label": "Allow Material Transfer from Purchase Receipt to Purchase Invoice"
+ },
+ {
+ "description": "If mentioned, the system will allow only the users with this Role to create or modify any stock transaction earlier than the latest stock transaction for a specific item and warehouse. If set as blank, it allows all users to create/edit back-dated transactions.",
+ "fieldname": "role_allowed_to_create_edit_back_dated_transactions",
+ "fieldtype": "Link",
+ "label": "Role Allowed to Create/Edit Back-dated Transactions",
+ "options": "Role"
+ },
+ {
+ "fieldname": "column_break_26",
+ "fieldtype": "Column Break"
+ },
+ {
+ "fieldname": "control_historical_stock_transactions_section",
+ "fieldtype": "Section Break",
+ "label": "Control Historical Stock Transactions"
+ },
+ {
+ "default": "0",
+ "fieldname": "disable_serial_no_and_batch_selector",
+ "fieldtype": "Check",
+ "label": "Disable Serial No And Batch Selector"
}
],
"icon": "icon-cog",
@@ -217,7 +241,7 @@
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
- "modified": "2020-11-23 15:26:54.225608",
+ "modified": "2021-01-18 13:15:38.352796",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock Settings",
diff --git a/erpnext/stock/doctype/stock_settings/stock_settings.py b/erpnext/stock/doctype/stock_settings/stock_settings.py
index 4c7828b..3b9608b 100644
--- a/erpnext/stock/doctype/stock_settings/stock_settings.py
+++ b/erpnext/stock/doctype/stock_settings/stock_settings.py
@@ -55,7 +55,7 @@
""")
if sle:
- frappe.throw(_("Can't change valuation method, as there are transactions against some items which does not have it's own valuation method"))
+ frappe.throw(_("Can't change the valuation method, as there are transactions against some items which do not have its own valuation method"))
def validate_clean_description_html(self):
if int(self.clean_description_html or 0) \
diff --git a/erpnext/stock/doctype/warehouse/test_warehouse.py b/erpnext/stock/doctype/warehouse/test_warehouse.py
index 3101e8a..95478f6 100644
--- a/erpnext/stock/doctype/warehouse/test_warehouse.py
+++ b/erpnext/stock/doctype/warehouse/test_warehouse.py
@@ -10,13 +10,10 @@
import erpnext
from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry
-from erpnext import set_perpetual_inventory
from erpnext.accounts.doctype.account.test_account import get_inventory_account, create_account
-
test_records = frappe.get_test_records('Warehouse')
-
class TestWarehouse(unittest.TestCase):
def setUp(self):
if not frappe.get_value('Item', '_Test Item'):
@@ -37,63 +34,63 @@
self.assertEqual(child_warehouse.is_group, 0)
def test_warehouse_renaming(self):
- set_perpetual_inventory(1)
- create_warehouse("Test Warehouse for Renaming 1")
- account = get_inventory_account("_Test Company", "Test Warehouse for Renaming 1 - _TC")
+ create_warehouse("Test Warehouse for Renaming 1", company="_Test Company with perpetual inventory")
+ account = get_inventory_account("_Test Company with perpetual inventory", "Test Warehouse for Renaming 1 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse", filters={"account": account}))
# Rename with abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - _TC"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - _TC", "Test Warehouse for Renaming 2 - _TC")
+ if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 2 - TCP1"):
+ frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1")
+ frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 1 - TCP1", "Test Warehouse for Renaming 2 - TCP1")
self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - _TC"}))
+ filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Rename without abbr
- if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - _TC"):
- frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC")
+ if frappe.db.exists("Warehouse", "Test Warehouse for Renaming 3 - TCP1"):
+ frappe.delete_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - _TC", "Test Warehouse for Renaming 3")
+ frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 2 - TCP1", "Test Warehouse for Renaming 3")
self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Renaming 1 - _TC"}))
+ filters={"account": "Test Warehouse for Renaming 1 - TCP1"}))
# Another rename with multiple dashes
- if frappe.db.exists("Warehouse", "Test - Warehouse - Company - _TC"):
- frappe.delete_doc("Warehouse", "Test - Warehouse - Company - _TC")
- frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - _TC", "Test - Warehouse - Company")
+ if frappe.db.exists("Warehouse", "Test - Warehouse - Company - TCP1"):
+ frappe.delete_doc("Warehouse", "Test - Warehouse - Company - TCP1")
+ frappe.rename_doc("Warehouse", "Test Warehouse for Renaming 3 - TCP1", "Test - Warehouse - Company")
def test_warehouse_merging(self):
- set_perpetual_inventory(1)
+ company = "_Test Company with perpetual inventory"
+ create_warehouse("Test Warehouse for Merging 1", company=company,
+ properties={"parent_warehouse": "All Warehouses - TCP1"})
+ create_warehouse("Test Warehouse for Merging 2", company=company,
+ properties={"parent_warehouse": "All Warehouses - TCP1"})
- create_warehouse("Test Warehouse for Merging 1")
- create_warehouse("Test Warehouse for Merging 2")
-
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - _TC",
- qty=1, rate=100)
- make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - _TC",
- qty=1, rate=100)
+ make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 1 - TCP1",
+ qty=1, rate=100, company=company)
+ make_stock_entry(item_code="_Test Item", target="Test Warehouse for Merging 2 - TCP1",
+ qty=1, rate=100, company=company)
existing_bin_qty = (
cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - _TC"}, "actual_qty"))
+ {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 1 - TCP1"}, "actual_qty"))
+ cint(frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty"))
+ {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty"))
)
- frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - _TC",
- "Test Warehouse for Merging 2 - _TC", merge=True)
+ frappe.rename_doc("Warehouse", "Test Warehouse for Merging 1 - TCP1",
+ "Test Warehouse for Merging 2 - TCP1", merge=True)
- self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - _TC"))
+ self.assertFalse(frappe.db.exists("Warehouse", "Test Warehouse for Merging 1 - TCP1"))
bin_qty = frappe.db.get_value("Bin",
- {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - _TC"}, "actual_qty")
+ {"item_code": "_Test Item", "warehouse": "Test Warehouse for Merging 2 - TCP1"}, "actual_qty")
self.assertEqual(bin_qty, existing_bin_qty)
self.assertTrue(frappe.db.get_value("Warehouse",
- filters={"account": "Test Warehouse for Merging 2 - _TC"}))
+ filters={"account": "Test Warehouse for Merging 2 - TCP1"}))
def create_warehouse(warehouse_name, properties=None, company=None):
if not company:
diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py
index cd86be3..6c84f16 100644
--- a/erpnext/stock/doctype/warehouse/warehouse.py
+++ b/erpnext/stock/doctype/warehouse/warehouse.py
@@ -29,7 +29,6 @@
self.set_onload('account', account)
load_address_and_contact(self)
-
def on_update(self):
self.update_nsm_model()
diff --git a/erpnext/stock/doctype/warehouse/warehouse_tree.js b/erpnext/stock/doctype/warehouse/warehouse_tree.js
index 918d2f1..3665c05 100644
--- a/erpnext/stock/doctype/warehouse/warehouse_tree.js
+++ b/erpnext/stock/doctype/warehouse/warehouse_tree.js
@@ -19,7 +19,7 @@
ignore_fields:["parent_warehouse"],
onrender: function(node) {
if (node.data && node.data.balance!==undefined) {
- $('<span class="balance-area pull-right text-muted small">'
+ $('<span class="balance-area pull-right">'
+ format_currency(Math.abs(node.data.balance), node.data.company_currency)
+ '</span>').insertBefore(node.$ul);
}
diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py
index 08f7a83..873cfec 100644
--- a/erpnext/stock/get_item_details.py
+++ b/erpnext/stock/get_item_details.py
@@ -19,7 +19,7 @@
from six import string_types, iteritems
-sales_doctypes = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']
+sales_doctypes = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice', 'POS Invoice']
purchase_doctypes = ['Material Request', 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice']
@frappe.whitelist()
@@ -74,7 +74,9 @@
update_party_blanket_order(args, out)
- get_price_list_rate(args, item, out)
+ if not doc or cint(doc.get('is_return')) == 0:
+ # get price list rate only if the invoice is not a credit or debit note
+ get_price_list_rate(args, item, out)
if args.customer and cint(args.is_pos):
out.update(get_pos_profile_item_details(args.company, args))
@@ -672,6 +674,8 @@
and price_list=%(price_list)s
and ifnull(uom, '') in ('', %(uom)s)"""
+ conditions += "and ifnull(batch_no, '') in ('', %(batch_no)s)"
+
if not ignore_party:
if args.get("customer"):
conditions += " and customer=%(customer)s"
@@ -690,7 +694,7 @@
return frappe.db.sql(""" select name, price_list_rate, uom
from `tabItem Price` {conditions}
- order by valid_from desc, uom desc """.format(conditions=conditions), args)
+ order by valid_from desc, batch_no desc, uom desc """.format(conditions=conditions), args)
def get_price_list_rate_for(args, item_code):
"""
@@ -709,6 +713,7 @@
"uom": args.get('uom'),
"transaction_date": args.get('transaction_date'),
"posting_date": args.get('posting_date'),
+ "batch_no": args.get('batch_no')
}
item_price_data = 0
diff --git a/erpnext/stock/landed_taxes_and_charges_common.js b/erpnext/stock/landed_taxes_and_charges_common.js
new file mode 100644
index 0000000..f3f6196
--- /dev/null
+++ b/erpnext/stock/landed_taxes_and_charges_common.js
@@ -0,0 +1,62 @@
+let document_list = ['Landed Cost Voucher', 'Stock Entry'];
+
+document_list.forEach((doctype) => {
+ frappe.ui.form.on(doctype, {
+ refresh: function(frm) {
+ let tax_field = frm.doc.doctype == 'Landed Cost Voucher' ? 'taxes' : 'additional_costs';
+ frm.set_query("expense_account", tax_field, function() {
+ return {
+ filters: {
+ "account_type": ['in', ["Tax", "Chargeable", "Income Account", "Expenses Included In Valuation", "Expenses Included In Asset Valuation"]],
+ "company": frm.doc.company
+ }
+ };
+ });
+ },
+
+ set_account_currency: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ if (row.expense_account) {
+ frappe.db.get_value('Account', row.expense_account, 'account_currency', function(value) {
+ frappe.model.set_value(cdt, cdn, "account_currency", value.account_currency);
+ frm.events.set_exchange_rate(frm, cdt, cdn);
+ });
+ }
+ },
+
+ set_exchange_rate: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency;
+
+ if (row.account_currency == company_currency) {
+ row.exchange_rate = 1;
+ frm.set_df_property('taxes', 'hidden', 1, row.name, 'exchange_rate');
+ } else if (!row.exchange_rate || row.exchange_rate == 1) {
+ frm.set_df_property('taxes', 'hidden', 0, row.name, 'exchange_rate');
+ frappe.call({
+ method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_exchange_rate",
+ args: {
+ posting_date: frm.doc.posting_date,
+ account: row.expense_account,
+ account_currency: row.account_currency,
+ company: frm.doc.company
+ },
+ callback: function(r) {
+ if (r.message) {
+ frappe.model.set_value(cdt, cdn, "exchange_rate", r.message);
+ }
+ }
+ });
+ }
+
+ frm.refresh_field('taxes');
+ },
+
+ set_base_amount: function(frm, cdt, cdn) {
+ let row = locals[cdt][cdn];
+ frappe.model.set_value(cdt, cdn, "base_amount",
+ flt(flt(row.amount)*row.exchange_rate, precision("base_amount", row)));
+ }
+ });
+});
+
diff --git a/erpnext/stock/module_onboarding/stock/stock.json b/erpnext/stock/module_onboarding/stock/stock.json
index 1d5bf8c..8474648 100644
--- a/erpnext/stock/module_onboarding/stock/stock.json
+++ b/erpnext/stock/module_onboarding/stock/stock.json
@@ -19,7 +19,7 @@
"documentation_url": "https://docs.erpnext.com/docs/user/manual/en/stock",
"idx": 0,
"is_complete": 0,
- "modified": "2020-07-08 14:22:07.951891",
+ "modified": "2020-10-14 14:54:42.741971",
"modified_by": "Administrator",
"module": "Stock",
"name": "Stock",
diff --git a/erpnext/stock/onboarding_step/create_a_product/create_a_product.json b/erpnext/stock/onboarding_step/create_a_product/create_a_product.json
index d2068e1..335137d 100644
--- a/erpnext/stock/onboarding_step/create_a_product/create_a_product.json
+++ b/erpnext/stock/onboarding_step/create_a_product/create_a_product.json
@@ -8,7 +8,7 @@
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-12 18:30:02.489949",
+ "modified": "2020-10-14 14:53:00.133574",
"modified_by": "Administrator",
"name": "Create a Product",
"owner": "Administrator",
diff --git a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json b/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json
index b7811a4..9012493 100644
--- a/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json
+++ b/erpnext/stock/onboarding_step/create_a_purchase_receipt/create_a_purchase_receipt.json
@@ -8,7 +8,7 @@
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-19 18:59:13.266713",
+ "modified": "2020-10-14 14:53:25.618434",
"modified_by": "Administrator",
"name": "Create a Purchase Receipt",
"owner": "Administrator",
diff --git a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json
index 2b83f65..09902b8 100644
--- a/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json
+++ b/erpnext/stock/onboarding_step/create_a_stock_entry/create_a_stock_entry.json
@@ -8,7 +8,7 @@
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-15 03:30:58.047696",
+ "modified": "2020-10-14 14:53:00.105905",
"modified_by": "Administrator",
"name": "Create a Stock Entry",
"owner": "Administrator",
diff --git a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json
index 7a64224..ef61fa3 100644
--- a/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json
+++ b/erpnext/stock/onboarding_step/create_a_supplier/create_a_supplier.json
@@ -8,7 +8,7 @@
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-14 22:09:10.043554",
+ "modified": "2020-10-14 14:53:00.120455",
"modified_by": "Administrator",
"name": "Create a Supplier",
"owner": "Administrator",
diff --git a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json
index 009a44f..212e505 100644
--- a/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json
+++ b/erpnext/stock/onboarding_step/introduction_to_stock_entry/introduction_to_stock_entry.json
@@ -8,7 +8,7 @@
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-05-26 15:55:41.457289",
+ "modified": "2020-10-14 14:53:00.075177",
"modified_by": "Administrator",
"name": "Introduction to Stock Entry",
"owner": "Administrator",
diff --git a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json
index 9457dee..75940ed 100644
--- a/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json
+++ b/erpnext/stock/onboarding_step/setup_your_warehouse/setup_your_warehouse.json
@@ -8,7 +8,7 @@
"is_mandatory": 0,
"is_single": 0,
"is_skipped": 0,
- "modified": "2020-07-04 12:33:16.970031",
+ "modified": "2020-10-14 14:53:25.538900",
"modified_by": "Administrator",
"name": "Setup your Warehouse",
"owner": "Administrator",
diff --git a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json
index 7591bff..ae34afa 100644
--- a/erpnext/stock/onboarding_step/stock_settings/stock_settings.json
+++ b/erpnext/stock/onboarding_step/stock_settings/stock_settings.json
@@ -8,7 +8,7 @@
"is_mandatory": 0,
"is_single": 1,
"is_skipped": 0,
- "modified": "2020-05-15 03:55:15.444151",
+ "modified": "2020-10-14 14:53:00.092504",
"modified_by": "Administrator",
"name": "Stock Settings",
"owner": "Administrator",
diff --git a/erpnext/stock/page/stock_balance/stock_balance.js b/erpnext/stock/page/stock_balance/stock_balance.js
index da21c6b..bddffd4 100644
--- a/erpnext/stock/page/stock_balance/stock_balance.js
+++ b/erpnext/stock/page/stock_balance/stock_balance.js
@@ -65,6 +65,9 @@
frappe.require('assets/js/item-dashboard.min.js', function() {
page.item_dashboard = new erpnext.stock.ItemDashboard({
parent: page.main,
+ page_length: 20,
+ method: 'erpnext.stock.dashboard.item_dashboard.get_data',
+ template: 'item_dashboard_list'
})
page.item_dashboard.before_refresh = function() {
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/stock/page/warehouse_capacity_summary/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/stock/page/warehouse_capacity_summary/__init__.py
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html
new file mode 100644
index 0000000..90112c7
--- /dev/null
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.html
@@ -0,0 +1,40 @@
+{% for d in data %}
+ <div class="dashboard-list-item" style="padding: 7px 15px;">
+ <div class="row">
+ <div class="col-sm-2 small" style="margin-top: 8px;">
+ <a data-type="warehouse" data-name="{{ d.warehouse }}">{{ d.warehouse }}</a>
+ </div>
+ <div class="col-sm-2 small" style="margin-top: 8px; ">
+ <a data-type="item" data-name="{{ d.item_code }}">{{ d.item_code }}</a>
+ </div>
+ <div class="col-sm-1 small" style="margin-top: 8px; ">
+ {{ d.stock_capacity }}
+ </div>
+ <div class="col-sm-2 small" style="margin-top: 8px; ">
+ {{ d.actual_qty }}
+ </div>
+ <div class="col-sm-2 small">
+ <div class="progress" title="Occupied Qty: {{ d.actual_qty }}" style="margin-bottom: 4px; height: 7px; margin-top: 14px;">
+ <div class="progress-bar" role="progressbar"
+ aria-valuenow="{{ d.percent_occupied }}"
+ aria-valuemin="0" aria-valuemax="100"
+ style="width:{{ d.percent_occupied }}%;
+ background-color: {{ d.color }}">
+ </div>
+ </div>
+ </div>
+ <div class="col-sm-1 small" style="margin-top: 8px;">
+ {{ d.percent_occupied }}%
+ </div>
+ {% if can_write %}
+ <div class="col-sm-1 text-right" style="margin-top: 2px;">
+ <button class="btn btn-default btn-xs btn-edit"
+ style="margin-top: 4px;margin-bottom: 4px;"
+ data-warehouse="{{ d.warehouse }}"
+ data-item="{{ escape(d.item_code) }}"
+ data-company="{{ escape(d.company) }}">{{ __("Edit Capacity") }}</a>
+ </div>
+ {% endif %}
+ </div>
+ </div>
+{% endfor %}
\ No newline at end of file
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
new file mode 100644
index 0000000..b610e7d
--- /dev/null
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.js
@@ -0,0 +1,120 @@
+frappe.pages['warehouse-capacity-summary'].on_page_load = function(wrapper) {
+ var page = frappe.ui.make_app_page({
+ parent: wrapper,
+ title: 'Warehouse Capacity Summary',
+ single_column: true
+ });
+ page.set_secondary_action('Refresh', () => page.capacity_dashboard.refresh(), 'octicon octicon-sync');
+ page.start = 0;
+
+ page.company_field = page.add_field({
+ fieldname: 'company',
+ label: __('Company'),
+ fieldtype: 'Link',
+ options: 'Company',
+ reqd: 1,
+ default: frappe.defaults.get_default("company"),
+ change: function() {
+ page.capacity_dashboard.start = 0;
+ page.capacity_dashboard.refresh();
+ }
+ });
+
+ page.warehouse_field = page.add_field({
+ fieldname: 'warehouse',
+ label: __('Warehouse'),
+ fieldtype: 'Link',
+ options: 'Warehouse',
+ change: function() {
+ page.capacity_dashboard.start = 0;
+ page.capacity_dashboard.refresh();
+ }
+ });
+
+ page.item_field = page.add_field({
+ fieldname: 'item_code',
+ label: __('Item'),
+ fieldtype: 'Link',
+ options: 'Item',
+ change: function() {
+ page.capacity_dashboard.start = 0;
+ page.capacity_dashboard.refresh();
+ }
+ });
+
+ page.parent_warehouse_field = page.add_field({
+ fieldname: 'parent_warehouse',
+ label: __('Parent Warehouse'),
+ fieldtype: 'Link',
+ options: 'Warehouse',
+ get_query: function() {
+ return {
+ filters: {
+ "is_group": 1
+ }
+ };
+ },
+ change: function() {
+ page.capacity_dashboard.start = 0;
+ page.capacity_dashboard.refresh();
+ }
+ });
+
+ page.sort_selector = new frappe.ui.SortSelector({
+ parent: page.wrapper.find('.page-form'),
+ args: {
+ sort_by: 'stock_capacity',
+ sort_order: 'desc',
+ options: [
+ {fieldname: 'stock_capacity', label: __('Capacity (Stock UOM)')},
+ {fieldname: 'percent_occupied', label: __('% Occupied')},
+ {fieldname: 'actual_qty', label: __('Balance Qty (Stock ')}
+ ]
+ },
+ change: function(sort_by, sort_order) {
+ page.capacity_dashboard.sort_by = sort_by;
+ page.capacity_dashboard.sort_order = sort_order;
+ page.capacity_dashboard.start = 0;
+ page.capacity_dashboard.refresh();
+ }
+ });
+
+ frappe.require('assets/js/item-dashboard.min.js', function() {
+ $(frappe.render_template('warehouse_capacity_summary_header')).appendTo(page.main);
+
+ page.capacity_dashboard = new erpnext.stock.ItemDashboard({
+ page_name: "warehouse-capacity-summary",
+ page_length: 10,
+ parent: page.main,
+ sort_by: 'stock_capacity',
+ sort_order: 'desc',
+ method: 'erpnext.stock.dashboard.warehouse_capacity_dashboard.get_data',
+ template: 'warehouse_capacity_summary'
+ });
+
+ page.capacity_dashboard.before_refresh = function() {
+ this.item_code = page.item_field.get_value();
+ this.warehouse = page.warehouse_field.get_value();
+ this.parent_warehouse = page.parent_warehouse_field.get_value();
+ this.company = page.company_field.get_value();
+ };
+
+ page.capacity_dashboard.refresh();
+
+ let setup_click = function(doctype) {
+ page.main.on('click', 'a[data-type="'+ doctype.toLowerCase() +'"]', function() {
+ var name = $(this).attr('data-name');
+ var field = page[doctype.toLowerCase() + '_field'];
+ if (field.get_value()===name) {
+ frappe.set_route('Form', doctype, name);
+ } else {
+ field.set_input(name);
+ page.capacity_dashboard.refresh();
+ }
+ });
+ };
+
+ setup_click('Item');
+ setup_click('Warehouse');
+ });
+};
\ No newline at end of file
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json
new file mode 100644
index 0000000..a6e5b45
--- /dev/null
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary.json
@@ -0,0 +1,26 @@
+{
+ "content": null,
+ "creation": "2020-11-25 12:07:54.056208",
+ "docstatus": 0,
+ "doctype": "Page",
+ "idx": 0,
+ "modified": "2020-11-25 11:07:54.056208",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "warehouse-capacity-summary",
+ "owner": "Administrator",
+ "page_name": "Warehouse Capacity Summary",
+ "roles": [
+ {
+ "role": "Stock User"
+ },
+ {
+ "role": "Stock Manager"
+ }
+ ],
+ "script": null,
+ "standard": "Yes",
+ "style": null,
+ "system_page": 0,
+ "title": "Warehouse Capacity Summary"
+}
\ No newline at end of file
diff --git a/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html
new file mode 100644
index 0000000..acaf180
--- /dev/null
+++ b/erpnext/stock/page/warehouse_capacity_summary/warehouse_capacity_summary_header.html
@@ -0,0 +1,19 @@
+<div class="dashboard-list-item" style="padding: 12px 15px;">
+ <div class="row">
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ Warehouse
+ </div>
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ Item
+ </div>
+ <div class="col-sm-1 small text-muted" style="margin-top: 8px;">
+ Stock Capacity
+ </div>
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ Balance Stock Qty
+ </div>
+ <div class="col-sm-2 small text-muted" style="margin-top: 8px;">
+ % Occupied
+ </div>
+ </div>
+</div>
\ No newline at end of file
diff --git a/erpnext/stock/report/stock_ageing/stock_ageing.py b/erpnext/stock/report/stock_ageing/stock_ageing.py
index 8aaf7ab..ff603fc 100644
--- a/erpnext/stock/report/stock_ageing/stock_ageing.py
+++ b/erpnext/stock/report/stock_ageing/stock_ageing.py
@@ -233,7 +233,8 @@
from `tabItem` {item_conditions}) item
where item_code = item.name and
company = %(company)s and
- posting_date <= %(to_date)s
+ posting_date <= %(to_date)s and
+ is_cancelled != 1
{sle_conditions}
order by posting_date, posting_time, sle.creation, actual_qty""" #nosec
.format(item_conditions=get_item_conditions(filters),
diff --git a/erpnext/stock/report/stock_analytics/stock_analytics.py b/erpnext/stock/report/stock_analytics/stock_analytics.py
index 54eefdf..0cc8ca4 100644
--- a/erpnext/stock/report/stock_analytics/stock_analytics.py
+++ b/erpnext/stock/report/stock_analytics/stock_analytics.py
@@ -7,9 +7,11 @@
from frappe.utils import getdate, flt
from erpnext.stock.report.stock_balance.stock_balance import (get_items, get_stock_ledger_entries, get_item_details)
from erpnext.accounts.utils import get_fiscal_year
+from erpnext.stock.utils import is_reposting_item_valuation_in_progress
from six import iteritems
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
filters = frappe._dict(filters or {})
columns = get_columns(filters)
data = get_data(filters)
diff --git a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
index 1af68dd..14d543b 100644
--- a/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
+++ b/erpnext/stock/report/stock_and_account_value_comparison/stock_and_account_value_comparison.py
@@ -57,8 +57,7 @@
if report_filters.account:
stock_accounts = [report_filters.account]
else:
- stock_accounts = [k.name
- for k in get_stock_accounts(report_filters.company)]
+ stock_accounts = get_stock_accounts(report_filters.company)
filters.update({
"account": ("in", stock_accounts)
diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py
index ccd0100..e5d4d62 100644
--- a/erpnext/stock/report/stock_balance/stock_balance.py
+++ b/erpnext/stock/report/stock_balance/stock_balance.py
@@ -7,12 +7,13 @@
from frappe.utils import flt, cint, getdate, now, date_diff
from erpnext.stock.utils import add_additional_uom_columns
from erpnext.stock.report.stock_ledger.stock_ledger import get_item_group_condition
-
+from erpnext.stock.utils import is_reposting_item_valuation_in_progress
from erpnext.stock.report.stock_ageing.stock_ageing import get_fifo_queue, get_average_age
from six import iteritems
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
if not filters: filters = {}
validate_filters(filters)
diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py
index 86af5e0..7b5701a 100644
--- a/erpnext/stock/report/stock_ledger/stock_ledger.py
+++ b/erpnext/stock/report/stock_ledger/stock_ledger.py
@@ -5,11 +5,12 @@
import frappe
from frappe.utils import cint, flt
-from erpnext.stock.utils import update_included_uom_in_report
+from erpnext.stock.utils import update_included_uom_in_report, is_reposting_item_valuation_in_progress
from frappe import _
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
include_uom = filters.get("include_uom")
columns = get_columns()
items = get_items(filters)
diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
index c8efb16..1183e41 100644
--- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
+++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py
@@ -5,9 +5,10 @@
import frappe
from frappe import _
from frappe.utils import flt, today
-from erpnext.stock.utils import update_included_uom_in_report
+from erpnext.stock.utils import update_included_uom_in_report, is_reposting_item_valuation_in_progress
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
filters = frappe._dict(filters or {})
include_uom = filters.get("include_uom")
columns = get_columns()
diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
index ebcb106..04f7d34 100644
--- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
+++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py
@@ -11,9 +11,11 @@
from erpnext.stock.report.stock_balance.stock_balance import (get_item_details,
get_item_reorder_details, get_item_warehouse_map, get_items, get_stock_ledger_entries)
from erpnext.stock.report.stock_ageing.stock_ageing import get_fifo_queue, get_average_age
+from erpnext.stock.utils import is_reposting_item_valuation_in_progress
from six import iteritems
def execute(filters=None):
+ is_reposting_item_valuation_in_progress()
if not filters: filters = {}
validate_filters(filters)
diff --git a/erpnext/stock/stock_balance.py b/erpnext/stock/stock_balance.py
index b5ae1b7..8ba1f1c 100644
--- a/erpnext/stock/stock_balance.py
+++ b/erpnext/stock/stock_balance.py
@@ -6,6 +6,7 @@
from frappe.utils import flt, cstr, nowdate, nowtime
from erpnext.stock.utils import update_bin
from erpnext.stock.stock_ledger import update_entries_after
+from erpnext.controllers.stock_controller import create_repost_item_valuation_entry
def repost(only_actual=False, allow_negative_stock=False, allow_zero_rate=False, only_bin=False):
"""
@@ -56,12 +57,18 @@
update_bin_qty(item_code, warehouse, qty_dict)
def repost_actual_qty(item_code, warehouse, allow_zero_rate=False, allow_negative_stock=False):
- update_entries_after({ "item_code": item_code, "warehouse": warehouse },
- allow_zero_rate=allow_zero_rate, allow_negative_stock=allow_negative_stock)
+ create_repost_item_valuation_entry({
+ "item_code": item_code,
+ "warehouse": warehouse,
+ "posting_date": "1900-01-01",
+ "posting_time": "00:01",
+ "allow_negative_stock": allow_negative_stock,
+ "allow_zero_rate": allow_zero_rate
+ })
def get_balance_qty_from_sle(item_code, warehouse):
balance_qty = frappe.db.sql("""select qty_after_transaction from `tabStock Ledger Entry`
- where item_code=%s and warehouse=%s
+ where item_code=%s and warehouse=%s and is_cancelled=0
order by posting_date desc, posting_time desc, creation desc
limit 1""", (item_code, warehouse))
@@ -191,7 +198,7 @@
print(d[0], d[1], d[2], serial_nos[0][0])
sle = frappe.db.sql("""select valuation_rate, company from `tabStock Ledger Entry`
- where item_code = %s and warehouse = %s
+ where item_code = %s and warehouse = %s and is_cancelled = 0
order by posting_date desc limit 1""", (d[0], d[1]))
sle_dict = {
@@ -223,7 +230,8 @@
})
update_bin(args)
- update_entries_after({
+
+ create_repost_item_valuation_entry({
"item_code": d[0],
"warehouse": d[1],
"posting_date": posting_date,
diff --git a/erpnext/stock/stock_ledger.py b/erpnext/stock/stock_ledger.py
index f4490f1..f54b3c1 100644
--- a/erpnext/stock/stock_ledger.py
+++ b/erpnext/stock/stock_ledger.py
@@ -5,9 +5,10 @@
import frappe, erpnext
from frappe import _
from frappe.utils import cint, flt, cstr, now, now_datetime
+from frappe.model.meta import get_field_precision
from erpnext.stock.utils import get_valuation_method, get_incoming_outgoing_rate_for_cancel
+from erpnext.stock.utils import get_bin
import json
-
from six import iteritems
# future reposting
@@ -22,37 +23,44 @@
cancel = sl_entries[0].get("is_cancelled")
if cancel:
+ validate_cancellation(sl_entries)
set_as_cancel(sl_entries[0].get('voucher_type'), sl_entries[0].get('voucher_no'))
for sle in sl_entries:
- sle_id = None
- if via_landed_cost_voucher or cancel:
- sle['posting_date'] = now_datetime().strftime('%Y-%m-%d')
- sle['posting_time'] = now_datetime().strftime('%H:%M:%S.%f')
+ if cancel:
+ sle['actual_qty'] = -flt(sle.get('actual_qty'))
- if cancel:
- sle['actual_qty'] = -flt(sle.get('actual_qty'))
+ if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'):
+ sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
+ sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
+ sle['incoming_rate'] = 0.0
- if sle['actual_qty'] < 0 and not sle.get('outgoing_rate'):
- sle['outgoing_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
- sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
- sle['incoming_rate'] = 0.0
-
- if sle['actual_qty'] > 0 and not sle.get('incoming_rate'):
- sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
- sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
- sle['outgoing_rate'] = 0.0
-
+ if sle['actual_qty'] > 0 and not sle.get('incoming_rate'):
+ sle['incoming_rate'] = get_incoming_outgoing_rate_for_cancel(sle.item_code,
+ sle.voucher_type, sle.voucher_no, sle.voucher_detail_no)
+ sle['outgoing_rate'] = 0.0
if sle.get("actual_qty") or sle.get("voucher_type")=="Stock Reconciliation":
- sle_id = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
+ sle_doc = make_entry(sle, allow_negative_stock, via_landed_cost_voucher)
- args = sle.copy()
- args.update({
- "sle_id": sle_id
- })
+ args = sle_doc.as_dict()
update_bin(args, allow_negative_stock, via_landed_cost_voucher)
+def validate_cancellation(args):
+ if args[0].get("is_cancelled"):
+ repost_entry = frappe.db.get_value("Repost Item Valuation", {
+ 'voucher_type': args[0].voucher_type,
+ 'voucher_no': args[0].voucher_no,
+ 'docstatus': 1
+ }, ['name', 'status'], as_dict=1)
+
+ if repost_entry:
+ if repost_entry.status == 'In Progress':
+ frappe.throw(_("Cannot cancel the transaction. Reposting of item valuation on submission is not completed yet."))
+ if repost_entry.status == 'Queued':
+ doc = frappe.get_doc("Repost Item Valuation", repost_entry.name)
+ doc.cancel()
+ doc.delete()
def set_as_cancel(voucher_type, voucher_no):
frappe.db.sql("""update `tabStock Ledger Entry` set is_cancelled=1,
@@ -68,8 +76,37 @@
sle.via_landed_cost_voucher = via_landed_cost_voucher
sle.insert()
sle.submit()
- return sle.name
+ return sle
+def repost_future_sle(args=None, voucher_type=None, voucher_no=None, allow_negative_stock=None, via_landed_cost_voucher=False):
+ if not args and voucher_type and voucher_no:
+ args = get_args_for_voucher(voucher_type, voucher_no)
+
+ distinct_item_warehouses = [(d.item_code, d.warehouse) for d in args]
+
+ i = 0
+ while i < len(args):
+ obj = update_entries_after({
+ "item_code": args[i].item_code,
+ "warehouse": args[i].warehouse,
+ "posting_date": args[i].posting_date,
+ "posting_time": args[i].posting_time,
+ "creation": args[i].get("creation")
+ }, allow_negative_stock=allow_negative_stock, via_landed_cost_voucher=via_landed_cost_voucher)
+
+ for item_wh, new_sle in iteritems(obj.new_items):
+ if item_wh not in distinct_item_warehouses:
+ args.append(new_sle)
+
+ i += 1
+
+def get_args_for_voucher(voucher_type, voucher_no):
+ return frappe.db.get_all("Stock Ledger Entry",
+ filters={"voucher_type": voucher_type, "voucher_no": voucher_no},
+ fields=["item_code", "warehouse", "posting_date", "posting_time", "creation"],
+ order_by="creation asc",
+ group_by="item_code, warehouse"
+ )
class update_entries_after(object):
"""
@@ -86,141 +123,328 @@
}
"""
def __init__(self, args, allow_zero_rate=False, allow_negative_stock=None, via_landed_cost_voucher=False, verbose=1):
- from frappe.model.meta import get_field_precision
-
- self.exceptions = []
+ self.exceptions = {}
self.verbose = verbose
self.allow_zero_rate = allow_zero_rate
- self.allow_negative_stock = allow_negative_stock
self.via_landed_cost_voucher = via_landed_cost_voucher
- if not self.allow_negative_stock:
- self.allow_negative_stock = cint(frappe.db.get_single_value("Stock Settings",
- "allow_negative_stock"))
+ self.allow_negative_stock = allow_negative_stock \
+ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
- self.args = args
- for key, value in iteritems(args):
- setattr(self, key, value)
+ self.args = frappe._dict(args)
+ self.item_code = args.get("item_code")
+ if self.args.sle_id:
+ self.args['name'] = self.args.sle_id
- self.previous_sle = self.get_sle_before_datetime()
- self.previous_sle = self.previous_sle[0] if self.previous_sle else frappe._dict()
+ self.company = frappe.get_cached_value("Warehouse", self.args.warehouse, "company")
+ self.get_precision()
+ self.valuation_method = get_valuation_method(self.item_code)
+ self.new_items = {}
+
+ self.data = frappe._dict()
+ self.initialize_previous_data(self.args)
+
+ self.build()
+
+ def get_precision(self):
+ company_base_currency = frappe.get_cached_value('Company', self.company, "default_currency")
+ self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
+ currency=company_base_currency)
+
+ def initialize_previous_data(self, args):
+ """
+ Get previous sl entries for current item for each related warehouse
+ and assigns into self.data dict
+
+ :Data Structure:
+
+ self.data = {
+ warehouse1: {
+ 'previus_sle': {},
+ 'qty_after_transaction': 10,
+ 'valuation_rate': 100,
+ 'stock_value': 1000,
+ 'prev_stock_value': 1000,
+ 'stock_queue': '[[10, 100]]',
+ 'stock_value_difference': 1000
+ }
+ }
+
+ """
+ self.data.setdefault(args.warehouse, frappe._dict())
+ warehouse_dict = self.data[args.warehouse]
+ previous_sle = self.get_previous_sle_of_current_voucher(args)
+ warehouse_dict.previous_sle = previous_sle
for key in ("qty_after_transaction", "valuation_rate", "stock_value"):
- setattr(self, key, flt(self.previous_sle.get(key)))
+ setattr(warehouse_dict, key, flt(previous_sle.get(key)))
- self.company = frappe.db.get_value("Warehouse", self.warehouse, "company")
- self.precision = get_field_precision(frappe.get_meta("Stock Ledger Entry").get_field("stock_value"),
- currency=frappe.get_cached_value('Company', self.company, "default_currency"))
+ warehouse_dict.update({
+ "prev_stock_value": previous_sle.stock_value or 0.0,
+ "stock_queue": json.loads(previous_sle.stock_queue or "[]"),
+ "stock_value_difference": 0.0
+ })
- self.prev_stock_value = self.previous_sle.stock_value or 0.0
- self.stock_queue = json.loads(self.previous_sle.stock_queue or "[]")
- self.valuation_method = get_valuation_method(self.item_code)
- self.stock_value_difference = 0.0
- self.build(args.get('sle_id'))
+ def get_previous_sle_of_current_voucher(self, args):
+ """get stock ledger entries filtered by specific posting datetime conditions"""
- def build(self, sle_id):
- if sle_id:
- sle = get_sle_by_id(sle_id)
- self.process_sle(sle)
+ args['time_format'] = '%H:%i:%s'
+ if not args.get("posting_date"):
+ args["posting_date"] = "1900-01-01"
+ if not args.get("posting_time"):
+ args["posting_time"] = "00:00"
+
+ sle = frappe.db.sql("""
+ select *, timestamp(posting_date, posting_time) as "timestamp"
+ from `tabStock Ledger Entry`
+ where item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and is_cancelled = 0
+ and timestamp(posting_date, time_format(posting_time, %(time_format)s)) < timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
+ order by timestamp(posting_date, posting_time) desc, creation desc
+ limit 1""", args, as_dict=1)
+
+ return sle[0] if sle else frappe._dict()
+
+
+ def build(self):
+ from erpnext.controllers.stock_controller import check_if_future_sle_exists
+
+ if self.args.get("sle_id"):
+ self.process_sle_against_current_timestamp()
+ if not check_if_future_sle_exists(self.args):
+ self.update_bin()
else:
- # includes current entry!
- entries_to_fix = self.get_sle_after_datetime()
- for sle in entries_to_fix:
+ entries_to_fix = self.get_future_entries_to_fix()
+
+ i = 0
+ while i < len(entries_to_fix):
+ sle = entries_to_fix[i]
+ i += 1
+
self.process_sle(sle)
+ if sle.dependant_sle_voucher_detail_no:
+ entries_to_fix = self.get_dependent_entries_to_fix(entries_to_fix, sle)
+
+ self.update_bin()
+
if self.exceptions:
self.raise_exceptions()
- self.update_bin()
+ def process_sle_against_current_timestamp(self):
+ sl_entries = self.get_sle_against_current_voucher()
+ for sle in sl_entries:
+ self.process_sle(sle)
- def update_bin(self):
- # update bin
- bin_name = frappe.db.get_value("Bin", {
- "item_code": self.item_code,
- "warehouse": self.warehouse
- })
+ def get_sle_against_current_voucher(self):
+ self.args['time_format'] = '%H:%i:%s'
- if not bin_name:
- bin_doc = frappe.get_doc({
- "doctype": "Bin",
- "item_code": self.item_code,
- "warehouse": self.warehouse
- })
- bin_doc.insert(ignore_permissions=True)
- else:
- bin_doc = frappe.get_doc("Bin", bin_name)
+ return frappe.db.sql("""
+ select
+ *, timestamp(posting_date, posting_time) as "timestamp"
+ from
+ `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and timestamp(posting_date, time_format(posting_time, %(time_format)s)) = timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s))
- bin_doc.update({
- "valuation_rate": self.valuation_rate,
- "actual_qty": self.qty_after_transaction,
- "stock_value": self.stock_value
- })
- bin_doc.flags.via_stock_ledger_entry = True
+ order by
+ creation ASC
+ for update
+ """, self.args, as_dict=1)
- bin_doc.save(ignore_permissions=True)
+ def get_future_entries_to_fix(self):
+ # includes current entry!
+ args = self.data[self.args.warehouse].previous_sle \
+ or frappe._dict({"item_code": self.item_code, "warehouse": self.args.warehouse})
+
+ return list(self.get_sle_after_datetime(args))
+
+ def get_dependent_entries_to_fix(self, entries_to_fix, sle):
+ dependant_sle = get_sle_by_voucher_detail_no(sle.dependant_sle_voucher_detail_no,
+ excluded_sle=sle.name)
+
+ if not dependant_sle:
+ return entries_to_fix
+ elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse == self.args.warehouse:
+ return entries_to_fix
+ elif dependant_sle.item_code != self.item_code:
+ if (dependant_sle.item_code, dependant_sle.warehouse) not in self.new_items:
+ self.new_items[(dependant_sle.item_code, dependant_sle.warehouse)] = dependant_sle
+ return entries_to_fix
+ elif dependant_sle.item_code == self.item_code and dependant_sle.warehouse in self.data:
+ return entries_to_fix
+ self.initialize_previous_data(dependant_sle)
+
+ args = self.data[dependant_sle.warehouse].previous_sle \
+ or frappe._dict({"item_code": self.item_code, "warehouse": dependant_sle.warehouse})
+ future_sle_for_dependant = list(self.get_sle_after_datetime(args))
+
+ entries_to_fix.extend(future_sle_for_dependant)
+ return sorted(entries_to_fix, key=lambda k: k['timestamp'])
def process_sle(self, sle):
+ # previous sle data for this warehouse
+ self.wh_data = self.data[sle.warehouse]
+
if (sle.serial_no and not self.via_landed_cost_voucher) or not cint(self.allow_negative_stock):
# validate negative stock for serialized items, fifo valuation
# or when negative stock is not allowed for moving average
if not self.validate_negative_stock(sle):
- self.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
return
+ # Get dynamic incoming/outgoing rate
+ self.get_dynamic_incoming_outgoing_rate(sle)
+
if sle.serial_no:
self.get_serialized_values(sle)
- self.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
if sle.voucher_type == "Stock Reconciliation":
- self.qty_after_transaction = sle.qty_after_transaction
+ self.wh_data.qty_after_transaction = sle.qty_after_transaction
- self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
if sle.voucher_type=="Stock Reconciliation" and not sle.batch_no:
# assert
- self.valuation_rate = sle.valuation_rate
- self.qty_after_transaction = sle.qty_after_transaction
- self.stock_queue = [[self.qty_after_transaction, self.valuation_rate]]
- self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
+ self.wh_data.valuation_rate = sle.valuation_rate
+ self.wh_data.qty_after_transaction = sle.qty_after_transaction
+ self.wh_data.stock_queue = [[self.wh_data.qty_after_transaction, self.wh_data.valuation_rate]]
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
if self.valuation_method == "Moving Average":
self.get_moving_average_values(sle)
- self.qty_after_transaction += flt(sle.actual_qty)
- self.stock_value = flt(self.qty_after_transaction) * flt(self.valuation_rate)
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.stock_value = flt(self.wh_data.qty_after_transaction) * flt(self.wh_data.valuation_rate)
else:
self.get_fifo_values(sle)
- self.qty_after_transaction += flt(sle.actual_qty)
- self.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue))
+ self.wh_data.qty_after_transaction += flt(sle.actual_qty)
+ self.wh_data.stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))
# rounding as per precision
- self.stock_value = flt(self.stock_value, self.precision)
-
- stock_value_difference = self.stock_value - self.prev_stock_value
-
- self.prev_stock_value = self.stock_value
+ self.wh_data.stock_value = flt(self.wh_data.stock_value, self.precision)
+ stock_value_difference = self.wh_data.stock_value - self.wh_data.prev_stock_value
+ self.wh_data.prev_stock_value = self.wh_data.stock_value
# update current sle
- sle.qty_after_transaction = self.qty_after_transaction
- sle.valuation_rate = self.valuation_rate
- sle.stock_value = self.stock_value
- sle.stock_queue = json.dumps(self.stock_queue)
+ sle.qty_after_transaction = self.wh_data.qty_after_transaction
+ sle.valuation_rate = self.wh_data.valuation_rate
+ sle.stock_value = self.wh_data.stock_value
+ sle.stock_queue = json.dumps(self.wh_data.stock_queue)
sle.stock_value_difference = stock_value_difference
sle.doctype="Stock Ledger Entry"
frappe.get_doc(sle).db_update()
+ self.update_outgoing_rate_on_transaction(sle)
+
def validate_negative_stock(self, sle):
"""
validate negative stock for entries current datetime onwards
will not consider cancelled entries
"""
- diff = self.qty_after_transaction + flt(sle.actual_qty)
+ diff = self.wh_data.qty_after_transaction + flt(sle.actual_qty)
if diff < 0 and abs(diff) > 0.0001:
# negative stock!
exc = sle.copy().update({"diff": diff})
- self.exceptions.append(exc)
+ self.exceptions.setdefault(sle.warehouse, []).append(exc)
return False
else:
return True
+ def get_dynamic_incoming_outgoing_rate(self, sle):
+ # Get updated incoming/outgoing rate from transaction
+ if sle.recalculate_rate:
+ rate = self.get_incoming_outgoing_rate_from_transaction(sle)
+
+ if flt(sle.actual_qty) >= 0:
+ sle.incoming_rate = rate
+ else:
+ sle.outgoing_rate = rate
+
+ def get_incoming_outgoing_rate_from_transaction(self, sle):
+ rate = 0
+ # Material Transfer, Repack, Manufacturing
+ if sle.voucher_type == "Stock Entry":
+ rate = frappe.db.get_value("Stock Entry Detail", sle.voucher_detail_no, "valuation_rate")
+ # Sales and Purchase Return
+ elif sle.voucher_type in ("Purchase Receipt", "Purchase Invoice", "Delivery Note", "Sales Invoice"):
+ if frappe.get_cached_value(sle.voucher_type, sle.voucher_no, "is_return"):
+ from erpnext.controllers.sales_and_purchase_return import get_rate_for_return # don't move this import to top
+ rate = get_rate_for_return(sle.voucher_type, sle.voucher_no, sle.item_code, voucher_detail_no=sle.voucher_detail_no)
+ else:
+ if sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
+ rate_field = "valuation_rate"
+ else:
+ rate_field = "incoming_rate"
+
+ # check in item table
+ item_code, incoming_rate = frappe.db.get_value(sle.voucher_type + " Item",
+ sle.voucher_detail_no, ["item_code", rate_field])
+
+ if item_code == sle.item_code:
+ rate = incoming_rate
+ else:
+ if sle.voucher_type in ("Delivery Note", "Sales Invoice"):
+ ref_doctype = "Packed Item"
+ else:
+ ref_doctype = "Purchase Receipt Item Supplied"
+
+ rate = frappe.db.get_value(ref_doctype, {"parent_detail_docname": sle.voucher_detail_no,
+ "item_code": sle.item_code}, rate_field)
+
+ return rate
+
+ def update_outgoing_rate_on_transaction(self, sle):
+ """
+ Update outgoing rate in Stock Entry, Delivery Note, Sales Invoice and Sales Return
+ In case of Stock Entry, also calculate FG Item rate and total incoming/outgoing amount
+ """
+ if sle.actual_qty and sle.voucher_detail_no:
+ outgoing_rate = abs(flt(sle.stock_value_difference)) / abs(sle.actual_qty)
+
+ if flt(sle.actual_qty) < 0 and sle.voucher_type == "Stock Entry":
+ self.update_rate_on_stock_entry(sle, outgoing_rate)
+ elif sle.voucher_type in ("Delivery Note", "Sales Invoice"):
+ self.update_rate_on_delivery_and_sales_return(sle, outgoing_rate)
+ elif flt(sle.actual_qty) < 0 and sle.voucher_type in ("Purchase Receipt", "Purchase Invoice"):
+ self.update_rate_on_purchase_receipt(sle, outgoing_rate)
+
+ def update_rate_on_stock_entry(self, sle, outgoing_rate):
+ frappe.db.set_value("Stock Entry Detail", sle.voucher_detail_no, "basic_rate", outgoing_rate)
+
+ # Update outgoing item's rate, recalculate FG Item's rate and total incoming/outgoing amount
+ stock_entry = frappe.get_doc("Stock Entry", sle.voucher_no)
+ stock_entry.calculate_rate_and_amount(reset_outgoing_rate=False, raise_error_if_no_rate=False)
+ stock_entry.db_update()
+ for d in stock_entry.items:
+ d.db_update()
+
+ def update_rate_on_delivery_and_sales_return(self, sle, outgoing_rate):
+ # Update item's incoming rate on transaction
+ item_code = frappe.db.get_value(sle.voucher_type + " Item", sle.voucher_detail_no, "item_code")
+ if item_code == sle.item_code:
+ frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "incoming_rate", outgoing_rate)
+ else:
+ # packed item
+ frappe.db.set_value("Packed Item",
+ {"parent_detail_docname": sle.voucher_detail_no, "item_code": sle.item_code},
+ "incoming_rate", outgoing_rate)
+
+ def update_rate_on_purchase_receipt(self, sle, outgoing_rate):
+ if frappe.db.exists(sle.voucher_type + " Item", sle.voucher_detail_no):
+ frappe.db.set_value(sle.voucher_type + " Item", sle.voucher_detail_no, "base_net_rate", outgoing_rate)
+ else:
+ frappe.db.set_value("Purchase Receipt Item Supplied", sle.voucher_detail_no, "rate", outgoing_rate)
+
+ # Recalculate subcontracted item's rate in case of subcontracted purchase receipt/invoice
+ if frappe.db.get_value(sle.voucher_type, sle.voucher_no, "is_subcontracted"):
+ doc = frappe.get_doc(sle.voucher_type, sle.voucher_no)
+ doc.update_valuation_rate(reset_outgoing_rate=False)
+ for d in (doc.items + doc.supplied_items):
+ d.db_update()
+
def get_serialized_values(self, sle):
incoming_rate = flt(sle.incoming_rate)
actual_qty = flt(sle.actual_qty)
@@ -228,7 +452,7 @@
if incoming_rate < 0:
# wrong incoming rate
- incoming_rate = self.valuation_rate
+ incoming_rate = self.wh_data.valuation_rate
stock_value_change = 0
if incoming_rate:
@@ -236,22 +460,25 @@
elif actual_qty < 0:
# In case of delivery/stock issue, get average purchase rate
# of serial nos of current entry
- outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
- stock_value_change = -1 * outgoing_value
+ if not sle.is_cancelled:
+ outgoing_value = self.get_incoming_value_for_serial_nos(sle, serial_nos)
+ stock_value_change = -1 * outgoing_value
+ else:
+ stock_value_change = actual_qty * sle.outgoing_rate
- new_stock_qty = self.qty_after_transaction + actual_qty
+ new_stock_qty = self.wh_data.qty_after_transaction + actual_qty
if new_stock_qty > 0:
- new_stock_value = (self.qty_after_transaction * self.valuation_rate) + stock_value_change
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + stock_value_change
if new_stock_value >= 0:
# calculate new valuation rate only if stock value is positive
# else it remains the same as that of previous entry
- self.valuation_rate = new_stock_value / new_stock_qty
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
- if not self.valuation_rate and sle.voucher_detail_no:
+ if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_rate:
- self.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
+ self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company))
@@ -287,39 +514,38 @@
def get_moving_average_values(self, sle):
actual_qty = flt(sle.actual_qty)
- new_stock_qty = flt(self.qty_after_transaction) + actual_qty
+ new_stock_qty = flt(self.wh_data.qty_after_transaction) + actual_qty
if new_stock_qty >= 0:
if actual_qty > 0:
- if flt(self.qty_after_transaction) <= 0:
- self.valuation_rate = sle.incoming_rate
+ if flt(self.wh_data.qty_after_transaction) <= 0:
+ self.wh_data.valuation_rate = sle.incoming_rate
else:
- new_stock_value = (self.qty_after_transaction * self.valuation_rate) + \
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
(actual_qty * sle.incoming_rate)
- self.valuation_rate = new_stock_value / new_stock_qty
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
elif sle.outgoing_rate:
if new_stock_qty:
- new_stock_value = (self.qty_after_transaction * self.valuation_rate) + \
+ new_stock_value = (self.wh_data.qty_after_transaction * self.wh_data.valuation_rate) + \
(actual_qty * sle.outgoing_rate)
- self.valuation_rate = new_stock_value / new_stock_qty
+ self.wh_data.valuation_rate = new_stock_value / new_stock_qty
else:
- self.valuation_rate = sle.outgoing_rate
-
+ self.wh_data.valuation_rate = sle.outgoing_rate
else:
- if flt(self.qty_after_transaction) >= 0 and sle.outgoing_rate:
- self.valuation_rate = sle.outgoing_rate
+ if flt(self.wh_data.qty_after_transaction) >= 0 and sle.outgoing_rate:
+ self.wh_data.valuation_rate = sle.outgoing_rate
- if not self.valuation_rate and actual_qty > 0:
- self.valuation_rate = sle.incoming_rate
+ if not self.wh_data.valuation_rate and actual_qty > 0:
+ self.wh_data.valuation_rate = sle.incoming_rate
# Get valuation rate from previous SLE or Item master, if item does not have the
# allow zero valuration rate flag set
- if not self.valuation_rate and sle.voucher_detail_no:
+ if not self.wh_data.valuation_rate and sle.voucher_detail_no:
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
- self.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
+ self.wh_data.valuation_rate = get_valuation_rate(sle.item_code, sle.warehouse,
sle.voucher_type, sle.voucher_no, self.allow_zero_rate,
currency=erpnext.get_company_currency(sle.company))
@@ -329,22 +555,22 @@
outgoing_rate = flt(sle.outgoing_rate)
if actual_qty > 0:
- if not self.stock_queue:
- self.stock_queue.append([0, 0])
+ if not self.wh_data.stock_queue:
+ self.wh_data.stock_queue.append([0, 0])
# last row has the same rate, just updated the qty
- if self.stock_queue[-1][1]==incoming_rate:
- self.stock_queue[-1][0] += actual_qty
+ if self.wh_data.stock_queue[-1][1]==incoming_rate:
+ self.wh_data.stock_queue[-1][0] += actual_qty
else:
- if self.stock_queue[-1][0] > 0:
- self.stock_queue.append([actual_qty, incoming_rate])
+ if self.wh_data.stock_queue[-1][0] > 0:
+ self.wh_data.stock_queue.append([actual_qty, incoming_rate])
else:
- qty = self.stock_queue[-1][0] + actual_qty
- self.stock_queue[-1] = [qty, incoming_rate]
+ qty = self.wh_data.stock_queue[-1][0] + actual_qty
+ self.wh_data.stock_queue[-1] = [qty, incoming_rate]
else:
qty_to_pop = abs(actual_qty)
while qty_to_pop:
- if not self.stock_queue:
+ if not self.wh_data.stock_queue:
# Get valuation rate from last sle if exists or from valuation rate field in item master
allow_zero_valuation_rate = self.check_if_allow_zero_valuation_rate(sle.voucher_type, sle.voucher_detail_no)
if not allow_zero_valuation_rate:
@@ -354,35 +580,35 @@
else:
_rate = 0
- self.stock_queue.append([0, _rate])
+ self.wh_data.stock_queue.append([0, _rate])
index = None
if outgoing_rate > 0:
# Find the entry where rate matched with outgoing rate
- for i, v in enumerate(self.stock_queue):
+ for i, v in enumerate(self.wh_data.stock_queue):
if v[1] == outgoing_rate:
index = i
break
# If no entry found with outgoing rate, collapse stack
if index == None:
- new_stock_value = sum((d[0]*d[1] for d in self.stock_queue)) - qty_to_pop*outgoing_rate
- new_stock_qty = sum((d[0] for d in self.stock_queue)) - qty_to_pop
- self.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
+ new_stock_value = sum((d[0]*d[1] for d in self.wh_data.stock_queue)) - qty_to_pop*outgoing_rate
+ new_stock_qty = sum((d[0] for d in self.wh_data.stock_queue)) - qty_to_pop
+ self.wh_data.stock_queue = [[new_stock_qty, new_stock_value/new_stock_qty if new_stock_qty > 0 else outgoing_rate]]
break
else:
index = 0
# select first batch or the batch with same rate
- batch = self.stock_queue[index]
+ batch = self.wh_data.stock_queue[index]
if qty_to_pop >= batch[0]:
# consume current batch
qty_to_pop = qty_to_pop - batch[0]
- self.stock_queue.pop(index)
- if not self.stock_queue and qty_to_pop:
+ self.wh_data.stock_queue.pop(index)
+ if not self.wh_data.stock_queue and qty_to_pop:
# stock finished, qty still remains to be withdrawn
# negative stock, keep in as a negative batch
- self.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]])
+ self.wh_data.stock_queue.append([-qty_to_pop, outgoing_rate or batch[1]])
break
else:
@@ -391,14 +617,14 @@
batch[0] = batch[0] - qty_to_pop
qty_to_pop = 0
- stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.stock_queue))
- stock_qty = sum((flt(batch[0]) for batch in self.stock_queue))
+ stock_value = sum((flt(batch[0]) * flt(batch[1]) for batch in self.wh_data.stock_queue))
+ stock_qty = sum((flt(batch[0]) for batch in self.wh_data.stock_queue))
if stock_qty:
- self.valuation_rate = stock_value / flt(stock_qty)
+ self.wh_data.valuation_rate = stock_value / flt(stock_qty)
- if not self.stock_queue:
- self.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.valuation_rate])
+ if not self.wh_data.stock_queue:
+ self.wh_data.stock_queue.append([0, sle.incoming_rate or sle.outgoing_rate or self.wh_data.valuation_rate])
def check_if_allow_zero_valuation_rate(self, voucher_type, voucher_detail_no):
ref_item_dt = ""
@@ -413,39 +639,55 @@
else:
return 0
- def get_sle_before_datetime(self):
+ def get_sle_before_datetime(self, args):
"""get previous stock ledger entry before current time-bucket"""
- if self.args.get('sle_id'):
- self.args['name'] = self.args.get('sle_id')
+ sle = get_stock_ledger_entries(args, "<", "desc", "limit 1", for_update=False)
+ sle = sle[0] if sle else frappe._dict()
+ return sle
- return get_stock_ledger_entries(self.args, "<=", "desc", "limit 1", for_update=False)
-
- def get_sle_after_datetime(self):
+ def get_sle_after_datetime(self, args):
"""get Stock Ledger Entries after a particular datetime, for reposting"""
- return get_stock_ledger_entries(self.previous_sle or frappe._dict({
- "item_code": self.args.get("item_code"), "warehouse": self.args.get("warehouse") }),
- ">", "asc", for_update=True, check_serial_no=False)
+ return get_stock_ledger_entries(args, ">", "asc", for_update=True, check_serial_no=False)
def raise_exceptions(self):
- deficiency = min(e["diff"] for e in self.exceptions)
+ msg_list = []
+ for warehouse, exceptions in iteritems(self.exceptions):
+ deficiency = min(e["diff"] for e in exceptions)
- if ((self.exceptions[0]["voucher_type"], self.exceptions[0]["voucher_no"]) in
- frappe.local.flags.currently_saving):
+ if ((exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]) in
+ frappe.local.flags.currently_saving):
- msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
- abs(deficiency), frappe.get_desk_link('Item', self.item_code),
- frappe.get_desk_link('Warehouse', self.warehouse))
- else:
- msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
- abs(deficiency), frappe.get_desk_link('Item', self.item_code),
- frappe.get_desk_link('Warehouse', self.warehouse),
- self.exceptions[0]["posting_date"], self.exceptions[0]["posting_time"],
- frappe.get_desk_link(self.exceptions[0]["voucher_type"], self.exceptions[0]["voucher_no"]))
+ msg = _("{0} units of {1} needed in {2} to complete this transaction.").format(
+ abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]),
+ frappe.get_desk_link('Warehouse', warehouse))
+ else:
+ msg = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ abs(deficiency), frappe.get_desk_link('Item', exceptions[0]["item_code"]),
+ frappe.get_desk_link('Warehouse', warehouse),
+ exceptions[0]["posting_date"], exceptions[0]["posting_time"],
+ frappe.get_desk_link(exceptions[0]["voucher_type"], exceptions[0]["voucher_no"]))
- if self.verbose:
- frappe.throw(msg, NegativeStockError, title='Insufficient Stock')
- else:
- raise NegativeStockError(msg)
+ if msg:
+ msg_list.append(msg)
+
+ if msg_list:
+ message = "\n\n".join(msg_list)
+ if self.verbose:
+ frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+ else:
+ raise NegativeStockError(message)
+
+ def update_bin(self):
+ # update bin for each warehouse
+ for warehouse, data in iteritems(self.data):
+ bin_doc = get_bin(self.item_code, warehouse)
+ bin_doc.update({
+ "valuation_rate": data.valuation_rate,
+ "actual_qty": data.qty_after_transaction,
+ "stock_value": data.stock_value
+ })
+ bin_doc.flags.via_stock_ledger_entry = True
+ bin_doc.save(ignore_permissions=True)
def get_previous_sle(args, for_update=False):
"""
@@ -489,6 +731,7 @@
select *, timestamp(posting_date, posting_time) as "timestamp"
from `tabStock Ledger Entry`
where item_code = %%(item_code)s
+ and is_cancelled = 0
%(conditions)s
order by timestamp(posting_date, posting_time) %(order)s, creation %(order)s
%(limit)s %(for_update)s""" % {
@@ -498,10 +741,11 @@
"order": order
}, previous_sle, as_dict=1, debug=debug)
-def get_sle_by_id(sle_id):
- return frappe.db.get_all('Stock Ledger Entry',
- fields=['*', 'timestamp(posting_date, posting_time) as timestamp'],
- filters={'name': sle_id})[0]
+def get_sle_by_voucher_detail_no(voucher_detail_no, excluded_sle=None):
+ return frappe.db.get_value('Stock Ledger Entry',
+ {'voucher_detail_no': voucher_detail_no, 'name': ['!=', excluded_sle]},
+ ['item_code', 'warehouse', 'posting_date', 'posting_time', 'timestamp(posting_date, posting_time) as timestamp'],
+ as_dict=1)
def get_valuation_rate(item_code, warehouse, voucher_type, voucher_no,
allow_zero_rate=False, currency=None, company=None, raise_error_if_no_rate=True):
@@ -529,7 +773,7 @@
order by posting_date desc, posting_time desc, name desc limit 1""", (item_code, voucher_no, voucher_type))
if last_valuation_rate:
- return flt(last_valuation_rate[0][0]) # as there is previous records, it might come with zero rate
+ return flt(last_valuation_rate[0][0])
# If negative stock allowed, and item delivered without any incoming entry,
# system does not found any SLE, then take valuation rate from Item
@@ -561,3 +805,55 @@
frappe.throw(msg=msg, title=_("Valuation Rate Missing"))
return valuation_rate
+
+def update_qty_in_future_sle(args, allow_negative_stock=None):
+ frappe.db.sql("""
+ update `tabStock Ledger Entry`
+ set qty_after_transaction = qty_after_transaction + {qty}
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_no != %(voucher_no)s
+ and is_cancelled = 0
+ and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s)
+ or (
+ timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s)
+ and creation > %(creation)s
+ )
+ )
+ """.format(qty=args.actual_qty), args)
+
+ validate_negative_qty_in_future_sle(args, allow_negative_stock)
+
+def validate_negative_qty_in_future_sle(args, allow_negative_stock=None):
+ allow_negative_stock = allow_negative_stock \
+ or cint(frappe.db.get_single_value("Stock Settings", "allow_negative_stock"))
+
+ if args.actual_qty < 0 and not allow_negative_stock:
+ sle = get_future_sle_with_negative_qty(args)
+ if sle:
+ message = _("{0} units of {1} needed in {2} on {3} {4} for {5} to complete this transaction.").format(
+ abs(sle[0]["qty_after_transaction"]),
+ frappe.get_desk_link('Item', args.item_code),
+ frappe.get_desk_link('Warehouse', args.warehouse),
+ sle[0]["posting_date"], sle[0]["posting_time"],
+ frappe.get_desk_link(sle[0]["voucher_type"], sle[0]["voucher_no"]))
+
+ frappe.throw(message, NegativeStockError, title='Insufficient Stock')
+
+def get_future_sle_with_negative_qty(args):
+ return frappe.db.sql("""
+ select
+ qty_after_transaction, posting_date, posting_time,
+ voucher_type, voucher_no
+ from `tabStock Ledger Entry`
+ where
+ item_code = %(item_code)s
+ and warehouse = %(warehouse)s
+ and voucher_no != %(voucher_no)s
+ and timestamp(posting_date, posting_time) >= timestamp(%(posting_date)s, %(posting_time)s)
+ and is_cancelled = 0
+ and qty_after_transaction < 0
+ order by timestamp(posting_date, posting_time) asc
+ limit 1
+ """, args, as_dict=1)
\ No newline at end of file
diff --git a/erpnext/stock/utils.py b/erpnext/stock/utils.py
index f9ac254..0af3d90 100644
--- a/erpnext/stock/utils.py
+++ b/erpnext/stock/utils.py
@@ -5,7 +5,7 @@
import frappe, erpnext
from frappe import _
import json
-from frappe.utils import flt, cstr, nowdate, nowtime
+from frappe.utils import flt, cstr, nowdate, nowtime, get_link_to_form
from six import string_types
@@ -63,6 +63,7 @@
SELECT item_code, stock_value, name, warehouse
FROM `tabStock Ledger Entry` sle
WHERE posting_date <= %s {0}
+ and is_cancelled = 0
ORDER BY timestamp(posting_date, posting_time) DESC, creation DESC
""".format(condition), values, as_dict=1)
@@ -211,7 +212,7 @@
currency=erpnext.get_company_currency(args.get('company')), company=args.get('company'),
raise_error_if_no_rate=raise_error_if_no_rate)
- return in_rate
+ return flt(in_rate)
def get_avg_purchase_rate(serial_nos):
"""get average value of serial numbers"""
@@ -283,6 +284,10 @@
if frappe.db.get_value("Warehouse", warehouse, "is_group"):
frappe.throw(_("Group node warehouse is not allowed to select for transactions"))
+def validate_disabled_warehouse(warehouse):
+ if frappe.db.get_value("Warehouse", warehouse, "disabled"):
+ frappe.throw(_("Disabled Warehouse {0} cannot be used for this transaction.").format(get_link_to_form('Warehouse', warehouse)))
+
def update_included_uom_in_report(columns, result, include_uom, conversion_factors):
if not include_uom or not conversion_factors:
return
@@ -375,4 +380,10 @@
outgoing_rate = outgoing_rate[0][0] if outgoing_rate else 0.0
- return outgoing_rate
\ No newline at end of file
+ return outgoing_rate
+
+def is_reposting_item_valuation_in_progress():
+ reposting_in_progress = frappe.db.exists("Repost Item Valuation",
+ {'docstatus': 1, 'status': ['in', ['Queued','In Progress']]})
+ if reposting_in_progress:
+ frappe.msgprint(_("Item valuation reposting in progress. Report might show incorrect item valuation."), alert=1)
\ No newline at end of file
diff --git a/erpnext/stock/workspace/stock/stock.json b/erpnext/stock/workspace/stock/stock.json
new file mode 100644
index 0000000..3221dc4
--- /dev/null
+++ b/erpnext/stock/workspace/stock/stock.json
@@ -0,0 +1,721 @@
+{
+ "cards_label": "Masters & Reports",
+ "category": "Modules",
+ "charts": [
+ {
+ "chart_name": "Warehouse wise Stock Value"
+ }
+ ],
+ "creation": "2020-03-02 15:43:10.096528",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "stock",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Stock",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Items and Pricing",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item",
+ "link_to": "Item",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Group",
+ "link_to": "Item Group",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Product Bundle",
+ "link_to": "Product Bundle",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Price List",
+ "link_to": "Price List",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Price",
+ "link_to": "Item Price",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Shipping Rule",
+ "link_to": "Shipping Rule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Pricing Rule",
+ "link_to": "Pricing Rule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Alternative",
+ "link_to": "Item Alternative",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Manufacturer",
+ "link_to": "Item Manufacturer",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Customs Tariff Number",
+ "link_to": "Customs Tariff Number",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock Transactions",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Material Request",
+ "link_to": "Material Request",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock Entry",
+ "link_to": "Stock Entry",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Customer",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Delivery Note",
+ "link_to": "Delivery Note",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item, Supplier",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Purchase Receipt",
+ "link_to": "Purchase Receipt",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Pick List",
+ "link_to": "Pick List",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Delivery Trip",
+ "link_to": "Delivery Trip",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Stock Ledger",
+ "link_to": "Stock Ledger",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Stock Balance",
+ "link_to": "Stock Balance",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Stock Projected Qty",
+ "link_to": "Stock Projected Qty",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock Summary",
+ "link_to": "stock-balance",
+ "link_type": "Page",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Stock Ageing",
+ "link_to": "Stock Ageing",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Item Price Stock",
+ "link_to": "Item Price Stock",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock Settings",
+ "link_to": "Stock Settings",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Warehouse",
+ "link_to": "Warehouse",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Unit of Measure (UOM)",
+ "link_to": "UOM",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Variant Settings",
+ "link_to": "Item Variant Settings",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Brand",
+ "link_to": "Brand",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item Attribute",
+ "link_to": "Item Attribute",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "UOM Conversion Factor",
+ "link_to": "UOM Conversion Factor",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Serial No and Batch",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Serial No",
+ "link_to": "Serial No",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Batch",
+ "link_to": "Batch",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Installation Note",
+ "link_to": "Installation Note",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Serial No",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Serial No Service Contract Expiry",
+ "link_to": "Serial No Service Contract Expiry",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Serial No",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Serial No Status",
+ "link_to": "Serial No Status",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Serial No",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Serial No Warranty Expiry",
+ "link_to": "Serial No Warranty Expiry",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Tools",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Stock Reconciliation",
+ "link_to": "Stock Reconciliation",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Landed Cost Voucher",
+ "link_to": "Landed Cost Voucher",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Packing Slip",
+ "link_to": "Packing Slip",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quality Inspection",
+ "link_to": "Quality Inspection",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quality Inspection Template",
+ "link_to": "Quality Inspection Template",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Quick Stock Balance",
+ "link_to": "Quick Stock Balance",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Key Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Item Price",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Item-wise Price List Rate",
+ "link_to": "Item-wise Price List Rate",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Stock Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Stock Analytics",
+ "link_to": "Stock Analytics",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Stock Qty vs Serial No Count",
+ "link_to": "Stock Qty vs Serial No Count",
+ "link_type": "Report",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Delivery Note",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Delivery Note Trends",
+ "link_to": "Delivery Note Trends",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Receipt",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Purchase Receipt Trends",
+ "link_to": "Purchase Receipt Trends",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Sales Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Sales Order Analysis",
+ "link_to": "Sales Order Analysis",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Purchase Order Analysis",
+ "link_to": "Purchase Order Analysis",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Bin",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Item Shortage Report",
+ "link_to": "Item Shortage Report",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Batch",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Batch-Wise Balance History",
+ "link_to": "Batch-Wise Balance History",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Other Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Material Request",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Requested Items To Be Transferred",
+ "link_to": "Requested Items To Be Transferred",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Stock Ledger Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Batch Item Expiry Status",
+ "link_to": "Batch Item Expiry Status",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Price List",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Item Prices",
+ "link_to": "Item Prices",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Itemwise Recommended Reorder Level",
+ "link_to": "Itemwise Recommended Reorder Level",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Item",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Item Variant Details",
+ "link_to": "Item Variant Details",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Subcontracted Raw Materials To Be Transferred",
+ "link_to": "Subcontracted Raw Materials To Be Transferred",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Purchase Order",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Subcontracted Item To Be Received",
+ "link_to": "Subcontracted Item To Be Received",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "Stock Ledger Entry",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "Stock and Account Value Comparison",
+ "link_to": "Stock and Account Value Comparison",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:36.282890",
+ "modified_by": "Administrator",
+ "module": "Stock",
+ "name": "Stock",
+ "onboarding": "Stock",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "color": "Green",
+ "format": "{} Available",
+ "label": "Item",
+ "link_to": "Item",
+ "stats_filter": "{\n \"disabled\" : 0\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Yellow",
+ "format": "{} Pending",
+ "label": "Material Request",
+ "link_to": "Material Request",
+ "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"Pending\"\n}",
+ "type": "DocType"
+ },
+ {
+ "label": "Stock Entry",
+ "link_to": "Stock Entry",
+ "type": "DocType"
+ },
+ {
+ "color": "Yellow",
+ "format": "{} To Bill",
+ "label": "Purchase Receipt",
+ "link_to": "Purchase Receipt",
+ "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"To Bill\"\n}",
+ "type": "DocType"
+ },
+ {
+ "color": "Yellow",
+ "format": "{} To Bill",
+ "label": "Delivery Note",
+ "link_to": "Delivery Note",
+ "stats_filter": "{\n \"company\": [\"like\", '%' + frappe.defaults.get_global_default(\"company\") + '%'],\n \"status\": \"To Bill\"\n}",
+ "type": "DocType"
+ },
+ {
+ "label": "Stock Ledger",
+ "link_to": "Stock Ledger",
+ "type": "Report"
+ },
+ {
+ "label": "Stock Balance",
+ "link_to": "Stock Balance",
+ "type": "Report"
+ },
+ {
+ "label": "Dashboard",
+ "link_to": "Stock",
+ "type": "Dashboard"
+ }
+ ],
+ "shortcuts_label": "Quick Access"
+}
\ No newline at end of file
diff --git a/erpnext/support/desk_page/support/support.json b/erpnext/support/desk_page/support/support.json
deleted file mode 100644
index 28410f3..0000000
--- a/erpnext/support/desk_page/support/support.json
+++ /dev/null
@@ -1,73 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Issues",
- "links": "[\n {\n \"description\": \"Support queries from customers.\",\n \"label\": \"Issue\",\n \"name\": \"Issue\",\n \"onboard\": 1,\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Issue Type.\",\n \"label\": \"Issue Type\",\n \"name\": \"Issue Type\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Issue Priority.\",\n \"label\": \"Issue Priority\",\n \"name\": \"Issue Priority\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Maintenance",
- "links": "[\n {\n \"label\": \"Maintenance Schedule\",\n \"name\": \"Maintenance Schedule\",\n \"type\": \"doctype\"\n },\n {\n \"label\": \"Maintenance Visit\",\n \"name\": \"Maintenance Visit\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Service Level Agreement",
- "links": "[\n {\n \"description\": \"Service Level Agreement.\",\n \"label\": \"Service Level Agreement\",\n \"name\": \"Service Level Agreement\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Warranty",
- "links": "[\n {\n \"description\": \"Warranty Claim against Serial No.\",\n \"label\": \"Warranty Claim\",\n \"name\": \"Warranty Claim\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Single unit of an Item.\",\n \"label\": \"Serial No\",\n \"name\": \"Serial No\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Settings",
- "links": "[\n {\n \"label\": \"Support Settings\",\n \"name\": \"Support Settings\",\n \"type\": \"doctype\"\n }\n]"
- },
- {
- "hidden": 0,
- "label": "Reports",
- "links": "[\n {\n \"dependencies\": [\n \"Issue\"\n ],\n \"doctype\": \"Issue\",\n \"is_query_report\": true,\n \"label\": \"First Response Time for Issues\",\n \"name\": \"First Response Time for Issues\",\n \"type\": \"report\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [],
- "creation": "2020-03-02 15:48:23.224699",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Support",
- "modified": "2020-08-11 15:49:34.307341",
- "modified_by": "Administrator",
- "module": "Support",
- "name": "Support",
- "owner": "Administrator",
- "pin_to_bottom": 0,
- "pin_to_top": 0,
- "shortcuts": [
- {
- "color": "#ffc4c4",
- "format": "{} Assigned",
- "label": "Issue",
- "link_to": "Issue",
- "stats_filter": "{\n \"_assign\": [\"like\", '%' + frappe.session.user + '%'],\n \"status\": \"Open\"\n}",
- "type": "DocType"
- },
- {
- "label": "Maintenance Visit",
- "link_to": "Maintenance Visit",
- "type": "DocType"
- },
- {
- "label": "Service Level Agreement",
- "link_to": "Service Level Agreement",
- "type": "DocType"
- }
- ]
-}
\ No newline at end of file
diff --git a/erpnext/support/doctype/issue/issue.js b/erpnext/support/doctype/issue/issue.js
index fe01d4b..9fe12f9 100644
--- a/erpnext/support/doctype/issue/issue.js
+++ b/erpnext/support/doctype/issue/issue.js
@@ -1,6 +1,13 @@
frappe.ui.form.on("Issue", {
onload: function(frm) {
frm.email_field = "raised_by";
+ frm.set_query("customer", function () {
+ return {
+ filters: {
+ "disabled": 0
+ }
+ };
+ });
frappe.db.get_value("Support Settings", {name: "Support Settings"},
["allow_resetting_service_level_agreement", "track_service_level_agreement"], (r) => {
@@ -21,14 +28,14 @@
},
callback: function (r) {
if (r && r.message) {
- frm.set_query('priority', function() {
+ frm.set_query("priority", function() {
return {
filters: {
"name": ["in", r.message.priority],
}
};
});
- frm.set_query('service_level_agreement', function() {
+ frm.set_query("service_level_agreement", function() {
return {
filters: {
"name": ["in", r.message.service_level_agreements],
@@ -42,12 +49,12 @@
},
refresh: function (frm) {
- if (frm.doc.status !== "Closed" && frm.doc.agreement_status === "Ongoing") {
- if (frm.doc.service_level_agreement) {
+ if (frm.doc.status !== "Closed") {
+ if (frm.doc.service_level_agreement && frm.doc.agreement_status === "Ongoing") {
frappe.call({
- 'method': 'frappe.client.get',
+ "method": "frappe.client.get",
args: {
- doctype: 'Service Level Agreement',
+ doctype: "Service Level Agreement",
name: frm.doc.service_level_agreement
},
callback: function(data) {
@@ -127,8 +134,8 @@
reset_sla.clear();
frappe.show_alert({
- indicator: 'green',
- message: __('Resetting Service Level Agreement.')
+ indicator: "green",
+ message: __("Resetting Service Level Agreement.")
});
frm.call("reset_service_level_agreement", {
@@ -145,56 +152,73 @@
reset_sla.show();
},
- timeline_refresh: function(frm) {
- // create button for "Help Article"
- if(frappe.model.can_create('Help Article')) {
- // Removing Help Article button if exists to avoid multiple occurance
- frm.timeline.wrapper.find('.comment-header .asset-details .btn-add-to-kb').remove();
- $('<button class="btn btn-xs btn-link btn-add-to-kb text-muted hidden-xs pull-right">'+
- __('Help Article') + '</button>')
- .appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])'))
- .on('click', function() {
- var content = $(this).parents('.timeline-item:first').find('.timeline-item-content').html();
- var doc = frappe.model.get_new_doc('Help Article');
- doc.title = frm.doc.subject;
- doc.content = content;
- frappe.set_route('Form', 'Help Article', doc.name);
- });
- }
- if (!frm.timeline.wrapper.find('.btn-split-issue').length) {
- let split_issue = __("Split Issue")
- $(`<button class="btn btn-xs btn-link btn-add-to-kb text-muted hidden-xs btn-split-issue pull-right" style="display:inline-block; margin-right: 15px">
- ${split_issue}
- </button>`)
- .appendTo(frm.timeline.wrapper.find('.comment-header .asset-details:not([data-communication-type="Comment"])'))
- if (!frm.timeline.wrapper.data("split-issue-event-attached")){
+ timeline_refresh: function(frm) {
+ if (!frm.timeline.wrapper.find(".btn-split-issue").length) {
+ let split_issue_btn = $(`
+ <a class="action-btn btn-split-issue" title="${__("Split Issue")}">
+ ${frappe.utils.icon('branch', 'sm')}
+ </a>
+ `);
+
+ let communication_box = frm.timeline.wrapper.find('.timeline-item[data-doctype="Communication"]');
+ communication_box.find('.actions').prepend(split_issue_btn);
+
+ if (!frm.timeline.wrapper.data("split-issue-event-attached")) {
frm.timeline.wrapper.on('click', '.btn-split-issue', (e) => {
var dialog = new frappe.ui.Dialog({
title: __("Split Issue"),
fields: [
- {fieldname: 'subject', fieldtype: 'Data', reqd:1, label: __('Subject'), description: __('All communications including and above this shall be moved into the new Issue')}
+ {
+ fieldname: "subject",
+ fieldtype: "Data",
+ reqd: 1,
+ label: __("Subject"),
+ description: __("All communications including and above this shall be moved into the new Issue")
+ }
],
primary_action_label: __("Split"),
- primary_action: function() {
+ primary_action: () => {
frm.call("split_issue", {
subject: dialog.fields_dict.subject.value,
communication_id: e.currentTarget.closest(".timeline-item").getAttribute("data-name")
}, (r) => {
- let url = window.location.href
- let arr = url.split("/");
- let result = arr[0] + "//" + arr[2]
- frappe.msgprint(`New issue created: <a href="${result}/desk#Form/Issue/${r.message}">${r.message}</a>`)
+ frappe.msgprint(`New issue created: <a href="/app/issue/${r.message}">${r.message}</a>`);
frm.reload_doc();
dialog.hide();
});
}
});
- dialog.show()
- })
- frm.timeline.wrapper.data("split-issue-event-attached", true)
+ dialog.show();
+ });
+ frm.timeline.wrapper.data("split-issue-event-attached", true);
}
}
+
+ // create button for "Help Article"
+ // if (frappe.model.can_create("Help Article")) {
+ // // Removing Help Article button if exists to avoid multiple occurrence
+ // frm.timeline.wrapper.find('.action-btn .btn-add-to-kb').remove();
+
+ // let help_article = $(`
+ // <a class="action-btn btn-add-to-kb" title="${__('Help Article')}">
+ // ${frappe.utils.icon('solid-info', 'sm')}
+ // </a>
+ // `);
+
+ // let communication_box = frm.timeline.wrapper.find('.timeline-item[data-doctype="Communication"]');
+ // communication_box.find('.actions').prepend(help_article);
+ // if (!frm.timeline.wrapper.data("help-article-event-attached")) {
+ // frm.timeline.wrapper.on('click', '.btn-add-to-kb', function () {
+ // const content = $(this).parents('.timeline-item[data-doctype="Communication"]:first').find(".content").html();
+ // const doc = frappe.model.get_new_doc("Help Article");
+ // doc.title = frm.doc.subject;
+ // doc.content = content;
+ // frappe.set_route("Form", "Help Article", doc.name);
+ // });
+ // }
+ // frm.timeline.wrapper.data("help-article-event-attached", true);
+ // }
},
});
@@ -226,7 +250,7 @@
function get_time_left(timestamp, agreement_status) {
const diff = moment(timestamp).diff(moment());
const diff_display = diff >= 44500 ? moment.duration(diff).humanize() : "Failed";
- let indicator = (diff_display == 'Failed' && agreement_status != "Fulfilled") ? "red" : "green";
+ let indicator = (diff_display == "Failed" && agreement_status != "Fulfilled") ? "red" : "green";
return {"diff_display": diff_display, "indicator": indicator};
}
diff --git a/erpnext/support/doctype/issue/issue.py b/erpnext/support/doctype/issue/issue.py
index 62b39cc..bbbbc4a 100644
--- a/erpnext/support/doctype/issue/issue.py
+++ b/erpnext/support/doctype/issue/issue.py
@@ -207,14 +207,17 @@
"comment_type": "Info",
"reference_doctype": "Issue",
"reference_name": replicated_issue.name,
- "content": " - Split the Issue from <a href='#Form/Issue/{0}'>{1}</a>".format(self.name, frappe.bold(self.name)),
+ "content": " - Split the Issue from <a href='/app/Form/Issue/{0}'>{1}</a>".format(self.name, frappe.bold(self.name)),
}).insert(ignore_permissions=True)
return replicated_issue.name
def before_insert(self):
if frappe.db.get_single_value("Support Settings", "track_service_level_agreement"):
- self.set_response_and_resolution_time()
+ if frappe.flags.in_test:
+ self.set_response_and_resolution_time(priority=self.priority, service_level_agreement=self.service_level_agreement)
+ else:
+ self.set_response_and_resolution_time()
def set_response_and_resolution_time(self, priority=None, service_level_agreement=None):
service_level_agreement = get_active_service_level_agreement_for(priority=priority,
diff --git a/erpnext/support/doctype/issue/issue_list.js b/erpnext/support/doctype/issue/issue_list.js
index 513a8dc..e04498e 100644
--- a/erpnext/support/doctype/issue/issue_list.js
+++ b/erpnext/support/doctype/issue/issue_list.js
@@ -28,7 +28,7 @@
} else if (doc.status === 'Closed') {
return [__(doc.status), "green", "status,=," + doc.status];
} else {
- return [__(doc.status), "darkgrey", "status,=," + doc.status];
+ return [__(doc.status), "gray", "status,=," + doc.status];
}
}
}
diff --git a/erpnext/support/doctype/issue/test_issue.py b/erpnext/support/doctype/issue/test_issue.py
index c962dc6..483bb15 100644
--- a/erpnext/support/doctype/issue/test_issue.py
+++ b/erpnext/support/doctype/issue/test_issue.py
@@ -135,15 +135,19 @@
self.assertEqual(flt(issue.total_hold_time, 2), 2700)
-def make_issue(creation=None, customer=None, index=0):
+def make_issue(creation=None, customer=None, index=0, priority=None, issue_type=None):
issue = frappe.get_doc({
"doctype": "Issue",
"subject": "Service Level Agreement Issue {0}".format(index),
"customer": customer,
"raised_by": "test@example.com",
"description": "Service Level Agreement Issue",
+ "issue_type": issue_type,
+ "priority": priority,
"creation": creation,
- "service_level_agreement_creation": creation
+ "opening_date": creation,
+ "service_level_agreement_creation": creation,
+ "company": "_Test Company"
}).insert(ignore_permissions=True)
return issue
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/support/report/issue_analytics/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/support/report/issue_analytics/__init__.py
diff --git a/erpnext/support/report/issue_analytics/issue_analytics.js b/erpnext/support/report/issue_analytics/issue_analytics.js
new file mode 100644
index 0000000..f87b2c2
--- /dev/null
+++ b/erpnext/support/report/issue_analytics/issue_analytics.js
@@ -0,0 +1,141 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Issue Analytics"] = {
+ "filters": [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ fieldname: "based_on",
+ label: __("Based On"),
+ fieldtype: "Select",
+ options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"],
+ default: "Customer",
+ reqd: 1
+ },
+ {
+ fieldname: "from_date",
+ label: __("From Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_global_default("year_start_date"),
+ reqd: 1
+ },
+ {
+ fieldname:"to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_global_default("year_end_date"),
+ reqd: 1
+ },
+ {
+ fieldname: "range",
+ label: __("Range"),
+ fieldtype: "Select",
+ options: [
+ { "value": "Weekly", "label": __("Weekly") },
+ { "value": "Monthly", "label": __("Monthly") },
+ { "value": "Quarterly", "label": __("Quarterly") },
+ { "value": "Yearly", "label": __("Yearly") }
+ ],
+ default: "Monthly",
+ reqd: 1
+ },
+ {
+ fieldname: "status",
+ label: __("Status"),
+ fieldtype: "Select",
+ options:[
+ {label: __('Open'), value: 'Open'},
+ {label: __('Replied'), value: 'Replied'},
+ {label: __('Resolved'), value: 'Resolved'},
+ {label: __('Closed'), value: 'Closed'}
+ ]
+ },
+ {
+ fieldname: "priority",
+ label: __("Issue Priority"),
+ fieldtype: "Link",
+ options: "Issue Priority"
+ },
+ {
+ fieldname: "customer",
+ label: __("Customer"),
+ fieldtype: "Link",
+ options: "Customer"
+ },
+ {
+ fieldname: "project",
+ label: __("Project"),
+ fieldtype: "Link",
+ options: "Project"
+ },
+ {
+ fieldname: "assigned_to",
+ label: __("Assigned To"),
+ fieldtype: "Link",
+ options: "User"
+ }
+ ],
+ after_datatable_render: function(datatable_obj) {
+ $(datatable_obj.wrapper).find(".dt-row-0").find('input[type=checkbox]').click();
+ },
+ get_datatable_options(options) {
+ return Object.assign(options, {
+ checkboxColumn: true,
+ events: {
+ onCheckRow: function(data) {
+ if (data && data.length) {
+ row_name = data[2].content;
+ row_values = data.slice(3).map(function(column) {
+ return column.content;
+ })
+ entry = {
+ 'name': row_name,
+ 'values': row_values
+ }
+
+ let raw_data = frappe.query_report.chart.data;
+ let new_datasets = raw_data.datasets;
+
+ var found = false;
+
+ for(var i=0; i < new_datasets.length; i++){
+ if (new_datasets[i].name == row_name){
+ found = true;
+ new_datasets.splice(i,1);
+ break;
+ }
+ }
+
+ if (!found){
+ new_datasets.push(entry);
+ }
+
+ let new_data = {
+ labels: raw_data.labels,
+ datasets: new_datasets
+ }
+
+ setTimeout(() => {
+ frappe.query_report.chart.update(new_data)
+ },500)
+
+
+ setTimeout(() => {
+ frappe.query_report.chart.draw(true);
+ }, 1000)
+
+ frappe.query_report.raw_chart_data = new_data;
+ }
+ },
+ }
+ });
+ }
+};
\ No newline at end of file
diff --git a/erpnext/support/report/issue_analytics/issue_analytics.json b/erpnext/support/report/issue_analytics/issue_analytics.json
new file mode 100644
index 0000000..dd18498
--- /dev/null
+++ b/erpnext/support/report/issue_analytics/issue_analytics.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 1,
+ "columns": [],
+ "creation": "2020-10-09 19:52:10.227317",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-10-11 19:43:19.358625",
+ "modified_by": "Administrator",
+ "module": "Support",
+ "name": "Issue Analytics",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Issue",
+ "report_name": "Issue Analytics",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Support Team"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/support/report/issue_analytics/issue_analytics.py b/erpnext/support/report/issue_analytics/issue_analytics.py
new file mode 100644
index 0000000..3fdb10d
--- /dev/null
+++ b/erpnext/support/report/issue_analytics/issue_analytics.py
@@ -0,0 +1,221 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from six import iteritems
+from frappe import _, scrub
+from frappe.utils import getdate, flt, add_to_date, add_days
+from erpnext.accounts.utils import get_fiscal_year
+
+def execute(filters=None):
+ return IssueAnalytics(filters).run()
+
+class IssueAnalytics(object):
+ def __init__(self, filters=None):
+ """Issue Analytics Report"""
+ self.filters = frappe._dict(filters or {})
+ self.get_period_date_ranges()
+
+ def run(self):
+ self.get_columns()
+ self.get_data()
+ self.get_chart_data()
+
+ return self.columns, self.data, None, self.chart
+
+ def get_columns(self):
+ self.columns = []
+
+ if self.filters.based_on == 'Customer':
+ self.columns.append({
+ 'label': _('Customer'),
+ 'options': 'Customer',
+ 'fieldname': 'customer',
+ 'fieldtype': 'Link',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Assigned To':
+ self.columns.append({
+ 'label': _('User'),
+ 'fieldname': 'user',
+ 'fieldtype': 'Link',
+ 'options': 'User',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Issue Type':
+ self.columns.append({
+ 'label': _('Issue Type'),
+ 'fieldname': 'issue_type',
+ 'fieldtype': 'Link',
+ 'options': 'Issue Type',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Issue Priority':
+ self.columns.append({
+ 'label': _('Issue Priority'),
+ 'fieldname': 'priority',
+ 'fieldtype': 'Link',
+ 'options': 'Issue Priority',
+ 'width': 200
+ })
+
+ for end_date in self.periodic_daterange:
+ period = self.get_period(end_date)
+ self.columns.append({
+ 'label': _(period),
+ 'fieldname': scrub(period),
+ 'fieldtype': 'Int',
+ 'width': 120
+ })
+
+ self.columns.append({
+ 'label': _('Total'),
+ 'fieldname': 'total',
+ 'fieldtype': 'Int',
+ 'width': 120
+ })
+
+ def get_data(self):
+ self.get_issues()
+ self.get_rows()
+
+ def get_period(self, date):
+ months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+
+ if self.filters.range == 'Weekly':
+ period = 'Week ' + str(date.isocalendar()[1])
+ elif self.filters.range == 'Monthly':
+ period = str(months[date.month - 1])
+ elif self.filters.range == 'Quarterly':
+ period = 'Quarter ' + str(((date.month - 1) // 3) + 1)
+ else:
+ year = get_fiscal_year(date, self.filters.company)
+ period = str(year[0])
+
+ if getdate(self.filters.from_date).year != getdate(self.filters.to_date).year and self.filters.range != 'Yearly':
+ period += ' ' + str(date.year)
+
+ return period
+
+ def get_period_date_ranges(self):
+ from dateutil.relativedelta import relativedelta, MO
+ from_date, to_date = getdate(self.filters.from_date), getdate(self.filters.to_date)
+
+ increment = {
+ 'Monthly': 1,
+ 'Quarterly': 3,
+ 'Half-Yearly': 6,
+ 'Yearly': 12
+ }.get(self.filters.range, 1)
+
+ if self.filters.range in ['Monthly', 'Quarterly']:
+ from_date = from_date.replace(day=1)
+ elif self.filters.range == 'Yearly':
+ from_date = get_fiscal_year(from_date)[1]
+ else:
+ from_date = from_date + relativedelta(from_date, weekday=MO(-1))
+
+ self.periodic_daterange = []
+ for dummy in range(1, 53):
+ if self.filters.range == 'Weekly':
+ period_end_date = add_days(from_date, 6)
+ else:
+ period_end_date = add_to_date(from_date, months=increment, days=-1)
+
+ if period_end_date > to_date:
+ period_end_date = to_date
+
+ self.periodic_daterange.append(period_end_date)
+
+ from_date = add_days(period_end_date, 1)
+ if period_end_date == to_date:
+ break
+
+ def get_issues(self):
+ filters = self.get_common_filters()
+ self.field_map = {
+ 'Customer': 'customer',
+ 'Issue Type': 'issue_type',
+ 'Issue Priority': 'priority',
+ 'Assigned To': '_assign'
+ }
+
+ self.entries = frappe.db.get_all('Issue',
+ fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date'],
+ filters=filters
+ )
+
+ def get_common_filters(self):
+ filters = {}
+ filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date])
+
+ if self.filters.get('assigned_to'):
+ filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%')
+
+ for entry in ['company', 'status', 'priority', 'customer', 'project']:
+ if self.filters.get(entry):
+ filters[entry] = self.filters.get(entry)
+
+ return filters
+
+ def get_rows(self):
+ self.data = []
+ self.get_periodic_data()
+
+ for entity, period_data in iteritems(self.issue_periodic_data):
+ if self.filters.based_on == 'Customer':
+ row = {'customer': entity}
+ elif self.filters.based_on == 'Assigned To':
+ row = {'user': entity}
+ elif self.filters.based_on == 'Issue Type':
+ row = {'issue_type': entity}
+ elif self.filters.based_on == 'Issue Priority':
+ row = {'priority': entity}
+
+ total = 0
+ for end_date in self.periodic_daterange:
+ period = self.get_period(end_date)
+ amount = flt(period_data.get(period, 0.0))
+ row[scrub(period)] = amount
+ total += amount
+
+ row['total'] = total
+
+ self.data.append(row)
+
+ def get_periodic_data(self):
+ self.issue_periodic_data = frappe._dict()
+
+ for d in self.entries:
+ period = self.get_period(d.get('opening_date'))
+
+ if self.filters.based_on == 'Assigned To':
+ if d._assign:
+ for entry in json.loads(d._assign):
+ self.issue_periodic_data.setdefault(entry, frappe._dict()).setdefault(period, 0.0)
+ self.issue_periodic_data[entry][period] += 1
+
+ else:
+ field = self.field_map.get(self.filters.based_on)
+ value = d.get(field)
+ if not value:
+ value = _('Not Specified')
+
+ self.issue_periodic_data.setdefault(value, frappe._dict()).setdefault(period, 0.0)
+ self.issue_periodic_data[value][period] += 1
+
+ def get_chart_data(self):
+ length = len(self.columns)
+ labels = [d.get('label') for d in self.columns[1:length-1]]
+ self.chart = {
+ 'data': {
+ 'labels': labels,
+ 'datasets': []
+ },
+ 'type': 'line'
+ }
\ No newline at end of file
diff --git a/erpnext/support/report/issue_analytics/test_issue_analytics.py b/erpnext/support/report/issue_analytics/test_issue_analytics.py
new file mode 100644
index 0000000..7748319
--- /dev/null
+++ b/erpnext/support/report/issue_analytics/test_issue_analytics.py
@@ -0,0 +1,214 @@
+from __future__ import unicode_literals
+import unittest
+import frappe
+from frappe.utils import getdate, add_months
+from erpnext.support.report.issue_analytics.issue_analytics import execute
+from erpnext.support.doctype.issue.test_issue import make_issue, create_customer
+from erpnext.support.doctype.service_level_agreement.test_service_level_agreement import create_service_level_agreements_for_issues
+from frappe.desk.form.assign_to import add as add_assignment
+
+months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec']
+
+class TestIssueAnalytics(unittest.TestCase):
+ @classmethod
+ def setUpClass(self):
+ frappe.db.sql("delete from `tabIssue` where company='_Test Company'")
+ frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1)
+
+ current_month_date = getdate()
+ last_month_date = add_months(current_month_date, -1)
+ self.current_month = str(months[current_month_date.month - 1]).lower()
+ self.last_month = str(months[last_month_date.month - 1]).lower()
+ if current_month_date.year != last_month_date.year:
+ self.current_month += '_' + str(current_month_date.year)
+ self.last_month += '_' + str(last_month_date.year)
+
+ def test_issue_analytics(self):
+ create_service_level_agreements_for_issues()
+ create_issue_types()
+ create_records()
+
+ self.compare_result_for_customer()
+ self.compare_result_for_issue_type()
+ self.compare_result_for_issue_priority()
+ self.compare_result_for_assignment()
+
+ def compare_result_for_customer(self):
+ filters = {
+ 'company': '_Test Company',
+ 'based_on': 'Customer',
+ 'from_date': add_months(getdate(), -1),
+ 'to_date': getdate(),
+ 'range': 'Monthly'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ 'customer': '__Test Customer 2',
+ self.last_month: 1.0,
+ self.current_month: 0.0,
+ 'total': 1.0
+ },
+ {
+ 'customer': '__Test Customer 1',
+ self.last_month: 0.0,
+ self.current_month: 1.0,
+ 'total': 1.0
+ },
+ {
+ 'customer': '__Test Customer',
+ self.last_month: 1.0,
+ self.current_month: 1.0,
+ 'total': 2.0
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
+
+ def compare_result_for_issue_type(self):
+ filters = {
+ 'company': '_Test Company',
+ 'based_on': 'Issue Type',
+ 'from_date': add_months(getdate(), -1),
+ 'to_date': getdate(),
+ 'range': 'Monthly'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ 'issue_type': 'Discomfort',
+ self.last_month: 1.0,
+ self.current_month: 0.0,
+ 'total': 1.0
+ },
+ {
+ 'issue_type': 'Service Request',
+ self.last_month: 0.0,
+ self.current_month: 1.0,
+ 'total': 1.0
+ },
+ {
+ 'issue_type': 'Bug',
+ self.last_month: 1.0,
+ self.current_month: 1.0,
+ 'total': 2.0
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
+
+ def compare_result_for_issue_priority(self):
+ filters = {
+ 'company': '_Test Company',
+ 'based_on': 'Issue Priority',
+ 'from_date': add_months(getdate(), -1),
+ 'to_date': getdate(),
+ 'range': 'Monthly'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ 'priority': 'Medium',
+ self.last_month: 1.0,
+ self.current_month: 1.0,
+ 'total': 2.0
+ },
+ {
+ 'priority': 'Low',
+ self.last_month: 1.0,
+ self.current_month: 0.0,
+ 'total': 1.0
+ },
+ {
+ 'priority': 'High',
+ self.last_month: 0.0,
+ self.current_month: 1.0,
+ 'total': 1.0
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
+
+ def compare_result_for_assignment(self):
+ filters = {
+ 'company': '_Test Company',
+ 'based_on': 'Assigned To',
+ 'from_date': add_months(getdate(), -1),
+ 'to_date': getdate(),
+ 'range': 'Monthly'
+ }
+
+ report = execute(filters)
+
+ expected_data = [
+ {
+ 'user': 'test@example.com',
+ self.last_month: 1.0,
+ self.current_month: 1.0,
+ 'total': 2.0
+ },
+ {
+ 'user': 'test1@example.com',
+ self.last_month: 2.0,
+ self.current_month: 1.0,
+ 'total': 3.0
+ }
+ ]
+
+ self.assertEqual(expected_data, report[1]) # rows
+ self.assertEqual(len(report[0]), 4) # cols
+
+
+def create_issue_types():
+ for entry in ['Bug', 'Service Request', 'Discomfort']:
+ if not frappe.db.exists('Issue Type', entry):
+ frappe.get_doc({
+ 'doctype': 'Issue Type',
+ '__newname': entry
+ }).insert()
+
+
+def create_records():
+ create_customer("__Test Customer", "_Test SLA Customer Group", "__Test SLA Territory")
+ create_customer("__Test Customer 1", "_Test SLA Customer Group", "__Test SLA Territory")
+ create_customer("__Test Customer 2", "_Test SLA Customer Group", "__Test SLA Territory")
+
+ current_month_date = getdate()
+ last_month_date = add_months(current_month_date, -1)
+
+ issue = make_issue(current_month_date, "__Test Customer", 2, "High", "Bug")
+ add_assignment({
+ "assign_to": ["test@example.com"],
+ "doctype": "Issue",
+ "name": issue.name
+ })
+
+ issue = make_issue(last_month_date, "__Test Customer", 2, "Low", "Bug")
+ add_assignment({
+ "assign_to": ["test1@example.com"],
+ "doctype": "Issue",
+ "name": issue.name
+ })
+
+ issue = make_issue(current_month_date, "__Test Customer 1", 2, "Medium", "Service Request")
+ add_assignment({
+ "assign_to": ["test1@example.com"],
+ "doctype": "Issue",
+ "name": issue.name
+ })
+
+ issue = make_issue(last_month_date, "__Test Customer 2", 2, "Medium", "Discomfort")
+ add_assignment({
+ "assign_to": ["test@example.com", "test1@example.com"],
+ "doctype": "Issue",
+ "name": issue.name
+ })
\ No newline at end of file
diff --git a/erpnext/accounts/page/bank_reconciliation/__init__.py b/erpnext/support/report/issue_summary/__init__.py
similarity index 100%
copy from erpnext/accounts/page/bank_reconciliation/__init__.py
copy to erpnext/support/report/issue_summary/__init__.py
diff --git a/erpnext/support/report/issue_summary/issue_summary.js b/erpnext/support/report/issue_summary/issue_summary.js
new file mode 100644
index 0000000..684482a
--- /dev/null
+++ b/erpnext/support/report/issue_summary/issue_summary.js
@@ -0,0 +1,73 @@
+// Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+/* eslint-disable */
+
+frappe.query_reports["Issue Summary"] = {
+ "filters": [
+ {
+ fieldname: "company",
+ label: __("Company"),
+ fieldtype: "Link",
+ options: "Company",
+ default: frappe.defaults.get_user_default("Company"),
+ reqd: 1
+ },
+ {
+ fieldname: "based_on",
+ label: __("Based On"),
+ fieldtype: "Select",
+ options: ["Customer", "Issue Type", "Issue Priority", "Assigned To"],
+ default: "Customer",
+ reqd: 1
+ },
+ {
+ fieldname: "from_date",
+ label: __("From Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_global_default("year_start_date"),
+ reqd: 1
+ },
+ {
+ fieldname:"to_date",
+ label: __("To Date"),
+ fieldtype: "Date",
+ default: frappe.defaults.get_global_default("year_end_date"),
+ reqd: 1
+ },
+ {
+ fieldname: "status",
+ label: __("Status"),
+ fieldtype: "Select",
+ options:[
+ {label: __('Open'), value: 'Open'},
+ {label: __('Replied'), value: 'Replied'},
+ {label: __('Resolved'), value: 'Resolved'},
+ {label: __('Closed'), value: 'Closed'}
+ ]
+ },
+ {
+ fieldname: "priority",
+ label: __("Issue Priority"),
+ fieldtype: "Link",
+ options: "Issue Priority"
+ },
+ {
+ fieldname: "customer",
+ label: __("Customer"),
+ fieldtype: "Link",
+ options: "Customer"
+ },
+ {
+ fieldname: "project",
+ label: __("Project"),
+ fieldtype: "Link",
+ options: "Project"
+ },
+ {
+ fieldname: "assigned_to",
+ label: __("Assigned To"),
+ fieldtype: "Link",
+ options: "User"
+ }
+ ]
+};
\ No newline at end of file
diff --git a/erpnext/support/report/issue_summary/issue_summary.json b/erpnext/support/report/issue_summary/issue_summary.json
new file mode 100644
index 0000000..b8a580c
--- /dev/null
+++ b/erpnext/support/report/issue_summary/issue_summary.json
@@ -0,0 +1,26 @@
+{
+ "add_total_row": 0,
+ "columns": [],
+ "creation": "2020-10-12 01:01:55.181777",
+ "disable_prepared_report": 0,
+ "disabled": 0,
+ "docstatus": 0,
+ "doctype": "Report",
+ "filters": [],
+ "idx": 0,
+ "is_standard": "Yes",
+ "modified": "2020-10-12 14:54:55.655920",
+ "modified_by": "Administrator",
+ "module": "Support",
+ "name": "Issue Summary",
+ "owner": "Administrator",
+ "prepared_report": 0,
+ "ref_doctype": "Issue",
+ "report_name": "Issue Summary",
+ "report_type": "Script Report",
+ "roles": [
+ {
+ "role": "Support Team"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/support/report/issue_summary/issue_summary.py b/erpnext/support/report/issue_summary/issue_summary.py
new file mode 100644
index 0000000..3d73531
--- /dev/null
+++ b/erpnext/support/report/issue_summary/issue_summary.py
@@ -0,0 +1,353 @@
+# Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+import frappe
+import json
+from six import iteritems
+from frappe import _, scrub
+from frappe.utils import flt
+
+def execute(filters=None):
+ return IssueSummary(filters).run()
+
+class IssueSummary(object):
+ def __init__(self, filters=None):
+ self.filters = frappe._dict(filters or {})
+
+ def run(self):
+ self.get_columns()
+ self.get_data()
+ self.get_chart_data()
+ self.get_report_summary()
+
+ return self.columns, self.data, None, self.chart, self.report_summary
+
+ def get_columns(self):
+ self.columns = []
+
+ if self.filters.based_on == 'Customer':
+ self.columns.append({
+ 'label': _('Customer'),
+ 'options': 'Customer',
+ 'fieldname': 'customer',
+ 'fieldtype': 'Link',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Assigned To':
+ self.columns.append({
+ 'label': _('User'),
+ 'fieldname': 'user',
+ 'fieldtype': 'Link',
+ 'options': 'User',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Issue Type':
+ self.columns.append({
+ 'label': _('Issue Type'),
+ 'fieldname': 'issue_type',
+ 'fieldtype': 'Link',
+ 'options': 'Issue Type',
+ 'width': 200
+ })
+
+ elif self.filters.based_on == 'Issue Priority':
+ self.columns.append({
+ 'label': _('Issue Priority'),
+ 'fieldname': 'priority',
+ 'fieldtype': 'Link',
+ 'options': 'Issue Priority',
+ 'width': 200
+ })
+
+ self.statuses = ['Open', 'Replied', 'Resolved', 'Closed']
+ for status in self.statuses:
+ self.columns.append({
+ 'label': _(status),
+ 'fieldname': scrub(status),
+ 'fieldtype': 'Int',
+ 'width': 80
+ })
+
+ self.columns.append({
+ 'label': _('Total Issues'),
+ 'fieldname': 'total_issues',
+ 'fieldtype': 'Int',
+ 'width': 100
+ })
+
+ self.sla_status_map = {
+ 'SLA Failed': 'failed',
+ 'SLA Fulfilled': 'fulfilled',
+ 'SLA Ongoing': 'ongoing'
+ }
+
+ for label, fieldname in self.sla_status_map.items():
+ self.columns.append({
+ 'label': _(label),
+ 'fieldname': fieldname,
+ 'fieldtype': 'Int',
+ 'width': 100
+ })
+
+ self.metrics = ['Avg First Response Time', 'Avg Response Time', 'Avg Hold Time',
+ 'Avg Resolution Time', 'Avg User Resolution Time']
+
+ for metric in self.metrics:
+ self.columns.append({
+ 'label': _(metric),
+ 'fieldname': scrub(metric),
+ 'fieldtype': 'Duration',
+ 'width': 170
+ })
+
+ def get_data(self):
+ self.get_issues()
+ self.get_rows()
+
+ def get_issues(self):
+ filters = self.get_common_filters()
+ self.field_map = {
+ 'Customer': 'customer',
+ 'Issue Type': 'issue_type',
+ 'Issue Priority': 'priority',
+ 'Assigned To': '_assign'
+ }
+
+ self.entries = frappe.db.get_all('Issue',
+ fields=[self.field_map.get(self.filters.based_on), 'name', 'opening_date', 'status', 'avg_response_time',
+ 'first_response_time', 'total_hold_time', 'user_resolution_time', 'resolution_time', 'agreement_status'],
+ filters=filters
+ )
+
+ def get_common_filters(self):
+ filters = {}
+ filters['opening_date'] = ('between', [self.filters.from_date, self.filters.to_date])
+
+ if self.filters.get('assigned_to'):
+ filters['_assign'] = ('like', '%' + self.filters.get('assigned_to') + '%')
+
+ for entry in ['company', 'status', 'priority', 'customer', 'project']:
+ if self.filters.get(entry):
+ filters[entry] = self.filters.get(entry)
+
+ return filters
+
+ def get_rows(self):
+ self.data = []
+ self.get_summary_data()
+
+ for entity, data in iteritems(self.issue_summary_data):
+ if self.filters.based_on == 'Customer':
+ row = {'customer': entity}
+ elif self.filters.based_on == 'Assigned To':
+ row = {'user': entity}
+ elif self.filters.based_on == 'Issue Type':
+ row = {'issue_type': entity}
+ elif self.filters.based_on == 'Issue Priority':
+ row = {'priority': entity}
+
+ for status in self.statuses:
+ count = flt(data.get(status, 0.0))
+ row[scrub(status)] = count
+
+ row['total_issues'] = data.get('total_issues', 0.0)
+
+ for sla_status in self.sla_status_map.values():
+ value = flt(data.get(sla_status), 0.0)
+ row[sla_status] = value
+
+ for metric in self.metrics:
+ value = flt(data.get(scrub(metric)), 0.0)
+ row[scrub(metric)] = value
+
+ self.data.append(row)
+
+ def get_summary_data(self):
+ self.issue_summary_data = frappe._dict()
+
+ for d in self.entries:
+ status = d.status
+ agreement_status = scrub(d.agreement_status)
+
+ if self.filters.based_on == 'Assigned To':
+ if d._assign:
+ for entry in json.loads(d._assign):
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(status, 0.0)
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(agreement_status, 0.0)
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault('total_issues', 0.0)
+ self.issue_summary_data[entry][status] += 1
+ self.issue_summary_data[entry][agreement_status] += 1
+ self.issue_summary_data[entry]['total_issues'] += 1
+
+ else:
+ field = self.field_map.get(self.filters.based_on)
+ value = d.get(field)
+ if not value:
+ value = _('Not Specified')
+
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(status, 0.0)
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(agreement_status, 0.0)
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault('total_issues', 0.0)
+ self.issue_summary_data[value][status] += 1
+ self.issue_summary_data[value][agreement_status] += 1
+ self.issue_summary_data[value]['total_issues'] += 1
+
+ self.get_metrics_data()
+
+ def get_metrics_data(self):
+ issues = []
+
+ metrics_list = ['avg_response_time', 'avg_first_response_time', 'avg_hold_time',
+ 'avg_resolution_time', 'avg_user_resolution_time']
+
+ for entry in self.entries:
+ issues.append(entry.name)
+
+ field = self.field_map.get(self.filters.based_on)
+
+ if issues:
+ if self.filters.based_on == 'Assigned To':
+ assignment_map = frappe._dict()
+ for d in self.entries:
+ if d._assign:
+ for entry in json.loads(d._assign):
+ for metric in metrics_list:
+ self.issue_summary_data.setdefault(entry, frappe._dict()).setdefault(metric, 0.0)
+
+ self.issue_summary_data[entry]['avg_response_time'] += d.get('avg_response_time') or 0.0
+ self.issue_summary_data[entry]['avg_first_response_time'] += d.get('first_response_time') or 0.0
+ self.issue_summary_data[entry]['avg_hold_time'] += d.get('total_hold_time') or 0.0
+ self.issue_summary_data[entry]['avg_resolution_time'] += d.get('resolution_time') or 0.0
+ self.issue_summary_data[entry]['avg_user_resolution_time'] += d.get('user_resolution_time') or 0.0
+
+ if not assignment_map.get(entry):
+ assignment_map[entry] = 0
+ assignment_map[entry] += 1
+
+ for entry in assignment_map:
+ for metric in metrics_list:
+ self.issue_summary_data[entry][metric] /= flt(assignment_map.get(entry))
+
+ else:
+ data = frappe.db.sql("""
+ SELECT
+ {0}, AVG(first_response_time) as avg_frt,
+ AVG(avg_response_time) as avg_resp_time,
+ AVG(total_hold_time) as avg_hold_time,
+ AVG(resolution_time) as avg_resolution_time,
+ AVG(user_resolution_time) as avg_user_resolution_time
+ FROM `tabIssue`
+ WHERE
+ name IN %(issues)s
+ GROUP BY {0}
+ """.format(field), {'issues': issues}, as_dict=1)
+
+ for entry in data:
+ value = entry.get(field)
+ if not value:
+ value = _('Not Specified')
+
+ for metric in metrics_list:
+ self.issue_summary_data.setdefault(value, frappe._dict()).setdefault(metric, 0.0)
+
+ self.issue_summary_data[value]['avg_response_time'] = entry.get('avg_resp_time') or 0.0
+ self.issue_summary_data[value]['avg_first_response_time'] = entry.get('avg_frt') or 0.0
+ self.issue_summary_data[value]['avg_hold_time'] = entry.get('avg_hold_time') or 0.0
+ self.issue_summary_data[value]['avg_resolution_time'] = entry.get('avg_resolution_time') or 0.0
+ self.issue_summary_data[value]['avg_user_resolution_time'] = entry.get('avg_user_resolution_time') or 0.0
+
+ def get_chart_data(self):
+ if not self.data:
+ return None
+
+ labels = []
+ open_issues = []
+ replied_issues = []
+ resolved_issues = []
+ closed_issues = []
+
+ entity = self.filters.based_on
+ entity_field = self.field_map.get(entity)
+ if entity == 'Assigned To':
+ entity_field = 'user'
+
+ for entry in self.data:
+ labels.append(entry.get(entity_field))
+ open_issues.append(entry.get('open'))
+ replied_issues.append(entry.get('replied'))
+ resolved_issues.append(entry.get('resolved'))
+ closed_issues.append(entry.get('closed'))
+
+ self.chart = {
+ 'data': {
+ 'labels': labels[:30],
+ 'datasets': [
+ {
+ 'name': 'Open',
+ 'values': open_issues[:30]
+ },
+ {
+ 'name': 'Replied',
+ 'values': replied_issues[:30]
+ },
+ {
+ 'name': 'Resolved',
+ 'values': resolved_issues[:30]
+ },
+ {
+ 'name': 'Closed',
+ 'values': closed_issues[:30]
+ }
+ ]
+ },
+ 'type': 'bar',
+ 'barOptions': {
+ 'stacked': True
+ }
+ }
+
+ def get_report_summary(self):
+ if not self.data:
+ return None
+
+ open_issues = 0
+ replied = 0
+ resolved = 0
+ closed = 0
+
+ for entry in self.data:
+ open_issues += entry.get('open')
+ replied += entry.get('replied')
+ resolved += entry.get('resolved')
+ closed += entry.get('closed')
+
+ self.report_summary = [
+ {
+ 'value': open_issues,
+ 'indicator': 'Red',
+ 'label': _('Open'),
+ 'datatype': 'Int',
+ },
+ {
+ 'value': replied,
+ 'indicator': 'Grey',
+ 'label': _('Replied'),
+ 'datatype': 'Int',
+ },
+ {
+ 'value': resolved,
+ 'indicator': 'Green',
+ 'label': _('Resolved'),
+ 'datatype': 'Int',
+ },
+ {
+ 'value': closed,
+ 'indicator': 'Green',
+ 'label': _('Closed'),
+ 'datatype': 'Int',
+ }
+ ]
+
diff --git a/erpnext/support/workspace/support/support.json b/erpnext/support/workspace/support/support.json
new file mode 100644
index 0000000..01a8676
--- /dev/null
+++ b/erpnext/support/workspace/support/support.json
@@ -0,0 +1,186 @@
+{
+ "category": "Modules",
+ "charts": [],
+ "creation": "2020-03-02 15:48:23.224699",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "icon": "support",
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Support",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Issues",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Issue",
+ "link_to": "Issue",
+ "link_type": "DocType",
+ "onboard": 1,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Issue Type",
+ "link_to": "Issue Type",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Issue Priority",
+ "link_to": "Issue Priority",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Maintenance",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Maintenance Schedule",
+ "link_to": "Maintenance Schedule",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Maintenance Visit",
+ "link_to": "Maintenance Visit",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Service Level Agreement",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Service Level Agreement",
+ "link_to": "Service Level Agreement",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Warranty",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Warranty Claim",
+ "link_to": "Warranty Claim",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Serial No",
+ "link_to": "Serial No",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Settings",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Support Settings",
+ "link_to": "Support Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Reports",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "Issue",
+ "hidden": 0,
+ "is_query_report": 1,
+ "label": "First Response Time for Issues",
+ "link_to": "First Response Time for Issues",
+ "link_type": "Report",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:37.073482",
+ "modified_by": "Administrator",
+ "module": "Support",
+ "name": "Support",
+ "owner": "Administrator",
+ "pin_to_bottom": 0,
+ "pin_to_top": 0,
+ "shortcuts": [
+ {
+ "color": "Yellow",
+ "format": "{} Assigned",
+ "label": "Issue",
+ "link_to": "Issue",
+ "stats_filter": "{\n \"_assign\": [\"like\", '%' + frappe.session.user + '%'],\n \"status\": \"Open\"\n}",
+ "type": "DocType"
+ },
+ {
+ "label": "Maintenance Visit",
+ "link_to": "Maintenance Visit",
+ "type": "DocType"
+ },
+ {
+ "label": "Service Level Agreement",
+ "link_to": "Service Level Agreement",
+ "type": "DocType"
+ }
+ ]
+}
\ No newline at end of file
diff --git a/erpnext/telephony/doctype/call_log/call_log.js b/erpnext/telephony/doctype/call_log/call_log.js
index 977f86d..e7afa0b 100644
--- a/erpnext/telephony/doctype/call_log/call_log.js
+++ b/erpnext/telephony/doctype/call_log/call_log.js
@@ -2,7 +2,26 @@
// For license information, please see license.txt
frappe.ui.form.on('Call Log', {
- // refresh: function(frm) {
-
- // }
+ refresh: function(frm) {
+ frm.events.setup_recording_audio_control(frm);
+ const incoming_call = frm.doc.type == 'Incoming';
+ frm.add_custom_button(incoming_call ? __('Callback'): __('Call Again'), () => {
+ const number = incoming_call ? frm.doc.from : frm.doc.to;
+ frappe.phone_call.handler(number, frm);
+ });
+ },
+ setup_recording_audio_control(frm) {
+ const recording_wrapper = frm.get_field('recording_html').$wrapper;
+ if (!frm.doc.recording_url || frm.doc.recording_url == 'null') {
+ recording_wrapper.empty();
+ } else {
+ recording_wrapper.addClass('input-max-width');
+ recording_wrapper.html(`
+ <audio
+ controls
+ src="${frm.doc.recording_url}">
+ </audio>
+ `);
+ }
+ }
});
diff --git a/erpnext/telephony/doctype/call_log/call_log.json b/erpnext/telephony/doctype/call_log/call_log.json
index 55ad2ba..1d6c39e 100644
--- a/erpnext/telephony/doctype/call_log/call_log.json
+++ b/erpnext/telephony/doctype/call_log/call_log.json
@@ -5,35 +5,27 @@
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
+ "call_details_section",
"id",
"from",
"to",
- "column_break_3",
- "received_by",
"medium",
- "caller_information",
- "contact",
- "contact_name",
- "column_break_10",
+ "start_time",
+ "end_time",
+ "column_break_4",
+ "type",
"customer",
- "lead",
- "lead_name",
- "section_break_5",
"status",
"duration",
- "recording_url"
+ "recording_url",
+ "recording_html",
+ "section_break_11",
+ "summary",
+ "section_break_19",
+ "links"
],
"fields": [
{
- "fieldname": "column_break_3",
- "fieldtype": "Column Break"
- },
- {
- "fieldname": "section_break_5",
- "fieldtype": "Section Break",
- "label": "Call Details"
- },
- {
"fieldname": "id",
"fieldtype": "Data",
"label": "ID",
@@ -50,6 +42,7 @@
{
"fieldname": "to",
"fieldtype": "Data",
+ "in_list_view": 1,
"label": "To",
"read_only": 1
},
@@ -58,13 +51,13 @@
"fieldtype": "Select",
"in_list_view": 1,
"label": "Status",
- "options": "Ringing\nIn Progress\nCompleted\nMissed",
+ "options": "Ringing\nIn Progress\nCompleted\nFailed\nBusy\nNo Answer\nQueued\nCanceled",
"read_only": 1
},
{
"description": "Call Duration in seconds",
"fieldname": "duration",
- "fieldtype": "Int",
+ "fieldtype": "Duration",
"in_list_view": 1,
"label": "Duration",
"read_only": 1
@@ -72,8 +65,8 @@
{
"fieldname": "recording_url",
"fieldtype": "Data",
- "label": "Recording URL",
- "read_only": 1
+ "hidden": 1,
+ "label": "Recording URL"
},
{
"fieldname": "medium",
@@ -82,51 +75,52 @@
"read_only": 1
},
{
- "fieldname": "received_by",
- "fieldtype": "Link",
- "label": "Received By",
- "options": "Employee",
+ "fieldname": "type",
+ "fieldtype": "Select",
+ "label": "Type",
+ "options": "Incoming\nOutgoing",
"read_only": 1
},
{
- "fieldname": "caller_information",
+ "fieldname": "recording_html",
+ "fieldtype": "HTML",
+ "label": "Recording HTML"
+ },
+ {
+ "fieldname": "section_break_19",
"fieldtype": "Section Break",
- "label": "Caller Information"
+ "label": "Reference"
},
{
- "fieldname": "contact",
- "fieldtype": "Link",
- "label": "Contact",
- "options": "Contact",
- "read_only": 1
+ "fieldname": "links",
+ "fieldtype": "Table",
+ "label": "Links",
+ "options": "Dynamic Link"
},
{
- "fieldname": "lead",
- "fieldtype": "Link",
- "label": "Lead ",
- "options": "Lead",
- "read_only": 1
- },
- {
- "fetch_from": "contact.name",
- "fieldname": "contact_name",
- "fieldtype": "Data",
- "hidden": 1,
- "in_list_view": 1,
- "label": "Contact Name",
- "read_only": 1
- },
- {
- "fieldname": "column_break_10",
+ "fieldname": "column_break_4",
"fieldtype": "Column Break"
},
{
- "fetch_from": "lead.lead_name",
- "fieldname": "lead_name",
- "fieldtype": "Data",
- "hidden": 1,
- "in_list_view": 1,
- "label": "Lead Name",
+ "fieldname": "summary",
+ "fieldtype": "Small Text"
+ },
+ {
+ "fieldname": "section_break_11",
+ "fieldtype": "Section Break",
+ "hide_border": 1,
+ "label": "Call Summary"
+ },
+ {
+ "fieldname": "start_time",
+ "fieldtype": "Datetime",
+ "label": "Start Time",
+ "read_only": 1
+ },
+ {
+ "fieldname": "end_time",
+ "fieldtype": "Datetime",
+ "label": "End Time",
"read_only": 1
},
{
@@ -135,11 +129,17 @@
"label": "Customer",
"options": "Customer",
"read_only": 1
+ },
+ {
+ "fieldname": "call_details_section",
+ "fieldtype": "Section Break",
+ "label": "Call Details"
}
],
+ "in_create": 1,
"index_web_pages_for_search": 1,
"links": [],
- "modified": "2020-11-25 14:32:44.407815",
+ "modified": "2021-02-08 14:23:28.744844",
"modified_by": "Administrator",
"module": "Telephony",
"name": "Call Log",
@@ -162,8 +162,8 @@
"role": "Employee"
}
],
- "sort_field": "modified",
- "sort_order": "ASC",
+ "sort_field": "creation",
+ "sort_order": "DESC",
"title_field": "from",
"track_changes": 1,
"track_views": 1
diff --git a/erpnext/telephony/doctype/call_log/call_log.py b/erpnext/telephony/doctype/call_log/call_log.py
index 296473e..4d553df 100644
--- a/erpnext/telephony/doctype/call_log/call_log.py
+++ b/erpnext/telephony/doctype/call_log/call_log.py
@@ -8,40 +8,83 @@
from frappe.model.document import Document
from erpnext.crm.doctype.utils import get_scheduled_employees_for_popup, strip_number
from frappe.contacts.doctype.contact.contact import get_contact_with_phone_number
+from frappe.core.doctype.dynamic_link.dynamic_link import deduplicate_dynamic_links
+
from erpnext.crm.doctype.lead.lead import get_lead_with_phone_number
+END_CALL_STATUSES = ['No Answer', 'Completed', 'Busy', 'Failed']
+ONGOING_CALL_STATUSES = ['Ringing', 'In Progress']
+
+
class CallLog(Document):
+ def validate(self):
+ deduplicate_dynamic_links(self)
+
def before_insert(self):
- number = strip_number(self.get('from'))
- self.contact = get_contact_with_phone_number(number)
- self.lead = get_lead_with_phone_number(number)
- if self.contact:
- contact = frappe.get_doc("Contact", self.contact)
- self.customer = contact.get_link_for("Customer")
+ """Add lead(third party person) links to the document.
+ """
+ lead_number = self.get('from') if self.is_incoming_call() else self.get('to')
+ lead_number = strip_number(lead_number)
+
+ contact = get_contact_with_phone_number(strip_number(lead_number))
+ if contact:
+ self.add_link(link_type='Contact', link_name=contact)
+
+ lead = get_lead_with_phone_number(lead_number)
+ if lead:
+ self.add_link(link_type='Lead', link_name=lead)
def after_insert(self):
self.trigger_call_popup()
def on_update(self):
+ def _is_call_missed(doc_before_save, doc_after_save):
+ # FIXME: This works for Exotel but not for all telepony providers
+ return doc_before_save.to != doc_after_save.to and doc_after_save.status not in END_CALL_STATUSES
+
+ def _is_call_ended(doc_before_save, doc_after_save):
+ return doc_before_save.status not in END_CALL_STATUSES and self.status in END_CALL_STATUSES
+
doc_before_save = self.get_doc_before_save()
if not doc_before_save: return
- if doc_before_save.status in ['Ringing'] and self.status in ['Missed', 'Completed']:
- frappe.publish_realtime('call_{id}_disconnected'.format(id=self.id), self)
- elif doc_before_save.to != self.to:
+
+ if _is_call_missed(doc_before_save, self):
+ frappe.publish_realtime('call_{id}_missed'.format(id=self.id), self)
self.trigger_call_popup()
+ if _is_call_ended(doc_before_save, self):
+ frappe.publish_realtime('call_{id}_ended'.format(id=self.id), self)
+
+ def is_incoming_call(self):
+ return self.type == 'Incoming'
+
+ def add_link(self, link_type, link_name):
+ self.append('links', {
+ 'link_doctype': link_type,
+ 'link_name': link_name
+ })
+
def trigger_call_popup(self):
- scheduled_employees = get_scheduled_employees_for_popup(self.medium)
- employee_emails = get_employees_with_number(self.to)
+ if self.is_incoming_call():
+ scheduled_employees = get_scheduled_employees_for_popup(self.medium)
+ employee_emails = get_employees_with_number(self.to)
- # check if employees with matched number are scheduled to receive popup
- emails = set(scheduled_employees).intersection(employee_emails)
+ # check if employees with matched number are scheduled to receive popup
+ emails = set(scheduled_employees).intersection(employee_emails)
- # # if no employee found with matching phone number then show popup to scheduled employees
- # emails = emails or scheduled_employees if employee_emails
+ if frappe.conf.developer_mode:
+ self.add_comment(text=f"""
+ Scheduled Employees: {scheduled_employees}
+ Matching Employee: {employee_emails}
+ Show Popup To: {emails}
+ """)
- for email in emails:
- frappe.publish_realtime('show_call_popup', self, user=email)
+ if employee_emails and not emails:
+ self.add_comment(text=_("No employee was scheduled for call popup"))
+
+ for email in emails:
+ frappe.publish_realtime('show_call_popup', self, user=email)
+
@frappe.whitelist()
def add_call_summary(call_log, summary):
@@ -65,34 +108,69 @@
return employee_emails
-def set_caller_information(doc, state):
- '''Called from hooks on creation of Lead or Contact'''
- if doc.doctype not in ['Lead', 'Contact']: return
-
- numbers = [doc.get('phone'), doc.get('mobile_no')]
- # contact for Contact and lead for Lead
- fieldname = doc.doctype.lower()
-
- # contact_name or lead_name
- display_name_field = '{}_name'.format(fieldname)
-
- # Contact now has all the nos saved in child table
- if doc.doctype == 'Contact':
+def link_existing_conversations(doc, state):
+ """
+ Called from hooks on creation of Contact or Lead to link all the existing conversations.
+ """
+ if doc.doctype != 'Contact': return
+ try:
numbers = [d.phone for d in doc.phone_nos]
- for number in numbers:
- number = strip_number(number)
- if not number: continue
+ for number in numbers:
+ number = strip_number(number)
+ if not number: continue
+ logs = frappe.db.sql_list("""
+ SELECT cl.name FROM `tabCall Log` cl
+ LEFT JOIN `tabDynamic Link` dl
+ ON cl.name = dl.parent
+ WHERE (cl.`from` like %(phone_number)s or cl.`to` like %(phone_number)s)
+ GROUP BY cl.name
+ HAVING SUM(
+ CASE
+ WHEN dl.link_doctype = %(doctype)s AND dl.link_name = %(docname)s
+ THEN 1
+ ELSE 0
+ END
+ )=0
+ """, dict(
+ phone_number='%{}'.format(number),
+ docname=doc.name,
+ doctype = doc.doctype
+ )
+ )
- filters = frappe._dict({
- 'from': ['like', '%{}'.format(number)],
- fieldname: ''
+ for log in logs:
+ call_log = frappe.get_doc('Call Log', log)
+ call_log.add_link(link_type=doc.doctype, link_name=doc.name)
+ call_log.save()
+ frappe.db.commit()
+ except Exception:
+ frappe.log_error(title=_('Error during caller information update'))
+
+def get_linked_call_logs(doctype, docname):
+ # content will be shown in timeline
+ logs = frappe.get_all('Dynamic Link', fields=['parent'], filters={
+ 'parenttype': 'Call Log',
+ 'link_doctype': doctype,
+ 'link_name': docname
+ })
+
+ logs = set([log.parent for log in logs])
+
+ logs = frappe.get_all('Call Log', fields=['*'], filters={
+ 'name': ['in', logs]
+ })
+
+ timeline_contents = []
+ for log in logs:
+ log.show_call_button = 0
+ timeline_contents.append({
+ 'icon': 'call',
+ 'is_card': True,
+ 'creation': log.creation,
+ 'template': 'call_link',
+ 'template_data': log
})
- logs = frappe.get_all('Call Log', filters=filters)
+ return timeline_contents
- for log in logs:
- frappe.db.set_value('Call Log', log.name, {
- fieldname: doc.name,
- display_name_field: doc.get_title()
- }, update_modified=False)
diff --git a/erpnext/accounts/doctype/bank_statement_settings/__init__.py b/erpnext/telephony/doctype/voice_call_settings/__init__.py
similarity index 100%
copy from erpnext/accounts/doctype/bank_statement_settings/__init__.py
copy to erpnext/telephony/doctype/voice_call_settings/__init__.py
diff --git a/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py b/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py
new file mode 100644
index 0000000..85d6add
--- /dev/null
+++ b/erpnext/telephony/doctype/voice_call_settings/test_voice_call_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors
+# See license.txt
+from __future__ import unicode_literals
+
+# import frappe
+import unittest
+
+class TestVoiceCallSettings(unittest.TestCase):
+ pass
diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js
new file mode 100644
index 0000000..4a61b61
--- /dev/null
+++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Voice Call Settings', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json
new file mode 100644
index 0000000..25e55a2
--- /dev/null
+++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.json
@@ -0,0 +1,124 @@
+{
+ "actions": [],
+ "autoname": "field:user",
+ "creation": "2020-12-08 16:52:40.590146",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "user",
+ "call_receiving_device",
+ "column_break_3",
+ "greeting_message",
+ "agent_busy_message",
+ "agent_unavailable_message"
+ ],
+ "fields": [
+ {
+ "fieldname": "user",
+ "fieldtype": "Link",
+ "in_list_view": 1,
+ "label": "User",
+ "options": "User",
+ "permlevel": 1,
+ "reqd": 1,
+ "unique": 1
+ },
+ {
+ "fieldname": "greeting_message",
+ "fieldtype": "Data",
+ "label": "Greeting Message"
+ },
+ {
+ "fieldname": "agent_busy_message",
+ "fieldtype": "Data",
+ "label": "Agent Busy Message"
+ },
+ {
+ "fieldname": "agent_unavailable_message",
+ "fieldtype": "Data",
+ "label": "Agent Unavailable Message"
+ },
+ {
+ "default": "Computer",
+ "fieldname": "call_receiving_device",
+ "fieldtype": "Select",
+ "label": "Call Receiving Device",
+ "options": "Computer\nPhone"
+ },
+ {
+ "fieldname": "column_break_3",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "index_web_pages_for_search": 1,
+ "links": [],
+ "modified": "2020-12-14 18:49:34.600194",
+ "modified_by": "Administrator",
+ "module": "Telephony",
+ "name": "Voice Call Settings",
+ "owner": "Administrator",
+ "permissions": [
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "create": 1,
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "permlevel": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "delete": 1,
+ "email": 1,
+ "export": 1,
+ "permlevel": 2,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "System Manager",
+ "share": 1,
+ "write": 1
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "permlevel": 2,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "All",
+ "share": 1
+ }
+ ],
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py
new file mode 100644
index 0000000..ad3bbf1
--- /dev/null
+++ b/erpnext/telephony/doctype/voice_call_settings/voice_call_settings.py
@@ -0,0 +1,10 @@
+# -*- coding: utf-8 -*-
+# Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
+# For license information, please see license.txt
+
+from __future__ import unicode_literals
+# import frappe
+from frappe.model.document import Document
+
+class VoiceCallSettings(Document):
+ pass
diff --git a/erpnext/templates/emails/birthday_reminder.html b/erpnext/templates/emails/birthday_reminder.html
new file mode 100644
index 0000000..12cdf1e
--- /dev/null
+++ b/erpnext/templates/emails/birthday_reminder.html
@@ -0,0 +1,25 @@
+<div class="gray-container text-center">
+ <div>
+ {% for person in birthday_persons %}
+ {% if person.image %}
+ <img
+ class="avatar-frame standard-image"
+ src="{{ person.image }}"
+ style="{{ css_style or '' }}"
+ title="{{ person.name }}">
+ </span>
+ {% else %}
+ <span
+ class="avatar-frame standard-image"
+ style="{{ css_style or '' }}"
+ title="{{ person.name }}">
+ {{ frappe.utils.get_abbr(person.name) }}
+ </span>
+ {% endif %}
+ {% endfor %}
+ </div>
+ <div style="margin-top: 15px">
+ <span>{{ reminder_text }}</span>
+ <p class="text-muted">{{ message }}</p>
+ </div>
+</div>
\ No newline at end of file
diff --git a/erpnext/templates/generators/item/item.html b/erpnext/templates/generators/item/item.html
index d3691a6..135982d 100644
--- a/erpnext/templates/generators/item/item.html
+++ b/erpnext/templates/generators/item/item.html
@@ -3,21 +3,25 @@
{% block title %} {{ title }} {% endblock %}
{% block breadcrumbs %}
+<div class="item-breadcrumbs small text-muted">
{% include "templates/includes/breadcrumbs.html" %}
+</div>
{% endblock %}
{% block page_content %}
-{% from "erpnext/templates/includes/macros.html" import product_image %}
-<div class="item-content">
- <div class="product-page-content" itemscope itemtype="http://schema.org/Product">
- <div class="row mb-5">
- {% include "templates/generators/item/item_image.html" %}
- {% include "templates/generators/item/item_details.html" %}
+<div class="product-container">
+ {% from "erpnext/templates/includes/macros.html" import product_image %}
+ <div class="item-content">
+ <div class="product-page-content" itemscope itemtype="http://schema.org/Product">
+ <div class="row mb-5">
+ {% include "templates/generators/item/item_image.html" %}
+ {% include "templates/generators/item/item_details.html" %}
+ </div>
+
+ {% include "templates/generators/item/item_specifications.html" %}
+
+ {{ doc.website_content or '' }}
</div>
-
- {% include "templates/generators/item/item_specifications.html" %}
-
- {{ doc.website_content or '' }}
</div>
</div>
{% endblock %}
diff --git a/erpnext/templates/generators/item/item_add_to_cart.html b/erpnext/templates/generators/item/item_add_to_cart.html
index dbf15de..f5adbf0 100644
--- a/erpnext/templates/generators/item/item_add_to_cart.html
+++ b/erpnext/templates/generators/item/item_add_to_cart.html
@@ -6,10 +6,10 @@
<div class="item-cart row mt-2" data-variant-item-code="{{ item_code }}">
<div class="col-md-12">
{% if cart_settings.show_price and product_info.price %}
- <h4>
+ <div class="product-price">
{{ product_info.price.formatted_price_sales_uom }}
- <small class="text-muted">({{ product_info.price.formatted_price }} / {{ product_info.uom }})</small>
- </h4>
+ <small class="formatted-price">({{ product_info.price.formatted_price }} / {{ product_info.uom }})</small>
+ </div>
{% else %}
{{ _("Unit of Measurement") }} : {{ product_info.uom }}
{% endif %}
@@ -17,11 +17,11 @@
{% if cart_settings.show_stock_availability %}
<div>
{% if product_info.in_stock == 0 %}
- <span class="text-danger">
+ <span class="text-danger no-stock">
{{ _('Not in stock') }}
</span>
{% elif product_info.in_stock == 1 %}
- <span class="text-success">
+ <span class="text-success has-stock">
{{ _('In stock') }}
{% if product_info.show_stock_qty and product_info.stock_qty %}
({{ product_info.stock_qty[0][0] }})
@@ -30,7 +30,7 @@
{% endif %}
</div>
{% endif %}
- <div class="mt-3">
+ <div class="mt-5 mb-5">
{% if product_info.price and (cart_settings.allow_items_not_in_stock or product_info.in_stock) %}
<a href="/cart"
class="btn btn-light btn-view-in-cart {% if not product_info.qty %}hidden{% endif %}"
@@ -40,8 +40,13 @@
</a>
<button
data-item-code="{{item_code}}"
- class="btn btn-outline-primary btn-add-to-cart {% if product_info.qty %}hidden{% endif %}"
+ class="btn btn-primary btn-add-to-cart {% if product_info.qty %}hidden{% endif %} w-100"
>
+ <span class="mr-2">
+ <svg class="icon icon-md">
+ <use href="#icon-assets"></use>
+ </svg>
+ </span>
{{ _("Add to Cart") }}
</button>
{% endif %}
diff --git a/erpnext/templates/generators/item/item_configure.html b/erpnext/templates/generators/item/item_configure.html
index 73f9ec9..b61ac73 100644
--- a/erpnext/templates/generators/item/item_configure.html
+++ b/erpnext/templates/generators/item/item_configure.html
@@ -1,9 +1,9 @@
{% if shopping_cart and shopping_cart.cart_settings.enabled %}
{% set cart_settings = shopping_cart.cart_settings %}
-<div class="mt-3">
+<div class="mt-5 mb-6">
{% if cart_settings.enable_variants | int %}
- <button class="btn btn-primary btn-configure"
+ <button class="btn btn-primary-light btn-configure"
data-item-code="{{ doc.name }}"
data-item-name="{{ doc.item_name }}"
>
diff --git a/erpnext/templates/generators/item/item_configure.js b/erpnext/templates/generators/item/item_configure.js
index 5fd9011..8eadb84 100644
--- a/erpnext/templates/generators/item/item_configure.js
+++ b/erpnext/templates/generators/item/item_configure.js
@@ -187,42 +187,55 @@
}
get_html_for_item_found({ filtered_items_count, filtered_items, exact_match, product_info }) {
- const exact_match_message = __('1 exact match.');
- const one_item = exact_match.length === 1 ?
- exact_match[0] :
- filtered_items_count === 1 ?
- filtered_items[0] : '';
+ const one_item = exact_match.length === 1
+ ? exact_match[0]
+ : filtered_items_count === 1
+ ? filtered_items[0]
+ : '';
const item_add_to_cart = one_item ? `
- <div class="alert alert-success d-flex justify-content-between align-items-center" role="alert">
- <div>
- <div>${one_item} ${product_info && product_info.price ? '(' + product_info.price.formatted_price_sales_uom + ')' : ''}</div>
- </div>
- <a href data-action="btn_add_to_cart" data-item-code="${one_item}">
- ${__('Add to cart')}
- </a>
- </div>
- `: '';
+ <button data-item-code="${one_item}"
+ class="btn btn-primary btn-add-to-cart w-100"
+ data-action="btn_add_to_cart"
+ >
+ <span class="mr-2">
+ ${frappe.utils.icon('assets', 'md')}
+ </span>
+ ${__("Add to Cart")}s
+ </button>
+ ` : '';
const items_found = filtered_items_count === 1 ?
__('{0} item found.', [filtered_items_count]) :
__('{0} items found.', [filtered_items_count]);
- const item_found_status = `
- <div class="alert alert-warning d-flex justify-content-between align-items-center" role="alert">
- <span>
- ${exact_match.length === 1 ? '' : items_found}
- ${exact_match.length === 1 ? `<span>${exact_match_message}</span>` : ''}
- </span>
- <a href data-action="btn_clear_values">
- ${__('Clear values')}
+ /* eslint-disable indent */
+ const item_found_status = exact_match.length === 1
+ ? `<div class="alert alert-success d-flex justify-content-between align-items-center" role="alert">
+ <div><div>
+ ${one_item}
+ ${product_info && product_info.price
+ ? '(' + product_info.price.formatted_price_sales_uom + ')'
+ : ''
+ }
+ </div></div>
+ <a href data-action="btn_clear_values" data-item-code="${one_item}">
+ ${__('Clear Values')}
</a>
- </div>
- `;
+ </div>`
+ : `<div class="alert alert-warning d-flex justify-content-between align-items-center" role="alert">
+ <span>
+ ${items_found}
+ </span>
+ <a href data-action="btn_clear_values">
+ ${__('Clear values')}
+ </a>
+ </div>`;
+ /* eslint-disable indent */
return `
- ${item_add_to_cart}
${item_found_status}
+ ${item_add_to_cart}
`;
}
@@ -254,8 +267,8 @@
}
append_status_area() {
- this.dialog.$status_area = $('<div class="status-area">');
- this.dialog.$wrapper.find('.modal-body').prepend(this.dialog.$status_area);
+ this.dialog.$status_area = $('<div class="status-area mt-5">');
+ this.dialog.$wrapper.find('.modal-body').append(this.dialog.$status_area);
this.dialog.$wrapper.on('click', '[data-action]', (e) => {
e.preventDefault();
const $target = $(e.currentTarget);
@@ -263,7 +276,7 @@
const method = this[action];
method.call(this, e);
});
- this.dialog.$body.css({ maxHeight: '75vh', overflow: 'auto', overflowX: 'hidden' });
+ this.dialog.$wrapper.addClass('item-configurator-dialog');
}
get_next_attribute_and_values(selected_attributes) {
diff --git a/erpnext/templates/generators/item/item_details.html b/erpnext/templates/generators/item/item_details.html
index 4cbecb0..3b77585 100644
--- a/erpnext/templates/generators/item/item_details.html
+++ b/erpnext/templates/generators/item/item_details.html
@@ -1,14 +1,21 @@
-<div class="col-md-8">
+<div class="col-md-7 product-details">
<!-- title -->
-<h1 itemprop="name">
+<h1 class="product-title" itemprop="name">
{{ item_name }}
</h1>
-<p class="text-muted">
+<p class="product-code">
<span>{{ _("Item Code") }}:</span>
<span itemprop="productID">{{ doc.name }}</span>
</p>
+{% if has_variants %}
+ <!-- configure template -->
+ {% include "templates/generators/item/item_configure.html" %}
+{% else %}
+ <!-- add variant to cart -->
+ {% include "templates/generators/item/item_add_to_cart.html" %}
+{% endif %}
<!-- description -->
-<div itemprop="description">
+<div class="product-description" itemprop="description">
{% if frappe.utils.strip_html(doc.web_long_description or '') %}
{{ doc.web_long_description | safe }}
{% elif frappe.utils.strip_html(doc.description or '') %}
@@ -17,12 +24,4 @@
{{ _("No description given") }}
{% endif %}
</div>
-
-{% if has_variants %}
- <!-- configure template -->
- {% include "templates/generators/item/item_configure.html" %}
-{% else %}
- <!-- add variant to cart -->
- {% include "templates/generators/item/item_add_to_cart.html" %}
-{% endif %}
</div>
diff --git a/erpnext/templates/generators/item/item_image.html b/erpnext/templates/generators/item/item_image.html
index 5d46a45..39a30d0 100644
--- a/erpnext/templates/generators/item/item_image.html
+++ b/erpnext/templates/generators/item/item_image.html
@@ -1,42 +1,42 @@
-<div class="col-md-4 h-100">
-{% if slides %}
-{{ product_image(slides[0].image, 'product-image') }}
-<div class="item-slideshow">
- {% for item in slides %}
- <img class="item-slideshow-image mt-2 {% if loop.first %}active{% endif %}"
- src="{{ item.image }}" alt="{{ item.heading }}">
- {% endfor %}
-</div>
-<!-- Simple image slideshow -->
-<script>
- frappe.ready(() => {
- $('.page_content').on('click', '.item-slideshow-image', (e) => {
- const $img = $(e.currentTarget);
- const link = $img.prop('src');
- const $product_image = $('.product-image');
- $product_image.find('a').prop('href', link);
- $product_image.find('img').prop('src', link);
+<div class="col-md-5 h-100 d-flex">
+ {% if slides %}
+ <div class="item-slideshow d-flex flex-column mr-3">
+ {% for item in slides %}
+ <img class="item-slideshow-image mb-2 {% if loop.first %}active{% endif %}"
+ src="{{ item.image }}" alt="{{ item.heading }}">
+ {% endfor %}
+ </div>
+ {{ product_image(slides[0].image, 'product-image') }}
+ <!-- Simple image slideshow -->
+ <script>
+ frappe.ready(() => {
+ $('.page_content').on('click', '.item-slideshow-image', (e) => {
+ const $img = $(e.currentTarget);
+ const link = $img.prop('src');
+ const $product_image = $('.product-image');
+ $product_image.find('a').prop('href', link);
+ $product_image.find('img').prop('src', link);
- $('.item-slideshow-image').removeClass('active');
- $img.addClass('active');
- });
- })
-</script>
-{% else %}
-{{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }}
-{% endif %}
+ $('.item-slideshow-image').removeClass('active');
+ $img.addClass('active');
+ });
+ })
+ </script>
+ {% else %}
+ {{ product_image(website_image or image or 'no-image.jpg', alt=website_image_alt or item_name) }}
+ {% endif %}
-<!-- Simple image preview -->
+ <!-- Simple image preview -->
-<div class="image-zoom-view" style="display: none;">
- <button type="button" class="close" aria-label="Close">
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
- stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x">
- <line x1="18" y1="6" x2="6" y2="18"></line>
- <line x1="6" y1="6" x2="18" y2="18"></line>
- </svg>
- </button>
-</div>
+ <div class="image-zoom-view" style="display: none;">
+ <button type="button" class="close" aria-label="Close">
+ <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor"
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-x">
+ <line x1="18" y1="6" x2="6" y2="18"></line>
+ <line x1="6" y1="6" x2="18" y2="18"></line>
+ </svg>
+ </button>
+ </div>
</div>
<style>
.website-image {
diff --git a/erpnext/templates/generators/item_group.html b/erpnext/templates/generators/item_group.html
index 40a064f..393c3a4 100644
--- a/erpnext/templates/generators/item_group.html
+++ b/erpnext/templates/generators/item_group.html
@@ -1,38 +1,139 @@
{% extends "templates/web.html" %}
-{% block header %}<h1>{{ name }}</h1>{% endblock %}
+{% block header %}
+<!-- <h2>{{ title }}</h2> -->
+{% endblock header %}
+
+{% block script %}
+<script type="text/javascript" src="/all-products/index.js"></script>
+{% endblock %}
{% block page_content %}
<div class="item-group-content" itemscope itemtype="http://schema.org/Product">
<div class="item-group-slideshow">
{% if slideshow %}<!-- slideshow -->
- {% include "templates/includes/slideshow.html" %}
+ {{ web_block(
+ "Hero Slider",
+ values=slideshow,
+ add_container=0,
+ add_top_padding=0,
+ add_bottom_padding=0,
+ ) }}
{% endif %}
+ <h2 class="mt-3">{{ title }}</h2>
{% if description %}<!-- description -->
- <div class="mb-3" itemprop="description">{{ description or ""}}</div>
+ <div class="item-group-description text-muted mb-5" itemprop="description">{{ description or ""}}</div>
{% endif %}
</div>
<div class="row">
- <div class="col-md-8">
- {% if items %}
- <div id="search-list">
- {% for i in range(0, page_length) %}
- {% if items[i] %}
- {%- set item = items[i] %}
+ <div class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
+ <div class="row products-list">
+ {% if items %}
+ {% for item in items %}
{% include "erpnext/www/all-products/item_row.html" %}
- {% endif %}
+ {% endfor %}
+ {% else %}
+ {% include "erpnext/www/all-products/not_found.html" %}
+ {% endif %}
+ </div>
+ </div>
+ <div class="col-12 order-1 col-md-3 order-md-1">
+ <div class="collapse d-md-block mr-4 filters-section" id="product-filters">
+ <div class="d-flex justify-content-between align-items-center mb-5 title-section">
+ <div class="mb-4 filters-title" > {{ _('Filters') }} </div>
+ <a class="mb-4 clear-filters" href="/{{ doc.route }}">{{ _('Clear All') }}</a>
+ </div>
+ {% for field_filter in field_filters %}
+ {%- set item_field = field_filter[0] %}
+ {%- set values = field_filter[1] %}
+ <div class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">{{ item_field.label }}</div>
+
+ {% if values | len > 20 %}
+ <!-- show inline filter if values more than 20 -->
+ <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
+ {% endif %}
+
+ {% if values %}
+ <div class="filter-options">
+ {% for value in values %}
+ <div class="checkbox" data-value="{{ value }}">
+ <label for="{{value}}">
+ <input type="checkbox"
+ class="product-filter field-filter"
+ id="{{value}}"
+ data-filter-name="{{ item_field.fieldname }}"
+ data-filter-value="{{ value }}"
+ >
+ <span class="label-area">{{ value }}</span>
+ </label>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <i class="text-muted">{{ _('No values') }}</i>
+ {% endif %}
+ </div>
+ {% endfor %}
+
+ {% for attribute in attribute_filters %}
+ <div class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">{{ attribute.name}}</div>
+ {% if values | len > 20 %}
+ <!-- show inline filter if values more than 20 -->
+ <input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
+ {% endif %}
+
+ {% if attribute.item_attribute_values %}
+ <div class="filter-options">
+ {% for attr_value in attribute.item_attribute_values %}
+ <div class="checkbox">
+ <label data-value="{{ value }}">
+ <input type="checkbox"
+ class="product-filter attribute-filter"
+ id="{{attr_value.name}}"
+ data-attribute-name="{{ attribute.name }}"
+ data-attribute-value="{{ attr_value.attribute_value }}"
+ {% if attr_value.checked %} checked {% endif %}>
+ <span class="label-area">{{ attr_value.attribute_value }}</span>
+ </label>
+ </div>
+ {% endfor %}
+ </div>
+ {% else %}
+ <i class="text-muted">{{ _('No values') }}</i>
+ {% endif %}
+ </div>
{% endfor %}
</div>
- <div class="item-group-nav-buttons">
- {% if frappe.form_dict.start|int > 0 %}
- <a class="btn btn-outline-secondary" href="/{{ pathname }}?start={{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</a>
- {% endif %}
- {% if items|length > page_length %}
- <a class="btn btn-outline-secondary" href="/{{ pathname }}?start={{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</a>
- {% endif %}
- </div>
- {% else %}
- <div class="text-muted">{{ _("No items listed") }}.</div>
+
+ <script>
+ frappe.ready(() => {
+ $('.product-filter-filter').on('keydown', frappe.utils.debounce((e) => {
+ const $input = $(e.target);
+ const keyword = ($input.val() || '').toLowerCase();
+ const $filter_options = $input.next('.filter-options');
+
+ $filter_options.find('.custom-control').show();
+ $filter_options.find('.custom-control').each((i, el) => {
+ const $el = $(el);
+ const value = $el.data('value').toLowerCase();
+ if (!value.includes(keyword)) {
+ $el.hide();
+ }
+ });
+ }, 300));
+ })
+ </script>
+ </div>
+ </div>
+ <div class="row">
+ <div class="col-12">
+ {% if frappe.form_dict.start|int > 0 %}
+ <button class="btn btn-outline-secondary btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</button>
+ {% endif %}
+ {% if items|length >= page_length %}
+ <button class="btn btn-outline-secondary btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
{% endif %}
</div>
</div>
diff --git a/erpnext/templates/generators/job_opening.html b/erpnext/templates/generators/job_opening.html
index f92e72e..c562db3 100644
--- a/erpnext/templates/generators/job_opening.html
+++ b/erpnext/templates/generators/job_opening.html
@@ -13,10 +13,21 @@
{%- if description -%}
<div>{{ description }}</div>
{% endif %}
+
+{%- if publish_salary_range -%}
+<div><b>{{_("Salary range per month")}}: </b>{{ frappe.format_value(frappe.utils.flt(lower_range), currency=currency) }} - {{ frappe.format_value(frappe.utils.flt(upper_range), currency=currency) }}</div>
+{% endif %}
+
<p style='margin-top: 30px'>
- <a class='btn btn-primary'
+ {%- if job_application_route -%}
+ <a class='btn btn-primary'
+ href='/{{job_application_route}}?new=1&job_title={{ doc.name }}'>
+ {{ _("Apply Now") }}</a>
+ {% else %}
+ <a class='btn btn-primary'
href='/job_application?new=1&job_title={{ doc.name }}'>
{{ _("Apply Now") }}</a>
+ {% endif %}
</p>
{% endblock %}
diff --git a/erpnext/templates/includes/address_row.html b/erpnext/templates/includes/address_row.html
index dadd2df..6d4dd54 100644
--- a/erpnext/templates/includes/address_row.html
+++ b/erpnext/templates/includes/address_row.html
@@ -2,7 +2,7 @@
<a href="/addresses?name={{ doc.name | urlencode }}" class="no-underline text-reset">
<div class="row">
<div class="col-3">
- <span class="indicator {{ "red" if doc.address_type=="Office" else "green" if doc.address_type=="Billing" else "blue" if doc.address_type=="Shipping" else "darkgrey" }}">{{ doc.address_title }}</span>
+ <span class="indicator {{ "red" if doc.address_type=="Office" else "green" if doc.address_type=="Billing" else "blue" if doc.address_type=="Shipping" else "gray" }}">{{ doc.address_title }}</span>
</div>
<div class="col-2"> {{ _(doc.address_type) }} </div>
<div class="col-2"> {{ doc.city }} </div>
diff --git a/erpnext/templates/includes/cart.js b/erpnext/templates/includes/cart.js
index c6dfd35..c390cd1 100644
--- a/erpnext/templates/includes/cart.js
+++ b/erpnext/templates/includes/cart.js
@@ -14,7 +14,7 @@
},
bind_events: function() {
- shopping_cart.bind_address_select();
+ shopping_cart.bind_address_picker_dialog();
shopping_cart.bind_place_order();
shopping_cart.bind_request_quotation();
shopping_cart.bind_change_qty();
@@ -23,28 +23,78 @@
shopping_cart.bind_coupon_code();
},
- bind_address_select: function() {
- $(".cart-addresses").on('click', '.address-card', function(e) {
- const $card = $(e.currentTarget);
- const address_type = $card.closest('[data-address-type]').attr('data-address-type');
- const address_name = $card.closest('[data-address-name]').attr('data-address-name');
- return frappe.call({
- type: "POST",
- method: "erpnext.shopping_cart.cart.update_cart_address",
- freeze: true,
- args: {
- address_type,
- address_name
- },
- callback: function(r) {
- if(!r.exc) {
- $(".cart-tax-items").html(r.message.taxes);
- }
- }
- });
+ bind_address_picker_dialog: function() {
+ const d = this.get_update_address_dialog();
+ this.parent.find('.btn-change-address').on('click', (e) => {
+ const type = $(e.currentTarget).parents('.address-container').attr('data-address-type');
+ $(d.get_field('address_picker').wrapper).html(
+ this.get_address_template(type)
+ );
+ d.show();
});
},
+ get_update_address_dialog() {
+ let d = new frappe.ui.Dialog({
+ title: "Select Address",
+ fields: [{
+ 'fieldtype': 'HTML',
+ 'fieldname': 'address_picker',
+ }],
+ primary_action_label: __('Set Address'),
+ primary_action: () => {
+ const $card = d.$wrapper.find('.address-card.active');
+ const address_type = $card.closest('[data-address-type]').attr('data-address-type');
+ const address_name = $card.closest('[data-address-name]').attr('data-address-name');
+ frappe.call({
+ type: "POST",
+ method: "erpnext.shopping_cart.cart.update_cart_address",
+ freeze: true,
+ args: {
+ address_type,
+ address_name
+ },
+ callback: function(r) {
+ d.hide();
+ if (!r.exc) {
+ $(".cart-tax-items").html(r.message.taxes);
+ shopping_cart.parent.find(
+ `.address-container[data-address-type="${address_type}"]`
+ ).html(r.message.address);
+ }
+ }
+ });
+ }
+ });
+
+ return d;
+ },
+
+ get_address_template(type) {
+ return {
+ shipping: `<div class="mb-3" data-section="shipping-address">
+ <div class="row no-gutters" data-fieldname="shipping_address_name">
+ {% for address in shipping_addresses %}
+ <div class="mr-3 mb-3 w-100" data-address-name="{{address.name}}" data-address-type="shipping"
+ {% if doc.shipping_address_name == address.name %} data-active {% endif %}>
+ {% include "templates/includes/cart/address_picker_card.html" %}
+ </div>
+ {% endfor %}
+ </div>
+ </div>`,
+ billing: `<div class="mb-3" data-section="billing-address">
+ <div class="row no-gutters" data-fieldname="customer_address">
+ {% for address in billing_addresses %}
+ <div class="mr-3 mb-3 w-100" data-address-name="{{address.name}}" data-address-type="billing"
+ {% if doc.shipping_address_name == address.name %} data-active {% endif %}>
+ {% include "templates/includes/cart/address_picker_card.html" %}
+ </div>
+ {% endfor %}
+ </div>
+ </div>`,
+ }[type];
+ },
+
bind_place_order: function() {
$(".btn-place-order").on("click", function() {
shopping_cart.place_order(this);
@@ -221,6 +271,7 @@
frappe.ready(function() {
$(".cart-icon").hide();
+ shopping_cart.parent = $(".cart-container");
shopping_cart.bind_events();
});
diff --git a/erpnext/templates/includes/cart/address_card.html b/erpnext/templates/includes/cart/address_card.html
index 646210e..667144b 100644
--- a/erpnext/templates/includes/cart/address_card.html
+++ b/erpnext/templates/includes/cart/address_card.html
@@ -1,12 +1,17 @@
<div class="card address-card h-100">
- <div class="check" style="position: absolute; right: 15px; top: 15px;">
- <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
+ <div class="btn btn-sm btn-default btn-change-address" style="position: absolute; right: 0; top: 0;">
+ {{ _('Change') }}
</div>
- <div class="card-body">
- <h5 class="card-title">{{ address.title }}</h5>
- <p class="card-text text-muted">
+ <div class="card-body p-0">
+ <div class="card-title">{{ address.title }}</div>
+ <div class="card-text mb-2">
{{ address.display }}
- </p>
- <a href="/addresses?name={{address.name}}" class="card-link">{{ _('Edit') }}</a>
+ </div>
+ <a href="/addresses?name={{address.name}}" class="card-link">
+ <svg class="icon icon-sm">
+ <use href="#icon-edit"></use>
+ </svg>
+ {{ _('Edit') }}
+ </a>
</div>
</div>
diff --git a/erpnext/templates/includes/cart/address_picker_card.html b/erpnext/templates/includes/cart/address_picker_card.html
new file mode 100644
index 0000000..2334ea2
--- /dev/null
+++ b/erpnext/templates/includes/cart/address_picker_card.html
@@ -0,0 +1,12 @@
+<div class="card address-card h-100">
+ <div class="check" style="position: absolute; right: 15px; top: 15px;">
+ <svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-check"><polyline points="20 6 9 17 4 12"></polyline></svg>
+ </div>
+ <div class="card-body">
+ <h5 class="card-title">{{ address.title }}</h5>
+ <p class="card-text text-muted">
+ {{ address.display }}
+ </p>
+ <a href="/addresses?name={{address.name}}" class="card-link">{{ _('Edit') }}</a>
+ </div>
+</div>
\ No newline at end of file
diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html
index aa25c88..84a9430 100644
--- a/erpnext/templates/includes/cart/cart_address.html
+++ b/erpnext/templates/includes/cart/cart_address.html
@@ -14,31 +14,39 @@
</div>
</div>
{% endif %}
-<div class="mb-3" data-section="shipping-address">
- <h6 class="text-uppercase">{{ _("Shipping Address") }}</h6>
+<div class="mb-3 frappe-card p-5" data-section="shipping-address">
+ <h6>{{ _("Shipping Address") }}</h6>
+ <hr>
+ {% for address in shipping_addresses %}
+ {% if doc.shipping_address_name == address.name %}
<div class="row no-gutters" data-fieldname="shipping_address_name">
- {% for address in shipping_addresses %}
- <div class="mr-3 mb-3 w-25" data-address-name="{{address.name}}" data-address-type="shipping" {% if doc.shipping_address_name == address.name %} data-active {% endif %}>
- {% include "templates/includes/cart/address_card.html" %}
- </div>
- {% endfor %}
+ <div class="w-100 address-container" data-address-name="{{address.name}}" data-address-type="shipping" data-active>
+ {% include "templates/includes/cart/address_card.html" %}
+ </div>
</div>
+ {% endif %}
+ {% endfor %}
</div>
-<div class="mb-3" data-section="billing-address">
- <h6 class="text-uppercase">{{ _("Billing Address") }}</h6>
- <div class="row no-gutters" data-fieldname="customer_address">
- {% for address in billing_addresses %}
- <div class="mr-3 mb-3 w-25" data-address-name="{{address.name}}" data-address-type="billing" {% if doc.customer_address == address.name %} data-active {% endif %}>
- {% include "templates/includes/cart/address_card.html" %}
- </div>
- {% endfor %}
- </div>
+<div class="checkbox ml-1 mb-2">
+ <label for="input_same_billing">
+ <input type="checkbox" id="input_same_billing" checked>
+ <span class="label-area">{{ _('Billing Address is same as Shipping Address') }}</span>
+ </label>
</div>
-<div class="custom-control custom-checkbox">
- <input type="checkbox" class="custom-control-input" id="input_same_billing" checked>
- <label class="custom-control-label" for="input_same_billing">{{ _('Billing Address is same as Shipping Address') }}</label>
+<div class="mb-3 frappe-card p-5" data-section="billing-address">
+ <h6>{{ _("Billing Address") }}</h6>
+ <hr>
+ {% for address in billing_addresses %}
+ {% if doc.customer_address == address.name %}
+ <div class="row no-gutters" data-fieldname="customer_address">
+ <div class="w-100 address-container" data-address-name="{{address.name}}" data-address-type="billing" data-active>
+ {% include "templates/includes/cart/address_card.html" %}
+ </div>
+ </div>
+ {% endif %}
+ {% endfor %}
</div>
-<button class="btn btn-outline-primary btn-sm mt-3 btn-new-address">{{ _("Add a new address") }}</button>
+<button class="btn btn-outline-primary btn-sm mt-1 btn-new-address bg-white">{{ _("Add a new address") }}</button>
<script>
frappe.ready(() => {
diff --git a/erpnext/templates/includes/cart/cart_address_picker.html b/erpnext/templates/includes/cart/cart_address_picker.html
new file mode 100644
index 0000000..72cc5f5
--- /dev/null
+++ b/erpnext/templates/includes/cart/cart_address_picker.html
@@ -0,0 +1,4 @@
+<div class="mb-3 frappe-card p-5" data-section="shipping-address">
+ <h6>{{ _("Shipping Address") }}</h6>
+</div>
+
diff --git a/erpnext/templates/includes/cart/cart_items.html b/erpnext/templates/includes/cart/cart_items.html
index ca5744b..75441c4 100644
--- a/erpnext/templates/includes/cart/cart_items.html
+++ b/erpnext/templates/includes/cart/cart_items.html
@@ -1,15 +1,15 @@
{% for d in doc.items %}
<tr data-name="{{ d.name }}">
<td>
- <div class="font-weight-bold">
+ <div class="item-title mb-1">
{{ d.item_name }}
</div>
- <div>
+ <div class="item-subtitle">
{{ d.item_code }}
</div>
{%- set variant_of = frappe.db.get_value('Item', d.item_code, 'variant_of') %}
{% if variant_of %}
- <span class="text-muted">
+ <span class="item-subtitle">
{{ _('Variant of') }} <a href="{{frappe.db.get_value('Item', variant_of, 'route')}}">{{ variant_of }}</a>
</span>
{% endif %}
@@ -20,20 +20,20 @@
<td class="text-right">
<div class="input-group number-spinner">
<span class="input-group-prepend d-none d-sm-inline-block">
- <button class="btn btn-outline-secondary cart-btn" data-dir="dwn">–</button>
+ <button class="btn cart-btn" data-dir="dwn">–</button>
</span>
- <input class="form-control text-right cart-qty border-secondary" value="{{ d.get_formatted('qty') }}" data-item-code="{{ d.item_code }}">
+ <input class="form-control text-center cart-qty" value="{{ d.get_formatted('qty') }}" data-item-code="{{ d.item_code }}">
<span class="input-group-append d-none d-sm-inline-block">
- <button class="btn btn-outline-secondary cart-btn" data-dir="up">+</button>
+ <button class="btn cart-btn" data-dir="up">+</button>
</span>
</div>
</td>
{% if cart_settings.enable_checkout %}
- <td class="text-right">
+ <td class="text-right item-subtotal">
<div>
{{ d.get_formatted('amount') }}
</div>
- <span class="text-muted">
+ <span class="item-rate">
{{ _('Rate:') }} {{ d.get_formatted('rate') }}
</span>
</td>
diff --git a/erpnext/templates/includes/footer/footer_extension.html b/erpnext/templates/includes/footer/footer_extension.html
index 6171b61..c7f0d06 100644
--- a/erpnext/templates/includes/footer/footer_extension.html
+++ b/erpnext/templates/includes/footer/footer_extension.html
@@ -1,12 +1,12 @@
{% if not hide_footer_signup %}
<div class="input-group">
- <input type="text" class="form-control border-secondary"
+ <input type="text" class="form-control"
id="footer-subscribe-email"
placeholder="{{ _('Your email address...') }}"
aria-label="{{ _('Your email address...') }}"
aria-describedby="footer-subscribe-button">
<div class="input-group-append">
- <button class="btn btn-sm btn-outline-secondary"
+ <button class="btn btn-sm btn-default"
type="button" id="footer-subscribe-button">{{ _("Get Updates") }}</button>
</div>
</div>
diff --git a/erpnext/templates/includes/issue_row.html b/erpnext/templates/includes/issue_row.html
index ff868fa..d909c5f 100644
--- a/erpnext/templates/includes/issue_row.html
+++ b/erpnext/templates/includes/issue_row.html
@@ -2,7 +2,7 @@
<a href="/issues?name={{ doc.name }}" class="no-underline">
<div class="row py-4 border-bottom">
<div class="col-3 d-flex align-items-center">
- {% set indicator = 'red' if doc.status == 'Open' else 'darkgrey' %}
+ {% set indicator = 'red' if doc.status == 'Open' else 'gray' %}
{% set indicator = 'green' if doc.status == 'Closed' else indicator %}
<span class="d-inline-flex indicator {{ indicator }}"></span>
{{ doc.name }}
@@ -10,7 +10,7 @@
<div class="col-5 text-muted">
{{ doc.subject }}</div>
<div class="col-2 d-flex align-items-center text-muted">
- {% set indicator = 'red' if doc.status == 'Open' else 'darkgrey' %}
+ {% set indicator = 'red' if doc.status == 'Open' else 'gray' %}
{% set indicator = 'green' if doc.status == 'Closed' else indicator %}
{% set indicator = 'orange' if doc.status == 'Open' and doc.priority == 'Medium' else indicator %}
{% set indicator = 'yellow' if doc.status == 'Open' and doc.priority == 'Low' else indicator %}
diff --git a/erpnext/templates/includes/macros.html b/erpnext/templates/includes/macros.html
index ea6b00f..c44bfb5 100644
--- a/erpnext/templates/includes/macros.html
+++ b/erpnext/templates/includes/macros.html
@@ -7,10 +7,10 @@
</div>
{% endmacro %}
-{% macro product_image(website_image, css_class="", alt="") %}
- <div class="border text-center rounded h-100 {{ css_class }}" style="overflow: hidden;">
+{% macro product_image(website_image, css_class="product-image", alt="") %}
+ <div class="border text-center rounded {{ css_class }}" style="overflow: hidden;">
<img itemprop="image" class="website-image h-100 w-100" alt="{{ alt }}" src="{{ frappe.utils.quoted(website_image or 'no-image.jpg') | abs_url }}">
- </div>
+ </div>
{% endmacro %}
{% macro media_image(website_image, name, css_class="") %}
@@ -18,13 +18,13 @@
{% if not website_image -%}
<div class="sidebar-standard-image"> <div class="standard-image" style="background-color: rgb(250, 251, 252);">{{name}}</div> </div>
{%- endif %}
- {% if website_image -%}
+ {% if website_image -%}
<a href="{{ frappe.utils.quoted(website_image) }}">
<img itemprop="image" src="{{ frappe.utils.quoted(website_image) | abs_url }}"
class="img-responsive img-thumbnail sidebar-image" style="min-height:100%; min-width:100%;">
</a>
- {%- endif %}
- </div>
+ {%- endif %}
+ </div>
{% endmacro %}
{% macro render_homepage_section(section) %}
@@ -40,7 +40,7 @@
<div class="col-md-{{ section.column_value }} mb-4">
<div class="card h-100 justify-content-between">
{% if card.image %}
- <div class="website-image-lazy" data-class="card-img-top h-100" data-src="{{ card.image }}" data-alt="{{ card.title }}"></div>
+ <div class="website-image-lazy" data-class="card-img-top h-75" data-src="{{ card.image }}" data-alt="{{ card.title }}"></div>
{% endif %}
<div class="card-body">
<h5 class="card-title">{{ card.title }}</h5>
@@ -57,4 +57,67 @@
</section>
{% endif %}
-{% endmacro %}
\ No newline at end of file
+{% endmacro %}
+
+{%- macro item_card(title, image, url, description, rate, category, is_featured=False, is_full_width=False, align="Left") -%}
+{%- set align_items_class = resolve_class({
+ 'align-items-end': align == 'Right',
+ 'align-items-center': align == 'Center',
+ 'align-items-start': align == 'Left',
+}) -%}
+{%- set col_size = 3 if is_full_width else 4 -%}
+{% if is_featured %}
+<div class="col-sm-{{ col_size*2 }} item-card">
+ <div class="card featured-item {{ align_items_class }}">
+ {% if image %}
+ <div class="row no-gutters">
+ <div class="col-md-6">
+ <img class="card-img" src="{{ image }}" alt="{{ title }}">
+ </div>
+ <div class="col-md-6">
+ {{ item_card_body(title, description, url, rate, category, is_featured, align) }}
+ </div>
+ </div>
+ {% else %}
+ <div class="col-md-12">
+ {{ item_card_body(title, description, url, rate, category, is_featured, align) }}
+ </div>
+ {% endif %}
+ </div>
+</div>
+{% else %}
+<div class="col-sm-{{ col_size }} item-card">
+ <div class="card {{ align_items_class }}">
+ {% if image %}
+ <div class="card-img-container">
+ <img class="card-img" src="{{ image }}" alt="{{ title }}">
+ </div>
+ {% else %}
+ <div class="card-img-top no-image">
+ {{ frappe.utils.get_abbr(title) }}
+ </div>
+ {% endif %}
+ {{ item_card_body(title, description, url, rate, category, is_featured, align) }}
+ </div>
+</div>
+{% endif %}
+{%- endmacro -%}
+
+{%- macro item_card_body(title, description, url, rate, category, is_featured, align) -%}
+{%- set align_class = resolve_class({
+ 'text-right': align == 'Right',
+ 'text-center': align == 'Center' and not is_featured,
+ 'text-left': align == 'Left' or is_featured,
+}) -%}
+<div class="card-body {{ align_class }}">
+ <div class="product-title">{{ title or '' }}</div>
+ {% if is_featured %}
+ <div class="product-price">{{ rate or '' }}</div>
+ <div class="product-description ellipsis">{{ description or '' }}</div>
+ {% else %}
+ <div class="product-category">{{ category or '' }}</div>
+ <div class="product-price">{{ rate or '' }}</div>
+ {% endif %}
+</div>
+<a href="/{{ url or '#' }}" class="stretched-link"></a>
+{%- endmacro -%}
\ No newline at end of file
diff --git a/erpnext/templates/includes/navbar/navbar_items.html b/erpnext/templates/includes/navbar/navbar_items.html
index 4daf0e7..133d99e 100644
--- a/erpnext/templates/includes/navbar/navbar_items.html
+++ b/erpnext/templates/includes/navbar/navbar_items.html
@@ -2,9 +2,11 @@
{% block navbar_right_extension %}
<li class="shopping-cart cart-icon hidden">
- <a href="/cart" class="nav-link">
- {{ _("Cart") }}
- <span class="badge badge-primary" id="cart-count"></span>
+ <a class="nav-link" href="/cart">
+ <svg class="icon icon-lg">
+ <use href="#icon-assets"></use>
+ </svg>
+ <span class="badge badge-primary cart-badge" id="cart-count"></span>
</a>
</li>
{% endblock %}
\ No newline at end of file
diff --git a/erpnext/templates/includes/order/order_taxes.html b/erpnext/templates/includes/order/order_taxes.html
index ebec838..d2c458e 100644
--- a/erpnext/templates/includes/order/order_taxes.html
+++ b/erpnext/templates/includes/order/order_taxes.html
@@ -29,12 +29,12 @@
{{ _("Discount") }}
</th>
<th class="text-right tot_quotation_discount">
- {% set tot_quotation_discount = [] %}
- {%- for item in doc.items -%}
- {% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
- * item.discount_percentage) / 100)) %}{% endif %}
- {% endfor %}
- {{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }}
+ {% set tot_quotation_discount = [] %}
+ {%- for item in doc.items -%}
+ {% if tot_quotation_discount.append((((item.price_list_rate * item.qty)
+ * item.discount_percentage) / 100)) %}{% endif %}
+ {% endfor %}
+ {{ frappe.utils.fmt_money((tot_quotation_discount | sum),currency=doc.currency) }}
</th>
</tr>
{% endif %}
@@ -47,51 +47,52 @@
{{ _("Total Amount") }}
</th>
<th class="text-right">
- <span>
- {% set total_amount = [] %}
- {%- for item in doc.items -%}
- {% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %}
- {% endfor %}
- {{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }}
- </span>
+ <span>
+ {% set total_amount = [] %}
+ {%- for item in doc.items -%}
+ {% if total_amount.append((item.price_list_rate * item.qty)) %}{% endif %}
+ {% endfor %}
+ {{ frappe.utils.fmt_money((total_amount | sum),currency=doc.currency) }}
+ </span>
</th>
</tr>
<tr>
- <th class="text-right" colspan="2">
- {{ _("Applied Coupon Code") }}
- </th>
- <th class="text-right">
- <span>
- {%- for row in frappe.get_all(doctype="Coupon Code",
- fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
- <span>{{ row.coupon_code }}</span>
- {% endfor %}
- </span>
- </th>
+ <th class="text-right" colspan="2">
+ {{ _("Applied Coupon Code") }}
+ </th>
+ <th class="text-right">
+ <span>
+ {%- for row in frappe.get_all(doctype="Coupon Code",
+ fields=["coupon_code"], filters={ "name":doc.coupon_code}) -%}
+ <span>{{ row.coupon_code }}</span>
+ {% endfor %}
+ </span>
+ </th>
</tr>
<tr>
- <th class="text-right" colspan="2">
- {{ _("Discount") }}
- </th>
- <th class="text-right">
- <span>
- {% set tot_SO_discount = [] %}
- {%- for item in doc.items -%}
- {% if tot_SO_discount.append((((item.price_list_rate * item.qty)
- * item.discount_percentage) / 100)) %}{% endif %}
- {% endfor %}
- {{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
- </span>
- </th>
+ <th class="text-right" colspan="2">
+ {{ _("Discount") }}
+ </th>
+ <th class="text-right">
+ <span>
+ {% set tot_SO_discount = [] %}
+ {%- for item in doc.items -%}
+ {% if tot_SO_discount.append((((item.price_list_rate * item.qty)
+ * item.discount_percentage) / 100)) %}{% endif %}
+ {% endfor %}
+ {{ frappe.utils.fmt_money((tot_SO_discount | sum),currency=doc.currency) }}
+ </span>
+ </th>
</tr>
{% endif %}
{% endif %}
<tr>
- <th class="text-right" colspan="2">
+ <th></th>
+ <th class="item-grand-total">
{{ _("Grand Total") }}
</th>
- <th class="text-right">
+ <th class="text-right item-grand-total">
{{ doc.get_formatted("grand_total") }}
</th>
</tr>
diff --git a/erpnext/templates/includes/products_as_list.html b/erpnext/templates/includes/products_as_list.html
index 88910d0..9bf9fd9 100644
--- a/erpnext/templates/includes/products_as_list.html
+++ b/erpnext/templates/includes/products_as_list.html
@@ -1,4 +1,4 @@
-{% from "erpnext/templates/includes/macros.html" import product_image_square %}
+{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
<a class="product-link product-list-link" href="{{ route|abs_url }}">
<div class='row'>
diff --git a/erpnext/templates/includes/projects/project_row.html b/erpnext/templates/includes/projects/project_row.html
index 73c83ef..4c8c40d 100644
--- a/erpnext/templates/includes/projects/project_row.html
+++ b/erpnext/templates/includes/projects/project_row.html
@@ -15,7 +15,7 @@
</div>
</div>
{% else %}
- <span class="indicator {{ "red" if doc.status=="Open" else "darkgrey" }}">
+ <span class="indicator {{ "red" if doc.status=="Open" else "gray" }}">
{{ doc.status }}</span>
{% endif %}
</div>
diff --git a/erpnext/templates/includes/projects/project_tasks.html b/erpnext/templates/includes/projects/project_tasks.html
index 94c692c..50b9f4b 100644
--- a/erpnext/templates/includes/projects/project_tasks.html
+++ b/erpnext/templates/includes/projects/project_tasks.html
@@ -3,7 +3,7 @@
<a class="no-decoration task-link {{ task.css_seen }}" href="/tasks?name={{ task.name }}">
<div class='row project-item'>
<div class='col-xs-9'>
- <span class="indicator {{ "red" if task.status=="Open" else "green" if task.status=="Closed" else "darkgrey" }}" title="{{ task.status }}" > {{ task.subject }}</span>
+ <span class="indicator {{ "red" if task.status=="Open" else "green" if task.status=="Closed" else "gray" }}" title="{{ task.status }}" > {{ task.subject }}</span>
<div class="small text-muted item-timestamp"
title="{{ frappe.utils.pretty_date(task.modified) }}">
{{ _("modified") }} {{ frappe.utils.pretty_date(task.modified) }}
@@ -16,9 +16,9 @@
</span>
{% else %}
<span class="avatar avatar-small standard-image" title="Assigned to {{ task.todo.owner }}">
-
+
</span>
- {% endif %}
+ {% endif %}
{% endif %} </div>
<div class='col-xs-2'>
<span class="pull-right list-comment-count small {{ "text-extra-muted" if task.comment_count==0 else "text-muted" }}">
diff --git a/erpnext/templates/includes/projects/project_timesheets.html b/erpnext/templates/includes/projects/project_timesheets.html
index fb3806c..05a07c1 100644
--- a/erpnext/templates/includes/projects/project_timesheets.html
+++ b/erpnext/templates/includes/projects/project_timesheets.html
@@ -3,19 +3,19 @@
<a class="no-decoration timesheet-link {{ timesheet.css_seen }}" href="/timesheet/{{ timesheet.info.name}}">
<div class='row project-item'>
<div class='col-xs-10'>
- <span class="indicator {{ "blue" if timesheet.info.status=="Submitted" else "red" if timesheet.info.status=="Draft" else "darkgrey" }}" title="{{ timesheet.info.status }}" > {{ timesheet.info.name }} </span>
+ <span class="indicator {{ "blue" if timesheet.info.status=="Submitted" else "red" if timesheet.info.status=="Draft" else "gray" }}" title="{{ timesheet.info.status }}" > {{ timesheet.info.name }} </span>
<div class="small text-muted item-timestamp">
{{ _("From") }} {{ frappe.format_date(timesheet.from_time) }} {{ _("to") }} {{ frappe.format_date(timesheet.to_time) }}
</div>
</div>
<div class='col-xs-1' style="margin-right:-30px;">
<span class="avatar avatar-small" title="{{ timesheet.info.modified_by }}"> <img src="{{ timesheet.info.user_image }}" style="display:flex;"></span>
- </div>
+ </div>
<div class='col-xs-1'>
<span class="pull-right list-comment-count small {{ "text-extra-muted" if timesheet.comment_count==0 else "text-muted" }}">
<i class="octicon octicon-comment-discussion"></i>
{{ timesheet.info.comment_count }}
- </span>
+ </span>
</div>
</div>
</a>
diff --git a/erpnext/templates/includes/timesheet/timesheet_row.html b/erpnext/templates/includes/timesheet/timesheet_row.html
index 4852f59..0f9cc77 100644
--- a/erpnext/templates/includes/timesheet/timesheet_row.html
+++ b/erpnext/templates/includes/timesheet/timesheet_row.html
@@ -1,7 +1,7 @@
<div class="web-list-item transaction-list-item">
<div class="row">
<div class="col-xs-3">
- <span class='indicator {{ "red" if doc.status=="Cancelled" else "green" if doc.status=="Billed" else "blue" if doc.status=="Submitted" else "darkgrey" }} small'>
+ <span class='indicator {{ "red" if doc.status=="Cancelled" else "green" if doc.status=="Billed" else "blue" if doc.status=="Submitted" else "gray" }} small'>
{{ doc.name }}
</span>
</div>
diff --git a/erpnext/templates/includes/transaction_row.html b/erpnext/templates/includes/transaction_row.html
index fd4835e..930d0c2 100644
--- a/erpnext/templates/includes/transaction_row.html
+++ b/erpnext/templates/includes/transaction_row.html
@@ -1,7 +1,7 @@
<div class="web-list-item transaction-list-item">
<div class="row">
<div class="col-sm-4">
- <span class="indicator small {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
+ <span class="indicator small {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "gray") }}">
{{ doc.name }}</span>
<div class="small text-muted transaction-time"
title="{{ frappe.utils.format_datetime(doc.modified, "medium") }}">
diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html
index 3033d15..ea34371 100644
--- a/erpnext/templates/pages/cart.html
+++ b/erpnext/templates/pages/cart.html
@@ -2,7 +2,7 @@
{% block title %} {{ _("Shopping Cart") }} {% endblock %}
-{% block header %}<h1>{{ _("Shopping Cart") }}</h1>{% endblock %}
+{% block header %}<h3 class="shopping-cart-header mt-2 mb-6">{{ _("Shopping Cart") }}</h1>{% endblock %}
<!--
{% block script %}
@@ -18,94 +18,122 @@
{% from "templates/includes/macros.html" import item_name_and_description %}
+{% if doc.items %}
<div class="cart-container">
- <div id="cart-error" class="alert alert-danger" style="display: none;"></div>
+ <div class="row m-0">
+ <div class="col-md-8 frappe-card p-5">
+ <div>
+ <div id="cart-error" class="alert alert-danger" style="display: none;"></div>
+ <div class="cart-items-header">
+ {{ _('Items') }}
+ </div>
+ <table class="table mt-3 cart-table">
+ <thead>
+ <tr>
+ <th width="60%">{{ _('Item') }}</th>
+ <th width="20%">{{ _('Quantity') }}</th>
+ {% if cart_settings.enable_checkout %}
+ <th width="20%" class="text-right">{{ _('Subtotal') }}</th>
+ {% endif %}
+ </tr>
+ </thead>
+ <tbody class="cart-items">
+ {% include "templates/includes/cart/cart_items.html" %}
+ </tbody>
+ {% if cart_settings.enable_checkout %}
+ <tfoot class="cart-tax-items">
+ {% include "templates/includes/order/order_taxes.html" %}
+ </tfoot>
+ {% endif %}
+ </table>
+ </div>
+ <div class="row">
+ <div class="col-4">
+ {% if cart_settings.enable_checkout %}
+ <a class="btn btn-outline-primary" href="/orders">
+ {{ _('See past orders') }}
+ </a>
+ {% else %}
+ <a class="btn btn-outline-primary" href="/quotations">
+ {{ _('See past quotations') }}
+ </a>
+ {% endif %}
+ </div>
+ <div class="col-8">
+ {% if doc.items %}
+ <div class="place-order-container">
+ <a class="btn btn-primary-light mr-2" href="/all-products">
+ {{ _("Continue Shopping") }}
+ </a>
+ {% if cart_settings.enable_checkout %}
+ <button class="btn btn-primary btn-place-order" type="button">
+ {{ _("Place Order") }}
+ </button>
+ {% else %}
+ <button class="btn btn-primary btn-request-for-quotation" type="button">
+ {{ _("Request for Quotation") }}
+ </button>
+ {% endif %}
+ </div>
+ {% endif %}
+ </div>
+ </div>
- {% if doc.items %}
- <table class="table table-bordered mt-3">
- <thead>
- <tr>
- <th width="60%">{{ _('Item') }}</th>
- <th width="20%" class="text-right">{{ _('Quantity') }}</th>
- {% if cart_settings.enable_checkout %}
- <th width="20%" class="text-right">{{ _('Subtotal') }}</th>
- {% endif %}
- </tr>
- </thead>
- <tbody class="cart-items">
- {% include "templates/includes/cart/cart_items.html" %}
- </tbody>
- {% if cart_settings.enable_checkout %}
- <tfoot class="cart-tax-items">
- {% include "templates/includes/order/order_taxes.html" %}
- </tfoot>
- {% endif %}
- </table>
- {% else %}
- <p class="text-muted">{{ _('Your cart is Empty') }}</p>
- {% endif %}
- {% if doc.items %}
- <div class="place-order-container">
- {% if cart_settings.enable_checkout %}
- <button class="btn btn-primary btn-place-order" type="button">
- {{ _("Place Order") }}
- </button>
- {% else %}
- <button class="btn btn-primary btn-request-for-quotation" type="button">
- {{ _("Request for Quotation") }}
- </button>
+ {% if doc.items %}
+ {% if doc.tc_name %}
+ <div class="terms-and-conditions-link">
+ <a href class="link-terms-and-conditions" data-terms-name="{{ doc.tc_name }}">
+ {{ _("Terms and Conditions") }}
+ </a>
+ <script>
+ frappe.ready(() => {
+ $('.link-terms-and-conditions').click((e) => {
+ e.preventDefault();
+ const $link = $(e.target);
+ const terms_name = $link.attr('data-terms-name');
+ show_terms_and_conditions(terms_name);
+ })
+ });
+ function show_terms_and_conditions(terms_name) {
+ frappe.call('erpnext.shopping_cart.cart.get_terms_and_conditions', { terms_name })
+ .then(r => {
+ frappe.msgprint({
+ title: terms_name,
+ message: r.message
+ });
+ });
+ }
+ </script>
+ </div>
{% endif %}
</div>
- {% endif %}
- {% if doc.items %}
- {% if doc.tc_name %}
- <div class="terms-and-conditions-link">
- <a href class="link-terms-and-conditions" data-terms-name="{{ doc.tc_name }}">
- {{ _("Terms and Conditions") }}
- </a>
- <script>
- frappe.ready(() => {
- $('.link-terms-and-conditions').click((e) => {
- e.preventDefault();
- const $link = $(e.target);
- const terms_name = $link.attr('data-terms-name');
- show_terms_and_conditions(terms_name);
- })
- });
- function show_terms_and_conditions(terms_name) {
- frappe.call('erpnext.shopping_cart.cart.get_terms_and_conditions', { terms_name })
- .then(r => {
- frappe.msgprint({
- title: terms_name,
- message: r.message
- });
- });
- }
- </script>
+ <div class="col-md-4">
+ <div class="cart-addresses">
+ {% include "templates/includes/cart/cart_address.html" %}
+ </div>
</div>
- {% endif %}
-
- <div class="cart-addresses mt-5">
- {% include "templates/includes/cart/cart_address.html" %}
+ {% endif %}
</div>
- {% endif %}
</div>
-
-<div class="row mt-5">
- <div class="col-12">
- {% if cart_settings.enable_checkout %}
- <a href="/orders">
+{% else %}
+<div class="cart-empty frappe-card">
+ <div class="cart-empty-state">
+ <img src="/assets/erpnext/images/ui-states/cart-empty-state.png" alt="Empty State">
+ </div>
+ <div class="cart-empty-message mt-4">{{ _('Your cart is Empty') }}</p>
+ {% if cart_settings.enable_checkout %}
+ <a class="btn btn-outline-primary" href="/orders">
{{ _('See past orders') }}
</a>
{% else %}
- <a href="/quotations">
+ <a class="btn btn-outline-primary" href="/quotations">
{{ _('See past quotations') }}
</a>
- {% endif %}
- </div>
+ {% endif %}
</div>
+{% endif %}
{% endblock %}
diff --git a/erpnext/templates/pages/material_request_info.html b/erpnext/templates/pages/material_request_info.html
index c7a7802..0c2772e 100644
--- a/erpnext/templates/pages/material_request_info.html
+++ b/erpnext/templates/pages/material_request_info.html
@@ -20,7 +20,7 @@
<div class="row transaction-subheading">
<div class="col-xs-6">
- <span class="indicator {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
+ <span class="indicator {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "gray") }}">
{{ _(doc.get('indicator_title')) or _(doc.status) or _("Submitted") }}
</span>
</div>
diff --git a/erpnext/templates/pages/order.html b/erpnext/templates/pages/order.html
index af7af11..07dd676 100644
--- a/erpnext/templates/pages/order.html
+++ b/erpnext/templates/pages/order.html
@@ -8,7 +8,7 @@
{% block title %}{{ doc.name }}{% endblock %}
{% block header %}
- <h1 class="m-0">{{ doc.name }}</h1>
+ <h2 class="m-0">{{ doc.name }}</h2>
{% endblock %}
{% block header_actions %}
@@ -33,7 +33,7 @@
<div class="row transaction-subheading">
<div class="col-6">
- <span class="indicator {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
+ <span class="indicator-pill {{ doc.indicator_color or ("blue" if doc.docstatus==1 else "darkgrey") }}">
{% if doc.doctype == "Quotation" and not doc.docstatus %}
{{ _("Pending") }}
{% else %}
@@ -41,7 +41,7 @@
{% endif %}
</span>
</div>
- <div class="col-6 text-muted text-right small">
+ <div class="col-6 text-muted text-right small pt-3">
{{ frappe.utils.format_date(doc.transaction_date, 'medium') }}
{% if doc.valid_till %}
<p>
@@ -66,38 +66,39 @@
{% endif %}
<div class="order-container">
-
<!-- items -->
- <div class="order-item-table">
- <div class="row order-items order-item-header text-muted">
- <div class="col-sm-6 col-6 h6 text-uppercase">
+ <table class="order-item-table w-100 table">
+ <thead class="order-items order-item-header">
+ <th width="60%">
{{ _("Item") }}
- </div>
- <div class="col-sm-3 col-xs-3 text-right h6 text-uppercase">
+ </th>
+ <th width="20%" class="text-right">
{{ _("Quantity") }}
- </div>
- <div class="col-sm-3 col-xs-3 text-right h6 text-uppercase">
+ </th>
+ <th width="20%" class="text-right">
{{ _("Amount") }}
- </div>
- </div>
+ </th>
+ </thead>
+ <tbody>
{% for d in doc.items %}
- <div class="row order-items">
- <div class="col-sm-6 col-6">
+ <tr class="order-items">
+ <td>
{{ item_name_and_description(d) }}
- </div>
- <div class="col-sm-3 col-xs-3 text-right">
+ </td>
+ <td class="text-right">
{{ d.qty }}
{% if d.delivered_qty is defined and d.delivered_qty != None %}
<p class="text-muted small">{{ _("Delivered") }} {{ d.delivered_qty }}</p>
{% endif %}
- </div>
- <div class="col-sm-3 col-xs-3 text-right">
+ </td>
+ <td class="text-right">
{{ d.get_formatted("amount") }}
<p class="text-muted small">{{ _("Rate:") }} {{ d.get_formatted("rate") }}</p>
- </div>
- </div>
+ </td>
+ </tr>
{% endfor %}
- </div>
+ </tbody>
+ </table>
<!-- taxes -->
<div class="order-taxes d-flex justify-content-end">
<table>
diff --git a/erpnext/templates/pages/rfq.html b/erpnext/templates/pages/rfq.html
index 5b27a94..6e2edb6 100644
--- a/erpnext/templates/pages/rfq.html
+++ b/erpnext/templates/pages/rfq.html
@@ -77,13 +77,13 @@
<div class="web-list-item transaction-list-item quotations" idx="{{d.name}}">
<div class="row">
<div class="col-sm-6">
- <span class="indicator darkgrey">{{d.name}}</span>
+ <span class="indicator gray">{{d.name}}</span>
</div>
<div class="col-sm-3">
- <span class="small darkgrey">{{d.status}}</span>
+ <span class="small gray">{{d.status}}</span>
</div>
<div class="col-sm-3">
- <span class="small darkgrey">{{d.transaction_date}}</span>
+ <span class="small gray">{{d.transaction_date}}</span>
</div>
</div>
<a class="transaction-item-link" href="/quotations/{{d.name}}">Link</a>
diff --git a/erpnext/templates/print_formats/includes/item_table_description.html b/erpnext/templates/print_formats/includes/item_table_description.html
index 070cca5..7569e50 100644
--- a/erpnext/templates/print_formats/includes/item_table_description.html
+++ b/erpnext/templates/print_formats/includes/item_table_description.html
@@ -1,7 +1,7 @@
-{%- set compact = doc.flags.compact_item_print -%}
-{%- set compact_fields = doc.flags.compact_item_fields -%}
+{%- set compact = print_settings.compact_item_print -%}
+{%- set compact_fields = parent_doc.flags.compact_item_fields -%}
{%- set display_columns = visible_columns|map(attribute="fieldname")| list -%}
-{%- set columns = doc.flags.format_columns(display_columns, compact_fields) -%}
+{%- set columns = parent_doc.flags.format_columns(display_columns, compact_fields) -%}
{% if doc.in_format_data("image") and doc.get("image") and "image" in display_columns -%}
<div class="pull-left" style="max-width: 40%; margin-right: 10px;">
@@ -11,11 +11,11 @@
<div>
{% if doc.in_format_data("item_code") and "item_code" in display_columns -%}
- <div class="primary">
- {% if compact %}<strong>{% endif %}
- {{ _(doc.item_code) }}
- {% if compact %}</strong>{% endif %}
+ {% if compact %}
+ <div class="primary compact-item">
+ {{ _(doc.item_code) }}
</div>
+ {% endif %}
{%- endif %}
{%- if doc.in_format_data("item_name") and "item_name" in display_columns and
diff --git a/erpnext/templates/print_formats/includes/item_table_qty.html b/erpnext/templates/print_formats/includes/item_table_qty.html
index ecaaef4..8e68f1c 100644
--- a/erpnext/templates/print_formats/includes/item_table_qty.html
+++ b/erpnext/templates/print_formats/includes/item_table_qty.html
@@ -1,4 +1,4 @@
-{% set qty_first=frappe.db.get_single_value("Print Settings", "print_uom_after_quantity") %}
+{% set qty_first=print_settings.print_uom_after_quantity %}
{% if qty_first %}
{{ doc.get_formatted("qty", doc) }}
{% if (doc.uom and not doc.is_print_hide("uom")) %} {{ _(doc.uom) }}
diff --git a/erpnext/templates/print_formats/includes/items.html b/erpnext/templates/print_formats/includes/items.html
new file mode 100644
index 0000000..55598e7
--- /dev/null
+++ b/erpnext/templates/print_formats/includes/items.html
@@ -0,0 +1,37 @@
+{%- if data -%}
+ {%- set visible_columns = get_visible_columns(doc.get(df.fieldname),
+ table_meta, df) -%}
+ <div {{ fieldmeta(df) }}>
+ <table class="table table-bordered table-condensed">
+ <thead>
+ <tr>
+ <th style="width: 40px" class="table-sr">{{ _("Sr") }}</th>
+ {% for tdf in visible_columns %}
+ {% if (data and not print_settings.compact_item_print) or tdf.fieldname in doc.flags.compact_item_fields %}
+ <th style="width: {{ get_width(tdf) }};" class="{{ get_align_class(tdf) }}" {{ fieldmeta(df) }}>
+ {{ _(tdf.label) }}</th>
+ {% endif %}
+ {% endfor %}
+ </tr>
+ </thead>
+ <tbody>
+ {% for d in data %}
+ <tr>
+ <td class="table-sr">{{ d.idx }}</td>
+ {% for tdf in visible_columns %}
+ {% if not print_settings.compact_item_print or tdf.fieldname in doc.flags.compact_item_fields %}
+ <td class="{{ get_align_class(tdf) }}" {{ fieldmeta(df) }}>
+ {% if doc.child_print_templates %}
+ {%- set child_templates = doc.child_print_templates.get(df.fieldname) %}
+ <div class="value">{{ print_value(tdf, d, doc, visible_columns, child_templates) }}</div></td>
+ {% else %}
+ <div class="value">{{ print_value(tdf, d, doc, visible_columns) }}</div></td>
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+ </tr>
+ {% endfor %}
+ </tbody>
+ </table>
+ </div>
+{%- endif -%}
diff --git a/erpnext/templates/print_formats/includes/taxes.html b/erpnext/templates/print_formats/includes/taxes.html
index 6e984f3..1935542 100644
--- a/erpnext/templates/print_formats/includes/taxes.html
+++ b/erpnext/templates/print_formats/includes/taxes.html
@@ -17,13 +17,13 @@
{{ render_discount_amount(doc) }}
{%- endif -%}
{%- for charge in data -%}
- {%- if (charge.tax_amount or doc.flags.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%}
+ {%- if (charge.tax_amount or print_settings.print_taxes_with_zero_amount) and (not charge.included_in_print_rate or doc.flags.show_inclusive_tax_in_print) -%}
<div class="row">
<div class="col-xs-5 {%- if doc.align_labels_right %} text-right{%- endif -%}">
- <label>{{ charge.get_formatted("description") }}</label></div>
+ <label>{{ charge.get_formatted("description") }}</label>
+ </div>
<div class="col-xs-7 text-right">
- {{ frappe.format_value(frappe.utils.flt(charge.tax_amount),
- table_meta.get_field("tax_amount"), doc, currency=doc.currency) }}
+ {{ charge.get_formatted('tax_amount', doc) }}
</div>
</div>
{%- endif -%}
diff --git a/erpnext/tests/test_search.py b/erpnext/tests/test_search.py
index 566495f..f60e5e4 100644
--- a/erpnext/tests/test_search.py
+++ b/erpnext/tests/test_search.py
@@ -6,13 +6,13 @@
class TestSearch(unittest.TestCase):
# Search for the word "cond", part of the word "conduire" (Lead) in french.
def test_contact_search_in_foreign_language(self):
- frappe.local.lang = 'fr'
- output = filter_dynamic_link_doctypes("DocType", "cond", "name", 0, 20, {
- 'fieldtype': 'HTML',
- 'fieldname': 'contact_html'
- })
- result = [['found' for x in y if x=="Lead"] for y in output]
- self.assertTrue(['found'] in result)
-
- def tearDown(self):
- frappe.local.lang = 'en'
\ No newline at end of file
+ try:
+ frappe.local.lang = 'fr'
+ output = filter_dynamic_link_doctypes("DocType", "cond", "name", 0, 20, {
+ 'fieldtype': 'HTML',
+ 'fieldname': 'contact_html'
+ })
+ result = [['found' for x in y if x=="Lead"] for y in output]
+ self.assertTrue(['found'] in result)
+ finally:
+ frappe.local.lang = 'en'
diff --git a/erpnext/utilities/bot.py b/erpnext/utilities/bot.py
index 0e5e95d..b2e74da 100644
--- a/erpnext/utilities/bot.py
+++ b/erpnext/utilities/bot.py
@@ -26,12 +26,12 @@
for warehouse in warehouses:
qty = frappe.db.get_value("Bin", {'item_code': item[0], 'warehouse': warehouse.name}, 'actual_qty')
if qty:
- out.append(_('{0} units of [{1}](#Form/Item/{1}) found in [{2}](#Form/Warehouse/{2})').format(qty,
+ out.append(_('{0} units of [{1}](/app/Form/Item/{1}) found in [{2}](/app/Form/Warehouse/{2})').format(qty,
item[0], warehouse.name))
found = True
if not found:
- out.append(_('[{0}](#Form/Item/{0}) is out of stock').format(item[0]))
+ out.append(_('[{0}](/app/Form/Item/{0}) is out of stock').format(item[0]))
return "\n\n".join(out)
diff --git a/erpnext/utilities/desk_page/utilities/utilities.json b/erpnext/utilities/desk_page/utilities/utilities.json
deleted file mode 100644
index 591eab5..0000000
--- a/erpnext/utilities/desk_page/utilities/utilities.json
+++ /dev/null
@@ -1,29 +0,0 @@
-{
- "cards": [
- {
- "hidden": 0,
- "label": "Video",
- "links": "[\n {\n \"description\": \"Video\",\n \"label\": \"Video\",\n \"name\": \"Video\",\n \"type\": \"doctype\"\n },\n {\n \"description\": \"Video settings\",\n \"label\": \"Video Settings\",\n \"name\": \"Video Settings\",\n \"type\": \"doctype\"\n }\n]"
- }
- ],
- "category": "Modules",
- "charts": [],
- "creation": "2020-09-10 12:21:22.335307",
- "developer_mode_only": 0,
- "disable_user_customization": 0,
- "docstatus": 0,
- "doctype": "Desk Page",
- "extends_another_page": 0,
- "hide_custom": 0,
- "idx": 0,
- "is_standard": 1,
- "label": "Utilities",
- "modified": "2020-09-10 12:33:30.089853",
- "modified_by": "user@erpnext.com",
- "module": "Utilities",
- "name": "Utilities",
- "owner": "user@erpnext.com",
- "pin_to_bottom": 1,
- "pin_to_top": 0,
- "shortcuts": []
-}
\ No newline at end of file
diff --git a/erpnext/utilities/workspace/utilities/utilities.json b/erpnext/utilities/workspace/utilities/utilities.json
new file mode 100644
index 0000000..2f9250e
--- /dev/null
+++ b/erpnext/utilities/workspace/utilities/utilities.json
@@ -0,0 +1,51 @@
+{
+ "category": "Modules",
+ "charts": [],
+ "creation": "2020-09-10 12:21:22.335307",
+ "developer_mode_only": 0,
+ "disable_user_customization": 0,
+ "docstatus": 0,
+ "doctype": "Workspace",
+ "extends_another_page": 0,
+ "hide_custom": 0,
+ "idx": 0,
+ "is_standard": 1,
+ "label": "Utilities",
+ "links": [
+ {
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Video",
+ "onboard": 0,
+ "type": "Card Break"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Video",
+ "link_to": "Video",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ },
+ {
+ "dependencies": "",
+ "hidden": 0,
+ "is_query_report": 0,
+ "label": "Video Settings",
+ "link_to": "Video Settings",
+ "link_type": "DocType",
+ "onboard": 0,
+ "type": "Link"
+ }
+ ],
+ "modified": "2020-12-01 13:38:36.711884",
+ "modified_by": "Administrator",
+ "module": "Utilities",
+ "name": "Utilities",
+ "owner": "user@erpnext.com",
+ "pin_to_bottom": 1,
+ "pin_to_top": 0,
+ "shortcuts": []
+}
\ No newline at end of file
diff --git a/erpnext/www/all-products/index.html b/erpnext/www/all-products/index.html
index 0126b59..92c76ad 100644
--- a/erpnext/www/all-products/index.html
+++ b/erpnext/www/all-products/index.html
@@ -1,12 +1,11 @@
{% extends "templates/web.html" %}
-
{% block title %}{{ _('Products') }}{% endblock %}
{% block header %}
-<h1>{{ _('Products') }}</h1>
+<div class="mb-6">{{ _('Products') }}</div>
{% endblock header %}
{% block page_content %}
-<div class="row">
+<div class="row" style="display: none;">
<div class="col-8">
<div class="input-group input-group-sm mb-3">
<input type="search" class="form-control" placeholder="{{_('Search')}}"
@@ -31,27 +30,34 @@
</div>
<div class="row">
- <div class="col-12 order-2 col-md-8 order-md-1 products-list">
- {% if items %}
- {% for item in items %}
- {% include "erpnext/www/all-products/item_row.html" %}
- {% endfor %}
- {% else %}
- {% include "erpnext/www/all-products/not_found.html" %}
- {% endif %}
+ <div class="col-12 order-2 col-md-9 order-md-2 item-card-group-section">
+ <div class="row products-list">
+ {% if items %}
+ {% for item in items %}
+ {% include "erpnext/www/all-products/item_row.html" %}
+ {% endfor %}
+ {% else %}
+ {% include "erpnext/www/all-products/not_found.html" %}
+ {% endif %}
+ </div>
</div>
- <div class="col-12 order-1 col-md-4 order-md-2">
+ <div class="col-12 order-1 col-md-3 order-md-1">
{% if frappe.form_dict.start or frappe.form_dict.field_filters or frappe.form_dict.attribute_filters or frappe.form_dict.search %}
- <a class="mb-3 d-inline-block" href="/all-products">{{ _('Clear filters') }}</a>
+
+
{% endif %}
- <div class="collapse d-md-block" id="product-filters">
+ <div class="collapse d-md-block mr-4 filters-section" id="product-filters">
+ <div class="d-flex justify-content-between align-items-center mb-5 title-section">
+ <div class="mb-4 filters-title" > {{ _('Filters') }} </div>
+ <a class="mb-4 clear-filters" href="/all-products">{{ _('Clear All') }}</a>
+ </div>
{% for field_filter in field_filters %}
{%- set item_field = field_filter[0] %}
{%- set values = field_filter[1] %}
- <div class="mb-4">
- <h6>{{ item_field.label }}</h6>
+ <div class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">{{ item_field.label }}</div>
{% if values | len > 20 %}
<!-- show inline filter if values more than 20 -->
@@ -61,15 +67,15 @@
{% if values %}
<div class="filter-options">
{% for value in values %}
- <div class="custom-control custom-checkbox" data-value="{{ value }}">
- <input type="checkbox"
- class="product-filter field-filter custom-control-input"
- id="{{value}}"
- data-filter-name="{{ item_field.fieldname }}"
- data-filter-value="{{ value }}"
- >
- <label class="custom-control-label" for="{{value}}">
- {{ value }}
+ <div class="checkbox" data-value="{{ value }}">
+ <label for="{{value}}">
+ <input type="checkbox"
+ class="product-filter field-filter"
+ id="{{value}}"
+ data-filter-name="{{ item_field.fieldname }}"
+ data-filter-value="{{ value }}"
+ >
+ <span class="label-area">{{ value }}</span>
</label>
</div>
{% endfor %}
@@ -81,9 +87,8 @@
{% endfor %}
{% for attribute in attribute_filters %}
- <div class="mb-4">
- <h6>{{ attribute.name }}</h6>
-
+ <div class="mb-4 filter-block pb-5">
+ <div class="filter-label mb-3">{{ attribute.name}}</div>
{% if values | len > 20 %}
<!-- show inline filter if values more than 20 -->
<input type="text" class="form-control form-control-sm mb-2 product-filter-filter"/>
@@ -92,16 +97,15 @@
{% if attribute.item_attribute_values %}
<div class="filter-options">
{% for attr_value in attribute.item_attribute_values %}
- <div class="custom-control custom-checkbox" data-value="{{ value }}">
- <input type="checkbox"
- class="product-filter attribute-filter custom-control-input"
- id="{{attr_value.name}}"
- data-attribute-name="{{ attribute.name }}"
- data-attribute-value="{{ attr_value.attribute_value }}"
- {% if attr_value.checked %} checked {% endif %}
- >
- <label class="custom-control-label" for="{{attr_value.name}}">
- {{ attr_value.attribute_value }}
+ <div class="checkbox">
+ <label data-value="{{ value }}">
+ <input type="checkbox"
+ class="product-filter attribute-filter"
+ id="{{attr_value.name}}"
+ data-attribute-name="{{ attribute.name }}"
+ data-attribute-value="{{ attr_value.attribute_value }}"
+ {% if attr_value.checked %} checked {% endif %}>
+ <span class="label-area">{{ attr_value.attribute_value }}</span>
</label>
</div>
{% endfor %}
@@ -133,13 +137,15 @@
</script>
</div>
</div>
-<div class="row">
- <div class="col-12">
+<div class="row product-paging-area mt-5">
+ <div class="col-3">
+ </div>
+ <div class="col-9 text-right">
{% if frappe.form_dict.start|int > 0 %}
- <button class="btn btn-outline-secondary btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</button>
+ <button class="btn btn-default btn-prev" data-start="{{ frappe.form_dict.start|int - page_length }}">{{ _("Prev") }}</button>
{% endif %}
{% if items|length >= page_length %}
- <button class="btn btn-outline-secondary btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
+ <button class="btn btn-default btn-next" data-start="{{ frappe.form_dict.start|int + page_length }}">{{ _("Next") }}</button>
{% endif %}
</div>
</div>
@@ -158,6 +164,4 @@
});
</script>
-{% endblock %}
-
-
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/all-products/index.js b/erpnext/www/all-products/index.js
index cb9e7e6..0721056 100644
--- a/erpnext/www/all-products/index.js
+++ b/erpnext/www/all-products/index.js
@@ -54,7 +54,7 @@
field_filters: JSON.stringify(if_key_exists(this.field_filters)),
attribute_filters: JSON.stringify(if_key_exists(this.attribute_filters)),
});
- window.history.pushState('filters', '', '/all-products?' + query_string);
+ window.history.pushState('filters', '', `${location.pathname}?` + query_string);
$('.page_content input').prop('disabled', true);
this.get_items_with_filters()
diff --git a/erpnext/www/all-products/index.py b/erpnext/www/all-products/index.py
index 0394e4b..fd6400f 100644
--- a/erpnext/www/all-products/index.py
+++ b/erpnext/www/all-products/index.py
@@ -1,6 +1,8 @@
import frappe
from erpnext.portal.product_configurator.utils import (get_products_for_website, get_product_settings,
get_field_filter_data, get_attribute_filter_data)
+from erpnext.shopping_cart.product_query import ProductQuery
+from erpnext.shopping_cart.filters import ProductFiltersBuilder
sitemap = 1
@@ -10,19 +12,25 @@
search = frappe.form_dict.search
field_filters = frappe.parse_json(frappe.form_dict.field_filters)
attribute_filters = frappe.parse_json(frappe.form_dict.attribute_filters)
+ start = frappe.parse_json(frappe.form_dict.start)
else:
search = field_filters = attribute_filters = None
+ start = 0
- context.items = get_products_for_website(field_filters, attribute_filters, search)
+ engine = ProductQuery()
+ context.items = engine.query(attribute_filters, field_filters, search, start)
+
+ # Add homepage as parent
+ context.parents = [{"name": frappe._("Home"), "route":"/"}]
product_settings = get_product_settings()
- context.field_filters = get_field_filter_data() \
- if product_settings.enable_field_filters else []
+ filter_engine = ProductFiltersBuilder()
- context.attribute_filters = get_attribute_filter_data() \
- if product_settings.enable_attribute_filters else []
+ context.field_filters = filter_engine.get_field_filters()
+ context.attribute_filters = filter_engine.get_attribute_fitlers()
context.product_settings = product_settings
- context.page_length = product_settings.products_per_page
+ context.body_class = "product-page"
+ context.page_length = product_settings.products_per_page or 20
context.no_cache = 1
diff --git a/erpnext/www/all-products/item_row.html b/erpnext/www/all-products/item_row.html
index 9e62826..20fc9a4 100644
--- a/erpnext/www/all-products/item_row.html
+++ b/erpnext/www/all-products/item_row.html
@@ -1,24 +1,7 @@
-<div class="card mb-3">
- <div class="row no-gutters">
- <div class="col-md-3">
- <div class="card-body">
- <a class="no-underline" href="/{{ item.route }}">
- <img class="website-image" src="{{ item.website_image or item.image or 'no-image.jpg' }}" alt="{{ item.item_name }}">
- </a>
- </div>
- </div>
- <div class="col-md-9">
- <div class="card-body">
- <h5 class="card-title">
- <a class="text-dark" href="/{{ item.route }}">
- {{ item.item_name or item.name }}
- </a>
- </h5>
- <p class="card-text">
- {{ item.website_description or item.description or '<i class="text-muted">No description</i>' }}
- </p>
- <a href="/{{ item.route }}" class="btn btn-sm btn-light">{{ _('More details') }}</a>
- </div>
- </div>
- </div>
-</div>
+{% from "erpnext/templates/includes/macros.html" import item_card, item_card_body %}
+
+{{ item_card(
+ item.item_name or item.name, item.website_image or item.image, item.route, item.website_description or item.description,
+ item.formatted_price, item.item_group
+) }}
+
diff --git a/package.json b/package.json
index 1b2dc9e..d12661b 100644
--- a/package.json
+++ b/package.json
@@ -15,6 +15,7 @@
"snyk": "^1.290.1"
},
"dependencies": {
+ "onscan.js": "^1.5.2"
},
"scripts": {
"snyk-protect": "snyk protect",
diff --git a/requirements.txt b/requirements.txt
index c4f9171..5a35236 100644
--- a/requirements.txt
+++ b/requirements.txt
@@ -2,8 +2,8 @@
frappe
gocardless-pro==1.11.0
googlemaps==3.1.1
-pandas==1.0.5
-plaid-python==6.0.0
+pandas>=1.0.5
+plaid-python>=7.0.0
pycountry==19.8.18
PyGithub==1.44.1
python-stdnum==1.12
@@ -12,3 +12,4 @@
tweepy==3.8.0
Unidecode==1.1.1
WooCommerce==2.1.1
+pycryptodome==3.9.8
diff --git a/yarn.lock b/yarn.lock
index 97a0635..e5a2da1 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1217,6 +1217,11 @@
dependencies:
mimic-fn "^1.0.0"
+onscan.js@^1.5.2:
+ version "1.5.2"
+ resolved "https://registry.yarnpkg.com/onscan.js/-/onscan.js-1.5.2.tgz#14ed636e5f4c3f0a78bacbf9a505dad3140ee341"
+ integrity sha512-9oGYy2gXYRjvXO9GYqqVca0VuCTAmWhbmX3egBSBP13rXiMNb+dKPJzKFEeECGqPBpf0m40Zoo+GUQ7eCackdw==
+
opn@^5.5.0:
version "5.5.0"
resolved "https://registry.yarnpkg.com/opn/-/opn-5.5.0.tgz#fc7164fab56d235904c51c3b27da6758ca3b9bfc"