Merge branch 'develop' into de-translate-employee
This commit is contained in:
		
						commit
						195e8af985
					
				| @ -2648,6 +2648,7 @@ class TestSalesInvoice(unittest.TestCase): | ||||
| 		# reset | ||||
| 		einvoice_settings = frappe.get_doc("E Invoice Settings") | ||||
| 		einvoice_settings.enable = 0 | ||||
| 		einvoice_settings.save() | ||||
| 		frappe.flags.country = country | ||||
| 
 | ||||
| 	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): | ||||
| 	"""Get matching tax rule""" | ||||
| 	args = frappe._dict(args) | ||||
| 	from_date = to_date = posting_date | ||||
| 	if not posting_date: | ||||
| 		from_date = "1900-01-01" | ||||
| 		to_date = "4000-01-01" | ||||
| 	conditions = [] | ||||
| 
 | ||||
| 	conditions = [ | ||||
| 		"""(from_date is null or from_date <= '{0}') | ||||
| 		and (to_date is null or to_date >= '{1}')""".format( | ||||
| 			from_date, to_date | ||||
| 	if posting_date: | ||||
| 		conditions.append( | ||||
| 			f"""(from_date is null or from_date <= '{posting_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( | ||||
| 		"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 | ||||
| 			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} | ||||
| 		FROM | ||||
| 			`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.model.mapper import get_mapped_doc | ||||
| 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.setup.utils import get_exchange_rate | ||||
| @ -215,20 +215,20 @@ class Opportunity(TransactionBase): | ||||
| 
 | ||||
| 			if self.party_name and self.opportunity_from == "Customer": | ||||
| 				if self.contact_person: | ||||
| 					opts.description = "Contact " + cstr(self.contact_person) | ||||
| 					opts.description = f"Contact {self.contact_person}" | ||||
| 				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": | ||||
| 				if self.contact_display: | ||||
| 					opts.description = "Contact " + cstr(self.contact_display) | ||||
| 					opts.description = f"Contact {self.contact_display}" | ||||
| 				else: | ||||
| 					opts.description = "Contact lead " + cstr(self.party_name) | ||||
| 					opts.description = f"Contact lead {self.party_name}" | ||||
| 
 | ||||
| 			opts.subject = opts.description | ||||
| 			opts.description += ". By : " + cstr(self.contact_by) | ||||
| 			opts.description += f". By : {self.contact_by}" | ||||
| 
 | ||||
| 			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) | ||||
| 
 | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| import unittest | ||||
| 
 | ||||
| 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.test_lead import make_lead | ||||
| @ -97,6 +97,22 @@ class TestOpportunity(unittest.TestCase): | ||||
| 		self.assertEqual(quotation_comment_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(): | ||||
| 	new_lead_email_id = "new{}@example.com".format(random_string(5)) | ||||
|  | ||||
| @ -139,7 +139,7 @@ class TestShoppingCart(unittest.TestCase): | ||||
| 		tax_rule_master = set_taxes( | ||||
| 			quotation.party_name, | ||||
| 			"Customer", | ||||
| 			quotation.transaction_date, | ||||
| 			None, | ||||
| 			quotation.company, | ||||
| 			customer_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" | ||||
| 
 | ||||
| 
 | ||||
| develop_version = "13.x.x-develop" | ||||
| develop_version = "14.x.x-develop" | ||||
| 
 | ||||
| app_include_js = "erpnext.bundle.js" | ||||
| 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) { | ||||
|  | ||||
| @ -254,7 +254,18 @@ class LeaveAllocation(Document): | ||||
| 		# Adding a day to include To Date in the difference | ||||
| 		date_difference = date_diff(self.to_date, self.from_date) + 1 | ||||
| 		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): | ||||
| 		if self.unused_leaves: | ||||
|  | ||||
| @ -69,22 +69,44 @@ class TestLeaveAllocation(FrappeTestCase): | ||||
| 		self.assertRaises(frappe.ValidationError, doc.save) | ||||
| 
 | ||||
| 	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( | ||||
| 			{ | ||||
| 				"doctype": "Leave Allocation", | ||||
| 				"__islocal": 1, | ||||
| 				"employee": self.employee.name, | ||||
| 				"employee_name": self.employee.employee_name, | ||||
| 				"leave_type": "_Test Leave Type", | ||||
| 				"leave_type": leave_type.name, | ||||
| 				"from_date": getdate("2015-09-1"), | ||||
| 				"to_date": getdate("2015-09-30"), | ||||
| 				"new_leaves_allocated": 35, | ||||
| 				"carry_forward": 1, | ||||
| 			} | ||||
| 		) | ||||
| 
 | ||||
| 		# allocated leave more than period | ||||
| 		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): | ||||
| 		allocation = frappe.get_doc( | ||||
| 			{ | ||||
|  | ||||
| @ -745,7 +745,7 @@ class TestLeaveApplication(unittest.TestCase): | ||||
| 
 | ||||
| 		i = 0 | ||||
| 		while i < 14: | ||||
| 			allocate_earned_leaves(ignore_duplicates=True) | ||||
| 			allocate_earned_leaves() | ||||
| 			i += 1 | ||||
| 		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) | ||||
| 		i = 0 | ||||
| 		while i < 6: | ||||
| 			allocate_earned_leaves(ignore_duplicates=True) | ||||
| 			allocate_earned_leaves() | ||||
| 			i += 1 | ||||
| 		self.assertEqual(get_leave_balance_on(employee.name, leave_type, nowdate()), 9) | ||||
| 
 | ||||
|  | ||||
| @ -4,6 +4,7 @@ | ||||
| import unittest | ||||
| 
 | ||||
| import frappe | ||||
| from frappe.tests.utils import FrappeTestCase | ||||
| 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 ( | ||||
| @ -18,7 +19,7 @@ from erpnext.hr.doctype.leave_policy_assignment.leave_policy_assignment import ( | ||||
| test_dependencies = ["Employee"] | ||||
| 
 | ||||
| 
 | ||||
| class TestLeavePolicyAssignment(unittest.TestCase): | ||||
| class TestLeavePolicyAssignment(FrappeTestCase): | ||||
| 	def setUp(self): | ||||
| 		for doctype in [ | ||||
| 			"Leave Period", | ||||
| @ -39,6 +40,9 @@ class TestLeavePolicyAssignment(unittest.TestCase): | ||||
| 		leave_policy = create_leave_policy() | ||||
| 		leave_policy.submit() | ||||
| 
 | ||||
| 		self.employee.date_of_joining = get_first_day(leave_period.from_date) | ||||
| 		self.employee.save() | ||||
| 
 | ||||
| 		data = { | ||||
| 			"assignment_based_on": "Leave Period", | ||||
| 			"leave_policy": leave_policy.name, | ||||
| @ -188,19 +192,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): | ||||
| 		) | ||||
| 		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): | ||||
| 		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.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): | ||||
| 		# tests leave alloc for earned leaves for assignment based on joining date in policy assignment | ||||
| 		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(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): | ||||
| 		# 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( | ||||
| @ -330,20 +294,6 @@ class TestLeavePolicyAssignment(unittest.TestCase): | ||||
| 		) | ||||
| 		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): | ||||
| 		# 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) | ||||
| @ -377,21 +327,7 @@ class TestLeavePolicyAssignment(unittest.TestCase): | ||||
| 		self.assertEqual(effective_from, self.employee.date_of_joining) | ||||
| 		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): | ||||
| 		frappe.db.rollback() | ||||
| 		frappe.db.set_value("Employee", self.employee.name, "date_of_joining", self.original_doj) | ||||
| 		frappe.flags.current_date = None | ||||
| 
 | ||||
|  | ||||
| @ -19,6 +19,7 @@ | ||||
|   "fraction_of_daily_salary_per_leave", | ||||
|   "is_optional_leave", | ||||
|   "allow_negative", | ||||
|   "allow_over_allocation", | ||||
|   "include_holiday", | ||||
|   "is_compensatory", | ||||
|   "carry_forward_section", | ||||
| @ -211,15 +212,23 @@ | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Fraction of Daily Salary per Leave", | ||||
|    "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", | ||||
|  "idx": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-10-02 11:59:40.503359", | ||||
|  "modified": "2022-05-09 05:01:38.957545", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "HR", | ||||
|  "name": "Leave Type", | ||||
|  "naming_rule": "By fieldname", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
| @ -251,5 +260,6 @@ | ||||
|  ], | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "states": [], | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -269,7 +269,7 @@ def generate_leave_encashment(): | ||||
| 		create_leave_encashment(leave_allocation=leave_allocation) | ||||
| 
 | ||||
| 
 | ||||
| def allocate_earned_leaves(ignore_duplicates=False): | ||||
| def allocate_earned_leaves(): | ||||
| 	"""Allocate earned leaves to Employees""" | ||||
| 	e_leave_types = get_earned_leaves() | ||||
| 	today = getdate() | ||||
| @ -305,14 +305,10 @@ def allocate_earned_leaves(ignore_duplicates=False): | ||||
| 			if check_effective_date( | ||||
| 				from_date, today, e_leave_type.earned_leave_frequency, e_leave_type.based_on_date_of_joining | ||||
| 			): | ||||
| 				update_previous_leave_allocation( | ||||
| 					allocation, annual_allocation, e_leave_type, ignore_duplicates | ||||
| 				) | ||||
| 				update_previous_leave_allocation(allocation, annual_allocation, e_leave_type) | ||||
| 
 | ||||
| 
 | ||||
| def update_previous_leave_allocation( | ||||
| 	allocation, annual_allocation, e_leave_type, ignore_duplicates=False | ||||
| ): | ||||
| def update_previous_leave_allocation(allocation, annual_allocation, e_leave_type): | ||||
| 	earned_leaves = get_monthly_earned_leave( | ||||
| 		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: | ||||
| 		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) | ||||
| 			create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) | ||||
| 		allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) | ||||
| 		create_additional_leave_ledger_entry(allocation, earned_leaves, today_date) | ||||
| 
 | ||||
| 			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( | ||||
| 					frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) | ||||
| 				) | ||||
| 			else: | ||||
| 				text = _("allocated {0} leave(s) via scheduler on {1}").format( | ||||
| 					frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) | ||||
| 				) | ||||
| 		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( | ||||
| 				frappe.bold(earned_leaves), frappe.bold(formatdate(today_date)) | ||||
| 			) | ||||
| 		else: | ||||
| 			text = _("allocated {0} leave(s) via scheduler on {1}").format( | ||||
| 				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): | ||||
|  | ||||
| @ -99,8 +99,21 @@ erpnext.setup_einvoice_actions = (doctype) => { | ||||
| 									...data | ||||
| 								}, | ||||
| 								freeze: true, | ||||
| 								callback: () => frm.reload_doc() || d.hide(), | ||||
| 								error: () => d.hide() | ||||
| 								callback: () => { | ||||
| 									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') | ||||
| @ -136,29 +149,83 @@ erpnext.setup_einvoice_actions = (doctype) => { | ||||
| 			} | ||||
| 
 | ||||
| 			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 = () => { | ||||
| 					let message = __('Cancellation of e-way bill is currently not supported.') + ' '; | ||||
| 					message += '<br><br>'; | ||||
| 					message += __('You must first use the portal to cancel the e-way bill and then update the cancelled status in the ERPNext system.'); | ||||
| 					const d = new frappe.ui.Dialog({ | ||||
| 						title: __('Cancel E-Way Bill'), | ||||
| 						fields: fields, | ||||
| 						primary_action: function() { | ||||
| 							const data = d.get_values(); | ||||
| 							frappe.call({ | ||||
| 								method: 'erpnext.regional.india.e_invoice.utils.cancel_eway_bill', | ||||
| 								args: { | ||||
| 									doctype, | ||||
| 									docname: name, | ||||
| 									eway_bill: ewaybill, | ||||
| 									reason: data.reason.split('-')[0], | ||||
| 									remark: data.remark | ||||
| 								}, | ||||
| 								freeze: true, | ||||
| 								callback: () => { | ||||
| 									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({ | ||||
| 						title: __('Update E-Way Bill Cancelled Status?'), | ||||
| 						message: message, | ||||
| 						indicator: 'orange', | ||||
| 						title: __("Generate QRCode"), | ||||
| 						message: __("Generate and attach QR Code using IRN?"), | ||||
| 						primary_action: { | ||||
| 							action: function() { | ||||
| 								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 }, | ||||
| 									freeze: true, | ||||
| 									callback: () => frm.reload_doc() || dialog.hide() | ||||
| 									callback: () => frm.reload_doc() || dialog.hide(), | ||||
| 									error: () => dialog.hide() | ||||
| 								}); | ||||
| 							} | ||||
| 						}, | ||||
| 						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) => { | ||||
| 	return [ | ||||
| 		{ | ||||
| 			'fieldname': 'transporter', | ||||
| 			'label': 'Transporter', | ||||
| 			'fieldtype': 'Link', | ||||
| 			'options': 'Supplier', | ||||
| 			'default': frm.doc.transporter | ||||
| 			fieldname: "eway_part_a_section_break", | ||||
| 			fieldtype: "Section Break", | ||||
| 			label: "Part A", | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'gst_transporter_id', | ||||
| 			'label': 'GST Transporter ID', | ||||
| 			'fieldtype': 'Data', | ||||
| 			'default': frm.doc.gst_transporter_id | ||||
| 			fieldname: "transporter", | ||||
| 			label: "Transporter", | ||||
| 			fieldtype: "Link", | ||||
| 			options: "Supplier", | ||||
| 			default: frm.doc.transporter, | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'driver', | ||||
| 			'label': 'Driver', | ||||
| 			'fieldtype': 'Link', | ||||
| 			'options': 'Driver', | ||||
| 			'default': frm.doc.driver | ||||
| 			fieldname: "transporter_name", | ||||
| 			label: "Transporter Name", | ||||
| 			fieldtype: "Data", | ||||
| 			read_only: 1, | ||||
| 			default: frm.doc.transporter_name, | ||||
| 			depends_on: "transporter", | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'lr_no', | ||||
| 			'label': 'Transport Receipt No', | ||||
| 			'fieldtype': 'Data', | ||||
| 			'default': frm.doc.lr_no | ||||
| 			fieldname: "part_a_column_break", | ||||
| 			fieldtype: "Column Break", | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'vehicle_no', | ||||
| 			'label': 'Vehicle No', | ||||
| 			'fieldtype': 'Data', | ||||
| 			'default': frm.doc.vehicle_no | ||||
| 			fieldname: "gst_transporter_id", | ||||
| 			label: "GST Transporter ID", | ||||
| 			fieldtype: "Data", | ||||
| 			default: frm.doc.gst_transporter_id, | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'distance', | ||||
| 			'label': 'Distance (in km)', | ||||
| 			'fieldtype': 'Float', | ||||
| 			'default': frm.doc.distance | ||||
| 			fieldname: "distance", | ||||
| 			label: "Distance (in km)", | ||||
| 			fieldtype: "Float", | ||||
| 			default: frm.doc.distance, | ||||
| 			description: 'Set as zero to auto calculate distance using pin codes', | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'transporter_col_break', | ||||
| 			'fieldtype': 'Column Break', | ||||
| 			fieldname: "eway_part_b_section_break", | ||||
| 			fieldtype: "Section Break", | ||||
| 			label: "Part B", | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'transporter_name', | ||||
| 			'label': 'Transporter Name', | ||||
| 			'fieldtype': 'Data', | ||||
| 			'read_only': 1, | ||||
| 			'default': frm.doc.transporter_name, | ||||
| 			'depends_on': 'transporter' | ||||
| 			fieldname: "mode_of_transport", | ||||
| 			label: "Mode of Transport", | ||||
| 			fieldtype: "Select", | ||||
| 			options: `\nRoad\nAir\nRail\nShip`, | ||||
| 			default: frm.doc.mode_of_transport, | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'mode_of_transport', | ||||
| 			'label': 'Mode of Transport', | ||||
| 			'fieldtype': 'Select', | ||||
| 			'options': `\nRoad\nAir\nRail\nShip`, | ||||
| 			'default': frm.doc.mode_of_transport | ||||
| 			fieldname: "gst_vehicle_type", | ||||
| 			label: "GST Vehicle Type", | ||||
| 			fieldtype: "Select", | ||||
| 			options: `Regular\nOver Dimensional Cargo (ODC)`, | ||||
| 			depends_on: 'eval:(doc.mode_of_transport === "Road")', | ||||
| 			default: frm.doc.gst_vehicle_type, | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'driver_name', | ||||
| 			'label': 'Driver Name', | ||||
| 			'fieldtype': 'Data', | ||||
| 			'fetch_from': 'driver.full_name', | ||||
| 			'read_only': 1, | ||||
| 			'default': frm.doc.driver_name, | ||||
| 			'depends_on': 'driver' | ||||
| 			fieldname: "vehicle_no", | ||||
| 			label: "Vehicle No", | ||||
| 			fieldtype: "Data", | ||||
| 			default: frm.doc.vehicle_no, | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'lr_date', | ||||
| 			'label': 'Transport Receipt Date', | ||||
| 			'fieldtype': 'Date', | ||||
| 			'default': frm.doc.lr_date | ||||
| 			fieldname: "part_b_column_break", | ||||
| 			fieldtype: "Column Break", | ||||
| 		}, | ||||
| 		{ | ||||
| 			'fieldname': 'gst_vehicle_type', | ||||
| 			'label': 'GST Vehicle Type', | ||||
| 			'fieldtype': 'Select', | ||||
| 			'options': `Regular\nOver Dimensional Cargo (ODC)`, | ||||
| 			'depends_on': 'eval:(doc.mode_of_transport === "Road")', | ||||
| 			'default': frm.doc.gst_vehicle_type | ||||
| 		} | ||||
| 			fieldname: "lr_date", | ||||
| 			label: "Transport Receipt Date", | ||||
| 			fieldtype: "Date", | ||||
| 			default: frm.doc.lr_date, | ||||
| 		}, | ||||
| 		{ | ||||
| 			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"), | ||||
| 		) | ||||
| 
 | ||||
| 	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_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.generate_irn_url = self.base_url + "/enriched/ei/api/invoice" | ||||
| 		self.gstin_details_url = self.base_url + "/enriched/ei/api/master/gstin" | ||||
| 		self.cancel_ewaybill_url = self.base_url + "/enriched/ewb/ewayapi?action=CANEWB" | ||||
| 		self.cancel_ewaybill_url = self.base_url + "/enriched/ei/api/ewayapi" | ||||
| 		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): | ||||
| 		self.invoice = None | ||||
| @ -857,8 +863,8 @@ class GSPConnector: | ||||
| 		return res | ||||
| 
 | ||||
| 	def auto_refresh_token(self): | ||||
| 		self.fetch_auth_token() | ||||
| 		self.token_auto_refreshed = True | ||||
| 		self.fetch_auth_token() | ||||
| 
 | ||||
| 	def log_request(self, url, headers, data, res): | ||||
| 		headers.update({"password": self.credentials.password}) | ||||
| @ -998,6 +1004,37 @@ class GSPConnector: | ||||
| 
 | ||||
| 		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): | ||||
| 		headers = self.get_headers() | ||||
| 
 | ||||
| @ -1113,6 +1150,19 @@ class GSPConnector: | ||||
| 				self.invoice.eway_bill_validity = res.get("result").get("EwbValidTill") | ||||
| 				self.invoice.eway_bill_cancelled = 0 | ||||
| 				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 = { | ||||
| 					"doctype": self.invoice.doctype, | ||||
| 					"docname": self.invoice.name, | ||||
| @ -1135,7 +1185,6 @@ class GSPConnector: | ||||
| 		headers = self.get_headers() | ||||
| 		data = json.dumps({"ewbNo": eway_bill, "cancelRsnCode": reason, "cancelRmrk": remark}, indent=4) | ||||
| 		headers["username"] = headers["user_name"] | ||||
| 		del headers["user_name"] | ||||
| 		try: | ||||
| 			res = self.make_request("post", self.cancel_ewaybill_url, headers, data) | ||||
| 			if res.get("success"): | ||||
| @ -1186,8 +1235,6 @@ class GSPConnector: | ||||
| 		return errors | ||||
| 
 | ||||
| 	def raise_error(self, raise_exception=False, errors=None): | ||||
| 		if errors is None: | ||||
| 			errors = [] | ||||
| 		title = _("E Invoice Request Failed") | ||||
| 		if errors: | ||||
| 			frappe.throw(errors, title=title, as_list=1) | ||||
| @ -1228,13 +1275,18 @@ class GSPConnector: | ||||
| 
 | ||||
| 	def attach_qrcode_image(self): | ||||
| 		qrcode = self.invoice.signed_qr_code | ||||
| 		doctype = self.invoice.doctype | ||||
| 		docname = self.invoice.name | ||||
| 		filename = "QRCode_{}.png".format(docname).replace(os.path.sep, "__") | ||||
| 
 | ||||
| 		qr_image = io.BytesIO() | ||||
| 		url = qrcreate(qrcode, error="L") | ||||
| 		url.png(qr_image, scale=2, quiet_zone=1) | ||||
| 		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( | ||||
| 			{ | ||||
| 				"doctype": "File", | ||||
| @ -1243,12 +1295,12 @@ class GSPConnector: | ||||
| 				"attached_to_name": docname, | ||||
| 				"attached_to_field": "qrcode_image", | ||||
| 				"is_private": 0, | ||||
| 				"content": qr_image.getvalue(), | ||||
| 				"content": qr_image, | ||||
| 			} | ||||
| 		) | ||||
| 		_file.save() | ||||
| 		frappe.db.commit() | ||||
| 		self.invoice.qrcode_image = _file.file_url | ||||
| 		return _file | ||||
| 
 | ||||
| 	def update_invoice(self): | ||||
| 		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) | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def generate_qrcode(doctype, docname): | ||||
| 	gsp_connector = GSPConnector(doctype, docname) | ||||
| 	gsp_connector.fetch_and_attach_qrcode_from_irn() | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def generate_eway_bill(doctype, docname, **kwargs): | ||||
| 	gsp_connector = GSPConnector(doctype, docname) | ||||
| @ -1300,13 +1358,9 @@ def generate_eway_bill(doctype, docname, **kwargs): | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def cancel_eway_bill(doctype, docname): | ||||
| 	# TODO: uncomment when eway_bill api from Adequare is enabled | ||||
| 	# gsp_connector = GSPConnector(doctype, docname) | ||||
| 	# 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) | ||||
| def cancel_eway_bill(doctype, docname, eway_bill, reason, remark): | ||||
| 	gsp_connector = GSPConnector(doctype, docname) | ||||
| 	gsp_connector.cancel_eway_bill(eway_bill, reason, remark) | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
|  | ||||
| @ -32,7 +32,7 @@ def _execute(filters=None): | ||||
| 	added_item = [] | ||||
| 	for d in item_list: | ||||
| 		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 | ||||
| 			for tax in tax_columns: | ||||
| 				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] | ||||
| 
 | ||||
| 			for tax in tax_columns: | ||||
| 				item_tax = itemised_tax.get((d.parent, d.item_code), {}).get(tax, {}) | ||||
| 				row += [item_tax.get("tax_amount", 0)] | ||||
| 
 | ||||
| 			data.append(row) | ||||
| 			added_item.append((d.parent, d.item_code)) | ||||
| 	if data: | ||||
| @ -64,6 +62,7 @@ def get_columns(): | ||||
| 		{"fieldname": "description", "label": _("Description"), "fieldtype": "Data", "width": 300}, | ||||
| 		{"fieldname": "stock_uom", "label": _("Stock UOM"), "fieldtype": "Data", "width": 100}, | ||||
| 		{"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": "taxable_amount", | ||||
| @ -106,16 +105,25 @@ def get_items(filters): | ||||
| 			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_price_list_rate) as base_price_list_rate, | ||||
| 			`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code, | ||||
| 			`tabGST HSN Code`.description | ||||
| 		from `tabSales Invoice`, `tabSales Invoice Item`, `tabGST HSN Code` | ||||
| 		where `tabSales Invoice`.name = `tabSales Invoice Item`.parent | ||||
| 			`tabSales Invoice Item`.parent, | ||||
| 			`tabSales Invoice Item`.item_code, | ||||
| 			`tabGST HSN Code`.description, | ||||
| 			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 Item`.gst_hsn_code is not NULL | ||||
| 			and `tabSales Invoice Item`.gst_hsn_code = `tabGST HSN Code`.name %s %s | ||||
| 		group by | ||||
| 			`tabSales Invoice Item`.parent, `tabSales Invoice Item`.item_code | ||||
| 
 | ||||
| 			`tabSales Invoice Item`.parent, | ||||
| 			`tabSales Invoice Item`.item_code | ||||
| 		""" | ||||
| 		% (conditions, match_conditions), | ||||
| 		filters, | ||||
| @ -213,15 +221,16 @@ def get_merged_data(columns, data): | ||||
| 	result = [] | ||||
| 
 | ||||
| 	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): | ||||
| 			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: | ||||
| 				if merged_hsn_dict.get(row[0], {}).get(d["fieldname"], ""): | ||||
| 					merged_hsn_dict[row[0]][d["fieldname"]] += row[i] | ||||
| 				if merged_hsn_dict.get(key, {}).get(d["fieldname"], ""): | ||||
| 					merged_hsn_dict[key][d["fieldname"]] += row[i] | ||||
| 				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(): | ||||
| 		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) | ||||
| 
 | ||||
| 	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)} | ||||
| 
 | ||||
| @ -271,7 +280,7 @@ def get_hsn_wise_json_data(filters, report_data): | ||||
| 			"desc": hsn.get("description"), | ||||
| 			"uqc": hsn.get("stock_uom").upper(), | ||||
| 			"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)), | ||||
| 			"iamt": 0.0, | ||||
| 			"camt": 0.0, | ||||
|  | ||||
| @ -479,16 +479,20 @@ erpnext.PointOfSale.Controller = class { | ||||
| 		frappe.dom.freeze(); | ||||
| 		this.frm = this.get_new_frm(this.frm); | ||||
| 		this.frm.doc.items = []; | ||||
| 		const res = await frappe.call({ | ||||
| 		return frappe.call({ | ||||
| 			method: "erpnext.accounts.doctype.pos_invoice.pos_invoice.make_sales_return", | ||||
| 			args: { | ||||
| 				'source_name': doc.name, | ||||
| 				'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() { | ||||
|  | ||||
| @ -238,4 +238,5 @@ def get_chart_data(data): | ||||
| 			"datasets": [{"name": _("Total Sales Amount"), "values": datapoints[:30]}], | ||||
| 		}, | ||||
| 		"type": "bar", | ||||
| 		"fieldtype": "Currency", | ||||
| 	} | ||||
|  | ||||
| @ -54,4 +54,5 @@ def get_chart_data(data, conditions, filters): | ||||
| 		}, | ||||
| 		"type": "line", | ||||
| 		"lineOptions": {"regionFill": 1}, | ||||
| 		"fieldtype": "Currency", | ||||
| 	} | ||||
|  | ||||
| @ -415,3 +415,8 @@ class Analytics(object): | ||||
| 		else: | ||||
| 			labels = [d.get("label") for d in self.columns[1 : length - 1]] | ||||
| 		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", | ||||
| 		"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.work_order import make_stock_entry | ||||
| 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 ( | ||||
| 	create_stock_reconciliation, | ||||
| ) | ||||
| @ -180,9 +183,12 @@ def make_items(): | ||||
| 		if not frappe.db.exists("Item", item_code): | ||||
| 			create_item(item_code) | ||||
| 
 | ||||
| 	create_stock_reconciliation( | ||||
| 		item_code="Test FG A RW 1", warehouse="_Test Warehouse - _TC", qty=10, rate=2000 | ||||
| 	) | ||||
| 	try: | ||||
| 		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"): | ||||
| 		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] | ||||
| 		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): | ||||
| 		company = frappe.db.get_value("Warehouse", "_Test Warehouse 2 - _TC1", "company") | ||||
| 		frappe.get_doc("User", "test2@example.com").add_roles( | ||||
|  | ||||
| @ -1183,6 +1183,42 @@ class TestStockLedgerEntry(FrappeTestCase): | ||||
| 		backdated.cancel() | ||||
| 		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): | ||||
| 	args = frappe._dict(args) | ||||
|  | ||||
| @ -62,6 +62,7 @@ class StockReconciliation(StockController): | ||||
| 		self.make_sle_on_cancel() | ||||
| 		self.make_gl_entries_on_cancel() | ||||
| 		self.repost_future_sle_and_gle() | ||||
| 		self.delete_auto_created_batches() | ||||
| 
 | ||||
| 	def remove_items_with_no_change(self): | ||||
| 		"""Remove items if qty or rate is not changed""" | ||||
| @ -456,7 +457,7 @@ class StockReconciliation(StockController): | ||||
| 
 | ||||
| 			key = (d.item_code, d.warehouse) | ||||
| 			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 | ||||
| 			elif d.serial_no: | ||||
| 				data = merge_similar_entries[key] | ||||
|  | ||||
| @ -31,6 +31,7 @@ class TestStockReconciliation(FrappeTestCase): | ||||
| 
 | ||||
| 	def tearDown(self): | ||||
| 		frappe.local.future_sle = {} | ||||
| 		frappe.flags.pop("dont_execute_stock_reposts", None) | ||||
| 
 | ||||
| 	def test_reco_for_fifo(self): | ||||
| 		self._test_reco_sle_gle("FIFO") | ||||
| @ -250,7 +251,7 @@ class TestStockReconciliation(FrappeTestCase): | ||||
| 		warehouse = "_Test Warehouse for Stock Reco2 - _TC" | ||||
| 
 | ||||
| 		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.submit() | ||||
| @ -288,6 +289,84 @@ class TestStockReconciliation(FrappeTestCase): | ||||
| 			stock_doc = frappe.get_doc("Stock Reconciliation", d) | ||||
| 			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): | ||||
| 		item_code = "Stock-Reco-customer-Item-100" | ||||
| 		create_item( | ||||
| @ -306,6 +385,7 @@ class TestStockReconciliation(FrappeTestCase): | ||||
| 		------------------------------------------- | ||||
| 		Var		| Doc	|	Qty	| Balance | ||||
| 		------------------------------------------- | ||||
| 		PR5     | PR    |   10  |  10   (posting date: today-4) [backdated] | ||||
| 		SR5		| Reco	|	0	|	8	(posting date: today-4) [backdated] | ||||
| 		PR1		| PR	|	10	|	18	(posting date: today-3) | ||||
| 		PR2		| PR	|	1	|	19	(posting date: today-2) | ||||
| @ -315,6 +395,14 @@ class TestStockReconciliation(FrappeTestCase): | ||||
| 		item_code = make_item().name | ||||
| 		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( | ||||
| 			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( | ||||
| 			item_code=item_code, warehouse=warehouse, qty=1, rate=100, posting_date=nowdate() | ||||
| 		) | ||||
| 
 | ||||
| 		pr1_balance = frappe.db.get_value( | ||||
| 			"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) | ||||
| 		assertBalance(pr1, 10) | ||||
| 		assertBalance(pr3, 12) | ||||
| 
 | ||||
| 		# post backdated stock reco in between | ||||
| 		sr4 = create_stock_reconciliation( | ||||
| 			item_code=item_code, warehouse=warehouse, qty=6, rate=100, posting_date=add_days(nowdate(), -1) | ||||
| 		) | ||||
| 		pr3_balance = frappe.db.get_value( | ||||
| 			"Stock Ledger Entry", {"voucher_no": pr3.name, "is_cancelled": 0}, "qty_after_transaction" | ||||
| 		) | ||||
| 		self.assertEqual(pr3_balance, 7) | ||||
| 		assertBalance(pr3, 7) | ||||
| 
 | ||||
| 		# post backdated stock reco at the start | ||||
| 		sr5 = create_stock_reconciliation( | ||||
| 			item_code=item_code, warehouse=warehouse, qty=8, rate=100, posting_date=add_days(nowdate(), -4) | ||||
| 		) | ||||
| 		pr1_balance = frappe.db.get_value( | ||||
| 			"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" | ||||
| 		assertBalance(pr1, 18) | ||||
| 		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( | ||||
| 			"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, 18) | ||||
| 		self.assertEqual(pr2_balance, 19) | ||||
| 		self.assertEqual(sr4_balance, 6)  # check if future stock reco is unaffected | ||||
| 		assertBalance(pr5, 10) | ||||
| 		# check if future stock reco is unaffected | ||||
| 		assertBalance(sr4, 6) | ||||
| 		assertBalance(sr5, 8) | ||||
| 
 | ||||
| 		# cancel backdated stock reco and check future impact | ||||
| 		sr5.cancel() | ||||
| 		pr1_balance = frappe.db.get_value( | ||||
| 			"Stock Ledger Entry", {"voucher_no": pr1.name, "is_cancelled": 0}, "qty_after_transaction" | ||||
| 		) | ||||
| 		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() | ||||
| 		assertBalance(pr1, 10) | ||||
| 		assertBalance(pr2, 11) | ||||
| 		assertBalance(sr4, 6)  # check if future stock reco is unaffected | ||||
| 
 | ||||
| 	@change_settings("Stock Settings", {"allow_negative_stock": 0}) | ||||
| 	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 | ||||
| 		frappe.flags.dont_execute_stock_reposts = True | ||||
| 		self.addCleanup(frappe.flags.pop, "dont_execute_stock_reposts") | ||||
| 
 | ||||
| 		item_code = make_item().name | ||||
| 		warehouse = "_Test Warehouse - _TC" | ||||
| @ -684,11 +746,13 @@ def create_stock_reconciliation(**args): | ||||
| 		}, | ||||
| 	) | ||||
| 
 | ||||
| 	try: | ||||
| 		if not args.do_not_submit: | ||||
| 			sr.submit() | ||||
| 	except EmptyStockReconciliationItemsError: | ||||
| 		pass | ||||
| 	if not args.do_not_save: | ||||
| 		sr.insert() | ||||
| 		try: | ||||
| 			if not args.do_not_submit: | ||||
| 				sr.submit() | ||||
| 		except EmptyStockReconciliationItemsError: | ||||
| 			pass | ||||
| 	return sr | ||||
| 
 | ||||
| 
 | ||||
|  | ||||
| @ -111,17 +111,17 @@ def get_columns(): | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname": "posting_date", | ||||
| 			"fieldtype": "Date", | ||||
| 			"fieldtype": "Data", | ||||
| 			"label": _("Posting Date"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname": "posting_time", | ||||
| 			"fieldtype": "Time", | ||||
| 			"fieldtype": "Data", | ||||
| 			"label": _("Posting Time"), | ||||
| 		}, | ||||
| 		{ | ||||
| 			"fieldname": "creation", | ||||
| 			"fieldtype": "Datetime", | ||||
| 			"fieldtype": "Data", | ||||
| 			"label": _("Creation"), | ||||
| 		}, | ||||
| 		{ | ||||
|  | ||||
| @ -1303,6 +1303,8 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): | ||||
| 	datetime_limit_condition = "" | ||||
| 	qty_shift = args.actual_qty | ||||
| 
 | ||||
| 	args["time_format"] = "%H:%i:%s" | ||||
| 
 | ||||
| 	# find difference/shift in qty caused by stock reconciliation | ||||
| 	if args.voucher_type == "Stock Reconciliation": | ||||
| 		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) | ||||
| 
 | ||||
| 	frappe.db.sql( | ||||
| 		""" | ||||
| 		f""" | ||||
| 		update `tabStock Ledger Entry` | ||||
| 		set qty_after_transaction = qty_after_transaction + {qty_shift} | ||||
| 		where | ||||
| @ -1323,16 +1325,10 @@ def update_qty_in_future_sle(args, allow_negative_stock=False): | ||||
| 			and warehouse = %(warehouse)s | ||||
| 			and voucher_no != %(voucher_no)s | ||||
| 			and is_cancelled = 0 | ||||
| 			and (timestamp(posting_date, posting_time) > timestamp(%(posting_date)s, %(posting_time)s) | ||||
| 				or ( | ||||
| 					timestamp(posting_date, posting_time) = timestamp(%(posting_date)s, %(posting_time)s) | ||||
| 					and creation > %(creation)s | ||||
| 				) | ||||
| 			) | ||||
| 			and timestamp(posting_date, time_format(posting_time, %(time_format)s)) | ||||
| 				> timestamp(%(posting_date)s, time_format(%(posting_time)s, %(time_format)s)) | ||||
| 		{datetime_limit_condition} | ||||
| 		""".format( | ||||
| 			qty_shift=qty_shift, datetime_limit_condition=datetime_limit_condition | ||||
| 		), | ||||
| 		""", | ||||
| 		args, | ||||
| 	) | ||||
| 
 | ||||
| @ -1383,6 +1379,7 @@ def get_next_stock_reco(args): | ||||
| 					and creation > %(creation)s | ||||
| 				) | ||||
| 			) | ||||
| 		order by timestamp(posting_date, posting_time) asc, creation asc | ||||
| 		limit 1 | ||||
| 	""", | ||||
| 		args, | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user