Merge branch 'develop' into dimension-wise-accounts-balance-reports
This commit is contained in:
		
						commit
						82ebc47ba1
					
				| @ -5,7 +5,7 @@ import frappe | |||||||
| from erpnext.hooks import regional_overrides | from erpnext.hooks import regional_overrides | ||||||
| from frappe.utils import getdate | from frappe.utils import getdate | ||||||
| 
 | 
 | ||||||
| __version__ = '13.0.0-dev' | __version__ = '13.0.1' | ||||||
| 
 | 
 | ||||||
| def get_default_company(user=None): | def get_default_company(user=None): | ||||||
| 	'''Get default company for user''' | 	'''Get default company for user''' | ||||||
|  | |||||||
| @ -42,10 +42,9 @@ let add_fields_to_mapping_table = function (frm) { | |||||||
| 		}); | 		}); | ||||||
| 	}); | 	}); | ||||||
| 
 | 
 | ||||||
| 	frappe.meta.get_docfield("Bank Transaction Mapping", "bank_transaction_field", | 	frm.fields_dict.bank_transaction_mapping.grid.update_docfield_property( | ||||||
| 		frm.doc.name).options = options; | 		'bank_transaction_field', 'options', options | ||||||
| 
 | 	); | ||||||
| 	frm.fields_dict.bank_transaction_mapping.grid.refresh(); |  | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
| erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { | erpnext.integrations.refreshPlaidLink = class refreshPlaidLink { | ||||||
|  | |||||||
| @ -327,18 +327,16 @@ erpnext.accounts.JournalEntry = frappe.ui.form.Controller.extend({ | |||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	setup_balance_formatter: function() { | 	setup_balance_formatter: function() { | ||||||
| 		var me = this; | 		const formatter = function(value, df, options, doc) { | ||||||
| 		$.each(["balance", "party_balance"], function(i, field) { |  | ||||||
| 			var df = frappe.meta.get_docfield("Journal Entry Account", field, me.frm.doc.name); |  | ||||||
| 			df.formatter = function(value, df, options, doc) { |  | ||||||
| 			var currency = frappe.meta.get_field_currency(df, doc); | 			var currency = frappe.meta.get_field_currency(df, doc); | ||||||
| 			var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : ""; | 			var dr_or_cr = value ? ('<label>' + (value > 0.0 ? __("Dr") : __("Cr")) + '</label>') : ""; | ||||||
| 			return "<div style='text-align: right'>" | 			return "<div style='text-align: right'>" | ||||||
| 				+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency)) | 				+ ((value==null || value==="") ? "" : format_currency(Math.abs(value), currency)) | ||||||
| 				+ " " + dr_or_cr | 				+ " " + dr_or_cr | ||||||
| 				+ "</div>"; | 				+ "</div>"; | ||||||
| 			} | 		}; | ||||||
| 		}) | 		this.frm.fields_dict.accounts.grid.update_docfield_property('balance', 'formatter', formatter); | ||||||
|  | 		this.frm.fields_dict.accounts.grid.update_docfield_property('party_balance', 'formatter', formatter); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	reference_name: function(doc, cdt, cdn) { | 	reference_name: function(doc, cdt, cdn) { | ||||||
| @ -431,15 +429,6 @@ cur_frm.cscript.validate = function(doc,cdt,cdn) { | |||||||
| 	cur_frm.cscript.update_totals(doc); | 	cur_frm.cscript.update_totals(doc); | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){ |  | ||||||
| 	if(doc.select_print_heading){ |  | ||||||
| 		// print heading
 |  | ||||||
| 		cur_frm.pformat.print_heading = doc.select_print_heading; |  | ||||||
| 	} |  | ||||||
| 	else |  | ||||||
| 		cur_frm.pformat.print_heading = __("Journal Entry"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| frappe.ui.form.on("Journal Entry Account", { | frappe.ui.form.on("Journal Entry Account", { | ||||||
| 	party: function(frm, cdt, cdn) { | 	party: function(frm, cdt, cdn) { | ||||||
| 		var d = frappe.get_doc(cdt, cdn); | 		var d = frappe.get_doc(cdt, cdn); | ||||||
| @ -511,8 +500,11 @@ $.extend(erpnext.journal_entry, { | |||||||
| 		}; | 		}; | ||||||
| 
 | 
 | ||||||
| 		$.each(field_label_map, function (fieldname, label) { | 		$.each(field_label_map, function (fieldname, label) { | ||||||
| 			var df = frappe.meta.get_docfield("Journal Entry Account", fieldname, frm.doc.name); | 			frm.fields_dict.accounts.grid.update_docfield_property( | ||||||
| 			df.label = frm.doc.multi_currency ? (label + " in Account Currency") : label; | 				fieldname, | ||||||
|  | 				'label', | ||||||
|  | 				frm.doc.multi_currency ? (label + " in Account Currency") : label | ||||||
|  | 			); | ||||||
| 		}) | 		}) | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -280,7 +280,7 @@ | |||||||
|  "idx": 1, |  "idx": 1, | ||||||
|  "istable": 1, |  "istable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2020-06-24 14:06:54.833738", |  "modified": "2020-06-26 14:06:54.833738", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Accounts", |  "module": "Accounts", | ||||||
|  "name": "Journal Entry Account", |  "name": "Journal Entry Account", | ||||||
|  | |||||||
| @ -234,8 +234,9 @@ erpnext.accounts.PaymentReconciliationController = frappe.ui.form.Controller.ext | |||||||
| 		}); | 		}); | ||||||
| 
 | 
 | ||||||
| 		if (invoices) { | 		if (invoices) { | ||||||
| 			frappe.meta.get_docfield("Payment Reconciliation Payment", "invoice_number", | 			this.frm.fields_dict.payment.grid.update_docfield_property( | ||||||
| 				me.frm.doc.name).options = "\n" + invoices.join("\n"); | 				'invoice_number', 'options', "\n" + invoices.join("\n") | ||||||
|  | 			); | ||||||
| 
 | 
 | ||||||
| 			$.each(me.frm.doc.payments || [], function(i, p) { | 			$.each(me.frm.doc.payments || [], function(i, p) { | ||||||
| 				if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null; | 				if(!in_list(invoices, cstr(p.invoice_number))) p.invoice_number = null; | ||||||
|  | |||||||
| @ -108,7 +108,6 @@ class POSInvoice(SalesInvoice): | |||||||
| 				filters = { "item_code": d.item_code, "warehouse": d.warehouse } | 				filters = { "item_code": d.item_code, "warehouse": d.warehouse } | ||||||
| 				if d.batch_no: | 				if d.batch_no: | ||||||
| 					filters["batch_no"] = d.batch_no | 					filters["batch_no"] = d.batch_no | ||||||
| 
 |  | ||||||
| 				reserved_serial_nos = get_pos_reserved_serial_nos(filters) | 				reserved_serial_nos = get_pos_reserved_serial_nos(filters) | ||||||
| 				serial_nos = get_serial_nos(d.serial_no) | 				serial_nos = get_serial_nos(d.serial_no) | ||||||
| 				invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos] | 				invalid_serial_nos = [s for s in serial_nos if s in reserved_serial_nos] | ||||||
|  | |||||||
| @ -12,6 +12,7 @@ from frappe.utils.background_jobs import enqueue | |||||||
| from frappe.model.mapper import map_doc, map_child_doc | from frappe.model.mapper import map_doc, map_child_doc | ||||||
| from frappe.utils.scheduler import is_scheduler_inactive | from frappe.utils.scheduler import is_scheduler_inactive | ||||||
| from frappe.core.page.background_jobs.background_jobs import get_info | from frappe.core.page.background_jobs.background_jobs import get_info | ||||||
|  | import json | ||||||
| 
 | 
 | ||||||
| from six import iteritems | from six import iteritems | ||||||
| 
 | 
 | ||||||
| @ -78,8 +79,11 @@ class POSInvoiceMergeLog(Document): | |||||||
| 		sales_invoice = self.merge_pos_invoice_into(sales_invoice, data) | 		sales_invoice = self.merge_pos_invoice_into(sales_invoice, data) | ||||||
| 
 | 
 | ||||||
| 		sales_invoice.is_consolidated = 1 | 		sales_invoice.is_consolidated = 1 | ||||||
|  | 		sales_invoice.set_posting_time = 1 | ||||||
|  | 		sales_invoice.posting_date = getdate(self.posting_date) | ||||||
| 		sales_invoice.save() | 		sales_invoice.save() | ||||||
| 		sales_invoice.submit() | 		sales_invoice.submit() | ||||||
|  | 
 | ||||||
| 		self.consolidated_invoice = sales_invoice.name | 		self.consolidated_invoice = sales_invoice.name | ||||||
| 
 | 
 | ||||||
| 		return sales_invoice.name | 		return sales_invoice.name | ||||||
| @ -91,10 +95,13 @@ class POSInvoiceMergeLog(Document): | |||||||
| 		credit_note = self.merge_pos_invoice_into(credit_note, data) | 		credit_note = self.merge_pos_invoice_into(credit_note, data) | ||||||
| 
 | 
 | ||||||
| 		credit_note.is_consolidated = 1 | 		credit_note.is_consolidated = 1 | ||||||
|  | 		credit_note.set_posting_time = 1 | ||||||
|  | 		credit_note.posting_date = getdate(self.posting_date) | ||||||
| 		# TODO: return could be against multiple sales invoice which could also have been consolidated? | 		# TODO: return could be against multiple sales invoice which could also have been consolidated? | ||||||
| 		# credit_note.return_against = self.consolidated_invoice | 		# credit_note.return_against = self.consolidated_invoice | ||||||
| 		credit_note.save() | 		credit_note.save() | ||||||
| 		credit_note.submit() | 		credit_note.submit() | ||||||
|  | 
 | ||||||
| 		self.consolidated_credit_note = credit_note.name | 		self.consolidated_credit_note = credit_note.name | ||||||
| 
 | 
 | ||||||
| 		return credit_note.name | 		return credit_note.name | ||||||
| @ -131,12 +138,14 @@ class POSInvoiceMergeLog(Document): | |||||||
| 					if t.account_head == tax.account_head and t.cost_center == tax.cost_center: | 					if t.account_head == tax.account_head and t.cost_center == tax.cost_center: | ||||||
| 						t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount) | 						t.tax_amount = flt(t.tax_amount) + flt(tax.tax_amount_after_discount_amount) | ||||||
| 						t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount) | 						t.base_tax_amount = flt(t.base_tax_amount) + flt(tax.base_tax_amount_after_discount_amount) | ||||||
|  | 						update_item_wise_tax_detail(t, tax) | ||||||
| 						found = True | 						found = True | ||||||
| 				if not found: | 				if not found: | ||||||
| 					tax.charge_type = 'Actual' | 					tax.charge_type = 'Actual' | ||||||
| 					tax.included_in_print_rate = 0 | 					tax.included_in_print_rate = 0 | ||||||
| 					tax.tax_amount = tax.tax_amount_after_discount_amount | 					tax.tax_amount = tax.tax_amount_after_discount_amount | ||||||
| 					tax.base_tax_amount = tax.base_tax_amount_after_discount_amount | 					tax.base_tax_amount = tax.base_tax_amount_after_discount_amount | ||||||
|  | 					tax.item_wise_tax_detail = tax.item_wise_tax_detail | ||||||
| 					taxes.append(tax) | 					taxes.append(tax) | ||||||
| 
 | 
 | ||||||
| 			for payment in doc.get('payments'): | 			for payment in doc.get('payments'): | ||||||
| @ -168,8 +177,6 @@ class POSInvoiceMergeLog(Document): | |||||||
| 		sales_invoice = frappe.new_doc('Sales Invoice') | 		sales_invoice = frappe.new_doc('Sales Invoice') | ||||||
| 		sales_invoice.customer = self.customer | 		sales_invoice.customer = self.customer | ||||||
| 		sales_invoice.is_pos = 1 | 		sales_invoice.is_pos = 1 | ||||||
| 		# date can be pos closing date? |  | ||||||
| 		sales_invoice.posting_date = getdate(nowdate()) |  | ||||||
| 
 | 
 | ||||||
| 		return sales_invoice | 		return sales_invoice | ||||||
| 
 | 
 | ||||||
| @ -187,6 +194,26 @@ class POSInvoiceMergeLog(Document): | |||||||
| 			si.flags.ignore_validate = True | 			si.flags.ignore_validate = True | ||||||
| 			si.cancel() | 			si.cancel() | ||||||
| 
 | 
 | ||||||
|  | def update_item_wise_tax_detail(consolidate_tax_row, tax_row): | ||||||
|  | 	consolidated_tax_detail = json.loads(consolidate_tax_row.item_wise_tax_detail) | ||||||
|  | 	tax_row_detail = json.loads(tax_row.item_wise_tax_detail) | ||||||
|  | 
 | ||||||
|  | 	if not consolidated_tax_detail: | ||||||
|  | 		consolidated_tax_detail = {} | ||||||
|  | 
 | ||||||
|  | 	for item_code, tax_data in tax_row_detail.items(): | ||||||
|  | 		if consolidated_tax_detail.get(item_code): | ||||||
|  | 			consolidated_tax_data = consolidated_tax_detail.get(item_code) | ||||||
|  | 			consolidated_tax_detail.update({ | ||||||
|  | 				item_code: [consolidated_tax_data[0], consolidated_tax_data[1] + tax_data[1]] | ||||||
|  | 			}) | ||||||
|  | 		else: | ||||||
|  | 			consolidated_tax_detail.update({ | ||||||
|  | 				item_code: [tax_data[0], tax_data[1]] | ||||||
|  | 			}) | ||||||
|  | 
 | ||||||
|  | 	consolidate_tax_row.item_wise_tax_detail = json.dumps(consolidated_tax_detail, separators=(',', ':')) | ||||||
|  | 
 | ||||||
| def get_all_unconsolidated_invoices(): | def get_all_unconsolidated_invoices(): | ||||||
| 	filters = { | 	filters = { | ||||||
| 		'consolidated_invoice': [ 'in', [ '', None ]], | 		'consolidated_invoice': [ 'in', [ '', None ]], | ||||||
| @ -214,7 +241,7 @@ def consolidate_pos_invoices(pos_invoices=[], closing_entry={}): | |||||||
| 
 | 
 | ||||||
| 	if len(invoices) >= 5 and closing_entry: | 	if len(invoices) >= 5 and closing_entry: | ||||||
| 		closing_entry.set_status(update=True, status='Queued') | 		closing_entry.set_status(update=True, status='Queued') | ||||||
| 		enqueue_job(create_merge_logs, invoice_by_customer, closing_entry) | 		enqueue_job(create_merge_logs, invoice_by_customer=invoice_by_customer, closing_entry=closing_entry) | ||||||
| 	else: | 	else: | ||||||
| 		create_merge_logs(invoice_by_customer, closing_entry) | 		create_merge_logs(invoice_by_customer, closing_entry) | ||||||
| 
 | 
 | ||||||
| @ -227,14 +254,14 @@ def unconsolidate_pos_invoices(closing_entry): | |||||||
| 
 | 
 | ||||||
| 	if len(merge_logs) >= 5: | 	if len(merge_logs) >= 5: | ||||||
| 		closing_entry.set_status(update=True, status='Queued') | 		closing_entry.set_status(update=True, status='Queued') | ||||||
| 		enqueue_job(cancel_merge_logs, merge_logs, closing_entry) | 		enqueue_job(cancel_merge_logs, merge_logs=merge_logs, closing_entry=closing_entry) | ||||||
| 	else: | 	else: | ||||||
| 		cancel_merge_logs(merge_logs, closing_entry) | 		cancel_merge_logs(merge_logs, closing_entry) | ||||||
| 
 | 
 | ||||||
| def create_merge_logs(invoice_by_customer, closing_entry={}): | def create_merge_logs(invoice_by_customer, closing_entry={}): | ||||||
| 	for customer, invoices in iteritems(invoice_by_customer): | 	for customer, invoices in iteritems(invoice_by_customer): | ||||||
| 		merge_log = frappe.new_doc('POS Invoice Merge Log') | 		merge_log = frappe.new_doc('POS Invoice Merge Log') | ||||||
| 		merge_log.posting_date = getdate(nowdate()) | 		merge_log.posting_date = getdate(closing_entry.get('posting_date')) | ||||||
| 		merge_log.customer = customer | 		merge_log.customer = customer | ||||||
| 		merge_log.pos_closing_entry = closing_entry.get('name', None) | 		merge_log.pos_closing_entry = closing_entry.get('name', None) | ||||||
| 
 | 
 | ||||||
| @ -256,7 +283,7 @@ def cancel_merge_logs(merge_logs, closing_entry={}): | |||||||
| 		closing_entry.set_status(update=True, status='Cancelled') | 		closing_entry.set_status(update=True, status='Cancelled') | ||||||
| 		closing_entry.update_opening_entry(for_cancel=True) | 		closing_entry.update_opening_entry(for_cancel=True) | ||||||
| 
 | 
 | ||||||
| def enqueue_job(job, invoice_by_customer, closing_entry): | def enqueue_job(job, merge_logs=None, invoice_by_customer=None, closing_entry=None): | ||||||
| 	check_scheduler_status() | 	check_scheduler_status() | ||||||
| 
 | 
 | ||||||
| 	job_name = closing_entry.get("name") | 	job_name = closing_entry.get("name") | ||||||
| @ -269,6 +296,7 @@ def enqueue_job(job, invoice_by_customer, closing_entry): | |||||||
| 			job_name=job_name, | 			job_name=job_name, | ||||||
| 			closing_entry=closing_entry, | 			closing_entry=closing_entry, | ||||||
| 			invoice_by_customer=invoice_by_customer, | 			invoice_by_customer=invoice_by_customer, | ||||||
|  | 			merge_logs=merge_logs, | ||||||
| 			now=frappe.conf.developer_mode or frappe.flags.in_test | 			now=frappe.conf.developer_mode or frappe.flags.in_test | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ from __future__ import unicode_literals | |||||||
| 
 | 
 | ||||||
| import frappe | import frappe | ||||||
| import unittest | import unittest | ||||||
|  | import json | ||||||
| from erpnext.accounts.doctype.pos_invoice.test_pos_invoice import create_pos_invoice | 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.pos_invoice import make_sales_return | ||||||
| from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices | from erpnext.accounts.doctype.pos_invoice_merge_log.pos_invoice_merge_log import consolidate_pos_invoices | ||||||
| @ -99,4 +100,51 @@ class TestPOSInvoiceMergeLog(unittest.TestCase): | |||||||
| 			frappe.db.sql("delete from `tabPOS Profile`") | 			frappe.db.sql("delete from `tabPOS Profile`") | ||||||
| 			frappe.db.sql("delete from `tabPOS Invoice`") | 			frappe.db.sql("delete from `tabPOS Invoice`") | ||||||
| 
 | 
 | ||||||
|  | 	def test_consolidated_invoice_item_taxes(self): | ||||||
|  | 		frappe.db.sql("delete from `tabPOS Invoice`") | ||||||
|  | 
 | ||||||
|  | 		try: | ||||||
|  | 			inv = create_pos_invoice(qty=1, rate=100, do_not_save=True) | ||||||
|  | 
 | ||||||
|  | 			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": 9 | ||||||
|  | 			}) | ||||||
|  | 			inv.insert() | ||||||
|  | 			inv.submit() | ||||||
|  | 
 | ||||||
|  | 			inv2 = create_pos_invoice(qty=1, rate=100, do_not_save=True) | ||||||
|  | 			inv2.get('items')[0].item_code = '_Test Item 2' | ||||||
|  | 			inv2.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": 5 | ||||||
|  | 			}) | ||||||
|  | 			inv2.insert() | ||||||
|  | 			inv2.submit() | ||||||
|  | 
 | ||||||
|  | 			consolidate_pos_invoices() | ||||||
|  | 			inv.load_from_db() | ||||||
|  | 
 | ||||||
|  | 			consolidated_invoice = frappe.get_doc('Sales Invoice', inv.consolidated_invoice) | ||||||
|  | 			item_wise_tax_detail = json.loads(consolidated_invoice.get('taxes')[0].item_wise_tax_detail) | ||||||
|  | 
 | ||||||
|  | 			tax_rate, amount = item_wise_tax_detail.get('_Test Item') | ||||||
|  | 			self.assertEqual(tax_rate, 9) | ||||||
|  | 			self.assertEqual(amount, 9) | ||||||
|  | 
 | ||||||
|  | 			tax_rate2, amount2 = item_wise_tax_detail.get('_Test Item 2') | ||||||
|  | 			self.assertEqual(tax_rate2, 5) | ||||||
|  | 			self.assertEqual(amount2, 5) | ||||||
|  | 		finally: | ||||||
|  | 			frappe.set_user("Administrator") | ||||||
|  | 			frappe.db.sql("delete from `tabPOS Profile`") | ||||||
|  | 			frappe.db.sql("delete from `tabPOS Invoice`") | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -70,6 +70,7 @@ class POSProfile(Document): | |||||||
| 				{"parent": d.mode_of_payment, "company": self.company}, | 				{"parent": d.mode_of_payment, "company": self.company}, | ||||||
| 				"default_account" | 				"default_account" | ||||||
| 			) | 			) | ||||||
|  | 
 | ||||||
| 			if not account: | 			if not account: | ||||||
| 				invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment)) | 				invalid_modes.append(get_link_to_form("Mode of Payment", d.mode_of_payment)) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -92,11 +92,21 @@ def make_pos_profile(**args): | |||||||
| 		"write_off_cost_center":  args.write_off_cost_center or "_Test Write Off Cost Center - _TC" | 		"write_off_cost_center":  args.write_off_cost_center or "_Test Write Off Cost Center - _TC" | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
| 	payments = [{ | 	mode_of_payment = frappe.get_doc("Mode of Payment", "Cash") | ||||||
|  | 	company = args.company or "_Test Company" | ||||||
|  | 	default_account = args.income_account or "Sales - _TC" | ||||||
|  | 
 | ||||||
|  | 	if not frappe.db.get_value("Mode of Payment Account", {"company": company, "parent": "Cash"}): | ||||||
|  | 		mode_of_payment.append("accounts", { | ||||||
|  | 			"company": company, | ||||||
|  | 			"default_account": default_account | ||||||
|  | 		}) | ||||||
|  | 		mode_of_payment.save() | ||||||
|  | 
 | ||||||
|  | 	pos_profile.append("payments", { | ||||||
| 		'mode_of_payment': 'Cash', | 		'mode_of_payment': 'Cash', | ||||||
| 		'default': 1 | 		'default': 1 | ||||||
| 	}] | 	}) | ||||||
| 	pos_profile.set("payments", payments) |  | ||||||
| 
 | 
 | ||||||
| 	if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"): | 	if not frappe.db.exists("POS Profile", args.name or "_Test POS Profile"): | ||||||
| 		pos_profile.insert() | 		pos_profile.insert() | ||||||
|  | |||||||
| @ -16,8 +16,11 @@ frappe.ui.form.on('POS Settings', { | |||||||
| 				} | 				} | ||||||
| 			}); | 			}); | ||||||
| 
 | 
 | ||||||
| 			frappe.meta.get_docfield("POS Field", "fieldname", frm.doc.name).options = [""].concat(fields); | 			frm.fields_dict.invoice_fields.grid.update_docfield_property( | ||||||
|  | 				'fieldname', 'options', [""].concat(fields) | ||||||
|  | 			); | ||||||
| 		}); | 		}); | ||||||
|  | 
 | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -328,6 +328,21 @@ class TestPricingRule(unittest.TestCase): | |||||||
| 		self.assertEquals(item.discount_amount, 110) | 		self.assertEquals(item.discount_amount, 110) | ||||||
| 		self.assertEquals(item.rate, 990) | 		self.assertEquals(item.rate, 990) | ||||||
| 
 | 
 | ||||||
|  | 	def test_pricing_rule_with_margin_and_discount_amount(self): | ||||||
|  | 		frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') | ||||||
|  | 		make_pricing_rule(selling=1, margin_type="Percentage", margin_rate_or_amount=10, | ||||||
|  | 			rate_or_discount="Discount Amount", discount_amount=110) | ||||||
|  | 		si = create_sales_invoice(do_not_save=True) | ||||||
|  | 		si.items[0].price_list_rate = 1000 | ||||||
|  | 		si.payment_schedule = [] | ||||||
|  | 		si.insert(ignore_permissions=True) | ||||||
|  | 
 | ||||||
|  | 		item = si.items[0] | ||||||
|  | 		self.assertEquals(item.margin_rate_or_amount, 10) | ||||||
|  | 		self.assertEquals(item.rate_with_margin, 1100) | ||||||
|  | 		self.assertEquals(item.discount_amount, 110) | ||||||
|  | 		self.assertEquals(item.rate, 990) | ||||||
|  | 
 | ||||||
| 	def test_pricing_rule_for_product_discount_on_same_item(self): | 	def test_pricing_rule_for_product_discount_on_same_item(self): | ||||||
| 		frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') | 		frappe.delete_doc_if_exists('Pricing Rule', '_Test Pricing Rule') | ||||||
| 		test_record = { | 		test_record = { | ||||||
| @ -560,6 +575,7 @@ def make_pricing_rule(**args): | |||||||
| 		"margin_rate_or_amount": args.margin_rate_or_amount or 0.0, | 		"margin_rate_or_amount": args.margin_rate_or_amount or 0.0, | ||||||
| 		"condition": args.condition or '', | 		"condition": args.condition or '', | ||||||
| 		"priority": 1, | 		"priority": 1, | ||||||
|  | 		"discount_amount": args.discount_amount or 0.0, | ||||||
| 		"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 | 		"apply_multiple_pricing_rules": args.apply_multiple_pricing_rules or 0 | ||||||
| 	}) | 	}) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -471,7 +471,7 @@ def apply_pricing_rule_on_transaction(doc): | |||||||
| 
 | 
 | ||||||
| 					if not d.get(pr_field): continue | 					if not d.get(pr_field): continue | ||||||
| 
 | 
 | ||||||
| 					if d.validate_applied_rule and doc.get(field) < d.get(pr_field): | 					if d.validate_applied_rule and doc.get(field) is not None and doc.get(field) < d.get(pr_field): | ||||||
| 						frappe.msgprint(_("User has not applied rule on the invoice {0}") | 						frappe.msgprint(_("User has not applied rule on the invoice {0}") | ||||||
| 							.format(doc.name)) | 							.format(doc.name)) | ||||||
| 					else: | 					else: | ||||||
|  | |||||||
| @ -92,7 +92,7 @@ frappe.ui.form.on('Process Statement Of Accounts', { | |||||||
| 							frm.refresh_field('customers'); | 							frm.refresh_field('customers'); | ||||||
| 						} | 						} | ||||||
| 						else{ | 						else{ | ||||||
| 							frappe.msgprint('No Customers found with selected options.'); | 							frappe.throw('No Customers found with selected options.'); | ||||||
| 						} | 						} | ||||||
| 					} | 					} | ||||||
| 				} | 				} | ||||||
|  | |||||||
| @ -126,9 +126,11 @@ def get_customers_based_on_sales_person(sales_person): | |||||||
| 	sales_person_records = frappe._dict() | 	sales_person_records = frappe._dict() | ||||||
| 	for d in records: | 	for d in records: | ||||||
| 		sales_person_records.setdefault(d.parenttype, set()).add(d.parent) | 		sales_person_records.setdefault(d.parenttype, set()).add(d.parent) | ||||||
| 	customers = frappe.get_list('Customer', fields=['name', 'email_id'], \ | 	if sales_person_records.get('Customer'): | ||||||
|  | 		return frappe.get_list('Customer', fields=['name', 'email_id'], \ | ||||||
| 			filters=[['name', 'in', list(sales_person_records['Customer'])]]) | 			filters=[['name', 'in', list(sales_person_records['Customer'])]]) | ||||||
| 	return customers | 	else: | ||||||
|  | 		return [] | ||||||
| 
 | 
 | ||||||
| def get_recipients_and_cc(customer, doc): | def get_recipients_and_cc(customer, doc): | ||||||
| 	recipients = [] | 	recipients = [] | ||||||
|  | |||||||
| @ -496,15 +496,6 @@ cur_frm.fields_dict['items'].grid.get_field('project').get_query = function(doc, | |||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 | 
 | ||||||
| cur_frm.cscript.select_print_heading = function(doc,cdt,cdn){ |  | ||||||
| 	if(doc.select_print_heading){ |  | ||||||
| 		// print heading
 |  | ||||||
| 		cur_frm.pformat.print_heading = doc.select_print_heading; |  | ||||||
| 	} |  | ||||||
| 	else |  | ||||||
| 		cur_frm.pformat.print_heading = __("Purchase Invoice"); |  | ||||||
| } |  | ||||||
| 
 |  | ||||||
| frappe.ui.form.on("Purchase Invoice", { | frappe.ui.form.on("Purchase Invoice", { | ||||||
| 	setup: function(frm) { | 	setup: function(frm) { | ||||||
| 		frm.custom_make_buttons = { | 		frm.custom_make_buttons = { | ||||||
|  | |||||||
| @ -127,7 +127,6 @@ | |||||||
|   "write_off_cost_center", |   "write_off_cost_center", | ||||||
|   "advances_section", |   "advances_section", | ||||||
|   "allocate_advances_automatically", |   "allocate_advances_automatically", | ||||||
|   "adjust_advance_taxes", |  | ||||||
|   "get_advances", |   "get_advances", | ||||||
|   "advances", |   "advances", | ||||||
|   "payment_schedule_section", |   "payment_schedule_section", | ||||||
| @ -1326,13 +1325,6 @@ | |||||||
|    "label": "Project", |    "label": "Project", | ||||||
|    "options": "Project" |    "options": "Project" | ||||||
|   }, |   }, | ||||||
|   { |  | ||||||
|    "default": "0", |  | ||||||
|    "description": "Taxes paid while advance payment will be adjusted against this invoice", |  | ||||||
|    "fieldname": "adjust_advance_taxes", |  | ||||||
|    "fieldtype": "Check", |  | ||||||
|    "label": "Adjust Advance Taxes" |  | ||||||
|   }, |  | ||||||
|   { |   { | ||||||
|    "depends_on": "eval:doc.is_internal_supplier", |    "depends_on": "eval:doc.is_internal_supplier", | ||||||
|    "description": "Unrealized Profit / Loss account for intra-company transfers", |    "description": "Unrealized Profit / Loss account for intra-company transfers", | ||||||
| @ -1378,7 +1370,7 @@ | |||||||
|  "idx": 204, |  "idx": 204, | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-09 21:12:30.422084", |  "modified": "2021-03-30 22:45:58.334107", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Accounts", |  "module": "Accounts", | ||||||
|  "name": "Purchase Invoice", |  "name": "Purchase Invoice", | ||||||
|  | |||||||
| @ -1,14 +1,14 @@ | |||||||
| var globalOnload = frappe.listview_settings['Sales Invoice'].onload; | var globalOnload = frappe.listview_settings['Sales Invoice'].onload; | ||||||
| frappe.listview_settings['Sales Invoice'].onload = function (doclist) { | frappe.listview_settings['Sales Invoice'].onload = function (list_view) { | ||||||
| 
 | 
 | ||||||
| 	// Provision in case onload event is added to sales_invoice.js in future
 | 	// Provision in case onload event is added to sales_invoice.js in future
 | ||||||
| 	if (globalOnload) { | 	if (globalOnload) { | ||||||
| 		globalOnload(doclist); | 		globalOnload(list_view); | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	const action = () => { | 	const action = () => { | ||||||
| 		const selected_docs = doclist.get_checked_items(); | 		const selected_docs = list_view.get_checked_items(); | ||||||
| 		const docnames = doclist.get_checked_items(true); | 		const docnames = list_view.get_checked_items(true); | ||||||
| 
 | 
 | ||||||
| 		for (let doc of selected_docs) { | 		for (let doc of selected_docs) { | ||||||
| 			if (doc.docstatus !== 1) { | 			if (doc.docstatus !== 1) { | ||||||
| @ -19,7 +19,7 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) { | |||||||
| 		frappe.call({ | 		frappe.call({ | ||||||
| 			method: 'erpnext.regional.india.utils.generate_ewb_json', | 			method: 'erpnext.regional.india.utils.generate_ewb_json', | ||||||
| 			args: { | 			args: { | ||||||
| 				'dt': doclist.doctype, | 				'dt': list_view.doctype, | ||||||
| 				'dn': docnames | 				'dn': docnames | ||||||
| 			}, | 			}, | ||||||
| 			callback: function(r) { | 			callback: function(r) { | ||||||
| @ -35,5 +35,140 @@ frappe.listview_settings['Sales Invoice'].onload = function (doclist) { | |||||||
| 		}); | 		}); | ||||||
| 	}; | 	}; | ||||||
| 
 | 
 | ||||||
| 	doclist.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false); | 	list_view.page.add_actions_menu_item(__('Generate E-Way Bill JSON'), action, false); | ||||||
|  | 
 | ||||||
|  | 	const generate_irns = () => { | ||||||
|  | 		const docnames = list_view.get_checked_items(true); | ||||||
|  | 		if (docnames && docnames.length) { | ||||||
|  | 			frappe.call({ | ||||||
|  | 				method: 'erpnext.regional.india.e_invoice.utils.generate_einvoices', | ||||||
|  | 				args: { docnames }, | ||||||
|  | 				freeze: true, | ||||||
|  | 				freeze_message: __('Generating E-Invoices...') | ||||||
|  | 			}); | ||||||
|  | 		} else { | ||||||
|  | 			frappe.msgprint({ | ||||||
|  | 				message: __('Please select at least one sales invoice to generate IRN'), | ||||||
|  | 				title: __('No Invoice Selected'), | ||||||
|  | 				indicator: 'red' | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	const cancel_irns = () => { | ||||||
|  | 		const docnames = list_view.get_checked_items(true); | ||||||
|  | 
 | ||||||
|  | 		const fields = [ | ||||||
|  | 			{ | ||||||
|  | 				"label": "Reason", | ||||||
|  | 				"fieldname": "reason", | ||||||
|  | 				"fieldtype": "Select", | ||||||
|  | 				"reqd": 1, | ||||||
|  | 				"default": "1-Duplicate", | ||||||
|  | 				"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] | ||||||
|  | 			}, | ||||||
|  | 			{  | ||||||
|  | 				"label": "Remark", | ||||||
|  | 				"fieldname": "remark", | ||||||
|  | 				"fieldtype": "Data", | ||||||
|  | 				"reqd": 1 | ||||||
|  | 			} | ||||||
|  | 		]; | ||||||
|  | 
 | ||||||
|  | 		const d = new frappe.ui.Dialog({ | ||||||
|  | 			title: __("Cancel IRN"), | ||||||
|  | 			fields: fields, | ||||||
|  | 			primary_action: function() { | ||||||
|  | 				const data = d.get_values(); | ||||||
|  | 				frappe.call({ | ||||||
|  | 					method: 'erpnext.regional.india.e_invoice.utils.cancel_irns', | ||||||
|  | 					args: {  | ||||||
|  | 						doctype: list_view.doctype, | ||||||
|  | 						docnames, | ||||||
|  | 						reason: data.reason.split('-')[0], | ||||||
|  | 						remark: data.remark | ||||||
|  | 					}, | ||||||
|  | 					freeze: true, | ||||||
|  | 					freeze_message: __('Cancelling E-Invoices...'), | ||||||
|  | 				}); | ||||||
|  | 				d.hide(); | ||||||
|  | 			}, | ||||||
|  | 			primary_action_label: __('Submit') | ||||||
|  | 		}); | ||||||
|  | 		d.show(); | ||||||
|  | 	}; | ||||||
|  | 
 | ||||||
|  | 	let einvoicing_enabled = false; | ||||||
|  | 	frappe.db.get_single_value("E Invoice Settings", "enable").then(enabled => { | ||||||
|  | 		einvoicing_enabled = enabled; | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	list_view.$result.on("change", "input[type=checkbox]", () => { | ||||||
|  | 		if (einvoicing_enabled) { | ||||||
|  | 			const docnames = list_view.get_checked_items(true); | ||||||
|  | 			// show/hide e-invoicing actions when no sales invoices are checked
 | ||||||
|  | 			if (docnames && docnames.length) { | ||||||
|  | 				// prevent adding actions twice if e-invoicing action group already exists
 | ||||||
|  | 				if (list_view.page.get_inner_group_button(__('E-Invoicing')).length == 0) { | ||||||
|  | 					list_view.page.add_inner_button(__('Generate IRNs'), generate_irns, __('E-Invoicing')); | ||||||
|  | 					list_view.page.add_inner_button(__('Cancel IRNs'), cancel_irns, __('E-Invoicing')); | ||||||
|  | 				} | ||||||
|  | 			} else { | ||||||
|  | 				list_view.page.remove_inner_button(__('Generate IRNs'), __('E-Invoicing')); | ||||||
|  | 				list_view.page.remove_inner_button(__('Cancel IRNs'), __('E-Invoicing')); | ||||||
|  | 			} | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	frappe.realtime.on("bulk_einvoice_generation_complete", (data) => { | ||||||
|  | 		const { failures, user, invoices } = data; | ||||||
|  | 		 | ||||||
|  | 		if (invoices.length != failures.length) { | ||||||
|  | 			frappe.msgprint({ | ||||||
|  | 				message: __('{0} e-invoices generated successfully', [invoices.length]), | ||||||
|  | 				title: __('Bulk E-Invoice Generation Complete'), | ||||||
|  | 				indicator: 'orange' | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (failures && failures.length && user == frappe.session.user) { | ||||||
|  | 			let message = ` | ||||||
|  | 				Failed to generate IRNs for following ${failures.length} sales invoices: | ||||||
|  | 				<ul style="padding-left: 20px; padding-top: 5px;"> | ||||||
|  | 					${failures.map(d => `<li>${d.docname}</li>`).join('')} | ||||||
|  | 				</ul> | ||||||
|  | 			`;
 | ||||||
|  | 			frappe.msgprint({ | ||||||
|  | 				message: message, | ||||||
|  | 				title: __('Bulk E-Invoice Generation Complete'), | ||||||
|  | 				indicator: 'orange' | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
|  | 
 | ||||||
|  | 	frappe.realtime.on("bulk_einvoice_cancellation_complete", (data) => { | ||||||
|  | 		const { failures, user, invoices } = data; | ||||||
|  | 
 | ||||||
|  | 		if (invoices.length != failures.length) { | ||||||
|  | 			frappe.msgprint({ | ||||||
|  | 				message: __('{0} e-invoices cancelled successfully', [invoices.length]), | ||||||
|  | 				title: __('Bulk E-Invoice Cancellation Complete'), | ||||||
|  | 				indicator: 'orange' | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		if (failures && failures.length && user == frappe.session.user) { | ||||||
|  | 			let message = ` | ||||||
|  | 				Failed to cancel IRNs for following ${failures.length} sales invoices: | ||||||
|  | 				<ul style="padding-left: 20px; padding-top: 5px;"> | ||||||
|  | 					${failures.map(d => `<li>${d.docname}</li>`).join('')} | ||||||
|  | 				</ul> | ||||||
|  | 			`;
 | ||||||
|  | 			frappe.msgprint({ | ||||||
|  | 				message: message, | ||||||
|  | 				title: __('Bulk E-Invoice Cancellation Complete'), | ||||||
|  | 				indicator: 'orange' | ||||||
|  | 			}); | ||||||
|  | 		} | ||||||
|  | 	}); | ||||||
| }; | }; | ||||||
| @ -1,9 +1,6 @@ | |||||||
| // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 | // Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
 | ||||||
| // License: GNU General Public License v3. See license.txt
 | // License: GNU General Public License v3. See license.txt
 | ||||||
| 
 | 
 | ||||||
| // print heading
 |  | ||||||
| cur_frm.pformat.print_heading = 'Invoice'; |  | ||||||
| 
 |  | ||||||
| {% include 'erpnext/selling/sales_common.js' %}; | {% include 'erpnext/selling/sales_common.js' %}; | ||||||
| frappe.provide("erpnext.accounts"); | frappe.provide("erpnext.accounts"); | ||||||
| 
 | 
 | ||||||
| @ -916,7 +913,7 @@ frappe.ui.form.on('Sales Invoice Timesheet', { | |||||||
| 				}, | 				}, | ||||||
| 				callback: function(r, rt) { | 				callback: function(r, rt) { | ||||||
| 					if(r.message){ | 					if(r.message){ | ||||||
| 						data = r.message; | 						let data = r.message; | ||||||
| 						frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours); | 						frappe.model.set_value(cdt, cdn, "billing_hours", data.billing_hours); | ||||||
| 						frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount); | 						frappe.model.set_value(cdt, cdn, "billing_amount", data.billing_amount); | ||||||
| 						frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail); | 						frappe.model.set_value(cdt, cdn, "timesheet_detail", data.timesheet_detail); | ||||||
|  | |||||||
| @ -24,6 +24,7 @@ from erpnext.accounts.deferred_revenue import validate_service_stop_date | |||||||
| from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details | from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details | ||||||
| from frappe.model.utils import get_fetch_values | from frappe.model.utils import get_fetch_values | ||||||
| from frappe.contacts.doctype.address.address import get_address_display | from frappe.contacts.doctype.address.address import get_address_display | ||||||
|  | from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import get_party_tax_withholding_details | ||||||
| 
 | 
 | ||||||
| from erpnext.healthcare.utils import manage_invoice_submit_cancel | from erpnext.healthcare.utils import manage_invoice_submit_cancel | ||||||
| 
 | 
 | ||||||
| @ -211,6 +212,9 @@ class SalesInvoice(SellingController): | |||||||
| 		# this sequence because outstanding may get -ve | 		# this sequence because outstanding may get -ve | ||||||
| 		self.make_gl_entries() | 		self.make_gl_entries() | ||||||
| 
 | 
 | ||||||
|  | 		if self.update_stock == 1: | ||||||
|  | 			self.repost_future_sle_and_gle() | ||||||
|  | 
 | ||||||
| 		if self.update_stock == 1: | 		if self.update_stock == 1: | ||||||
| 			self.repost_future_sle_and_gle() | 			self.repost_future_sle_and_gle() | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1166,10 +1166,12 @@ class TestSalesInvoice(unittest.TestCase): | |||||||
| 
 | 
 | ||||||
| 	def test_create_so_with_margin(self): | 	def test_create_so_with_margin(self): | ||||||
| 		si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True) | 		si = create_sales_invoice(item_code="_Test Item", qty=1, do_not_submit=True) | ||||||
| 		price_list_rate = 100 | 		price_list_rate = flt(100) * flt(si.plc_conversion_rate) | ||||||
| 		si.items[0].price_list_rate = price_list_rate | 		si.items[0].price_list_rate = price_list_rate | ||||||
| 		si.items[0].margin_type = 'Percentage' | 		si.items[0].margin_type = 'Percentage' | ||||||
| 		si.items[0].margin_rate_or_amount = 25 | 		si.items[0].margin_rate_or_amount = 25 | ||||||
|  | 		si.items[0].discount_amount = 0.0 | ||||||
|  | 		si.items[0].discount_percentage = 0.0 | ||||||
| 		si.save() | 		si.save() | ||||||
| 		self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) | 		self.assertEqual(si.get("items")[0].rate, flt((price_list_rate*25)/100 + price_list_rate)) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -406,9 +406,10 @@ def check_if_advance_entry_modified(args): | |||||||
| 		throw(_("""Payment Entry has been modified after you pulled it. Please pull it again.""")) | 		throw(_("""Payment Entry has been modified after you pulled it. Please pull it again.""")) | ||||||
| 
 | 
 | ||||||
| def validate_allocated_amount(args): | def validate_allocated_amount(args): | ||||||
|  | 	precision = args.get('precision') or frappe.db.get_single_value("System Settings", "currency_precision") | ||||||
| 	if args.get("allocated_amount") < 0: | 	if args.get("allocated_amount") < 0: | ||||||
| 		throw(_("Allocated amount cannot be negative")) | 		throw(_("Allocated amount cannot be negative")) | ||||||
| 	elif args.get("allocated_amount") > args.get("unadjusted_amount"): | 	elif flt(args.get("allocated_amount"), precision) > flt(args.get("unadjusted_amount"), precision): | ||||||
| 		throw(_("Allocated amount cannot be greater than unadjusted amount")) | 		throw(_("Allocated amount cannot be greater than unadjusted amount")) | ||||||
| 
 | 
 | ||||||
| def update_reference_in_journal_entry(d, jv_obj): | def update_reference_in_journal_entry(d, jv_obj): | ||||||
|  | |||||||
| @ -443,6 +443,16 @@ | |||||||
|    "onboard": 0, |    "onboard": 0, | ||||||
|    "type": "Link" |    "type": "Link" | ||||||
|   }, |   }, | ||||||
|  |   { | ||||||
|  |     "dependencies": "GL Entry", | ||||||
|  |     "hidden": 0, | ||||||
|  |     "is_query_report": 1, | ||||||
|  |     "label": "UAE VAT 201", | ||||||
|  |     "link_to": "UAE VAT 201", | ||||||
|  |     "link_type": "Report", | ||||||
|  |     "onboard": 0, | ||||||
|  |     "type": "Link" | ||||||
|  |    }, | ||||||
|   { |   { | ||||||
|    "hidden": 0, |    "hidden": 0, | ||||||
|    "is_query_report": 0, |    "is_query_report": 0, | ||||||
|  | |||||||
							
								
								
									
										471
									
								
								erpnext/change_log/v13/v13_0_0.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										471
									
								
								erpnext/change_log/v13/v13_0_0.md
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,471 @@ | |||||||
|  | # Version 13.0.0 Release Notes | ||||||
|  | 
 | ||||||
|  | ### Accounting | ||||||
|  | - [New and refreshed POS](https://github.com/frappe/erpnext/pull/20789) | ||||||
|  | - [GST E-invoicing for India](https://docs.erpnext.com/docs/user/manual/en/regional/india/setup-e-invoicing) | ||||||
|  | - [Distributed Cost Center](https://docs.erpnext.com/docs/user/manual/en/accounts/distributed-cost-center) | ||||||
|  | - [Process Bulk Statement Of Accounts](https://docs.erpnext.com/docs/user/manual/en/accounts/process-statement-of-accounts) | ||||||
|  | - [More controlled deferred revenue booking](https://docs.erpnext.com/docs/user/manual/en/accounts/process-deferred-accounting) | ||||||
|  | - [Dunning](https://docs.erpnext.com/docs/user/manual/en/accounts/dunning) | ||||||
|  | - [Journal Entry Template](https://docs.erpnext.com/docs/user/manual/en/accounts/journal-entry-template) | ||||||
|  | - [POS Register report](https://github.com/frappe/erpnext/pull/23313) | ||||||
|  | - [UAE VAT 201 Report](https://github.com/frappe/erpnext/pull/23447) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### Loan Management | ||||||
|  | - [Loan Application](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-application) | ||||||
|  | - [Loan](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan) | ||||||
|  | - [Loan Security Pledge](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-security-pledge) | ||||||
|  | - [Loan Disbursement](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-disbursement) | ||||||
|  | - [Loan Repayment](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-repayment) | ||||||
|  | - [Loan Interest Accrual](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-interest-accrual) | ||||||
|  | - [Loan Write Off](https://docs.erpnext.com/docs/user/manual/en/loan-management/loan-write-off) | ||||||
|  | 
 | ||||||
|  | ### Healthcare | ||||||
|  | - [Refactored Healthcare Module](https://docs.erpnext.com/docs/user/manual/en/healthcare) | ||||||
|  | - [Rehabilitation Module](https://docs.erpnext.com/docs/user/manual/en/healthcare/exercise_type) | ||||||
|  | - [Laboratory Module](https://docs.erpnext.com/docs/user/manual/en/healthcare/setup_laboratory) | ||||||
|  | - [Patient Progress Page](https://github.com/frappe/erpnext/pull/22474) | ||||||
|  | - [Inpatient Medication Order and Entry](https://docs.erpnext.com/docs/user/manual/en/healthcare/inpatient_medication_entry) | ||||||
|  | - [Therapy Plan Template](https://docs.erpnext.com/docs/user/manual/en/healthcare/therapy_plan) | ||||||
|  | - [Multi company support in Healthcare](https://github.com/frappe/erpnext/pull/21290) | ||||||
|  | - [Inpatient Medication Orders Script Report](https://github.com/frappe/erpnext/pull/23984) | ||||||
|  | - [Patient History Enhancements](https://github.com/frappe/erpnext/pull/24033) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | ### Stock | ||||||
|  | - [Putaway](https://docs.erpnext.com/docs/user/manual/en/stock/putaway-rule) | ||||||
|  | - [More accurate stock valuation in case of back-dated stock transactions](https://github.com/frappe/erpnext/pull/24183) | ||||||
|  | - [Repost item costing via background job](https://github.com/frappe/erpnext/pull/24183) | ||||||
|  | - [Item valuation for internal stock transfers](https://github.com/frappe/erpnext/pull/24200) | ||||||
|  | - [Multi currency in Landed Cost Voucher](https://github.com/frappe/erpnext/pull/24127) | ||||||
|  | - [Formula based Quality Inspection](https://docs.erpnext.com/docs/user/manual/en/stock/quality-inspection) | ||||||
|  | - [Value Based and Numeric Quality Inspection](https://github.com/frappe/erpnext/pull/24181) | ||||||
|  | - [Shipment](https://github.com/frappe/erpnext/pull/22914) | ||||||
|  | - [Return tracking in PR/DN](https://github.com/frappe/erpnext/pull/22859) | ||||||
|  | 
 | ||||||
|  | ### Manufacturing | ||||||
|  | - [Production forecasting using Exponential Smoothing method](https://docs.erpnext.com/docs/user/manual/en/manufacturing/reports/demand-driven-forecasting) | ||||||
|  | - [BOM Template](https://docs.erpnext.com/docs/user/manual/en/manufacturing/bill-of-materials#34-bom-template) | ||||||
|  | - [Downtime Entry](https://docs.erpnext.com/docs/user/manual/en/manufacturing/downtime-entry) | ||||||
|  | - [Quality Inspection on Job Card](https://github.com/frappe/erpnext/pull/23964) | ||||||
|  | - New Reports | ||||||
|  |   - Production Planning Report ([#21763](https://github.com/frappe/erpnext/pull/21763)) | ||||||
|  |   - BOM Operations Time ([#21763](https://github.com/frappe/erpnext/pull/21763)) | ||||||
|  |   - Work Order Summary ([#21430](https://github.com/frappe/erpnext/pull/21430)) | ||||||
|  |   - Job card Summary ([#21430](https://github.com/frappe/erpnext/pull/21430)) | ||||||
|  |   - Downtime Analysis ([#21430](https://github.com/frappe/erpnext/pull/21430)) | ||||||
|  |   - Quality Inspection ([#21430](https://github.com/frappe/erpnext/pull/21430)) | ||||||
|  | 
 | ||||||
|  | ### HR | ||||||
|  | - [Leave policy assignment](https://github.com/frappe/erpnext/pull/23112) | ||||||
|  | - [In and Out time in attendance](https://github.com/frappe/erpnext/pull/21547) | ||||||
|  | - [Shift management](https://docs.erpnext.com/docs/user/manual/en/human-resources/shift-management) | ||||||
|  | - [Recruitment analytics](https://github.com/frappe/erpnext/pull/21732) | ||||||
|  | - [Bulk Mark Attendance](https://github.com/frappe/erpnext/pull/20062) | ||||||
|  | - [Leave type with partial payment](https://github.com/frappe/erpnext/pull/23173) | ||||||
|  | - New and enhanced reports | ||||||
|  |     - Employee Analytics ([#21705](https://github.com/frappe/erpnext/pull/21705)) | ||||||
|  |     - Employee Leave Balance ([#20754](https://github.com/frappe/erpnext/pull/20754)) | ||||||
|  |     - Employee Leave Balance Summary ([#20754](https://github.com/frappe/erpnext/pull/20754)) | ||||||
|  | 
 | ||||||
|  | ### Payroll | ||||||
|  | - [Multi-currency payroll](https://github.com/frappe/erpnext/pull/23519) | ||||||
|  | - [Payroll based on attendance](https://github.com/frappe/erpnext/pull/21258) | ||||||
|  | - [Payroll based on employee cost center](https://github.com/frappe/erpnext/pull/21609) | ||||||
|  | - [Recurring Additional Salary](https://github.com/frappe/erpnext/pull/20936) | ||||||
|  | - [Compute Year to Date for Salary Slip components](https://github.com/frappe/erpnext/pull/24362) | ||||||
|  | - New Reports | ||||||
|  |   - Income Tax Deductions | ||||||
|  |   - Professional Tax Deductions | ||||||
|  |   - Provident Fund Deductions | ||||||
|  |   - Total Salary Payments Based on Payment Mode | ||||||
|  |   - Salary Payments via ECS | ||||||
|  | 
 | ||||||
|  | ### CRM | ||||||
|  | - [Social Media Post](https://docs.erpnext.com/docs/user/manual/en/CRM/social-media-post) | ||||||
|  | - [Make Quotation against Blanket Order](https://docs.erpnext.com/docs/user/manual/en/selling/blanket-order) | ||||||
|  | - [Calendar View for Opportunity](https://github.com/frappe/erpnext/pull/21280) | ||||||
|  | 
 | ||||||
|  | ### Selling | ||||||
|  | - [Batch wise item pricing](https://github.com/frappe/erpnext/pull/24470) | ||||||
|  | - [Refreshed shopping cart](https://github.com/frappe/erpnext/pull/22617) | ||||||
|  | - [Territory-wise Sales Report](https://github.com/frappe/erpnext/pull/20428) | ||||||
|  | 
 | ||||||
|  | #### Buying | ||||||
|  | - [Multi UOM support in Request for Quotation](https://github.com/frappe/erpnext/pull/22249) | ||||||
|  | - [Provision to make RFQ against Opportunity](https://github.com/frappe/erpnext/pull/22765) | ||||||
|  | - [Item Rate in Stock UOM in purchase cycle](https://github.com/frappe/erpnext/pull/24315) | ||||||
|  | - New Reports | ||||||
|  |   - Requested Items To Order ([#21611](https://github.com/frappe/erpnext/pull/21611)) | ||||||
|  |   - Purchase Order Analysis ([#21611](https://github.com/frappe/erpnext/pull/21611)) | ||||||
|  |   - Supplier Quotation Comparison report ([#23323](https://github.com/frappe/erpnext/pull/23323)) | ||||||
|  | 
 | ||||||
|  | ### Project | ||||||
|  | - [Project template with dependent tasks](https://github.com/frappe/erpnext/pull/24092) | ||||||
|  | - [Project Summary Report](https://github.com/frappe/erpnext/pull/21587) | ||||||
|  | 
 | ||||||
|  | ### Support | ||||||
|  | - [Help Articles on support portal](https://github.com/frappe/erpnext/pull/22194) | ||||||
|  | - [Issue Metrics and SLA Enhancements](https://github.com/frappe/erpnext/pull/21617) | ||||||
|  | - [Issue Summary Script Report](https://docs.erpnext.com/docs/user/manual/en/support/support_reports) | ||||||
|  | - [Issue Analytics Script Report](https://docs.erpnext.com/docs/user/manual/en/support/support_reports) | ||||||
|  | 
 | ||||||
|  | ### Non-Profits | ||||||
|  | - [80G Certificates and Donations](https://docs.erpnext.com/docs/user/manual/en/non_profit/tax_exemption_80g_certificate) | ||||||
|  | 
 | ||||||
|  | #### Integrations | ||||||
|  | - [Woocommerce Integration](https://docs.erpnext.com/docs/user/manual/en/erpnext_integration/woocommerce_integration) | ||||||
|  | - [Taxjar Integration](https://github.com/frappe/erpnext/pull/21047) | ||||||
|  | - [M-pesa Integration](https://docs.erpnext.com/docs/user/manual/en/erpnext_integration/mpesa-integration) | ||||||
|  | - [Telephony feature using Twillio](https://github.com/frappe/erpnext/pull/24032) | ||||||
|  | - [Voice Call Settings](https://github.com/frappe/erpnext/pull/24126) | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | #### Other Enhancements and Fixes | ||||||
|  | - Accounting Dimensions in Budget Variance Report ([#19973](https://github.com/frappe/erpnext/pull/19973)) | ||||||
|  | - "Sync Now" option in Plaid Settings ([#23602](https://github.com/frappe/erpnext/pull/23602)) | ||||||
|  | - Custom Fields in POS ([#19876](https://github.com/frappe/erpnext/pull/19876)) | ||||||
|  | - [Inter Warehouse Stock Transfer in Purchase Receipt](https://docs.erpnext.com/docs/user/manual/en/stock/articles/material-transfer-from-delivery-note) | ||||||
|  | - [Accounts Payable Report based on Payment Terms](https://docs.erpnext.com/docs/user/manual/en/accounts/accounting-reports) | ||||||
|  | - Configurable accounting dimension filters and validations ([#23912](https://github.com/frappe/erpnext/pull/23912)) | ||||||
|  | - Territory tree in Customer Acquisition and Loyalty report ([#21668](https://github.com/frappe/erpnext/pull/21668)) | ||||||
|  | - Allow Purchase Invoice Creation Without Purchase Order Checkbox in Supplier ([#20864](https://github.com/frappe/erpnext/pull/20864)) | ||||||
|  | - Gross Profit In Quotation ([#21795](https://github.com/frappe/erpnext/pull/21795)) | ||||||
|  | - Notify credit controller users for credit limit extension via Email ([#22213](https://github.com/frappe/erpnext/pull/22213)) | ||||||
|  | - Run MRP at parent level in the production plan and make material transfer based upon materials availability ([#21545](https://github.com/frappe/erpnext/pull/21545)) | ||||||
|  | - Balance Serial Nos in Stock Ledger report ([#23675](https://github.com/frappe/erpnext/pull/23675)) | ||||||
|  | - Youtube interactions via Video  ([#22867](https://github.com/frappe/erpnext/pull/22867)) | ||||||
|  | - Consider Holiday List in Student Leave Application and Attendance ([#23388](https://github.com/frappe/erpnext/pull/23388)) | ||||||
|  | - Patient appointment status changes ([#24201](https://github.com/frappe/erpnext/pull/24201)) | ||||||
|  | - Sales order status filter added for production plan ([#23805](https://github.com/frappe/erpnext/pull/23805)) | ||||||
|  | - Monthly attendance sheet report group by Department, Designation, Employee Grade and Branch ([#21331](https://github.com/frappe/erpnext/pull/21331)) | ||||||
|  | - Upload Attendance template now have pre-filled holiday status ([#20947](https://github.com/frappe/erpnext/pull/20947)) | ||||||
|  | - Provision to disable serial no and batch selector ([#24398](https://github.com/frappe/erpnext/pull/24398)) | ||||||
|  | 
 | ||||||
|  | <details> | ||||||
|  | <summary>More</summary> | ||||||
|  | 
 | ||||||
|  | - Fetch Items from BOM in Stock Entry([#19498](https://github.com/frappe/erpnext/pull/19498)) | ||||||
|  | - Supplier Sourced Items in BOM ([#23557](https://github.com/frappe/erpnext/pull/23557)) | ||||||
|  | - Close Production Plan ([#23728](https://github.com/frappe/erpnext/pull/23728)) | ||||||
|  | - Button to create Stock Entry for Drug Shortage ([#24012](https://github.com/frappe/erpnext/pull/24012)) | ||||||
|  | - Added column cost center in Accounts Receivable report ([#23835](https://github.com/frappe/erpnext/pull/23835)) | ||||||
|  | - Added jinja templating in Contract Template ([#24046](https://github.com/frappe/erpnext/pull/24046)) | ||||||
|  | - Make account number length configurable ([#23845](https://github.com/frappe/erpnext/pull/23845)) | ||||||
|  | - Add company and correct filter in bank reconciliation statement ([#23614](https://github.com/frappe/erpnext/pull/23614)) | ||||||
|  | - Added Condition field in Pricing Rule ([#23014](https://github.com/frappe/erpnext/pull/23014)) | ||||||
|  | - Open lead status on next contact date ([#23445](https://github.com/frappe/erpnext/pull/23445)) | ||||||
|  | - [Tax Category in POS Profile](https://docs.erpnext.com/docs/user/manual/en/accounts/pos-profile) | ||||||
|  | - Added phone field in product Inquiry ([#23170](https://github.com/frappe/erpnext/pull/23170)) | ||||||
|  | - Allow Discharge despite Unbilled Healthcare Services ([#24281](https://github.com/frappe/erpnext/pull/24281)) | ||||||
|  | - Do Not Bill Patient Encounters for Inpatients ([#24355](https://github.com/frappe/erpnext/pull/24355)) | ||||||
|  | - Autofill Supplier pop-up when only 1 Supplier in RFQ ([#22512](https://github.com/frappe/erpnext/pull/22512)) | ||||||
|  | - Accounting entries for service item in Purchase receipt ([#22223](https://github.com/frappe/erpnext/pull/22223)) | ||||||
|  | - Added Project in Sales Analytics report ([#23309](https://github.com/frappe/erpnext/pull/23309)) | ||||||
|  | - Added all companies option in employee tree to view employee across all companies ([#22573](https://github.com/frappe/erpnext/pull/22573)) | ||||||
|  | - Email Group Option In Email Campaign ([#22731](https://github.com/frappe/erpnext/pull/22731)) | ||||||
|  | - Stock Report Enhancements ([#21727](https://github.com/frappe/erpnext/pull/21727)) | ||||||
|  | - Added range for age in stock ageing ([#22622](https://github.com/frappe/erpnext/pull/22622)) | ||||||
|  | - Report Summary in Financial Statement([#20876](https://github.com/frappe/erpnext/pull/20876)) | ||||||
|  | - Added sequence id in routing for the completion of operations sequentially ([#23641](https://github.com/frappe/erpnext/pull/23641)) | ||||||
|  | - Nested Set filtering for Accounting Dimension | ||||||
|  | - Add/Remove Items from submitted Sales/Purchase Order | ||||||
|  | - Provision to edit Item Details from Marketplace | ||||||
|  | - Scan Barcode in Purchase Receipt | ||||||
|  | - Disable Rounded Totals Checkbox for Salary Slips in HR Settings | ||||||
|  | 
 | ||||||
|  | - Renamed Loan Management to Loan on Desk Page ([#21877](https://github.com/frappe/erpnext/pull/21877)) | ||||||
|  | - Added Expense Approver field in Employee master ([#22244](https://github.com/frappe/erpnext/pull/22244)) | ||||||
|  | - Bill all hours by default on Timesheet ([#22155](https://github.com/frappe/erpnext/pull/22155)) | ||||||
|  | - Unable to cancel employee advance ([#22374](https://github.com/frappe/erpnext/pull/22374)) | ||||||
|  | - Status error in purchase invoice ([#22351](https://github.com/frappe/erpnext/pull/22351)) | ||||||
|  | - Item-wise sales and purchase register export ([#22184](https://github.com/frappe/erpnext/pull/22184)) | ||||||
|  | - Billing address in for Purchase documents ([#22233](https://github.com/frappe/erpnext/pull/22233)) | ||||||
|  | - Handle canceled entries in financial statements ([#22231](https://github.com/frappe/erpnext/pull/22231)) | ||||||
|  | - Default period start date and period end date for financial statements ([#22011](https://github.com/frappe/erpnext/pull/22011)) | ||||||
|  | - Update Packed Items via Update Items in Sales Order ([#22392](https://github.com/frappe/erpnext/pull/22392)) | ||||||
|  | - Hide delete company transactions button if not system manager ([#21839](https://github.com/frappe/erpnext/pull/21839)) | ||||||
|  | - Skipping total row for tree-view reports ([#22350](https://github.com/frappe/erpnext/pull/22350)) | ||||||
|  | - Cancelled entries in tds payable monthly report ([#22131](https://github.com/frappe/erpnext/pull/22131)) | ||||||
|  | - Inter-company Invoice currency for multicurrency transactions ([#21984](https://github.com/frappe/erpnext/pull/21984)) | ||||||
|  | - Filter batches based on item and warehouse in Pick List (develop) ([#21780](https://github.com/frappe/erpnext/pull/21780)) | ||||||
|  | - Set cost center in Expense Claim child based on parent (if missing) ([#22175](https://github.com/frappe/erpnext/pull/22175)) | ||||||
|  | - Item wise backdated stock entry posting for immutable ledger ([#22366](https://github.com/frappe/erpnext/pull/22366)) | ||||||
|  | - Shopping cart UI fixes ([#22137](https://github.com/frappe/erpnext/pull/22137)) | ||||||
|  | - Filter Leave Type based on allocation for a particular employee ([#22050](https://github.com/frappe/erpnext/pull/22050)) | ||||||
|  | - Party validation for inter-warehouse transaction ([#22186](https://github.com/frappe/erpnext/pull/22186)) | ||||||
|  | - Manufacturing dashboard and work order summary chart ([#21946](https://github.com/frappe/erpnext/pull/21946)) | ||||||
|  | - IP Admission and Discharge, Minor fixes ([#21817](https://github.com/frappe/erpnext/pull/21817)) | ||||||
|  | - Validation of Purchase Order against Material Request missing ([#22192](https://github.com/frappe/erpnext/pull/22192)) | ||||||
|  | - Staffing Plan validation ([#22379](https://github.com/frappe/erpnext/pull/22379)) | ||||||
|  | - Do not allow backdated stock transactions in previous fiscal year ([#21967](https://github.com/frappe/erpnext/pull/21967)) | ||||||
|  | - Employee Advance Return not working ([#21812](https://github.com/frappe/erpnext/pull/21812)) | ||||||
|  | - Added card for reports on education desk ([#21853](https://github.com/frappe/erpnext/pull/21853)) | ||||||
|  | - Refactored project summary report  ([#21943](https://github.com/frappe/erpnext/pull/21943)) | ||||||
|  | - Revenue and Customer Count only in date range in Customer Acquitition Report ([#22210](https://github.com/frappe/erpnext/pull/22210)) | ||||||
|  | - Alternative item not working for subcontract ([#22386](https://github.com/frappe/erpnext/pull/22386)) | ||||||
|  | - Unable to create batched Item ([#22393](https://github.com/frappe/erpnext/pull/22393)) | ||||||
|  | - Filters for the manufacturing reports ([#21960](https://github.com/frappe/erpnext/pull/21960)) | ||||||
|  | - Raw material warehouse in Production Planning Report ([#21982](https://github.com/frappe/erpnext/pull/21982)) | ||||||
|  | - Allowed LWP leave types to select in Leave Application even if there is no allocation against them ([#22197](https://github.com/frappe/erpnext/pull/22197)) | ||||||
|  | - Report not working on parameter Grade ([#21951](https://github.com/frappe/erpnext/pull/21951)) | ||||||
|  | - Allow to enter Relieving date if employee status is Left ([#22242](https://github.com/frappe/erpnext/pull/22242)) | ||||||
|  | - Resetting lost reason in opportunity and quotation ([#22378](https://github.com/frappe/erpnext/pull/22378)) | ||||||
|  | - Filtering issues in opening invoice creation tool ([#21969](https://github.com/frappe/erpnext/pull/21969)) | ||||||
|  | - Set default reference Id for "On Previous Row Amount" and "On Previous Row Total" ([#22346](https://github.com/frappe/erpnext/pull/22346)) | ||||||
|  | - UX date range field separated in from and to date fields. ([#21765](https://github.com/frappe/erpnext/pull/21765)) | ||||||
|  | - Enable show_configure_button when shopping cart is enabled ([#22468](https://github.com/frappe/erpnext/pull/22468)) | ||||||
|  | - Setup status indicators for Job Offer and Job Applicant (develop) ([#22445](https://github.com/frappe/erpnext/pull/22445)) | ||||||
|  | - Item-wise sales history report ([#22783](https://github.com/frappe/erpnext/pull/22783)) | ||||||
|  | - Setting filter for project in kanban board ([#22717](https://github.com/frappe/erpnext/pull/22717)) | ||||||
|  | - Dashboard For Timesheet ([#22750](https://github.com/frappe/erpnext/pull/22750)) | ||||||
|  | - Handle custom statuses for the pause SLA configuration ([#22349](https://github.com/frappe/erpnext/pull/22349)) | ||||||
|  | - Quality Feedback and Template ([#22571](https://github.com/frappe/erpnext/pull/22571)) | ||||||
|  | - Unable to change link from new lead to existing customer ([#22787](https://github.com/frappe/erpnext/pull/22787)) | ||||||
|  | - Move Issue List actions under 'Actions' dropdown (ux) ([#22710](https://github.com/frappe/erpnext/pull/22710)) | ||||||
|  | - Cost center should only show option of selected company ([#22598](https://github.com/frappe/erpnext/pull/22598)) | ||||||
|  | - Serial No Rename does not affect  Stock Ledger Entry ([#22746](https://github.com/frappe/erpnext/pull/22746)) | ||||||
|  | - Descriptions not copied while creating Fees from Fee Structure ([#22792](https://github.com/frappe/erpnext/pull/22792)) | ||||||
|  | - Company filter for cost_center and expense_account in all sales and purchase transactions ([#22478](https://github.com/frappe/erpnext/pull/22478)) | ||||||
|  | - Arrangements of filters for reports accounts payable & receivable  ([#22636](https://github.com/frappe/erpnext/pull/22636)) | ||||||
|  | - Update the project after task deletion so that the % completed shows correct value ([#22591](https://github.com/frappe/erpnext/pull/22591)) | ||||||
|  | - Block Invalid Serial No updates in Maintenance Schedule ([#22665](https://github.com/frappe/erpnext/pull/22665)) | ||||||
|  | - Fetch item price in sales invoice based on it's validity ([#22563](https://github.com/frappe/erpnext/pull/22563)) | ||||||
|  | - Add view ledger button for cancelled docs ([#22432](https://github.com/frappe/erpnext/pull/22432)) | ||||||
|  | - Allow creating SLA documents even if SLA tracking is not enabled ([#22608](https://github.com/frappe/erpnext/pull/22608)) | ||||||
|  | - Quotation list view blank if quotation_to field not set as a standard filter ([#22672](https://github.com/frappe/erpnext/pull/22672)) | ||||||
|  | - Salary deductions report fixes ([#22397](https://github.com/frappe/erpnext/pull/22397)) | ||||||
|  | 22727)) | ||||||
|  | - Incorrect delivered qty in Supplier-Wise Sales Analytics ([#22631](https://github.com/frappe/erpnext/pull/22631)) | ||||||
|  | - Moved parent warehouse to top section also added a section break ([#22708](https://github.com/frappe/erpnext/pull/22708)) | ||||||
|  | - Skip Progress and Completed by fields on Task Duplication ([#22565](https://github.com/frappe/erpnext/pull/22565)) | ||||||
|  | - Incorrect stock after merging the items ([#22526](https://github.com/frappe/erpnext/pull/22526)) | ||||||
|  | - Letter head not found in opening invoice creation tool ([#22488](https://github.com/frappe/erpnext/pull/22488)) | ||||||
|  | - Cannot cancel asset and asset movement ([#22441](https://github.com/frappe/erpnext/pull/22441)) | ||||||
|  | - Fetch project-related info in Timesheet ([#22423](https://github.com/frappe/erpnext/pull/22423)) | ||||||
|  | - Currency symbol not showing as per company currency in stock balance report ([#22724](https://github.com/frappe/erpnext/pull/22724)) | ||||||
|  | - Add default cost center in payment reconciliation JV ([#22614](https://github.com/frappe/erpnext/pull/22614)) | ||||||
|  | - Stock Reconciliation Invalid Quantity for Batched Item ([#22726](https://github.com/frappe/erpnext/pull/22726)) | ||||||
|  | - Project link not set in accounts other than profit and loss accounts ([#22051](https://github.com/frappe/erpnext/pull/22051)) | ||||||
|  | - Buying price for non stock item in gross profit report ([#22616](https://github.com/frappe/erpnext/pull/22616)) | ||||||
|  | - Multi currency payment reconciliation ([#22738](https://github.com/frappe/erpnext/pull/22738)) | ||||||
|  | - Cannot cancel assets with repair pending ([#22440](https://github.com/frappe/erpnext/pull/22440)) | ||||||
|  | - Reset homepage to home after unchecking products page ([#22736](https://github.com/frappe/erpnext/pull/22736)) | ||||||
|  | - Generic Message in previous doc validation for buying and selling ([#22546](https://github.com/frappe/erpnext/pull/22546)) | ||||||
|  | - Expense claim outstanding while making payment entry ([#22735](https://github.com/frappe/erpnext/pull/22735)) | ||||||
|  | - Take parent cost center for child if no cost center at child in expense claim ([#22496](https://github.com/frappe/erpnext/pull/22496)) | ||||||
|  | - Consider company fiscal year for getting balance ([#22577](https://github.com/frappe/erpnext/pull/22577)) | ||||||
|  | - Pick List empty table and Serial-Batch items handling ([#22426](https://github.com/frappe/erpnext/pull/22426)) | ||||||
|  | - Show total row in print format of financial statement ([#22693](https://github.com/frappe/erpnext/pull/22693)) | ||||||
|  | - Set Root as Parent if no parent in new tree view node ([#22497](https://github.com/frappe/erpnext/pull/22497)) | ||||||
|  | - Multiple pos issues ([#23725](https://github.com/frappe/erpnext/pull/23725)) | ||||||
|  | - Calculate taxes if tax is based on item quantity and inclusive on item price ([#23001](https://github.com/frappe/erpnext/pull/23001)) | ||||||
|  | - Contact us button not visible in the website for the non variant items ([#23217](https://github.com/frappe/erpnext/pull/23217)) | ||||||
|  | - Not able to make Material Request from Sales Order ([#23669](https://github.com/frappe/erpnext/pull/23669)) | ||||||
|  | - Capture advance payments in payment order ([#23256](https://github.com/frappe/erpnext/pull/23256)) | ||||||
|  | - Program and Course Enrollment fixes ([#23333](https://github.com/frappe/erpnext/pull/23333)) | ||||||
|  | - Cannot create asset if cwip disabled and account not set ([#23580](https://github.com/frappe/erpnext/pull/23580)) | ||||||
|  | - Cannot merge pos invoices with inclusive tax ([#23541](https://github.com/frappe/erpnext/pull/23541)) | ||||||
|  | - Do not allow Company as accounting dimension ([#23755](https://github.com/frappe/erpnext/pull/23755)) | ||||||
|  | - Set value of wrong Bank Account field in Payment Entry ([#22302](https://github.com/frappe/erpnext/pull/22302)) | ||||||
|  | - Reverse journal entry for multi-currency ([#23165](https://github.com/frappe/erpnext/pull/23165)) | ||||||
|  | - Updated integrations desk page ([#23772](https://github.com/frappe/erpnext/pull/23772)) | ||||||
|  | - Assessment Result child table not visible when accessed via Assessment Plan dashboard ([#22880](https://github.com/frappe/erpnext/pull/22880)) | ||||||
|  | - Conversion factor fixes in Stock Entry ([#23407](https://github.com/frappe/erpnext/pull/23407)) | ||||||
|  | - Total calculations for multi-currency RCM invoices ([#23072](https://github.com/frappe/erpnext/pull/23072)) | ||||||
|  | - Show accounts in financial statements upto level 20 ([#23718](https://github.com/frappe/erpnext/pull/23718)) | ||||||
|  | - Consolidated financial statement sums values into wrong parent ([#23288](https://github.com/frappe/erpnext/pull/23288)) | ||||||
|  | - Set SLA variance in seconds for Duration fieldtype ([#23765](https://github.com/frappe/erpnext/pull/23765)) | ||||||
|  | - Added missing reports on selling desk ([#23548](https://github.com/frappe/erpnext/pull/23548)) | ||||||
|  | - Fixed heading in the mobile view ([#23145](https://github.com/frappe/erpnext/pull/23145)) | ||||||
|  | - Misleading filters on Item tax Template Link field ([#22918](https://github.com/frappe/erpnext/pull/22918)) | ||||||
|  | - Do not consider opening entries for TDS calculation ([#23597](https://github.com/frappe/erpnext/pull/23597)) | ||||||
|  | - Attendance calendar map fix ([#23245](https://github.com/frappe/erpnext/pull/23245)) | ||||||
|  | - Post cancellation accounting entry on posting date instead of current ([#23361](https://github.com/frappe/erpnext/pull/23361)) | ||||||
|  | - Set Customer only if Contact is present ([#23704](https://github.com/frappe/erpnext/pull/23704)) | ||||||
|  | - Add Delivery Note Count in Sales Invoice Dashboard ([#23161](https://github.com/frappe/erpnext/pull/23161)) | ||||||
|  | - Breadcrumbs for Maintenance Visit and Schedule ([#23369](https://github.com/frappe/erpnext/pull/23369)) | ||||||
|  | - Raise Error on over receipt/consumption for sub-contracted PR ([#23195](https://github.com/frappe/erpnext/pull/23195)) | ||||||
|  | - Validate if company not set in the Payment Entry ([#23419](https://github.com/frappe/erpnext/pull/23419)) | ||||||
|  | - Ignore company and bank account doctype while deleting company transactions ([#22953](https://github.com/frappe/erpnext/pull/22953)) | ||||||
|  | - Sales funnel data is inconsistent ([#23110](https://github.com/frappe/erpnext/pull/23110)) | ||||||
|  | - Credit Limit Email not working ([#23059](https://github.com/frappe/erpnext/pull/23059)) | ||||||
|  | - Add Company in list fields to fetch for Expense Claim ([#23007](https://github.com/frappe/erpnext/pull/23007)) | ||||||
|  | - Issue form cleaned up and renamed Minutes to First Response field ([#23066](https://github.com/frappe/erpnext/pull/23066)) | ||||||
|  | - Quotation lost reason options fix ([#22814](https://github.com/frappe/erpnext/pull/22814)) | ||||||
|  | - Tax amounts in HSN Wise Outward summary ([#23076](https://github.com/frappe/erpnext/pull/23076)) | ||||||
|  | - Patient Appointment not able to save ([#23434](https://github.com/frappe/erpnext/pull/23434)) | ||||||
|  | - Removed Working Hours field from Company ([#23009](https://github.com/frappe/erpnext/pull/23009)) | ||||||
|  | - Added check-in time validation in the Inpatient Record - Transfer ([#22958](https://github.com/frappe/erpnext/pull/22958)) | ||||||
|  | - Handle Blank from/to range in Numeric Item Attribute ([#23483](https://github.com/frappe/erpnext/pull/23483)) | ||||||
|  | - Sequence Matcher error in Bank Reconciliation ([#23539](https://github.com/frappe/erpnext/pull/23539)) | ||||||
|  | - Fixed Conversion Factor rate for the BOM Exploded Item ([#23151](https://github.com/frappe/erpnext/pull/23151)) | ||||||
|  | - Payment Schedule not fetching ([#23476](https://github.com/frappe/erpnext/pull/23476)) | ||||||
|  | - Validate if removed Item Attributes exist in variant items ([#22911](https://github.com/frappe/erpnext/pull/22911)) | ||||||
|  | - Set default billing address for purchase documents ([#22950](https://github.com/frappe/erpnext/pull/22950)) | ||||||
|  | - Added help link in navbar settings ([#22943](https://github.com/frappe/erpnext/pull/22943)) | ||||||
|  | - Apply TDS on Purchase Invoice creation from Purchase Order and Purchase Receipt ([#23282](https://github.com/frappe/erpnext/pull/23282)) | ||||||
|  | - Education Module fixes ([#23714](https://github.com/frappe/erpnext/pull/23714)) | ||||||
|  | - Filter out cancelled entries in customer ledger summary ([#23205](https://github.com/frappe/erpnext/pull/23205)) | ||||||
|  | - Fiscal Year and Tax Rates for Italy ([#23623](https://github.com/frappe/erpnext/pull/23623)) | ||||||
|  | - Production Plan incorrect Work Order qty ([#23264](https://github.com/frappe/erpnext/pull/23264)) | ||||||
|  | - Added new filters in the Batch-wise Balance History report ([#23676](https://github.com/frappe/erpnext/pull/23676)) | ||||||
|  | - Update state code and union territory for Daman and Diu ([#22988](https://github.com/frappe/erpnext/pull/22988)) | ||||||
|  | - Set Stock UOM in item while creating Material Request from Stock Entry ([#23436](https://github.com/frappe/erpnext/pull/23436)) | ||||||
|  | - Sales Order to Purchase Order flow improvement ([#23357](https://github.com/frappe/erpnext/pull/23357)) | ||||||
|  | - Student Admission and Student Applicant fixes ([#23515](https://github.com/frappe/erpnext/pull/23515)) | ||||||
|  | - Loan disbursement amount validation ([#24000](https://github.com/frappe/erpnext/pull/24000)) | ||||||
|  | - Making company address read-only in delivery note ([#23890](https://github.com/frappe/erpnext/pull/23890)) | ||||||
|  | - BOM stock report color showing always red ([#23994](https://github.com/frappe/erpnext/pull/23994)) | ||||||
|  | - Added filter for customer field in Issue ([#24051](https://github.com/frappe/erpnext/pull/24051)) | ||||||
|  | - Added project link in timesheet form ([#23764](https://github.com/frappe/erpnext/pull/23764)) | ||||||
|  | - Update integrations desk page ([#23767](https://github.com/frappe/erpnext/pull/23767)) | ||||||
|  | - Place of supply change on address change ([#23941](https://github.com/frappe/erpnext/pull/23941)) | ||||||
|  | - TDS calculation, skip invoices with "Apply Tax Withholding Amount" has disabled ([#23672](https://github.com/frappe/erpnext/pull/23672)) | ||||||
|  | - Auto fetch serial nos with modified conversion factor ([#23854](https://github.com/frappe/erpnext/pull/23854)) | ||||||
|  | - Default cost center in item master not set in stock entry ([#23877](https://github.com/frappe/erpnext/pull/23877)) | ||||||
|  | - Incorrect de-link serial no and batch ([#23947](https://github.com/frappe/erpnext/pull/23947)) | ||||||
|  | - Accounting for internal transfer invoices within same company ([#24021](https://github.com/frappe/erpnext/pull/24021)) | ||||||
|  | - Multiple pricing rule with margin type as Percentage is not working ([#24205](https://github.com/frappe/erpnext/pull/24205)) | ||||||
|  | - Added Purchase Order to Global Search ([#24055](https://github.com/frappe/erpnext/pull/24055)) | ||||||
|  | - Cannot expand row in update items dialog ([#23839](https://github.com/frappe/erpnext/pull/23839)) | ||||||
|  | - Maintain stock can't be changed it there is product bundle ([#23989](https://github.com/frappe/erpnext/pull/23989)) | ||||||
|  | - SO to PO Mapping Issue ([#23820](https://github.com/frappe/erpnext/pull/23820)) | ||||||
|  | - Asset with value zero doesn't show up in fixed asset register ([#24091](https://github.com/frappe/erpnext/pull/24091)) | ||||||
|  | - Cannot save customer email & phone ([#23797](https://github.com/frappe/erpnext/pull/23797)) | ||||||
|  | - Incorrect balance value in stock balance report ([#24048](https://github.com/frappe/erpnext/pull/24048)) | ||||||
|  | - Payment Terms not fetched in Purchase Invoice from Purchase Receipt ([#23735](https://github.com/frappe/erpnext/pull/23735)) | ||||||
|  | - Fix for LMS Sign Up link ([#23743](https://github.com/frappe/erpnext/pull/23743)) | ||||||
|  | - Incorrect stock quantity if 'Allow Multiple Material Consumption… ([#24116](https://github.com/frappe/erpnext/pull/24116)) | ||||||
|  | - Added wrong absent days calculation in salary slip ([#23897](https://github.com/frappe/erpnext/pull/23897)) | ||||||
|  | - Purchase receipt to purchase invoice bill date mapping ([#23967](https://github.com/frappe/erpnext/pull/23967)) | ||||||
|  | - Overriding po ([#24022](https://github.com/frappe/erpnext/pull/24022)) | ||||||
|  | - Do not cancel reference document on Quality Inspection cancellation ([#24198](https://github.com/frappe/erpnext/pull/24198)) | ||||||
|  | - Get formatted value in 'taxes' print template ([#24035](https://github.com/frappe/erpnext/pull/24035)) | ||||||
|  | - Don't overrule Item Price via Pricing Rule Rate if 0 ([#23636](https://github.com/frappe/erpnext/pull/23636)) | ||||||
|  | - Job card error handling for operations field ([#23991](https://github.com/frappe/erpnext/pull/23991)) | ||||||
|  | - Validation for journal entry with 0 debit and credit values ([#23975](https://github.com/frappe/erpnext/pull/23975)) | ||||||
|  | - Check if customer exists in product listing ([#24030](https://github.com/frappe/erpnext/pull/24030)) | ||||||
|  | - Asset finance book posting date fix ([#23778](https://github.com/frappe/erpnext/pull/23778)) | ||||||
|  | - Same source and target tables in Status Updater's update query ([#24110](https://github.com/frappe/erpnext/pull/24110)) | ||||||
|  | - Asset finance book depreciation posting date fix ([#23833](https://github.com/frappe/erpnext/pull/23833)) | ||||||
|  | - Ignore exception during leave ledger creation from patch ([#24005](https://github.com/frappe/erpnext/pull/24005)) | ||||||
|  | - Added link of bank reconciliation and clearance in accounting desk page ([#23850](https://github.com/frappe/erpnext/pull/23850)) | ||||||
|  | - Sales invoice add button from sales order dashboard ([#24077](https://github.com/frappe/erpnext/pull/24077)) | ||||||
|  | - Incorrect calculation for consumed qty for subcontract item ([#23257](https://github.com/frappe/erpnext/pull/23257)) | ||||||
|  | - Incorrect required_qty in Production Planning Report ([#24074](https://github.com/frappe/erpnext/pull/24074)) | ||||||
|  | - Email digest user not found ([#23949](https://github.com/frappe/erpnext/pull/23949)) | ||||||
|  | - Delete Receive at Warehouse entry on cancellation of Send to War… ([#24115](https://github.com/frappe/erpnext/pull/24115)) | ||||||
|  | - Added TDS Payable account number and an error message ([#24065](https://github.com/frappe/erpnext/pull/24065)) | ||||||
|  | - Override field_map for job card gantt ([#24155](https://github.com/frappe/erpnext/pull/24155)) | ||||||
|  | - Old shopify order syncing date ([#23990](https://github.com/frappe/erpnext/pull/23990)) | ||||||
|  | - Shipping chanrges not sync in erpnext from shopify ([#24114](https://github.com/frappe/erpnext/pull/24114)) | ||||||
|  | - GSTR B2C report ([#24039](https://github.com/frappe/erpnext/pull/24039)) | ||||||
|  | - Ignore cancelled entries in stock balance report ([#23757](https://github.com/frappe/erpnext/pull/23757)) | ||||||
|  | - Stock ageing report not working ([#23923](https://github.com/frappe/erpnext/pull/23923)) | ||||||
|  | - Incorrect assign to in Maintenance Schedule  ([#23831](https://github.com/frappe/erpnext/pull/23831)) | ||||||
|  | - Improve UX of DATEV report ([#23892](https://github.com/frappe/erpnext/pull/23892)) | ||||||
|  | - Set SLA variance in seconds for Duration fieldtype ([#23765](https://github.com/frappe/erpnext/pull/23765)) | ||||||
|  | - dDouble exception in payroll ([#24078](https://github.com/frappe/erpnext/pull/24078)) | ||||||
|  | - Make asset dashboard charts public ([#23751](https://github.com/frappe/erpnext/pull/23751)) | ||||||
|  | - Don't copy terms and discount from SO to PO ([#23903](https://github.com/frappe/erpnext/pull/23903)) | ||||||
|  | - Ignore doctypes on company transaction delete ([#23864](https://github.com/frappe/erpnext/pull/23864)) | ||||||
|  | - Error handling in Upload Attendance  ([#23907](https://github.com/frappe/erpnext/pull/23907)) | ||||||
|  | - Tax template update on customer address change ([#24160](https://github.com/frappe/erpnext/pull/24160)) | ||||||
|  | - Not able to save bom ([#23910](https://github.com/frappe/erpnext/pull/23910)) | ||||||
|  | - Enable Allow Auto Repeat for standard doctypes having auto_repeat field ([#23776](https://github.com/frappe/erpnext/pull/23776)) | ||||||
|  | - Place of Supply fix in Sales Invoices ([#23785](https://github.com/frappe/erpnext/pull/23785)) | ||||||
|  | - Opening invoices in GSTR-1 report ([#24117](https://github.com/frappe/erpnext/pull/24117)) | ||||||
|  | - Partial serial no return issue ([#24208](https://github.com/frappe/erpnext/pull/24208)) | ||||||
|  | - Import taxjar globally in the taxjar_integration module ([#24027](https://github.com/frappe/erpnext/pull/24027)) | ||||||
|  | - Payroll attendance error ([#23887](https://github.com/frappe/erpnext/pull/23887)) | ||||||
|  | - Loan application link on creating loan ([#23937](https://github.com/frappe/erpnext/pull/23937)) | ||||||
|  | - POS item search includes non stock items ([#23914](https://github.com/frappe/erpnext/pull/23914)) | ||||||
|  | - Paid amount in Sales Invoice POS return resets to 0 ([#24057](https://github.com/frappe/erpnext/pull/24057)) | ||||||
|  | - Fiscal year can be shorter than 12 months ([#23838](https://github.com/frappe/erpnext/pull/23838)) | ||||||
|  | - Loan repayment type option remove ([#23582](https://github.com/frappe/erpnext/pull/23582)) | ||||||
|  | - Item wise tax calculation ([#23744](https://github.com/frappe/erpnext/pull/23744)) | ||||||
|  | - Enabling track changes for stock settings ([#23982](https://github.com/frappe/erpnext/pull/23982)) | ||||||
|  | - Added link of bank reconciliation and clearance in accounting desk page ([#23809](https://github.com/frappe/erpnext/pull/23809)) | ||||||
|  | - Location data on Asset to use command(make_demo) ([#23825](https://github.com/frappe/erpnext/pull/23825)) | ||||||
|  | - Handle Account and Item None not found in Opening Invoice Creation Tool ([#23559](https://github.com/frappe/erpnext/pull/23559)) | ||||||
|  | - Multiple subcontracting issues ([#23662](https://github.com/frappe/erpnext/pull/23662)) | ||||||
|  | - Sequence id override with workstation column ([#23810](https://github.com/frappe/erpnext/pull/23810)) | ||||||
|  | - Leave policy dashboard fix and roles ([#24170](https://github.com/frappe/erpnext/pull/24170)) | ||||||
|  | - Scan barcode does not update barcode item field in sales order ([#24090](https://github.com/frappe/erpnext/pull/24090)) | ||||||
|  | - Item price duplicate checking ([#23408](https://github.com/frappe/erpnext/pull/23408)) | ||||||
|  | - Tax template update on supplier change for India ([#24060](https://github.com/frappe/erpnext/pull/24060)) | ||||||
|  | - Consumed qty logic for subcontracted raw materials ([#23314](https://github.com/frappe/erpnext/pull/23314)) | ||||||
|  | - Finance book not getting added in journal Entry of asset value adjustment ([#24100](https://github.com/frappe/erpnext/pull/24100)) | ||||||
|  | - Set proper state code in ewaybill JSON when GST category is SEZ ([#23953](https://github.com/frappe/erpnext/pull/23953)) | ||||||
|  | - Copying po no when mapping doc ([#23729](https://github.com/frappe/erpnext/pull/23729)) | ||||||
|  | - Duplicate items validation for POS Invoice when allow multiple items is disabled ([#23896](https://github.com/frappe/erpnext/pull/23896)) | ||||||
|  | - Do not allow Company as accounting dimension ([#23749](https://github.com/frappe/erpnext/pull/23749)) | ||||||
|  | - Validation for duplicate Tax Category ([#23978](https://github.com/frappe/erpnext/pull/23978)) | ||||||
|  | - Therapy plan and session fixes ([#23817](https://github.com/frappe/erpnext/pull/23817)) | ||||||
|  | - Pricing rule with transaction not working for additional product ([#24053](https://github.com/frappe/erpnext/pull/24053)) | ||||||
|  | - Inpatient Medication Order and Entry fixes ([#23799](https://github.com/frappe/erpnext/pull/23799)) | ||||||
|  | - Avoid using SQL query to get fiscal year dates ([#24050](https://github.com/frappe/erpnext/pull/24050)) | ||||||
|  | - Auto Statewise gst tax template ([#23832](https://github.com/frappe/erpnext/pull/23832)) | ||||||
|  | - On save sequence id column override with workstation ([#23812](https://github.com/frappe/erpnext/pull/23812)) | ||||||
|  | - Multiple pricing rules are not working on selling side ([#22711](https://github.com/frappe/erpnext/pull/22711)) | ||||||
|  | - Salary slip popup error ([#24192](https://github.com/frappe/erpnext/pull/24192)) | ||||||
|  | - Multiple pricing rule with margin type as Percentage is not working ([#24204](https://github.com/frappe/erpnext/pull/24204)) | ||||||
|  | - Allow statistical component in salary structure. ([#24424](https://github.com/frappe/erpnext/pull/24424)) | ||||||
|  | - Set current asset value before calculating difference amount ([#24119](https://github.com/frappe/erpnext/pull/24119)) | ||||||
|  | - To use Stock UoM in BOM Stock Report ([#24339](https://github.com/frappe/erpnext/pull/24339)) | ||||||
|  | - Accounting entries of asset when submitting purchase receipt ([#24191](https://github.com/frappe/erpnext/pull/24191)) | ||||||
|  | - Batch/Serial Selector for Scanned Batched Item ([#24338](https://github.com/frappe/erpnext/pull/24338)) | ||||||
|  | - Link timesheets with corresponding projects ([#24346](https://github.com/frappe/erpnext/pull/24346)) | ||||||
|  | - Material request wrong status issue ([#24019](https://github.com/frappe/erpnext/pull/24019)) | ||||||
|  | - UX issues in e-invoicing ([#24358](https://github.com/frappe/erpnext/pull/24358)) | ||||||
|  | - Company Wise Valuation Rate for RM in BOM ([#24324](https://github.com/frappe/erpnext/pull/24324)) | ||||||
|  | - Stock ageing should not take cancelled stock entries. ([#24437](https://github.com/frappe/erpnext/pull/24437)) | ||||||
|  | - Partial loan security unpledging ([#24252](https://github.com/frappe/erpnext/pull/24252)) | ||||||
|  | - Asset depreciation ledger ([#24226](https://github.com/frappe/erpnext/pull/24226)) | ||||||
|  | - Back Update from QC based on Batch No ([#24329](https://github.com/frappe/erpnext/pull/24329)) | ||||||
|  | - Fix for not having fiscal year while creating new company ([#24130](https://github.com/frappe/erpnext/pull/24130)) | ||||||
|  | - E-invoice print format not showing other charges ([#24474](https://github.com/frappe/erpnext/pull/24474)) | ||||||
|  | - Tax template update on customer address change ([#24146](https://github.com/frappe/erpnext/pull/24146)) | ||||||
|  | - Do not manufacture same serial no multiple times ([#24164](https://github.com/frappe/erpnext/pull/24164)) | ||||||
|  | - Ignore group cost center validation for period closing voucher ([#24375](https://github.com/frappe/erpnext/pull/24375)) | ||||||
|  | - Partial serial no return issue ([#24207](https://github.com/frappe/erpnext/pull/24207)) | ||||||
|  | - GSTR-1 double entry issue ([#24376](https://github.com/frappe/erpnext/pull/24376)) | ||||||
|  | - Not able to create dunning from sales invoice ([#24349](https://github.com/frappe/erpnext/pull/24349)) | ||||||
|  | - Set company in leave allocation and leave ledger entry ([#24296](https://github.com/frappe/erpnext/pull/24296)) | ||||||
|  | - Allow leave policy assignment to be canceled. ([#24265](https://github.com/frappe/erpnext/pull/24265)) | ||||||
|  | - Removed all day event from shift assignment calendar ([#24397](https://github.com/frappe/erpnext/pull/24397)) | ||||||
|  | - Tax calculation on salary slip for the first month ([#24272](https://github.com/frappe/erpnext/pull/24272)) | ||||||
|  | - Validate tax template for tax category ([#24402](https://github.com/frappe/erpnext/pull/24402)) | ||||||
|  | - Numeric/Non-numeric QI UX ([#24517](https://github.com/frappe/erpnext/pull/24517)) | ||||||
|  | - Finished good produced qty validation ([#24220](https://github.com/frappe/erpnext/pull/24220)) | ||||||
|  | - Incorrect serial no in the subcontracted purchase receipt ([#24354](https://github.com/frappe/erpnext/pull/24354)) | ||||||
|  | - Don't validate warehouse values between Material Request and Stock Entry ([#24294](https://github.com/frappe/erpnext/pull/24294)) | ||||||
|  | - Don't cancel job card if manufacturing entry has made ([#24063](https://github.com/frappe/erpnext/pull/24063)) | ||||||
|  | - Subscription prepaid date validation ([#24356](https://github.com/frappe/erpnext/pull/24356)) | ||||||
|  | - Payment Period based on invoice date report fix/refactor ([#24378](https://github.com/frappe/erpnext/pull/24378)) | ||||||
|  | - Drop ship partial order fixed ([#24072](https://github.com/frappe/erpnext/pull/24072)) | ||||||
|  | - Payment entry multi-currency issue ([#24332](https://github.com/frappe/erpnext/pull/24332)) | ||||||
|  | - Multiple pricing rule issue ([#24515](https://github.com/frappe/erpnext/pull/24515)) | ||||||
|  | - Last purchase rate not updating when voucher cancelled if only one voucher is present ([#24322](https://github.com/frappe/erpnext/pull/24322)) | ||||||
|  | - Do not cancel reference document on Quality Inspection cancellation ([#24197](https://github.com/frappe/erpnext/pull/24197)) | ||||||
|  | - Refactored fetching & validating address from erpnext rather than gst portal ([#24297](https://github.com/frappe/erpnext/pull/24297)) | ||||||
|  | - Opportunity Status fix ([#22944](https://github.com/frappe/erpnext/pull/22944)) | ||||||
|  | - Fixed stock and account balance syncing ([#24644](https://github.com/frappe/erpnext/pull/24644)) | ||||||
|  | - Fixed incorrect stock ledger qty in the stock ledger report and bin ([#24649](https://github.com/frappe/erpnext/pull/24649)) | ||||||
|  | - Fixed Consolidated Financial Statement report ([#24580](https://github.com/frappe/erpnext/pull/24580)) | ||||||
|  | - Repost incompleted backdated transactions ([#24991](https://github.com/frappe/erpnext/pull/24991)) | ||||||
|  | - Unequal debit and credit issue on RCM Invoice ([#24838](https://github.com/frappe/erpnext/pull/24838)) | ||||||
|  | - Period list for exponential smoothing forecasting report ([#24983](https://github.com/frappe/erpnext/pull/24983)) | ||||||
|  | - POS Opening Entry with empty balance detail rows ([#24891](https://github.com/frappe/erpnext/pull/24891)) | ||||||
|  | - Use account_name only in consolidated report ([#24840](https://github.com/frappe/erpnext/pull/24840)) | ||||||
|  | - Validation of job card in stock entry ([#24882](https://github.com/frappe/erpnext/pull/24882)) | ||||||
|  | - Incorrect Nil Exempt and Non GST amount in GSTR3B report ([#24918](https://github.com/frappe/erpnext/pull/24918)) | ||||||
|  | - TDS check getting checked after reload ([#24973](https://github.com/frappe/erpnext/pull/24973)) | ||||||
|  | - Membership and Donation API fixes ([#24900](https://github.com/frappe/erpnext/pull/24900)) | ||||||
|  | - Allow zero valuation in stock reconciliation ([#24985](https://github.com/frappe/erpnext/pull/24985)) | ||||||
|  | - Simplified logic for additional salary ([#24907](https://github.com/frappe/erpnext/pull/24907)) | ||||||
|  | - Allow to select item code in batch naming ([#24825](https://github.com/frappe/erpnext/pull/24825)) | ||||||
|  | - Membership renewal validation (#24963) ([#24964](https://github.com/frappe/erpnext/pull/24964)) | ||||||
|  | </details> | ||||||
| @ -659,6 +659,7 @@ class AccountsController(TransactionBase): | |||||||
| 					'dr_or_cr': dr_or_cr, | 					'dr_or_cr': dr_or_cr, | ||||||
| 					'unadjusted_amount': flt(d.advance_amount), | 					'unadjusted_amount': flt(d.advance_amount), | ||||||
| 					'allocated_amount': flt(d.allocated_amount), | 					'allocated_amount': flt(d.allocated_amount), | ||||||
|  | 					'precision': d.precision('advance_amount'), | ||||||
| 					'exchange_rate': (self.conversion_rate | 					'exchange_rate': (self.conversion_rate | ||||||
| 						if self.party_account_currency != self.company_currency else 1), | 						if self.party_account_currency != self.company_currency else 1), | ||||||
| 					'grand_total': (self.base_grand_total | 					'grand_total': (self.base_grand_total | ||||||
|  | |||||||
| @ -406,8 +406,7 @@ class StockController(AccountsController): | |||||||
| 	def set_rate_of_stock_uom(self): | 	def set_rate_of_stock_uom(self): | ||||||
| 		if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]: | 		if self.doctype in ["Purchase Receipt", "Purchase Invoice", "Purchase Order", "Sales Invoice", "Sales Order", "Delivery Note", "Quotation"]: | ||||||
| 			for d in self.get("items"): | 			for d in self.get("items"): | ||||||
| 				if d.conversion_factor: | 				d.stock_uom_rate = d.rate / (d.conversion_factor or 1) | ||||||
| 					d.stock_uom_rate = d.rate / d.conversion_factor |  | ||||||
| 
 | 
 | ||||||
| 	def validate_internal_transfer(self): | 	def validate_internal_transfer(self): | ||||||
| 		if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ | 		if self.doctype in ('Sales Invoice', 'Delivery Note', 'Purchase Invoice', 'Purchase Receipt') \ | ||||||
|  | |||||||
| @ -113,10 +113,12 @@ class calculate_taxes_and_totals(object): | |||||||
| 					item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) | 					item.rate_with_margin, item.base_rate_with_margin = self.calculate_margin(item) | ||||||
| 					if flt(item.rate_with_margin) > 0: | 					if flt(item.rate_with_margin) > 0: | ||||||
| 						item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) | 						item.rate = flt(item.rate_with_margin * (1.0 - (item.discount_percentage / 100.0)), item.precision("rate")) | ||||||
|  | 
 | ||||||
| 						if item.discount_amount and not item.discount_percentage: | 						if item.discount_amount and not item.discount_percentage: | ||||||
| 							item.rate -= item.discount_amount | 							item.rate = item.rate_with_margin - item.discount_amount | ||||||
| 						else: | 						else: | ||||||
| 							item.discount_amount = item.rate_with_margin - item.rate | 							item.discount_amount = item.rate_with_margin - item.rate | ||||||
|  | 
 | ||||||
| 					elif flt(item.price_list_rate) > 0: | 					elif flt(item.price_list_rate) > 0: | ||||||
| 						item.discount_amount = item.price_list_rate - item.rate | 						item.discount_amount = item.price_list_rate - item.rate | ||||||
| 				elif flt(item.price_list_rate) > 0 and not item.discount_amount: | 				elif flt(item.price_list_rate) > 0 and not item.discount_amount: | ||||||
| @ -147,7 +149,9 @@ class calculate_taxes_and_totals(object): | |||||||
| 				validate_taxes_and_charges(tax) | 				validate_taxes_and_charges(tax) | ||||||
| 				validate_inclusive_tax(tax, self.doc) | 				validate_inclusive_tax(tax, self.doc) | ||||||
| 
 | 
 | ||||||
|  | 			if not self.doc.get('is_consolidated'): | ||||||
| 				tax.item_wise_tax_detail = {} | 				tax.item_wise_tax_detail = {} | ||||||
|  | 
 | ||||||
| 			tax_fields = ["total", "tax_amount_after_discount_amount", | 			tax_fields = ["total", "tax_amount_after_discount_amount", | ||||||
| 				"tax_amount_for_current_item", "grand_total_for_current_item", | 				"tax_amount_for_current_item", "grand_total_for_current_item", | ||||||
| 				"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] | 				"tax_fraction_for_current_item", "grand_total_fraction_for_current_item"] | ||||||
| @ -338,6 +342,8 @@ class calculate_taxes_and_totals(object): | |||||||
| 			current_tax_amount = tax_rate * item.qty | 			current_tax_amount = tax_rate * item.qty | ||||||
| 
 | 
 | ||||||
| 		current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount) | 		current_tax_amount = self.get_final_current_tax_amount(tax, current_tax_amount) | ||||||
|  | 
 | ||||||
|  | 		if not self.doc.get("is_consolidated"): | ||||||
| 			self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) | 			self.set_item_wise_tax(item, tax, tax_rate, current_tax_amount) | ||||||
| 
 | 
 | ||||||
| 		return current_tax_amount | 		return current_tax_amount | ||||||
| @ -440,6 +446,7 @@ class calculate_taxes_and_totals(object): | |||||||
| 			self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) | 			self._set_in_company_currency(self.doc, ["rounding_adjustment", "rounded_total"]) | ||||||
| 
 | 
 | ||||||
| 	def _cleanup(self): | 	def _cleanup(self): | ||||||
|  | 		if not self.doc.get('is_consolidated'): | ||||||
| 			for tax in self.doc.get("taxes"): | 			for tax in self.doc.get("taxes"): | ||||||
| 				tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) | 				tax.item_wise_tax_detail = json.dumps(tax.item_wise_tax_detail, separators=(',', ':')) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -25,7 +25,7 @@ def get_transaction_list(doctype, txt=None, filters=None, limit_start=0, limit_p | |||||||
| 
 | 
 | ||||||
| 	if not filters: filters = [] | 	if not filters: filters = [] | ||||||
| 
 | 
 | ||||||
| 	if doctype in ['Supplier Quotation', 'Purchase Invoice', 'Quotation']: | 	if doctype in ['Supplier Quotation', 'Purchase Invoice']: | ||||||
| 		filters.append((doctype, 'docstatus', '<', 2)) | 		filters.append((doctype, 'docstatus', '<', 2)) | ||||||
| 	else: | 	else: | ||||||
| 		filters.append((doctype, 'docstatus', '=', 1)) | 		filters.append((doctype, 'docstatus', '=', 1)) | ||||||
|  | |||||||
| @ -22,7 +22,7 @@ class ShopifySettings(unittest.TestCase): | |||||||
| 			frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) | 			frappe.db.set_value('Stock Settings', None, 'allow_negative_stock', 1) | ||||||
| 
 | 
 | ||||||
| 		# use the fixture data | 		# use the fixture data | ||||||
| 		import_doc(frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json")) | 		import_doc(path=frappe.get_app_path("erpnext", "erpnext_integrations/doctype/shopify_settings/test_data/custom_field.json")) | ||||||
| 
 | 
 | ||||||
| 		frappe.reload_doctype("Customer") | 		frappe.reload_doctype("Customer") | ||||||
| 		frappe.reload_doctype("Sales Order") | 		frappe.reload_doctype("Sales Order") | ||||||
|  | |||||||
| @ -39,11 +39,13 @@ frappe.ui.form.on('Patient Assessment', { | |||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	set_score_range: function(frm) { | 	set_score_range: function(frm) { | ||||||
| 		let options = []; | 		let options = ['']; | ||||||
| 		for(let i = frm.doc.scale_min; i <= frm.doc.scale_max; i++) { | 		for(let i = frm.doc.scale_min; i <= frm.doc.scale_max; i++) { | ||||||
| 			options.push(i); | 			options.push(i); | ||||||
| 		} | 		} | ||||||
| 		frappe.meta.get_docfield('Patient Assessment Sheet', 'score', frm.doc.name).options = [''].concat(options); | 		frm.fields_dict.assessment_sheet.grid.update_docfield_property( | ||||||
|  | 			'score', 'options', options | ||||||
|  | 		); | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	calculate_total_score: function(frm, cdt, cdn) { | 	calculate_total_score: function(frm, cdt, cdn) { | ||||||
|  | |||||||
| @ -58,8 +58,12 @@ frappe.ui.form.on('Therapy Plan', { | |||||||
| 		} | 		} | ||||||
| 
 | 
 | ||||||
| 		if (frm.doc.therapy_plan_template) { | 		if (frm.doc.therapy_plan_template) { | ||||||
| 			frappe.meta.get_docfield('Therapy Plan Detail', 'therapy_type', frm.doc.name).read_only = 1; | 			frm.fields_dict.therapy_plan_details.grid.update_docfield_property( | ||||||
| 			frappe.meta.get_docfield('Therapy Plan Detail', 'no_of_sessions', frm.doc.name).read_only = 1; | 				'therapy_type', 'read_only', 1 | ||||||
|  | 			); | ||||||
|  | 			frm.fields_dict.therapy_plan_details.grid.update_docfield_property( | ||||||
|  | 				'no_of_sessions', 'read_only', 1 | ||||||
|  | 			); | ||||||
| 		} | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -262,7 +262,8 @@ doc_events = { | |||||||
| 		], | 		], | ||||||
| 		"on_trash": "erpnext.regional.check_deletion_permission", | 		"on_trash": "erpnext.regional.check_deletion_permission", | ||||||
| 		"validate": [ | 		"validate": [ | ||||||
| 			"erpnext.regional.india.utils.validate_document_name" | 			"erpnext.regional.india.utils.validate_document_name", | ||||||
|  | 			"erpnext.regional.india.utils.update_taxable_values" | ||||||
| 		] | 		] | ||||||
| 	}, | 	}, | ||||||
| 	"Purchase Invoice": { | 	"Purchase Invoice": { | ||||||
|  | |||||||
| @ -35,7 +35,8 @@ class Attendance(Document): | |||||||
| 				and docstatus != 2 | 				and docstatus != 2 | ||||||
| 		""", (self.employee, getdate(self.attendance_date), self.name)) | 		""", (self.employee, getdate(self.attendance_date), self.name)) | ||||||
| 		if res: | 		if res: | ||||||
| 			frappe.throw(_("Attendance for employee {0} is already marked").format(self.employee)) | 			frappe.throw(_("Attendance for employee {0} is already marked for the date {1}").format( | ||||||
|  | 				frappe.bold(self.employee), frappe.bold(self.attendance_date))) | ||||||
| 
 | 
 | ||||||
| 	def check_leave_record(self): | 	def check_leave_record(self): | ||||||
| 		leave_record = frappe.db.sql(""" | 		leave_record = frappe.db.sql(""" | ||||||
|  | |||||||
| @ -200,7 +200,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 14:42:47.321368", |  "modified": "2021-03-31 22:31:53.746659", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "HR", |  "module": "HR", | ||||||
|  "name": "Employee Advance", |  "name": "Employee Advance", | ||||||
|  | |||||||
| @ -154,7 +154,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 14:45:27.948207", |  "modified": "2021-03-31 22:32:55.492327", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "HR", |  "module": "HR", | ||||||
|  "name": "Leave Encashment", |  "name": "Leave Encashment", | ||||||
|  | |||||||
| @ -23,6 +23,7 @@ | |||||||
|   "rate_of_interest", |   "rate_of_interest", | ||||||
|   "is_secured_loan", |   "is_secured_loan", | ||||||
|   "disbursement_date", |   "disbursement_date", | ||||||
|  |   "closure_date", | ||||||
|   "disbursed_amount", |   "disbursed_amount", | ||||||
|   "column_break_11", |   "column_break_11", | ||||||
|   "maximum_loan_amount", |   "maximum_loan_amount", | ||||||
| @ -348,12 +349,18 @@ | |||||||
|    "no_copy": 1, |    "no_copy": 1, | ||||||
|    "options": "Company:company:default_currency", |    "options": "Company:company:default_currency", | ||||||
|    "read_only": 1 |    "read_only": 1 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "closure_date", | ||||||
|  |    "fieldtype": "Date", | ||||||
|  |    "label": "Closure Date", | ||||||
|  |    "read_only": 1 | ||||||
|   } |   } | ||||||
|  ], |  ], | ||||||
|  "index_web_pages_for_search": 1, |  "index_web_pages_for_search": 1, | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2020-11-24 12:27:23.208240", |  "modified": "2021-04-10 09:28:21.946972", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Loan Management", |  "module": "Loan Management", | ||||||
|  "name": "Loan", |  "name": "Loan", | ||||||
|  | |||||||
| @ -523,33 +523,7 @@ class TestLoan(unittest.TestCase): | |||||||
| 		self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0)) | 		self.assertEqual(flt(repayment_entry.total_interest_paid, 0), flt(interest_amount, 0)) | ||||||
| 
 | 
 | ||||||
| 	def test_penalty(self): | 	def test_penalty(self): | ||||||
| 		pledge = [{ | 		loan, amounts = create_loan_scenario_for_penalty(self) | ||||||
| 			"loan_security": "Test Security 1", |  | ||||||
| 			"qty": 4000.00 |  | ||||||
| 		}] |  | ||||||
| 
 |  | ||||||
| 		loan_application = create_loan_application('_Test Company', self.applicant2, 'Demand Loan', pledge) |  | ||||||
| 		create_pledge(loan_application) |  | ||||||
| 
 |  | ||||||
| 		loan = create_demand_loan(self.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') |  | ||||||
| 		loan.submit() |  | ||||||
| 
 |  | ||||||
| 		self.assertEquals(loan.loan_amount, 1000000) |  | ||||||
| 
 |  | ||||||
| 		first_date = '2019-10-01' |  | ||||||
| 		last_date = '2019-10-30' |  | ||||||
| 
 |  | ||||||
| 		make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) |  | ||||||
| 		process_loan_interest_accrual_for_demand_loans(posting_date = last_date) |  | ||||||
| 
 |  | ||||||
| 		amounts = calculate_amounts(loan.name, add_days(last_date, 1)) |  | ||||||
| 		paid_amount = amounts['interest_amount']/2 |  | ||||||
| 
 |  | ||||||
| 		repayment_entry = create_repayment_entry(loan.name, self.applicant2, add_days(last_date, 5), |  | ||||||
| 			paid_amount) |  | ||||||
| 
 |  | ||||||
| 		repayment_entry.submit() |  | ||||||
| 
 |  | ||||||
| 		# 30 days - grace period | 		# 30 days - grace period | ||||||
| 		penalty_days = 30 - 4 | 		penalty_days = 30 - 4 | ||||||
| 		penalty_applicable_amount = flt(amounts['interest_amount']/2) | 		penalty_applicable_amount = flt(amounts['interest_amount']/2) | ||||||
| @ -559,8 +533,28 @@ class TestLoan(unittest.TestCase): | |||||||
| 		calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual', | 		calculated_penalty_amount = frappe.db.get_value('Loan Interest Accrual', | ||||||
| 			{'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount') | 			{'process_loan_interest_accrual': process, 'loan': loan.name}, 'penalty_amount') | ||||||
| 
 | 
 | ||||||
|  | 		self.assertEquals(loan.loan_amount, 1000000) | ||||||
| 		self.assertEquals(calculated_penalty_amount, penalty_amount) | 		self.assertEquals(calculated_penalty_amount, penalty_amount) | ||||||
| 
 | 
 | ||||||
|  | 	def test_penalty_repayment(self): | ||||||
|  | 		loan, dummy = create_loan_scenario_for_penalty(self) | ||||||
|  | 		amounts = calculate_amounts(loan.name, '2019-11-30 00:00:00') | ||||||
|  | 
 | ||||||
|  | 		first_penalty = 10000 | ||||||
|  | 		second_penalty = amounts['penalty_amount'] - 10000 | ||||||
|  | 
 | ||||||
|  | 		repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:00', 10000) | ||||||
|  | 		repayment_entry.submit() | ||||||
|  | 
 | ||||||
|  | 		amounts = calculate_amounts(loan.name, '2019-11-30 00:00:01') | ||||||
|  | 		self.assertEquals(amounts['penalty_amount'], second_penalty) | ||||||
|  | 
 | ||||||
|  | 		repayment_entry = create_repayment_entry(loan.name, self.applicant2, '2019-11-30 00:00:01', second_penalty) | ||||||
|  | 		repayment_entry.submit() | ||||||
|  | 
 | ||||||
|  | 		amounts = calculate_amounts(loan.name, '2019-11-30 00:00:02') | ||||||
|  | 		self.assertEquals(amounts['penalty_amount'], 0) | ||||||
|  | 
 | ||||||
| 	def test_loan_write_off_limit(self): | 	def test_loan_write_off_limit(self): | ||||||
| 		pledge = [{ | 		pledge = [{ | ||||||
| 			"loan_security": "Test Security 1", | 			"loan_security": "Test Security 1", | ||||||
| @ -651,6 +645,32 @@ class TestLoan(unittest.TestCase): | |||||||
| 		amounts = calculate_amounts(loan.name, add_days(last_date, 5)) | 		amounts = calculate_amounts(loan.name, add_days(last_date, 5)) | ||||||
| 		self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0) | 		self.assertEquals(flt(amounts['pending_principal_amount'], 0), 0) | ||||||
| 
 | 
 | ||||||
|  | def create_loan_scenario_for_penalty(doc): | ||||||
|  | 	pledge = [{ | ||||||
|  | 		"loan_security": "Test Security 1", | ||||||
|  | 		"qty": 4000.00 | ||||||
|  | 	}] | ||||||
|  | 
 | ||||||
|  | 	loan_application = create_loan_application('_Test Company', doc.applicant2, 'Demand Loan', pledge) | ||||||
|  | 	create_pledge(loan_application) | ||||||
|  | 	loan = create_demand_loan(doc.applicant2, "Demand Loan", loan_application, posting_date='2019-10-01') | ||||||
|  | 	loan.submit() | ||||||
|  | 
 | ||||||
|  | 	first_date = '2019-10-01' | ||||||
|  | 	last_date = '2019-10-30' | ||||||
|  | 
 | ||||||
|  | 	make_loan_disbursement_entry(loan.name, loan.loan_amount, disbursement_date=first_date) | ||||||
|  | 	process_loan_interest_accrual_for_demand_loans(posting_date = last_date) | ||||||
|  | 
 | ||||||
|  | 	amounts = calculate_amounts(loan.name, add_days(last_date, 1)) | ||||||
|  | 	paid_amount = amounts['interest_amount']/2 | ||||||
|  | 
 | ||||||
|  | 	repayment_entry = create_repayment_entry(loan.name, doc.applicant2, add_days(last_date, 5), | ||||||
|  | 		paid_amount) | ||||||
|  | 
 | ||||||
|  | 	repayment_entry.submit() | ||||||
|  | 
 | ||||||
|  | 	return loan, amounts | ||||||
| 
 | 
 | ||||||
| def create_loan_accounts(): | def create_loan_accounts(): | ||||||
| 	if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): | 	if not frappe.db.exists("Account", "Loans and Advances (Assets) - _TC"): | ||||||
|  | |||||||
| @ -20,6 +20,10 @@ | |||||||
|   "cost_center", |   "cost_center", | ||||||
|   "customer_details_section", |   "customer_details_section", | ||||||
|   "bank_account", |   "bank_account", | ||||||
|  |   "disbursement_references_section", | ||||||
|  |   "reference_date", | ||||||
|  |   "column_break_17", | ||||||
|  |   "reference_number", | ||||||
|   "amended_from" |   "amended_from" | ||||||
|  ], |  ], | ||||||
|  "fields": [ |  "fields": [ | ||||||
| @ -126,12 +130,31 @@ | |||||||
|   { |   { | ||||||
|    "fieldname": "column_break_8", |    "fieldname": "column_break_8", | ||||||
|    "fieldtype": "Column Break" |    "fieldtype": "Column Break" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "disbursement_references_section", | ||||||
|  |    "fieldtype": "Section Break", | ||||||
|  |    "label": "Disbursement References" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "reference_date", | ||||||
|  |    "fieldtype": "Date", | ||||||
|  |    "label": "Reference Date" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "column_break_17", | ||||||
|  |    "fieldtype": "Column Break" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "reference_number", | ||||||
|  |    "fieldtype": "Data", | ||||||
|  |    "label": "Reference Number" | ||||||
|   } |   } | ||||||
|  ], |  ], | ||||||
|  "index_web_pages_for_search": 1, |  "index_web_pages_for_search": 1, | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2020-11-06 10:04:30.882322", |  "modified": "2021-04-10 10:03:41.502210", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Loan Management", |  "module": "Loan Management", | ||||||
|  "name": "Loan Disbursement", |  "name": "Loan Disbursement", | ||||||
|  | |||||||
| @ -239,14 +239,16 @@ | |||||||
|   { |   { | ||||||
|    "fieldname": "total_penalty_paid", |    "fieldname": "total_penalty_paid", | ||||||
|    "fieldtype": "Currency", |    "fieldtype": "Currency", | ||||||
|  |    "hidden": 1, | ||||||
|    "label": "Total Penalty Paid", |    "label": "Total Penalty Paid", | ||||||
|    "options": "Company:company:default_currency" |    "options": "Company:company:default_currency", | ||||||
|  |    "read_only": 1 | ||||||
|   } |   } | ||||||
|  ], |  ], | ||||||
|  "index_web_pages_for_search": 1, |  "index_web_pages_for_search": 1, | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-04-05 13:45:19.137896", |  "modified": "2021-04-10 10:00:31.859076", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Loan Management", |  "module": "Loan Management", | ||||||
|  "name": "Loan Repayment", |  "name": "Loan Repayment", | ||||||
|  | |||||||
| @ -75,7 +75,7 @@ class LoanRepayment(AccountsController): | |||||||
| 			"docstatus": 1, "against_loan": self.against_loan}, 'posting_date') | 			"docstatus": 1, "against_loan": self.against_loan}, 'posting_date') | ||||||
| 
 | 
 | ||||||
| 		if future_repayment_date: | 		if future_repayment_date: | ||||||
| 			frappe.throw("Repayment already made till date {0}".format(getdate(future_repayment_date))) | 			frappe.throw("Repayment already made till date {0}".format(get_datetime(future_repayment_date))) | ||||||
| 
 | 
 | ||||||
| 	def validate_amount(self): | 	def validate_amount(self): | ||||||
| 		precision = cint(frappe.db.get_default("currency_precision")) or 2 | 		precision = cint(frappe.db.get_default("currency_precision")) or 2 | ||||||
| @ -83,10 +83,6 @@ class LoanRepayment(AccountsController): | |||||||
| 		if not self.amount_paid: | 		if not self.amount_paid: | ||||||
| 			frappe.throw(_("Amount paid cannot be zero")) | 			frappe.throw(_("Amount paid cannot be zero")) | ||||||
| 
 | 
 | ||||||
| 		if not self.shortfall_amount and self.amount_paid < self.penalty_amount: |  | ||||||
| 			msg = _("Paid amount cannot be less than {0}").format(self.penalty_amount) |  | ||||||
| 			frappe.throw(msg) |  | ||||||
| 
 |  | ||||||
| 	def book_unaccrued_interest(self): | 	def book_unaccrued_interest(self): | ||||||
| 		precision = cint(frappe.db.get_default("currency_precision")) or 2 | 		precision = cint(frappe.db.get_default("currency_precision")) or 2 | ||||||
| 		if self.total_interest_paid > self.interest_payable: | 		if self.total_interest_paid > self.interest_payable: | ||||||
| @ -231,6 +227,14 @@ class LoanRepayment(AccountsController): | |||||||
| 		gle_map = [] | 		gle_map = [] | ||||||
| 		loan_details = frappe.get_doc("Loan", self.against_loan) | 		loan_details = frappe.get_doc("Loan", self.against_loan) | ||||||
| 
 | 
 | ||||||
|  | 		if self.shortfall_amount and self.amount_paid > self.shortfall_amount: | ||||||
|  | 			remarks = _("Shortfall Repayment of {0}.\nRepayment against Loan: {1}").format(self.shortfall_amount, | ||||||
|  | 				self.against_loan) | ||||||
|  | 		elif self.shortfall_amount: | ||||||
|  | 			remarks = _("Shortfall Repayment of {0}").format(self.shortfall_amount) | ||||||
|  | 		else: | ||||||
|  | 			remarks = _("Repayment against Loan: ") + self.against_loan | ||||||
|  | 
 | ||||||
| 		if self.total_penalty_paid: | 		if self.total_penalty_paid: | ||||||
| 			gle_map.append( | 			gle_map.append( | ||||||
| 				self.get_gl_dict({ | 				self.get_gl_dict({ | ||||||
| @ -271,7 +275,7 @@ class LoanRepayment(AccountsController): | |||||||
| 				"debit_in_account_currency": self.amount_paid, | 				"debit_in_account_currency": self.amount_paid, | ||||||
| 				"against_voucher_type": "Loan", | 				"against_voucher_type": "Loan", | ||||||
| 				"against_voucher": self.against_loan, | 				"against_voucher": self.against_loan, | ||||||
| 				"remarks": _("Repayment against Loan: ") + self.against_loan, | 				"remarks": remarks, | ||||||
| 				"cost_center": self.cost_center, | 				"cost_center": self.cost_center, | ||||||
| 				"posting_date": getdate(self.posting_date) | 				"posting_date": getdate(self.posting_date) | ||||||
| 			}) | 			}) | ||||||
| @ -287,7 +291,7 @@ class LoanRepayment(AccountsController): | |||||||
| 				"credit_in_account_currency": self.amount_paid, | 				"credit_in_account_currency": self.amount_paid, | ||||||
| 				"against_voucher_type": "Loan", | 				"against_voucher_type": "Loan", | ||||||
| 				"against_voucher": self.against_loan, | 				"against_voucher": self.against_loan, | ||||||
| 				"remarks": _("Repayment against Loan: ") + self.against_loan, | 				"remarks": remarks, | ||||||
| 				"cost_center": self.cost_center, | 				"cost_center": self.cost_center, | ||||||
| 				"posting_date": getdate(self.posting_date) | 				"posting_date": getdate(self.posting_date) | ||||||
| 			}) | 			}) | ||||||
| @ -338,6 +342,18 @@ def get_accrued_interest_entries(against_loan, posting_date=None): | |||||||
| 
 | 
 | ||||||
| 	return unpaid_accrued_entries | 	return unpaid_accrued_entries | ||||||
| 
 | 
 | ||||||
|  | def get_penalty_details(against_loan): | ||||||
|  | 	penalty_details = frappe.db.sql(""" | ||||||
|  | 		SELECT posting_date, (penalty_amount - total_penalty_paid) as pending_penalty_amount | ||||||
|  | 		FROM `tabLoan Repayment` where posting_date >= (SELECT MAX(posting_date) from `tabLoan Repayment` | ||||||
|  | 		where against_loan = %s) and docstatus = 1 and against_loan = %s | ||||||
|  | 	""", (against_loan, against_loan)) | ||||||
|  | 
 | ||||||
|  | 	if penalty_details: | ||||||
|  | 		return penalty_details[0][0], flt(penalty_details[0][1]) | ||||||
|  | 	else: | ||||||
|  | 		return None, 0 | ||||||
|  | 
 | ||||||
| # This function returns the amounts that are payable at the time of loan repayment based on posting date | # This function returns the amounts that are payable at the time of loan repayment based on posting date | ||||||
| # So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable | # So it pulls all the unpaid Loan Interest Accrual Entries and calculates the penalty if applicable | ||||||
| 
 | 
 | ||||||
| @ -348,6 +364,7 @@ def get_amounts(amounts, against_loan, posting_date): | |||||||
| 	loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type) | 	loan_type_details = frappe.get_doc("Loan Type", against_loan_doc.loan_type) | ||||||
| 	accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name, posting_date) | 	accrued_interest_entries = get_accrued_interest_entries(against_loan_doc.name, posting_date) | ||||||
| 
 | 
 | ||||||
|  | 	computed_penalty_date, pending_penalty_amount = get_penalty_details(against_loan) | ||||||
| 	pending_accrual_entries = {} | 	pending_accrual_entries = {} | ||||||
| 
 | 
 | ||||||
| 	total_pending_interest = 0 | 	total_pending_interest = 0 | ||||||
| @ -362,8 +379,13 @@ def get_amounts(amounts, against_loan, posting_date): | |||||||
| 		# and if no_of_late days are positive then penalty is levied | 		# and if no_of_late days are positive then penalty is levied | ||||||
| 
 | 
 | ||||||
| 		due_date = add_days(entry.posting_date, 1) | 		due_date = add_days(entry.posting_date, 1) | ||||||
| 		no_of_late_days = date_diff(posting_date, | 		due_date_after_grace_period = add_days(due_date, loan_type_details.grace_period_in_days) | ||||||
| 			add_days(due_date, loan_type_details.grace_period_in_days)) + 1 | 
 | ||||||
|  | 		# Consider one day after already calculated penalty | ||||||
|  | 		if computed_penalty_date and getdate(computed_penalty_date) >= due_date_after_grace_period: | ||||||
|  | 			due_date_after_grace_period = add_days(computed_penalty_date, 1) | ||||||
|  | 
 | ||||||
|  | 		no_of_late_days = date_diff(posting_date, due_date_after_grace_period) + 1 | ||||||
| 
 | 
 | ||||||
| 		if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular': | 		if no_of_late_days > 0 and (not against_loan_doc.repay_from_salary) and entry.accrual_type == 'Regular': | ||||||
| 			penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days) | 			penalty_amount += (entry.interest_amount * (loan_type_details.penalty_interest_rate / 100) * no_of_late_days) | ||||||
| @ -401,7 +423,7 @@ def get_amounts(amounts, against_loan, posting_date): | |||||||
| 	amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) | 	amounts["pending_principal_amount"] = flt(pending_principal_amount, precision) | ||||||
| 	amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) | 	amounts["payable_principal_amount"] = flt(payable_principal_amount, precision) | ||||||
| 	amounts["interest_amount"] = flt(total_pending_interest, precision) | 	amounts["interest_amount"] = flt(total_pending_interest, precision) | ||||||
| 	amounts["penalty_amount"] = flt(penalty_amount, precision) | 	amounts["penalty_amount"] = flt(penalty_amount + pending_penalty_amount, precision) | ||||||
| 	amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) | 	amounts["payable_amount"] = flt(payable_principal_amount + total_pending_interest + penalty_amount, precision) | ||||||
| 	amounts["pending_accrual_entries"] = pending_accrual_entries | 	amounts["pending_accrual_entries"] = pending_accrual_entries | ||||||
| 	amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) | 	amounts["unaccrued_interest"] = flt(unaccrued_interest, precision) | ||||||
|  | |||||||
| @ -6,7 +6,7 @@ from __future__ import unicode_literals | |||||||
| import frappe | import frappe | ||||||
| from frappe import _ | from frappe import _ | ||||||
| from frappe.model.document import Document | from frappe.model.document import Document | ||||||
| from frappe.utils import get_datetime, flt | from frappe.utils import get_datetime, flt, getdate | ||||||
| import json | import json | ||||||
| from six import iteritems | from six import iteritems | ||||||
| from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price | from erpnext.loan_management.doctype.loan_security_price.loan_security_price import get_loan_security_price | ||||||
| @ -113,7 +113,11 @@ class LoanSecurityUnpledge(Document): | |||||||
| 				pledged_qty += qty | 				pledged_qty += qty | ||||||
| 
 | 
 | ||||||
| 			if not pledged_qty: | 			if not pledged_qty: | ||||||
| 				frappe.db.set_value('Loan', self.loan, 'status', 'Closed') | 				frappe.db.set_value('Loan', self.loan, | ||||||
|  | 					{ | ||||||
|  | 						'status': 'Closed', | ||||||
|  | 						'closure_date': getdate() | ||||||
|  | 					}) | ||||||
| 
 | 
 | ||||||
| @frappe.whitelist() | @frappe.whitelist() | ||||||
| def get_pledged_security_qty(loan): | def get_pledged_security_qty(loan): | ||||||
|  | |||||||
| @ -93,15 +93,15 @@ class TestBOM(unittest.TestCase): | |||||||
| 		base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) | 		base_raw_material_cost = raw_material_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) | ||||||
| 		base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) | 		base_op_cost = op_cost * flt(bom.conversion_rate, bom.precision("conversion_rate")) | ||||||
| 
 | 
 | ||||||
| 		# test amounts in selected currency | 		# test amounts in selected currency, almostEqual checks for 7 digits by default | ||||||
| 		self.assertEqual(bom.operating_cost, op_cost) | 		self.assertAlmostEqual(bom.operating_cost, op_cost) | ||||||
| 		self.assertEqual(bom.raw_material_cost, raw_material_cost) | 		self.assertAlmostEqual(bom.raw_material_cost, raw_material_cost) | ||||||
| 		self.assertEqual(bom.total_cost, raw_material_cost + op_cost) | 		self.assertAlmostEqual(bom.total_cost, raw_material_cost + op_cost) | ||||||
| 
 | 
 | ||||||
| 		# test amounts in selected currency | 		# test amounts in selected currency | ||||||
| 		self.assertEqual(bom.base_operating_cost, base_op_cost) | 		self.assertAlmostEqual(bom.base_operating_cost, base_op_cost) | ||||||
| 		self.assertEqual(bom.base_raw_material_cost, base_raw_material_cost) | 		self.assertAlmostEqual(bom.base_raw_material_cost, base_raw_material_cost) | ||||||
| 		self.assertEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) | 		self.assertAlmostEqual(bom.base_total_cost, base_raw_material_cost + base_op_cost) | ||||||
| 
 | 
 | ||||||
| 	def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): | 	def test_bom_cost_multi_uom_multi_currency_based_on_price_list(self): | ||||||
| 		frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1) | 		frappe.db.set_value("Price List", "_Test Price List", "price_not_uom_dependent", 1) | ||||||
|  | |||||||
| @ -433,6 +433,7 @@ def make_material_request(source_name, target_doc=None): | |||||||
| def make_stock_entry(source_name, target_doc=None): | def make_stock_entry(source_name, target_doc=None): | ||||||
| 	def update_item(obj, target, source_parent): | 	def update_item(obj, target, source_parent): | ||||||
| 		target.t_warehouse = source_parent.wip_warehouse | 		target.t_warehouse = source_parent.wip_warehouse | ||||||
|  | 		target.conversion_factor = 1 | ||||||
| 
 | 
 | ||||||
| 	def set_missing_values(source, target): | 	def set_missing_values(source, target): | ||||||
| 		target.purpose = "Material Transfer for Manufacture" | 		target.purpose = "Material Transfer for Manufacture" | ||||||
|  | |||||||
| @ -11,10 +11,9 @@ frappe.ui.form.on('Routing', { | |||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	display_sequence_id_column: function(frm) { | 	display_sequence_id_column: function(frm) { | ||||||
| 		frappe.meta.get_docfield("BOM Operation", "sequence_id", | 		frm.fields_dict.operations.grid.update_docfield_property( | ||||||
| 			frm.doc.name).in_list_view = true; | 			'sequence_id', 	'in_list_view', 1 | ||||||
| 
 | 		); | ||||||
| 		frm.fields_dict.operations.grid.refresh(); |  | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	calculate_operating_cost: function(frm, child) { | 	calculate_operating_cost: function(frm, child) { | ||||||
|  | |||||||
| @ -19,7 +19,7 @@ | |||||||
|  "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing", |  "documentation_url": "https://docs.erpnext.com/docs/user/manual/en/manufacturing", | ||||||
|  "idx": 0, |  "idx": 0, | ||||||
|  "is_complete": 0, |  "is_complete": 0, | ||||||
|  "modified": "2020-07-08 14:05:56.197563", |  "modified": "2020-06-29 20:25:36.899106", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Manufacturing", |  "module": "Manufacturing", | ||||||
|  "name": "Manufacturing", |  "name": "Manufacturing", | ||||||
|  | |||||||
| @ -756,11 +756,18 @@ erpnext.patches.v13_0.update_payment_terms_outstanding | |||||||
| erpnext.patches.v12_0.add_state_code_for_ladakh | erpnext.patches.v12_0.add_state_code_for_ladakh | ||||||
| erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl | erpnext.patches.v13_0.item_reposting_for_incorrect_sl_and_gl | ||||||
| erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes | erpnext.patches.v13_0.delete_old_bank_reconciliation_doctypes | ||||||
| erpnext.patches.v13_0.update_vehicle_no_reqd_condition | erpnext.patches.v12_0.update_vehicle_no_reqd_condition | ||||||
|  | erpnext.patches.v12_0.add_einvoice_status_field #2021-03-17 | ||||||
|  | erpnext.patches.v12_0.add_einvoice_summary_report_permissions | ||||||
| erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation | erpnext.patches.v13_0.setup_fields_for_80g_certificate_and_donation | ||||||
| erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings | erpnext.patches.v13_0.rename_membership_settings_to_non_profit_settings | ||||||
| erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae | erpnext.patches.v13_0.setup_gratuity_rule_for_india_and_uae | ||||||
| erpnext.patches.v13_0.setup_uae_vat_fields | erpnext.patches.v13_0.setup_uae_vat_fields | ||||||
| execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext') | execute:frappe.db.set_value('System Settings', None, 'app_name', 'ERPNext') | ||||||
|  | erpnext.patches.v12_0.add_company_link_to_einvoice_settings | ||||||
| erpnext.patches.v13_0.rename_discharge_date_in_ip_record | erpnext.patches.v13_0.rename_discharge_date_in_ip_record | ||||||
|  | erpnext.patches.v12_0.create_taxable_value_field | ||||||
|  | erpnext.patches.v12_0.add_gst_category_in_delivery_note | ||||||
| erpnext.patches.v12_0.purchase_receipt_status | erpnext.patches.v12_0.purchase_receipt_status | ||||||
|  | erpnext.patches.v13_0.fix_non_unique_represents_company | ||||||
|  | erpnext.patches.v12_0.add_document_type_field_for_italy_einvoicing | ||||||
|  | |||||||
| @ -0,0 +1,16 @@ | |||||||
|  | from __future__ import unicode_literals | ||||||
|  | import frappe | ||||||
|  | 
 | ||||||
|  | def execute(): | ||||||
|  | 	company = frappe.get_all('Company', filters = {'country': 'India'}) | ||||||
|  | 	if not company or not frappe.db.count('E Invoice User'): | ||||||
|  | 		return | ||||||
|  | 
 | ||||||
|  | 	frappe.reload_doc("regional", "doctype", "e_invoice_user") | ||||||
|  | 	for creds in frappe.db.get_all('E Invoice User', fields=['name', 'gstin']): | ||||||
|  | 		company_name = frappe.db.sql(""" | ||||||
|  | 			select dl.link_name from `tabAddress` a, `tabDynamic Link` dl | ||||||
|  | 			where a.gstin = %s and dl.parent = a.name and dl.link_doctype = 'Company' | ||||||
|  | 		""", (creds.get('gstin'))) | ||||||
|  | 		if company_name and len(company_name) == 1: | ||||||
|  | 			frappe.db.set_value('E Invoice User', creds.get('name'), 'company', company_name[0][0]) | ||||||
| @ -0,0 +1,18 @@ | |||||||
|  | from __future__ import unicode_literals | ||||||
|  | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields | ||||||
|  | import frappe | ||||||
|  | 
 | ||||||
|  | def execute(): | ||||||
|  | 	company = frappe.get_all('Company', filters = {'country': 'Italy'}) | ||||||
|  | 	if not company: | ||||||
|  | 		return | ||||||
|  | 
 | ||||||
|  | 	custom_fields = { | ||||||
|  | 		'Sales Invoice': [ | ||||||
|  | 			dict(fieldname='type_of_document', label='Type of Document', | ||||||
|  | 				fieldtype='Select', insert_after='customer_fiscal_code', | ||||||
|  | 				options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'), | ||||||
|  | 		] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	create_custom_fields(custom_fields, update=True) | ||||||
							
								
								
									
										69
									
								
								erpnext/patches/v12_0/add_einvoice_status_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								erpnext/patches/v12_0/add_einvoice_status_field.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,69 @@ | |||||||
|  | from __future__ import unicode_literals | ||||||
|  | import json | ||||||
|  | import frappe | ||||||
|  | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields | ||||||
|  | 
 | ||||||
|  | def execute(): | ||||||
|  | 	company = frappe.get_all('Company', filters = {'country': 'India'}) | ||||||
|  | 	if not company: | ||||||
|  | 		return | ||||||
|  | 
 | ||||||
|  | 	# move hidden einvoice fields to a different section | ||||||
|  | 	custom_fields = { | ||||||
|  | 		'Sales Invoice': [ | ||||||
|  | 			dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', | ||||||
|  | 			print_hide=1, hidden=1), | ||||||
|  | 		 | ||||||
|  | 			dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', | ||||||
|  | 				no_copy=1, print_hide=1), | ||||||
|  | 			 | ||||||
|  | 			dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), | ||||||
|  | 
 | ||||||
|  | 			dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',  | ||||||
|  | 				no_copy=1, print_hide=1), | ||||||
|  | 
 | ||||||
|  | 			dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', | ||||||
|  | 				no_copy=1, print_hide=1, read_only=1), | ||||||
|  | 
 | ||||||
|  | 			dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice', | ||||||
|  | 				no_copy=1, print_hide=1, read_only=1), | ||||||
|  | 
 | ||||||
|  | 			dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code', | ||||||
|  | 				no_copy=1, print_hide=1, read_only=1), | ||||||
|  | 
 | ||||||
|  | 			dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image', | ||||||
|  | 				options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1), | ||||||
|  | 
 | ||||||
|  | 			dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON', | ||||||
|  | 				hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1) | ||||||
|  | 		] | ||||||
|  | 	} | ||||||
|  | 	create_custom_fields(custom_fields, update=True) | ||||||
|  | 
 | ||||||
|  | 	if frappe.db.exists('E Invoice Settings') and frappe.db.get_single_value('E Invoice Settings', 'enable'): | ||||||
|  | 		frappe.db.sql(''' | ||||||
|  | 			UPDATE `tabSales Invoice` SET einvoice_status = 'Pending' | ||||||
|  | 			WHERE | ||||||
|  | 				posting_date >= '2021-04-01' | ||||||
|  | 				AND ifnull(irn, '') = '' | ||||||
|  | 				AND ifnull(`billing_address_gstin`, '') != ifnull(`company_gstin`, '') | ||||||
|  | 				AND ifnull(gst_category, '') in ('Registered Regular', 'SEZ', 'Overseas', 'Deemed Export') | ||||||
|  | 		''') | ||||||
|  | 
 | ||||||
|  | 		# set appropriate statuses | ||||||
|  | 		frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Generated' | ||||||
|  | 			WHERE ifnull(irn, '') != '' AND ifnull(irn_cancelled, 0) = 0''') | ||||||
|  | 
 | ||||||
|  | 		frappe.db.sql('''UPDATE `tabSales Invoice` SET einvoice_status = 'Cancelled' | ||||||
|  | 			WHERE ifnull(irn_cancelled, 0) = 1''') | ||||||
|  | 
 | ||||||
|  | 	# set correct acknowledgement in e-invoices | ||||||
|  | 	einvoices = frappe.get_all('Sales Invoice', {'irn': ['is', 'set']}, ['name', 'signed_einvoice']) | ||||||
|  | 
 | ||||||
|  | 	if einvoices: | ||||||
|  | 		for inv in einvoices: | ||||||
|  | 			signed_einvoice = inv.get('signed_einvoice') | ||||||
|  | 			if signed_einvoice: | ||||||
|  | 				signed_einvoice = json.loads(signed_einvoice) | ||||||
|  | 				frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_no', signed_einvoice.get('AckNo'), update_modified=False) | ||||||
|  | 				frappe.db.set_value('Sales Invoice', inv.get('name'), 'ack_date', signed_einvoice.get('AckDt'), update_modified=False) | ||||||
| @ -0,0 +1,18 @@ | |||||||
|  | from __future__ import unicode_literals | ||||||
|  | import frappe | ||||||
|  | 
 | ||||||
|  | def execute(): | ||||||
|  | 	company = frappe.get_all('Company', filters = {'country': 'India'}) | ||||||
|  | 	if not company: | ||||||
|  | 		return | ||||||
|  | 
 | ||||||
|  | 	if frappe.db.exists('Report', 'E-Invoice Summary') and \ | ||||||
|  | 		not frappe.db.get_value('Custom Role', dict(report='E-Invoice Summary')): | ||||||
|  | 		frappe.get_doc(dict( | ||||||
|  | 			doctype='Custom Role', | ||||||
|  | 			report='E-Invoice Summary', | ||||||
|  | 			roles= [ | ||||||
|  | 				dict(role='Accounts User'), | ||||||
|  | 				dict(role='Accounts Manager') | ||||||
|  | 			] | ||||||
|  | 		)).insert()	 | ||||||
							
								
								
									
										19
									
								
								erpnext/patches/v12_0/add_gst_category_in_delivery_note.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								erpnext/patches/v12_0/add_gst_category_in_delivery_note.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,19 @@ | |||||||
|  | from __future__ import unicode_literals | ||||||
|  | import frappe | ||||||
|  | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields | ||||||
|  | 
 | ||||||
|  | def execute(): | ||||||
|  | 	company = frappe.get_all('Company', filters = {'country': 'India'}) | ||||||
|  | 	if not company: | ||||||
|  | 		return | ||||||
|  | 
 | ||||||
|  | 	custom_fields = { | ||||||
|  | 		'Delivery Note': [ | ||||||
|  | 			dict(fieldname='gst_category', label='GST Category', | ||||||
|  | 				fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1, | ||||||
|  | 				options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', | ||||||
|  | 				fetch_from='customer.gst_category', fetch_if_empty=1), | ||||||
|  | 		] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	create_custom_fields(custom_fields, update=True) | ||||||
							
								
								
									
										18
									
								
								erpnext/patches/v12_0/create_taxable_value_field.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								erpnext/patches/v12_0/create_taxable_value_field.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,18 @@ | |||||||
|  | from __future__ import unicode_literals | ||||||
|  | import frappe | ||||||
|  | from frappe.custom.doctype.custom_field.custom_field import create_custom_fields | ||||||
|  | 
 | ||||||
|  | def execute(): | ||||||
|  | 	company = frappe.get_all('Company', filters = {'country': 'India'}) | ||||||
|  | 	if not company: | ||||||
|  | 		return | ||||||
|  | 
 | ||||||
|  | 	custom_fields = { | ||||||
|  | 		'Sales Invoice Item': [ | ||||||
|  | 			dict(fieldname='taxable_value', label='Taxable Value', | ||||||
|  | 				fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", | ||||||
|  | 				print_hide=1) | ||||||
|  | 		] | ||||||
|  | 	} | ||||||
|  | 
 | ||||||
|  | 	create_custom_fields(custom_fields, update=True) | ||||||
| @ -1,6 +1,7 @@ | |||||||
| import frappe | import frappe | ||||||
| 
 | 
 | ||||||
| def execute(): | def execute(): | ||||||
|  | 	frappe.reload_doc('custom', 'doctype', 'custom_field') | ||||||
| 	company = frappe.get_all('Company', filters = {'country': 'India'}) | 	company = frappe.get_all('Company', filters = {'country': 'India'}) | ||||||
| 	if not company: | 	if not company: | ||||||
| 		return | 		return | ||||||
| @ -12,7 +12,10 @@ def execute(): | |||||||
| 		'Employee Tax Exemption Declaration', | 		'Employee Tax Exemption Declaration', | ||||||
| 		'Employee Tax Exemption Proof Submission', | 		'Employee Tax Exemption Proof Submission', | ||||||
| 		'Employee Tax Exemption Declaration Category', | 		'Employee Tax Exemption Declaration Category', | ||||||
|         'Employee Tax Exemption Proof Submission Detail' | 		'Employee Tax Exemption Proof Submission Detail', | ||||||
|  | 		'gratuity_rule', | ||||||
|  | 		'gratuity_rule_slab', | ||||||
|  | 		'gratuity_applicable_component' | ||||||
| 	] | 	] | ||||||
| 
 | 
 | ||||||
| 	for doctype in doctypes: | 	for doctype in doctypes: | ||||||
|  | |||||||
| @ -11,4 +11,8 @@ def execute(): | |||||||
| 	if not company: | 	if not company: | ||||||
| 		return | 		return | ||||||
| 
 | 
 | ||||||
|  | 
 | ||||||
|  | 	frappe.reload_doc('accounts', 'doctype', 'pos_invoice') | ||||||
|  | 	frappe.reload_doc('accounts', 'doctype', 'pos_invoice_item') | ||||||
|  | 
 | ||||||
| 	make_custom_fields() | 	make_custom_fields() | ||||||
| @ -0,0 +1,8 @@ | |||||||
|  | import frappe | ||||||
|  | 
 | ||||||
|  | def execute(): | ||||||
|  | 	frappe.db.sql(""" | ||||||
|  | 		update tabCustomer | ||||||
|  | 		set represents_company = NULL | ||||||
|  | 		where represents_company = '' | ||||||
|  | 	""") | ||||||
| @ -18,6 +18,7 @@ def execute(): | |||||||
| 
 | 
 | ||||||
| 		for old_dt, new_dt in doctypes.items(): | 		for old_dt, new_dt in doctypes.items(): | ||||||
| 			if not frappe.db.table_exists(new_dt) and frappe.db.table_exists(old_dt): | 			if not frappe.db.table_exists(new_dt) and frappe.db.table_exists(old_dt): | ||||||
|  | 				frappe.reload_doc('healthcare', 'doctype', frappe.scrub(old_dt)) | ||||||
| 				frappe.rename_doc('DocType', old_dt, new_dt, force=True) | 				frappe.rename_doc('DocType', old_dt, new_dt, force=True) | ||||||
| 				frappe.reload_doc('healthcare', 'doctype', frappe.scrub(new_dt)) | 				frappe.reload_doc('healthcare', 'doctype', frappe.scrub(new_dt)) | ||||||
| 				frappe.delete_doc_if_exists('DocType', old_dt) | 				frappe.delete_doc_if_exists('DocType', old_dt) | ||||||
| @ -36,6 +37,18 @@ def execute(): | |||||||
| 				SET parentfield = %(parentfield)s | 				SET parentfield = %(parentfield)s | ||||||
| 			""".format(doctype), {'parentfield': parentfield}) | 			""".format(doctype), {'parentfield': parentfield}) | ||||||
| 
 | 
 | ||||||
|  | 		# copy renamed child table fields (fields were already renamed in old doctype json, hence sql) | ||||||
|  | 		frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_name = test_name""") | ||||||
|  | 		frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_event = test_event""") | ||||||
|  | 		frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_uom = test_uom""") | ||||||
|  | 		frappe.db.sql("""UPDATE `tabNormal Test Result` SET lab_test_comment = test_comment""") | ||||||
|  | 		frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_event = test_event""") | ||||||
|  | 		frappe.db.sql("""UPDATE `tabNormal Test Template` SET lab_test_uom = test_uom""") | ||||||
|  | 		frappe.db.sql("""UPDATE `tabDescriptive Test Result` SET lab_test_particulars = test_particulars""") | ||||||
|  | 		frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_template = test_template""") | ||||||
|  | 		frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_description = test_description""") | ||||||
|  | 		frappe.db.sql("""UPDATE `tabLab Test Group Template` SET lab_test_rate = test_rate""") | ||||||
|  | 
 | ||||||
| 		# rename field | 		# rename field | ||||||
| 		frappe.reload_doc('healthcare', 'doctype', 'lab_test') | 		frappe.reload_doc('healthcare', 'doctype', 'lab_test') | ||||||
| 		if frappe.db.has_column('Lab Test', 'special_toggle'): | 		if frappe.db.has_column('Lab Test', 'special_toggle'): | ||||||
|  | |||||||
| @ -20,9 +20,11 @@ def execute(): | |||||||
| 	frappe.clear_cache() | 	frappe.clear_cache() | ||||||
| 	frappe.flags.warehouse_account_map = {} | 	frappe.flags.warehouse_account_map = {} | ||||||
| 
 | 
 | ||||||
|  | 	company_list = [] | ||||||
|  | 
 | ||||||
| 	data = frappe.db.sql(''' | 	data = frappe.db.sql(''' | ||||||
| 		SELECT | 		SELECT | ||||||
| 			name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time | 			name, item_code, warehouse, voucher_type, voucher_no, posting_date, posting_time, company | ||||||
| 		FROM | 		FROM | ||||||
| 			`tabStock Ledger Entry` | 			`tabStock Ledger Entry` | ||||||
| 		WHERE | 		WHERE | ||||||
| @ -36,6 +38,9 @@ def execute(): | |||||||
| 	total_sle = len(data) | 	total_sle = len(data) | ||||||
| 	i = 0 | 	i = 0 | ||||||
| 	for d in data: | 	for d in data: | ||||||
|  | 		if d.company not in company_list: | ||||||
|  | 			company_list.append(d.company) | ||||||
|  | 
 | ||||||
| 		update_entries_after({ | 		update_entries_after({ | ||||||
| 			"item_code": d.item_code, | 			"item_code": d.item_code, | ||||||
| 			"warehouse": d.warehouse, | 			"warehouse": d.warehouse, | ||||||
| @ -53,7 +58,9 @@ def execute(): | |||||||
| 
 | 
 | ||||||
| 	print("Reposting General Ledger Entries...") | 	print("Reposting General Ledger Entries...") | ||||||
| 
 | 
 | ||||||
|  | 	if data: | ||||||
| 		for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): | 		for row in frappe.get_all('Company', filters= {'enable_perpetual_inventory': 1}): | ||||||
|  | 			if row.name in company_list: | ||||||
| 				update_gl_entries_after(posting_date, posting_time, company=row.name) | 				update_gl_entries_after(posting_date, posting_time, company=row.name) | ||||||
| 
 | 
 | ||||||
| 	frappe.db.auto_commit_on_many_writes = 0 | 	frappe.db.auto_commit_on_many_writes = 0 | ||||||
|  | |||||||
| @ -6,8 +6,9 @@ def execute(): | |||||||
| 	if "Healthcare" not in frappe.get_active_domains(): | 	if "Healthcare" not in frappe.get_active_domains(): | ||||||
| 		return | 		return | ||||||
| 
 | 
 | ||||||
| 	frappe.reload_doc("healthcare", "doctype", "Therapy Session") |  | ||||||
| 	frappe.reload_doc("healthcare", "doctype", "Inpatient Medication Order") | 	frappe.reload_doc("healthcare", "doctype", "Inpatient Medication Order") | ||||||
|  | 	frappe.reload_doc("healthcare", "doctype", "Therapy Session") | ||||||
|  | 	frappe.reload_doc("healthcare", "doctype", "Clinical Procedure") | ||||||
| 	frappe.reload_doc("healthcare", "doctype", "Patient History Settings") | 	frappe.reload_doc("healthcare", "doctype", "Patient History Settings") | ||||||
| 	frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type") | 	frappe.reload_doc("healthcare", "doctype", "Patient History Standard Document Type") | ||||||
| 	frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type") | 	frappe.reload_doc("healthcare", "doctype", "Patient History Custom Document Type") | ||||||
|  | |||||||
| @ -9,4 +9,8 @@ def execute(): | |||||||
| 	if not company: | 	if not company: | ||||||
| 		return | 		return | ||||||
| 
 | 
 | ||||||
|  | 	frappe.reload_doc('regional', 'report', 'uae_vat_201') | ||||||
|  | 	frappe.reload_doc('regional', 'doctype', 'uae_vat_settings') | ||||||
|  | 	frappe.reload_doc('regional', 'doctype', 'uae_vat_account') | ||||||
|  | 
 | ||||||
| 	setup() | 	setup() | ||||||
| @ -175,7 +175,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 14:45:48.566756", |  "modified": "2021-03-31 22:33:59.098532", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Payroll", |  "module": "Payroll", | ||||||
|  "name": "Additional Salary", |  "name": "Additional Salary", | ||||||
|  | |||||||
| @ -147,7 +147,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 14:46:22.465521", |  "modified": "2021-03-31 22:35:08.940087", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Payroll", |  "module": "Payroll", | ||||||
|  "name": "Employee Benefit Application", |  "name": "Employee Benefit Application", | ||||||
|  | |||||||
| @ -144,7 +144,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 15:51:51.489269", |  "modified": "2021-03-31 22:37:21.024625", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Payroll", |  "module": "Payroll", | ||||||
|  "name": "Employee Benefit Claim", |  "name": "Employee Benefit Claim", | ||||||
|  | |||||||
| @ -94,7 +94,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 14:48:00.919839", |  "modified": "2021-03-31 22:38:20.332316", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Payroll", |  "module": "Payroll", | ||||||
|  "name": "Employee Incentive", |  "name": "Employee Incentive", | ||||||
|  | |||||||
| @ -119,7 +119,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 20:41:57.387749", |  "modified": "2021-03-31 22:39:59.237361", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Payroll", |  "module": "Payroll", | ||||||
|  "name": "Employee Tax Exemption Declaration", |  "name": "Employee Tax Exemption Declaration", | ||||||
|  | |||||||
| @ -142,7 +142,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 20:48:32.639885", |  "modified": "2021-03-31 22:41:13.723339", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Payroll", |  "module": "Payroll", | ||||||
|  "name": "Employee Tax Exemption Proof Submission", |  "name": "Employee Tax Exemption Proof Submission", | ||||||
|  | |||||||
| @ -104,7 +104,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 20:53:33.323712", |  "modified": "2021-03-31 22:42:08.139520", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Payroll", |  "module": "Payroll", | ||||||
|  "name": "Income Tax Slab", |  "name": "Income Tax Slab", | ||||||
|  | |||||||
| @ -567,6 +567,7 @@ def create_salary_slips_for_employees(employees, args, publish_progress=True): | |||||||
| 			if publish_progress: | 			if publish_progress: | ||||||
| 				frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)), | 				frappe.publish_progress(count*100/len(set(employees) - set(salary_slips_exists_for)), | ||||||
| 					title = _("Creating Salary Slips...")) | 					title = _("Creating Salary Slips...")) | ||||||
|  | 
 | ||||||
| 		else: | 		else: | ||||||
| 			salary_slips_not_created.append(emp) | 			salary_slips_not_created.append(emp) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -105,7 +105,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 14:50:29.401020", |  "modified": "2021-03-31 22:43:28.363644", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Payroll", |  "module": "Payroll", | ||||||
|  "name": "Retention Bonus", |  "name": "Retention Bonus", | ||||||
|  | |||||||
| @ -120,6 +120,7 @@ frappe.ui.form.on("Salary Slip", { | |||||||
| 					frm.set_df_property("exchange_rate", "description", "" ); | 					frm.set_df_property("exchange_rate", "description", "" ); | ||||||
| 				} | 				} | ||||||
| 			} | 			} | ||||||
|  | 		} | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	exchange_rate: function(frm) { | 	exchange_rate: function(frm) { | ||||||
|  | |||||||
| @ -631,7 +631,7 @@ | |||||||
|  "idx": 9, |  "idx": 9, | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 15:39:28.817166", |  "modified": "2021-03-31 22:44:09.772331", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Payroll", |  "module": "Payroll", | ||||||
|  "name": "Salary Slip", |  "name": "Salary Slip", | ||||||
|  | |||||||
| @ -598,10 +598,10 @@ class SalarySlip(TransactionBase): | |||||||
| 				continue | 				continue | ||||||
| 
 | 
 | ||||||
| 			if ( | 			if ( | ||||||
| 				not d.additional_salary | 				(not d.additional_salary | ||||||
| 				and (not additional_salary or additional_salary.overwrite) | 				and (not additional_salary or additional_salary.overwrite)) | ||||||
| 				or additional_salary | 				or (additional_salary | ||||||
| 				and additional_salary.name == d.additional_salary | 				and additional_salary.name == d.additional_salary) | ||||||
| 			): | 			): | ||||||
| 				component_row = d | 				component_row = d | ||||||
| 				break | 				break | ||||||
| @ -611,7 +611,7 @@ class SalarySlip(TransactionBase): | |||||||
| 			self.set(component_type, [ | 			self.set(component_type, [ | ||||||
| 				d for d in self.get(component_type) | 				d for d in self.get(component_type) | ||||||
| 				if d.salary_component != component_data.salary_component | 				if d.salary_component != component_data.salary_component | ||||||
| 				or d.additional_salary and additional_salary.name != d.additional_salary | 				or (d.additional_salary and additional_salary.name != d.additional_salary) | ||||||
| 				or d == component_row | 				or d == component_row | ||||||
| 			]) | 			]) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -312,7 +312,7 @@ class TestSalarySlip(unittest.TestCase): | |||||||
| 		frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'") | 		frappe.db.sql("DELETE FROM `tabSalary Slip` where employee_name = 'test_ytd@salary.com'") | ||||||
| 
 | 
 | ||||||
| 		create_salary_slips_for_payroll_period(applicant, salary_structure.name, | 		create_salary_slips_for_payroll_period(applicant, salary_structure.name, | ||||||
| 			payroll_period, deduct_random=False) | 			payroll_period, deduct_random=False, num=6) | ||||||
| 
 | 
 | ||||||
| 		salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name': | 		salary_slips = frappe.get_all('Salary Slip', fields=['year_to_date', 'net_pay'], filters={'employee_name': | ||||||
| 			'test_ytd@salary.com'}, order_by = 'posting_date') | 			'test_ytd@salary.com'}, order_by = 'posting_date') | ||||||
|  | |||||||
| @ -114,9 +114,16 @@ frappe.ui.form.on('Salary Structure', { | |||||||
| 				frm.trigger('assign_to_employees') | 				frm.trigger('assign_to_employees') | ||||||
| 			}) | 			}) | ||||||
| 		} | 		} | ||||||
|  | 
 | ||||||
|  | 		// set columns read-only
 | ||||||
| 		let fields_read_only = ["is_tax_applicable", "is_flexible_benefit", "variable_based_on_taxable_salary"]; | 		let fields_read_only = ["is_tax_applicable", "is_flexible_benefit", "variable_based_on_taxable_salary"]; | ||||||
| 		fields_read_only.forEach(function(field) { | 		fields_read_only.forEach(function(field) { | ||||||
| 			frappe.meta.get_docfield("Salary Detail", field, frm.doc.name).read_only = 1; | 			frm.fields_dict.earnings.grid.update_docfield_property( | ||||||
|  | 				field, 'read_only', 1 | ||||||
|  | 			); | ||||||
|  | 			frm.fields_dict.deductions.grid.update_docfield_property( | ||||||
|  | 				field, 'read_only', 1 | ||||||
|  | 			); | ||||||
| 		}); | 		}); | ||||||
| 		frm.trigger('set_earning_deduction_component'); | 		frm.trigger('set_earning_deduction_component'); | ||||||
| 	}, | 	}, | ||||||
|  | |||||||
| @ -164,7 +164,13 @@ def create_salary_structure_assignment(employee, salary_structure, from_date=Non | |||||||
| 	salary_structure_assignment.employee = employee | 	salary_structure_assignment.employee = employee | ||||||
| 	salary_structure_assignment.base = 50000 | 	salary_structure_assignment.base = 50000 | ||||||
| 	salary_structure_assignment.variable = 5000 | 	salary_structure_assignment.variable = 5000 | ||||||
| 	salary_structure_assignment.from_date = from_date or add_days(nowdate(), -1) | 
 | ||||||
|  | 	if getdate(nowdate()).day == 1: | ||||||
|  | 		date = from_date or nowdate() | ||||||
|  | 	else: | ||||||
|  | 		date = from_date or add_days(nowdate(), -1) | ||||||
|  | 
 | ||||||
|  | 	salary_structure_assignment.from_date = date | ||||||
| 	salary_structure_assignment.salary_structure = salary_structure | 	salary_structure_assignment.salary_structure = salary_structure | ||||||
| 	salary_structure_assignment.currency = currency | 	salary_structure_assignment.currency = currency | ||||||
| 	salary_structure_assignment.payroll_payable_account = get_payable_account(company) | 	salary_structure_assignment.payroll_payable_account = get_payable_account(company) | ||||||
|  | |||||||
| @ -145,7 +145,7 @@ | |||||||
|  ], |  ], | ||||||
|  "is_submittable": 1, |  "is_submittable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-03-31 15:49:36.361253", |  "modified": "2021-03-31 22:44:46.267974", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Payroll", |  "module": "Payroll", | ||||||
|  "name": "Salary Structure Assignment", |  "name": "Salary Structure Assignment", | ||||||
|  | |||||||
| @ -8,7 +8,7 @@ | |||||||
|  "is_mandatory": 1, |  "is_mandatory": 1, | ||||||
|  "is_single": 0, |  "is_single": 0, | ||||||
|  "is_skipped": 0, |  "is_skipped": 0, | ||||||
|  "modified": "2020-06-01 11:53:54.553947", |  "modified": "2020-06-29 11:53:54.553947", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "name": "Create Payroll Period", |  "name": "Create Payroll Period", | ||||||
|  "owner": "Administrator", |  "owner": "Administrator", | ||||||
|  | |||||||
| @ -1,19 +1,19 @@ | |||||||
| { | { | ||||||
|  "action": "Go to Page", |  "action": "Update Settings", | ||||||
|  "creation": "2020-06-04 16:34:29.664917", |  "creation": "2020-06-04 16:34:29.664917", | ||||||
|  "docstatus": 0, |  "docstatus": 0, | ||||||
|  "doctype": "Onboarding Step", |  "doctype": "Onboarding Step", | ||||||
|  "idx": 0, |  "idx": 0, | ||||||
|  "is_complete": 0, |  "is_complete": 0, | ||||||
|  "is_mandatory": 0, |  "is_mandatory": 0, | ||||||
|  "is_single": 0, |  "is_single": 1, | ||||||
|  "is_skipped": 0, |  "is_skipped": 0, | ||||||
|  "modified": "2020-06-04 16:34:29.664917", |  "modified": "2020-06-29 16:34:29.664917", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "name": "Payroll Settings", |  "name": "Payroll Settings", | ||||||
|  "owner": "Administrator", |  "owner": "Administrator", | ||||||
|  "path": "#Form/Payroll Settings", |  "reference_document": "Payroll Settings", | ||||||
|  "show_full_form": 0, |  "show_full_form": 0, | ||||||
|  "title": "Payroll Settings", |  "title": "Payroll Settings", | ||||||
|  "validate_action": 1 |  "validate_action": 0 | ||||||
| } | } | ||||||
| @ -10,10 +10,12 @@ frappe.ui.form.on('Products Settings', { | |||||||
| 				df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden | 				df => ['Link', 'Table MultiSelect'].includes(df.fieldtype) && !df.hidden | ||||||
| 			).map(df => ({ label: df.label, value: df.fieldname })); | 			).map(df => ({ label: df.label, value: df.fieldname })); | ||||||
| 
 | 
 | ||||||
| 			const field = frappe.meta.get_docfield("Website Filter Field", "fieldname", frm.docname); | 			frm.fields_dict.filter_fields.grid.update_docfield_property( | ||||||
| 			field.fieldtype = 'Select'; | 				'fieldname', 'fieldtype', 'Select' | ||||||
| 			field.options = valid_fields; | 			); | ||||||
| 			frm.fields_dict.filter_fields.grid.refresh(); | 			frm.fields_dict.filter_fields.grid.update_docfield_property( | ||||||
|  | 				'fieldname', 'options', valid_fields | ||||||
|  | 			); | ||||||
| 		}); | 		}); | ||||||
| 	} | 	} | ||||||
| }); | }); | ||||||
|  | |||||||
| @ -276,74 +276,3 @@ erpnext.taxes.set_conditional_mandatory_rate_or_amount = function(grid_row) { | |||||||
| 		} | 		} | ||||||
| 	} | 	} | ||||||
| } | } | ||||||
| 
 |  | ||||||
| 
 |  | ||||||
| // For customizing print
 |  | ||||||
| cur_frm.pformat.total = function(doc) { return ''; } |  | ||||||
| cur_frm.pformat.discount_amount = function(doc) { return ''; } |  | ||||||
| cur_frm.pformat.grand_total = function(doc) { return ''; } |  | ||||||
| cur_frm.pformat.rounded_total = function(doc) { return ''; } |  | ||||||
| cur_frm.pformat.in_words = function(doc) { return ''; } |  | ||||||
| 
 |  | ||||||
| cur_frm.pformat.taxes= function(doc){ |  | ||||||
| 	//function to make row of table
 |  | ||||||
| 	var make_row = function(title, val, bold, is_negative) { |  | ||||||
| 		var bstart = '<b>'; var bend = '</b>'; |  | ||||||
| 		return '<tr><td style="width:50%;">' + (bold?bstart:'') + title + (bold?bend:'') + '</td>' |  | ||||||
| 			+ '<td style="width:50%;text-align:right;">' + (is_negative ? '- ' : '') |  | ||||||
| 		+ format_currency(val, doc.currency) + '</td></tr>'; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	function print_hide(fieldname) { |  | ||||||
| 		var doc_field = frappe.meta.get_docfield(doc.doctype, fieldname, doc.name); |  | ||||||
| 		return doc_field.print_hide; |  | ||||||
| 	} |  | ||||||
| 
 |  | ||||||
| 	out =''; |  | ||||||
| 	if (!doc.print_without_amount) { |  | ||||||
| 		var cl = doc.taxes || []; |  | ||||||
| 
 |  | ||||||
| 		// outer table
 |  | ||||||
| 		var out='<div><table class="noborder" style="width:100%"><tr><td style="width: 60%"></td><td>'; |  | ||||||
| 
 |  | ||||||
| 		// main table
 |  | ||||||
| 
 |  | ||||||
| 		out +='<table class="noborder" style="width:100%">'; |  | ||||||
| 
 |  | ||||||
| 		if(!print_hide('total')) { |  | ||||||
| 			out += make_row('Total', doc.total, 1); |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Discount Amount on net total
 |  | ||||||
| 		if(!print_hide('discount_amount') && doc.apply_discount_on == "Net Total" && doc.discount_amount) |  | ||||||
| 			out += make_row('Discount Amount', doc.discount_amount, 0, 1); |  | ||||||
| 
 |  | ||||||
| 		// add rows
 |  | ||||||
| 		if(cl.length){ |  | ||||||
| 			for(var i=0;i<cl.length;i++) { |  | ||||||
| 				if(cl[i].tax_amount!=0 && !cl[i].included_in_print_rate) |  | ||||||
| 					out += make_row(cl[i].description, cl[i].tax_amount, 0); |  | ||||||
| 			} |  | ||||||
| 		} |  | ||||||
| 
 |  | ||||||
| 		// Discount Amount on grand total
 |  | ||||||
| 		if(!print_hide('discount_amount') && doc.apply_discount_on == "Grand Total" && doc.discount_amount) |  | ||||||
| 			out += make_row('Discount Amount', doc.discount_amount, 0, 1); |  | ||||||
| 
 |  | ||||||
| 		// grand total
 |  | ||||||
| 		if(!print_hide('grand_total')) |  | ||||||
| 			out += make_row('Grand Total', doc.grand_total, 1); |  | ||||||
| 
 |  | ||||||
| 		if(!print_hide('rounded_total')) |  | ||||||
| 			out += make_row('Rounded Total', doc.rounded_total, 1); |  | ||||||
| 
 |  | ||||||
| 		if(doc.in_words && !print_hide('in_words')) { |  | ||||||
| 			out +='</table></td></tr>'; |  | ||||||
| 			out += '<tr><td colspan = "2">'; |  | ||||||
| 			out += '<table><tr><td style="width:25%;"><b>In Words</b></td>'; |  | ||||||
| 			out += '<td style="width:50%;">' + doc.in_words + '</td></tr>'; |  | ||||||
| 		} |  | ||||||
| 		out += '</table></td></tr></table></div>'; |  | ||||||
| 	} |  | ||||||
| 	return out; |  | ||||||
| } |  | ||||||
							
								
								
									
										14
									
								
								erpnext/public/js/website_theme.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										14
									
								
								erpnext/public/js/website_theme.js
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,14 @@ | |||||||
|  | // Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
 | ||||||
|  | // MIT License. See license.txt
 | ||||||
|  | 
 | ||||||
|  | frappe.ui.form.on('Website Theme', { | ||||||
|  | 	validate(frm) { | ||||||
|  | 		let theme_scss = frm.doc.theme_scss; | ||||||
|  | 		if (theme_scss && theme_scss.includes('frappe/public/scss/website') | ||||||
|  | 			&& !theme_scss.includes('erpnext/public/scss/website') | ||||||
|  | 		) { | ||||||
|  | 			frm.set_value('theme_scss', | ||||||
|  | 				`${frm.doc.theme_scss}\n@import "erpnext/public/scss/website";`); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  | }); | ||||||
| @ -8,6 +8,7 @@ | |||||||
|   "enable", |   "enable", | ||||||
|   "section_break_2", |   "section_break_2", | ||||||
|   "sandbox_mode", |   "sandbox_mode", | ||||||
|  |   "applicable_from", | ||||||
|   "credentials", |   "credentials", | ||||||
|   "auth_token", |   "auth_token", | ||||||
|   "token_expiry" |   "token_expiry" | ||||||
| @ -48,12 +49,19 @@ | |||||||
|    "fieldname": "sandbox_mode", |    "fieldname": "sandbox_mode", | ||||||
|    "fieldtype": "Check", |    "fieldtype": "Check", | ||||||
|    "label": "Sandbox Mode" |    "label": "Sandbox Mode" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "applicable_from", | ||||||
|  |    "fieldtype": "Date", | ||||||
|  |    "in_list_view": 1, | ||||||
|  |    "label": "Applicable From", | ||||||
|  |    "reqd": 1 | ||||||
|   } |   } | ||||||
|  ], |  ], | ||||||
|  "index_web_pages_for_search": 1, |  "index_web_pages_for_search": 1, | ||||||
|  "issingle": 1, |  "issingle": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-01-13 12:04:49.449199", |  "modified": "2021-03-30 12:26:25.538294", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Regional", |  "module": "Regional", | ||||||
|  "name": "E Invoice Settings", |  "name": "E Invoice Settings", | ||||||
|  | |||||||
| @ -5,6 +5,7 @@ | |||||||
|  "editable_grid": 1, |  "editable_grid": 1, | ||||||
|  "engine": "InnoDB", |  "engine": "InnoDB", | ||||||
|  "field_order": [ |  "field_order": [ | ||||||
|  |   "company", | ||||||
|   "gstin", |   "gstin", | ||||||
|   "username", |   "username", | ||||||
|   "password" |   "password" | ||||||
| @ -30,12 +31,20 @@ | |||||||
|    "in_list_view": 1, |    "in_list_view": 1, | ||||||
|    "label": "Password", |    "label": "Password", | ||||||
|    "reqd": 1 |    "reqd": 1 | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "fieldname": "company", | ||||||
|  |    "fieldtype": "Link", | ||||||
|  |    "in_list_view": 1, | ||||||
|  |    "label": "Company", | ||||||
|  |    "options": "Company", | ||||||
|  |    "reqd": 1 | ||||||
|   } |   } | ||||||
|  ], |  ], | ||||||
|  "index_web_pages_for_search": 1, |  "index_web_pages_for_search": 1, | ||||||
|  "istable": 1, |  "istable": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2020-12-22 15:10:53.466205", |  "modified": "2021-03-22 12:16:56.365616", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "Regional", |  "module": "Regional", | ||||||
|  "name": "E Invoice User", |  "name": "E Invoice User", | ||||||
|  | |||||||
| @ -3,4 +3,17 @@ import frappe | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def setup(company=None, patch=True): | def setup(company=None, patch=True): | ||||||
| 	pass | 	add_custom_roles_for_reports() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
|  | def add_custom_roles_for_reports(): | ||||||
|  | 	"""Add Access Control to UAE VAT 201.""" | ||||||
|  | 	if not frappe.db.get_value('Custom Role', dict(report='DATEV')): | ||||||
|  | 		frappe.get_doc(dict( | ||||||
|  | 			doctype='Custom Role', | ||||||
|  | 			report='DATEV', | ||||||
|  | 			roles= [ | ||||||
|  | 				dict(role='Accounts User'), | ||||||
|  | 				dict(role='Accounts Manager') | ||||||
|  | 			] | ||||||
|  | 		)).insert() | ||||||
| @ -69,7 +69,7 @@ state_numbers = { | |||||||
|  "Mizoram": "15", |  "Mizoram": "15", | ||||||
|  "Nagaland": "13", |  "Nagaland": "13", | ||||||
|  "Odisha": "21", |  "Odisha": "21", | ||||||
|  "Other Territory": "98", |  "Other Territory": "97", | ||||||
|  "Pondicherry": "34", |  "Pondicherry": "34", | ||||||
|  "Punjab": "03", |  "Punjab": "03", | ||||||
|  "Rajasthan": "08", |  "Rajasthan": "08", | ||||||
|  | |||||||
| @ -1,12 +1,13 @@ | |||||||
| erpnext.setup_einvoice_actions = (doctype) => { | erpnext.setup_einvoice_actions = (doctype) => { | ||||||
| 	frappe.ui.form.on(doctype, { | 	frappe.ui.form.on(doctype, { | ||||||
| 		async refresh(frm) { | 		async refresh(frm) { | ||||||
| 			const einvoicing_enabled = await frappe.db.get_single_value("E Invoice Settings", "enable"); | 			const res = await frappe.call({ | ||||||
| 			const supply_type = frm.doc.gst_category; | 				method: 'erpnext.regional.india.e_invoice.utils.validate_eligibility', | ||||||
| 			const valid_supply_type = ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'].includes(supply_type); | 				args: { doc: frm.doc } | ||||||
| 			const company_transaction = frm.doc.billing_address_gstin == frm.doc.company_gstin; | 			}); | ||||||
|  | 			const invoice_eligible = res.message; | ||||||
| 
 | 
 | ||||||
| 			if (cint(einvoicing_enabled) == 0 || !valid_supply_type || company_transaction) return; | 			if (!invoice_eligible) return; | ||||||
| 
 | 
 | ||||||
| 			const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; | 			const { doctype, irn, irn_cancelled, ewaybill, eway_bill_cancelled, name, __unsaved } = frm.doc; | ||||||
| 
 | 
 | ||||||
| @ -109,45 +110,25 @@ erpnext.setup_einvoice_actions = (doctype) => { | |||||||
| 			} | 			} | ||||||
| 
 | 
 | ||||||
| 			if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { | 			if (irn && ewaybill && !irn_cancelled && !eway_bill_cancelled) { | ||||||
| 				const fields = [ |  | ||||||
| 					{ |  | ||||||
| 						"label": "Reason", |  | ||||||
| 						"fieldname": "reason", |  | ||||||
| 						"fieldtype": "Select", |  | ||||||
| 						"reqd": 1, |  | ||||||
| 						"default": "1-Duplicate", |  | ||||||
| 						"options": ["1-Duplicate", "2-Data Entry Error", "3-Order Cancelled", "4-Other"] |  | ||||||
| 					}, |  | ||||||
| 					{ |  | ||||||
| 						"label": "Remark", |  | ||||||
| 						"fieldname": "remark", |  | ||||||
| 						"fieldtype": "Data", |  | ||||||
| 						"reqd": 1 |  | ||||||
| 					} |  | ||||||
| 				]; |  | ||||||
| 				const action = () => { | 				const action = () => { | ||||||
| 					const d = new frappe.ui.Dialog({ | 					let message = __('Cancellation of e-way bill is currently not supported. '); | ||||||
| 						title: __('Cancel E-Way Bill'), | 					message += '<br><br>'; | ||||||
| 						fields: fields, | 					message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); | ||||||
|  | 
 | ||||||
|  | 					frappe.msgprint({ | ||||||
|  | 						title: __('Update E-Way Bill Cancelled Status?'), | ||||||
|  | 						message: message, | ||||||
|  | 						indicator: 'orange', | ||||||
| 						primary_action: function() { | 						primary_action: function() { | ||||||
| 							const data = d.get_values(); |  | ||||||
| 							frappe.call({ | 							frappe.call({ | ||||||
| 								method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', | 								method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', | ||||||
| 								args: { | 								args: { doctype, docname: name }, | ||||||
| 									doctype, |  | ||||||
| 									docname: name, |  | ||||||
| 									eway_bill: ewaybill, |  | ||||||
| 									reason: data.reason.split('-')[0], |  | ||||||
| 									remark: data.remark |  | ||||||
| 								}, |  | ||||||
| 								freeze: true, | 								freeze: true, | ||||||
| 								callback: () => frm.reload_doc() || d.hide(), | 								callback: () => frm.reload_doc() | ||||||
| 								error: () => d.hide() |  | ||||||
| 							}); | 							}); | ||||||
| 						}, | 						}, | ||||||
| 						primary_action_label: __('Submit') | 						primary_action_label: __('Yes') | ||||||
| 					}); | 					}); | ||||||
| 					d.show(); |  | ||||||
| 				}; | 				}; | ||||||
| 				add_custom_button(__("Cancel E-Way Bill"), action); | 				add_custom_button(__("Cancel E-Way Bill"), action); | ||||||
| 			} | 			} | ||||||
|  | |||||||
| @ -15,18 +15,43 @@ import traceback | |||||||
| import io | import io | ||||||
| from frappe import _, bold | from frappe import _, bold | ||||||
| from pyqrcode import create as qrcreate | from pyqrcode import create as qrcreate | ||||||
|  | from frappe.utils.background_jobs import enqueue | ||||||
|  | from frappe.utils.scheduler import is_scheduler_inactive | ||||||
|  | from frappe.core.page.background_jobs.background_jobs import get_info | ||||||
| from frappe.integrations.utils import make_post_request, make_get_request | from frappe.integrations.utils import make_post_request, make_get_request | ||||||
| from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply | from erpnext.regional.india.utils import get_gst_accounts, get_place_of_supply | ||||||
| from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form | from frappe.utils.data import cstr, cint, format_date, flt, time_diff_in_seconds, now_datetime, add_to_date, get_link_to_form, getdate, time_diff_in_hours | ||||||
|  | 
 | ||||||
|  | @frappe.whitelist() | ||||||
|  | def validate_eligibility(doc): | ||||||
|  | 	if isinstance(doc, six.string_types): | ||||||
|  | 		doc = json.loads(doc) | ||||||
|  | 
 | ||||||
|  | 	invalid_doctype = doc.get('doctype') != 'Sales Invoice' | ||||||
|  | 	if invalid_doctype: | ||||||
|  | 		return False | ||||||
|  | 
 | ||||||
|  | 	einvoicing_enabled = cint(frappe.db.get_single_value('E Invoice Settings', 'enable')) | ||||||
|  | 	if not einvoicing_enabled: | ||||||
|  | 		return False | ||||||
|  | 
 | ||||||
|  | 	einvoicing_eligible_from = frappe.db.get_single_value('E Invoice Settings', 'applicable_from') or '2021-04-01' | ||||||
|  | 	if getdate(doc.get('posting_date')) < getdate(einvoicing_eligible_from): | ||||||
|  | 		return False | ||||||
| 
 | 
 | ||||||
| def validate_einvoice_fields(doc): |  | ||||||
| 	einvoicing_enabled = cint(frappe.db.get_value('E Invoice Settings', 'E Invoice Settings', 'enable')) |  | ||||||
| 	invalid_doctype = doc.doctype != 'Sales Invoice' |  | ||||||
| 	invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] | 	invalid_supply_type = doc.get('gst_category') not in ['Registered Regular', 'SEZ', 'Overseas', 'Deemed Export'] | ||||||
| 	company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') | 	company_transaction = doc.get('billing_address_gstin') == doc.get('company_gstin') | ||||||
| 	no_taxes_applied = not doc.get('taxes') | 	no_taxes_applied = not doc.get('taxes') | ||||||
| 
 | 
 | ||||||
| 	if not einvoicing_enabled or invalid_doctype or invalid_supply_type or company_transaction or no_taxes_applied: | 	if invalid_supply_type or company_transaction or no_taxes_applied: | ||||||
|  | 		return False | ||||||
|  | 
 | ||||||
|  | 	return True | ||||||
|  | 
 | ||||||
|  | def validate_einvoice_fields(doc): | ||||||
|  | 	invoice_eligible = validate_eligibility(doc) | ||||||
|  | 
 | ||||||
|  | 	if not invoice_eligible: | ||||||
| 		return | 		return | ||||||
| 
 | 
 | ||||||
| 	if doc.docstatus == 0 and doc._action == 'save': | 	if doc.docstatus == 0 and doc._action == 'save': | ||||||
| @ -35,6 +60,8 @@ def validate_einvoice_fields(doc): | |||||||
| 		if len(doc.name) > 16: | 		if len(doc.name) > 16: | ||||||
| 			raise_document_name_too_long_error() | 			raise_document_name_too_long_error() | ||||||
| 
 | 
 | ||||||
|  | 		doc.einvoice_status = 'Pending' | ||||||
|  | 
 | ||||||
| 	elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: | 	elif doc.docstatus == 1 and doc._action == 'submit' and not doc.irn: | ||||||
| 		frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) | 		frappe.throw(_('You must generate IRN before submitting the document.'), title=_('Missing IRN')) | ||||||
| 
 | 
 | ||||||
| @ -76,6 +103,9 @@ def get_transaction_details(invoice): | |||||||
| 	)) | 	)) | ||||||
| 
 | 
 | ||||||
| def get_doc_details(invoice): | def get_doc_details(invoice): | ||||||
|  | 	if getdate(invoice.posting_date) < getdate('2021-01-01'): | ||||||
|  | 		frappe.throw(_('IRN generation is not allowed for invoices dated before 1st Jan 2021'), title=_('Not Allowed')) | ||||||
|  | 
 | ||||||
| 	invoice_type = 'CRN' if invoice.is_return else 'INV' | 	invoice_type = 'CRN' if invoice.is_return else 'INV' | ||||||
| 
 | 
 | ||||||
| 	invoice_name = invoice.name | 	invoice_name = invoice.name | ||||||
| @ -87,56 +117,39 @@ def get_doc_details(invoice): | |||||||
| 		invoice_date=invoice_date | 		invoice_date=invoice_date | ||||||
| 	)) | 	)) | ||||||
| 
 | 
 | ||||||
| def get_party_details(address_name, company_address=None, billing_address=None, shipping_address=None): | def validate_address_fields(address, is_shipping_address): | ||||||
| 	d = frappe.get_all('Address', filters={'name': address_name}, fields=['*'])[0] | 	if ((not address.gstin and not is_shipping_address) | ||||||
| 
 | 		or not address.city | ||||||
| 	if ((not d.gstin and not shipping_address) | 		or not address.pincode | ||||||
| 		or not d.city | 		or not address.address_title | ||||||
| 		or not d.pincode | 		or not address.address_line1 | ||||||
| 		or not d.address_title | 		or not address.gst_state_number): | ||||||
| 		or not d.address_line1 |  | ||||||
| 		or not d.gst_state_number): |  | ||||||
| 
 | 
 | ||||||
| 		frappe.throw( | 		frappe.throw( | ||||||
| 			msg=_('Address lines, city, pincode, gstin is mandatory for address {}. Please set them and try again.').format( | 			msg=_('Address Lines, City, Pincode, GSTIN are mandatory for address {}. Please set them and try again.').format(address.name), | ||||||
| 				get_link_to_form('Address', address_name) |  | ||||||
| 			), |  | ||||||
| 			title=_('Missing Address Fields') | 			title=_('Missing Address Fields') | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
| 	if d.gst_state_number == 97: | def get_party_details(address_name, is_shipping_address=False): | ||||||
|  | 	addr = frappe.get_doc('Address', address_name) | ||||||
|  | 	 | ||||||
|  | 	validate_address_fields(addr, is_shipping_address) | ||||||
|  | 
 | ||||||
|  | 	if addr.gst_state_number == 97: | ||||||
| 		# according to einvoice standard | 		# according to einvoice standard | ||||||
| 		pincode = 999999 | 		addr.pincode = 999999 | ||||||
| 
 | 
 | ||||||
| 	party_address_details = frappe._dict(dict( | 	party_address_details = frappe._dict(dict( | ||||||
| 		legal_name=sanitize_for_json(d.address_title), | 		legal_name=sanitize_for_json(addr.address_title), | ||||||
| 		location=sanitize_for_json(d.city), | 		location=sanitize_for_json(addr.city), | ||||||
| 		pincode=d.pincode, | 		pincode=addr.pincode, gstin=addr.gstin, | ||||||
| 		state_code=d.gst_state_number, | 		state_code=addr.gst_state_number, | ||||||
| 		address_line1=sanitize_for_json(d.address_line1), | 		address_line1=sanitize_for_json(addr.address_line1), | ||||||
| 		address_line2=sanitize_for_json(d.address_line2) | 		address_line2=sanitize_for_json(addr.address_line2) | ||||||
| 	)) | 	)) | ||||||
| 	if d.gstin: | 
 | ||||||
| 		party_address_details.gstin = d.gstin |  | ||||||
| 	return party_address_details | 	return party_address_details | ||||||
| 
 | 
 | ||||||
| def get_gstin_details(gstin): |  | ||||||
| 	if not hasattr(frappe.local, 'gstin_cache'): |  | ||||||
| 		frappe.local.gstin_cache = {} |  | ||||||
| 
 |  | ||||||
| 	key = gstin |  | ||||||
| 	details = frappe.local.gstin_cache.get(key) |  | ||||||
| 	if details: |  | ||||||
| 		return details |  | ||||||
| 
 |  | ||||||
| 	details = frappe.cache().hget('gstin_cache', key) |  | ||||||
| 	if details: |  | ||||||
| 		frappe.local.gstin_cache[key] = details |  | ||||||
| 		return details |  | ||||||
| 
 |  | ||||||
| 	if not details: |  | ||||||
| 		return GSPConnector.get_gstin_details(gstin) |  | ||||||
| 
 |  | ||||||
| def get_overseas_address_details(address_name): | def get_overseas_address_details(address_name): | ||||||
| 	address_title, address_line1, address_line2, city = frappe.db.get_value( | 	address_title, address_line1, address_line2, city = frappe.db.get_value( | ||||||
| 		'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city'] | 		'Address', address_name, ['address_title', 'address_line1', 'address_line2', 'city'] | ||||||
| @ -171,10 +184,15 @@ def get_item_list(invoice): | |||||||
| 		item.description = sanitize_for_json(d.item_name) | 		item.description = sanitize_for_json(d.item_name) | ||||||
| 
 | 
 | ||||||
| 		item.qty = abs(item.qty) | 		item.qty = abs(item.qty) | ||||||
|  | 
 | ||||||
|  | 		if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: | ||||||
|  | 			item.discount_amount = abs(item.base_amount - item.base_net_amount) | ||||||
|  | 		else: | ||||||
| 			item.discount_amount = 0 | 			item.discount_amount = 0 | ||||||
| 		item.unit_rate = abs(item.base_net_amount / item.qty) | 
 | ||||||
| 		item.gross_amount = abs(item.base_net_amount) | 		item.unit_rate = abs((abs(item.taxable_value) - item.discount_amount)/ item.qty) | ||||||
| 		item.taxable_value = abs(item.base_net_amount) | 		item.gross_amount = abs(item.taxable_value) + item.discount_amount | ||||||
|  | 		item.taxable_value = abs(item.taxable_value) | ||||||
| 
 | 
 | ||||||
| 		item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None | 		item.batch_expiry_date = frappe.db.get_value('Batch', d.batch_no, 'expiry_date') if d.batch_no else None | ||||||
| 		item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None | 		item.batch_expiry_date = format_date(item.batch_expiry_date, 'dd/mm/yyyy') if item.batch_expiry_date else None | ||||||
| @ -207,11 +225,11 @@ def update_item_taxes(invoice, item): | |||||||
| 		is_applicable = t.tax_amount and t.account_head in gst_accounts_list | 		is_applicable = t.tax_amount and t.account_head in gst_accounts_list | ||||||
| 		if is_applicable: | 		if is_applicable: | ||||||
| 			# this contains item wise tax rate & tax amount (incl. discount) | 			# this contains item wise tax rate & tax amount (incl. discount) | ||||||
| 			item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code) | 			item_tax_detail = json.loads(t.item_wise_tax_detail).get(item.item_code or item.item_name) | ||||||
| 
 | 
 | ||||||
| 			item_tax_rate = item_tax_detail[0] | 			item_tax_rate = item_tax_detail[0] | ||||||
| 			# item tax amount excluding discount amount | 			# item tax amount excluding discount amount | ||||||
| 			item_tax_amount = (item_tax_rate / 100) * item.base_net_amount | 			item_tax_amount = (item_tax_rate / 100) * item.taxable_value | ||||||
| 
 | 
 | ||||||
| 			if t.account_head in gst_accounts.cess_account: | 			if t.account_head in gst_accounts.cess_account: | ||||||
| 				item_tax_amount_after_discount = item_tax_detail[1] | 				item_tax_amount_after_discount = item_tax_detail[1] | ||||||
| @ -225,6 +243,9 @@ def update_item_taxes(invoice, item): | |||||||
| 				if t.account_head in gst_accounts[f'{tax_type}_account']: | 				if t.account_head in gst_accounts[f'{tax_type}_account']: | ||||||
| 					item.tax_rate += item_tax_rate | 					item.tax_rate += item_tax_rate | ||||||
| 					item[f'{tax_type}_amount'] += abs(item_tax_amount) | 					item[f'{tax_type}_amount'] += abs(item_tax_amount) | ||||||
|  | 		else: | ||||||
|  | 			# TODO: other charges per item | ||||||
|  | 			pass | ||||||
| 
 | 
 | ||||||
| 	return item | 	return item | ||||||
| 
 | 
 | ||||||
| @ -232,10 +253,14 @@ def get_invoice_value_details(invoice): | |||||||
| 	invoice_value_details = frappe._dict(dict()) | 	invoice_value_details = frappe._dict(dict()) | ||||||
| 
 | 
 | ||||||
| 	if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: | 	if invoice.apply_discount_on == 'Net Total' and invoice.discount_amount: | ||||||
| 		invoice_value_details.base_total = abs(invoice.base_total) | 		# Discount already applied on net total which means on items | ||||||
| 		invoice_value_details.invoice_discount_amt = abs(invoice.base_discount_amount) | 		invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) | ||||||
|  | 		invoice_value_details.invoice_discount_amt = 0 | ||||||
|  | 	elif invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount: | ||||||
|  | 		invoice_value_details.invoice_discount_amt = invoice.base_discount_amount | ||||||
|  | 		invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) | ||||||
| 	else: | 	else: | ||||||
| 		invoice_value_details.base_total = abs(invoice.base_net_total) | 		invoice_value_details.base_total = abs(sum([i.taxable_value for i in invoice.get('items')])) | ||||||
| 		# since tax already considers discount amount | 		# since tax already considers discount amount | ||||||
| 		invoice_value_details.invoice_discount_amt = 0 | 		invoice_value_details.invoice_discount_amt = 0 | ||||||
| 
 | 
 | ||||||
| @ -256,7 +281,11 @@ def update_invoice_taxes(invoice, invoice_value_details): | |||||||
| 	invoice_value_details.total_igst_amt = 0 | 	invoice_value_details.total_igst_amt = 0 | ||||||
| 	invoice_value_details.total_cess_amt = 0 | 	invoice_value_details.total_cess_amt = 0 | ||||||
| 	invoice_value_details.total_other_charges = 0 | 	invoice_value_details.total_other_charges = 0 | ||||||
|  | 	considered_rows = [] | ||||||
|  | 
 | ||||||
| 	for t in invoice.taxes: | 	for t in invoice.taxes: | ||||||
|  | 		tax_amount = t.base_tax_amount if (invoice.apply_discount_on == 'Grand Total' and invoice.discount_amount) \ | ||||||
|  | 						else t.base_tax_amount_after_discount_amount | ||||||
| 		if t.account_head in gst_accounts_list: | 		if t.account_head in gst_accounts_list: | ||||||
| 			if t.account_head in gst_accounts.cess_account: | 			if t.account_head in gst_accounts.cess_account: | ||||||
| 				# using after discount amt since item also uses after discount amt for cess calc | 				# using after discount amt since item also uses after discount amt for cess calc | ||||||
| @ -264,12 +293,26 @@ def update_invoice_taxes(invoice, invoice_value_details): | |||||||
| 
 | 
 | ||||||
| 			for tax_type in ['igst', 'cgst', 'sgst']: | 			for tax_type in ['igst', 'cgst', 'sgst']: | ||||||
| 				if t.account_head in gst_accounts[f'{tax_type}_account']: | 				if t.account_head in gst_accounts[f'{tax_type}_account']: | ||||||
| 					invoice_value_details[f'total_{tax_type}_amt'] += abs(t.base_tax_amount_after_discount_amount) | 
 | ||||||
|  | 					invoice_value_details[f'total_{tax_type}_amt'] += abs(tax_amount) | ||||||
|  | 				update_other_charges(t, invoice_value_details, gst_accounts_list, invoice, considered_rows) | ||||||
| 		else: | 		else: | ||||||
| 			invoice_value_details.total_other_charges += abs(t.base_tax_amount_after_discount_amount) | 			invoice_value_details.total_other_charges += abs(tax_amount) | ||||||
| 
 | 
 | ||||||
| 	return invoice_value_details | 	return invoice_value_details | ||||||
| 
 | 
 | ||||||
|  | def update_other_charges(tax_row, invoice_value_details, gst_accounts_list, invoice, considered_rows): | ||||||
|  | 	prev_row_id = cint(tax_row.row_id) - 1 | ||||||
|  | 	if tax_row.account_head in gst_accounts_list and prev_row_id not in considered_rows: | ||||||
|  | 		if tax_row.charge_type == 'On Previous Row Amount': | ||||||
|  | 			amount = invoice.get('taxes')[prev_row_id].tax_amount_after_discount_amount | ||||||
|  | 			invoice_value_details.total_other_charges -= abs(amount) | ||||||
|  | 			considered_rows.append(prev_row_id) | ||||||
|  | 		if tax_row.charge_type == 'On Previous Row Total': | ||||||
|  | 			amount = invoice.get('taxes')[prev_row_id].base_total - invoice.base_net_total | ||||||
|  | 			invoice_value_details.total_other_charges -= abs(amount) | ||||||
|  | 			considered_rows.append(prev_row_id) | ||||||
|  | 
 | ||||||
| def get_payment_details(invoice): | def get_payment_details(invoice): | ||||||
| 	payee_name = invoice.company | 	payee_name = invoice.company | ||||||
| 	mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) | 	mode_of_payment = ', '.join([d.mode_of_payment for d in invoice.payments]) | ||||||
| @ -282,6 +325,10 @@ def get_payment_details(invoice): | |||||||
| 	)) | 	)) | ||||||
| 
 | 
 | ||||||
| def get_return_doc_reference(invoice): | def get_return_doc_reference(invoice): | ||||||
|  | 	if not invoice.return_against: | ||||||
|  | 		frappe.throw(_('For generating IRN, reference to the original invoice is mandatory for a credit note. Please set {} field to generate e-invoice.') | ||||||
|  | 			.format(frappe.bold('Return Against')), title=_('Missing Field')) | ||||||
|  | 
 | ||||||
| 	invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') | 	invoice_date = frappe.db.get_value('Sales Invoice', invoice.return_against, 'posting_date') | ||||||
| 	return frappe._dict(dict( | 	return frappe._dict(dict( | ||||||
| 		invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') | 		invoice_name=invoice.return_against, invoice_date=format_date(invoice_date, 'dd/mm/yyyy') | ||||||
| @ -289,7 +336,11 @@ def get_return_doc_reference(invoice): | |||||||
| 
 | 
 | ||||||
| def get_eway_bill_details(invoice): | def get_eway_bill_details(invoice): | ||||||
| 	if invoice.is_return: | 	if invoice.is_return: | ||||||
| 		frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes'), title=_('E Invoice Validation Failed')) | 		frappe.throw(_('E-Way Bill cannot be generated for Credit Notes & Debit Notes. Please clear fields in the Transporter Section of the invoice.'), | ||||||
|  | 			title=_('Invalid Fields')) | ||||||
|  | 	 | ||||||
|  | 	if not invoice.distance: | ||||||
|  | 		frappe.throw(_('Distance is mandatory for generating e-way bill for an e-invoice.'), title=_('Missing Field')) | ||||||
| 
 | 
 | ||||||
| 	mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } | 	mode_of_transport = { '': '', 'Road': '1', 'Air': '2', 'Rail': '3', 'Ship': '4' } | ||||||
| 	vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } | 	vehicle_type = { 'Regular': 'R', 'Over Dimensional Cargo (ODC)': 'O' } | ||||||
| @ -307,9 +358,15 @@ def get_eway_bill_details(invoice): | |||||||
| 
 | 
 | ||||||
| def validate_mandatory_fields(invoice): | def validate_mandatory_fields(invoice): | ||||||
| 	if not invoice.company_address: | 	if not invoice.company_address: | ||||||
| 		frappe.throw(_('Company Address is mandatory to fetch company GSTIN details.'), title=_('Missing Fields')) | 		frappe.throw( | ||||||
|  | 			_('Company Address is mandatory to fetch company GSTIN details. Please set Company Address and try again.'), | ||||||
|  | 			title=_('Missing Fields') | ||||||
|  | 		) | ||||||
| 	if not invoice.customer_address: | 	if not invoice.customer_address: | ||||||
| 		frappe.throw(_('Customer Address is mandatory to fetch customer GSTIN details.'), title=_('Missing Fields')) | 		frappe.throw( | ||||||
|  | 			_('Customer Address is mandatory to fetch customer GSTIN details. Please set Company Address and try again.'), | ||||||
|  | 			title=_('Missing Fields') | ||||||
|  | 		) | ||||||
| 	if not frappe.db.get_value('Address', invoice.company_address, 'gstin'): | 	if not frappe.db.get_value('Address', invoice.company_address, 'gstin'): | ||||||
| 		frappe.throw( | 		frappe.throw( | ||||||
| 			_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), | 			_('GSTIN is mandatory to fetch company GSTIN details. Please enter GSTIN in selected company address.'), | ||||||
| @ -321,6 +378,39 @@ def validate_mandatory_fields(invoice): | |||||||
| 			title=_('Missing Fields') | 			title=_('Missing Fields') | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
|  | def validate_totals(einvoice): | ||||||
|  | 	item_list = einvoice['ItemList'] | ||||||
|  | 	value_details = einvoice['ValDtls'] | ||||||
|  | 
 | ||||||
|  | 	total_item_ass_value = 0 | ||||||
|  | 	total_item_cgst_value = 0 | ||||||
|  | 	total_item_sgst_value = 0 | ||||||
|  | 	total_item_igst_value = 0 | ||||||
|  | 	total_item_value = 0 | ||||||
|  | 	for item in item_list: | ||||||
|  | 		total_item_ass_value += flt(item['AssAmt']) | ||||||
|  | 		total_item_cgst_value += flt(item['CgstAmt']) | ||||||
|  | 		total_item_sgst_value += flt(item['SgstAmt']) | ||||||
|  | 		total_item_igst_value += flt(item['IgstAmt']) | ||||||
|  | 		total_item_value += flt(item['TotItemVal']) | ||||||
|  | 
 | ||||||
|  | 		if abs(flt(item['AssAmt']) * flt(item['GstRt']) / 100) - (flt(item['CgstAmt']) + flt(item['SgstAmt']) + flt(item['IgstAmt'])) > 1: | ||||||
|  | 			frappe.throw(_('Row #{}: GST rate is invalid. Please remove tax rows with zero tax amount from taxes table.').format(item.idx)) | ||||||
|  | 
 | ||||||
|  | 	if abs(flt(value_details['AssVal']) - total_item_ass_value) > 1: | ||||||
|  | 		frappe.throw(_('Total Taxable Value of the items is not equal to the Invoice Net Total. Please check item taxes / discounts for any correction.')) | ||||||
|  | 
 | ||||||
|  | 	if abs(flt(value_details['TotInvVal']) + flt(value_details['Discount']) - total_item_value) > 1: | ||||||
|  | 		frappe.throw(_('Total Value of the items is not equal to the Invoice Grand Total. Please check item taxes / discounts for any correction.')) | ||||||
|  | 
 | ||||||
|  | 	calculated_invoice_value = \ | ||||||
|  | 			flt(value_details['AssVal']) + flt(value_details['CgstVal']) \ | ||||||
|  | 			+ flt(value_details['SgstVal']) + flt(value_details['IgstVal']) \ | ||||||
|  | 			+ flt(value_details['OthChrg']) - flt(value_details['Discount']) | ||||||
|  | 
 | ||||||
|  | 	if abs(flt(value_details['TotInvVal']) - calculated_invoice_value) > 1: | ||||||
|  | 		frappe.throw(_('Total Item Value + Taxes - Discount is not equal to the Invoice Grand Total. Please check taxes / discounts for any correction.')) | ||||||
|  | 
 | ||||||
| def make_einvoice(invoice): | def make_einvoice(invoice): | ||||||
| 	validate_mandatory_fields(invoice) | 	validate_mandatory_fields(invoice) | ||||||
| 
 | 
 | ||||||
| @ -330,12 +420,12 @@ def make_einvoice(invoice): | |||||||
| 	item_list = get_item_list(invoice) | 	item_list = get_item_list(invoice) | ||||||
| 	doc_details = get_doc_details(invoice) | 	doc_details = get_doc_details(invoice) | ||||||
| 	invoice_value_details = get_invoice_value_details(invoice) | 	invoice_value_details = get_invoice_value_details(invoice) | ||||||
| 	seller_details = get_party_details(invoice.company_address, company_address=1) | 	seller_details = get_party_details(invoice.company_address) | ||||||
| 
 | 
 | ||||||
| 	if invoice.gst_category == 'Overseas': | 	if invoice.gst_category == 'Overseas': | ||||||
| 		buyer_details = get_overseas_address_details(invoice.customer_address) | 		buyer_details = get_overseas_address_details(invoice.customer_address) | ||||||
| 	else: | 	else: | ||||||
| 		buyer_details = get_party_details(invoice.customer_address, billing_address=1) | 		buyer_details = get_party_details(invoice.customer_address) | ||||||
| 		place_of_supply = get_place_of_supply(invoice, invoice.doctype) | 		place_of_supply = get_place_of_supply(invoice, invoice.doctype) | ||||||
| 		if place_of_supply: | 		if place_of_supply: | ||||||
| 			place_of_supply = place_of_supply.split('-')[0] | 			place_of_supply = place_of_supply.split('-')[0] | ||||||
| @ -343,20 +433,23 @@ def make_einvoice(invoice): | |||||||
| 			place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2] | 			place_of_supply = sanitize_for_json(invoice.billing_address_gstin)[:2] | ||||||
| 		buyer_details.update(dict(place_of_supply=place_of_supply)) | 		buyer_details.update(dict(place_of_supply=place_of_supply)) | ||||||
| 
 | 
 | ||||||
|  | 	seller_details.update(dict(legal_name=invoice.company)) | ||||||
|  | 	buyer_details.update(dict(legal_name=invoice.customer_name or invoice.customer)) | ||||||
|  | 
 | ||||||
| 	shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) | 	shipping_details = payment_details = prev_doc_details = eway_bill_details = frappe._dict({}) | ||||||
| 	if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: | 	if invoice.shipping_address_name and invoice.customer_address != invoice.shipping_address_name: | ||||||
| 		if invoice.gst_category == 'Overseas': | 		if invoice.gst_category == 'Overseas': | ||||||
| 			shipping_details = get_overseas_address_details(invoice.shipping_address_name) | 			shipping_details = get_overseas_address_details(invoice.shipping_address_name) | ||||||
| 		else: | 		else: | ||||||
| 			shipping_details = get_party_details(invoice.shipping_address_name, shipping_address=1) | 			shipping_details = get_party_details(invoice.shipping_address_name, is_shipping_address=True) | ||||||
| 
 | 
 | ||||||
| 	if invoice.is_pos and invoice.base_paid_amount: | 	if invoice.is_pos and invoice.base_paid_amount: | ||||||
| 		payment_details = get_payment_details(invoice) | 		payment_details = get_payment_details(invoice) | ||||||
| 
 | 
 | ||||||
| 	if invoice.is_return and invoice.return_against: | 	if invoice.is_return: | ||||||
| 		prev_doc_details = get_return_doc_reference(invoice) | 		prev_doc_details = get_return_doc_reference(invoice) | ||||||
| 
 | 
 | ||||||
| 	if invoice.transporter: | 	if invoice.transporter and flt(invoice.distance) and not invoice.is_return: | ||||||
| 		eway_bill_details = get_eway_bill_details(invoice) | 		eway_bill_details = get_eway_bill_details(invoice) | ||||||
| 
 | 
 | ||||||
| 	# not yet implemented | 	# not yet implemented | ||||||
| @ -369,18 +462,70 @@ def make_einvoice(invoice): | |||||||
| 		period_details=period_details, prev_doc_details=prev_doc_details, | 		period_details=period_details, prev_doc_details=prev_doc_details, | ||||||
| 		export_details=export_details, eway_bill_details=eway_bill_details | 		export_details=export_details, eway_bill_details=eway_bill_details | ||||||
| 	) | 	) | ||||||
| 	einvoice = safe_json_load(einvoice) |  | ||||||
| 
 | 
 | ||||||
| 	validations = json.loads(read_json('einv_validation')) | 	try: | ||||||
| 	errors = validate_einvoice(validations, einvoice) | 		einvoice = safe_json_load(einvoice) | ||||||
| 	if errors: | 		einvoice = santize_einvoice_fields(einvoice) | ||||||
|  | 		validate_totals(einvoice) | ||||||
|  | 
 | ||||||
|  | 	except Exception: | ||||||
|  | 		log_error(einvoice) | ||||||
|  | 		link_to_error_list = '<a href="List/Error Log/List?method=E Invoice Request Failed">Error Log</a>' | ||||||
|  | 		frappe.throw( | ||||||
|  | 			_('An error occurred while creating e-invoice for {}. Please check {} for more information.').format( | ||||||
|  | 				invoice.name, link_to_error_list), | ||||||
|  | 			title=_('E Invoice Creation Failed') | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | 	return einvoice | ||||||
|  | 
 | ||||||
|  | def log_error(data=None): | ||||||
|  | 	if not isinstance(data, dict): | ||||||
|  | 		data = json.loads(data) | ||||||
|  | 
 | ||||||
|  | 	seperator = "--" * 50 | ||||||
|  | 	err_tb = traceback.format_exc() | ||||||
|  | 	err_msg = str(sys.exc_info()[1]) | ||||||
|  | 	data = json.dumps(data, indent=4) | ||||||
|  | 
 | ||||||
| 	message = "\n".join([ | 	message = "\n".join([ | ||||||
| 			"E Invoice: ", json.dumps(einvoice, indent=4), | 		"Error", err_msg, seperator, | ||||||
| 			"-" * 50, | 		"Data:", data, seperator, | ||||||
| 			"Errors: ", json.dumps(errors, indent=4) | 		"Exception:", err_tb | ||||||
| 	]) | 	]) | ||||||
| 		frappe.log_error(title="E Invoice Validation Failed", message=message) | 	frappe.log_error(title=_('E Invoice Request Failed'), message=message) | ||||||
| 		frappe.throw(errors, title=_('E Invoice Validation Failed'), as_list=1) | 
 | ||||||
|  | def santize_einvoice_fields(einvoice): | ||||||
|  | 	int_fields = ["Pin","Distance","CrDay"] | ||||||
|  | 	float_fields = ["Qty","FreeQty","UnitPrice","TotAmt","Discount","PreTaxVal","AssAmt","GstRt","IgstAmt","CgstAmt","SgstAmt","CesRt","CesAmt","CesNonAdvlAmt","StateCesRt","StateCesAmt","StateCesNonAdvlAmt","OthChrg","TotItemVal","AssVal","CgstVal","SgstVal","IgstVal","CesVal","StCesVal","Discount","OthChrg","RndOffAmt","TotInvVal","TotInvValFc","PaidAmt","PaymtDue","ExpDuty",] | ||||||
|  | 	copy = einvoice.copy() | ||||||
|  | 	for key, value in copy.items(): | ||||||
|  | 		if isinstance(value, list): | ||||||
|  | 			for idx, d in enumerate(value): | ||||||
|  | 				santized_dict = santize_einvoice_fields(d) | ||||||
|  | 				if santized_dict: | ||||||
|  | 					einvoice[key][idx] = santized_dict | ||||||
|  | 				else: | ||||||
|  | 					einvoice[key].pop(idx) | ||||||
|  | 
 | ||||||
|  | 			if not einvoice[key]: | ||||||
|  | 				einvoice.pop(key, None) | ||||||
|  | 
 | ||||||
|  | 		elif isinstance(value, dict): | ||||||
|  | 			santized_dict = santize_einvoice_fields(value) | ||||||
|  | 			if santized_dict: | ||||||
|  | 				einvoice[key] = santized_dict | ||||||
|  | 			else: | ||||||
|  | 				einvoice.pop(key, None) | ||||||
|  | 
 | ||||||
|  | 		elif not value or value == "None": | ||||||
|  | 			einvoice.pop(key, None) | ||||||
|  | 
 | ||||||
|  | 		elif key in float_fields: | ||||||
|  | 			einvoice[key] = flt(value, 2) | ||||||
|  | 
 | ||||||
|  | 		elif key in int_fields: | ||||||
|  | 			einvoice[key] = cint(value) | ||||||
| 
 | 
 | ||||||
| 	return einvoice | 	return einvoice | ||||||
| 
 | 
 | ||||||
| @ -396,72 +541,22 @@ def safe_json_load(json_string): | |||||||
| 		snippet = json_string[start:end] | 		snippet = json_string[start:end] | ||||||
| 		frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet)) | 		frappe.throw(_("Error in input data. Please check for any special characters near following input: <br> {}").format(snippet)) | ||||||
| 
 | 
 | ||||||
| def validate_einvoice(validations, einvoice, errors=None): | class RequestFailed(Exception): | ||||||
| 	if errors is None: | 	pass | ||||||
| 		errors = [] | class CancellationNotAllowed(Exception): | ||||||
| 	for fieldname, field_validation in validations.items(): | 	pass | ||||||
| 		value = einvoice.get(fieldname, None) |  | ||||||
| 		if not value or value == "None": |  | ||||||
| 			# remove keys with empty values |  | ||||||
| 			einvoice.pop(fieldname, None) |  | ||||||
| 			continue |  | ||||||
| 
 |  | ||||||
| 		value_type = field_validation.get("type").lower() |  | ||||||
| 		if value_type in ['object', 'array']: |  | ||||||
| 			child_validations = field_validation.get('properties') |  | ||||||
| 
 |  | ||||||
| 			if isinstance(value, list): |  | ||||||
| 				for d in value: |  | ||||||
| 					validate_einvoice(child_validations, d, errors) |  | ||||||
| 					if not d: |  | ||||||
| 						# remove empty dicts |  | ||||||
| 						einvoice.pop(fieldname, None) |  | ||||||
| 			else: |  | ||||||
| 				validate_einvoice(child_validations, value, errors) |  | ||||||
| 				if not value: |  | ||||||
| 					# remove empty dicts |  | ||||||
| 					einvoice.pop(fieldname, None) |  | ||||||
| 			continue |  | ||||||
| 
 |  | ||||||
| 		# convert to int or str |  | ||||||
| 		if value_type == 'string': |  | ||||||
| 			einvoice[fieldname] = str(value) |  | ||||||
| 		elif value_type == 'number': |  | ||||||
| 			is_integer = '.' not in str(field_validation.get('maximum')) |  | ||||||
| 			precision = 3 if '.999' in str(field_validation.get('maximum')) else 2 |  | ||||||
| 			einvoice[fieldname] = flt(value, precision) if not is_integer else cint(value) |  | ||||||
| 			value = einvoice[fieldname] |  | ||||||
| 
 |  | ||||||
| 		max_length = field_validation.get('maxLength') |  | ||||||
| 		minimum = flt(field_validation.get('minimum')) |  | ||||||
| 		maximum = flt(field_validation.get('maximum')) |  | ||||||
| 		pattern_str = field_validation.get('pattern') |  | ||||||
| 		pattern = re.compile(pattern_str or '') |  | ||||||
| 
 |  | ||||||
| 		label = field_validation.get('description') or fieldname |  | ||||||
| 
 |  | ||||||
| 		if value_type == 'string' and len(value) > max_length: |  | ||||||
| 			errors.append(_('{} should not exceed {} characters').format(label, max_length)) |  | ||||||
| 		if value_type == 'number' and (value > maximum or value < minimum): |  | ||||||
| 			errors.append(_('{} {} should be between {} and {}').format(label, value, minimum, maximum)) |  | ||||||
| 		if pattern_str and not pattern.match(value): |  | ||||||
| 			errors.append(field_validation.get('validationMsg')) |  | ||||||
| 
 |  | ||||||
| 	return errors |  | ||||||
| 
 |  | ||||||
| class RequestFailed(Exception): pass |  | ||||||
| 
 | 
 | ||||||
| class GSPConnector(): | class GSPConnector(): | ||||||
| 	def __init__(self, doctype=None, docname=None): | 	def __init__(self, doctype=None, docname=None): | ||||||
| 		self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') | 		self.doctype = doctype | ||||||
| 		sandbox_mode = self.e_invoice_settings.sandbox_mode | 		self.docname = docname | ||||||
| 
 | 
 | ||||||
| 		self.invoice = frappe.get_cached_doc(doctype, docname) if doctype and docname else None | 		self.set_invoice() | ||||||
| 		self.credentials = self.get_credentials() | 		self.set_credentials() | ||||||
| 
 | 
 | ||||||
| 		# authenticate url is same for sandbox & live | 		# authenticate url is same for sandbox & live | ||||||
| 		self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token' | 		self.authenticate_url = 'https://gsp.adaequare.com/gsp/authenticate?grant_type=token' | ||||||
| 		self.base_url = 'https://gsp.adaequare.com' if not sandbox_mode else 'https://gsp.adaequare.com/test' | 		self.base_url = 'https://gsp.adaequare.com' if not self.e_invoice_settings.sandbox_mode else 'https://gsp.adaequare.com/test' | ||||||
| 
 | 
 | ||||||
| 		self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' | 		self.cancel_irn_url = self.base_url + '/enriched/ei/api/invoice/cancel' | ||||||
| 		self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' | 		self.irn_details_url = self.base_url + '/enriched/ei/api/invoice/irn' | ||||||
| @ -470,15 +565,26 @@ class GSPConnector(): | |||||||
| 		self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB' | 		self.cancel_ewaybill_url = self.base_url + '/enriched/ewb/ewayapi?action=CANEWB' | ||||||
| 		self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' | 		self.generate_ewaybill_url = self.base_url + '/enriched/ei/api/ewaybill' | ||||||
| 
 | 
 | ||||||
| 	def get_credentials(self): | 	def set_invoice(self): | ||||||
| 		if self.invoice: | 		self.invoice = None | ||||||
| 			gstin = self.get_seller_gstin() | 		if self.doctype and self.docname: | ||||||
|  | 			self.invoice = frappe.get_cached_doc(self.doctype, self.docname) | ||||||
|  | 
 | ||||||
|  | 	def set_credentials(self): | ||||||
|  | 		self.e_invoice_settings = frappe.get_cached_doc('E Invoice Settings') | ||||||
|  | 
 | ||||||
| 		if not self.e_invoice_settings.enable: | 		if not self.e_invoice_settings.enable: | ||||||
| 			frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings"))) | 			frappe.throw(_("E-Invoicing is disabled. Please enable it from {} to generate e-invoices.").format(get_link_to_form("E Invoice Settings", "E Invoice Settings"))) | ||||||
| 			credentials = next(d for d in self.e_invoice_settings.credentials if d.gstin == gstin) | 
 | ||||||
|  | 		if self.invoice: | ||||||
|  | 			gstin = self.get_seller_gstin() | ||||||
|  | 			credentials_for_gstin = [d for d in self.e_invoice_settings.credentials if d.gstin == gstin] | ||||||
|  | 			if credentials_for_gstin: | ||||||
|  | 				self.credentials = credentials_for_gstin[0] | ||||||
| 			else: | 			else: | ||||||
| 			credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None | 				frappe.throw(_('Cannot find e-invoicing credentials for selected Company GSTIN. Please check E-Invoice Settings')) | ||||||
| 		return credentials | 		else: | ||||||
|  | 			self.credentials = self.e_invoice_settings.credentials[0] if self.e_invoice_settings.credentials else None | ||||||
| 
 | 
 | ||||||
| 	def get_seller_gstin(self): | 	def get_seller_gstin(self): | ||||||
| 		gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin') | 		gstin = self.invoice.company_gstin or frappe.db.get_value('Address', self.invoice.company_address, 'gstin') | ||||||
| @ -529,7 +635,7 @@ class GSPConnector(): | |||||||
| 			self.e_invoice_settings.reload() | 			self.e_invoice_settings.reload() | ||||||
| 
 | 
 | ||||||
| 		except Exception: | 		except Exception: | ||||||
| 			self.log_error(res) | 			log_error(res) | ||||||
| 			self.raise_error(True) | 			self.raise_error(True) | ||||||
| 
 | 
 | ||||||
| 	def get_headers(self): | 	def get_headers(self): | ||||||
| @ -551,16 +657,15 @@ class GSPConnector(): | |||||||
| 			if res.get('success'): | 			if res.get('success'): | ||||||
| 				return res.get('result') | 				return res.get('result') | ||||||
| 			else: | 			else: | ||||||
| 				self.log_error(res) | 				log_error(res) | ||||||
| 				raise RequestFailed | 				raise RequestFailed | ||||||
| 
 | 
 | ||||||
| 		except RequestFailed: | 		except RequestFailed: | ||||||
| 			self.raise_error() | 			self.raise_error() | ||||||
| 
 | 
 | ||||||
| 		except Exception: | 		except Exception: | ||||||
| 			self.log_error() | 			log_error() | ||||||
| 			self.raise_error(True) | 			self.raise_error(True) | ||||||
| 
 |  | ||||||
| 	@staticmethod | 	@staticmethod | ||||||
| 	def get_gstin_details(gstin): | 	def get_gstin_details(gstin): | ||||||
| 		'''fetch and cache GSTIN details''' | 		'''fetch and cache GSTIN details''' | ||||||
| @ -576,12 +681,13 @@ class GSPConnector(): | |||||||
| 		return details | 		return details | ||||||
| 
 | 
 | ||||||
| 	def generate_irn(self): | 	def generate_irn(self): | ||||||
|  | 		data = {} | ||||||
|  | 		try: | ||||||
| 			headers = self.get_headers() | 			headers = self.get_headers() | ||||||
| 			einvoice = make_einvoice(self.invoice) | 			einvoice = make_einvoice(self.invoice) | ||||||
| 			data = json.dumps(einvoice, indent=4) | 			data = json.dumps(einvoice, indent=4) | ||||||
| 
 |  | ||||||
| 		try: |  | ||||||
| 			res = self.make_request('post', self.generate_irn_url, headers, data) | 			res = self.make_request('post', self.generate_irn_url, headers, data) | ||||||
|  | 
 | ||||||
| 			if res.get('success'): | 			if res.get('success'): | ||||||
| 				self.set_einvoice_data(res.get('result')) | 				self.set_einvoice_data(res.get('result')) | ||||||
| 
 | 
 | ||||||
| @ -601,12 +707,36 @@ class GSPConnector(): | |||||||
| 
 | 
 | ||||||
| 		except RequestFailed: | 		except RequestFailed: | ||||||
| 			errors = self.sanitize_error_message(res.get('message')) | 			errors = self.sanitize_error_message(res.get('message')) | ||||||
|  | 			self.set_failed_status(errors=errors) | ||||||
| 			self.raise_error(errors=errors) | 			self.raise_error(errors=errors) | ||||||
| 
 | 
 | ||||||
| 		except Exception: | 		except Exception as e: | ||||||
| 			self.log_error(data) | 			self.set_failed_status(errors=str(e)) | ||||||
|  | 			log_error(data) | ||||||
| 			self.raise_error(True) | 			self.raise_error(True) | ||||||
| 
 | 
 | ||||||
|  | 	@staticmethod | ||||||
|  | 	def bulk_generate_irn(invoices): | ||||||
|  | 		gsp_connector = GSPConnector() | ||||||
|  | 		gsp_connector.doctype = 'Sales Invoice' | ||||||
|  | 
 | ||||||
|  | 		failed = [] | ||||||
|  | 
 | ||||||
|  | 		for invoice in invoices: | ||||||
|  | 			try: | ||||||
|  | 				gsp_connector.docname = invoice | ||||||
|  | 				gsp_connector.set_invoice() | ||||||
|  | 				gsp_connector.set_credentials() | ||||||
|  | 				gsp_connector.generate_irn() | ||||||
|  | 
 | ||||||
|  | 			except Exception as e: | ||||||
|  | 				failed.append({ | ||||||
|  | 					'docname': invoice, | ||||||
|  | 					'message': str(e) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 		return failed | ||||||
|  | 
 | ||||||
| 	def get_irn_details(self, irn): | 	def get_irn_details(self, irn): | ||||||
| 		headers = self.get_headers() | 		headers = self.get_headers() | ||||||
| 
 | 
 | ||||||
| @ -623,10 +753,18 @@ class GSPConnector(): | |||||||
| 			self.raise_error(errors=errors) | 			self.raise_error(errors=errors) | ||||||
| 
 | 
 | ||||||
| 		except Exception: | 		except Exception: | ||||||
| 			self.log_error() | 			log_error() | ||||||
| 			self.raise_error(True) | 			self.raise_error(True) | ||||||
| 
 | 
 | ||||||
| 	def cancel_irn(self, irn, reason, remark): | 	def cancel_irn(self, irn, reason, remark): | ||||||
|  | 		data, res = {}, {} | ||||||
|  | 		try: | ||||||
|  | 			# validate cancellation | ||||||
|  | 			if time_diff_in_hours(now_datetime(), self.invoice.ack_date) > 24: | ||||||
|  | 				frappe.throw(_('E-Invoice cannot be cancelled after 24 hours of IRN generation.'), title=_('Not Allowed'), exc=CancellationNotAllowed) | ||||||
|  | 			if not irn: | ||||||
|  | 				frappe.throw(_('IRN not found. You must generate IRN before cancelling.'), title=_('Not Allowed'), exc=CancellationNotAllowed) | ||||||
|  | 
 | ||||||
| 			headers = self.get_headers() | 			headers = self.get_headers() | ||||||
| 			data = json.dumps({ | 			data = json.dumps({ | ||||||
| 				'Irn': irn, | 				'Irn': irn, | ||||||
| @ -634,10 +772,11 @@ class GSPConnector(): | |||||||
| 				'Cnlrem': remark | 				'Cnlrem': remark | ||||||
| 			}, indent=4) | 			}, indent=4) | ||||||
| 
 | 
 | ||||||
| 		try: |  | ||||||
| 			res = self.make_request('post', self.cancel_irn_url, headers, data) | 			res = self.make_request('post', self.cancel_irn_url, headers, data) | ||||||
| 			if res.get('success'): | 			if res.get('success') or '9999' in res.get('message'): | ||||||
| 				self.invoice.irn_cancelled = 1 | 				self.invoice.irn_cancelled = 1 | ||||||
|  | 				self.invoice.irn_cancel_date = res.get('result')['CancelDate'] if res.get('result') else "" | ||||||
|  | 				self.invoice.einvoice_status = 'Cancelled' | ||||||
| 				self.invoice.flags.updater_reference = { | 				self.invoice.flags.updater_reference = { | ||||||
| 					'doctype': self.invoice.doctype, | 					'doctype': self.invoice.doctype, | ||||||
| 					'docname': self.invoice.name, | 					'docname': self.invoice.name, | ||||||
| @ -650,12 +789,41 @@ class GSPConnector(): | |||||||
| 
 | 
 | ||||||
| 		except RequestFailed: | 		except RequestFailed: | ||||||
| 			errors = self.sanitize_error_message(res.get('message')) | 			errors = self.sanitize_error_message(res.get('message')) | ||||||
|  | 			self.set_failed_status(errors=errors) | ||||||
| 			self.raise_error(errors=errors) | 			self.raise_error(errors=errors) | ||||||
| 
 | 
 | ||||||
| 		except Exception: | 		except CancellationNotAllowed as e: | ||||||
| 			self.log_error(data) | 			self.set_failed_status(errors=str(e)) | ||||||
|  | 			self.raise_error(errors=str(e)) | ||||||
|  | 
 | ||||||
|  | 		except Exception as e: | ||||||
|  | 			self.set_failed_status(errors=str(e)) | ||||||
|  | 			log_error(data) | ||||||
| 			self.raise_error(True) | 			self.raise_error(True) | ||||||
| 
 | 
 | ||||||
|  | 	@staticmethod | ||||||
|  | 	def bulk_cancel_irn(invoices, reason, remark): | ||||||
|  | 		gsp_connector = GSPConnector() | ||||||
|  | 		gsp_connector.doctype = 'Sales Invoice' | ||||||
|  | 
 | ||||||
|  | 		failed = [] | ||||||
|  | 
 | ||||||
|  | 		for invoice in invoices: | ||||||
|  | 			try: | ||||||
|  | 				gsp_connector.docname = invoice | ||||||
|  | 				gsp_connector.set_invoice() | ||||||
|  | 				gsp_connector.set_credentials() | ||||||
|  | 				irn = gsp_connector.invoice.irn | ||||||
|  | 				gsp_connector.cancel_irn(irn, reason, remark) | ||||||
|  | 
 | ||||||
|  | 			except Exception as e: | ||||||
|  | 				failed.append({ | ||||||
|  | 					'docname': invoice, | ||||||
|  | 					'message': str(e) | ||||||
|  | 				}) | ||||||
|  | 
 | ||||||
|  | 		return failed | ||||||
|  | 
 | ||||||
| 	def generate_eway_bill(self, **kwargs): | 	def generate_eway_bill(self, **kwargs): | ||||||
| 		args = frappe._dict(kwargs) | 		args = frappe._dict(kwargs) | ||||||
| 
 | 
 | ||||||
| @ -694,7 +862,7 @@ class GSPConnector(): | |||||||
| 			self.raise_error(errors=errors) | 			self.raise_error(errors=errors) | ||||||
| 
 | 
 | ||||||
| 		except Exception: | 		except Exception: | ||||||
| 			self.log_error(data) | 			log_error(data) | ||||||
| 			self.raise_error(True) | 			self.raise_error(True) | ||||||
| 
 | 
 | ||||||
| 	def cancel_eway_bill(self, eway_bill, reason, remark): | 	def cancel_eway_bill(self, eway_bill, reason, remark): | ||||||
| @ -726,7 +894,7 @@ class GSPConnector(): | |||||||
| 			self.raise_error(errors=errors) | 			self.raise_error(errors=errors) | ||||||
| 
 | 
 | ||||||
| 		except Exception: | 		except Exception: | ||||||
| 			self.log_error(data) | 			log_error(data) | ||||||
| 			self.raise_error(True) | 			self.raise_error(True) | ||||||
| 
 | 
 | ||||||
| 	def sanitize_error_message(self, message): | 	def sanitize_error_message(self, message): | ||||||
| @ -741,6 +909,9 @@ class GSPConnector(): | |||||||
| 			] | 			] | ||||||
| 			then we trim down the message by looping over errors | 			then we trim down the message by looping over errors | ||||||
| 		''' | 		''' | ||||||
|  | 		if not message: | ||||||
|  | 			return [] | ||||||
|  | 
 | ||||||
| 		errors = re.findall(': [^:]+', message) | 		errors = re.findall(': [^:]+', message) | ||||||
| 		for idx, e in enumerate(errors): | 		for idx, e in enumerate(errors): | ||||||
| 			# remove colons | 			# remove colons | ||||||
| @ -752,22 +923,6 @@ class GSPConnector(): | |||||||
| 
 | 
 | ||||||
| 		return errors | 		return errors | ||||||
| 
 | 
 | ||||||
| 	def log_error(self, data={}): |  | ||||||
| 		if not isinstance(data, dict): |  | ||||||
| 			data = json.loads(data) |  | ||||||
| 
 |  | ||||||
| 		seperator = "--" * 50 |  | ||||||
| 		err_tb = traceback.format_exc() |  | ||||||
| 		err_msg = str(sys.exc_info()[1]) |  | ||||||
| 		data = json.dumps(data, indent=4) |  | ||||||
| 
 |  | ||||||
| 		message = "\n".join([ |  | ||||||
| 			"Error", err_msg, seperator, |  | ||||||
| 			"Data:", data, seperator, |  | ||||||
| 			"Exception:", err_tb |  | ||||||
| 		]) |  | ||||||
| 		frappe.log_error(title=_('E Invoice Request Failed'), message=message) |  | ||||||
| 
 |  | ||||||
| 	def raise_error(self, raise_exception=False, errors=[]): | 	def raise_error(self, raise_exception=False, errors=[]): | ||||||
| 		title = _('E Invoice Request Failed') | 		title = _('E Invoice Request Failed') | ||||||
| 		if errors: | 		if errors: | ||||||
| @ -790,7 +945,10 @@ class GSPConnector(): | |||||||
| 		self.invoice.ack_no = res.get('AckNo') | 		self.invoice.ack_no = res.get('AckNo') | ||||||
| 		self.invoice.ack_date = res.get('AckDt') | 		self.invoice.ack_date = res.get('AckDt') | ||||||
| 		self.invoice.signed_einvoice = dec_signed_invoice | 		self.invoice.signed_einvoice = dec_signed_invoice | ||||||
|  | 		self.invoice.ack_no = res.get('AckNo') | ||||||
|  | 		self.invoice.ack_date = res.get('AckDt') | ||||||
| 		self.invoice.signed_qr_code = res.get('SignedQRCode') | 		self.invoice.signed_qr_code = res.get('SignedQRCode') | ||||||
|  | 		self.invoice.einvoice_status = 'Generated' | ||||||
| 
 | 
 | ||||||
| 		self.attach_qrcode_image() | 		self.attach_qrcode_image() | ||||||
| 
 | 
 | ||||||
| @ -800,7 +958,6 @@ class GSPConnector(): | |||||||
| 			'label': _('IRN Generated') | 			'label': _('IRN Generated') | ||||||
| 		} | 		} | ||||||
| 		self.update_invoice() | 		self.update_invoice() | ||||||
| 
 |  | ||||||
| 	def attach_qrcode_image(self): | 	def attach_qrcode_image(self): | ||||||
| 		qrcode = self.invoice.signed_qr_code | 		qrcode = self.invoice.signed_qr_code | ||||||
| 		doctype = self.invoice.doctype | 		doctype = self.invoice.doctype | ||||||
| @ -827,6 +984,17 @@ class GSPConnector(): | |||||||
| 		self.invoice.flags.ignore_validate = True | 		self.invoice.flags.ignore_validate = True | ||||||
| 		self.invoice.save() | 		self.invoice.save() | ||||||
| 
 | 
 | ||||||
|  | 	def set_failed_status(self, errors=None): | ||||||
|  | 		frappe.db.rollback() | ||||||
|  | 		self.invoice.einvoice_status = 'Failed' | ||||||
|  | 		self.invoice.failure_description = self.get_failure_message(errors) if errors else "" | ||||||
|  | 		self.update_invoice() | ||||||
|  | 		frappe.db.commit() | ||||||
|  | 	 | ||||||
|  | 	def get_failure_message(self, errors): | ||||||
|  | 		if isinstance(errors, list): | ||||||
|  | 			errors = ', '.join(errors) | ||||||
|  | 		return errors | ||||||
| 
 | 
 | ||||||
| def sanitize_for_json(string): | def sanitize_for_json(string): | ||||||
| 	"""Escape JSON specific characters from a string.""" | 	"""Escape JSON specific characters from a string.""" | ||||||
| @ -856,5 +1024,114 @@ def generate_eway_bill(doctype, docname, **kwargs): | |||||||
| 
 | 
 | ||||||
| @frappe.whitelist() | @frappe.whitelist() | ||||||
| def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): | def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): | ||||||
| 	gsp_connector = GSPConnector(doctype, docname) | 	# TODO: uncomment when eway_bill api from Adequare is enabled | ||||||
| 	gsp_connector.cancel_eway_bill(eway_bill, reason, remark) | 	# gsp_connector = GSPConnector(doctype, docname) | ||||||
|  | 	# gsp_connector.cancel_eway_bill(eway_bill, reason, remark) | ||||||
|  | 
 | ||||||
|  | 	# update cancelled status only, to be able to cancel irn next | ||||||
|  | 	frappe.db.set_value(doctype, docname, 'eway_bill_cancelled', 1) | ||||||
|  | 
 | ||||||
|  | @frappe.whitelist() | ||||||
|  | def generate_einvoices(docnames): | ||||||
|  | 	docnames = json.loads(docnames) or [] | ||||||
|  | 
 | ||||||
|  | 	if len(docnames) < 10: | ||||||
|  | 		failures = GSPConnector.bulk_generate_irn(docnames) | ||||||
|  | 		frappe.local.message_log = [] | ||||||
|  | 
 | ||||||
|  | 		if failures: | ||||||
|  | 			show_bulk_action_failure_message(failures) | ||||||
|  | 
 | ||||||
|  | 		success = len(docnames) - len(failures) | ||||||
|  | 		frappe.msgprint( | ||||||
|  | 			_('{} e-invoices generated successfully').format(success), | ||||||
|  | 			title=_('Bulk E-Invoice Generation Complete') | ||||||
|  | 		) | ||||||
|  | 			 | ||||||
|  | 	else: | ||||||
|  | 		enqueue_bulk_action(schedule_bulk_generate_irn, docnames=docnames) | ||||||
|  | 
 | ||||||
|  | def schedule_bulk_generate_irn(docnames): | ||||||
|  | 	failures = GSPConnector.bulk_generate_irn(docnames) | ||||||
|  | 	frappe.local.message_log = [] | ||||||
|  | 
 | ||||||
|  | 	frappe.publish_realtime("bulk_einvoice_generation_complete", { | ||||||
|  | 		"user": frappe.session.user, | ||||||
|  | 		"failures": failures, | ||||||
|  | 		"invoices": docnames | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | def show_bulk_action_failure_message(failures): | ||||||
|  | 	for doc in failures: | ||||||
|  | 		docname = '<a href="sales-invoice/{0}">{0}</a>'.format(doc.get('docname')) | ||||||
|  | 		message = doc.get('message').replace("'", '"') | ||||||
|  | 		if message[0] == '[': | ||||||
|  | 			errors = json.loads(message) | ||||||
|  | 			error_list = ''.join(['<li>{}</li>'.format(err) for err in errors]) | ||||||
|  | 			message = '''{} has following errors:<br> | ||||||
|  | 				<ul style="padding-left: 20px; padding-top: 5px">{}</ul>'''.format(docname, error_list) | ||||||
|  | 		else: | ||||||
|  | 			message = '{} - {}'.format(docname, message) | ||||||
|  | 
 | ||||||
|  | 		frappe.msgprint( | ||||||
|  | 			message, | ||||||
|  | 			title=_('Bulk E-Invoice Generation Complete'), | ||||||
|  | 			indicator='red' | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | @frappe.whitelist() | ||||||
|  | def cancel_irns(docnames, reason, remark): | ||||||
|  | 	docnames = json.loads(docnames) or [] | ||||||
|  | 
 | ||||||
|  | 	if len(docnames) < 10: | ||||||
|  | 		failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark) | ||||||
|  | 		frappe.local.message_log = [] | ||||||
|  | 
 | ||||||
|  | 		if failures: | ||||||
|  | 			show_bulk_action_failure_message(failures) | ||||||
|  | 
 | ||||||
|  | 		success = len(docnames) - len(failures) | ||||||
|  | 		frappe.msgprint( | ||||||
|  | 			_('{} e-invoices cancelled successfully').format(success), | ||||||
|  | 			title=_('Bulk E-Invoice Cancellation Complete') | ||||||
|  | 		) | ||||||
|  | 	else: | ||||||
|  | 		enqueue_bulk_action(schedule_bulk_cancel_irn, docnames=docnames, reason=reason, remark=remark) | ||||||
|  | 
 | ||||||
|  | def schedule_bulk_cancel_irn(docnames, reason, remark): | ||||||
|  | 	failures = GSPConnector.bulk_cancel_irn(docnames, reason, remark) | ||||||
|  | 	frappe.local.message_log = [] | ||||||
|  | 
 | ||||||
|  | 	frappe.publish_realtime("bulk_einvoice_cancellation_complete", { | ||||||
|  | 		"user": frappe.session.user, | ||||||
|  | 		"failures": failures, | ||||||
|  | 		"invoices": docnames | ||||||
|  | 	}) | ||||||
|  | 
 | ||||||
|  | def enqueue_bulk_action(job, **kwargs): | ||||||
|  | 	check_scheduler_status() | ||||||
|  | 
 | ||||||
|  | 	enqueue( | ||||||
|  | 		job, | ||||||
|  | 		**kwargs, | ||||||
|  | 		queue="long", | ||||||
|  | 		timeout=10000, | ||||||
|  | 		event="processing_bulk_einvoice_action", | ||||||
|  | 		now=frappe.conf.developer_mode or frappe.flags.in_test, | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	if job == schedule_bulk_generate_irn: | ||||||
|  | 		msg = _('E-Invoices will be generated in a background process.') | ||||||
|  | 	else: | ||||||
|  | 		msg = _('E-Invoices will be cancelled in a background process.') | ||||||
|  | 
 | ||||||
|  | 	frappe.msgprint(msg, alert=1) | ||||||
|  | 
 | ||||||
|  | def check_scheduler_status(): | ||||||
|  | 	if is_scheduler_inactive() and not frappe.flags.in_test: | ||||||
|  | 		frappe.throw(_("Scheduler is inactive. Cannot enqueue job."), title=_("Scheduler Inactive")) | ||||||
|  | 
 | ||||||
|  | def job_already_enqueued(job_name): | ||||||
|  | 	enqueued_jobs = [d.get("job_name") for d in get_info()] | ||||||
|  | 	if job_name in enqueued_jobs: | ||||||
|  | 		return True | ||||||
| @ -12,14 +12,14 @@ from erpnext.accounts.utils import get_fiscal_year, FiscalYearError | |||||||
| from frappe.utils import today | from frappe.utils import today | ||||||
| 
 | 
 | ||||||
| def setup(company=None, patch=True): | def setup(company=None, patch=True): | ||||||
| 	setup_company_independent_fixtures() | 	setup_company_independent_fixtures(patch=patch) | ||||||
| 	if not patch: | 	if not patch: | ||||||
| 		make_fixtures(company) | 		make_fixtures(company) | ||||||
| 
 | 
 | ||||||
| # TODO: for all countries | # TODO: for all countries | ||||||
| def setup_company_independent_fixtures(): | def setup_company_independent_fixtures(patch=False): | ||||||
| 	make_custom_fields() | 	make_custom_fields() | ||||||
| 	make_property_setters() | 	make_property_setters(patch=patch) | ||||||
| 	add_permissions() | 	add_permissions() | ||||||
| 	add_custom_roles_for_reports() | 	add_custom_roles_for_reports() | ||||||
| 	frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) | 	frappe.enqueue('erpnext.regional.india.setup.add_hsn_sac_codes', now=frappe.flags.in_test) | ||||||
| @ -51,7 +51,7 @@ def create_hsn_codes(data, code_field): | |||||||
| 
 | 
 | ||||||
| def add_custom_roles_for_reports(): | def add_custom_roles_for_reports(): | ||||||
| 	for report_name in ('GST Sales Register', 'GST Purchase Register', | 	for report_name in ('GST Sales Register', 'GST Purchase Register', | ||||||
| 		'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill'): | 		'GST Itemised Sales Register', 'GST Itemised Purchase Register', 'Eway Bill', 'E-Invoice Summary'): | ||||||
| 
 | 
 | ||||||
| 		if not frappe.db.get_value('Custom Role', dict(report=report_name)): | 		if not frappe.db.get_value('Custom Role', dict(report=report_name)): | ||||||
| 			frappe.get_doc(dict( | 			frappe.get_doc(dict( | ||||||
| @ -112,8 +112,9 @@ def add_print_formats(): | |||||||
| 	frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0) | 	frappe.db.set_value("Print Format", "GST Tax Invoice", "disabled", 0) | ||||||
| 	frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0) | 	frappe.db.set_value("Print Format", "GST E-Invoice", "disabled", 0) | ||||||
| 
 | 
 | ||||||
| def make_property_setters(): | def make_property_setters(patch=False): | ||||||
| 	# GST rules do not allow for an invoice no. bigger than 16 characters | 	# GST rules do not allow for an invoice no. bigger than 16 characters | ||||||
|  | 	if not patch: | ||||||
| 		make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') | 		make_property_setter('Sales Invoice', 'naming_series', 'options', 'SINV-.YY.-\nSRET-.YY.-', '') | ||||||
| 		make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') | 		make_property_setter('Purchase Invoice', 'naming_series', 'options', 'PINV-.YY.-\nPRET-.YY.-', '') | ||||||
| 
 | 
 | ||||||
| @ -127,6 +128,9 @@ def make_custom_fields(update=True): | |||||||
| 	is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST', | 	is_non_gst = dict(fieldname='is_non_gst', label='Is Non GST', | ||||||
| 		fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt', | 		fieldtype='Check', fetch_from='item_code.is_non_gst', insert_after='is_nil_exempt', | ||||||
| 		print_hide=1) | 		print_hide=1) | ||||||
|  | 	taxable_value = dict(fieldname='taxable_value', label='Taxable Value', | ||||||
|  | 		fieldtype='Currency', insert_after='base_net_amount', hidden=1, options="Company:company:default_currency", | ||||||
|  | 		print_hide=1) | ||||||
| 
 | 
 | ||||||
| 	purchase_invoice_gst_category = [ | 	purchase_invoice_gst_category = [ | ||||||
| 		dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', | 		dict(fieldname='gst_section', label='GST Details', fieldtype='Section Break', | ||||||
| @ -156,6 +160,13 @@ def make_custom_fields(update=True): | |||||||
| 			fetch_if_empty=1), | 			fetch_if_empty=1), | ||||||
| 	] | 	] | ||||||
| 
 | 
 | ||||||
|  | 	delivery_note_gst_category = [ | ||||||
|  | 		dict(fieldname='gst_category', label='GST Category', | ||||||
|  | 			fieldtype='Select', insert_after='gst_vehicle_type', print_hide=1, | ||||||
|  | 			options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', | ||||||
|  | 			fetch_from='customer.gst_category', fetch_if_empty=1), | ||||||
|  | 	] | ||||||
|  | 
 | ||||||
| 	invoice_gst_fields = [ | 	invoice_gst_fields = [ | ||||||
| 		dict(fieldname='invoice_copy', label='Invoice Copy', | 		dict(fieldname='invoice_copy', label='Invoice Copy', | ||||||
| 			fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1, | 			fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1, | ||||||
| @ -408,21 +419,37 @@ def make_custom_fields(update=True): | |||||||
| 		dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, | 		dict(fieldname='irn', label='IRN', fieldtype='Data', read_only=1, insert_after='customer', no_copy=1, print_hide=1, | ||||||
| 			depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), | 			depends_on='eval:in_list(["Registered Regular", "SEZ", "Overseas", "Deemed Export"], doc.gst_category) && doc.irn_cancelled === 0'), | ||||||
| 
 | 
 | ||||||
| 		dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='irn', no_copy=1, print_hide=1), |  | ||||||
| 
 |  | ||||||
| 		dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), |  | ||||||
| 
 |  | ||||||
| 		dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, | 		dict(fieldname='irn_cancelled', label='IRN Cancelled', fieldtype='Check', no_copy=1, print_hide=1, | ||||||
| 			depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), | 			depends_on='eval:(doc.irn_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), | ||||||
| 
 | 
 | ||||||
| 		dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, | 		dict(fieldname='eway_bill_cancelled', label='E-Way Bill Cancelled', fieldtype='Check', no_copy=1, print_hide=1, | ||||||
| 			depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), | 			depends_on='eval:(doc.eway_bill_cancelled === 1)', read_only=1, allow_on_submit=1, insert_after='customer'), | ||||||
| 
 | 
 | ||||||
| 		dict(fieldname='signed_einvoice', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), | 		dict(fieldname='einvoice_section', label='E-Invoice Fields', fieldtype='Section Break', insert_after='gst_vehicle_type', | ||||||
|  | 			print_hide=1, hidden=1), | ||||||
| 		 | 		 | ||||||
| 		dict(fieldname='signed_qr_code', fieldtype='Code', options='JSON', hidden=1, no_copy=1, print_hide=1, read_only=1), | 		dict(fieldname='ack_no', label='Ack. No.', fieldtype='Data', read_only=1, hidden=1, insert_after='einvoice_section', | ||||||
|  | 			no_copy=1, print_hide=1), | ||||||
| 		 | 		 | ||||||
| 		dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, no_copy=1, print_hide=1, read_only=1) | 		dict(fieldname='ack_date', label='Ack. Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_no', no_copy=1, print_hide=1), | ||||||
|  | 
 | ||||||
|  | 		dict(fieldname='irn_cancel_date', label='Cancel Date', fieldtype='Data', read_only=1, hidden=1, insert_after='ack_date',  | ||||||
|  | 			no_copy=1, print_hide=1), | ||||||
|  | 
 | ||||||
|  | 		dict(fieldname='signed_einvoice', label='Signed E-Invoice', fieldtype='Code', options='JSON', hidden=1, insert_after='irn_cancel_date', | ||||||
|  | 			no_copy=1, print_hide=1, read_only=1), | ||||||
|  | 
 | ||||||
|  | 		dict(fieldname='signed_qr_code', label='Signed QRCode', fieldtype='Code', options='JSON', hidden=1, insert_after='signed_einvoice', | ||||||
|  | 			no_copy=1, print_hide=1, read_only=1), | ||||||
|  | 
 | ||||||
|  | 		dict(fieldname='qrcode_image', label='QRCode', fieldtype='Attach Image', hidden=1, insert_after='signed_qr_code', | ||||||
|  | 			no_copy=1, print_hide=1, read_only=1), | ||||||
|  | 
 | ||||||
|  | 		dict(fieldname='einvoice_status', label='E-Invoice Status', fieldtype='Select', insert_after='qrcode_image', | ||||||
|  | 			options='\nPending\nGenerated\nCancelled\nFailed', default=None, hidden=1, no_copy=1, print_hide=1, read_only=1), | ||||||
|  | 
 | ||||||
|  | 		dict(fieldname='failure_description', label='E-Invoice Failure Description', fieldtype='Code', options='JSON', | ||||||
|  | 			hidden=1, insert_after='einvoice_status', no_copy=1, print_hide=1, read_only=1) | ||||||
| 	] | 	] | ||||||
| 
 | 
 | ||||||
| 	custom_fields = { | 	custom_fields = { | ||||||
| @ -438,7 +465,7 @@ def make_custom_fields(update=True): | |||||||
| 		'Purchase Order': purchase_invoice_gst_fields, | 		'Purchase Order': purchase_invoice_gst_fields, | ||||||
| 		'Purchase Receipt': purchase_invoice_gst_fields, | 		'Purchase Receipt': purchase_invoice_gst_fields, | ||||||
| 		'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, | 		'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields + si_einvoice_fields, | ||||||
| 		'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, | 		'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields + delivery_note_gst_category, | ||||||
| 		'Sales Order': sales_invoice_gst_fields, | 		'Sales Order': sales_invoice_gst_fields, | ||||||
| 		'Tax Category': inter_state_gst_field, | 		'Tax Category': inter_state_gst_field, | ||||||
| 		'Item': [ | 		'Item': [ | ||||||
| @ -453,7 +480,7 @@ def make_custom_fields(update=True): | |||||||
| 		'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | 		'Supplier Quotation Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | ||||||
| 		'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | 		'Sales Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | ||||||
| 		'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | 		'Delivery Note Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | ||||||
| 		'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | 		'Sales Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst, taxable_value], | ||||||
| 		'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | 		'Purchase Order Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | ||||||
| 		'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | 		'Purchase Receipt Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | ||||||
| 		'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | 		'Purchase Invoice Item': [hsn_sac_field, nil_rated_exempt, is_non_gst], | ||||||
|  | |||||||
| @ -12,14 +12,14 @@ class TestIndiaUtils(unittest.TestCase): | |||||||
| 		mock_get_cached.return_value = "India"  # mock country | 		mock_get_cached.return_value = "India"  # mock country | ||||||
| 		posting_date = "2021-05-01" | 		posting_date = "2021-05-01" | ||||||
| 
 | 
 | ||||||
| 		invalid_names = [ "SI$1231", "012345678901234567", "SI 2020 05",  | 		invalid_names = ["SI$1231", "012345678901234567", "SI 2020 05", | ||||||
| 				"SI.2020.0001", "PI2021 - 001" ] | 				"SI.2020.0001", "PI2021 - 001"] | ||||||
| 		for name in invalid_names: | 		for name in invalid_names: | ||||||
| 			doc = frappe._dict(name=name, posting_date=posting_date) | 			doc = frappe._dict(name=name, posting_date=posting_date) | ||||||
| 			self.assertRaises(frappe.ValidationError, validate_document_name, doc) | 			self.assertRaises(frappe.ValidationError, validate_document_name, doc) | ||||||
| 
 | 
 | ||||||
| 		valid_names = [ "012345678901236", "SI/2020/0001", "SI/2020-0001", | 		valid_names = ["012345678901236", "SI/2020/0001", "SI/2020-0001", | ||||||
| 			"2020-PI-0001", "PI2020-0001" ] | 			"2020-PI-0001", "PI2020-0001"] | ||||||
| 		for name in valid_names: | 		for name in valid_names: | ||||||
| 			doc = frappe._dict(name=name, posting_date=posting_date) | 			doc = frappe._dict(name=name, posting_date=posting_date) | ||||||
| 			try: | 			try: | ||||||
|  | |||||||
| @ -2,7 +2,7 @@ from __future__ import unicode_literals | |||||||
| import frappe, re, json | import frappe, re, json | ||||||
| from frappe import _ | from frappe import _ | ||||||
| import erpnext | import erpnext | ||||||
| from frappe.utils import cstr, flt, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate | from frappe.utils import cstr, flt, cint, date_diff, nowdate, round_based_on_smallest_currency_fraction, money_in_words, getdate | ||||||
| from erpnext.regional.india import states, state_numbers | from erpnext.regional.india import states, state_numbers | ||||||
| from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount | from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_taxable_amount | ||||||
| from erpnext.controllers.accounts_controller import get_taxes_and_charges | from erpnext.controllers.accounts_controller import get_taxes_and_charges | ||||||
| @ -41,24 +41,25 @@ def validate_gstin_for_india(doc, method): | |||||||
| 		return | 		return | ||||||
| 
 | 
 | ||||||
| 	if len(doc.gstin) != 15: | 	if len(doc.gstin) != 15: | ||||||
| 		frappe.throw(_("Invalid GSTIN! A GSTIN must have 15 characters.")) | 		frappe.throw(_("A GSTIN must have 15 characters."), title=_("Invalid GSTIN")) | ||||||
| 
 | 
 | ||||||
| 	if gst_category and gst_category == 'UIN Holders': | 	if gst_category and gst_category == 'UIN Holders': | ||||||
| 		if not GSTIN_UIN_FORMAT.match(doc.gstin): | 		if not GSTIN_UIN_FORMAT.match(doc.gstin): | ||||||
| 			frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers")) | 			frappe.throw(_("The input you've entered doesn't match the GSTIN format for UIN Holders or Non-Resident OIDAR Service Providers"), | ||||||
|  | 				title=_("Invalid GSTIN")) | ||||||
| 	else: | 	else: | ||||||
| 		if not GSTIN_FORMAT.match(doc.gstin): | 		if not GSTIN_FORMAT.match(doc.gstin): | ||||||
| 			frappe.throw(_("Invalid GSTIN! The input you've entered doesn't match the format of GSTIN.")) | 			frappe.throw(_("The input you've entered doesn't match the format of GSTIN."), title=_("Invalid GSTIN")) | ||||||
| 
 | 
 | ||||||
| 		validate_gstin_check_digit(doc.gstin) | 		validate_gstin_check_digit(doc.gstin) | ||||||
| 		set_gst_state_and_state_number(doc) | 		set_gst_state_and_state_number(doc) | ||||||
| 
 | 
 | ||||||
| 		if not doc.gst_state: | 		if not doc.gst_state: | ||||||
| 			frappe.throw(_("Please Enter GST state")) | 			frappe.throw(_("Please enter GST state"), title=_("Invalid State")) | ||||||
| 
 | 
 | ||||||
| 		if doc.gst_state_number != doc.gstin[:2]: | 		if doc.gst_state_number != doc.gstin[:2]: | ||||||
| 			frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") | 			frappe.throw(_("First 2 digits of GSTIN should match with State number {0}.") | ||||||
| 				.format(doc.gst_state_number)) | 				.format(doc.gst_state_number), title=_("Invalid GSTIN")) | ||||||
| 
 | 
 | ||||||
| def validate_pan_for_india(doc, method): | def validate_pan_for_india(doc, method): | ||||||
| 	if doc.get('country') != 'India' or not doc.pan: | 	if doc.get('country') != 'India' or not doc.pan: | ||||||
| @ -154,6 +155,7 @@ def set_place_of_supply(doc, method=None): | |||||||
| 
 | 
 | ||||||
| def validate_document_name(doc, method=None): | def validate_document_name(doc, method=None): | ||||||
| 	"""Validate GST invoice number requirements.""" | 	"""Validate GST invoice number requirements.""" | ||||||
|  | 
 | ||||||
| 	country = frappe.get_cached_value("Company", doc.company, "country") | 	country = frappe.get_cached_value("Company", doc.company, "country") | ||||||
| 
 | 
 | ||||||
| 	# Date was chosen as start of next FY to avoid irritating current users. | 	# Date was chosen as start of next FY to avoid irritating current users. | ||||||
| @ -832,3 +834,48 @@ def get_regional_round_off_accounts(company, account_list): | |||||||
| 	account_list.extend(gst_account_list) | 	account_list.extend(gst_account_list) | ||||||
| 
 | 
 | ||||||
| 	return account_list | 	return account_list | ||||||
|  | 
 | ||||||
|  | def update_taxable_values(doc, method): | ||||||
|  | 	country = frappe.get_cached_value('Company', doc.company, 'country') | ||||||
|  | 
 | ||||||
|  | 	if country != 'India': | ||||||
|  | 		return | ||||||
|  | 
 | ||||||
|  | 	gst_accounts = get_gst_accounts(doc.company) | ||||||
|  | 
 | ||||||
|  | 	# Only considering sgst account to avoid inflating taxable value | ||||||
|  | 	gst_account_list = gst_accounts.get('sgst_account', []) + gst_accounts.get('sgst_account', []) \ | ||||||
|  | 		+ gst_accounts.get('igst_account', []) | ||||||
|  | 
 | ||||||
|  | 	additional_taxes = 0 | ||||||
|  | 	total_charges = 0 | ||||||
|  | 	item_count = 0 | ||||||
|  | 	considered_rows = [] | ||||||
|  | 
 | ||||||
|  | 	for tax in doc.get('taxes'): | ||||||
|  | 		prev_row_id = cint(tax.row_id) - 1 | ||||||
|  | 		if tax.account_head in gst_account_list and prev_row_id not in considered_rows: | ||||||
|  | 			if tax.charge_type == 'On Previous Row Amount': | ||||||
|  | 				additional_taxes += doc.get('taxes')[prev_row_id].tax_amount_after_discount_amount | ||||||
|  | 				considered_rows.append(prev_row_id) | ||||||
|  | 			if tax.charge_type == 'On Previous Row Total': | ||||||
|  | 				additional_taxes += doc.get('taxes')[prev_row_id].base_total - doc.base_net_total | ||||||
|  | 				considered_rows.append(prev_row_id) | ||||||
|  | 
 | ||||||
|  | 	for item in doc.get('items'): | ||||||
|  | 		if doc.apply_discount_on == 'Grand Total' and doc.discount_amount: | ||||||
|  | 			proportionate_value = item.base_amount if doc.base_total else item.qty | ||||||
|  | 			total_value = doc.base_total if doc.base_total else doc.total_qty | ||||||
|  | 		else: | ||||||
|  | 			proportionate_value = item.base_net_amount if doc.base_net_total else item.qty | ||||||
|  | 			total_value = doc.base_net_total if doc.base_net_total else doc.total_qty | ||||||
|  | 
 | ||||||
|  | 		applicable_charges = flt(flt(proportionate_value * (flt(additional_taxes) / flt(total_value)), | ||||||
|  | 			item.precision('taxable_value'))) | ||||||
|  | 		item.taxable_value = applicable_charges + proportionate_value | ||||||
|  | 		total_charges += applicable_charges | ||||||
|  | 		item_count += 1 | ||||||
|  | 
 | ||||||
|  | 	if total_charges != additional_taxes: | ||||||
|  | 		diff = additional_taxes - total_charges | ||||||
|  | 		doc.get('items')[item_count - 1].taxable_value += diff | ||||||
|  | |||||||
| @ -139,6 +139,9 @@ def make_custom_fields(update=True): | |||||||
| 			dict(fieldname='customer_fiscal_code', label='Customer Fiscal Code', | 			dict(fieldname='customer_fiscal_code', label='Customer Fiscal Code', | ||||||
| 				fieldtype='Data', insert_after='cb_e_invoicing_reference', read_only=1, | 				fieldtype='Data', insert_after='cb_e_invoicing_reference', read_only=1, | ||||||
| 				fetch_from="customer.fiscal_code"), | 				fetch_from="customer.fiscal_code"), | ||||||
|  | 			dict(fieldname='type_of_document', label='Type of Document', | ||||||
|  | 				fieldtype='Select', insert_after='customer_fiscal_code', | ||||||
|  | 				options='\nTD01\nTD02\nTD03\nTD04\nTD05\nTD06\nTD16\nTD17\nTD18\nTD19\nTD20\nTD21\nTD22\nTD23\nTD24\nTD25\nTD26\nTD27'), | ||||||
| 		], | 		], | ||||||
| 		'Purchase Invoice Item': invoice_item_fields, | 		'Purchase Invoice Item': invoice_item_fields, | ||||||
| 		'Sales Order Item': invoice_item_fields, | 		'Sales Order Item': invoice_item_fields, | ||||||
|  | |||||||
| @ -57,6 +57,7 @@ def prepare_invoice(invoice, progressive_number): | |||||||
| 	invoice.company_address_data = company_address | 	invoice.company_address_data = company_address | ||||||
| 
 | 
 | ||||||
| 	#Set invoice type | 	#Set invoice type | ||||||
|  | 	if not invoice.type_of_document: | ||||||
| 		if invoice.is_return and invoice.return_against: | 		if invoice.is_return and invoice.return_against: | ||||||
| 			invoice.type_of_document = "TD04" #Credit Note (Nota di Credito) | 			invoice.type_of_document = "TD04" #Credit Note (Nota di Credito) | ||||||
| 			invoice.return_against_unamended =  get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against)) | 			invoice.return_against_unamended =  get_unamended_name(frappe.get_doc("Sales Invoice", invoice.return_against)) | ||||||
|  | |||||||
| @ -1,29 +1,22 @@ | |||||||
| { | { | ||||||
|  "add_total_row": 0, |  "add_total_row": 0, | ||||||
| 	"apply_user_permissions": 0, |  "columns": [], | ||||||
|  "creation": "2019-04-24 08:45:16.650129", |  "creation": "2019-04-24 08:45:16.650129", | ||||||
|  |  "disable_prepared_report": 0, | ||||||
|  "disabled": 0, |  "disabled": 0, | ||||||
| 	"icon": "octicon octicon-repo-pull", |  | ||||||
| 	"color": "#4CB944", |  | ||||||
|  "docstatus": 0, |  "docstatus": 0, | ||||||
|  "doctype": "Report", |  "doctype": "Report", | ||||||
|  |  "filters": [], | ||||||
|  "idx": 0, |  "idx": 0, | ||||||
|  "is_standard": "Yes", |  "is_standard": "Yes", | ||||||
|  |  "modified": "2021-04-06 12:23:00.379517", | ||||||
|  |  "modified_by": "Administrator", | ||||||
|  "module": "Regional", |  "module": "Regional", | ||||||
|  "name": "DATEV", |  "name": "DATEV", | ||||||
|  "owner": "Administrator", |  "owner": "Administrator", | ||||||
|  |  "prepared_report": 0, | ||||||
|  "ref_doctype": "GL Entry", |  "ref_doctype": "GL Entry", | ||||||
|  "report_name": "DATEV", |  "report_name": "DATEV", | ||||||
|  "report_type": "Script Report", |  "report_type": "Script Report", | ||||||
| 	"roles": [ |  "roles": [] | ||||||
| 		{ |  | ||||||
| 			"role": "Accounts User" |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			"role": "Accounts Manager" |  | ||||||
| 		}, |  | ||||||
| 		{ |  | ||||||
| 			"role": "Auditor" |  | ||||||
| 		} |  | ||||||
| 	] |  | ||||||
| } | } | ||||||
| @ -0,0 +1,55 @@ | |||||||
|  | // Copyright (c) 2016, Frappe Technologies Pvt. Ltd. and contributors
 | ||||||
|  | // For license information, please see license.txt
 | ||||||
|  | /* eslint-disable */ | ||||||
|  | 
 | ||||||
|  | frappe.query_reports["E-Invoice Summary"] = { | ||||||
|  | 	"filters": [ | ||||||
|  | 		{ | ||||||
|  | 			"fieldtype": "Link", | ||||||
|  | 			"options": "Company", | ||||||
|  | 			"reqd": 1, | ||||||
|  | 			"fieldname": "company", | ||||||
|  | 			"label": __("Company"), | ||||||
|  | 			"default": frappe.defaults.get_user_default("Company"), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"fieldtype": "Link", | ||||||
|  | 			"options": "Customer", | ||||||
|  | 			"fieldname": "customer", | ||||||
|  | 			"label": __("Customer") | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"fieldtype": "Date", | ||||||
|  | 			"reqd": 1, | ||||||
|  | 			"fieldname": "from_date", | ||||||
|  | 			"label": __("From Date"), | ||||||
|  | 			"default": frappe.datetime.add_months(frappe.datetime.get_today(), -1), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"fieldtype": "Date", | ||||||
|  | 			"reqd": 1, | ||||||
|  | 			"fieldname": "to_date", | ||||||
|  | 			"label": __("To Date"), | ||||||
|  | 			"default": frappe.datetime.get_today(), | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"fieldtype": "Select", | ||||||
|  | 			"fieldname": "status", | ||||||
|  | 			"label": __("Status"), | ||||||
|  | 			"options": "\nPending\nGenerated\nCancelled\nFailed" | ||||||
|  | 		} | ||||||
|  | 	], | ||||||
|  | 
 | ||||||
|  | 	"formatter": function (value, row, column, data, default_formatter) { | ||||||
|  | 		value = default_formatter(value, row, column, data); | ||||||
|  | 
 | ||||||
|  | 		if (column.fieldname == "einvoice_status" && value) { | ||||||
|  | 			if (value == 'Pending') value = `<span class="bold" style="color: var(--text-on-orange)">${value}</span>`; | ||||||
|  | 			else if (value == 'Generated') value = `<span class="bold" style="color: var(--text-on-green)">${value}</span>`; | ||||||
|  | 			else if (value == 'Cancelled') value = `<span class="bold" style="color: var(--text-on-red)">${value}</span>`; | ||||||
|  | 			else if (value == 'Failed') value = `<span class="bold"  style="color: var(--text-on-red)">${value}</span>`; | ||||||
|  | 		} | ||||||
|  | 
 | ||||||
|  | 		return value; | ||||||
|  | 	} | ||||||
|  | }; | ||||||
| @ -0,0 +1,28 @@ | |||||||
|  | { | ||||||
|  |  "add_total_row": 0, | ||||||
|  |  "columns": [], | ||||||
|  |  "creation": "2021-03-12 11:23:37.312294", | ||||||
|  |  "disable_prepared_report": 0, | ||||||
|  |  "disabled": 0, | ||||||
|  |  "docstatus": 0, | ||||||
|  |  "doctype": "Report", | ||||||
|  |  "filters": [], | ||||||
|  |  "idx": 0, | ||||||
|  |  "is_standard": "Yes", | ||||||
|  |  "json": "{}", | ||||||
|  |  "letter_head": "Logo", | ||||||
|  |  "modified": "2021-03-12 12:36:48.689413", | ||||||
|  |  "modified_by": "Administrator", | ||||||
|  |  "module": "Regional", | ||||||
|  |  "name": "E-Invoice Summary", | ||||||
|  |  "owner": "Administrator", | ||||||
|  |  "prepared_report": 0, | ||||||
|  |  "ref_doctype": "Sales Invoice", | ||||||
|  |  "report_name": "E-Invoice Summary", | ||||||
|  |  "report_type": "Script Report", | ||||||
|  |  "roles": [ | ||||||
|  |   { | ||||||
|  |    "role": "Administrator" | ||||||
|  |   } | ||||||
|  |  ] | ||||||
|  | } | ||||||
							
								
								
									
										106
									
								
								erpnext/regional/report/e_invoice_summary/e_invoice_summary.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								erpnext/regional/report/e_invoice_summary/e_invoice_summary.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,106 @@ | |||||||
|  | # Copyright (c) 2013, Frappe Technologies Pvt. Ltd. and contributors | ||||||
|  | # For license information, please see license.txt | ||||||
|  | 
 | ||||||
|  | from __future__ import unicode_literals | ||||||
|  | import frappe | ||||||
|  | from frappe import _ | ||||||
|  | 
 | ||||||
|  | def execute(filters=None): | ||||||
|  | 	validate_filters(filters) | ||||||
|  | 
 | ||||||
|  | 	columns = get_columns() | ||||||
|  | 	data = get_data(filters) | ||||||
|  | 
 | ||||||
|  | 	return columns, data | ||||||
|  | 
 | ||||||
|  | def validate_filters(filters={}): | ||||||
|  | 	filters = frappe._dict(filters) | ||||||
|  | 
 | ||||||
|  | 	if not filters.company: | ||||||
|  | 		frappe.throw(_('{} is mandatory for generating E-Invoice Summary Report').format(_('Company')), title=_('Invalid Filter')) | ||||||
|  | 	if filters.company: | ||||||
|  | 		# validate if company has e-invoicing enabled | ||||||
|  | 		pass | ||||||
|  | 	if not filters.from_date or not filters.to_date: | ||||||
|  | 		frappe.throw(_('From Date & To Date is mandatory for generating E-Invoice Summary Report'), title=_('Invalid Filter')) | ||||||
|  | 	if filters.from_date > filters.to_date: | ||||||
|  | 		frappe.throw(_('From Date must be before To Date'), title=_('Invalid Filter')) | ||||||
|  | 
 | ||||||
|  | def get_data(filters={}): | ||||||
|  | 	query_filters = { | ||||||
|  | 		'posting_date': ['between', [filters.from_date, filters.to_date]], | ||||||
|  | 		'einvoice_status': ['is', 'set'], | ||||||
|  | 		'company': filters.company | ||||||
|  | 	} | ||||||
|  | 	if filters.customer: | ||||||
|  | 		query_filters['customer'] = filters.customer | ||||||
|  | 	if filters.status: | ||||||
|  | 		query_filters['einvoice_status'] = filters.status | ||||||
|  | 
 | ||||||
|  | 	data = frappe.get_all( | ||||||
|  | 		'Sales Invoice', | ||||||
|  | 		filters=query_filters, | ||||||
|  | 		fields=[d.get('fieldname') for d in get_columns()] | ||||||
|  | 	) | ||||||
|  | 
 | ||||||
|  | 	return data | ||||||
|  | 
 | ||||||
|  | def get_columns(): | ||||||
|  | 	return [ | ||||||
|  | 		{ | ||||||
|  | 			"fieldtype": "Date", | ||||||
|  | 			"fieldname": "posting_date", | ||||||
|  | 			"label": _("Posting Date"), | ||||||
|  | 			"width": 0 | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"fieldtype": "Link",  | ||||||
|  | 			"fieldname": "name",  | ||||||
|  | 			"label": _("Sales Invoice"), | ||||||
|  | 			"options": "Sales Invoice", | ||||||
|  | 			"width": 140 | ||||||
|  | 		}, | ||||||
|  | 		{  | ||||||
|  | 			"fieldtype": "Data",  | ||||||
|  | 			"fieldname": "einvoice_status",  | ||||||
|  | 			"label": _("Status"),  | ||||||
|  | 			"width": 100 | ||||||
|  | 		}, | ||||||
|  | 		{  | ||||||
|  | 			"fieldtype": "Link", | ||||||
|  | 			"fieldname": "customer", | ||||||
|  | 			"options": "Customer", | ||||||
|  | 			"label": _("Customer") | ||||||
|  | 		}, | ||||||
|  | 		{  | ||||||
|  | 			"fieldtype": "Check", | ||||||
|  | 			"fieldname": "is_return", | ||||||
|  | 			"label": _("Is Return"), | ||||||
|  | 			"width": 85 | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"fieldtype": "Data",  | ||||||
|  | 			"fieldname": "ack_no",  | ||||||
|  | 			"label": "Ack. No.",  | ||||||
|  | 			"width": 145 | ||||||
|  | 		}, | ||||||
|  | 		{  | ||||||
|  | 			"fieldtype": "Data",  | ||||||
|  | 			"fieldname": "ack_date",  | ||||||
|  | 			"label": "Ack. Date",  | ||||||
|  | 			"width": 165 | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"fieldtype": "Data",  | ||||||
|  | 			"fieldname": "irn",  | ||||||
|  | 			"label": _("IRN No."), | ||||||
|  | 			"width": 250 | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			"fieldtype": "Currency", | ||||||
|  | 			"options": "Company:company:default_currency",  | ||||||
|  | 			"fieldname": "base_grand_total",  | ||||||
|  | 			"label": _("Grand Total"), | ||||||
|  | 			"width": 120 | ||||||
|  | 		} | ||||||
|  | 	] | ||||||
| @ -199,7 +199,7 @@ class Gstr1Report(object): | |||||||
| 		self.item_tax_rate = frappe._dict() | 		self.item_tax_rate = frappe._dict() | ||||||
| 
 | 
 | ||||||
| 		items = frappe.db.sql(""" | 		items = frappe.db.sql(""" | ||||||
| 			select item_code, parent, base_net_amount, item_tax_rate | 			select item_code, parent, taxable_value, item_tax_rate | ||||||
| 			from `tab%s Item` | 			from `tab%s Item` | ||||||
| 			where parent in (%s) | 			where parent in (%s) | ||||||
| 		""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) | 		""" % (self.doctype, ', '.join(['%s']*len(self.invoices))), tuple(self.invoices), as_dict=1) | ||||||
| @ -207,7 +207,7 @@ class Gstr1Report(object): | |||||||
| 		for d in items: | 		for d in items: | ||||||
| 			if d.item_code not in self.invoice_items.get(d.parent, {}): | 			if d.item_code not in self.invoice_items.get(d.parent, {}): | ||||||
| 				self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, | 				self.invoice_items.setdefault(d.parent, {}).setdefault(d.item_code, | ||||||
| 					sum(i.get('base_net_amount', 0) for i in items | 					sum(i.get('taxable_value', 0) for i in items | ||||||
| 						if i.item_code == d.item_code and i.parent == d.parent)) | 						if i.item_code == d.item_code and i.parent == d.parent)) | ||||||
| 
 | 
 | ||||||
| 				item_tax_rate = {} | 				item_tax_rate = {} | ||||||
|  | |||||||
| @ -212,7 +212,8 @@ | |||||||
|    "fieldtype": "Link", |    "fieldtype": "Link", | ||||||
|    "ignore_user_permissions": 1, |    "ignore_user_permissions": 1, | ||||||
|    "label": "Represents Company", |    "label": "Represents Company", | ||||||
|    "options": "Company" |    "options": "Company", | ||||||
|  |    "unique": 1 | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|    "depends_on": "represents_company", |    "depends_on": "represents_company", | ||||||
|  | |||||||
| @ -279,11 +279,6 @@ erpnext.PointOfSale.Controller = class { | |||||||
| 					const item_row = frappe.model.get_doc(cdt, cdn); | 					const item_row = frappe.model.get_doc(cdt, cdn); | ||||||
| 					if (item_row && item_row[fieldname] != value) { | 					if (item_row && item_row[fieldname] != value) { | ||||||
| 
 | 
 | ||||||
| 						if (fieldname === 'qty' && flt(value) == 0) { |  | ||||||
| 							this.remove_item_from_cart(); |  | ||||||
| 							return; |  | ||||||
| 						} |  | ||||||
| 
 |  | ||||||
| 						const { item_code, batch_no, uom } = this.item_details.current_item; | 						const { item_code, batch_no, uom } = this.item_details.current_item; | ||||||
| 						const event = { | 						const event = { | ||||||
| 							field: fieldname, | 							field: fieldname, | ||||||
|  | |||||||
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