Merge branch 'develop' into gross-profit-percentage
This commit is contained in:
		
						commit
						5f30104e00
					
				| @ -8,6 +8,7 @@ | ||||
| [](https://github.com/frappe/erpnext/actions/workflows/server-tests.yml) | ||||
| [](https://www.codetriage.com/frappe/erpnext) | ||||
| [](https://codecov.io/gh/frappe/erpnext) | ||||
| [](https://hub.docker.com/r/frappe/erpnext-worker) | ||||
| 
 | ||||
| [https://erpnext.com](https://erpnext.com) | ||||
| 
 | ||||
|  | ||||
| @ -8,7 +8,7 @@ frappe.ui.form.on('Accounting Dimension Filter', { | ||||
| 		} | ||||
| 
 | ||||
| 		let help_content = | ||||
| 			`<table class="table table-bordered" style="background-color: #f9f9f9;">
 | ||||
| 			`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
 | ||||
| 				<tr><td> | ||||
| 					<p> | ||||
| 						<i class="fa fa-hand-right"></i> | ||||
|  | ||||
| @ -6,7 +6,7 @@ frappe.provide("erpnext.accounts.dimensions"); | ||||
| frappe.ui.form.on('Loyalty Program', { | ||||
| 	setup: function(frm) { | ||||
| 		var help_content = | ||||
| 			`<table class="table table-bordered" style="background-color: #f9f9f9;">
 | ||||
| 			`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
 | ||||
| 				<tr><td> | ||||
| 					<h4> | ||||
| 						<i class="fa fa-hand-right"></i> | ||||
|  | ||||
| @ -38,7 +38,7 @@ frappe.ui.form.on('Pricing Rule', { | ||||
| 
 | ||||
| 	refresh: function(frm) { | ||||
| 		var help_content = | ||||
| 			`<table class="table table-bordered" style="background-color: #f9f9f9;">
 | ||||
| 			`<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
 | ||||
| 				<tr><td> | ||||
| 					<h4> | ||||
| 						<i class="fa fa-hand-right"></i> | ||||
|  | ||||
| @ -20,6 +20,9 @@ price_discount_fields = ['rate_or_discount', 'apply_discount_on', 'apply_discoun | ||||
| product_discount_fields = ['free_item', 'free_qty', 'free_item_uom', | ||||
| 	'free_item_rate', 'same_item', 'is_recursive', 'apply_multiple_pricing_rules'] | ||||
| 
 | ||||
| class TransactionExists(frappe.ValidationError): | ||||
| 	pass | ||||
| 
 | ||||
| class PromotionalScheme(Document): | ||||
| 	def validate(self): | ||||
| 		if not self.selling and not self.buying: | ||||
| @ -28,6 +31,40 @@ class PromotionalScheme(Document): | ||||
| 			or self.product_discount_slabs): | ||||
| 			frappe.throw(_("Price or product discount slabs are required")) | ||||
| 
 | ||||
| 		self.validate_applicable_for() | ||||
| 		self.validate_pricing_rules() | ||||
| 
 | ||||
| 	def validate_applicable_for(self): | ||||
| 		if self.applicable_for: | ||||
| 			applicable_for = frappe.scrub(self.applicable_for) | ||||
| 
 | ||||
| 			if not self.get(applicable_for): | ||||
| 				msg = (f'The field {frappe.bold(self.applicable_for)} is required') | ||||
| 				frappe.throw(_(msg)) | ||||
| 
 | ||||
| 	def validate_pricing_rules(self): | ||||
| 		if self.is_new(): | ||||
| 			return | ||||
| 
 | ||||
| 		transaction_exists = False | ||||
| 		docnames = [] | ||||
| 
 | ||||
| 		# If user has changed applicable for | ||||
| 		if self._doc_before_save.applicable_for == self.applicable_for: | ||||
| 			return | ||||
| 
 | ||||
| 		docnames = frappe.get_all('Pricing Rule', | ||||
| 			filters= {'promotional_scheme': self.name}) | ||||
| 
 | ||||
| 		for docname in docnames: | ||||
| 			if frappe.db.exists('Pricing Rule Detail', | ||||
| 				{'pricing_rule': docname.name, 'docstatus': ('<', 2)}): | ||||
| 				raise_for_transaction_exists(self.name) | ||||
| 
 | ||||
| 		if docnames and not transaction_exists: | ||||
| 			for docname in docnames: | ||||
| 				frappe.delete_doc('Pricing Rule', docname.name) | ||||
| 
 | ||||
| 	def on_update(self): | ||||
| 		pricing_rules = frappe.get_all( | ||||
| 			'Pricing Rule', | ||||
| @ -67,6 +104,13 @@ class PromotionalScheme(Document): | ||||
| 			{'promotional_scheme': self.name}): | ||||
| 			frappe.delete_doc('Pricing Rule', rule.name) | ||||
| 
 | ||||
| def raise_for_transaction_exists(name): | ||||
| 	msg = (f"""You can't change the {frappe.bold(_('Applicable For'))} | ||||
| 		because transactions are present against the Promotional Scheme {frappe.bold(name)}. """) | ||||
| 	msg += 'Kindly disable this Promotional Scheme and create new for new Applicable For.' | ||||
| 
 | ||||
| 	frappe.throw(_(msg), TransactionExists) | ||||
| 
 | ||||
| def get_pricing_rules(doc, rules=None): | ||||
| 	if rules is None: | ||||
| 		rules = {} | ||||
| @ -84,45 +128,59 @@ def _get_pricing_rules(doc, child_doc, discount_fields, rules=None): | ||||
| 	new_doc = [] | ||||
| 	args = get_args_for_pricing_rule(doc) | ||||
| 	applicable_for = frappe.scrub(doc.get('applicable_for')) | ||||
| 
 | ||||
| 	for idx, d in enumerate(doc.get(child_doc)): | ||||
| 		if d.name in rules: | ||||
| 			for applicable_for_value in args.get(applicable_for): | ||||
| 				temp_args = args.copy() | ||||
| 				docname = frappe.get_all( | ||||
| 					'Pricing Rule', | ||||
| 					fields = ["promotional_scheme_id", "name", applicable_for], | ||||
| 					filters = { | ||||
| 						'promotional_scheme_id': d.name, | ||||
| 						applicable_for: applicable_for_value | ||||
| 					} | ||||
| 				) | ||||
| 
 | ||||
| 				if docname: | ||||
| 					pr = frappe.get_doc('Pricing Rule', docname[0].get('name')) | ||||
| 					temp_args[applicable_for] = applicable_for_value | ||||
| 					pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) | ||||
| 				else: | ||||
| 					pr = frappe.new_doc("Pricing Rule") | ||||
| 					pr.title = doc.name | ||||
| 					temp_args[applicable_for] = applicable_for_value | ||||
| 					pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) | ||||
| 
 | ||||
| 			if not args.get(applicable_for): | ||||
| 				docname = get_pricing_rule_docname(d) | ||||
| 				pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname) | ||||
| 				new_doc.append(pr) | ||||
| 			else: | ||||
| 				for applicable_for_value in args.get(applicable_for): | ||||
| 					docname = get_pricing_rule_docname(d, applicable_for, applicable_for_value) | ||||
| 					pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, | ||||
| 						d, docname, applicable_for, applicable_for_value) | ||||
| 					new_doc.append(pr) | ||||
| 
 | ||||
| 		else: | ||||
| 		elif args.get(applicable_for): | ||||
| 			applicable_for_values = args.get(applicable_for) or [] | ||||
| 			for applicable_for_value in applicable_for_values: | ||||
| 				pr = frappe.new_doc("Pricing Rule") | ||||
| 				pr.title = doc.name | ||||
| 				temp_args = args.copy() | ||||
| 				temp_args[applicable_for] = applicable_for_value | ||||
| 				pr = set_args(temp_args, pr, doc, child_doc, discount_fields, d) | ||||
| 				pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, | ||||
| 					d, applicable_for=applicable_for, value= applicable_for_value) | ||||
| 
 | ||||
| 				new_doc.append(pr) | ||||
| 		else: | ||||
| 			pr = prepare_pricing_rule(args, doc, child_doc, discount_fields, d) | ||||
| 			new_doc.append(pr) | ||||
| 
 | ||||
| 	return new_doc | ||||
| 
 | ||||
| def get_pricing_rule_docname(row: dict, applicable_for: str = None, applicable_for_value: str = None) -> str: | ||||
| 	fields = ['promotional_scheme_id', 'name'] | ||||
| 	filters = { | ||||
| 		'promotional_scheme_id': row.name | ||||
| 	} | ||||
| 
 | ||||
| 	if applicable_for: | ||||
| 		fields.append(applicable_for) | ||||
| 		filters[applicable_for] = applicable_for_value | ||||
| 
 | ||||
| 	docname = frappe.get_all('Pricing Rule', fields = fields, filters = filters) | ||||
| 	return docname[0].name if docname else '' | ||||
| 
 | ||||
| def prepare_pricing_rule(args, doc, child_doc, discount_fields, d, docname=None, applicable_for=None, value=None): | ||||
| 	if docname: | ||||
| 		pr = frappe.get_doc("Pricing Rule", docname) | ||||
| 	else: | ||||
| 		pr = frappe.new_doc("Pricing Rule") | ||||
| 
 | ||||
| 	pr.title = doc.name | ||||
| 	temp_args = args.copy() | ||||
| 
 | ||||
| 	if value: | ||||
| 		temp_args[applicable_for] = value | ||||
| 
 | ||||
| 	return set_args(temp_args, pr, doc, child_doc, discount_fields, d) | ||||
| 
 | ||||
| def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): | ||||
| 	pr.update(args) | ||||
| @ -145,6 +203,7 @@ def set_args(args, pr, doc, child_doc, discount_fields, child_doc_fields): | ||||
| 				apply_on: d.get(apply_on), | ||||
| 				'uom': d.uom | ||||
| 			}) | ||||
| 
 | ||||
| 	return pr | ||||
| 
 | ||||
| def get_args_for_pricing_rule(doc): | ||||
|  | ||||
| @ -5,10 +5,17 @@ import unittest | ||||
| 
 | ||||
| import frappe | ||||
| 
 | ||||
| from erpnext.accounts.doctype.promotional_scheme.promotional_scheme import TransactionExists | ||||
| from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order | ||||
| 
 | ||||
| 
 | ||||
| class TestPromotionalScheme(unittest.TestCase): | ||||
| 	def setUp(self): | ||||
| 		if frappe.db.exists('Promotional Scheme', '_Test Scheme'): | ||||
| 			frappe.delete_doc('Promotional Scheme', '_Test Scheme') | ||||
| 
 | ||||
| 	def test_promotional_scheme(self): | ||||
| 		ps = make_promotional_scheme() | ||||
| 		ps = make_promotional_scheme(applicable_for='Customer', customer='_Test Customer') | ||||
| 		price_rules = frappe.get_all('Pricing Rule', fields = ["promotional_scheme_id", "name", "creation"], | ||||
| 			filters = {'promotional_scheme': ps.name}) | ||||
| 		self.assertTrue(len(price_rules),1) | ||||
| @ -39,22 +46,62 @@ class TestPromotionalScheme(unittest.TestCase): | ||||
| 			filters = {'promotional_scheme': ps.name}) | ||||
| 		self.assertEqual(price_rules, []) | ||||
| 
 | ||||
| def make_promotional_scheme(): | ||||
| 	def test_promotional_scheme_without_applicable_for(self): | ||||
| 		ps = make_promotional_scheme() | ||||
| 		price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) | ||||
| 
 | ||||
| 		self.assertTrue(len(price_rules), 1) | ||||
| 		frappe.delete_doc('Promotional Scheme', ps.name) | ||||
| 
 | ||||
| 		price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) | ||||
| 		self.assertEqual(price_rules, []) | ||||
| 
 | ||||
| 	def test_change_applicable_for_in_promotional_scheme(self): | ||||
| 		ps = make_promotional_scheme() | ||||
| 		price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) | ||||
| 		self.assertTrue(len(price_rules), 1) | ||||
| 
 | ||||
| 		so = make_sales_order(qty=5, currency='USD', do_not_save=True) | ||||
| 		so.set_missing_values() | ||||
| 		so.save() | ||||
| 		self.assertEqual(price_rules[0].name, so.pricing_rules[0].pricing_rule) | ||||
| 
 | ||||
| 		ps.applicable_for = 'Customer' | ||||
| 		ps.append('customer', { | ||||
| 			'customer': '_Test Customer' | ||||
| 		}) | ||||
| 
 | ||||
| 		self.assertRaises(TransactionExists, ps.save) | ||||
| 
 | ||||
| 		frappe.delete_doc('Sales Order', so.name) | ||||
| 		frappe.delete_doc('Promotional Scheme', ps.name) | ||||
| 		price_rules = frappe.get_all('Pricing Rule', filters = {'promotional_scheme': ps.name}) | ||||
| 		self.assertEqual(price_rules, []) | ||||
| 
 | ||||
| def make_promotional_scheme(**args): | ||||
| 	args = frappe._dict(args) | ||||
| 
 | ||||
| 	ps = frappe.new_doc('Promotional Scheme') | ||||
| 	ps.name = '_Test Scheme' | ||||
| 	ps.append('items',{ | ||||
| 		'item_code': '_Test Item' | ||||
| 	}) | ||||
| 
 | ||||
| 	ps.selling = 1 | ||||
| 	ps.append('price_discount_slabs',{ | ||||
| 		'min_qty': 4, | ||||
| 		'validate_applied_rule': 0, | ||||
| 		'discount_percentage': 20, | ||||
| 		'rule_description': 'Test' | ||||
| 	}) | ||||
| 	ps.applicable_for = 'Customer' | ||||
| 	ps.append('customer',{ | ||||
| 		'customer': "_Test Customer" | ||||
| 	}) | ||||
| 
 | ||||
| 	ps.company = '_Test Company' | ||||
| 	if args.applicable_for: | ||||
| 		ps.applicable_for = args.applicable_for | ||||
| 		ps.append(frappe.scrub(args.applicable_for), { | ||||
| 			frappe.scrub(args.applicable_for): args.get(frappe.scrub(args.applicable_for)) | ||||
| 		}) | ||||
| 
 | ||||
| 	ps.save() | ||||
| 
 | ||||
| 	return ps | ||||
|  | ||||
| @ -136,7 +136,7 @@ | ||||
|    "label": "Threshold for Suggestion" | ||||
|   }, | ||||
|   { | ||||
|    "default": "1", | ||||
|    "default": "0", | ||||
|    "fieldname": "validate_applied_rule", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Validate Applied Rule" | ||||
| @ -169,7 +169,7 @@ | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-08-19 15:49:29.598727", | ||||
|  "modified": "2021-11-16 00:25:33.843996", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Promotional Scheme Price Discount", | ||||
|  | ||||
| @ -105,7 +105,7 @@ frappe.ui.form.on('Production Plan', { | ||||
| 		} | ||||
| 		frm.trigger("material_requirement"); | ||||
| 
 | ||||
| 		const projected_qty_formula = ` <table class="table table-bordered" style="background-color: #f9f9f9;">
 | ||||
| 		const projected_qty_formula = ` <table class="table table-bordered" style="background-color: var(--scrollbar-track-color);">
 | ||||
| 			<tr><td style="padding-left:25px"> | ||||
| 				<div> | ||||
| 				<h3 style="text-decoration: underline;"> | ||||
|  | ||||
| @ -35,7 +35,7 @@ erpnext.stock.LandedCostVoucher = class LandedCostVoucher extends erpnext.stock. | ||||
| 	refresh() { | ||||
| 		var help_content = | ||||
| 			`<br><br>
 | ||||
| 			<table class="table table-bordered" style="background-color: #f9f9f9;"> | ||||
| 			<table class="table table-bordered" style="background-color: var(--scrollbar-track-color);"> | ||||
| 				<tr><td> | ||||
| 					<h4> | ||||
| 						<i class="fa fa-hand-right"></i> | ||||
|  | ||||
| @ -342,7 +342,7 @@ def check_serial_no_validity_on_cancel(serial_no, sle): | ||||
| 	is_stock_reco = sle.voucher_type == "Stock Reconciliation" | ||||
| 	msg = None | ||||
| 
 | ||||
| 	if sr and (actual_qty < 0 or is_stock_reco) and sr.warehouse != sle.warehouse: | ||||
| 	if sr and (actual_qty < 0 or is_stock_reco) and (sr.warehouse and sr.warehouse != sle.warehouse): | ||||
| 		# receipt(inward) is being cancelled | ||||
| 		msg = _("Cannot cancel {0} {1} as Serial No {2} does not belong to the warehouse {3}").format( | ||||
| 			sle.voucher_type, doc_link, sr_link, frappe.bold(sle.warehouse)) | ||||
|  | ||||
| @ -399,6 +399,34 @@ class TestStockReconciliation(ERPNextTestCase): | ||||
| 			, do_not_submit=True) | ||||
| 		self.assertRaises(frappe.ValidationError, sr.submit) | ||||
| 
 | ||||
| 	def test_serial_no_cancellation(self): | ||||
| 
 | ||||
| 		from erpnext.stock.doctype.stock_entry.test_stock_entry import make_stock_entry | ||||
| 		item = create_item("Stock-Reco-Serial-Item-9", is_stock_item=1) | ||||
| 		if not item.has_serial_no: | ||||
| 			item.has_serial_no = 1 | ||||
| 			item.serial_no_series = "SRS9.####" | ||||
| 			item.save() | ||||
| 
 | ||||
| 		item_code = item.name | ||||
| 		warehouse = "_Test Warehouse - _TC" | ||||
| 
 | ||||
| 		se1 = make_stock_entry(item_code=item_code, target=warehouse, qty=10, basic_rate=700) | ||||
| 
 | ||||
| 		serial_nos = get_serial_nos(se1.items[0].serial_no) | ||||
| 		# reduce 1 item | ||||
| 		serial_nos.pop() | ||||
| 		new_serial_nos = "\n".join(serial_nos) | ||||
| 
 | ||||
| 		sr = create_stock_reconciliation(item_code=item.name, warehouse=warehouse, serial_no=new_serial_nos, qty=9) | ||||
| 		sr.cancel() | ||||
| 
 | ||||
| 		active_sr_no = frappe.get_all("Serial No", | ||||
| 				filters={"item_code": item_code, "warehouse": warehouse, "status": "Active"}) | ||||
| 
 | ||||
| 		self.assertEqual(len(active_sr_no), 10) | ||||
| 
 | ||||
| 
 | ||||
| def create_batch_item_with_batch(item_name, batch_id): | ||||
| 	batch_item_doc = create_item(item_name, is_stock_item=1) | ||||
| 	if not batch_item_doc.has_batch_no: | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user