Merge branch 'develop' into de-translate-employee
This commit is contained in:
		
						commit
						195e8af985
					
				| @ -2648,6 +2648,7 @@ class TestSalesInvoice(unittest.TestCase): | |||||||
| 		# reset | 		# reset | ||||||
| 		einvoice_settings = frappe.get_doc("E Invoice Settings") | 		einvoice_settings = frappe.get_doc("E Invoice Settings") | ||||||
| 		einvoice_settings.enable = 0 | 		einvoice_settings.enable = 0 | ||||||
|  | 		einvoice_settings.save() | ||||||
| 		frappe.flags.country = country | 		frappe.flags.country = country | ||||||
| 
 | 
 | ||||||
| 	def test_einvoice_json(self): | 	def test_einvoice_json(self): | ||||||
|  | |||||||
| @ -163,17 +163,15 @@ def get_party_details(party, party_type, args=None): | |||||||
| def get_tax_template(posting_date, args): | def get_tax_template(posting_date, args): | ||||||
| 	"""Get matching tax rule""" | 	"""Get matching tax rule""" | ||||||
| 	args = frappe._dict(args) | 	args = frappe._dict(args) | ||||||
| 	from_date = to_date = posting_date | 	conditions = [] | ||||||
| 	if not posting_date: |  | ||||||
| 		from_date = "1900-01-01" |  | ||||||
| 		to_date = "4000-01-01" |  | ||||||
| 
 | 
 | ||||||
| 	conditions = [ | 	if posting_date: | ||||||
| 		"""(from_date is null or from_date <= '{0}') | 		conditions.append( | ||||||
| 		and (to_date is null or to_date >= '{1}')""".format( | 			f"""(from_date is null or from_date <= '{posting_date}') | ||||||
| 			from_date, to_date | 			and (to_date is null or to_date >= '{posting_date}')""" | ||||||
| 		) | 		) | ||||||
| 	] | 	else: | ||||||
|  | 		conditions.append("(from_date is null) and (to_date is null)") | ||||||
| 
 | 
 | ||||||
| 	conditions.append( | 	conditions.append( | ||||||
| 		"ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category")))) | 		"ifnull(tax_category, '') = {0}".format(frappe.db.escape(cstr(args.get("tax_category")))) | ||||||
|  | |||||||
| @ -62,7 +62,7 @@ def get_pos_entries(filters, group_by_field): | |||||||
| 		""" | 		""" | ||||||
| 		SELECT | 		SELECT | ||||||
| 			p.posting_date, p.name as pos_invoice, p.pos_profile, | 			p.posting_date, p.name as pos_invoice, p.pos_profile, | ||||||
| 			p.owner, p.base_grand_total as grand_total, p.base_paid_amount as paid_amount, | 			p.owner, p.base_grand_total as grand_total, p.base_paid_amount - p.change_amount  as paid_amount, | ||||||
| 			p.customer, p.is_return {select_mop_field} | 			p.customer, p.is_return {select_mop_field} | ||||||
| 		FROM | 		FROM | ||||||
| 			`tabPOS Invoice` p {from_sales_invoice_payment} | 			`tabPOS Invoice` p {from_sales_invoice_payment} | ||||||
|  | |||||||
| @ -9,7 +9,7 @@ from frappe import _ | |||||||
| from frappe.email.inbox import link_communication_to_document | from frappe.email.inbox import link_communication_to_document | ||||||
| from frappe.model.mapper import get_mapped_doc | from frappe.model.mapper import get_mapped_doc | ||||||
| from frappe.query_builder import DocType | from frappe.query_builder import DocType | ||||||
| from frappe.utils import cint, cstr, flt, get_fullname | from frappe.utils import cint, flt, get_fullname | ||||||
| 
 | 
 | ||||||
| from erpnext.crm.utils import add_link_in_communication, copy_comments | from erpnext.crm.utils import add_link_in_communication, copy_comments | ||||||
| from erpnext.setup.utils import get_exchange_rate | from erpnext.setup.utils import get_exchange_rate | ||||||
| @ -215,20 +215,20 @@ class Opportunity(TransactionBase): | |||||||
| 
 | 
 | ||||||
| 			if self.party_name and self.opportunity_from == "Customer": | 			if self.party_name and self.opportunity_from == "Customer": | ||||||
| 				if self.contact_person: | 				if self.contact_person: | ||||||
| 					opts.description = "Contact " + cstr(self.contact_person) | 					opts.description = f"Contact {self.contact_person}" | ||||||
| 				else: | 				else: | ||||||
| 					opts.description = "Contact customer " + cstr(self.party_name) | 					opts.description = f"Contact customer {self.party_name}" | ||||||
| 			elif self.party_name and self.opportunity_from == "Lead": | 			elif self.party_name and self.opportunity_from == "Lead": | ||||||
| 				if self.contact_display: | 				if self.contact_display: | ||||||
| 					opts.description = "Contact " + cstr(self.contact_display) | 					opts.description = f"Contact {self.contact_display}" | ||||||
| 				else: | 				else: | ||||||
| 					opts.description = "Contact lead " + cstr(self.party_name) | 					opts.description = f"Contact lead {self.party_name}" | ||||||
| 
 | 
 | ||||||
| 			opts.subject = opts.description | 			opts.subject = opts.description | ||||||
| 			opts.description += ". By : " + cstr(self.contact_by) | 			opts.description += f". By : {self.contact_by}" | ||||||
| 
 | 
 | ||||||
| 			if self.to_discuss: | 			if self.to_discuss: | ||||||
| 				opts.description += " To Discuss : " + cstr(self.to_discuss) | 				opts.description += f" To Discuss : {frappe.render_template(self.to_discuss, {'doc': self})}" | ||||||
| 
 | 
 | ||||||
| 			super(Opportunity, self).add_calendar_event(opts, force) | 			super(Opportunity, self).add_calendar_event(opts, force) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -4,7 +4,7 @@ | |||||||
| import unittest | import unittest | ||||||
| 
 | 
 | ||||||
| import frappe | import frappe | ||||||
| from frappe.utils import now_datetime, random_string, today | from frappe.utils import add_days, now_datetime, random_string, today | ||||||
| 
 | 
 | ||||||
| from erpnext.crm.doctype.lead.lead import make_customer | from erpnext.crm.doctype.lead.lead import make_customer | ||||||
| from erpnext.crm.doctype.lead.test_lead import make_lead | from erpnext.crm.doctype.lead.test_lead import make_lead | ||||||
| @ -97,6 +97,22 @@ class TestOpportunity(unittest.TestCase): | |||||||
| 		self.assertEqual(quotation_comment_count, 4) | 		self.assertEqual(quotation_comment_count, 4) | ||||||
| 		self.assertEqual(quotation_communication_count, 4) | 		self.assertEqual(quotation_communication_count, 4) | ||||||
| 
 | 
 | ||||||
|  | 	def test_render_template_for_to_discuss(self): | ||||||
|  | 		doc = make_opportunity(with_items=0, opportunity_from="Lead") | ||||||
|  | 		doc.contact_by = "test@example.com" | ||||||
|  | 		doc.contact_date = add_days(today(), days=2) | ||||||
|  | 		doc.to_discuss = "{{ doc.name }} test data" | ||||||
|  | 		doc.save() | ||||||
|  | 
 | ||||||
|  | 		event = frappe.get_all( | ||||||
|  | 			"Event Participants", | ||||||
|  | 			fields=["parent"], | ||||||
|  | 			filters={"reference_doctype": doc.doctype, "reference_docname": doc.name}, | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | 		event_description = frappe.db.get_value("Event", event[0].parent, "description") | ||||||
|  | 		self.assertTrue(doc.name in event_description) | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def make_opportunity_from_lead(): | def make_opportunity_from_lead(): | ||||||
| 	new_lead_email_id = "new{}@example.com".format(random_string(5)) | 	new_lead_email_id = "new{}@example.com".format(random_string(5)) | ||||||
|  | |||||||
| @ -139,7 +139,7 @@ class TestShoppingCart(unittest.TestCase): | |||||||
| 		tax_rule_master = set_taxes( | 		tax_rule_master = set_taxes( | ||||||
| 			quotation.party_name, | 			quotation.party_name, | ||||||
| 			"Customer", | 			"Customer", | ||||||
| 			quotation.transaction_date, | 			None, | ||||||
| 			quotation.company, | 			quotation.company, | ||||||
| 			customer_group=None, | 			customer_group=None, | ||||||
| 			supplier_group=None, | 			supplier_group=None, | ||||||
|  | |||||||
| @ -12,7 +12,7 @@ source_link = "https://github.com/frappe/erpnext" | |||||||
| app_logo_url = "/assets/erpnext/images/erpnext-logo.svg" | app_logo_url = "/assets/erpnext/images/erpnext-logo.svg" | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| develop_version = "13.x.x-develop" | develop_version = "14.x.x-develop" | ||||||
| 
 | 
 | ||||||
| app_include_js = "erpnext.bundle.js" | app_include_js = "erpnext.bundle.js" | ||||||
| app_include_css = "erpnext.bundle.css" | app_include_css = "erpnext.bundle.css" | ||||||
|  | |||||||
| @ -34,15 +34,6 @@ frappe.ui.form.on("Leave Allocation", { | |||||||
| 				}); | 				}); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 
 |  | ||||||
| 		// make new leaves allocated field read only if allocation is created via leave policy assignment
 |  | ||||||
| 		// and leave type is earned leave, since these leaves would be allocated via the scheduler
 |  | ||||||
| 		if (frm.doc.leave_policy_assignment) { |  | ||||||
| 			frappe.db.get_value("Leave Type", frm.doc.leave_type, "is_earned_leave", (r) => { |  | ||||||
| 				if (r && cint(r.is_earned_leave)) |  | ||||||
| 					frm.set_df_property("new_leaves_allocated", "read_only", 1); |  | ||||||
| 			}); |  | ||||||
| 		} |  | ||||||
| 	}, | 	}, | ||||||
| 
 | 
 | ||||||
| 	expire_allocation: function(frm) { | 	expire_allocation: function(frm) { | ||||||
|  | |||||||
| @ -254,7 +254,18 @@ class LeaveAllocation(Document): | |||||||
| 		# Adding a day to include To Date in the difference | 		# Adding a day to include To Date in the difference | ||||||
| 		date_difference = date_diff(self.to_date, self.from_date) + 1 | 		date_difference = date_diff(self.to_date, self.from_date) + 1 | ||||||
| 		if date_difference < self.total_leaves_allocated: | 		if date_difference < self.total_leaves_allocated: | ||||||
| 			frappe.throw(_("Total allocated leaves are more than days in the period"), OverAllocationError) | 			if frappe.db.get_value("Leave Type", self.leave_type, "allow_over_allocation"): | ||||||
|  | 				frappe.msgprint( | ||||||
|  | 					_("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"), | ||||||
|  | 					indicator="orange", | ||||||
|  | 					alert=True, | ||||||
|  | 				) | ||||||
|  | 			else: | ||||||
|  | 				frappe.throw( | ||||||
|  | 					_("<b>Total Leaves Allocated</b> are more than the number of days in the allocation period"), | ||||||
|  | 					exc=OverAllocationError, | ||||||
|  | 					title=_("Over Allocation"), | ||||||
|  | 				) | ||||||
| 
 | 
 | ||||||
| 	def create_leave_ledger_entry(self, submit=True): | 	def create_leave_ledger_entry(self, submit=True): | ||||||
| 		if self.unused_leaves: | 		if self.unused_leaves: | ||||||
|  | |||||||
| @ -69,22 +69,44 @@ class TestLeaveAllocation(FrappeTestCase): | |||||||
| 		self.assertRaises(frappe.ValidationError, doc.save) | 		self.assertRaises(frappe.ValidationError, doc.save) | ||||||
| 
 | 
 | ||||||
| 	def test_validation_for_over_allocation(self): | 	def test_validation_for_over_allocation(self): | ||||||
|  | 		leave_type = create_leave_type(leave_type_name="Test Over Allocation", is_carry_forward=1) | ||||||
|  | 		leave_type.save() | ||||||
|  | 
 | ||||||
| 		doc = frappe.get_doc( | 		doc = frappe.get_doc( | ||||||
| 			{ | 			{ | ||||||
| 				"doctype": "Leave Allocation", | 				"doctype": "Leave Allocation", | ||||||
| 				"__islocal": 1, | 				"__islocal": 1, | ||||||
| 				"employee": self.employee.name, | 				"employee": self.employee.name, | ||||||
| 				"employee_name": self.employee.employee_name, | 				"employee_name": self.employee.employee_name, | ||||||
| 				"leave_type": "_Test Leave Type", | 				"leave_type": leave_type.name, | ||||||
| 				"from_date": getdate("2015-09-1"), | 				"from_date": getdate("2015-09-1"), | ||||||
| 				"to_date": getdate("2015-09-30"), | 				"to_date": getdate("2015-09-30"), | ||||||
| 				"new_leaves_allocated": 35, | 				"new_leaves_allocated": 35, | ||||||
|  | 				"carry_forward": 1, | ||||||
| 			} | 			} | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
| 		# allocated leave more than period | 		# allocated leave more than period | ||||||
| 		self.assertRaises(OverAllocationError, doc.save) | 		self.assertRaises(OverAllocationError, doc.save) | ||||||
| 
 | 
 | ||||||
|  | 		leave_type.allow_over_allocation = 1 | ||||||
|  | 		leave_type.save() | ||||||
|  | 
 | ||||||
|  | 		# allows creating a leave allocation with more leave days than period days | ||||||
|  | 		doc = frappe.get_doc( | ||||||
|  | 			{ | ||||||
|  | 				"doctype": "Leave Allocation", | ||||||
|  | 				"__islocal": 1, | ||||||
|  | 				"employee": self.employee.name, | ||||||
|  | 				"employee_name": self.employee.employee_name, | ||||||
|  | 				"leave_type": leave_type.name, | ||||||
|  | 				"from_date": getdate("2015-09-1"), | ||||||
|  | 				"to_date": getdate("2015-09-30"), | ||||||
|  | 				"new_leaves_allocated": 35, | ||||||
|  | 				"carry_forward": 1, | ||||||
|  | 			} | ||||||
|  | 		).insert() | ||||||
|  | 
 | ||||||
| 	def test_validation_for_over_allocation_post_submission(self): | 	def test_validation_for_over_allocation_post_submission(self): | ||||||
| 		allocation = frappe.get_doc( | 		allocation = frappe.get_doc( | ||||||
| 			{ | 			{ | ||||||
|  | |||||||
| @ -745,7 +745,7 @@ class TestLeaveApplication(unittest.TestCase): | |||||||
| 
 | 
 | ||||||
| 		i = 0 | 		i = 0 | ||||||
| 		while i < 14: | 		while i < 14: | ||||||
| 			allocate_earned_leaves(ignore_duplicates=True) | 			allocate_earned_leaves() | ||||||
| 			i += 1 | 			i += 1 | ||||||
| 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) | 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 6) | ||||||
| 
 | 
 | ||||||
| @ -753,7 +753,7 @@ class TestLeaveApplication(unittest.TestCase): | |||||||
| 		frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0) | 		frappe.db.set_value("Leave Type", leave_type, "max_leaves_allowed", 0) | ||||||
| 		i = 0 | 		i = 0 | ||||||
| 		while i < 6: | 		while i < 6: | ||||||
| 			allocate_earned_leaves(ignore_duplicates=True) | 			allocate_earned_leaves() | ||||||
| 			i += 1 | 			i += 1 | ||||||
| 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) | 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -4,6 +4,7 @@ | |||||||
| import unittest | import unittest | ||||||
| 
 | 
 | ||||||
| import frappe | import frappe | ||||||
|  | from frappe.tests.utils import FrappeTestCase | ||||||
| from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate | from frappe.utils import add_days, add_months, get_first_day, get_last_day, getdate | ||||||
| 
 | 
 | ||||||
| from erpnext.hr.doctype.leave_application.test_leave_application import ( | from erpnext.hr.doctype.leave_application.test_leave_application import ( | ||||||
| @ -18,7 +19,7 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( | |||||||
| test_dependencies = ["Employee"] | test_dependencies = ["Employee"] | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| class TestLeavePolicyAssignment(unittest.TestCase): | class TestLeavePolicyAssignment(FrappeTestCase): | ||||||
| 	def setUp(self): | 	def setUp(self): | ||||||
| 		for doctype in [ | 		for doctype in [ | ||||||
| 			"Leave Period", | 			"Leave Period", | ||||||
| @ -39,6 +40,9 @@ class TestLeavePolicyAssignment(unittest.TestCase): | |||||||
| 		leave_policy = create_leave_policy() | 		leave_policy = create_leave_policy() | ||||||
| 		leave_policy.submit() | 		leave_policy.submit() | ||||||
| 
 | 
 | ||||||
|  | 		self.employee.date_of_joining = get_first_day(leave_period.from_date) | ||||||
|  | 		self.employee.save() | ||||||
|  | 
 | ||||||
| 		data = { | 		data = { | ||||||
| 			"assignment_based_on": "Leave Period", | 			"assignment_based_on": "Leave Period", | ||||||
| 			"leave_policy": leave_policy.name, | 			"leave_policy": leave_policy.name, | ||||||
| @ -188,19 +192,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): | |||||||
| 		) | 		) | ||||||
| 		self.assertEqual(leaves_allocated, 3) | 		self.assertEqual(leaves_allocated, 3) | ||||||
| 
 | 
 | ||||||
| 		# if the daily job is not completed yet, there is another check present |  | ||||||
| 		# to ensure leave is not already allocated to avoid duplication |  | ||||||
| 		from erpnext.hr.utils import allocate_earned_leaves |  | ||||||
| 
 |  | ||||||
| 		allocate_earned_leaves() |  | ||||||
| 
 |  | ||||||
| 		leaves_allocated = frappe.db.get_value( |  | ||||||
| 			"Leave Allocation", |  | ||||||
| 			{"leave_policy_assignment": leave_policy_assignments[0]}, |  | ||||||
| 			"total_leaves_allocated", |  | ||||||
| 		) |  | ||||||
| 		self.assertEqual(leaves_allocated, 3) |  | ||||||
| 
 |  | ||||||
| 	def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self): | 	def test_earned_leave_alloc_for_passed_months_with_cf_leaves_based_on_leave_period(self): | ||||||
| 		from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation | 		from erpnext.hr.doctype.leave_allocation.test_leave_allocation import create_leave_allocation | ||||||
| 
 | 
 | ||||||
| @ -242,20 +233,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): | |||||||
| 		self.assertEqual(details.unused_leaves, 5) | 		self.assertEqual(details.unused_leaves, 5) | ||||||
| 		self.assertEqual(details.total_leaves_allocated, 7) | 		self.assertEqual(details.total_leaves_allocated, 7) | ||||||
| 
 | 
 | ||||||
| 		# if the daily job is not completed yet, there is another check present |  | ||||||
| 		# to ensure leave is not already allocated to avoid duplication |  | ||||||
| 		from erpnext.hr.utils import is_earned_leave_already_allocated |  | ||||||
| 
 |  | ||||||
| 		frappe.flags.current_date = get_last_day(getdate()) |  | ||||||
| 
 |  | ||||||
| 		allocation = frappe.get_doc("Leave Allocation", details.name) |  | ||||||
| 		# 1 leave is still pending to be allocated, irrespective of carry forwarded leaves |  | ||||||
| 		self.assertFalse( |  | ||||||
| 			is_earned_leave_already_allocated( |  | ||||||
| 				allocation, leave_policy.leave_policy_details[0].annual_allocation |  | ||||||
| 			) |  | ||||||
| 		) |  | ||||||
| 
 |  | ||||||
| 	def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self): | 	def test_earned_leave_alloc_for_passed_months_based_on_joining_date(self): | ||||||
| 		# tests leave alloc for earned leaves for assignment based on joining date in policy assignment | 		# tests leave alloc for earned leaves for assignment based on joining date in policy assignment | ||||||
| 		leave_type = create_earned_leave_type("Test Earned Leave") | 		leave_type = create_earned_leave_type("Test Earned Leave") | ||||||
| @ -288,19 +265,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): | |||||||
| 		self.assertEqual(effective_from, self.employee.date_of_joining) | 		self.assertEqual(effective_from, self.employee.date_of_joining) | ||||||
| 		self.assertEqual(leaves_allocated, 3) | 		self.assertEqual(leaves_allocated, 3) | ||||||
| 
 | 
 | ||||||
| 		# to ensure leave is not already allocated to avoid duplication |  | ||||||
| 		from erpnext.hr.utils import allocate_earned_leaves |  | ||||||
| 
 |  | ||||||
| 		frappe.flags.current_date = get_last_day(getdate()) |  | ||||||
| 		allocate_earned_leaves() |  | ||||||
| 
 |  | ||||||
| 		leaves_allocated = frappe.db.get_value( |  | ||||||
| 			"Leave Allocation", |  | ||||||
| 			{"leave_policy_assignment": leave_policy_assignments[0]}, |  | ||||||
| 			"total_leaves_allocated", |  | ||||||
| 		) |  | ||||||
| 		self.assertEqual(leaves_allocated, 3) |  | ||||||
| 
 |  | ||||||
| 	def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self): | 	def test_grant_leaves_on_doj_for_earned_leaves_based_on_leave_period(self): | ||||||
| 		# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type | 		# tests leave alloc based on leave period for earned leaves with "based on doj" configuration in leave type | ||||||
| 		leave_period, leave_policy = setup_leave_period_and_policy( | 		leave_period, leave_policy = setup_leave_period_and_policy( | ||||||
| @ -330,20 +294,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): | |||||||
| 		) | 		) | ||||||
| 		self.assertEqual(leaves_allocated, 3) | 		self.assertEqual(leaves_allocated, 3) | ||||||
| 
 | 
 | ||||||
| 		# if the daily job is not completed yet, there is another check present |  | ||||||
| 		# to ensure leave is not already allocated to avoid duplication |  | ||||||
| 		from erpnext.hr.utils import allocate_earned_leaves |  | ||||||
| 
 |  | ||||||
| 		frappe.flags.current_date = get_first_day(getdate()) |  | ||||||
| 		allocate_earned_leaves() |  | ||||||
| 
 |  | ||||||
| 		leaves_allocated = frappe.db.get_value( |  | ||||||
| 			"Leave Allocation", |  | ||||||
| 			{"leave_policy_assignment": leave_policy_assignments[0]}, |  | ||||||
| 			"total_leaves_allocated", |  | ||||||
| 		) |  | ||||||
| 		self.assertEqual(leaves_allocated, 3) |  | ||||||
| 
 |  | ||||||
| 	def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self): | 	def test_grant_leaves_on_doj_for_earned_leaves_based_on_joining_date(self): | ||||||
| 		# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type | 		# tests leave alloc based on joining date for earned leaves with "based on doj" configuration in leave type | ||||||
| 		leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True) | 		leave_type = create_earned_leave_type("Test Earned Leave", based_on_doj=True) | ||||||
| @ -377,21 +327,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): | |||||||
| 		self.assertEqual(effective_from, self.employee.date_of_joining) | 		self.assertEqual(effective_from, self.employee.date_of_joining) | ||||||
| 		self.assertEqual(leaves_allocated, 3) | 		self.assertEqual(leaves_allocated, 3) | ||||||
| 
 | 
 | ||||||
| 		# to ensure leave is not already allocated to avoid duplication |  | ||||||
| 		from erpnext.hr.utils import allocate_earned_leaves |  | ||||||
| 
 |  | ||||||
| 		frappe.flags.current_date = get_first_day(getdate()) |  | ||||||
| 		allocate_earned_leaves() |  | ||||||
| 
 |  | ||||||
| 		leaves_allocated = frappe.db.get_value( |  | ||||||
| 			"Leave Allocation", |  | ||||||
| 			{"leave_policy_assignment": leave_policy_assignments[0]}, |  | ||||||
| 			"total_leaves_allocated", |  | ||||||
| 		) |  | ||||||
| 		self.assertEqual(leaves_allocated, 3) |  | ||||||
| 
 |  | ||||||
| 	def tearDown(self): | 	def tearDown(self): | ||||||
| 		frappe.db.rollback() |  | ||||||
| 		frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj) | 		frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj) | ||||||
| 		frappe.flags.current_date = None | 		frappe.flags.current_date = None | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -19,6 +19,7 @@ | |||||||
|   "fraction_of_daily_salary_per_leave", |   "fraction_of_daily_salary_per_leave", | ||||||
|   "is_optional_leave", |   "is_optional_leave", | ||||||
|   "allow_negative", |   "allow_negative", | ||||||
|  |   "allow_over_allocation", | ||||||
|   "include_holiday", |   "include_holiday", | ||||||
|   "is_compensatory", |   "is_compensatory", | ||||||
|   "carry_forward_section", |   "carry_forward_section", | ||||||
| @ -211,15 +212,23 @@ | |||||||
|    "fieldtype": "Float", |    "fieldtype": "Float", | ||||||
|    "label": "Fraction of Daily Salary per Leave", |    "label": "Fraction of Daily Salary per Leave", | ||||||
|    "mandatory_depends_on": "eval:doc.is_ppl == 1" |    "mandatory_depends_on": "eval:doc.is_ppl == 1" | ||||||
|  |   }, | ||||||
|  |   { | ||||||
|  |    "default": "0", | ||||||
|  |    "description": "Allows allocating more leaves than the number of days in the allocation period.", | ||||||
|  |    "fieldname": "allow_over_allocation", | ||||||
|  |    "fieldtype": "Check", | ||||||
|  |    "label": "Allow Over Allocation" | ||||||
|   } |   } | ||||||
|  ], |  ], | ||||||
|  "icon": "fa fa-flag", |  "icon": "fa fa-flag", | ||||||
|  "idx": 1, |  "idx": 1, | ||||||
|  "links": [], |  "links": [], | ||||||
|  "modified": "2021-10-02 11:59:40.503359", |  "modified": "2022-05-09 05:01:38.957545", | ||||||
|  "modified_by": "Administrator", |  "modified_by": "Administrator", | ||||||
|  "module": "HR", |  "module": "HR", | ||||||
|  "name": "Leave Type", |  "name": "Leave Type", | ||||||
|  |  "naming_rule": "By fieldname", | ||||||
|  "owner": "Administrator", |  "owner": "Administrator", | ||||||
|  "permissions": [ |  "permissions": [ | ||||||
|   { |   { | ||||||
| @ -251,5 +260,6 @@ | |||||||
|  ], |  ], | ||||||
|  "sort_field": "modified", |  "sort_field": "modified", | ||||||
|  "sort_order": "DESC", |  "sort_order": "DESC", | ||||||
|  |  "states": [], | ||||||
|  "track_changes": 1 |  "track_changes": 1 | ||||||
| } | } | ||||||
| @ -269,7 +269,7 @@ def generate_leave_encashment(): | |||||||
| 		create_leave_encashment(leave_allocation=leave_allocation) | 		create_leave_encashment(leave_allocation=leave_allocation) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def allocate_earned_leaves(ignore_duplicates=False): | def allocate_earned_leaves(): | ||||||
| 	"""Allocate earned leaves to Employees""" | 	"""Allocate earned leaves to Employees""" | ||||||
| 	e_leave_types = get_earned_leaves() | 	e_leave_types = get_earned_leaves() | ||||||
| 	today = getdate() | 	today = getdate() | ||||||
| @ -305,14 +305,10 @@ def allocate_earned_leaves(ignore_duplicates=False): | |||||||
| 			if check_effective_date( | 			if check_effective_date( | ||||||
| 				from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining | 				from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining | ||||||
| 			): | 			): | ||||||
| 				update_previous_leave_allocation( | 				update_previous_leave_allocation(allocation, annual_allocation, e_leave_type) | ||||||
| 					allocation, annual_allocation, e_leave_type, ignore_duplicates |  | ||||||
| 				) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def update_previous_leave_allocation( | def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type): | ||||||
| 	allocation, annual_allocation, e_leave_type, ignore_duplicates=False |  | ||||||
| ): |  | ||||||
| 	earned_leaves = get_monthly_earned_leave( | 	earned_leaves = get_monthly_earned_leave( | ||||||
| 		annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding | 		annual_allocation, e_leave_type.earned_leave_frequency, e_leave_type.rounding | ||||||
| 	) | 	) | ||||||
| @ -326,20 +322,19 @@ def update_previous_leave_allocation( | |||||||
| 	if new_allocation != allocation.total_leaves_allocated: | 	if new_allocation != allocation.total_leaves_allocated: | ||||||
| 		today_date = today() | 		today_date = today() | ||||||
| 
 | 
 | ||||||
| 		if ignore_duplicates or not is_earned_leave_already_allocated(allocation, annual_allocation): | 		allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) | ||||||
| 			allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) | 		create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) | ||||||
| 			create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) |  | ||||||
| 
 | 
 | ||||||
| 			if e_leave_type.based_on_date_of_joining: | 		if e_leave_type.based_on_date_of_joining: | ||||||
| 				text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format( | 			text = _("allocated {0} leave(s) via scheduler on {1} based on the date of joining").format( | ||||||
| 					frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) | 				frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) | ||||||
| 				) | 			) | ||||||
| 			else: | 		else: | ||||||
| 				text = _("allocated {0} leave(s) via scheduler on {1}").format( | 			text = _("allocated {0} leave(s) via scheduler on {1}").format( | ||||||
| 					frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) | 				frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) | ||||||
| 				) | 			) | ||||||
| 
 | 
 | ||||||
| 			allocation.add_comment(comment_type="Info", text=text) | 		allocation.add_comment(comment_type="Info", text=text) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| def get_monthly_earned_leave(annual_leaves, frequency, rounding): | def get_monthly_earned_leave(annual_leaves, frequency, rounding): | ||||||
|  | |||||||
| @ -99,8 +99,21 @@ erpnext.setup_einvoice_actions = (doctype) => { | |||||||
| 									...data | 									...data | ||||||
| 								}, | 								}, | ||||||
| 								freeze: true, | 								freeze: true, | ||||||
| 								callback: () => frm.reload_doc() || d.hide(), | 								callback: () => { | ||||||
| 								error: () => d.hide() | 									frappe.show_alert({ | ||||||
|  | 										message: __('E-Way Bill Generated successfully'), | ||||||
|  | 										indicator: 'green' | ||||||
|  | 									}, 7); | ||||||
|  | 									frm.reload_doc(); | ||||||
|  | 									d.hide(); | ||||||
|  | 								}, | ||||||
|  | 								error: () => { | ||||||
|  | 									frappe.show_alert({ | ||||||
|  | 										message: __('E-Way Bill was not Generated'), | ||||||
|  | 										indicator: 'red' | ||||||
|  | 									}, 7); | ||||||
|  | 									d.hide(); | ||||||
|  | 								} | ||||||
| 							}); | 							}); | ||||||
| 						}, | 						}, | ||||||
| 						primary_action_label: __('Submit') | 						primary_action_label: __('Submit') | ||||||
| @ -136,29 +149,83 @@ 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 = () => { | ||||||
| 					let message = __('Cancellation of e-way bill is currently not supported.') + ' '; | 					const d = new frappe.ui.Dialog({ | ||||||
| 					message += '<br><br>'; | 						title: __('Cancel E-Way Bill'), | ||||||
| 					message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); | 						fields: fields, | ||||||
|  | 						primary_action: function() { | ||||||
|  | 							const data = d.get_values(); | ||||||
|  | 							frappe.call({ | ||||||
|  | 								method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', | ||||||
|  | 								args: { | ||||||
|  | 									doctype, | ||||||
|  | 									docname: name, | ||||||
|  | 									eway_bill: ewaybill, | ||||||
|  | 									reason: data.reason.split('-')[0], | ||||||
|  | 									remark: data.remark | ||||||
|  | 								}, | ||||||
|  | 								freeze: true, | ||||||
|  | 								callback: () => { | ||||||
|  | 									frappe.show_alert({ | ||||||
|  | 										message: __('E-Way Bill Cancelled successfully'), | ||||||
|  | 										indicator: 'green' | ||||||
|  | 									}, 7); | ||||||
|  | 									frm.reload_doc(); | ||||||
|  | 									d.hide(); | ||||||
|  | 								}, | ||||||
|  | 								error: () => { | ||||||
|  | 									frappe.show_alert({ | ||||||
|  | 										message: __('E-Way Bill was not Cancelled'), | ||||||
|  | 										indicator: 'red' | ||||||
|  | 									}, 7); | ||||||
|  | 									d.hide(); | ||||||
|  | 								} | ||||||
|  | 							}); | ||||||
|  | 						}, | ||||||
|  | 						primary_action_label: __('Submit') | ||||||
|  | 					}); | ||||||
|  | 					d.show(); | ||||||
|  | 				}; | ||||||
|  | 				add_custom_button(__("Cancel E-Way Bill"), action); | ||||||
|  | 			} | ||||||
| 
 | 
 | ||||||
|  | 			if (irn && !irn_cancelled) { | ||||||
|  | 				const action = () => { | ||||||
| 					const dialog = frappe.msgprint({ | 					const dialog = frappe.msgprint({ | ||||||
| 						title: __('Update E-Way Bill Cancelled Status?'), | 						title: __("Generate QRCode"), | ||||||
| 						message: message, | 						message: __("Generate and attach QR Code using IRN?"), | ||||||
| 						indicator: 'orange', |  | ||||||
| 						primary_action: { | 						primary_action: { | ||||||
| 							action: function() { | 							action: function() { | ||||||
| 								frappe.call({ | 								frappe.call({ | ||||||
| 									method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', | 									method: 'erpnext.regional.india.e_invoice.utils.generate_qrcode', | ||||||
| 									args: { doctype, docname: name }, | 									args: { doctype, docname: name }, | ||||||
| 									freeze: true, | 									freeze: true, | ||||||
| 									callback: () => frm.reload_doc() || dialog.hide() | 									callback: () => frm.reload_doc() || dialog.hide(), | ||||||
|  | 									error: () => dialog.hide() | ||||||
| 								}); | 								}); | ||||||
| 							} | 							} | ||||||
| 						}, | 						}, | ||||||
| 						primary_action_label: __('Yes') | 						primary_action_label: __('Yes') | ||||||
| 					}); | 					}); | ||||||
|  | 					dialog.show(); | ||||||
| 				}; | 				}; | ||||||
| 				add_custom_button(__("Cancel E-Way Bill"), action); | 				add_custom_button(__("Generate QRCode"), action); | ||||||
| 			} | 			} | ||||||
| 		} | 		} | ||||||
| 	}); | 	}); | ||||||
| @ -167,85 +234,100 @@ erpnext.setup_einvoice_actions = (doctype) => { | |||||||
| const get_ewaybill_fields = (frm) => { | const get_ewaybill_fields = (frm) => { | ||||||
| 	return [ | 	return [ | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'transporter', | 			fieldname: "eway_part_a_section_break", | ||||||
| 			'label': 'Transporter', | 			fieldtype: "Section Break", | ||||||
| 			'fieldtype': 'Link', | 			label: "Part A", | ||||||
| 			'options': 'Supplier', |  | ||||||
| 			'default': frm.doc.transporter |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'gst_transporter_id', | 			fieldname: "transporter", | ||||||
| 			'label': 'GST Transporter ID', | 			label: "Transporter", | ||||||
| 			'fieldtype': 'Data', | 			fieldtype: "Link", | ||||||
| 			'default': frm.doc.gst_transporter_id | 			options: "Supplier", | ||||||
|  | 			default: frm.doc.transporter, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'driver', | 			fieldname: "transporter_name", | ||||||
| 			'label': 'Driver', | 			label: "Transporter Name", | ||||||
| 			'fieldtype': 'Link', | 			fieldtype: "Data", | ||||||
| 			'options': 'Driver', | 			read_only: 1, | ||||||
| 			'default': frm.doc.driver | 			default: frm.doc.transporter_name, | ||||||
|  | 			depends_on: "transporter", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'lr_no', | 			fieldname: "part_a_column_break", | ||||||
| 			'label': 'Transport Receipt No', | 			fieldtype: "Column Break", | ||||||
| 			'fieldtype': 'Data', |  | ||||||
| 			'default': frm.doc.lr_no |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'vehicle_no', | 			fieldname: "gst_transporter_id", | ||||||
| 			'label': 'Vehicle No', | 			label: "GST Transporter ID", | ||||||
| 			'fieldtype': 'Data', | 			fieldtype: "Data", | ||||||
| 			'default': frm.doc.vehicle_no | 			default: frm.doc.gst_transporter_id, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'distance', | 			fieldname: "distance", | ||||||
| 			'label': 'Distance (in km)', | 			label: "Distance (in km)", | ||||||
| 			'fieldtype': 'Float', | 			fieldtype: "Float", | ||||||
| 			'default': frm.doc.distance | 			default: frm.doc.distance, | ||||||
|  | 			description: 'Set as zero to auto calculate distance using pin codes', | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'transporter_col_break', | 			fieldname: "eway_part_b_section_break", | ||||||
| 			'fieldtype': 'Column Break', | 			fieldtype: "Section Break", | ||||||
|  | 			label: "Part B", | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'transporter_name', | 			fieldname: "mode_of_transport", | ||||||
| 			'label': 'Transporter Name', | 			label: "Mode of Transport", | ||||||
| 			'fieldtype': 'Data', | 			fieldtype: "Select", | ||||||
| 			'read_only': 1, | 			options: `\nRoad\nAir\nRail\nShip`, | ||||||
| 			'default': frm.doc.transporter_name, | 			default: frm.doc.mode_of_transport, | ||||||
| 			'depends_on': 'transporter' |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'mode_of_transport', | 			fieldname: "gst_vehicle_type", | ||||||
| 			'label': 'Mode of Transport', | 			label: "GST Vehicle Type", | ||||||
| 			'fieldtype': 'Select', | 			fieldtype: "Select", | ||||||
| 			'options': `\nRoad\nAir\nRail\nShip`, | 			options: `Regular\nOver Dimensional Cargo (ODC)`, | ||||||
| 			'default': frm.doc.mode_of_transport | 			depends_on: 'eval:(doc.mode_of_transport === "Road")', | ||||||
|  | 			default: frm.doc.gst_vehicle_type, | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'driver_name', | 			fieldname: "vehicle_no", | ||||||
| 			'label': 'Driver Name', | 			label: "Vehicle No", | ||||||
| 			'fieldtype': 'Data', | 			fieldtype: "Data", | ||||||
| 			'fetch_from': 'driver.full_name', | 			default: frm.doc.vehicle_no, | ||||||
| 			'read_only': 1, |  | ||||||
| 			'default': frm.doc.driver_name, |  | ||||||
| 			'depends_on': 'driver' |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'lr_date', | 			fieldname: "part_b_column_break", | ||||||
| 			'label': 'Transport Receipt Date', | 			fieldtype: "Column Break", | ||||||
| 			'fieldtype': 'Date', |  | ||||||
| 			'default': frm.doc.lr_date |  | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			'fieldname': 'gst_vehicle_type', | 			fieldname: "lr_date", | ||||||
| 			'label': 'GST Vehicle Type', | 			label: "Transport Receipt Date", | ||||||
| 			'fieldtype': 'Select', | 			fieldtype: "Date", | ||||||
| 			'options': `Regular\nOver Dimensional Cargo (ODC)`, | 			default: frm.doc.lr_date, | ||||||
| 			'depends_on': 'eval:(doc.mode_of_transport === "Road")', | 		}, | ||||||
| 			'default': frm.doc.gst_vehicle_type | 		{ | ||||||
| 		} | 			fieldname: "lr_no", | ||||||
|  | 			label: "Transport Receipt No", | ||||||
|  | 			fieldtype: "Data", | ||||||
|  | 			default: frm.doc.lr_no, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			fieldname: "driver", | ||||||
|  | 			label: "Driver", | ||||||
|  | 			fieldtype: "Link", | ||||||
|  | 			options: "Driver", | ||||||
|  | 			default: frm.doc.driver, | ||||||
|  | 		}, | ||||||
|  | 		{ | ||||||
|  | 			fieldname: "driver_name", | ||||||
|  | 			label: "Driver Name", | ||||||
|  | 			fieldtype: "Data", | ||||||
|  | 			fetch_from: "driver.full_name", | ||||||
|  | 			read_only: 1, | ||||||
|  | 			default: frm.doc.driver_name, | ||||||
|  | 			depends_on: "driver", | ||||||
|  | 		}, | ||||||
| 	]; | 	]; | ||||||
| }; | }; | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -167,7 +167,12 @@ def get_doc_details(invoice): | |||||||
| 			title=_("Not Allowed"), | 			title=_("Not Allowed"), | ||||||
| 		) | 		) | ||||||
| 
 | 
 | ||||||
| 	invoice_type = "CRN" if invoice.is_return else "INV" | 	if invoice.is_return: | ||||||
|  | 		invoice_type = "CRN" | ||||||
|  | 	elif invoice.is_debit_note: | ||||||
|  | 		invoice_type = "DBN" | ||||||
|  | 	else: | ||||||
|  | 		invoice_type = "INV" | ||||||
| 
 | 
 | ||||||
| 	invoice_name = invoice.name | 	invoice_name = invoice.name | ||||||
| 	invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy") | 	invoice_date = format_date(invoice.posting_date, "dd/mm/yyyy") | ||||||
| @ -792,8 +797,9 @@ class GSPConnector: | |||||||
| 		self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn" | 		self.irn_details_url = self.base_url + "/enriched/ei/api/invoice/irn" | ||||||
| 		self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice" | 		self.generate_irn_url = self.base_url + "/enriched/ei/api/invoice" | ||||||
| 		self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" | 		self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" | ||||||
| 		self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB" | 		self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi" | ||||||
| 		self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill" | 		self.generate_ewaybill_url = self.base_url + "/enriched/ei/api/ewaybill" | ||||||
|  | 		self.get_qrcode_url = self.base_url + "/enriched/ei/others/qr/image" | ||||||
| 
 | 
 | ||||||
| 	def set_invoice(self): | 	def set_invoice(self): | ||||||
| 		self.invoice = None | 		self.invoice = None | ||||||
| @ -857,8 +863,8 @@ class GSPConnector: | |||||||
| 		return res | 		return res | ||||||
| 
 | 
 | ||||||
| 	def auto_refresh_token(self): | 	def auto_refresh_token(self): | ||||||
| 		self.fetch_auth_token() |  | ||||||
| 		self.token_auto_refreshed = True | 		self.token_auto_refreshed = True | ||||||
|  | 		self.fetch_auth_token() | ||||||
| 
 | 
 | ||||||
| 	def log_request(self, url, headers, data, res): | 	def log_request(self, url, headers, data, res): | ||||||
| 		headers.update({"password": self.credentials.password}) | 		headers.update({"password": self.credentials.password}) | ||||||
| @ -998,6 +1004,37 @@ class GSPConnector: | |||||||
| 
 | 
 | ||||||
| 		return failed | 		return failed | ||||||
| 
 | 
 | ||||||
|  | 	def fetch_and_attach_qrcode_from_irn(self): | ||||||
|  | 		qrcode = self.get_qrcode_from_irn(self.invoice.irn) | ||||||
|  | 		if qrcode: | ||||||
|  | 			qrcode_file = self.create_qr_code_file(qrcode) | ||||||
|  | 			frappe.db.set_value("Sales Invoice", self.invoice.name, "qrcode_image", qrcode_file.file_url) | ||||||
|  | 			frappe.msgprint(_("QR Code attached to the invoice"), alert=True) | ||||||
|  | 		else: | ||||||
|  | 			frappe.msgprint(_("QR Code not found for the IRN"), alert=True) | ||||||
|  | 
 | ||||||
|  | 	def get_qrcode_from_irn(self, irn): | ||||||
|  | 		import requests | ||||||
|  | 
 | ||||||
|  | 		headers = self.get_headers() | ||||||
|  | 		headers.update({"width": "215", "height": "215", "imgtype": "jpg", "irn": irn}) | ||||||
|  | 
 | ||||||
|  | 		try: | ||||||
|  | 			# using requests.get instead of make_request to avoid parsing the response | ||||||
|  | 			res = requests.get(self.get_qrcode_url, headers=headers) | ||||||
|  | 			self.log_request(self.get_qrcode_url, headers, None, None) | ||||||
|  | 			if res.status_code == 200: | ||||||
|  | 				return res.content | ||||||
|  | 			else: | ||||||
|  | 				raise RequestFailed(str(res.content, "utf-8")) | ||||||
|  | 
 | ||||||
|  | 		except RequestFailed as e: | ||||||
|  | 			self.raise_error(errors=str(e)) | ||||||
|  | 
 | ||||||
|  | 		except Exception: | ||||||
|  | 			log_error() | ||||||
|  | 			self.raise_error() | ||||||
|  | 
 | ||||||
| 	def get_irn_details(self, irn): | 	def get_irn_details(self, irn): | ||||||
| 		headers = self.get_headers() | 		headers = self.get_headers() | ||||||
| 
 | 
 | ||||||
| @ -1113,6 +1150,19 @@ class GSPConnector: | |||||||
| 				self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill") | 				self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill") | ||||||
| 				self.invoice.eway_bill_cancelled = 0 | 				self.invoice.eway_bill_cancelled = 0 | ||||||
| 				self.invoice.update(args) | 				self.invoice.update(args) | ||||||
|  | 				if res.get("info"): | ||||||
|  | 					info = res.get("info") | ||||||
|  | 					# when we have more features (responses) in eway bill, we can add them using below forloop. | ||||||
|  | 					for msg in info: | ||||||
|  | 						if msg.get("InfCd") == "EWBPPD": | ||||||
|  | 							pin_to_pin_distance = int(re.search(r"\d+", msg.get("Desc")).group()) | ||||||
|  | 							frappe.msgprint( | ||||||
|  | 								_("Auto Calculated Distance is {} KM.").format(str(pin_to_pin_distance)), | ||||||
|  | 								title="Notification", | ||||||
|  | 								indicator="green", | ||||||
|  | 								alert=True, | ||||||
|  | 							) | ||||||
|  | 							self.invoice.distance = flt(pin_to_pin_distance) | ||||||
| 				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, | ||||||
| @ -1135,7 +1185,6 @@ class GSPConnector: | |||||||
| 		headers = self.get_headers() | 		headers = self.get_headers() | ||||||
| 		data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4) | 		data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4) | ||||||
| 		headers["username"] = headers["user_name"] | 		headers["username"] = headers["user_name"] | ||||||
| 		del headers["user_name"] |  | ||||||
| 		try: | 		try: | ||||||
| 			res = self.make_request("post", self.cancel_ewaybill_url, headers, data) | 			res = self.make_request("post", self.cancel_ewaybill_url, headers, data) | ||||||
| 			if res.get("success"): | 			if res.get("success"): | ||||||
| @ -1186,8 +1235,6 @@ class GSPConnector: | |||||||
| 		return errors | 		return errors | ||||||
| 
 | 
 | ||||||
| 	def raise_error(self, raise_exception=False, errors=None): | 	def raise_error(self, raise_exception=False, errors=None): | ||||||
| 		if errors is None: |  | ||||||
| 			errors = [] |  | ||||||
| 		title = _("E Invoice Request Failed") | 		title = _("E Invoice Request Failed") | ||||||
| 		if errors: | 		if errors: | ||||||
| 			frappe.throw(errors, title=title, as_list=1) | 			frappe.throw(errors, title=title, as_list=1) | ||||||
| @ -1228,13 +1275,18 @@ class GSPConnector: | |||||||
| 
 | 
 | ||||||
| 	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 |  | ||||||
| 		docname = self.invoice.name |  | ||||||
| 		filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__") |  | ||||||
| 
 | 
 | ||||||
| 		qr_image = io.BytesIO() | 		qr_image = io.BytesIO() | ||||||
| 		url = qrcreate(qrcode, error="L") | 		url = qrcreate(qrcode, error="L") | ||||||
| 		url.png(qr_image, scale=2, quiet_zone=1) | 		url.png(qr_image, scale=2, quiet_zone=1) | ||||||
|  | 		qrcode_file = self.create_qr_code_file(qr_image.getvalue()) | ||||||
|  | 		self.invoice.qrcode_image = qrcode_file.file_url | ||||||
|  | 
 | ||||||
|  | 	def create_qr_code_file(self, qr_image): | ||||||
|  | 		doctype = self.invoice.doctype | ||||||
|  | 		docname = self.invoice.name | ||||||
|  | 		filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__") | ||||||
|  | 
 | ||||||
| 		_file = frappe.get_doc( | 		_file = frappe.get_doc( | ||||||
| 			{ | 			{ | ||||||
| 				"doctype": "File", | 				"doctype": "File", | ||||||
| @ -1243,12 +1295,12 @@ class GSPConnector: | |||||||
| 				"attached_to_name": docname, | 				"attached_to_name": docname, | ||||||
| 				"attached_to_field": "qrcode_image", | 				"attached_to_field": "qrcode_image", | ||||||
| 				"is_private": 0, | 				"is_private": 0, | ||||||
| 				"content": qr_image.getvalue(), | 				"content": qr_image, | ||||||
| 			} | 			} | ||||||
| 		) | 		) | ||||||
| 		_file.save() | 		_file.save() | ||||||
| 		frappe.db.commit() | 		frappe.db.commit() | ||||||
| 		self.invoice.qrcode_image = _file.file_url | 		return _file | ||||||
| 
 | 
 | ||||||
| 	def update_invoice(self): | 	def update_invoice(self): | ||||||
| 		self.invoice.flags.ignore_validate_update_after_submit = True | 		self.invoice.flags.ignore_validate_update_after_submit = True | ||||||
| @ -1293,6 +1345,12 @@ def cancel_irn(doctype, docname, irn, reason, remark): | |||||||
| 	gsp_connector.cancel_irn(irn, reason, remark) | 	gsp_connector.cancel_irn(irn, reason, remark) | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | @frappe.whitelist() | ||||||
|  | def generate_qrcode(doctype, docname): | ||||||
|  | 	gsp_connector = GSPConnector(doctype, docname) | ||||||
|  | 	gsp_connector.fetch_and_attach_qrcode_from_irn() | ||||||
|  | 
 | ||||||
|  | 
 | ||||||
| @frappe.whitelist() | @frappe.whitelist() | ||||||
| def generate_eway_bill(doctype, docname, **kwargs): | def generate_eway_bill(doctype, docname, **kwargs): | ||||||
| 	gsp_connector = GSPConnector(doctype, docname) | 	gsp_connector = GSPConnector(doctype, docname) | ||||||
| @ -1300,13 +1358,9 @@ def generate_eway_bill(doctype, docname, **kwargs): | |||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frappe.whitelist() | @frappe.whitelist() | ||||||
| def cancel_eway_bill(doctype, docname): | def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): | ||||||
| 	# TODO: uncomment when eway_bill api from Adequare is enabled | 	gsp_connector = GSPConnector(doctype, docname) | ||||||
| 	# gsp_connector = GSPConnector(doctype, docname) | 	gsp_connector.cancel_eway_bill(eway_bill, reason, remark) | ||||||
| 	# gsp_connector.cancel_eway_bill(eway_bill, reason, remark) |  | ||||||
| 
 |  | ||||||
| 	frappe.db.set_value(doctype, docname, "ewaybill", "") |  | ||||||
| 	frappe.db.set_value(doctype, docname, "eway_bill_cancelled", 1) |  | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
| @frappe.whitelist() | @frappe.whitelist() | ||||||
|  | |||||||
| @ -32,7 +32,7 @@ def _execute(filters=None): | |||||||
| 	added_item = [] | 	added_item = [] | ||||||
| 	for d in item_list: | 	for d in item_list: | ||||||
| 		if (d.parent, d.item_code) not in added_item: | 		if (d.parent, d.item_code) not in added_item: | ||||||
| 			row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty] | 			row = [d.gst_hsn_code, d.description, d.stock_uom, d.stock_qty, d.tax_rate] | ||||||
| 			total_tax = 0 | 			total_tax = 0 | ||||||
| 			for tax in tax_columns: | 			for tax in tax_columns: | ||||||
| 				item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) | 				item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) | ||||||
| @ -40,11 +40,9 @@ def _execute(filters=None): | |||||||
| 
 | 
 | ||||||
| 			row += [d.base_net_amount + total_tax] | 			row += [d.base_net_amount + total_tax] | ||||||
| 			row += [d.base_net_amount] | 			row += [d.base_net_amount] | ||||||
| 
 |  | ||||||
| 			for tax in tax_columns: | 			for tax in tax_columns: | ||||||
| 				item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) | 				item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) | ||||||
| 				row += [item_tax.get("tax_amount", 0)] | 				row += [item_tax.get("tax_amount", 0)] | ||||||
| 
 |  | ||||||
| 			data.append(row) | 			data.append(row) | ||||||
| 			added_item.append((d.parent, d.item_code)) | 			added_item.append((d.parent, d.item_code)) | ||||||
| 	if data: | 	if data: | ||||||
| @ -64,6 +62,7 @@ def get_columns(): | |||||||
| 		{"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300}, | 		{"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300}, | ||||||
| 		{"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100}, | 		{"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100}, | ||||||
| 		{"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90}, | 		{"fieldname": "stock_qty", "label": _("Stock Qty"), "fieldtype": "Float", "width": 90}, | ||||||
|  | 		{"fieldname": "tax_rate", "label": _("Tax Rate"), "fieldtype": "Data", "width": 90}, | ||||||
| 		{"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120}, | 		{"fieldname": "total_amount", "label": _("Total Amount"), "fieldtype": "Currency", "width": 120}, | ||||||
| 		{ | 		{ | ||||||
| 			"fieldname": "taxable_amount", | 			"fieldname": "taxable_amount", | ||||||
| @ -106,16 +105,25 @@ def get_items(filters): | |||||||
| 			sum(`tabSales Invoice Item`.stock_qty) as stock_qty, | 			sum(`tabSales Invoice Item`.stock_qty) as stock_qty, | ||||||
| 			sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount, | 			sum(`tabSales Invoice Item`.base_net_amount) as base_net_amount, | ||||||
| 			sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate, | 			sum(`tabSales Invoice Item`.base_price_list_rate) as base_price_list_rate, | ||||||
| 			`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code, | 			`tabSales Invoice Item`.parent, | ||||||
| 			`tabGST HSN Code`.description | 			`tabSales Invoice Item`.item_code, | ||||||
| 		from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code` | 			`tabGST HSN Code`.description, | ||||||
| 		where `tabSales Invoice`.name = `tabSales Invoice Item`.parent | 			json_extract(`tabSales Taxes and Charges`.item_wise_tax_detail, | ||||||
|  | 			concat('$."' , `tabSales Invoice Item`.item_code, '"[0]')) * count(distinct `tabSales Taxes and Charges`.name) as tax_rate | ||||||
|  | 		from | ||||||
|  | 			`tabSales Invoice`, | ||||||
|  | 			`tabSales Invoice Item`, | ||||||
|  | 			`tabGST HSN Code`, | ||||||
|  | 			`tabSales Taxes and Charges` | ||||||
|  | 		where | ||||||
|  | 			`tabSales Invoice`.name = `tabSales Invoice Item`.parent | ||||||
|  | 			and `tabSales Taxes and Charges`.parent = `tabSales Invoice`.name | ||||||
| 			and `tabSales Invoice`.docstatus = 1 | 			and `tabSales Invoice`.docstatus = 1 | ||||||
| 			and `tabSales Invoice Item`.gst_hsn_code is not NULL | 			and `tabSales Invoice Item`.gst_hsn_code is not NULL | ||||||
| 			and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s | 			and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s | ||||||
| 		group by | 		group by | ||||||
| 			`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code | 			`tabSales Invoice Item`.parent, | ||||||
| 
 | 			`tabSales Invoice Item`.item_code | ||||||
| 		""" | 		""" | ||||||
| 		% (conditions, match_conditions), | 		% (conditions, match_conditions), | ||||||
| 		filters, | 		filters, | ||||||
| @ -213,15 +221,16 @@ def get_merged_data(columns, data): | |||||||
| 	result = [] | 	result = [] | ||||||
| 
 | 
 | ||||||
| 	for row in data: | 	for row in data: | ||||||
| 		merged_hsn_dict.setdefault(row[0], {}) | 		key = row[0] + "-" + str(row[4]) | ||||||
|  | 		merged_hsn_dict.setdefault(key, {}) | ||||||
| 		for i, d in enumerate(columns): | 		for i, d in enumerate(columns): | ||||||
| 			if d["fieldtype"] not in ("Int", "Float", "Currency"): | 			if d["fieldtype"] not in ("Int", "Float", "Currency"): | ||||||
| 				merged_hsn_dict[row[0]][d["fieldname"]] = row[i] | 				merged_hsn_dict[key][d["fieldname"]] = row[i] | ||||||
| 			else: | 			else: | ||||||
| 				if merged_hsn_dict.get(row[0], {}).get(d["fieldname"], ""): | 				if merged_hsn_dict.get(key, {}).get(d["fieldname"], ""): | ||||||
| 					merged_hsn_dict[row[0]][d["fieldname"]] += row[i] | 					merged_hsn_dict[key][d["fieldname"]] += row[i] | ||||||
| 				else: | 				else: | ||||||
| 					merged_hsn_dict[row[0]][d["fieldname"]] = row[i] | 					merged_hsn_dict[key][d["fieldname"]] = row[i] | ||||||
| 
 | 
 | ||||||
| 	for key, value in merged_hsn_dict.items(): | 	for key, value in merged_hsn_dict.items(): | ||||||
| 		result.append(value) | 		result.append(value) | ||||||
| @ -240,7 +249,7 @@ def get_json(filters, report_name, data): | |||||||
| 
 | 
 | ||||||
| 	fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) | 	fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) | ||||||
| 
 | 
 | ||||||
| 	gst_json = {"version": "GST2.3.4", "hash": "hash", "gstin": gstin, "fp": fp} | 	gst_json = {"version": "GST3.0.3", "hash": "hash", "gstin": gstin, "fp": fp} | ||||||
| 
 | 
 | ||||||
| 	gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)} | 	gst_json["hsn"] = {"data": get_hsn_wise_json_data(filters, report_data)} | ||||||
| 
 | 
 | ||||||
| @ -271,7 +280,7 @@ def get_hsn_wise_json_data(filters, report_data): | |||||||
| 			"desc": hsn.get("description"), | 			"desc": hsn.get("description"), | ||||||
| 			"uqc": hsn.get("stock_uom").upper(), | 			"uqc": hsn.get("stock_uom").upper(), | ||||||
| 			"qty": hsn.get("stock_qty"), | 			"qty": hsn.get("stock_qty"), | ||||||
| 			"val": flt(hsn.get("total_amount"), 2), | 			"rt": flt(hsn.get("tax_rate"), 2), | ||||||
| 			"txval": flt(hsn.get("taxable_amount", 2)), | 			"txval": flt(hsn.get("taxable_amount", 2)), | ||||||
| 			"iamt": 0.0, | 			"iamt": 0.0, | ||||||
| 			"camt": 0.0, | 			"camt": 0.0, | ||||||
|  | |||||||
| @ -479,16 +479,20 @@ erpnext.PointOfSale.Controller = class { | |||||||
| 		frappe.dom.freeze(); | 		frappe.dom.freeze(); | ||||||
| 		this.frm = this.get_new_frm(this.frm); | 		this.frm = this.get_new_frm(this.frm); | ||||||
| 		this.frm.doc.items = []; | 		this.frm.doc.items = []; | ||||||
| 		const res = await frappe.call({ | 		return frappe.call({ | ||||||
| 			method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return", | 			method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return", | ||||||
| 			args: { | 			args: { | ||||||
| 				'source_name': doc.name, | 				'source_name': doc.name, | ||||||
| 				'target_doc': this.frm.doc | 				'target_doc': this.frm.doc | ||||||
|  | 			}, | ||||||
|  | 			callback: (r) => { | ||||||
|  | 				frappe.model.sync(r.message); | ||||||
|  | 				frappe.get_doc(r.message.doctype, r.message.name).__run_link_triggers = false; | ||||||
|  | 				this.set_pos_profile_data().then(() => { | ||||||
|  | 					frappe.dom.unfreeze(); | ||||||
|  | 				}); | ||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
| 		frappe.model.sync(res.message); |  | ||||||
| 		await this.set_pos_profile_data(); |  | ||||||
| 		frappe.dom.unfreeze(); |  | ||||||
| 	} | 	} | ||||||
| 
 | 
 | ||||||
| 	set_pos_profile_data() { | 	set_pos_profile_data() { | ||||||
|  | |||||||
| @ -238,4 +238,5 @@ def get_chart_data(data): | |||||||
| 			"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}], | 			"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}], | ||||||
| 		}, | 		}, | ||||||
| 		"type": "bar", | 		"type": "bar", | ||||||
|  | 		"fieldtype": "Currency", | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -54,4 +54,5 @@ def get_chart_data(data, conditions, filters): | |||||||
| 		}, | 		}, | ||||||
| 		"type": "line", | 		"type": "line", | ||||||
| 		"lineOptions": {"regionFill": 1}, | 		"lineOptions": {"regionFill": 1}, | ||||||
|  | 		"fieldtype": "Currency", | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -415,3 +415,8 @@ class Analytics(object): | |||||||
| 		else: | 		else: | ||||||
| 			labels = [d.get("label") for d in self.columns[1 : length - 1]] | 			labels = [d.get("label") for d in self.columns[1 : length - 1]] | ||||||
| 		self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"} | 		self.chart = {"data": {"labels": labels, "datasets": []}, "type": "line"} | ||||||
|  | 
 | ||||||
|  | 		if self.filters["value_quantity"] == "Value": | ||||||
|  | 			self.chart["fieldtype"] = "Currency" | ||||||
|  | 		else: | ||||||
|  | 			self.chart["fieldtype"] = "Float" | ||||||
|  | |||||||
| @ -51,4 +51,5 @@ def get_chart_data(data, conditions, filters): | |||||||
| 		}, | 		}, | ||||||
| 		"type": "line", | 		"type": "line", | ||||||
| 		"lineOptions": {"regionFill": 1}, | 		"lineOptions": {"regionFill": 1}, | ||||||
|  | 		"fieldtype": "Currency", | ||||||
| 	} | 	} | ||||||
|  | |||||||
| @ -16,6 +16,9 @@ from erpnext.manufacturing.doctype.production_plan.test_production_plan import m | |||||||
| from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record | from erpnext.manufacturing.doctype.work_order.test_work_order import make_wo_order_test_record | ||||||
| from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry | from erpnext.manufacturing.doctype.work_order.work_order import make_stock_entry | ||||||
| from erpnext.stock.doctype.item.test_item import create_item | from erpnext.stock.doctype.item.test_item import create_item | ||||||
|  | from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import ( | ||||||
|  | 	EmptyStockReconciliationItemsError, | ||||||
|  | ) | ||||||
| from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( | from erpnext.stock.doctype.stock_reconciliation.test_stock_reconciliation import ( | ||||||
| 	create_stock_reconciliation, | 	create_stock_reconciliation, | ||||||
| ) | ) | ||||||
| @ -180,9 +183,12 @@ def make_items(): | |||||||
| 		if not frappe.db.exists("Item", item_code): | 		if not frappe.db.exists("Item", item_code): | ||||||
| 			create_item(item_code) | 			create_item(item_code) | ||||||
| 
 | 
 | ||||||
| 	create_stock_reconciliation( | 	try: | ||||||
| 		item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000 | 		create_stock_reconciliation( | ||||||
| 	) | 			item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000 | ||||||
|  | 		) | ||||||
|  | 	except EmptyStockReconciliationItemsError: | ||||||
|  | 		pass | ||||||
| 
 | 
 | ||||||
| 	if frappe.db.exists("Item", "Test FG A RW 1"): | 	if frappe.db.exists("Item", "Test FG A RW 1"): | ||||||
| 		doc = frappe.get_doc("Item", "Test FG A RW 1") | 		doc = frappe.get_doc("Item", "Test FG A RW 1") | ||||||
|  | |||||||
| @ -652,6 +652,104 @@ class TestStockEntry(FrappeTestCase): | |||||||
| 		serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] | 		serial_no = get_serial_nos(se.get("items")[0].serial_no)[0] | ||||||
| 		self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse")) | 		self.assertFalse(frappe.db.get_value("Serial No", serial_no, "warehouse")) | ||||||
| 
 | 
 | ||||||
|  | 	def test_serial_batch_item_stock_entry(self): | ||||||
|  | 		""" | ||||||
|  | 		Behaviour: 1) Submit Stock Entry (Receipt) with Serial & Batched Item | ||||||
|  | 		2) Cancel same Stock Entry | ||||||
|  | 		Expected Result: 1) Batch is created with Reference in Serial No | ||||||
|  | 		2) Batch is deleted and Serial No is Inactive | ||||||
|  | 		""" | ||||||
|  | 		from erpnext.stock.doctype.batch.batch import get_batch_qty | ||||||
|  | 
 | ||||||
|  | 		item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) | ||||||
|  | 		if not item: | ||||||
|  | 			item = create_item("Batched and Serialised Item") | ||||||
|  | 			item.has_batch_no = 1 | ||||||
|  | 			item.create_new_batch = 1 | ||||||
|  | 			item.has_serial_no = 1 | ||||||
|  | 			item.batch_number_series = "B-BATCH-.##" | ||||||
|  | 			item.serial_no_series = "S-.####" | ||||||
|  | 			item.save() | ||||||
|  | 		else: | ||||||
|  | 			item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) | ||||||
|  | 
 | ||||||
|  | 		se = make_stock_entry( | ||||||
|  | 			item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100 | ||||||
|  | 		) | ||||||
|  | 		batch_no = se.items[0].batch_no | ||||||
|  | 		serial_no = get_serial_nos(se.items[0].serial_no)[0] | ||||||
|  | 		batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) | ||||||
|  | 
 | ||||||
|  | 		batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no") | ||||||
|  | 		self.assertEqual(batch_in_serial_no, batch_no) | ||||||
|  | 
 | ||||||
|  | 		self.assertEqual(batch_qty, 1) | ||||||
|  | 
 | ||||||
|  | 		se.cancel() | ||||||
|  | 
 | ||||||
|  | 		batch_in_serial_no = frappe.db.get_value("Serial No", serial_no, "batch_no") | ||||||
|  | 		self.assertEqual(batch_in_serial_no, None) | ||||||
|  | 
 | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", serial_no, "status"), "Inactive") | ||||||
|  | 		self.assertEqual(frappe.db.exists("Batch", batch_no), None) | ||||||
|  | 
 | ||||||
|  | 	def test_serial_batch_item_qty_deduction(self): | ||||||
|  | 		""" | ||||||
|  | 		Behaviour: Create 2 Stock Entries, both adding Serial Nos to same batch | ||||||
|  | 		Expected: 1) Cancelling first Stock Entry (origin transaction of created batch) | ||||||
|  | 		should throw a LinkExistsError | ||||||
|  | 		2) Cancelling second Stock Entry should make Serial Nos that are, linked to mentioned batch | ||||||
|  | 		and in that transaction only, Inactive. | ||||||
|  | 		""" | ||||||
|  | 		from erpnext.stock.doctype.batch.batch import get_batch_qty | ||||||
|  | 
 | ||||||
|  | 		item = frappe.db.exists("Item", {"item_name": "Batched and Serialised Item"}) | ||||||
|  | 		if not item: | ||||||
|  | 			item = create_item("Batched and Serialised Item") | ||||||
|  | 			item.has_batch_no = 1 | ||||||
|  | 			item.create_new_batch = 1 | ||||||
|  | 			item.has_serial_no = 1 | ||||||
|  | 			item.batch_number_series = "B-BATCH-.##" | ||||||
|  | 			item.serial_no_series = "S-.####" | ||||||
|  | 			item.save() | ||||||
|  | 		else: | ||||||
|  | 			item = frappe.get_doc("Item", {"item_name": "Batched and Serialised Item"}) | ||||||
|  | 
 | ||||||
|  | 		se1 = make_stock_entry( | ||||||
|  | 			item_code=item.item_code, target="_Test Warehouse - _TC", qty=1, basic_rate=100 | ||||||
|  | 		) | ||||||
|  | 		batch_no = se1.items[0].batch_no | ||||||
|  | 		serial_no1 = get_serial_nos(se1.items[0].serial_no)[0] | ||||||
|  | 
 | ||||||
|  | 		# Check Source (Origin) Document of Batch | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Batch", batch_no, "reference_name"), se1.name) | ||||||
|  | 
 | ||||||
|  | 		se2 = make_stock_entry( | ||||||
|  | 			item_code=item.item_code, | ||||||
|  | 			target="_Test Warehouse - _TC", | ||||||
|  | 			qty=1, | ||||||
|  | 			basic_rate=100, | ||||||
|  | 			batch_no=batch_no, | ||||||
|  | 		) | ||||||
|  | 		serial_no2 = get_serial_nos(se2.items[0].serial_no)[0] | ||||||
|  | 
 | ||||||
|  | 		batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) | ||||||
|  | 		self.assertEqual(batch_qty, 2) | ||||||
|  | 
 | ||||||
|  | 		se2.cancel() | ||||||
|  | 
 | ||||||
|  | 		# Check decrease in Batch Qty | ||||||
|  | 		batch_qty = get_batch_qty(batch_no, "_Test Warehouse - _TC", item.item_code) | ||||||
|  | 		self.assertEqual(batch_qty, 1) | ||||||
|  | 
 | ||||||
|  | 		# Check if Serial No from Stock Entry 1 is intact | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "batch_no"), batch_no) | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", serial_no1, "status"), "Active") | ||||||
|  | 
 | ||||||
|  | 		# Check if Serial No from Stock Entry 2 is Unlinked and Inactive | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "batch_no"), None) | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", serial_no2, "status"), "Inactive") | ||||||
|  | 
 | ||||||
| 	def test_warehouse_company_validation(self): | 	def test_warehouse_company_validation(self): | ||||||
| 		company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company") | 		company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company") | ||||||
| 		frappe.get_doc("User", "test2@example.com").add_roles( | 		frappe.get_doc("User", "test2@example.com").add_roles( | ||||||
|  | |||||||
| @ -1183,6 +1183,42 @@ class TestStockLedgerEntry(FrappeTestCase): | |||||||
| 		backdated.cancel() | 		backdated.cancel() | ||||||
| 		self.assertEqual([1], ordered_qty_after_transaction()) | 		self.assertEqual([1], ordered_qty_after_transaction()) | ||||||
| 
 | 
 | ||||||
|  | 	def test_timestamp_clash(self): | ||||||
|  | 
 | ||||||
|  | 		item = make_item().name | ||||||
|  | 		warehouse = "_Test Warehouse - _TC" | ||||||
|  | 
 | ||||||
|  | 		reciept = make_stock_entry( | ||||||
|  | 			item_code=item, | ||||||
|  | 			to_warehouse=warehouse, | ||||||
|  | 			qty=100, | ||||||
|  | 			rate=10, | ||||||
|  | 			posting_date="2021-01-01", | ||||||
|  | 			posting_time="01:00:00", | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | 		consumption = make_stock_entry( | ||||||
|  | 			item_code=item, | ||||||
|  | 			from_warehouse=warehouse, | ||||||
|  | 			qty=50, | ||||||
|  | 			posting_date="2021-01-01", | ||||||
|  | 			posting_time="02:00:00.1234",  # ms are possible when submitted without editing posting time | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | 		backdated_receipt = make_stock_entry( | ||||||
|  | 			item_code=item, | ||||||
|  | 			to_warehouse=warehouse, | ||||||
|  | 			qty=100, | ||||||
|  | 			posting_date="2021-01-01", | ||||||
|  | 			rate=10, | ||||||
|  | 			posting_time="02:00:00",  # same posting time as consumption but ms part stripped | ||||||
|  | 		) | ||||||
|  | 
 | ||||||
|  | 		try: | ||||||
|  | 			backdated_receipt.cancel() | ||||||
|  | 		except Exception as e: | ||||||
|  | 			self.fail("Double processing of qty for clashing timestamp.") | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
| def create_repack_entry(**args): | def create_repack_entry(**args): | ||||||
| 	args = frappe._dict(args) | 	args = frappe._dict(args) | ||||||
|  | |||||||
| @ -62,6 +62,7 @@ class StockReconciliation(StockController): | |||||||
| 		self.make_sle_on_cancel() | 		self.make_sle_on_cancel() | ||||||
| 		self.make_gl_entries_on_cancel() | 		self.make_gl_entries_on_cancel() | ||||||
| 		self.repost_future_sle_and_gle() | 		self.repost_future_sle_and_gle() | ||||||
|  | 		self.delete_auto_created_batches() | ||||||
| 
 | 
 | ||||||
| 	def remove_items_with_no_change(self): | 	def remove_items_with_no_change(self): | ||||||
| 		"""Remove items if qty or rate is not changed""" | 		"""Remove items if qty or rate is not changed""" | ||||||
| @ -456,7 +457,7 @@ class StockReconciliation(StockController): | |||||||
| 
 | 
 | ||||||
| 			key = (d.item_code, d.warehouse) | 			key = (d.item_code, d.warehouse) | ||||||
| 			if key not in merge_similar_entries: | 			if key not in merge_similar_entries: | ||||||
| 				d.total_amount = d.actual_qty * d.valuation_rate | 				d.total_amount = flt(d.actual_qty) * d.valuation_rate | ||||||
| 				merge_similar_entries[key] = d | 				merge_similar_entries[key] = d | ||||||
| 			elif d.serial_no: | 			elif d.serial_no: | ||||||
| 				data = merge_similar_entries[key] | 				data = merge_similar_entries[key] | ||||||
|  | |||||||
| @ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase): | |||||||
| 
 | 
 | ||||||
| 	def tearDown(self): | 	def tearDown(self): | ||||||
| 		frappe.local.future_sle = {} | 		frappe.local.future_sle = {} | ||||||
|  | 		frappe.flags.pop("dont_execute_stock_reposts", None) | ||||||
| 
 | 
 | ||||||
| 	def test_reco_for_fifo(self): | 	def test_reco_for_fifo(self): | ||||||
| 		self._test_reco_sle_gle("FIFO") | 		self._test_reco_sle_gle("FIFO") | ||||||
| @ -250,7 +251,7 @@ class TestStockReconciliation(FrappeTestCase): | |||||||
| 		warehouse = "_Test Warehouse for Stock Reco2 - _TC" | 		warehouse = "_Test Warehouse for Stock Reco2 - _TC" | ||||||
| 
 | 
 | ||||||
| 		sr = create_stock_reconciliation( | 		sr = create_stock_reconciliation( | ||||||
| 			item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_submit=1 | 			item_code=item_code, warehouse=warehouse, qty=5, rate=200, do_not_save=1 | ||||||
| 		) | 		) | ||||||
| 		sr.save() | 		sr.save() | ||||||
| 		sr.submit() | 		sr.submit() | ||||||
| @ -288,6 +289,84 @@ class TestStockReconciliation(FrappeTestCase): | |||||||
| 			stock_doc = frappe.get_doc("Stock Reconciliation", d) | 			stock_doc = frappe.get_doc("Stock Reconciliation", d) | ||||||
| 			stock_doc.cancel() | 			stock_doc.cancel() | ||||||
| 
 | 
 | ||||||
|  | 	def test_stock_reco_for_serial_and_batch_item(self): | ||||||
|  | 		item = create_item("_TestBatchSerialItemReco") | ||||||
|  | 		item.has_batch_no = 1 | ||||||
|  | 		item.create_new_batch = 1 | ||||||
|  | 		item.has_serial_no = 1 | ||||||
|  | 		item.batch_number_series = "TBS-BATCH-.##" | ||||||
|  | 		item.serial_no_series = "TBS-.####" | ||||||
|  | 		item.save() | ||||||
|  | 
 | ||||||
|  | 		warehouse = "_Test Warehouse for Stock Reco2 - _TC" | ||||||
|  | 
 | ||||||
|  | 		sr = create_stock_reconciliation(item_code=item.item_code, warehouse=warehouse, qty=1, rate=100) | ||||||
|  | 
 | ||||||
|  | 		batch_no = sr.items[0].batch_no | ||||||
|  | 
 | ||||||
|  | 		serial_nos = get_serial_nos(sr.items[0].serial_no) | ||||||
|  | 		self.assertEqual(len(serial_nos), 1) | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "batch_no"), batch_no) | ||||||
|  | 
 | ||||||
|  | 		sr.cancel() | ||||||
|  | 
 | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", serial_nos[0], "status"), "Inactive") | ||||||
|  | 		self.assertEqual(frappe.db.exists("Batch", batch_no), None) | ||||||
|  | 
 | ||||||
|  | 	def test_stock_reco_for_serial_and_batch_item_with_future_dependent_entry(self): | ||||||
|  | 		""" | ||||||
|  | 		Behaviour: 1) Create Stock Reconciliation, which will be the origin document | ||||||
|  | 		of a new batch having a serial no | ||||||
|  | 		2) Create a Stock Entry that adds a serial no to the same batch following this | ||||||
|  | 		Stock Reconciliation | ||||||
|  | 		3) Cancel Stock Entry | ||||||
|  | 		Expected Result: 3) Serial No only in the Stock Entry is Inactive and Batch qty decreases | ||||||
|  | 		""" | ||||||
|  | 		from erpnext.stock.doctype.batch.batch import get_batch_qty | ||||||
|  | 		from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry | ||||||
|  | 
 | ||||||
|  | 		item = create_item("_TestBatchSerialItemDependentReco") | ||||||
|  | 		item.has_batch_no = 1 | ||||||
|  | 		item.create_new_batch = 1 | ||||||
|  | 		item.has_serial_no = 1 | ||||||
|  | 		item.batch_number_series = "TBSD-BATCH-.##" | ||||||
|  | 		item.serial_no_series = "TBSD-.####" | ||||||
|  | 		item.save() | ||||||
|  | 
 | ||||||
|  | 		warehouse = "_Test Warehouse for Stock Reco2 - _TC" | ||||||
|  | 
 | ||||||
|  | 		stock_reco = create_stock_reconciliation( | ||||||
|  | 			item_code=item.item_code, warehouse=warehouse, qty=1, rate=100 | ||||||
|  | 		) | ||||||
|  | 		batch_no = stock_reco.items[0].batch_no | ||||||
|  | 		reco_serial_no = get_serial_nos(stock_reco.items[0].serial_no)[0] | ||||||
|  | 
 | ||||||
|  | 		stock_entry = make_stock_entry( | ||||||
|  | 			item_code=item.item_code, target=warehouse, qty=1, basic_rate=100, batch_no=batch_no | ||||||
|  | 		) | ||||||
|  | 		serial_no_2 = get_serial_nos(stock_entry.items[0].serial_no)[0] | ||||||
|  | 
 | ||||||
|  | 		# Check Batch qty after 2 transactions | ||||||
|  | 		batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) | ||||||
|  | 		self.assertEqual(batch_qty, 2) | ||||||
|  | 
 | ||||||
|  | 		# Cancel latest stock document | ||||||
|  | 		stock_entry.cancel() | ||||||
|  | 
 | ||||||
|  | 		# Check Batch qty after cancellation | ||||||
|  | 		batch_qty = get_batch_qty(batch_no, warehouse, item.item_code) | ||||||
|  | 		self.assertEqual(batch_qty, 1) | ||||||
|  | 
 | ||||||
|  | 		# Check if Serial No from Stock Reconcilation is intact | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "batch_no"), batch_no) | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", reco_serial_no, "status"), "Active") | ||||||
|  | 
 | ||||||
|  | 		# Check if Serial No from Stock Entry is Unlinked and Inactive | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "batch_no"), None) | ||||||
|  | 		self.assertEqual(frappe.db.get_value("Serial No", serial_no_2, "status"), "Inactive") | ||||||
|  | 
 | ||||||
|  | 		stock_reco.cancel() | ||||||
|  | 
 | ||||||
| 	def test_customer_provided_items(self): | 	def test_customer_provided_items(self): | ||||||
| 		item_code = "Stock-Reco-customer-Item-100" | 		item_code = "Stock-Reco-customer-Item-100" | ||||||
| 		create_item( | 		create_item( | ||||||
| @ -306,6 +385,7 @@ class TestStockReconciliation(FrappeTestCase): | |||||||
| 		------------------------------------------- | 		------------------------------------------- | ||||||
| 		Var		| Doc	|	Qty	| Balance | 		Var		| Doc	|	Qty	| Balance | ||||||
| 		------------------------------------------- | 		------------------------------------------- | ||||||
|  | 		PR5     | PR    |   10  |  10   (posting date: today-4) [backdated] | ||||||
| 		SR5		| Reco	|	0	|	8	(posting date: today-4) [backdated] | 		SR5		| Reco	|	0	|	8	(posting date: today-4) [backdated] | ||||||
| 		PR1		| PR	|	10	|	18	(posting date: today-3) | 		PR1		| PR	|	10	|	18	(posting date: today-3) | ||||||
| 		PR2		| PR	|	1	|	19	(posting date: today-2) | 		PR2		| PR	|	1	|	19	(posting date: today-2) | ||||||
| @ -315,6 +395,14 @@ class TestStockReconciliation(FrappeTestCase): | |||||||
| 		item_code = make_item().name | 		item_code = make_item().name | ||||||
| 		warehouse = "_Test Warehouse - _TC" | 		warehouse = "_Test Warehouse - _TC" | ||||||
| 
 | 
 | ||||||
|  | 		frappe.flags.dont_execute_stock_reposts = True | ||||||
|  | 
 | ||||||
|  | 		def assertBalance(doc, qty_after_transaction): | ||||||
|  | 			sle_balance = frappe.db.get_value( | ||||||
|  | 				"Stock Ledger Entry", {"voucher_no": doc.name, "is_cancelled": 0}, "qty_after_transaction" | ||||||
|  | 			) | ||||||
|  | 			self.assertEqual(sle_balance, qty_after_transaction) | ||||||
|  | 
 | ||||||
| 		pr1 = make_purchase_receipt( | 		pr1 = make_purchase_receipt( | ||||||
| 			item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) | 			item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -3) | ||||||
| 		) | 		) | ||||||
| @ -324,62 +412,37 @@ class TestStockReconciliation(FrappeTestCase): | |||||||
| 		pr3 = make_purchase_receipt( | 		pr3 = make_purchase_receipt( | ||||||
| 			item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate() | 			item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate() | ||||||
| 		) | 		) | ||||||
| 
 | 		assertBalance(pr1, 10) | ||||||
| 		pr1_balance = frappe.db.get_value( | 		assertBalance(pr3, 12) | ||||||
| 			"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" |  | ||||||
| 		) |  | ||||||
| 		pr3_balance = frappe.db.get_value( |  | ||||||
| 			"Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" |  | ||||||
| 		) |  | ||||||
| 		self.assertEqual(pr1_balance, 10) |  | ||||||
| 		self.assertEqual(pr3_balance, 12) |  | ||||||
| 
 | 
 | ||||||
| 		# post backdated stock reco in between | 		# post backdated stock reco in between | ||||||
| 		sr4 = create_stock_reconciliation( | 		sr4 = create_stock_reconciliation( | ||||||
| 			item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1) | 			item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1) | ||||||
| 		) | 		) | ||||||
| 		pr3_balance = frappe.db.get_value( | 		assertBalance(pr3, 7) | ||||||
| 			"Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" |  | ||||||
| 		) |  | ||||||
| 		self.assertEqual(pr3_balance, 7) |  | ||||||
| 
 | 
 | ||||||
| 		# post backdated stock reco at the start | 		# post backdated stock reco at the start | ||||||
| 		sr5 = create_stock_reconciliation( | 		sr5 = create_stock_reconciliation( | ||||||
| 			item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4) | 			item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4) | ||||||
| 		) | 		) | ||||||
| 		pr1_balance = frappe.db.get_value( | 		assertBalance(pr1, 18) | ||||||
| 			"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" | 		assertBalance(pr2, 19) | ||||||
|  | 		assertBalance(sr4, 6)  # check if future stock reco is unaffected | ||||||
|  | 
 | ||||||
|  | 		# Make a backdated receipt and check only entries till first SR are affected | ||||||
|  | 		pr5 = make_purchase_receipt( | ||||||
|  | 			item_code=item_code, warehouse=warehouse, qty=10, rate=100, posting_date=add_days(nowdate(), -5) | ||||||
| 		) | 		) | ||||||
| 		pr2_balance = frappe.db.get_value( | 		assertBalance(pr5, 10) | ||||||
| 			"Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction" | 		# check if future stock reco is unaffected | ||||||
| 		) | 		assertBalance(sr4, 6) | ||||||
| 		sr4_balance = frappe.db.get_value( | 		assertBalance(sr5, 8) | ||||||
| 			"Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction" |  | ||||||
| 		) |  | ||||||
| 		self.assertEqual(pr1_balance, 18) |  | ||||||
| 		self.assertEqual(pr2_balance, 19) |  | ||||||
| 		self.assertEqual(sr4_balance, 6)  # check if future stock reco is unaffected |  | ||||||
| 
 | 
 | ||||||
| 		# cancel backdated stock reco and check future impact | 		# cancel backdated stock reco and check future impact | ||||||
| 		sr5.cancel() | 		sr5.cancel() | ||||||
| 		pr1_balance = frappe.db.get_value( | 		assertBalance(pr1, 10) | ||||||
| 			"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" | 		assertBalance(pr2, 11) | ||||||
| 		) | 		assertBalance(sr4, 6)  # check if future stock reco is unaffected | ||||||
| 		pr2_balance = frappe.db.get_value( |  | ||||||
| 			"Stock Ledger Entry", {"voucher_no": pr2.name, "is_cancelled": 0}, "qty_after_transaction" |  | ||||||
| 		) |  | ||||||
| 		sr4_balance = frappe.db.get_value( |  | ||||||
| 			"Stock Ledger Entry", {"voucher_no": sr4.name, "is_cancelled": 0}, "qty_after_transaction" |  | ||||||
| 		) |  | ||||||
| 		self.assertEqual(pr1_balance, 10) |  | ||||||
| 		self.assertEqual(pr2_balance, 11) |  | ||||||
| 		self.assertEqual(sr4_balance, 6)  # check if future stock reco is unaffected |  | ||||||
| 
 |  | ||||||
| 		# teardown |  | ||||||
| 		sr4.cancel() |  | ||||||
| 		pr3.cancel() |  | ||||||
| 		pr2.cancel() |  | ||||||
| 		pr1.cancel() |  | ||||||
| 
 | 
 | ||||||
| 	@change_settings("Stock Settings", {"allow_negative_stock": 0}) | 	@change_settings("Stock Settings", {"allow_negative_stock": 0}) | ||||||
| 	def test_backdated_stock_reco_future_negative_stock(self): | 	def test_backdated_stock_reco_future_negative_stock(self): | ||||||
| @ -485,7 +548,6 @@ class TestStockReconciliation(FrappeTestCase): | |||||||
| 
 | 
 | ||||||
| 		# repost will make this test useless, qty should update in realtime without reposts | 		# repost will make this test useless, qty should update in realtime without reposts | ||||||
| 		frappe.flags.dont_execute_stock_reposts = True | 		frappe.flags.dont_execute_stock_reposts = True | ||||||
| 		self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts") |  | ||||||
| 
 | 
 | ||||||
| 		item_code = make_item().name | 		item_code = make_item().name | ||||||
| 		warehouse = "_Test Warehouse - _TC" | 		warehouse = "_Test Warehouse - _TC" | ||||||
| @ -684,11 +746,13 @@ def create_stock_reconciliation(**args): | |||||||
| 		}, | 		}, | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
| 	try: | 	if not args.do_not_save: | ||||||
| 		if not args.do_not_submit: | 		sr.insert() | ||||||
| 			sr.submit() | 		try: | ||||||
| 	except EmptyStockReconciliationItemsError: | 			if not args.do_not_submit: | ||||||
| 		pass | 				sr.submit() | ||||||
|  | 		except EmptyStockReconciliationItemsError: | ||||||
|  | 			pass | ||||||
| 	return sr | 	return sr | ||||||
| 
 | 
 | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -111,17 +111,17 @@ def get_columns(): | |||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"fieldname": "posting_date", | 			"fieldname": "posting_date", | ||||||
| 			"fieldtype": "Date", | 			"fieldtype": "Data", | ||||||
| 			"label": _("Posting Date"), | 			"label": _("Posting Date"), | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"fieldname": "posting_time", | 			"fieldname": "posting_time", | ||||||
| 			"fieldtype": "Time", | 			"fieldtype": "Data", | ||||||
| 			"label": _("Posting Time"), | 			"label": _("Posting Time"), | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
| 			"fieldname": "creation", | 			"fieldname": "creation", | ||||||
| 			"fieldtype": "Datetime", | 			"fieldtype": "Data", | ||||||
| 			"label": _("Creation"), | 			"label": _("Creation"), | ||||||
| 		}, | 		}, | ||||||
| 		{ | 		{ | ||||||
|  | |||||||
| @ -1303,6 +1303,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): | |||||||
| 	datetime_limit_condition = "" | 	datetime_limit_condition = "" | ||||||
| 	qty_shift = args.actual_qty | 	qty_shift = args.actual_qty | ||||||
| 
 | 
 | ||||||
|  | 	args["time_format"] = "%H:%i:%s" | ||||||
|  | 
 | ||||||
| 	# find difference/shift in qty caused by stock reconciliation | 	# find difference/shift in qty caused by stock reconciliation | ||||||
| 	if args.voucher_type == "Stock Reconciliation": | 	if args.voucher_type == "Stock Reconciliation": | ||||||
| 		qty_shift = get_stock_reco_qty_shift(args) | 		qty_shift = get_stock_reco_qty_shift(args) | ||||||
| @ -1315,7 +1317,7 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): | |||||||
| 		datetime_limit_condition = get_datetime_limit_condition(detail) | 		datetime_limit_condition = get_datetime_limit_condition(detail) | ||||||
| 
 | 
 | ||||||
| 	frappe.db.sql( | 	frappe.db.sql( | ||||||
| 		""" | 		f""" | ||||||
| 		update `tabStock Ledger Entry` | 		update `tabStock Ledger Entry` | ||||||
| 		set qty_after_transaction = qty_after_transaction + {qty_shift} | 		set qty_after_transaction = qty_after_transaction + {qty_shift} | ||||||
| 		where | 		where | ||||||
| @ -1323,16 +1325,10 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): | |||||||
| 			and warehouse = %(warehouse)s | 			and warehouse = %(warehouse)s | ||||||
| 			and voucher_no != %(voucher_no)s | 			and voucher_no != %(voucher_no)s | ||||||
| 			and is_cancelled = 0 | 			and is_cancelled = 0 | ||||||
| 			and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s) | 			and timestamp(posting_date, time_format(posting_time, %(time_format)s)) | ||||||
| 				or ( | 				> timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) | ||||||
| 					timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s) |  | ||||||
| 					and creation > %(creation)s |  | ||||||
| 				) |  | ||||||
| 			) |  | ||||||
| 		{datetime_limit_condition} | 		{datetime_limit_condition} | ||||||
| 		""".format( | 		""", | ||||||
| 			qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition |  | ||||||
| 		), |  | ||||||
| 		args, | 		args, | ||||||
| 	) | 	) | ||||||
| 
 | 
 | ||||||
| @ -1383,6 +1379,7 @@ def get_next_stock_reco(args): | |||||||
| 					and creation > %(creation)s | 					and creation > %(creation)s | ||||||
| 				) | 				) | ||||||
| 			) | 			) | ||||||
|  | 		order by timestamp(posting_date, posting_time) asc, creation asc | ||||||
| 		limit 1 | 		limit 1 | ||||||
| 	""", | 	""", | ||||||
| 		args, | 		args, | ||||||
|  | |||||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user