Merge branch 'develop' into gstr_3b_nil_exempt_fixed
This commit is contained in:
		
						commit
						c6c51d7312
					
				| @ -120,6 +120,7 @@ def get_booking_dates(doc, item, posting_date=None): | ||||
| 	prev_gl_entry = frappe.db.sql(''' | ||||
| 		select name, posting_date from `tabGL Entry` where company=%s and account=%s and | ||||
| 		voucher_type=%s and voucher_no=%s and voucher_detail_no=%s | ||||
| 		and is_cancelled = 0 | ||||
| 		order by posting_date desc limit 1 | ||||
| 	''', (doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) | ||||
| 
 | ||||
| @ -227,6 +228,7 @@ def get_already_booked_amount(doc, item): | ||||
| 	gl_entries_details = frappe.db.sql(''' | ||||
| 		select sum({0}) as total_credit, sum({1}) as total_credit_in_account_currency, voucher_detail_no | ||||
| 		from `tabGL Entry` where company=%s and account=%s and voucher_type=%s and voucher_no=%s and voucher_detail_no=%s | ||||
| 		and is_cancelled = 0 | ||||
| 		group by voucher_detail_no | ||||
| 	'''.format(total_credit_debit, total_credit_debit_currency), | ||||
| 		(doc.company, item.get(deferred_account), doc.doctype, doc.name, item.name), as_dict=True) | ||||
| @ -282,7 +284,7 @@ def book_deferred_income_or_expense(doc, deferred_process, posting_date=None): | ||||
| 			return | ||||
| 
 | ||||
| 		# check if books nor frozen till endate: | ||||
| 		if getdate(end_date) >= getdate(accounts_frozen_upto): | ||||
| 		if accounts_frozen_upto and (end_date) <= getdate(accounts_frozen_upto): | ||||
| 			end_date = get_last_day(add_days(accounts_frozen_upto, 1)) | ||||
| 
 | ||||
| 		if via_journal_entry: | ||||
|  | ||||
| @ -194,8 +194,14 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 			frm.doc.paid_from_account_currency != frm.doc.paid_to_account_currency)); | ||||
| 
 | ||||
| 		frm.toggle_display("base_paid_amount", frm.doc.paid_from_account_currency != company_currency); | ||||
| 		frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges && | ||||
| 			(frm.doc.paid_from_account_currency != company_currency)); | ||||
| 
 | ||||
| 		if (frm.doc.payment_type == "Pay") { | ||||
| 			frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges && | ||||
| 				(frm.doc.paid_to_account_currency != company_currency)); | ||||
| 		} else { | ||||
| 			frm.toggle_display("base_total_taxes_and_charges", frm.doc.total_taxes_and_charges && | ||||
| 				(frm.doc.paid_from_account_currency != company_currency)); | ||||
| 		} | ||||
| 
 | ||||
| 		frm.toggle_display("base_received_amount", ( | ||||
| 			frm.doc.paid_to_account_currency != company_currency | ||||
| @ -230,7 +236,8 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 		var company_currency = frm.doc.company? frappe.get_doc(":Company", frm.doc.company).default_currency: ""; | ||||
| 
 | ||||
| 		frm.set_currency_labels(["base_paid_amount", "base_received_amount", "base_total_allocated_amount", | ||||
| 			"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax"], company_currency); | ||||
| 			"difference_amount", "base_paid_amount_after_tax", "base_received_amount_after_tax", | ||||
| 			"base_total_taxes_and_charges"], company_currency); | ||||
| 
 | ||||
| 		frm.set_currency_labels(["paid_amount"], frm.doc.paid_from_account_currency); | ||||
| 		frm.set_currency_labels(["received_amount"], frm.doc.paid_to_account_currency); | ||||
| @ -339,6 +346,8 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 			} | ||||
| 			frm.set_party_account_based_on_party = true; | ||||
| 
 | ||||
| 			let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; | ||||
| 
 | ||||
| 			return frappe.call({ | ||||
| 				method: "erpnext.accounts.doctype.payment_entry.payment_entry.get_party_details", | ||||
| 				args: { | ||||
| @ -372,7 +381,11 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 								if (r.message.bank_account) { | ||||
| 									frm.set_value("bank_account", r.message.bank_account); | ||||
| 								} | ||||
| 							} | ||||
| 							}, | ||||
| 							() => frm.events.set_current_exchange_rate(frm, "source_exchange_rate", | ||||
| 									frm.doc.paid_from_account_currency, company_currency), | ||||
| 							() => frm.events.set_current_exchange_rate(frm, "target_exchange_rate", | ||||
| 									frm.doc.paid_to_account_currency, company_currency) | ||||
| 						]); | ||||
| 					} | ||||
| 				} | ||||
| @ -476,14 +489,14 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 	}, | ||||
| 
 | ||||
| 	paid_from_account_currency: function(frm) { | ||||
| 		if(!frm.doc.paid_from_account_currency) return; | ||||
| 		var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; | ||||
| 		if(!frm.doc.paid_from_account_currency || !frm.doc.company) return; | ||||
| 		let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; | ||||
| 
 | ||||
| 		if (frm.doc.paid_from_account_currency == company_currency) { | ||||
| 			frm.set_value("source_exchange_rate", 1); | ||||
| 		} else if (frm.doc.paid_from){ | ||||
| 			if (in_list(["Internal Transfer", "Pay"], frm.doc.payment_type)) { | ||||
| 				var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; | ||||
| 				let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; | ||||
| 				frappe.call({ | ||||
| 					method: "erpnext.setup.utils.get_exchange_rate", | ||||
| 					args: { | ||||
| @ -503,8 +516,8 @@ frappe.ui.form.on('Payment Entry', { | ||||
| 	}, | ||||
| 
 | ||||
| 	paid_to_account_currency: function(frm) { | ||||
| 		if(!frm.doc.paid_to_account_currency) return; | ||||
| 		var company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; | ||||
| 		if(!frm.doc.paid_to_account_currency || !frm.doc.company) return; | ||||
| 		let company_currency = frappe.get_doc(":Company", frm.doc.company).default_currency; | ||||
| 
 | ||||
| 		frm.events.set_current_exchange_rate(frm, "target_exchange_rate", | ||||
| 			frm.doc.paid_to_account_currency, company_currency); | ||||
|  | ||||
| @ -66,7 +66,9 @@ | ||||
|   "tax_withholding_category", | ||||
|   "section_break_56", | ||||
|   "taxes", | ||||
|   "section_break_60", | ||||
|   "base_total_taxes_and_charges", | ||||
|   "column_break_61", | ||||
|   "total_taxes_and_charges", | ||||
|   "deductions_or_loss_section", | ||||
|   "deductions", | ||||
| @ -715,12 +717,21 @@ | ||||
|    "fieldtype": "Data", | ||||
|    "hidden": 1, | ||||
|    "label": "Paid To Account Type" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "column_break_61", | ||||
|    "fieldtype": "Column Break" | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "section_break_60", | ||||
|    "fieldtype": "Section Break", | ||||
|    "hide_border": 1 | ||||
|   } | ||||
|  ], | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-11-24 18:58:24.919764", | ||||
|  "modified": "2022-02-23 20:08:39.559814", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Accounts", | ||||
|  "name": "Payment Entry", | ||||
| @ -763,6 +774,7 @@ | ||||
|  "show_name_in_global_search": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "states": [], | ||||
|  "title_field": "title", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -934,8 +934,12 @@ class PaymentEntry(AccountsController): | ||||
| 
 | ||||
| 			tax.base_total = tax.total * self.source_exchange_rate | ||||
| 
 | ||||
| 			self.total_taxes_and_charges += current_tax_amount | ||||
| 			self.base_total_taxes_and_charges += current_tax_amount * self.source_exchange_rate | ||||
| 			if self.payment_type == 'Pay': | ||||
| 				self.base_total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) | ||||
| 				self.total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) | ||||
| 			else: | ||||
| 				self.base_total_taxes_and_charges += flt(current_tax_amount / self.target_exchange_rate) | ||||
| 				self.total_taxes_and_charges += flt(current_tax_amount / self.source_exchange_rate) | ||||
| 
 | ||||
| 		if self.get('taxes'): | ||||
| 			self.paid_amount_after_tax = self.get('taxes')[-1].base_total | ||||
|  | ||||
| @ -633,6 +633,45 @@ class TestPaymentEntry(unittest.TestCase): | ||||
| 		self.assertEqual(flt(expected_party_balance), party_balance) | ||||
| 		self.assertEqual(flt(expected_party_account_balance), party_account_balance) | ||||
| 
 | ||||
| 	def test_multi_currency_payment_entry_with_taxes(self): | ||||
| 		payment_entry = create_payment_entry(party='_Test Supplier USD', paid_to = '_Test Payable USD - _TC', | ||||
| 			save=True) | ||||
| 		payment_entry.append('taxes', { | ||||
| 			'account_head': '_Test Account Service Tax - _TC', | ||||
| 			'charge_type': 'Actual', | ||||
| 			'tax_amount': 10, | ||||
| 			'add_deduct_tax': 'Add', | ||||
| 			'description': 'Test' | ||||
| 		}) | ||||
| 
 | ||||
| 		payment_entry.save() | ||||
| 		self.assertEqual(payment_entry.base_total_taxes_and_charges, 10) | ||||
| 		self.assertEqual(flt(payment_entry.total_taxes_and_charges, 2), flt(10 / payment_entry.target_exchange_rate, 2)) | ||||
| 
 | ||||
| def create_payment_entry(**args): | ||||
| 	payment_entry = frappe.new_doc('Payment Entry') | ||||
| 	payment_entry.company = args.get('company') or '_Test Company' | ||||
| 	payment_entry.payment_type = args.get('payment_type') or 'Pay' | ||||
| 	payment_entry.party_type = args.get('party_type') or 'Supplier' | ||||
| 	payment_entry.party = args.get('party') or '_Test Supplier' | ||||
| 	payment_entry.paid_from = args.get('paid_from') or '_Test Bank - _TC' | ||||
| 	payment_entry.paid_to = args.get('paid_to') or 'Creditors - _TC' | ||||
| 	payment_entry.paid_amount = args.get('paid_amount') or 1000 | ||||
| 
 | ||||
| 	payment_entry.setup_party_account_field() | ||||
| 	payment_entry.set_missing_values() | ||||
| 	payment_entry.set_exchange_rate() | ||||
| 	payment_entry.received_amount = payment_entry.paid_amount / payment_entry.target_exchange_rate | ||||
| 	payment_entry.reference_no = 'Test001' | ||||
| 	payment_entry.reference_date = nowdate() | ||||
| 
 | ||||
| 	if args.get('save'): | ||||
| 		payment_entry.save() | ||||
| 		if args.get('submit'): | ||||
| 			payment_entry.submit() | ||||
| 
 | ||||
| 	return payment_entry | ||||
| 
 | ||||
| def create_payment_terms_template(): | ||||
| 
 | ||||
| 	create_payment_term('Basic Amount Receivable') | ||||
|  | ||||
| @ -1595,6 +1595,56 @@ class TestSalesInvoice(unittest.TestCase): | ||||
| 			self.assertEqual(expected_values[gle.account][1], gle.debit) | ||||
| 			self.assertEqual(expected_values[gle.account][2], gle.credit) | ||||
| 
 | ||||
| 	def test_rounding_adjustment_3(self): | ||||
| 		si = create_sales_invoice(do_not_save=True) | ||||
| 		si.items = [] | ||||
| 		for d in [(1122, 2), (1122.01, 1), (1122.01, 1)]: | ||||
| 			si.append("items", { | ||||
| 				"item_code": "_Test Item", | ||||
| 				"gst_hsn_code": "999800", | ||||
| 				"warehouse": "_Test Warehouse - _TC", | ||||
| 				"qty": d[1], | ||||
| 				"rate": d[0], | ||||
| 				"income_account": "Sales - _TC", | ||||
| 				"cost_center": "_Test Cost Center - _TC" | ||||
| 			}) | ||||
| 		for tax_account in ["_Test Account VAT - _TC", "_Test Account Service Tax - _TC"]: | ||||
| 			si.append("taxes", { | ||||
| 				"charge_type": "On Net Total", | ||||
| 				"account_head": tax_account, | ||||
| 				"description": tax_account, | ||||
| 				"rate": 6, | ||||
| 				"cost_center": "_Test Cost Center - _TC", | ||||
| 				"included_in_print_rate": 1 | ||||
| 			}) | ||||
| 		si.save() | ||||
| 		si.submit() | ||||
| 		self.assertEqual(si.net_total, 4007.16) | ||||
| 		self.assertEqual(si.grand_total, 4488.02) | ||||
| 		self.assertEqual(si.total_taxes_and_charges, 480.86) | ||||
| 		self.assertEqual(si.rounding_adjustment, -0.02) | ||||
| 
 | ||||
| 		expected_values = dict((d[0], d) for d in [ | ||||
| 			[si.debit_to, 4488.0, 0.0], | ||||
| 			["_Test Account Service Tax - _TC", 0.0, 240.43], | ||||
| 			["_Test Account VAT - _TC", 0.0, 240.43], | ||||
| 			["Sales - _TC", 0.0, 4007.15], | ||||
| 			["Round Off - _TC", 0.01, 0] | ||||
| 		]) | ||||
| 
 | ||||
| 		gl_entries = frappe.db.sql("""select account, debit, credit | ||||
| 			from `tabGL Entry` where voucher_type='Sales Invoice' and voucher_no=%s | ||||
| 			order by account asc""", si.name, as_dict=1) | ||||
| 
 | ||||
| 		debit_credit_diff = 0 | ||||
| 		for gle in gl_entries: | ||||
| 			self.assertEqual(expected_values[gle.account][0], gle.account) | ||||
| 			self.assertEqual(expected_values[gle.account][1], gle.debit) | ||||
| 			self.assertEqual(expected_values[gle.account][2], gle.credit) | ||||
| 			debit_credit_diff += (gle.debit - gle.credit) | ||||
| 
 | ||||
| 		self.assertEqual(debit_credit_diff, 0) | ||||
| 
 | ||||
| 	def test_sales_invoice_with_shipping_rule(self): | ||||
| 		from erpnext.accounts.doctype.shipping_rule.test_shipping_rule import create_shipping_rule | ||||
| 
 | ||||
|  | ||||
| @ -55,5 +55,8 @@ def validate_disabled(doc): | ||||
| 		frappe.throw(_("Disabled template must not be default template")) | ||||
| 
 | ||||
| def validate_for_tax_category(doc): | ||||
| 	if not doc.tax_category: | ||||
| 		return | ||||
| 
 | ||||
| 	if frappe.db.exists(doc.doctype, {"company": doc.company, "tax_category": doc.tax_category, "disabled": 0, "name": ["!=", doc.name]}): | ||||
| 		frappe.throw(_("A template with tax category {0} already exists. Only one template is allowed with each tax category").format(frappe.bold(doc.tax_category))) | ||||
|  | ||||
| @ -274,7 +274,7 @@ def make_round_off_gle(gl_map, debit_credit_diff, precision): | ||||
| 				debit_credit_diff += flt(d.credit) | ||||
| 			round_off_account_exists = True | ||||
| 
 | ||||
| 	if round_off_account_exists and abs(debit_credit_diff) <= (1.0 / (10**precision)): | ||||
| 	if round_off_account_exists and abs(debit_credit_diff) < (1.0 / (10**precision)): | ||||
| 		gl_map.remove(round_off_gle) | ||||
| 		return | ||||
| 
 | ||||
|  | ||||
| @ -316,6 +316,16 @@ class PurchaseOrder(BuyingController): | ||||
| 			'target_ref_field': 'stock_qty', | ||||
| 			'source_field': 'stock_qty' | ||||
| 		}) | ||||
| 		self.status_updater.append({ | ||||
| 			'source_dt': 'Purchase Order Item', | ||||
| 			'target_dt': 'Packed Item', | ||||
| 			'target_field': 'ordered_qty', | ||||
| 			'target_parent_dt': 'Sales Order', | ||||
| 			'target_parent_field': '', | ||||
| 			'join_field': 'sales_order_packed_item', | ||||
| 			'target_ref_field': 'qty', | ||||
| 			'source_field': 'stock_qty' | ||||
| 		}) | ||||
| 
 | ||||
| 	def update_delivered_qty_in_sales_order(self): | ||||
| 		"""Update delivered qty in Sales Order for drop ship""" | ||||
|  | ||||
| @ -63,6 +63,7 @@ | ||||
|   "material_request_item", | ||||
|   "sales_order", | ||||
|   "sales_order_item", | ||||
|   "sales_order_packed_item", | ||||
|   "supplier_quotation", | ||||
|   "supplier_quotation_item", | ||||
|   "col_break5", | ||||
| @ -837,21 +838,30 @@ | ||||
|    "label": "Product Bundle", | ||||
|    "options": "Product Bundle", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "fieldname": "sales_order_packed_item", | ||||
|    "fieldtype": "Data", | ||||
|    "label": "Sales Order Packed Item", | ||||
|    "no_copy": 1, | ||||
|    "print_hide": 1 | ||||
|   } | ||||
|  ], | ||||
|  "idx": 1, | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-08-30 20:06:26.712097", | ||||
|  "modified": "2022-02-02 13:10:18.398976", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Buying", | ||||
|  "name": "Purchase Order Item", | ||||
|  "naming_rule": "Random", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [], | ||||
|  "quick_entry": 1, | ||||
|  "search_fields": "item_name", | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "states": [], | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -507,13 +507,41 @@ class StockController(AccountsController): | ||||
| 			"voucher_no": self.name, | ||||
| 			"company": self.company | ||||
| 		}) | ||||
| 		if future_sle_exists(args): | ||||
| 
 | ||||
| 		if future_sle_exists(args) or repost_required_for_queue(self): | ||||
| 			item_based_reposting =  cint(frappe.db.get_single_value("Stock Reposting Settings", "item_based_reposting")) | ||||
| 			if item_based_reposting: | ||||
| 				create_item_wise_repost_entries(voucher_type=self.doctype, voucher_no=self.name) | ||||
| 			else: | ||||
| 				create_repost_item_valuation_entry(args) | ||||
| 
 | ||||
| def repost_required_for_queue(doc: StockController) -> bool: | ||||
| 	"""check if stock document contains repeated item-warehouse with queue based valuation. | ||||
| 
 | ||||
| 	if queue exists for repeated items then SLEs need to reprocessed in background again. | ||||
| 	""" | ||||
| 
 | ||||
| 	consuming_sles = frappe.db.get_all("Stock Ledger Entry", | ||||
| 		filters={ | ||||
| 			"voucher_type": doc.doctype, | ||||
| 			"voucher_no": doc.name, | ||||
| 			"actual_qty": ("<", 0), | ||||
| 			"is_cancelled": 0 | ||||
| 		}, | ||||
| 		fields=["item_code", "warehouse", "stock_queue"] | ||||
| 	) | ||||
| 	item_warehouses = [(sle.item_code, sle.warehouse) for sle in consuming_sles] | ||||
| 
 | ||||
| 	unique_item_warehouses = set(item_warehouses) | ||||
| 
 | ||||
| 	if len(unique_item_warehouses) == len(item_warehouses): | ||||
| 		return False | ||||
| 
 | ||||
| 	for sle in consuming_sles: | ||||
| 		if sle.stock_queue != "[]":  # using FIFO/LIFO valuation | ||||
| 			return True | ||||
| 	return False | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def make_quality_inspections(doctype, docname, items): | ||||
|  | ||||
| @ -2,7 +2,7 @@ | ||||
|  "actions": [], | ||||
|  "allow_import": 1, | ||||
|  "autoname": "naming_series:", | ||||
|  "creation": "2017-10-09 14:26:29.612365", | ||||
|  "creation": "2022-01-17 18:36:51.450395", | ||||
|  "doctype": "DocType", | ||||
|  "editable_grid": 1, | ||||
|  "engine": "InnoDB", | ||||
| @ -121,7 +121,7 @@ | ||||
|    "fieldtype": "Select", | ||||
|    "label": "Status", | ||||
|    "no_copy": 1, | ||||
|    "options": "Draft\nPaid\nUnpaid\nClaimed\nCancelled", | ||||
|    "options": "Draft\nPaid\nUnpaid\nClaimed\nReturned\nPartly Claimed and Returned\nCancelled", | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
| @ -200,7 +200,7 @@ | ||||
|  ], | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-09-11 18:38:38.617478", | ||||
|  "modified": "2022-01-17 19:33:52.345823", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "HR", | ||||
|  "name": "Employee Advance", | ||||
| @ -237,5 +237,41 @@ | ||||
|  "search_fields": "employee,employee_name", | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "states": [ | ||||
|   { | ||||
|    "color": "Red", | ||||
|    "custom": 1, | ||||
|    "title": "Draft" | ||||
|   }, | ||||
|   { | ||||
|    "color": "Green", | ||||
|    "custom": 1, | ||||
|    "title": "Paid" | ||||
|   }, | ||||
|   { | ||||
|    "color": "Orange", | ||||
|    "custom": 1, | ||||
|    "title": "Unpaid" | ||||
|   }, | ||||
|   { | ||||
|    "color": "Blue", | ||||
|    "custom": 1, | ||||
|    "title": "Claimed" | ||||
|   }, | ||||
|   { | ||||
|    "color": "Gray", | ||||
|    "title": "Returned" | ||||
|   }, | ||||
|   { | ||||
|    "color": "Yellow", | ||||
|    "title": "Partly Claimed and Returned" | ||||
|   }, | ||||
|   { | ||||
|    "color": "Red", | ||||
|    "custom": 1, | ||||
|    "title": "Cancelled" | ||||
|   } | ||||
|  ], | ||||
|  "title_field": "employee_name", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -27,19 +27,33 @@ class EmployeeAdvance(Document): | ||||
| 
 | ||||
| 	def on_cancel(self): | ||||
| 		self.ignore_linked_doctypes = ('GL Entry') | ||||
| 		self.set_status(update=True) | ||||
| 
 | ||||
| 	def set_status(self, update=False): | ||||
| 		precision = self.precision("paid_amount") | ||||
| 		total_amount = flt(flt(self.claimed_amount) + flt(self.return_amount), precision) | ||||
| 		status = None | ||||
| 
 | ||||
| 	def set_status(self): | ||||
| 		if self.docstatus == 0: | ||||
| 			self.status = "Draft" | ||||
| 		if self.docstatus == 1: | ||||
| 			if self.claimed_amount and flt(self.claimed_amount) == flt(self.paid_amount): | ||||
| 				self.status = "Claimed" | ||||
| 			elif self.paid_amount and self.advance_amount == flt(self.paid_amount): | ||||
| 				self.status = "Paid" | ||||
| 			status = "Draft" | ||||
| 		elif self.docstatus == 1: | ||||
| 			if flt(self.claimed_amount) > 0 and flt(self.claimed_amount, precision) == flt(self.paid_amount, precision): | ||||
| 				status = "Claimed" | ||||
| 			elif flt(self.return_amount) > 0 and flt(self.return_amount, precision) == flt(self.paid_amount, precision): | ||||
| 				status = "Returned" | ||||
| 			elif flt(self.claimed_amount) > 0 and (flt(self.return_amount) > 0) and total_amount == flt(self.paid_amount, precision): | ||||
| 				status = "Partly Claimed and Returned" | ||||
| 			elif flt(self.paid_amount) > 0 and flt(self.advance_amount, precision) == flt(self.paid_amount, precision): | ||||
| 				status = "Paid" | ||||
| 			else: | ||||
| 				self.status = "Unpaid" | ||||
| 				status = "Unpaid" | ||||
| 		elif self.docstatus == 2: | ||||
| 			self.status = "Cancelled" | ||||
| 			status = "Cancelled" | ||||
| 
 | ||||
| 		if update: | ||||
| 			self.db_set("status", status) | ||||
| 		else: | ||||
| 			self.status = status | ||||
| 
 | ||||
| 	def set_total_advance_paid(self): | ||||
| 		gle = frappe.qb.DocType("GL Entry") | ||||
| @ -85,9 +99,7 @@ class EmployeeAdvance(Document): | ||||
| 
 | ||||
| 		self.db_set("paid_amount", paid_amount) | ||||
| 		self.db_set("return_amount", return_amount) | ||||
| 		self.set_status() | ||||
| 		frappe.db.set_value("Employee Advance", self.name , "status", self.status) | ||||
| 
 | ||||
| 		self.set_status(update=True) | ||||
| 
 | ||||
| 	def update_claimed_amount(self): | ||||
| 		claimed_amount = frappe.db.sql(""" | ||||
| @ -103,8 +115,8 @@ class EmployeeAdvance(Document): | ||||
| 
 | ||||
| 		frappe.db.set_value("Employee Advance", self.name, "claimed_amount", flt(claimed_amount)) | ||||
| 		self.reload() | ||||
| 		self.set_status() | ||||
| 		frappe.db.set_value("Employee Advance", self.name, "status", self.status) | ||||
| 		self.set_status(update=True) | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_pending_amount(employee, posting_date): | ||||
| @ -222,7 +234,8 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, | ||||
| 		'reference_name': employee_advance_name, | ||||
| 		'party_type': 'Employee', | ||||
| 		'party': employee, | ||||
| 		'is_advance': 'Yes' | ||||
| 		'is_advance': 'Yes', | ||||
| 		'cost_center': erpnext.get_default_cost_center(company) | ||||
| 	}) | ||||
| 
 | ||||
| 	bank_amount = flt(return_amount) if bank_cash_account.account_currency==currency \ | ||||
| @ -233,7 +246,8 @@ def make_return_entry(employee, company, employee_advance_name, return_amount, | ||||
| 		"debit_in_account_currency": bank_amount, | ||||
| 		"account_currency": bank_cash_account.account_currency, | ||||
| 		"account_type": bank_cash_account.account_type, | ||||
| 		"exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1 | ||||
| 		"exchange_rate": flt(exchange_rate) if bank_cash_account.account_currency == currency else 1, | ||||
| 		"cost_center": erpnext.get_default_cost_center(company) | ||||
| 	}) | ||||
| 
 | ||||
| 	return je.as_dict() | ||||
|  | ||||
| @ -4,7 +4,7 @@ | ||||
| import unittest | ||||
| 
 | ||||
| import frappe | ||||
| from frappe.utils import nowdate | ||||
| from frappe.utils import flt, nowdate | ||||
| 
 | ||||
| import erpnext | ||||
| from erpnext.hr.doctype.employee.test_employee import make_employee | ||||
| @ -12,12 +12,21 @@ from erpnext.hr.doctype.employee_advance.employee_advance import ( | ||||
| 	EmployeeAdvanceOverPayment, | ||||
| 	create_return_through_additional_salary, | ||||
| 	make_bank_entry, | ||||
| 	make_return_entry, | ||||
| ) | ||||
| from erpnext.hr.doctype.expense_claim.expense_claim import get_advances | ||||
| from erpnext.hr.doctype.expense_claim.test_expense_claim import ( | ||||
| 	get_payable_account, | ||||
| 	make_expense_claim, | ||||
| ) | ||||
| from erpnext.payroll.doctype.salary_component.test_salary_component import create_salary_component | ||||
| from erpnext.payroll.doctype.salary_structure.test_salary_structure import make_salary_structure | ||||
| 
 | ||||
| 
 | ||||
| class TestEmployeeAdvance(unittest.TestCase): | ||||
| 	def setUp(self): | ||||
| 		frappe.db.delete("Employee Advance") | ||||
| 
 | ||||
| 	def test_paid_amount_and_status(self): | ||||
| 		employee_name = make_employee("_T@employe.advance") | ||||
| 		advance = make_employee_advance(employee_name) | ||||
| @ -52,9 +61,102 @@ class TestEmployeeAdvance(unittest.TestCase): | ||||
| 		self.assertEqual(advance.paid_amount, 0) | ||||
| 		self.assertEqual(advance.status, "Unpaid") | ||||
| 
 | ||||
| 		advance.cancel() | ||||
| 		advance.reload() | ||||
| 		self.assertEqual(advance.status, "Cancelled") | ||||
| 
 | ||||
| 	def test_claimed_status(self): | ||||
| 		# CLAIMED Status check, full amount claimed | ||||
| 		payable_account = get_payable_account("_Test Company") | ||||
| 		claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) | ||||
| 
 | ||||
| 		advance = make_employee_advance(claim.employee) | ||||
| 		pe = make_payment_entry(advance) | ||||
| 		pe.submit() | ||||
| 
 | ||||
| 		claim = get_advances_for_claim(claim, advance.name) | ||||
| 		claim.save() | ||||
| 		claim.submit() | ||||
| 
 | ||||
| 		advance.reload() | ||||
| 		self.assertEqual(advance.claimed_amount, 1000) | ||||
| 		self.assertEqual(advance.status, "Claimed") | ||||
| 
 | ||||
| 		# advance should not be shown in claims | ||||
| 		advances = get_advances(claim.employee) | ||||
| 		advances = [entry.name for entry in advances] | ||||
| 		self.assertTrue(advance.name not in advances) | ||||
| 
 | ||||
| 		# cancel claim; status should be Paid | ||||
| 		claim.cancel() | ||||
| 		advance.reload() | ||||
| 		self.assertEqual(advance.claimed_amount, 0) | ||||
| 		self.assertEqual(advance.status, "Paid") | ||||
| 
 | ||||
| 	def test_partly_claimed_and_returned_status(self): | ||||
| 		payable_account = get_payable_account("_Test Company") | ||||
| 		claim = make_expense_claim(payable_account, 1000, 1000, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) | ||||
| 
 | ||||
| 		advance = make_employee_advance(claim.employee) | ||||
| 		pe = make_payment_entry(advance) | ||||
| 		pe.submit() | ||||
| 
 | ||||
| 		# PARTLY CLAIMED AND RETURNED status check | ||||
| 		# 500 Claimed, 500 Returned | ||||
| 		claim = make_expense_claim(payable_account, 500, 500, "_Test Company", "Travel Expenses - _TC", do_not_submit=True) | ||||
| 
 | ||||
| 		advance = make_employee_advance(claim.employee) | ||||
| 		pe = make_payment_entry(advance) | ||||
| 		pe.submit() | ||||
| 
 | ||||
| 		claim = get_advances_for_claim(claim, advance.name, amount=500) | ||||
| 		claim.save() | ||||
| 		claim.submit() | ||||
| 
 | ||||
| 		advance.reload() | ||||
| 		self.assertEqual(advance.claimed_amount, 500) | ||||
| 		self.assertEqual(advance.status, "Paid") | ||||
| 
 | ||||
| 		entry = make_return_entry( | ||||
| 			employee=advance.employee, | ||||
| 			company=advance.company, | ||||
| 			employee_advance_name=advance.name, | ||||
| 			return_amount=flt(advance.paid_amount - advance.claimed_amount), | ||||
| 			advance_account=advance.advance_account, | ||||
| 			mode_of_payment=advance.mode_of_payment, | ||||
| 			currency=advance.currency, | ||||
| 			exchange_rate=advance.exchange_rate | ||||
| 		) | ||||
| 
 | ||||
| 		entry = frappe.get_doc(entry) | ||||
| 		entry.insert() | ||||
| 		entry.submit() | ||||
| 
 | ||||
| 		advance.reload() | ||||
| 		self.assertEqual(advance.return_amount, 500) | ||||
| 		self.assertEqual(advance.status, "Partly Claimed and Returned") | ||||
| 
 | ||||
| 		# advance should not be shown in claims | ||||
| 		advances = get_advances(claim.employee) | ||||
| 		advances = [entry.name for entry in advances] | ||||
| 		self.assertTrue(advance.name not in advances) | ||||
| 
 | ||||
| 		# Cancel return entry; status should change to PAID | ||||
| 		entry.cancel() | ||||
| 		advance.reload() | ||||
| 		self.assertEqual(advance.return_amount, 0) | ||||
| 		self.assertEqual(advance.status, "Paid") | ||||
| 
 | ||||
| 		# advance should be shown in claims | ||||
| 		advances = get_advances(claim.employee) | ||||
| 		advances = [entry.name for entry in advances] | ||||
| 		self.assertTrue(advance.name in advances) | ||||
| 
 | ||||
| 	def test_repay_unclaimed_amount_from_salary(self): | ||||
| 		employee_name = make_employee("_T@employe.advance") | ||||
| 		advance = make_employee_advance(employee_name, {"repay_unclaimed_amount_from_salary": 1}) | ||||
| 		pe = make_payment_entry(advance) | ||||
| 		pe.submit() | ||||
| 
 | ||||
| 		args = {"type": "Deduction"} | ||||
| 		create_salary_component("Advance Salary - Deduction", **args) | ||||
| @ -82,11 +184,13 @@ class TestEmployeeAdvance(unittest.TestCase): | ||||
| 
 | ||||
| 		advance.reload() | ||||
| 		self.assertEqual(advance.return_amount, 1000) | ||||
| 		self.assertEqual(advance.status, "Returned") | ||||
| 
 | ||||
| 		# update advance return amount on additional salary cancellation | ||||
| 		additional_salary.cancel() | ||||
| 		advance.reload() | ||||
| 		self.assertEqual(advance.return_amount, 700) | ||||
| 		self.assertEqual(advance.status, "Paid") | ||||
| 
 | ||||
| 	def tearDown(self): | ||||
| 		frappe.db.rollback() | ||||
| @ -118,3 +222,24 @@ def make_employee_advance(employee_name, args=None): | ||||
| 	doc.submit() | ||||
| 
 | ||||
| 	return doc | ||||
| 
 | ||||
| 
 | ||||
| def get_advances_for_claim(claim, advance_name, amount=None): | ||||
| 	advances = get_advances(claim.employee, advance_name) | ||||
| 
 | ||||
| 	for entry in advances: | ||||
| 		if amount: | ||||
| 			allocated_amount = amount | ||||
| 		else: | ||||
| 			allocated_amount = flt(entry.paid_amount) - flt(entry.claimed_amount) | ||||
| 
 | ||||
| 		claim.append("advances", { | ||||
| 			"employee_advance": entry.name, | ||||
| 			"posting_date": entry.posting_date, | ||||
| 			"advance_account": entry.advance_account, | ||||
| 			"advance_paid": entry.paid_amount, | ||||
| 			"unclaimed_amount": allocated_amount, | ||||
| 			"allocated_amount": allocated_amount | ||||
| 		}) | ||||
| 
 | ||||
| 	return claim | ||||
| @ -171,7 +171,7 @@ frappe.ui.form.on("Expense Claim", { | ||||
| 					['docstatus', '=', 1], | ||||
| 					['employee', '=', frm.doc.employee], | ||||
| 					['paid_amount', '>', 0], | ||||
| 					['status', '!=', 'Claimed'] | ||||
| 					['status', 'not in', ['Claimed', 'Returned', 'Partly Claimed and Returned']] | ||||
| 				] | ||||
| 			}; | ||||
| 		}); | ||||
|  | ||||
| @ -23,10 +23,10 @@ class ExpenseClaim(AccountsController): | ||||
| 
 | ||||
| 	def validate(self): | ||||
| 		validate_active_employee(self.employee) | ||||
| 		self.validate_advances() | ||||
| 		set_employee_name(self) | ||||
| 		self.validate_sanctioned_amount() | ||||
| 		self.calculate_total_amount() | ||||
| 		set_employee_name(self) | ||||
| 		self.validate_advances() | ||||
| 		self.set_expense_account(validate=True) | ||||
| 		self.set_payable_account() | ||||
| 		self.set_cost_center() | ||||
| @ -341,18 +341,27 @@ def get_expense_claim_account(expense_claim_type, company): | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_advances(employee, advance_id=None): | ||||
| 	if not advance_id: | ||||
| 		condition = 'docstatus=1 and employee={0} and paid_amount > 0 and paid_amount > claimed_amount + return_amount'.format(frappe.db.escape(employee)) | ||||
| 	else: | ||||
| 		condition = 'name={0}'.format(frappe.db.escape(advance_id)) | ||||
| 	advance = frappe.qb.DocType("Employee Advance") | ||||
| 
 | ||||
| 	return frappe.db.sql(""" | ||||
| 		select | ||||
| 			name, posting_date, paid_amount, claimed_amount, advance_account | ||||
| 		from | ||||
| 			`tabEmployee Advance` | ||||
| 		where {0} | ||||
| 	""".format(condition), as_dict=1) | ||||
| 	query = ( | ||||
| 		frappe.qb.from_(advance) | ||||
| 		.select( | ||||
| 			advance.name, advance.posting_date, advance.paid_amount, | ||||
| 			advance.claimed_amount, advance.advance_account | ||||
| 		) | ||||
| 	) | ||||
| 
 | ||||
| 	if not advance_id: | ||||
| 		query = query.where( | ||||
| 			(advance.docstatus == 1) | ||||
| 			& (advance.employee == employee) | ||||
| 			& (advance.paid_amount > 0) | ||||
| 			& (advance.status.notin(["Claimed", "Returned", "Partly Claimed and Returned"])) | ||||
| 		) | ||||
| 	else: | ||||
| 		query = query.where(advance.name == advance_id) | ||||
| 
 | ||||
| 	return query.run(as_dict=True) | ||||
| 
 | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
|  | ||||
| @ -918,7 +918,7 @@ def validate_bom_no(item, bom_no): | ||||
| 			frappe.throw(_("BOM {0} does not belong to Item {1}").format(bom_no, item)) | ||||
| 
 | ||||
| @frappe.whitelist() | ||||
| def get_children(doctype, parent=None, is_root=False, **filters): | ||||
| def get_children(parent=None, is_root=False, **filters): | ||||
| 	if not parent or parent=="BOM": | ||||
| 		frappe.msgprint(_('Please select a BOM')) | ||||
| 		return | ||||
|  | ||||
| @ -7,7 +7,7 @@ def get_data(): | ||||
| 		'transactions': [ | ||||
| 			{ | ||||
| 				'label': _('Manufacture'), | ||||
| 				'items': ['BOM', 'Work Order', 'Job Card', 'Timesheet'] | ||||
| 				'items': ['BOM', 'Work Order', 'Job Card'] | ||||
| 			} | ||||
| 		] | ||||
| 	} | ||||
|  | ||||
| @ -232,7 +232,7 @@ frappe.ui.form.on('Production Plan', { | ||||
| 		}); | ||||
| 	}, | ||||
| 	combine_items: function (frm) { | ||||
| 		frm.clear_table('prod_plan_references'); | ||||
| 		frm.clear_table("prod_plan_references"); | ||||
| 
 | ||||
| 		frappe.call({ | ||||
| 			method: "get_items", | ||||
| @ -247,6 +247,13 @@ frappe.ui.form.on('Production Plan', { | ||||
| 		}); | ||||
| 	}, | ||||
| 
 | ||||
| 	combine_sub_items: (frm) => { | ||||
| 		if (frm.doc.sub_assembly_items.length > 0) { | ||||
| 			frm.clear_table("sub_assembly_items"); | ||||
| 			frm.trigger("get_sub_assembly_items"); | ||||
| 		} | ||||
| 	}, | ||||
| 
 | ||||
| 	get_sub_assembly_items: function(frm) { | ||||
| 		frm.dirty(); | ||||
| 
 | ||||
|  | ||||
| @ -36,6 +36,7 @@ | ||||
|   "prod_plan_references", | ||||
|   "section_break_24", | ||||
|   "get_sub_assembly_items", | ||||
|   "combine_sub_items", | ||||
|   "sub_assembly_items", | ||||
|   "material_request_planning", | ||||
|   "include_non_stock_items", | ||||
| @ -340,7 +341,6 @@ | ||||
|   { | ||||
|    "fieldname": "prod_plan_references", | ||||
|    "fieldtype": "Table", | ||||
|    "hidden": 1, | ||||
|    "label": "Production Plan Item Reference", | ||||
|    "options": "Production Plan Item Reference" | ||||
|   }, | ||||
| @ -370,16 +370,23 @@ | ||||
|    "fieldname": "to_delivery_date", | ||||
|    "fieldtype": "Date", | ||||
|    "label": "To Delivery Date" | ||||
|   }, | ||||
|   { | ||||
|    "default": "0", | ||||
|    "fieldname": "combine_sub_items", | ||||
|    "fieldtype": "Check", | ||||
|    "label": "Consolidate Sub Assembly Items" | ||||
|   } | ||||
|  ], | ||||
|  "icon": "fa fa-calendar", | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "is_submittable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-09-06 18:35:59.642232", | ||||
|  "modified": "2022-02-23 17:16:10.629378", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Manufacturing", | ||||
|  "name": "Production Plan", | ||||
|  "naming_rule": "By \"Naming Series\" field", | ||||
|  "owner": "Administrator", | ||||
|  "permissions": [ | ||||
|   { | ||||
|  | ||||
| @ -21,7 +21,8 @@ from frappe.utils import ( | ||||
| ) | ||||
| from frappe.utils.csvutils import build_csv_response | ||||
| 
 | ||||
| from erpnext.manufacturing.doctype.bom.bom import get_children, validate_bom_no | ||||
| from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children | ||||
| from erpnext.manufacturing.doctype.bom.bom import validate_bom_no | ||||
| from erpnext.manufacturing.doctype.work_order.work_order import get_item_details | ||||
| from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults | ||||
| 
 | ||||
| @ -570,17 +571,28 @@ class ProductionPlan(Document): | ||||
| 
 | ||||
| 	@frappe.whitelist() | ||||
| 	def get_sub_assembly_items(self, manufacturing_type=None): | ||||
| 		"Fetch sub assembly items and optionally combine them." | ||||
| 		self.sub_assembly_items = [] | ||||
| 		sub_assembly_items_store = [] # temporary store to process all subassembly items | ||||
| 
 | ||||
| 		for row in self.po_items: | ||||
| 			bom_data = [] | ||||
| 			get_sub_assembly_items(row.bom_no, bom_data, row.planned_qty) | ||||
| 			self.set_sub_assembly_items_based_on_level(row, bom_data, manufacturing_type) | ||||
| 			sub_assembly_items_store.extend(bom_data) | ||||
| 
 | ||||
| 		self.sub_assembly_items.sort(key= lambda d: d.bom_level, reverse=True) | ||||
| 		for idx, row in enumerate(self.sub_assembly_items, start=1): | ||||
| 			row.idx = idx | ||||
| 		if self.combine_sub_items: | ||||
| 			# Combine subassembly items | ||||
| 			sub_assembly_items_store = self.combine_subassembly_items(sub_assembly_items_store) | ||||
| 
 | ||||
| 		sub_assembly_items_store.sort(key= lambda d: d.bom_level, reverse=True) # sort by bom level | ||||
| 
 | ||||
| 		for idx, row in enumerate(sub_assembly_items_store): | ||||
| 			row.idx = idx + 1 | ||||
| 			self.append("sub_assembly_items", row) | ||||
| 
 | ||||
| 	def set_sub_assembly_items_based_on_level(self, row, bom_data, manufacturing_type=None): | ||||
| 		"Modify bom_data, set additional details." | ||||
| 		for data in bom_data: | ||||
| 			data.qty = data.stock_qty | ||||
| 			data.production_plan_item = row.name | ||||
| @ -589,7 +601,32 @@ class ProductionPlan(Document): | ||||
| 			data.type_of_manufacturing = manufacturing_type or ("Subcontract" if data.is_sub_contracted_item | ||||
| 				else "In House") | ||||
| 
 | ||||
| 			self.append("sub_assembly_items", data) | ||||
| 	def combine_subassembly_items(self, sub_assembly_items_store): | ||||
| 		"Aggregate if same: Item, Warehouse, Inhouse/Outhouse Manu.g, BOM No." | ||||
| 		key_wise_data = {} | ||||
| 		for row in sub_assembly_items_store: | ||||
| 			key = ( | ||||
| 				row.get("production_item"), row.get("fg_warehouse"), | ||||
| 				row.get("bom_no"), row.get("type_of_manufacturing") | ||||
| 			) | ||||
| 			if key not in key_wise_data: | ||||
| 				# intialise (item, wh, bom no, man.g type) wise dict | ||||
| 				key_wise_data[key] = row | ||||
| 				continue | ||||
| 
 | ||||
| 			existing_row = key_wise_data[key] | ||||
| 			if existing_row: | ||||
| 				# if row with same (item, wh, bom no, man.g type) key, merge | ||||
| 				existing_row.qty += flt(row.qty) | ||||
| 				existing_row.stock_qty += flt(row.stock_qty) | ||||
| 				existing_row.bom_level = max(existing_row.bom_level, row.bom_level) | ||||
| 				continue | ||||
| 			else: | ||||
| 				# add row with key | ||||
| 				key_wise_data[key] = row | ||||
| 
 | ||||
| 		sub_assembly_items_store = [key_wise_data[key] for key in key_wise_data] # unpack into single level list | ||||
| 		return sub_assembly_items_store | ||||
| 
 | ||||
| 	def all_items_completed(self): | ||||
| 		all_items_produced = all(flt(d.planned_qty) - flt(d.produced_qty) < 0.000001 | ||||
| @ -1031,7 +1068,7 @@ def get_item_data(item_code): | ||||
| 	} | ||||
| 
 | ||||
| def get_sub_assembly_items(bom_no, bom_data, to_produce_qty, indent=0): | ||||
| 	data = get_children('BOM', parent = bom_no) | ||||
| 	data = get_bom_children(parent=bom_no) | ||||
| 	for d in data: | ||||
| 		if d.expandable: | ||||
| 			parent_item_code = frappe.get_cached_value("BOM", bom_no, "item") | ||||
|  | ||||
| @ -38,6 +38,9 @@ class TestProductionPlan(FrappeTestCase): | ||||
| 			if not frappe.db.get_value('BOM', {'item': item}): | ||||
| 				make_bom(item = item, raw_materials = raw_materials) | ||||
| 
 | ||||
| 	def tearDown(self) -> None: | ||||
| 		frappe.db.rollback() | ||||
| 
 | ||||
| 	def test_production_plan_mr_creation(self): | ||||
| 		"Test if MRs are created for unavailable raw materials." | ||||
| 		pln = create_production_plan(item_code='Test Production Item 1') | ||||
| @ -110,7 +113,7 @@ class TestProductionPlan(FrappeTestCase): | ||||
| 			item_code='Test Production Item 1', | ||||
| 			ignore_existing_ordered_qty=1 | ||||
| 		) | ||||
| 		self.assertTrue(len(pln.mr_items), 1) | ||||
| 		self.assertTrue(len(pln.mr_items)) | ||||
| 		self.assertTrue(flt(pln.mr_items[0].quantity), 1.0) | ||||
| 
 | ||||
| 		sr1.cancel() | ||||
| @ -151,7 +154,7 @@ class TestProductionPlan(FrappeTestCase): | ||||
| 			use_multi_level_bom=0, | ||||
| 			ignore_existing_ordered_qty=0 | ||||
| 		) | ||||
| 		self.assertTrue(len(pln.mr_items), 0) | ||||
| 		self.assertFalse(len(pln.mr_items)) | ||||
| 
 | ||||
| 		sr1.cancel() | ||||
| 		sr2.cancel() | ||||
| @ -258,6 +261,51 @@ class TestProductionPlan(FrappeTestCase): | ||||
| 		pln.reload() | ||||
| 		pln.cancel() | ||||
| 
 | ||||
| 	def test_production_plan_combine_subassembly(self): | ||||
| 		""" | ||||
| 		Test combining Sub assembly items belonging to the same BOM in Prod Plan. | ||||
| 		1) Red-Car -> Wheel (sub assembly) > BOM-WHEEL-001 | ||||
| 		2) Green-Car -> Wheel (sub assembly) > BOM-WHEEL-001 | ||||
| 		""" | ||||
| 		from erpnext.manufacturing.doctype.bom.test_bom import create_nested_bom | ||||
| 
 | ||||
| 		bom_tree_1 = { | ||||
| 			"Red-Car": {"Wheel": {"Rubber": {}}} | ||||
| 		} | ||||
| 		bom_tree_2 = { | ||||
| 			"Green-Car": {"Wheel": {"Rubber": {}}} | ||||
| 		} | ||||
| 
 | ||||
| 		parent_bom_1 = create_nested_bom(bom_tree_1, prefix="") | ||||
| 		parent_bom_2 = create_nested_bom(bom_tree_2, prefix="") | ||||
| 
 | ||||
| 		# make sure both boms use same subassembly bom | ||||
| 		subassembly_bom = parent_bom_1.items[0].bom_no | ||||
| 		frappe.db.set_value("BOM Item", parent_bom_2.items[0].name, "bom_no", subassembly_bom) | ||||
| 
 | ||||
| 		plan = create_production_plan(item_code="Red-Car", use_multi_level_bom=1, do_not_save=True) | ||||
| 		plan.append("po_items", { # Add Green-Car to Prod Plan | ||||
| 			'use_multi_level_bom': 1, | ||||
| 			'item_code': "Green-Car", | ||||
| 			'bom_no': frappe.db.get_value('Item', "Green-Car", 'default_bom'), | ||||
| 			'planned_qty': 1, | ||||
| 			'planned_start_date': now_datetime() | ||||
| 		}) | ||||
| 		plan.get_sub_assembly_items() | ||||
| 		self.assertTrue(len(plan.sub_assembly_items), 2) | ||||
| 
 | ||||
| 		plan.combine_sub_items = 1 | ||||
| 		plan.get_sub_assembly_items() | ||||
| 
 | ||||
| 		self.assertTrue(len(plan.sub_assembly_items), 1) # check if sub-assembly items merged | ||||
| 		self.assertEqual(plan.sub_assembly_items[0].qty, 2.0) | ||||
| 		self.assertEqual(plan.sub_assembly_items[0].stock_qty, 2.0) | ||||
| 
 | ||||
| 		# change warehouse in one row, sub-assemblies should not merge | ||||
| 		plan.po_items[0].warehouse = "Finished Goods - _TC" | ||||
| 		plan.get_sub_assembly_items() | ||||
| 		self.assertTrue(len(plan.sub_assembly_items), 2) | ||||
| 
 | ||||
| 	def test_pp_to_mr_customer_provided(self): | ||||
| 		" Test Material Request from Production Plan for Customer Provided Item." | ||||
| 		create_item('CUST-0987', is_customer_provided_item = 1, customer = '_Test Customer', is_purchase_item = 0) | ||||
| @ -532,6 +580,7 @@ class TestProductionPlan(FrappeTestCase): | ||||
| 			wip_warehouse='Work In Progress - _TC', | ||||
| 			fg_warehouse='Finished Goods - _TC', | ||||
| 			skip_transfer=1, | ||||
| 			use_multi_level_bom=1, | ||||
| 			do_not_submit=True | ||||
| 		) | ||||
| 		wo.production_plan = pln.name | ||||
| @ -576,6 +625,7 @@ class TestProductionPlan(FrappeTestCase): | ||||
| 			wip_warehouse='Work In Progress - _TC', | ||||
| 			fg_warehouse='Finished Goods - _TC', | ||||
| 			skip_transfer=1, | ||||
| 			use_multi_level_bom=1, | ||||
| 			do_not_submit=True | ||||
| 		) | ||||
| 		wo.production_plan = pln.name | ||||
|  | ||||
| @ -17,7 +17,7 @@ frappe.ui.form.on('Routing', { | ||||
| 	}, | ||||
| 
 | ||||
| 	calculate_operating_cost: function(frm, child) { | ||||
| 		const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, 2); | ||||
| 		const operating_cost = flt(flt(child.hour_rate) * flt(child.time_in_mins) / 60, precision("operating_cost", child)); | ||||
| 		frappe.model.set_value(child.doctype, child.name, "operating_cost", operating_cost); | ||||
| 	} | ||||
| }); | ||||
|  | ||||
| @ -20,7 +20,8 @@ class Routing(Document): | ||||
| 		for operation in self.operations: | ||||
| 			if not operation.hour_rate: | ||||
| 				operation.hour_rate = frappe.db.get_value("Workstation", operation.workstation, 'hour_rate') | ||||
| 			operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, 2) | ||||
| 			operation.operating_cost = flt(flt(operation.hour_rate) * flt(operation.time_in_mins) / 60, | ||||
| 					operation.precision("operating_cost")) | ||||
| 
 | ||||
| 	def set_routing_id(self): | ||||
| 		sequence_id = 0 | ||||
|  | ||||
| @ -1040,7 +1040,7 @@ def make_wo_order_test_record(**args): | ||||
| 	wo_order.scrap_warehouse = args.fg_warehouse or "_Test Scrap Warehouse - _TC" | ||||
| 	wo_order.company = args.company or "_Test Company" | ||||
| 	wo_order.stock_uom = args.stock_uom or "_Test UOM" | ||||
| 	wo_order.use_multi_level_bom=0 | ||||
| 	wo_order.use_multi_level_bom= args.use_multi_level_bom or 0 | ||||
| 	wo_order.skip_transfer=args.skip_transfer or 0 | ||||
| 	wo_order.get_items_and_operations_from_bom() | ||||
| 	wo_order.sales_order = args.sales_order or None | ||||
|  | ||||
| @ -11,9 +11,9 @@ def get_data(): | ||||
| 			}, | ||||
| 			{ | ||||
| 				'label': _('Transaction'), | ||||
| 				'items': ['Work Order', 'Job Card', 'Timesheet'] | ||||
| 				'items': ['Work Order', 'Job Card',] | ||||
| 			} | ||||
| 		], | ||||
| 		'disable_create_buttons': ['BOM', 'Routing', 'Operation', | ||||
| 			'Work Order', 'Job Card', 'Timesheet'] | ||||
| 			'Work Order', 'Job Card',] | ||||
| 	} | ||||
|  | ||||
| @ -356,4 +356,5 @@ erpnext.patches.v14_0.delete_amazon_mws_doctype | ||||
| erpnext.patches.v13_0.set_work_order_qty_in_so_from_mr | ||||
| erpnext.patches.v13_0.update_accounts_in_loan_docs | ||||
| erpnext.patches.v14_0.update_batch_valuation_flag | ||||
| erpnext.patches.v14_0.delete_non_profit_doctypes | ||||
| erpnext.patches.v14_0.delete_non_profit_doctypes | ||||
| erpnext.patches.v14_0.update_employee_advance_status | ||||
							
								
								
									
										26
									
								
								erpnext/patches/v14_0/update_employee_advance_status.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								erpnext/patches/v14_0/update_employee_advance_status.py
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,26 @@ | ||||
| import frappe | ||||
| 
 | ||||
| 
 | ||||
| def execute(): | ||||
| 	frappe.reload_doc('hr', 'doctype', 'employee_advance') | ||||
| 
 | ||||
| 	advance = frappe.qb.DocType('Employee Advance') | ||||
| 	(frappe.qb | ||||
| 		.update(advance) | ||||
| 		.set(advance.status, 'Returned') | ||||
| 		.where( | ||||
| 			(advance.docstatus == 1) | ||||
| 			& ((advance.return_amount) & (advance.paid_amount == advance.return_amount)) | ||||
| 			& (advance.status == 'Paid') | ||||
| 		) | ||||
| 	).run() | ||||
| 
 | ||||
| 	(frappe.qb | ||||
| 		.update(advance) | ||||
| 		.set(advance.status, 'Partly Claimed and Returned') | ||||
| 		.where( | ||||
| 			(advance.docstatus == 1) | ||||
| 			& ((advance.claimed_amount & advance.return_amount) & (advance.paid_amount == (advance.return_amount + advance.claimed_amount))) | ||||
| 			& (advance.status == 'Paid') | ||||
| 		) | ||||
| 	).run() | ||||
| @ -105,6 +105,8 @@ class AdditionalSalary(Document): | ||||
| 				return_amount += self.amount | ||||
| 
 | ||||
| 			frappe.db.set_value("Employee Advance", self.ref_docname, "return_amount", return_amount) | ||||
| 			advance = frappe.get_doc("Employee Advance", self.ref_docname) | ||||
| 			advance.set_status(update=True) | ||||
| 
 | ||||
| 	def update_employee_referral(self, cancel=False): | ||||
| 		if self.ref_doctype == "Employee Referral": | ||||
|  | ||||
| @ -116,7 +116,7 @@ frappe.ui.form.on("Timesheet", { | ||||
| 
 | ||||
| 	currency: function(frm) { | ||||
| 		let base_currency = frappe.defaults.get_global_default('currency'); | ||||
| 		if (base_currency != frm.doc.currency) { | ||||
| 		if (frm.doc.currency && (base_currency != frm.doc.currency)) { | ||||
| 			frappe.call({ | ||||
| 				method: "erpnext.setup.utils.get_exchange_rate", | ||||
| 				args: { | ||||
|  | ||||
| @ -562,6 +562,7 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex | ||||
| 		var me = this; | ||||
| 		var dialog = new frappe.ui.Dialog({ | ||||
| 			title: __("Select Items"), | ||||
| 			size: "large", | ||||
| 			fields: [ | ||||
| 				{ | ||||
| 					"fieldtype": "Check", | ||||
| @ -663,7 +664,8 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex | ||||
| 			} else { | ||||
| 				let po_items = []; | ||||
| 				me.frm.doc.items.forEach(d => { | ||||
| 					let pending_qty = (flt(d.stock_qty) - flt(d.ordered_qty)) / flt(d.conversion_factor); | ||||
| 					let ordered_qty = me.get_ordered_qty(d, me.frm.doc); | ||||
| 					let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor); | ||||
| 					if (pending_qty > 0) { | ||||
| 						po_items.push({ | ||||
| 							"doctype": "Sales Order Item", | ||||
| @ -689,6 +691,24 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex | ||||
| 		dialog.show(); | ||||
| 	} | ||||
| 
 | ||||
| 	get_ordered_qty(item, so) { | ||||
| 		let ordered_qty = item.ordered_qty; | ||||
| 		if (so.packed_items) { | ||||
| 			// calculate ordered qty based on packed items in case of product bundle
 | ||||
| 			let packed_items = so.packed_items.filter( | ||||
| 				(pi) => pi.parent_detail_docname == item.name | ||||
| 			); | ||||
| 			if (packed_items) { | ||||
| 				ordered_qty = packed_items.reduce( | ||||
| 					(sum, pi) => sum + flt(pi.ordered_qty), | ||||
| 					0 | ||||
| 				); | ||||
| 				ordered_qty = ordered_qty / packed_items.length; | ||||
| 			} | ||||
| 		} | ||||
| 		return ordered_qty; | ||||
| 	} | ||||
| 
 | ||||
| 	hold_sales_order(){ | ||||
| 		var me = this; | ||||
| 		var d = new frappe.ui.Dialog({ | ||||
|  | ||||
| @ -877,6 +877,9 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): | ||||
| 		target.stock_qty = (flt(source.stock_qty) - flt(source.ordered_qty)) | ||||
| 		target.project = source_parent.project | ||||
| 
 | ||||
| 	def update_item_for_packed_item(source, target, source_parent): | ||||
| 		target.qty = flt(source.qty) - flt(source.ordered_qty) | ||||
| 
 | ||||
| 	# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) | ||||
| 	doc = get_mapped_doc("Sales Order", source_name, { | ||||
| 		"Sales Order": { | ||||
| @ -920,6 +923,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): | ||||
| 		"Packed Item": { | ||||
| 			"doctype": "Purchase Order Item", | ||||
| 			"field_map":  [ | ||||
| 				["name", "sales_order_packed_item"], | ||||
| 				["parent", "sales_order"], | ||||
| 				["uom", "uom"], | ||||
| 				["conversion_factor", "conversion_factor"], | ||||
| @ -934,6 +938,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): | ||||
| 				"supplier", | ||||
| 				"pricing_rules" | ||||
| 			], | ||||
| 			"postprocess": update_item_for_packed_item, | ||||
| 			"condition": lambda doc: doc.parent_item in items_to_map | ||||
| 		} | ||||
| 	}, target_doc, set_missing_values) | ||||
|  | ||||
| @ -959,6 +959,42 @@ class TestSalesOrder(FrappeTestCase): | ||||
| 		self.assertEqual(purchase_order.items[0].item_code, "_Test Bundle Item 1") | ||||
| 		self.assertEqual(purchase_order.items[1].item_code, "_Test Bundle Item 2") | ||||
| 
 | ||||
| 	def test_purchase_order_updates_packed_item_ordered_qty(self): | ||||
| 		""" | ||||
| 			Tests if the packed item's `ordered_qty` is updated with the quantity of the Purchase Order | ||||
| 		""" | ||||
| 		from erpnext.selling.doctype.sales_order.sales_order import make_purchase_order | ||||
| 
 | ||||
| 		product_bundle = make_item("_Test Product Bundle", {"is_stock_item": 0}) | ||||
| 		make_item("_Test Bundle Item 1", {"is_stock_item": 1}) | ||||
| 		make_item("_Test Bundle Item 2", {"is_stock_item": 1}) | ||||
| 
 | ||||
| 		make_product_bundle("_Test Product Bundle", | ||||
| 			["_Test Bundle Item 1", "_Test Bundle Item 2"]) | ||||
| 
 | ||||
| 		so_items = [ | ||||
| 			{ | ||||
| 				"item_code": product_bundle.item_code, | ||||
| 				"warehouse": "", | ||||
| 				"qty": 2, | ||||
| 				"rate": 400, | ||||
| 				"delivered_by_supplier": 1, | ||||
| 				"supplier": '_Test Supplier' | ||||
| 			} | ||||
| 		] | ||||
| 
 | ||||
| 		so = make_sales_order(item_list=so_items) | ||||
| 
 | ||||
| 		purchase_order = make_purchase_order(so.name, selected_items=so_items) | ||||
| 		purchase_order.supplier = "_Test Supplier" | ||||
| 		purchase_order.set_warehouse = "_Test Warehouse - _TC" | ||||
| 		purchase_order.save() | ||||
| 		purchase_order.submit() | ||||
| 
 | ||||
| 		so.reload() | ||||
| 		self.assertEqual(so.packed_items[0].ordered_qty, 2) | ||||
| 		self.assertEqual(so.packed_items[1].ordered_qty, 2) | ||||
| 
 | ||||
| 	def test_reserved_qty_for_closing_so(self): | ||||
| 		bin = frappe.get_all("Bin", filters={"item_code": "_Test Item", "warehouse": "_Test Warehouse - _TC"}, | ||||
| 			fields=["reserved_qty"]) | ||||
|  | ||||
| @ -3,7 +3,6 @@ | ||||
| 
 | ||||
| 
 | ||||
| import json | ||||
| import os | ||||
| 
 | ||||
| import frappe | ||||
| import frappe.defaults | ||||
| @ -422,14 +421,14 @@ def get_name_with_abbr(name, company): | ||||
| 	return " - ".join(parts) | ||||
| 
 | ||||
| def install_country_fixtures(company, country): | ||||
| 	path = frappe.get_app_path('erpnext', 'regional', frappe.scrub(country)) | ||||
| 	if os.path.exists(path.encode("utf-8")): | ||||
| 		try: | ||||
| 			module_name = "erpnext.regional.{0}.setup.setup".format(frappe.scrub(country)) | ||||
| 			frappe.get_attr(module_name)(company, False) | ||||
| 		except Exception as e: | ||||
| 			frappe.log_error() | ||||
| 			frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country))) | ||||
| 	try: | ||||
| 		module_name = f"erpnext.regional.{frappe.scrub(country)}.setup.setup" | ||||
| 		frappe.get_attr(module_name)(company, False) | ||||
| 	except ImportError: | ||||
| 		pass | ||||
| 	except Exception: | ||||
| 		frappe.log_error() | ||||
| 		frappe.throw(_("Failed to setup defaults for country {0}. Please contact support.").format(frappe.bold(country))) | ||||
| 
 | ||||
| 
 | ||||
| def update_company_current_month_sales(company): | ||||
|  | ||||
| @ -11,6 +11,7 @@ from erpnext.accounts.doctype.account.test_account import create_account, get_in | ||||
| from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice | ||||
| from erpnext.accounts.utils import update_gl_entries_after | ||||
| from erpnext.assets.doctype.asset.test_asset import create_asset_category, create_fixed_asset_item | ||||
| from erpnext.stock.doctype.delivery_note.test_delivery_note import create_delivery_note | ||||
| from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import ( | ||||
| 	get_gl_entries, | ||||
| 	make_purchase_receipt, | ||||
| @ -177,6 +178,53 @@ class TestLandedCostVoucher(FrappeTestCase): | ||||
| 		self.assertEqual(serial_no.purchase_rate - serial_no_rate, 5.0) | ||||
| 		self.assertEqual(serial_no.warehouse, "Stores - TCP1") | ||||
| 
 | ||||
| 	def test_serialized_lcv_delivered(self): | ||||
| 		"""In some cases you'd want to deliver before you can know all the | ||||
| 		landed costs, this should be allowed for serial nos too. | ||||
| 
 | ||||
| 		Case: | ||||
| 			- receipt a serial no @ X rate | ||||
| 			- delivery the serial no @ X rate | ||||
| 			- add LCV to receipt X + Y | ||||
| 			- LCV should be successful | ||||
| 			- delivery should reflect X+Y valuation. | ||||
| 		""" | ||||
| 		serial_no = "LCV_TEST_SR_NO" | ||||
| 		item_code = "_Test Serialized Item" | ||||
| 		warehouse = "Stores - TCP1" | ||||
| 
 | ||||
| 		pr = make_purchase_receipt(company="_Test Company with perpetual inventory", | ||||
| 				warehouse=warehouse, qty=1, rate=200, | ||||
| 				item_code=item_code, serial_no=serial_no) | ||||
| 
 | ||||
| 		serial_no_rate = frappe.db.get_value("Serial No", serial_no, "purchase_rate") | ||||
| 
 | ||||
| 		# deliver it before creating LCV | ||||
| 		dn = create_delivery_note(item_code=item_code, | ||||
| 				company='_Test Company with perpetual inventory', warehouse='Stores - TCP1', | ||||
| 				serial_no=serial_no, qty=1, rate=500, | ||||
| 				cost_center = 'Main - TCP1', expense_account = "Cost of Goods Sold - TCP1") | ||||
| 
 | ||||
| 		charges = 10 | ||||
| 		create_landed_cost_voucher("Purchase Receipt", pr.name, pr.company, charges=charges) | ||||
| 
 | ||||
| 		new_purchase_rate = serial_no_rate + charges | ||||
| 
 | ||||
| 		serial_no = frappe.db.get_value("Serial No", serial_no, | ||||
| 			["warehouse", "purchase_rate"], as_dict=1) | ||||
| 
 | ||||
| 		self.assertEqual(serial_no.purchase_rate, new_purchase_rate) | ||||
| 
 | ||||
| 		stock_value_difference = frappe.db.get_value("Stock Ledger Entry", | ||||
| 				filters={ | ||||
| 					"voucher_no": dn.name, | ||||
| 					"voucher_type": dn.doctype, | ||||
| 					"is_cancelled": 0  # LCV cancels with same name. | ||||
| 				}, | ||||
| 				fieldname="stock_value_difference") | ||||
| 
 | ||||
| 		# reposting should update the purchase rate in future delivery | ||||
| 		self.assertEqual(stock_value_difference, -new_purchase_rate) | ||||
| 
 | ||||
| 	def test_landed_cost_voucher_for_odd_numbers (self): | ||||
| 		pr = make_purchase_receipt(company="_Test Company with perpetual inventory", warehouse = "Stores - TCP1", supplier_warehouse = "Work in Progress - TCP1", do_not_save=True) | ||||
|  | ||||
| @ -26,6 +26,7 @@ | ||||
|   "section_break_13", | ||||
|   "actual_qty", | ||||
|   "projected_qty", | ||||
|   "ordered_qty", | ||||
|   "column_break_16", | ||||
|   "incoming_rate", | ||||
|   "page_break", | ||||
| @ -224,13 +225,21 @@ | ||||
|    "label": "Rate", | ||||
|    "print_hide": 1, | ||||
|    "read_only": 1 | ||||
|   }, | ||||
|   { | ||||
|    "allow_on_submit": 1, | ||||
|    "fieldname": "ordered_qty", | ||||
|    "fieldtype": "Float", | ||||
|    "label": "Ordered Qty", | ||||
|    "no_copy": 1, | ||||
|    "read_only": 1 | ||||
|   } | ||||
|  ], | ||||
|  "idx": 1, | ||||
|  "index_web_pages_for_search": 1, | ||||
|  "istable": 1, | ||||
|  "links": [], | ||||
|  "modified": "2022-01-28 16:03:30.780111", | ||||
|  "modified": "2022-02-22 12:57:45.325488", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Stock", | ||||
|  "name": "Packed Item", | ||||
|  | ||||
| @ -161,6 +161,15 @@ class TestPurchaseReceipt(FrappeTestCase): | ||||
| 				qty=abs(existing_bin_qty) | ||||
| 			) | ||||
| 
 | ||||
| 		existing_bin_qty, existing_bin_stock_value = frappe.db.get_value( | ||||
| 			"Bin", | ||||
| 			{ | ||||
| 				"item_code": "_Test Item", | ||||
| 				"warehouse": "_Test Warehouse - _TC" | ||||
| 			}, | ||||
| 			["actual_qty", "stock_value"] | ||||
| 		) | ||||
| 
 | ||||
| 		pr = make_purchase_receipt() | ||||
| 
 | ||||
| 		stock_value_difference = frappe.db.get_value( | ||||
|  | ||||
| @ -389,10 +389,13 @@ class TestStockLedgerEntry(FrappeTestCase): | ||||
| 			) | ||||
| 
 | ||||
| 
 | ||||
| 	def assertSLEs(self, doc, expected_sles): | ||||
| 	def assertSLEs(self, doc, expected_sles, sle_filters=None): | ||||
| 		""" Compare sorted SLEs, useful for vouchers that create multiple SLEs for same line""" | ||||
| 		sles = frappe.get_all("Stock Ledger Entry", fields=["*"], | ||||
| 				filters={"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled":0}, | ||||
| 
 | ||||
| 		filters = {"voucher_no": doc.name, "voucher_type": doc.doctype, "is_cancelled": 0} | ||||
| 		if sle_filters: | ||||
| 			filters.update(sle_filters) | ||||
| 		sles = frappe.get_all("Stock Ledger Entry", fields=["*"], filters=filters, | ||||
| 			order_by="timestamp(posting_date, posting_time), creation") | ||||
| 
 | ||||
| 		for exp_sle, act_sle in zip(expected_sles, sles): | ||||
| @ -665,6 +668,78 @@ class TestStockLedgerEntry(FrappeTestCase): | ||||
| 			{"actual_qty": -10, "stock_value_difference": -10*40, "stock_queue": []}, | ||||
| 		])) | ||||
| 
 | ||||
| 	def test_fifo_dependent_consumption(self): | ||||
| 		item = make_item("_TestFifoTransferRates") | ||||
| 		source = "_Test Warehouse - _TC" | ||||
| 		target = "Stores - _TC" | ||||
| 
 | ||||
| 		rates = [10 * i for i in range(1, 20)] | ||||
| 
 | ||||
| 		receipt = make_stock_entry(item_code=item.name, target=source, qty=10, do_not_save=True, rate=10) | ||||
| 		for rate in rates[1:]: | ||||
| 			row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False) | ||||
| 			row.basic_rate = rate | ||||
| 			receipt.append("items", row) | ||||
| 
 | ||||
| 		receipt.save() | ||||
| 		receipt.submit() | ||||
| 
 | ||||
| 		expected_queues = [] | ||||
| 		for idx, rate in enumerate(rates, start=1): | ||||
| 			expected_queues.append( | ||||
| 				{"stock_queue": [[10, 10 * i] for i in range(1, idx + 1)]} | ||||
| 			) | ||||
| 		self.assertSLEs(receipt, expected_queues) | ||||
| 
 | ||||
| 		transfer = make_stock_entry(item_code=item.name, source=source, target=target, qty=10, do_not_save=True, rate=10) | ||||
| 		for rate in rates[1:]: | ||||
| 			row = frappe.copy_doc(transfer.items[0], ignore_no_copy=False) | ||||
| 			transfer.append("items", row) | ||||
| 
 | ||||
| 		transfer.save() | ||||
| 		transfer.submit() | ||||
| 
 | ||||
| 		# same exact queue should be transferred | ||||
| 		self.assertSLEs(transfer, expected_queues, sle_filters={"warehouse": target}) | ||||
| 
 | ||||
| 	def test_fifo_multi_item_repack_consumption(self): | ||||
| 		rm = make_item("_TestFifoRepackRM") | ||||
| 		packed = make_item("_TestFifoRepackFinished") | ||||
| 		warehouse = "_Test Warehouse - _TC" | ||||
| 
 | ||||
| 		rates = [10 * i for i in range(1, 5)] | ||||
| 
 | ||||
| 		receipt = make_stock_entry(item_code=rm.name, target=warehouse, qty=10, do_not_save=True, rate=10) | ||||
| 		for rate in rates[1:]: | ||||
| 			row = frappe.copy_doc(receipt.items[0], ignore_no_copy=False) | ||||
| 			row.basic_rate = rate | ||||
| 			receipt.append("items", row) | ||||
| 
 | ||||
| 		receipt.save() | ||||
| 		receipt.submit() | ||||
| 
 | ||||
| 		repack = make_stock_entry(item_code=rm.name, source=warehouse, qty=10, | ||||
| 				do_not_save=True, rate=10, purpose="Repack") | ||||
| 		for rate in rates[1:]: | ||||
| 			row = frappe.copy_doc(repack.items[0], ignore_no_copy=False) | ||||
| 			repack.append("items", row) | ||||
| 
 | ||||
| 		repack.append("items", { | ||||
| 			"item_code": packed.name, | ||||
| 			"t_warehouse": warehouse, | ||||
| 			"qty": 1, | ||||
| 			"transfer_qty": 1, | ||||
| 		}) | ||||
| 
 | ||||
| 		repack.save() | ||||
| 		repack.submit() | ||||
| 
 | ||||
| 		# same exact queue should be transferred | ||||
| 		self.assertSLEs(repack, [ | ||||
| 			{"incoming_rate": sum(rates) * 10} | ||||
| 		], sle_filters={"item_code": packed.name}) | ||||
| 
 | ||||
| 
 | ||||
| def create_repack_entry(**args): | ||||
| 	args = frappe._dict(args) | ||||
| 	repack = frappe.new_doc("Stock Entry") | ||||
|  | ||||
| @ -244,7 +244,7 @@ | ||||
|  "idx": 1, | ||||
|  "is_tree": 1, | ||||
|  "links": [], | ||||
|  "modified": "2021-12-03 04:40:06.414630", | ||||
|  "modified": "2022-03-01 02:37:48.034944", | ||||
|  "modified_by": "Administrator", | ||||
|  "module": "Stock", | ||||
|  "name": "Warehouse", | ||||
| @ -301,5 +301,7 @@ | ||||
|  "show_name_in_global_search": 1, | ||||
|  "sort_field": "modified", | ||||
|  "sort_order": "DESC", | ||||
|  "title_field": "warehouse_name" | ||||
|  "states": [], | ||||
|  "title_field": "warehouse_name", | ||||
|  "track_changes": 1 | ||||
| } | ||||
| @ -21,6 +21,7 @@ SLE_FIELDS = ( | ||||
| 	"stock_value", | ||||
| 	"stock_value_difference", | ||||
| 	"valuation_rate", | ||||
| 	"voucher_detail_no", | ||||
| ) | ||||
| 
 | ||||
| 
 | ||||
| @ -66,7 +67,9 @@ def add_invariant_check_fields(sles): | ||||
| 		balance_qty += sle.actual_qty | ||||
| 		balance_stock_value += sle.stock_value_difference | ||||
| 		if sle.voucher_type == "Stock Reconciliation" and not sle.batch_no: | ||||
| 			balance_qty = sle.qty_after_transaction | ||||
| 			balance_qty = frappe.db.get_value("Stock Reconciliation Item", sle.voucher_detail_no, "qty") | ||||
| 			if balance_qty is None: | ||||
| 				balance_qty = sle.qty_after_transaction | ||||
| 
 | ||||
| 		sle.fifo_queue_qty = fifo_qty | ||||
| 		sle.fifo_stock_value = fifo_value | ||||
|  | ||||
| @ -28,6 +28,16 @@ class SerialNoExistsInFutureTransaction(frappe.ValidationError): | ||||
| 
 | ||||
| 
 | ||||
| def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_voucher=False): | ||||
| 	""" Create SL entries from SL entry dicts | ||||
| 
 | ||||
| 		args: | ||||
| 			- allow_negative_stock: disable negative stock valiations if true | ||||
| 			- via_landed_cost_voucher: landed cost voucher cancels and reposts | ||||
| 			entries of purchase document. This flag is used to identify if | ||||
| 			cancellation and repost is happening via landed cost voucher, in | ||||
| 			such cases certain validations need to be ignored (like negative | ||||
| 					stock) | ||||
| 	""" | ||||
| 	from erpnext.controllers.stock_controller import future_sle_exists | ||||
| 	if sl_entries: | ||||
| 		cancel = sl_entries[0].get("is_cancelled") | ||||
| @ -39,7 +49,7 @@ def make_sl_entries(sl_entries, allow_negative_stock=False, via_landed_cost_vouc | ||||
| 		future_sle_exists(args, sl_entries) | ||||
| 
 | ||||
| 		for sle in sl_entries: | ||||
| 			if sle.serial_no: | ||||
| 			if sle.serial_no and not via_landed_cost_voucher: | ||||
| 				validate_serial_no(sle) | ||||
| 
 | ||||
| 			if cancel: | ||||
|  | ||||
| @ -1,27 +1 @@ | ||||
| {% set domains = frappe.get_doc("Domain Settings").active_domains %} | ||||
| {% set links = { | ||||
| 	'Manufacturing': '/manufacturing', | ||||
| 	'Services': '/services', | ||||
| 	'Retail': '/retail', | ||||
| 	'Distribution': '/distribution', | ||||
| 	'Non Profit': '/non-profit', | ||||
| 	'Education': '/education', | ||||
| 	'Healthcare': '/healthcare', | ||||
| 	'Agriculture': '/agriculture', | ||||
| 	'Hospitality': '' | ||||
| } %} | ||||
| 
 | ||||
| {% set link = '' %} | ||||
| {% set label = '' %} | ||||
| {% if domains %} | ||||
| 	{% set label = domains[0].domain %} | ||||
| 	{% set link = links[label] %} | ||||
| {% endif %} | ||||
| 
 | ||||
| {% if label == "Services" %} | ||||
| 	{% set label = "Service" %} | ||||
| {% endif %} | ||||
| 
 | ||||
| 
 | ||||
| 
 | ||||
| <a href="https://erpnext.com{{ link }}?source=website_footer" target="_blank" class="text-muted">Powered by ERPNext - {{ '' if domains else 'Open Source' }} ERP Software {{ ('for ' + label + ' Companies') if domains else '' }}</a> | ||||
| <a href="https://erpnext.com?source=website_footer" target="_blank" class="text-muted">Powered by ERPNext</a> | ||||
|  | ||||
| @ -25,7 +25,7 @@ class TestPointOfSale(unittest.TestCase): | ||||
| 		Test Stock and Service Item Search. | ||||
| 		""" | ||||
| 
 | ||||
| 		pos_profile = make_pos_profile() | ||||
| 		pos_profile = make_pos_profile(name="Test POS Profile for Search") | ||||
| 		item1 = make_item("Test Search Stock Item", {"is_stock_item": 1}) | ||||
| 		make_stock_entry( | ||||
| 			item_code="Test Search Stock Item", | ||||
|  | ||||
		Loading…
	
	
			
			x
			
			
		
	
		Reference in New Issue
	
	Block a user