Merge branch 'develop' into FIX-34354
This commit is contained in:
		
						commit
						0753aa5ab7
					
				| @ -221,12 +221,15 @@ class PaymentReconciliation(Document): | ||||
| 
 | ||||
| 	def get_difference_amount(self, payment_entry, invoice, allocated_amount): | ||||
| 		difference_amount = 0 | ||||
| 		if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( | ||||
| 			"exchange_rate", 1 | ||||
| 		): | ||||
| 			allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount | ||||
| 			allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount | ||||
| 			difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate | ||||
| 		if frappe.get_cached_value( | ||||
| 			"Account", self.receivable_payable_account, "account_currency" | ||||
| 		) != frappe.get_cached_value("Company", self.company, "default_currency"): | ||||
| 			if invoice.get("exchange_rate") and payment_entry.get("exchange_rate", 1) != invoice.get( | ||||
| 				"exchange_rate", 1 | ||||
| 			): | ||||
| 				allocated_amount_in_ref_rate = payment_entry.get("exchange_rate", 1) * allocated_amount | ||||
| 				allocated_amount_in_inv_rate = invoice.get("exchange_rate", 1) * allocated_amount | ||||
| 				difference_amount = allocated_amount_in_ref_rate - allocated_amount_in_inv_rate | ||||
| 
 | ||||
| 		return difference_amount | ||||
| 
 | ||||
|  | ||||
| @ -5,7 +5,7 @@ import unittest | ||||
| 
 | ||||
| import frappe | ||||
| from frappe import qb | ||||
| from frappe.tests.utils import FrappeTestCase | ||||
| from frappe.tests.utils import FrappeTestCase, change_settings | ||||
| from frappe.utils import add_days, flt, nowdate | ||||
| 
 | ||||
| from erpnext import get_default_cost_center | ||||
| @ -349,6 +349,11 @@ class TestPaymentReconciliation(FrappeTestCase): | ||||
| 		invoices = [x.as_dict() for x in pr.get("invoices")] | ||||
| 		payments = [x.as_dict() for x in pr.get("payments")] | ||||
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) | ||||
| 
 | ||||
| 		# Difference amount should not be calculated for base currency accounts | ||||
| 		for row in pr.allocation: | ||||
| 			self.assertEqual(flt(row.get("difference_amount")), 0.0) | ||||
| 
 | ||||
| 		pr.reconcile() | ||||
| 
 | ||||
| 		si.reload() | ||||
| @ -390,6 +395,11 @@ class TestPaymentReconciliation(FrappeTestCase): | ||||
| 		invoices = [x.as_dict() for x in pr.get("invoices")] | ||||
| 		payments = [x.as_dict() for x in pr.get("payments")] | ||||
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) | ||||
| 
 | ||||
| 		# Difference amount should not be calculated for base currency accounts | ||||
| 		for row in pr.allocation: | ||||
| 			self.assertEqual(flt(row.get("difference_amount")), 0.0) | ||||
| 
 | ||||
| 		pr.reconcile() | ||||
| 
 | ||||
| 		# check PR tool output | ||||
| @ -414,6 +424,11 @@ class TestPaymentReconciliation(FrappeTestCase): | ||||
| 		invoices = [x.as_dict() for x in pr.get("invoices")] | ||||
| 		payments = [x.as_dict() for x in pr.get("payments")] | ||||
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) | ||||
| 
 | ||||
| 		# Difference amount should not be calculated for base currency accounts | ||||
| 		for row in pr.allocation: | ||||
| 			self.assertEqual(flt(row.get("difference_amount")), 0.0) | ||||
| 
 | ||||
| 		pr.reconcile() | ||||
| 
 | ||||
| 		# assert outstanding | ||||
| @ -450,6 +465,11 @@ class TestPaymentReconciliation(FrappeTestCase): | ||||
| 		invoices = [x.as_dict() for x in pr.get("invoices")] | ||||
| 		payments = [x.as_dict() for x in pr.get("payments")] | ||||
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) | ||||
| 
 | ||||
| 		# Difference amount should not be calculated for base currency accounts | ||||
| 		for row in pr.allocation: | ||||
| 			self.assertEqual(flt(row.get("difference_amount")), 0.0) | ||||
| 
 | ||||
| 		pr.reconcile() | ||||
| 
 | ||||
| 		self.assertEqual(pr.get("invoices"), []) | ||||
| @ -824,6 +844,52 @@ class TestPaymentReconciliation(FrappeTestCase): | ||||
| 		payment_vouchers = [x.get("reference_name") for x in pr.get("payments")] | ||||
| 		self.assertCountEqual(payment_vouchers, [je2.name, pe2.name]) | ||||
| 
 | ||||
| 	@change_settings( | ||||
| 		"Accounts Settings", | ||||
| 		{ | ||||
| 			"allow_multi_currency_invoices_against_single_party_account": 1, | ||||
| 		}, | ||||
| 	) | ||||
| 	def test_no_difference_amount_for_base_currency_accounts(self): | ||||
| 		# Make Sale Invoice | ||||
| 		si = self.create_sales_invoice( | ||||
| 			qty=1, rate=1, posting_date=nowdate(), do_not_save=True, do_not_submit=True | ||||
| 		) | ||||
| 		si.customer = self.customer | ||||
| 		si.currency = "EUR" | ||||
| 		si.conversion_rate = 85 | ||||
| 		si.debit_to = self.debit_to | ||||
| 		si.save().submit() | ||||
| 
 | ||||
| 		# Make payment using Payment Entry | ||||
| 		pe1 = create_payment_entry( | ||||
| 			company=self.company, | ||||
| 			payment_type="Receive", | ||||
| 			party_type="Customer", | ||||
| 			party=self.customer, | ||||
| 			paid_from=self.debit_to, | ||||
| 			paid_to=self.bank, | ||||
| 			paid_amount=100, | ||||
| 		) | ||||
| 
 | ||||
| 		pe1.save() | ||||
| 		pe1.submit() | ||||
| 
 | ||||
| 		pr = self.create_payment_reconciliation() | ||||
| 		pr.party = self.customer | ||||
| 		pr.receivable_payable_account = self.debit_to | ||||
| 		pr.get_unreconciled_entries() | ||||
| 
 | ||||
| 		self.assertEqual(len(pr.invoices), 1) | ||||
| 		self.assertEqual(len(pr.payments), 1) | ||||
| 
 | ||||
| 		invoices = [x.as_dict() for x in pr.invoices] | ||||
| 		payments = [pr.payments[0].as_dict()] | ||||
| 		pr.allocate_entries(frappe._dict({"invoices": invoices, "payments": payments})) | ||||
| 
 | ||||
| 		self.assertEqual(pr.allocation[0].allocated_amount, 85) | ||||
| 		self.assertEqual(pr.allocation[0].difference_amount, 0) | ||||
| 
 | ||||
| 
 | ||||
| def make_customer(customer_name, currency=None): | ||||
| 	if not frappe.db.exists("Customer", customer_name): | ||||
|  | ||||
| @ -16,6 +16,7 @@ | ||||
|   "transaction_settings_section", | ||||
|   "po_required", | ||||
|   "pr_required", | ||||
|   "over_order_allowance", | ||||
|   "column_break_12", | ||||
|   "maintain_same_rate", | ||||
|   "set_landed_cost_based_on_purchase_invoice_rate", | ||||
| @ -156,6 +157,13 @@ | ||||
|    "fieldname": "set_landed_cost_based_on_purchase_invoice_rate", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Set Landed Cost Based on Purchase Invoice Rate" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", | ||||
|    "fieldname": "over_order_allowance", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Over Order Allowance (%)" | ||||
|   } | ||||
|  ], | ||||
|  "icon": "fa fa-cog", | ||||
| @ -163,7 +171,7 @@ | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "issingle": 1, | ||||
|  "links": [], | ||||
|  "modified": "2023-02-28 15:41:32.686805", | ||||
|  "modified": "2023-03-02 17:02:14.404622", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Buying Settings", | ||||
|  | ||||
| @ -21,6 +21,9 @@ from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category | ||||
| from erpnext.accounts.party import get_party_account, get_party_account_currency | ||||
| from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items | ||||
| from erpnext.controllers.buying_controller import BuyingController | ||||
| from erpnext.manufacturing.doctype.blanket_order.blanket_order import ( | ||||
| 	validate_against_blanket_order, | ||||
| ) | ||||
| from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults | ||||
| from erpnext.stock.doctype.item.item import get_item_defaults, get_last_purchase_details | ||||
| from erpnext.stock.stock_balance import get_ordered_qty, update_bin_qty | ||||
| @ -69,6 +72,7 @@ class PurchaseOrder(BuyingController): | ||||
| 		self.validate_with_previous_doc() | ||||
| 		self.validate_for_subcontracting() | ||||
| 		self.validate_minimum_order_qty() | ||||
| 		validate_against_blanket_order(self) | ||||
| 
 | ||||
| 		if self.is_old_subcontracting_flow: | ||||
| 			self.validate_bom_for_subcontracting_items() | ||||
|  | ||||
| @ -7,6 +7,12 @@ frappe.ui.form.on('Blanket Order', { | ||||
| 	}, | ||||
| 
 | ||||
| 	setup: function(frm) { | ||||
| 		frm.custom_make_buttons = { | ||||
| 			'Purchase Order': 'Purchase Order', | ||||
| 			'Sales Order': 'Sales Order', | ||||
| 			'Quotation': 'Quotation', | ||||
| 		}; | ||||
| 
 | ||||
| 		frm.add_fetch("customer", "customer_name", "customer_name"); | ||||
| 		frm.add_fetch("supplier", "supplier_name", "supplier_name"); | ||||
| 	}, | ||||
|  | ||||
| @ -6,6 +6,7 @@ import frappe | ||||
| from frappe import _ | ||||
| from frappe.model.document import Document | ||||
| from frappe.model.mapper import get_mapped_doc | ||||
| from frappe.query_builder.functions import Sum | ||||
| from frappe.utils import flt, getdate | ||||
| 
 | ||||
| from erpnext.stock.doctype.item.item import get_item_defaults | ||||
| @ -29,21 +30,23 @@ class BlanketOrder(Document): | ||||
| 
 | ||||
| 	def update_ordered_qty(self): | ||||
| 		ref_doctype = "Sales Order" if self.blanket_order_type == "Selling" else "Purchase Order" | ||||
| 
 | ||||
| 		trans = frappe.qb.DocType(ref_doctype) | ||||
| 		trans_item = frappe.qb.DocType(f"{ref_doctype} Item") | ||||
| 
 | ||||
| 		item_ordered_qty = frappe._dict( | ||||
| 			frappe.db.sql( | ||||
| 				""" | ||||
| 			select trans_item.item_code, sum(trans_item.stock_qty) as qty | ||||
| 			from `tab{0} Item` trans_item, `tab{0}` trans | ||||
| 			where trans.name = trans_item.parent | ||||
| 				and trans_item.blanket_order=%s | ||||
| 				and trans.docstatus=1 | ||||
| 				and trans.status not in ('Closed', 'Stopped') | ||||
| 			group by trans_item.item_code | ||||
| 		""".format( | ||||
| 					ref_doctype | ||||
| 				), | ||||
| 				self.name, | ||||
| 			) | ||||
| 			( | ||||
| 				frappe.qb.from_(trans_item) | ||||
| 				.from_(trans) | ||||
| 				.select(trans_item.item_code, Sum(trans_item.stock_qty).as_("qty")) | ||||
| 				.where( | ||||
| 					(trans.name == trans_item.parent) | ||||
| 					& (trans_item.blanket_order == self.name) | ||||
| 					& (trans.docstatus == 1) | ||||
| 					& (trans.status.notin(["Stopped", "Closed"])) | ||||
| 				) | ||||
| 				.groupby(trans_item.item_code) | ||||
| 			).run() | ||||
| 		) | ||||
| 
 | ||||
| 		for d in self.items: | ||||
| @ -79,7 +82,43 @@ def make_order(source_name): | ||||
| 				"doctype": doctype + " Item", | ||||
| 				"field_map": {"rate": "blanket_order_rate", "parent": "blanket_order"}, | ||||
| 				"postprocess": update_item, | ||||
| 				"condition": lambda item: (flt(item.qty) - flt(item.ordered_qty)) > 0, | ||||
| 			}, | ||||
| 		}, | ||||
| 	) | ||||
| 	return target_doc | ||||
| 
 | ||||
| 
 | ||||
| def validate_against_blanket_order(order_doc): | ||||
| 	if order_doc.doctype in ("Sales Order", "Purchase Order"): | ||||
| 		order_data = {} | ||||
| 
 | ||||
| 		for item in order_doc.get("items"): | ||||
| 			if item.against_blanket_order and item.blanket_order: | ||||
| 				if item.blanket_order in order_data: | ||||
| 					if item.item_code in order_data[item.blanket_order]: | ||||
| 						order_data[item.blanket_order][item.item_code] += item.qty | ||||
| 					else: | ||||
| 						order_data[item.blanket_order][item.item_code] = item.qty | ||||
| 				else: | ||||
| 					order_data[item.blanket_order] = {item.item_code: item.qty} | ||||
| 
 | ||||
| 		if order_data: | ||||
| 			allowance = flt( | ||||
| 				frappe.db.get_single_value( | ||||
| 					"Selling Settings" if order_doc.doctype == "Sales Order" else "Buying Settings", | ||||
| 					"over_order_allowance", | ||||
| 				) | ||||
| 			) | ||||
| 			for bo_name, item_data in order_data.items(): | ||||
| 				bo_doc = frappe.get_doc("Blanket Order", bo_name) | ||||
| 				for item in bo_doc.get("items"): | ||||
| 					if item.item_code in item_data: | ||||
| 						remaining_qty = item.qty - item.ordered_qty | ||||
| 						allowed_qty = remaining_qty + (remaining_qty * (allowance / 100)) | ||||
| 						if allowed_qty < item_data[item.item_code]: | ||||
| 							frappe.throw( | ||||
| 								_("Item {0} cannot be ordered more than {1} against Blanket Order {2}.").format( | ||||
| 									item.item_code, allowed_qty, bo_name | ||||
| 								) | ||||
| 							) | ||||
|  | ||||
| @ -63,6 +63,33 @@ class TestBlanketOrder(FrappeTestCase): | ||||
| 		po1.currency = get_company_currency(po1.company) | ||||
| 		self.assertEqual(po1.items[0].qty, (bo.items[0].qty - bo.items[0].ordered_qty)) | ||||
| 
 | ||||
| 	def test_over_order_allowance(self): | ||||
| 		# Sales Order | ||||
| 		bo = make_blanket_order(blanket_order_type="Selling", quantity=100) | ||||
| 
 | ||||
| 		frappe.flags.args.doctype = "Sales Order" | ||||
| 		so = make_order(bo.name) | ||||
| 		so.currency = get_company_currency(so.company) | ||||
| 		so.delivery_date = today() | ||||
| 		so.items[0].qty = 110 | ||||
| 		self.assertRaises(frappe.ValidationError, so.submit) | ||||
| 
 | ||||
| 		frappe.db.set_single_value("Selling Settings", "over_order_allowance", 10) | ||||
| 		so.submit() | ||||
| 
 | ||||
| 		# Purchase Order | ||||
| 		bo = make_blanket_order(blanket_order_type="Purchasing", quantity=100) | ||||
| 
 | ||||
| 		frappe.flags.args.doctype = "Purchase Order" | ||||
| 		po = make_order(bo.name) | ||||
| 		po.currency = get_company_currency(po.company) | ||||
| 		po.schedule_date = today() | ||||
| 		po.items[0].qty = 110 | ||||
| 		self.assertRaises(frappe.ValidationError, po.submit) | ||||
| 
 | ||||
| 		frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10) | ||||
| 		po.submit() | ||||
| 
 | ||||
| 
 | ||||
| def make_blanket_order(**args): | ||||
| 	args = frappe._dict(args) | ||||
|  | ||||
| @ -21,6 +21,9 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( | ||||
| ) | ||||
| from erpnext.accounts.party import get_party_account | ||||
| from erpnext.controllers.selling_controller import SellingController | ||||
| from erpnext.manufacturing.doctype.blanket_order.blanket_order import ( | ||||
| 	validate_against_blanket_order, | ||||
| ) | ||||
| from erpnext.manufacturing.doctype.production_plan.production_plan import ( | ||||
| 	get_items_for_material_requests, | ||||
| ) | ||||
| @ -52,6 +55,7 @@ class SalesOrder(SellingController): | ||||
| 		self.validate_warehouse() | ||||
| 		self.validate_drop_ship() | ||||
| 		self.validate_serial_no_based_delivery() | ||||
| 		validate_against_blanket_order(self) | ||||
| 		validate_inter_company_party( | ||||
| 			self.doctype, self.customer, self.company, self.inter_company_order_reference | ||||
| 		) | ||||
|  | ||||
| @ -24,6 +24,7 @@ | ||||
|   "so_required", | ||||
|   "dn_required", | ||||
|   "sales_update_frequency", | ||||
|   "over_order_allowance", | ||||
|   "column_break_5", | ||||
|   "allow_multiple_items", | ||||
|   "allow_against_multiple_purchase_orders", | ||||
| @ -179,6 +180,12 @@ | ||||
|    "fieldname": "allow_sales_order_creation_for_expired_quotation", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Allow Sales Order Creation For Expired Quotation" | ||||
|   }, | ||||
|   { | ||||
|    "description": "Percentage you are allowed to order more against the Blanket Order Quantity. For example: If you have a Blanket Order of Quantity 100 units. and your Allowance is 10% then you are allowed to order 110 units.", | ||||
|    "fieldname": "over_order_allowance", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Over Order Allowance (%)" | ||||
|   } | ||||
|  ], | ||||
|  "icon": "fa fa-cog", | ||||
| @ -186,7 +193,7 @@ | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "issingle": 1, | ||||
|  "links": [], | ||||
|  "modified": "2023-02-04 12:37:53.380857", | ||||
|  "modified": "2023-03-03 11:16:54.333615", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Selling", | ||||
|  "name": "Selling Settings", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user