Merge branch 'develop' into naming-series-proj
This commit is contained in:
		
						commit
						f30c389e4d
					
				
							
								
								
									
										48
									
								
								.github/helper/documentation.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										48
									
								
								.github/helper/documentation.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,48 @@ | ||||
| import sys | ||||
| import requests | ||||
| from urllib.parse import urlparse | ||||
| 
 | ||||
| 
 | ||||
| docs_repos = [ | ||||
| 	"frappe_docs", | ||||
| 	"erpnext_documentation", | ||||
| 	"erpnext_com", | ||||
| 	"frappe_io", | ||||
| ] | ||||
| 
 | ||||
| 
 | ||||
| def uri_validator(x): | ||||
| 	result = urlparse(x) | ||||
| 	return all([result.scheme, result.netloc, result.path]) | ||||
| 
 | ||||
| def docs_link_exists(body): | ||||
| 	for line in body.splitlines(): | ||||
| 		for word in line.split(): | ||||
| 			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: | ||||
| 						return True | ||||
| 
 | ||||
| 
 | ||||
| if __name__ == "__main__": | ||||
| 	pr = sys.argv[1] | ||||
| 	response = requests.get("https://api.github.com/repos/frappe/erpnext/pulls/{}".format(pr)) | ||||
| 
 | ||||
| 	if response.ok: | ||||
| 		payload = response.json() | ||||
| 		title = payload.get("title", "").lower() | ||||
| 		head_sha = payload.get("head", {}).get("sha") | ||||
| 		body = payload.get("body", "").lower() | ||||
| 
 | ||||
| 		if title.startswith("feat") and head_sha and "no-docs" not in body: | ||||
| 			if docs_link_exists(body): | ||||
| 				print("Documentation Link Found. You're Awesome! 🎉") | ||||
| 
 | ||||
| 			else: | ||||
| 				print("Documentation Link Not Found! ⚠️") | ||||
| 				sys.exit(1) | ||||
| 
 | ||||
| 		else: | ||||
| 			print("Skipping documentation checks... 🏃") | ||||
							
								
								
									
										60
									
								
								.github/helper/translation.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										60
									
								
								.github/helper/translation.py
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,60 @@ | ||||
| import re | ||||
| 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)*)*\)") | ||||
| 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[\"']") | ||||
| starts_with_f_pattern = re.compile(r"_\(f") | ||||
| 
 | ||||
| # skip first argument | ||||
| files = sys.argv[1:] | ||||
| files_to_scan = [_file for _file in files if _file.endswith(('.py', '.js'))] | ||||
| 
 | ||||
| for _file in files_to_scan: | ||||
| 	with open(_file, 'r') as f: | ||||
| 		print(f'Checking: {_file}') | ||||
| 		file_lines = f.readlines() | ||||
| 		for line_number, line in enumerate(file_lines, 1): | ||||
| 			if 'frappe-lint: disable-translate' in line: | ||||
| 				continue | ||||
| 
 | ||||
| 			start_matches = start_pattern.search(line) | ||||
| 			if start_matches: | ||||
| 				starts_with_f = starts_with_f_pattern.search(line) | ||||
| 
 | ||||
| 				if starts_with_f: | ||||
| 					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]}') | ||||
| 						continue | ||||
| 					else: | ||||
| 						continue | ||||
| 
 | ||||
| 				match = pattern.search(line) | ||||
| 				error_found = False | ||||
| 
 | ||||
| 				if not match and line.endswith(',\n'): | ||||
| 					# concat remaining text to validate multiline pattern | ||||
| 					line = "".join(file_lines[line_number - 1:]) | ||||
| 					line = line[start_matches.start() + 1:] | ||||
| 					match = pattern.match(line) | ||||
| 
 | ||||
| 				if not match: | ||||
| 					error_found = True | ||||
| 					print(f'\nTranslation syntax error at line number {line_number + 1}\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]}') | ||||
| 
 | ||||
| 				if error_found: | ||||
| 					errors_encounter += 1 | ||||
| 
 | ||||
| if errors_encounter > 0: | ||||
| 	print('\nVisit "https://frappeframework.com/docs/user/en/translations" to learn about valid translation strings.') | ||||
| 	sys.exit(1) | ||||
| else: | ||||
| 	print('\nGood To Go!') | ||||
							
								
								
									
										24
									
								
								.github/workflows/docs-checker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								.github/workflows/docs-checker.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,24 @@ | ||||
| name: 'Documentation Required' | ||||
| on: | ||||
|   pull_request: | ||||
|     types: [ opened, synchronize, reopened, edited ] | ||||
| 
 | ||||
| jobs: | ||||
|   build: | ||||
|     runs-on: ubuntu-latest | ||||
| 
 | ||||
|     steps: | ||||
|       - name: 'Setup Environment' | ||||
|         uses: actions/setup-python@v2 | ||||
|         with: | ||||
|           python-version: 3.6 | ||||
| 
 | ||||
|       - name: 'Clone repo' | ||||
|         uses: actions/checkout@v2 | ||||
| 
 | ||||
|       - name: Validate Docs | ||||
|         env: | ||||
|           PR_NUMBER: ${{ github.event.number }} | ||||
|         run: | | ||||
|           pip install requests --quiet | ||||
|           python $GITHUB_WORKSPACE/.github/helper/documentation.py $PR_NUMBER | ||||
							
								
								
									
										22
									
								
								.github/workflows/translation_linter.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/translation_linter.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @ -0,0 +1,22 @@ | ||||
| name: Frappe Linter | ||||
| on: | ||||
|   pull_request: | ||||
|     branches: | ||||
|       - develop | ||||
|       - version-12-hotfix | ||||
|       - version-11-hotfix | ||||
| jobs: | ||||
|   check_translation: | ||||
|     name: Translation Syntax Check | ||||
|     runs-on: ubuntu-18.04 | ||||
|     steps: | ||||
|     - uses: actions/checkout@v2 | ||||
|     - name: Setup python3 | ||||
|       uses: actions/setup-python@v1 | ||||
|       with: | ||||
|         python-version: 3.6 | ||||
|     - name: Validating Translation Syntax | ||||
|       run: | | ||||
|         git fetch origin $GITHUB_BASE_REF:$GITHUB_BASE_REF -q | ||||
|         files=$(git diff --name-only --diff-filter=d $GITHUB_BASE_REF) | ||||
|         python $GITHUB_WORKSPACE/.github/helper/translation.py $files | ||||
| @ -16,7 +16,7 @@ | ||||
| ERPNext as a monolith includes the following areas for managing businesses: | ||||
| 
 | ||||
| 1. [Accounting](https://erpnext.com/open-source-accounting) | ||||
| 1. [Inventory](https://erpnext.com/distribution/inventory-management-system) | ||||
| 1. [Warehouse Management](https://erpnext.com/distribution/warehouse-management-system) | ||||
| 1. [CRM](https://erpnext.com/open-source-crm) | ||||
| 1. [Sales](https://erpnext.com/open-source-sales-purchase) | ||||
| 1. [Purchase](https://erpnext.com/open-source-sales-purchase) | ||||
|  | ||||
| @ -1,58 +1,126 @@ | ||||
| { | ||||
|  "custom_fields": [ | ||||
|   { | ||||
|    "_assign": null,  | ||||
|    "_comments": null,  | ||||
|    "_liked_by": null,  | ||||
|    "_user_tags": null,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "collapsible_depends_on": null,  | ||||
|    "columns": 0,  | ||||
|    "creation": "2018-12-28 22:29:21.828090",  | ||||
|    "default": null,  | ||||
|    "depends_on": null,  | ||||
|    "description": null,  | ||||
|    "docstatus": 0,  | ||||
|    "dt": "Address",  | ||||
|    "fetch_from": null,  | ||||
|    "fieldname": "tax_category",  | ||||
|    "fieldtype": "Link",  | ||||
|    "hidden": 0,  | ||||
|    "idx": 14,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "insert_after": "fax",  | ||||
|    "label": "Tax Category",  | ||||
|    "modified": "2018-12-28 22:29:21.828090",  | ||||
|    "modified_by": "Administrator",  | ||||
|    "name": "Address-tax_category",  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Tax Category",  | ||||
|    "owner": "Administrator",  | ||||
|    "parent": null,  | ||||
|    "parentfield": null,  | ||||
|    "parenttype": null,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "print_width": null,  | ||||
|    "read_only": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0,  | ||||
|    "_assign": null, | ||||
|    "_comments": null, | ||||
|    "_liked_by": null, | ||||
|    "_user_tags": null, | ||||
|    "allow_in_quick_entry": 0, | ||||
|    "allow_on_submit": 0, | ||||
|    "bold": 0, | ||||
|    "collapsible": 0, | ||||
|    "collapsible_depends_on": null, | ||||
|    "columns": 0, | ||||
|    "creation": "2018-12-28 22:29:21.828090", | ||||
|    "default": null, | ||||
|    "depends_on": null, | ||||
|    "description": null, | ||||
|    "docstatus": 0, | ||||
|    "dt": "Address", | ||||
|    "fetch_from": null, | ||||
|    "fetch_if_empty": 0, | ||||
|    "fieldname": "tax_category", | ||||
|    "fieldtype": "Link", | ||||
|    "hidden": 0, | ||||
|    "hide_border": 0, | ||||
|    "hide_days": 0, | ||||
|    "hide_seconds": 0, | ||||
|    "idx": 15, | ||||
|    "ignore_user_permissions": 0, | ||||
|    "ignore_xss_filter": 0, | ||||
|    "in_global_search": 0, | ||||
|    "in_list_view": 0, | ||||
|    "in_preview": 0, | ||||
|    "in_standard_filter": 0, | ||||
|    "insert_after": "fax", | ||||
|    "label": "Tax Category", | ||||
|    "length": 0, | ||||
|    "mandatory_depends_on": null, | ||||
|    "modified": "2018-12-28 22:29:21.828090", | ||||
|    "modified_by": "Administrator", | ||||
|    "name": "Address-tax_category", | ||||
|    "no_copy": 0, | ||||
|    "options": "Tax Category", | ||||
|    "owner": "Administrator", | ||||
|    "parent": null, | ||||
|    "parentfield": null, | ||||
|    "parenttype": null, | ||||
|    "permlevel": 0, | ||||
|    "precision": "", | ||||
|    "print_hide": 0, | ||||
|    "print_hide_if_no_value": 0, | ||||
|    "print_width": null, | ||||
|    "read_only": 0, | ||||
|    "read_only_depends_on": null, | ||||
|    "report_hide": 0, | ||||
|    "reqd": 0, | ||||
|    "search_index": 0, | ||||
|    "translatable": 0, | ||||
|    "unique": 0, | ||||
|    "width": null | ||||
|   }, | ||||
|   { | ||||
|    "_assign": null, | ||||
|    "_comments": null, | ||||
|    "_liked_by": null, | ||||
|    "_user_tags": null, | ||||
|    "allow_in_quick_entry": 0, | ||||
|    "allow_on_submit": 0, | ||||
|    "bold": 0, | ||||
|    "collapsible": 0, | ||||
|    "collapsible_depends_on": null, | ||||
|    "columns": 0, | ||||
|    "creation": "2020-10-14 17:41:40.878179", | ||||
|    "default": "0", | ||||
|    "depends_on": null, | ||||
|    "description": null, | ||||
|    "docstatus": 0, | ||||
|    "dt": "Address", | ||||
|    "fetch_from": null, | ||||
|    "fetch_if_empty": 0, | ||||
|    "fieldname": "is_your_company_address", | ||||
|    "fieldtype": "Check", | ||||
|    "hidden": 0, | ||||
|    "hide_border": 0, | ||||
|    "hide_days": 0, | ||||
|    "hide_seconds": 0, | ||||
|    "idx": 20, | ||||
|    "ignore_user_permissions": 0, | ||||
|    "ignore_xss_filter": 0, | ||||
|    "in_global_search": 0, | ||||
|    "in_list_view": 0, | ||||
|    "in_preview": 0, | ||||
|    "in_standard_filter": 0, | ||||
|    "insert_after": "linked_with", | ||||
|    "label": "Is Your Company Address", | ||||
|    "length": 0, | ||||
|    "mandatory_depends_on": null, | ||||
|    "modified": "2020-10-14 17:41:40.878179", | ||||
|    "modified_by": "Administrator", | ||||
|    "name": "Address-is_your_company_address", | ||||
|    "no_copy": 0, | ||||
|    "options": null, | ||||
|    "owner": "Administrator", | ||||
|    "parent": null, | ||||
|    "parentfield": null, | ||||
|    "parenttype": null, | ||||
|    "permlevel": 0, | ||||
|    "precision": "", | ||||
|    "print_hide": 0, | ||||
|    "print_hide_if_no_value": 0, | ||||
|    "print_width": null, | ||||
|    "read_only": 0, | ||||
|    "read_only_depends_on": null, | ||||
|    "report_hide": 0, | ||||
|    "reqd": 0, | ||||
|    "search_index": 0, | ||||
|    "translatable": 0, | ||||
|    "unique": 0, | ||||
|    "width": null | ||||
|   } | ||||
|  ],  | ||||
|  "custom_perms": [],  | ||||
|  "doctype": "Address",  | ||||
|  "property_setters": [],  | ||||
|  ], | ||||
|  "custom_perms": [], | ||||
|  "doctype": "Address", | ||||
|  "property_setters": [], | ||||
|  "sync_on_migrate": 1 | ||||
| } | ||||
							
								
								
									
										42
									
								
								erpnext/accounts/custom/address.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								erpnext/accounts/custom/address.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.contacts.doctype.address.address import Address | ||||
| from frappe.contacts.doctype.address.address import get_address_templates | ||||
| 
 | ||||
| class ERPNextAddress(Address): | ||||
| 	def validate(self): | ||||
| 		self.validate_reference() | ||||
| 		super(ERPNextAddress, self).validate() | ||||
| 
 | ||||
| 	def link_address(self): | ||||
| 		"""Link address based on owner""" | ||||
| 		if self.is_your_company_address: | ||||
| 			return | ||||
| 
 | ||||
| 		return super(ERPNextAddress, self).link_address() | ||||
| 
 | ||||
| 	def validate_reference(self): | ||||
| 		if self.is_your_company_address and not [ | ||||
| 			row for row in self.links if row.link_doctype == "Company" | ||||
| 		]: | ||||
| 			frappe.throw(_("Address needs to be linked to a Company. Please add a row for Company in the Links table."), | ||||
| 				title=_("Company Not Linked")) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_shipping_address(company, address = None): | ||||
| 	filters = [ | ||||
| 		["Dynamic Link", "link_doctype", "=", "Company"], | ||||
| 		["Dynamic Link", "link_name", "=", company], | ||||
| 		["Address", "is_your_company_address", "=", 1] | ||||
| 	] | ||||
| 	fields = ["*"] | ||||
| 	if address and frappe.db.get_value('Dynamic Link', | ||||
| 		{'parent': address, 'link_name': company}): | ||||
| 		filters.append(["Address", "name", "=", address]) | ||||
| 
 | ||||
| 	address = frappe.get_all("Address", filters=filters, fields=fields) or {} | ||||
| 
 | ||||
| 	if address: | ||||
| 		address_as_dict = address[0] | ||||
| 		name, address_template = get_address_templates(address_as_dict) | ||||
| 		return address_as_dict.get("name"), frappe.render_template(address_template, address_as_dict) | ||||
| @ -43,7 +43,7 @@ | ||||
|   { | ||||
|    "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 Reconciliation\",\n        \"name\": \"bank-reconciliation\",\n        \"type\": \"page\"\n    },\n    {\n        \"label\": \"Bank Clearance\",\n        \"name\": \"Bank Clearance\",\n        \"type\": \"doctype\"\n    },\n    {\n        \"label\": \"Bank Statement Transaction Entry\",\n        \"name\": \"Bank Statement Transaction Entry\",\n        \"type\": \"doctype\"\n    },\n    {\n        \"label\": \"Bank Statement Settings\",\n        \"name\": \"Bank Statement Settings\",\n        \"type\": \"doctype\"\n    }\n]" | ||||
|    "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 Statement Transaction Entry\",\n        \"name\": \"Bank Statement Transaction Entry\",\n        \"type\": \"doctype\"\n    },\n    {\n        \"label\": \"Bank Statement Settings\",\n        \"name\": \"Bank Statement Settings\",\n        \"type\": \"doctype\"\n    }\n]" | ||||
|   }, | ||||
|   { | ||||
|    "hidden": 0, | ||||
| @ -53,7 +53,7 @@ | ||||
|   { | ||||
|    "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]" | ||||
|    "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, | ||||
| @ -98,7 +98,7 @@ | ||||
|  "idx": 0, | ||||
|  "is_standard": 1, | ||||
|  "label": "Accounting", | ||||
|  "modified": "2020-09-03 10:37:07.865801", | ||||
|  "modified": "2020-10-08 20:31:46.022470", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Accounting", | ||||
| @ -108,7 +108,7 @@ | ||||
|  "pin_to_top": 0, | ||||
|  "shortcuts": [ | ||||
|   { | ||||
|    "label": "Chart Of Accounts", | ||||
|    "label": "Chart of Accounts", | ||||
|    "link_to": "Account", | ||||
|    "type": "DocType" | ||||
|   }, | ||||
| @ -147,11 +147,6 @@ | ||||
|    "link_to": "Trial Balance", | ||||
|    "type": "Report" | ||||
|   }, | ||||
|   { | ||||
|    "label": "Point of Sale", | ||||
|    "link_to": "point-of-sale", | ||||
|    "type": "Page" | ||||
|   }, | ||||
|   { | ||||
|    "label": "Dashboard", | ||||
|    "link_to": "Accounts", | ||||
|  | ||||
| @ -117,7 +117,9 @@ class Account(NestedSet): | ||||
| 
 | ||||
| 			for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True): | ||||
| 				parent_acc_name_map[d["company"]] = d["name"] | ||||
| 
 | ||||
| 			if not parent_acc_name_map: return | ||||
| 
 | ||||
| 			self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) | ||||
| 
 | ||||
| 	def validate_group_or_ledger(self): | ||||
| @ -289,10 +291,30 @@ def validate_account_number(name, account_number, company): | ||||
| 				.format(account_number, account_with_same_number)) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def update_account_number(name, account_name, account_number=None): | ||||
| 
 | ||||
| def update_account_number(name, account_name, account_number=None, from_descendant=False): | ||||
| 	account = frappe.db.get_value("Account", name, "company", as_dict=True) | ||||
| 	if not account: return | ||||
| 
 | ||||
| 	old_acc_name, old_acc_number = frappe.db.get_value('Account', name, \ | ||||
| 				["account_name", "account_number"]) | ||||
| 
 | ||||
| 	# check if account exists in parent company | ||||
| 	ancestors = get_ancestors_of("Company", account.company) | ||||
| 	allow_independent_account_creation = frappe.get_value("Company", account.company, "allow_account_creation_against_child_company") | ||||
| 
 | ||||
| 	if ancestors and not allow_independent_account_creation: | ||||
| 		for ancestor in ancestors: | ||||
| 			if frappe.db.get_value("Account", {'account_name': old_acc_name, 'company': ancestor}, 'name'): | ||||
| 				# same account in parent company exists | ||||
| 				allow_child_account_creation = _("Allow Account Creation Against Child Company") | ||||
| 
 | ||||
| 				message = _("Account {0} exists in parent company {1}.").format(frappe.bold(old_acc_name), frappe.bold(ancestor)) | ||||
| 				message += "<br>" + _("Renaming it is only allowed via parent company {0}, \ | ||||
| 					to avoid mismatch.").format(frappe.bold(ancestor)) + "<br><br>" | ||||
| 				message += _("To overrule this, enable '{0}' in company {1}").format(allow_child_account_creation, frappe.bold(account.company)) | ||||
| 
 | ||||
| 				frappe.throw(message, title=_("Rename Not Allowed")) | ||||
| 
 | ||||
| 	validate_account_number(name, account_number, account.company) | ||||
| 	if account_number: | ||||
| 		frappe.db.set_value("Account", name, "account_number", account_number.strip()) | ||||
| @ -300,6 +322,12 @@ def update_account_number(name, account_name, account_number=None): | ||||
| 		frappe.db.set_value("Account", name, "account_number", "") | ||||
| 	frappe.db.set_value("Account", name, "account_name", account_name.strip()) | ||||
| 
 | ||||
| 	if not from_descendant: | ||||
| 		# Update and rename in child company accounts as well | ||||
| 		descendants = get_descendants_of('Company', account.company) | ||||
| 		if descendants: | ||||
| 			sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number, old_acc_number) | ||||
| 
 | ||||
| 	new_name = get_account_autoname(account_number, account_name, account.company) | ||||
| 	if name != new_name: | ||||
| 		frappe.rename_doc("Account", name, new_name, force=1) | ||||
| @ -330,3 +358,14 @@ def get_root_company(company): | ||||
| 	# return the topmost company in the hierarchy | ||||
| 	ancestors = get_ancestors_of('Company', company, "lft asc") | ||||
| 	return [ancestors[0]] if ancestors else [] | ||||
| 
 | ||||
| def sync_update_account_number_in_child(descendants, old_acc_name, account_name, account_number=None, old_acc_number=None): | ||||
| 	filters = { | ||||
| 		"company": ["in", descendants], | ||||
| 		"account_name": old_acc_name, | ||||
| 	} | ||||
| 	if old_acc_number: | ||||
| 		filters["account_number"] = old_acc_number | ||||
| 
 | ||||
| 	for d in frappe.db.get_values('Account', filters=filters, fieldname=["company", "name"], as_dict=True): | ||||
| 			update_account_number(d["name"], account_name, account_number, from_descendant=True) | ||||
|  | ||||
| @ -2,7 +2,7 @@ frappe.provide("frappe.treeview_settings") | ||||
| 
 | ||||
| frappe.treeview_settings["Account"] = { | ||||
| 	breadcrumb: "Accounts", | ||||
| 	title: __("Chart Of Accounts"), | ||||
| 	title: __("Chart of Accounts"), | ||||
| 	get_tree_root: false, | ||||
| 	filters: [ | ||||
| 		{ | ||||
| @ -97,7 +97,7 @@ frappe.treeview_settings["Account"] = { | ||||
| 		treeview.page.add_inner_button(__("Journal Entry"), function() { | ||||
| 			frappe.new_doc('Journal Entry', {company: get_company()}); | ||||
| 		}, __('Create')); | ||||
| 		treeview.page.add_inner_button(__("New Company"), function() { | ||||
| 		treeview.page.add_inner_button(__("Company"), function() { | ||||
| 			frappe.new_doc('Company'); | ||||
| 		}, __('Create')); | ||||
| 
 | ||||
|  | ||||
| @ -5,8 +5,7 @@ from __future__ import unicode_literals | ||||
| import unittest | ||||
| import frappe | ||||
| from erpnext.stock import get_warehouse_account, get_company_default_inventory_account | ||||
| from erpnext.accounts.doctype.account.account import update_account_number | ||||
| from erpnext.accounts.doctype.account.account import merge_account | ||||
| from erpnext.accounts.doctype.account.account import update_account_number, merge_account | ||||
| 
 | ||||
| class TestAccount(unittest.TestCase): | ||||
| 	def test_rename_account(self): | ||||
| @ -99,7 +98,8 @@ class TestAccount(unittest.TestCase): | ||||
| 			"Softwares - _TC", doc.is_group, doc.root_type, doc.company) | ||||
| 
 | ||||
| 	def test_account_sync(self): | ||||
| 		del frappe.local.flags["ignore_root_company_validation"] | ||||
| 		frappe.local.flags.pop("ignore_root_company_validation", None) | ||||
| 
 | ||||
| 		acc = frappe.new_doc("Account") | ||||
| 		acc.account_name = "Test Sync Account" | ||||
| 		acc.parent_account = "Temporary Accounts - _TC3" | ||||
| @ -111,6 +111,55 @@ class TestAccount(unittest.TestCase): | ||||
| 		self.assertEqual(acc_tc_4, "Test Sync Account - _TC4") | ||||
| 		self.assertEqual(acc_tc_5, "Test Sync Account - _TC5") | ||||
| 
 | ||||
| 	def test_account_rename_sync(self): | ||||
| 		frappe.local.flags.pop("ignore_root_company_validation", None) | ||||
| 
 | ||||
| 		acc = frappe.new_doc("Account") | ||||
| 		acc.account_name = "Test Rename Account" | ||||
| 		acc.parent_account = "Temporary Accounts - _TC3" | ||||
| 		acc.company = "_Test Company 3" | ||||
| 		acc.insert() | ||||
| 
 | ||||
| 		# Rename account in parent company | ||||
| 		update_account_number(acc.name, "Test Rename Sync Account", "1234") | ||||
| 
 | ||||
| 		# Check if renamed in children | ||||
| 		self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 4", "account_number": "1234"})) | ||||
| 		self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Rename Sync Account", "company": "_Test Company 5", "account_number": "1234"})) | ||||
| 
 | ||||
| 		frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC3") | ||||
| 		frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC4") | ||||
| 		frappe.delete_doc("Account", "1234 - Test Rename Sync Account - _TC5") | ||||
| 
 | ||||
| 	def test_child_company_account_rename_sync(self): | ||||
| 		frappe.local.flags.pop("ignore_root_company_validation", None) | ||||
| 
 | ||||
| 		acc = frappe.new_doc("Account") | ||||
| 		acc.account_name = "Test Group Account" | ||||
| 		acc.parent_account = "Temporary Accounts - _TC3" | ||||
| 		acc.is_group = 1 | ||||
| 		acc.company = "_Test Company 3" | ||||
| 		acc.insert() | ||||
| 
 | ||||
| 		self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 4"})) | ||||
| 		self.assertTrue(frappe.db.exists("Account", {'account_name': "Test Group Account", "company": "_Test Company 5"})) | ||||
| 
 | ||||
| 		# Try renaming child company account | ||||
| 		acc_tc_5 = frappe.db.get_value('Account', {'account_name': "Test Group Account", "company": "_Test Company 5"}) | ||||
| 		self.assertRaises(frappe.ValidationError, update_account_number, acc_tc_5, "Test Modified Account") | ||||
| 
 | ||||
| 		# Rename child company account with allow_account_creation_against_child_company enabled | ||||
| 		frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 1) | ||||
| 
 | ||||
| 		update_account_number(acc_tc_5, "Test Modified Account") | ||||
| 		self.assertTrue(frappe.db.exists("Account", {'name': "Test Modified Account - _TC5", "company": "_Test Company 5"})) | ||||
| 
 | ||||
| 		frappe.db.set_value("Company", "_Test Company 5", "allow_account_creation_against_child_company", 0) | ||||
| 
 | ||||
| 		to_delete = ["Test Group Account - _TC3", "Test Group Account - _TC4", "Test Modified Account - _TC5"] | ||||
| 		for doc in to_delete: | ||||
| 			frappe.delete_doc("Account", doc) | ||||
| 
 | ||||
| def _make_test_records(verbose): | ||||
| 	from frappe.test_runner import make_test_objects | ||||
| 
 | ||||
|  | ||||
| @ -104,7 +104,7 @@ | ||||
|    "default": "1", | ||||
|    "fieldname": "unlink_advance_payment_on_cancelation_of_order", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Unlink Advance Payment on Cancelation of Order" | ||||
|    "label": "Unlink Advance Payment on Cancellation of Order" | ||||
|   }, | ||||
|   { | ||||
|    "default": "1", | ||||
| @ -223,9 +223,10 @@ | ||||
|  ], | ||||
|  "icon": "icon-cog", | ||||
|  "idx": 1, | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "issingle": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-08-03 20:13:26.043092", | ||||
|  "modified": "2020-10-07 14:58:50.325577", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Accounts Settings", | ||||
|  | ||||
| @ -55,7 +55,7 @@ class BankStatementTransactionEntry(Document): | ||||
| 
 | ||||
| 	def populate_payment_entries(self): | ||||
| 		if self.bank_statement is None: return | ||||
| 		filename = self.bank_statement.split("/")[-1] | ||||
| 		file_url = self.bank_statement | ||||
| 		if (len(self.new_transaction_items + self.reconciled_transaction_items) > 0): | ||||
| 			frappe.throw(_("Transactions already retreived from the statement")) | ||||
| 
 | ||||
| @ -65,7 +65,7 @@ class BankStatementTransactionEntry(Document): | ||||
| 		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(filename, 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"])) | ||||
| @ -398,20 +398,21 @@ def get_transaction_info(headers, header_index, row): | ||||
| 			transaction[header] = "" | ||||
| 	return transaction | ||||
| 
 | ||||
| def get_transaction_entries(filename, headers): | ||||
| def get_transaction_entries(file_url, headers): | ||||
| 	header_index = {} | ||||
| 	rows, transactions = [], [] | ||||
| 
 | ||||
| 	if (filename.lower().endswith("xlsx")): | ||||
| 	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_id=filename) | ||||
| 	elif (filename.lower().endswith("csv")): | ||||
| 		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_name": filename}) | ||||
| 		_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 (filename.lower().endswith("xls")): | ||||
| 	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")) | ||||
|  | ||||
| @ -91,15 +91,11 @@ class TestBankTransaction(unittest.TestCase): | ||||
| 		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) | ||||
| 
 | ||||
| def add_transactions(): | ||||
| 	if frappe.flags.test_bank_transactions_created: | ||||
| 		return | ||||
| 
 | ||||
| 	frappe.set_user("Administrator") | ||||
| def create_bank_account(bank_name="Citi Bank", account_name="_Test Bank - _TC"): | ||||
| 	try: | ||||
| 		frappe.get_doc({ | ||||
| 			"doctype": "Bank", | ||||
| 			"bank_name":"Citi Bank", | ||||
| 			"bank_name":bank_name, | ||||
| 		}).insert() | ||||
| 	except frappe.DuplicateEntryError: | ||||
| 		pass | ||||
| @ -108,12 +104,19 @@ def add_transactions(): | ||||
| 		frappe.get_doc({ | ||||
| 			"doctype": "Bank Account", | ||||
| 			"account_name":"Checking Account", | ||||
| 			"bank": "Citi Bank", | ||||
| 			"account": "_Test Bank - _TC" | ||||
| 			"bank": bank_name, | ||||
| 			"account": account_name | ||||
| 		}).insert() | ||||
| 	except frappe.DuplicateEntryError: | ||||
| 		pass | ||||
| 
 | ||||
| def add_transactions(): | ||||
| 	if frappe.flags.test_bank_transactions_created: | ||||
| 		return | ||||
| 
 | ||||
| 	frappe.set_user("Administrator") | ||||
| 	create_bank_account() | ||||
| 
 | ||||
| 	doc = frappe.get_doc({ | ||||
| 		"doctype": "Bank Transaction", | ||||
| 		"description":"1512567 BG/000002918 OPSKATTUZWXXX AT776000000098709837 Herr G", | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -195,7 +195,7 @@ def build_response_as_excel(writer): | ||||
| 	reader = csv.reader(f) | ||||
| 
 | ||||
| 	from frappe.utils.xlsxutils import make_xlsx | ||||
| 	xlsx_file = make_xlsx(reader, "Chart Of Accounts Importer Template") | ||||
| 	xlsx_file = make_xlsx(reader, "Chart of Accounts Importer Template") | ||||
| 
 | ||||
| 	f.close() | ||||
| 	os.remove(filename) | ||||
|  | ||||
| @ -38,8 +38,8 @@ | ||||
|    "reqd": 1 | ||||
|   } | ||||
|  ], | ||||
|  "modified": "2020-06-18 20:27:42.615842", | ||||
|  "modified_by": "ahmad@havenir.com", | ||||
|  "modified": "2020-09-18 17:26:09.703215", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Item Tax Template", | ||||
|  "owner": "Administrator", | ||||
|  | ||||
| @ -210,7 +210,7 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ | ||||
| 			$.each(this.frm.doc.accounts || [], function(i, jvd) { | ||||
| 				frappe.model.set_default_values(jvd); | ||||
| 			}); | ||||
| 			var posting_date = this.frm.posting_date; | ||||
| 			var posting_date = this.frm.doc.posting_date; | ||||
| 			if(!this.frm.doc.amended_from) this.frm.set_value('posting_date', posting_date || frappe.datetime.get_today()); | ||||
| 		} | ||||
| 	}, | ||||
|  | ||||
| @ -22,8 +22,12 @@ class JournalEntry(AccountsController): | ||||
| 		return self.voucher_type | ||||
| 
 | ||||
| 	def validate(self): | ||||
| 		if self.voucher_type == 'Opening Entry': | ||||
| 			self.is_opening = 'Yes' | ||||
| 
 | ||||
| 		if not self.is_opening: | ||||
| 			self.is_opening='No' | ||||
| 
 | ||||
| 		self.clearance_date = None | ||||
| 
 | ||||
| 		self.validate_party() | ||||
| @ -1023,7 +1027,7 @@ def make_inter_company_journal_entry(name, voucher_type, company): | ||||
| 	return journal_entry.as_dict() | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def make_reverse_journal_entry(source_name, target_doc=None, ignore_permissions=False): | ||||
| def make_reverse_journal_entry(source_name, target_doc=None): | ||||
| 	from frappe.model.mapper import get_mapped_doc | ||||
| 
 | ||||
| 	def update_accounts(source, target, source_parent): | ||||
| @ -1049,6 +1053,6 @@ def make_reverse_journal_entry(source_name, target_doc=None, ignore_permissions= | ||||
| 			}, | ||||
| 			"postprocess": update_accounts, | ||||
| 		}, | ||||
| 	}, target_doc, ignore_permissions=ignore_permissions) | ||||
| 	}, target_doc) | ||||
| 
 | ||||
| 	return doclist | ||||
| @ -195,88 +195,91 @@ def create_sales_invoice_record(qty=1): | ||||
| 
 | ||||
| def create_records(): | ||||
| 	# create a new loyalty Account | ||||
| 	if frappe.db.exists("Account", "Loyalty - _TC"): | ||||
| 		return | ||||
| 
 | ||||
| 	frappe.get_doc({ | ||||
| 		"doctype": "Account", | ||||
| 		"account_name": "Loyalty", | ||||
| 		"parent_account": "Direct Expenses - _TC", | ||||
| 		"company": "_Test Company", | ||||
| 		"is_group": 0, | ||||
| 		"account_type": "Expense Account", | ||||
| 	}).insert() | ||||
| 	if not frappe.db.exists("Account", "Loyalty - _TC"): | ||||
| 		frappe.get_doc({ | ||||
| 			"doctype": "Account", | ||||
| 			"account_name": "Loyalty", | ||||
| 			"parent_account": "Direct Expenses - _TC", | ||||
| 			"company": "_Test Company", | ||||
| 			"is_group": 0, | ||||
| 			"account_type": "Expense Account", | ||||
| 		}).insert() | ||||
| 
 | ||||
| 	# create a new loyalty program Single tier | ||||
| 	frappe.get_doc({ | ||||
| 		"doctype": "Loyalty Program", | ||||
| 		"loyalty_program_name": "Test Single Loyalty", | ||||
| 		"auto_opt_in": 1, | ||||
| 		"from_date": today(), | ||||
| 		"loyalty_program_type": "Single Tier Program", | ||||
| 		"conversion_factor": 1, | ||||
| 		"expiry_duration": 10, | ||||
| 		"company": "_Test Company", | ||||
| 		"cost_center": "Main - _TC", | ||||
| 		"expense_account": "Loyalty - _TC", | ||||
| 		"collection_rules": [{ | ||||
| 			'tier_name': 'Silver', | ||||
| 			'collection_factor': 1000, | ||||
| 			'min_spent': 1000 | ||||
| 		}] | ||||
| 	}).insert() | ||||
| 
 | ||||
| 	# create a new customer | ||||
| 	frappe.get_doc({ | ||||
| 		"customer_group": "_Test Customer Group", | ||||
| 		"customer_name": "Test Loyalty Customer", | ||||
| 		"customer_type": "Individual", | ||||
| 		"doctype": "Customer", | ||||
| 		"territory": "_Test Territory" | ||||
| 	}).insert() | ||||
| 
 | ||||
| 	# create a new loyalty program Multiple tier | ||||
| 	frappe.get_doc({ | ||||
| 		"doctype": "Loyalty Program", | ||||
| 		"loyalty_program_name": "Test Multiple Loyalty", | ||||
| 		"auto_opt_in": 1, | ||||
| 		"from_date": today(), | ||||
| 		"loyalty_program_type": "Multiple Tier Program", | ||||
| 		"conversion_factor": 1, | ||||
| 		"expiry_duration": 10, | ||||
| 		"company": "_Test Company", | ||||
| 		"cost_center": "Main - _TC", | ||||
| 		"expense_account": "Loyalty - _TC", | ||||
| 		"collection_rules": [ | ||||
| 			{ | ||||
| 	if not frappe.db.exists("Loyalty Program","Test Single Loyalty"): | ||||
| 		frappe.get_doc({ | ||||
| 			"doctype": "Loyalty Program", | ||||
| 			"loyalty_program_name": "Test Single Loyalty", | ||||
| 			"auto_opt_in": 1, | ||||
| 			"from_date": today(), | ||||
| 			"loyalty_program_type": "Single Tier Program", | ||||
| 			"conversion_factor": 1, | ||||
| 			"expiry_duration": 10, | ||||
| 			"company": "_Test Company", | ||||
| 			"cost_center": "Main - _TC", | ||||
| 			"expense_account": "Loyalty - _TC", | ||||
| 			"collection_rules": [{ | ||||
| 				'tier_name': 'Silver', | ||||
| 				'collection_factor': 1000, | ||||
| 				'min_spent': 10000 | ||||
| 			}, | ||||
| 			{ | ||||
| 				'tier_name': 'Gold', | ||||
| 				'collection_factor': 1000, | ||||
| 				'min_spent': 19000 | ||||
| 			} | ||||
| 		] | ||||
| 	}).insert() | ||||
| 				'min_spent': 1000 | ||||
| 			}] | ||||
| 		}).insert() | ||||
| 
 | ||||
| 	# create a new customer | ||||
| 	if not frappe.db.exists("Customer","Test Loyalty Customer"): | ||||
| 		frappe.get_doc({ | ||||
| 			"customer_group": "_Test Customer Group", | ||||
| 			"customer_name": "Test Loyalty Customer", | ||||
| 			"customer_type": "Individual", | ||||
| 			"doctype": "Customer", | ||||
| 			"territory": "_Test Territory" | ||||
| 		}).insert() | ||||
| 
 | ||||
| 	# create a new loyalty program Multiple tier | ||||
| 	if not frappe.db.exists("Loyalty Program","Test Multiple Loyalty"): | ||||
| 		frappe.get_doc({ | ||||
| 			"doctype": "Loyalty Program", | ||||
| 			"loyalty_program_name": "Test Multiple Loyalty", | ||||
| 			"auto_opt_in": 1, | ||||
| 			"from_date": today(), | ||||
| 			"loyalty_program_type": "Multiple Tier Program", | ||||
| 			"conversion_factor": 1, | ||||
| 			"expiry_duration": 10, | ||||
| 			"company": "_Test Company", | ||||
| 			"cost_center": "Main - _TC", | ||||
| 			"expense_account": "Loyalty - _TC", | ||||
| 			"collection_rules": [ | ||||
| 				{ | ||||
| 					'tier_name': 'Silver', | ||||
| 					'collection_factor': 1000, | ||||
| 					'min_spent': 10000 | ||||
| 				}, | ||||
| 				{ | ||||
| 					'tier_name': 'Gold', | ||||
| 					'collection_factor': 1000, | ||||
| 					'min_spent': 19000 | ||||
| 				} | ||||
| 			] | ||||
| 		}).insert() | ||||
| 
 | ||||
| 	# create an item | ||||
| 	item = frappe.get_doc({ | ||||
| 		"doctype": "Item", | ||||
| 		"item_code": "Loyal Item", | ||||
| 		"item_name": "Loyal Item", | ||||
| 		"item_group": "All Item Groups", | ||||
| 		"company": "_Test Company", | ||||
| 		"is_stock_item": 1, | ||||
| 		"opening_stock": 100, | ||||
| 		"valuation_rate": 10000, | ||||
| 	}).insert() | ||||
| 	if not frappe.db.exists("Item", "Loyal Item"): | ||||
| 		frappe.get_doc({ | ||||
| 			"doctype": "Item", | ||||
| 			"item_code": "Loyal Item", | ||||
| 			"item_name": "Loyal Item", | ||||
| 			"item_group": "All Item Groups", | ||||
| 			"company": "_Test Company", | ||||
| 			"is_stock_item": 1, | ||||
| 			"opening_stock": 100, | ||||
| 			"valuation_rate": 10000, | ||||
| 		}).insert() | ||||
| 
 | ||||
| 	# create item price | ||||
| 	frappe.get_doc({ | ||||
| 		"doctype": "Item Price", | ||||
| 		"price_list": "Standard Selling", | ||||
| 		"item_code": item.item_code, | ||||
| 		"price_list_rate": 10000 | ||||
| 	}).insert() | ||||
| 	if not frappe.db.exists("Item Price", {"price_list": "Standard Selling", "item_code": "Loyal Item"}): | ||||
| 		frappe.get_doc({ | ||||
| 			"doctype": "Item Price", | ||||
| 			"price_list": "Standard Selling", | ||||
| 			"item_code": "Loyal Item", | ||||
| 			"price_list_rate": 10000 | ||||
| 		}).insert() | ||||
|  | ||||
| @ -45,11 +45,11 @@ | ||||
|  ], | ||||
|  "icon": "fa fa-credit-card", | ||||
|  "idx": 1, | ||||
|  "modified": "2019-08-14 14:58:42.079115", | ||||
|  "modified_by": "sammish.thundiyil@gmail.com", | ||||
|  "modified": "2020-09-18 17:26:09.703215", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Mode of Payment", | ||||
|  "owner": "harshada@webnotestech.com", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "create": 1, | ||||
|  | ||||
| @ -12,9 +12,10 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 
 | ||||
| 	setup: function(frm) { | ||||
| 		frm.set_query("paid_from", function() { | ||||
| 			frm.events.validate_company(frm); | ||||
| 
 | ||||
| 			var account_types = in_list(["Pay", "Internal Transfer"], frm.doc.payment_type) ? | ||||
| 				["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; | ||||
| 
 | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					"account_type": ["in", account_types], | ||||
| @ -23,13 +24,16 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		frm.set_query("party_type", function() { | ||||
| 			frm.events.validate_company(frm); | ||||
| 			return{ | ||||
| 				filters: { | ||||
| 					"name": ["in", Object.keys(frappe.boot.party_account_types)], | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		frm.set_query("party_bank_account", function() { | ||||
| 			return { | ||||
| 				filters: { | ||||
| @ -39,6 +43,7 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		frm.set_query("bank_account", function() { | ||||
| 			return { | ||||
| 				filters: { | ||||
| @ -47,6 +52,7 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		frm.set_query("contact_person", function() { | ||||
| 			if (frm.doc.party) { | ||||
| 				return { | ||||
| @ -58,10 +64,12 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 				}; | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		frm.set_query("paid_to", function() { | ||||
| 			frm.events.validate_company(frm); | ||||
| 
 | ||||
| 			var account_types = in_list(["Receive", "Internal Transfer"], frm.doc.payment_type) ? | ||||
| 				["Bank", "Cash"] : [frappe.boot.party_account_types[frm.doc.party_type]]; | ||||
| 
 | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					"account_type": ["in", account_types], | ||||
| @ -150,6 +158,12 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 		frm.events.show_general_ledger(frm); | ||||
| 	}, | ||||
| 
 | ||||
| 	validate_company: (frm) => { | ||||
| 		if (!frm.doc.company){ | ||||
| 			frappe.throw({message:__("Please select a Company first."), title: __("Mandatory")}); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	company: function(frm) { | ||||
| 		frm.events.hide_unhide_fields(frm); | ||||
| 		frm.events.set_dynamic_labels(frm); | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "allow_import": 1, | ||||
|  "autoname": "naming_series:", | ||||
|  "creation": "2016-06-01 14:38:51.012597", | ||||
| @ -63,6 +64,7 @@ | ||||
|   "cost_center", | ||||
|   "section_break_12", | ||||
|   "status", | ||||
|   "custom_remarks", | ||||
|   "remarks", | ||||
|   "column_break_16", | ||||
|   "letter_head", | ||||
| @ -462,7 +464,8 @@ | ||||
|    "fieldname": "remarks", | ||||
|    "fieldtype": "Small Text", | ||||
|    "label": "Remarks", | ||||
|    "no_copy": 1 | ||||
|    "no_copy": 1, | ||||
|    "read_only_depends_on": "eval:doc.custom_remarks == 0" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_16", | ||||
| @ -573,10 +576,18 @@ | ||||
|    "label": "Status", | ||||
|    "options": "\nDraft\nSubmitted\nCancelled", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "custom_remarks", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Custom Remarks" | ||||
|   } | ||||
|  ], | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "is_submittable": 1, | ||||
|  "modified": "2019-12-08 13:02:30.016610", | ||||
|  "links": [], | ||||
|  "modified": "2020-09-02 13:39:43.383705", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Payment Entry", | ||||
|  | ||||
| @ -453,7 +453,7 @@ class PaymentEntry(AccountsController): | ||||
| 				frappe.throw(_("Reference No and Reference Date is mandatory for Bank transaction")) | ||||
| 
 | ||||
| 	def set_remarks(self): | ||||
| 		if self.remarks: return | ||||
| 		if self.custom_remarks: return | ||||
| 
 | ||||
| 		if self.payment_type=="Internal Transfer": | ||||
| 			remarks = [_("Amount {0} {1} transferred from {2} to {3}") | ||||
| @ -1172,30 +1172,23 @@ def make_payment_order(source_name, target_doc=None): | ||||
| 	from frappe.model.mapper import get_mapped_doc | ||||
| 	def set_missing_values(source, target): | ||||
| 		target.payment_order_type = "Payment Entry" | ||||
| 		target.append('references', dict( | ||||
| 			reference_doctype="Payment Entry", | ||||
| 			reference_name=source.name, | ||||
| 			bank_account=source.party_bank_account, | ||||
| 			amount=source.paid_amount, | ||||
| 			account=source.paid_to, | ||||
| 			supplier=source.party, | ||||
| 			mode_of_payment=source.mode_of_payment, | ||||
| 		)) | ||||
| 
 | ||||
| 	def update_item(source_doc, target_doc, source_parent): | ||||
| 		target_doc.bank_account = source_parent.party_bank_account | ||||
| 		target_doc.amount = source_doc.allocated_amount | ||||
| 		target_doc.account = source_parent.paid_to | ||||
| 		target_doc.payment_entry = source_parent.name | ||||
| 		target_doc.supplier = source_parent.party | ||||
| 		target_doc.mode_of_payment = source_parent.mode_of_payment | ||||
| 
 | ||||
| 
 | ||||
| 	doclist = get_mapped_doc("Payment Entry", source_name,	{ | ||||
| 	doclist = get_mapped_doc("Payment Entry", source_name, { | ||||
| 		"Payment Entry": { | ||||
| 			"doctype": "Payment Order", | ||||
| 			"validation": { | ||||
| 				"docstatus": ["=", 1] | ||||
| 			} | ||||
| 		}, | ||||
| 		"Payment Entry Reference": { | ||||
| 			"doctype": "Payment Order Reference", | ||||
| 			"validation": { | ||||
| 				"docstatus": ["=", 1] | ||||
| 			}, | ||||
| 			"postprocess": update_item | ||||
| 		}, | ||||
| 		} | ||||
| 
 | ||||
| 	}, target_doc, set_missing_values) | ||||
| 
 | ||||
|  | ||||
| @ -1,12 +1,14 @@ | ||||
| frappe.listview_settings['Payment Entry'] = { | ||||
| 
 | ||||
| 	onload: function(listview) { | ||||
| 		listview.page.fields_dict.party_type.get_query = function() { | ||||
| 			return { | ||||
| 				"filters": { | ||||
| 					"name": ["in", Object.keys(frappe.boot.party_account_types)], | ||||
| 				} | ||||
| 		if (listview.page.fields_dict.party_type) { | ||||
| 			listview.page.fields_dict.party_type.get_query = function() { | ||||
| 				return { | ||||
| 					"filters": { | ||||
| 						"name": ["in", Object.keys(frappe.boot.party_account_types)], | ||||
| 					} | ||||
| 				}; | ||||
| 			}; | ||||
| 		}; | ||||
| 		} | ||||
| 	} | ||||
| }; | ||||
| @ -21,10 +21,15 @@ class PaymentOrder(Document): | ||||
| 		if cancel: | ||||
| 			status = 'Initiated' | ||||
| 
 | ||||
| 		ref_field = "status" if self.payment_order_type == "Payment Request" else "payment_order_status" | ||||
| 		if self.payment_order_type == "Payment Request": | ||||
| 			ref_field = "status" | ||||
| 			ref_doc_field = frappe.scrub(self.payment_order_type) | ||||
| 		else: | ||||
| 			ref_field = "payment_order_status" | ||||
| 			ref_doc_field = "reference_name" | ||||
| 
 | ||||
| 		for d in self.references: | ||||
| 			frappe.db.set_value(self.payment_order_type, d.get(frappe.scrub(self.payment_order_type)), ref_field, status) | ||||
| 			frappe.db.set_value(self.payment_order_type, d.get(ref_doc_field), ref_field, status) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| @frappe.validate_and_sanitize_search_inputs | ||||
|  | ||||
| @ -5,6 +5,45 @@ from __future__ import unicode_literals | ||||
| 
 | ||||
| import frappe | ||||
| import unittest | ||||
| from frappe.utils import getdate | ||||
| from erpnext.accounts.doctype.bank_transaction.test_bank_transaction import create_bank_account | ||||
| from erpnext.accounts.doctype.payment_entry.payment_entry import get_payment_entry, make_payment_order | ||||
| from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice | ||||
| 
 | ||||
| class TestPaymentOrder(unittest.TestCase): | ||||
| 	pass | ||||
| 	def setUp(self): | ||||
| 		create_bank_account() | ||||
| 
 | ||||
| 	def tearDown(self): | ||||
| 		for bt in frappe.get_all("Payment Order"): | ||||
| 			doc = frappe.get_doc("Payment Order", bt.name) | ||||
| 			doc.cancel() | ||||
| 			doc.delete() | ||||
| 
 | ||||
| 	def test_payment_order_creation_against_payment_entry(self): | ||||
| 		purchase_invoice = make_purchase_invoice() | ||||
| 		payment_entry = get_payment_entry("Purchase Invoice", purchase_invoice.name, bank_account="_Test Bank - _TC") | ||||
| 		payment_entry.reference_no = "_Test_Payment_Order" | ||||
| 		payment_entry.reference_date = getdate() | ||||
| 		payment_entry.party_bank_account = "Checking Account - Citi Bank" | ||||
| 		payment_entry.insert() | ||||
| 		payment_entry.submit() | ||||
| 
 | ||||
| 		doc = create_payment_order_against_payment_entry(payment_entry, "Payment Entry") | ||||
| 		reference_doc = doc.get("references")[0] | ||||
| 		self.assertEquals(reference_doc.reference_name, payment_entry.name) | ||||
| 		self.assertEquals(reference_doc.reference_doctype, "Payment Entry") | ||||
| 		self.assertEquals(reference_doc.supplier, "_Test Supplier") | ||||
| 		self.assertEquals(reference_doc.amount, 250) | ||||
| 
 | ||||
| def create_payment_order_against_payment_entry(ref_doc, order_type): | ||||
| 	payment_order = frappe.get_doc(dict( | ||||
| 		doctype="Payment Order", | ||||
| 		company="_Test Company", | ||||
| 		payment_order_type=order_type, | ||||
| 		company_bank_account="Checking Account - Citi Bank" | ||||
| 	)) | ||||
| 	doc = make_payment_order(ref_doc.name, payment_order) | ||||
| 	doc.save() | ||||
| 	doc.submit() | ||||
| 	return doc | ||||
| @ -1,4 +1,5 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2018-07-20 16:38:06.630813", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
| @ -10,7 +11,6 @@ | ||||
|   "column_break_4", | ||||
|   "supplier", | ||||
|   "payment_request", | ||||
|   "payment_entry", | ||||
|   "mode_of_payment", | ||||
|   "bank_account_details", | ||||
|   "bank_account", | ||||
| @ -103,17 +103,12 @@ | ||||
|    "no_copy": 1, | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "payment_entry", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Payment Entry", | ||||
|    "options": "Payment Entry", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "modified": "2019-05-08 13:56:25.724557", | ||||
|  "links": [], | ||||
|  "modified": "2020-09-04 08:29:51.014390", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Payment Order Reference", | ||||
|  | ||||
| @ -37,6 +37,11 @@ frappe.ui.form.on("Payment Reconciliation Payment", { | ||||
| erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.extend({ | ||||
| 	onload: function() { | ||||
| 		var me = this; | ||||
| 
 | ||||
| 		this.frm.set_query("party", function() { | ||||
| 			check_mandatory(me.frm); | ||||
| 		}); | ||||
| 
 | ||||
| 		this.frm.set_query("party_type", function() { | ||||
| 			return { | ||||
| 				"filters": { | ||||
| @ -46,37 +51,39 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext | ||||
| 		}); | ||||
| 
 | ||||
| 		this.frm.set_query('receivable_payable_account', function() { | ||||
| 			if(!me.frm.doc.company || !me.frm.doc.party_type) { | ||||
| 				frappe.msgprint(__("Please select Company and Party Type first")); | ||||
| 			} else { | ||||
| 				return{ | ||||
| 					filters: { | ||||
| 						"company": me.frm.doc.company, | ||||
| 						"is_group": 0, | ||||
| 						"account_type": frappe.boot.party_account_types[me.frm.doc.party_type] | ||||
| 					} | ||||
| 				}; | ||||
| 			} | ||||
| 
 | ||||
| 			check_mandatory(me.frm); | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					"company": me.frm.doc.company, | ||||
| 					"is_group": 0, | ||||
| 					"account_type": frappe.boot.party_account_types[me.frm.doc.party_type] | ||||
| 				} | ||||
| 			}; | ||||
| 		}); | ||||
| 
 | ||||
| 		this.frm.set_query('bank_cash_account', function() { | ||||
| 			if(!me.frm.doc.company) { | ||||
| 				frappe.msgprint(__("Please select Company first")); | ||||
| 			} else { | ||||
| 				return{ | ||||
| 					filters:[ | ||||
| 						['Account', 'company', '=', me.frm.doc.company], | ||||
| 						['Account', 'is_group', '=', 0], | ||||
| 						['Account', 'account_type', 'in', ['Bank', 'Cash']] | ||||
| 					] | ||||
| 				}; | ||||
| 			} | ||||
| 			check_mandatory(me.frm, true); | ||||
| 			return { | ||||
| 				filters:[ | ||||
| 					['Account', 'company', '=', me.frm.doc.company], | ||||
| 					['Account', 'is_group', '=', 0], | ||||
| 					['Account', 'account_type', 'in', ['Bank', 'Cash']] | ||||
| 				] | ||||
| 			}; | ||||
| 		}); | ||||
| 
 | ||||
| 		this.frm.set_value('party_type', ''); | ||||
| 		this.frm.set_value('party', ''); | ||||
| 		this.frm.set_value('receivable_payable_account', ''); | ||||
| 
 | ||||
| 		var check_mandatory = (frm, only_company=false) => { | ||||
| 			var title = __("Mandatory"); | ||||
| 			if (only_company && !frm.doc.company) { | ||||
| 				frappe.throw({message: __("Please Select a Company First"), title: title}); | ||||
| 			} else if (!frm.doc.company || !frm.doc.party_type) { | ||||
| 				frappe.throw({message: __("Please Select Both Company and Party Type First"), title: title}); | ||||
| 			} | ||||
| 		}; | ||||
| 	}, | ||||
| 
 | ||||
| 	refresh: function() { | ||||
| @ -90,7 +97,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext | ||||
| 
 | ||||
| 	party: function() { | ||||
| 		var me = this | ||||
| 		if(!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) { | ||||
| 		if (!me.frm.doc.receivable_payable_account && me.frm.doc.party_type && me.frm.doc.party) { | ||||
| 			return frappe.call({ | ||||
| 				method: "erpnext.accounts.party.get_party_account", | ||||
| 				args: { | ||||
| @ -99,7 +106,7 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext | ||||
| 					party: me.frm.doc.party | ||||
| 				}, | ||||
| 				callback: function(r) { | ||||
| 					if(!r.exc && r.message) { | ||||
| 					if (!r.exc && r.message) { | ||||
| 						me.frm.set_value("receivable_payable_account", r.message); | ||||
| 					} | ||||
| 				} | ||||
|  | ||||
| @ -99,6 +99,7 @@ class PaymentReconciliation(Document): | ||||
| 				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 | ||||
| 			Having | ||||
| 				amount > 0 | ||||
|  | ||||
| @ -45,6 +45,7 @@ | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "description": "Provide the invoice portion in percent", | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 1,  | ||||
| @ -170,6 +171,7 @@ | ||||
|    "unique": 0 | ||||
|   },  | ||||
|   { | ||||
|    "description": "Give number of days according to prior selection", | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 1,  | ||||
| @ -305,7 +307,7 @@ | ||||
|  "issingle": 0,  | ||||
|  "istable": 0,  | ||||
|  "max_attachments": 0,  | ||||
|  "modified": "2018-03-08 10:47:32.830478",  | ||||
|  "modified": "2020-10-14 10:47:32.830478",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Accounts",  | ||||
|  "name": "Payment Term",  | ||||
| @ -381,4 +383,4 @@ | ||||
|  "sort_order": "DESC",  | ||||
|  "track_changes": 1,  | ||||
|  "track_seen": 0 | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -291,11 +291,11 @@ | ||||
|  "issingle": 0,  | ||||
|  "istable": 0,  | ||||
|  "max_attachments": 0,  | ||||
|  "modified": "2018-08-21 16:15:49.089450",  | ||||
|  "modified": "2020-09-18 17:26:09.703215",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Accounts",  | ||||
|  "name": "Period Closing Voucher",  | ||||
|  "owner": "jai@webnotestech.com",  | ||||
|  "owner": "Administrator",  | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "amend": 1,  | ||||
|  | ||||
| @ -55,14 +55,48 @@ frappe.ui.form.on('POS Closing Entry', { | ||||
| 			}, | ||||
| 			callback: (r) => { | ||||
| 				let pos_docs = r.message; | ||||
| 				set_form_data(pos_docs, frm) | ||||
| 				refresh_fields(frm) | ||||
| 				set_html_data(frm) | ||||
| 				set_form_data(pos_docs, frm); | ||||
| 				refresh_fields(frm); | ||||
| 				set_html_data(frm); | ||||
| 			} | ||||
| 		}) | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
| cur_frm.cscript.before_pos_transactions_remove = function(doc, cdt, cdn) { | ||||
| 	const removed_row = locals[cdt][cdn]; | ||||
| 
 | ||||
| 	if (!removed_row.pos_invoice) return; | ||||
| 
 | ||||
| 	frappe.db.get_doc("POS Invoice", removed_row.pos_invoice).then(doc => { | ||||
| 		cur_frm.doc.grand_total -= flt(doc.grand_total); | ||||
| 		cur_frm.doc.net_total -= flt(doc.net_total); | ||||
| 		cur_frm.doc.total_quantity -= flt(doc.total_qty); | ||||
| 		refresh_payments(doc, cur_frm, 1); | ||||
| 		refresh_taxes(doc, cur_frm, 1); | ||||
| 		refresh_fields(cur_frm); | ||||
| 		set_html_data(cur_frm); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| frappe.ui.form.on('POS Invoice Reference', { | ||||
| 	pos_invoice(frm, cdt, cdn) { | ||||
| 		const added_row = locals[cdt][cdn]; | ||||
| 
 | ||||
| 		if (!added_row.pos_invoice) return; | ||||
| 
 | ||||
| 		frappe.db.get_doc("POS Invoice", added_row.pos_invoice).then(doc => { | ||||
| 			frm.doc.grand_total += flt(doc.grand_total); | ||||
| 			frm.doc.net_total += flt(doc.net_total); | ||||
| 			frm.doc.total_quantity += flt(doc.total_qty); | ||||
| 			refresh_payments(doc, frm); | ||||
| 			refresh_taxes(doc, frm); | ||||
| 			refresh_fields(frm); | ||||
| 			set_html_data(frm); | ||||
| 		}); | ||||
| 	} | ||||
| }) | ||||
| 
 | ||||
| frappe.ui.form.on('POS Closing Entry Detail', { | ||||
| 	closing_amount: (frm, cdt, cdn) => { | ||||
| 		const row = locals[cdt][cdn]; | ||||
| @ -76,8 +110,8 @@ function set_form_data(data, frm) { | ||||
| 		frm.doc.grand_total += flt(d.grand_total); | ||||
| 		frm.doc.net_total += flt(d.net_total); | ||||
| 		frm.doc.total_quantity += flt(d.total_qty); | ||||
| 		add_to_payments(d, frm); | ||||
| 		add_to_taxes(d, frm); | ||||
| 		refresh_payments(d, frm); | ||||
| 		refresh_taxes(d, frm); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| @ -90,11 +124,12 @@ function add_to_pos_transaction(d, frm) { | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| function add_to_payments(d, frm) { | ||||
| function refresh_payments(d, frm, remove) { | ||||
| 	d.payments.forEach(p => { | ||||
| 		const payment = frm.doc.payment_reconciliation.find(pay => pay.mode_of_payment === p.mode_of_payment); | ||||
| 		if (payment) { | ||||
| 			payment.expected_amount += flt(p.amount); | ||||
| 			if (!remove) payment.expected_amount += flt(p.amount); | ||||
| 			else payment.expected_amount -= flt(p.amount); | ||||
| 		} else { | ||||
| 			frm.add_child("payment_reconciliation", { | ||||
| 				mode_of_payment: p.mode_of_payment, | ||||
| @ -105,11 +140,12 @@ function add_to_payments(d, frm) { | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| function add_to_taxes(d, frm) { | ||||
| function refresh_taxes(d, frm, remove) { | ||||
| 	d.taxes.forEach(t => { | ||||
| 		const tax = frm.doc.taxes.find(tx => tx.account_head === t.account_head && tx.rate === t.rate); | ||||
| 		if (tax) { | ||||
| 			tax.amount += flt(t.tax_amount);  | ||||
| 			if (!remove) tax.amount += flt(t.tax_amount); | ||||
| 			else tax.amount -= flt(t.tax_amount); | ||||
| 		} else { | ||||
| 			frm.add_child("taxes", { | ||||
| 				account_head: t.account_head, | ||||
|  | ||||
| @ -45,7 +45,7 @@ class TestPOSClosingEntry(unittest.TestCase): | ||||
| 		frappe.set_user("Administrator") | ||||
| 		frappe.db.sql("delete from `tabPOS Profile`") | ||||
| 
 | ||||
| def init_user_and_profile(): | ||||
| def init_user_and_profile(**args): | ||||
| 	user = 'test@example.com' | ||||
| 	test_user = frappe.get_doc('User', user) | ||||
| 
 | ||||
| @ -53,7 +53,7 @@ def init_user_and_profile(): | ||||
| 	test_user.add_roles(*roles) | ||||
| 	frappe.set_user(user) | ||||
| 
 | ||||
| 	pos_profile = make_pos_profile() | ||||
| 	pos_profile = make_pos_profile(**args) | ||||
| 	pos_profile.append('applicable_for_users', { | ||||
| 		'default': 1, | ||||
| 		'user': user | ||||
|  | ||||
| @ -279,7 +279,8 @@ | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Is Return (Credit Note)", | ||||
|    "no_copy": 1, | ||||
|    "print_hide": 1 | ||||
|    "print_hide": 1, | ||||
|    "set_only_once": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break1", | ||||
| @ -1578,9 +1579,10 @@ | ||||
|   } | ||||
|  ], | ||||
|  "icon": "fa fa-file-text", | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-29 15:08:39.337385", | ||||
|  "modified": "2020-09-07 12:43:09.138720", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Invoice", | ||||
|  | ||||
| @ -92,21 +92,19 @@ class POSInvoice(SalesInvoice): | ||||
| 
 | ||||
| 				if len(invalid_serial_nos): | ||||
| 					multiple_nos = 's' if len(invalid_serial_nos) > 1 else '' | ||||
| 					frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. \ | ||||
| 						Please select valid serial no.".format(d.idx, multiple_nos, | ||||
| 						frappe.bold(', '.join(invalid_serial_nos)))), title=_("Not Available")) | ||||
| 					frappe.throw(_("Row #{}: Serial No{}. {} has already been transacted into another POS Invoice. Please select valid serial no.").format( | ||||
| 						d.idx, multiple_nos, frappe.bold(', '.join(invalid_serial_nos))), title=_("Not Available")) | ||||
| 			else: | ||||
| 				if allow_negative_stock: | ||||
| 					return | ||||
| 
 | ||||
| 				available_stock = get_stock_availability(d.item_code, d.warehouse) | ||||
| 				if not (flt(available_stock) > 0): | ||||
| 					frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.' | ||||
| 						.format(d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse))), title=_("Not Available")) | ||||
| 					frappe.throw(_('Row #{}: Item Code: {} is not available under warehouse {}.').format( | ||||
| 						d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse)), title=_("Not Available")) | ||||
| 				elif flt(available_stock) < flt(d.qty): | ||||
| 					frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. \ | ||||
| 						Available quantity {}.'.format(d.idx, frappe.bold(d.item_code), | ||||
| 						frappe.bold(d.warehouse), frappe.bold(d.qty))), title=_("Not Available")) | ||||
| 					frappe.msgprint(_('Row #{}: Stock quantity not enough for Item Code: {} under warehouse {}. Available quantity {}.').format( | ||||
| 						d.idx, frappe.bold(d.item_code), frappe.bold(d.warehouse), frappe.bold(d.qty)), title=_("Not Available")) | ||||
| 
 | ||||
| 	def validate_serialised_or_batched_item(self): | ||||
| 		for d in self.get("items"): | ||||
| @ -117,14 +115,14 @@ class POSInvoice(SalesInvoice): | ||||
| 
 | ||||
| 
 | ||||
| 			if serialized and batched and (no_batch_selected or no_serial_selected): | ||||
| 				frappe.throw(_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.' | ||||
| 						.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) | ||||
| 				frappe.throw(_('Row #{}: Please select a serial no and batch against item: {} or remove it to complete transaction.').format( | ||||
| 					d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) | ||||
| 			if serialized and no_serial_selected: | ||||
| 				frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.' | ||||
| 						.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) | ||||
| 				frappe.throw(_('Row #{}: No serial number selected against item: {}. Please select one or remove it to complete transaction.').format( | ||||
| 					d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) | ||||
| 			if batched and no_batch_selected: | ||||
| 				frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.' | ||||
| 						.format(d.idx, frappe.bold(d.item_code))), title=_("Invalid Item")) | ||||
| 				frappe.throw(_('Row #{}: No batch selected against item: {}. Please select a batch or remove it to complete transaction.').format( | ||||
| 					d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) | ||||
| 
 | ||||
| 	def validate_return_items(self): | ||||
| 		if not self.get("is_return"): return | ||||
| @ -139,7 +137,8 @@ class POSInvoice(SalesInvoice): | ||||
| 			frappe.throw(_("At least one mode of payment is required for POS invoice.")) | ||||
| 
 | ||||
| 	def validate_change_account(self): | ||||
| 		if frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company: | ||||
| 		if self.change_amount and self.account_for_change_amount and \ | ||||
| 			frappe.db.get_value("Account", self.account_for_change_amount, "company") != self.company: | ||||
| 			frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company)) | ||||
| 
 | ||||
| 	def validate_change_amount(self): | ||||
| @ -150,7 +149,7 @@ class POSInvoice(SalesInvoice): | ||||
| 			self.base_change_amount = flt(self.base_paid_amount - base_grand_total + flt(self.base_write_off_amount)) | ||||
| 
 | ||||
| 		if flt(self.change_amount) and not self.account_for_change_amount: | ||||
| 			msgprint(_("Please enter Account for Change Amount"), raise_exception=1) | ||||
| 			frappe.msgprint(_("Please enter Account for Change Amount"), raise_exception=1) | ||||
| 
 | ||||
| 	def verify_payment_amount(self): | ||||
| 		for entry in self.payments: | ||||
| @ -166,7 +165,7 @@ class POSInvoice(SalesInvoice): | ||||
| 				total_amount_in_payments += payment.amount | ||||
| 			invoice_total = self.rounded_total or self.grand_total | ||||
| 			if total_amount_in_payments < invoice_total: | ||||
| 				frappe.throw(_("Total payments amount can't be greater than {}".format(-invoice_total))) | ||||
| 				frappe.throw(_("Total payments amount can't be greater than {}").format(-invoice_total)) | ||||
| 
 | ||||
| 	def validate_loyalty_transaction(self): | ||||
| 		if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center): | ||||
|  | ||||
| @ -7,6 +7,8 @@ import frappe | ||||
| import unittest, copy, time | ||||
| from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile | ||||
| from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return | ||||
| 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 | ||||
| 
 | ||||
| class TestPOSInvoice(unittest.TestCase): | ||||
| 	def test_timestamp_change(self): | ||||
| @ -182,8 +184,9 @@ class TestPOSInvoice(unittest.TestCase): | ||||
| 	def test_pos_returns_with_repayment(self): | ||||
| 		pos = create_pos_invoice(qty = 10, do_not_save=True) | ||||
| 
 | ||||
| 		pos.set('payments', []) | ||||
| 		pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 500}) | ||||
| 		pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500}) | ||||
| 		pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 500, 'default': 1}) | ||||
| 		pos.insert() | ||||
| 		pos.submit() | ||||
| 
 | ||||
| @ -200,8 +203,9 @@ class TestPOSInvoice(unittest.TestCase): | ||||
| 			income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, | ||||
| 			cost_center = "Main - _TC", do_not_save=True) | ||||
| 
 | ||||
| 		pos.set('payments', []) | ||||
| 		pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 50}) | ||||
| 		pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60}) | ||||
| 		pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 60, 'default': 1}) | ||||
| 
 | ||||
| 		pos.insert() | ||||
| 		pos.submit() | ||||
| @ -219,29 +223,29 @@ class TestPOSInvoice(unittest.TestCase): | ||||
| 		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 with perpetual inventory', | ||||
| 			target_warehouse="Stores - TCP1", cost_center='Main - TCP1', expense_account='Cost of Goods Sold - TCP1') | ||||
| 		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 with perpetual inventory', debit_to='Debtors - TCP1', | ||||
| 			account_for_change_amount='Cash - TCP1', warehouse='Stores - TCP1', income_account='Sales - TCP1', | ||||
| 			expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1', | ||||
| 		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': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 1000}) | ||||
| 		pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) | ||||
| 
 | ||||
| 		pos.insert() | ||||
| 		pos.submit() | ||||
| 
 | ||||
| 		pos2 = create_pos_invoice(company='_Test Company with perpetual inventory', debit_to='Debtors - TCP1', | ||||
| 			account_for_change_amount='Cash - TCP1', warehouse='Stores - TCP1', income_account='Sales - TCP1', | ||||
| 			expense_account='Cost of Goods Sold - TCP1', cost_center='Main - TCP1', | ||||
| 		pos2 = 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) | ||||
| 
 | ||||
| 		pos2.get("items")[0].serial_no = serial_nos[0] | ||||
| 		pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 1000}) | ||||
| 		pos2.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 1000}) | ||||
| 
 | ||||
| 		self.assertRaises(frappe.ValidationError, pos2.insert) | ||||
| 
 | ||||
| @ -284,6 +288,119 @@ class TestPOSInvoice(unittest.TestCase): | ||||
| 		after_redeem_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) | ||||
| 		self.assertEqual(after_redeem_lp_details.loyalty_points, 9) | ||||
| 
 | ||||
| 	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 | ||||
| 
 | ||||
| 		frappe.db.sql("delete from `tabPOS Invoice`") | ||||
| 		test_user, pos_profile = init_user_and_profile() | ||||
| 		pos_inv = create_pos_invoice(rate=300, additional_discount_percentage=10, do_not_submit=1) | ||||
| 		pos_inv.append('payments', { | ||||
| 			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 270 | ||||
| 		}) | ||||
| 		pos_inv.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() | ||||
| 
 | ||||
| 		merge_pos_invoices() | ||||
| 
 | ||||
| 		pos_inv.load_from_db() | ||||
| 		rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") | ||||
| 		self.assertEqual(rounded_total, 3470) | ||||
| 		frappe.set_user("Administrator") | ||||
| 
 | ||||
| 	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 | ||||
| 
 | ||||
| 		frappe.db.sql("delete from `tabPOS Invoice`") | ||||
| 		test_user, pos_profile = init_user_and_profile() | ||||
| 		pos_inv = create_pos_invoice(rate=300, do_not_submit=1) | ||||
| 		pos_inv.append('payments', { | ||||
| 			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 | ||||
| 		}) | ||||
| 		pos_inv.append('taxes', { | ||||
| 			"charge_type": "On Net Total", | ||||
| 			"account_head": "_Test Account Service Tax - _TC", | ||||
| 			"cost_center": "_Test Cost Center - _TC", | ||||
| 			"description": "Service Tax", | ||||
| 			"rate": 14, | ||||
| 			'included_in_print_rate': 1 | ||||
| 		}) | ||||
| 		pos_inv.submit() | ||||
| 
 | ||||
| 		pos_inv2 = create_pos_invoice(rate=300, qty=2, do_not_submit=1) | ||||
| 		pos_inv2.additional_discount_percentage = 10 | ||||
| 		pos_inv2.append('payments', { | ||||
| 			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 540 | ||||
| 		}) | ||||
| 		pos_inv2.append('taxes', { | ||||
| 			"charge_type": "On Net Total", | ||||
| 			"account_head": "_Test Account Service Tax - _TC", | ||||
| 			"cost_center": "_Test Cost Center - _TC", | ||||
| 			"description": "Service Tax", | ||||
| 			"rate": 14, | ||||
| 			'included_in_print_rate': 1 | ||||
| 		}) | ||||
| 		pos_inv2.submit() | ||||
| 
 | ||||
| 		merge_pos_invoices() | ||||
| 
 | ||||
| 		pos_inv.load_from_db() | ||||
| 		rounded_total = frappe.db.get_value("Sales Invoice", pos_inv.consolidated_invoice, "rounded_total") | ||||
| 		self.assertEqual(rounded_total, 840) | ||||
| 		frappe.set_user("Administrator") | ||||
| 
 | ||||
| 	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 | ||||
| 
 | ||||
| 		if not frappe.db.get_single_value("Selling Settings", "validate_selling_price"): | ||||
| 			frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 1) | ||||
| 
 | ||||
| 		make_purchase_receipt(item_code="_Test Item", warehouse="_Test Warehouse - _TC", qty=1, rate=300) | ||||
| 		frappe.db.sql("delete from `tabPOS Invoice`") | ||||
| 		test_user, pos_profile = init_user_and_profile() | ||||
| 		pos_inv = create_pos_invoice(rate=300, do_not_submit=1) | ||||
| 		pos_inv.append('payments', { | ||||
| 			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 300 | ||||
| 		}) | ||||
| 		pos_inv.append('taxes', { | ||||
| 			"charge_type": "On Net Total", | ||||
| 			"account_head": "_Test Account Service Tax - _TC", | ||||
| 			"cost_center": "_Test Cost Center - _TC", | ||||
| 			"description": "Service Tax", | ||||
| 			"rate": 14, | ||||
| 			'included_in_print_rate': 1 | ||||
| 		}) | ||||
| 		self.assertRaises(frappe.ValidationError, pos_inv.submit) | ||||
| 
 | ||||
| 		pos_inv2 = create_pos_invoice(rate=400, do_not_submit=1) | ||||
| 		pos_inv2.append('payments', { | ||||
| 			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 400 | ||||
| 		}) | ||||
| 		pos_inv2.append('taxes', { | ||||
| 			"charge_type": "On Net Total", | ||||
| 			"account_head": "_Test Account Service Tax - _TC", | ||||
| 			"cost_center": "_Test Cost Center - _TC", | ||||
| 			"description": "Service Tax", | ||||
| 			"rate": 14, | ||||
| 			'included_in_print_rate': 1 | ||||
| 		}) | ||||
| 		pos_inv2.submit() | ||||
| 
 | ||||
| 		merge_pos_invoices() | ||||
| 
 | ||||
| 		pos_inv2.load_from_db() | ||||
| 		rounded_total = frappe.db.get_value("Sales Invoice", pos_inv2.consolidated_invoice, "rounded_total") | ||||
| 		self.assertEqual(rounded_total, 400) | ||||
| 		frappe.set_user("Administrator") | ||||
| 		frappe.db.set_value("Selling Settings", "Selling Settings", "validate_selling_price", 0) | ||||
| 
 | ||||
| def create_pos_invoice(**args): | ||||
| 	args = frappe._dict(args) | ||||
| 	pos_profile = None | ||||
| @ -292,12 +409,11 @@ def create_pos_invoice(**args): | ||||
| 		pos_profile.save() | ||||
| 
 | ||||
| 	pos_inv = frappe.new_doc("POS Invoice") | ||||
| 	pos_inv.update(args) | ||||
| 	pos_inv.update_stock = 1 | ||||
| 	pos_inv.is_pos = 1 | ||||
| 	pos_inv.pos_profile = args.pos_profile or pos_profile.name | ||||
| 
 | ||||
| 	pos_inv.set_missing_values() | ||||
| 
 | ||||
| 	if args.posting_date: | ||||
| 		pos_inv.set_posting_time = 1 | ||||
| 	pos_inv.posting_date = args.posting_date or frappe.utils.nowdate() | ||||
| @ -311,6 +427,8 @@ def create_pos_invoice(**args): | ||||
| 	pos_inv.conversion_rate = args.conversion_rate or 1 | ||||
| 	pos_inv.account_for_change_amount = args.account_for_change_amount or "Cash - _TC" | ||||
| 
 | ||||
| 	pos_inv.set_missing_values() | ||||
| 
 | ||||
| 	pos_inv.append("items", { | ||||
| 		"item_code": args.item or args.item_code or "_Test Item", | ||||
| 		"warehouse": args.warehouse or "_Test Warehouse - _TC", | ||||
|  | ||||
| @ -24,11 +24,20 @@ class POSInvoiceMergeLog(Document): | ||||
| 
 | ||||
| 	def validate_pos_invoice_status(self): | ||||
| 		for d in self.pos_invoices: | ||||
| 			status, docstatus = frappe.db.get_value('POS Invoice', d.pos_invoice, ['status', 'docstatus']) | ||||
| 			status, docstatus, is_return, return_against = frappe.db.get_value( | ||||
| 				'POS Invoice', d.pos_invoice, ['status', 'docstatus', 'is_return', 'return_against']) | ||||
| 
 | ||||
| 			if docstatus != 1: | ||||
| 				frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice)) | ||||
| 			if status in ['Consolidated']: | ||||
| 			if status == "Consolidated": | ||||
| 				frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status)) | ||||
| 			if is_return and return_against not in [d.pos_invoice for d in self.pos_invoices] and status != "Consolidated": | ||||
| 				# if return entry is not getting merged in the current pos closing and if it is not consolidated | ||||
| 				frappe.throw( | ||||
| 					_("Row #{}: Return Invoice {} cannot be made against unconsolidated invoice. \ | ||||
| 					You can add original invoice {} manually to proceed.") | ||||
| 					.format(d.idx, frappe.bold(d.pos_invoice), frappe.bold(return_against)) | ||||
| 				) | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] | ||||
| @ -36,12 +45,12 @@ class POSInvoiceMergeLog(Document): | ||||
| 		returns = [d for d in pos_invoice_docs if d.get('is_return') == 1] | ||||
| 		sales = [d for d in pos_invoice_docs if d.get('is_return') == 0] | ||||
| 
 | ||||
| 		sales_invoice = self.process_merging_into_sales_invoice(sales) | ||||
| 		sales_invoice, credit_note = "", "" | ||||
| 		if sales: | ||||
| 			sales_invoice = self.process_merging_into_sales_invoice(sales) | ||||
| 		 | ||||
| 		if len(returns): | ||||
| 		if returns: | ||||
| 			credit_note = self.process_merging_into_credit_note(returns) | ||||
| 		else: | ||||
| 			credit_note = "" | ||||
| 
 | ||||
| 		self.save() # save consolidated_sales_invoice & consolidated_credit_note ref in merge log | ||||
| 
 | ||||
| @ -87,17 +96,28 @@ class POSInvoiceMergeLog(Document): | ||||
| 				loyalty_amount_sum += doc.loyalty_amount | ||||
| 			 | ||||
| 			for item in doc.get('items'): | ||||
| 				items.append(item) | ||||
| 				found = False | ||||
| 				for i in items: | ||||
| 					if (i.item_code == item.item_code and not i.serial_no and not i.batch_no and | ||||
| 						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) | ||||
| 			 | ||||
| 			for tax in doc.get('taxes'): | ||||
| 				found = False | ||||
| 				for t in taxes: | ||||
| 					if t.account_head == tax.account_head and t.cost_center == tax.cost_center and t.rate == tax.rate: | ||||
| 						t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount) | ||||
| 						t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount) | ||||
| 					if t.account_head == tax.account_head and t.cost_center == tax.cost_center: | ||||
| 						t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount) | ||||
| 						t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount) | ||||
| 						found = True | ||||
| 				if not found: | ||||
| 					tax.charge_type = 'Actual' | ||||
| 					tax.included_in_print_rate = 0 | ||||
| 					tax.tax_amount = tax.tax_amount_after_discount_amount | ||||
| 					tax.base_tax_amount = tax.base_tax_amount_after_discount_amount | ||||
| 					taxes.append(tax) | ||||
| 
 | ||||
| 			for payment in doc.get('payments'): | ||||
| @ -118,6 +138,8 @@ class POSInvoiceMergeLog(Document): | ||||
| 		invoice.set('items', items) | ||||
| 		invoice.set('payments', payments) | ||||
| 		invoice.set('taxes', taxes) | ||||
| 		invoice.additional_discount_percentage = 0 | ||||
| 		invoice.discount_amount = 0.0 | ||||
| 
 | ||||
| 		return invoice | ||||
| 	 | ||||
|  | ||||
| @ -5,13 +5,14 @@ | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.utils import cint | ||||
| 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): | ||||
| 	def validate(self): | ||||
| 		self.validate_pos_profile_and_cashier() | ||||
| 		self.validate_payment_method_account() | ||||
| 		self.set_status() | ||||
| 
 | ||||
| 	def validate_pos_profile_and_cashier(self): | ||||
| @ -20,6 +21,14 @@ class POSOpeningEntry(StatusUpdater): | ||||
| 
 | ||||
| 		if not cint(frappe.db.get_value("User", self.user, "enabled")): | ||||
| 			frappe.throw(_("User {} has been disabled. Please select valid user/cashier".format(self.user))) | ||||
| 	 | ||||
| 	def validate_payment_method_account(self): | ||||
| 		for d in self.balance_details: | ||||
| 			account = frappe.db.get_value("Mode of Payment Account",  | ||||
| 				{"parent": d.mode_of_payment, "company": self.company}, "default_account") | ||||
| 			if not account: | ||||
| 				frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") | ||||
| 					.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		self.set_status(update=True) | ||||
| @ -15,15 +15,6 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) { | ||||
| 	erpnext.queries.setup_queries(frm, "Warehouse", function() { | ||||
| 		return erpnext.queries.warehouse(frm.doc); | ||||
| 	}); | ||||
| 
 | ||||
| 	frm.call({ | ||||
| 		method: "erpnext.accounts.doctype.pos_profile.pos_profile.get_series", | ||||
| 		callback: function(r) { | ||||
| 			if(!r.exc) { | ||||
| 				set_field_options("naming_series", r.message); | ||||
| 			} | ||||
| 		} | ||||
| 	}); | ||||
| }); | ||||
| 
 | ||||
| frappe.ui.form.on('POS Profile', { | ||||
|  | ||||
| @ -8,7 +8,6 @@ | ||||
|  "field_order": [ | ||||
|   "disabled", | ||||
|   "section_break_2", | ||||
|   "naming_series", | ||||
|   "customer", | ||||
|   "company", | ||||
|   "country", | ||||
| @ -59,17 +58,6 @@ | ||||
|    "fieldname": "section_break_2", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "naming_series", | ||||
|    "fieldtype": "Select", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Series", | ||||
|    "no_copy": 1, | ||||
|    "oldfieldname": "naming_series", | ||||
|    "oldfieldtype": "Select", | ||||
|    "options": "[Select]", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "customer", | ||||
|    "fieldtype": "Link", | ||||
| @ -323,7 +311,7 @@ | ||||
|  "icon": "icon-cog", | ||||
|  "idx": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-06-29 12:20:30.977272", | ||||
|  "modified": "2020-10-01 17:29:27.759088", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Profile", | ||||
| @ -350,4 +338,4 @@ | ||||
|  ], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC" | ||||
| } | ||||
| } | ||||
| @ -4,7 +4,7 @@ | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import msgprint, _ | ||||
| from frappe.utils import cint, now | ||||
| from frappe.utils import cint, now, get_link_to_form | ||||
| from six import iteritems | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| @ -13,7 +13,7 @@ class POSProfile(Document): | ||||
| 		self.validate_default_profile() | ||||
| 		self.validate_all_link_fields() | ||||
| 		self.validate_duplicate_groups() | ||||
| 		self.check_default_payment() | ||||
| 		self.validate_payment_methods() | ||||
| 
 | ||||
| 	def validate_default_profile(self): | ||||
| 		for row in self.applicable_for_users: | ||||
| @ -52,14 +52,23 @@ class POSProfile(Document): | ||||
| 		if len(customer_groups) != len(set(customer_groups)): | ||||
| 			frappe.throw(_("Duplicate customer group found in the cutomer group table"), title = "Duplicate Customer Group") | ||||
| 
 | ||||
| 	def check_default_payment(self): | ||||
| 		if self.payments: | ||||
| 			default_mode_of_payment = [d.default for d in self.payments if d.default] | ||||
| 			if not default_mode_of_payment: | ||||
| 				frappe.throw(_("Set default mode of payment")) | ||||
| 	def validate_payment_methods(self): | ||||
| 		if not self.payments: | ||||
| 			frappe.throw(_("Payment methods are mandatory. Please add at least one payment method.")) | ||||
| 
 | ||||
| 			if len(default_mode_of_payment) > 1: | ||||
| 				frappe.throw(_("Multiple default mode of payment is not allowed")) | ||||
| 		default_mode_of_payment = [d.default for d in self.payments if d.default] | ||||
| 		if not default_mode_of_payment: | ||||
| 			frappe.throw(_("Please select a default mode of payment")) | ||||
| 
 | ||||
| 		if len(default_mode_of_payment) > 1: | ||||
| 			frappe.throw(_("You can only select one mode of payment as default")) | ||||
| 		 | ||||
| 		for d in self.payments: | ||||
| 			account = frappe.db.get_value("Mode of Payment Account",  | ||||
| 				{"parent": d.mode_of_payment, "company": self.company}, "default_account") | ||||
| 			if not account: | ||||
| 				frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") | ||||
| 					.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) | ||||
| 
 | ||||
| 	def on_update(self): | ||||
| 		self.set_defaults() | ||||
| @ -100,10 +109,6 @@ def get_child_nodes(group_type, root): | ||||
| 	return frappe.db.sql(""" Select name, lft, rgt from `tab{tab}` where | ||||
| 			lft >= {lft} and rgt <= {rgt} order by lft""".format(tab=group_type, lft=lft, rgt=rgt), as_dict=1) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_series(): | ||||
| 	return frappe.get_meta("POS Invoice").get_field("naming_series").options or "s" | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| @frappe.validate_and_sanitize_search_inputs | ||||
| def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): | ||||
|  | ||||
| @ -7,10 +7,10 @@ frappe.ui.form.on('POS Settings', { | ||||
| 	}, | ||||
| 
 | ||||
| 	get_invoice_fields: function(frm) { | ||||
| 		frappe.model.with_doctype("Sales Invoice", () => { | ||||
| 			var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { | ||||
| 		frappe.model.with_doctype("POS Invoice", () => { | ||||
| 			var fields = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) { | ||||
| 				if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || | ||||
| 					d.fieldtype === 'Table') { | ||||
| 					['Table', 'Button'].includes(d.fieldtype)) { | ||||
| 					return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; | ||||
| 				} else { | ||||
| 					return null; | ||||
| @ -25,7 +25,7 @@ frappe.ui.form.on('POS Settings', { | ||||
| frappe.ui.form.on("POS Field", { | ||||
| 	fieldname: function(frm, doctype, name) { | ||||
| 		var doc = frappe.get_doc(doctype, name); | ||||
| 		var df = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { | ||||
| 		var df = $.map(frappe.get_doc("DocType", "POS Invoice").fields, function(d) { | ||||
| 			return doc.fieldname == d.fieldname ? d : null; | ||||
| 		})[0]; | ||||
| 
 | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "allow_import": 1, | ||||
|  "allow_rename": 1, | ||||
|  "autoname": "field:title", | ||||
| @ -71,6 +72,7 @@ | ||||
|   "section_break_13", | ||||
|   "threshold_percentage", | ||||
|   "priority", | ||||
|   "condition", | ||||
|   "column_break_66", | ||||
|   "apply_multiple_pricing_rules", | ||||
|   "apply_discount_on_rate", | ||||
| @ -550,11 +552,18 @@ | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Promotional Scheme", | ||||
|    "options": "Promotional Scheme" | ||||
|   }, | ||||
|   { | ||||
|    "description": "Simple Python Expression, Example: territory != 'All Territories'", | ||||
|    "fieldname": "condition", | ||||
|    "fieldtype": "Code", | ||||
|    "label": "Condition" | ||||
|   } | ||||
|  ], | ||||
|  "icon": "fa fa-gift", | ||||
|  "idx": 1, | ||||
|  "modified": "2019-12-18 17:29:22.957077", | ||||
|  "links": [], | ||||
|  "modified": "2020-08-26 12:24:44.740734", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Pricing Rule", | ||||
|  | ||||
| @ -6,9 +6,10 @@ from __future__ import unicode_literals | ||||
| import frappe | ||||
| import json | ||||
| import copy | ||||
| import re | ||||
| 
 | ||||
| from frappe import throw, _ | ||||
| from frappe.utils import flt, cint, getdate | ||||
| 
 | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| from six import string_types | ||||
| @ -30,6 +31,7 @@ class PricingRule(Document): | ||||
| 		self.validate_max_discount() | ||||
| 		self.validate_price_list_with_currency() | ||||
| 		self.validate_dates() | ||||
| 		self.validate_condition() | ||||
| 
 | ||||
| 		if not self.margin_type: self.margin_rate_or_amount = 0.0 | ||||
| 
 | ||||
| @ -140,6 +142,10 @@ class PricingRule(Document): | ||||
| 		if self.valid_from and self.valid_upto and getdate(self.valid_from) > getdate(self.valid_upto): | ||||
| 			frappe.throw(_("Valid from date must be less than valid upto date")) | ||||
| 
 | ||||
| 	def validate_condition(self): | ||||
| 		if self.condition and ("=" in self.condition) and re.match("""[\w\.:_]+\s*={1}\s*[\w\.@'"]+""", self.condition): | ||||
| 			frappe.throw(_("Invalid condition expression")) | ||||
| 
 | ||||
| #-------------------------------------------------------------------------------- | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
|  | ||||
| @ -429,7 +429,34 @@ class TestPricingRule(unittest.TestCase): | ||||
| 		details = get_item_details(args) | ||||
| 
 | ||||
| 		self.assertTrue(details) | ||||
| 
 | ||||
| 	 | ||||
| 	def test_pricing_rule_for_condition(self): | ||||
| 		frappe.delete_doc_if_exists("Pricing Rule", "_Test Pricing Rule") | ||||
| 		 | ||||
| 		make_pricing_rule(selling=1, margin_type="Percentage", \ | ||||
| 			condition="customer=='_Test Customer 1' and is_return==0", discount_percentage=10) | ||||
| 		 | ||||
| 		# Incorrect Customer and Correct is_return value | ||||
| 		si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 2", is_return=0) | ||||
| 		si.items[0].price_list_rate = 1000 | ||||
| 		si.submit() | ||||
| 		item = si.items[0] | ||||
| 		self.assertEquals(item.rate, 100) | ||||
| 		 | ||||
| 		# Correct Customer and Incorrect is_return value | ||||
| 		si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=1, qty=-1) | ||||
| 		si.items[0].price_list_rate = 1000 | ||||
| 		si.submit() | ||||
| 		item = si.items[0] | ||||
| 		self.assertEquals(item.rate, 100) | ||||
| 		 | ||||
| 		# Correct Customer and correct is_return value | ||||
| 		si = create_sales_invoice(do_not_submit=True, customer="_Test Customer 1", is_return=0) | ||||
| 		si.items[0].price_list_rate = 1000 | ||||
| 		si.submit() | ||||
| 		item = si.items[0] | ||||
| 		self.assertEquals(item.rate, 900) | ||||
| 		 | ||||
| def make_pricing_rule(**args): | ||||
| 	args = frappe._dict(args) | ||||
| 
 | ||||
| @ -448,7 +475,8 @@ def make_pricing_rule(**args): | ||||
| 		"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 | ||||
| 		"margin_rate_or_amount": args.margin_rate_or_amount or 0.0, | ||||
| 		"condition": args.condition or '' | ||||
| 	}) | ||||
| 
 | ||||
| 	apply_on = doc.apply_on.replace(' ', '_').lower() | ||||
|  | ||||
| @ -37,6 +37,8 @@ def get_pricing_rules(args, doc=None): | ||||
| 
 | ||||
| 	rules = [] | ||||
| 
 | ||||
| 	pricing_rules = filter_pricing_rule_based_on_condition(pricing_rules, doc) | ||||
| 
 | ||||
| 	if not pricing_rules: return [] | ||||
| 
 | ||||
| 	if apply_multiple_pricing_rules(pricing_rules): | ||||
| @ -51,6 +53,23 @@ def get_pricing_rules(args, doc=None): | ||||
| 
 | ||||
| 	return rules | ||||
| 
 | ||||
| def filter_pricing_rule_based_on_condition(pricing_rules, doc=None): | ||||
| 	filtered_pricing_rules = [] | ||||
| 	if doc: | ||||
| 		for pricing_rule in pricing_rules: | ||||
| 			if pricing_rule.condition: | ||||
| 				try: | ||||
| 					if frappe.safe_eval(pricing_rule.condition, None, doc.as_dict()): | ||||
| 						filtered_pricing_rules.append(pricing_rule) | ||||
| 				except: | ||||
| 					pass | ||||
| 			else: | ||||
| 				filtered_pricing_rules.append(pricing_rule) | ||||
| 	else: | ||||
| 		filtered_pricing_rules = pricing_rules | ||||
| 
 | ||||
| 	return filtered_pricing_rules | ||||
| 
 | ||||
| def _get_pricing_rules(apply_on, args, values): | ||||
| 	apply_on_field = frappe.scrub(apply_on) | ||||
| 
 | ||||
|  | ||||
| @ -10,13 +10,15 @@ frappe.ui.form.on('Process Deferred Accounting', { | ||||
| 				} | ||||
| 			}; | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 		if (frm.doc.company) { | ||||
| 	type: function(frm) { | ||||
| 		if (frm.doc.company && frm.doc.type) { | ||||
| 			frm.set_query("account", function() { | ||||
| 				return { | ||||
| 					filters: { | ||||
| 						'company': frm.doc.company, | ||||
| 						'root_type': 'Liability', | ||||
| 						'root_type': frm.doc.type === 'Income' ? 'Liability' : 'Asset', | ||||
| 						'is_group': 0 | ||||
| 					} | ||||
| 				}; | ||||
|  | ||||
| @ -60,6 +60,7 @@ | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval: doc.type", | ||||
|    "fieldname": "account", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Account", | ||||
| @ -73,9 +74,10 @@ | ||||
|    "reqd": 1 | ||||
|   } | ||||
|  ], | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-02-06 18:18:09.852844", | ||||
|  "modified": "2020-09-03 18:07:02.463754", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Process Deferred Accounting", | ||||
|  | ||||
| @ -25,6 +25,12 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ | ||||
| 				this.frm.set_df_property("credit_to", "print_hide", 0); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		// Trigger supplier event on load if supplier is available
 | ||||
| 		// The reason for this is PI can be created from PR or PO and supplier is pre populated
 | ||||
| 		if (this.frm.doc.supplier && this.frm.doc.__islocal) { | ||||
| 			this.frm.trigger('supplier'); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	refresh: function(doc) { | ||||
| @ -135,6 +141,8 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 
 | ||||
| 		this.frm.set_df_property("tax_withholding_category", "hidden", doc.apply_tds ? 0 : 1); | ||||
| 	}, | ||||
| 
 | ||||
| 	unblock_invoice: function() { | ||||
|  | ||||
| @ -361,6 +361,7 @@ | ||||
|    "fieldname": "bill_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Supplier Invoice Date", | ||||
|    "no_copy": 1, | ||||
|    "oldfieldname": "bill_date", | ||||
|    "oldfieldtype": "Date", | ||||
|    "print_hide": 1 | ||||
| @ -1333,8 +1334,7 @@ | ||||
|  "icon": "fa fa-file-text", | ||||
|  "idx": 204, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-08-03 23:20:04.466153", | ||||
|  "modified": "2020-09-21 12:22:09.164068", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Purchase Invoice", | ||||
|  | ||||
| @ -132,6 +132,11 @@ class PurchaseInvoice(BuyingController): | ||||
| 		if not self.due_date: | ||||
| 			self.due_date = get_due_date(self.posting_date, "Supplier", self.supplier, self.company,  self.bill_date) | ||||
| 
 | ||||
| 		tds_category = frappe.db.get_value("Supplier", self.supplier, "tax_withholding_category") | ||||
| 		if tds_category and not for_validate: | ||||
| 			self.apply_tds = 1 | ||||
| 			self.tax_withholding_category = tds_category | ||||
| 
 | ||||
| 		super(PurchaseInvoice, self).set_missing_values(for_validate) | ||||
| 
 | ||||
| 	def check_conversion_rate(self): | ||||
| @ -400,8 +405,6 @@ class PurchaseInvoice(BuyingController): | ||||
| 		update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) | ||||
| 
 | ||||
| 	def make_gl_entries(self, gl_entries=None): | ||||
| 		if not self.grand_total: | ||||
| 			return | ||||
| 		if not gl_entries: | ||||
| 			gl_entries = self.get_gl_entries() | ||||
| 
 | ||||
| @ -708,7 +711,8 @@ class PurchaseInvoice(BuyingController): | ||||
| 									item.item_tax_amount / self.conversion_rate) | ||||
| 						}, item=item)) | ||||
| 				else: | ||||
| 					cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) | ||||
| 					cwip_account = get_asset_account("capital_work_in_progress_account", | ||||
| 						asset_category=item.asset_category,company=self.company) | ||||
| 
 | ||||
| 					cwip_account_currency = get_account_currency(cwip_account) | ||||
| 					gl_entries.append(self.get_gl_dict({ | ||||
|  | ||||
| @ -1002,7 +1002,8 @@ def make_purchase_invoice(**args): | ||||
| 		"cost_center": args.cost_center or "_Test Cost Center - _TC", | ||||
| 		"project": args.project, | ||||
| 		"rejected_warehouse": args.rejected_warehouse or "", | ||||
| 		"rejected_serial_no": args.rejected_serial_no or "" | ||||
| 		"rejected_serial_no": args.rejected_serial_no or "", | ||||
| 		"asset_location": args.location or "" | ||||
| 	}) | ||||
| 
 | ||||
| 	if args.get_taxes_and_charges: | ||||
|  | ||||
| @ -210,7 +210,7 @@ | ||||
|  "idx": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-03-12 14:53:47.679439", | ||||
|  "modified": "2020-09-18 17:26:09.703215", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Purchase Taxes and Charges", | ||||
|  | ||||
| @ -78,7 +78,7 @@ | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Purchase Taxes and Charges Template", | ||||
|  "owner": "wasim@webnotestech.com", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "email": 1, | ||||
|  | ||||
| @ -19,6 +19,7 @@ | ||||
|   "is_return", | ||||
|   "column_break1", | ||||
|   "company", | ||||
|   "company_tax_id", | ||||
|   "posting_date", | ||||
|   "posting_time", | ||||
|   "set_posting_time", | ||||
| @ -1825,7 +1826,7 @@ | ||||
|    "fieldtype": "Table", | ||||
|    "hide_days": 1, | ||||
|    "hide_seconds": 1, | ||||
|    "label": "Sales Team1", | ||||
|    "label": "Sales Contributions and Incentives", | ||||
|    "oldfieldname": "sales_team", | ||||
|    "oldfieldtype": "Table", | ||||
|    "options": "Sales Team", | ||||
| @ -1926,6 +1927,7 @@ | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "depends_on": "eval:(doc.is_pos && doc.is_consolidated)", | ||||
|    "fieldname": "is_consolidated", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Is Consolidated", | ||||
| @ -1940,13 +1942,20 @@ | ||||
|    "hide_seconds": 1, | ||||
|    "label": "Is Internal Customer", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "company.tax_id", | ||||
|    "fieldname": "company_tax_id", | ||||
|    "fieldtype": "Data", | ||||
|    "label": "Company Tax ID", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "icon": "fa fa-file-text", | ||||
|  "idx": 181, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-08-27 01:56:28.532140", | ||||
|  "modified": "2020-10-09 15:59:57.544736", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Sales Invoice", | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| 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 | ||||
| from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate, get_link_to_form | ||||
| from frappe import _, msgprint, throw | ||||
| from erpnext.accounts.party import get_party_account, get_due_date | ||||
| from frappe.model.mapper import get_mapped_doc | ||||
| @ -428,7 +428,7 @@ class SalesInvoice(SellingController): | ||||
| 			if pos.get('account_for_change_amount'): | ||||
| 				self.account_for_change_amount = pos.get('account_for_change_amount') | ||||
| 
 | ||||
| 			for fieldname in ('naming_series', 'currency', 'letter_head', 'tc_name', | ||||
| 			for fieldname in ('currency', 'letter_head', 'tc_name', | ||||
| 				'company', 'select_print_heading', 'write_off_account', 'taxes_and_charges', | ||||
| 				'write_off_cost_center', 'apply_discount_on', 'cost_center'): | ||||
| 					if (not for_validate) or (for_validate and not self.get(fieldname)): | ||||
| @ -572,7 +572,8 @@ class SalesInvoice(SellingController): | ||||
| 
 | ||||
| 	def validate_pos(self): | ||||
| 		if self.is_return: | ||||
| 			if flt(self.paid_amount) + flt(self.write_off_amount) - flt(self.grand_total) > \ | ||||
| 			invoice_total = self.rounded_total or self.grand_total | ||||
| 			if flt(self.paid_amount) + flt(self.write_off_amount) - flt(invoice_total) > \ | ||||
| 				1.0/(10.0**(self.precision("grand_total") + 1.0)): | ||||
| 					frappe.throw(_("Paid amount + Write Off Amount can not be greater than Grand Total")) | ||||
| 
 | ||||
| @ -1372,7 +1373,7 @@ def get_bank_cash_account(mode_of_payment, company): | ||||
| 		{"parent": mode_of_payment, "company": company}, "default_account") | ||||
| 	if not account: | ||||
| 		frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") | ||||
| 			.format(mode_of_payment)) | ||||
| 			.format(get_link_to_form("Mode of Payment", mode_of_payment)), title=_("Missing Account")) | ||||
| 	return { | ||||
| 		"account": account | ||||
| 	} | ||||
| @ -1612,18 +1613,16 @@ def update_multi_mode_option(doc, pos_profile): | ||||
| 		payment.type = payment_mode.type | ||||
| 
 | ||||
| 	doc.set('payments', []) | ||||
| 	if not pos_profile or not pos_profile.get('payments'): | ||||
| 		for payment_mode in get_all_mode_of_payments(doc): | ||||
| 			append_payment(payment_mode) | ||||
| 		return | ||||
| 
 | ||||
| 	for pos_payment_method in pos_profile.get('payments'): | ||||
| 		pos_payment_method = pos_payment_method.as_dict() | ||||
| 
 | ||||
| 		payment_mode = get_mode_of_payment_info(pos_payment_method.mode_of_payment, doc.company) | ||||
| 		if payment_mode: | ||||
| 			payment_mode[0].default = pos_payment_method.default | ||||
| 			append_payment(payment_mode[0]) | ||||
| 		if not payment_mode: | ||||
| 			frappe.throw(_("Please set default Cash or Bank account in Mode of Payment {0}") | ||||
| 				.format(get_link_to_form("Mode of Payment", pos_payment_method.mode_of_payment)), title=_("Missing Account")) | ||||
| 
 | ||||
| 		payment_mode[0].default = pos_payment_method.default | ||||
| 		append_payment(payment_mode[0]) | ||||
| 
 | ||||
| def get_all_mode_of_payments(doc): | ||||
| 	return frappe.db.sql(""" | ||||
|  | ||||
| @ -13,8 +13,7 @@ def get_data(): | ||||
| 			'Auto Repeat': 'reference_document', | ||||
| 		}, | ||||
| 		'internal_links': { | ||||
| 			'Sales Order': ['items', 'sales_order'], | ||||
| 			'Delivery Note': ['items', 'delivery_note'] | ||||
| 			'Sales Order': ['items', 'sales_order'] | ||||
| 		}, | ||||
| 		'transactions': [ | ||||
| 			{ | ||||
|  | ||||
| @ -345,13 +345,14 @@ class Subscription(Document): | ||||
| 			invoice.set_taxes() | ||||
| 
 | ||||
| 		# Due date | ||||
| 		invoice.append( | ||||
| 			'payment_schedule', | ||||
| 			{ | ||||
| 				'due_date': add_days(invoice.posting_date, cint(self.days_until_due)), | ||||
| 				'invoice_portion': 100 | ||||
| 			} | ||||
| 		) | ||||
| 		if self.days_until_due: | ||||
| 			invoice.append( | ||||
| 				'payment_schedule', | ||||
| 				{ | ||||
| 					'due_date': add_days(invoice.posting_date, cint(self.days_until_due)), | ||||
| 					'invoice_portion': 100 | ||||
| 				} | ||||
| 			) | ||||
| 
 | ||||
| 		# Discounts | ||||
| 		if self.additional_discount_percentage: | ||||
|  | ||||
| @ -6,6 +6,8 @@ from __future__ import unicode_literals | ||||
| import frappe | ||||
| import unittest | ||||
| from erpnext.accounts.doctype.tax_rule.tax_rule import IncorrectCustomerGroup, IncorrectSupplierType, ConflictingTaxRule, get_tax_template | ||||
| from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity | ||||
| from erpnext.crm.doctype.opportunity.opportunity import make_quotation | ||||
| 
 | ||||
| test_records = frappe.get_test_records('Tax Rule') | ||||
| 
 | ||||
| @ -144,6 +146,23 @@ class TestTaxRule(unittest.TestCase): | ||||
| 		self.assertEqual(get_tax_template("2015-01-01", {"customer":"_Test Customer", "billing_city": "Test City 1"}), | ||||
| 			"_Test Sales Taxes and Charges Template 1 - _TC") | ||||
| 
 | ||||
| 	def test_taxes_fetch_via_tax_rule(self): | ||||
| 		make_tax_rule(customer= "_Test Customer", billing_city = "_Test City", | ||||
| 			sales_tax_template = "_Test Sales Taxes and Charges Template - _TC", save=1) | ||||
| 
 | ||||
| 		# create opportunity for customer | ||||
| 		opportunity = make_opportunity(with_items=1) | ||||
| 
 | ||||
| 		# make quotation from opportunity | ||||
| 		quotation = make_quotation(opportunity.name) | ||||
| 		quotation.save() | ||||
| 
 | ||||
| 		self.assertEqual(quotation.taxes_and_charges, "_Test Sales Taxes and Charges Template - _TC") | ||||
| 
 | ||||
| 		# Check if accounts heads and rate fetched are also fetched from tax template or not | ||||
| 		self.assertTrue(len(quotation.taxes) > 0) | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| def make_tax_rule(**args): | ||||
| 	args = frappe._dict(args) | ||||
|  | ||||
| @ -106,6 +106,7 @@ def get_tds_amount(suppliers, net_total, company, tax_details, fiscal_year_detai | ||||
| 			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] | ||||
| @ -192,6 +193,7 @@ def get_advance_vouchers(suppliers, fiscal_year=None, company=None, from_date=No | ||||
| 		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): | ||||
|  | ||||
| @ -171,7 +171,7 @@ def validate_account_for_perpetual_inventory(gl_map): | ||||
| 					frappe.throw(_("Account: {0} can only be updated via Stock Transactions") | ||||
| 						.format(account), StockAccountInvalidTransaction) | ||||
| 
 | ||||
| 			elif account_bal != stock_bal: | ||||
| 			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")) | ||||
| 
 | ||||
| @ -320,7 +320,6 @@ def make_reverse_gl_entries(gl_entries=None, voucher_type=None, voucher_no=None, | ||||
| 
 | ||||
| 			entry['remarks'] = "On cancellation of " + entry['voucher_no'] | ||||
| 			entry['is_cancelled'] = 1 | ||||
| 			entry['posting_date'] = today() | ||||
| 
 | ||||
| 			if entry['debit'] or entry['credit']: | ||||
| 				make_entry(entry, adv_adj, "Yes") | ||||
|  | ||||
| @ -20,7 +20,7 @@ | ||||
|  "owner": "Administrator", | ||||
|  "steps": [ | ||||
|   { | ||||
|    "step": "Chart Of Accounts" | ||||
|    "step": "Chart of Accounts" | ||||
|   }, | ||||
|   { | ||||
|    "step": "Setup Taxes" | ||||
|  | ||||
| @ -10,11 +10,11 @@ | ||||
|  "is_skipped": 0, | ||||
|  "modified": "2020-05-14 17:40:28.410447", | ||||
|  "modified_by": "Administrator", | ||||
|  "name": "Chart Of Accounts", | ||||
|  "name": "Chart of Accounts", | ||||
|  "owner": "Administrator", | ||||
|  "path": "Tree/Account", | ||||
|  "reference_document": "Account", | ||||
|  "show_full_form": 0, | ||||
|  "title": "Review Chart Of Accounts", | ||||
|  "title": "Review Chart of Accounts", | ||||
|  "validate_action": 0 | ||||
| } | ||||
| @ -72,7 +72,7 @@ erpnext.accounts.bankReconciliation = class BankReconciliation { | ||||
| 	check_plaid_status() { | ||||
| 		const me = this; | ||||
| 		frappe.db.get_value("Plaid Settings", "Plaid Settings", "enabled", (r) => { | ||||
| 			if (r && r.enabled == "1") { | ||||
| 			if (r && r.enabled === "1") { | ||||
| 				me.plaid_status = "active" | ||||
| 			} else { | ||||
| 				me.plaid_status = "inactive" | ||||
| @ -139,7 +139,7 @@ erpnext.accounts.bankTransactionUpload = class bankTransactionUpload { | ||||
| 	} | ||||
| 
 | ||||
| 	make() { | ||||
| 		const me = this;	 | ||||
| 		const me = this; | ||||
| 		new frappe.ui.FileUploader({ | ||||
| 			method: 'erpnext.accounts.doctype.bank_transaction.bank_transaction_upload.upload_bank_statement', | ||||
| 			allow_multiple: 0, | ||||
| @ -214,31 +214,35 @@ erpnext.accounts.bankTransactionSync = class bankTransactionSync { | ||||
| 
 | ||||
| 	init_config() { | ||||
| 		const me = this; | ||||
| 		frappe.xcall('erpnext.erpnext_integrations.doctype.plaid_settings.plaid_settings.plaid_configuration') | ||||
| 		.then(result => { | ||||
| 			me.plaid_env = result.plaid_env; | ||||
| 			me.plaid_public_key = result.plaid_public_key; | ||||
| 			me.client_name = result.client_name; | ||||
| 			me.sync_transactions() | ||||
| 		}) | ||||
| 		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", (v) => { | ||||
| 		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: v['bank'], | ||||
| 				bank: r.bank, | ||||
| 				bank_account: me.parent.bank_account, | ||||
| 				freeze: true | ||||
| 			}) | ||||
| 			.then((result) => { | ||||
| 				let result_title = (result.length > 0) ? __("{0} bank transaction(s) created", [result.length]) : __("This bank account is already synchronized") | ||||
| 				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>` | ||||
| 				<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'}); | ||||
| 				frappe.show_alert({ message: __("Bank account '{0}' has been synchronized", [me.parent.bank_account]), indicator: 'green' }); | ||||
| 			}) | ||||
| 		}) | ||||
| 	} | ||||
| @ -384,7 +388,7 @@ erpnext.accounts.ReconciliationRow = class ReconciliationRow { | ||||
| 		}) | ||||
| 
 | ||||
| 		frappe.xcall('erpnext.accounts.page.bank_reconciliation.bank_reconciliation.get_linked_payments', | ||||
| 			{bank_transaction: data, freeze:true, freeze_message:__("Finding linked payments")} | ||||
| 			{ bank_transaction: data, freeze: true, freeze_message: __("Finding linked payments") } | ||||
| 		).then((result) => { | ||||
| 			me.make_dialog(result) | ||||
| 		}) | ||||
|  | ||||
| @ -19,8 +19,7 @@ def reconcile(bank_transaction, payment_doctype, payment_name): | ||||
| 	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)) | ||||
| 		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")) | ||||
| @ -83,50 +82,30 @@ def check_matching_amount(bank_account, company, transaction): | ||||
| 		"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)]]) | ||||
| 
 | ||||
| 	if transaction.credit > 0: | ||||
| 		journal_entries = frappe.db.sql(""" | ||||
| 			SELECT | ||||
| 				'Journal Entry' as doctype, je.name, je.posting_date, je.cheque_no as reference_no, | ||||
| 				je.pay_to_recd_from as party, je.cheque_date as reference_date, jea.debit_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 = %s | ||||
| 			AND | ||||
| 				jea.debit_in_account_currency like %s | ||||
| 			AND | ||||
| 				je.docstatus = 1 | ||||
| 		""", (bank_account, amount), as_dict=True) | ||||
| 	else: | ||||
| 		journal_entries = frappe.db.sql(""" | ||||
| 			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.credit_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.credit_in_account_currency like %(txt)s | ||||
| 			AND | ||||
| 				je.docstatus = 1 | ||||
| 		""", { | ||||
| 			'bank_account': bank_account, | ||||
| 			'txt': '%%%s%%' % amount | ||||
| 		}, as_dict=True) | ||||
| 	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(""" | ||||
|  | ||||
| @ -203,7 +203,7 @@ def set_account_and_due_date(party, account, party_type, company, posting_date, | ||||
| 	return out | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_party_account(party_type, party, company): | ||||
| def get_party_account(party_type, party, company=None): | ||||
| 	"""Returns the account for the given `party`. | ||||
| 		Will first search in party (Customer / Supplier) record, if not found, | ||||
| 		will search in group (Customer Group / Supplier Group), | ||||
|  | ||||
| @ -34,7 +34,7 @@ frappe.query_reports["Accounts Receivable"] = { | ||||
| 					filters: { | ||||
| 						'company': company | ||||
| 					} | ||||
| 				} | ||||
| 				}; | ||||
| 			} | ||||
| 		}, | ||||
| 		{ | ||||
|  | ||||
| @ -617,9 +617,19 @@ class ReceivablePayableReport(object): | ||||
| 		elif party_type_field=="supplier": | ||||
| 			self.add_supplier_filters(conditions, values) | ||||
| 
 | ||||
| 		if self.filters.cost_center: | ||||
| 			self.get_cost_center_conditions(conditions) | ||||
| 
 | ||||
| 		self.add_accounting_dimensions_filters(conditions, values) | ||||
| 		return " and ".join(conditions), values | ||||
| 
 | ||||
| 	def get_cost_center_conditions(self, conditions): | ||||
| 		lft, rgt = frappe.db.get_value("Cost Center", self.filters.cost_center, ["lft", "rgt"]) | ||||
| 		cost_center_list = [center.name for center in frappe.get_list("Cost Center", filters = {'lft': (">=", lft), 'rgt': ("<=", rgt)})] | ||||
| 
 | ||||
| 		cost_center_string = '", "'.join(cost_center_list) | ||||
| 		conditions.append('cost_center in ("{0}")'.format(cost_center_string)) | ||||
| 
 | ||||
| 	def get_order_by_condition(self): | ||||
| 		if self.filters.get('group_by_party'): | ||||
| 			return "order by party, posting_date" | ||||
|  | ||||
| @ -3,6 +3,14 @@ | ||||
| 
 | ||||
| frappe.query_reports["Bank Reconciliation Statement"] = { | ||||
| 	"filters": [ | ||||
| 		{ | ||||
| 			"fieldname":"company", | ||||
| 			"label": __("Company"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"options": "Company", | ||||
| 			"reqd": 1, | ||||
| 			"default": frappe.defaults.get_user_default("Company") | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname":"account", | ||||
| 			"label": __("Bank Account"), | ||||
| @ -12,11 +20,14 @@ frappe.query_reports["Bank Reconciliation Statement"] = { | ||||
| 				locals[":Company"][frappe.defaults.get_user_default("Company")]["default_bank_account"]: "", | ||||
| 			"reqd": 1, | ||||
| 			"get_query": function() { | ||||
| 				var company = frappe.query_report.get_filter_value('company') | ||||
| 				return { | ||||
| 					"query": "erpnext.controllers.queries.get_account_list", | ||||
| 					"filters": [ | ||||
| 						['Account', 'account_type', 'in', 'Bank, Cash'], | ||||
| 						['Account', 'is_group', '=', 0], | ||||
| 						['Account', 'disabled', '=', 0], | ||||
| 						['Account', 'company', '=', company], | ||||
| 					] | ||||
| 				} | ||||
| 			} | ||||
| @ -34,4 +45,4 @@ frappe.query_reports["Bank Reconciliation Statement"] = { | ||||
| 			"fieldtype": "Check" | ||||
| 		}, | ||||
| 	] | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -256,7 +256,7 @@ def accumulate_values_into_parents(accounts, accounts_by_name, companies): | ||||
| 	"""accumulate children's values in parent accounts""" | ||||
| 	for d in reversed(accounts): | ||||
| 		if d.parent_account: | ||||
| 			account = d.parent_account.split('-')[0].strip() | ||||
| 			account = d.parent_account.split(' - ')[0].strip() | ||||
| 			if not accounts_by_name.get(account): | ||||
| 				continue | ||||
| 
 | ||||
|  | ||||
| @ -268,9 +268,9 @@ class GrossProfitGenerator(object): | ||||
| 	def get_last_purchase_rate(self, item_code, row): | ||||
| 		condition = '' | ||||
| 		if row.project: | ||||
| 			condition += " AND a.project='%s'" % (row.project) | ||||
| 			condition += " AND a.project=%s" % (frappe.db.escape(row.project)) | ||||
| 		elif row.cost_center: | ||||
| 			condition += " AND a.cost_center='%s'" % (row.cost_center) | ||||
| 			condition += " AND a.cost_center=%s" % (frappe.db.escape(row.cost_center)) | ||||
| 		if self.filters.to_date: | ||||
| 			condition += " AND modified='%s'" % (self.filters.to_date) | ||||
| 
 | ||||
|  | ||||
							
								
								
									
										76
									
								
								erpnext/accounts/report/pos_register/pos_register.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										76
									
								
								erpnext/accounts/report/pos_register/pos_register.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,76 @@ | ||||
| // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
 | ||||
| // For license information, please see license.txt
 | ||||
| /* eslint-disable */ | ||||
| 
 | ||||
| frappe.query_reports["POS Register"] = { | ||||
| 	"filters": [ | ||||
| 		{ | ||||
| 			"fieldname":"company", | ||||
| 			"label": __("Company"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"options": "Company", | ||||
| 			"default": frappe.defaults.get_user_default("Company"), | ||||
| 			"reqd": 1 | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname":"from_date", | ||||
| 			"label": __("From Date"), | ||||
| 			"fieldtype": "Date", | ||||
| 			"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), | ||||
| 			"reqd": 1, | ||||
| 			"width": "60px" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname":"to_date", | ||||
| 			"label": __("To Date"), | ||||
| 			"fieldtype": "Date", | ||||
| 			"default": frappe.datetime.get_today(), | ||||
| 			"reqd": 1, | ||||
| 			"width": "60px" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname":"pos_profile", | ||||
| 			"label": __("POS Profile"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"options": "POS Profile" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname":"cashier", | ||||
| 			"label": __("Cashier"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"options": "User" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname":"customer", | ||||
| 			"label": __("Customer"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"options": "Customer" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname":"mode_of_payment", | ||||
| 			"label": __("Payment Method"), | ||||
| 			"fieldtype": "Link", | ||||
| 			"options": "Mode of Payment" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname":"group_by", | ||||
| 			"label": __("Group by"), | ||||
| 			"fieldtype": "Select", | ||||
| 			"options": ["", "POS Profile", "Cashier", "Payment Method", "Customer"], | ||||
| 			"default": "POS Profile" | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname":"is_return", | ||||
| 			"label": __("Is Return"), | ||||
| 			"fieldtype": "Check" | ||||
| 		}, | ||||
| 	], | ||||
| 	"formatter": function(value, row, column, data, default_formatter) { | ||||
| 		value = default_formatter(value, row, column, data); | ||||
| 		if (data && data.bold) { | ||||
| 			value = value.bold(); | ||||
| 
 | ||||
| 		} | ||||
| 		return value; | ||||
| 	} | ||||
| }; | ||||
							
								
								
									
										30
									
								
								erpnext/accounts/report/pos_register/pos_register.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								erpnext/accounts/report/pos_register/pos_register.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,30 @@ | ||||
| { | ||||
|  "add_total_row": 0, | ||||
|  "columns": [], | ||||
|  "creation": "2020-09-10 19:25:03.766871", | ||||
|  "disable_prepared_report": 0, | ||||
|  "disabled": 0, | ||||
|  "docstatus": 0, | ||||
|  "doctype": "Report", | ||||
|  "filters": [], | ||||
|  "idx": 0, | ||||
|  "is_standard": "Yes", | ||||
|  "json": "{}", | ||||
|  "modified": "2020-09-10 19:25:15.851331", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Register", | ||||
|  "owner": "Administrator", | ||||
|  "prepared_report": 0, | ||||
|  "ref_doctype": "POS Invoice", | ||||
|  "report_name": "POS Register", | ||||
|  "report_type": "Script Report", | ||||
|  "roles": [ | ||||
|   { | ||||
|    "role": "Accounts Manager" | ||||
|   }, | ||||
|   { | ||||
|    "role": "Accounts User" | ||||
|   } | ||||
|  ] | ||||
| } | ||||
							
								
								
									
										222
									
								
								erpnext/accounts/report/pos_register/pos_register.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										222
									
								
								erpnext/accounts/report/pos_register/pos_register.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,222 @@ | ||||
| # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import _, _dict | ||||
| from erpnext import get_company_currency, get_default_company | ||||
| from erpnext.accounts.report.sales_register.sales_register import get_mode_of_payments | ||||
| 
 | ||||
| def execute(filters=None): | ||||
| 	if not filters: | ||||
| 		return [], [] | ||||
| 	 | ||||
| 	validate_filters(filters) | ||||
| 
 | ||||
| 	columns = get_columns(filters) | ||||
| 
 | ||||
| 	group_by_field = get_group_by_field(filters.get("group_by")) | ||||
| 
 | ||||
| 	pos_entries = get_pos_entries(filters, group_by_field) | ||||
| 	if group_by_field != "mode_of_payment": | ||||
| 		concat_mode_of_payments(pos_entries) | ||||
| 
 | ||||
| 	# return only entries if group by is unselected | ||||
| 	if not group_by_field: | ||||
| 		return columns, pos_entries | ||||
| 
 | ||||
| 	# handle grouping | ||||
| 	invoice_map, grouped_data = {}, [] | ||||
| 	for d in pos_entries: | ||||
| 		invoice_map.setdefault(d[group_by_field], []).append(d) | ||||
| 	 | ||||
| 	for key in invoice_map: | ||||
| 		invoices = invoice_map[key] | ||||
| 		grouped_data += invoices | ||||
| 		add_subtotal_row(grouped_data, invoices, group_by_field, key) | ||||
| 
 | ||||
| 	# move group by column to first position | ||||
| 	column_index = next((index for (index, d) in enumerate(columns) if d["fieldname"] == group_by_field), None) | ||||
| 	columns.insert(0, columns.pop(column_index)) | ||||
| 
 | ||||
| 	return columns, grouped_data | ||||
| 
 | ||||
| def get_pos_entries(filters, group_by_field): | ||||
| 	conditions = get_conditions(filters) | ||||
| 	order_by = "p.posting_date" | ||||
| 	select_mop_field, from_sales_invoice_payment, group_by_mop_condition = "", "", "" | ||||
| 	if group_by_field == "mode_of_payment": | ||||
| 		select_mop_field = ", sip.mode_of_payment" | ||||
| 		from_sales_invoice_payment = ", `tabSales Invoice Payment` sip" | ||||
| 		group_by_mop_condition = "sip.parent = p.name AND ifnull(sip.base_amount, 0) != 0 AND" | ||||
| 		order_by += ", sip.mode_of_payment" | ||||
| 
 | ||||
| 	elif group_by_field: | ||||
| 		order_by += ", p.{}".format(group_by_field) | ||||
| 
 | ||||
| 	return frappe.db.sql( | ||||
| 		""" | ||||
| 		SELECT  | ||||
| 			p.posting_date, p.name as pos_invoice, p.pos_profile, | ||||
| 			p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount, | ||||
| 			p.customer, p.is_return {select_mop_field} | ||||
| 		FROM | ||||
| 			`tabPOS Invoice` p {from_sales_invoice_payment} | ||||
| 		WHERE | ||||
| 			{group_by_mop_condition} | ||||
| 			{conditions} | ||||
| 		ORDER BY | ||||
| 			{order_by} | ||||
| 		""".format( | ||||
| 			select_mop_field=select_mop_field, | ||||
| 			from_sales_invoice_payment=from_sales_invoice_payment, | ||||
| 			group_by_mop_condition=group_by_mop_condition, | ||||
| 			conditions=conditions, | ||||
| 			order_by=order_by | ||||
| 		), filters, as_dict=1) | ||||
| 
 | ||||
| def concat_mode_of_payments(pos_entries): | ||||
| 	mode_of_payments = get_mode_of_payments(set([d.pos_invoice for d in pos_entries])) | ||||
| 	for entry in pos_entries: | ||||
| 		if mode_of_payments.get(entry.pos_invoice): | ||||
| 			entry.mode_of_payment = ", ".join(mode_of_payments.get(entry.pos_invoice, [])) | ||||
| 
 | ||||
| def add_subtotal_row(data, group_invoices, group_by_field, group_by_value): | ||||
| 	grand_total = sum([d.grand_total for d in group_invoices]) | ||||
| 	paid_amount = sum([d.paid_amount for d in group_invoices]) | ||||
| 	data.append({ | ||||
| 		group_by_field: group_by_value, | ||||
| 		"grand_total": grand_total, | ||||
| 		"paid_amount": paid_amount, | ||||
| 		"bold": 1 | ||||
| 	}) | ||||
| 	data.append({}) | ||||
| 
 | ||||
| def validate_filters(filters): | ||||
| 	if not filters.get("company"): | ||||
| 		frappe.throw(_("{0} is mandatory").format(_("Company"))) | ||||
| 	 | ||||
| 	if not filters.get("from_date") and not filters.get("to_date"): | ||||
| 		frappe.throw(_("{0} and {1} are mandatory").format(frappe.bold(_("From Date")), frappe.bold(_("To Date")))) | ||||
| 	 | ||||
| 	if filters.from_date > filters.to_date: | ||||
| 		frappe.throw(_("From Date must be before To Date")) | ||||
| 
 | ||||
| 	if (filters.get("pos_profile") and filters.get("group_by") == _('POS Profile')): | ||||
| 		frappe.throw(_("Can not filter based on POS Profile, if grouped by POS Profile")) | ||||
| 	 | ||||
| 	if (filters.get("customer") and filters.get("group_by") == _('Customer')): | ||||
| 		frappe.throw(_("Can not filter based on Customer, if grouped by Customer")) | ||||
| 	 | ||||
| 	if (filters.get("owner") and filters.get("group_by") == _('Cashier')): | ||||
| 		frappe.throw(_("Can not filter based on Cashier, if grouped by Cashier")) | ||||
| 	 | ||||
| 	if (filters.get("mode_of_payment") and filters.get("group_by") == _('Payment Method')): | ||||
| 		frappe.throw(_("Can not filter based on Payment Method, if grouped by Payment Method")) | ||||
| 
 | ||||
| def get_conditions(filters): | ||||
| 	conditions = "company = %(company)s AND posting_date >= %(from_date)s AND posting_date <= %(to_date)s".format( | ||||
| 		company=filters.get("company"), | ||||
| 		from_date=filters.get("from_date"), | ||||
| 		to_date=filters.get("to_date")) | ||||
| 
 | ||||
| 	if filters.get("pos_profile"): | ||||
| 		conditions += " AND pos_profile = %(pos_profile)s".format(pos_profile=filters.get("pos_profile")) | ||||
| 	 | ||||
| 	if filters.get("owner"): | ||||
| 		conditions += " AND owner = %(owner)s".format(owner=filters.get("owner")) | ||||
| 	 | ||||
| 	if filters.get("customer"): | ||||
| 		conditions += " AND customer = %(customer)s".format(customer=filters.get("customer")) | ||||
| 	 | ||||
| 	if filters.get("is_return"): | ||||
| 		conditions += " AND is_return = %(is_return)s".format(is_return=filters.get("is_return")) | ||||
| 	 | ||||
| 	if filters.get("mode_of_payment"): | ||||
| 		conditions += """ | ||||
| 			AND EXISTS( | ||||
| 					SELECT name FROM `tabSales Invoice Payment` sip | ||||
| 					WHERE parent=p.name AND ifnull(sip.mode_of_payment, '') = %(mode_of_payment)s | ||||
| 				)""" | ||||
| 	 | ||||
| 	return conditions | ||||
| 
 | ||||
| def get_group_by_field(group_by): | ||||
| 	group_by_field = "" | ||||
| 
 | ||||
| 	if group_by == "POS Profile": | ||||
| 		group_by_field = "pos_profile" | ||||
| 	elif group_by == "Cashier": | ||||
| 		group_by_field = "owner" | ||||
| 	elif group_by == "Customer": | ||||
| 		group_by_field = "customer" | ||||
| 	elif group_by == "Payment Method": | ||||
| 		group_by_field = "mode_of_payment" | ||||
| 	 | ||||
| 	return group_by_field | ||||
| 
 | ||||
| def get_columns(filters): | ||||
| 	columns = [ | ||||
| 		{ | ||||
| 			"label": _("Posting Date"), | ||||
| 			"fieldname": "posting_date", | ||||
| 			"fieldtype": "Date", | ||||
| 			"width": 90 | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("POS Invoice"), | ||||
| 			"fieldname": "pos_invoice", | ||||
| 			"fieldtype": "Link", | ||||
| 			"options": "POS Invoice", | ||||
| 			"width": 120 | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Customer"), | ||||
| 			"fieldname": "customer", | ||||
| 			"fieldtype": "Link", | ||||
| 			"options": "Customer", | ||||
| 			"width": 120 | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("POS Profile"), | ||||
| 			"fieldname": "pos_profile", | ||||
| 			"fieldtype": "Link", | ||||
| 			"options": "POS Profile", | ||||
| 			"width": 160 | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Cashier"), | ||||
| 			"fieldname": "owner", | ||||
| 			"fieldtype": "Link", | ||||
| 			"options": "User", | ||||
| 			"width": 140 | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Grand Total"), | ||||
| 			"fieldname": "grand_total", | ||||
| 			"fieldtype": "Currency", | ||||
| 			"options": "company:currency", | ||||
| 			"width": 120 | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Paid Amount"), | ||||
| 			"fieldname": "paid_amount", | ||||
| 			"fieldtype": "Currency", | ||||
| 			"options": "company:currency", | ||||
| 			"width": 120 | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Payment Method"), | ||||
| 			"fieldname": "mode_of_payment", | ||||
| 			"fieldtype": "Data", | ||||
| 			"width": 150 | ||||
| 		}, | ||||
| 		{ | ||||
| 			"label": _("Is Return"), | ||||
| 			"fieldname": "is_return", | ||||
| 			"fieldtype": "Data", | ||||
| 			"width": 80 | ||||
| 		}, | ||||
| 	] | ||||
| 
 | ||||
| 	return columns | ||||
| @ -32,12 +32,12 @@ def execute(filters=None): | ||||
| 
 | ||||
| 	chart = get_chart_data(filters, columns, income, expense, net_profit_loss) | ||||
| 
 | ||||
| 	default_currency = frappe.get_cached_value('Company', filters.company, "default_currency") | ||||
| 	report_summary = get_report_summary(period_list, filters.periodicity, income, expense, net_profit_loss, default_currency) | ||||
| 	currency = filters.presentation_currency or frappe.get_cached_value('Company', filters.company, "default_currency") | ||||
| 	report_summary = get_report_summary(period_list, filters.periodicity, income, expense, net_profit_loss, currency) | ||||
| 
 | ||||
| 	return columns, data, None, chart, report_summary | ||||
| 
 | ||||
| def get_report_summary(period_list, periodicity, income, expense, net_profit_loss, default_currency, consolidated=False): | ||||
| def get_report_summary(period_list, periodicity, income, expense, net_profit_loss, currency, consolidated=False): | ||||
| 	net_income, net_expense, net_profit = 0.0, 0.0, 0.0 | ||||
| 
 | ||||
| 	for period in period_list: | ||||
| @ -64,19 +64,19 @@ def get_report_summary(period_list, periodicity, income, expense, net_profit_los | ||||
| 			"indicator": "Green" if net_profit > 0 else "Red", | ||||
| 			"label": profit_label, | ||||
| 			"datatype": "Currency", | ||||
| 			"currency": net_profit_loss.get("currency") if net_profit_loss else default_currency | ||||
| 			"currency": currency | ||||
| 		}, | ||||
| 		{ | ||||
| 			"value": net_income, | ||||
| 			"label": income_label, | ||||
| 			"datatype": "Currency", | ||||
| 			"currency": income[-1].get('currency') if income else default_currency | ||||
| 			"currency": currency | ||||
| 		}, | ||||
| 		{ | ||||
| 			"value": net_expense, | ||||
| 			"label": expense_label, | ||||
| 			"datatype": "Currency", | ||||
| 			"currency": expense[-1].get('currency') if expense else default_currency | ||||
| 			"currency": currency | ||||
| 		} | ||||
| 	] | ||||
| 
 | ||||
| @ -143,4 +143,4 @@ def get_chart_data(filters, columns, income, expense, net_profit_loss): | ||||
| 
 | ||||
| 	chart["fieldtype"] = "Currency" | ||||
| 
 | ||||
| 	return chart | ||||
| 	return chart | ||||
|  | ||||
| @ -72,6 +72,12 @@ frappe.require("assets/erpnext/js/financial_statements.js", function() { | ||||
| 				"fieldtype": "Link", | ||||
| 				"options": "Finance Book", | ||||
| 			}, | ||||
| 			{ | ||||
| 				"fieldname": "presentation_currency", | ||||
| 				"label": __("Currency"), | ||||
| 				"fieldtype": "Select", | ||||
| 				"options": erpnext.get_presentation_currency_list() | ||||
| 			}, | ||||
| 			{ | ||||
| 				"fieldname": "with_period_closing_entry", | ||||
| 				"label": __("Period Closing Entry"), | ||||
|  | ||||
| @ -56,7 +56,7 @@ def get_data(filters): | ||||
| 	accounts = frappe.db.sql("""select name, account_number, parent_account, account_name, root_type, report_type, lft, rgt | ||||
| 
 | ||||
| 		from `tabAccount` where company=%s order by lft""", filters.company, as_dict=True) | ||||
| 	company_currency = erpnext.get_company_currency(filters.company) | ||||
| 	company_currency = filters.presentation_currency or erpnext.get_company_currency(filters.company) | ||||
| 
 | ||||
| 	if not accounts: | ||||
| 		return None | ||||
|  | ||||
| @ -683,6 +683,7 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters | ||||
| 		where | ||||
| 			party_type = %(party_type)s and party = %(party)s | ||||
| 			and account = %(account)s and {dr_or_cr} > 0 | ||||
| 			and is_cancelled=0 | ||||
| 			{condition} | ||||
| 			and ((voucher_type = 'Journal Entry' | ||||
| 					and (against_voucher = '' or against_voucher is null)) | ||||
| @ -705,6 +706,7 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters | ||||
| 			and account = %(account)s | ||||
| 			and {payment_dr_or_cr} > 0 | ||||
| 			and against_voucher is not null and against_voucher != '' | ||||
| 			and is_cancelled=0 | ||||
| 		group by against_voucher_type, against_voucher | ||||
| 	""".format(payment_dr_or_cr=payment_dr_or_cr), { | ||||
| 		"party_type": party_type, | ||||
|  | ||||
| @ -252,13 +252,6 @@ frappe.ui.form.on('Asset', { | ||||
| 		}) | ||||
| 	}, | ||||
| 
 | ||||
| 	available_for_use_date: function(frm) { | ||||
| 		$.each(frm.doc.finance_books || [], function(i, d) { | ||||
| 			if(!d.depreciation_start_date) d.depreciation_start_date = frm.doc.available_for_use_date; | ||||
| 		}); | ||||
| 		refresh_field("finance_books"); | ||||
| 	}, | ||||
| 
 | ||||
| 	is_existing_asset: function(frm) { | ||||
| 		frm.trigger("toggle_reference_doc"); | ||||
| 		// frm.toggle_reqd("next_depreciation_date", (!frm.doc.is_existing_asset && frm.doc.calculate_depreciation));
 | ||||
| @ -438,6 +431,15 @@ frappe.ui.form.on('Asset Finance Book', { | ||||
| 		} | ||||
| 
 | ||||
| 		frappe.flags.dont_change_rate = false; | ||||
| 	}, | ||||
| 
 | ||||
| 	depreciation_start_date: function(frm, cdt, cdn) { | ||||
| 		const book = locals[cdt][cdn]; | ||||
| 		if (frm.doc.available_for_use_date && book.depreciation_start_date == frm.doc.available_for_use_date) { | ||||
| 			frappe.msgprint(__(`Depreciation Posting Date should not be equal to Available for Use Date.`)); | ||||
| 			book.depreciation_start_date = ""; | ||||
| 			frm.refresh_field("finance_books"); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -6,7 +6,7 @@ from __future__ import unicode_literals | ||||
| import frappe, erpnext, math, json | ||||
| from frappe import _ | ||||
| from six import string_types | ||||
| from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days | ||||
| from frappe.utils import flt, add_months, cint, nowdate, getdate, today, date_diff, month_diff, add_days, get_last_day, get_datetime | ||||
| from frappe.model.document import Document | ||||
| from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account | ||||
| from erpnext.assets.doctype.asset.depreciation \ | ||||
| @ -83,6 +83,11 @@ class Asset(AccountsController): | ||||
| 		if not self.available_for_use_date: | ||||
| 			frappe.throw(_("Available for use date is required")) | ||||
| 
 | ||||
| 		for d in self.finance_books: | ||||
| 			if d.depreciation_start_date == self.available_for_use_date: | ||||
| 				frappe.throw(_("Row #{}: Depreciation Posting Date should not be equal to Available for Use Date.").format(d.idx), | ||||
| 					title=_("Incorrect Date")) | ||||
| 
 | ||||
| 	def set_missing_values(self): | ||||
| 		if not self.asset_category: | ||||
| 			self.asset_category = frappe.get_cached_value("Item", self.item_code, "asset_category") | ||||
| @ -126,7 +131,7 @@ class Asset(AccountsController): | ||||
| 
 | ||||
| 	def validate_gross_and_purchase_amount(self): | ||||
| 		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.") | ||||
| @ -135,6 +140,10 @@ class Asset(AccountsController): | ||||
| 	def make_asset_movement(self): | ||||
| 		reference_doctype = 'Purchase Receipt' if self.purchase_receipt else 'Purchase Invoice' | ||||
| 		reference_docname = self.purchase_receipt or self.purchase_invoice | ||||
| 		transaction_date = getdate(self.purchase_date) | ||||
| 		if reference_docname: | ||||
| 			posting_date, posting_time = frappe.db.get_value(reference_doctype, reference_docname, ["posting_date", "posting_time"]) | ||||
| 			transaction_date = get_datetime("{} {}".format(posting_date, posting_time)) | ||||
| 		assets = [{ | ||||
| 			'asset': self.name, | ||||
| 			'asset_name': self.asset_name, | ||||
| @ -146,7 +155,7 @@ class Asset(AccountsController): | ||||
| 			'assets': assets, | ||||
| 			'purpose': 'Receipt', | ||||
| 			'company': self.company, | ||||
| 			'transaction_date': getdate(nowdate()), | ||||
| 			'transaction_date': transaction_date, | ||||
| 			'reference_doctype': reference_doctype, | ||||
| 			'reference_name': reference_docname | ||||
| 		}).insert() | ||||
| @ -294,7 +303,7 @@ class Asset(AccountsController): | ||||
| 		if not row.depreciation_start_date: | ||||
| 			if not self.available_for_use_date: | ||||
| 				frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) | ||||
| 			row.depreciation_start_date = self.available_for_use_date | ||||
| 			row.depreciation_start_date = get_last_day(self.available_for_use_date) | ||||
| 
 | ||||
| 		if not self.is_existing_asset: | ||||
| 			self.opening_accumulated_depreciation = 0 | ||||
| @ -457,50 +466,63 @@ class Asset(AccountsController): | ||||
| 
 | ||||
| 	def validate_make_gl_entry(self): | ||||
| 		purchase_document = self.get_purchase_document() | ||||
| 		asset_bought_with_invoice = purchase_document == self.purchase_invoice | ||||
| 		fixed_asset_account, cwip_account = self.get_asset_accounts() | ||||
| 		cwip_enabled = is_cwip_accounting_enabled(self.asset_category) | ||||
| 		# check if expense already has been booked in case of cwip was enabled after purchasing asset | ||||
| 		expense_booked = False | ||||
| 		cwip_booked = False | ||||
| 
 | ||||
| 		if asset_bought_with_invoice: | ||||
| 			expense_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", | ||||
| 				(purchase_document, fixed_asset_account), as_dict=1) | ||||
| 		else: | ||||
| 			cwip_booked = frappe.db.sql("""SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""", | ||||
| 				(purchase_document, cwip_account), as_dict=1) | ||||
| 
 | ||||
| 		if cwip_enabled and (expense_booked or not cwip_booked): | ||||
| 			# if expense has already booked from invoice or cwip is booked from receipt | ||||
| 		if not purchase_document: | ||||
| 			return False | ||||
| 		elif not cwip_enabled and (not expense_booked or cwip_booked): | ||||
| 			# if cwip is disabled but expense hasn't been booked yet | ||||
| 			return True | ||||
| 		elif cwip_enabled: | ||||
| 			# default condition | ||||
| 			return True | ||||
| 
 | ||||
| 		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) | ||||
| 
 | ||||
| 		query = """SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s""" | ||||
| 		if asset_bought_with_invoice: | ||||
| 			# with invoice purchase either expense or cwip has been booked | ||||
| 			expense_booked = frappe.db.sql(query, (purchase_document, fixed_asset_account), as_dict=1) | ||||
| 			if expense_booked: | ||||
| 				# if expense is already booked from invoice then do not make gl entries regardless of cwip enabled/disabled | ||||
| 				return False | ||||
| 
 | ||||
| 			cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1) | ||||
| 			if cwip_booked: | ||||
| 				# if cwip is booked from invoice then make gl entries regardless of cwip enabled/disabled | ||||
| 				return True | ||||
| 		else: | ||||
| 			# with receipt purchase either cwip has been booked or no entries have been made | ||||
| 			if not cwip_account: | ||||
| 				# if cwip account isn't available do not make gl entries | ||||
| 				return False | ||||
| 
 | ||||
| 			cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1) | ||||
| 			# if cwip is not booked from receipt then do not make gl entries | ||||
| 			# if cwip is booked from receipt then make gl entries | ||||
| 			return cwip_booked | ||||
| 
 | ||||
| 	def get_purchase_document(self): | ||||
| 		asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value('Purchase Invoice', self.purchase_invoice, 'update_stock') | ||||
| 		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: | ||||
| 			cwip_account = get_asset_account("capital_work_in_progress_account", self.name, self.asset_category, self.company) | ||||
| 		except: | ||||
| 			# if no cwip account found in category or company and "cwip is enabled" then raise else silently pass | ||||
| 			if cwip_enabled: | ||||
| 				raise | ||||
| 
 | ||||
| 	def get_asset_accounts(self): | ||||
| 		fixed_asset_account = get_asset_category_account('fixed_asset_account', asset=self.name, | ||||
| 					asset_category = self.asset_category, company = self.company) | ||||
| 
 | ||||
| 		cwip_account = get_asset_account("capital_work_in_progress_account", | ||||
| 			self.name, self.asset_category, self.company) | ||||
| 
 | ||||
| 		return fixed_asset_account, cwip_account | ||||
| 		return cwip_account | ||||
| 
 | ||||
| 	def make_gl_entries(self): | ||||
| 		gl_entries = [] | ||||
| 
 | ||||
| 		purchase_document = self.get_purchase_document() | ||||
| 		fixed_asset_account, cwip_account = self.get_asset_accounts() | ||||
| 		fixed_asset_account, cwip_account = self.get_fixed_asset_account(), self.get_cwip_account() | ||||
| 
 | ||||
| 		if (purchase_document and self.purchase_receipt_amount and self.available_for_use_date <= nowdate()): | ||||
| 
 | ||||
| @ -552,14 +574,18 @@ class Asset(AccountsController): | ||||
| 			return 100 * (1 - flt(depreciation_rate, float_precision)) | ||||
| 
 | ||||
| def update_maintenance_status(): | ||||
| 	assets = frappe.get_all('Asset', filters = {'docstatus': 1, 'maintenance_required': 1}) | ||||
| 	assets = frappe.get_all( | ||||
| 		"Asset", filters={"docstatus": 1, "maintenance_required": 1} | ||||
| 	) | ||||
| 
 | ||||
| 	for asset in assets: | ||||
| 		asset = frappe.get_doc("Asset", asset.name) | ||||
| 		if frappe.db.exists('Asset Maintenance Task', {'parent': asset.name, 'next_due_date': today()}): | ||||
| 			asset.set_status('In Maintenance') | ||||
| 		if frappe.db.exists('Asset Repair', {'asset_name': asset.name, 'repair_status': 'Pending'}): | ||||
| 			asset.set_status('Out of Order') | ||||
| 		if frappe.db.exists("Asset Repair", {"asset_name": asset.name, "repair_status": "Pending"}): | ||||
| 			asset.set_status("Out of Order") | ||||
| 		elif frappe.db.exists("Asset Maintenance Task", {"parent": asset.name, "next_due_date": today()}): | ||||
| 			asset.set_status("In Maintenance") | ||||
| 		else: | ||||
| 			asset.set_status() | ||||
| 
 | ||||
| def make_post_gl_entry(): | ||||
| 
 | ||||
|  | ||||
| @ -9,6 +9,7 @@ from frappe.utils import cstr, nowdate, getdate, flt, get_last_day, add_days, ad | ||||
| from erpnext.assets.doctype.asset.depreciation import post_depreciation_entries, scrap_asset, restore_asset | ||||
| from erpnext.assets.doctype.asset.asset import make_sales_invoice | ||||
| from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt | ||||
| from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice | ||||
| from erpnext.stock.doctype.purchase_receipt.purchase_receipt import make_purchase_invoice as make_invoice | ||||
| 
 | ||||
| class TestAsset(unittest.TestCase): | ||||
| @ -371,19 +372,18 @@ class TestAsset(unittest.TestCase): | ||||
| 		asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') | ||||
| 		asset = frappe.get_doc('Asset', asset_name) | ||||
| 		asset.calculate_depreciation = 1 | ||||
| 		asset.available_for_use_date = nowdate() | ||||
| 		asset.purchase_date = nowdate() | ||||
| 		asset.available_for_use_date = '2020-01-01' | ||||
| 		asset.purchase_date = '2020-01-01' | ||||
| 		asset.append("finance_books", { | ||||
| 			"expected_value_after_useful_life": 10000, | ||||
| 			"depreciation_method": "Straight Line", | ||||
| 			"total_number_of_depreciations": 3, | ||||
| 			"frequency_of_depreciation": 10, | ||||
| 			"depreciation_start_date": nowdate() | ||||
| 			"total_number_of_depreciations": 10, | ||||
| 			"frequency_of_depreciation": 1 | ||||
| 		}) | ||||
| 		asset.insert() | ||||
| 		asset.submit() | ||||
| 
 | ||||
| 		post_depreciation_entries(date=add_months(nowdate(), 10)) | ||||
| 		post_depreciation_entries(date=add_months('2020-01-01', 4)) | ||||
| 
 | ||||
| 		scrap_asset(asset.name) | ||||
| 
 | ||||
| @ -392,9 +392,9 @@ class TestAsset(unittest.TestCase): | ||||
| 		self.assertTrue(asset.journal_entry_for_scrap) | ||||
| 
 | ||||
| 		expected_gle = ( | ||||
| 			("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), | ||||
| 			("_Test Accumulated Depreciations - _TC", 36000.0, 0.0), | ||||
| 			("_Test Fixed Asset - _TC", 0.0, 100000.0), | ||||
| 			("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) | ||||
| 			("_Test Gain/Loss on Asset Disposal - _TC", 64000.0, 0.0) | ||||
| 		) | ||||
| 
 | ||||
| 		gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` | ||||
| @ -466,8 +466,7 @@ class TestAsset(unittest.TestCase): | ||||
| 			"expected_value_after_useful_life": 10000, | ||||
| 			"depreciation_method": "Straight Line", | ||||
| 			"total_number_of_depreciations": 3, | ||||
| 			"frequency_of_depreciation": 10, | ||||
| 			"depreciation_start_date": "2020-06-06" | ||||
| 			"frequency_of_depreciation": 10 | ||||
| 		}) | ||||
| 		asset.insert() | ||||
| 		accumulated_depreciation_after_full_schedule = \ | ||||
| @ -560,81 +559,6 @@ class TestAsset(unittest.TestCase): | ||||
| 
 | ||||
| 		self.assertEqual(gle, expected_gle) | ||||
| 
 | ||||
| 	def test_gle_with_cwip_toggling(self): | ||||
| 		# TEST: purchase an asset with cwip enabled and then disable cwip and try submitting the asset | ||||
| 		frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) | ||||
| 
 | ||||
| 		pr = make_purchase_receipt(item_code="Macbook Pro", | ||||
| 			qty=1, rate=5000, do_not_submit=True, location="Test Location") | ||||
| 		pr.set('taxes', [{ | ||||
| 			'category': 'Total', | ||||
| 			'add_deduct_tax': 'Add', | ||||
| 			'charge_type': 'On Net Total', | ||||
| 			'account_head': '_Test Account Service Tax - _TC', | ||||
| 			'description': '_Test Account Service Tax', | ||||
| 			'cost_center': 'Main - _TC', | ||||
| 			'rate': 5.0 | ||||
| 		}, { | ||||
| 			'category': 'Valuation and Total', | ||||
| 			'add_deduct_tax': 'Add', | ||||
| 			'charge_type': 'On Net Total', | ||||
| 			'account_head': '_Test Account Shipping Charges - _TC', | ||||
| 			'description': '_Test Account Shipping Charges', | ||||
| 			'cost_center': 'Main - _TC', | ||||
| 			'rate': 5.0 | ||||
| 		}]) | ||||
| 		pr.submit() | ||||
| 		expected_gle = ( | ||||
| 			("Asset Received But Not Billed - _TC", 0.0, 5250.0), | ||||
| 			("CWIP Account - _TC", 5250.0, 0.0) | ||||
| 		) | ||||
| 		pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` | ||||
| 			where voucher_type='Purchase Receipt' and voucher_no = %s | ||||
| 			order by account""", pr.name) | ||||
| 		self.assertEqual(pr_gle, expected_gle) | ||||
| 
 | ||||
| 		pi = make_invoice(pr.name) | ||||
| 		pi.submit() | ||||
| 		expected_gle = ( | ||||
| 			("_Test Account Service Tax - _TC", 250.0, 0.0), | ||||
| 			("_Test Account Shipping Charges - _TC", 250.0, 0.0), | ||||
| 			("Asset Received But Not Billed - _TC", 5250.0, 0.0), | ||||
| 			("Creditors - _TC", 0.0, 5500.0), | ||||
| 			("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), | ||||
| 		) | ||||
| 		pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` | ||||
| 			where voucher_type='Purchase Invoice' and voucher_no = %s | ||||
| 			order by account""", pi.name) | ||||
| 		self.assertEqual(pi_gle, expected_gle) | ||||
| 
 | ||||
| 		asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') | ||||
| 		asset_doc = frappe.get_doc('Asset', asset) | ||||
| 		month_end_date = get_last_day(nowdate()) | ||||
| 		asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) | ||||
| 		self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) | ||||
| 		asset_doc.append("finance_books", { | ||||
| 			"expected_value_after_useful_life": 200, | ||||
| 			"depreciation_method": "Straight Line", | ||||
| 			"total_number_of_depreciations": 3, | ||||
| 			"frequency_of_depreciation": 10, | ||||
| 			"depreciation_start_date": month_end_date | ||||
| 		}) | ||||
| 
 | ||||
| 		# disable cwip and try submitting | ||||
| 		frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) | ||||
| 		asset_doc.submit() | ||||
| 		# asset should have gl entries even if cwip is disabled | ||||
| 		expected_gle = ( | ||||
| 			("_Test Fixed Asset - _TC", 5250.0, 0.0), | ||||
| 			("CWIP Account - _TC", 0.0, 5250.0) | ||||
| 		) | ||||
| 		gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` | ||||
| 			where voucher_type='Asset' and voucher_no = %s | ||||
| 			order by account""", asset_doc.name) | ||||
| 		self.assertEqual(gle, expected_gle) | ||||
| 
 | ||||
| 		frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) | ||||
| 
 | ||||
| 	def test_expense_head(self): | ||||
| 		pr = make_purchase_receipt(item_code="Macbook Pro", | ||||
| 			qty=2, rate=200000.0, location="Test Location") | ||||
| @ -642,6 +566,74 @@ class TestAsset(unittest.TestCase): | ||||
| 		doc = make_invoice(pr.name) | ||||
| 
 | ||||
| 		self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) | ||||
| 	 | ||||
| 	def test_asset_cwip_toggling_cases(self): | ||||
| 		cwip = frappe.db.get_value("Asset Category", "Computers", "enable_cwip_accounting") | ||||
| 		name = frappe.db.get_value("Asset Category Account", filters={"parent": "Computers"}, fieldname=["name"]) | ||||
| 		cwip_acc = "CWIP Account - _TC" | ||||
| 
 | ||||
| 		frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) | ||||
| 		frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", "") | ||||
| 		frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", "") | ||||
| 
 | ||||
| 		# case 0 -- PI with cwip disable, Asset with cwip disabled, No cwip account set | ||||
| 		pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) | ||||
| 		asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') | ||||
| 		asset_doc = frappe.get_doc('Asset', asset) | ||||
| 		asset_doc.available_for_use_date = nowdate() | ||||
| 		asset_doc.calculate_depreciation = 0 | ||||
| 		asset_doc.submit() | ||||
| 		gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) | ||||
| 		self.assertFalse(gle) | ||||
| 
 | ||||
| 		# case 1 -- PR with cwip disabled, Asset with cwip enabled | ||||
| 		pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location") | ||||
| 		frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) | ||||
| 		frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) | ||||
| 		asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') | ||||
| 		asset_doc = frappe.get_doc('Asset', asset) | ||||
| 		asset_doc.available_for_use_date = nowdate() | ||||
| 		asset_doc.calculate_depreciation = 0 | ||||
| 		asset_doc.submit() | ||||
| 		gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) | ||||
| 		self.assertFalse(gle) | ||||
| 
 | ||||
| 		# case 2 -- PR with cwip enabled, Asset with cwip disabled | ||||
| 		pr = make_purchase_receipt(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location") | ||||
| 		frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) | ||||
| 		asset = frappe.db.get_value('Asset', {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') | ||||
| 		asset_doc = frappe.get_doc('Asset', asset) | ||||
| 		asset_doc.available_for_use_date = nowdate() | ||||
| 		asset_doc.calculate_depreciation = 0 | ||||
| 		asset_doc.submit() | ||||
| 		gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) | ||||
| 		self.assertTrue(gle) | ||||
| 
 | ||||
| 		# case 3 -- PI with cwip disabled, Asset with cwip enabled | ||||
| 		pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) | ||||
| 		frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 1) | ||||
| 		asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') | ||||
| 		asset_doc = frappe.get_doc('Asset', asset) | ||||
| 		asset_doc.available_for_use_date = nowdate() | ||||
| 		asset_doc.calculate_depreciation = 0 | ||||
| 		asset_doc.submit() | ||||
| 		gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) | ||||
| 		self.assertFalse(gle) | ||||
| 
 | ||||
| 		# case 4 -- PI with cwip enabled, Asset with cwip disabled | ||||
| 		pi = make_purchase_invoice(item_code="Macbook Pro", qty=1, rate=200000.0, location="Test Location", update_stock=1) | ||||
| 		frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", 0) | ||||
| 		asset = frappe.db.get_value('Asset', {'purchase_invoice': pi.name, 'docstatus': 0}, 'name') | ||||
| 		asset_doc = frappe.get_doc('Asset', asset) | ||||
| 		asset_doc.available_for_use_date = nowdate() | ||||
| 		asset_doc.calculate_depreciation = 0 | ||||
| 		asset_doc.submit() | ||||
| 		gle = frappe.db.sql("""select name from `tabGL Entry` where voucher_type='Asset' and voucher_no = %s""", asset_doc.name) | ||||
| 		self.assertTrue(gle) | ||||
| 
 | ||||
| 		frappe.db.set_value("Asset Category", "Computers", "enable_cwip_accounting", cwip) | ||||
| 		frappe.db.set_value("Asset Category Account", name, "capital_work_in_progress_account", cwip_acc) | ||||
| 		frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account", cwip_acc) | ||||
| 
 | ||||
| def create_asset_data(): | ||||
| 	if not frappe.db.exists("Asset Category", "Computers"): | ||||
|  | ||||
| @ -5,7 +5,7 @@ | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.utils import cint | ||||
| from frappe.utils import cint, get_link_to_form | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class AssetCategory(Document): | ||||
| @ -13,6 +13,7 @@ class AssetCategory(Document): | ||||
| 		self.validate_finance_books() | ||||
| 		self.validate_account_types() | ||||
| 		self.validate_account_currency() | ||||
| 		self.valide_cwip_account() | ||||
| 
 | ||||
| 	def validate_finance_books(self): | ||||
| 		for d in self.finance_books: | ||||
| @ -58,6 +59,21 @@ class AssetCategory(Document): | ||||
| 						frappe.throw(_("Row #{}: {} of {} should be {}. Please modify the account or select a different account.") | ||||
| 							.format(d.idx, frappe.unscrub(key_to_match), frappe.bold(selected_account), frappe.bold(expected_key_type)), | ||||
| 							title=_("Invalid Account")) | ||||
| 	 | ||||
| 	def valide_cwip_account(self): | ||||
| 		if self.enable_cwip_accounting: | ||||
| 			missing_cwip_accounts_for_company = [] | ||||
| 			for d in self.accounts: | ||||
| 				if (not d.capital_work_in_progress_account and  | ||||
| 					not frappe.db.get_value("Company", d.company_name, "capital_work_in_progress_account")): | ||||
| 					missing_cwip_accounts_for_company.append(get_link_to_form("Company", d.company_name)) | ||||
| 
 | ||||
| 			if missing_cwip_accounts_for_company: | ||||
| 				msg = _("""To enable Capital Work in Progress Accounting, """) | ||||
| 				msg += _("""you must select Capital Work in Progress Account in accounts table""") | ||||
| 				msg += "<br><br>" | ||||
| 				msg += _("You can also set default CWIP account in Company {}").format(", ".join(missing_cwip_accounts_for_company)) | ||||
| 				frappe.throw(msg, title=_("Missing Account")) | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
|  | ||||
| @ -26,4 +26,22 @@ class TestAssetCategory(unittest.TestCase): | ||||
| 			asset_category.insert() | ||||
| 		except frappe.DuplicateEntryError: | ||||
| 			pass | ||||
| 			 | ||||
| 
 | ||||
| 	def test_cwip_accounting(self): | ||||
| 		company_cwip_acc = frappe.db.get_value("Company", "_Test Company", "capital_work_in_progress_account") | ||||
| 		frappe.db.set_value("Company", "_Test Company", "capital_work_in_progress_account", "") | ||||
| 
 | ||||
| 		asset_category = frappe.new_doc("Asset Category") | ||||
| 		asset_category.asset_category_name = "Computers" | ||||
| 		asset_category.enable_cwip_accounting = 1 | ||||
| 
 | ||||
| 		asset_category.total_number_of_depreciations = 3 | ||||
| 		asset_category.frequency_of_depreciation = 3 | ||||
| 		asset_category.append("accounts", { | ||||
| 			"company_name": "_Test Company", | ||||
| 			"fixed_asset_account": "_Test Fixed Asset - _TC", | ||||
| 			"accumulated_depreciation_account": "_Test Accumulated Depreciations - _TC", | ||||
| 			"depreciation_expense_account": "_Test Depreciations - _TC" | ||||
| 		}) | ||||
| 
 | ||||
| 		self.assertRaises(frappe.ValidationError, asset_category.insert) | ||||
| @ -1,347 +1,99 @@ | ||||
| { | ||||
|  "allow_copy": 0,  | ||||
|  "allow_events_in_timeline": 0,  | ||||
|  "allow_guest_to_view": 0,  | ||||
|  "allow_import": 0,  | ||||
|  "allow_rename": 0,  | ||||
|  "beta": 0,  | ||||
|  "creation": "2018-05-08 14:44:37.095570",  | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "",  | ||||
|  "editable_grid": 1,  | ||||
|  "engine": "InnoDB",  | ||||
|  "actions": [], | ||||
|  "creation": "2018-05-08 14:44:37.095570", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "finance_book", | ||||
|   "depreciation_method", | ||||
|   "total_number_of_depreciations", | ||||
|   "column_break_5", | ||||
|   "frequency_of_depreciation", | ||||
|   "depreciation_start_date", | ||||
|   "expected_value_after_useful_life", | ||||
|   "value_after_depreciation", | ||||
|   "rate_of_depreciation" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "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": "finance_book",  | ||||
|    "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": "Finance Book",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Finance Book",  | ||||
|    "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": "finance_book", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Finance Book", | ||||
|    "options": "Finance Book" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "depreciation_method",  | ||||
|    "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": "Depreciation Method",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual",  | ||||
|    "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": "depreciation_method", | ||||
|    "fieldtype": "Select", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Depreciation Method", | ||||
|    "options": "\nStraight Line\nDouble Declining Balance\nWritten Down Value\nManual", | ||||
|    "reqd": 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": "total_number_of_depreciations",  | ||||
|    "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": "Total Number of Depreciations",  | ||||
|    "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": "total_number_of_depreciations", | ||||
|    "fieldtype": "Int", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Total Number of Depreciations", | ||||
|    "reqd": 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_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,  | ||||
|    "label": "",  | ||||
|    "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_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "frequency_of_depreciation",  | ||||
|    "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": "Frequency of Depreciation (Months)",  | ||||
|    "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": "frequency_of_depreciation", | ||||
|    "fieldtype": "Int", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Frequency of Depreciation (Months)", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "depends_on": "eval:parent.doctype == 'Asset'",  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "depreciation_start_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": "Depreciation Start 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 | ||||
|   },  | ||||
|    "depends_on": "eval:parent.doctype == 'Asset'", | ||||
|    "fieldname": "depreciation_start_date", | ||||
|    "fieldtype": "Date", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Depreciation Posting Date", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "default": "0",  | ||||
|    "depends_on": "eval:parent.doctype == 'Asset'",  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "expected_value_after_useful_life",  | ||||
|    "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": "Expected Value After Useful Life",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Company:company:default_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 | ||||
|   },  | ||||
|    "default": "0", | ||||
|    "depends_on": "eval:parent.doctype == 'Asset'", | ||||
|    "fieldname": "expected_value_after_useful_life", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Expected Value After Useful Life", | ||||
|    "options": "Company:company:default_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": "value_after_depreciation",  | ||||
|    "fieldtype": "Currency",  | ||||
|    "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": "Value After Depreciation",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 1,  | ||||
|    "options": "Company:company:default_currency",  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 1,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 1,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "value_after_depreciation", | ||||
|    "fieldtype": "Currency", | ||||
|    "hidden": 1, | ||||
|    "label": "Value After Depreciation", | ||||
|    "no_copy": 1, | ||||
|    "options": "Company:company:default_currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "depends_on": "eval:doc.depreciation_method == 'Written Down Value'",  | ||||
|    "description": "In Percentage",  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "rate_of_depreciation",  | ||||
|    "fieldtype": "Percent",  | ||||
|    "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": "Rate of Depreciation",  | ||||
|    "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 | ||||
|    "depends_on": "eval:doc.depreciation_method == 'Written Down Value'", | ||||
|    "description": "In Percentage", | ||||
|    "fieldname": "rate_of_depreciation", | ||||
|    "fieldtype": "Percent", | ||||
|    "label": "Rate of Depreciation" | ||||
|   } | ||||
|  ],  | ||||
|  "has_web_view": 0,  | ||||
|  "hide_toolbar": 0,  | ||||
|  "idx": 0,  | ||||
|  "in_create": 0,  | ||||
|  "is_submittable": 0,  | ||||
|  "issingle": 0,  | ||||
|  "istable": 1,  | ||||
|  "max_attachments": 0,  | ||||
|  "modified": "2019-04-09 19:45:14.523488",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Assets",  | ||||
|  "name": "Asset Finance Book",  | ||||
|  "name_case": "",  | ||||
|  "owner": "Administrator",  | ||||
|  "permissions": [],  | ||||
|  "quick_entry": 1,  | ||||
|  "read_only": 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": "2020-09-16 12:11:30.631788", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Assets", | ||||
|  "name": "Asset Finance Book", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -32,8 +32,7 @@ class TestAssetMovement(unittest.TestCase): | ||||
| 			"next_depreciation_date": "2020-12-31", | ||||
| 			"depreciation_method": "Straight Line", | ||||
| 			"total_number_of_depreciations": 3, | ||||
| 			"frequency_of_depreciation": 10, | ||||
| 			"depreciation_start_date": "2020-06-06" | ||||
| 			"frequency_of_depreciation": 10 | ||||
| 		}) | ||||
| 
 | ||||
| 		if asset.docstatus == 0: | ||||
| @ -82,8 +81,7 @@ class TestAssetMovement(unittest.TestCase): | ||||
| 			"next_depreciation_date": "2020-12-31", | ||||
| 			"depreciation_method": "Straight Line", | ||||
| 			"total_number_of_depreciations": 3, | ||||
| 			"frequency_of_depreciation": 10, | ||||
| 			"depreciation_start_date": "2020-06-06" | ||||
| 			"frequency_of_depreciation": 10 | ||||
| 		}) | ||||
| 		if asset.docstatus == 0: | ||||
| 			asset.submit() | ||||
|  | ||||
| @ -33,7 +33,7 @@ | ||||
|   { | ||||
|    "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\": \"Quoted Item Comparison\",\n        \"name\": \"Quoted Item 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]" | ||||
|    "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, | ||||
| @ -60,7 +60,7 @@ | ||||
|  "idx": 0, | ||||
|  "is_standard": 1, | ||||
|  "label": "Buying", | ||||
|  "modified": "2020-06-29 19:30:24.983050", | ||||
|  "modified": "2020-09-30 14:40:55.638458", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Buying", | ||||
|  | ||||
| @ -1084,7 +1084,7 @@ | ||||
|  "idx": 105, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-07-31 14:13:44.610190", | ||||
|  "modified": "2020-10-07 14:31:57.661221", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Purchase Order", | ||||
| @ -1130,10 +1130,11 @@ | ||||
|    "write": 1 | ||||
|   } | ||||
|  ], | ||||
|  "search_fields": "status, transaction_date, supplier,grand_total", | ||||
|  "search_fields": "status, transaction_date, supplier, grand_total", | ||||
|  "show_name_in_global_search": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "timeline_field": "supplier", | ||||
|  "title_field": "supplier" | ||||
|  "title_field": "supplier_name", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -89,7 +89,7 @@ class TestPurchaseOrder(unittest.TestCase): | ||||
| 		frappe.db.set_value("Accounts Settings", None, "over_billing_allowance", 0) | ||||
| 
 | ||||
| 
 | ||||
| 	def test_update_child_qty_rate(self): | ||||
| 	def test_update_child(self): | ||||
| 		mr = make_material_request(qty=10) | ||||
| 		po = make_purchase_order(mr.name) | ||||
| 		po.supplier = "_Test Supplier" | ||||
| @ -119,7 +119,7 @@ class TestPurchaseOrder(unittest.TestCase): | ||||
| 		self.assertEqual(get_ordered_qty(), existing_ordered_qty + 3) | ||||
| 
 | ||||
| 
 | ||||
| 	def test_add_new_item_in_update_child_qty_rate(self): | ||||
| 	def test_update_child_adding_new_item(self): | ||||
| 		po = create_purchase_order(do_not_save=1) | ||||
| 		po.items[0].qty = 4 | ||||
| 		po.save() | ||||
| @ -145,7 +145,7 @@ class TestPurchaseOrder(unittest.TestCase): | ||||
| 		self.assertEqual(po.status, 'To Receive and Bill') | ||||
| 
 | ||||
| 
 | ||||
| 	def test_remove_item_in_update_child_qty_rate(self): | ||||
| 	def test_update_child_removing_item(self): | ||||
| 		po = create_purchase_order(do_not_save=1) | ||||
| 		po.items[0].qty = 4 | ||||
| 		po.save() | ||||
| @ -185,7 +185,7 @@ class TestPurchaseOrder(unittest.TestCase): | ||||
| 		self.assertEquals(len(po.get('items')), 1) | ||||
| 		self.assertEqual(po.status, 'To Receive and Bill') | ||||
| 
 | ||||
| 	def test_update_child_qty_rate_perm(self): | ||||
| 	def test_update_child_perm(self): | ||||
| 		po = create_purchase_order(item_code= "_Test Item", qty=4) | ||||
| 
 | ||||
| 		user = 'test@example.com' | ||||
| @ -202,6 +202,110 @@ class TestPurchaseOrder(unittest.TestCase): | ||||
| 		self.assertRaises(frappe.ValidationError, update_child_qty_rate,'Purchase Order', trans_item, po.name) | ||||
| 		frappe.set_user("Administrator") | ||||
| 
 | ||||
| 	def test_update_child_with_tax_template(self): | ||||
| 		""" | ||||
| 			Test Action: Create a PO with one item having its tax account head already in the PO. | ||||
| 			Add the same item + new item with tax template via Update Items. | ||||
| 			Expected result: First Item's tax row is updated. New tax row is added for second Item. | ||||
| 		""" | ||||
| 		if not frappe.db.exists("Item", "Test Item with Tax"): | ||||
| 			make_item("Test Item with Tax", { | ||||
| 				'is_stock_item': 1, | ||||
| 			}) | ||||
| 
 | ||||
| 		if not frappe.db.exists("Item Tax Template", {"title": 'Test Update Items Template'}): | ||||
| 			frappe.get_doc({ | ||||
| 				'doctype': 'Item Tax Template', | ||||
| 				'title': 'Test Update Items Template', | ||||
| 				'company': '_Test Company', | ||||
| 				'taxes': [ | ||||
| 					{ | ||||
| 						'tax_type': "_Test Account Service Tax - _TC", | ||||
| 						'tax_rate': 10, | ||||
| 					} | ||||
| 				] | ||||
| 			}).insert() | ||||
| 
 | ||||
| 		new_item_with_tax = frappe.get_doc("Item", "Test Item with Tax") | ||||
| 
 | ||||
| 		new_item_with_tax.append("taxes", { | ||||
| 			"item_tax_template": "Test Update Items Template", | ||||
| 			"valid_from": nowdate() | ||||
| 		}) | ||||
| 		new_item_with_tax.save() | ||||
| 
 | ||||
| 		tax_template = "_Test Account Excise Duty @ 10" | ||||
| 		item =  "_Test Item Home Desktop 100" | ||||
| 		if not frappe.db.exists("Item Tax", {"parent":item, "item_tax_template":tax_template}): | ||||
| 			item_doc = frappe.get_doc("Item", item) | ||||
| 			item_doc.append("taxes", { | ||||
| 				"item_tax_template": tax_template, | ||||
| 				"valid_from": nowdate() | ||||
| 			}) | ||||
| 			item_doc.save() | ||||
| 		else: | ||||
| 			# update valid from | ||||
| 			frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = CURDATE() | ||||
| 				where parent = %(item)s and item_tax_template = %(tax)s""", | ||||
| 					{"item": item, "tax": tax_template}) | ||||
| 
 | ||||
| 		po = create_purchase_order(item_code=item, qty=1, do_not_save=1) | ||||
| 
 | ||||
| 		po.append("taxes", { | ||||
| 			"account_head": "_Test Account Excise Duty - _TC", | ||||
| 			"charge_type": "On Net Total", | ||||
| 			"cost_center": "_Test Cost Center - _TC", | ||||
| 			"description": "Excise Duty", | ||||
| 			"doctype": "Purchase Taxes and Charges", | ||||
| 			"rate": 10 | ||||
| 		}) | ||||
| 		po.insert() | ||||
| 		po.submit() | ||||
| 
 | ||||
| 		self.assertEqual(po.taxes[0].tax_amount, 50) | ||||
| 		self.assertEqual(po.taxes[0].total, 550) | ||||
| 
 | ||||
| 		items = json.dumps([ | ||||
| 			{'item_code' : item, 'rate' : 500, 'qty' : 1, 'docname': po.items[0].name}, | ||||
| 			{'item_code' : item, 'rate' : 100, 'qty' : 1}, # added item whose tax account head already exists in PO | ||||
| 			{'item_code' : new_item_with_tax.name, 'rate' : 100, 'qty' : 1} # added item whose tax account head  is missing in PO | ||||
| 		]) | ||||
| 		update_child_qty_rate('Purchase Order', items, po.name) | ||||
| 
 | ||||
| 		po.reload() | ||||
| 		self.assertEqual(po.taxes[0].tax_amount, 70) | ||||
| 		self.assertEqual(po.taxes[0].total, 770) | ||||
| 		self.assertEqual(po.taxes[1].account_head, "_Test Account Service Tax - _TC") | ||||
| 		self.assertEqual(po.taxes[1].tax_amount, 70) | ||||
| 		self.assertEqual(po.taxes[1].total, 840) | ||||
| 
 | ||||
| 		# teardown | ||||
| 		frappe.db.sql("""UPDATE `tabItem Tax` set valid_from = NULL | ||||
| 			where parent = %(item)s and item_tax_template = %(tax)s""", {"item": item, "tax": tax_template}) | ||||
| 		po.cancel() | ||||
| 		po.delete() | ||||
| 		new_item_with_tax.delete() | ||||
| 		frappe.get_doc("Item Tax Template", "Test Update Items Template").delete() | ||||
| 
 | ||||
| 	def test_update_child_uom_conv_factor_change(self): | ||||
| 		po = create_purchase_order(item_code="_Test FG Item", is_subcontracted="Yes") | ||||
| 		total_reqd_qty = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) | ||||
| 
 | ||||
| 		trans_item = json.dumps([{ | ||||
| 			'item_code': po.get("items")[0].item_code, | ||||
| 			'rate': po.get("items")[0].rate, | ||||
| 			'qty': po.get("items")[0].qty, | ||||
| 			'uom': "_Test UOM 1", | ||||
| 			'conversion_factor': 2, | ||||
| 			'docname': po.get("items")[0].name | ||||
| 		}]) | ||||
| 		update_child_qty_rate('Purchase Order', trans_item, po.name) | ||||
| 		po.reload() | ||||
| 
 | ||||
| 		total_reqd_qty_after_change = sum([d.get("required_qty") for d in po.as_dict().get("supplied_items")]) | ||||
| 
 | ||||
| 		self.assertEqual(total_reqd_qty_after_change, 2 * total_reqd_qty) | ||||
| 
 | ||||
| 	def test_update_qty(self): | ||||
| 		po = create_purchase_order() | ||||
| 
 | ||||
| @ -585,12 +689,12 @@ class TestPurchaseOrder(unittest.TestCase): | ||||
| 		make_subcontracted_item(item_code) | ||||
| 
 | ||||
| 		po = create_purchase_order(item_code=item_code, qty=1, | ||||
| 			is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") | ||||
| 			is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=1) | ||||
| 
 | ||||
| 		name = frappe.db.get_value('BOM', {'item': item_code}, 'name') | ||||
| 		bom = frappe.get_doc('BOM', name) | ||||
| 
 | ||||
| 		exploded_items = sorted([d.item_code for d in bom.exploded_items]) | ||||
| 		exploded_items = sorted([d.item_code for d in bom.exploded_items if not d.get('sourced_by_supplier')]) | ||||
| 		supplied_items = sorted([d.rm_item_code for d in po.supplied_items]) | ||||
| 		self.assertEquals(exploded_items, supplied_items) | ||||
| 
 | ||||
| @ -598,7 +702,7 @@ class TestPurchaseOrder(unittest.TestCase): | ||||
| 			is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC", include_exploded_items=0) | ||||
| 
 | ||||
| 		supplied_items1 = sorted([d.rm_item_code for d in po1.supplied_items]) | ||||
| 		bom_items = sorted([d.item_code for d in bom.items]) | ||||
| 		bom_items = sorted([d.item_code for d in bom.items if not d.get('sourced_by_supplier')]) | ||||
| 
 | ||||
| 		self.assertEquals(supplied_items1, bom_items) | ||||
| 
 | ||||
|  | ||||
| @ -148,11 +148,11 @@ | ||||
|  "idx": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-03-12 15:43:53.862897", | ||||
|  "modified": "2020-09-18 17:26:09.703215", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Purchase Order Item Supplied", | ||||
|  "owner": "dhanalekshmi@webnotestech.com", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC" | ||||
|  | ||||
| @ -188,11 +188,11 @@ | ||||
|  "idx": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-04-10 18:09:33.997618", | ||||
|  "modified": "2020-09-18 17:26:09.703215", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Purchase Receipt Item Supplied", | ||||
|  "owner": "wasim@webnotestech.com", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  | ||||
| @ -22,8 +22,6 @@ frappe.ui.form.on("Request for Quotation",{ | ||||
| 	}, | ||||
| 
 | ||||
| 	onload: function(frm) { | ||||
| 		frm.add_fetch('email_template', 'response', 'message_for_supplier'); | ||||
| 
 | ||||
| 		if(!frm.doc.message_for_supplier) { | ||||
| 			frm.set_value("message_for_supplier", __("Please supply the specified items at the best possible rates")) | ||||
| 		} | ||||
| @ -179,7 +177,7 @@ frappe.ui.form.on("Request for Quotation",{ | ||||
| 			dialog.hide(); | ||||
| 			return frappe.call({ | ||||
| 				type: "GET", | ||||
| 				method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation", | ||||
| 				method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq", | ||||
| 				args: { | ||||
| 					"source_name": doc.name, | ||||
| 					"for_supplier": args.supplier | ||||
| @ -194,6 +192,66 @@ frappe.ui.form.on("Request for Quotation",{ | ||||
| 			}); | ||||
| 		}); | ||||
| 		dialog.show() | ||||
| 	}, | ||||
| 
 | ||||
| 	preview: (frm) => { | ||||
| 		let dialog = new frappe.ui.Dialog({ | ||||
| 			title: __('Preview Email'), | ||||
| 			fields: [ | ||||
| 				{ | ||||
| 					label: __('Supplier'), | ||||
| 					fieldtype: 'Select', | ||||
| 					fieldname: 'supplier', | ||||
| 					options: frm.doc.suppliers.map(row => row.supplier), | ||||
| 					reqd: 1 | ||||
| 				}, | ||||
| 				{ | ||||
| 					fieldtype: 'Column Break', | ||||
| 					fieldname: 'col_break_1', | ||||
| 				}, | ||||
| 				{ | ||||
| 					label: __('Subject'), | ||||
| 					fieldtype: 'Data', | ||||
| 					fieldname: 'subject', | ||||
| 					read_only: 1, | ||||
| 					depends_on: 'subject' | ||||
| 				}, | ||||
| 				{ | ||||
| 					fieldtype: 'Section Break', | ||||
| 					fieldname: 'sec_break_1', | ||||
| 					hide_border: 1 | ||||
| 				}, | ||||
| 				{ | ||||
| 					label: __('Email'), | ||||
| 					fieldtype: 'HTML', | ||||
| 					fieldname: 'email_preview' | ||||
| 				}, | ||||
| 				{ | ||||
| 					fieldtype: 'Section Break', | ||||
| 					fieldname: 'sec_break_2' | ||||
| 				}, | ||||
| 				{ | ||||
| 					label: __('Note'), | ||||
| 					fieldtype: 'HTML', | ||||
| 					fieldname: 'note' | ||||
| 				} | ||||
| 			] | ||||
| 		}); | ||||
| 
 | ||||
| 		dialog.fields_dict['supplier'].df.onchange = () => { | ||||
| 			var supplier = dialog.get_value('supplier'); | ||||
| 			frm.call('get_supplier_email_preview', {supplier: supplier}).then(result => { | ||||
| 				dialog.fields_dict.email_preview.$wrapper.empty(); | ||||
| 				dialog.fields_dict.email_preview.$wrapper.append(result.message); | ||||
| 			}); | ||||
| 
 | ||||
| 		} | ||||
| 
 | ||||
| 		dialog.fields_dict.note.$wrapper.append(`<p class="small text-muted">This is a preview of the email to be sent. A PDF of the document will
 | ||||
| 			automatically be attached with the email.</p>`); | ||||
| 
 | ||||
| 		dialog.set_value("subject", frm.doc.subject); | ||||
| 		dialog.show(); | ||||
| 	} | ||||
| }) | ||||
| 
 | ||||
| @ -276,7 +334,7 @@ erpnext.buying.RequestforQuotationController = erpnext.buying.BuyingController.e | ||||
| 					}) | ||||
| 				}, __("Get items from")); | ||||
| 			// Get items from Opportunity
 | ||||
|             this.frm.add_custom_button(__('Opportunity'), | ||||
| 			this.frm.add_custom_button(__('Opportunity'), | ||||
| 				function() { | ||||
| 					erpnext.utils.map_current_doc({ | ||||
| 						method: "erpnext.crm.doctype.opportunity.opportunity.make_request_for_quotation", | ||||
|  | ||||
| @ -1,5 +1,5 @@ | ||||
| { | ||||
|  "actions": "", | ||||
|  "actions": [], | ||||
|  "allow_import": 1, | ||||
|  "autoname": "naming_series:", | ||||
|  "creation": "2016-02-25 01:24:07.224790", | ||||
| @ -19,7 +19,12 @@ | ||||
|   "items", | ||||
|   "link_to_mrs", | ||||
|   "supplier_response_section", | ||||
|   "salutation", | ||||
|   "email_template", | ||||
|   "col_break_email_1", | ||||
|   "subject", | ||||
|   "preview", | ||||
|   "sec_break_email_2", | ||||
|   "message_for_supplier", | ||||
|   "terms_section_break", | ||||
|   "tc_name", | ||||
| @ -126,8 +131,10 @@ | ||||
|    "label": "Link to Material Requests" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:!doc.__islocal", | ||||
|    "fieldname": "supplier_response_section", | ||||
|    "fieldtype": "Section Break" | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Email Details" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "email_template", | ||||
| @ -137,6 +144,8 @@ | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "email_template.response", | ||||
|    "fetch_if_empty": 1, | ||||
|    "fieldname": "message_for_supplier", | ||||
|    "fieldtype": "Text Editor", | ||||
|    "in_list_view": 1, | ||||
| @ -230,12 +239,45 @@ | ||||
|    "options": "Request for Quotation", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "email_template.subject", | ||||
|    "fetch_if_empty": 1, | ||||
|    "fieldname": "subject", | ||||
|    "fieldtype": "Data", | ||||
|    "label": "Subject", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "description": "Select a greeting for the receiver. E.g. Mr., Ms., etc.", | ||||
|    "fieldname": "salutation", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Salutation", | ||||
|    "no_copy": 1, | ||||
|    "options": "Salutation", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "col_break_email_1", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:!doc.docstatus==1", | ||||
|    "fieldname": "preview", | ||||
|    "fieldtype": "Button", | ||||
|    "label": "Preview Email" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:!doc.__islocal", | ||||
|    "fieldname": "sec_break_email_2", | ||||
|    "fieldtype": "Section Break", | ||||
|    "hide_border": 1 | ||||
|   } | ||||
|  ], | ||||
|  "icon": "fa fa-shopping-cart", | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-06-25 14:37:21.140194", | ||||
|  "modified": "2020-10-01 14:54:50.888729", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Request for Quotation", | ||||
|  | ||||
| @ -51,7 +51,7 @@ class RequestforQuotation(BuyingController): | ||||
| 
 | ||||
| 	def validate_email_id(self, args): | ||||
| 		if not args.email_id: | ||||
| 			frappe.throw(_("Row {0}: For Supplier {0}, Email Address is Required to Send Email").format(args.idx, args.supplier)) | ||||
| 			frappe.throw(_("Row {0}: For Supplier {1}, Email Address is Required to send an email").format(args.idx, frappe.bold(args.supplier))) | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		frappe.db.set(self, 'status', 'Submitted') | ||||
| @ -62,17 +62,31 @@ class RequestforQuotation(BuyingController): | ||||
| 	def on_cancel(self): | ||||
| 		frappe.db.set(self, 'status', 'Cancelled') | ||||
| 
 | ||||
| 	def get_supplier_email_preview(self, supplier): | ||||
| 		"""Returns formatted email preview as string.""" | ||||
| 		rfq_suppliers = list(filter(lambda row: row.supplier == supplier, self.suppliers)) | ||||
| 		rfq_supplier = rfq_suppliers[0] | ||||
| 
 | ||||
| 		self.validate_email_id(rfq_supplier) | ||||
| 
 | ||||
| 		message  = self.supplier_rfq_mail(rfq_supplier, '', self.get_link(), True) | ||||
| 
 | ||||
| 		return message | ||||
| 
 | ||||
| 	def send_to_supplier(self): | ||||
| 		"""Sends RFQ mail to involved suppliers.""" | ||||
| 		for rfq_supplier in self.suppliers: | ||||
| 			if rfq_supplier.send_email: | ||||
| 				self.validate_email_id(rfq_supplier) | ||||
| 
 | ||||
| 				# make new user if required | ||||
| 				update_password_link = self.update_supplier_contact(rfq_supplier, self.get_link()) | ||||
| 				update_password_link, contact = self.update_supplier_contact(rfq_supplier, self.get_link()) | ||||
| 
 | ||||
| 				self.update_supplier_part_no(rfq_supplier) | ||||
| 				self.supplier_rfq_mail(rfq_supplier, update_password_link, self.get_link()) | ||||
| 				rfq_supplier.email_sent = 1 | ||||
| 				if not rfq_supplier.contact: | ||||
| 					rfq_supplier.contact = contact | ||||
| 				rfq_supplier.save() | ||||
| 
 | ||||
| 	def get_link(self): | ||||
| @ -87,18 +101,19 @@ class RequestforQuotation(BuyingController): | ||||
| 
 | ||||
| 	def update_supplier_contact(self, rfq_supplier, link): | ||||
| 		'''Create a new user for the supplier if not set in contact''' | ||||
| 		update_password_link = '' | ||||
| 		update_password_link, contact = '', '' | ||||
| 
 | ||||
| 		if frappe.db.exists("User", rfq_supplier.email_id): | ||||
| 			user = frappe.get_doc("User", rfq_supplier.email_id) | ||||
| 		else: | ||||
| 			user, update_password_link = self.create_user(rfq_supplier, link) | ||||
| 
 | ||||
| 		self.update_contact_of_supplier(rfq_supplier, user) | ||||
| 		contact = self.link_supplier_contact(rfq_supplier, user) | ||||
| 
 | ||||
| 		return update_password_link | ||||
| 		return update_password_link, contact | ||||
| 
 | ||||
| 	def update_contact_of_supplier(self, rfq_supplier, user): | ||||
| 	def link_supplier_contact(self, rfq_supplier, user): | ||||
| 		"""If no Contact, create a new contact against Supplier. If Contact exists, check if email and user id set.""" | ||||
| 		if rfq_supplier.contact: | ||||
| 			contact = frappe.get_doc("Contact", rfq_supplier.contact) | ||||
| 		else: | ||||
| @ -115,6 +130,10 @@ class RequestforQuotation(BuyingController): | ||||
| 
 | ||||
| 		contact.save(ignore_permissions=True) | ||||
| 
 | ||||
| 		if not rfq_supplier.contact: | ||||
| 			# return contact to later update, RFQ supplier row's contact | ||||
| 			return contact.name | ||||
| 
 | ||||
| 	def create_user(self, rfq_supplier, link): | ||||
| 		user = frappe.get_doc({ | ||||
| 			'doctype': 'User', | ||||
| @ -129,22 +148,36 @@ class RequestforQuotation(BuyingController): | ||||
| 
 | ||||
| 		return user, update_password_link | ||||
| 
 | ||||
| 	def supplier_rfq_mail(self, data, update_password_link, rfq_link): | ||||
| 	def supplier_rfq_mail(self, data, update_password_link, rfq_link, preview=False): | ||||
| 		full_name = get_user_fullname(frappe.session['user']) | ||||
| 		if full_name == "Guest": | ||||
| 			full_name = "Administrator" | ||||
| 
 | ||||
| 		# send document dict and some important data from suppliers row | ||||
| 		# to render message_for_supplier from any template | ||||
| 		doc_args = self.as_dict() | ||||
| 		doc_args.update({ | ||||
| 			'supplier': data.get('supplier'), | ||||
| 			'supplier_name': data.get('supplier_name') | ||||
| 		}) | ||||
| 
 | ||||
| 		args = { | ||||
| 			'update_password_link': update_password_link, | ||||
| 			'message': frappe.render_template(self.message_for_supplier, data.as_dict()), | ||||
| 			'message': frappe.render_template(self.message_for_supplier, doc_args), | ||||
| 			'rfq_link': rfq_link, | ||||
| 			'user_fullname': full_name | ||||
| 			'user_fullname': full_name, | ||||
| 			'supplier_name' : data.get('supplier_name'), | ||||
| 			'supplier_salutation' : self.salutation or 'Dear Mx.', | ||||
| 		} | ||||
| 
 | ||||
| 		subject = _("Request for Quotation") | ||||
| 		subject = self.subject or _("Request for Quotation") | ||||
| 		template = "templates/emails/request_for_quotation.html" | ||||
| 		sender = frappe.session.user not in STANDARD_USERS and frappe.session.user or None | ||||
| 		message = frappe.get_template(template).render(args) | ||||
| 
 | ||||
| 		if preview: | ||||
| 			return message | ||||
| 
 | ||||
| 		attachments = self.get_attachments() | ||||
| 
 | ||||
| 		self.send_email(data, sender, subject, message, attachments) | ||||
| @ -214,14 +247,14 @@ def get_supplier_contacts(doctype, txt, searchfield, start, page_len, filters): | ||||
| 		and `tabDynamic Link`.link_name like %(txt)s) and `tabContact`.name = `tabDynamic Link`.parent | ||||
| 		limit %(start)s, %(page_len)s""", {"start": start, "page_len":page_len, "txt": "%%%s%%" % txt, "name": filters.get('supplier')}) | ||||
| 
 | ||||
| # This method is used to make supplier quotation from material request form. | ||||
| @frappe.whitelist() | ||||
| def make_supplier_quotation(source_name, for_supplier, target_doc=None): | ||||
| def make_supplier_quotation_from_rfq(source_name, target_doc=None, for_supplier=None): | ||||
| 	def postprocess(source, target_doc): | ||||
| 		target_doc.supplier = for_supplier | ||||
| 		args = get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True) | ||||
| 		target_doc.currency = args.currency or get_party_account_currency('Supplier', for_supplier, source.company) | ||||
| 		target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value('Buying Settings', None, 'buying_price_list') | ||||
| 		if for_supplier: | ||||
| 			target_doc.supplier = for_supplier | ||||
| 			args = get_party_details(for_supplier, party_type="Supplier", ignore_permissions=True) | ||||
| 			target_doc.currency = args.currency or get_party_account_currency('Supplier', for_supplier, source.company) | ||||
| 			target_doc.buying_price_list = args.buying_price_list or frappe.db.get_value('Buying Settings', None, 'buying_price_list') | ||||
| 		set_missing_values(source, target_doc) | ||||
| 
 | ||||
| 	doclist = get_mapped_doc("Request for Quotation", source_name, { | ||||
| @ -354,3 +387,32 @@ def get_supplier_tag(): | ||||
| 		frappe.cache().hset("Supplier", "Tags", tags) | ||||
| 
 | ||||
| 	return frappe.cache().hget("Supplier", "Tags") | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| @frappe.validate_and_sanitize_search_inputs | ||||
| def get_rfq_containing_supplier(doctype, txt, searchfield, start, page_len, filters): | ||||
| 	conditions = "" | ||||
| 	if txt: | ||||
| 		conditions += "and rfq.name like '%%"+txt+"%%' " | ||||
| 
 | ||||
| 	if filters.get("transaction_date"): | ||||
| 		conditions += "and rfq.transaction_date = '{0}'".format(filters.get("transaction_date")) | ||||
| 
 | ||||
| 	rfq_data = frappe.db.sql(""" | ||||
| 		select | ||||
| 			distinct rfq.name, rfq.transaction_date, | ||||
| 			rfq.company | ||||
| 		from | ||||
| 			`tabRequest for Quotation` rfq, `tabRequest for Quotation Supplier` rfq_supplier | ||||
| 		where | ||||
| 			rfq.name = rfq_supplier.parent | ||||
| 			and rfq_supplier.supplier = '{0}' | ||||
| 			and rfq.docstatus = 1 | ||||
| 			and rfq.company = '{1}' | ||||
| 			{2} | ||||
| 		order by rfq.transaction_date ASC | ||||
| 		limit %(page_len)s offset %(start)s """ \ | ||||
| 		.format(filters.get("supplier"), filters.get("company"), conditions), | ||||
| 			{"page_len": page_len, "start": start}, as_dict=1) | ||||
| 
 | ||||
| 	return rfq_data | ||||
| @ -9,7 +9,7 @@ import frappe | ||||
| from frappe.utils import nowdate | ||||
| from erpnext.stock.doctype.item.test_item import make_item | ||||
| from erpnext.templates.pages.rfq import check_supplier_has_docname_access | ||||
| from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation | ||||
| from erpnext.buying.doctype.request_for_quotation.request_for_quotation import make_supplier_quotation_from_rfq | ||||
| from erpnext.buying.doctype.request_for_quotation.request_for_quotation import create_supplier_quotation | ||||
| from erpnext.crm.doctype.opportunity.test_opportunity import make_opportunity | ||||
| from erpnext.crm.doctype.opportunity.opportunity import make_request_for_quotation as make_rfq | ||||
| @ -22,7 +22,7 @@ class TestRequestforQuotation(unittest.TestCase): | ||||
| 		self.assertEqual(rfq.get('suppliers')[1].quote_status, 'Pending') | ||||
| 
 | ||||
| 		# Submit the first supplier quotation | ||||
| 		sq = make_supplier_quotation(rfq.name, rfq.get('suppliers')[0].supplier) | ||||
| 		sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[0].supplier) | ||||
| 		sq.submit() | ||||
| 
 | ||||
| 		# No Quote first supplier quotation | ||||
| @ -37,10 +37,10 @@ class TestRequestforQuotation(unittest.TestCase): | ||||
| 	def test_make_supplier_quotation(self): | ||||
| 		rfq = make_request_for_quotation() | ||||
| 
 | ||||
| 		sq = make_supplier_quotation(rfq.name, rfq.get('suppliers')[0].supplier) | ||||
| 		sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[0].supplier) | ||||
| 		sq.submit() | ||||
| 
 | ||||
| 		sq1 = make_supplier_quotation(rfq.name, rfq.get('suppliers')[1].supplier) | ||||
| 		sq1 = make_supplier_quotation_from_rfq(rfq.name, for_supplier=rfq.get('suppliers')[1].supplier) | ||||
| 		sq1.submit() | ||||
| 
 | ||||
| 		self.assertEqual(sq.supplier, rfq.get('suppliers')[0].supplier) | ||||
| @ -62,7 +62,7 @@ class TestRequestforQuotation(unittest.TestCase): | ||||
| 
 | ||||
| 		rfq = make_request_for_quotation(supplier_data=supplier_wt_appos) | ||||
| 
 | ||||
| 		sq = make_supplier_quotation(rfq.name, supplier_wt_appos[0].get("supplier")) | ||||
| 		sq = make_supplier_quotation_from_rfq(rfq.name, for_supplier=supplier_wt_appos[0].get("supplier")) | ||||
| 		sq.submit() | ||||
| 
 | ||||
| 		frappe.form_dict = frappe.local("form_dict") | ||||
|  | ||||
| @ -1,362 +1,112 @@ | ||||
| { | ||||
|  "allow_copy": 0,  | ||||
|  "allow_guest_to_view": 0,  | ||||
|  "allow_import": 0,  | ||||
|  "allow_rename": 0,  | ||||
|  "beta": 0,  | ||||
|  "creation": "2016-03-29 05:59:11.896885",  | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "",  | ||||
|  "editable_grid": 1,  | ||||
|  "engine": "InnoDB",  | ||||
|  "actions": [], | ||||
|  "creation": "2016-03-29 05:59:11.896885", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "send_email", | ||||
|   "email_sent", | ||||
|   "supplier", | ||||
|   "contact", | ||||
|   "no_quote", | ||||
|   "quote_status", | ||||
|   "column_break_3", | ||||
|   "supplier_name", | ||||
|   "email_id", | ||||
|   "download_pdf" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 1,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "default": "1",  | ||||
|    "fieldname": "send_email",  | ||||
|    "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": "Send Email",  | ||||
|    "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, | ||||
|    "default": "1", | ||||
|    "fieldname": "send_email", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Send Email" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 1,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "default": "0",  | ||||
|    "depends_on": "eval:doc.docstatus >= 1",  | ||||
|    "fieldname": "email_sent",  | ||||
|    "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": "Email Sent",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 1,  | ||||
|    "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, | ||||
|    "default": "0", | ||||
|    "depends_on": "eval:doc.docstatus >= 1", | ||||
|    "fieldname": "email_sent", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Email Sent", | ||||
|    "no_copy": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 4,  | ||||
|    "fieldname": "supplier",  | ||||
|    "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": "Supplier",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Supplier",  | ||||
|    "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": 4, | ||||
|    "fieldname": "supplier", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Supplier", | ||||
|    "options": "Supplier", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 3,  | ||||
|    "fieldname": "contact",  | ||||
|    "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": "Contact",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 1,  | ||||
|    "options": "Contact",  | ||||
|    "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, | ||||
|    "columns": 3, | ||||
|    "fieldname": "contact", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Contact", | ||||
|    "no_copy": 1, | ||||
|    "options": "Contact" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 1,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "depends_on": "eval:doc.docstatus >= 1 && doc.quote_status != 'Received'",  | ||||
|    "fieldname": "no_quote",  | ||||
|    "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": "No Quote",  | ||||
|    "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, | ||||
|    "default": "0", | ||||
|    "depends_on": "eval:doc.docstatus >= 1 && doc.quote_status != 'Received'", | ||||
|    "fieldname": "no_quote", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "No Quote" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 1,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "depends_on": "eval:doc.docstatus >= 1 && !doc.no_quote",  | ||||
|    "fieldname": "quote_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": "Quote Status",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Pending\nReceived\nNo Quote",  | ||||
|    "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, | ||||
|    "depends_on": "eval:doc.docstatus >= 1 && !doc.no_quote", | ||||
|    "fieldname": "quote_status", | ||||
|    "fieldtype": "Select", | ||||
|    "label": "Quote Status", | ||||
|    "options": "Pending\nReceived\nNo Quote", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "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,  | ||||
|    "translatable": 0, | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "column_break_3", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 1,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "bold": 1, | ||||
|    "fetch_from": "supplier.supplier_name", | ||||
|    "fieldname": "supplier_name",  | ||||
|    "fieldtype": "Read Only",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 1,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Supplier 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": "supplier_name", | ||||
|    "fieldtype": "Read Only", | ||||
|    "in_global_search": 1, | ||||
|    "label": "Supplier Name" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 3,  | ||||
|    "columns": 3, | ||||
|    "fetch_from": "contact.email_id", | ||||
|    "fieldname": "email_id",  | ||||
|    "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": "Email Id",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 1,  | ||||
|    "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": "email_id", | ||||
|    "fieldtype": "Data", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Email Id", | ||||
|    "no_copy": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 1,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "download_pdf",  | ||||
|    "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": "Download PDF",  | ||||
|    "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": "download_pdf", | ||||
|    "fieldtype": "Button", | ||||
|    "label": "Download PDF" | ||||
|   } | ||||
|  ],  | ||||
|  "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-05-16 22:43:30.212408", | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Buying",  | ||||
|  "name": "Request for Quotation Supplier",  | ||||
|  "name_case": "",  | ||||
|  "owner": "Administrator",  | ||||
|  "permissions": [],  | ||||
|  "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 | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-09-28 19:31:11.855588", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Request for Quotation Supplier", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -8,8 +8,7 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext | ||||
| 	setup: function() { | ||||
| 		this.frm.custom_make_buttons = { | ||||
| 			'Purchase Order': 'Purchase Order', | ||||
| 			'Quotation': 'Quotation', | ||||
| 			'Subscription': 'Subscription' | ||||
| 			'Quotation': 'Quotation' | ||||
| 		} | ||||
| 
 | ||||
| 		this._super(); | ||||
| @ -28,12 +27,6 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext | ||||
| 			cur_frm.page.set_inner_btn_group_as_primary(__('Create')); | ||||
| 			cur_frm.add_custom_button(__("Quotation"), this.make_quotation, | ||||
| 				__('Create')); | ||||
| 
 | ||||
| 			if(!this.frm.doc.auto_repeat) { | ||||
| 				cur_frm.add_custom_button(__('Subscription'), function() { | ||||
| 					erpnext.utils.make_subscription(me.frm.doc.doctype, me.frm.doc.name) | ||||
| 				}, __('Create')) | ||||
| 			} | ||||
| 		} | ||||
| 		else if (this.frm.doc.docstatus===0) { | ||||
| 
 | ||||
| @ -54,6 +47,27 @@ erpnext.buying.SupplierQuotationController = erpnext.buying.BuyingController.ext | ||||
| 						} | ||||
| 					}) | ||||
| 				}, __("Get items from")); | ||||
| 
 | ||||
| 			this.frm.add_custom_button(__("Request for Quotation"), | ||||
| 			function() { | ||||
| 				if (!me.frm.doc.supplier) { | ||||
| 					frappe.throw({message:__("Please select a Supplier"), title:__("Mandatory")}) | ||||
| 				} | ||||
| 				erpnext.utils.map_current_doc({ | ||||
| 					method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.make_supplier_quotation_from_rfq", | ||||
| 					source_doctype: "Request for Quotation", | ||||
| 					target: me.frm, | ||||
| 					setters: { | ||||
| 						company: me.frm.doc.company, | ||||
| 						transaction_date: null | ||||
| 					}, | ||||
| 					get_query_filters: { | ||||
| 						supplier: me.frm.doc.supplier | ||||
| 					}, | ||||
| 					get_query_method: "erpnext.buying.doctype.request_for_quotation.request_for_quotation.get_rfq_containing_supplier" | ||||
| 
 | ||||
| 				}) | ||||
| 			}, __("Get items from")); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
|  | ||||
| @ -159,6 +159,7 @@ | ||||
|    "default": "Today", | ||||
|    "fieldname": "transaction_date", | ||||
|    "fieldtype": "Date", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Date", | ||||
|    "oldfieldname": "transaction_date", | ||||
|    "oldfieldtype": "Date", | ||||
| @ -798,6 +799,7 @@ | ||||
|   { | ||||
|    "fieldname": "valid_till", | ||||
|    "fieldtype": "Date", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Valid Till" | ||||
|   } | ||||
|  ], | ||||
| @ -805,7 +807,7 @@ | ||||
|  "idx": 29, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-07-18 05:10:45.556792", | ||||
|  "modified": "2020-10-01 20:56:17.932007", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Supplier Quotation", | ||||
|  | ||||
| @ -12,6 +12,8 @@ | ||||
|   "item_name", | ||||
|   "column_break_3", | ||||
|   "lead_time_days", | ||||
|   "expected_delivery_date", | ||||
|   "is_free_item", | ||||
|   "section_break_5", | ||||
|   "description", | ||||
|   "item_group", | ||||
| @ -19,20 +21,18 @@ | ||||
|   "col_break1", | ||||
|   "image", | ||||
|   "image_view", | ||||
|   "manufacture_details", | ||||
|   "manufacturer", | ||||
|   "column_break_15", | ||||
|   "manufacturer_part_no", | ||||
|   "quantity_and_rate", | ||||
|   "qty", | ||||
|   "stock_uom", | ||||
|   "price_list_rate", | ||||
|   "discount_percentage", | ||||
|   "discount_amount", | ||||
|   "col_break2", | ||||
|   "uom", | ||||
|   "conversion_factor", | ||||
|   "stock_qty", | ||||
|   "sec_break_price_list", | ||||
|   "price_list_rate", | ||||
|   "discount_percentage", | ||||
|   "discount_amount", | ||||
|   "col_break_price_list", | ||||
|   "base_price_list_rate", | ||||
|   "sec_break1", | ||||
|   "rate", | ||||
| @ -42,7 +42,6 @@ | ||||
|   "base_rate", | ||||
|   "base_amount", | ||||
|   "pricing_rules", | ||||
|   "is_free_item", | ||||
|   "section_break_24", | ||||
|   "net_rate", | ||||
|   "net_amount", | ||||
| @ -56,7 +55,6 @@ | ||||
|   "weight_uom", | ||||
|   "warehouse_and_reference", | ||||
|   "warehouse", | ||||
|   "project", | ||||
|   "prevdoc_doctype", | ||||
|   "material_request", | ||||
|   "sales_order", | ||||
| @ -65,13 +63,19 @@ | ||||
|   "material_request_item", | ||||
|   "request_for_quotation_item", | ||||
|   "item_tax_rate", | ||||
|   "manufacture_details", | ||||
|   "manufacturer", | ||||
|   "column_break_15", | ||||
|   "manufacturer_part_no", | ||||
|   "ad_sec_break", | ||||
|   "project", | ||||
|   "section_break_44", | ||||
|   "page_break" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "bold": 1, | ||||
|    "columns": 4, | ||||
|    "columns": 2, | ||||
|    "fieldname": "item_code", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
| @ -107,7 +111,7 @@ | ||||
|   { | ||||
|    "fieldname": "lead_time_days", | ||||
|    "fieldtype": "Int", | ||||
|    "label": "Lead Time in days" | ||||
|    "label": "Supplier Lead Time (days)" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
| @ -162,7 +166,6 @@ | ||||
|   { | ||||
|    "fieldname": "stock_uom", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Stock UOM", | ||||
|    "options": "UOM", | ||||
|    "print_hide": 1, | ||||
| @ -196,6 +199,7 @@ | ||||
|   { | ||||
|    "fieldname": "uom", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "UOM", | ||||
|    "options": "UOM", | ||||
|    "print_hide": 1, | ||||
| @ -237,7 +241,7 @@ | ||||
|    "fieldname": "rate", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Rate ", | ||||
|    "label": "Rate", | ||||
|    "oldfieldname": "import_rate", | ||||
|    "oldfieldtype": "Currency", | ||||
|    "options": "currency" | ||||
| @ -289,14 +293,6 @@ | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "is_free_item", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Is Free Item", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_24", | ||||
|    "fieldtype": "Section Break" | ||||
| @ -528,12 +524,43 @@ | ||||
|   { | ||||
|    "fieldname": "column_break_15", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "sec_break_price_list", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "col_break_price_list", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "ad_sec_break", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Accounting Dimensions" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "depends_on": "is_free_item", | ||||
|    "fieldname": "is_free_item", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Is Free Item", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "bold": 1, | ||||
|    "fieldname": "expected_delivery_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Expected Delivery Date" | ||||
|   } | ||||
|  ], | ||||
|  "idx": 1, | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-04-07 18:35:51.175947", | ||||
|  "modified": "2020-10-19 12:36:26.913211", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Supplier Quotation Item", | ||||
|  | ||||
Some files were not shown because too many files have changed in this diff Show More
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user