Merge branch 'develop' into education-dashboard
This commit is contained in:
		
						commit
						fdcaecb58c
					
				| @ -147,10 +147,15 @@ | ||||
|    "link_to": "Trial Balance", | ||||
|    "type": "Report" | ||||
|   }, | ||||
|   { | ||||
|    "label": "Point of Sale", | ||||
|    "link_to": "point-of-sale", | ||||
|    "type": "Page" | ||||
|   }, | ||||
|   { | ||||
|    "label": "Dashboard", | ||||
|    "link_to": "Accounts", | ||||
|    "type": "Dashboard" | ||||
|   } | ||||
|  ] | ||||
| } | ||||
| } | ||||
|  | ||||
| @ -13,7 +13,6 @@ | ||||
|   "bank_name", | ||||
|   "swift_number", | ||||
|   "column_break_1", | ||||
|   "branch_code", | ||||
|   "website", | ||||
|   "address_and_contact", | ||||
|   "address_html", | ||||
| @ -51,15 +50,6 @@ | ||||
|    "fieldtype": "Column Break", | ||||
|    "search_index": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_in_quick_entry": 1, | ||||
|    "fieldname": "branch_code", | ||||
|    "fieldtype": "Data", | ||||
|    "in_list_view": 1, | ||||
|    "in_standard_filter": 1, | ||||
|    "label": "Branch Code", | ||||
|    "unique": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "address_and_contact", | ||||
|    "fieldtype": "Section Break", | ||||
| @ -111,7 +101,7 @@ | ||||
|   } | ||||
|  ], | ||||
|  "links": [], | ||||
|  "modified": "2020-03-25 21:22:33.496264", | ||||
|  "modified": "2020-07-17 14:00:13.105433", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Bank", | ||||
|  | ||||
| @ -23,6 +23,7 @@ | ||||
|   "account_details_section", | ||||
|   "iban", | ||||
|   "column_break_12", | ||||
|   "branch_code", | ||||
|   "bank_account_no", | ||||
|   "address_and_contact", | ||||
|   "address_html", | ||||
| @ -197,10 +198,16 @@ | ||||
|    "fieldtype": "Data", | ||||
|    "label": "Mask", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "branch_code", | ||||
|    "fieldtype": "Data", | ||||
|    "in_global_search": 1, | ||||
|    "label": "Branch Code" | ||||
|   } | ||||
|  ], | ||||
|  "links": [], | ||||
|  "modified": "2020-04-06 21:00:45.379804", | ||||
|  "modified": "2020-07-17 13:59:50.795412", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Bank Account", | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| cur_frm.add_fetch('bank_account','account','account'); | ||||
| cur_frm.add_fetch('bank_account','bank_account_no','bank_account_no'); | ||||
| cur_frm.add_fetch('bank_account','iban','iban'); | ||||
| cur_frm.add_fetch('bank','branch_code','branch_code'); | ||||
| cur_frm.add_fetch('bank_account','branch_code','branch_code'); | ||||
| cur_frm.add_fetch('bank','swift_number','swift_number'); | ||||
| 
 | ||||
| frappe.ui.form.on('Bank Guarantee', { | ||||
|  | ||||
							
								
								
									
										149
									
								
								erpnext/accounts/doctype/dunning/dunning.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								erpnext/accounts/doctype/dunning/dunning.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,149 @@ | ||||
| // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
 | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| frappe.ui.form.on("Dunning", { | ||||
| 	setup: function (frm) { | ||||
| 		frm.set_query("sales_invoice", () => { | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					docstatus: 1, | ||||
| 					company: frm.doc.company, | ||||
| 					outstanding_amount: [">", 0], | ||||
| 					status: "Overdue" | ||||
| 				}, | ||||
| 			}; | ||||
| 		}); | ||||
| 		frm.set_query("income_account", () => { | ||||
| 			return { | ||||
| 				filters: { | ||||
| 					company: frm.doc.company, | ||||
| 					root_type: "Income", | ||||
| 					is_group: 0 | ||||
| 				} | ||||
| 			}; | ||||
| 		}); | ||||
| 	}, | ||||
| 	refresh: function (frm) { | ||||
| 		frm.set_df_property("company", "read_only", frm.doc.__islocal ? 0 : 1); | ||||
| 		frm.set_df_property( | ||||
| 			"sales_invoice", | ||||
| 			"read_only", | ||||
| 			frm.doc.__islocal ? 0 : 1 | ||||
| 		); | ||||
| 		if (frm.doc.docstatus === 1 && frm.doc.status === "Unresolved") { | ||||
| 			frm.add_custom_button(__("Resolve"), () => { | ||||
| 				frm.set_value("status", "Resolved"); | ||||
| 			}); | ||||
| 		} | ||||
| 		if (frm.doc.docstatus === 1 && frm.doc.status !== "Resolved") { | ||||
| 			frm.add_custom_button( | ||||
| 				__("Payment"), | ||||
| 				function () { | ||||
| 					frm.events.make_payment_entry(frm); | ||||
| 				},__("Create") | ||||
| 			); | ||||
| 			frm.page.set_inner_btn_group_as_primary(__("Create")); | ||||
| 		} | ||||
| 	}, | ||||
| 	overdue_days: function (frm) { | ||||
| 		frappe.db.get_value( | ||||
| 			"Dunning Type", | ||||
| 			{ | ||||
| 				start_day: ["<", frm.doc.overdue_days], | ||||
| 				end_day: [">=", frm.doc.overdue_days], | ||||
| 			}, | ||||
| 			"dunning_type", | ||||
| 			(r) => { | ||||
| 				if (r) { | ||||
| 					frm.set_value("dunning_type", r.dunning_type); | ||||
| 				} else { | ||||
| 					frm.set_value("dunning_type", ""); | ||||
| 					frm.set_value("rate_of_interest", ""); | ||||
| 					frm.set_value("dunning_fee", ""); | ||||
| 				} | ||||
| 			} | ||||
| 		); | ||||
| 	}, | ||||
| 	dunning_type: function (frm) { | ||||
| 		frm.trigger("get_dunning_letter_text"); | ||||
| 	}, | ||||
| 	language: function (frm) { | ||||
| 		frm.trigger("get_dunning_letter_text"); | ||||
| 	}, | ||||
| 	get_dunning_letter_text: function (frm) { | ||||
| 		if (frm.doc.dunning_type) { | ||||
| 			frappe.call({ | ||||
| 				method: | ||||
| 				"erpnext.accounts.doctype.dunning.dunning.get_dunning_letter_text", | ||||
| 				args: { | ||||
| 					dunning_type: frm.doc.dunning_type, | ||||
| 					language: frm.doc.language, | ||||
| 					doc: frm.doc, | ||||
| 				}, | ||||
| 				callback: function (r) { | ||||
| 					if (r.message) { | ||||
| 						frm.set_value("body_text", r.message.body_text); | ||||
| 						frm.set_value("closing_text", r.message.closing_text); | ||||
| 						frm.set_value("language", r.message.language); | ||||
| 					} else { | ||||
| 						frm.set_value("body_text", ""); | ||||
| 						frm.set_value("closing_text", ""); | ||||
| 					} | ||||
| 				}, | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 	due_date: function (frm) { | ||||
| 		frm.trigger("calculate_overdue_days"); | ||||
| 	}, | ||||
| 	posting_date: function (frm) { | ||||
| 		frm.trigger("calculate_overdue_days"); | ||||
| 	}, | ||||
| 	rate_of_interest: function (frm) { | ||||
| 		frm.trigger("calculate_interest_and_amount"); | ||||
| 	}, | ||||
| 	outstanding_amount: function (frm) { | ||||
| 		frm.trigger("calculate_interest_and_amount"); | ||||
| 	}, | ||||
| 	interest_amount: function (frm) { | ||||
| 		frm.trigger("calculate_interest_and_amount"); | ||||
| 	}, | ||||
| 	dunning_fee: function (frm) { | ||||
| 		frm.trigger("calculate_interest_and_amount"); | ||||
| 	}, | ||||
| 	sales_invoice: function (frm) { | ||||
| 		frm.trigger("calculate_overdue_days"); | ||||
| 	}, | ||||
| 	calculate_overdue_days: function (frm) { | ||||
| 		if (frm.doc.posting_date && frm.doc.due_date) { | ||||
| 			const overdue_days = moment(frm.doc.posting_date).diff( | ||||
| 				frm.doc.due_date, | ||||
| 				"days" | ||||
| 			); | ||||
| 			frm.set_value("overdue_days", overdue_days); | ||||
| 		} | ||||
| 	}, | ||||
| 	calculate_interest_and_amount: function (frm) { | ||||
| 		const interest_per_year = frm.doc.outstanding_amount * frm.doc.rate_of_interest / 100; | ||||
| 		const interest_amount = interest_per_year / 365 * frm.doc.overdue_days || 0; | ||||
| 		const dunning_amount = interest_amount + frm.doc.dunning_fee; | ||||
| 		const grand_total = frm.doc.outstanding_amount + dunning_amount; | ||||
| 		frm.set_value("interest_amount", interest_amount); | ||||
| 		frm.set_value("dunning_amount", dunning_amount); | ||||
| 		frm.set_value("grand_total", grand_total); | ||||
| 	}, | ||||
| 	make_payment_entry: function (frm) { | ||||
| 		return frappe.call({ | ||||
| 			method: | ||||
| 			"erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry", | ||||
| 			args: { | ||||
| 				dt: frm.doc.doctype, | ||||
| 				dn: frm.doc.name, | ||||
| 			}, | ||||
| 			callback: function (r) { | ||||
| 				var doc = frappe.model.sync(r.message); | ||||
| 				frappe.set_route("Form", doc[0].doctype, doc[0].name); | ||||
| 			}, | ||||
| 		}); | ||||
| 	}, | ||||
| }); | ||||
							
								
								
									
										370
									
								
								erpnext/accounts/doctype/dunning/dunning.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										370
									
								
								erpnext/accounts/doctype/dunning/dunning.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,370 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "allow_events_in_timeline": 1, | ||||
|  "autoname": "naming_series:", | ||||
|  "creation": "2019-07-05 16:34:31.013238", | ||||
|  "doctype": "DocType", | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "title", | ||||
|   "naming_series", | ||||
|   "sales_invoice", | ||||
|   "customer", | ||||
|   "customer_name", | ||||
|   "outstanding_amount", | ||||
|   "currency", | ||||
|   "conversion_rate", | ||||
|   "column_break_3", | ||||
|   "company", | ||||
|   "posting_date", | ||||
|   "posting_time", | ||||
|   "due_date", | ||||
|   "overdue_days", | ||||
|   "address_and_contact_section", | ||||
|   "address_display", | ||||
|   "contact_display", | ||||
|   "contact_mobile", | ||||
|   "contact_email", | ||||
|   "column_break_18", | ||||
|   "company_address_display", | ||||
|   "section_break_6", | ||||
|   "dunning_type", | ||||
|   "interest_amount", | ||||
|   "column_break_8", | ||||
|   "rate_of_interest", | ||||
|   "dunning_fee", | ||||
|   "section_break_12", | ||||
|   "dunning_amount", | ||||
|   "grand_total", | ||||
|   "income_account", | ||||
|   "column_break_17", | ||||
|   "status", | ||||
|   "printing_setting_section", | ||||
|   "language", | ||||
|   "body_text", | ||||
|   "column_break_22", | ||||
|   "letter_head", | ||||
|   "closing_text", | ||||
|   "amended_from" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "company", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Company", | ||||
|    "options": "Company", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "DUNN-.MM.-.YY.-", | ||||
|    "fieldname": "naming_series", | ||||
|    "fieldtype": "Select", | ||||
|    "label": "Series", | ||||
|    "options": "DUNN-.MM.-.YY.-" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "sales_invoice", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "in_standard_filter": 1, | ||||
|    "label": "Sales Invoice", | ||||
|    "options": "Sales Invoice", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.customer_name", | ||||
|    "fieldname": "customer_name", | ||||
|    "fieldtype": "Data", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Customer Name", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.outstanding_amount", | ||||
|    "fieldname": "outstanding_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Outstanding Amount", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_3", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "Today", | ||||
|    "fieldname": "posting_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Date" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "overdue_days", | ||||
|    "fieldtype": "Int", | ||||
|    "label": "Overdue Days", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_6", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "dunning_type", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "in_standard_filter": 1, | ||||
|    "label": "Dunning Type", | ||||
|    "options": "Dunning Type", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "interest_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Interest Amount", | ||||
|    "precision": "2", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_8", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fetch_from": "dunning_type.dunning_fee", | ||||
|    "fetch_if_empty": 1, | ||||
|    "fieldname": "dunning_fee", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Dunning Fee", | ||||
|    "precision": "2" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_12", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_17", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "printing_setting_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Printing Setting" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "language", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Print Language", | ||||
|    "options": "Language" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "letter_head", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Letter Head", | ||||
|    "options": "Letter Head" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_22", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.currency", | ||||
|    "fieldname": "currency", | ||||
|    "fieldtype": "Link", | ||||
|    "hidden": 1, | ||||
|    "label": "Currency", | ||||
|    "options": "Currency", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "amended_from", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Amended From", | ||||
|    "no_copy": 1, | ||||
|    "options": "Dunning", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "default": "{customer_name}", | ||||
|    "fieldname": "title", | ||||
|    "fieldtype": "Data", | ||||
|    "hidden": 1, | ||||
|    "label": "Title" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "body_text", | ||||
|    "fieldtype": "Text Editor", | ||||
|    "label": "Body Text" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "closing_text", | ||||
|    "fieldtype": "Text Editor", | ||||
|    "label": "Closing Text" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.due_date", | ||||
|    "fieldname": "due_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Due Date", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "posting_time", | ||||
|    "fieldtype": "Time", | ||||
|    "label": "Posting Time" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fetch_from": "dunning_type.interest_rate", | ||||
|    "fetch_if_empty": 1, | ||||
|    "fieldname": "rate_of_interest", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Rate of Interest (%) Yearly" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "address_and_contact_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Address and Contact" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.address_display", | ||||
|    "fieldname": "address_display", | ||||
|    "fieldtype": "Small Text", | ||||
|    "label": "Address", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.contact_display", | ||||
|    "fieldname": "contact_display", | ||||
|    "fieldtype": "Small Text", | ||||
|    "label": "Contact", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.contact_mobile", | ||||
|    "fieldname": "contact_mobile", | ||||
|    "fieldtype": "Small Text", | ||||
|    "label": "Mobile No", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_18", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.company_address_display", | ||||
|    "fieldname": "company_address_display", | ||||
|    "fieldtype": "Small Text", | ||||
|    "label": "Company Address", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.contact_email", | ||||
|    "fieldname": "contact_email", | ||||
|    "fieldtype": "Data", | ||||
|    "label": "Contact Email", | ||||
|    "options": "Email", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.customer", | ||||
|    "fieldname": "customer", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Customer", | ||||
|    "options": "Customer", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "grand_total", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Grand Total", | ||||
|    "precision": "2", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "default": "Unresolved", | ||||
|    "fieldname": "status", | ||||
|    "fieldtype": "Select", | ||||
|    "in_standard_filter": 1, | ||||
|    "label": "Status", | ||||
|    "options": "Draft\nResolved\nUnresolved\nCancelled" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "dunning_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "hidden": 1, | ||||
|    "label": "Dunning Amount", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "income_account", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Income Account", | ||||
|    "options": "Account" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "sales_invoice.conversion_rate", | ||||
|    "fieldname": "conversion_rate", | ||||
|    "fieldtype": "Float", | ||||
|    "hidden": 1, | ||||
|    "label": "Conversion Rate", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-07-21 18:20:23.512151", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Dunning", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "amend": 1, | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "System Manager", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Accounts Manager", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Accounts User", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   } | ||||
|  ], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "ASC", | ||||
|  "title_field": "customer_name", | ||||
|  "track_changes": 1 | ||||
| } | ||||
							
								
								
									
										119
									
								
								erpnext/accounts/doctype/dunning/dunning.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								erpnext/accounts/doctype/dunning/dunning.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,119 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| import json | ||||
| from six import string_types | ||||
| from frappe.utils import getdate, get_datetime, rounded, flt | ||||
| from erpnext.loan_management.doctype.loan_interest_accrual.loan_interest_accrual import days_in_year | ||||
| from erpnext.accounts.general_ledger import make_gl_entries, make_reverse_gl_entries | ||||
| from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions | ||||
| from erpnext.controllers.accounts_controller import AccountsController | ||||
| 
 | ||||
| 
 | ||||
| class Dunning(AccountsController): | ||||
| 	def validate(self): | ||||
| 		self.validate_overdue_days() | ||||
| 		self.validate_amount() | ||||
| 		if not self.income_account: | ||||
| 			self.income_account = frappe.db.get_value('Company', self.company, 'default_income_account') | ||||
| 
 | ||||
| 	def validate_overdue_days(self): | ||||
| 		self.overdue_days = (getdate(self.posting_date) - getdate(self.due_date)).days or 0 | ||||
| 
 | ||||
| 	def validate_amount(self): | ||||
| 		amounts = calculate_interest_and_amount( | ||||
| 			self.posting_date, self.outstanding_amount, self.rate_of_interest, self.dunning_fee, self.overdue_days) | ||||
| 		if self.interest_amount != amounts.get('interest_amount'): | ||||
| 			self.interest_amount = amounts.get('interest_amount') | ||||
| 		if self.dunning_amount != amounts.get('dunning_amount'): | ||||
| 			self.dunning_amount = amounts.get('dunning_amount') | ||||
| 		if self.grand_total != amounts.get('grand_total'): | ||||
| 			self.grand_total = amounts.get('grand_total') | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		self.make_gl_entries() | ||||
| 
 | ||||
| 	def on_cancel(self): | ||||
| 		if self.dunning_amount: | ||||
| 			self.ignore_linked_doctypes = ('GL Entry', 'Stock Ledger Entry') | ||||
| 			make_reverse_gl_entries(voucher_type=self.doctype, voucher_no=self.name) | ||||
| 
 | ||||
| 	def make_gl_entries(self): | ||||
| 		if not self.dunning_amount: | ||||
| 			return | ||||
| 		gl_entries = [] | ||||
| 		invoice_fields = ["project", "cost_center", "debit_to", "party_account_currency", "conversion_rate", "cost_center"] | ||||
| 		inv = frappe.db.get_value("Sales Invoice", self.sales_invoice, invoice_fields, as_dict=1) | ||||
| 		accounting_dimensions = get_accounting_dimensions() | ||||
| 		invoice_fields.extend(accounting_dimensions) | ||||
| 		dunning_in_company_currency = flt(self.dunning_amount * inv.conversion_rate) | ||||
| 		default_cost_center = frappe.get_cached_value('Company',  self.company,  'cost_center') | ||||
| 		gl_entries.append( | ||||
| 			self.get_gl_dict({ | ||||
| 				"account": inv.debit_to, | ||||
| 				"party_type": "Customer", | ||||
| 				"party": self.customer, | ||||
| 				"due_date": self.due_date, | ||||
| 				"against": self.income_account, | ||||
| 				"debit": dunning_in_company_currency, | ||||
| 				"debit_in_account_currency": self.dunning_amount, | ||||
| 				"against_voucher": self.name, | ||||
| 				"against_voucher_type": "Dunning", | ||||
| 				"cost_center": inv.cost_center or default_cost_center, | ||||
| 				"project": inv.project | ||||
| 			}, inv.party_account_currency, item=inv) | ||||
| 		) | ||||
| 		gl_entries.append( | ||||
| 			self.get_gl_dict({ | ||||
| 				"account": self.income_account, | ||||
| 				"against": self.customer, | ||||
| 				"credit": dunning_in_company_currency, | ||||
| 				"cost_center": inv.cost_center or default_cost_center, | ||||
| 				"credit_in_account_currency": self.dunning_amount, | ||||
| 				"project": inv.project | ||||
| 			}, item=inv) | ||||
| 		) | ||||
| 		make_gl_entries(gl_entries, cancel=(self.docstatus == 2), update_outstanding="No", merge_entries=False) | ||||
| 
 | ||||
| 
 | ||||
| def resolve_dunning(doc, state): | ||||
| 	for reference in doc.references: | ||||
| 		if reference.reference_doctype == 'Sales Invoice' and reference.outstanding_amount <= 0: | ||||
| 			dunnings = frappe.get_list('Dunning', filters={ | ||||
| 				'sales_invoice': reference.reference_name, 'status': ('!=', 'Resolved')}) | ||||
| 
 | ||||
| 			for dunning in dunnings: | ||||
| 				frappe.db.set_value("Dunning", dunning.name, "status", 'Resolved') | ||||
| 
 | ||||
| def calculate_interest_and_amount(posting_date, outstanding_amount, rate_of_interest, dunning_fee, overdue_days): | ||||
| 	interest_amount = 0 | ||||
| 	if rate_of_interest: | ||||
| 		interest_per_year = rounded(flt(outstanding_amount) * flt(rate_of_interest))/100 | ||||
| 		interest_amount = ( | ||||
|             interest_per_year / days_in_year(get_datetime(posting_date).year)) * int(overdue_days) | ||||
| 		grand_total = flt(outstanding_amount) + flt(interest_amount) + flt(dunning_fee) | ||||
| 	dunning_amount = flt(interest_amount) + flt(dunning_fee) | ||||
| 	return { | ||||
| 		'interest_amount': interest_amount, | ||||
| 		'grand_total': grand_total, | ||||
| 		'dunning_amount': dunning_amount} | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_dunning_letter_text(dunning_type, doc, language=None): | ||||
| 	if isinstance(doc, string_types): | ||||
| 		doc = json.loads(doc) | ||||
| 	if language: | ||||
| 		filters = {'parent': dunning_type, 'language': language} | ||||
| 	else: | ||||
| 		filters = {'parent': dunning_type, 'is_default_language': 1} | ||||
| 	letter_text = frappe.db.get_value('Dunning Letter Text', filters, | ||||
| 		['body_text', 'closing_text', 'language'], as_dict=1) | ||||
| 	if letter_text: | ||||
| 		return { | ||||
| 			'body_text': frappe.render_template(letter_text.body_text, doc), | ||||
| 			'closing_text': frappe.render_template(letter_text.closing_text, doc), | ||||
| 			'language': letter_text.language | ||||
| 		} | ||||
							
								
								
									
										9
									
								
								erpnext/accounts/doctype/dunning/dunning_list.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								erpnext/accounts/doctype/dunning/dunning_list.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,9 @@ | ||||
| frappe.listview_settings["Dunning"] = { | ||||
| 	get_indicator: function (doc) { | ||||
| 		if (doc.status === "Resolved") { | ||||
| 			return [__("Resolved"), "green", "status,=,Resolved"]; | ||||
| 		} else { | ||||
| 			return [__("Unresolved"), "red", "status,=,Unresolved"]; | ||||
| 		} | ||||
| 	}, | ||||
| }; | ||||
							
								
								
									
										100
									
								
								erpnext/accounts/doctype/dunning/test_dunning.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										100
									
								
								erpnext/accounts/doctype/dunning/test_dunning.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,100 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors | ||||
| # See license.txt | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import frappe | ||||
| import unittest | ||||
| from frappe.utils import add_days, today, nowdate | ||||
| from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import unlink_payment_on_cancel_of_invoice | ||||
| from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice_against_cost_center | ||||
| from erpnext.accounts.doctype.dunning.dunning import calculate_interest_and_amount | ||||
| from erpnext.accounts.doctype.payment_entry.test_payment_entry import get_payment_entry | ||||
| 
 | ||||
| 
 | ||||
| class TestDunning(unittest.TestCase): | ||||
| 	@classmethod | ||||
| 	def setUpClass(self): | ||||
| 		create_dunning_type() | ||||
| 		unlink_payment_on_cancel_of_invoice() | ||||
| 
 | ||||
| 	@classmethod | ||||
| 	def tearDownClass(self): | ||||
| 		unlink_payment_on_cancel_of_invoice(0) | ||||
| 
 | ||||
| 	def test_dunning(self): | ||||
| 		dunning = create_dunning() | ||||
| 		amounts = calculate_interest_and_amount( | ||||
| 			dunning.posting_date, dunning.outstanding_amount, dunning.rate_of_interest, dunning.dunning_fee, dunning.overdue_days) | ||||
| 		self.assertEqual(round(amounts.get('interest_amount'), 2), 0.44) | ||||
| 		self.assertEqual(round(amounts.get('dunning_amount'), 2), 20.44) | ||||
| 		self.assertEqual(round(amounts.get('grand_total'), 2), 120.44) | ||||
| 	 | ||||
| 	def test_gl_entries(self): | ||||
| 		dunning = create_dunning() | ||||
| 		dunning.submit() | ||||
| 		gl_entries = frappe.db.sql("""select account, debit, credit | ||||
| 			from `tabGL Entry` where voucher_type='Dunning' and voucher_no=%s | ||||
| 			order by account asc""", dunning.name, as_dict=1) | ||||
| 		self.assertTrue(gl_entries) | ||||
| 		expected_values = dict((d[0], d) for d in [ | ||||
| 			['Debtors - _TC', 20.44, 0.0], | ||||
| 			['Sales - _TC',  0.0, 20.44] | ||||
| 		]) | ||||
| 		for gle in gl_entries: | ||||
| 			self.assertEquals(expected_values[gle.account][0], gle.account) | ||||
| 			self.assertEquals(expected_values[gle.account][1], gle.debit) | ||||
| 			self.assertEquals(expected_values[gle.account][2], gle.credit) | ||||
| 
 | ||||
| 	def test_payment_entry(self): | ||||
| 		dunning = create_dunning() | ||||
| 		dunning.submit() | ||||
| 		pe = get_payment_entry("Dunning", dunning.name) | ||||
| 		pe.reference_no = "1" | ||||
| 		pe.reference_date = nowdate() | ||||
| 		pe.paid_from_account_currency = dunning.currency | ||||
| 		pe.paid_to_account_currency = dunning.currency | ||||
| 		pe.source_exchange_rate = 1 | ||||
| 		pe.target_exchange_rate = 1 | ||||
| 		pe.insert() | ||||
| 		pe.submit() | ||||
| 		si_doc = frappe.get_doc('Sales Invoice', dunning.sales_invoice) | ||||
| 		self.assertEqual(si_doc.outstanding_amount, 0) | ||||
| 
 | ||||
| 
 | ||||
| def create_dunning(): | ||||
| 	posting_date = add_days(today(), -20) | ||||
| 	due_date = add_days(today(), -15) | ||||
| 	sales_invoice = create_sales_invoice_against_cost_center( | ||||
| 		posting_date=posting_date, due_date=due_date, status='Overdue') | ||||
| 	dunning_type = frappe.get_doc("Dunning Type", 'First Notice') | ||||
| 	dunning = frappe.new_doc("Dunning") | ||||
| 	dunning.sales_invoice = sales_invoice.name | ||||
| 	dunning.customer_name = sales_invoice.customer_name | ||||
| 	dunning.outstanding_amount = sales_invoice.outstanding_amount | ||||
| 	dunning.debit_to = sales_invoice.debit_to | ||||
| 	dunning.currency = sales_invoice.currency | ||||
| 	dunning.company = sales_invoice.company | ||||
| 	dunning.posting_date = nowdate() | ||||
| 	dunning.due_date = sales_invoice.due_date | ||||
| 	dunning.dunning_type = 'First Notice' | ||||
| 	dunning.rate_of_interest = dunning_type.rate_of_interest | ||||
| 	dunning.dunning_fee = dunning_type.dunning_fee | ||||
| 	dunning.save() | ||||
| 	return dunning | ||||
| 
 | ||||
| def create_dunning_type(): | ||||
| 	dunning_type = frappe.new_doc("Dunning Type") | ||||
| 	dunning_type.dunning_type = 'First Notice' | ||||
| 	dunning_type.start_day = 10 | ||||
| 	dunning_type.end_day = 20 | ||||
| 	dunning_type.dunning_fee = 20 | ||||
| 	dunning_type.rate_of_interest = 8 | ||||
| 	dunning_type.append( | ||||
| 		"dunning_letter_text", { | ||||
| 			'language': 'en', | ||||
| 			'body_text': 'We have still not received payment for our invoice ', | ||||
| 			'closing_text': 'We kindly request that you pay the outstanding amount immediately, including interest and late fees.' | ||||
| 		} | ||||
| 	) | ||||
| 	dunning_type.save() | ||||
| @ -0,0 +1,70 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2019-12-06 04:25:40.215625", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "language", | ||||
|   "is_default_language", | ||||
|   "section_break_4", | ||||
|   "body_text", | ||||
|   "closing_text", | ||||
|   "section_break_7", | ||||
|   "body_and_closing_text_help" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "language", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Language", | ||||
|    "options": "Language" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "is_default_language", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Is Default Language" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_4", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "description": "Letter or Email Body Text", | ||||
|    "fieldname": "body_text", | ||||
|    "fieldtype": "Text Editor", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Body Text" | ||||
|   }, | ||||
|   { | ||||
|    "description": "Letter or Email Closing Text", | ||||
|    "fieldname": "closing_text", | ||||
|    "fieldtype": "Text Editor", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Closing Text" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_7", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "body_and_closing_text_help", | ||||
|    "fieldtype": "HTML", | ||||
|    "label": "Body and Closing Text Help", | ||||
|    "options": "<h4>Body Text and Closing Text Example</h4>\n\n<div>We have noticed that you have not yet paid invoice {{sales_invoice}} for {{frappe.db.get_value(\"Currency\", currency, \"symbol\")}} {{outstanding_amount}}. This is a friendly reminder that the invoice was due on {{due_date}}. Please pay the amount due immediately to avoid any further dunning cost.</div>\n\n<h4>How to get fieldnames</h4>\n\n<p>The fieldnames you can use in your template are the fields in the document. You can find out the fields of any documents via Setup > Customize Form View and selecting the document type (e.g. Sales Invoice)</p>\n\n<h4>Templating</h4>\n\n<p>Templates are compiled using the Jinja Templating Language. To learn more about Jinja, <a class=\"strong\" href=\"http://jinja.pocoo.org/docs/dev/templates/\">read this documentation.</a></p>" | ||||
|   } | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-07-14 18:02:35.988958", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Dunning Letter Text", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -0,0 +1,10 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| # import frappe | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class DunningLetterText(Document): | ||||
| 	pass | ||||
							
								
								
									
										8
									
								
								erpnext/accounts/doctype/dunning_type/dunning_type.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								erpnext/accounts/doctype/dunning_type/dunning_type.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,8 @@ | ||||
| // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
 | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| frappe.ui.form.on('Dunning Type', { | ||||
| 	// refresh: function(frm) {
 | ||||
| 
 | ||||
| 	// }
 | ||||
| }); | ||||
							
								
								
									
										129
									
								
								erpnext/accounts/doctype/dunning_type/dunning_type.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										129
									
								
								erpnext/accounts/doctype/dunning_type/dunning_type.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,129 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "allow_rename": 1, | ||||
|  "autoname": "field:dunning_type", | ||||
|  "creation": "2019-12-04 04:59:08.003664", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "dunning_type", | ||||
|   "overdue_interval_section", | ||||
|   "start_day", | ||||
|   "column_break_4", | ||||
|   "end_day", | ||||
|   "section_break_6", | ||||
|   "dunning_fee", | ||||
|   "column_break_8", | ||||
|   "rate_of_interest", | ||||
|   "text_block_section", | ||||
|   "dunning_letter_text" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "dunning_type", | ||||
|    "fieldtype": "Data", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Dunning Type", | ||||
|    "reqd": 1, | ||||
|    "unique": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "dunning_fee", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Dunning Fee" | ||||
|   }, | ||||
|   { | ||||
|    "description": "This section allows the user to set the Body and Closing text of the Dunning Letter for the Dunning Type based on language, which can be used in Print.", | ||||
|    "fieldname": "text_block_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Dunning Letter" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "dunning_letter_text", | ||||
|    "fieldtype": "Table", | ||||
|    "options": "Dunning Letter Text" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_4", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_6", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_8", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "overdue_interval_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Overdue Interval" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "start_day", | ||||
|    "fieldtype": "Int", | ||||
|    "label": "Start Day" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "end_day", | ||||
|    "fieldtype": "Int", | ||||
|    "label": "End Day" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "rate_of_interest", | ||||
|    "fieldtype": "Float", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Rate of Interest (%) Yearly" | ||||
|   } | ||||
|  ], | ||||
|  "links": [], | ||||
|  "modified": "2020-07-15 17:14:17.835074", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Dunning Type", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "System Manager", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Accounts Manager", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Administrator", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   } | ||||
|  ], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
							
								
								
									
										10
									
								
								erpnext/accounts/doctype/dunning_type/dunning_type.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								erpnext/accounts/doctype/dunning_type/dunning_type.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| # import frappe | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class DunningType(Document): | ||||
| 	pass | ||||
							
								
								
									
										10
									
								
								erpnext/accounts/doctype/dunning_type/test_dunning_type.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								erpnext/accounts/doctype/dunning_type/test_dunning_type.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,10 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors | ||||
| # See license.txt | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| # import frappe | ||||
| import unittest | ||||
| 
 | ||||
| class TestDunningType(unittest.TestCase): | ||||
| 	pass | ||||
| @ -1,426 +1,123 @@ | ||||
| { | ||||
|  "allow_copy": 0,  | ||||
|  "allow_guest_to_view": 0,  | ||||
|  "allow_import": 0,  | ||||
|  "allow_rename": 0,  | ||||
|  "autoname": "",  | ||||
|  "beta": 0,  | ||||
|  "creation": "2018-01-23 05:40:18.117583",  | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "",  | ||||
|  "editable_grid": 1,  | ||||
|  "engine": "InnoDB",  | ||||
|  "creation": "2018-01-23 05:40:18.117583", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "loyalty_program", | ||||
|   "loyalty_program_tier", | ||||
|   "customer", | ||||
|   "invoice_type", | ||||
|   "invoice", | ||||
|   "redeem_against", | ||||
|   "loyalty_points", | ||||
|   "purchase_amount", | ||||
|   "expiry_date", | ||||
|   "posting_date", | ||||
|   "company" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "loyalty_program",  | ||||
|    "fieldtype": "Link",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Loyalty Program",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Loyalty Program",  | ||||
|    "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": "loyalty_program", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Loyalty Program", | ||||
|    "options": "Loyalty Program" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "loyalty_program_tier",  | ||||
|    "fieldtype": "Data",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Loyalty Program Tier",  | ||||
|    "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": "loyalty_program_tier", | ||||
|    "fieldtype": "Data", | ||||
|    "label": "Loyalty Program Tier" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "customer",  | ||||
|    "fieldtype": "Link",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 1,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Customer",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Customer",  | ||||
|    "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": "customer", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Customer", | ||||
|    "options": "Customer" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "sales_invoice",  | ||||
|    "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": "Sales Invoice",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Sales Invoice",  | ||||
|    "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": "redeem_against", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Redeem Against", | ||||
|    "options": "Loyalty Point Entry" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "redeem_against",  | ||||
|    "fieldtype": "Link",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Redeem Against",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Loyalty Point Entry",  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "loyalty_points", | ||||
|    "fieldtype": "Int", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Loyalty Points" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "loyalty_points",  | ||||
|    "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": "Loyalty Points",  | ||||
|    "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": "purchase_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Purchase Amount" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "purchase_amount",  | ||||
|    "fieldtype": "Currency",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Purchase Amount",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "expiry_date", | ||||
|    "fieldtype": "Date", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Expiry Date" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "expiry_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": "Expiry 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 | ||||
|   },  | ||||
|    "fieldname": "posting_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Posting Date" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "posting_date",  | ||||
|    "fieldtype": "Date",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Posting 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 | ||||
|   },  | ||||
|    "fieldname": "company", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Company", | ||||
|    "options": "Company" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "company",  | ||||
|    "fieldtype": "Link",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Company",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Company",  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0 | ||||
|    "fieldname": "invoice_type", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Invoice Type", | ||||
|    "options": "DocType" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "invoice", | ||||
|    "fieldtype": "Dynamic Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Invoice", | ||||
|    "options": "invoice_type" | ||||
|   } | ||||
|  ],  | ||||
|  "has_web_view": 0,  | ||||
|  "hide_heading": 0,  | ||||
|  "hide_toolbar": 0,  | ||||
|  "idx": 0,  | ||||
|  "image_view": 0,  | ||||
|  "in_create": 1,  | ||||
|  "is_submittable": 0,  | ||||
|  "issingle": 0,  | ||||
|  "istable": 0,  | ||||
|  "max_attachments": 0,  | ||||
|  "modified": "2018-08-29 16:05:22.810347",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Accounts",  | ||||
|  "name": "Loyalty Point Entry",  | ||||
|  "name_case": "",  | ||||
|  "owner": "Administrator",  | ||||
|  ], | ||||
|  "in_create": 1, | ||||
|  "modified": "2020-01-30 17:27:55.964242", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Loyalty Point Entry", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "amend": 0,  | ||||
|    "cancel": 0,  | ||||
|    "create": 0,  | ||||
|    "delete": 0,  | ||||
|    "email": 1,  | ||||
|    "export": 1,  | ||||
|    "if_owner": 0,  | ||||
|    "import": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print": 1,  | ||||
|    "read": 1,  | ||||
|    "report": 1,  | ||||
|    "role": "Auditor",  | ||||
|    "set_user_permissions": 0,  | ||||
|    "share": 0,  | ||||
|    "submit": 0,  | ||||
|    "write": 0 | ||||
|   },  | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Auditor" | ||||
|   }, | ||||
|   { | ||||
|    "amend": 0,  | ||||
|    "cancel": 0,  | ||||
|    "create": 0,  | ||||
|    "delete": 0,  | ||||
|    "email": 1,  | ||||
|    "export": 1,  | ||||
|    "if_owner": 0,  | ||||
|    "import": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print": 1,  | ||||
|    "read": 1,  | ||||
|    "report": 1,  | ||||
|    "role": "Accounts Manager",  | ||||
|    "set_user_permissions": 0,  | ||||
|    "share": 0,  | ||||
|    "submit": 0,  | ||||
|    "write": 0 | ||||
|   },  | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Accounts Manager" | ||||
|   }, | ||||
|   { | ||||
|    "amend": 0,  | ||||
|    "cancel": 0,  | ||||
|    "create": 0,  | ||||
|    "delete": 0,  | ||||
|    "email": 1,  | ||||
|    "export": 1,  | ||||
|    "if_owner": 0,  | ||||
|    "import": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print": 1,  | ||||
|    "read": 1,  | ||||
|    "report": 1,  | ||||
|    "role": "Accounts User",  | ||||
|    "set_user_permissions": 0,  | ||||
|    "share": 0,  | ||||
|    "submit": 0,  | ||||
|    "write": 0 | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Accounts User" | ||||
|   } | ||||
|  ],  | ||||
|  "quick_entry": 1,  | ||||
|  "read_only": 0,  | ||||
|  "read_only_onload": 0,  | ||||
|  "show_name_in_global_search": 0,  | ||||
|  "sort_field": "modified",  | ||||
|  "sort_order": "DESC",  | ||||
|  "title_field": "customer",  | ||||
|  "track_changes": 1,  | ||||
|  "track_seen": 0 | ||||
|  ], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "title_field": "customer", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -18,7 +18,7 @@ def get_loyalty_point_entries(customer, loyalty_program, company, expiry_date=No | ||||
| 		date = today() | ||||
| 
 | ||||
| 	return frappe.db.sql(''' | ||||
| 		select name, loyalty_points, expiry_date, loyalty_program_tier, sales_invoice | ||||
| 		select name, loyalty_points, expiry_date, loyalty_program_tier, invoice_type, invoice | ||||
| 		from `tabLoyalty Point Entry` | ||||
| 		where customer=%s and loyalty_program=%s | ||||
| 			and expiry_date>=%s and loyalty_points>0 and company=%s | ||||
|  | ||||
| @ -36,7 +36,8 @@ def get_loyalty_details(customer, loyalty_program, expiry_date=None, company=Non | ||||
| 		return {"loyalty_points": 0, "total_spent": 0} | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, silent=False, include_expired_entry=False, current_transaction_amount=0): | ||||
| def get_loyalty_program_details_with_points(customer, loyalty_program=None, expiry_date=None, company=None, \ | ||||
| 		silent=False, include_expired_entry=False, current_transaction_amount=0): | ||||
| 	lp_details = get_loyalty_program_details(customer, loyalty_program, company=company, silent=silent) | ||||
| 	loyalty_program = frappe.get_doc("Loyalty Program", loyalty_program) | ||||
| 	lp_details.update(get_loyalty_details(customer, loyalty_program.name, expiry_date, company, include_expired_entry)) | ||||
| @ -59,10 +60,10 @@ def get_loyalty_program_details(customer, loyalty_program=None, expiry_date=None | ||||
| 	if not loyalty_program: | ||||
| 		loyalty_program = frappe.db.get_value("Customer", customer, "loyalty_program") | ||||
| 
 | ||||
| 		if not (loyalty_program or silent): | ||||
| 		if not loyalty_program and not silent: | ||||
| 			frappe.throw(_("Customer isn't enrolled in any Loyalty Program")) | ||||
| 		elif silent and not loyalty_program: | ||||
| 			return frappe._dict({"loyalty_program": None}) | ||||
| 			return frappe._dict({"loyalty_programs": None}) | ||||
| 
 | ||||
| 	if not company: | ||||
| 		company = frappe.db.get_default("company") or frappe.get_all("Company")[0].name | ||||
|  | ||||
| @ -27,7 +27,7 @@ class TestLoyaltyProgram(unittest.TestCase): | ||||
| 		customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) | ||||
| 		earned_points = get_points_earned(si_original) | ||||
| 
 | ||||
| 		lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer}) | ||||
| 		lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) | ||||
| 
 | ||||
| 		self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program) | ||||
| 		self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier) | ||||
| @ -42,8 +42,8 @@ class TestLoyaltyProgram(unittest.TestCase): | ||||
| 
 | ||||
| 		earned_after_redemption = get_points_earned(si_redeem) | ||||
| 
 | ||||
| 		lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'redeem_against': lpe.name}) | ||||
| 		lpe_earn = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]}) | ||||
| 		lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name}) | ||||
| 		lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]}) | ||||
| 
 | ||||
| 		self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption) | ||||
| 		self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points)) | ||||
| @ -66,7 +66,7 @@ class TestLoyaltyProgram(unittest.TestCase): | ||||
| 
 | ||||
| 		earned_points = get_points_earned(si_original) | ||||
| 
 | ||||
| 		lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer}) | ||||
| 		lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) | ||||
| 
 | ||||
| 		self.assertEqual(si_original.get('loyalty_program'), customer.loyalty_program) | ||||
| 		self.assertEqual(lpe.get('loyalty_program_tier'), customer.loyalty_program_tier) | ||||
| @ -82,8 +82,8 @@ class TestLoyaltyProgram(unittest.TestCase): | ||||
| 		customer = frappe.get_doc('Customer', {"customer_name": "Test Loyalty Customer"}) | ||||
| 		earned_after_redemption = get_points_earned(si_redeem) | ||||
| 
 | ||||
| 		lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'redeem_against': lpe.name}) | ||||
| 		lpe_earn = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]}) | ||||
| 		lpe_redeem = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'redeem_against': lpe.name}) | ||||
| 		lpe_earn = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_redeem.name, 'name': ['!=', lpe_redeem.name]}) | ||||
| 
 | ||||
| 		self.assertEqual(lpe_earn.loyalty_points, earned_after_redemption) | ||||
| 		self.assertEqual(lpe_redeem.loyalty_points, (-1*earned_points)) | ||||
| @ -101,7 +101,7 @@ class TestLoyaltyProgram(unittest.TestCase): | ||||
| 		si.insert() | ||||
| 		si.submit() | ||||
| 
 | ||||
| 		lpe = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si.name, 'customer': si.customer}) | ||||
| 		lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si.name, 'customer': si.customer}) | ||||
| 		self.assertEqual(True, not (lpe is None)) | ||||
| 
 | ||||
| 		# cancelling sales invoice | ||||
| @ -118,7 +118,7 @@ class TestLoyaltyProgram(unittest.TestCase): | ||||
| 		si_original.submit() | ||||
| 
 | ||||
| 		earned_points = get_points_earned(si_original) | ||||
| 		lpe_original = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer}) | ||||
| 		lpe_original = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) | ||||
| 		self.assertEqual(lpe_original.loyalty_points, earned_points) | ||||
| 
 | ||||
| 		# create sales invoice return | ||||
| @ -130,10 +130,10 @@ class TestLoyaltyProgram(unittest.TestCase): | ||||
| 		si_return.submit() | ||||
| 
 | ||||
| 		# fetch original invoice again as its status would have been updated | ||||
| 		si_original = frappe.get_doc('Sales Invoice', lpe_original.sales_invoice) | ||||
| 		si_original = frappe.get_doc('Sales Invoice', lpe_original.invoice) | ||||
| 
 | ||||
| 		earned_points = get_points_earned(si_original) | ||||
| 		lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'sales_invoice': si_original.name, 'customer': si_original.customer}) | ||||
| 		lpe_after_return = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'Sales Invoice', 'invoice': si_original.name, 'customer': si_original.customer}) | ||||
| 		self.assertEqual(lpe_after_return.loyalty_points, earned_points) | ||||
| 		self.assertEqual(True, (lpe_original.loyalty_points > lpe_after_return.loyalty_points)) | ||||
| 
 | ||||
|  | ||||
| @ -90,7 +90,7 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 
 | ||||
| 		frm.set_query("reference_doctype", "references", function() { | ||||
| 			if (frm.doc.party_type=="Customer") { | ||||
| 				var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry"]; | ||||
| 				var doctypes = ["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"]; | ||||
| 			} else if (frm.doc.party_type=="Supplier") { | ||||
| 				var doctypes = ["Purchase Order", "Purchase Invoice", "Journal Entry"]; | ||||
| 			} else if (frm.doc.party_type=="Employee") { | ||||
| @ -125,7 +125,7 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 			const child = locals[cdt][cdn]; | ||||
| 			const filters = {"docstatus": 1, "company": doc.company}; | ||||
| 			const party_type_doctypes = ['Sales Invoice', 'Sales Order', 'Purchase Invoice', | ||||
| 				'Purchase Order', 'Expense Claim', 'Fees']; | ||||
| 				'Purchase Order', 'Expense Claim', 'Fees', 'Dunning']; | ||||
| 
 | ||||
| 			if (in_list(party_type_doctypes, child.reference_doctype)) { | ||||
| 				filters[doc.party_type.toLowerCase()] = doc.party; | ||||
| @ -863,10 +863,10 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 			} | ||||
| 
 | ||||
| 			if(frm.doc.party_type=="Customer" && | ||||
| 				!in_list(["Sales Order", "Sales Invoice", "Journal Entry"], row.reference_doctype) | ||||
| 				!in_list(["Sales Order", "Sales Invoice", "Journal Entry", "Dunning"], row.reference_doctype) | ||||
| 			) { | ||||
| 				frappe.model.set_value(row.doctype, row.name, "reference_doctype", null); | ||||
| 				frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice or Journal Entry", [row.idx])); | ||||
| 				frappe.msgprint(__("Row #{0}: Reference Document Type must be one of Sales Order, Sales Invoice, Journal Entry or Dunning", [row.idx])); | ||||
| 				return false; | ||||
| 			} | ||||
| 
 | ||||
|  | ||||
| @ -199,8 +199,8 @@ class PaymentEntry(AccountsController): | ||||
| 
 | ||||
| 	def validate_account_type(self, account, account_types): | ||||
| 		account_type = frappe.db.get_value("Account", account, "account_type") | ||||
| 		if account_type not in account_types: | ||||
| 			frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types))) | ||||
| 		# if account_type not in account_types: | ||||
| 		# 	frappe.throw(_("Account Type for {0} must be {1}").format(account, comma_or(account_types))) | ||||
| 
 | ||||
| 	def set_exchange_rate(self): | ||||
| 		if self.paid_from and not self.source_exchange_rate: | ||||
| @ -223,7 +223,7 @@ class PaymentEntry(AccountsController): | ||||
| 		if self.party_type == "Student": | ||||
| 			valid_reference_doctypes = ("Fees") | ||||
| 		elif self.party_type == "Customer": | ||||
| 			valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry") | ||||
| 			valid_reference_doctypes = ("Sales Order", "Sales Invoice", "Journal Entry", "Dunning") | ||||
| 		elif self.party_type == "Supplier": | ||||
| 			valid_reference_doctypes = ("Purchase Order", "Purchase Invoice", "Journal Entry") | ||||
| 		elif self.party_type == "Employee": | ||||
| @ -897,6 +897,10 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre | ||||
| 		total_amount = ref_doc.get("grand_total") | ||||
| 		exchange_rate = 1 | ||||
| 		outstanding_amount = ref_doc.get("outstanding_amount") | ||||
| 	if reference_doctype == "Dunning": | ||||
| 		total_amount = ref_doc.get("dunning_amount") | ||||
| 		exchange_rate = 1 | ||||
| 		outstanding_amount = ref_doc.get("dunning_amount") | ||||
| 	elif reference_doctype == "Journal Entry" and ref_doc.docstatus == 1: | ||||
| 		total_amount = ref_doc.get("total_amount") | ||||
| 		if ref_doc.multi_currency: | ||||
| @ -907,7 +911,7 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre | ||||
| 	elif reference_doctype != "Journal Entry": | ||||
| 		if party_account_currency == company_currency: | ||||
| 			if ref_doc.doctype == "Expense Claim": | ||||
| 				total_amount = ref_doc.total_sanctioned_amount | ||||
| 				total_amount = flt(ref_doc.total_sanctioned_amount) + flt(ref_doc.total_taxes_and_charges) | ||||
| 			elif ref_doc.doctype == "Employee Advance": | ||||
| 				total_amount = ref_doc.advance_amount | ||||
| 			else: | ||||
| @ -925,8 +929,8 @@ def get_reference_details(reference_doctype, reference_name, party_account_curre | ||||
| 			outstanding_amount = ref_doc.get("outstanding_amount") | ||||
| 			bill_no = ref_doc.get("bill_no") | ||||
| 		elif reference_doctype == "Expense Claim": | ||||
| 			outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) \ | ||||
| 				- flt(ref_doc.get("total_amount+reimbursed")) - flt(ref_doc.get("total_advance_amount")) | ||||
| 			outstanding_amount = flt(ref_doc.get("total_sanctioned_amount")) + flt(ref_doc.get("total_taxes_and_charges"))\ | ||||
| 				- flt(ref_doc.get("total_amount_reimbursed")) - flt(ref_doc.get("total_advance_amount")) | ||||
| 		elif reference_doctype == "Employee Advance": | ||||
| 			outstanding_amount = ref_doc.advance_amount - flt(ref_doc.paid_amount) | ||||
| 		else: | ||||
| @ -951,7 +955,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= | ||||
| 	if dt in ("Sales Order", "Purchase Order") and flt(doc.per_billed, 2) > 0: | ||||
| 		frappe.throw(_("Can only make payment against unbilled {0}").format(dt)) | ||||
| 
 | ||||
| 	if dt in ("Sales Invoice", "Sales Order"): | ||||
| 	if dt in ("Sales Invoice", "Sales Order", "Dunning"): | ||||
| 		party_type = "Customer" | ||||
| 	elif dt in ("Purchase Invoice", "Purchase Order"): | ||||
| 		party_type = "Supplier" | ||||
| @ -980,7 +984,7 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= | ||||
| 		party_account_currency = doc.get("party_account_currency") or get_account_currency(party_account) | ||||
| 
 | ||||
| 	# payment type | ||||
| 	if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees") and doc.outstanding_amount > 0)) \ | ||||
| 	if (dt == "Sales Order" or (dt in ("Sales Invoice", "Fees", "Dunning") and doc.outstanding_amount > 0)) \ | ||||
| 		or (dt=="Purchase Invoice" and doc.outstanding_amount < 0): | ||||
| 			payment_type = "Receive" | ||||
| 	else: | ||||
| @ -1006,6 +1010,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= | ||||
| 	elif dt == "Fees": | ||||
| 		grand_total = doc.grand_total | ||||
| 		outstanding_amount = doc.outstanding_amount | ||||
| 	elif dt == "Dunning": | ||||
| 		grand_total = doc.grand_total | ||||
| 		outstanding_amount = doc.grand_total | ||||
| 	else: | ||||
| 		if party_account_currency == doc.company_currency: | ||||
| 			grand_total = flt(doc.get("base_rounded_total") or doc.base_grand_total) | ||||
| @ -1075,15 +1082,35 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= | ||||
| 			for reference in get_reference_as_per_payment_terms(doc.payment_schedule, dt, dn, doc, grand_total, outstanding_amount): | ||||
| 				pe.append('references', reference) | ||||
| 		else: | ||||
| 			pe.append("references", { | ||||
| 				'reference_doctype': dt, | ||||
| 				'reference_name': dn, | ||||
| 				"bill_no": doc.get("bill_no"), | ||||
| 				"due_date": doc.get("due_date"), | ||||
| 				'total_amount': grand_total, | ||||
| 				'outstanding_amount': outstanding_amount, | ||||
| 				'allocated_amount': outstanding_amount | ||||
| 			}) | ||||
| 			if dt == "Dunning": | ||||
| 				pe.append("references", { | ||||
| 					'reference_doctype': 'Sales Invoice', | ||||
| 					'reference_name': doc.get('sales_invoice'), | ||||
| 					"bill_no": doc.get("bill_no"), | ||||
| 					"due_date": doc.get("due_date"), | ||||
| 					'total_amount': doc.get('outstanding_amount'), | ||||
| 					'outstanding_amount': doc.get('outstanding_amount'), | ||||
| 					'allocated_amount': doc.get('outstanding_amount') | ||||
| 				}) | ||||
| 				pe.append("references", { | ||||
| 					'reference_doctype': dt, | ||||
| 					'reference_name': dn, | ||||
| 					"bill_no": doc.get("bill_no"), | ||||
| 					"due_date": doc.get("due_date"), | ||||
| 					'total_amount': doc.get('dunning_amount'), | ||||
| 					'outstanding_amount': doc.get('dunning_amount'), | ||||
| 					'allocated_amount': doc.get('dunning_amount') | ||||
| 				}) | ||||
| 			else:	 | ||||
| 				pe.append("references", { | ||||
| 					'reference_doctype': dt, | ||||
| 					'reference_name': dn, | ||||
| 					"bill_no": doc.get("bill_no"), | ||||
| 					"due_date": doc.get("due_date"), | ||||
| 					'total_amount': grand_total, | ||||
| 					'outstanding_amount': outstanding_amount, | ||||
| 					'allocated_amount': outstanding_amount | ||||
| 				}) | ||||
| 
 | ||||
| 	pe.setup_party_account_field() | ||||
| 	pe.set_missing_values() | ||||
| @ -1172,4 +1199,4 @@ def make_payment_order(source_name, target_doc=None): | ||||
| 
 | ||||
| 	}, target_doc, set_missing_values) | ||||
| 
 | ||||
| 	return doclist | ||||
| 	return doclist | ||||
| @ -73,6 +73,10 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext | ||||
| 				}; | ||||
| 			} | ||||
| 		}); | ||||
| 
 | ||||
| 		this.frm.set_value('party_type', ''); | ||||
| 		this.frm.set_value('party', ''); | ||||
| 		this.frm.set_value('receivable_payable_account', ''); | ||||
| 	}, | ||||
| 
 | ||||
| 	refresh: function() { | ||||
|  | ||||
| @ -48,7 +48,8 @@ class PaymentReconciliation(Document): | ||||
| 			select | ||||
| 				"Journal Entry" as reference_type, t1.name as reference_name, | ||||
| 				t1.posting_date, t1.remark as remarks, t2.name as reference_row, | ||||
| 				{dr_or_cr} as amount, t2.is_advance | ||||
| 				{dr_or_cr} as amount, t2.is_advance, | ||||
| 				t2.account_currency as currency | ||||
| 			from | ||||
| 				`tabJournal Entry` t1, `tabJournal Entry Account` t2 | ||||
| 			where | ||||
| @ -88,7 +89,8 @@ class PaymentReconciliation(Document): | ||||
| 			if self.party_type == 'Customer' else "Purchase Invoice") | ||||
| 
 | ||||
| 		return frappe.db.sql(""" SELECT `tab{doc}`.name as reference_name, %(voucher_type)s as reference_type, | ||||
| 				(sum(`tabGL Entry`.{dr_or_cr}) - sum(`tabGL Entry`.{reconciled_dr_or_cr})) as amount | ||||
| 				(sum(`tabGL Entry`.{dr_or_cr}) - sum(`tabGL Entry`.{reconciled_dr_or_cr})) as amount, | ||||
| 				account_currency as currency | ||||
| 			FROM `tab{doc}`, `tabGL Entry` | ||||
| 			WHERE | ||||
| 				(`tab{doc}`.name = `tabGL Entry`.against_voucher or `tab{doc}`.name = `tabGL Entry`.voucher_no) | ||||
| @ -141,6 +143,7 @@ class PaymentReconciliation(Document): | ||||
| 			ent.invoice_number = e.get('voucher_no') | ||||
| 			ent.invoice_date = e.get('posting_date') | ||||
| 			ent.amount = flt(e.get('invoice_amount')) | ||||
| 			ent.currency = e.get('currency') | ||||
| 			ent.outstanding_amount = e.get('outstanding_amount') | ||||
| 
 | ||||
| 	def reconcile(self, args): | ||||
| @ -269,11 +272,14 @@ def reconcile_dr_cr_note(dr_cr_notes, company): | ||||
| 		reconcile_dr_or_cr = ('debit_in_account_currency' | ||||
| 			if d.dr_or_cr == 'credit_in_account_currency' else 'credit_in_account_currency') | ||||
| 
 | ||||
| 		company_currency = erpnext.get_company_currency(company) | ||||
| 
 | ||||
| 		jv = frappe.get_doc({ | ||||
| 			"doctype": "Journal Entry", | ||||
| 			"voucher_type": voucher_type, | ||||
| 			"posting_date": today(), | ||||
| 			"company": company, | ||||
| 			"multi_currency": 1 if d.currency != company_currency else 0, | ||||
| 			"accounts": [ | ||||
| 				{ | ||||
| 					'account': d.account, | ||||
|  | ||||
| @ -1,183 +1,80 @@ | ||||
| { | ||||
|  "allow_copy": 0,  | ||||
|  "allow_import": 0,  | ||||
|  "allow_rename": 0,  | ||||
|  "beta": 0,  | ||||
|  "creation": "2014-07-09 16:14:23.672922",  | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "",  | ||||
|  "editable_grid": 1,  | ||||
|  "actions": [], | ||||
|  "creation": "2014-07-09 16:14:23.672922", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "invoice_type", | ||||
|   "invoice_number", | ||||
|   "invoice_date", | ||||
|   "col_break1", | ||||
|   "amount", | ||||
|   "outstanding_amount", | ||||
|   "currency" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "fieldname": "invoice_type",  | ||||
|    "fieldtype": "Select",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 1,  | ||||
|    "label": "Invoice Type",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Sales Invoice\nPurchase Invoice\nJournal Entry",  | ||||
|    "permlevel": 0,  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 1,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "invoice_type", | ||||
|    "fieldtype": "Select", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Invoice Type", | ||||
|    "options": "Sales Invoice\nPurchase Invoice\nJournal Entry", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "fieldname": "invoice_number",  | ||||
|    "fieldtype": "Dynamic Link",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 1,  | ||||
|    "label": "Invoice Number",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "invoice_type",  | ||||
|    "permlevel": 0,  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 1,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "invoice_number", | ||||
|    "fieldtype": "Dynamic Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Invoice Number", | ||||
|    "options": "invoice_type", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "fieldname": "invoice_date",  | ||||
|    "fieldtype": "Date",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 1,  | ||||
|    "label": "Invoice Date",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 1,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "invoice_date", | ||||
|    "fieldtype": "Date", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Invoice Date", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "fieldname": "col_break1",  | ||||
|    "fieldtype": "Column Break",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "label": "",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "col_break1", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "fieldname": "amount",  | ||||
|    "fieldtype": "Currency",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 1,  | ||||
|    "label": "Amount",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 1,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Amount", | ||||
|    "options": "currency", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "fieldname": "outstanding_amount",  | ||||
|    "fieldtype": "Currency",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_list_view": 1,  | ||||
|    "label": "Outstanding Amount",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 1,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "unique": 0 | ||||
|    "fieldname": "outstanding_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Outstanding Amount", | ||||
|    "options": "currency", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "currency", | ||||
|    "fieldtype": "Link", | ||||
|    "hidden": 1, | ||||
|    "label": "Currency", | ||||
|    "options": "Currency" | ||||
|   } | ||||
|  ],  | ||||
|  "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": "2016-07-11 03:28:03.588476",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Accounts",  | ||||
|  "name": "Payment Reconciliation Invoice",  | ||||
|  "name_case": "",  | ||||
|  "owner": "Administrator",  | ||||
|  "permissions": [],  | ||||
|  "quick_entry": 1,  | ||||
|  "read_only": 0,  | ||||
|  "read_only_onload": 0,  | ||||
|  "sort_field": "modified",  | ||||
|  "sort_order": "DESC",  | ||||
|  "track_seen": 0 | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-07-19 18:12:27.964073", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Payment Reconciliation Invoice", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -1,7 +1,9 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2014-07-09 16:13:35.452759", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "reference_type", | ||||
|   "reference_name", | ||||
| @ -16,7 +18,8 @@ | ||||
|   "difference_account", | ||||
|   "difference_amount", | ||||
|   "sec_break1", | ||||
|   "remark" | ||||
|   "remark", | ||||
|   "currency" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
| @ -73,6 +76,7 @@ | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Amount", | ||||
|    "options": "currency", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
| @ -81,6 +85,7 @@ | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Allocated amount", | ||||
|    "options": "currency", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
| @ -106,16 +111,25 @@ | ||||
|    "fieldname": "difference_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Difference Amount", | ||||
|    "options": "currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_10", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "currency", | ||||
|    "fieldtype": "Link", | ||||
|    "hidden": 1, | ||||
|    "label": "Currency", | ||||
|    "options": "Currency" | ||||
|   } | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "modified": "2019-06-24 00:08:11.150796", | ||||
|  "links": [], | ||||
|  "modified": "2020-07-19 18:12:41.682347", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Payment Reconciliation Payment", | ||||
|  | ||||
| @ -211,7 +211,7 @@ | ||||
|    "label": "IBAN" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "bank.branch_code", | ||||
|    "fetch_from": "bank_account.branch_code", | ||||
|    "fetch_if_empty": 1, | ||||
|    "fieldname": "branch_code", | ||||
|    "fieldtype": "Read Only", | ||||
| @ -352,7 +352,7 @@ | ||||
|  "in_create": 1, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-29 17:38:49.392713", | ||||
|  "modified": "2020-07-17 14:06:42.185763", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Payment Request", | ||||
|  | ||||
| @ -140,9 +140,6 @@ class PaymentRequest(Document): | ||||
| 		}) | ||||
| 
 | ||||
| 	def set_as_paid(self): | ||||
| 		if frappe.session.user == "Guest": | ||||
| 			frappe.set_user("Administrator") | ||||
| 
 | ||||
| 		payment_entry = self.create_payment_entry() | ||||
| 		self.make_invoice() | ||||
| 
 | ||||
| @ -254,7 +251,7 @@ class PaymentRequest(Document): | ||||
| 
 | ||||
| 		if status in ["Authorized", "Completed"]: | ||||
| 			redirect_to = None | ||||
| 			self.run_method("set_as_paid") | ||||
| 			self.set_as_paid() | ||||
| 
 | ||||
| 			# if shopping cart enabled and in session | ||||
| 			if (shopping_cart_settings.enabled and hasattr(frappe.local, "session") | ||||
|  | ||||
| @ -12,15 +12,15 @@ | ||||
| 								</thead> | ||||
| 								<tbody> | ||||
| 									<tr> | ||||
| 										<td class="text-left">{{ _('Grand Total') }}</td> | ||||
| 										<td class='text-right'>{{ data.grand_total or '' }} {{ currency.symbol }}</td> | ||||
| 										<td class="text-left font-bold">{{ _('Grand Total') }}</td> | ||||
| 										<td class='text-right'> {{ frappe.utils.fmt_money(data.grand_total or '', currency=currency) }}</td> | ||||
| 									</tr> | ||||
| 									<tr> | ||||
| 										<td class="text-left">{{ _('Net Total') }}</td> | ||||
| 										<td class='text-right'>{{ data.net_total or '' }} {{ currency.symbol }}</td> | ||||
| 										<td class="text-left font-bold">{{ _('Net Total') }}</td> | ||||
| 										<td class='text-right'> {{ frappe.utils.fmt_money(data.net_total or '', currency=currency) }}</td> | ||||
| 									</tr> | ||||
| 									<tr> | ||||
| 										<td class="text-left">{{ _('Total Quantity') }}</td> | ||||
| 										<td class="text-left font-bold">{{ _('Total Quantity') }}</td> | ||||
| 										<td class='text-right'>{{ data.total_quantity or '' }}</td> | ||||
| 									</tr> | ||||
| 
 | ||||
| @ -45,7 +45,7 @@ | ||||
| 								{% for d in data.payment_reconciliation %} | ||||
| 									<tr> | ||||
| 										<td class="text-left">{{ d.mode_of_payment }}</td> | ||||
| 										<td class='text-right'>{{ d.expected_amount }} {{ currency.symbol }}</td> | ||||
| 										<td class='text-right'> {{ frappe.utils.fmt_money(d.expected_amount - d.opening_amount, currency=currency) }}</td> | ||||
| 									</tr> | ||||
| 								{% endfor %} | ||||
| 							</tbody> | ||||
| @ -55,12 +55,14 @@ | ||||
| 				<!-- Section end --> | ||||
| 
 | ||||
| 				<!-- Taxes section --> | ||||
| 				{% if data.taxes %} | ||||
| 				<div> | ||||
| 						<h6 class="text-center uppercase" style="color: #8D99A6">{{ _("Taxes") }}</h6> | ||||
| 						<div class="tax-break-up" style="overflow-x: auto;"> | ||||
| 							<table class="table table-bordered table-hover"> | ||||
| 								<thead> | ||||
| 									<tr> | ||||
| 										<th class="text-left">{{ _("Account") }}</th> | ||||
| 										<th class="text-left">{{ _("Rate") }}</th> | ||||
| 										<th class="text-right">{{ _("Amount") }}</th> | ||||
| 									</tr> | ||||
| @ -68,14 +70,16 @@ | ||||
| 								<tbody> | ||||
| 								{% for d in data.taxes %} | ||||
| 									<tr> | ||||
| 										<td class="text-left">{{ d.account_head }}</td> | ||||
| 										<td class="text-left">{{ d.rate }} %</td> | ||||
| 										<td class='text-right'>{{ d.amount }} {{ currency.symbol }}</td> | ||||
| 										<td class='text-right'> {{ frappe.utils.fmt_money(d.amount, currency=currency) }}</td> | ||||
| 									</tr> | ||||
| 								{% endfor %} | ||||
| 							</tbody> | ||||
| 						</table> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				{% endif %} | ||||
| 				<!-- Section end --> | ||||
| 
 | ||||
| 			</div> | ||||
							
								
								
									
										149
									
								
								erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										149
									
								
								erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,149 @@ | ||||
| // Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors
 | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| frappe.ui.form.on('POS Closing Entry', { | ||||
| 	onload: function(frm) { | ||||
| 		frm.set_query("pos_profile", function(doc) { | ||||
| 			return { | ||||
| 				filters: { 'user': doc.user } | ||||
| 			}; | ||||
| 		}); | ||||
| 
 | ||||
| 		frm.set_query("user", function(doc) { | ||||
| 			return { | ||||
| 				query: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_cashiers", | ||||
| 				filters: { 'parent': doc.pos_profile } | ||||
| 			}; | ||||
| 		}); | ||||
| 
 | ||||
| 		frm.set_query("pos_opening_entry", function(doc) { | ||||
| 			return { filters: { 'status': 'Open', 'docstatus': 1 } }; | ||||
| 		}); | ||||
| 		 | ||||
| 		if (frm.doc.docstatus === 0) frm.set_value("period_end_date", frappe.datetime.now_datetime()); | ||||
| 		if (frm.doc.docstatus === 1) set_html_data(frm); | ||||
| 	}, | ||||
| 
 | ||||
| 	pos_opening_entry(frm) { | ||||
| 		if (frm.doc.pos_opening_entry && frm.doc.period_start_date && frm.doc.period_end_date && frm.doc.user) { | ||||
| 			reset_values(frm); | ||||
| 			frm.trigger("set_opening_amounts"); | ||||
| 			frm.trigger("get_pos_invoices"); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	set_opening_amounts(frm) { | ||||
| 		frappe.db.get_doc("POS Opening Entry", frm.doc.pos_opening_entry) | ||||
| 			.then(({ balance_details }) => { | ||||
| 				balance_details.forEach(detail => { | ||||
| 					frm.add_child("payment_reconciliation", { | ||||
| 						mode_of_payment: detail.mode_of_payment, | ||||
| 						opening_amount: detail.opening_amount, | ||||
| 						expected_amount: detail.opening_amount | ||||
| 					}); | ||||
| 				}) | ||||
| 			}); | ||||
| 	}, | ||||
| 
 | ||||
| 	get_pos_invoices(frm) { | ||||
| 		frappe.call({ | ||||
| 			method: 'erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_pos_invoices', | ||||
| 			args: { | ||||
| 				start: frappe.datetime.get_datetime_as_string(frm.doc.period_start_date), | ||||
| 				end: frappe.datetime.get_datetime_as_string(frm.doc.period_end_date), | ||||
| 				user: frm.doc.user | ||||
| 			}, | ||||
| 			callback: (r) => { | ||||
| 				let pos_docs = r.message; | ||||
| 				set_form_data(pos_docs, 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]; | ||||
| 		frappe.model.set_value(cdt, cdn, "difference", flt(row.expected_amount - row.closing_amount)) | ||||
| 	} | ||||
| }) | ||||
| 
 | ||||
| function set_form_data(data, frm) { | ||||
| 	data.forEach(d => { | ||||
| 		add_to_pos_transaction(d, 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); | ||||
| 	}); | ||||
| } | ||||
| 
 | ||||
| function add_to_pos_transaction(d, frm) { | ||||
| 	frm.add_child("pos_transactions", { | ||||
| 		pos_invoice: d.name, | ||||
| 		posting_date: d.posting_date, | ||||
| 		grand_total: d.grand_total, | ||||
| 		customer: d.customer | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| function add_to_payments(d, frm) { | ||||
| 	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); | ||||
| 		} else { | ||||
| 			frm.add_child("payment_reconciliation", { | ||||
| 				mode_of_payment: p.mode_of_payment, | ||||
| 				opening_amount: 0, | ||||
| 				expected_amount: p.amount | ||||
| 			}) | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| function add_to_taxes(d, frm) { | ||||
| 	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);  | ||||
| 		} else { | ||||
| 			frm.add_child("taxes", { | ||||
| 				account_head: t.account_head, | ||||
| 				rate: t.rate, | ||||
| 				amount: t.tax_amount | ||||
| 			}) | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
| 
 | ||||
| function reset_values(frm) { | ||||
| 	frm.set_value("pos_transactions", []); | ||||
| 	frm.set_value("payment_reconciliation", []); | ||||
| 	frm.set_value("taxes", []); | ||||
| 	frm.set_value("grand_total", 0); | ||||
| 	frm.set_value("net_total", 0); | ||||
| 	frm.set_value("total_quantity", 0); | ||||
| } | ||||
| 
 | ||||
| function refresh_fields(frm) { | ||||
| 	frm.refresh_field("pos_transactions"); | ||||
| 	frm.refresh_field("payment_reconciliation"); | ||||
| 	frm.refresh_field("taxes"); | ||||
| 	frm.refresh_field("grand_total"); | ||||
| 	frm.refresh_field("net_total"); | ||||
| 	frm.refresh_field("total_quantity"); | ||||
| } | ||||
| 
 | ||||
| function set_html_data(frm) { | ||||
| 	frappe.call({ | ||||
| 		method: "get_payment_reconciliation_details", | ||||
| 		doc: frm.doc, | ||||
| 		callback: (r) => { | ||||
| 			frm.get_field("payment_reconciliation_details").$wrapper.html(r.message); | ||||
| 		} | ||||
| 	}) | ||||
| } | ||||
| @ -0,0 +1,242 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "autoname": "POS-CLO-.YYYY.-.#####", | ||||
|  "creation": "2018-05-28 19:06:40.830043", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "period_start_date", | ||||
|   "period_end_date", | ||||
|   "column_break_3", | ||||
|   "posting_date", | ||||
|   "pos_opening_entry", | ||||
|   "section_break_5", | ||||
|   "company", | ||||
|   "column_break_7", | ||||
|   "pos_profile", | ||||
|   "user", | ||||
|   "section_break_12", | ||||
|   "pos_transactions", | ||||
|   "section_break_9", | ||||
|   "payment_reconciliation_details", | ||||
|   "section_break_11", | ||||
|   "payment_reconciliation", | ||||
|   "section_break_13", | ||||
|   "grand_total", | ||||
|   "net_total", | ||||
|   "total_quantity", | ||||
|   "column_break_16", | ||||
|   "taxes", | ||||
|   "section_break_14", | ||||
|   "amended_from" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fetch_from": "pos_opening_entry.period_start_date", | ||||
|    "fieldname": "period_start_date", | ||||
|    "fieldtype": "Datetime", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Period Start Date", | ||||
|    "read_only": 1, | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "Today", | ||||
|    "fieldname": "period_end_date", | ||||
|    "fieldtype": "Datetime", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Period End Date", | ||||
|    "read_only": 1, | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_3", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "Today", | ||||
|    "fieldname": "posting_date", | ||||
|    "fieldtype": "Date", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Posting Date", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_5", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "company", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Company", | ||||
|    "options": "Company", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_7", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "pos_opening_entry.pos_profile", | ||||
|    "fieldname": "pos_profile", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "POS Profile", | ||||
|    "options": "POS Profile", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "pos_opening_entry.user", | ||||
|    "fieldname": "user", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Cashier", | ||||
|    "options": "User", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_9", | ||||
|    "fieldtype": "Section Break", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:doc.docstatus==1", | ||||
|    "fieldname": "payment_reconciliation_details", | ||||
|    "fieldtype": "HTML" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_11", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Modes of Payment" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "payment_reconciliation", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "Payment Reconciliation", | ||||
|    "options": "POS Closing Entry Detail" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "collapsible_depends_on": "eval:doc.docstatus==0", | ||||
|    "fieldname": "section_break_13", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Details" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "grand_total", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Grand Total", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "net_total", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Net Total", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "total_quantity", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Total Quantity", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_16", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "taxes", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "Taxes", | ||||
|    "options": "POS Closing Entry Taxes", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_12", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Linked Invoices" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_14", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "amended_from", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Amended From", | ||||
|    "no_copy": 1, | ||||
|    "options": "POS Closing Entry", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "pos_transactions", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "POS Transactions", | ||||
|    "options": "POS Invoice Reference", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "pos_opening_entry", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "POS Opening Entry", | ||||
|    "options": "POS Opening Entry", | ||||
|    "reqd": 1 | ||||
|   } | ||||
|  ], | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-29 15:03:22.226113", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Closing Entry", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "System Manager", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Sales Manager", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Administrator", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   } | ||||
|  ], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
							
								
								
									
										127
									
								
								erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										127
									
								
								erpnext/accounts/doctype/pos_closing_entry/pos_closing_entry.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,127 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| import json | ||||
| from frappe import _ | ||||
| from frappe.model.document import Document | ||||
| from frappe.utils import getdate, get_datetime, flt | ||||
| from collections import defaultdict | ||||
| from erpnext.controllers.taxes_and_totals import get_itemised_tax_breakup_data | ||||
| from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices | ||||
| 
 | ||||
| class POSClosingEntry(Document): | ||||
| 	def validate(self): | ||||
| 		user = frappe.get_all('POS Closing Entry', | ||||
| 			filters = { 'user': self.user, 'docstatus': 1 }, | ||||
| 			or_filters = { | ||||
| 					'period_start_date': ('between', [self.period_start_date, self.period_end_date]), | ||||
| 					'period_end_date': ('between', [self.period_start_date, self.period_end_date]) | ||||
| 			}) | ||||
| 
 | ||||
| 		if user: | ||||
| 			frappe.throw(_("POS Closing Entry {} against {} between selected period" | ||||
| 				.format(frappe.bold("already exists"), frappe.bold(self.user))), title=_("Invalid Period")) | ||||
| 		 | ||||
| 		if frappe.db.get_value("POS Opening Entry", self.pos_opening_entry, "status") != "Open": | ||||
| 			frappe.throw(_("Selected POS Opening Entry should be open."), title=_("Invalid Opening Entry")) | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		merge_pos_invoices(self.pos_transactions) | ||||
| 		opening_entry = frappe.get_doc("POS Opening Entry", self.pos_opening_entry) | ||||
| 		opening_entry.pos_closing_entry = self.name | ||||
| 		opening_entry.set_status() | ||||
| 		opening_entry.save() | ||||
| 
 | ||||
| 	def get_payment_reconciliation_details(self): | ||||
| 		currency = frappe.get_cached_value('Company', self.company,  "default_currency") | ||||
| 		return frappe.render_template("erpnext/accounts/doctype/pos_closing_entry/closing_voucher_details.html", | ||||
| 			{"data": self, "currency": currency}) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_cashiers(doctype, txt, searchfield, start, page_len, filters): | ||||
| 	cashiers_list = frappe.get_all("POS Profile User", filters=filters, fields=['user']) | ||||
| 	return [c['user'] for c in cashiers_list] | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_pos_invoices(start, end, user): | ||||
| 	data = frappe.db.sql(""" | ||||
| 	select  | ||||
| 		name, timestamp(posting_date, posting_time) as "timestamp" | ||||
| 	from  | ||||
| 		`tabPOS Invoice` | ||||
| 	where  | ||||
| 		owner = %s and docstatus = 1 and  | ||||
| 		(consolidated_invoice is NULL or consolidated_invoice = '') | ||||
| 	""", (user), as_dict=1) | ||||
| 
 | ||||
| 	data = list(filter(lambda d: get_datetime(start) <= get_datetime(d.timestamp) <= get_datetime(end), data)) | ||||
| 	# need to get taxes and payments so can't avoid get_doc | ||||
| 	data = [frappe.get_doc("POS Invoice", d.name).as_dict() for d in data] | ||||
| 
 | ||||
| 	return data | ||||
| 
 | ||||
| def make_closing_entry_from_opening(opening_entry): | ||||
| 	closing_entry = frappe.new_doc("POS Closing Entry") | ||||
| 	closing_entry.pos_opening_entry = opening_entry.name | ||||
| 	closing_entry.period_start_date = opening_entry.period_start_date | ||||
| 	closing_entry.period_end_date = frappe.utils.get_datetime() | ||||
| 	closing_entry.pos_profile = opening_entry.pos_profile | ||||
| 	closing_entry.user = opening_entry.user | ||||
| 	closing_entry.company = opening_entry.company | ||||
| 	closing_entry.grand_total = 0 | ||||
| 	closing_entry.net_total = 0 | ||||
| 	closing_entry.total_quantity = 0 | ||||
| 
 | ||||
| 	invoices = get_pos_invoices(closing_entry.period_start_date, closing_entry.period_end_date, closing_entry.user) | ||||
| 
 | ||||
| 	pos_transactions = [] | ||||
| 	taxes = [] | ||||
| 	payments = [] | ||||
| 	for detail in opening_entry.balance_details: | ||||
| 		payments.append(frappe._dict({ | ||||
| 			'mode_of_payment': detail.mode_of_payment, | ||||
| 			'opening_amount': detail.opening_amount, | ||||
| 			'expected_amount': detail.opening_amount | ||||
| 		})) | ||||
| 
 | ||||
| 	for d in invoices: | ||||
| 		pos_transactions.append(frappe._dict({ | ||||
| 			'pos_invoice': d.name, | ||||
| 			'posting_date': d.posting_date, | ||||
| 			'grand_total': d.grand_total, | ||||
| 			'customer': d.customer | ||||
| 		})) | ||||
| 		closing_entry.grand_total += flt(d.grand_total) | ||||
| 		closing_entry.net_total += flt(d.net_total) | ||||
| 		closing_entry.total_quantity += flt(d.total_qty) | ||||
| 
 | ||||
| 		for t in d.taxes: | ||||
| 			existing_tax = [tx for tx in taxes if tx.account_head == t.account_head and tx.rate == t.rate] | ||||
| 			if existing_tax: | ||||
| 				existing_tax[0].amount += flt(t.tax_amount);  | ||||
| 			else: | ||||
| 				taxes.append(frappe._dict({ | ||||
| 					'account_head': t.account_head, | ||||
| 					'rate': t.rate, | ||||
| 					'amount': t.tax_amount | ||||
| 				})) | ||||
| 
 | ||||
| 		for p in d.payments: | ||||
| 			existing_pay = [pay for pay in payments if pay.mode_of_payment == p.mode_of_payment] | ||||
| 			if existing_pay: | ||||
| 				existing_pay[0].expected_amount += flt(p.amount); | ||||
| 			else: | ||||
| 				payments.append(frappe._dict({ | ||||
| 					'mode_of_payment': p.mode_of_payment, | ||||
| 					'opening_amount': 0, | ||||
| 					'expected_amount': p.amount | ||||
| 				})) | ||||
| 
 | ||||
| 	closing_entry.set("pos_transactions", pos_transactions) | ||||
| 	closing_entry.set("payment_reconciliation", payments) | ||||
| 	closing_entry.set("taxes", taxes) | ||||
| 
 | ||||
| 	return closing_entry | ||||
| @ -2,15 +2,15 @@ | ||||
| // rename this file from _test_[name] to test_[name] to activate
 | ||||
| // and remove above this line
 | ||||
| 
 | ||||
| QUnit.test("test: POS Closing Voucher", function (assert) { | ||||
| QUnit.test("test: POS Closing Entry", function (assert) { | ||||
| 	let done = assert.async(); | ||||
| 
 | ||||
| 	// number of asserts
 | ||||
| 	assert.expect(1); | ||||
| 
 | ||||
| 	frappe.run_serially([ | ||||
| 		// insert a new POS Closing Voucher
 | ||||
| 		() => frappe.tests.make('POS Closing Voucher', [ | ||||
| 		// insert a new POS Closing Entry
 | ||||
| 		() => frappe.tests.make('POS Closing Entry', [ | ||||
| 			// values to be set
 | ||||
| 			{key: 'value'} | ||||
| 		]), | ||||
| @ -0,0 +1,64 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2018, Frappe Technologies Pvt. Ltd. and Contributors | ||||
| # See license.txt | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| import unittest | ||||
| from frappe.utils import nowdate | ||||
| from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice | ||||
| from erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry import make_closing_entry_from_opening | ||||
| from erpnext.accounts.doctype.pos_opening_entry.test_pos_opening_entry import create_opening_entry | ||||
| from erpnext.accounts.doctype.pos_profile.test_pos_profile import make_pos_profile | ||||
| 
 | ||||
| class TestPOSClosingEntry(unittest.TestCase): | ||||
| 	def test_pos_closing_entry(self): | ||||
| 		test_user, pos_profile = init_user_and_profile() | ||||
| 
 | ||||
| 		opening_entry = create_opening_entry(pos_profile, test_user.name) | ||||
| 
 | ||||
| 		pos_inv1 = create_pos_invoice(rate=3500, do_not_submit=1) | ||||
| 		pos_inv1.append('payments', { | ||||
| 			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3500 | ||||
| 		}) | ||||
| 		pos_inv1.submit() | ||||
| 
 | ||||
| 		pos_inv2 = create_pos_invoice(rate=3200, do_not_submit=1) | ||||
| 		pos_inv2.append('payments', { | ||||
| 			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 3200 | ||||
| 		}) | ||||
| 		pos_inv2.submit() | ||||
| 
 | ||||
| 		pcv_doc = make_closing_entry_from_opening(opening_entry) | ||||
| 		payment = pcv_doc.payment_reconciliation[0] | ||||
| 
 | ||||
| 		self.assertEqual(payment.mode_of_payment, 'Cash') | ||||
| 
 | ||||
| 		for d in pcv_doc.payment_reconciliation: | ||||
| 			if d.mode_of_payment == 'Cash': | ||||
| 				d.closing_amount = 6700 | ||||
| 
 | ||||
| 		pcv_doc.submit() | ||||
| 
 | ||||
| 		self.assertEqual(pcv_doc.total_quantity, 2) | ||||
| 		self.assertEqual(pcv_doc.net_total, 6700) | ||||
| 
 | ||||
| 		frappe.set_user("Administrator") | ||||
| 		frappe.db.sql("delete from `tabPOS Profile`") | ||||
| 
 | ||||
| def init_user_and_profile(): | ||||
| 	user = 'test@example.com' | ||||
| 	test_user = frappe.get_doc('User', user) | ||||
| 
 | ||||
| 	roles = ("Accounts Manager", "Accounts User", "Sales Manager") | ||||
| 	test_user.add_roles(*roles) | ||||
| 	frappe.set_user(user) | ||||
| 
 | ||||
| 	pos_profile = make_pos_profile() | ||||
| 	pos_profile.append('applicable_for_users', { | ||||
| 		'default': 1, | ||||
| 		'user': user | ||||
| 	}) | ||||
| 
 | ||||
| 	pos_profile.save() | ||||
| 
 | ||||
| 	return test_user, pos_profile | ||||
| @ -0,0 +1,70 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2018-05-28 19:10:47.580174", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "mode_of_payment", | ||||
|   "opening_amount", | ||||
|   "closing_amount", | ||||
|   "expected_amount", | ||||
|   "difference" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "mode_of_payment", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Mode of Payment", | ||||
|    "options": "Mode of Payment", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "expected_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Expected Amount", | ||||
|    "options": "company:company_currency", | ||||
|    "read_only": 1, | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "difference", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Difference", | ||||
|    "options": "company:company_currency", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "opening_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Opening Amount", | ||||
|    "options": "company:company_currency", | ||||
|    "read_only": 1, | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "closing_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Closing Amount", | ||||
|    "options": "company:company_currency", | ||||
|    "reqd": 1 | ||||
|   } | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-29 15:03:34.533607", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Closing Entry Detail", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -5,5 +5,5 @@ | ||||
| from __future__ import unicode_literals | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class POSClosingVoucherTaxes(Document): | ||||
| class POSClosingEntryDetail(Document): | ||||
| 	pass | ||||
| @ -0,0 +1,48 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2018-05-30 09:11:22.535470", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "account_head", | ||||
|   "rate", | ||||
|   "amount" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "rate", | ||||
|    "fieldtype": "Percent", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Rate", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Amount", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "account_head", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Account Head", | ||||
|    "options": "Account", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-29 15:03:39.872884", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Closing Entry Taxes", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -5,5 +5,5 @@ | ||||
| from __future__ import unicode_literals | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class POSClosingVoucherDetails(Document): | ||||
| class POSClosingEntryTaxes(Document): | ||||
| 	pass | ||||
							
								
								
									
										205
									
								
								erpnext/accounts/doctype/pos_invoice/pos_invoice.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										205
									
								
								erpnext/accounts/doctype/pos_invoice/pos_invoice.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,205 @@ | ||||
| // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
 | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| {% include 'erpnext/selling/sales_common.js' %}; | ||||
| 
 | ||||
| erpnext.selling.POSInvoiceController = erpnext.selling.SellingController.extend({ | ||||
| 	setup(doc) { | ||||
| 		this.setup_posting_date_time_check(); | ||||
| 		this._super(doc); | ||||
| 	}, | ||||
| 
 | ||||
| 	onload() { | ||||
| 		this._super(); | ||||
| 		if(this.frm.doc.__islocal && this.frm.doc.is_pos) { | ||||
| 			//Load pos profile data on the invoice if the default value of Is POS is 1
 | ||||
| 
 | ||||
| 			me.frm.script_manager.trigger("is_pos"); | ||||
| 			me.frm.refresh_fields(); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	refresh(doc) { | ||||
| 		this._super(); | ||||
| 		if (doc.docstatus == 1 && !doc.is_return) { | ||||
| 			if(doc.outstanding_amount >= 0 || Math.abs(flt(doc.outstanding_amount)) < flt(doc.grand_total)) { | ||||
| 				cur_frm.add_custom_button(__('Return'), | ||||
| 					this.make_sales_return, __('Create')); | ||||
| 				cur_frm.page.set_inner_btn_group_as_primary(__('Create')); | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if (this.frm.doc.is_return) { | ||||
| 			this.frm.return_print_format = "Sales Invoice Return"; | ||||
| 			cur_frm.set_value('consolidated_invoice', ''); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	is_pos: function(frm){ | ||||
| 		this.set_pos_data(); | ||||
| 	}, | ||||
| 
 | ||||
| 	set_pos_data: function() { | ||||
| 		if(this.frm.doc.is_pos) { | ||||
| 			this.frm.set_value("allocate_advances_automatically", 0); | ||||
| 			if(!this.frm.doc.company) { | ||||
| 				this.frm.set_value("is_pos", 0); | ||||
| 				frappe.msgprint(__("Please specify Company to proceed")); | ||||
| 			} else { | ||||
| 				var me = this; | ||||
| 				return this.frm.call({ | ||||
| 					doc: me.frm.doc, | ||||
| 					method: "set_missing_values", | ||||
| 					callback: function(r) { | ||||
| 						if(!r.exc) { | ||||
| 							if(r.message) { | ||||
| 								me.frm.pos_print_format = r.message.print_format || ""; | ||||
| 								me.frm.meta.default_print_format = r.message.print_format || ""; | ||||
| 								me.frm.allow_edit_rate = r.message.allow_edit_rate; | ||||
| 								me.frm.allow_edit_discount = r.message.allow_edit_discount; | ||||
| 								me.frm.doc.campaign = r.message.campaign; | ||||
| 								me.frm.allow_print_before_pay = r.message.allow_print_before_pay; | ||||
| 							} | ||||
| 							me.frm.script_manager.trigger("update_stock"); | ||||
| 							me.calculate_taxes_and_totals(); | ||||
| 							if(me.frm.doc.taxes_and_charges) { | ||||
| 								me.frm.script_manager.trigger("taxes_and_charges"); | ||||
| 							} | ||||
| 							frappe.model.set_default_values(me.frm.doc); | ||||
| 							me.set_dynamic_labels(); | ||||
| 							 | ||||
| 						} | ||||
| 					} | ||||
| 				}); | ||||
| 			} | ||||
| 		} | ||||
| 		else this.frm.trigger("refresh"); | ||||
| 	}, | ||||
| 
 | ||||
| 	customer() { | ||||
| 		if (!this.frm.doc.customer) return | ||||
| 
 | ||||
| 		if (this.frm.doc.is_pos){ | ||||
| 			var pos_profile = this.frm.doc.pos_profile; | ||||
| 		} | ||||
| 		var me = this; | ||||
| 		if(this.frm.updating_party_details) return; | ||||
| 		erpnext.utils.get_party_details(this.frm, | ||||
| 			"erpnext.accounts.party.get_party_details", { | ||||
| 				posting_date: this.frm.doc.posting_date, | ||||
| 				party: this.frm.doc.customer, | ||||
| 				party_type: "Customer", | ||||
| 				account: this.frm.doc.debit_to, | ||||
| 				price_list: this.frm.doc.selling_price_list, | ||||
| 				pos_profile: pos_profile | ||||
| 			}, function() { | ||||
| 				me.apply_pricing_rule(); | ||||
| 			}); | ||||
| 	}, | ||||
| 
 | ||||
| 	amount: function(){ | ||||
| 		this.write_off_outstanding_amount_automatically() | ||||
| 	}, | ||||
| 
 | ||||
| 	change_amount: function(){ | ||||
| 		if(this.frm.doc.paid_amount > this.frm.doc.grand_total){ | ||||
| 			this.calculate_write_off_amount(); | ||||
| 		}else { | ||||
| 			this.frm.set_value("change_amount", 0.0); | ||||
| 			this.frm.set_value("base_change_amount", 0.0); | ||||
| 		} | ||||
| 
 | ||||
| 		this.frm.refresh_fields(); | ||||
| 	}, | ||||
| 
 | ||||
| 	loyalty_amount: function(){ | ||||
| 		this.calculate_outstanding_amount(); | ||||
| 		this.frm.refresh_field("outstanding_amount"); | ||||
| 		this.frm.refresh_field("paid_amount"); | ||||
| 		this.frm.refresh_field("base_paid_amount"); | ||||
| 	}, | ||||
| 
 | ||||
| 	write_off_outstanding_amount_automatically: function() { | ||||
| 		if(cint(this.frm.doc.write_off_outstanding_amount_automatically)) { | ||||
| 			frappe.model.round_floats_in(this.frm.doc, ["grand_total", "paid_amount"]); | ||||
| 			// this will make outstanding amount 0
 | ||||
| 			this.frm.set_value("write_off_amount", | ||||
| 				flt(this.frm.doc.grand_total - this.frm.doc.paid_amount - this.frm.doc.total_advance, precision("write_off_amount")) | ||||
| 			); | ||||
| 			this.frm.toggle_enable("write_off_amount", false); | ||||
| 
 | ||||
| 		} else { | ||||
| 			this.frm.toggle_enable("write_off_amount", true); | ||||
| 		} | ||||
| 
 | ||||
| 		this.calculate_outstanding_amount(false); | ||||
| 		this.frm.refresh_fields(); | ||||
| 	}, | ||||
| 
 | ||||
| 	make_sales_return: function() { | ||||
| 		frappe.model.open_mapped_doc({ | ||||
| 			method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return", | ||||
| 			frm: cur_frm | ||||
| 		}) | ||||
| 	}, | ||||
| }) | ||||
| 
 | ||||
| $.extend(cur_frm.cscript, new erpnext.selling.POSInvoiceController({ frm: cur_frm })) | ||||
| 
 | ||||
| frappe.ui.form.on('POS Invoice', { | ||||
| 	redeem_loyalty_points: function(frm) { | ||||
| 		frm.events.get_loyalty_details(frm); | ||||
| 	}, | ||||
| 
 | ||||
| 	loyalty_points: function(frm) { | ||||
| 		if (frm.redemption_conversion_factor) { | ||||
| 			frm.events.set_loyalty_points(frm); | ||||
| 		} else { | ||||
| 			frappe.call({ | ||||
| 				method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_redeemption_factor", | ||||
| 				args: { | ||||
| 					"loyalty_program": frm.doc.loyalty_program | ||||
| 				}, | ||||
| 				callback: function(r) { | ||||
| 					if (r) { | ||||
| 						frm.redemption_conversion_factor = r.message; | ||||
| 						frm.events.set_loyalty_points(frm); | ||||
| 					} | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	get_loyalty_details: function(frm) { | ||||
| 		if (frm.doc.customer && frm.doc.redeem_loyalty_points) { | ||||
| 			frappe.call({ | ||||
| 				method: "erpnext.accounts.doctype.loyalty_program.loyalty_program.get_loyalty_program_details", | ||||
| 				args: { | ||||
| 					"customer": frm.doc.customer, | ||||
| 					"loyalty_program": frm.doc.loyalty_program, | ||||
| 					"expiry_date": frm.doc.posting_date, | ||||
| 					"company": frm.doc.company | ||||
| 				}, | ||||
| 				callback: function(r) { | ||||
| 					if (r) { | ||||
| 						frm.set_value("loyalty_redemption_account", r.message.expense_account); | ||||
| 						frm.set_value("loyalty_redemption_cost_center", r.message.cost_center); | ||||
| 						frm.redemption_conversion_factor = r.message.conversion_factor; | ||||
| 					} | ||||
| 				} | ||||
| 			}); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	set_loyalty_points: function(frm) { | ||||
| 		if (frm.redemption_conversion_factor) { | ||||
| 			let loyalty_amount = flt(frm.redemption_conversion_factor*flt(frm.doc.loyalty_points), precision("loyalty_amount")); | ||||
| 			var remaining_amount = flt(frm.doc.grand_total) - flt(frm.doc.total_advance) - flt(frm.doc.write_off_amount); | ||||
| 			if (frm.doc.grand_total && (remaining_amount < loyalty_amount)) { | ||||
| 				let redeemable_points = parseInt(remaining_amount/frm.redemption_conversion_factor); | ||||
| 				frappe.throw(__("You can only redeem max {0} points in this order.",[redeemable_points])); | ||||
| 			} | ||||
| 			frm.set_value("loyalty_amount", loyalty_amount); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
							
								
								
									
										1637
									
								
								erpnext/accounts/doctype/pos_invoice/pos_invoice.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1637
									
								
								erpnext/accounts/doctype/pos_invoice/pos_invoice.json
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										374
									
								
								erpnext/accounts/doctype/pos_invoice/pos_invoice.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										374
									
								
								erpnext/accounts/doctype/pos_invoice/pos_invoice.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,374 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.model.document import Document | ||||
| from erpnext.controllers.selling_controller import SellingController | ||||
| from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate | ||||
| from erpnext.accounts.utils import get_account_currency | ||||
| from erpnext.accounts.party import get_party_account, get_due_date | ||||
| from erpnext.accounts.doctype.loyalty_program.loyalty_program import \ | ||||
| 	get_loyalty_program_details_with_points, validate_loyalty_points | ||||
| 
 | ||||
| from erpnext.accounts.doctype.sales_invoice.sales_invoice import SalesInvoice, get_bank_cash_account, update_multi_mode_option | ||||
| from erpnext.stock.doctype.serial_no.serial_no import get_pos_reserved_serial_nos | ||||
| 
 | ||||
| from six import iteritems | ||||
| 
 | ||||
| class POSInvoice(SalesInvoice): | ||||
| 	def __init__(self, *args, **kwargs): | ||||
| 		super(POSInvoice, self).__init__(*args, **kwargs) | ||||
| 	 | ||||
| 	def validate(self): | ||||
| 		if not cint(self.is_pos): | ||||
| 			frappe.throw(_("POS Invoice should have {} field checked.").format(frappe.bold("Include Payment"))) | ||||
| 
 | ||||
| 		# run on validate method of selling controller | ||||
| 		super(SalesInvoice, self).validate() | ||||
| 		self.validate_auto_set_posting_time() | ||||
| 		self.validate_pos_paid_amount() | ||||
| 		self.validate_pos_return() | ||||
| 		self.validate_uom_is_integer("stock_uom", "stock_qty") | ||||
| 		self.validate_uom_is_integer("uom", "qty") | ||||
| 		self.validate_debit_to_acc() | ||||
| 		self.validate_write_off_account() | ||||
| 		self.validate_change_amount() | ||||
| 		self.validate_change_account() | ||||
| 		self.validate_item_cost_centers() | ||||
| 		self.validate_serialised_or_batched_item() | ||||
| 		self.validate_stock_availablility() | ||||
| 		self.validate_return_items() | ||||
| 		self.set_status() | ||||
| 		self.set_account_for_mode_of_payment() | ||||
| 		self.validate_pos() | ||||
| 		self.verify_payment_amount() | ||||
| 		self.validate_loyalty_transaction() | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		# create the loyalty point ledger entry if the customer is enrolled in any loyalty program | ||||
| 		if self.loyalty_program: | ||||
| 			self.make_loyalty_point_entry() | ||||
| 		elif self.is_return and self.return_against and self.loyalty_program: | ||||
| 			against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) | ||||
| 			against_psi_doc.delete_loyalty_point_entry() | ||||
| 			against_psi_doc.make_loyalty_point_entry() | ||||
| 		if self.redeem_loyalty_points and self.loyalty_points: | ||||
| 			self.apply_loyalty_points() | ||||
| 		self.set_status(update=True) | ||||
| 	 | ||||
| 	def on_cancel(self): | ||||
| 		# run on cancel method of selling controller | ||||
| 		super(SalesInvoice, self).on_cancel() | ||||
| 		if self.loyalty_program: | ||||
| 			self.delete_loyalty_point_entry() | ||||
| 		elif self.is_return and self.return_against and self.loyalty_program: | ||||
| 			against_psi_doc = frappe.get_doc("POS Invoice", self.return_against) | ||||
| 			against_psi_doc.delete_loyalty_point_entry() | ||||
| 			against_psi_doc.make_loyalty_point_entry() | ||||
| 		 | ||||
| 	def validate_stock_availablility(self): | ||||
| 		allow_negative_stock = frappe.db.get_value('Stock Settings', None, 'allow_negative_stock') | ||||
| 		 | ||||
| 		for d in self.get('items'): | ||||
| 			if d.serial_no: | ||||
| 				filters = { | ||||
| 					"item_code": d.item_code, | ||||
| 					"warehouse": d.warehouse, | ||||
| 					"delivery_document_no": "", | ||||
| 					"sales_invoice": "" | ||||
| 				} | ||||
| 				if d.batch_no: | ||||
| 					filters["batch_no"] = d.batch_no | ||||
| 				reserved_serial_nos, unreserved_serial_nos = get_pos_reserved_serial_nos(filters) | ||||
| 				serial_nos = d.serial_no.split("\n") | ||||
| 				serial_nos = ' '.join(serial_nos).split() # remove whitespaces | ||||
| 				invalid_serial_nos = [] | ||||
| 				for s in serial_nos: | ||||
| 					if s in reserved_serial_nos: | ||||
| 						invalid_serial_nos.append(s) | ||||
| 				 | ||||
| 				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")) | ||||
| 			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")) | ||||
| 				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")) | ||||
| 	 | ||||
| 	def validate_serialised_or_batched_item(self): | ||||
| 		for d in self.get("items"): | ||||
| 			serialized = d.get("has_serial_no") | ||||
| 			batched = d.get("has_batch_no") | ||||
| 			no_serial_selected = not d.get("serial_no") | ||||
| 			no_batch_selected = not d.get("batch_no") | ||||
| 
 | ||||
| 
 | ||||
| 			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")) | ||||
| 			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")) | ||||
| 			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")) | ||||
| 	 | ||||
| 	def validate_return_items(self): | ||||
| 		if not self.get("is_return"): return | ||||
| 
 | ||||
| 		for d in self.get("items"): | ||||
| 			if d.get("qty") > 0: | ||||
| 				frappe.throw(_("Row #{}: You cannot add postive quantities in a return invoice. Please remove item {} to complete the return.") | ||||
| 					.format(d.idx, frappe.bold(d.item_code)), title=_("Invalid Item")) | ||||
| 
 | ||||
| 	def validate_pos_paid_amount(self): | ||||
| 		if len(self.payments) == 0 and self.is_pos: | ||||
| 			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: | ||||
| 			frappe.throw(_("The selected change account {} doesn't belongs to Company {}.").format(self.account_for_change_amount, self.company)) | ||||
| 
 | ||||
| 	def validate_change_amount(self): | ||||
| 		grand_total = flt(self.rounded_total) or flt(self.grand_total) | ||||
| 		base_grand_total = flt(self.base_rounded_total) or flt(self.base_grand_total) | ||||
| 		if not flt(self.change_amount) and grand_total < flt(self.paid_amount): | ||||
| 			self.change_amount = flt(self.paid_amount - grand_total + flt(self.write_off_amount)) | ||||
| 			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) | ||||
| 
 | ||||
| 	def verify_payment_amount(self): | ||||
| 		for entry in self.payments: | ||||
| 			if not self.is_return and entry.amount < 0: | ||||
| 				frappe.throw(_("Row #{0} (Payment Table): Amount must be positive").format(entry.idx)) | ||||
| 			if self.is_return and entry.amount > 0: | ||||
| 				frappe.throw(_("Row #{0} (Payment Table): Amount must be negative").format(entry.idx)) | ||||
| 	 | ||||
| 	def validate_pos_return(self): | ||||
| 		if self.is_pos and self.is_return: | ||||
| 			total_amount_in_payments = 0 | ||||
| 			for payment in self.payments: | ||||
| 				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))) | ||||
| 	 | ||||
| 	def validate_loyalty_transaction(self): | ||||
| 		if self.redeem_loyalty_points and (not self.loyalty_redemption_account or not self.loyalty_redemption_cost_center): | ||||
| 			expense_account, cost_center = frappe.db.get_value('Loyalty Program', self.loyalty_program, ["expense_account", "cost_center"]) | ||||
| 			if not self.loyalty_redemption_account: | ||||
| 				self.loyalty_redemption_account = expense_account  | ||||
| 			if not self.loyalty_redemption_cost_center: | ||||
| 				self.loyalty_redemption_cost_center = cost_center | ||||
| 
 | ||||
| 		if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points: | ||||
| 			validate_loyalty_points(self, self.loyalty_points) | ||||
| 
 | ||||
| 	def set_status(self, update=False, status=None, update_modified=True): | ||||
| 		if self.is_new(): | ||||
| 			if self.get('amended_from'): | ||||
| 				self.status = 'Draft' | ||||
| 			return | ||||
| 
 | ||||
| 		if not status: | ||||
| 			if self.docstatus == 2: | ||||
| 				status = "Cancelled" | ||||
| 			elif self.docstatus == 1: | ||||
| 				if self.consolidated_invoice: | ||||
| 					self.status = "Consolidated" | ||||
| 				elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed': | ||||
| 					self.status = "Overdue and Discounted" | ||||
| 				elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) < getdate(nowdate()): | ||||
| 					self.status = "Overdue" | ||||
| 				elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()) and self.is_discounted and self.get_discounting_status()=='Disbursed': | ||||
| 					self.status = "Unpaid and Discounted" | ||||
| 				elif flt(self.outstanding_amount) > 0 and getdate(self.due_date) >= getdate(nowdate()): | ||||
| 					self.status = "Unpaid" | ||||
| 				elif flt(self.outstanding_amount) <= 0 and self.is_return == 0 and frappe.db.get_value('POS Invoice', {'is_return': 1, 'return_against': self.name, 'docstatus': 1}): | ||||
| 					self.status = "Credit Note Issued" | ||||
| 				elif self.is_return == 1: | ||||
| 					self.status = "Return" | ||||
| 				elif flt(self.outstanding_amount)<=0: | ||||
| 					self.status = "Paid" | ||||
| 				else: | ||||
| 					self.status = "Submitted" | ||||
| 			else: | ||||
| 				self.status = "Draft" | ||||
| 
 | ||||
| 		if update: | ||||
| 			self.db_set('status', self.status, update_modified = update_modified) | ||||
| 	 | ||||
| 	def set_pos_fields(self, for_validate=False): | ||||
| 		"""Set retail related fields from POS Profiles""" | ||||
| 		from erpnext.stock.get_item_details import get_pos_profile_item_details, get_pos_profile | ||||
| 		if not self.pos_profile: | ||||
| 			pos_profile = get_pos_profile(self.company) or {} | ||||
| 			self.pos_profile = pos_profile.get('name') | ||||
| 
 | ||||
| 		pos = {} | ||||
| 		if self.pos_profile: | ||||
| 			pos = frappe.get_doc('POS Profile', self.pos_profile) | ||||
| 
 | ||||
| 		if not self.get('payments') and not for_validate: | ||||
| 			update_multi_mode_option(self, pos) | ||||
| 
 | ||||
| 		if not self.account_for_change_amount: | ||||
| 			self.account_for_change_amount = frappe.get_cached_value('Company',  self.company,  'default_cash_account') | ||||
| 
 | ||||
| 		if pos: | ||||
| 			if not for_validate: | ||||
| 				self.tax_category = pos.get("tax_category") | ||||
| 
 | ||||
| 			if not for_validate and not self.customer: | ||||
| 				self.customer = pos.customer | ||||
| 
 | ||||
| 			self.ignore_pricing_rule = pos.ignore_pricing_rule | ||||
| 			if pos.get('account_for_change_amount'): | ||||
| 				self.account_for_change_amount = pos.get('account_for_change_amount') | ||||
| 			if pos.get('warehouse'): | ||||
| 				self.set_warehouse = pos.get('warehouse') | ||||
| 
 | ||||
| 			for fieldname in ('naming_series', '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)): | ||||
| 						self.set(fieldname, pos.get(fieldname)) | ||||
| 
 | ||||
| 			if pos.get("company_address"): | ||||
| 				self.company_address = pos.get("company_address") | ||||
| 
 | ||||
| 			if self.customer: | ||||
| 				customer_price_list, customer_group = frappe.db.get_value("Customer", self.customer, ['default_price_list', 'customer_group']) | ||||
| 				customer_group_price_list = frappe.db.get_value("Customer Group", customer_group, 'default_price_list') | ||||
| 				selling_price_list = customer_price_list or customer_group_price_list or pos.get('selling_price_list') | ||||
| 			else: | ||||
| 				selling_price_list = pos.get('selling_price_list') | ||||
| 
 | ||||
| 			if selling_price_list: | ||||
| 				self.set('selling_price_list', selling_price_list) | ||||
| 
 | ||||
| 			if not for_validate: | ||||
| 				self.update_stock = cint(pos.get("update_stock")) | ||||
| 
 | ||||
| 			# set pos values in items | ||||
| 			for item in self.get("items"): | ||||
| 				if item.get('item_code'): | ||||
| 					profile_details = get_pos_profile_item_details(pos, frappe._dict(item.as_dict()), pos) | ||||
| 					for fname, val in iteritems(profile_details): | ||||
| 						if (not for_validate) or (for_validate and not item.get(fname)): | ||||
| 							item.set(fname, val) | ||||
| 
 | ||||
| 			# fetch terms | ||||
| 			if self.tc_name and not self.terms: | ||||
| 				self.terms = frappe.db.get_value("Terms and Conditions", self.tc_name, "terms") | ||||
| 
 | ||||
| 			# fetch charges | ||||
| 			if self.taxes_and_charges and not len(self.get("taxes")): | ||||
| 				self.set_taxes() | ||||
| 
 | ||||
| 		return pos | ||||
| 
 | ||||
| 	def set_missing_values(self, for_validate=False): | ||||
| 		pos = self.set_pos_fields(for_validate) | ||||
| 
 | ||||
| 		if not self.debit_to: | ||||
| 			self.debit_to = get_party_account("Customer", self.customer, self.company) | ||||
| 			self.party_account_currency = frappe.db.get_value("Account", self.debit_to, "account_currency", cache=True) | ||||
| 		if not self.due_date and self.customer: | ||||
| 			self.due_date = get_due_date(self.posting_date, "Customer", self.customer, self.company) | ||||
| 
 | ||||
| 		super(SalesInvoice, self).set_missing_values(for_validate) | ||||
| 
 | ||||
| 		print_format = pos.get("print_format") if pos else None | ||||
| 		if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')): | ||||
| 			print_format = 'POS Invoice' | ||||
| 
 | ||||
| 		if pos: | ||||
| 			return { | ||||
| 				"print_format": print_format, | ||||
| 				"allow_edit_rate": pos.get("allow_user_to_edit_rate"), | ||||
| 				"allow_edit_discount": pos.get("allow_user_to_edit_discount"), | ||||
| 				"campaign": pos.get("campaign"), | ||||
| 				"allow_print_before_pay": pos.get("allow_print_before_pay") | ||||
| 			} | ||||
| 
 | ||||
| 	def set_account_for_mode_of_payment(self): | ||||
| 		self.payments = [d for d in self.payments if d.amount or d.base_amount or d.default] | ||||
| 		for pay in self.payments: | ||||
| 			if not pay.account: | ||||
| 				pay.account = get_bank_cash_account(pay.mode_of_payment, self.company).get("account") | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_stock_availability(item_code, warehouse): | ||||
| 	latest_sle = frappe.db.sql("""select qty_after_transaction  | ||||
| 		from `tabStock Ledger Entry`  | ||||
| 		where item_code = %s and warehouse = %s | ||||
| 		order by posting_date desc, posting_time desc | ||||
| 		limit 1""", (item_code, warehouse), as_dict=1) | ||||
| 	 | ||||
| 	pos_sales_qty = frappe.db.sql("""select sum(p_item.qty) as qty | ||||
| 		from `tabPOS Invoice` p, `tabPOS Invoice Item` p_item | ||||
| 		where p.name = p_item.parent  | ||||
| 		and p.consolidated_invoice is NULL  | ||||
| 		and p.docstatus = 1 | ||||
| 		and p_item.docstatus = 1 | ||||
| 		and p_item.item_code = %s | ||||
| 		and p_item.warehouse = %s | ||||
| 		""", (item_code, warehouse), as_dict=1) | ||||
| 	 | ||||
| 	sle_qty = latest_sle[0].qty_after_transaction or 0 if latest_sle else 0 | ||||
| 	pos_sales_qty = pos_sales_qty[0].qty or 0 if pos_sales_qty else 0 | ||||
| 	 | ||||
| 	if sle_qty and pos_sales_qty and sle_qty > pos_sales_qty: | ||||
| 		return sle_qty - pos_sales_qty | ||||
| 	else: | ||||
| 		# when sle_qty is 0 | ||||
| 		# when sle_qty > 0 and pos_sales_qty is 0 | ||||
| 		return sle_qty | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def make_sales_return(source_name, target_doc=None): | ||||
| 	from erpnext.controllers.sales_and_purchase_return import make_return_doc | ||||
| 	return make_return_doc("POS Invoice", source_name, target_doc) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def make_merge_log(invoices): | ||||
| 	import json | ||||
| 	from six import string_types | ||||
| 
 | ||||
| 	if isinstance(invoices, string_types): | ||||
| 		invoices = json.loads(invoices) | ||||
| 
 | ||||
| 	if len(invoices) == 0: | ||||
| 		frappe.throw(_('Atleast one invoice has to be selected.')) | ||||
| 
 | ||||
| 	merge_log = frappe.new_doc("POS Invoice Merge Log") | ||||
| 	merge_log.posting_date = getdate(nowdate()) | ||||
| 	for inv in invoices: | ||||
| 		inv_data = frappe.db.get_values("POS Invoice", inv.get('name'),  | ||||
| 			["customer", "posting_date", "grand_total"], as_dict=1)[0] | ||||
| 		merge_log.customer = inv_data.customer | ||||
| 		merge_log.append("pos_invoices", { | ||||
| 			'pos_invoice': inv.get('name'), | ||||
| 			'customer': inv_data.customer, | ||||
| 			'posting_date': inv_data.posting_date, | ||||
| 			'grand_total': inv_data.grand_total  | ||||
| 		}) | ||||
| 
 | ||||
| 	if merge_log.get('pos_invoices'): | ||||
| 		return merge_log.as_dict() | ||||
							
								
								
									
										42
									
								
								erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										42
									
								
								erpnext/accounts/doctype/pos_invoice/pos_invoice_list.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,42 @@ | ||||
| // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 | ||||
| // License: GNU General Public License v3. See license.txt
 | ||||
| 
 | ||||
| // render
 | ||||
| frappe.listview_settings['POS Invoice'] = { | ||||
| 	add_fields: ["customer", "customer_name", "base_grand_total", "outstanding_amount", "due_date", "company", | ||||
| 		"currency", "is_return"], | ||||
| 	get_indicator: function(doc) { | ||||
| 		var status_color = { | ||||
| 			"Draft": "red", | ||||
| 			"Unpaid": "orange", | ||||
| 			"Paid": "green", | ||||
| 			"Submitted": "blue", | ||||
| 			"Consolidated": "green", | ||||
| 			"Return": "darkgrey", | ||||
| 			"Unpaid and Discounted": "orange", | ||||
| 			"Overdue and Discounted": "red", | ||||
| 			"Overdue": "red" | ||||
| 
 | ||||
| 		}; | ||||
| 		return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; | ||||
| 	}, | ||||
| 	right_column: "grand_total", | ||||
| 	onload: function(me) { | ||||
| 		me.page.add_action_item('Make Merge Log', function() { | ||||
| 			const invoices = me.get_checked_items(); | ||||
| 			frappe.call({ | ||||
| 				method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_merge_log", | ||||
| 				freeze: true, | ||||
| 				args:{ | ||||
| 					"invoices": invoices | ||||
| 				}, | ||||
| 				callback: function (r) { | ||||
| 					if (r.message) { | ||||
| 						var doc = frappe.model.sync(r.message)[0]; | ||||
| 						frappe.set_route("Form", doc.doctype, doc.name); | ||||
| 					} | ||||
| 				} | ||||
| 			}); | ||||
| 		}); | ||||
| 	}, | ||||
| }; | ||||
							
								
								
									
										324
									
								
								erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										324
									
								
								erpnext/accounts/doctype/pos_invoice/test_pos_invoice.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,324 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors | ||||
| # See license.txt | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| 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 | ||||
| 
 | ||||
| class TestPOSInvoice(unittest.TestCase): | ||||
| 	def test_timestamp_change(self): | ||||
| 		w = create_pos_invoice(do_not_save=1) | ||||
| 		w.docstatus = 0 | ||||
| 		w.insert() | ||||
| 
 | ||||
| 		w2 = frappe.get_doc(w.doctype, w.name) | ||||
| 
 | ||||
| 		import time | ||||
| 		time.sleep(1) | ||||
| 		w.save() | ||||
| 
 | ||||
| 		import time | ||||
| 		time.sleep(1) | ||||
| 		self.assertRaises(frappe.TimestampMismatchError, w2.save) | ||||
| 	 | ||||
| 	def test_change_naming_series(self): | ||||
| 		inv = create_pos_invoice(do_not_submit=1) | ||||
| 		inv.naming_series = 'TEST-' | ||||
| 
 | ||||
| 		self.assertRaises(frappe.CannotChangeConstantError, inv.save) | ||||
| 	 | ||||
| 	def test_discount_and_inclusive_tax(self): | ||||
| 		inv = create_pos_invoice(qty=100, rate=50, do_not_save=1) | ||||
| 		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 | ||||
| 		}) | ||||
| 		inv.insert() | ||||
| 
 | ||||
| 		self.assertEqual(inv.net_total, 4385.96) | ||||
| 		self.assertEqual(inv.grand_total, 5000) | ||||
| 
 | ||||
| 		inv.reload() | ||||
| 
 | ||||
| 		inv.discount_amount = 100 | ||||
| 		inv.apply_discount_on = 'Net Total' | ||||
| 		inv.payment_schedule = [] | ||||
| 
 | ||||
| 		inv.save() | ||||
| 
 | ||||
| 		self.assertEqual(inv.net_total, 4285.96) | ||||
| 		self.assertEqual(inv.grand_total, 4885.99) | ||||
| 
 | ||||
| 		inv.reload() | ||||
| 
 | ||||
| 		inv.discount_amount = 100 | ||||
| 		inv.apply_discount_on = 'Grand Total' | ||||
| 		inv.payment_schedule = [] | ||||
| 
 | ||||
| 		inv.save() | ||||
| 
 | ||||
| 		self.assertEqual(inv.net_total, 4298.25) | ||||
| 		self.assertEqual(inv.grand_total, 4900.00) | ||||
| 	 | ||||
| 	def test_tax_calculation_with_multiple_items(self): | ||||
| 		inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=True) | ||||
| 		item_row = inv.get("items")[0] | ||||
| 		for qty in (54, 288, 144, 430): | ||||
| 			item_row_copy = copy.deepcopy(item_row) | ||||
| 			item_row_copy.qty = qty | ||||
| 			inv.append("items", item_row_copy) | ||||
| 
 | ||||
| 		inv.append("taxes", { | ||||
| 			"account_head": "_Test Account VAT - _TC", | ||||
| 			"charge_type": "On Net Total", | ||||
| 			"cost_center": "_Test Cost Center - _TC", | ||||
| 			"description": "VAT", | ||||
| 			"doctype": "Sales Taxes and Charges", | ||||
| 			"rate": 19 | ||||
| 		}) | ||||
| 		inv.insert() | ||||
| 
 | ||||
| 		self.assertEqual(inv.net_total, 4600) | ||||
| 
 | ||||
| 		self.assertEqual(inv.get("taxes")[0].tax_amount, 874.0) | ||||
| 		self.assertEqual(inv.get("taxes")[0].total, 5474.0) | ||||
| 
 | ||||
| 		self.assertEqual(inv.grand_total, 5474.0) | ||||
| 
 | ||||
| 	def test_tax_calculation_with_item_tax_template(self): | ||||
| 		inv = create_pos_invoice(qty=84, rate=4.6, do_not_save=1) | ||||
| 		item_row = inv.get("items")[0] | ||||
| 
 | ||||
| 		add_items = [ | ||||
| 			(54, '_Test Account Excise Duty @ 12'), | ||||
| 			(288, '_Test Account Excise Duty @ 15'), | ||||
| 			(144, '_Test Account Excise Duty @ 20'), | ||||
| 			(430, '_Test Item Tax Template 1') | ||||
| 		] | ||||
| 		for qty, item_tax_template in add_items: | ||||
| 			item_row_copy = copy.deepcopy(item_row) | ||||
| 			item_row_copy.qty = qty | ||||
| 			item_row_copy.item_tax_template = item_tax_template | ||||
| 			inv.append("items", item_row_copy) | ||||
| 
 | ||||
| 		inv.append("taxes", { | ||||
| 			"account_head": "_Test Account Excise Duty - _TC", | ||||
| 			"charge_type": "On Net Total", | ||||
| 			"cost_center": "_Test Cost Center - _TC", | ||||
| 			"description": "Excise Duty", | ||||
| 			"doctype": "Sales Taxes and Charges", | ||||
| 			"rate": 11 | ||||
| 		}) | ||||
| 		inv.append("taxes", { | ||||
| 			"account_head": "_Test Account Education Cess - _TC", | ||||
| 			"charge_type": "On Net Total", | ||||
| 			"cost_center": "_Test Cost Center - _TC", | ||||
| 			"description": "Education Cess", | ||||
| 			"doctype": "Sales Taxes and Charges", | ||||
| 			"rate": 0 | ||||
| 		}) | ||||
| 		inv.append("taxes", { | ||||
| 			"account_head": "_Test Account S&H Education Cess - _TC", | ||||
| 			"charge_type": "On Net Total", | ||||
| 			"cost_center": "_Test Cost Center - _TC", | ||||
| 			"description": "S&H Education Cess", | ||||
| 			"doctype": "Sales Taxes and Charges", | ||||
| 			"rate": 3 | ||||
| 		}) | ||||
| 		inv.insert() | ||||
| 
 | ||||
| 		self.assertEqual(inv.net_total, 4600) | ||||
| 
 | ||||
| 		self.assertEqual(inv.get("taxes")[0].tax_amount, 502.41) | ||||
| 		self.assertEqual(inv.get("taxes")[0].total, 5102.41) | ||||
| 
 | ||||
| 		self.assertEqual(inv.get("taxes")[1].tax_amount, 197.80) | ||||
| 		self.assertEqual(inv.get("taxes")[1].total, 5300.21) | ||||
| 
 | ||||
| 		self.assertEqual(inv.get("taxes")[2].tax_amount, 375.36) | ||||
| 		self.assertEqual(inv.get("taxes")[2].total, 5675.57) | ||||
| 
 | ||||
| 		self.assertEqual(inv.grand_total, 5675.57) | ||||
| 		self.assertEqual(inv.rounding_adjustment, 0.43) | ||||
| 		self.assertEqual(inv.rounded_total, 5676.0) | ||||
| 	 | ||||
| 	def test_tax_calculation_with_multiple_items_and_discount(self): | ||||
| 		inv = create_pos_invoice(qty=1, rate=75, do_not_save=True) | ||||
| 		item_row = inv.get("items")[0] | ||||
| 		for rate in (500, 200, 100, 50, 50): | ||||
| 			item_row_copy = copy.deepcopy(item_row) | ||||
| 			item_row_copy.price_list_rate = rate | ||||
| 			item_row_copy.rate = rate | ||||
| 			inv.append("items", item_row_copy) | ||||
| 
 | ||||
| 		inv.apply_discount_on = "Net Total" | ||||
| 		inv.discount_amount = 75.0 | ||||
| 
 | ||||
| 		inv.append("taxes", { | ||||
| 			"account_head": "_Test Account VAT - _TC", | ||||
| 			"charge_type": "On Net Total", | ||||
| 			"cost_center": "_Test Cost Center - _TC", | ||||
| 			"description": "VAT", | ||||
| 			"doctype": "Sales Taxes and Charges", | ||||
| 			"rate": 24 | ||||
| 		}) | ||||
| 		inv.insert() | ||||
| 
 | ||||
| 		self.assertEqual(inv.total, 975) | ||||
| 		self.assertEqual(inv.net_total, 900) | ||||
| 
 | ||||
| 		self.assertEqual(inv.get("taxes")[0].tax_amount, 216.0) | ||||
| 		self.assertEqual(inv.get("taxes")[0].total, 1116.0) | ||||
| 
 | ||||
| 		self.assertEqual(inv.grand_total, 1116.0) | ||||
| 
 | ||||
| 	def test_pos_returns_with_repayment(self): | ||||
| 		pos = create_pos_invoice(qty = 10, do_not_save=True) | ||||
| 
 | ||||
| 		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.insert() | ||||
| 		pos.submit() | ||||
| 
 | ||||
| 		pos_return = make_sales_return(pos.name) | ||||
| 
 | ||||
| 		pos_return.insert() | ||||
| 		pos_return.submit() | ||||
| 
 | ||||
| 		self.assertEqual(pos_return.get('payments')[0].amount, -500) | ||||
| 		self.assertEqual(pos_return.get('payments')[1].amount, -500) | ||||
| 	 | ||||
| 	def test_pos_change_amount(self): | ||||
| 		pos = create_pos_invoice(company= "_Test Company", debit_to="Debtors - _TC", | ||||
| 			income_account = "Sales - _TC", expense_account = "Cost of Goods Sold - _TC", rate=105, | ||||
| 			cost_center = "Main - _TC", do_not_save=True) | ||||
| 
 | ||||
| 		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.insert() | ||||
| 		pos.submit() | ||||
| 
 | ||||
| 		self.assertEqual(pos.grand_total, 105.0) | ||||
| 		self.assertEqual(pos.change_amount, 5.0) | ||||
| 	 | ||||
| 	def test_without_payment(self): | ||||
| 		inv = create_pos_invoice(do_not_save=1) | ||||
| 		# Check that the invoice cannot be submitted without payments | ||||
| 		inv.payments = [] | ||||
| 		self.assertRaises(frappe.ValidationError, inv.insert) | ||||
| 	 | ||||
| 	def test_serialized_item_transaction(self): | ||||
| 		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_serialized_item | ||||
| 		from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos | ||||
| 
 | ||||
| 		se = make_serialized_item(target_warehouse="_Test Warehouse - _TC") | ||||
| 		serial_nos = get_serial_nos(se.get("items")[0].serial_no) | ||||
| 
 | ||||
| 		pos = create_pos_invoice(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 - _TC', 'amount': 1000}) | ||||
| 
 | ||||
| 		pos.insert() | ||||
| 		pos.submit() | ||||
| 
 | ||||
| 		pos2 = create_pos_invoice(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 - _TC', 'amount': 1000}) | ||||
| 		 | ||||
| 		self.assertRaises(frappe.ValidationError, pos2.insert) | ||||
| 	 | ||||
| 	def test_loyalty_points(self): | ||||
| 		from erpnext.accounts.doctype.loyalty_program.test_loyalty_program import create_records | ||||
| 		from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points | ||||
| 
 | ||||
| 		create_records() | ||||
| 		frappe.db.set_value("Customer", "Test Loyalty Customer", "loyalty_program", "Test Single Loyalty") | ||||
| 		before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty") | ||||
| 
 | ||||
| 		inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000) | ||||
| 
 | ||||
| 		lpe = frappe.get_doc('Loyalty Point Entry', {'invoice_type': 'POS Invoice', 'invoice': inv.name, 'customer': inv.customer}) | ||||
| 		after_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) | ||||
| 
 | ||||
| 		self.assertEqual(inv.get('loyalty_program'), "Test Single Loyalty") | ||||
| 		self.assertEqual(lpe.loyalty_points, 10) | ||||
| 		self.assertEqual(after_lp_details.loyalty_points, before_lp_details.loyalty_points + 10) | ||||
| 
 | ||||
| 		inv.cancel() | ||||
| 		after_cancel_lp_details = get_loyalty_program_details_with_points(inv.customer, company=inv.company, loyalty_program=inv.loyalty_program) | ||||
| 		self.assertEqual(after_cancel_lp_details.loyalty_points, before_lp_details.loyalty_points) | ||||
| 	 | ||||
| 	def test_loyalty_points_redeemption(self): | ||||
| 		from erpnext.accounts.doctype.loyalty_program.loyalty_program import get_loyalty_program_details_with_points | ||||
| 		# add 10 loyalty points | ||||
| 		create_pos_invoice(customer="Test Loyalty Customer", rate=10000) | ||||
| 
 | ||||
| 		before_lp_details = get_loyalty_program_details_with_points("Test Loyalty Customer", company="_Test Company", loyalty_program="Test Single Loyalty") | ||||
| 		 | ||||
| 		inv = create_pos_invoice(customer="Test Loyalty Customer", rate=10000, do_not_save=1) | ||||
| 		inv.redeem_loyalty_points = 1 | ||||
| 		inv.loyalty_points = before_lp_details.loyalty_points | ||||
| 		inv.loyalty_amount = inv.loyalty_points * before_lp_details.conversion_factor | ||||
| 		inv.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 10000 - inv.loyalty_amount}) | ||||
| 		inv.paid_amount = 10000 | ||||
| 		inv.submit() | ||||
| 
 | ||||
| 		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 create_pos_invoice(**args): | ||||
| 	args = frappe._dict(args) | ||||
| 	pos_profile = None | ||||
| 	if not args.pos_profile: | ||||
| 		pos_profile = make_pos_profile() | ||||
| 		pos_profile.save() | ||||
| 
 | ||||
| 	pos_inv = frappe.new_doc("POS Invoice") | ||||
| 	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() | ||||
| 
 | ||||
| 	pos_inv.company = args.company or "_Test Company" | ||||
| 	pos_inv.customer = args.customer or "_Test Customer" | ||||
| 	pos_inv.debit_to = args.debit_to or "Debtors - _TC" | ||||
| 	pos_inv.is_return = args.is_return | ||||
| 	pos_inv.return_against = args.return_against | ||||
| 	pos_inv.currency=args.currency or "INR" | ||||
| 	pos_inv.conversion_rate = args.conversion_rate or 1 | ||||
| 	pos_inv.account_for_change_amount = "Cash - _TC" | ||||
| 
 | ||||
| 	pos_inv.append("items", { | ||||
| 		"item_code": args.item or args.item_code or "_Test Item", | ||||
| 		"warehouse": args.warehouse or "_Test Warehouse - _TC", | ||||
| 		"qty": args.qty or 1, | ||||
| 		"rate": args.rate if args.get("rate") is not None else 100, | ||||
| 		"income_account": args.income_account or "Sales - _TC", | ||||
| 		"expense_account": args.expense_account or "Cost of Goods Sold - _TC", | ||||
| 		"cost_center": args.cost_center or "_Test Cost Center - _TC", | ||||
| 		"serial_no": args.serial_no | ||||
| 	}) | ||||
| 
 | ||||
| 	if not args.do_not_save: | ||||
| 		pos_inv.insert() | ||||
| 		if not args.do_not_submit: | ||||
| 			pos_inv.submit() | ||||
| 		else: | ||||
| 			pos_inv.payment_schedule = [] | ||||
| 	else: | ||||
| 		pos_inv.payment_schedule = [] | ||||
| 
 | ||||
| 	return pos_inv | ||||
							
								
								
									
										805
									
								
								erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										805
									
								
								erpnext/accounts/doctype/pos_invoice_item/pos_invoice_item.json
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,805 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "autoname": "hash", | ||||
|  "creation": "2020-01-27 13:04:55.229516", | ||||
|  "doctype": "DocType", | ||||
|  "document_type": "Document", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "barcode", | ||||
|   "item_code", | ||||
|   "col_break1", | ||||
|   "item_name", | ||||
|   "customer_item_code", | ||||
|   "description_section", | ||||
|   "description", | ||||
|   "item_group", | ||||
|   "brand", | ||||
|   "image_section", | ||||
|   "image", | ||||
|   "image_view", | ||||
|   "quantity_and_rate", | ||||
|   "qty", | ||||
|   "stock_uom", | ||||
|   "col_break2", | ||||
|   "uom", | ||||
|   "conversion_factor", | ||||
|   "stock_qty", | ||||
|   "section_break_17", | ||||
|   "price_list_rate", | ||||
|   "base_price_list_rate", | ||||
|   "discount_and_margin", | ||||
|   "margin_type", | ||||
|   "margin_rate_or_amount", | ||||
|   "rate_with_margin", | ||||
|   "column_break_19", | ||||
|   "discount_percentage", | ||||
|   "discount_amount", | ||||
|   "base_rate_with_margin", | ||||
|   "section_break1", | ||||
|   "rate", | ||||
|   "amount", | ||||
|   "item_tax_template", | ||||
|   "col_break3", | ||||
|   "base_rate", | ||||
|   "base_amount", | ||||
|   "pricing_rules", | ||||
|   "is_free_item", | ||||
|   "section_break_21", | ||||
|   "net_rate", | ||||
|   "net_amount", | ||||
|   "column_break_24", | ||||
|   "base_net_rate", | ||||
|   "base_net_amount", | ||||
|   "drop_ship", | ||||
|   "delivered_by_supplier", | ||||
|   "accounting", | ||||
|   "income_account", | ||||
|   "is_fixed_asset", | ||||
|   "asset", | ||||
|   "finance_book", | ||||
|   "col_break4", | ||||
|   "expense_account", | ||||
|   "deferred_revenue", | ||||
|   "deferred_revenue_account", | ||||
|   "service_stop_date", | ||||
|   "enable_deferred_revenue", | ||||
|   "column_break_50", | ||||
|   "service_start_date", | ||||
|   "service_end_date", | ||||
|   "section_break_18", | ||||
|   "weight_per_unit", | ||||
|   "total_weight", | ||||
|   "column_break_21", | ||||
|   "weight_uom", | ||||
|   "warehouse_and_reference", | ||||
|   "warehouse", | ||||
|   "target_warehouse", | ||||
|   "quality_inspection", | ||||
|   "batch_no", | ||||
|   "col_break5", | ||||
|   "allow_zero_valuation_rate", | ||||
|   "serial_no", | ||||
|   "item_tax_rate", | ||||
|   "actual_batch_qty", | ||||
|   "actual_qty", | ||||
|   "edit_references", | ||||
|   "sales_order", | ||||
|   "so_detail", | ||||
|   "column_break_74", | ||||
|   "delivery_note", | ||||
|   "dn_detail", | ||||
|   "delivered_qty", | ||||
|   "accounting_dimensions_section", | ||||
|   "cost_center", | ||||
|   "dimension_col_break", | ||||
|   "project", | ||||
|   "section_break_54", | ||||
|   "page_break" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "barcode", | ||||
|    "fieldtype": "Data", | ||||
|    "label": "Barcode", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "bold": 1, | ||||
|    "columns": 4, | ||||
|    "fieldname": "item_code", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Item", | ||||
|    "oldfieldname": "item_code", | ||||
|    "oldfieldtype": "Link", | ||||
|    "options": "Item", | ||||
|    "search_index": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "col_break1", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "item_name", | ||||
|    "fieldtype": "Data", | ||||
|    "in_global_search": 1, | ||||
|    "label": "Item Name", | ||||
|    "oldfieldname": "item_name", | ||||
|    "oldfieldtype": "Data", | ||||
|    "print_hide": 1, | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "customer_item_code", | ||||
|    "fieldtype": "Data", | ||||
|    "hidden": 1, | ||||
|    "label": "Customer's Item Code", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "description_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Description" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "description", | ||||
|    "fieldtype": "Text Editor", | ||||
|    "label": "Description", | ||||
|    "oldfieldname": "description", | ||||
|    "oldfieldtype": "Text", | ||||
|    "print_width": "200px", | ||||
|    "reqd": 1, | ||||
|    "width": "200px" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "item_group", | ||||
|    "fieldtype": "Link", | ||||
|    "hidden": 1, | ||||
|    "label": "Item Group", | ||||
|    "oldfieldname": "item_group", | ||||
|    "oldfieldtype": "Link", | ||||
|    "options": "Item Group", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "brand", | ||||
|    "fieldtype": "Data", | ||||
|    "hidden": 1, | ||||
|    "label": "Brand Name", | ||||
|    "oldfieldname": "brand", | ||||
|    "oldfieldtype": "Data", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "image_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Image" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "image", | ||||
|    "fieldtype": "Attach", | ||||
|    "hidden": 1, | ||||
|    "label": "Image" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "image_view", | ||||
|    "fieldtype": "Image", | ||||
|    "label": "Image View", | ||||
|    "options": "image", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "quantity_and_rate", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "bold": 1, | ||||
|    "columns": 2, | ||||
|    "fieldname": "qty", | ||||
|    "fieldtype": "Float", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Quantity", | ||||
|    "oldfieldname": "qty", | ||||
|    "oldfieldtype": "Currency" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "stock_uom", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Stock UOM", | ||||
|    "options": "UOM", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "col_break2", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "uom", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "UOM", | ||||
|    "options": "UOM", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "conversion_factor", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "UOM Conversion Factor", | ||||
|    "print_hide": 1, | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "stock_qty", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Qty as per Stock UOM", | ||||
|    "no_copy": 1, | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_17", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "price_list_rate", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Price List Rate", | ||||
|    "oldfieldname": "ref_rate", | ||||
|    "oldfieldtype": "Currency", | ||||
|    "options": "currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "base_price_list_rate", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Price List Rate (Company Currency)", | ||||
|    "oldfieldname": "base_ref_rate", | ||||
|    "oldfieldtype": "Currency", | ||||
|    "options": "Company:company:default_currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "discount_and_margin", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Discount and Margin" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "price_list_rate", | ||||
|    "fieldname": "margin_type", | ||||
|    "fieldtype": "Select", | ||||
|    "label": "Margin Type", | ||||
|    "options": "\nPercentage\nAmount", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:doc.margin_type && doc.price_list_rate", | ||||
|    "fieldname": "margin_rate_or_amount", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Margin Rate or Amount", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount", | ||||
|    "fieldname": "rate_with_margin", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Rate With Margin", | ||||
|    "options": "currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_19", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "price_list_rate", | ||||
|    "fieldname": "discount_percentage", | ||||
|    "fieldtype": "Percent", | ||||
|    "label": "Discount (%) on Price List Rate with Margin", | ||||
|    "oldfieldname": "adj_rate", | ||||
|    "oldfieldtype": "Float", | ||||
|    "precision": "2", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "price_list_rate", | ||||
|    "fieldname": "discount_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Discount Amount", | ||||
|    "options": "currency" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:doc.margin_type && doc.price_list_rate && doc.margin_rate_or_amount", | ||||
|    "fieldname": "base_rate_with_margin", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Rate With Margin (Company Currency)", | ||||
|    "options": "Company:company:default_currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break1", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "bold": 1, | ||||
|    "columns": 2, | ||||
|    "fieldname": "rate", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Rate", | ||||
|    "oldfieldname": "export_rate", | ||||
|    "oldfieldtype": "Currency", | ||||
|    "options": "currency", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "columns": 2, | ||||
|    "fieldname": "amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Amount", | ||||
|    "oldfieldname": "export_amount", | ||||
|    "oldfieldtype": "Currency", | ||||
|    "options": "currency", | ||||
|    "read_only": 1, | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "item_tax_template", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Item Tax Template", | ||||
|    "options": "Item Tax Template", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "col_break3", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "base_rate", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Rate (Company Currency)", | ||||
|    "oldfieldname": "basic_rate", | ||||
|    "oldfieldtype": "Currency", | ||||
|    "options": "Company:company:default_currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1, | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "base_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Amount (Company Currency)", | ||||
|    "oldfieldname": "amount", | ||||
|    "oldfieldtype": "Currency", | ||||
|    "options": "Company:company:default_currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1, | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "pricing_rules", | ||||
|    "fieldtype": "Small Text", | ||||
|    "hidden": 1, | ||||
|    "label": "Pricing Rules", | ||||
|    "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_21", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "net_rate", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Net Rate", | ||||
|    "options": "currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "net_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Net Amount", | ||||
|    "options": "currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_24", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "base_net_rate", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Net Rate (Company Currency)", | ||||
|    "options": "Company:company:default_currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "base_net_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Net Amount (Company Currency)", | ||||
|    "options": "Company:company:default_currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "collapsible_depends_on": "eval:doc.delivered_by_supplier==1", | ||||
|    "fieldname": "drop_ship", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Drop Ship" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "delivered_by_supplier", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Delivered By Supplier", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "accounting", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Accounting Details" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "income_account", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Income Account", | ||||
|    "oldfieldname": "income_account", | ||||
|    "oldfieldtype": "Link", | ||||
|    "options": "Account", | ||||
|    "print_hide": 1, | ||||
|    "print_width": "120px", | ||||
|    "reqd": 1, | ||||
|    "width": "120px" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "is_fixed_asset", | ||||
|    "fieldtype": "Check", | ||||
|    "hidden": 1, | ||||
|    "label": "Is Fixed Asset", | ||||
|    "no_copy": 1, | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "asset", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Asset", | ||||
|    "no_copy": 1, | ||||
|    "options": "Asset" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "asset", | ||||
|    "fieldname": "finance_book", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Finance Book", | ||||
|    "options": "Finance Book" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "col_break4", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "expense_account", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Expense Account", | ||||
|    "options": "Account", | ||||
|    "print_hide": 1, | ||||
|    "width": "120px" | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "deferred_revenue", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Deferred Revenue" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "enable_deferred_revenue", | ||||
|    "fieldname": "deferred_revenue_account", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Deferred Revenue Account", | ||||
|    "options": "Account" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "depends_on": "enable_deferred_revenue", | ||||
|    "fieldname": "service_stop_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Service Stop Date", | ||||
|    "no_copy": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "enable_deferred_revenue", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Enable Deferred Revenue" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_50", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "enable_deferred_revenue", | ||||
|    "fieldname": "service_start_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Service Start Date", | ||||
|    "no_copy": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "enable_deferred_revenue", | ||||
|    "fieldname": "service_end_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Service End Date", | ||||
|    "no_copy": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "section_break_18", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Item Weight Details" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "weight_per_unit", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Weight Per Unit", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "total_weight", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Total Weight", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_21", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "weight_uom", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Weight UOM", | ||||
|    "options": "UOM", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "collapsible_depends_on": "eval:doc.serial_no || doc.batch_no", | ||||
|    "fieldname": "warehouse_and_reference", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Stock Details" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "warehouse", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Warehouse", | ||||
|    "oldfieldname": "warehouse", | ||||
|    "oldfieldtype": "Link", | ||||
|    "options": "Warehouse", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "target_warehouse", | ||||
|    "fieldtype": "Link", | ||||
|    "hidden": 1, | ||||
|    "ignore_user_permissions": 1, | ||||
|    "label": "Customer Warehouse (Optional)", | ||||
|    "no_copy": 1, | ||||
|    "options": "Warehouse", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:!doc.__islocal", | ||||
|    "fieldname": "quality_inspection", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Quality Inspection", | ||||
|    "options": "Quality Inspection" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "batch_no", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Batch No", | ||||
|    "options": "Batch", | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "col_break5", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "allow_zero_valuation_rate", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Allow Zero Valuation Rate", | ||||
|    "no_copy": 1, | ||||
|    "print_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "serial_no", | ||||
|    "fieldtype": "Small Text", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Serial No", | ||||
|    "oldfieldname": "serial_no", | ||||
|    "oldfieldtype": "Small Text" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "item_tax_rate", | ||||
|    "fieldtype": "Small Text", | ||||
|    "hidden": 1, | ||||
|    "label": "Item Tax Rate", | ||||
|    "oldfieldname": "item_tax_rate", | ||||
|    "oldfieldtype": "Small Text", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "actual_batch_qty", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Available Batch Qty at Warehouse", | ||||
|    "no_copy": 1, | ||||
|    "print_hide": 1, | ||||
|    "print_width": "150px", | ||||
|    "read_only": 1, | ||||
|    "width": "150px" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "actual_qty", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Available Qty at Warehouse", | ||||
|    "oldfieldname": "actual_qty", | ||||
|    "oldfieldtype": "Currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "edit_references", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "References" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "sales_order", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Sales Order", | ||||
|    "no_copy": 1, | ||||
|    "oldfieldname": "sales_order", | ||||
|    "oldfieldtype": "Link", | ||||
|    "options": "Sales Order", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1, | ||||
|    "search_index": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "so_detail", | ||||
|    "fieldtype": "Data", | ||||
|    "hidden": 1, | ||||
|    "label": "Sales Order Item", | ||||
|    "no_copy": 1, | ||||
|    "oldfieldname": "so_detail", | ||||
|    "oldfieldtype": "Data", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1, | ||||
|    "search_index": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_74", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "delivery_note", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Delivery Note", | ||||
|    "no_copy": 1, | ||||
|    "oldfieldname": "delivery_note", | ||||
|    "oldfieldtype": "Link", | ||||
|    "options": "Delivery Note", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1, | ||||
|    "search_index": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "dn_detail", | ||||
|    "fieldtype": "Data", | ||||
|    "hidden": 1, | ||||
|    "label": "Delivery Note Item", | ||||
|    "no_copy": 1, | ||||
|    "oldfieldname": "dn_detail", | ||||
|    "oldfieldtype": "Data", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1, | ||||
|    "search_index": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "delivered_qty", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Delivered Qty", | ||||
|    "oldfieldname": "delivered_qty", | ||||
|    "oldfieldtype": "Currency", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "accounting_dimensions_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Accounting Dimensions" | ||||
|   }, | ||||
|   { | ||||
|    "default": ":Company", | ||||
|    "fieldname": "cost_center", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Cost Center", | ||||
|    "oldfieldname": "cost_center", | ||||
|    "oldfieldtype": "Link", | ||||
|    "options": "Cost Center", | ||||
|    "print_hide": 1, | ||||
|    "print_width": "120px", | ||||
|    "reqd": 1, | ||||
|    "width": "120px" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "dimension_col_break", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_54", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "default": "0", | ||||
|    "fieldname": "page_break", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Page Break", | ||||
|    "no_copy": 1, | ||||
|    "print_hide": 1, | ||||
|    "report_hide": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "project", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Project", | ||||
|    "options": "Project" | ||||
|   } | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-07-22 13:40:34.418346", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Invoice Item", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC" | ||||
| } | ||||
| @ -0,0 +1,10 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| # import frappe | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class POSInvoiceItem(Document): | ||||
| 	pass | ||||
| @ -0,0 +1,16 @@ | ||||
| // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
 | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| frappe.ui.form.on('POS Invoice Merge Log', { | ||||
| 	setup: function(frm) { | ||||
| 		frm.set_query("pos_invoice", "pos_invoices", doc => { | ||||
| 			return{ | ||||
| 				filters: {  | ||||
| 					'docstatus': 1, | ||||
| 					'customer': doc.customer,  | ||||
| 					'consolidated_invoice': ''  | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
| @ -0,0 +1,147 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2020-01-28 11:56:33.945372", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "posting_date", | ||||
|   "customer", | ||||
|   "section_break_3", | ||||
|   "pos_invoices", | ||||
|   "references_section", | ||||
|   "consolidated_invoice", | ||||
|   "column_break_7", | ||||
|   "consolidated_credit_note", | ||||
|   "amended_from" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "posting_date", | ||||
|    "fieldtype": "Date", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Posting Date", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "customer", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Customer", | ||||
|    "options": "Customer", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_3", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "pos_invoices", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "POS Invoices", | ||||
|    "options": "POS Invoice Reference", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "collapsible": 1, | ||||
|    "fieldname": "references_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "References" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "amended_from", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Amended From", | ||||
|    "no_copy": 1, | ||||
|    "options": "POS Invoice Merge Log", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "consolidated_invoice", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Consolidated Sales Invoice", | ||||
|    "options": "Sales Invoice", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_7", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "consolidated_credit_note", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Consolidated Credit Note", | ||||
|    "options": "Sales Invoice", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-29 15:08:41.317100", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Invoice Merge Log", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "System Manager", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Sales Manager", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Sales User", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Administrator", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   } | ||||
|  ], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -0,0 +1,180 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_days, cstr, nowdate | ||||
| from frappe.model.document import Document | ||||
| from frappe.model.mapper import map_doc | ||||
| from frappe.model import default_fields | ||||
| 
 | ||||
| from six import iteritems | ||||
| 
 | ||||
| class POSInvoiceMergeLog(Document): | ||||
| 	def validate(self): | ||||
| 		self.validate_customer() | ||||
| 		self.validate_pos_invoice_status() | ||||
| 
 | ||||
| 	def validate_customer(self): | ||||
| 		for d in self.pos_invoices: | ||||
| 			if d.customer != self.customer: | ||||
| 				frappe.throw(_("Row #{}: POS Invoice {} is not against customer {}").format(d.idx, d.pos_invoice, self.customer)) | ||||
| 
 | ||||
| 	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']) | ||||
| 			if docstatus != 1: | ||||
| 				frappe.throw(_("Row #{}: POS Invoice {} is not submitted yet").format(d.idx, d.pos_invoice)) | ||||
| 			if status in ['Consolidated']: | ||||
| 				frappe.throw(_("Row #{}: POS Invoice {} has been {}").format(d.idx, d.pos_invoice, status)) | ||||
| 
 | ||||
| 	def on_submit(self): | ||||
| 		pos_invoice_docs = [frappe.get_doc("POS Invoice", d.pos_invoice) for d in self.pos_invoices] | ||||
| 
 | ||||
| 		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) | ||||
| 		 | ||||
| 		if len(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 | ||||
| 
 | ||||
| 		self.update_pos_invoices(sales_invoice, credit_note) | ||||
| 
 | ||||
| 	def process_merging_into_sales_invoice(self, data): | ||||
| 		sales_invoice = self.get_new_sales_invoice() | ||||
| 		 | ||||
| 		sales_invoice = self.merge_pos_invoice_into(sales_invoice, data) | ||||
| 
 | ||||
| 		sales_invoice.is_consolidated = 1 | ||||
| 		sales_invoice.save() | ||||
| 		sales_invoice.submit() | ||||
| 		self.consolidated_invoice = sales_invoice.name | ||||
| 
 | ||||
| 		return sales_invoice.name | ||||
| 
 | ||||
| 	def process_merging_into_credit_note(self, data): | ||||
| 		credit_note = self.get_new_sales_invoice() | ||||
| 		credit_note.is_return = 1 | ||||
| 
 | ||||
| 		credit_note = self.merge_pos_invoice_into(credit_note, data) | ||||
| 
 | ||||
| 		credit_note.is_consolidated = 1 | ||||
| 		# TODO: return could be against multiple sales invoice which could also have been consolidated? | ||||
| 		credit_note.return_against = self.consolidated_invoice | ||||
| 		credit_note.save() | ||||
| 		credit_note.submit() | ||||
| 		self.consolidated_credit_note = credit_note.name | ||||
| 
 | ||||
| 		return credit_note.name | ||||
| 	 | ||||
| 	def merge_pos_invoice_into(self, invoice, data): | ||||
| 		items, payments, taxes = [], [], [] | ||||
| 		loyalty_amount_sum, loyalty_points_sum = 0, 0 | ||||
| 		for doc in data: | ||||
| 			map_doc(doc, invoice, table_map={ "doctype": invoice.doctype }) | ||||
| 			 | ||||
| 			if doc.redeem_loyalty_points: | ||||
| 				invoice.loyalty_redemption_account = doc.loyalty_redemption_account | ||||
| 				invoice.loyalty_redemption_cost_center = doc.loyalty_redemption_cost_center | ||||
| 				loyalty_points_sum += doc.loyalty_points | ||||
| 				loyalty_amount_sum += doc.loyalty_amount | ||||
| 			 | ||||
| 			for item in doc.get('items'): | ||||
| 				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) | ||||
| 						found = True | ||||
| 				if not found: | ||||
| 					tax.charge_type = 'Actual' | ||||
| 					taxes.append(tax) | ||||
| 
 | ||||
| 			for payment in doc.get('payments'): | ||||
| 				found = False | ||||
| 				for pay in payments: | ||||
| 					if pay.account == payment.account and pay.mode_of_payment == payment.mode_of_payment: | ||||
| 						pay.amount = flt(pay.amount) + flt(payment.amount) | ||||
| 						pay.base_amount = flt(pay.base_amount) + flt(payment.base_amount) | ||||
| 						found = True | ||||
| 				if not found: | ||||
| 					payments.append(payment) | ||||
| 
 | ||||
| 		if loyalty_points_sum: | ||||
| 			invoice.redeem_loyalty_points = 1 | ||||
| 			invoice.loyalty_points = loyalty_points_sum | ||||
| 			invoice.loyalty_amount = loyalty_amount_sum | ||||
| 
 | ||||
| 		invoice.set('items', items) | ||||
| 		invoice.set('payments', payments) | ||||
| 		invoice.set('taxes', taxes) | ||||
| 
 | ||||
| 		return invoice | ||||
| 	 | ||||
| 	def get_new_sales_invoice(self): | ||||
| 		sales_invoice = frappe.new_doc('Sales Invoice') | ||||
| 		sales_invoice.customer = self.customer | ||||
| 		sales_invoice.is_pos = 1 | ||||
| 		# date can be pos closing date? | ||||
| 		sales_invoice.posting_date = getdate(nowdate()) | ||||
| 
 | ||||
| 		return sales_invoice | ||||
| 	 | ||||
| 	def update_pos_invoices(self, sales_invoice, credit_note): | ||||
| 		for d in self.pos_invoices: | ||||
| 			doc = frappe.get_doc('POS Invoice', d.pos_invoice) | ||||
| 			if not doc.is_return: | ||||
| 				doc.update({'consolidated_invoice': sales_invoice}) | ||||
| 			else: | ||||
| 				doc.update({'consolidated_invoice': credit_note}) | ||||
| 			doc.set_status(update=True) | ||||
| 			doc.save() | ||||
| 
 | ||||
| def get_all_invoices(): | ||||
| 	filters = { | ||||
| 		'consolidated_invoice': [ 'in', [ '', None ]], | ||||
| 		'status': ['not in', ['Consolidated']], | ||||
| 		'docstatus': 1 | ||||
| 	} | ||||
| 	pos_invoices = frappe.db.get_all('POS Invoice', filters=filters, | ||||
| 		fields=["name as pos_invoice", 'posting_date', 'grand_total', 'customer']) | ||||
| 	 | ||||
| 	return pos_invoices | ||||
| 
 | ||||
| def get_invoices_customer_map(pos_invoices): | ||||
| 	# pos_invoice_customer_map = { 'Customer 1': [{}, {}, {}], 'Custoemr 2' : [{}] } | ||||
| 	pos_invoice_customer_map = {} | ||||
| 	for invoice in pos_invoices: | ||||
| 		customer = invoice.get('customer') | ||||
| 		pos_invoice_customer_map.setdefault(customer, []) | ||||
| 		pos_invoice_customer_map[customer].append(invoice) | ||||
| 	 | ||||
| 	return pos_invoice_customer_map | ||||
| 
 | ||||
| def merge_pos_invoices(pos_invoices=[]): | ||||
| 	if not pos_invoices: | ||||
| 		pos_invoices = get_all_invoices() | ||||
| 	 | ||||
| 	pos_invoice_map = get_invoices_customer_map(pos_invoices) | ||||
| 	create_merge_logs(pos_invoice_map) | ||||
| 
 | ||||
| def create_merge_logs(pos_invoice_customer_map): | ||||
| 	for customer, invoices in iteritems(pos_invoice_customer_map): | ||||
| 		merge_log = frappe.new_doc('POS Invoice Merge Log') | ||||
| 		merge_log.posting_date = getdate(nowdate()) | ||||
| 		merge_log.customer = customer | ||||
| 
 | ||||
| 		merge_log.set('pos_invoices', invoices) | ||||
| 		merge_log.save(ignore_permissions=True) | ||||
| 		merge_log.submit() | ||||
| 
 | ||||
| @ -0,0 +1,98 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors | ||||
| # See license.txt | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import frappe | ||||
| import unittest | ||||
| from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice | ||||
| from erpnext.accounts.doctype.pos_invoice.pos_invoice import make_sales_return | ||||
| from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import merge_pos_invoices | ||||
| from erpnext.accounts.doctype.pos_closing_entry.test_pos_closing_entry import init_user_and_profile | ||||
| 
 | ||||
| class TestPOSInvoiceMergeLog(unittest.TestCase): | ||||
| 	def test_consolidated_invoice_creation(self): | ||||
| 		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.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() | ||||
| 
 | ||||
| 		pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) | ||||
| 		pos_inv3.append('payments', { | ||||
| 			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 | ||||
| 		}) | ||||
| 		pos_inv3.submit() | ||||
| 
 | ||||
| 		merge_pos_invoices() | ||||
| 
 | ||||
| 		pos_inv.load_from_db() | ||||
| 		self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) | ||||
| 
 | ||||
| 		pos_inv3.load_from_db() | ||||
| 		self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) | ||||
| 
 | ||||
| 		self.assertFalse(pos_inv.consolidated_invoice == pos_inv3.consolidated_invoice) | ||||
| 
 | ||||
| 		frappe.set_user("Administrator") | ||||
| 		frappe.db.sql("delete from `tabPOS Profile`") | ||||
| 		frappe.db.sql("delete from `tabPOS Invoice`") | ||||
| 	 | ||||
| 	def test_consolidated_credit_note_creation(self): | ||||
| 		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.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() | ||||
| 
 | ||||
| 		pos_inv3 = create_pos_invoice(customer="_Test Customer 2", rate=2300, do_not_submit=1) | ||||
| 		pos_inv3.append('payments', { | ||||
| 			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 2300 | ||||
| 		}) | ||||
| 		pos_inv3.submit() | ||||
| 
 | ||||
| 		pos_inv_cn = make_sales_return(pos_inv.name) | ||||
| 		pos_inv_cn.set("payments", []) | ||||
| 		pos_inv_cn.append('payments', { | ||||
| 			'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': -300 | ||||
| 		}) | ||||
| 		pos_inv_cn.paid_amount = -300 | ||||
| 		pos_inv_cn.submit() | ||||
| 
 | ||||
| 		merge_pos_invoices() | ||||
| 
 | ||||
| 		pos_inv.load_from_db() | ||||
| 		self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv.consolidated_invoice)) | ||||
| 
 | ||||
| 		pos_inv3.load_from_db() | ||||
| 		self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv3.consolidated_invoice)) | ||||
| 
 | ||||
| 		pos_inv_cn.load_from_db() | ||||
| 		self.assertTrue(frappe.db.exists("Sales Invoice", pos_inv_cn.consolidated_invoice)) | ||||
| 		self.assertTrue(frappe.db.get_value("Sales Invoice", pos_inv_cn.consolidated_invoice, "is_return")) | ||||
| 
 | ||||
| 		frappe.set_user("Administrator") | ||||
| 		frappe.db.sql("delete from `tabPOS Profile`") | ||||
| 		frappe.db.sql("delete from `tabPOS Invoice`") | ||||
| 
 | ||||
| 
 | ||||
| @ -0,0 +1,65 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2020-01-28 11:54:47.149392", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "pos_invoice", | ||||
|   "posting_date", | ||||
|   "column_break_3", | ||||
|   "customer", | ||||
|   "grand_total" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "pos_invoice", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "POS Invoice", | ||||
|    "options": "POS Invoice", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_3", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "pos_invoice.customer", | ||||
|    "fieldname": "customer", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Customer", | ||||
|    "options": "Customer", | ||||
|    "read_only": 1, | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "pos_invoice.posting_date", | ||||
|    "fieldname": "posting_date", | ||||
|    "fieldtype": "Date", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Date", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fetch_from": "pos_invoice.grand_total", | ||||
|    "fieldname": "grand_total", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Amount", | ||||
|    "reqd": 1 | ||||
|   } | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-29 15:08:42.194979", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Invoice Reference", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -0,0 +1,10 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| # import frappe | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class POSInvoiceReference(Document): | ||||
| 	pass | ||||
| @ -0,0 +1,56 @@ | ||||
| // Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors
 | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| frappe.ui.form.on('POS Opening Entry', { | ||||
| 	setup(frm) { | ||||
| 		if (frm.doc.docstatus == 0) { | ||||
| 			frm.trigger('set_posting_date_read_only'); | ||||
| 			frm.set_value('period_start_date', frappe.datetime.now_datetime()); | ||||
| 			frm.set_value('user', frappe.session.user); | ||||
| 		} | ||||
| 
 | ||||
| 		frm.set_query("user", function(doc) { | ||||
| 			return { | ||||
| 				query: "erpnext.accounts.doctype.pos_closing_entry.pos_closing_entry.get_cashiers", | ||||
| 				filters: { 'parent': doc.pos_profile } | ||||
| 			}; | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	refresh(frm) { | ||||
| 		// set default posting date / time
 | ||||
| 		if(frm.doc.docstatus == 0) { | ||||
| 			if(!frm.doc.posting_date) { | ||||
| 				frm.set_value('posting_date', frappe.datetime.nowdate()); | ||||
| 			} | ||||
| 			frm.trigger('set_posting_date_read_only'); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	set_posting_date_read_only(frm) { | ||||
| 		if(frm.doc.docstatus == 0 && frm.doc.set_posting_date) { | ||||
| 			frm.set_df_property('posting_date', 'read_only', 0); | ||||
| 		} else { | ||||
| 			frm.set_df_property('posting_date', 'read_only', 1); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	set_posting_date(frm) { | ||||
| 		frm.trigger('set_posting_date_read_only'); | ||||
| 	}, | ||||
| 
 | ||||
| 	pos_profile: (frm) => { | ||||
| 		if (frm.doc.pos_profile) { | ||||
| 			frappe.db.get_doc("POS Profile", frm.doc.pos_profile) | ||||
| 				.then(({ payments }) => { | ||||
| 					if (payments.length) { | ||||
| 						frm.doc.balance_details = []; | ||||
| 						payments.forEach(({ mode_of_payment }) => { | ||||
| 							frm.add_child("balance_details", { mode_of_payment }); | ||||
| 						}) | ||||
| 						frm.refresh_field("balance_details"); | ||||
| 					} | ||||
| 				}); | ||||
| 		} | ||||
| 	} | ||||
| }); | ||||
| @ -0,0 +1,185 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "autoname": "POS-OPE-.YYYY.-.#####", | ||||
|  "creation": "2020-03-05 16:58:53.083708", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "period_start_date", | ||||
|   "period_end_date", | ||||
|   "status", | ||||
|   "column_break_3", | ||||
|   "posting_date", | ||||
|   "set_posting_date", | ||||
|   "section_break_5", | ||||
|   "company", | ||||
|   "pos_profile", | ||||
|   "pos_closing_entry", | ||||
|   "column_break_7", | ||||
|   "user", | ||||
|   "opening_balance_details_section", | ||||
|   "balance_details", | ||||
|   "section_break_9", | ||||
|   "amended_from" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "period_start_date", | ||||
|    "fieldtype": "Datetime", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Period Start Date", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "period_end_date", | ||||
|    "fieldtype": "Date", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Period End Date", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_3", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "Today", | ||||
|    "fieldname": "posting_date", | ||||
|    "fieldtype": "Date", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Posting Date", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_5", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "company", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Company", | ||||
|    "options": "Company", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "pos_profile", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "POS Profile", | ||||
|    "options": "POS Profile", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_7", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "user", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Cashier", | ||||
|    "options": "User", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_9", | ||||
|    "fieldtype": "Section Break", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "amended_from", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Amended From", | ||||
|    "no_copy": 1, | ||||
|    "options": "POS Opening Entry", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "set_posting_date", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Set Posting Date" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "default": "Draft", | ||||
|    "fieldname": "status", | ||||
|    "fieldtype": "Select", | ||||
|    "hidden": 1, | ||||
|    "label": "Status", | ||||
|    "options": "Draft\nOpen\nClosed\nCancelled", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "pos_closing_entry", | ||||
|    "fieldtype": "Data", | ||||
|    "label": "POS Closing Entry", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "opening_balance_details_section", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "balance_details", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "Opening Balance Details", | ||||
|    "options": "POS Opening Entry Detail", | ||||
|    "reqd": 1 | ||||
|   } | ||||
|  ], | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-29 15:08:40.955310", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Opening Entry", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "System Manager", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Sales Manager", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "cancel": 1, | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Administrator", | ||||
|    "share": 1, | ||||
|    "submit": 1, | ||||
|    "write": 1 | ||||
|   } | ||||
|  ], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -0,0 +1,25 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.utils import cint | ||||
| 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.set_status() | ||||
| 
 | ||||
| 	def validate_pos_profile_and_cashier(self): | ||||
| 		if self.company != frappe.db.get_value("POS Profile", self.pos_profile, "company"): | ||||
| 			frappe.throw(_("POS Profile {} does not belongs to company {}".format(self.pos_profile, self.company))) | ||||
| 
 | ||||
| 		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 on_submit(self): | ||||
| 		self.set_status(update=True) | ||||
| @ -0,0 +1,16 @@ | ||||
| // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 | ||||
| // License: GNU General Public License v3. See license.txt
 | ||||
| 
 | ||||
| // render
 | ||||
| frappe.listview_settings['POS Opening Entry'] = { | ||||
| 	get_indicator: function(doc) { | ||||
| 		var status_color = { | ||||
| 			"Draft": "grey", | ||||
| 			"Open": "orange", | ||||
| 			"Closed": "green", | ||||
| 			"Cancelled": "red" | ||||
| 
 | ||||
| 		}; | ||||
| 		return [__(doc.status), status_color[doc.status], "status,=,"+doc.status]; | ||||
| 	} | ||||
| }; | ||||
| @ -0,0 +1,28 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and Contributors | ||||
| # See license.txt | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import frappe | ||||
| import unittest | ||||
| 
 | ||||
| class TestPOSOpeningEntry(unittest.TestCase): | ||||
| 	pass | ||||
| 
 | ||||
| def create_opening_entry(pos_profile, user): | ||||
| 	entry = frappe.new_doc("POS Opening Entry") | ||||
| 	entry.pos_profile = pos_profile.name | ||||
| 	entry.user = user | ||||
| 	entry.company = pos_profile.company | ||||
| 	entry.period_start_date = frappe.utils.get_datetime() | ||||
| 
 | ||||
| 	balance_details = []; | ||||
| 	for d in pos_profile.payments: | ||||
| 		balance_details.append(frappe._dict({ | ||||
| 			'mode_of_payment': d.mode_of_payment | ||||
| 		})) | ||||
| 	 | ||||
| 	entry.set("balance_details", balance_details) | ||||
| 	entry.submit() | ||||
| 	 | ||||
| 	return entry.as_dict()	 | ||||
| @ -0,0 +1,42 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2020-04-28 16:44:32.440794", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "mode_of_payment", | ||||
|   "opening_amount" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "mode_of_payment", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Mode of Payment", | ||||
|    "options": "Mode of Payment", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "opening_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Opening Amount", | ||||
|    "options": "company:company_currency", | ||||
|    "reqd": 1 | ||||
|   } | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-29 15:08:41.949378", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Opening Entry Detail", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -0,0 +1,10 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| # import frappe | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class POSOpeningEntryDetail(Document): | ||||
| 	pass | ||||
| @ -0,0 +1,40 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "creation": "2020-04-30 14:37:08.148707", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "default", | ||||
|   "mode_of_payment" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "default": "0", | ||||
|    "depends_on": "eval:parent.doctype == 'POS Profile'", | ||||
|    "fieldname": "default", | ||||
|    "fieldtype": "Check", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Default" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "mode_of_payment", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Mode of Payment", | ||||
|    "options": "Mode of Payment", | ||||
|    "reqd": 1 | ||||
|   } | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-29 15:08:41.704844", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Payment Method", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC" | ||||
| } | ||||
| @ -0,0 +1,10 @@ | ||||
| # -*- coding: utf-8 -*- | ||||
| # Copyright (c) 2020, Frappe Technologies Pvt. Ltd. and contributors | ||||
| # For license information, please see license.txt | ||||
| 
 | ||||
| from __future__ import unicode_literals | ||||
| # import frappe | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| class POSPaymentMethod(Document): | ||||
| 	pass | ||||
| @ -28,7 +28,7 @@ frappe.ui.form.on("POS Profile", "onload", function(frm) { | ||||
| 
 | ||||
| frappe.ui.form.on('POS Profile', { | ||||
| 	setup: function(frm) { | ||||
| 		frm.set_query("print_format_for_online", function() { | ||||
| 		frm.set_query("print_format", function() { | ||||
| 			return { | ||||
| 				filters: [ | ||||
| 					['Print Format', 'doc_type', '=', 'Sales Invoice'], | ||||
| @ -49,12 +49,6 @@ frappe.ui.form.on('POS Profile', { | ||||
| 			return { filters: { doc_type: "Sales Invoice", print_format_type: "JS"} }; | ||||
| 		}); | ||||
| 
 | ||||
| 		frappe.db.get_value('POS Settings', 'POS Settings', 'use_pos_in_offline_mode', (r) => { | ||||
| 			const is_offline = r && cint(r.use_pos_in_offline_mode) | ||||
| 			frm.toggle_display('offline_pos_section', is_offline); | ||||
| 			frm.toggle_display('print_format_for_online', !is_offline); | ||||
| 		}); | ||||
| 
 | ||||
| 		frm.set_query('company_address', function(doc) { | ||||
| 			if(!doc.company) { | ||||
| 				frappe.throw(__('Please set Company')); | ||||
|  | ||||
| @ -1,4 +1,5 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "allow_rename": 1, | ||||
|  "autoname": "Prompt", | ||||
|  "creation": "2013-05-24 12:15:51", | ||||
| @ -11,17 +12,12 @@ | ||||
|   "customer", | ||||
|   "company", | ||||
|   "country", | ||||
|   "warehouse", | ||||
|   "campaign", | ||||
|   "company_address", | ||||
|   "column_break_9", | ||||
|   "update_stock", | ||||
|   "ignore_pricing_rule", | ||||
|   "allow_delete", | ||||
|   "allow_user_to_edit_rate", | ||||
|   "allow_user_to_edit_discount", | ||||
|   "allow_print_before_pay", | ||||
|   "display_items_in_stock", | ||||
|   "warehouse", | ||||
|   "campaign", | ||||
|   "company_address", | ||||
|   "section_break_15", | ||||
|   "applicable_for_users", | ||||
|   "section_break_11", | ||||
| @ -31,16 +27,11 @@ | ||||
|   "column_break_16", | ||||
|   "customer_groups", | ||||
|   "section_break_16", | ||||
|   "print_format_for_online", | ||||
|   "print_format", | ||||
|   "letter_head", | ||||
|   "column_break0", | ||||
|   "tc_name", | ||||
|   "select_print_heading", | ||||
|   "offline_pos_section", | ||||
|   "territory", | ||||
|   "column_break_31", | ||||
|   "print_format", | ||||
|   "customer_group", | ||||
|   "section_break_19", | ||||
|   "selling_price_list", | ||||
|   "currency", | ||||
| @ -104,15 +95,6 @@ | ||||
|    "fieldtype": "Read Only", | ||||
|    "label": "Country" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "update_stock", | ||||
|    "fieldname": "warehouse", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Warehouse", | ||||
|    "oldfieldname": "warehouse", | ||||
|    "oldfieldtype": "Link", | ||||
|    "options": "Warehouse" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "campaign", | ||||
|    "fieldtype": "Link", | ||||
| @ -129,48 +111,6 @@ | ||||
|    "fieldname": "column_break_9", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "1", | ||||
|    "fieldname": "update_stock", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Update Stock" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "ignore_pricing_rule", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Ignore Pricing Rule" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "allow_delete", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Allow Delete" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "allow_user_to_edit_rate", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Allow user to edit Rate" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "allow_user_to_edit_discount", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Allow user to edit Discount" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "allow_print_before_pay", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Allow Print Before Pay" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "display_items_in_stock", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Display Items In Stock" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_15", | ||||
|    "fieldtype": "Section Break", | ||||
| @ -185,13 +125,13 @@ | ||||
|   { | ||||
|    "fieldname": "section_break_11", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Mode of Payment" | ||||
|    "label": "Payment Methods" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "payments", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "Sales Invoice Payment", | ||||
|    "options": "Sales Invoice Payment" | ||||
|    "options": "POS Payment Method", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_14", | ||||
| @ -220,12 +160,6 @@ | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Print Settings" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "print_format_for_online", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Print Format for Online", | ||||
|    "options": "Print Format" | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "letter_head", | ||||
| @ -258,39 +192,6 @@ | ||||
|    "oldfieldtype": "Select", | ||||
|    "options": "Print Heading" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "offline_pos_section", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Offline POS Settings" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "territory", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Territory", | ||||
|    "oldfieldname": "territory", | ||||
|    "oldfieldtype": "Link", | ||||
|    "options": "Territory", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_31", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "default": "Point of Sale", | ||||
|    "fieldname": "print_format", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Print Format", | ||||
|    "options": "Print Format" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "customer_group", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Customer Group", | ||||
|    "options": "Customer Group", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_19", | ||||
|    "fieldtype": "Section Break", | ||||
| @ -380,20 +281,49 @@ | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Accounting Dimensions" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "dimension_col_break", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "tax_category", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Tax Category", | ||||
|    "options": "Tax Category" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "dimension_col_break", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "print_format", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Print Format", | ||||
|    "options": "Print Format" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "update_stock", | ||||
|    "fieldname": "warehouse", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Warehouse", | ||||
|    "oldfieldname": "warehouse", | ||||
|    "oldfieldtype": "Link", | ||||
|    "options": "Warehouse", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "update_stock", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Update Stock" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "ignore_pricing_rule", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Ignore Pricing Rule" | ||||
|   } | ||||
|  ], | ||||
|  "icon": "icon-cog", | ||||
|  "idx": 1, | ||||
|  "modified": "2020-01-24 15:52:03.797701", | ||||
|  "links": [], | ||||
|  "modified": "2020-06-29 12:20:30.977272", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Profile", | ||||
|  | ||||
| @ -5,8 +5,6 @@ from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import msgprint, _ | ||||
| from frappe.utils import cint, now | ||||
| from erpnext.accounts.doctype.sales_invoice.pos import get_child_nodes | ||||
| from erpnext.accounts.doctype.sales_invoice.sales_invoice import set_account_for_mode_of_payment | ||||
| from six import iteritems | ||||
| from frappe.model.document import Document | ||||
| 
 | ||||
| @ -16,7 +14,6 @@ class POSProfile(Document): | ||||
| 		self.validate_all_link_fields() | ||||
| 		self.validate_duplicate_groups() | ||||
| 		self.check_default_payment() | ||||
| 		self.validate_customer_territory_group() | ||||
| 
 | ||||
| 	def validate_default_profile(self): | ||||
| 		for row in self.applicable_for_users: | ||||
| @ -64,19 +61,6 @@ class POSProfile(Document): | ||||
| 			if len(default_mode_of_payment) > 1: | ||||
| 				frappe.throw(_("Multiple default mode of payment is not allowed")) | ||||
| 
 | ||||
| 	def validate_customer_territory_group(self): | ||||
| 		if not frappe.db.get_single_value('POS Settings', 'use_pos_in_offline_mode'): | ||||
| 			return | ||||
| 
 | ||||
| 		if not self.territory: | ||||
| 			frappe.throw(_("Territory is Required in POS Profile"), title="Mandatory Field") | ||||
| 
 | ||||
| 		if not self.customer_group: | ||||
| 			frappe.throw(_("Customer Group is Required in POS Profile"), title="Mandatory Field") | ||||
| 
 | ||||
| 	def before_save(self): | ||||
| 		set_account_for_mode_of_payment(self) | ||||
| 
 | ||||
| 	def on_update(self): | ||||
| 		self.set_defaults() | ||||
| 
 | ||||
| @ -111,9 +95,14 @@ def get_item_groups(pos_profile): | ||||
| 
 | ||||
| 	return list(set(item_groups)) | ||||
| 
 | ||||
| def get_child_nodes(group_type, root): | ||||
| 	lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"]) | ||||
| 	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("Sales Invoice").get_field("naming_series").options or "" | ||||
| 	return frappe.get_meta("POS Invoice").get_field("naming_series").options or "s" | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def pos_profile_query(doctype, txt, searchfield, start, page_len, filters): | ||||
|  | ||||
| @ -8,7 +8,7 @@ def get_data(): | ||||
| 		'fieldname': 'pos_profile', | ||||
| 		'transactions': [ | ||||
| 			{ | ||||
| 				'items': ['Sales Invoice', 'POS Closing Voucher'] | ||||
| 				'items': ['Sales Invoice', 'POS Closing Entry', 'POS Opening Entry'] | ||||
| 			} | ||||
| 		] | ||||
| 	} | ||||
|  | ||||
| @ -6,7 +6,7 @@ from __future__ import unicode_literals | ||||
| import frappe | ||||
| import unittest | ||||
| from erpnext.stock.get_item_details import get_pos_profile | ||||
| from erpnext.accounts.doctype.sales_invoice.pos import get_items_list, get_customers_list | ||||
| from erpnext.accounts.doctype.pos_profile.pos_profile import get_child_nodes | ||||
| 
 | ||||
| class TestPOSProfile(unittest.TestCase): | ||||
| 	def test_pos_profile(self): | ||||
| @ -29,6 +29,44 @@ class TestPOSProfile(unittest.TestCase): | ||||
| 
 | ||||
| 		frappe.db.sql("delete from `tabPOS Profile`") | ||||
| 
 | ||||
| def get_customers_list(pos_profile={}): | ||||
| 	cond = "1=1" | ||||
| 	customer_groups = [] | ||||
| 	if pos_profile.get('customer_groups'): | ||||
| 		# Get customers based on the customer groups defined in the POS profile | ||||
| 		for d in pos_profile.get('customer_groups'): | ||||
| 			customer_groups.extend([d.get('name') for d in get_child_nodes('Customer Group', d.get('customer_group'))]) | ||||
| 		cond = "customer_group in (%s)" % (', '.join(['%s'] * len(customer_groups))) | ||||
| 
 | ||||
| 	return frappe.db.sql(""" select name, customer_name, customer_group, | ||||
| 		territory, customer_pos_id from tabCustomer where disabled = 0 | ||||
| 		and {cond}""".format(cond=cond), tuple(customer_groups), as_dict=1) or {} | ||||
| 
 | ||||
| def get_items_list(pos_profile, company): | ||||
| 	cond = "" | ||||
| 	args_list = [] | ||||
| 	if pos_profile.get('item_groups'): | ||||
| 		# Get items based on the item groups defined in the POS profile | ||||
| 		for d in pos_profile.get('item_groups'): | ||||
| 			args_list.extend([d.name for d in get_child_nodes('Item Group', d.item_group)]) | ||||
| 		if args_list: | ||||
| 			cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list))) | ||||
| 
 | ||||
| 	return frappe.db.sql(""" | ||||
| 		select | ||||
| 			i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no, | ||||
| 			i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image, | ||||
| 			id.expense_account, id.selling_cost_center, id.default_warehouse, | ||||
| 			i.sales_uom, c.conversion_factor | ||||
| 		from | ||||
| 			`tabItem` i | ||||
| 		left join `tabItem Default` id on id.parent = i.name and id.company = %s | ||||
| 		left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom | ||||
| 		where | ||||
| 			i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 and i.is_fixed_asset = 0 | ||||
| 			{cond} | ||||
| 		""".format(cond=cond), tuple([company] + args_list), as_dict=1) | ||||
| 
 | ||||
| def make_pos_profile(**args): | ||||
| 	frappe.db.sql("delete from `tabPOS Profile`") | ||||
| 
 | ||||
| @ -50,6 +88,12 @@ def make_pos_profile(**args): | ||||
| 		"write_off_account":  args.write_off_account or "_Test Write Off - _TC", | ||||
| 		"write_off_cost_center":  args.write_off_cost_center or "_Test Write Off Cost Center - _TC" | ||||
| 	}) | ||||
| 	 | ||||
| 	payments = [{ | ||||
| 		'mode_of_payment': 'Cash', | ||||
| 		'default': 1 | ||||
| 	}] | ||||
| 	pos_profile.set("payments", payments) | ||||
| 
 | ||||
| 	if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"): | ||||
| 		pos_profile.insert() | ||||
|  | ||||
| @ -26,7 +26,7 @@ | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-01 09:46:47.599173", | ||||
|  "modified": "2020-05-13 23:57:33.627305", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Profile User", | ||||
|  | ||||
| @ -6,27 +6,19 @@ frappe.ui.form.on('POS Settings', { | ||||
| 		frm.trigger("get_invoice_fields"); | ||||
| 	}, | ||||
| 
 | ||||
| 	use_pos_in_offline_mode: function(frm) { | ||||
| 		frm.trigger("get_invoice_fields"); | ||||
| 	}, | ||||
| 
 | ||||
| 	get_invoice_fields: function(frm) { | ||||
| 		if (!frm.doc.use_pos_in_offline_mode) { | ||||
| 			frappe.model.with_doctype("Sales Invoice", () => { | ||||
| 				var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { | ||||
| 					if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || | ||||
| 						d.fieldtype === 'Table') { | ||||
| 						return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; | ||||
| 					} else { | ||||
| 						return null; | ||||
| 					} | ||||
| 				}); | ||||
| 
 | ||||
| 				frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields); | ||||
| 		frappe.model.with_doctype("Sales Invoice", () => { | ||||
| 			var fields = $.map(frappe.get_doc("DocType", "Sales Invoice").fields, function(d) { | ||||
| 				if (frappe.model.no_value_type.indexOf(d.fieldtype) === -1 || | ||||
| 					d.fieldtype === 'Table') { | ||||
| 					return { label: d.label + ' (' + d.fieldtype + ')', value: d.fieldname }; | ||||
| 				} else { | ||||
| 					return null; | ||||
| 				} | ||||
| 			}); | ||||
| 		} else { | ||||
| 			frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""]; | ||||
| 		} | ||||
| 
 | ||||
| 			frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields); | ||||
| 		}); | ||||
| 	} | ||||
| }); | ||||
| 
 | ||||
|  | ||||
| @ -5,24 +5,11 @@ | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "use_pos_in_offline_mode", | ||||
|   "section_break_2", | ||||
|   "fields" | ||||
|   "invoice_fields" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "use_pos_in_offline_mode", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Use POS in Offline Mode" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_2", | ||||
|    "fieldtype": "Section Break" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:!doc.use_pos_in_offline_mode", | ||||
|    "fieldname": "fields", | ||||
|    "fieldname": "invoice_fields", | ||||
|    "fieldtype": "Table", | ||||
|    "label": "POS Field", | ||||
|    "options": "POS Field" | ||||
| @ -30,7 +17,7 @@ | ||||
|  ], | ||||
|  "issingle": 1, | ||||
|  "links": [], | ||||
|  "modified": "2019-12-26 11:50:47.122997", | ||||
|  "modified": "2020-06-01 15:46:41.478928", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "POS Settings", | ||||
|  | ||||
| @ -276,7 +276,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa | ||||
| 
 | ||||
| 		item_details.has_pricing_rule = 1 | ||||
| 
 | ||||
| 		item_details.pricing_rules = ','.join([d.pricing_rule for d in rules]) | ||||
| 		item_details.pricing_rules = frappe.as_json([d.pricing_rule for d in rules]) | ||||
| 
 | ||||
| 		if not doc: return item_details | ||||
| 
 | ||||
| @ -366,7 +366,7 @@ def set_discount_amount(rate, item_details): | ||||
| 
 | ||||
| def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): | ||||
| 	from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items | ||||
| 	for d in pricing_rules.split(','): | ||||
| 	for d in json.loads(pricing_rules): | ||||
| 		if not d or not frappe.db.exists("Pricing Rule", d): continue | ||||
| 		pricing_rule = frappe.get_cached_doc('Pricing Rule', d) | ||||
| 
 | ||||
|  | ||||
| @ -448,7 +448,7 @@ def apply_pricing_rule_on_transaction(doc): | ||||
| 				doc.set_missing_values() | ||||
| 
 | ||||
| def get_applied_pricing_rules(item_row): | ||||
| 	return (item_row.get("pricing_rules").split(',') | ||||
| 	return (json.loads(item_row.get("pricing_rules")) | ||||
| 		if item_row.get("pricing_rules") else []) | ||||
| 
 | ||||
| def get_product_discount_rule(pricing_rule, item_details, args=None, doc=None): | ||||
|  | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @ -1,626 +0,0 @@ | ||||
| # Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors | ||||
| # License: GNU General Public License v3. See license.txt | ||||
| from __future__ import unicode_literals | ||||
| 
 | ||||
| import json | ||||
| 
 | ||||
| import frappe | ||||
| from erpnext.accounts.party import get_party_account_currency | ||||
| from erpnext.controllers.accounts_controller import get_taxes_and_charges | ||||
| from erpnext.setup.utils import get_exchange_rate | ||||
| from erpnext.stock.get_item_details import get_pos_profile | ||||
| from frappe import _ | ||||
| from frappe.core.doctype.communication.email import make | ||||
| from frappe.utils import nowdate, cint | ||||
| 
 | ||||
| from six import string_types, iteritems | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_pos_data(): | ||||
| 	doc = frappe.new_doc('Sales Invoice') | ||||
| 	doc.is_pos = 1 | ||||
| 	pos_profile = get_pos_profile(doc.company) or {} | ||||
| 	if not pos_profile: | ||||
| 		frappe.throw(_("POS Profile is required to use Point-of-Sale")) | ||||
| 
 | ||||
| 	if not doc.company: | ||||
| 		doc.company = pos_profile.get('company') | ||||
| 
 | ||||
| 	doc.update_stock = pos_profile.get('update_stock') | ||||
| 
 | ||||
| 	if pos_profile.get('name'): | ||||
| 		pos_profile = frappe.get_doc('POS Profile', pos_profile.get('name')) | ||||
| 		pos_profile.validate() | ||||
| 
 | ||||
| 	company_data = get_company_data(doc.company) | ||||
| 	update_pos_profile_data(doc, pos_profile, company_data) | ||||
| 	update_multi_mode_option(doc, pos_profile) | ||||
| 	default_print_format = pos_profile.get('print_format') or "Point of Sale" | ||||
| 	print_template = frappe.db.get_value('Print Format', default_print_format, 'html') | ||||
| 	items_list = get_items_list(pos_profile, doc.company) | ||||
| 	customers = get_customers_list(pos_profile) | ||||
| 
 | ||||
| 	doc.plc_conversion_rate = update_plc_conversion_rate(doc, pos_profile) | ||||
| 
 | ||||
| 	return { | ||||
| 		'doc': doc, | ||||
| 		'default_customer': pos_profile.get('customer'), | ||||
| 		'items': items_list, | ||||
| 		'item_groups': get_item_groups(pos_profile), | ||||
| 		'customers': customers, | ||||
| 		'address': get_customers_address(customers), | ||||
| 		'contacts': get_contacts(customers), | ||||
| 		'serial_no_data': get_serial_no_data(pos_profile, doc.company), | ||||
| 		'batch_no_data': get_batch_no_data(), | ||||
| 		'barcode_data': get_barcode_data(items_list), | ||||
| 		'tax_data': get_item_tax_data(), | ||||
| 		'price_list_data': get_price_list_data(doc.selling_price_list, doc.plc_conversion_rate), | ||||
| 		'customer_wise_price_list': get_customer_wise_price_list(), | ||||
| 		'bin_data': get_bin_data(pos_profile), | ||||
| 		'pricing_rules': get_pricing_rule_data(doc), | ||||
| 		'print_template': print_template, | ||||
| 		'pos_profile': pos_profile, | ||||
| 		'meta': get_meta() | ||||
| 	} | ||||
| 
 | ||||
| def update_plc_conversion_rate(doc, pos_profile): | ||||
| 	conversion_rate = 1.0 | ||||
| 
 | ||||
| 	price_list_currency = frappe.get_cached_value("Price List", doc.selling_price_list, "currency") | ||||
| 	if pos_profile.get("currency") != price_list_currency: | ||||
| 		conversion_rate = get_exchange_rate(price_list_currency, | ||||
| 			pos_profile.get("currency"), nowdate(), args="for_selling") or 1.0 | ||||
| 
 | ||||
| 	return conversion_rate | ||||
| 
 | ||||
| def get_meta(): | ||||
| 	doctype_meta = { | ||||
| 		'customer': frappe.get_meta('Customer'), | ||||
| 		'invoice': frappe.get_meta('Sales Invoice') | ||||
| 	} | ||||
| 
 | ||||
| 	for row in frappe.get_all('DocField', fields=['fieldname', 'options'], | ||||
|             filters={'parent': 'Sales Invoice', 'fieldtype': 'Table'}): | ||||
| 		doctype_meta[row.fieldname] = frappe.get_meta(row.options) | ||||
| 
 | ||||
| 	return doctype_meta | ||||
| 
 | ||||
| 
 | ||||
| def get_company_data(company): | ||||
| 	return frappe.get_all('Company', fields=["*"], filters={'name': company})[0] | ||||
| 
 | ||||
| 
 | ||||
| def update_pos_profile_data(doc, pos_profile, company_data): | ||||
| 	doc.campaign = pos_profile.get('campaign') | ||||
| 	if pos_profile and not pos_profile.get('country'): | ||||
| 		pos_profile.country = company_data.country | ||||
| 
 | ||||
| 	doc.write_off_account = pos_profile.get('write_off_account') or \ | ||||
| 		company_data.write_off_account | ||||
| 	doc.change_amount_account = pos_profile.get('change_amount_account') or \ | ||||
| 		company_data.default_cash_account | ||||
| 	doc.taxes_and_charges = pos_profile.get('taxes_and_charges') | ||||
| 	if doc.taxes_and_charges: | ||||
| 		update_tax_table(doc) | ||||
| 
 | ||||
| 	doc.currency = pos_profile.get('currency') or company_data.default_currency | ||||
| 	doc.conversion_rate = 1.0 | ||||
| 
 | ||||
| 	if doc.currency != company_data.default_currency: | ||||
| 		doc.conversion_rate = get_exchange_rate(doc.currency, company_data.default_currency, doc.posting_date, args="for_selling") | ||||
| 
 | ||||
| 	doc.selling_price_list = pos_profile.get('selling_price_list') or \ | ||||
| 		frappe.db.get_value('Selling Settings', None, 'selling_price_list') | ||||
| 	doc.naming_series = pos_profile.get('naming_series') or 'SINV-' | ||||
| 	doc.letter_head = pos_profile.get('letter_head') or company_data.default_letter_head | ||||
| 	doc.ignore_pricing_rule = pos_profile.get('ignore_pricing_rule') or 0 | ||||
| 	doc.apply_discount_on = pos_profile.get('apply_discount_on') or 'Grand Total' | ||||
| 	doc.customer_group = pos_profile.get('customer_group') or get_root('Customer Group') | ||||
| 	doc.territory = pos_profile.get('territory') or get_root('Territory') | ||||
| 	doc.terms = frappe.db.get_value('Terms and Conditions', pos_profile.get('tc_name'), 'terms') or doc.terms or '' | ||||
| 	doc.offline_pos_name = '' | ||||
| 
 | ||||
| 
 | ||||
| def get_root(table): | ||||
| 	root = frappe.db.sql(""" select name from `tab%(table)s` having | ||||
| 		min(lft)""" % {'table': table}, as_dict=1) | ||||
| 
 | ||||
| 	return root[0].name | ||||
| 
 | ||||
| 
 | ||||
| def update_multi_mode_option(doc, pos_profile): | ||||
| 	from frappe.model import default_fields | ||||
| 
 | ||||
| 	if not pos_profile or not pos_profile.get('payments'): | ||||
| 		for payment in get_mode_of_payment(doc): | ||||
| 			payments = doc.append('payments', {}) | ||||
| 			payments.mode_of_payment = payment.parent | ||||
| 			payments.account = payment.default_account | ||||
| 			payments.type = payment.type | ||||
| 
 | ||||
| 		return | ||||
| 
 | ||||
| 	for payment_mode in pos_profile.payments: | ||||
| 		payment_mode = payment_mode.as_dict() | ||||
| 
 | ||||
| 		for fieldname in default_fields: | ||||
| 			if fieldname in payment_mode: | ||||
| 				del payment_mode[fieldname] | ||||
| 
 | ||||
| 		doc.append('payments', payment_mode) | ||||
| 
 | ||||
| 
 | ||||
| def get_mode_of_payment(doc): | ||||
| 	return frappe.db.sql(""" | ||||
| 		select mpa.default_account, mpa.parent, mp.type as type  | ||||
| 		from `tabMode of Payment Account` mpa,`tabMode of Payment` mp  | ||||
| 		where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""", | ||||
| 	{'company': doc.company}, as_dict=1) | ||||
| 
 | ||||
| 
 | ||||
| def update_tax_table(doc): | ||||
| 	taxes = get_taxes_and_charges('Sales Taxes and Charges Template', doc.taxes_and_charges) | ||||
| 	for tax in taxes: | ||||
| 		doc.append('taxes', tax) | ||||
| 
 | ||||
| 
 | ||||
| def get_items_list(pos_profile, company): | ||||
| 	cond = "" | ||||
| 	args_list = [] | ||||
| 	if pos_profile.get('item_groups'): | ||||
| 		# Get items based on the item groups defined in the POS profile | ||||
| 		for d in pos_profile.get('item_groups'): | ||||
| 			args_list.extend([d.name for d in get_child_nodes('Item Group', d.item_group)]) | ||||
| 		if args_list: | ||||
| 			cond = "and i.item_group in (%s)" % (', '.join(['%s'] * len(args_list))) | ||||
| 
 | ||||
| 	return frappe.db.sql(""" | ||||
| 		select | ||||
| 			i.name, i.item_code, i.item_name, i.description, i.item_group, i.has_batch_no, | ||||
| 			i.has_serial_no, i.is_stock_item, i.brand, i.stock_uom, i.image, | ||||
| 			id.expense_account, id.selling_cost_center, id.default_warehouse, | ||||
| 			i.sales_uom, c.conversion_factor | ||||
| 		from | ||||
| 			`tabItem` i | ||||
| 		left join `tabItem Default` id on id.parent = i.name and id.company = %s | ||||
| 		left join `tabUOM Conversion Detail` c on i.name = c.parent and i.sales_uom = c.uom | ||||
| 		where | ||||
| 			i.disabled = 0 and i.has_variants = 0 and i.is_sales_item = 1 | ||||
| 			{cond} | ||||
| 		""".format(cond=cond), tuple([company] + args_list), as_dict=1) | ||||
| 
 | ||||
| 
 | ||||
| def get_item_groups(pos_profile): | ||||
| 	item_group_dict = {} | ||||
| 	item_groups = frappe.db.sql("""Select name, | ||||
| 		lft, rgt from `tabItem Group` order by lft""", as_dict=1) | ||||
| 
 | ||||
| 	for data in item_groups: | ||||
| 		item_group_dict[data.name] = [data.lft, data.rgt] | ||||
| 	return item_group_dict | ||||
| 
 | ||||
| 
 | ||||
| def get_customers_list(pos_profile={}): | ||||
| 	cond = "1=1" | ||||
| 	customer_groups = [] | ||||
| 	if pos_profile.get('customer_groups'): | ||||
| 		# Get customers based on the customer groups defined in the POS profile | ||||
| 		for d in pos_profile.get('customer_groups'): | ||||
| 			customer_groups.extend([d.get('name') for d in get_child_nodes('Customer Group', d.get('customer_group'))]) | ||||
| 		cond = "customer_group in (%s)" % (', '.join(['%s'] * len(customer_groups))) | ||||
| 
 | ||||
| 	return frappe.db.sql(""" select name, customer_name, customer_group, | ||||
| 		territory, customer_pos_id from tabCustomer where disabled = 0 | ||||
| 		and {cond}""".format(cond=cond), tuple(customer_groups), as_dict=1) or {} | ||||
| 
 | ||||
| 
 | ||||
| def get_customers_address(customers): | ||||
| 	customer_address = {} | ||||
| 	if isinstance(customers, string_types): | ||||
| 		customers = [frappe._dict({'name': customers})] | ||||
| 
 | ||||
| 	for data in customers: | ||||
| 		address = frappe.db.sql(""" select name, address_line1, address_line2, city, state, | ||||
| 			email_id, phone, fax, pincode from `tabAddress` where is_primary_address =1 and name in | ||||
| 			(select parent from `tabDynamic Link` where link_doctype = 'Customer' and link_name = %s | ||||
| 			and parenttype = 'Address')""", data.name, as_dict=1) | ||||
| 		address_data = {} | ||||
| 		if address: | ||||
| 			address_data = address[0] | ||||
| 
 | ||||
| 		address_data.update({'full_name': data.customer_name, 'customer_pos_id': data.customer_pos_id}) | ||||
| 		customer_address[data.name] = address_data | ||||
| 
 | ||||
| 	return customer_address | ||||
| 
 | ||||
| 
 | ||||
| def get_contacts(customers): | ||||
| 	customer_contact = {} | ||||
| 	if isinstance(customers, string_types): | ||||
| 		customers = [frappe._dict({'name': customers})] | ||||
| 
 | ||||
| 	for data in customers: | ||||
| 		contact = frappe.db.sql(""" select email_id, phone, mobile_no from `tabContact` | ||||
| 			where is_primary_contact=1 and name in | ||||
| 			(select parent from `tabDynamic Link` where link_doctype = 'Customer' and link_name = %s | ||||
| 			and parenttype = 'Contact')""", data.name, as_dict=1) | ||||
| 		if contact: | ||||
| 			customer_contact[data.name] = contact[0] | ||||
| 
 | ||||
| 	return customer_contact | ||||
| 
 | ||||
| 
 | ||||
| def get_child_nodes(group_type, root): | ||||
| 	lft, rgt = frappe.db.get_value(group_type, root, ["lft", "rgt"]) | ||||
| 	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) | ||||
| 
 | ||||
| 
 | ||||
| def get_serial_no_data(pos_profile, company): | ||||
| 	# get itemwise serial no data | ||||
| 	# example {'Nokia Lumia 1020': {'SN0001': 'Pune'}} | ||||
| 	# where Nokia Lumia 1020 is item code, SN0001 is serial no and Pune is warehouse | ||||
| 
 | ||||
| 	cond = "1=1" | ||||
| 	if pos_profile.get('update_stock') and pos_profile.get('warehouse'): | ||||
| 		cond = "warehouse = %(warehouse)s" | ||||
| 
 | ||||
| 	serial_nos = frappe.db.sql("""select name, warehouse, item_code | ||||
| 		from `tabSerial No` where {0} and company = %(company)s """.format(cond),{ | ||||
| 			'company': company, 'warehouse': frappe.db.escape(pos_profile.get('warehouse')) | ||||
| 		}, as_dict=1) | ||||
| 
 | ||||
| 	itemwise_serial_no = {} | ||||
| 	for sn in serial_nos: | ||||
| 		if sn.item_code not in itemwise_serial_no: | ||||
| 			itemwise_serial_no.setdefault(sn.item_code, {}) | ||||
| 		itemwise_serial_no[sn.item_code][sn.name] = sn.warehouse | ||||
| 
 | ||||
| 	return itemwise_serial_no | ||||
| 
 | ||||
| 
 | ||||
| def get_batch_no_data(): | ||||
| 	# get itemwise batch no data | ||||
| 	# exmaple: {'LED-GRE': [Batch001, Batch002]} | ||||
| 	# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse | ||||
| 
 | ||||
| 	itemwise_batch = {} | ||||
| 	batches = frappe.db.sql("""select name, item from `tabBatch` | ||||
| 		where ifnull(expiry_date, '4000-10-10') >= curdate()""", as_dict=1) | ||||
| 
 | ||||
| 	for batch in batches: | ||||
| 		if batch.item not in itemwise_batch: | ||||
| 			itemwise_batch.setdefault(batch.item, []) | ||||
| 		itemwise_batch[batch.item].append(batch.name) | ||||
| 
 | ||||
| 	return itemwise_batch | ||||
| 
 | ||||
| 
 | ||||
| def get_barcode_data(items_list): | ||||
| 	# get itemwise batch no data | ||||
| 	# exmaple: {'LED-GRE': [Batch001, Batch002]} | ||||
| 	# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse | ||||
| 
 | ||||
| 	itemwise_barcode = {} | ||||
| 	for item in items_list: | ||||
| 		barcodes = frappe.db.sql(""" | ||||
| 			select barcode from `tabItem Barcode` where parent = %s | ||||
| 		""", item.item_code, as_dict=1) | ||||
| 
 | ||||
| 		for barcode in barcodes: | ||||
| 			if item.item_code not in itemwise_barcode: | ||||
| 				itemwise_barcode.setdefault(item.item_code, []) | ||||
| 			itemwise_barcode[item.item_code].append(barcode.get("barcode")) | ||||
| 
 | ||||
| 	return itemwise_barcode | ||||
| 
 | ||||
| 
 | ||||
| def get_item_tax_data(): | ||||
| 	# get default tax of an item | ||||
| 	# example: {'Consulting Services': {'Excise 12 - TS': '12.000'}} | ||||
| 
 | ||||
| 	itemwise_tax = {} | ||||
| 	taxes = frappe.db.sql(""" select parent, tax_type, tax_rate from `tabItem Tax Template Detail`""", as_dict=1) | ||||
| 
 | ||||
| 	for tax in taxes: | ||||
| 		if tax.parent not in itemwise_tax: | ||||
| 			itemwise_tax.setdefault(tax.parent, {}) | ||||
| 		itemwise_tax[tax.parent][tax.tax_type] = tax.tax_rate | ||||
| 
 | ||||
| 	return itemwise_tax | ||||
| 
 | ||||
| 
 | ||||
| def get_price_list_data(selling_price_list, conversion_rate): | ||||
| 	itemwise_price_list = {} | ||||
| 	price_lists = frappe.db.sql("""Select ifnull(price_list_rate, 0) as price_list_rate, | ||||
| 		item_code from `tabItem Price` ip where price_list = %(price_list)s""", | ||||
|         {'price_list': selling_price_list}, as_dict=1) | ||||
| 
 | ||||
| 	for item in price_lists: | ||||
| 		itemwise_price_list[item.item_code] = item.price_list_rate * conversion_rate | ||||
| 
 | ||||
| 	return itemwise_price_list | ||||
| 
 | ||||
| def get_customer_wise_price_list(): | ||||
| 	customer_wise_price = {} | ||||
| 	customer_price_list_mapping = frappe._dict(frappe.get_all('Customer',fields = ['default_price_list', 'name'], as_list=1)) | ||||
| 
 | ||||
| 	price_lists = frappe.db.sql(""" Select ifnull(price_list_rate, 0) as price_list_rate, | ||||
| 		item_code, price_list from `tabItem Price` """, as_dict=1) | ||||
| 
 | ||||
| 	for item in price_lists: | ||||
| 		if item.price_list and customer_price_list_mapping.get(item.price_list): | ||||
| 
 | ||||
| 			customer_wise_price.setdefault(customer_price_list_mapping.get(item.price_list),{}).setdefault( | ||||
| 				item.item_code, item.price_list_rate | ||||
| 			) | ||||
| 
 | ||||
| 	return customer_wise_price | ||||
| 
 | ||||
| def get_bin_data(pos_profile): | ||||
| 	itemwise_bin_data = {} | ||||
| 	filters = { 'actual_qty': ['>', 0] } | ||||
| 	if pos_profile.get('warehouse'): | ||||
| 		filters.update({ 'warehouse': pos_profile.get('warehouse') }) | ||||
| 
 | ||||
| 	bin_data = frappe.db.get_all('Bin', fields = ['item_code', 'warehouse', 'actual_qty'], filters=filters) | ||||
| 
 | ||||
| 	for bins in bin_data: | ||||
| 		if bins.item_code not in itemwise_bin_data: | ||||
| 			itemwise_bin_data.setdefault(bins.item_code, {}) | ||||
| 		itemwise_bin_data[bins.item_code][bins.warehouse] = bins.actual_qty | ||||
| 
 | ||||
| 	return itemwise_bin_data | ||||
| 
 | ||||
| 
 | ||||
| def get_pricing_rule_data(doc): | ||||
| 	pricing_rules = "" | ||||
| 	if doc.ignore_pricing_rule == 0: | ||||
| 		pricing_rules = frappe.db.sql(""" Select * from `tabPricing Rule` where docstatus < 2 | ||||
| 						and ifnull(for_price_list, '') in (%(price_list)s, '') and selling = 1 | ||||
| 						and ifnull(company, '') in (%(company)s, '') and disable = 0 and %(date)s | ||||
| 						between ifnull(valid_from, '2000-01-01') and ifnull(valid_upto, '2500-12-31') | ||||
| 						order by priority desc, name desc""", | ||||
|                         {'company': doc.company, 'price_list': doc.selling_price_list, 'date': nowdate()}, as_dict=1) | ||||
| 	return pricing_rules | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def make_invoice(pos_profile, doc_list={}, email_queue_list={}, customers_list={}): | ||||
| 	import json | ||||
| 
 | ||||
| 	if isinstance(doc_list, string_types): | ||||
| 		doc_list = json.loads(doc_list) | ||||
| 
 | ||||
| 	if isinstance(email_queue_list, string_types): | ||||
| 		email_queue_list = json.loads(email_queue_list) | ||||
| 
 | ||||
| 	if isinstance(customers_list, string_types): | ||||
| 		customers_list = json.loads(customers_list) | ||||
| 
 | ||||
| 	customers_list = make_customer_and_address(customers_list) | ||||
| 	name_list = [] | ||||
| 	for docs in doc_list: | ||||
| 		for name, doc in iteritems(docs): | ||||
| 			if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}): | ||||
| 				if isinstance(doc, dict): | ||||
| 					validate_records(doc) | ||||
| 					si_doc = frappe.new_doc('Sales Invoice') | ||||
| 					si_doc.offline_pos_name = name | ||||
| 					si_doc.update(doc) | ||||
| 					si_doc.set_posting_time = 1 | ||||
| 					si_doc.customer = get_customer_id(doc) | ||||
| 					si_doc.due_date = doc.get('posting_date') | ||||
| 					name_list = submit_invoice(si_doc, name, doc, name_list) | ||||
| 				else: | ||||
| 					doc.due_date = doc.get('posting_date') | ||||
| 					doc.customer = get_customer_id(doc) | ||||
| 					doc.set_posting_time = 1 | ||||
| 					doc.offline_pos_name = name | ||||
| 					name_list = submit_invoice(doc, name, doc, name_list) | ||||
| 			else: | ||||
| 				name_list.append(name) | ||||
| 
 | ||||
| 	email_queue = make_email_queue(email_queue_list) | ||||
| 
 | ||||
| 	if isinstance(pos_profile, string_types): | ||||
| 		pos_profile = json.loads(pos_profile) | ||||
| 
 | ||||
| 	customers = get_customers_list(pos_profile) | ||||
| 	return { | ||||
| 		'invoice': name_list, | ||||
| 		'email_queue': email_queue, | ||||
| 		'customers': customers_list, | ||||
| 		'synced_customers_list': customers, | ||||
| 		'synced_address': get_customers_address(customers), | ||||
| 		'synced_contacts': get_contacts(customers) | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| def validate_records(doc): | ||||
| 	validate_item(doc) | ||||
| 
 | ||||
| 
 | ||||
| def get_customer_id(doc, customer=None): | ||||
| 	cust_id = None | ||||
| 	if doc.get('customer_pos_id'): | ||||
| 		cust_id = frappe.db.get_value('Customer',{'customer_pos_id': doc.get('customer_pos_id')}, 'name') | ||||
| 
 | ||||
| 	if not cust_id: | ||||
| 		customer = customer or doc.get('customer') | ||||
| 		if frappe.db.exists('Customer', customer): | ||||
| 			cust_id = customer | ||||
| 		else: | ||||
| 			cust_id = add_customer(doc) | ||||
| 
 | ||||
| 	return cust_id | ||||
| 
 | ||||
| def make_customer_and_address(customers): | ||||
| 	customers_list = [] | ||||
| 	for customer, data in iteritems(customers): | ||||
| 		data = json.loads(data) | ||||
| 		cust_id = get_customer_id(data, customer) | ||||
| 		if not cust_id: | ||||
| 			cust_id = add_customer(data) | ||||
| 		else: | ||||
| 			frappe.db.set_value("Customer", cust_id, "customer_name", data.get('full_name')) | ||||
| 
 | ||||
| 		make_contact(data, cust_id) | ||||
| 		make_address(data, cust_id) | ||||
| 		customers_list.append(customer) | ||||
| 	frappe.db.commit() | ||||
| 	return customers_list | ||||
| 
 | ||||
| def add_customer(data): | ||||
| 	customer = data.get('full_name') or data.get('customer') | ||||
| 	if frappe.db.exists("Customer", customer.strip()): | ||||
| 		return customer.strip() | ||||
| 
 | ||||
| 	customer_doc = frappe.new_doc('Customer') | ||||
| 	customer_doc.customer_name = data.get('full_name') or data.get('customer') | ||||
| 	customer_doc.customer_pos_id = data.get('customer_pos_id') | ||||
| 	customer_doc.customer_type = 'Company' | ||||
| 	customer_doc.customer_group = get_customer_group(data) | ||||
| 	customer_doc.territory = get_territory(data) | ||||
| 	customer_doc.flags.ignore_mandatory = True | ||||
| 	customer_doc.save(ignore_permissions=True) | ||||
| 	frappe.db.commit() | ||||
| 	return customer_doc.name | ||||
| 
 | ||||
| def get_territory(data): | ||||
| 	if data.get('territory'): | ||||
| 		return data.get('territory') | ||||
| 
 | ||||
| 	return frappe.db.get_single_value('Selling Settings','territory') or _('All Territories') | ||||
| 
 | ||||
| def get_customer_group(data): | ||||
| 	if data.get('customer_group'): | ||||
| 		return data.get('customer_group') | ||||
| 
 | ||||
| 	return frappe.db.get_single_value('Selling Settings', 'customer_group') or frappe.db.get_value('Customer Group', {'is_group': 0}, 'name') | ||||
| 
 | ||||
| def make_contact(args, customer): | ||||
| 	if args.get('email_id') or args.get('phone'): | ||||
| 		name = frappe.db.get_value('Dynamic Link', | ||||
|             	{'link_doctype': 'Customer', 'link_name': customer, 'parenttype': 'Contact'}, 'parent') | ||||
| 
 | ||||
| 		args = { | ||||
| 			'first_name': args.get('full_name'), | ||||
| 			'email_id': args.get('email_id'), | ||||
| 			'phone': args.get('phone') | ||||
| 		} | ||||
| 
 | ||||
| 		doc = frappe.new_doc('Contact') | ||||
| 		if name: | ||||
| 			doc = frappe.get_doc('Contact', name) | ||||
| 
 | ||||
| 		doc.update(args) | ||||
| 		doc.is_primary_contact = 1 | ||||
| 		if not name: | ||||
| 			doc.append('links', { | ||||
| 				'link_doctype': 'Customer', | ||||
| 				'link_name': customer | ||||
| 			}) | ||||
| 		doc.flags.ignore_mandatory = True | ||||
| 		doc.save(ignore_permissions=True) | ||||
| 
 | ||||
| def make_address(args, customer): | ||||
| 	if not args.get('address_line1'): | ||||
| 		return | ||||
| 
 | ||||
| 	name = args.get('name') | ||||
| 
 | ||||
| 	if not name: | ||||
| 		data = get_customers_address(customer) | ||||
| 		name = data[customer].get('name') if data else None | ||||
| 
 | ||||
| 	if name: | ||||
| 		address = frappe.get_doc('Address', name) | ||||
| 	else: | ||||
| 		address = frappe.new_doc('Address') | ||||
| 		if args.get('company'): | ||||
| 			address.country = frappe.get_cached_value('Company', | ||||
| 				args.get('company'),  'country') | ||||
| 
 | ||||
| 		address.append('links', { | ||||
| 			'link_doctype': 'Customer', | ||||
| 			'link_name': customer | ||||
| 		}) | ||||
| 
 | ||||
| 	address.is_primary_address = 1 | ||||
| 	address.is_shipping_address = 1 | ||||
| 	address.update(args) | ||||
| 	address.flags.ignore_mandatory = True | ||||
| 	address.save(ignore_permissions=True) | ||||
| 
 | ||||
| def make_email_queue(email_queue): | ||||
| 	name_list = [] | ||||
| 
 | ||||
| 	for key, data in iteritems(email_queue): | ||||
| 		name = frappe.db.get_value('Sales Invoice', {'offline_pos_name': key}, 'name') | ||||
| 		if not name: continue | ||||
| 
 | ||||
| 		data = json.loads(data) | ||||
| 		sender = frappe.session.user | ||||
| 		print_format = "POS Invoice" if not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')) else None | ||||
| 
 | ||||
| 		attachments = [frappe.attach_print('Sales Invoice', name, print_format=print_format)] | ||||
| 
 | ||||
| 		make(subject=data.get('subject'), content=data.get('content'), recipients=data.get('recipients'), | ||||
|                     sender=sender, attachments=attachments, send_email=True, | ||||
|                     doctype='Sales Invoice', name=name) | ||||
| 		name_list.append(key) | ||||
| 
 | ||||
| 	return name_list | ||||
| 
 | ||||
| def validate_item(doc): | ||||
| 	for item in doc.get('items'): | ||||
| 		if not frappe.db.exists('Item', item.get('item_code')): | ||||
| 			item_doc = frappe.new_doc('Item') | ||||
| 			item_doc.name = item.get('item_code') | ||||
| 			item_doc.item_code = item.get('item_code') | ||||
| 			item_doc.item_name = item.get('item_name') | ||||
| 			item_doc.description = item.get('description') | ||||
| 			item_doc.stock_uom = item.get('stock_uom') | ||||
| 			item_doc.uom = item.get('uom') | ||||
| 			item_doc.item_group = item.get('item_group') | ||||
| 			item_doc.append('item_defaults', { | ||||
| 				"company": doc.get("company"), | ||||
| 				"default_warehouse": item.get('warehouse') | ||||
| 			}) | ||||
| 			item_doc.save(ignore_permissions=True) | ||||
| 			frappe.db.commit() | ||||
| 
 | ||||
| def submit_invoice(si_doc, name, doc, name_list): | ||||
| 	try: | ||||
| 		si_doc.insert() | ||||
| 		si_doc.submit() | ||||
| 		frappe.db.commit() | ||||
| 		name_list.append(name) | ||||
| 	except Exception as e: | ||||
| 		if frappe.message_log: | ||||
| 			frappe.message_log.pop() | ||||
| 		frappe.db.rollback() | ||||
| 		frappe.log_error(frappe.get_traceback()) | ||||
| 		name_list = save_invoice(doc, name, name_list) | ||||
| 
 | ||||
| 	return name_list | ||||
| 
 | ||||
| def save_invoice(doc, name, name_list): | ||||
| 	try: | ||||
| 		if not frappe.db.exists('Sales Invoice', {'offline_pos_name': name}): | ||||
| 			si = frappe.new_doc('Sales Invoice') | ||||
| 			si.update(doc) | ||||
| 			si.set_posting_time = 1 | ||||
| 			si.customer = get_customer_id(doc) | ||||
| 			si.due_date = doc.get('posting_date') | ||||
| 			si.flags.ignore_mandatory = True | ||||
| 			si.insert(ignore_permissions=True) | ||||
| 			frappe.db.commit() | ||||
| 			name_list.append(name) | ||||
| 	except Exception: | ||||
| 		frappe.db.rollback() | ||||
| 		frappe.log_error(frappe.get_traceback()) | ||||
| 
 | ||||
| 	return name_list | ||||
| @ -96,6 +96,12 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte | ||||
| 				cur_frm.add_custom_button(__('Invoice Discounting'), function() { | ||||
| 					cur_frm.events.create_invoice_discounting(cur_frm); | ||||
| 				}, __('Create')); | ||||
| 
 | ||||
| 				if (doc.due_date < frappe.datetime.get_today()) { | ||||
| 					cur_frm.add_custom_button(__('Dunning'), function() { | ||||
| 						cur_frm.events.create_dunning(cur_frm); | ||||
| 					}, __('Create')); | ||||
| 				} | ||||
| 			} | ||||
| 
 | ||||
| 			if (doc.docstatus === 1) { | ||||
| @ -276,7 +282,7 @@ erpnext.accounts.SalesInvoiceController = erpnext.selling.SellingController.exte | ||||
| 					"customer": this.frm.doc.customer | ||||
| 				}, | ||||
| 				callback: function(r) { | ||||
| 					if(r.message && r.message.length) { | ||||
| 					if(r.message && r.message.length > 1) { | ||||
| 						select_loyalty_program(me.frm, r.message); | ||||
| 					} | ||||
| 				} | ||||
| @ -824,6 +830,12 @@ frappe.ui.form.on('Sales Invoice', { | ||||
| 			method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_invoice_discounting", | ||||
| 			frm: frm | ||||
| 		}); | ||||
| 	}, | ||||
| 	create_dunning: function(frm) { | ||||
| 		frappe.model.open_mapped_doc({ | ||||
| 			method: "erpnext.accounts.doctype.sales_invoice.sales_invoice.create_dunning", | ||||
| 			frm: frm | ||||
| 		}); | ||||
| 	} | ||||
| }) | ||||
| 
 | ||||
|  | ||||
| @ -1,6 +1,7 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "allow_import": 1, | ||||
|  "allow_workflow": 1, | ||||
|  "autoname": "naming_series:", | ||||
|  "creation": "2013-05-24 19:29:05", | ||||
|  "doctype": "DocType", | ||||
| @ -13,6 +14,7 @@ | ||||
|   "customer_name", | ||||
|   "tax_id", | ||||
|   "is_pos", | ||||
|   "is_consolidated", | ||||
|   "pos_profile", | ||||
|   "offline_pos_name", | ||||
|   "is_return", | ||||
| @ -1158,6 +1160,7 @@ | ||||
|    "hide_days": 1, | ||||
|    "hide_seconds": 1, | ||||
|    "label": "In Words (Company Currency)", | ||||
|    "length": 240, | ||||
|    "oldfieldname": "in_words", | ||||
|    "oldfieldtype": "Data", | ||||
|    "print_hide": 1, | ||||
| @ -1215,6 +1218,7 @@ | ||||
|    "hide_days": 1, | ||||
|    "hide_seconds": 1, | ||||
|    "label": "In Words", | ||||
|    "length": 240, | ||||
|    "oldfieldname": "in_words_export", | ||||
|    "oldfieldtype": "Data", | ||||
|    "print_hide": 1, | ||||
| @ -1921,6 +1925,13 @@ | ||||
|    "hide_days": 1, | ||||
|    "hide_seconds": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "is_consolidated", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Is Consolidated", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fetch_from": "customer.is_internal_customer", | ||||
| @ -1936,7 +1947,7 @@ | ||||
|  "idx": 181, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-06-30 12:00:03.890180", | ||||
|  "modified": "2020-07-18 05:07:16.725974", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Sales Invoice", | ||||
|  | ||||
| @ -8,8 +8,6 @@ from frappe.utils import cint, flt, add_months, today, date_diff, getdate, add_d | ||||
| from frappe import _, msgprint, throw | ||||
| from erpnext.accounts.party import get_party_account, get_due_date | ||||
| from frappe.model.mapper import get_mapped_doc | ||||
| from erpnext.accounts.doctype.sales_invoice.pos import update_multi_mode_option | ||||
| 
 | ||||
| from erpnext.controllers.selling_controller import SellingController | ||||
| from erpnext.accounts.utils import get_account_currency | ||||
| from erpnext.stock.doctype.delivery_note.delivery_note import update_billed_amount_based_on_so | ||||
| @ -133,7 +131,7 @@ class SalesInvoice(SellingController): | ||||
| 		if self.is_pos and self.is_return: | ||||
| 			self.verify_payment_amount_is_negative() | ||||
| 
 | ||||
| 		if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points: | ||||
| 		if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points and not self.is_consolidated: | ||||
| 			validate_loyalty_points(self, self.loyalty_points) | ||||
| 
 | ||||
| 	def validate_fixed_asset(self): | ||||
| @ -200,13 +198,13 @@ class SalesInvoice(SellingController): | ||||
| 		update_linked_doc(self.doctype, self.name, self.inter_company_invoice_reference) | ||||
| 
 | ||||
| 		# create the loyalty point ledger entry if the customer is enrolled in any loyalty program | ||||
| 		if not self.is_return and self.loyalty_program: | ||||
| 		if not self.is_return and not self.is_consolidated and self.loyalty_program: | ||||
| 			self.make_loyalty_point_entry() | ||||
| 		elif self.is_return and self.return_against and self.loyalty_program: | ||||
| 		elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program: | ||||
| 			against_si_doc = frappe.get_doc("Sales Invoice", self.return_against) | ||||
| 			against_si_doc.delete_loyalty_point_entry() | ||||
| 			against_si_doc.make_loyalty_point_entry() | ||||
| 		if self.redeem_loyalty_points and self.loyalty_points: | ||||
| 		if self.redeem_loyalty_points and not self.is_consolidated and self.loyalty_points: | ||||
| 			self.apply_loyalty_points() | ||||
| 
 | ||||
| 		# Healthcare Service Invoice. | ||||
| @ -265,9 +263,9 @@ class SalesInvoice(SellingController): | ||||
| 		if frappe.db.get_single_value('Selling Settings', 'sales_update_frequency') == "Each Transaction": | ||||
| 			update_company_current_month_sales(self.company) | ||||
| 			self.update_project() | ||||
| 		if not self.is_return and self.loyalty_program: | ||||
| 		if not self.is_return and not self.is_consolidated and self.loyalty_program: | ||||
| 			self.delete_loyalty_point_entry() | ||||
| 		elif self.is_return and self.return_against and self.loyalty_program: | ||||
| 		elif self.is_return and self.return_against and not self.is_consolidated and self.loyalty_program: | ||||
| 			against_si_doc = frappe.get_doc("Sales Invoice", self.return_against) | ||||
| 			against_si_doc.delete_loyalty_point_entry() | ||||
| 			against_si_doc.make_loyalty_point_entry() | ||||
| @ -347,7 +345,7 @@ class SalesInvoice(SellingController): | ||||
| 
 | ||||
| 		super(SalesInvoice, self).set_missing_values(for_validate) | ||||
| 
 | ||||
| 		print_format = pos.get("print_format_for_online") if pos else None | ||||
| 		print_format = pos.get("print_format") if pos else None | ||||
| 		if not print_format and not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')): | ||||
| 			print_format = 'POS Invoice' | ||||
| 
 | ||||
| @ -420,8 +418,6 @@ class SalesInvoice(SellingController): | ||||
| 			self.account_for_change_amount = frappe.get_cached_value('Company',  self.company,  'default_cash_account') | ||||
| 
 | ||||
| 		if pos: | ||||
| 			self.allow_print_before_pay = pos.allow_print_before_pay | ||||
| 
 | ||||
| 			if not for_validate: | ||||
| 				self.tax_category = pos.get("tax_category") | ||||
| 
 | ||||
| @ -432,8 +428,8 @@ class SalesInvoice(SellingController): | ||||
| 			if pos.get('account_for_change_amount'): | ||||
| 				self.account_for_change_amount = pos.get('account_for_change_amount') | ||||
| 
 | ||||
| 			for fieldname in ('territory', 'naming_series', 'currency', 'letter_head', 'tc_name', | ||||
| 				'company', 'select_print_heading', 'cash_bank_account', 'write_off_account', 'taxes_and_charges', | ||||
| 			for fieldname in ('naming_series', '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)): | ||||
| 						self.set(fieldname, pos.get(fieldname)) | ||||
| @ -1123,7 +1119,8 @@ class SalesInvoice(SellingController): | ||||
| 				"loyalty_program": lp_details.loyalty_program, | ||||
| 				"loyalty_program_tier": lp_details.tier_name, | ||||
| 				"customer": self.customer, | ||||
| 				"sales_invoice": self.name, | ||||
| 				"invoice_type": self.doctype, | ||||
| 				"invoice": self.name, | ||||
| 				"loyalty_points": points_earned, | ||||
| 				"purchase_amount": eligible_amount, | ||||
| 				"expiry_date": add_days(self.posting_date, lp_details.expiry_duration), | ||||
| @ -1135,18 +1132,18 @@ class SalesInvoice(SellingController): | ||||
| 
 | ||||
| 	# valdite the redemption and then delete the loyalty points earned on cancel of the invoice | ||||
| 	def delete_loyalty_point_entry(self): | ||||
| 		lp_entry = frappe.db.sql("select name from `tabLoyalty Point Entry` where sales_invoice=%s", | ||||
| 		lp_entry = frappe.db.sql("select name from `tabLoyalty Point Entry` where invoice=%s", | ||||
| 			(self.name), as_dict=1) | ||||
| 
 | ||||
| 		if not lp_entry: return | ||||
| 		against_lp_entry = frappe.db.sql('''select name, sales_invoice from `tabLoyalty Point Entry` | ||||
| 		against_lp_entry = frappe.db.sql('''select name, invoice from `tabLoyalty Point Entry` | ||||
| 			where redeem_against=%s''', (lp_entry[0].name), as_dict=1) | ||||
| 		if against_lp_entry: | ||||
| 			invoice_list = ", ".join([d.sales_invoice for d in against_lp_entry]) | ||||
| 			frappe.throw(_('''Sales Invoice can't be cancelled since the Loyalty Points earned has been redeemed. | ||||
| 				First cancel the Sales Invoice No {0}''').format(invoice_list)) | ||||
| 			invoice_list = ", ".join([d.invoice for d in against_lp_entry]) | ||||
| 			frappe.throw(_('''{} can't be cancelled since the Loyalty Points earned has been redeemed. | ||||
| 				First cancel the {} No {}''').format(self.doctype, self.doctype, invoice_list)) | ||||
| 		else: | ||||
| 			frappe.db.sql('''delete from `tabLoyalty Point Entry` where sales_invoice=%s''', (self.name)) | ||||
| 			frappe.db.sql('''delete from `tabLoyalty Point Entry` where invoice=%s''', (self.name)) | ||||
| 			# Set loyalty program | ||||
| 			self.set_loyalty_program_tier() | ||||
| 
 | ||||
| @ -1172,7 +1169,9 @@ class SalesInvoice(SellingController): | ||||
| 
 | ||||
| 		points_to_redeem = self.loyalty_points | ||||
| 		for lp_entry in loyalty_point_entries: | ||||
| 			if lp_entry.sales_invoice == self.name: | ||||
| 			if lp_entry.invoice_type != self.doctype or lp_entry.invoice == self.name: | ||||
| 				# redeemption should be done against same doctype | ||||
| 				# also it shouldn't be against itself | ||||
| 				continue | ||||
| 			available_points = lp_entry.loyalty_points - flt(redemption_details.get(lp_entry.name)) | ||||
| 			if available_points > points_to_redeem: | ||||
| @ -1185,7 +1184,8 @@ class SalesInvoice(SellingController): | ||||
| 				"loyalty_program": self.loyalty_program, | ||||
| 				"loyalty_program_tier": lp_entry.loyalty_program_tier, | ||||
| 				"customer": self.customer, | ||||
| 				"sales_invoice": self.name, | ||||
| 				"invoice_type": self.doctype, | ||||
| 				"invoice": self.name, | ||||
| 				"redeem_against": lp_entry.name, | ||||
| 				"loyalty_points": -1*redeemed_points, | ||||
| 				"purchase_amount": self.grand_total, | ||||
| @ -1576,13 +1576,13 @@ def get_loyalty_programs(customer): | ||||
| 	from erpnext.selling.doctype.customer.customer import get_loyalty_programs | ||||
| 
 | ||||
| 	customer = frappe.get_doc('Customer', customer) | ||||
| 	if customer.loyalty_program: return | ||||
| 	if customer.loyalty_program: return [customer.loyalty_program] | ||||
| 
 | ||||
| 	lp_details = get_loyalty_programs(customer) | ||||
| 
 | ||||
| 	if len(lp_details) == 1: | ||||
| 		frappe.db.set(customer, 'loyalty_program', lp_details[0]) | ||||
| 		return [] | ||||
| 		return lp_details | ||||
| 	else: | ||||
| 		return lp_details | ||||
| 
 | ||||
| @ -1602,3 +1602,71 @@ def create_invoice_discounting(source_name, target_doc=None): | ||||
| 	}) | ||||
| 
 | ||||
| 	return invoice_discounting | ||||
| 
 | ||||
| def update_multi_mode_option(doc, pos_profile): | ||||
| 	def append_payment(payment_mode): | ||||
| 		payment = doc.append('payments', {}) | ||||
| 		payment.default = payment_mode.default | ||||
| 		payment.mode_of_payment = payment_mode.parent | ||||
| 		payment.account = payment_mode.default_account | ||||
| 		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) | ||||
| 		payment_mode[0].default = pos_payment_method.default | ||||
| 		append_payment(payment_mode[0]) | ||||
| 
 | ||||
| def get_all_mode_of_payments(doc): | ||||
| 	return frappe.db.sql(""" | ||||
| 		select mpa.default_account, mpa.parent, mp.type as type  | ||||
| 		from `tabMode of Payment Account` mpa,`tabMode of Payment` mp  | ||||
| 		where mpa.parent = mp.name and mpa.company = %(company)s and mp.enabled = 1""", | ||||
| 	{'company': doc.company}, as_dict=1) | ||||
| 
 | ||||
| def get_mode_of_payment_info(mode_of_payment, company): | ||||
| 	return frappe.db.sql(""" | ||||
| 		select mpa.default_account, mpa.parent, mp.type as type  | ||||
| 		from `tabMode of Payment Account` mpa,`tabMode of Payment` mp  | ||||
| 		where mpa.parent = mp.name and mpa.company = %s and mp.enabled = 1 and mp.name = %s""", | ||||
| 	(company, mode_of_payment), as_dict=1) | ||||
| 
 | ||||
| def create_dunning(source_name, target_doc=None): | ||||
| 	from frappe.model.mapper import get_mapped_doc | ||||
| 	from erpnext.accounts.doctype.dunning.dunning import get_dunning_letter_text, calculate_interest_and_amount | ||||
| 	def set_missing_values(source, target): | ||||
| 		target.sales_invoice = source_name | ||||
| 		target.outstanding_amount = source.outstanding_amount | ||||
| 		overdue_days = (getdate(target.posting_date) - getdate(source.due_date)).days | ||||
| 		target.overdue_days = overdue_days | ||||
| 		if frappe.db.exists('Dunning Type', {'start_day': [ | ||||
| 	                                '<', overdue_days], 'end_day': ['>=', overdue_days]}): | ||||
| 			dunning_type = frappe.get_doc('Dunning Type', {'start_day': [ | ||||
| 	                                '<', overdue_days], 'end_day': ['>=', overdue_days]}) | ||||
| 			target.dunning_type = dunning_type.name | ||||
| 			target.rate_of_interest = dunning_type.rate_of_interest | ||||
| 			target.dunning_fee = dunning_type.dunning_fee | ||||
| 			letter_text = get_dunning_letter_text(dunning_type = dunning_type.name, doc = target.as_dict()) | ||||
| 			if letter_text: | ||||
| 				target.body_text = letter_text.get('body_text') | ||||
| 				target.closing_text = letter_text.get('closing_text') | ||||
| 				target.language = letter_text.get('language') | ||||
| 			amounts = calculate_interest_and_amount(target.posting_date, target.outstanding_amount, | ||||
| 				target.rate_of_interest, target.dunning_fee, target.overdue_days) | ||||
| 			target.interest_amount = amounts.get('interest_amount') | ||||
| 			target.dunning_amount = amounts.get('dunning_amount') | ||||
| 			target.grand_total = amounts.get('grand_total') | ||||
| 
 | ||||
| 	doclist = get_mapped_doc("Sales Invoice", source_name,	{ | ||||
| 		"Sales Invoice": { | ||||
| 			"doctype": "Dunning", | ||||
| 		} | ||||
| 	}, target_doc, set_missing_values) | ||||
| 	return doclist | ||||
|  | ||||
| @ -706,37 +706,15 @@ class TestSalesInvoice(unittest.TestCase): | ||||
| 
 | ||||
| 		self.pos_gl_entry(si, pos, 50) | ||||
| 
 | ||||
| 	def test_pos_returns_without_repayment(self): | ||||
| 		pos_profile = make_pos_profile() | ||||
| 
 | ||||
| 		pos = create_sales_invoice(qty = 10, do_not_save=True) | ||||
| 		pos.is_pos = 1 | ||||
| 		pos.pos_profile = pos_profile.name | ||||
| 
 | ||||
| 		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.insert() | ||||
| 		pos.submit() | ||||
| 
 | ||||
| 		pos_return = create_sales_invoice(is_return=1, | ||||
| 			return_against=pos.name, qty=-5, do_not_save=True) | ||||
| 
 | ||||
| 		pos_return.is_pos = 1 | ||||
| 		pos_return.pos_profile = pos_profile.name | ||||
| 
 | ||||
| 		pos_return.insert() | ||||
| 		pos_return.submit() | ||||
| 
 | ||||
| 		self.assertFalse(pos_return.is_pos) | ||||
| 		self.assertFalse(pos_return.get('payments')) | ||||
| 
 | ||||
| 	def test_pos_returns_with_repayment(self): | ||||
| 		from erpnext.accounts.doctype.sales_invoice.sales_invoice import make_sales_return | ||||
| 
 | ||||
| 		pos_profile = make_pos_profile() | ||||
| 
 | ||||
| 		pos_profile.payments = [] | ||||
| 		pos_profile.append('payments', { | ||||
| 			'default': 1, | ||||
| 			'mode_of_payment': 'Cash', | ||||
| 			'amount': 0.0 | ||||
| 			'mode_of_payment': 'Cash' | ||||
| 		}) | ||||
| 
 | ||||
| 		pos_profile.save() | ||||
| @ -751,18 +729,12 @@ class TestSalesInvoice(unittest.TestCase): | ||||
| 		pos.insert() | ||||
| 		pos.submit() | ||||
| 
 | ||||
| 		pos_return = create_sales_invoice(is_return=1, | ||||
| 			return_against=pos.name, qty=-5, do_not_save=True) | ||||
| 		pos_return = make_sales_return(pos.name) | ||||
| 
 | ||||
| 		pos_return.is_pos = 1 | ||||
| 		pos_return.pos_profile = pos_profile.name | ||||
| 		pos_return.insert() | ||||
| 		pos_return.submit() | ||||
| 
 | ||||
| 		self.assertEqual(pos_return.get('payments')[0].amount, -500) | ||||
| 		pos_profile.payments = [] | ||||
| 		pos_profile.save() | ||||
| 
 | ||||
| 		self.assertEqual(pos_return.get('payments')[0].amount, -1000) | ||||
| 
 | ||||
| 	def test_pos_change_amount(self): | ||||
| 		make_pos_profile() | ||||
| @ -788,82 +760,6 @@ class TestSalesInvoice(unittest.TestCase): | ||||
| 		self.assertEqual(pos.grand_total, 100.0) | ||||
| 		self.assertEqual(pos.write_off_amount, -5) | ||||
| 
 | ||||
| 	def test_make_pos_invoice(self): | ||||
| 		from erpnext.accounts.doctype.sales_invoice.pos import make_invoice | ||||
| 
 | ||||
| 		pos_profile = make_pos_profile() | ||||
| 
 | ||||
| 		pr = make_purchase_receipt(company= "_Test Company with perpetual inventory", | ||||
| 			item_code= "_Test FG Item", | ||||
| 			warehouse= "Stores - TCP1", cost_center= "Main - TCP1") | ||||
| 
 | ||||
| 		pos = create_sales_invoice(company= "_Test Company with perpetual inventory", | ||||
| 			debit_to="Debtors - TCP1", item_code= "_Test FG Item", warehouse="Stores - TCP1", | ||||
| 			income_account = "Sales - TCP1", expense_account = "Cost of Goods Sold - TCP1", | ||||
| 			cost_center = "Main - TCP1", do_not_save=True) | ||||
| 
 | ||||
| 		pos.is_pos = 1 | ||||
| 		pos.update_stock = 1 | ||||
| 
 | ||||
| 		pos.append("payments", {'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - TCP1', 'amount': 50}) | ||||
| 		pos.append("payments", {'mode_of_payment': 'Cash', 'account': 'Cash - TCP1', 'amount': 50}) | ||||
| 
 | ||||
| 		taxes = get_taxes_and_charges() | ||||
| 		pos.taxes = [] | ||||
| 		for tax in taxes: | ||||
| 			pos.append("taxes", tax) | ||||
| 
 | ||||
| 		invoice_data = [{'09052016142': pos}] | ||||
| 		si = make_invoice(pos_profile, invoice_data).get('invoice') | ||||
| 		self.assertEqual(si[0], '09052016142') | ||||
| 
 | ||||
| 		sales_invoice = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': '09052016142', 'docstatus': 1}) | ||||
| 		si = frappe.get_doc('Sales Invoice', sales_invoice[0].name) | ||||
| 
 | ||||
| 		self.assertEqual(si.grand_total, 100) | ||||
| 
 | ||||
| 		self.pos_gl_entry(si, pos, 50) | ||||
| 
 | ||||
| 	def test_make_pos_invoice_in_draft(self): | ||||
| 		from erpnext.accounts.doctype.sales_invoice.pos import make_invoice | ||||
| 		from erpnext.stock.doctype.item.test_item import make_item | ||||
| 
 | ||||
| 		allow_negative_stock = frappe.db.get_single_value('Stock Settings', 'allow_negative_stock') | ||||
| 		if allow_negative_stock: | ||||
| 			frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 0) | ||||
| 
 | ||||
| 		pos_profile = make_pos_profile() | ||||
| 		timestamp = cint(time.time()) | ||||
| 
 | ||||
| 		item = make_item("_Test POS Item") | ||||
| 		pos = copy.deepcopy(test_records[1]) | ||||
| 		pos['items'][0]['item_code'] = item.name | ||||
| 		pos['items'][0]['warehouse'] = "_Test Warehouse - _TC" | ||||
| 		pos["is_pos"] = 1 | ||||
| 		pos["offline_pos_name"] = timestamp | ||||
| 		pos["update_stock"] = 1 | ||||
| 		pos["payments"] = [{'mode_of_payment': 'Bank Draft', 'account': '_Test Bank - _TC', 'amount': 300}, | ||||
| 							{'mode_of_payment': 'Cash', 'account': 'Cash - _TC', 'amount': 330}] | ||||
| 
 | ||||
| 		invoice_data = [{timestamp: pos}] | ||||
| 		si = make_invoice(pos_profile, invoice_data).get('invoice') | ||||
| 		self.assertEqual(si[0], timestamp) | ||||
| 
 | ||||
| 		sales_invoice = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': timestamp}) | ||||
| 		self.assertEqual(sales_invoice[0].docstatus, 0) | ||||
| 
 | ||||
| 		timestamp = cint(time.time()) | ||||
| 		pos["offline_pos_name"] = timestamp | ||||
| 		invoice_data = [{timestamp: pos}] | ||||
| 		si1 = make_invoice(pos_profile, invoice_data).get('invoice') | ||||
| 		self.assertEqual(si1[0], timestamp) | ||||
| 
 | ||||
| 		sales_invoice1 = frappe.get_all('Sales Invoice', fields =["*"], filters = {'offline_pos_name': timestamp}) | ||||
| 		self.assertEqual(sales_invoice1[0].docstatus, 0) | ||||
| 
 | ||||
| 		if allow_negative_stock: | ||||
| 			frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) | ||||
| 
 | ||||
| 	def pos_gl_entry(self, si, pos, cash_amount): | ||||
| 		# check stock ledger entries | ||||
| 		sle = frappe.db.sql("""select * from `tabStock Ledger Entry` | ||||
|  | ||||
| @ -795,7 +795,7 @@ | ||||
|  "idx": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-03-11 12:24:41.749986", | ||||
|  "modified": "2020-07-18 12:24:41.749986", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Sales Invoice Item", | ||||
|  | ||||
| @ -1,314 +1,90 @@ | ||||
| { | ||||
|  "allow_copy": 0,  | ||||
|  "allow_events_in_timeline": 0,  | ||||
|  "allow_guest_to_view": 0,  | ||||
|  "allow_import": 0,  | ||||
|  "allow_rename": 0,  | ||||
|  "beta": 0,  | ||||
|  "creation": "2016-05-08 23:49:38.842621",  | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "",  | ||||
|  "editable_grid": 1,  | ||||
|  "actions": [], | ||||
|  "creation": "2016-05-08 23:49:38.842621", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "default", | ||||
|   "mode_of_payment", | ||||
|   "amount", | ||||
|   "column_break_3", | ||||
|   "account", | ||||
|   "type", | ||||
|   "base_amount", | ||||
|   "clearance_date" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "depends_on": "eval:parent.doctype == 'POS Profile'",  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "default",  | ||||
|    "fieldtype": "Check",  | ||||
|    "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": "Default",  | ||||
|    "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": "mode_of_payment", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Mode of Payment", | ||||
|    "options": "Mode of Payment", | ||||
|    "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": "mode_of_payment",  | ||||
|    "fieldtype": "Link",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 1,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Mode of Payment",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Mode of Payment",  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 1,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "default": "0", | ||||
|    "depends_on": "eval:parent.doctype == 'Sales Invoice'", | ||||
|    "fieldname": "amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Amount", | ||||
|    "options": "currency", | ||||
|    "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 == 'Sales Invoice'",  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "amount",  | ||||
|    "fieldtype": "Currency",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 1,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Amount",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "currency",  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 1,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "fieldname": "column_break_3", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fetch_if_empty": 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": "account", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Account", | ||||
|    "options": "Account", | ||||
|    "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,  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "account",  | ||||
|    "fieldtype": "Link",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Account",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Account",  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 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 | ||||
|   },  | ||||
|    "fetch_from": "mode_of_payment.type", | ||||
|    "fieldname": "type", | ||||
|    "fieldtype": "Read Only", | ||||
|    "label": "Type" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fetch_from": "mode_of_payment.type",  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "type",  | ||||
|    "fieldtype": "Read Only",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Type",  | ||||
|    "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": "base_amount", | ||||
|    "fieldtype": "Currency", | ||||
|    "label": "Base Amount (Company Currency)", | ||||
|    "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,  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "base_amount",  | ||||
|    "fieldtype": "Currency",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Base Amount (Company Currency)",  | ||||
|    "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": "clearance_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Clearance Date", | ||||
|    "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,  | ||||
|    "fetch_if_empty": 0,  | ||||
|    "fieldname": "clearance_date",  | ||||
|    "fieldtype": "Date",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Clearance Date",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "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 | ||||
|    "default": "0", | ||||
|    "fieldname": "default", | ||||
|    "fieldtype": "Check", | ||||
|    "hidden": 1, | ||||
|    "label": "Default", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ],  | ||||
|  "has_web_view": 0,  | ||||
|  "hide_heading": 0,  | ||||
|  "hide_toolbar": 0,  | ||||
|  "idx": 0,  | ||||
|  "image_view": 0,  | ||||
|  "in_create": 0,  | ||||
|  "is_submittable": 0,  | ||||
|  "issingle": 0,  | ||||
|  "istable": 1,  | ||||
|  "max_attachments": 0,  | ||||
|  "modified": "2019-03-19 14:54:56.524556",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Accounts",  | ||||
|  "name": "Sales Invoice Payment",  | ||||
|  "name_case": "",  | ||||
|  "owner": "Administrator",  | ||||
|  "permissions": [],  | ||||
|  "quick_entry": 1,  | ||||
|  "read_only": 0,  | ||||
|  "read_only_onload": 0,  | ||||
|  "show_name_in_global_search": 0,  | ||||
|  "sort_field": "modified",  | ||||
|  "sort_order": "DESC",  | ||||
|  "track_changes": 0,  | ||||
|  "track_seen": 0,  | ||||
|  "track_views": 0 | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-05-05 16:51:20.091441", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Sales Invoice Payment", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC" | ||||
| } | ||||
| @ -2,6 +2,16 @@ | ||||
| // For license information, please see license.txt
 | ||||
| 
 | ||||
| frappe.ui.form.on('Subscription', { | ||||
| 	setup: function(frm) { | ||||
| 		frm.set_query('party_type', function() { | ||||
| 			return { | ||||
| 				filters : { | ||||
| 					name: ['in', ['Customer', 'Supplier']] | ||||
| 				} | ||||
| 			} | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	refresh: function(frm) { | ||||
| 		if(!frm.is_new()){ | ||||
| 			if(frm.doc.status !== 'Cancelled'){ | ||||
|  | ||||
| @ -6,14 +6,18 @@ | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "customer", | ||||
|   "cb_1", | ||||
|   "party_type", | ||||
|   "status", | ||||
|   "cb_1", | ||||
|   "party", | ||||
|   "subscription_period", | ||||
|   "start", | ||||
|   "start_date", | ||||
|   "end_date", | ||||
|   "cancelation_date", | ||||
|   "trial_period_start", | ||||
|   "trial_period_end", | ||||
|   "follow_calendar_months", | ||||
|   "generate_new_invoices_past_due_date", | ||||
|   "column_break_11", | ||||
|   "current_invoice_start", | ||||
|   "current_invoice_end", | ||||
| @ -23,7 +27,8 @@ | ||||
|   "sb_4", | ||||
|   "plans", | ||||
|   "sb_1", | ||||
|   "tax_template", | ||||
|   "sales_tax_template", | ||||
|   "purchase_tax_template", | ||||
|   "sb_2", | ||||
|   "apply_additional_discount", | ||||
|   "cb_2", | ||||
| @ -32,18 +37,10 @@ | ||||
|   "sb_3", | ||||
|   "invoices", | ||||
|   "accounting_dimensions_section", | ||||
|   "cost_center", | ||||
|   "dimension_col_break" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "fieldname": "customer", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Customer", | ||||
|    "options": "Customer", | ||||
|    "reqd": 1, | ||||
|    "set_only_once": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "cb_1", | ||||
| @ -53,7 +50,7 @@ | ||||
|    "fieldname": "status", | ||||
|    "fieldtype": "Select", | ||||
|    "label": "Status", | ||||
|    "options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid", | ||||
|    "options": "\nTrialling\nActive\nPast Due Date\nCancelled\nUnpaid\nCompleted", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
| @ -61,12 +58,6 @@ | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Subscription Period" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "start", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Subscription Start Date", | ||||
|    "set_only_once": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "cancelation_date", | ||||
|    "fieldtype": "Date", | ||||
| @ -137,16 +128,11 @@ | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:['Customer', 'Supplier'].includes(doc.party_type)", | ||||
|    "fieldname": "sb_1", | ||||
|    "fieldtype": "Section Break", | ||||
|    "label": "Taxes" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "tax_template", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Sales Taxes and Charges Template", | ||||
|    "options": "Sales Taxes and Charges Template" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "sb_2", | ||||
|    "fieldtype": "Section Break", | ||||
| @ -195,10 +181,74 @@ | ||||
|   { | ||||
|    "fieldname": "dimension_col_break", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "party_type", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Party Type", | ||||
|    "options": "DocType", | ||||
|    "reqd": 1, | ||||
|    "set_only_once": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "party", | ||||
|    "fieldtype": "Dynamic Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Party", | ||||
|    "options": "party_type", | ||||
|    "reqd": 1, | ||||
|    "set_only_once": 1 | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:doc.party_type === 'Customer'", | ||||
|    "fieldname": "sales_tax_template", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Sales Taxes and Charges Template", | ||||
|    "options": "Sales Taxes and Charges Template" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:doc.party_type === 'Supplier'", | ||||
|    "fieldname": "purchase_tax_template", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Purchase Taxes and Charges Template", | ||||
|    "options": "Purchase Taxes and Charges Template" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "description": "If this is checked subsequent new invoices will be created on calendar  month and quarter start dates irrespective of current invoice start date", | ||||
|    "fieldname": "follow_calendar_months", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Follow Calendar Months", | ||||
|    "set_only_once": 1 | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "description": "New invoices will be generated as per schedule even if current invoices are unpaid or past due date", | ||||
|    "fieldname": "generate_new_invoices_past_due_date", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Generate New Invoices Past Due Date" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "end_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Subscription End Date", | ||||
|    "set_only_once": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "start_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "Subscription Start Date", | ||||
|    "set_only_once": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "cost_center", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Cost Center", | ||||
|    "options": "Cost Center" | ||||
|   } | ||||
|  ], | ||||
|  "links": [], | ||||
|  "modified": "2020-01-27 14:37:32.845173", | ||||
|  "modified": "2020-06-25 10:52:52.265105", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Subscription", | ||||
|  | ||||
| @ -7,7 +7,7 @@ from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.model.document import Document | ||||
| from frappe.utils.data import nowdate, getdate, cint, add_days, date_diff, get_last_day, add_to_date, flt | ||||
| from frappe.utils.data import nowdate, getdate, cstr, cint, add_days, date_diff, get_last_day, add_to_date, flt | ||||
| from erpnext.accounts.doctype.subscription_plan.subscription_plan import get_plan_rate | ||||
| from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions | ||||
| 
 | ||||
| @ -15,7 +15,7 @@ from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import g | ||||
| class Subscription(Document): | ||||
| 	def before_insert(self): | ||||
| 		# update start just before the subscription doc is created | ||||
| 		self.update_subscription_period(self.start) | ||||
| 		self.update_subscription_period(self.start_date) | ||||
| 
 | ||||
| 	def update_subscription_period(self, date=None): | ||||
| 		""" | ||||
| @ -35,7 +35,9 @@ class Subscription(Document): | ||||
| 		If the `date` parameter is not given , it will be automatically set as today's | ||||
| 		date. | ||||
| 		""" | ||||
| 		if self.trial_period_start and self.is_trialling(): | ||||
| 		if self.is_new_subscription() and self.trial_period_end and getdate(self.trial_period_end) > getdate(self.start_date): | ||||
| 			self.current_invoice_start = add_days(self.trial_period_end, 1) | ||||
| 		elif self.trial_period_start and self.is_trialling(): | ||||
| 			self.current_invoice_start = self.trial_period_start | ||||
| 		elif date: | ||||
| 			self.current_invoice_start = date | ||||
| @ -53,15 +55,45 @@ class Subscription(Document): | ||||
| 		current billing period where `x` is the billing interval from the | ||||
| 		`Subscription Plan` in the `Subscription`. | ||||
| 		""" | ||||
| 		if self.is_trialling(): | ||||
| 		if self.is_trialling() and getdate(self.current_invoice_start) < getdate(self.trial_period_end): | ||||
| 			self.current_invoice_end = self.trial_period_end | ||||
| 		else: | ||||
| 			billing_cycle_info = self.get_billing_cycle_data() | ||||
| 			if billing_cycle_info: | ||||
| 				self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info) | ||||
| 				if self.is_new_subscription() and getdate(self.start_date) < getdate(self.current_invoice_start): | ||||
| 					self.current_invoice_end = add_to_date(self.start_date, **billing_cycle_info) | ||||
| 
 | ||||
| 					# For cases where trial period is for an entire billing interval | ||||
| 					if getdate(self.current_invoice_end) < getdate(self.current_invoice_start): | ||||
| 						self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info) | ||||
| 				else: | ||||
| 					self.current_invoice_end = add_to_date(self.current_invoice_start, **billing_cycle_info) | ||||
| 			else: | ||||
| 				self.current_invoice_end = get_last_day(self.current_invoice_start) | ||||
| 
 | ||||
| 			if self.follow_calendar_months: | ||||
| 				billing_info = self.get_billing_cycle_and_interval() | ||||
| 				billing_interval_count = billing_info[0]['billing_interval_count'] | ||||
| 				calendar_months = get_calendar_months(billing_interval_count) | ||||
| 				calendar_month = 0 | ||||
| 				current_invoice_end_month = getdate(self.current_invoice_end).month | ||||
| 				current_invoice_end_year = getdate(self.current_invoice_end).year | ||||
| 
 | ||||
| 				for month in calendar_months: | ||||
| 					if month <= current_invoice_end_month: | ||||
| 						calendar_month = month | ||||
| 
 | ||||
| 				if cint(calendar_month - billing_interval_count) <= 0 and \ | ||||
| 					getdate(self.current_invoice_start).month != 1: | ||||
| 					calendar_month = 12 | ||||
| 					current_invoice_end_year -= 1 | ||||
| 
 | ||||
| 				self.current_invoice_end = get_last_day(cstr(current_invoice_end_year) + '-' \ | ||||
| 					+ cstr(calendar_month) + '-01') | ||||
| 
 | ||||
| 			if self.end_date and getdate(self.current_invoice_end) > getdate(self.end_date): | ||||
| 				self.current_invoice_end = self.end_date | ||||
| 
 | ||||
| 	@staticmethod | ||||
| 	def validate_plans_billing_cycle(billing_cycle_data): | ||||
| 		""" | ||||
| @ -132,21 +164,22 @@ class Subscription(Document): | ||||
| 		""" | ||||
| 		if self.is_trialling(): | ||||
| 			self.status = 'Trialling' | ||||
| 		elif self.status == 'Past Due Date' and self.is_past_grace_period(): | ||||
| 		elif self.status == 'Active' and self.end_date and getdate() > getdate(self.end_date): | ||||
| 			self.status = 'Completed' | ||||
| 		elif self.is_past_grace_period(): | ||||
| 			subscription_settings = frappe.get_single('Subscription Settings') | ||||
| 			self.status = 'Cancelled' if cint(subscription_settings.cancel_after_grace) else 'Unpaid' | ||||
| 		elif self.status == 'Past Due Date' and not self.has_outstanding_invoice(): | ||||
| 			self.status = 'Active' | ||||
| 		elif self.current_invoice_is_past_due(): | ||||
| 		elif self.current_invoice_is_past_due() and not self.is_past_grace_period(): | ||||
| 			self.status = 'Past Due Date' | ||||
| 		elif not self.has_outstanding_invoice(): | ||||
| 			self.status = 'Active' | ||||
| 		elif self.is_new_subscription(): | ||||
| 			self.status = 'Active' | ||||
| 			# todo: then generate new invoice | ||||
| 		self.save() | ||||
| 
 | ||||
| 	def is_trialling(self): | ||||
| 		""" | ||||
| 		Returns `True` if the `Subscription` is trial period. | ||||
| 		Returns `True` if the `Subscription` is in trial period. | ||||
| 		""" | ||||
| 		return not self.period_has_passed(self.trial_period_end) and self.is_new_subscription() | ||||
| 
 | ||||
| @ -160,7 +193,7 @@ class Subscription(Document): | ||||
| 			return True | ||||
| 
 | ||||
| 		end_date = getdate(end_date) | ||||
| 		return getdate(nowdate()) > getdate(end_date) | ||||
| 		return getdate() > getdate(end_date) | ||||
| 
 | ||||
| 	def is_past_grace_period(self): | ||||
| 		""" | ||||
| @ -171,7 +204,7 @@ class Subscription(Document): | ||||
| 			subscription_settings = frappe.get_single('Subscription Settings') | ||||
| 			grace_period = cint(subscription_settings.grace_period) | ||||
| 
 | ||||
| 			return getdate(nowdate()) > add_days(current_invoice.due_date, grace_period) | ||||
| 			return getdate() > add_days(current_invoice.due_date, grace_period) | ||||
| 
 | ||||
| 	def current_invoice_is_past_due(self, current_invoice=None): | ||||
| 		""" | ||||
| @ -180,22 +213,24 @@ class Subscription(Document): | ||||
| 		if not current_invoice: | ||||
| 			current_invoice = self.get_current_invoice() | ||||
| 
 | ||||
| 		if not current_invoice: | ||||
| 		if not current_invoice or self.is_paid(current_invoice): | ||||
| 			return False | ||||
| 		else: | ||||
| 			return getdate(nowdate()) > getdate(current_invoice.due_date) | ||||
| 			return getdate() > getdate(current_invoice.due_date) | ||||
| 
 | ||||
| 	def get_current_invoice(self): | ||||
| 		""" | ||||
| 		Returns the most recent generated invoice. | ||||
| 		""" | ||||
| 		doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' | ||||
| 
 | ||||
| 		if len(self.invoices): | ||||
| 			current = self.invoices[-1] | ||||
| 			if frappe.db.exists('Sales Invoice', current.invoice): | ||||
| 				doc = frappe.get_doc('Sales Invoice', current.invoice) | ||||
| 			if frappe.db.exists(doctype, current.get('invoice')): | ||||
| 				doc = frappe.get_doc(doctype, current.get('invoice')) | ||||
| 				return doc | ||||
| 			else: | ||||
| 				frappe.throw(_('Invoice {0} no longer exists').format(current.invoice)) | ||||
| 				frappe.throw(_('Invoice {0} no longer exists').format(current.get('invoice'))) | ||||
| 
 | ||||
| 	def is_new_subscription(self): | ||||
| 		""" | ||||
| @ -206,6 +241,8 @@ class Subscription(Document): | ||||
| 	def validate(self): | ||||
| 		self.validate_trial_period() | ||||
| 		self.validate_plans_billing_cycle(self.get_billing_cycle_and_interval()) | ||||
| 		self.validate_end_date() | ||||
| 		self.validate_to_follow_calendar_months() | ||||
| 
 | ||||
| 	def validate_trial_period(self): | ||||
| 		""" | ||||
| @ -215,34 +252,72 @@ class Subscription(Document): | ||||
| 			if getdate(self.trial_period_end) < getdate(self.trial_period_start): | ||||
| 				frappe.throw(_('Trial Period End Date Cannot be before Trial Period Start Date')) | ||||
| 
 | ||||
| 		elif self.trial_period_start or self.trial_period_end: | ||||
| 		if self.trial_period_start and not self.trial_period_end: | ||||
| 			frappe.throw(_('Both Trial Period Start Date and Trial Period End Date must be set')) | ||||
| 
 | ||||
| 		if self.trial_period_start and getdate(self.trial_period_start) > getdate(self.start_date): | ||||
| 			frappe.throw(_('Trial Period Start date cannot be after Subscription Start Date')) | ||||
| 
 | ||||
| 	def validate_end_date(self): | ||||
| 		billing_cycle_info = self.get_billing_cycle_data() | ||||
| 		end_date = add_to_date(self.start_date, **billing_cycle_info) | ||||
| 
 | ||||
| 		if self.end_date and getdate(self.end_date) <= getdate(end_date): | ||||
| 			frappe.throw(_('Subscription End Date must be after {0} as per the subscription plan').format(end_date)) | ||||
| 
 | ||||
| 	def validate_to_follow_calendar_months(self): | ||||
| 		if self.follow_calendar_months: | ||||
| 			billing_info = self.get_billing_cycle_and_interval() | ||||
| 
 | ||||
| 			if not self.end_date: | ||||
| 				frappe.throw(_('Subscription End Date is mandatory to follow calendar months')) | ||||
| 
 | ||||
| 			if billing_info[0]['billing_interval'] != 'Month': | ||||
| 				frappe.throw('Billing Interval in Subscription Plan must be Month to follow calendar months') | ||||
| 
 | ||||
| 	def after_insert(self): | ||||
| 		# todo: deal with users who collect prepayments. Maybe a new Subscription Invoice doctype? | ||||
| 		self.set_subscription_status() | ||||
| 
 | ||||
| 	def generate_invoice(self, prorate=0): | ||||
| 		""" | ||||
| 		Creates a `Sales Invoice` for the `Subscription`, updates `self.invoices` and | ||||
| 		Creates a `Invoice` for the `Subscription`, updates `self.invoices` and | ||||
| 		saves the `Subscription`. | ||||
| 		""" | ||||
| 
 | ||||
| 		doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' | ||||
| 
 | ||||
| 		invoice = self.create_invoice(prorate) | ||||
| 		self.append('invoices', {'invoice': invoice.name}) | ||||
| 		self.append('invoices', { | ||||
| 			'document_type': doctype, | ||||
| 			'invoice': invoice.name | ||||
| 		}) | ||||
| 
 | ||||
| 		self.save() | ||||
| 
 | ||||
| 		return invoice | ||||
| 
 | ||||
| 	def create_invoice(self, prorate): | ||||
| 		""" | ||||
| 		Creates a `Sales Invoice`, submits it and returns it | ||||
| 		Creates a `Invoice`, submits it and returns it | ||||
| 		""" | ||||
| 		invoice = frappe.new_doc('Sales Invoice') | ||||
| 		invoice.set_posting_time = 1 | ||||
| 		invoice.posting_date = self.current_invoice_start | ||||
| 		invoice.customer = self.customer | ||||
| 		doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' | ||||
| 
 | ||||
| 		## Add dimesnions in invoice for subscription: | ||||
| 		invoice = frappe.new_doc(doctype) | ||||
| 		invoice.set_posting_time = 1 | ||||
| 		invoice.posting_date = self.current_invoice_start if self.generate_invoice_at_period_start \ | ||||
| 			else self.current_invoice_end | ||||
| 
 | ||||
| 		invoice.cost_center = self.cost_center | ||||
| 
 | ||||
| 		if doctype == 'Sales Invoice': | ||||
| 			invoice.customer = self.party | ||||
| 		else: | ||||
| 			invoice.supplier = self.party | ||||
| 			if frappe.db.get_value('Supplier', self.party, 'tax_withholding_category'): | ||||
| 				invoice.apply_tds = 1 | ||||
| 
 | ||||
| 		## Add dimensions in invoice for subscription: | ||||
| 		accounting_dimensions = get_accounting_dimensions() | ||||
| 
 | ||||
| 		for dimension in accounting_dimensions: | ||||
| @ -255,18 +330,25 @@ class Subscription(Document): | ||||
| 		# for that reason | ||||
| 		items_list = self.get_items_from_plans(self.plans, prorate) | ||||
| 		for item in items_list: | ||||
| 			invoice.append('items',	item) | ||||
| 			invoice.append('items', item) | ||||
| 
 | ||||
| 		# Taxes | ||||
| 		if self.tax_template: | ||||
| 			invoice.taxes_and_charges = self.tax_template | ||||
| 		tax_template = '' | ||||
| 
 | ||||
| 		if doctype == 'Sales Invoice' and self.sales_tax_template: | ||||
| 			tax_template = self.sales_tax_template | ||||
| 		if doctype == 'Purchase Invoice' and self.purchase_tax_template: | ||||
| 			tax_template = self.purchase_tax_template | ||||
| 
 | ||||
| 		if tax_template: | ||||
| 			invoice.taxes_and_charges = tax_template | ||||
| 			invoice.set_taxes() | ||||
| 
 | ||||
| 		# Due date | ||||
| 		invoice.append( | ||||
| 			'payment_schedule', | ||||
| 			{ | ||||
| 				'due_date': add_days(self.current_invoice_end, cint(self.days_until_due)), | ||||
| 				'due_date': add_days(invoice.posting_date, cint(self.days_until_due)), | ||||
| 				'invoice_portion': 100 | ||||
| 			} | ||||
| 		) | ||||
| @ -300,13 +382,42 @@ class Subscription(Document): | ||||
| 			prorate_factor = get_prorata_factor(self.current_invoice_end, self.current_invoice_start) | ||||
| 
 | ||||
| 		items = [] | ||||
| 		customer = self.customer | ||||
| 		party = self.party | ||||
| 		for plan in plans: | ||||
| 			item_code = frappe.db.get_value("Subscription Plan", plan.plan, "item") | ||||
| 			if not prorate: | ||||
| 				items.append({'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, customer)}) | ||||
| 			plan_doc = frappe.get_doc('Subscription Plan', plan.plan) | ||||
| 
 | ||||
| 			item_code = plan_doc.item | ||||
| 
 | ||||
| 			if self.party == 'Customer': | ||||
| 				deferred_field = 'enable_deferred_revenue' | ||||
| 			else: | ||||
| 				items.append({'item_code': item_code, 'qty': plan.qty, 'rate': (get_plan_rate(plan.plan, plan.qty, customer) * prorate_factor)}) | ||||
| 				deferred_field = 'enable_deferred_expense' | ||||
| 
 | ||||
| 			deferred = frappe.db.get_value('Item', item_code, deferred_field) | ||||
| 
 | ||||
| 			if not prorate: | ||||
| 				item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party, | ||||
| 					self.current_invoice_start, self.current_invoice_end), 'cost_center': plan_doc.cost_center} | ||||
| 			else: | ||||
| 				item = {'item_code': item_code, 'qty': plan.qty, 'rate': get_plan_rate(plan.plan, plan.qty, party, | ||||
| 					self.current_invoice_start, self.current_invoice_end, prorate_factor), 'cost_center': plan_doc.cost_center} | ||||
| 
 | ||||
| 			if deferred: | ||||
| 				item.update({ | ||||
| 					deferred_field: deferred, | ||||
| 					'service_start_date': self.current_invoice_start, | ||||
| 					'service_end_date': self.current_invoice_end | ||||
| 				}) | ||||
| 
 | ||||
| 			accounting_dimensions = get_accounting_dimensions() | ||||
| 
 | ||||
| 			for dimension in accounting_dimensions: | ||||
| 				if plan_doc.get(dimension): | ||||
| 					item.update({ | ||||
| 						dimension: plan_doc.get(dimension) | ||||
| 					}) | ||||
| 
 | ||||
| 			items.append(item) | ||||
| 
 | ||||
| 		return items | ||||
| 
 | ||||
| @ -322,12 +433,13 @@ class Subscription(Document): | ||||
| 		elif self.status in ['Past Due Date', 'Unpaid']: | ||||
| 			self.process_for_past_due_date() | ||||
| 
 | ||||
| 		self.set_subscription_status() | ||||
| 
 | ||||
| 		self.save() | ||||
| 
 | ||||
| 	def is_postpaid_to_invoice(self): | ||||
| 		return getdate(nowdate()) > getdate(self.current_invoice_end) or \ | ||||
| 			(getdate(nowdate()) >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) and \ | ||||
| 			not self.has_outstanding_invoice() | ||||
| 		return getdate() > getdate(self.current_invoice_end) or \ | ||||
| 			(getdate() >= getdate(self.current_invoice_end) and getdate(self.current_invoice_end) == getdate(self.current_invoice_start)) | ||||
| 
 | ||||
| 	def is_prepaid_to_invoice(self): | ||||
| 		if not self.generate_invoice_at_period_start: | ||||
| @ -337,14 +449,12 @@ class Subscription(Document): | ||||
| 			return True | ||||
| 
 | ||||
| 		# Check invoice dates and make sure it doesn't have outstanding invoices | ||||
| 		return getdate(nowdate()) >= getdate(self.current_invoice_start) and not self.has_outstanding_invoice() | ||||
| 		return getdate() >= getdate(self.current_invoice_start) | ||||
| 
 | ||||
| 	def is_current_invoice_paid(self): | ||||
| 		if self.is_new_subscription(): | ||||
| 			return False | ||||
| 	def is_current_invoice_generated(self): | ||||
| 		invoice = self.get_current_invoice() | ||||
| 
 | ||||
| 		last_invoice = frappe.get_doc('Sales Invoice', self.invoices[-1].invoice) | ||||
| 		if getdate(last_invoice.posting_date) == getdate(self.current_invoice_start) and last_invoice.status == 'Paid': | ||||
| 		if invoice and getdate(self.current_invoice_start) <= getdate(invoice.posting_date) <= getdate(self.current_invoice_end): | ||||
| 			return True | ||||
| 
 | ||||
| 		return False | ||||
| @ -358,21 +468,23 @@ class Subscription(Document): | ||||
| 		2. Change the `Subscription` status to 'Past Due Date' | ||||
| 		3. Change the `Subscription` status to 'Cancelled' | ||||
| 		""" | ||||
| 		if not self.is_current_invoice_paid() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): | ||||
| 			self.generate_invoice() | ||||
| 			if self.current_invoice_is_past_due(): | ||||
| 				self.status = 'Past Due Date' | ||||
| 		if getdate() > getdate(self.current_invoice_end) and self.is_prepaid_to_invoice(): | ||||
| 			self.update_subscription_period(add_days(self.current_invoice_end, 1)) | ||||
| 
 | ||||
| 		if self.current_invoice_is_past_due() and getdate(nowdate()) > getdate(self.current_invoice_end): | ||||
| 			self.status = 'Past Due Date' | ||||
| 		if not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() or self.is_prepaid_to_invoice()): | ||||
| 			prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') | ||||
| 			self.generate_invoice(prorate) | ||||
| 
 | ||||
| 		if self.cancel_at_period_end and getdate(nowdate()) > getdate(self.current_invoice_end): | ||||
| 		if self.cancel_at_period_end and getdate() > getdate(self.current_invoice_end): | ||||
| 			self.cancel_subscription_at_period_end() | ||||
| 
 | ||||
| 	def cancel_subscription_at_period_end(self): | ||||
| 		""" | ||||
| 		Called when `Subscription.cancel_at_period_end` is truthy | ||||
| 		""" | ||||
| 		if self.end_date and getdate() < getdate(self.end_date): | ||||
| 			return | ||||
| 
 | ||||
| 		self.status = 'Cancelled' | ||||
| 		if not self.cancelation_date: | ||||
| 			self.cancelation_date = nowdate() | ||||
| @ -390,14 +502,22 @@ class Subscription(Document): | ||||
| 		if not current_invoice: | ||||
| 			frappe.throw(_('Current invoice {0} is missing').format(current_invoice.invoice)) | ||||
| 		else: | ||||
| 			if self.is_not_outstanding(current_invoice): | ||||
| 			if not self.has_outstanding_invoice(): | ||||
| 				self.status = 'Active' | ||||
| 				self.update_subscription_period(add_days(self.current_invoice_end, 1)) | ||||
| 			else: | ||||
| 				self.set_status_grace_period() | ||||
| 
 | ||||
| 			if getdate() > getdate(self.current_invoice_end): | ||||
| 				self.update_subscription_period(add_days(self.current_invoice_end, 1)) | ||||
| 
 | ||||
| 			# Generate invoices periodically even if current invoice are unpaid | ||||
| 			if self.generate_new_invoices_past_due_date and not self.is_current_invoice_generated() and (self.is_postpaid_to_invoice() | ||||
| 				or self.is_prepaid_to_invoice()): | ||||
| 				prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') | ||||
| 				self.generate_invoice(prorate) | ||||
| 
 | ||||
| 	@staticmethod | ||||
| 	def is_not_outstanding(invoice): | ||||
| 	def is_paid(invoice): | ||||
| 		""" | ||||
| 		Return `True` if the given invoice is paid | ||||
| 		""" | ||||
| @ -407,11 +527,17 @@ class Subscription(Document): | ||||
| 		""" | ||||
| 		Returns `True` if the most recent invoice for the `Subscription` is not paid | ||||
| 		""" | ||||
| 		doctype = 'Sales Invoice' if self.party_type == 'Customer' else 'Purchase Invoice' | ||||
| 		current_invoice = self.get_current_invoice() | ||||
| 		if not current_invoice: | ||||
| 			return False | ||||
| 		invoice_list = [d.invoice for d in self.invoices] | ||||
| 
 | ||||
| 		outstanding_invoices = frappe.get_all(doctype, fields=['name'], | ||||
| 			filters={'status': ('!=', 'Paid'), 'name': ('in', invoice_list)}) | ||||
| 
 | ||||
| 		if outstanding_invoices: | ||||
| 			return True | ||||
| 		else: | ||||
| 			return not self.is_not_outstanding(current_invoice) | ||||
| 			False | ||||
| 
 | ||||
| 	def cancel_subscription(self): | ||||
| 		""" | ||||
| @ -419,7 +545,7 @@ class Subscription(Document): | ||||
| 		but it will not affect already created invoices. | ||||
| 		""" | ||||
| 		if self.status != 'Cancelled': | ||||
| 			to_generate_invoice = True if self.status == 'Active' else False | ||||
| 			to_generate_invoice = True if self.status == 'Active' and not self.generate_invoice_at_period_start else False | ||||
| 			to_prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') | ||||
| 			self.status = 'Cancelled' | ||||
| 			self.cancelation_date = nowdate() | ||||
| @ -435,7 +561,7 @@ class Subscription(Document): | ||||
| 		""" | ||||
| 		if self.status == 'Cancelled': | ||||
| 			self.status = 'Active' | ||||
| 			self.db_set('start', nowdate()) | ||||
| 			self.db_set('start_date', nowdate()) | ||||
| 			self.update_subscription_period(nowdate()) | ||||
| 			self.invoices = [] | ||||
| 			self.save() | ||||
| @ -447,6 +573,14 @@ class Subscription(Document): | ||||
| 		if invoice: | ||||
| 			return invoice.precision('grand_total') | ||||
| 
 | ||||
| def get_calendar_months(billing_interval): | ||||
| 	calendar_months = [] | ||||
| 	start = 0 | ||||
| 	while start < 12: | ||||
| 		start += billing_interval | ||||
| 		calendar_months.append(start) | ||||
| 
 | ||||
| 	return calendar_months | ||||
| 
 | ||||
| def get_prorata_factor(period_end, period_start): | ||||
| 	diff = flt(date_diff(nowdate(), period_start) + 1) | ||||
| @ -469,10 +603,7 @@ def get_all_subscriptions(): | ||||
| 	""" | ||||
| 	Returns all `Subscription` documents | ||||
| 	""" | ||||
| 	return frappe.db.sql( | ||||
| 		'select name from `tabSubscription` where status != "Cancelled"', | ||||
| 		as_dict=1 | ||||
| 	) | ||||
| 	return frappe.db.get_all('Subscription', {'status': ('!=','Cancelled')}) | ||||
| 
 | ||||
| 
 | ||||
| def process(data): | ||||
|  | ||||
| @ -4,6 +4,8 @@ frappe.listview_settings['Subscription'] = { | ||||
| 			return [__("Trialling"), "green"]; | ||||
| 		} else if(doc.status === 'Active') { | ||||
| 			return [__("Active"), "green"]; | ||||
| 		} else if(doc.status === 'Completed') { | ||||
| 				return [__("Completed"), "green"]; | ||||
| 		} else if(doc.status === 'Past Due Date') { | ||||
| 			return [__("Past Due Date"), "orange"]; | ||||
| 		} else if(doc.status === 'Unpaid') { | ||||
|  | ||||
| @ -7,7 +7,7 @@ import unittest | ||||
| 
 | ||||
| import frappe | ||||
| from erpnext.accounts.doctype.subscription.subscription import get_prorata_factor | ||||
| from frappe.utils.data import nowdate, add_days, add_to_date, add_months, date_diff, flt | ||||
| from frappe.utils.data import nowdate, add_days, add_to_date, add_months, date_diff, flt, get_date_str | ||||
| 
 | ||||
| 
 | ||||
| def create_plan(): | ||||
| @ -15,7 +15,7 @@ def create_plan(): | ||||
| 		plan = frappe.new_doc('Subscription Plan') | ||||
| 		plan.plan_name = '_Test Plan Name' | ||||
| 		plan.item = '_Test Non Stock Item' | ||||
| 		plan.price_determination = "Fixed rate" | ||||
| 		plan.price_determination = "Fixed Rate" | ||||
| 		plan.cost = 900 | ||||
| 		plan.billing_interval = 'Month' | ||||
| 		plan.billing_interval_count = 1 | ||||
| @ -25,7 +25,7 @@ def create_plan(): | ||||
| 		plan = frappe.new_doc('Subscription Plan') | ||||
| 		plan.plan_name = '_Test Plan Name 2' | ||||
| 		plan.item = '_Test Non Stock Item' | ||||
| 		plan.price_determination = "Fixed rate" | ||||
| 		plan.price_determination = "Fixed Rate" | ||||
| 		plan.cost = 1999 | ||||
| 		plan.billing_interval = 'Month' | ||||
| 		plan.billing_interval_count = 1 | ||||
| @ -35,12 +35,29 @@ def create_plan(): | ||||
| 		plan = frappe.new_doc('Subscription Plan') | ||||
| 		plan.plan_name = '_Test Plan Name 3' | ||||
| 		plan.item = '_Test Non Stock Item' | ||||
| 		plan.price_determination = "Fixed rate" | ||||
| 		plan.price_determination = "Fixed Rate" | ||||
| 		plan.cost = 1999 | ||||
| 		plan.billing_interval = 'Day' | ||||
| 		plan.billing_interval_count = 14 | ||||
| 		plan.insert() | ||||
| 
 | ||||
| 	# Defined a quarterly Subscription Plan | ||||
| 	if not frappe.db.exists('Subscription Plan', '_Test Plan Name 4'): | ||||
| 		plan = frappe.new_doc('Subscription Plan') | ||||
| 		plan.plan_name = '_Test Plan Name 4' | ||||
| 		plan.item = '_Test Non Stock Item' | ||||
| 		plan.price_determination = "Monthly Rate" | ||||
| 		plan.cost = 20000 | ||||
| 		plan.billing_interval = 'Month' | ||||
| 		plan.billing_interval_count = 3 | ||||
| 		plan.insert() | ||||
| 
 | ||||
| 	if not frappe.db.exists('Supplier', '_Test Supplier'): | ||||
| 		supplier = frappe.new_doc('Supplier') | ||||
| 		supplier.supplier_name = '_Test Supplier' | ||||
| 		supplier.supplier_group = 'All Supplier Groups' | ||||
| 		supplier.insert() | ||||
| 
 | ||||
| class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def setUp(self): | ||||
| @ -48,7 +65,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_create_subscription_with_trial_with_correct_period(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.trial_period_start = nowdate() | ||||
| 		subscription.trial_period_end = add_days(nowdate(), 30) | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| @ -56,8 +74,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 		self.assertEqual(subscription.trial_period_start, nowdate()) | ||||
| 		self.assertEqual(subscription.trial_period_end, add_days(nowdate(), 30)) | ||||
| 		self.assertEqual(subscription.trial_period_start, subscription.current_invoice_start) | ||||
| 		self.assertEqual(subscription.trial_period_end, subscription.current_invoice_end) | ||||
| 		self.assertEqual(add_days(subscription.trial_period_end, 1), get_date_str(subscription.current_invoice_start)) | ||||
| 		self.assertEqual(add_days(subscription.current_invoice_start, 30), get_date_str(subscription.current_invoice_end)) | ||||
| 		self.assertEqual(subscription.invoices, []) | ||||
| 		self.assertEqual(subscription.status, 'Trialling') | ||||
| 
 | ||||
| @ -65,7 +83,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_create_subscription_without_trial_with_correct_period(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 
 | ||||
| @ -81,7 +100,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_create_subscription_trial_with_wrong_dates(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.trial_period_end = nowdate() | ||||
| 		subscription.trial_period_start = add_days(nowdate(), 30) | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| @ -91,7 +111,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_create_subscription_multi_with_different_billing_fails(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.trial_period_end = nowdate() | ||||
| 		subscription.trial_period_start = add_days(nowdate(), 30) | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| @ -102,8 +123,9 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_invoice_is_generated_at_end_of_billing_period(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.start = '2018-01-01' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.start_date = '2018-01-01' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.insert() | ||||
| 
 | ||||
| @ -114,18 +136,22 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 		self.assertEqual(len(subscription.invoices), 1) | ||||
| 		self.assertEqual(subscription.current_invoice_start, '2018-01-01') | ||||
| 		self.assertEqual(subscription.status, 'Past Due Date') | ||||
| 		subscription.process() | ||||
| 		self.assertEqual(subscription.status, 'Unpaid') | ||||
| 		subscription.delete() | ||||
| 
 | ||||
| 	def test_status_goes_back_to_active_after_invoice_is_paid(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.start = '2018-01-01' | ||||
| 		subscription.start_date = '2018-01-01' | ||||
| 		subscription.insert() | ||||
| 		subscription.process()	# generate first invoice | ||||
| 		self.assertEqual(len(subscription.invoices), 1) | ||||
| 		self.assertEqual(subscription.status, 'Past Due Date') | ||||
| 
 | ||||
| 		# Status is unpaid as Days until Due is zero and grace period is Zero | ||||
| 		self.assertEqual(subscription.status, 'Unpaid') | ||||
| 
 | ||||
| 		subscription.get_current_invoice() | ||||
| 		current_invoice = subscription.get_current_invoice() | ||||
| @ -137,7 +163,7 @@ class TestSubscription(unittest.TestCase): | ||||
| 		subscription.process() | ||||
| 
 | ||||
| 		self.assertEqual(subscription.status, 'Active') | ||||
| 		self.assertEqual(subscription.current_invoice_start, add_months(subscription.start, 1)) | ||||
| 		self.assertEqual(subscription.current_invoice_start, add_months(subscription.start_date, 1)) | ||||
| 		self.assertEqual(len(subscription.invoices), 1) | ||||
| 
 | ||||
| 		subscription.delete() | ||||
| @ -149,16 +175,17 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.start = '2018-01-01' | ||||
| 		subscription.start_date = '2018-01-01' | ||||
| 		subscription.insert() | ||||
| 
 | ||||
| 		self.assertEqual(subscription.status, 'Active') | ||||
| 
 | ||||
| 		subscription.process()		# generate first invoice | ||||
| 
 | ||||
| 		self.assertEqual(subscription.status, 'Past Due Date') | ||||
| 
 | ||||
| 		subscription.process() | ||||
| 		# This should change status to Cancelled since grace period is 0 | ||||
| 		# And is backdated subscription so subscription will be cancelled after processing | ||||
| 		self.assertEqual(subscription.status, 'Cancelled') | ||||
| 
 | ||||
| 		settings.cancel_after_grace = default_grace_period_action | ||||
| @ -172,16 +199,14 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.start = '2018-01-01' | ||||
| 		subscription.start_date = '2018-01-01' | ||||
| 		subscription.insert() | ||||
| 		subscription.process()		# generate first invoice | ||||
| 
 | ||||
| 		self.assertEqual(subscription.status, 'Past Due Date') | ||||
| 
 | ||||
| 		subscription.process() | ||||
| 		# This should change status to Cancelled since grace period is 0 | ||||
| 		# Status is unpaid as Days until Due is zero and grace period is Zero | ||||
| 		self.assertEqual(subscription.status, 'Unpaid') | ||||
| 
 | ||||
| 		settings.cancel_after_grace = default_grace_period_action | ||||
| @ -190,10 +215,11 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_subscription_invoice_days_until_due(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.days_until_due = 10 | ||||
| 		subscription.start = add_months(nowdate(), -1) | ||||
| 		subscription.start_date = add_months(nowdate(), -1) | ||||
| 		subscription.insert() | ||||
| 		subscription.process()		# generate first invoice | ||||
| 		self.assertEqual(len(subscription.invoices), 1) | ||||
| @ -208,9 +234,10 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.start = '2018-01-01' | ||||
| 		subscription.start_date = '2018-01-01' | ||||
| 		subscription.insert() | ||||
| 		subscription.process()		# generate first invoice | ||||
| 
 | ||||
| @ -232,7 +259,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_subscription_remains_active_during_invoice_period(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 		subscription.process()		# no changes expected | ||||
| @ -258,7 +286,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_subscription_cancelation(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 		subscription.cancel_subscription() | ||||
| @ -274,7 +303,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 
 | ||||
| @ -309,7 +339,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 		subscription.cancel_subscription() | ||||
| @ -329,7 +360,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 		subscription.cancel_subscription() | ||||
| @ -353,16 +385,14 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.start = '2018-01-01' | ||||
| 		subscription.start_date = '2018-01-01' | ||||
| 		subscription.insert() | ||||
| 		subscription.process()	# generate first invoice | ||||
| 		invoices = len(subscription.invoices) | ||||
| 
 | ||||
| 		self.assertEqual(subscription.status, 'Past Due Date') | ||||
| 		self.assertEqual(len(subscription.invoices), invoices) | ||||
| 
 | ||||
| 		subscription.cancel_subscription() | ||||
| 		self.assertEqual(subscription.status, 'Cancelled') | ||||
| 		self.assertEqual(len(subscription.invoices), invoices) | ||||
| @ -387,15 +417,14 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.start = '2018-01-01' | ||||
| 		subscription.start_date = '2018-01-01' | ||||
| 		subscription.insert() | ||||
| 		subscription.process()		# generate first invoice | ||||
| 
 | ||||
| 		self.assertEqual(subscription.status, 'Past Due Date') | ||||
| 
 | ||||
| 		subscription.process() | ||||
| 		# Status is unpaid as Days until Due is zero and grace period is Zero | ||||
| 		self.assertEqual(subscription.status, 'Unpaid') | ||||
| 
 | ||||
| 		subscription.cancel_subscription() | ||||
| @ -424,16 +453,14 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.start = '2018-01-01' | ||||
| 		subscription.start_date = '2018-01-01' | ||||
| 		subscription.insert() | ||||
| 
 | ||||
| 		subscription.process()		# generate first invoice | ||||
| 
 | ||||
| 		self.assertEqual(subscription.status, 'Past Due Date') | ||||
| 
 | ||||
| 		subscription.process() | ||||
| 		# This should change status to Cancelled since grace period is 0 | ||||
| 		# This should change status to Unpaid since grace period is 0 | ||||
| 		self.assertEqual(subscription.status, 'Unpaid') | ||||
| 
 | ||||
| 		invoice = subscription.get_current_invoice() | ||||
| @ -445,7 +472,7 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 		# A new invoice is generated | ||||
| 		subscription.process() | ||||
| 		self.assertEqual(subscription.status, 'Past Due Date') | ||||
| 		self.assertEqual(subscription.status, 'Unpaid') | ||||
| 
 | ||||
| 		settings.cancel_after_grace = default_grace_period_action | ||||
| 		settings.save() | ||||
| @ -453,7 +480,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_restart_active_subscription(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 
 | ||||
| @ -463,7 +491,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_subscription_invoice_discount_percentage(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.additional_discount_percentage = 10 | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| @ -478,7 +507,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 
 | ||||
| 	def test_subscription_invoice_discount_amount(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.additional_discount_amount = 11 | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| @ -495,7 +525,8 @@ class TestSubscription(unittest.TestCase): | ||||
| 		# Create a non pre-billed subscription, processing should not create | ||||
| 		# invoices. | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 		subscription.process() | ||||
| @ -517,10 +548,12 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.customer = '_Test Customer' | ||||
| 		subscription.party_type = 'Customer' | ||||
| 		subscription.party = '_Test Customer' | ||||
| 		subscription.generate_invoice_at_period_start = True | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 		subscription.process() | ||||
| 		subscription.cancel_subscription() | ||||
| 
 | ||||
| 		self.assertEqual(len(subscription.invoices), 1) | ||||
| @ -538,3 +571,65 @@ class TestSubscription(unittest.TestCase): | ||||
| 		settings.save() | ||||
| 
 | ||||
| 		subscription.delete() | ||||
| 
 | ||||
| 	def test_subscription_with_follow_calendar_months(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.party_type = 'Supplier' | ||||
| 		subscription.party = '_Test Supplier' | ||||
| 		subscription.generate_invoice_at_period_start = 1 | ||||
| 		subscription.follow_calendar_months = 1 | ||||
| 
 | ||||
| 		# select subscription start date as '2018-01-15' | ||||
| 		subscription.start_date = '2018-01-15' | ||||
| 		subscription.end_date = '2018-07-15' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 
 | ||||
| 		# even though subscription starts at '2018-01-15' and Billing interval is Month and count 3 | ||||
| 		# First invoice will end at '2018-03-31' instead of '2018-04-14' | ||||
| 		self.assertEqual(get_date_str(subscription.current_invoice_end), '2018-03-31') | ||||
| 
 | ||||
| 	def test_subscription_generate_invoice_past_due(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.party_type = 'Supplier' | ||||
| 		subscription.party = '_Test Supplier' | ||||
| 		subscription.generate_invoice_at_period_start = 1 | ||||
| 		subscription.generate_new_invoices_past_due_date = 1 | ||||
| 		# select subscription start date as '2018-01-15' | ||||
| 		subscription.start_date = '2018-01-01' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 
 | ||||
| 		# Process subscription and create first invoice | ||||
| 		# Subscription status will be unpaid since due date has already passed | ||||
| 		subscription.process() | ||||
| 		self.assertEqual(len(subscription.invoices), 1) | ||||
| 		self.assertEqual(subscription.status, 'Unpaid') | ||||
| 
 | ||||
| 		# Now the Subscription is unpaid | ||||
| 		# Even then new invoice should be created as we have enabled `generate_new_invoices_past_due_date` in | ||||
| 		# subscription | ||||
| 
 | ||||
| 		subscription.process() | ||||
| 		self.assertEqual(len(subscription.invoices), 2) | ||||
| 
 | ||||
| 	def test_subscription_without_generate_invoice_past_due(self): | ||||
| 		subscription = frappe.new_doc('Subscription') | ||||
| 		subscription.party_type = 'Supplier' | ||||
| 		subscription.party = '_Test Supplier' | ||||
| 		subscription.generate_invoice_at_period_start = 1 | ||||
| 		# select subscription start date as '2018-01-15' | ||||
| 		subscription.start_date = '2018-01-01' | ||||
| 		subscription.append('plans', {'plan': '_Test Plan Name 4', 'qty': 1}) | ||||
| 		subscription.save() | ||||
| 
 | ||||
| 		# Process subscription and create first invoice | ||||
| 		# Subscription status will be unpaid since due date has already passed | ||||
| 		subscription.process() | ||||
| 		self.assertEqual(len(subscription.invoices), 1) | ||||
| 		self.assertEqual(subscription.status, 'Unpaid') | ||||
| 
 | ||||
| 		subscription.process() | ||||
| 		self.assertEqual(len(subscription.invoices), 1) | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -1,73 +1,40 @@ | ||||
| { | ||||
|  "allow_copy": 0,  | ||||
|  "allow_guest_to_view": 0,  | ||||
|  "allow_import": 0,  | ||||
|  "allow_rename": 0,  | ||||
|  "beta": 0,  | ||||
|  "creation": "2018-02-26 04:21:41.265055",  | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "",  | ||||
|  "editable_grid": 1,  | ||||
|  "engine": "InnoDB",  | ||||
|  "actions": [], | ||||
|  "creation": "2018-02-26 04:21:41.265055", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "document_type", | ||||
|   "invoice" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "invoice",  | ||||
|    "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": "Invoice",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Sales Invoice",  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 1,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0 | ||||
|    "fieldname": "document_type", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Document Type ", | ||||
|    "options": "DocType", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "invoice", | ||||
|    "fieldtype": "Dynamic Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Invoice", | ||||
|    "options": "document_type", | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ],  | ||||
|  "has_web_view": 0,  | ||||
|  "hide_heading": 0,  | ||||
|  "hide_toolbar": 0,  | ||||
|  "idx": 0,  | ||||
|  "image_view": 0,  | ||||
|  "in_create": 0,  | ||||
|  "is_submittable": 0,  | ||||
|  "issingle": 0,  | ||||
|  "istable": 1,  | ||||
|  "max_attachments": 0,  | ||||
|  "modified": "2018-02-26 10:48:07.033422",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Accounts",  | ||||
|  "name": "Subscription Invoice",  | ||||
|  "name_case": "",  | ||||
|  "owner": "Administrator",  | ||||
|  "permissions": [],  | ||||
|  "quick_entry": 1,  | ||||
|  "read_only": 0,  | ||||
|  "read_only_onload": 0,  | ||||
|  "show_name_in_global_search": 0,  | ||||
|  "sort_field": "modified",  | ||||
|  "sort_order": "DESC",  | ||||
|  "track_changes": 1,  | ||||
|  "track_seen": 0 | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-06-01 22:23:54.462718", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Subscription Invoice", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -1,4 +1,5 @@ | ||||
| { | ||||
|  "actions": [], | ||||
|  "allow_rename": 1, | ||||
|  "autoname": "field:plan_name", | ||||
|  "creation": "2018-02-24 11:31:23.066506", | ||||
| @ -24,6 +25,7 @@ | ||||
|   "column_break_16", | ||||
|   "payment_gateway", | ||||
|   "accounting_dimensions_section", | ||||
|   "cost_center", | ||||
|   "dimension_col_break" | ||||
|  ], | ||||
|  "fields": [ | ||||
| @ -60,8 +62,8 @@ | ||||
|   { | ||||
|    "fieldname": "price_determination", | ||||
|    "fieldtype": "Select", | ||||
|    "label": "Price Determination", | ||||
|    "options": "\nFixed rate\nBased on price list", | ||||
|    "label": "Subscription Price Based On", | ||||
|    "options": "\nFixed Rate\nBased On Price List\nMonthly Rate", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
| @ -69,7 +71,7 @@ | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "depends_on": "eval:doc.price_determination==\"Fixed rate\"", | ||||
|    "depends_on": "eval:['Fixed Rate', 'Monthly Rate'].includes(doc.price_determination)", | ||||
|    "fieldname": "cost", | ||||
|    "fieldtype": "Currency", | ||||
|    "in_list_view": 1, | ||||
| @ -136,9 +138,16 @@ | ||||
|   { | ||||
|    "fieldname": "dimension_col_break", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "cost_center", | ||||
|    "fieldtype": "Link", | ||||
|    "label": "Cost Center", | ||||
|    "options": "Cost Center" | ||||
|   } | ||||
|  ], | ||||
|  "modified": "2019-07-25 18:35:04.362556", | ||||
|  "links": [], | ||||
|  "modified": "2020-06-25 10:53:44.205774", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Subscription Plan", | ||||
| @ -155,6 +164,30 @@ | ||||
|    "role": "System Manager", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Accounts Manager", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "export": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "report": 1, | ||||
|    "role": "Accounts User", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   } | ||||
|  ], | ||||
|  "sort_field": "modified", | ||||
|  | ||||
| @ -5,6 +5,7 @@ | ||||
| from __future__ import unicode_literals | ||||
| import frappe | ||||
| from frappe import _ | ||||
| from frappe.utils import get_first_day, get_last_day, date_diff, flt, getdate | ||||
| from frappe.model.document import Document | ||||
| from erpnext.utilities.product import get_price | ||||
| 
 | ||||
| @ -17,12 +18,12 @@ class SubscriptionPlan(Document): | ||||
| 			frappe.throw(_('Billing Interval Count cannot be less than 1')) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_plan_rate(plan, quantity=1, customer=None): | ||||
| def get_plan_rate(plan, quantity=1, customer=None, start_date=None, end_date=None, prorate_factor=1): | ||||
| 	plan = frappe.get_doc("Subscription Plan", plan) | ||||
| 	if plan.price_determination == "Fixed rate": | ||||
| 		return plan.cost | ||||
| 	if plan.price_determination == "Fixed Rate": | ||||
| 		return plan.cost * prorate_factor | ||||
| 
 | ||||
| 	elif plan.price_determination == "Based on price list": | ||||
| 	elif plan.price_determination == "Based On Price List": | ||||
| 		if customer: | ||||
| 			customer_group = frappe.db.get_value("Customer", customer, "customer_group") | ||||
| 		else: | ||||
| @ -32,4 +33,25 @@ def get_plan_rate(plan, quantity=1, customer=None): | ||||
| 		if not price: | ||||
| 			return 0 | ||||
| 		else: | ||||
| 			return price.price_list_rate | ||||
| 			return price.price_list_rate * prorate_factor | ||||
| 
 | ||||
| 	elif plan.price_determination == 'Monthly Rate': | ||||
| 		start_date = getdate(start_date) | ||||
| 		end_date = getdate(end_date) | ||||
| 
 | ||||
| 		no_of_months = (end_date.year - start_date.year) * 12 + (end_date.month - start_date.month) + 1 | ||||
| 		cost = plan.cost * no_of_months | ||||
| 
 | ||||
| 		# Adjust cost if start or end date is not month start or end | ||||
| 		prorate = frappe.db.get_single_value('Subscription Settings', 'prorate') | ||||
| 
 | ||||
| 		if prorate: | ||||
| 			prorate_factor = flt(date_diff(start_date, get_first_day(start_date)) / date_diff( | ||||
| 				get_last_day(start_date), get_first_day(start_date)), 1) | ||||
| 
 | ||||
| 			prorate_factor += flt(date_diff(get_last_day(end_date), end_date) / date_diff( | ||||
| 				get_last_day(end_date), get_first_day(end_date)), 1) | ||||
| 
 | ||||
| 			cost -= (plan.cost * prorate_factor) | ||||
| 
 | ||||
| 		return cost | ||||
| @ -1,106 +1,40 @@ | ||||
| { | ||||
|  "allow_copy": 0,  | ||||
|  "allow_guest_to_view": 0,  | ||||
|  "allow_import": 0,  | ||||
|  "allow_rename": 0,  | ||||
|  "beta": 0,  | ||||
|  "creation": "2018-02-25 07:35:07.736146",  | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "",  | ||||
|  "editable_grid": 1,  | ||||
|  "engine": "InnoDB",  | ||||
|  "actions": [], | ||||
|  "creation": "2018-02-25 07:35:07.736146", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "plan", | ||||
|   "qty" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "qty",  | ||||
|    "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": "Quantity",  | ||||
|    "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": "qty", | ||||
|    "fieldtype": "Int", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Quantity", | ||||
|    "reqd": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_in_quick_entry": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "fieldname": "plan",  | ||||
|    "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": "Plan",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "options": "Subscription Plan",  | ||||
|    "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": "plan", | ||||
|    "fieldtype": "Link", | ||||
|    "in_list_view": 1, | ||||
|    "label": "Plan", | ||||
|    "options": "Subscription Plan", | ||||
|    "reqd": 1 | ||||
|   } | ||||
|  ],  | ||||
|  "has_web_view": 0,  | ||||
|  "hide_heading": 0,  | ||||
|  "hide_toolbar": 0,  | ||||
|  "idx": 0,  | ||||
|  "image_view": 0,  | ||||
|  "in_create": 0,  | ||||
|  "is_submittable": 0,  | ||||
|  "issingle": 0,  | ||||
|  "istable": 1,  | ||||
|  "max_attachments": 0,  | ||||
|  "modified": "2018-06-20 15:35:13.514699",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Accounts",  | ||||
|  "name": "Subscription Plan Detail",  | ||||
|  "name_case": "",  | ||||
|  "owner": "Administrator",  | ||||
|  "permissions": [],  | ||||
|  "quick_entry": 1,  | ||||
|  "read_only": 0,  | ||||
|  "read_only_onload": 0,  | ||||
|  "show_name_in_global_search": 0,  | ||||
|  "sort_field": "modified",  | ||||
|  "sort_order": "DESC",  | ||||
|  "track_changes": 1,  | ||||
|  "track_seen": 0 | ||||
|  ], | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-06-14 17:44:05.275100", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Subscription Plan Detail", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -1,179 +1,76 @@ | ||||
| { | ||||
|  "allow_copy": 0,  | ||||
|  "allow_guest_to_view": 0,  | ||||
|  "allow_import": 0,  | ||||
|  "allow_rename": 0,  | ||||
|  "beta": 0,  | ||||
|  "creation": "2018-02-26 06:13:37.910139",  | ||||
|  "custom": 0,  | ||||
|  "docstatus": 0,  | ||||
|  "doctype": "DocType",  | ||||
|  "document_type": "",  | ||||
|  "editable_grid": 1,  | ||||
|  "engine": "InnoDB",  | ||||
|  "actions": [], | ||||
|  "creation": "2018-02-26 06:13:37.910139", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
|  "field_order": [ | ||||
|   "grace_period", | ||||
|   "cancel_after_grace", | ||||
|   "prorate" | ||||
|  ], | ||||
|  "fields": [ | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "default": "1",  | ||||
|    "description": "Number of days after invoice date has elapsed before canceling subscription or marking subscription as unpaid",  | ||||
|    "fieldname": "grace_period",  | ||||
|    "fieldtype": "Int",  | ||||
|    "hidden": 0,  | ||||
|    "ignore_user_permissions": 0,  | ||||
|    "ignore_xss_filter": 0,  | ||||
|    "in_filter": 0,  | ||||
|    "in_global_search": 0,  | ||||
|    "in_list_view": 0,  | ||||
|    "in_standard_filter": 0,  | ||||
|    "label": "Grace Period",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "default": "1", | ||||
|    "description": "Number of days after invoice date has elapsed before canceling subscription or marking subscription as unpaid", | ||||
|    "fieldname": "grace_period", | ||||
|    "fieldtype": "Int", | ||||
|    "label": "Grace Period" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "default": "0",  | ||||
|    "fieldname": "cancel_after_grace",  | ||||
|    "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": "Cancel Invoice After Grace Period",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0 | ||||
|   },  | ||||
|    "default": "0", | ||||
|    "fieldname": "cancel_after_grace", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Cancel Subscription After Grace Period" | ||||
|   }, | ||||
|   { | ||||
|    "allow_bulk_edit": 0,  | ||||
|    "allow_on_submit": 0,  | ||||
|    "bold": 0,  | ||||
|    "collapsible": 0,  | ||||
|    "columns": 0,  | ||||
|    "default": "1",  | ||||
|    "fieldname": "prorate",  | ||||
|    "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": "Prorate",  | ||||
|    "length": 0,  | ||||
|    "no_copy": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "precision": "",  | ||||
|    "print_hide": 0,  | ||||
|    "print_hide_if_no_value": 0,  | ||||
|    "read_only": 0,  | ||||
|    "remember_last_selected_value": 0,  | ||||
|    "report_hide": 0,  | ||||
|    "reqd": 0,  | ||||
|    "search_index": 0,  | ||||
|    "set_only_once": 0,  | ||||
|    "translatable": 0,  | ||||
|    "unique": 0 | ||||
|    "default": "1", | ||||
|    "fieldname": "prorate", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Prorate" | ||||
|   } | ||||
|  ],  | ||||
|  "has_web_view": 0,  | ||||
|  "hide_heading": 0,  | ||||
|  "hide_toolbar": 0,  | ||||
|  "idx": 0,  | ||||
|  "image_view": 0,  | ||||
|  "in_create": 0,  | ||||
|  "is_submittable": 0,  | ||||
|  "issingle": 1,  | ||||
|  "istable": 0,  | ||||
|  "max_attachments": 0,  | ||||
|  "modified": "2018-02-26 13:58:09.455832",  | ||||
|  "modified_by": "Administrator",  | ||||
|  "module": "Accounts",  | ||||
|  "name": "Subscription Settings",  | ||||
|  "name_case": "",  | ||||
|  "owner": "Administrator",  | ||||
|  ], | ||||
|  "issingle": 1, | ||||
|  "links": [], | ||||
|  "modified": "2020-06-23 09:13:44.292792", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Subscription Settings", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|    "amend": 0,  | ||||
|    "apply_user_permissions": 0,  | ||||
|    "cancel": 0,  | ||||
|    "create": 1,  | ||||
|    "delete": 1,  | ||||
|    "email": 1,  | ||||
|    "export": 0,  | ||||
|    "if_owner": 0,  | ||||
|    "import": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print": 1,  | ||||
|    "read": 1,  | ||||
|    "report": 0,  | ||||
|    "role": "System Manager",  | ||||
|    "set_user_permissions": 0,  | ||||
|    "share": 1,  | ||||
|    "submit": 0,  | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "role": "System Manager", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   },  | ||||
|   }, | ||||
|   { | ||||
|    "amend": 0,  | ||||
|    "apply_user_permissions": 0,  | ||||
|    "cancel": 0,  | ||||
|    "create": 1,  | ||||
|    "delete": 1,  | ||||
|    "email": 1,  | ||||
|    "export": 0,  | ||||
|    "if_owner": 0,  | ||||
|    "import": 0,  | ||||
|    "permlevel": 0,  | ||||
|    "print": 1,  | ||||
|    "read": 1,  | ||||
|    "report": 0,  | ||||
|    "role": "Administrator",  | ||||
|    "set_user_permissions": 0,  | ||||
|    "share": 1,  | ||||
|    "submit": 0,  | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "role": "Accounts Manager", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   }, | ||||
|   { | ||||
|    "create": 1, | ||||
|    "delete": 1, | ||||
|    "email": 1, | ||||
|    "print": 1, | ||||
|    "read": 1, | ||||
|    "role": "Accounts User", | ||||
|    "share": 1, | ||||
|    "write": 1 | ||||
|   } | ||||
|  ],  | ||||
|  "quick_entry": 1,  | ||||
|  "read_only": 0,  | ||||
|  "read_only_onload": 0,  | ||||
|  "show_name_in_global_search": 0,  | ||||
|  "sort_field": "modified",  | ||||
|  "sort_order": "DESC",  | ||||
|  "track_changes": 1,  | ||||
|  "track_seen": 0 | ||||
|  ], | ||||
|  "quick_entry": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "track_changes": 1 | ||||
| } | ||||
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